00|这不是又一个 Agent 框架:DeepSeek Harness 到底在解什么问题
先别急着找那个循环
第一次打开这个仓库的人,几乎都会做同一件事:找那个写死的 while(true),想迅速抓住"跑 Agent 的那段代码"。
找不到。
你能找到的是几十个可以独立卸载的能力包,一句挂在最显眼位置的口号——everything is a plugin——以及一份不客气的提示:改动核心代码之前,先想清楚你正在动的是不是该拆掉的那一小块。于是第二个误区立刻跟上——"这又是一个把简单循环拆成框架的过度设计项目"。
这个判断是错的,而且错的位置很精确。玩具 Agent 死掉的原因,从来不是循环写得不够优雅。循环本身很便宜,十几行就能写完一个能用的版本。真正贵的是三件事必须同时成立:
- 换一套执行世界(本机、沙箱、远端),模型看见的工具契约不能跟着裂开;
- 进程重启之后,你必须能重建模型当时看见的那份上下文,而不是靠内存里另一份对话记录;
- 加审批、超时、压缩、人工命令这些能力时,不准去改循环本身。
DeepSeek Harness(后面简称 dsh)不是在卖"更好的 ReAct 循环"。它在卖一套可替换的能力边界。循环只是这套边界里最不特殊的一块。
1. 循环不是核心,核心被故意拆没了
先给判断:在这个仓库里,"核心"是一个必须消失的概念。
模型适配器、工具注册表、会话日志,以及 Agent 循环本身,全部以同样的方式挂在同一套共享运行环境上——没有一块代码天生比别的代码特殊,没有一块代码有资格说"你必须先 import 我"。扩展一个能力,做法永远是在旁边再挂一个同级的贡献者,而不是打开已有的那一块去插一段逻辑。
图里故意把 Agent 循环画成跟审批策略、压缩策略一样的方块——因为它们的地位确实一样。循环只是"叫模型、跑工具、再叫一次"这一件事的默认实现;换一个循环实现,其余方块不用动一根手指。
这不是一句比喻,打开仓库能直接看见字面意思。产品基础层的组装配置,就是一份{ id, name, config }的扁平列表——每一行只是"这个位置挂哪个包":
- id: sandbox
name: '@deepseek-ai/dsh-sandbox-local'
- id: approval
name: '@deepseek-ai/dsh-user-approval'
- id: tools
name: '@deepseek-ai/dsh-tools'
- id: system-prompt
name: '@deepseek-ai/dsh-system-prompt'
- id: agent-loop
name: '@deepseek-ai/dsh-agent-loop'
config:
agents: []沙箱、审批、工具注册表、Prompt 组装、默认循环实现——五行并排,字段结构完全一样,谁都没被标记成"这是核心,加载顺序要特殊处理"。组装脚本看到这份列表时,不知道也不需要知道哪一行是"循环":它只是按 name 找到对应的包,把它当成众多插件里普通的一个去挂载。
包内部的登记方式也是同一套。工具注册表、会话日志、Agent 注册表这几个包,各自的实现类都继承同一个 Cordis 基类 Service,构造函数里都做同一件事——比如 Agent 注册表的构造函数开头是 super(ctx, 'agents'),工具注册表、会话日志的构造函数分别是 super(ctx, 'tools')、super(ctx, 'sessions')。这一句话做的事情很朴素:把自己发布成 ctx.<那个名字> 这个键,同时框架暗中记下"这条发布跟随我所在的这段生命周期,生命周期结束就自动撤销"。没有一处框架代码需要认识"这是循环"或"这是工具"这种特殊概念——对 Cordis 而言,它们都只是调用了同一个基类构造函数的普通服务,区别仅仅是传进去的那个字符串键名。
更关键的一条纪律:这套仓库要求每一个能力包自己声明"我贡献了什么、卸载时收回什么"。你新写的审批插件和官方的工具注册表,遵守的是同一份规则。没有这一层会怎样?你会得到每个玩具仓库最终都会变成的样子:一个巨大的 runAgent() 函数里塞着工具分发、权限判断、重试、压缩、子代理调度、界面回调。Web 版和命令行版从某一行开始分叉,沙箱版再 fork 一次。三个月后没人敢动这个函数,因为它已经等于产品本身。
dsh 的选择相反:循环穷到只做"叫模型、跑工具、再叫一次"三件事,穷是为了让它可替换。第 02 章会讲清楚"贡献可以被撤走"这件事具体怎么落地;第 03 章会讲清楚"循环只是接口的一种实现"。现在你只需要记住一句话——依赖循环内部细节的代码,就是在把可换的司机焊死在车上。
2. Harness 这个词不是包装,是三根缰绳
很多人把 harness 读成 framework 的谦辞式说法。在这个项目里它是字面意思:一套把模型、工具、会话、人和进程箍在一起的马具,而且每一根缰绳都能单独换。
三根缰绳决定了它既不是"调用编排库",也不是"带界面的聊天机器人"。
第一根缰绳:模型看见的,必须能从日志重建
系统把会话日志当成模型上下文的唯一来源。模型实际看到的那份消息列表,是从日志"投影"出来的,不是内存里另外维护的一份真相。进程重启、话题分叉、生成聊天记录、上报遥测数据,全部从同一条事件流里长出来。这条纪律被总结成一句很硬的话:模型看见的,等于已经被记下的。
没有这一层会怎样?重启之后能不能恢复现场就成了玄学,界面上看到的内容和模型实际收到的内容会慢慢对不上,出问题的时候你只能对着内存里的一份临时变量猜。第 05 章专门拆这件事。
第二根缰绳:能力必须成套出现,不能只加一个函数
一个"能力"从来不是孤零零一个函数,而是三个角色同时出现:谁定义这个能力该长什么样、谁去真正实现它(可以有好几种实现,比如本机执行和远端沙箱执行)、谁去消费这个能力(通常是暴露给模型的一个工具)。三个角色只要缺一个,就不算把能力做完整。
以"执行命令"这个能力为例:文件系统和子进程共享同一套"执行世界"的定义,把这套定义指向远端沙箱,命令执行、伪终端、语言服务器这些能力会一起跟着搬过去,不需要给每个能力单独写一份沙箱版本。
注意箭头方向:消费者永远只依赖"定义",从不直接依赖某一个具体实现。这样换实现的时候,消费者一行都不用改。没有这一层会怎样?你以为自己只是加了一个"跑命令"的函数,实际上是把"怎么跑命令"这件善变的事,焊死在了"模型怎么说话"这件稳定的事里面。第 06 章会拿这个例子把三个角色彻底拆开讲。
第三根缰绳:产品形态是启动时叠出来的,不是在代码里 fork 出来的
一个正在运行的 dsh 实例,是按层顺序叠出来的一棵插件树。最底层是每种产品形态都要用的基础层——模型、工具、持久化、沙箱、审批策略;再往上叠一层就变成带界面的 Web 产品;换成叠另一层,就变成没有服务进程、跑一次就退出的命令行工具。层与层之间用配置补丁拼接,上面的层永远可以整行替换下面层里的某一条配置。
没有这一层会怎样?Web 版和命令行版会变成两个各自维护的仓库,同一个 bug 要修两遍。第 09 章专门讲这套启动时组合的机制。
3. 你在这本电子书里学的不是包名,是一条读者路径
这个项目本身的文档已经很详细了。你缺的不是又一份目录清单,你缺的是一条路径:先建立"这套系统到底在害怕什么",再按害怕的东西去理解设计。
后面十一章按这个顺序拆:
| 你现在可能有的误区 | 后面哪章拆穿它 |
|---|---|
| 先把包名背下来才能上手 | 01:只认三层结构——控制脊、可替换能力、产品外壳 |
| 插件系统就是一个依赖注入容器 | 02:真正贵的是"贡献可撤走"和"拦截可否决" |
| 扩展当然应该直接调用循环内部逻辑 | 03:Agent 是一份接口约定,循环只是默认实现 |
| Agent 就是一个 while 循环 | 04:真正的心跳是待处理队列、准入检查、请求-响应节拍 |
| 内存里的消息列表就是历史 | 05:历史是从日志投影出来的一个视图 |
| 加能力等于加一个工具函数 | 06:能力必须凑齐定义、实现、消费者三个角色 |
| 工具注册表就是一个名字到函数的映射 | 07:策略、超时、沙箱都挂在执行管道上,不进循环 |
| 子代理应该继承父代理的全部工具 | 08:能不能看见和归谁所有是同一件事,继承关系只是数据 |
| 换界面就要改运行时逻辑 | 09:换产品形态是叠一层配置,不是复制一份代码 |
| 人工输入的命令也是发给模型的消息 | 10:人机协作平面完全不经过循环 |
第 11 章会把这些收成五层判断,告诉你自己动手做类似系统时该先照抄哪几块。附录留给压缩、持久化、委托、界面渲染、运行时校验这几个真实但会打断主线的话题。
还有一件事必须现在说清楚:这个项目目前处于早期预览阶段,明确声明会有破坏性变更,磁盘存储格式也可能不向后兼容。所以这本电子书教的是判断力,不是一份保证明年还能直接对应到某一行代码的说明书。你现在读它,图的是看清楚设计者在打地基的时候,主动拒绝了哪些图省事的做法——那些拒绝,比当前的具体实现细节更值钱。
4. 结语:带走一句话
DeepSeek Harness 值得读,不是因为它把 Agent 循环写得更精致,而是因为它把循环降级成一个可替换的插件,逼所有真正贵的东西——日志、能力、组合、人机协作——都站到循环外面,变成边界。