DeepSeek Harness 架构拆解:一个"一切皆插件"的 Agent 运行框架

导语:很多人第一次听到 DeepSeek Harness(命令行 dsh),会以为它是 DeepSeek 的推理引擎。其实恰恰相反——它不含任何模型算力,而是一个 Agent 运行框架。本文从架构师视角,把它拆成三层、四个核心包、两个代码模板,讲清楚它”为什么这么设计”。

一、先纠一个概念:Harness 不是推理引擎

DeepSeek Harness 的官方定位是一条公式:

Agent = Model + Harness

Model 负责推理和决策,Harness 负责模型之外的一切:记忆、工具、权限、执行循环、会话管理、沙箱。模型算力是通过”模型适配器”接入的外部服务(DeepSeek、Anthropic、OpenAI,或任意 OpenAI 兼容网关)。

所以你想学它,其实是两件事:①它的插件化架构(如何把 Agent 运行时拆成可拼装的件);②它的程序应用(怎么写插件、怎么接自己的模型)。下面分而治之。

二、架构骨架:三层模型

整个框架只有三层,别被”200+ 个包”吓到:

  1. Cordis 微内核
    (约 2700 行,不可动部分仅 ~2%)。核心机制是”时空可组合性”:每个插件的每次改动都必须附带”如何撤销”的说明,卸载时按注册顺序逆序回滚;插件声明依赖,依赖项出现/消失时自动启停。
  2. 一切皆插件
    (其余十几万行全是插件)。工具、技能、UI、会话记录是插件,连 Agent 主循环本身也是可替换的插件。通过 profile/patch/--patch 叠加插件树,运行时热插拔。
  3. LLM 适配层
    packages/llm)。唯一和”推理引擎”交界的地方:LlmAdapter 契约 + StreamChunk 流协议 + 内容块词汇表。

一句话总结设计哲学:扩展方式不是改内核,而是把插件挂到别的插件旁边。

三、Cordis 内核:五个概念,四种分发

读任何核心包之前,先建立这五个概念——它们是整个框架的”地基”:

概念
含义
插件 = 实现 Service 的对象
函数、带 apply 的对象、或 Service 子类
上下文 = 服务容器
每个服务占一个 ctx.按 key 查服务,从不 import 实现
inject

 声明依赖
加载顺序由依赖图决定,不是手动编排启动序列
类型化事件通信
四种分发模式(见下)
注册 = 可逆副作用
ctx.effect()

/ctx.on() 装的东西,卸载时自动逆序撤销

四种分发模式,读 agent-loop 和 tools 管道前必须懂:

模式
await?
顺序
返回值
用在哪
emit
注册序
观察(广播事实)
waterfall
注册序
中间件/短路

(最关键)
parallel
并行扇出
独立消费者
serial
注册序
有序链式

Waterfall 语义是理解一切拦截的关键:监听器收到 (...args, next),调 next() 委托给下游,不调 next() 直接 return = 短路。”策略监听器有决定权就短路,观察监听器必须委托”。

四、主干四件套之一:session —— 事件溯源是唯一真源

Session 是一份由类型化 SessionEvent 组成的仅追加日志,是 agent 交互历史的唯一真源。LLM 消息历史是从日志”派生”出来的,从不单独存储;回放 = 从同一组事件重新派生。这就是 DDD 里的 Event Sourcing,只不过”聚合根”换成了一次对话。

三个设计分离,是它的精髓:

  • Surface 与 Log 分离
    :只有 3 种事件(user/messageassistant/messagetool/result)进入”有序 surface”。它们携带 SurfaceOpappend(追加)或 replace遮蔽一个区间)。日志永远只增不减,但”模型看到的历史”可以变短——这是压缩长上下文的机制。
  • 原始 chunk 与组装 message 分离
    assistant/chunk(token 级原始流,保回放保真)与 assistant/message(组装后,派生历史用)分开存。
  • 种子历史与实时工作分离
    :恢复/fork 的会话追加一条 session/end-seed 标记,区分”种子”与”本进程实时写入”。

数据完整性靠 append 的三道闸:JSON 可序列化校验(BigInt/函数/循环引用直接 throw)、deep-freeze(普通 JS 无法改写历史)、seq 连续性seq === log.length,持久化不能过滤任何事件)。

五、主干四件套之二:agent-loop —— 驱动器与拦截点

这一层最关键的决定是接口与实现分离agent/ 包只声明 Agent 接口,agent-loop/ 是唯一具体实现。扩展插件只依赖 agent,绝不依赖 agent-loop——于是替换 Agent 循环不需要改任何消费方。教科书级的依赖倒置。

四个 waterfall 拦截点是控制中枢:

事件
拦截什么
agent/pre-step 模型看到什么

:拒绝或改写进入步骤的消息
agent/request 调用配置

:替换 model/provider 配置
agent/request-error 重试

:返回 {kind:'retry'} 接管恢复
agent/turn-stopping 轮次关闭

:快关闭时可再 steer() 一步

最值得细品的是 agent/turn-stopping 的语义:数据决定结果,监听器顺序无法改变结果——机器重读 inbox,有 pending 就再跑一步,没有就关轮次。控制流被表达成了数据状态。

六、主干四件套之三:tools —— 受控执行管道

一次工具调用依次经过这条链:

tools/pre-execute (waterfall: allow/deny/ask)       ↓ 单调 guard(只能 deny,不能 allow)       ↓ tools/execute (waterfall: 超时/重试/指标)       ↓ tools/post-execute (waterfall: accept/replace/block)       ↓ finalizeContent → tools/result (emit)

三个安全设计值得抄走:

  • 单调策略
    :guard 故意没有 allow 结果,监听器顺序无法把 deny 变回 allow。权限只减不增,这是”默认拒绝、顺序无关”的安全模型。
  • 参数不可改写
    :模型发出的 arguments 一旦进日志就是审计证据,谁都不能改——历史、审计、UI、执行必须一致。
  • value/content 分离
    :程序拿到完整 value(不持久化),模型看到 content(持久化)。回放能重现展示,重建不了规范中间值。

七、主干四件套之四:llm —— 模型接缝

LlmAdapter 抽象类唯一必须实现的方法是 stream(),但配套一堆”必须遵守”的约定——薄接口 + 强约定:

  • usage
     在 finish 之前,finish 之后无分片
  • 工具参数全程保持原始 JSON 字符串
  • 两条错误路径共用一个规范化的 LlmFailure
  • 一次适配器调用 = 一次提供方尝试
    ,重试在 agent 层
  • 上下文溢出只有一个规范 code,消费方按 code 路由,绝不依赖提供方文本

核心洞察:适配器把”提供方的千奇百怪”归一化成”框架的单一真相”。上层永远面对干净的、提供方无关的语义,绝不去猜各家各报什么错。

八、贯穿始终的四条不变量

读完四件套,会发现四条主线贯穿所有包:

  1. 模型可见即已记录
    ——凡进模型的,都能从日志重建,有运行时断言。
  2. 参数/请求不可改写
    ——进日志/进模型的都是不可篡改的事实。
  3. 失败归一化
    ——LlmFailure、单一 code、空响应可重试。
  4. 一切可插拔
    ——从工具到循环,全部是可替换插件。

九、程序应用:两个代码模板

模板 A:最小工具插件

import { readFile } from 'node:fs/promises' import type { Context } from '@deepseek-ai/cordis' import { defineTool } from '@deepseek-ai/dsh-tools'  export const name = 'my-tool' export const inject = ['tools']        // 声明依赖,等 ctx.tools 就绪才启动  export function apply(ctx: Context) {   ctx.tools.register(defineTool({      // 注册是副作用,卸载自动注销     name: 'read_file',     description: 'Read a file from disk.',     parameters: {                      // 一个 schema 三合一:类型+校验+模型schema       path: { type: 'string', required: true, description: 'Absolute path' },       limit: { type: 'number' },     },     output: {       schema: { type: 'string' },       render: (_args, value) => [{ type: 'text', text: value }],  // value→content     },     async execute(args, exec) {       // args 已被校验并类型收窄;exec.signal 必须透传       return readFile(args.path, { encoding: 'utf8', signal: exec.signal })     },   })) }

模板 B:接自己的 vllm 模型(OpenAI 兼容)

import { LlmAdapter } from '@deepseek-ai/dsh-llm'  class VllmAdapter extends LlmAdapter {   async *stream(options: GenerateOptions): AsyncIterable {     const res = await fetch(`${this.baseUrl}/v1/chat/completions`, {       method: 'POST',       headers: { 'Content-Type': 'application/json',                  'Authorization': `Bearer ${this.apiKey}` },       body: JSON.stringify({ model: options.model, messages: options.messages,                              tools: options.tools, stream: true }),       signal: options.signal,          // 必须遵守取消信号     })     // 解析 SSE,按序 yield StreamChunk(text-delta / usage / finish...)   } }  export function apply(ctx: Context, config: { baseUrl: string; apiKey: string }) {   ctx.llm.registerAdapter(['my-vllm'], new VllmAdapter(config.baseUrl, config.apiKey)) }

你要做的全部:实现一个 stream(),把 vllm 的 OpenAI 兼容输出翻译成框架的 StreamChunk,然后 registerAdapter。重试、缓存、审计、日志全由框架接管。

结语

一句话总结这个框架的设计哲学:

Harness 是一个事件溯源的、一切皆插件的、依赖注入的执行底座——存的是事实(append-only 日志),跑的是循环(可替换的 driver),改的是数据(waterfall 决策 + 单调策略),接的是翻译器(LlmAdapter 归一化协议)。

如果你也在做 Agent 工程,最值得搬走的三样东西是:事件溯源 + 派生历史(存事实不存视图)、单调安全策略(顺序无关的默认拒绝)、薄接口 + 强约定(适配器归一化一切差异)。

© 版权声明
THE END
喜欢就支持一下吧
点赞70 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片