[控场AI]
· 10 分钟阅读· 5,028 字

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

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

用先写ledger、以测试退出码为准、隔离工作区三重约束,把Agent编程从不可追溯变成可回放。

文章复盘了一次浪费五小时的 AI 编程实验:三层重试共用一个无时间戳日志,导致三个 diff 无法对应运行记录;把 reviewer 模型的「看起来正确」当成测试通过而合并了有差一错误的补丁;writer 与 reviewer 共用工作区污染了 fixture,使哈希与 ledger 对不上。作者给出的修复是三条硬约束:在首次模型调用前写 ledger 行、只把 diff 与失败输出喂给 reviewer 并以测试退出码为最终裁决、用目录隔离加哈希门禁加写入路径白名单保证可证明性。最终形成六步循环,每步追加一行含耗时的 ledger,产出一份周五可回放的运行凭据。作者同时提醒,免费模型与免费服务器只是可用性声明,不适用于无人值守长循环或涉及凭据的任务。

引言:一次失败的AI编程实验

08:20我开了两个终端,19:40才关掉。一个跑着免费服务器,另一个跑着写补丁的Agent。到午饭时,我已经攒了三个diff,却完全分不清哪个来自哪次运行。

这不是模型的问题,而是我缺少计数器。本文复盘了那天浪费的五小时,并给出一个可复制、已被验证能撑过一周的六步工作流。

rss source: Letter to 08:20-Me: Stop Auditing the Agent, Audit the Counters

当天到底付出了什么代价

我花了大约三小时重跑循环,因为无法把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; }

撑过一周的六步循环

  1. 先冻结fixture包并记录哈希,再做其他事。
  2. 把计划写成JSON,用jq和白名单门禁验证。
  3. 在免费服务器上跑测试套件,捕获退出码和尾部输出。
  4. 只把diff和失败尾部发给免费模型做二次阅读。
  5. 对比reviewer结论与退出码,不一致则中止。
  6. 每一步追加一行ledger,包含实际耗时秒数。

六步产出一份周五能回放的凭据。这就是全部要点。

免费容量不适用的场景

免费访问是可用性声明,不是吞吐量承诺。我按短时爆发来规划,不按截止日期规划。也不对持久化做任何假设——产物立刻拉回本地。

场景仅本地免费服务器付费固定SLA
单文件修复,测试30秒内合适可选过重
需要系统包或服务慢合适合适
无人值守多小时循环有风险有风险合适
涉及凭据绝不可绝不可仅隔离runner
diff二次阅读合适合适可选

表格中有三行是判断问题,两行是硬性限制而非偏好。

局限与不建议使用的人

免费模型访问和免费服务器选项都是运营方提供的声明,需自行核实条款。我没有测量吞吐量,也不会把发布计划排在上面。

如果你的测试需要生产凭据或受监管数据,跳过这个工作流。如果需要合同级正常运行时间或数据驻留保证,跳过。如果人工审查者无法在合并前读取ledger,也跳过。

给08:20的我的建议

先写计数器再写提示词,先写哈希再写diff。reviewer和测试不一致时,相信测试,叫人。

想试的话,用免费模型访问和免费服务器选项跑一遍六步循环。即使明天两个都不用了,也把ledger留着。

披露:本文作为MonkeyCode产品推广的一部分撰写。

#AI编程#Agent工作流#AI代码审查#可观测性#CI测试#ledger计数器#免费模型API
分享:

相关推荐