运行时反馈循环 本节摘要:看不到真实命令输出的 Agent 在猜。一个反馈运行器(feedback runner)把 stdout、stderr、退出码、时序捕获进一条结构化记录,让下一轮能读。于是 Agent 对事实反应,而非对自己对事实的预测反应。诊断很经典:Agent 说「现在跑测试」,下一条消息说「所有测试通过」,现实是——没跑任何测试;Agent 想象了输出,或跑了命令却从不读结果,或读了结果却静默截掉了失败行。反馈运行器消除这道鸿沟:每条命令经运行器,每条记录带命令、捕获的 stdout 与 stderr、退出码、墙钟时长、一行 Agent 便签;Agent 下一轮读记录,验证门任务结束读记录。
本节摘要:看不到真实命令输出的 Agent 在猜。一个反馈运行器(feedback runner)把 stdout、stderr、退出码、时序捕获进一条结构化记录,让下一轮能读。于是 Agent 对事实反应,而非对自己对事实的预测反应。诊断很经典:Agent 说「现在跑测试」,下一条消息说「所有测试通过」,现实是——没跑任何测试;Agent 想象了输出,或跑了命令却从不读结果,或读了结果却静默截掉了失败行。反馈运行器消除这道鸿沟:每条命令经运行器,每条记录带命令、捕获的 stdout 与 stderr、退出码、墙钟时长、一行 Agent 便签;Agent 下一轮读记录,验证门任务结束读记录。本节定义反馈记录的七字段(command 精确 argv、stdout_tail/stderr_tail 尾 N 行、exit_code 明确成功信号、duration_ms 暴露慢探针与失控进程、started_at 重放时间戳、agent_note 期望便签),讲透截断是确定性的(50MB 日志会毁掉循环;首尾截断带
...截断 N 行...标记,无采样——Agent 要看的部分在尾部),区分反馈 vs 遥测(遥测第 23 节是给人跨时间审运行,反馈是本次运行的下一轮;共享字段但活在不同文件、不同保留),以及铁律无反馈即拒绝推进(运行器捕获退出码前出错,记录带exit_code: null+error,Agent 循环必须拒绝在 null 退出上声称成功——无退出,无进展)。本节用标准库实现run_with_feedback(包 subprocess、确定性截断、追加 JSONL),并给三条让运行器能交付的生产模式:写时脱敏非读时(Bearer/password/AKIA/slack token,盘上文件是攻击者够向的)、轮转策略非单文件(1MB 上限、.1/.2 滚动、.5 丢弃)、父命令 id 串重试链(command_id + parent_command_id,否则重试像独立成功、审计藏失败史)。读完本节,你能让 Agent 看到失败、据此修正,而非在 400 上宣布成功。
对应原课程:Phase 14 · Lesson 37 ·
runtime-feedback-loops(原英文phases/14-agent-engineering/37-runtime-feedback-loops/docs/en.md)。前置:第 32 节(最小工作台)、第 35 节(初始化脚本)。
阅读完本节,你应当能够:
Agent 说「现在跑测试」。下一条消息说「所有测试通过」。现实是没跑任何测试。Agent 想象了输出,或跑了命令却从不读结果,或读了结果却静默截掉了失败行。
反馈运行器消除这道鸿沟。每条命令经运行器。每条记录带命令、捕获的 stdout 与 stderr、退出码、墙钟时长、一行 Agent 便签。Agent 下一轮读记录。验证门任务结束读记录。
这正是第 26 节「级联错误」「成功幻觉」的直接防御——Agent 分不清「我失败」与「任务不可能」,常在 400 上幻觉成功消息;反馈运行器把真实退出码塞进循环,让 Agent 无法自欺。
| 字段 | 为何重要 |
|---|---|
command |
精确 argv,无 shell 展开意外 |
stdout_tail |
尾 N 行,确定性截断 |
stderr_tail |
尾 N 行,与 stdout 分开 |
exit_code |
不含糊的成功信号 |
duration_ms |
暴露慢探针与失控进程 |
started_at |
重放时间戳 |
agent_note |
Agent 写的关于期望的一行 |
一个 50MB 日志会毁掉循环。运行器用 ...截断 N 行... 标记做首尾截断,确定性——同样的输出恒产出同样的记录。无采样;Agent 要看的部分(最终错误、最终摘要)在尾部。
遥测(第 23 节 OTel GenAI 约定)是给人操作员跨时间审运行的。反馈是本次运行的下一轮。它们共享字段,但活在不同文件、不同保留里。
若运行器在捕获退出码前出错,记录带 exit_code: null 与 error: <原因>。Agent 循环必须拒绝在 null 退出上声称成功。无退出,无进展。
原课程 code/main.py 实现:
run_with_feedback(command, agent_note),包 subprocess.run,捕获 stdout/stderr/退出/时长,确定性截断,追加到 feedback_record.jsonl。def tail(text, max_lines=50): lines = text.splitlines() if len(lines) <= max_lines: return text head, tail = lines[:5], lines[-(max_lines-5):] # 首5 + 尾(N-5) return "\n".join(head) + f"\n...截断 {len(lines)-max_lines} 行...\n" + "\n".join(tail)
def run_with_feedback(command, agent_note, cwd=None): started = time.time() try: proc = subprocess.run(command, capture_output=True, text=True, cwd=cwd, timeout=300) exit_code, stdout, stderr, err = proc.returncode, proc.stdout, proc.stderr, None except Exception as e: exit_code, stdout, stderr, err = None, "", str(e), type(e).__name__ # null 退出 record = {"command": command, "cwd": cwd, "stdout_tail": tail(stdout), "stderr_tail": tail(stderr), "exit_code": exit_code, "error": err, "duration_ms": int((time.time()-started)*1000), "started_at": started, "agent_note": agent_note} append_jsonl("feedback_record.jsonl", record) return record
def agent_step(): note = "期望:测试通过" rec = run_with_feedback(["pytest", "tests/"], note) if rec["exit_code"] is None: return "BLOCKED: 无退出码,拒绝推进" # 无退出,无进展 if rec["exit_code"] != 0: return f"失败,据 stderr 修正: {rec['stderr_tail']}" return "通过"
运行 python3 code/main.py 会把三条反馈记录追加到 feedback_record.jsonl,每条最后一条内联打印。跨重跑 tail 文件看循环累积。
三条把运行器硬化到能交付。
写时脱敏,非读时。 任何碰 stdout 或 stderr 的记录都可能泄密。运行器在 JSONL 追加前过一道脱敏:剥匹配 ^Bearer 、password=、api[_-]?key=、AKIA[0-9A-Z]{16}(AWS)、xox[baprs]-(Slack)的行。读时脱敏是 footgun;盘上的文件是攻击者够向的。每季度按生产运行时观察到的密钥格式审计脱敏模式。
轮转策略,非单文件。 把 feedback_record.jsonl 上限定 1MB;溢出时滚到 .1、.2,丢 .5。Agent 循环只读当前文件,所以运行时代价有界。CI 工件存储拿完整滚全套。不轮转,文件变成每次加载器调用的瓶颈。
父命令 id 串重试链。 每记录得 command_id;重试带 parent_command_id 指向前次尝试。审查者的「失败尝试」列表(第 40 节)与验证门审计都跟链。没这链接,重试看着像独立成功,审计藏了失败史。
💡 设计要点:反馈运行器把「Agent 看到失败」从假设变成强制——每条命令的真实退出码进记录,循环拒绝在 null 退出上推进。这是第 26 节「成功幻觉」与「级联错误」的直接工程解药,也是第 06 节「400 要变观察字符串」在工作台层的落地。它与第 23 节遥测的区别要记牢:遥测给人跨时间审,反馈给本次运行的下一轮——共享字段但不同文件不同保留,别混。
| 运行时 | 反馈如何用 |
|---|---|
| Claude Code Bash 工具 | 已捕获 stdout/stderr/退出/时长;本节运行器是任何 Agent 产品的框架无关等价物 |
| LangGraph 节点 | 把任何 shell 节点包进运行器,让记录持久在图状态外 |
| CI 日志 | 把 JSONL 管进 CI 工件存储;审查者能重放任何命令无需重跑会话 |
运行器是一层薄包装,挺过每次框架迁移,因为它拥有记录的形状。
原课程 outputs/skill-feedback-runner.md:生成项目特定的 run_with_feedback.py,带对的截断预算、接好工作台的 JSONL 写入器、一个 Agent 每轮读的加载器。
cwd 字段,让同命令在不同目录跑可区分。^Bearer 或 password= 的行。在 fixture 记录上测。feedback_record.jsonl 上限定 1MB,滚到 .1、.2。论证轮转策略。parent_command_id 让重试链可见:哪条命令产出了下一条命令消费的输入。下一节,我们把反馈、范围、规则收口进验证门:任务结束前必跑的、失败即关闭的、签名留审计的最后一道闸,把「看起来不错」挡在 main 之外。