第一部分 · 总览
第 1 章:架构总览——一切皆插件
没有特权核心,只有一棵由配置拼出的插件树
从 README 的第一句话说起
打开 DeepSeek Harness 仓库根目录的 README.md,第一段话是:
DeepSeek Harness (
dsh) is an open-source agent harness developed by DeepSeek AI. It uses an architecture where everything is a plugin, and is powered by Cordis.
「everything is a plugin」不是修辞。在几乎所有的 agent 框架里,总有一层东西是「核心」:模型适配、工具注册表、会话管理、循环驱动。它们之间的调用关系是写死的调用图,扩展代码挂在核心外围。dsh 的赌注是:这一层不应该存在。模型适配器是插件,工具注册表是插件,会话日志是插件,连 Agent 循环本身——那个决定「下一步做什么」的心脏——也只是一个实现了 AgentFactory 接口、随时可以被另一个实现替换掉的插件。
架构文档 docs/architecture.md 的第一节把这件事说得很直白:
Every part of the product is a plugin, including the model adapter, the tool registry, the session log, and the agent loop itself, so every part is replaceable from configuration. There is no privileged core to patch: you extend dsh by mounting a plugin beside the others, and registrations are effects that unwind when their plugin unloads.
这一章先建立理解全书所需的三个坐标:Cordis 提供的基本原语、dsh 的事件三域、以及一次请求的 turn flow 鸟瞰。其余所有章节都是这三个坐标的放大。
Cordis 四件套:插件树的地基
Cordis 是一个被 vendored 进 vendor/cordis/ 的插件框架(仓库有自己的同步清单与修改记录)。它只有四个核心概念,理解它们就理解了整棵树的语法:
| 原语 | 是什么 | 在 dsh 里的例子 |
|---|---|---|
Context | 一棵插件树的运行期对象;服务、事件、effect 都挂在它上面,插件通过它互相发现 | 每个 agent 有一个自己的 scoped context(第 3 章);浏览器里甚至另有一棵 client context(第 17 章) |
Service | 注册在 Context 上的具名服务(ctx.sessions、ctx.tools、ctx.shell…),可注入、可替换 | ctx.tools 工具注册表、ctx.llm 模型接缝、ctx.shell shell 接缝 |
| typed events | 按名字分发的事件,类型通过 declare module 声明合并扩展;分 emit(通知)、serial(串行链)、waterfall(拦截链,必须调 next() 才放行)三种语义 | agent/pre-step(waterfall,可否决/改写模型输入)、tools/execute(waterfall)、session/event(emit) |
effect | 可逆的注册操作:插件装进树时执行,卸载时自动回滚 | ctx.tools.register(...) 返回 disposer;agent 的 scope 随 fiber 析构整体回滚(第 3 章) |
「注册即 effect」是这本书反复出现的主题。在 dsh 里,几乎所有「往系统里加东西」的操作都满足两条性质:可卸载(卸载时自动撤销,不留残余)与 按作用域生效(注册可以挂在全局,也可以挂在某个 agent 自己的 scope 上,后者只影响那一个会话)。这两条性质合起来,让「换掉一个能力」「给一个会话换一套工具」「卸载一个插件」全部变成 O(1) 的树操作,而不是迁移数据。
Cordis 被完整 vendored 进 vendor/cordis/,带 manifest 与上游 SHA、同步流程与本地修改记录(vendor/README.md)。对 dsh 而言,Cordis 不是「一个第三方库」,而是系统骨架本身——它需要按需打补丁(比如浏览器端 Loader 的 internal 模块系统注入,第 17 章),又要保证五十多个包引用同一份实现。vendoring 让这份依赖钉死、可审计、可修改。
三域事件:扩展点的第一层分类
dsh 的架构文档把事件分成三个域,每个域的语义不同。选对域,是「往系统里加东西」的第一决策:
- Session 事件(
turn/*、step/*、user/message、assistant/*、tool/*、request/*…)是追加进SessionEvent日志的持久事实,通过session/event广播。需要「重开页面后还在」的事实,必须是 session 事件——这是「模型可见 ⟺ 已记录」不变式的地基(第 4 章)。 - Agent 事件(
agent/pre-step、agent/request、agent/status、agent/turn-stopping…)携带一个活着的Agent对象,用于观察或拦截在飞的工作。拦截类(agent/pre-step、agent/request)是 waterfall,监听器不调next()就短路改写;通知类(agent/status)是 emit。 - Capability 事件(
fs/*、tools/*、telemetry/*…)把策略与适配器挂到能力接缝上,不需要 import 循环(第 7 章里 fs 的「写前必读」观测策略就是纯事件门禁)。
一个判断准则:这个事实需要活过 reload 吗?需要 → session 事件;只是在飞的工作需要被看到或拦截?需要 → agent 事件;只是某个接缝的策略?→ capability 事件。
Turn flow 鸟瞰:一次请求的骨架
dsh 的架构文档用一段伪代码定义了 turn flow。这里先给全景,第 3 章会逐行拆它:
turn/start
claim next-step input plus one queued message
assemble prompt sections + tool schemas
-> agent/pre-step reject | enter(messages)
reject, or a first enter rewritten empty -> close the turn with no step
step/start
append entered messages as user/message
derive model history from the log
agent/request -> llm/stream -> assistant/chunk* -> assistant/message
tool/call* -> tools/pre-execute -> tools/execute -> tools/post-execute -> tool/result*
step/end
tools owe another request, or next-step input arrived -> claim -> next step
-> agent/turn-stopping
turn/end
两个词需要先定义:一个 step 是「一次模型请求 + 它调用的工具」;一个 turn 是零到多个 step——从第一个输入被认领开始,到没有欠账为止。用户问一句、模型答一句、顺手调了三个工具,这算一个 turn 里的多个 step;用户追问下一句,才开新的 turn。
注意这段伪代码里没有一行是「框架的」。turn/start 与 turn/end 是 session 事件;agent/pre-step、agent/request、agent/turn-stopping 是 agent 事件的 waterfall;llm/stream 是 llm 接缝的事件;tools/pre-execute 等三个是工具管道的事件。循环的「默认行为」只是每个 waterfall 最内层的 next()——框架默认值也是插件。这就是「没有特权核心」在代码里的模样:ReactLoopAgent(第 3 章的主角)实现了一个公开的 Agent 接口,通过 AgentFactory 注入注册表;另一个产品完全可以实现同一个接口,用完全不同的循环语义(比如先规划后执行、或者交互式确认每一步),而模型适配、工具、会话日志一行不用改。
Capability seam:可替换能力的一等公民
dsh 的第三个坐标是 capability seam(能力接缝)——它把「可替换的能力」做成三个角色组成的显式概念:
| 角色 | 职责 | 例:shell 接缝 |
|---|---|---|
| Service Definition | 声明接口的抽象类(extends Service),定义词汇与错误码 | ShellExecutor:resolve(request) / run(spec) / start(spec) |
| Service Provider | 实现接口的插件,作为 ctx.<key> 注册;同一 context 只允许一个实现 | dsh-bash-local(本机 bash)、dsh-bash-sandbox(沙箱)、dsh-pwsh-local(Windows) |
| Consumer | 消费服务的插件,通常是一个模型工具 | dsh-tool-bash:bash 工具的 execute 调 ctx.shell |
「三件套完整才算 seam」是 dsh 的硬规则(docs/glossary.md 原话:The seam is the complete capability, never one role)。它的直接回报是一个在其他框架里很少见的性质:换一个 provider 就换掉整个产品的一个横切面。文件系统、子进程、shell、终端这些接缝共享同一套请求/响应词汇,所以把 ctx.fs 与 ctx.subprocess 从本地实现换成 e2b 远端实现(第 8 章),Bash、PTY、LSP 的消费者一行不改,整个执行世界整体搬进远程沙箱。第 7 章专章拆这个机制。
范式对照:三条路线放在一起
与两本前作放在一起,dsh 的定位会更清楚。把三种范式画成一句话:
| 工具调用范式 | 一切皆程序 | 一切皆插件 | |
|---|---|---|---|
| 代表 | pi(前作一)与多数 agent 框架 | Prime Agent(前作二) | DeepSeek Harness(本书) |
| 模型看到的 | 工具名 + JSON schema 清单 | 一个 ipython 工具 | 按 preset 拼装的工具集(可整体换成 run_code Code Mode) |
| 系统如何长出新能力 | 核心作者加工具/扩展 SDK | 模型用 /refine 沉淀经验 | 任何插件注册进 Context;模型甚至可以运行期安装插件(第 16 章) |
| 会话真相源 | 消息数组 + 会话树 | 消息 + kernel 变量空间 | append-only 事件日志,一切从日志重建 |
| 最大的赌注 | 核心小而稳 | 解释器足够通用 | 插件树足够健壮,不需要核心 |
在《Pi Agent 源码解析》里,agent-loop 是一个被精心维护的核心,扩展系统是核心外的第二层。在《Prime Agent 源码解析》里,核心干脆变成了「把一切能力折叠进 Python 命名空间」。dsh 走的是第三条路:把核心本身拆散成插件,然后用类型系统、事件与 effect 把它们重新缝合成一个没有接缝的系统。它不赌「哪个执行范式更通用」,它赌「可组合的树 + 可审计的日志」能容纳任意执行范式。
实践应用
- 把「可替换性」做成显式概念,而不是事后抽象:dsh 的 capability seam 用「三件套完整才算数」的规则,把「换后端」从重构变成配置。给任何 agent 系统做能力抽象时,先问:定义/提供/消费三个角色是否各自独立成包、独立演化?
- 「注册即 effect」消灭卸载债:把注册做成可逆操作(返回 disposer),HMR、会话销毁、插件卸载时自动回滚,是「系统不散架」的结构性保证。
- 用事件域给扩展点分类:持久事实 / 在飞拦截 / 接缝策略三域分明的系统,比「所有回调都叫 event」的系统更可维护——选对域是第一决策。
- 循环本身也可以是一个接口:把 agent 主循环从框架里拿出来,变成实现
AgentFactory的普通插件,换循环不需要动任何其他子系统。
总结
这一章建立了三个坐标:Cordis 的四件套(Context、Service、typed events、effect)是插件树的语法;三域事件是扩展点的第一层分类;turn flow 是系统行为的骨架——而它们全部指向同一个事实:dsh 没有特权核心,循环的默认行为也只是 waterfall 的最内层 next()。但「没有核心」不等于「没有装配」——一棵树总得从某个地方长出来。下一章我们追着 dsh web 命令,看 profile、bundle 与 patch 分层如何把五十多个包的插件树从配置里拼出来。