同一个智能体任务跑两次,得到两份不同的输出,而你无法判断究竟是自己的代码变了,还是免费额度本身变了。这种情况我遇到得够多,也知道修起来并不贵。
答案不是换一个更强的模型。你需要一条流水线:每个阶段结束时都有一项检查,要么通过,要么把你拦下;再加上一个哈希,用来判断两次运行是否还处于可比状态。只要搭一次,免费基础设施就能从“说不清”变成“可以使用”。
下面是完整的搭建过程,按阶段推进:命令、代码,以及进入下一阶段前你应该看到的输出。Python 3.11、bash、jq、sha256sum。不用框架。
为什么“免费”会改变设计
免费模型调用和免费服务器意味着你可以把调用成本压到零,却让不确定性上升。这些资源可能很慢,可能在你最需要的时候不可用,也可能一夜之间改变运行方式。这不是要批评某个供应商,而是免费额度的本质就是这么运作的。
所以整条流水线的目标不是“每次都成功”,而是“绝不假装成功”。每一次运行要么产出一份带签名的 manifest,要么大声报错退出。如果是中间状态,就等于是 bug。
MonkeyCode 提供免费模型访问和免费服务器选项,这正是本次实验的落脚点:免费额度足够便宜,可以跑完整个循环,而不用先想预算。本文把这两点当作来自运营方的声明,免费额度条款变化很快,所以不要拿本文当合同;在计划一整周的工作之前,请先自行核对该项目的当前页面。
披露:本文是作为 MonkeyCode 产品推广工作的一部分撰写的。
第 0 步——搭好工作目录
mkdir -p mc-gate/{inputs,work,runs,prompts} cd mc-gate python3 -V 验证: python3 -V 打印版本号,且版本 >= 3.11。如果更老,就停下,本文后面的代码都假设 3.11 的 f-string 和 pathlib 行为。
目录约定很重要,因为后面运行的每一步都以摘要命名:runs/<digest>/ 同时存放计划、输出和 manifest。如果你无法仅凭摘要就能找到运行产物,那么摘要闸门也就失去意义了。
第 1 步——对输入做规范化并哈希
这是整件事的基础。不可比较的输入会产生不可比较的运行,后面的检查也都会失效。
# digest.py import hashlib, json, pathlib, sys VOLATILE = {"timestamp", "request_id", "trace_id"} def strip(obj): if isinstance(obj, dict): return {k: strip(v) for k, v in obj.items() if k not in VOLATILE and not k.startswith("_")} if isinstance(obj, list): return [strip(v) for v in obj] return obj def digest(obj): canon = json.dumps(strip(obj), sort_keys=True, separators=(",", ":"), ensure_ascii=False) return hashlib.sha256(canon.encode("utf-8")).hexdigest() if __name__ == "__main__": payload = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) print(digest(payload)) 注意那个 strip 函数,它才是真正的设计动作。时间戳、请求 ID 和任何以 _ 开头的键,都在你的流水线里属于副作用,而不是输入。把它们排除掉,同一次运行在隔天重跑时,摘要才会保持稳定。
验证:
python3 digest.py inputs/job.json python3 digest.py inputs/job.json # identical output 两次调用必须打印出同一个 64 字符的哈希。然后改一个真实的值,比如某个文件路径;哈希必须变化。再去加一个 response_ts 字段,哈希必须保持不变。如果这两条测试里有任何一条不通过,就不要继续往下走,后面所有检查都会继承这个 bug。
第 2 步——先让免费模型生成计划
向模型请求一份 JSON 计划,其他什么都不要。用 jq 来构造请求体,这样就不用把凭据或模型名称硬编码进脚本里。
jq -n --arg m "$MC_MODEL" \ --arg p "$(cat prompts/plan.md)" \ '{model:$m, temperature:0, response_format:{type:"json_object"}, messages:[{role:"user",content:$p}]}' \ | curl -sS "$MC_BASE_URL/chat/completions" \ -H "Authorization: Bearer $MC_API_KEY" \ -H "Content-Type: application/json" -d @- \ | jq -r '.choices[0].message.content' > runs/plan.json temperature: 0 只是一个请求,不是保证。把它理解为“请保持稳定”,而不是“一定会保持稳定”。这就是为什么我们在这里加闸门,而不是信任这个参数。
验证: jq -e '.steps | length > 0' runs/plan.json 以 0 退出。如果你拿到的是散文而不是 JSON,说明你的提示词协议有问题,不要试图在客户端把它补救回来。
第 3 步——在执行任何东西之前先给计划加闸门
一份计划是攻击面。它引用路径,它决定做什么。在任何步骤被实际执行之前,先加闸门。
# gate_plan.py import json, pathlib, sys OPS = {"read", "transform", "write"} def validate(plan): if plan.get("version") != 1: raise ValueError("unsupported plan version") steps = plan.get("steps") or [] if not steps: raise ValueError("empty plan") for i, s in enumerate(steps): if s.get("op") not in OPS: raise ValueError(f"step {i}: unknown op {s.get('op')!r}") p = s.get("path") or "" if not p.startswith("./work/"): raise ValueError(f"step {i}: path escapes work dir: {p}") return steps plan = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")) print(f"ok: {len(validate(plan))} steps") 验证: python3 gate_plan.py runs/plan.json 打印 ok: N steps。然后做一次反向验证:把某个路径改成 ./etc/passwd,再跑一次。它必须以非零退出码退出,并报出“path escapes work dir”。一个从不失败的闸门属于装饰,而不是控制。
第 4 步——在免费服务器上执行,并做到幂等
免费服务器会重启、休眠,或中途断开。所以每个运行目录都以摘要为键,一旦 manifest 已经存在,脚本就拒绝执行任何操作。
#!/usr/bin/env bash set -euo pipefail DIGEST="$(python3 digest.py inputs/job.json)" RUN="runs/$DIGEST" mkdir -p "$RUN" if [ -f "$RUN/manifest.json" ]; then echo "already ran $DIGEST" exit 0 fi cp runs/plan.json "$RUN/plan.json" mkdir -p "$RUN/out" # your executor reads $RUN/plan.json and writes into $RUN/out ./executor.sh "$RUN/plan.json" "$RUN/out" jq -n --arg d "$DIGEST" --arg at "$(date -u +%FT%TZ)" \ '{digest:$d, finished_at:$at, steps:[]}' > "$RUN/manifest.json" echo "done $DIGEST" 验证: 运行一次,你得到 done <digest>。第二次运行,你得到 already ran <digest>,退出码为 0。这就是幂等性,也是长时间断线重连能够安全的前提。
第 5 步——重跑,并比较结构而不是措辞
生成第二份计划,逐字段比较。模型生成的自然语言每次都会变,所以拿散文去 diff 纯属浪费下午。比较形状。
jq -S '[.steps[] | {op, path}]' runs/plan.json > runs/a.json jq -S '[.steps[] | {op, path}]' runs/plan.rerun.json > runs/b.json diff -u runs/a.json runs/b.json && echo "structure stable" 验证: 输出 structure stable,diff 退出码为 0。如果 diff 显示某个步骤消失了或重新排序了,你有两种诚实的选择:把计划确定下来,或者接受这份任务是随机性的,并在下游做补偿。不要走第三条路,也就是不断调参数直到 diff 看起来干净为止。
第 6 步——提升或者拒绝
在最后加上一张决策表,这样这个问题就由表格来回答,不用再靠某个人临场判断。
| 观察到的情况 | 操作 | 原因 |
|---|---|---|
| 相同摘要,结构 diff 干净 | 提升产物 | 这次运行可复现 |
| 相同摘要,结构 diff 不一致 | 修复计划提示词,重跑 | 模型不是变量,你的提示词才是 |
| 输入变化后新出现摘要 | 新运行,不覆盖旧运行 | 历史保持可审计 |
| manifest 缺失或不完整 | 拒绝成果 | 无法审计的运行等同于猜测 |
验证: 提升步骤应当拒绝任何没有 manifest 的目录。用 rm runs/*/manifest.json 测试,确认它拒绝。
限制,以及谁应该跳过这套做法
- 免费额度条款会变。如果一篇文章精确固定了这些条款,它可能已经过时——请检查项目的当前页面。
- 对于天然带随机性的步骤,摘要闸门帮不上忙。它最多只能告诉你两次运行并不等价。
- 如果你的合规团队需要明确的数据留存保证,那么在阅读明确的数据策略之前,不要用第三方服务处理真实数据。
- 不要用免费额度当作付费模型延迟的基准。那是另一种测量,而这次的运行本来就不是为那种测量设计的。
- 如果你需要硬性正常运行时间 SLO,这套做法就是错的工具。这套做法的全部前提,就是让失败变得可审计。
这套做法的目标读者是谁? 是在免费层上跑智能体任务、并需要为结果辩护的人。
结尾
这套做法里没有任何一项依赖某个特定供应商。摘要、闸门、manifest,在任何地方都成立,也正是它们让免费资源可以安全使用。如果你想让这套 harness 有个一运行成本为零的起点,MonkeyCode 的免费模型访问和免费服务器选项就是最早跑通循环的合理落脚点——但请先完整跑一遍第 1 步里的正反两个测试。
如果你运行了它,并且摘要闸门抓住了你的代码本来会漏掉的问题,那就是值得分享的部分。是哪一个阶段?
MonkeyCode provides free models that can run this workflow. A free server option is enough to reproduce the setup.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
