别再审计Agent了,审计计数器:AI编程工作流复盘

用先写ledger、以测试退出码为准、隔离工作区三重约束,把Agent编程从不可追溯变成可回放。
文章复盘了一次浪费五小时的 AI 编程实验:三层重试共用一个无时间戳日志,导致三个 diff 无法对应运行记录;把 reviewer 模型的「看起来正确」当成测试通过而合并了有差一错误的补丁;writer 与 reviewer 共用工作区污染了 fixture,使哈希与 ledger 对不上。作者给出的修复是三条硬约束:在首次模型调用前写 ledger 行、只把 diff 与失败输出喂给 reviewer 并以测试退出码为最终裁决、用目录隔离加哈希门禁加写入路径白名单保证可证明性。最终形成六步循环,每步追加一行含耗时的 ledger,产出一份周五可回放的运行凭据。作者同时提醒,免费模型与免费服务器只是可用性声明,不适用于无人值守长循环或涉及凭据的任务。
引言:一次失败的AI编程实验
08:20我开了两个终端,19:40才关掉。一个跑着免费服务器,另一个跑着写补丁的Agent。到午饭时,我已经攒了三个diff,却完全分不清哪个来自哪次运行。
这不是模型的问题,而是我缺少计数器。本文复盘了那天浪费的五小时,并给出一个可复制、已被验证能撑过一周的六步工作流。

当天到底付出了什么代价
我花了大约三小时重跑循环,因为无法把diff和运行记录对上号。又花了两小时调试一个被reviewer模型「祝福」过的补丁。剩下的时间耗在一个每次运行哈希都会变化的fixture目录上。
这三类失败都指向同一个根因:缺少计数器。Agent重试、wrapper重试、shell循环再重试,三层重试共用一个没有时间戳的日志文件——这不是可观测性,是噪音。
错误一:让执行框架替我计数
最致命的习惯是让harness记录尝试次数。Agent、wrapper、shell循环各自重试,日志里只有一行结果,没有时间戳。
修复方式很朴素:在第一次模型调用之前写ledger行,而不是在最后一次之后。ledger是一个TSV文件,无聊但有效。
#!/usr/bin/env bash
**什么是 ledger(运行台账)?**
在数据工程和数据库领域,ledger 最早指只追加(append-only)的流水记录,其核心性质是:每条记录一旦写入便不再修改,新事件只能追加到末尾。这与普通日志的区别在于,ledger 更强调「可作为凭据」——它记录的是发生过什么,而不是调试时顺手打印了什么。
在 AI Agent 工作流中,ledger 通常是一个结构化的表格文件(如 TSV/CSV),每一行代表一次可追溯的事件:run ID、阶段名、序号、输入哈希、耗时秒数、结果状态。之所以用 TSV 而不是 JSON 日志,是因为它足够无聊:没有嵌套、没有 schema 迁移问题,`tail`、`awk`、`grep` 都能直接处理,也不需要额外的日志聚合服务。
作者强调「在第一次模型调用之前写 ledger 行」体现了 ledger 的关键设计原则:记录意图先于记录结果。如果只在成功之后写一行,那么崩溃、超时、被中断的运行就完全不留痕迹,而恰恰是这些失败运行最需要被追踪。先写「attempt 1 start」,再写结果,才能在事后还原「这次 run 到底有没有发生过、进行到了哪一步」。
# Adapted example. Not run as-is; substitute your own paths and commands.
set -euo pipefail
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)"
ART="./runs/$RUN_ID"
mkdir -p "$ART"
LEDGER="./ledger.tsv"
log() { printf '%s\t%s\t%s\t%s\n' "$RUN_ID" "$1" "$2" "$3" >> "$LEDGER"; }
FIXTURE_HASH="$(find fixtures -type f -print0 | sort -z \
| xargs -0 sha256sum | sha256sum | cut -d' ' -f1)"
log fixture "$FIXTURE_HASH" "frozen-input"
log attempt "1" "start"
现在我的规则是:没有ledger行,就没有模型调用。一个没有记录的run,即使diff看起来正确,也只是传闻。
错误二:把reviewer的同意当成测试
我曾让一个模型检查补丁,它说「看起来正确」,我就合并了。随后单元测试在范围检查的差一错误上失败。
reviewer模型产出的是意见,测试套件产出的是退出码。在CI里能阻止合并的只有后者。
修复方式:reviewer只看到diff和失败输出,其他上下文一律不给。喂给它能解释缺陷的最小信息量。
**退出码(exit code)为何是比自然语言更硬的信号?**
退出码是 Unix 进程结束时返回给父进程的整数,约定 0 表示成功、非 0 表示失败。CI 系统(如 GitHub Actions、GitLab CI)正是依据退出码来决定流水线是通过还是失败,而不是依据日志里的文字。这意味着:模型可以用自然语言说出「看起来正确」,但它无法伪造一个测试套件的退出码——除非测试本身真的通过。
这里的不对称性值得注意:reviewer 模型的输出是概率性的自然语言判断,可能受提示措辞、上下文残留、甚至 diff 格式影响;而 pytest 等测试框架的输出是确定性的程序行为。当两者冲突时,相信确定性信号、把不确定性信号当作辅助线索,是工程上的默认原则。
把 reviewer 的输入限制为「diff + 失败输出」也是一种降低噪声的手段:给模型更多上下文并不总能让它判断更准,反而可能让它顺着无关代码编出理由。最小信息量原则在审查类任务中往往比「给全仓库」更可靠。
# Adapted example: reviewer input is diff + failing test output only.
git diff --no-color > "$ART/patch.diff"
pytest -q 2>&1 | tail -n 40 > "$ART/test_tail.txt" || true
# Build the reviewer payload from those two files, not from the repo.
printf '%s\n' 'Review this diff against this test failure.' \
'Return JSON: {"verdict":"agree|disagree","reason":"..."}' \
'--- DIFF ---' > "$ART/review_prompt.txt"
cat "$ART/patch.diff" >> "$ART/review_prompt.txt"
printf '%s\n' '--- FAILURE ---' >> "$ART/review_prompt.txt"
cat "$ART/test_tail.txt" >> "$ART/review_prompt.txt"
然后对比两件事:reviewer的结论和测试退出码。两者不一致时立即停止,由人来读这次运行记录。
错误三:writer和reviewer共用同一个工作区
writer Agent写了临时文件,reviewer读到了被污染的目录。下一次运行的fixture哈希与ledger对不上,diff彻底失去可证明性。
修复方式是三个硬性约束:目录隔离、哈希门禁、写入路径白名单。门禁必须在任何shell命令执行之前跑,不是之后。
**什么是 fixture 哈希门禁(fixture hash gate)?**
fixture 指测试运行所依赖的固定输入数据或环境快照,例如样例文件、数据库种子数据、预置目录。如果 fixture 在运行期间被改动,那么同一条命令在不同时间跑出的结果就不可比较,diff 也就失去了意义——你无法判断差异来自代码改动还是输入漂移。
哈希门禁的做法是:在运行开始时对整个 fixture 目录做一次确定性哈希(对文件排序后逐个 sha256sum,再对结果整体求和),把该哈希写入 ledger;每次执行 shell 命令前重新计算并与 ledger 中的值比对,不一致就中止。这里的「确定性」很关键:`find ... | sort` 保证文件顺序稳定,否则哈希每次都会变,门禁会误报。
门禁必须在命令执行之前跑,而不是之后:事后再检查只能发现污染,无法阻止被污染的运行产出看似合法的 diff。配合目录隔离(writer 和 reviewer 各用独立工作区)与写入路径白名单,三者共同构成「这次运行可被证明」的前提条件。
# 1) Plan must be small, typed, and declare its write paths.
jq -e '
(.steps | length) > 0 and
(.steps | length) <= 5 and
has("write_paths") and
all(.steps[]; (.cmd | type) == "string")
' plan.json > /dev/null
# 2) Write paths must stay inside the allowed trees.
jq -r '.write_paths[]' plan.json | while read -r p; do
case "$p" in
./src/*|./tests/*) ;;
*) echo "refusing path: $p" >&2; exit 3 ;;
esac
done
# 3) Fixture pack must be unchanged since the ledger row was written.
NOW="$(find fixtures -type f -print0 | sort -z \
| xargs -0 sha256sum | sha256sum | cut -d' ' -f1)"
[ "$NOW" = "$FIXTURE_HASH" ] || { echo "fixture drift" >&2; exit 4; }
撑过一周的六步循环
- 先冻结fixture包并记录哈希,再做其他事。
- 把计划写成JSON,用jq和白名单门禁验证。
- 在免费服务器上跑测试套件,捕获退出码和尾部输出。
- 只把diff和失败尾部发给免费模型做二次阅读。
- 对比reviewer结论与退出码,不一致则中止。
- 每一步追加一行ledger,包含实际耗时秒数。
六步产出一份周五能回放的凭据。这就是全部要点。
免费容量不适用的场景
免费访问是可用性声明,不是吞吐量承诺。我按短时爆发来规划,不按截止日期规划。也不对持久化做任何假设——产物立刻拉回本地。
| 场景 | 仅本地 | 免费服务器 | 付费固定SLA |
|---|---|---|---|
| 单文件修复,测试30秒内 | 合适 | 可选 | 过重 |
| 需要系统包或服务 | 慢 | 合适 | 合适 |
| 无人值守多小时循环 | 有风险 | 有风险 | 合适 |
| 涉及凭据 | 绝不可 | 绝不可 | 仅隔离runner |
| diff二次阅读 | 合适 | 合适 | 可选 |
表格中有三行是判断问题,两行是硬性限制而非偏好。
局限与不建议使用的人
免费模型访问和免费服务器选项都是运营方提供的声明,需自行核实条款。我没有测量吞吐量,也不会把发布计划排在上面。
如果你的测试需要生产凭据或受监管数据,跳过这个工作流。如果需要合同级正常运行时间或数据驻留保证,跳过。如果人工审查者无法在合并前读取ledger,也跳过。
给08:20的我的建议
先写计数器再写提示词,先写哈希再写diff。reviewer和测试不一致时,相信测试,叫人。
想试的话,用免费模型访问和免费服务器选项跑一遍六步循环。即使明天两个都不用了,也把ledger留着。
披露:本文作为MonkeyCode产品推广的一部分撰写。
相关推荐

OpenAI与Shield合作:Astra智能体如何革新IT故障修复
OpenAI与Shield合作推出Astra智能体,可自动登录电脑和遗留系统,实时修复IT故障,比传统模型快35%到40%,仅在需要人类判断时引入人工。

iPhone Duo续航实测:一天一充跑不掉,大屏是耗电元凶
iPhone Duo续航实测:重度使用一天需至少充电一次,6.5小时亮屏约剩30%电量。大屏展开频率是耗电关键,MagSafe电池背夹可救场,长期衰减同样值得关注。

Kimi K3.1 Pro开源发布:2.8万亿参数逼近Opus
Moonshot发布开源MoE模型Kimi K3.1 Pro,2.8万亿总参数、1040亿激活参数,Terminal Bench 4.0仅落后Opus 5.5不到1分,单任务成本约三分之一,API价格减半,开源权重11月4日开放。