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

导语:很多人第一次听到 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 内核:五个概念,四种分发
读任何核心包之前,先建立这五个概念——它们是整个框架的"地基":
| 概念 | 含义 |
|---|---|
| 插件 = 实现 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/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 | 调用配置:替换 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 路由,绝不依赖提供方文本
核心洞察:适配器把"提供方的千奇百怪"归一化成"框架的单一真相"。上层永远面对干净的、提供方无关的语义,绝不去猜各家各报什么错。
八、贯穿始终的四条不变量
读完四件套,会发现四条主线贯穿所有包:
- 模型可见即已记录——凡进模型的,都能从日志重建,有运行时断言。
- 参数/请求不可改写——进日志/进模型的都是不可篡改的事实。
- 失败归一化——
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 工程,最值得搬走的三样东西是:事件溯源 + 派生历史(存事实不存视图)、单调安全策略(顺序无关的默认拒绝)、薄接口 + 强约定(适配器归一化一切差异)。
登录查看剩余 70% 内容
-
08.25
云缓存服务怎么计费?包年包月、按量付费、Serverless 按容量三种模式详细说明(阿里云 Tair)
-
08.25
为什么 Agent 的每个请求,都要先拍一张快照
-
08.25
SaaS 诊所系统,如何制作电子病历模板?
-
08.25
1688图片搜索(拍立淘)API实现同类商品批量比价和货物寻源方案
-
08.25
阿里云16核32G云服务器价格查询系统:包年包月和按量付费费用明细
-
08.25
在PPT中运用AI生成图片提升视觉效果的实用实用技巧
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏