第 2 章 LLM Agent 的最小执行循环
运行真实修改与测试,逐步分析正常、超时和假完成轨迹
2.1 先拆开模型输出与工具执行
本章目标:读懂并运行一个具备工具白名单、调用记录、错误反馈、预算和完成验收的闭环;能解释怎样把确定性策略替换为模型适配器。
模型生成的是 token。所谓工具调用,是运行时把某类输出识别为结构化动作请求,例如 name=read_file、arguments={"path":"pricing.py"}。文件不会因为模型写出这段 JSON 自动被读取,运行时必须明确执行它,再把返回值送回策略。
一个最小系统可以拆成四个接口:policy.next(history) 提出决策;tools.execute(name,args) 执行允许的动作;trace.append(...) 保存关联关系;accept_final(...) 根据证据判断能否结束。真实模型适配器还要处理消息格式、工具 schema、流式结果及调用 ID。
2.2 消息和状态为什么必须分开
模型决策通常是 tool 或 final,但工具结果还有成功、业务失败、超时、权限拒绝等情况。这些状态不是同一层。
{
"step": 1,
"call_id": "call-1",
"decision": {"kind": "tool", "name": "run_tests", "arguments": {}},
"result": {"status": "ok", "passed": false, "source_hash": "..."}
}
status=ok 表示测试工具成功运行,passed=false 表示被测代码不合格。如果把它统一标成“工具失败”,策略可能反复重启测试,而不修代码。相反,工具超时无法说明代码一定不合格,因为此次执行没有取得足够证据。
每条结果要关联唯一调用 ID。并行工具可能以不同顺序返回,仅靠“上一条消息”匹配会串结果。教学脚本顺序执行,因此用步数构造 ID;产品通常需要在请求生命周期中维护更完整的标识与状态。
2.3 逐步读正常修复轨迹
下载 repair_lab.py,或在仓库根目录运行:
python3 agent/examples/repair_lab.py --scenario happy
| 决策步 | 执行动作 | 新增证据 | 下一步为何合理 |
|---|---|---|---|
| 1 | run_tests |
边界、会员、负数不合格 | 需要查看对应实现 |
| 2 | read_file |
初始代码仅检查 > 100 |
能指出三个缺失规则 |
| 3 | replace_file |
当前源码哈希变化 | 写入成功仍不等于正确 |
| 4 | run_tests |
当前版本五项检查通过 | 取得本次完成依据 |
| 5 | final |
验收器核对当前哈希 | 接受结束并返回轨迹 |
重点读 Tools.execute 和 run,最后再读 DemoPolicy。策略中已预置正确补丁,目的是让你专注执行机制。把预置答案的成功率当成真实 LLM 修复率,会得出完全没有意义的结论。
原来的更小例子 mini_agent.py 只演示搜索与观察回填,适合先熟悉接口;它没有这里的修改、测试与版本验收链路。
2.4 为什么结束判断要核对当前版本
实验中的测试记录保存通过时的源码哈希。每次写入先清空 last_verified;最终回答到来时,运行时再次比较记录与当前文件。这样能拒绝“测试后又改代码,再引用旧成功”的路径。
# 核心机制摘录;完整上下文见 repair_lab.py。
verified = tools.last_verified == digest(root / "pricing.py")
status = "success" if verified else "rejected"
真实项目不能只哈希一个文件。测试结果还依赖其他源码、测试文件、依赖、配置和环境。因此需要绑定更完整的 revision 或工作区快照,并确认验收器本身未被 Agent 篡改。教学中的单文件哈希只展示“证据应绑定被测状态”这条原则。
2.5 错误返回之后,策略应怎样继续
python3 agent/examples/repair_lab.py --scenario timeout
python3 agent/examples/repair_lab.py --scenario invalid
python3 agent/examples/repair_lab.py --scenario stale
python3 agent/examples/repair_lab.py --scenario budget
timeout 在首次测试注入一次模拟超时,策略重试固定的只读检查,随后正常完成,共六步。invalid 请求读取 ../secret.txt,运行时拒绝越界路径并结束。stale 在测试通过后把错误代码写回,最终验收拒绝旧证据。budget 只给三步,代码虽已修改,但尚未测试,所以返回 budget_exhausted。
可恢复错误不必立即结束;持续权限拒绝也不值得无限重试。应定义每种错误能做的下一步,以及总次数或总时间上限。重试是否安全还取决于副作用,第 4 章会专门讨论。
2.6 ReAct 与模型适配器放在哪里
ReAct [导读] 的关键是行动和观察交替:新反馈改变下一步,而不只是生成更长的计划。一次 run_tests 给出三种缺陷,策略下一轮读取实现;测试通过后又有写入,就需要再次测试。这才是反馈进入决策。
接入模型时,适配器把目标、约束、工具描述和已取得证据转成模型输入,再把输出标准化为内部决策对象。不要直接把模型文本传给任意 eval 或 shell。未知工具、缺字段、错误类型应作为受控错误处理;不能为了让模型输出“顺利运行”绕过校验。
工具结果保留可信来源、状态和关键内容即可,不需要向用户公开模型的全部内部推理。用于调试的决策记录也应避免记录密钥或不必要的私人数据。
2.7 动手修改与参考检查
给脚本增加 empty_read 场景,模拟读取到空内容。你需要决定这是合法空文件、路径错误还是输出截断,给出明确状态,并使策略避免无证据写补丁。只新增一句提示而不修改状态处理,不能算完成练习。
参考检查:运行正常路径仍成功;空内容路径没有误报完成;新增结果关联正确调用 ID;预算耗尽仍能返回已有轨迹。再把终止判断改成“只要 policy 返回 final 就成功”,运行 false_final 与 stale,观察保护如何失效,然后恢复实现。
本章交付物:一条正常轨迹、四种异常路径、一次修改后的验收结果。应该能逐条回答:哪一层发现了错误,哪一层决定下一步,最终结束由谁接受。