两种Harness设计哲学——Claude Code的单体循环 vs DeepSeek Harness的微内核,以及我们的底座会怎么选
从去年研究 Claude Code,到这两个月追着 DeepSeek Harness(dsh)读文档,两套 harness 我都拆得差不多了。
它们解决的是同一组问题:循环怎么组织、上下文怎么管、工具怎么执行、出错了怎么恢复、怎么保证安全。但给出的答案经常截然相反。
作为 dsh 系列的最后一篇,把两者放在一起对比一下,也说说如果重做我们的底座,我会怎么选。
一、一句话概括
Claude Code 是一个产品:核心是一个单体的 Agent Loop,行为主要靠提示词控制,默认简单,需要时再叠加复杂度。它的很多设计,是在海量真实用户的数据上一点点打磨出来的防御性工程。
dsh 是一个平台:没有特权内核,连 loop 都是插件;会话是一条只追加的事件日志,模型看到的一切都从日志派生。它的很多设计,是想办法让错误的状态在结构上就无法出现。
简单的说,Claude Code 擅长"在运行时把错误兜住",dsh 擅长"在结构上让错误不可表示"。
二、逐项对比
| 维度 | Claude Code | DeepSeek Harness |
|---|---|---|
| 架构 | 单体循环,子系统挂在循环的各个阶段 | 微内核,一切都是插件,loop 可替换 |
| 状态的真相 | 消息数组 + 各子系统自己的状态 | 只追加的事件日志,历史是它的投影 |
| 缓存稳定 | 管理出来的:分段、边界、锁存、检测 | 长出来的:日志只追加、投影是纯函数 |
| 扩展 | 工具、技能、hooks、插件等扩展点 | 按"挂到哪里"表注册到事件和 seam |
| 重试 | 通信层统一重试,加降级 | 适配器一次调用一次尝试,重试是挂在事件上的策略插件 |
| 取消 | 中断检查 + 选择性级联取消 | 每 turn 一个取消持有者,协作式,等待静止 |
| 安全 | 权限管线 + 自动审核 + 沙箱 | 审批 seam + 沙箱,两个正交旋钮,fail-closed by composition |
| 多 agent | 子 Agent、Fork、协调者、共享任务图 | Session + Activation,子优先的所有权 |
| 长任务 | 计划模式、Todo 等内建能力 | plan、goal、jobs、workflow 全是可选插件 |
下面挑几项展开。
1. 缓存:管理出来的,还是长出来的
Claude Code 为了缓存稳定,做了一整套防御:系统提示词分静态区和动态区,易变段落要写理由,请求头锁存,日期记忆化,动态列表改成附件,最后还有一个专门的检测系统,告诉你缓存为什么掉了。
dsh 换了一个角度。日志只追加,消息由纯函数逐条投影,那么只要配置不变,每一次请求都是上一次的追加扩展。前缀天然就是稳定的。改写历史必须通过"声明式的替换",而每一次替换都在日志里留下记录,所以"缓存为什么掉了"不用去检测,翻日志就能看到。
不过要公平地说一句,dsh 的方式也不是零成本的:所有影响请求的东西都必须进日志,所有改写都必须走替换协议,对插件作者的约束要强得多。
2. 安全:管线,还是正交的旋钮
Claude Code 的安全是一条权限管线:deny、ask、工具自检、不可绕过的安全检查、模式裁决,灰色地带交给另一个模型审核,最后用沙箱兜底。
dsh 把安全拆成了两个正交的旋钮:
- 审批:回答"这个具体的动作能不能继续",结果是一个封闭的集合:
allowed-once、rejected、cancelled、unavailable,只有第一个放行,而且只放行被问到的那一次; - 沙箱:只管文件效果,并且执行的强度(
full或partial)作为一个事实上报,而不是一个承诺。
dsh 最有意思的一点,是它的安全默认值来自**"什么都不加载时会发生什么"**。没有回答者、回答者抛错、回答不合规,全部变成 unavailable,调用方拒绝。不加载审批插件,本身就是 fail-closed 的。这叫 fail-closed by composition。
两者在"一次授权不等于永久授权"上倒是不谋而合。Claude Code 把它写在提示词里,dsh 把它写进了类型:allowed-once,从名字上就没给"永久"留余地。dsh 还把"总是允许"这个功能推迟了,因为它需要先想清楚授权的范围是什么:一次调用?一个路径?一个前缀?一个会话?还是一个时间窗?
3. 正确性:运行时兜底,还是结构上不可表示
这一点是两者最根本的差别。
Claude Code 里到处是运行时的防御:每种自动恢复配一个单次尝试守卫,自动压缩连续失败 3 次就熔断,缓存中断有两阶段检测,自动审核被拒多了就回退给人。这些设计很务实,而且每一条背后基本都有一次真实的事故。
dsh 则反复强调"约束生产者,而不是放宽读者":错误的请求要在接口层面就不可表示,而不是事后比较、发现不一致再告警;所有模型可见的输入必须写在 turn 之内,开发模式下违反了直接抛错;失败信息里刻意没有 retryable 字段,免得把策略嵌进事实。
4. 压缩
压缩两边都有,思路也有差别。
dsh 把压缩拆成了两个触发:上下文压力大了,在下一次 pre-step 时压缩;请求已经溢出了,在请求错误的扩展点上做恢复。压缩时先裁剪工具结果,再做摘要;并且只有当历史确实被改写了(替换计数前进了),才去重试,否则保留原始的错误。容量归模型适配器,策略归压缩插件,计量归共享的 token meter,三者分开。
压缩这个话题,之前读 pi-mono 源码时专门写过一篇,这里就不展开了。
三、为什么会不一样
两套设计差别这么大,我觉得根本原因在于它们要解决的问题不一样。
Claude Code 是一个产品,由一个团队开发,有海量的真实用户。它可以在内部灰度、A/B 测试、看线上数据,所以很多设计都是"先观察,再修复",用数据驱动出来的。它不需要让别人替换自己的 loop。
dsh 是一个平台,要支撑 Web、无头、SDK、自动化服务好几种产品形态,还要允许社区在上面扩展、替换任何组件。插件作者它管不过来,没法靠"大家都小心一点"来保证正确性,只能把正确性写进结构里。
所以没有谁对谁错,产品倾向于运行时兜底,平台倾向于结构约束。
dsh 的文档里有一组"五个习惯",我觉得是它整套设计的底色:
- 让正确性显式化:写进结构,而不是事后用时序补偿;
- 保持单一的真相来源:一个事实只有一个家;
- 通过注册的 seam 扩展:新能力注册进来,而不是往已有的载体里加分支;
- 让改动面等于评审面:改动只包含它声明的行为所需的那些行;
- 要求证据:断言要可证伪、可重算,性能相关的改动要附带测量。
它的几篇事后复盘也指向同一个教训:单元测试全绿,不代表装配起来的系统是可用的。要用真实的加载器、真实的组合、真实的外部状态去验收。这条对我们这种插件化的底座同样适用。
四、我们的底座会怎么选
回头看我们的底座,它其实已经是两者的混合:Agent Loop 是照着 Claude Code 做成显式状态机的,工具、记忆、技能、观测则是以插件的形式挂在生命周期事件上,更接近 dsh 的方向。
这个系列读下来,我打算从 dsh 搬过来的有这么几样:
- 日志就是真相:模型历史从日志投影,改写历史走替换,模型看到的和人看到的分成两种投影,语音打断的问题正好用得上;
- 事件按域拆开:已经发生的事实、可以拦截的控制点、能力上的策略,三者不再混在一起;
- 一次调用一次尝试:只让一层负责重试,每一次尝试都进 trace;
- 取消区分有没有副作用:被取消的工具调用,明确标出"从未派发"还是"可能已经执行";
- 审批 fail-closed by composition:缺席即拒绝,授权只对这一次有效。
暂时不搬的:
- 把 loop 也做成可替换的插件。我们目前只有一种 loop,为"可替换"付出的抽象成本现阶段收益不大;
- 整套 Cordis 的插件框架。我们的底座是 Python 写的,照着思路做就行,没必要照搬框架。
而从 Claude Code 那边学来的东西,会继续保留:提示词作为控制面、缓存的防御和监控、所有自动流程的熔断、辅助路径按四个旋钮做能力降维。
如果真的从零重做一次,我大概会以 dsh 的日志模型为骨架,用 Claude Code 的工程经验做血肉。骨架决定了系统不容易出错,血肉决定了它在真实的用户面前好不好用。
这个系列到这里就告一段落了。
总结
- Claude Code 是产品,单体循环加提示词控制面,擅长在运行时把错误兜住;dsh 是平台,微内核加事件溯源,擅长在结构上让错误不可表示;
- Claude Code 的缓存稳定是管理出来的,dsh 的缓存稳定是从只追加的日志里长出来的;
- dsh 把安全拆成审批和沙箱两个正交旋钮,安全默认值来自"什么都不加载时会发生什么";
- 产品倾向于运行时兜底,平台倾向于结构约束,没有谁对谁错,取决于你要解决的问题;
- 单元测试全绿不代表装配后的系统可用,插件化的底座更要用真实的组合去验收。