我做了个零依赖 Agent Harness,然后用评测证伪了自己
一个把大模型当"不可靠依赖"来工程化的运行时:OS 强制沙箱、上下文压缩、独立判分,以及一段我把自己首轮结论撤回的消融实验。 仓库:github.com/zcf2230/age... | 零依赖 TypeScript,无构建步骤。
先说结论
市面上的 "Agent 教程" 99% 停在 while True: resp = llm(prompt); run(tool)。能跑,但它回避了三个让 agent 从 demo 变成系统的问题:
- 模型生成的代码敢不敢真执行? 敢,就得有真隔离。
- 它说"我做完了",算数吗? 不算,得有独立判分。
- 你怎么知道某个机制"真的有用"? 靠消融------而我的消融打脸了我自己。
这篇文章重点放在第 3 点,因为我觉得**"敢把证伪自己的数据摆出来"比"给出一个好看的百分比"更能说明我懂评测**。
一、整体架构:数据流与关注点分离
任务 JSON(提示词 + 初始文件 + 判分规则)→ 并发 runner 为每个任务建独立 workspace → agent 主循环驱动模型多轮调用工具(代码在沙箱里真跑、结果回填)→ 独立判分器读产物/跑测试验收 → 事件流与统计落盘 → bench/ablate 聚合出通过率、方差、机制贡献。
分层大致长这样:
bash
cli.ts (run/eval/bench/ablate/mcp/selftest)
└─ eval/runner 并发池 → 每任务独立 workspace
├─ agent/loop 决策→执行→回填主循环(finish 止损、轮数/成本/时限护栏、收尾兜底)
│ ├─ providers/openai OpenAI 兼容 + 指数退避重试 + toWire 序列化
│ ├─ agent/tools 7 工具;run_js 走 Node --permission 沙箱
│ ├─ agent/sandbox execFile + 超时 + env 白名单 + 路径封锁
│ ├─ agent/context 按回合分组、随预算缩放的 LLM 压缩
│ └─ agent/checkpoint trajectory.jsonl 事件流 + state.json 原子快照
├─ mcp/bridge 外部 MCP 服务器工具命名空间化注入
└─ eval/grade 4 类独立判分器 + protected 文件哈希校验
→ summary.json / bench.json(pass@R,方差,flaky) / ablation.json(配对消融)
四条设计原则(也是我想应聘 LLM 应用工程师岗时展示的东西):
- 模型可替换:provider 层把 DeepSeek / 任意 OpenAI 兼容端点隔开,换模型只改配置。
- 能力可扩展:工具是数据驱动的 schema,MCP 让外部能力即插即用。
- 不可靠被隔离:模型输出进沙箱执行、进独立判分验收。
- 一切可度量:每跑一个任务都产出结构化事件与判分结果,pass@R 和回归才有地基。
规模:2872 行零依赖 TypeScript / 17 个模块 / 无构建 ,Node 24 直接跑。审阅者 clone 下来 node src/cli.ts selftest 就能验------88 项离线断言全绿,不需要任何 API key。
二、三个硬机制(挑最能讲的说)
2.1 沙箱:不是"约定",是 OS 强制级
工具层做了路径校验(拒绝 .. / 绝对路径 / UNC)之后,很多人以为够了。不够。 因为模型能在 run_js 里这么干:
js
import fs from 'node:fs';
fs.readFileSync('C:/Windows/system.ini', 'utf8'); // 完全绕开我的工具层
所以模型生成的代码在 Node 运行时权限模型下执行:
css
node --permission --allow-fs-read=<workspace> --allow-fs-write=<workspace> solution.mjs
越界读写被内核级拒绝、child_process 默认禁用。子进程的环境变量走白名单 ,剥离 DEEPSEEK_API_KEY 等机密------就算模型被注入诱导,也拿不到密钥。
还有一个我最初漏掉、被评审抓出来的洞:判分器会执行模型写的交付物文件 。早期 run_test 判分用的是不带 --permission 的裸 exec,等于"把 payload 写进交付物、让判分器替我跑"就能越狱。修好之后,判分对模型文件施加同样的 jail ,并对 protected 验收脚本做 sha256 校验------模型用 run_js 偷改 test.mjs 会被判 grader_tampered 直接失败。
诚实的边界我也写进 README 没藏:Node 权限模型不拦网络出口 ,run_py 也没有对等的零依赖隔离。所以准确说法是"文件 + 子进程受控、网络未受控",生产要补容器/gVisor。
2.2 上下文压缩:按"回合"分组 + 随预算缩放
长会话不压就爆窗。但不能按 token 硬截------OpenAI 协议里 tool 消息必须紧跟带对应 tool_calls 的 assistant 消息,硬切会切断配对导致 400。
所以我以"回合"(一条 assistant + 它全部工具结果)为原子单位,超预算时把最老的若干回合摘要成一条 system 消息,保留最近能塞进预算的组。摘要提示词强制保留文件名/数字等可验证锚点,失败退化规则摘要;压缩后不更小就放弃(no-benefit),摘要调用自身 token 计入成本。
这个机制我踩过一个能写进教科书的坑,见第四节。
2.3 独立判分:看产物,不看自述
4 类判分器(answer_regex / answer_number / file_regex / run_test),绝不复用被测模型判分 。而且判分读的是 workspace 里的产物文件,不是模型嘴上的"我完成了":
- 模型即使没调
finish、耗尽了回合,只要文件写对了,判过; - 反过来满嘴"已完成"但没落盘,判不过。
每个任务上线前先跑"标准答案 + 负例"自校验------正是这一步抓出我自己判分规则写错了两次(不是模型错)。
三、真实跑分抓出的 4 个 bug(mock 全测不到)
本地自检全绿,一连真 API 多轮就崩。真实跑分闭环的价值就在这里:
- 422
missing field type:我内部把tool_calls扁平存成{id,name,arguments},回传 OpenAI 兼容 API 必须是嵌套{type:'function',function:{name,arguments}}。MockProvider 不校验 wire 格式所以全绿,只有真服务端会拒。修成纯函数toWire()+ 单测锁死。 - run_js 路径重复 :相对路径叠加 cwd 导致
Cannot find module,用path.resolve修。 - 判分器取"末位数字"取错 :
answer_number被答案里的中间量带偏,模型答对却判错。 - c01 期望正则我自己写错一行。
后三个都是判分/任务本身错,不是模型错------逼我建立了"判分器也要被测试"的纪律。
一句话教训:mock 全绿 ≠ 能上线;没有真实跑分闭环,你根本不知道自己的 agent 能不能对接真模型。
四、重点:我用 R=3 消融,证伪并撤回了我自己
我一开始做了个消融实验,得出两个漂亮结论:
"护栏让可靠性 +4.5%" | "上下文压缩省 4% token"
差点就写进简历了。然后一次严格评审指出:工程是真的,度量是假的。 逐条对,全中:
- n=1 单轮,噪声压过信号 :22 个任务、单任务 ~98.5% 通过率下,出现一次失败的先验概率约 28%,
1/22的差异根本分辨不了 4.5% 这个量级。 - 压缩是空对照 :我当时硬编码"保留最近 4 组",而强模型常 ≤8 回合、还并行发工具调用,分组数根本不超阈值 → 压缩永远没触发,所以"关 vs 开"token 当然几乎一样。
- 护栏效应和 g01 混淆:关沙箱那次挂的是同一个任务,而护栏是开着的也没救回来。
我完全接受了批评,重做了实验:
- 把压缩的保留组改成随 token 预算缩放 ,让它真的能触发(现在 x07/x10 长链任务实测
compactions=1且仍判对)。 - 消融改成每个配置 R=3 配对重跑 ,报 pass@R / 中位数 / 最低 / flaky,一共 264 次真实 API 调用。
结果------
四个配置全部落在噪声内;关掉任何一个机制都测不出可分辨的收益;g01 在所有配置里都 flaky;关压缩后 token 几乎不变。
也就是说:我首轮的漂亮数字,被我自己重新设计的实验证伪了。
我把 +4.5% 和 −4% 从简历和 README 里全部撤回 ,只保留一个可复现的信号:开放式任务 g01 的"何时该停"是真实的能力前沿,现有护栏不能可靠消除它的终止抖动。 这份打脸自己的数据我故意留在仓库 results/ablation.json 里供人复核。
需要说清楚的一点:R=3 足以否证 "有 +4.5% 收益"这个宣称,但不足以证实 "收益精确为零"。我没有反过来吹"机制确定没用",只说"首轮那个可分辨的效应不成立"。区分"证伪一个宣称"和"证明一个不存在",我觉得是评测的基本素养。
五、诚实的局限(不藏)
- 网络出口不拦:Node 权限模型的边界,生产要上容器/gVisor/egress 代理。
run_py无对等隔离:只有工具层约束 + 超时。- 单模型 :只评测过
deepseek-chat,所以我不声称任何跨模型结论。 - 机制收益测不出显著效应:不是机制没写对("存在且正确工作"有 88 项自检 + CI 背书),而是当前任务难度梯度下天花板效应太重,需要更大 R 和更难任务。
- g01 终止 flaky:真实前沿,我没假装解决了它。
六、下一步(按性价比)
- 第二个 provider 端点做真正的多模型对比;
- 容器/微 VM 沙箱后端补网络隔离;
- 把任务约束做成可执行元数据(constraints 由判分器强制执行);
- 更大 R + 更难的失败任务,让消融能测出真实效应。
我刻意没做流式输出和 Web 实时面板------判断它对"证明工程深度"的边际价值低,优先级排在了沙箱和评测后面。
附:一键复现
bash
cd agent-harness
node src/cli.ts selftest # 88 项断言秒过,不需要 key
node src/cli.ts tasks # 22 任务 / 7 能力域
node src/cli.ts bench --all --runs 3 --yes # pass@R / 方差 / flaky
node src/cli.ts ablate --all --runs 3 --yes # 配对消融(就是这一节证伪我自己的实验)
# 打开 viewer/replay.html,拖入 runs/.../trajectory.jsonl 看逐回合回放
真实跑分需要
DEEPSEEK_API_KEY,只走环境变量 ,仓库里config.json和runs/都已 gitignore。
如果这段"我干掉了自己结论"的经历对你有用,欢迎交流------比起又一个人人能做的小 agent,我更想聊怎么在没有 ground truth 的地方,建出可判分、可回归的质量闭环。