作者:互联网 时间: 2026-08-25 08:51:56
DeepSeek Harness 并非推理引擎,而是“一切皆插件”的Agent运行框架,三层架构+核心包拆解,揭秘架构师视角的设计逻辑。核心内容:1. Harness的定位与核心职责(非推理引擎,负责模型外的记忆、工具等)2. 架构骨架:三层模型(Cordis微内核、插件化设计、LLM适配层)3. Cordis内核的核心概念与四种分发模式(五个基础概念及事件通信机制)

导语:很多人第一次听到 DeepSeek Harness(命令行 dsh),会以为它是 DeepSeek 的推理引擎。其实恰恰相反——它不含任何模型算力,而是一个 Agent 运行框架。本文从架构师视角,把它拆成三层、四个核心包、两个代码模板,讲清楚它"为什么这么设计"。
DeepSeek Harness 的官方定位是一条公式:
Agent = Model + Harness
Model 负责推理和决策,Harness 负责模型之外的一切:记忆、工具、权限、执行循环、会话管理、沙箱。模型算力是通过"模型适配器"接入的外部服务(DeepSeek、Anthropic、OpenAI,或任意 OpenAI 兼容网关)。
所以你想学它,其实是两件事:①它的插件化架构(如何把 Agent 运行时拆成可拼装的件);②它的程序应用(怎么写插件、怎么接自己的模型)。下面分而治之。
整个框架只有三层,别被"200+ 个包"吓到:
--patch 叠加插件树,运行时热插拔。packages/llm)。唯一和"推理引擎"交界的地方:LlmAdapter 契约 + StreamChunk 流协议 + 内容块词汇表。一句话总结设计哲学:扩展方式不是改内核,而是把插件挂到别的插件旁边。
读任何核心包之前,先建立这五个概念——它们是整个框架的"地基":
| 概念 | 含义 |
|---|---|
| 插件 = 实现 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 是一份由类型化 SessionEvent 组成的仅追加日志,是 agent 交互历史的唯一真源。LLM 消息历史是从日志"派生"出来的,从不单独存储;回放 = 从同一组事件重新派生。这就是 DDD 里的 Event Sourcing,只不过"聚合根"换成了一次对话。
三个设计分离,是它的精髓:
user/message、assistant/message、tool/result)进入"有序 surface"。它们携带 SurfaceOp:append(追加)或 replace(遮蔽一个区间)。日志永远只增不减,但"模型看到的历史"可以变短——这是压缩长上下文的机制。assistant/chunk(token 级原始流,保回放保真)与 assistant/message(组装后,派生历史用)分开存。session/end-seed 标记,区分"种子"与"本进程实时写入"。数据完整性靠 append 的三道闸:JSON 可序列化校验(BigInt/函数/循环引用直接 throw)、deep-freeze(普通 JS 无法改写历史)、seq 连续性(seq === log.length,持久化不能过滤任何事件)。
这一层最关键的决定是接口与实现分离: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/pre-execute (waterfall: allow/deny/ask) ↓ 单调 guard(只能 deny,不能 allow) ↓ tools/execute (waterfall: 超时/重试/指标) ↓ tools/post-execute (waterfall: accept/replace/block) ↓ finalizeContent → tools/result (emit)三个安全设计值得抄走:
arguments 一旦进日志就是审计证据,谁都不能改——历史、审计、UI、执行必须一致。value(不持久化),模型看到 content(持久化)。回放能重现展示,重建不了规范中间值。LlmAdapter 抽象类唯一必须实现的方法是 stream(),但配套一堆"必须遵守"的约定——薄接口 + 强约定:
usage 在 finish 之前,finish 之后无分片LlmFailure核心洞察:适配器把"提供方的千奇百怪"归一化成"框架的单一真相"。上层永远面对干净的、提供方无关的语义,绝不去猜各家各报什么错。
读完四件套,会发现四条主线贯穿所有包:
LlmFailure、单一 code、空响应可重试。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 }) }, })) }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 工程,最值得搬走的三样东西是:事件溯源 + 派生历史(存事实不存视图)、单调安全策略(顺序无关的默认拒绝)、薄接口 + 强约定(适配器归一化一切差异)。
登录查看剩余 70% 内容