我做了个零依赖 Agent Harness,然后用评测证伪了自己

我做了个零依赖 Agent Harness,然后用评测证伪了自己

一个把大模型当"不可靠依赖"来工程化的运行时:OS 强制沙箱、上下文压缩、独立判分,以及一段我把自己首轮结论撤回的消融实验。 仓库:github.com/zcf2230/age... | 零依赖 TypeScript,无构建步骤。

先说结论

市面上的 "Agent 教程" 99% 停在 while True: resp = llm(prompt); run(tool)。能跑,但它回避了三个让 agent 从 demo 变成系统的问题:

  1. 模型生成的代码敢不敢真执行? 敢,就得有真隔离。
  2. 它说"我做完了",算数吗? 不算,得有独立判分。
  3. 你怎么知道某个机制"真的有用"? 靠消融------而我的消融打脸了我自己。

这篇文章重点放在第 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 多轮就崩。真实跑分闭环的价值就在这里:

  1. 422 missing field type :我内部把 tool_calls 扁平存成 {id,name,arguments},回传 OpenAI 兼容 API 必须是嵌套 {type:'function',function:{name,arguments}}。MockProvider 不校验 wire 格式所以全绿,只有真服务端会拒。修成纯函数 toWire() + 单测锁死。
  2. run_js 路径重复 :相对路径叠加 cwd 导致 Cannot find module,用 path.resolve 修。
  3. 判分器取"末位数字"取错 :answer_number 被答案里的中间量带偏,模型答对却判错。
  4. c01 期望正则我自己写错一行。

后三个都是判分/任务本身错,不是模型错------逼我建立了"判分器也要被测试"的纪律。

一句话教训:mock 全绿 ≠ 能上线;没有真实跑分闭环,你根本不知道自己的 agent 能不能对接真模型。


四、重点:我用 R=3 消融,证伪并撤回了我自己

我一开始做了个消融实验,得出两个漂亮结论:

"护栏让可靠性 +4.5%" | "上下文压缩省 4% token"

差点就写进简历了。然后一次严格评审指出:工程是真的,度量是假的。 逐条对,全中:

  • n=1 单轮,噪声压过信号 :22 个任务、单任务 ~98.5% 通过率下,出现一次失败的先验概率约 28%,1/22 的差异根本分辨不了 4.5% 这个量级。
  • 压缩是空对照 :我当时硬编码"保留最近 4 组",而强模型常 ≤8 回合、还并行发工具调用,分组数根本不超阈值 → 压缩永远没触发,所以"关 vs 开"token 当然几乎一样。
  • 护栏效应和 g01 混淆:关沙箱那次挂的是同一个任务,而护栏是开着的也没救回来。

我完全接受了批评,重做了实验:

  1. 把压缩的保留组改成随 token 预算缩放 ,让它真的能触发(现在 x07/x10 长链任务实测 compactions=1 且仍判对)。
  2. 消融改成每个配置 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:真实前沿,我没假装解决了它。

六、下一步(按性价比)

  1. 第二个 provider 端点做真正的多模型对比;
  2. 容器/微 VM 沙箱后端补网络隔离;
  3. 把任务约束做成可执行元数据(constraints 由判分器强制执行);
  4. 更大 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 的地方,建出可判分、可回归的质量闭环。

相关推荐
10年前端老司机2 小时前
干货分享|企业智能知识库 Rerank 重排序落地实践与踩坑总结
python·aigc·agent
吴佳浩2 小时前
Agent 可观测性(Observability):分布式追踪、链路诊断与 Token 成本精细化核算
人工智能·agent·ai编程
武子康2 小时前
Agent 能接进 IDE,为什么还不能随意互换?
人工智能·llm·agent
武子康2 小时前
ESP32-S3 Mini 和 C3 Mini 怎么买?从 PSRAM、USB 到一张可核对的采购单
人工智能·llm·agent
武子康2 小时前
权重装进了 24 GiB,四路 32K 上下文还装得下吗?
人工智能·llm·agent
潘锦2 小时前
做 Agent 架构时,OpenViking 的几个可以学习的点
agent·cto
小盆女神节奶粉2 小时前
Graph的基本流程理解
agent