两种Harness设计哲学——Claude Code的单体循环 vs DeepSeek Harness的微内核,以及我们的底座会怎么选

从去年研究 Claude Code,到这两个月追着 DeepSeek Harness(dsh)读文档,两套 harness 我都拆得差不多了。

它们解决的是同一组问题:循环怎么组织、上下文怎么管、工具怎么执行、出错了怎么恢复、怎么保证安全。但给出的答案经常截然相反。

作为 dsh 系列的最后一篇,把两者放在一起对比一下,也说说如果重做我们的底座,我会怎么选。

一、一句话概括

Claude Code 是一个产品:核心是一个单体的 Agent Loop,行为主要靠提示词控制,默认简单,需要时再叠加复杂度。它的很多设计,是在海量真实用户的数据上一点点打磨出来的防御性工程。

dsh 是一个平台:没有特权内核,连 loop 都是插件;会话是一条只追加的事件日志,模型看到的一切都从日志派生。它的很多设计,是想办法让错误的状态在结构上就无法出现。

简单的说,Claude Code 擅长"在运行时把错误兜住",dsh 擅长"在结构上让错误不可表示"。

二、逐项对比

维度Claude CodeDeepSeek 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 的文档里有一组"五个习惯",我觉得是它整套设计的底色:

  1. 让正确性显式化:写进结构,而不是事后用时序补偿;
  2. 保持单一的真相来源:一个事实只有一个家;
  3. 通过注册的 seam 扩展:新能力注册进来,而不是往已有的载体里加分支;
  4. 让改动面等于评审面:改动只包含它声明的行为所需的那些行;
  5. 要求证据:断言要可证伪、可重算,性能相关的改动要附带测量。

它的几篇事后复盘也指向同一个教训:单元测试全绿,不代表装配起来的系统是可用的。要用真实的加载器、真实的组合、真实的外部状态去验收。这条对我们这种插件化的底座同样适用。

四、我们的底座会怎么选

回头看我们的底座,它其实已经是两者的混合:Agent Loop 是照着 Claude Code 做成显式状态机的,工具、记忆、技能、观测则是以插件的形式挂在生命周期事件上,更接近 dsh 的方向。

这个系列读下来,我打算从 dsh 搬过来的有这么几样:

  1. 日志就是真相:模型历史从日志投影,改写历史走替换,模型看到的和人看到的分成两种投影,语音打断的问题正好用得上;
  2. 事件按域拆开:已经发生的事实、可以拦截的控制点、能力上的策略,三者不再混在一起;
  3. 一次调用一次尝试:只让一层负责重试,每一次尝试都进 trace;
  4. 取消区分有没有副作用:被取消的工具调用,明确标出"从未派发"还是"可能已经执行";
  5. 审批 fail-closed by composition:缺席即拒绝,授权只对这一次有效。

暂时不搬的:

  1. 把 loop 也做成可替换的插件。我们目前只有一种 loop,为"可替换"付出的抽象成本现阶段收益不大;
  2. 整套 Cordis 的插件框架。我们的底座是 Python 写的,照着思路做就行,没必要照搬框架。

而从 Claude Code 那边学来的东西,会继续保留:提示词作为控制面、缓存的防御和监控、所有自动流程的熔断、辅助路径按四个旋钮做能力降维。

如果真的从零重做一次,我大概会以 dsh 的日志模型为骨架,用 Claude Code 的工程经验做血肉。骨架决定了系统不容易出错,血肉决定了它在真实的用户面前好不好用。

这个系列到这里就告一段落了。

总结

  1. Claude Code 是产品,单体循环加提示词控制面,擅长在运行时把错误兜住;dsh 是平台,微内核加事件溯源,擅长在结构上让错误不可表示;
  2. Claude Code 的缓存稳定是管理出来的,dsh 的缓存稳定是从只追加的日志里长出来的;
  3. dsh 把安全拆成审批和沙箱两个正交旋钮,安全默认值来自"什么都不加载时会发生什么";
  4. 产品倾向于运行时兜底,平台倾向于结构约束,没有谁对谁错,取决于你要解决的问题;
  5. 单元测试全绿不代表装配后的系统可用,插件化的底座更要用真实的组合去验收。

延申

Coding Agent的心脏——Agent Loop:为什么要做成显式状态机,错误为什么要从轻到重地恢复

Coding Agent上下文装不下了怎么办——从pi-mono源码看上下文压缩

没有特权内核的Agent——DeepSeek Harness的Everything is a Plugin与微内核思想

相关资料