
DeepSeek Harness 拆解
对照源码,直接看DeepSeek Harness里最有价值的10大内容

两个多月前我在《别再问我训不训模型》里写过一段话:最让我意外的是 DeepSeek,一个全球最强的开源模型团队,专门在北京设了 Harness 团队,公开招 PM 和研发,做对标 Claude Code 的产品。
8 月 13 日晚上,仓库真的放出来了,叫 DeepSeek Harness,命令行工具缩写 dsh。GitHub 仓库北京时间 19:56 创建,当晚 npm 上连发好几个版。开源一个多小时 star 破两万,媒体报道说不到两天冲到十万,是 GitHub 有史以来涨星最快的项目。
我没急着跑 demo,先 clone 了下来,后来干脆把 git 历史和决策记录也翻了一遍。55 万行 TypeScript,219 个包,647 个测试文件。第一次提交在 6 月 10 日,只有三个文件:README、AGENTS.md,还有一个指向 CLAUDE.md 的软链。最后一次提交停在 8 月 13 日,发布全家桶,对应 PR 编号 2519。中间两个月,6683 个非合并提交,32 位作者,最猛的一天是 7 月 30 日,单日 471 个提交。
先纠正一个网上流传很广的说法:它不是 Rust 写的。源码里一行 Rust 都没有,主体是 TypeScript,Linux 沙箱那部分是个 300 行左右的 C 程序。传的人多,不等于对。
下面十个东西,按我认为的重要程度排。每一项我都查了它的出生日期,看完有个规律会浮出来:这些设计没有一项是项目第一天就有的。
一、循环不是内核,是个包
先说清楚循环:发上下文给模型,拿回复;模型要调工具,就去执行,把结果拼回去再问;说收手,这轮结束。所有 agent 框架的核心都是这段循环,所以多数框架把它焊死在主干里,想改得整个换。dsh 把它做成了普通包。
接口和实现拆成两个包:core/agent 定义 Agent 接口和事件词汇(ctx.agents),packages/core/agent-loop 是具体实现(ctx.agentLoop)。别的插件只对着 ctx.agents 编程,没人 import 循环源码,循环整个换掉,其他插件一行不用动。模型适配器、工具注册表、会话日志这些也都是插件,跟循环平起平坐。
这不是事后改造。第 5 个提交,6 月 11 日,标题就是 Implement the agent loop plugin,循环生下来就是插件。架构文档原话:不存在需要打补丁的特权内核,扩展 dsh 就是把插件挂到别的插件旁边。

底层框架叫 Cordis,vendor 进仓库。核心思想:注册是可逆副作用,ctx.effect() 记下撤销方式,卸载时逆序清理,热重载不残留。运行中的 dsh 是一棵从空根按序叠出的插件树,dsh --profile web --dump-config 逐行打印,每行注释标明出处和改动它的层,像读 Git blame。我试过,模型在树里也就是一行,和文件系统提供方、沙箱策略躺在一起。
搬家这种事,这仓库干过不止一次:6 月 20 日全仓库重排进分层目录;7 月 30 日按能力分组再排,顺手把 cordis 目录改名 self-modification;开源前夜又改成 extensions。一个目录两个月换两次名,不留别名不留垫片。焊死的架构,搬一次就够呛。
二、模型看到的一切,必须能从日志重建
打个比方:模型没有自己的记忆,它每一步看到的对话,都是开庭前重新念一遍的卷宗。卷宗里没有的,等于没发生过。
dsh 里没有"存了一份对话历史"这个概念。会话日志是一串只追加的事件,turn/start、user/message、assistant/chunk、tool/call、tool/result,发给模型的上下文由一个叫 deriveMessages() 的函数从这串日志投影出来。投影有专门的缓存包伺候,日志没变就不重算,所以这套纪律跑在生产负载下,不是纸面架构。
并发和取消也没漏。模型并行调五个工具,调度器真的并发执行,但结果永远按模型给出的顺序落日志,并发是实现细节,不进日志语义。中途取消,没来得及启动的调用会补写一条合成错误结果,保证重放永远成立。
光有约定不够,还有运行时断言。7 月 19 日他们做了一次全仓库的动作:一口气给十几个包各加一个 invariant.ts,把"这个包最不能破坏的东西"做成断言。agent-loop 那份挂在模型请求的必经之路上,先断言请求冻结、带 sessionId、日志里有 step/start,再把请求里的消息数组和投影结果各自 JSON 序列化,逐字节比对,失配就炸,错误名叫 log-reconstruction desync。我第一反应是这也太贵了,每次请求都付一遍全量序列化的税。再想想就明白了:这笔税买回来的是任何会话都能重放、能审计,纪律不靠 code review 维持,靠每次请求都在跑的谓词。顺带一提,事件溯源 6 月就写进了地基决策记录,执行它的断言 7 月 19 日才补装上。
类型层也堵死了。SessionEvent 是条件交叉类型,turn/start 在编译期就带不上 surfaceOp 这种字段,写错过不了编译。事件的 ignorable 标记缺省值是"必须":老版本运行时读到不认识的新事件,宁可拒绝重建整个会话,也不静默跳过。8 月 10 日一篇决策记录专门讲日志版本号,起因是发布前的 issue #1901,结论就一段:版本号用单个递增整数,不搞 major.minor,因为某一步能不能自动升级是那一步自己的属性,轮不到数字形状预先承诺。忘打标的后果,从静默吞掉半个会话,降级成多问一次。
三、模型这一步看什么,插件说了算
快递发出前过安检,安检员能放行、能拦下,也能把箱子里的东西换掉再放行。模型收到输入之后、请求发出去之前,也有这么一道机器,叫 agent/pre-step 瀑布事件。
先看挂在上面的都是谁。全仓库十三个监听器:两个 hooks 兼容桥,把 Claude Code 和 Codex 的 hook 协议翻译成 dsh 自己的事件,老用户的 hook 脚本大体能直接搬;压缩引擎;plan 模式;goal 驱动器;重复调用守卫;时间上下文,往每次请求里补带时区的当前时间和浏览器时区,不然模型连今天几号都得靠猜;tmux 上下文;技能加载器;等等。
监听器只有两个选择:放行,或者拒绝。放行时可以把整批消息整个换掉,压缩引擎就是这么干的,在同一个挂点把长历史换成摘要。瀑布是环绕式中间件,监听器不调 next() 就短路整条链;对单决策事件来说,短路就是设计意图,仓库规范里甚至明文要求瀑布监听器必须调 next(),这条排进了硬规矩清单。
拒绝也留痕。输入被拦时,日志里仍会落一个不含步骤的空轮次,这次尝试发生过、被谁拦的,事后查得到。系统提示词的组装走同一条瀑布,但部署方标记为 complete 的段落会在瀑布之后被强制放回去,插件改不穿部署方的底。想在请求前插项目背景、拦掉某类提问,都是挂一个监听器的事,循环一行不用改。
四、换一个轮子,全家搬走
笔记本不关心插座背后是火电、水电还是隔壁机房的沙箱。dsh 把每项能力拆成三个角色:Service Definition 定义接口,Service Provider 实现,Consumer 使用,官方叫 seam,接缝。术语表里还立了规矩:seam 指完整能力,不许拿它指其中一个角色。
有条细则我第一遍读当洁癖:Service Definition 绝不写成 TypeScript interface,必须是自带运行时上下文键的 Cordis Service。想通只用了一秒,interface 在运行时不存在,而接缝要在运行时被枚举、被替换,写成交叉类型就查无可查。类型是给编译器看的,接缝是给运行时用的。
拿 shell 举例:dsh-shell 是定义,dsh-bash-local 和 dsh-bash-sandbox 是两个提供方,dsh-tool-bash 是消费方。妙处在文件系统、进程、终端共享同一个执行世界,把它们指向远程沙箱,Bash、终端、LSP 语言服务就一起搬过去,不用给每个工具单独做远程版。抽象一致到连沙箱的拒绝方言都封装了:confine() 返回的不只是包装后的命令行参数,还有这个后端拒绝访问时 stderr 长什么样,上层不用知道底下是 bubblewrap 还是 Landlock。一个包也可以同时扮演两个角色,dsh-llm 既是 LLM 的定义方又是它的消费方。
subagent 也是一条接缝,提供方从进程内子 agent 到把另一个产品当后端都有,仓库里就有个 subagent-claude-code 包,让 dsh 把 Claude Code 当自己的子 agent 跑。LLM 那条接缝反而克制,流式词汇表只有七种 chunk,tool_choice、top_p 这些参数因为没有生产者,直接不进词汇表。默认模型是 deepseek-v4-flash 和 deepseek-v4-pro,上下文窗口按一百万 token 记。这套接缝是 6 月打地基时铺的,capability-seams 列在最早一批架构决策记录里,6 月 22 日连压缩都做成了接缝。
五、每个 agent 一个自己的世界
多 agent 系统最容易脏的就是工具箱,所有 agent 共享一套全局工具,互相污染。dsh 的答案是 scope:一项注册要么全局,要么属于恰好一个 agent。新来的 agent 不去公共工具间抢扳手,每人一间自己的工位,工具按人配发。钥匙就是 agent 本人,按对象同一性比较,名字撞了也没用。
同名注册,离得最近的赢。给某个 agent 单独注册一个同名工具,只在这个 agent 眼里替换掉全局那个。方向也讲究,scope 包 README 的原话:注册视图沿链向下继承,子作用域看得见祖先各层,近者遮蔽远者;事件放行沿链向上扩展,祖先的监听器收得到子孙的事件,反向永不成立。preset 的常驻挂载就是它所有 agent 的父作用域。四个出厂 preset:standard 是完整 coding agent,cordis 是标准版加自指工具集,code 面向代码任务,minimal 只剩 bash 和一个编辑器。
限制是另一条路。restriction 过滤全局工具集,多个限制取交集,被滤掉的工具连提示词都不进,调用也拒绝执行,跟不存在无法区分。这条路同样 fail closed。
创建 agent 时有个组装窗口:上下文先铸出来但不发布,你的配置代码在这个窗口里装工具、限权限、换 persona,成功才发布,失败整体回滚,任何观察者都看不到一个配置到一半的世界。有一条规矩很见功力:agent 中途想换 preset,只有它还什么都没产出时允许。因为日志里已经落下去的工具调用,新的工具组合可能再也做不出来,换装等于伪造历史。连这个会话用了哪个 preset,本身都是一条会话事件,理由还是那条:模型所见必须能从日志重建。
这个机制两周里长了两轮:7 月 8 日进注册原语,7 月 12 日推倒重写 RFC,7 月 21 日又落地分层存储。不心疼。
六、自动续跑的两个保险栓
长任务是 agent 落地最难的部分。dsh 给了两套机制,思想一致:自动化要有预算,停下来的时候人要在场。
第一套是 Goal,可以理解成自动驾驶。目标挂在会话上,四态状态机,active、paused、blocked、complete,轮次上限默认 256。驱动器源码里,每一轮启动前就三道闸:phase 必须 active,activation 必须 armed,已启动轮数没到上限。超限不会崩,目标转入 blocked,错误码 round-limit。同一会话里无关的人类轮次,不烧这份预算。
最有味道的是 armed 和 disarmed 这对状态。它标记"允许自动继续",但只存在进程内存,永远不写进日志。会话恢复或 fork 之后,目标还在,武装状态丢了,必须人类手动 resume 才重新武装。防的就是这种事:无人监督的自动循环,在你重启进程之后自己活了。

第二套是 Ralph,Geoffrey Huntley 那套玩法的产品化。每一轮起一个全新的 agent,不继承父对话,跨轮状态压成一份五字段交接:状态、摘要、证据、下一步、阻塞,序列化后不得超过 16384 字符。相当于每轮换一个新员工,交接全靠一张不超过一页的纸。轮数默认上限 256,而且模型只能往下调。规则也狠:某一轮的子 agent 普通失败,不重试这一轮,整个 run 直接终止。
旁边还有个循环卫生守卫。同一个工具用完全相同的参数连续调用,第 3 次温和提醒,第 5、8 次给详细版,附工具名、连击次数、参数预览。判定"完全相同"有讲究,参数先递归排序 JSON 键再序列化,键的顺序不同骗不过它。这个插件出生时叫 repeat-tool-guard,守卫,开源前夜全库改名时改成了 repeat-tool-reminder,提醒。名字跟设计一致:只提醒,不拦截,人类一发言整条链清零。
翻提交记录,goal 的诞生只用了三天:7 月 16 日先写 RFC,321 行;7 月 19 日一天五篇决策记录落地;7 月 20 日 Ralph 进来,它的决策记录开篇就跟 goal 和 workflow 划清界限,明说这是对前两种循环都不满意之后的第三种。256 这个数字在两套机制里各出现一次,我当它是同一个哲学落了两次地。
七、压缩先锯中间,再动用模型
搬家装箱,先扔包装泡沫和说明书,实在塞不下,才请档案员来写摘要目录。dsh 的上下文压缩就是这个顺序,两级阀门,便宜的先上。
触发线是上下文窗口的 0.8。第一级剪枝:超过 8192 字符的工具结果,保头部 4096、尾部 1024,中间挖掉,原位接一行标记文本,源码里这个常量就叫 PRUNE_MARKER,内容精确到前后各两个换行。这一级零模型调用,完全可重放。剪完重新测量,低于阈值就到此为止,模型一次都不用叫。
不够才进第二级摘要,提示词是固定的八段模板,从 Primary Request 到 Critical Context。两个细节单独说。摘要请求复用原会话的 system prompt 和消息前缀,压缩指令作为最后一条消息追加,为的是吃 provider 的 KV 前缀缓存:前缀没变,缓存就命中,摘要这次调用不浪费钱。选压缩区间的算法保证永远不拆散一个工具调用和它的结果这对配对,拆散的日志没法重放成合法请求,这又绕回了第二节那条纪律。
如果请求已经把窗口撑爆,还有第三条错误恢复通道:绕过阈值,尾部保留预算直接设为 0,强制一次有效缩减再重试。
提交记录把优先级排得明明白白。compaction 的决策记录 6 月 18 日就写了,接缝 6 月 22 日铺好;7 月 16 日剪枝连进三轮提交,round 1 做剪枝,round 2 加固集成,round 3 对齐契约。先用免费的锯子,再花钱请模型,决策比代码早四天。
八、agent 能拆自己坐的引擎

修理工随身带一台小机床,缺什么工具当场造一个。既然一切皆插件,那运行 harness 的 agent 能不能看见并修改这个插件系统本身。dsh 的答案是能。
packages/extensions/tool-cordis 给了七个工具:cordis_inspect_list 列出运行中的插件,cordis_inspect_query 查注册表,cordis_inspect_self 看自己,cordis_define 定义新插件,cordis_run 运行,cordis_stop 停,cordis_undefine 注销。插件代码跑在 node:vm 沙箱里,process 和 Buffer 保持 undefined,require、fetch 这类调用抛出的错误会指向 Cordis 的替代品。挂上去的插件只存在进程内存,重启即消失。
有意思的是 7 月 8 日它出生时的决策记录。当时只有三个工具,inspect、mount、unmount,设计重心根本不在"让模型跑代码",在三个正确性问题上:注册时就校验 schema,别等拼 prompt 时才炸;给模型一份生成的 API catalog,别让它盲猜签名;mount 上去的东西必须完全可回收。结构化逐能力注册的方案被明确拒绝了,理由是一个 mount 原语同时覆盖 tool、listener、service、inject 四种能力。信任立场也写得很直白:当成 bash 权限对待,沙箱只防全局污染,算不上安全边界。
它后来还长了一轮:8 月 12 日,开源前一天,一次提交把动态插件运行时和配套 UI 都扩了进来,第二天就开源。我在上一篇写过斯坦福的 Meta-Harness 实验,让模型自己改自己的 harness,小模型 Haiku 4.5 跑上了 TerminalBench-2 榜首,当时我觉得这事离产品还远,结果两个月后,DeepSeek 把它做成了 pnpm run demo:cordis 就能跑的功能。顺带一个真实的细节:这个模块的 README 还在描述旧的三工具版本,源码已经是七个工具。两个月 2500 个 PR 的仓库,文档追不上代码,反而显得诚实。
九、安全的默认是拒绝
两句话概括这块的设计:门禁断了电就锁死大门;寄快递前,把写着密码的便利贴从箱子上撕掉。
那 300 行 C 就是干这个的。landlock-run 是个自限后执行的启动器,C11 写的,静态链接 musl,启动子进程前先给自己的进程套上 Landlock 文件系统白名单,再执行目标命令。权限在 exec 之前就没收,程序起来时已经在笼子里。内核不支持时直接以退出码 125 失败,绝不放行一条未受限的命令。--probe 也不查版本号,而是真实构建并强制执行一个规则集再报告,因为有的内核有这个 syscall 但拒绝执行,问了也白问。沙箱后端按平台选:Linux 先找 bubblewrap 再落 Landlock,macOS 用 seatbelt,Windows 用受限令牌加 ACL。连这块都出过事故,仓库里有一篇编号复盘,检讨 landlock 的部分通知把子进程失败误报成了别的东西。

权限模型只有两个正交旋钮:沙箱模式管文件效果,审批策略管要不要问人,没有工具名 allowlist。审批链路同样 fail closed:回答服务不在、抛异常、返回词表外的值,一律归一成 unavailable,语义就是拒绝。升级审批唯一放行词是 allowed-once,且必须发生在一个打开的轮次里,审计事件要被轮次的提交边界包住,否则重放时会被当垃圾丢掉。
两个细节我专门核了源码。子进程的环境变量先过一道清洗,名字匹配 KEY、PASSWORD、SECRET、TOKEN 的一律不透传,DSH_ 前缀的也不透传,harness 自己的 DEEPSEEK_API_KEY 都不能隐式漏给子进程。凭据管理是引用制,配置文件里只出现环境变量名,永远不出现值;凭据文件权限不是 600 直接拒绝加载;YAML 解析出错只报行号列号,不引用出错的那行源文本,代码注释写着理由:密钥的名字可以印,值不行。
十、给 agent 立规矩的工程学
最后这块谈不上功能,更像给一群不睡觉、不看群、记性全靠一本卷宗的新同事写的员工手册,但可能是最值得抄走的。
这个仓库的默认假设是:主要贡献者是 agent。别忘了第一次提交只有三个文件,README、AGENTS.md,还有一个指向 CLAUDE.md 的软链。第一行代码进仓库那天,给 AI 写的规范已经在里面了。AGENTS.md 现在 149 行,第一句就是让 agent 在改 packages 之前先读架构文档,后面全是硬规矩:瀑布监听器必须调 next();跨包边界的 id 必须品牌化,不许裸 string;空的 catch 必须写清楚它吞掉了什么。
决策记录叫 Agent Note,implemented 目录 507 篇,另有 25 篇 proposed、11 篇 rejected,每篇固定四节:问题、决定、放弃的备选、后果。规矩是非平凡改动必须随 PR 附一篇。翻月份很有意思:6 月 51 篇,几乎全是架构地基,一篇 bug 修复都没有;7 月 258 篇,机制爆发,bug-fix 类 7 月 19 日才出现第一篇;8 月前 13 天 190 篇,转向产品化。提交数走的是同一条曲线,6 月 385 个,7 月 4227 个,8 月前 13 天 2071 个。仓库的成长史,写在两本日历里。
测试门槛是我见过最狠的:packages 下每个源文件,语句、分支、函数、行数四项覆盖率全部 100%,按文件算。配置里的注释写着理由:一个覆盖好的大文件,不能补贴一个裸文件。快照测试不需要 API key,因为夹具就是会话日志本身,日志里的流式事件存着每一个 chunk。这套体系也出过事,有编号复盘:某次 YAML 里的 disabled: !!js 表达式只在插件 config 内被求值,元数据被原样消费,truthy 对象导致文件系统工具在所有模式下永远禁用;更糟的是快照刷新把 UNKNOWN_TOOL 当成了新的预期基线。复盘里有句话,我原样翻译过来:快照刷新只算生产夹具,算不上正确性评审。之后的修法是三层护栏:显式覆盖层、配置校验拒绝元数据里的表达式节点、快照框架拒绝 UNKNOWN_TOOL。
被拒绝的方案也归档。有篇 note 提议删掉整个 workflow 进度事件系统,理由是没有任何生产监听者订阅,拒绝理由一行:这是故意留的观察接口,该做的是给它造消费者。月底 web 客户端浪潮真的把消费者造出来了。还有一篇全仓库的 Not Invented Here 审计,连负面结论都冻结成记录,免得后人从头再查一遍。两个例子:拒绝 vscode-jsonrpc 替换手写的 LSP 分帧,因为它表达不了消息大小上限,还把取消宽限期的语义弄反了;拒绝 p-retry,因为重试在这里是返回决策的瀑布监听器加从持久日志重放,根本没有可供它重新调用的函数,那些库的整个 API 依赖的前提不存在。
开源前夜还有一次全库改名,8 月 11 日的决策记录写得坦白:仓库长得比名字快,发布前最后一扇窗让全库改名变得便宜,留着弱名字会把偶然词汇固化成兼容性契约。那篇 note 还附了一张角色词表,Controller、Store、Directory、Presenter 各自什么场合不许用,命名被当成接口契约来管。于是 goal-session 改成 goal-round-driver,repeat-tool-guard 改成 repeat-tool-reminder,task 改成 job,无别名、无兼容包、无回退解析。整个 8 月是一场自觉的删除节食:删掉 TUI 包、删掉 CLI demo、删掉 SDK 工具链,开源当天的最后一批提交里还有一条,删掉首运行的 beta 提示。
贡献者名单里藏着一个彩蛋:Cordis 框架的原作者 Shigma,只有 2 个提交。第一名 Tianyi Cui,2252 个。框架是社区的,把它长成 harness 的活,是这个团队自己一行行干出来的。
现在我读一个新的 agent 框架,习惯先找它的不变量文件,再读主流程。dsh 每个包都有一个 src/invariant.ts,断言这个包最不能破坏的东西。十件事里有九件最后都落回那条日志纪律,唯一故意不进日志的,是 goal 的武装状态:机器停下来之后,必须有人重新点火。连什么不记录,都是设计过的。

十件事读到最后,印象最深的是这个仓库的默认假设:主要贡献者是 agent。员工手册、五百多篇决策记录、四项覆盖率 100% 的门槛,服务对象是一个不睡觉、不看群、记性全靠日志的家伙。写规矩的人心里门清,他们管的这东西,比自己勤快,也比自己忘性大。
Agent 能拆自己坐的引擎,官方管它叫创造模式,是四个出厂预设里唯一一个让 agent 改自己的。它在内存里定义插件、装上去用、用完卸掉,全程不用重启进程。Agent 能自己给自己造工具了,造完还能拆。到了这一步,人剩下的活变成另一件事:想清楚什么能拆,拆完凭什么证明没装坏,再把结论写进决策记录,让后来的人查得到。人还是在干活,只是干的活从拧螺丝变成了定规矩。
给 agent 立多少规矩算合适,我没想明白。立少了它乱来,立多了它缩手缩脚,AGENTS.md 里那些硬规矩,哪条是必要的、哪条是写的人自己吓自己,现在没人说得清。可能得等我真带过一个 agent 团队,踩过几个坑才答得上来。在那之前,我对这些规矩的态度是先用着,边用边怀疑。


在 AI 重塑一切的时代,重新理解人和工具的关系。

长按识别二维码关注
© SooKool · 公众号原创内容
夜雨聆风