dsh-harness-audit
English | 中文
审计 agent harness 的健壮性——并且拒绝任何拿不出代码证据的发现。
它解决什么问题
Agent 跑不稳,大多数时候不是模型不够聪明,而是循环本身有洞。这些洞有个共同特征:平时不报错,出事时无法复现。
- 工具执行完了,结果没写回对话。下一次请求带着一个没有答案的调用,被模型服务拒收(通常 400)。这是 agent harness 里出现频率最高的真实故障。
- 五个并行操作成功了四个,但整批被判定失败,成功的结果被丢弃。
- 外部动作已经生效之后才超时,重试于是又做一遍——消息发两次,款扣两次。
- 用户点了停止,子进程还在跑,请求还在发。
- 请求开头的内容每次都在变,服务端前缀缓存永远命中不了。不报错,只是每次请求都更贵,没人会发现。
- 线上出了问题,事后没法重建这次运行到底发生了什么。
这些问题共十五类,覆盖六个方面:
<!-- checks:start -->
State stays self-consistent
| 检查项 | 查什么 | |
|---|---|---|
C1 ★ | Tool-call pairing completeness | 查工具执行的每一种结束方式(正常返回、报错、超时、取消、提前返回、权限拒绝)是否都写回了结果,以及并行批次里一个失败会不会丢掉其余成功的结果。 |
C2 | History is append-only | 查有没有代码在消息写入之后又去修改它,而不是只追加新消息。 |
C3 | Crash and checkpoint semantics | 查分成两段的写入过程中如果进程挂掉,重启时读到的是什么,以及这种半截状态能不能被识别出来。 |
Untrusted input is treated as untrusted
| 检查项 | 查什么 | |
|---|---|---|
C4 ★ | Model output parsing | 查模型输出是怎么解析的:参数不合法、输出被截断、调用编号重复这些情况是被处理了,还是被当成可信输入。 |
C5 | Path and sandbox boundaries | 查模型给出的路径有没有解析后再与工作区根目录比对,而且必须在符号链接解析之后比对。 |
C6 | Secrets and ambient environment | 查模型生成的命令继承了什么环境变量,里面有没有密钥。 |
Failure is a first-class outcome
| 检查项 | 查什么 | |
|---|---|---|
C7 | Error taxonomy and retryability | 查失败是否带有一组封闭的错误码,并被明确分类为可重试/不可重试/致命,还是只给出一段没有分类的文字。 |
C8 | Partial success | 查多个独立结果会不会被合并成一个状态,导致一个失败掩盖或丢弃旁边那些成功。 |
C9 ★ | Idempotency and side-effect safety | 查重试包裹的范围里有什么:如果被重试的区间包含写入、执行命令或对外发消息,重试就会重复执行它。 |
Boundaries can be closed
| 检查项 | 查什么 | |
|---|---|---|
C10 ★ | Cancellation propagation | 查取消信号有没有真正传到对外请求和派生的子进程,还是只停在循环这一层。 |
C11 | Timeout layering | 查一次工具调用上有几层超时以及它们的大小关系 —— 工具预算必须早于底层资源超时。 |
C12 ★ | Loop and budget limits | 查轮数、工具调用次数、运行时长、token、委派深度上有没有硬性上限。 |
Finite resources are accounted for
| 检查项 | 查什么 | |
|---|---|---|
C13 ★ | Context management and truncation boundaries | 查会话变长时有没有上下文管理,以及截断切在哪里 —— 切开配对的调用与结果会破坏历史。 |
C14 ★ | Prompt prefix determinism | 查最新消息之前的所有内容(系统提示、工具定义、历史消息)在两次运行之间是否逐字节一致,这是缓存命中的前提。 |
The run is observable
| 检查项 | 查什么 | |
|---|---|---|
C15 | Trace completeness and replay | 查这次运行有没有留下覆盖轮次、工具调用与结果、重试、截断、审批的事件记录,且完整到足以重建。 |
★ 是七个关键项 —— /harness-audit p1 跑的就是这些。
<!-- checks:end -->
它怎么工作

三个阶段,前两个用模型,最后一个不用:
侦察 —— 一个子智能体先定位地标:agent 循环在哪、请求在哪组装、工具在哪派发、历史在哪追加。找不到某类地标是正常结果,不编造。
扇出 —— 每条检查交给一个独立的一次性子智能体,只给它这一条的判据和它依赖的地标。所需地标缺失的检查根本不会派发,直接记为"未覆盖"——让模型在没有目标的情况下自由发挥,是误报的主要来源。
汇总 —— 纯代码,不过模型。去重、按保守原则合并判定、排序、渲染。让模型写总结,正是 suspected 悄悄变成 confirmed、以及没有证据支撑的论断混进报告的途径。

每条检查一个条目、各自计费。这种隔离正是报告里那份"每维度 token 消耗"能够是实测值而不是估算的原因。
为什么可以信

判据本身可以要求模型给出文件行号,但只有工具能拒绝。每条上报必须通过六道校验:
1. 检查项属于本次运行 2. 路径是第一方代码——不是 .venv、site-packages、node_modules 3. 文件在工作区内真实存在(这同时挡住路径穿越) 4. 行号在文件范围内 5. 引用的代码确实出现在所声明行号的 ±3 行内 6. 判定为"可疑"时必须说明人工需要核实什么
第 5 条是关键。它按归一化文本比对(去缩进、压缩空白),不做逐字节比较,所以缩进差异不会造成假拒;但一段"看起来像代码却不在文件里"的内容会被退回,并告诉子智能体重查。实测中它反复生效:被拒后子智能体会重新打开文件、修正引用再提交。
范围约束同理。第一次跑一个 Python 项目时,23 个地标全部落在 .venv/site-packages/ 里——审计的是依赖的框架而不是项目本身。仅靠提示词说明不够,加上工具层拒收之后,同一个项目的运行从 698 秒降到 147 秒,token 从 19 万降到 4.6 万。
实测

拿一份预先埋好缺陷、答案已知的代码验证:七个真实的工具调用配对缺陷、一个受环境变量门控的可疑情况、外加一个刻意写正确的对照模块。
| 缺陷检出 | 8 / 8 |
| 误报 | 0 —— 对照模块一次都没被点名 |
| 证据拒收 | 10 次提交拒 2 次,均在重新提交后修正 |
| 成本 | 单维度约 205 秒,输入 6.1 万 / 输出 2.4 万 token |
唯一与答案卷不一致的一条,是审计器比答案卷更准:它读到那个环境变量的默认值,判断该路径默认可达,因此报为"确认"而非"可疑"——按判据它是对的。
累计已在 11 / 15 个维度上产出 70 条发现、21 次拒收,消耗约 167 万输入 token。
安装
先装 DeepSeek Harness
这是一个插件,需要有 dsh 才能挂载。装好 Node.js 之后:
npx @deepseek-ai/dsh webWeb UI 默认在 http://127.0.0.1:3080。或者从源码检出运行:
git clone https://github.com/deepseek-ai/deepseek-harness.git
cd deepseek-harness
pnpm install
pnpm run build
pnpm dsh web两点需要提前知道:
- DeepSeek Harness 处于 developer preview,明确会有破坏性变更。 本插件因此把 peer 依赖锁在
^0.1.0-rc.5。 dsh plugin是转发给 pnpm 的,所以 pnpm 必须在 PATH 上。
再装这个插件
dsh plugin --profile web add dsh-harness-auditnpm i dsh-harness-audit 只会下载包,不会激活插件——dsh plugin add 才会把它装进 profile,并让 package.json 里的 dsh.bundle 声明把配置层挂上。验证:
dsh --profile web --dump-config应该能看到一段 # == dsh-harness-audit。卸载用包名:
dsh plugin --profile web remove dsh-harness-audit用法
/harness-audit # 弹出维度选择器,每项都有人话说明
/harness-audit C1 # 单个维度
/harness-audit C1,C9 # 多个
/harness-audit p1 # 七个关键项
/harness-audit all # 全部十五项
命令立即返回,审计作为后台任务运行,不阻塞会话。跑完时 agent 会被唤醒并汇报。中途想看进度,让它调 job_output <id>。

无法解析的输入会被拒绝,不会被静默改判——早期版本只认 --checks,结果 --check c2 落到了配置默认值上,审计了另一个维度却一声不吭。
报告
每次运行产出两份文件,按本地时间和覆盖维度命名:
.harness-audit/report-2026-08-17_095736-C1.md
.harness-audit/report-2026-08-17_095736-C1.json结构固定,两次运行可以直接对比。其中两节不可省略:
- 未覆盖 —— 因地标缺失而没跑的检查会明确列出。没有这一节,读者会把"没查"当成"查过没问题"。
- 成本 —— 每条检查的 token 消耗,以及证据拒收次数。拒收率高说明子智能体在编造,该改的是提示词,不是放宽校验。
配置
| 字段 | 默认 | 含义 |
|---|---|---|
checks | [] | 要跑的检查项 id。空表示"询问",绝不表示"全审"。 |
priorityFloor | 2 | 只跑优先级不高于此值的检查(1 = 仅关键项)。显式点名的检查不受此限制。 |
concurrency | 3 | 同时进行的检查数。1 是受支持的最省也最好调试的路径。 |
subagentProvider | spawn | 子智能体提供方名称。 |
maxTokensPerCheck | 120000 | 仅供参考,见下方限制。 |
outputDir | .harness-audit | 报告目录,相对工作区。 |
announceOnStart | true | 启动时在对话里发一条通知,让全新会话有轮次可以承载任务 UI。 |
background | true | 作为后台任务运行。false 则阻塞命令直到跑完。 |
useLsp | true | 尝试 LSP 导航,不可用时降级为文本检索。 |
crossCheckAnalysis | false | 预留,尚未产出。 |
language | auto | 输出语言。auto 依次尝试 harness 语言设置、宿主语言、英文。 |
excludePaths | 依赖目录 | 被判为超出范围的目录名。设为 [] 可以刻意审计依赖里的框架。 |
已知限制
maxTokensPerCheck只是建议值。 子智能体 seam 没有整轮 token 上限(AgentOptions.maxTokens限制的是单次响应),所以这个预算只是告知子智能体并记入元信息,报告里给出的是实测消耗。它不被强制。crossCheckAnalysis尚未实现。 真做的时候,它的输出必须单独成节并标明是未经证据校验的模型推断。- LSP 可用性是按扩展名的。 seam 没有能力查询接口,只能靠
query()抛出的LspError观测,所以探测发生在侦察之后、用检测到的主语言进行,结果只对该语言有效。 - 成本归账需要本地子智能体提供方。 消耗是从子会话事件折叠出来的;远程提供方不发布本地子会话,其用量会读作 0。
- 全新会话里,首个轮次出现之前看不到任何东西。 命令不产生轮次,而对话区的 UI 槽位是严格会话绑定的,任务指示器没有地方挂。
announceOnStart就是为此存在的。
它不做什么
- 不发明判据。 十五条检查的标准写死在
src/criteria.ts里,子智能体拿到的是原文;插件自己的名称、分组、菜单文案都是导航用的,从不代替判据。 - 不替你下结论。
suspected就是suspected,报告会告诉你人工需要核实什么。 - 不搭测试套件。 把发现变成回归测试是另一件事,这里只做审计。
许可
MIT