导语:很多人第一次听到 DeepSeek Harness(命令行
dsh),会以为它是 DeepSeek 的推理引擎。其实恰恰相反——它不含任何模型算力,而是一个 Agent 运行框架。本文从架构师视角,把它拆成三层、四个核心包、两个代码模板,讲清楚它”为什么这么设计”。
一、先纠一个概念:Harness 不是推理引擎
DeepSeek Harness 的官方定位是一条公式:
Agent = Model + Harness
Model 负责推理和决策,Harness 负责模型之外的一切:记忆、工具、权限、执行循环、会话管理、沙箱。模型算力是通过”模型适配器”接入的外部服务(DeepSeek、Anthropic、OpenAI,或任意 OpenAI 兼容网关)。
所以你想学它,其实是两件事:①它的插件化架构(如何把 Agent 运行时拆成可拼装的件);②它的程序应用(怎么写插件、怎么接自己的模型)。下面分而治之。
二、架构骨架:三层模型
整个框架只有三层,别被”200+ 个包”吓到:
- Cordis 微内核
(约 2700 行,不可动部分仅 ~2%)。核心机制是”时空可组合性”:每个插件的每次改动都必须附带”如何撤销”的说明,卸载时按注册顺序逆序回滚;插件声明依赖,依赖项出现/消失时自动启停。 - 一切皆插件
(其余十几万行全是插件)。工具、技能、UI、会话记录是插件,连 Agent 主循环本身也是可替换的插件。通过 profile/patch/ --patch叠加插件树,运行时热插拔。 - LLM 适配层
( packages/llm)。唯一和”推理引擎”交界的地方:LlmAdapter契约 +StreamChunk流协议 + 内容块词汇表。
一句话总结设计哲学:扩展方式不是改内核,而是把插件挂到别的插件旁边。
三、Cordis 内核:五个概念,四种分发
读任何核心包之前,先建立这五个概念——它们是整个框架的”地基”:
|
|
|
|---|---|
|
|
apply 的对象、或 Service 子类 |
|
|
ctx.,按 key 查服务,从不 import 实现 |
inject
|
|
|
|
|
|
|
ctx.effect()
ctx.on() 装的东西,卸载时自动逆序撤销 |
四种分发模式,读 agent-loop 和 tools 管道前必须懂:
|
|
|
|
|
|
|---|---|---|---|---|
emit |
|
|
|
|
waterfall |
|
|
|
中间件/短路
|
parallel |
|
|
|
|
serial |
|
|
|
|
Waterfall 语义是理解一切拦截的关键:监听器收到 (...args, next),调 next() 委托给下游,不调 next() 直接 return = 短路。”策略监听器有决定权就短路,观察监听器必须委托”。
四、主干四件套之一:session —— 事件溯源是唯一真源
Session 是一份由类型化 SessionEvent 组成的仅追加日志,是 agent 交互历史的唯一真源。LLM 消息历史是从日志”派生”出来的,从不单独存储;回放 = 从同一组事件重新派生。这就是 DDD 里的 Event Sourcing,只不过”聚合根”换成了一次对话。
三个设计分离,是它的精髓:
- Surface 与 Log 分离
:只有 3 种事件( user/message、assistant/message、tool/result)进入”有序 surface”。它们携带SurfaceOp:append(追加)或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 |
调用配置
|
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 路由,绝不依赖提供方文本
核心洞察:适配器把”提供方的千奇百怪”归一化成”框架的单一真相”。上层永远面对干净的、提供方无关的语义,绝不去猜各家各报什么错。
八、贯穿始终的四条不变量
读完四件套,会发现四条主线贯穿所有包:
- 模型可见即已记录
——凡进模型的,都能从日志重建,有运行时断言。 - 参数/请求不可改写
——进日志/进模型的都是不可篡改的事实。 - 失败归一化
—— LlmFailure、单一 code、空响应可重试。 - 一切可插拔
——从工具到循环,全部是可替换插件。
九、程序应用:两个代码模板
模板 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 工程,最值得搬走的三样东西是:事件溯源 + 派生历史(存事实不存视图)、单调安全策略(顺序无关的默认拒绝)、薄接口 + 强约定(适配器归一化一切差异)。












