DeepSeek Harness 源码解读(十):六条设计纪律如何约束可替换运行时

DeepSeek Harness 源码解读(十):六条设计纪律如何约束可替换运行时

DeepSeek Harness 的价值不只在插件数量,而在一组持续约束所有插件的规则:注册必须可撤销,模型可见事实必须可重建,能力必须按角色拆分,控制流必须显式,错误配置必须尽早失败。本文以当前仓库的 effect、Session、事件、能力接口和 runtime invariant 为证据,收束整个源码解读系列。

项目地址:https://github.com/deepseek-ai/deepseek-harness

源码基线:仓库版本 0.1.0-rc.5。重点入口是根目录 AGENTS.mddocs/architecture.mdpackages/corepackages/runtime-diagnostics/invariants 及各能力 Service Definition。

一、本章要回答的问题

插件化系统很容易停留在形式上:代码拆成很多包,每个包都有注册函数,但状态仍由中心模块拥有,卸载后监听器仍然存活,模型看到的上下文无法恢复,配置错误直到执行中途才暴露。这样的系统只是"文件很多",并没有获得可替换性。

DeepSeek Harness 的关键不是允许插件做任意事情,而是规定插件必须怎样贡献行为、怎样留下持久事实、怎样调用下一层、怎样表达失败。本文把这些要求归纳为六条设计纪律,并说明它们分别解决什么工程风险。

二、核心结论:自由来自可执行的约束

六条纪律最终指向同一个结果:插件可以替换,但不能留下无法回收的状态;请求可以被改写,但不能脱离日志;能力可以增加,但不能绕开服务语义;类型可以扩展,但不能让未知分支悄悄落入错误路径。

这些规则并非只写在文档里。部分由 TypeScript 类型系统约束,部分由 Cordis effect 管理,部分由配置 schema 和运行时断言执行。代码评审仍然重要,但它不是唯一防线。

三、纪律一:每个注册都必须有生命周期所有者

根目录开发约定把它写成一句话:Registrations are effects。服务、工具、事件 listener、Provider 和后台资源都必须属于某个 Cordis Fiber,并在拥有者卸载时撤销。

默认 Agent Loop 注册工厂时不是直接修改全局变量,而是把注册放进 effect:

ts 复制代码
ctx.effect(() => ctx.agents.setFactory(this), 'agentLoop.setFactory()')

AgentRegistry.setFactory() 自身也返回 disposer。工具注册、模型 Adapter 注册和许多作用域层同样返回撤销函数。这样形成两层保证:注册 API 明确告诉调用方"这项贡献需要回收",Cordis 再把回收动作绑定到插件生命周期。

这条纪律解决三个问题:热重载不会累积重复 listener;Agent 级注册不会泄漏到其他 Agent;Provider 被替换后不会留下仍可被路由到的旧实例。

需要注意,effect 不是普通的"退出时清理"。它还表达所有权顺序。父 Fiber 卸载会触发子资源释放,异步 disposer 可以被等待,注册失败也能回滚已经建立的局部状态。一个没有明确拥有者的连接、定时器或注册表项,在插件系统中就是潜在泄漏。

四、纪律二:模型可见事实必须从日志重建

docs/architecture.md 给出的规则是 Model-visible means logged。任何进入模型请求的消息、工具结果或请求配置,都必须能从 Session 日志重新推导;新增模型可见输入时,需要扩展 SessionEventMap 并实现对应投影。

这条纪律把"当前内存里看起来正确"提升为"进程重启后仍能解释"。packages/core/session 使用追加事件保存事实,再由 deriveMessages()、请求头折叠和 surface 投影得到模型历史。工具执行、压缩、上下文注入和分叉会话不能私自维护另一份消息真相。

仓库还为这条规则提供了可执行检查。packages/core/agent-loop/src/invariant.tsllm/stream 前识别由默认循环构造的请求,确认请求冻结、携带 live Session 标识,并把请求消息与日志在派发时的派生结果比较;不一致时抛出带包名的 invariant 错误。

因此,插件若想给模型增加一句长期有效的上下文,正确做法不是在最终 messages 数组上临时 push。它应该通过 agent.inject() 进入被接纳的输入流程,或定义新的持久 Session 事件及投影。否则第一次调用也许成功,恢复、分叉和重放都会失去同一事实。

五、纪律三:新增行为优先挂插件,不侵入默认循环

packages/core/agent-loop 负责默认 Turn/Step 驱动,但它不是所有功能的收纳箱。审批、超时、重复调用提醒、上下文、压缩、子 Agent 和工具策略都通过服务或事件接入,而不是不断增加循环分支。

这条纪律要求先判断行为属于哪个领域:

  • 需要跨重启保留的事实进入 Session 事件;
  • 当前运行中的请求、工具或回合策略进入 agent/*tools/* 等 live 事件;
  • 可被多个 Consumer 使用的操作进入服务;
  • 部署差异进入 Profile、Bundle 或配置,而不是代码常量。

这样做的直接收益是默认循环只承担驱动语义。删除一个 guard 不会改动循环;替换文件 Provider 不会改动文件工具;增加 Web 界面也不需要让 Agent Loop 认识 HTTP。

这条纪律也有边界。如果需求确实改变 Turn、Step、取消或事件顺序,继续在外围叠 listener 反而会让语义更隐蔽。此时应该明确替换 AgentFactory,并更新架构中的生命周期图,而不是伪装成普通插件策略。

六、纪律四:可替换能力必须包含三个角色

DeepSeek Harness 把完整能力拆为 Service Definition、Service Provider 和 Consumer。三者不是目录命名习惯,而是替换成立的条件。

以 Shell 为例:

  • packages/shell/shell 定义 ctx.shell 的请求、解析后规格、结果和错误语义;
  • packages/shell/bash-local 或其他实现负责真正启动命令;
  • packages/shell/tool-bash 把能力暴露给模型,并负责工具参数与结果呈现。

Consumer 只依赖 Definition,Provider 只承诺 Definition,Profile 决定装载哪一个 Provider。这样,本地执行切换到远端执行时,模型工具不需要出现一套 E2B 分叉;审批和工具事件也可以继续工作。

三个角色缺一不可。只有接口没有真实 Provider,无法证明语义足够;只有 Provider 没有稳定 Definition,Consumer 会绑定实现;只有工具没有服务层,其他插件就只能绕过工具协议或复制执行逻辑。

这项拆分会增加包数量,但换来依赖方向稳定。包多不是目标,角色能独立演进才是目标。若一个能力只有单一实现、单一消费者且看不到替换需求,强行拆成三包同样属于过度设计。

七、纪律五:控制流和类型分支必须显式

Cordis waterfall listener 收到 next()。调用它表示把控制权交给下一层,直接返回表示短路。两种行为都合法,因此代码必须明确选择,不能把遗漏 next() 当作默认继续。

工具流水线中的 tools/pre-executetools/executetools/post-execute 都依赖这一语义。一个只想观察的 listener 必须调用 next();审批或策略插件若决定阻止,才返回拒绝结果。显式控制权让每个 listener 的责任可审查,也让包装超时、替换信号和清洗结果成为同一种组合方式。

类型分支采用相同原则。闭合判别联合的 switchassertNever() 结束:

ts 复制代码
export function assertNever(value: never, context?: string): never {
  const rendered = (JSON.stringify(value) as string | undefined) ?? String(value)
  throw new Error(`unreachable variant${context ? ` in ${context}` : ''}: ${rendered}`)
}

新增联合成员而忘记处理时,TypeScript 会在调用点报错;若不可信值逃过静态类型,运行时仍会给出明确诊断。相反,允许插件声明新成员的开放联合必须保留有说明的默认分支,不能假装世界是闭合的。

同样的显式性还体现在包边界默认值。ctx.shell 先用 resolve(request) 产生完整 ShellExecSpec,再交给 run()start();默认值不隐藏在执行中途的 ?? 表达式里。调用者和测试都能看到"请求如何变成最终执行规格"。

八、纪律六:错误配置和被破坏的关系必须尽早失败

插件系统最危险的失败不是报错,而是配置不完整时悄悄跳过。DeepSeek Harness 要求自包含错误在加载时失败,依赖关系则在最早能够确定时失败。

具体机制包括:

  • Cordis 服务重复注册会失败,避免两个 Provider 同时声称拥有 ctx.shell
  • 配置通过 schema 和额外语义校验,非法并发数、超时或未知字段不会留到第一次请求;
  • 跨包标识使用 Branded<B>,例如 SessionId 与普通字符串在类型层分开;
  • Session header 的版本、绝对路径和 lineage 字段在读取时验证;
  • ctx.invariants 允许每个包注册自己拥有的运行关系检查,失败时给出 INVARIANT 错误和包名。

packages/runtime-diagnostics/invariants 不替每个包猜测正确性。每个 ./invariant companion 只检查该包真正拥有、并能从权威事件或可变数据观察到的关系。默认循环检查请求是否可由日志重建,Session 检查 Turn、Step 和工具事件顺序,其他包没有可信关系可检查时应明确为空,而不是制造一个永远成功的样例断言。

"尽早失败"会让启动和开发阶段更严格,却能避免系统带着半套能力进入真实会话。对于会执行命令、写文件和持久化历史的 Agent,清晰拒绝比带着错误假设继续运行更安全。

九、六条纪律怎样互相支撑

单独看,每条规则都像局部编码规范;放在一起,它们形成一条完整链路:

  1. Profile 装载插件,配置错误立即失败;
  2. 插件通过 effect 注册服务、Provider、工具或 listener;
  3. Consumer 只依赖稳定 Definition,不绑定具体实现;
  4. 请求经过显式 waterfall,观察者和拦截者不会混淆;
  5. 模型可见结果追加为 Session 事件,再从日志投影;
  6. runtime invariant 检查关键关系没有在组合过程中被破坏;
  7. 插件卸载时,所有注册按所有权顺序撤销。

这条链解释了为什么"插件自由建立在硬约束上"。如果没有生命周期,替换会泄漏;没有日志纪律,恢复会分叉;没有角色拆分,Provider 无法替换;没有显式控制流,listener 会意外吞掉请求;没有早失败,错误组合会进入生产运行。

十、开发者预览意味着什么

仓库当前版本是 0.1.0-rc.5,README 明确标注开发者预览并提示会出现破坏兼容性的变更。根目录开发政策进一步选择"地基优先":在首个正式标签前,宁可统一重命名、重组包和拒绝旧磁盘格式,也不为尚未承诺的外部兼容性增加 shim。

这项选择与六条纪律并不矛盾。内部协议要求严格,外部兼容承诺仍然可以处于预发布状态。前者保证当前组合自洽,后者允许团队在正式稳定前修正抽象。

使用者应据此做三件事:锁定版本;把 Profile、Session 数据和插件 API 的升级纳入发布流程;不要把 RC 阶段的包路径或持久格式当作长期 ABI。评估项目时可以关注设计和能力,生产升级则必须按版本验证。

十一、把这些纪律带到自己的 Agent 项目

即使不采用 DeepSeek Harness,也可以用下面的问题审视自己的运行时:

  1. 一个工具、listener 或连接由谁拥有,模块卸载时谁负责回收?
  2. 模型下一次看到的每条内容,能否从持久事实重新生成?
  3. 新策略是否必须修改中心循环,还是存在稳定事件或服务入口?
  4. 可替换能力是否真的分开了接口、实现和消费方?
  5. 中间件继续、短路和替换结果是否在代码中显式可见?
  6. 错误配置、未知联合成员和跨组件关系破坏会在什么时候失败?

如果其中三四个问题没有明确答案,系统可能仍能演示,但随着 Provider、工具和入口增加,行为会越来越难恢复、替换和诊断。

十二、十篇源码解读的最终地图

本系列可以压缩为一条从组合到约束的阅读路径:

  1. 第一篇确定 Harness 不是单次模型调用器,而是 Agent 运行时;
  2. 第二篇解释 Cordis Context、Service、事件和 effect;
  3. 第三篇建立核心服务与事件域的整体结构;
  4. 第四篇跟踪一次 Turn 和 Step;
  5. 第五篇展开工具从参数到结果的执行链;
  6. 第六篇解释 Definition、Provider、Consumer 如何实现替换;
  7. 第七篇说明 Session 日志怎样成为模型历史的来源;
  8. 第八篇拆解文件、命令、审批、沙箱和 Guard 的安全分层;
  9. 第九篇把架构选择落到适用场景与扩展入口;
  10. 本篇用六条纪律解释这些机制为何能够长期共存。

第一次读仓库时,不必记住所有包名。先记住三条定位原则即可:运行组合看 Profile,临时行为看服务与事件,持久事实看 Session。遇到可替换能力,再检查 Definition、Provider、Consumer 是否完整。

十三、结语

DeepSeek Harness 展示的不是一种最短的 Agent 写法,而是一种面向长期演进的组织方式:把实现放进插件,把能力放在稳定服务后面,把过程交给明确事件,把模型历史交给追加日志,再用类型、配置校验和 runtime invariant 守住这些关系。

它因此比简单脚本更重,也比固定内核更难在几分钟内读懂。但当一个 Agent 产品需要多入口、长会话、可替换执行环境和持续扩展时,这些约束会把复杂度留在可定位、可卸载、可重放的位置。约束不是插件化的对立面,而是插件真正可以自由组合的前提。

相关推荐
阿里云大数据AI技术1 小时前
PAI支持一键部署Qwen3.8-Flash-Next、GLM-5.3等最新开源模型
人工智能·开源·llm
ovO1 小时前
DeepSeek Harness 源码解读(八):文件、命令、审批与沙箱如何协作
开源·agent
阿里云云原生1 小时前
经验自进化:自动挖掘经验资产,消融实验验证真实收益丨AgentLoop 数据飞轮实践(五)
agent
ovO2 小时前
DeepSeek Harness 源码解读(六):Provider、Consumer 与能力接缝
开源·agent
李燚2 小时前
把规则搬回家:三个 BC 的贫血→充血重构实录(第103篇)
golang·agent·ddd·领域驱动设计·eino·deepflux·eino adk
2601_962304913 小时前
2026年健康科普视频怎么制作:一条开源工具链从全手动到半自动的工程复盘
开源·音视频
ClouGence3 小时前
CloudDM 支持达梦、KingbaseES、GoldenDB,国产数据库也能统一管起来
数据库·sql·开源
zzzll11113 小时前
Langfuse:开源 LLM 可观测性与评估平台实战指南
开源
举个栗子。3 小时前
DBX:20MB 驾驭 90+ 种数据库的极简开源数据库管理器
数据库·开源