现在位置: 首页 > DeepSeek Harness > 正文

DeeSeek Harness 事故复盘与工程文化

dsh 仓库把每一次「不该出现却出现了」的 bug 都写成事故复盘(postmortem),并留下测试、文档与规则层面的防护。

理解这套复盘文化与测试纪律,你就能在 runoob 项目里活用它:不只是会用 dsh,而是会像维护者一样思考。

一句话:一个 bug 的价值,不在于那行修复,而在于为什么流程放过了它,以及新增了什么防护让同类问题下次明确报错。


复盘文化:四问

事故复盘记录的是:一个 bug 出现在了不该出现的地方(真实用户、已合并的 PR、已发布的版本)。

值得关注的是为什么我们的流程放过了它,而不仅仅是那一行修复。

它是一份回顾性的失败记录,回答四个问题:

四问要回答什么
什么坏了用一个简短段落让忙碌的读者在三十秒内吸收要点
机制是什么用直白的话说清根因,不归咎个人
为什么每道安全网都没拦住找出测试、工具、约定的缺口,而非一次性笔误
新增了什么防护测试、AGENTS.md 规则、ADR,让同类 bug 下次明确报错

不是所有 bug 都值得写复盘。

只有当 bug 同时满足三个条件时才写:隐蔽(机制不显而易见,细心的工程师也得费力重新推导)、系统性(逃逸原因是测试、工具、约定的缺口)、重新发现的代价高(消耗了真实调试时间,下次还会如此)。

事故复盘流程与测试金字塔图


四个真实案例

仓库目前有四篇事故复盘,每一篇都对应一类容易复发的工程失误。

下面逐个介绍,重点是「为什么逃逸」与「新增了什么防护」。

复盘 0001

ACP 服务器在连接时崩溃:export default 丢掉了插件的 inject。

什么坏了:编辑器(Zed)一连上,第一个 session/new 就报 cannot get property "agents" without inject

机制:插件多写了一个 export default apply,Loader 的 unwrapExports 取到了裸函数,把命名空间上的 inject 整个丢掉了。

为什么安全网没拦住:178 个绿色单元测试 + 100% 行覆盖率都在,但所有测试都通过手动 ctx.plugin(...) 挂载,绕过了真实 Loader 的加载路径。

新增防护:删除 default export;增加无需 key 的真实 Loader 冒烟测试;规则「测试真实入口路径,行覆盖率不等于行为覆盖率」。

复盘 0002

文件系统快照工具被一个字面量 !!js 对象永久禁用。

什么坏了:七个文件系统场景调用了注册表里不存在的工具,返回 UNKNOWN_TOOL

机制:作者用 disabled: !!js ... 想条件启用文件系统插件,但 Cordis 只在插件 config 内部对 JS 表达式求值;直接读 disabled 配置项时,它看到的是一个 truthy 对象。

为什么安全网没拦住:快照刷新把「确定性回放」当成了「行为正确」——它证明了回归被稳定复现,却没证明文件系统工具真的注册了。

新增防护:改用显式文件系统 overlay;静态配置守卫拒绝 Loader 配置项元数据里的表达式节点;快照框架拒绝结构化 UNKNOWN_TOOL 结果。

复盘 0003

Web agent 验收了替代服务器,而非承载其会话的 GUI。

什么坏了:agent 修改了 GUI 源码,却不知道当前会话对应哪个 URL、由哪个进程承载。

机制:它把裸 Vite 返回的 HTTP 200 当作成功(其实白屏),随后又去验收另一个端口上的替代 dsh web 服务器,从未探测 3081 端口。

为什么安全网没拦住:Web 组合没有向模型提供当前 GUI、规范 URL 或运行模式的身份信息;第一个回归测试还用「进程超时」冒充「快速失败」,产生误报。

新增防护:启动器发布规范环回 URL 与实际生产/开发模式(环境变量 + 提示词区段);独立 Vite 服务模式在配置阶段拒绝启动;分层真实路径测试覆盖 CLI、提示词、运行时事实与浏览器 HMR。

复盘 0004

Landlock 部分强制执行通知导致子进程失败被误归类。

什么坏了:在较旧的 Landlock ABI 内核上,ripgrep 无匹配时以退出码 1 正常结束,却被呈现为 SANDBOX_UNAVAILABLE 沙箱故障。

机制:launcher 打印无害的 landlock-run: partial enforcement (older Landlock ABI) 通知;harness 用不区分大小写的 landlock-run: 子串把通知与任意非零退出组合,误判为 runner 失败。

为什么安全网没拦住:沙箱结果类型只能表达一组子字符串,无法表达「Landlock 失败必须退出码 125 + 一行致命诊断」;测试矩阵从不构造「通知后跟非零子进程退出」的组合。

新增防护:RunnerFailureRule 携带允许退出码、逐行致命签名与精确排除的信息性行;文件系统搜索改用 ctx.subprocess 跑打包的 ripgrep,不再经过沙箱化 bash。

四个案例放在一起看,能总结出一条共同经验:测试必须走真实入口路径

手动挂载、mock 一切、把快照刷新当验收——都会让「单元全绿、产品却坏了」成为可能。


测试四层

仓库的测试策略是分层的,每层补上一层抓不到的盲区。

层级命令抓什么
单元测试pnpm run testvitest 跑包内测试,优先边界、错误路径、事件顺序、并发竞态
覆盖率门禁pnpm run test:coverage按文件 100% 覆盖;未覆盖行往往是该删除的死代码
真实 API e2epnpm run test:e2e带密钥调用真实提供方 API;缺密钥自动跳过,keyless CI 保持绿色
快照pnpm run test:snapshot / test:web无密钥预期输出覆盖对外行为;浏览器快照用 Chromium 回放比较

仓库是 DeepSeek 自己的,所以有一条特别原则:推理在这里很便宜,不要吝惜真实 API 测试

无密钥测试只能证明底层通路;只有带密钥运行才能证明 agent 能对接真实模型正常工作。

价值最高的是冒烟测试:启动真实示例、发送一条提示词、检查外部世界。

它们能捕获「单元测试全绿、产品却坏了」这一类 mock 无法发现的问题。

行覆盖率是必要条件,永远不是充分条件。

它证明行被执行过,不证明功能按交付预期工作。

0001 案例里 100% 覆盖率仍然放过了两个集成 bug,就是最好的注脚。

另一个原则是验证外部世界,而非自我报告

e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身输出做关键词探测会让作弊的 agent 通过。

断言未修改的文件应逐字节一致。


文档纪律:一个事实一个家

仓库的文档不是写完就完,而是有机制防止「文档与源码漂移」。

verify-type-equiv 是一道门禁:它用 TypeScript 解析器从源码提取类型声明的符号及其附带的 JSDoc,断言文档代码块同时匹配两者。

当你改动一个已记录的类型声明或其 JSDoc 时,门禁会失败,直到你更新粘贴内容。

这保证文档里的类型定义与源码永远一致,不会出现「文档抄的是旧版」。

另一条纪律是一个事实一个家:每个事实只在一个文件里维护,其余文件引用它。

例如工具 schema 的「真源」在 adding-a-tool.md,其它页面引用而不复制。

中文与英文文档通过双语配对维护,更新时先跑 pnpm run gen-doc-graphs 更新英文,再更新中文并验证配对。

对你自己的项目同样适用:把「唯一事实」放在一个地方,别让同一个配置散落在三份文档里。


动手示例:写一段三十秒执行摘要

复盘每一篇都以一段执行摘要开头,让忙碌的读者三十秒内吸收要点。

试着复盘一次你在 runoob 项目里遇到的 Agent 故障,按下面模板写:

# 复盘 0005:runoob 演示环境的工具黑名单没有生效

## 摘要
demo 环境里模型仍能调用被禁用的 fs_write,直接写入了工作区。
根因是黑名单插件用 `tools/pre-execute` 返回了 `next()`,
而真正负责拦截的守卫注册在插件卸载之后——加载顺序错了。
逃逸原因是加载顺序没有测试覆盖,`cordis.yml` 只测了「能起来」。
新增防护:给黑名单插件补一条「顺序敏感」的装配测试,
并在 AGENTS.md 记录「pre-execute 与 guard 的注册顺序」规则。

三十秒摘要的公式:坏什么 → 用直白的话说根因 → 为什么逃逸 → 可长期沿用的教训。


系列结语

到这里,DeepSeek Harness 入门教程的全部 28 篇就结束了。

回头看看这条路:从第 1 篇认识 dsh 的四大护栏,到学会安装、用 Web UI、用 Python SDK;从写第一个插件,到理解插件生命周期、服务与作用域、事件系统;从用三角色模式设计能力、接入任意 LLM,到打包发布;最后这五篇,补上了安全与工程文化这最后一块拼图。

你真正带走的,不只是 API 的用法,而是一套思考方式:

模型负责聪明,Harness 负责可靠。 约束不是限制,而是让 Agent 可预测、可审计、可回放的基石。

一切皆插件。 把策略放在扩展点上,而不是写进循环里,系统才能演进而不失控。

真实入口路径胜过一切 mock。 覆盖率、快照、冒烟测试各司其职,共同防止「全绿却坏了」。

故障不可怕,可怕的是不知道为什么。 复盘文化把一次事故变成一套防护。