运行时反馈循环


文档摘要

运行时反馈循环 本节摘要:看不到真实命令输出的 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 节(初始化脚本)。

学习目标

阅读完本节,你应当能够:

  1. 区分运行时反馈可观测性遥测
  2. 构建一个包 shell 命令、持久化结构化记录的反馈运行器
  3. 确定性地截断大输出,让循环待在 token 预算内。
  4. 反馈缺失时拒绝推进循环
  5. 说出三条让运行器能交付的生产模式。

一、问题与直觉

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 要看的部分(最终错误、最终摘要)在尾部。

反馈 vs 遥测

遥测(第 23 节 OTel GenAI 约定)是给人操作员跨时间审运行的。反馈是本次运行下一轮。它们共享字段,但活在不同文件不同保留里。

无反馈即拒绝推进

若运行器在捕获退出码前出错,记录带 exit_code: nullerror: <原因>。Agent 循环必须拒绝在 null 退出上声称成功。无退出,无进展

二、从零实现

原课程 code/main.py 实现:

  • run_with_feedback(command, agent_note),包 subprocess.run,捕获 stdout/stderr/退出/时长,确定性截断,追加到 feedback_record.jsonl
  • 一个小加载器,把 JSONL 流进 Python 列表。
  • 一个跑三条命令(成功、失败、慢)的演示,打印每命令的最后一条记录。

Step 1:确定性截断

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)

Step 2:反馈运行器

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

Step 3:循环读记录 + 拒绝 null 退出

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 每轮读的加载器。

五、练习

  1. (Easy) 每记录加 cwd 字段,让同命令在不同目录跑可区分。
  2. (Medium) 加脱敏步骤,剥匹配 ^Bearer password= 的行。在 fixture 记录上测。
  3. (Medium)feedback_record.jsonl 上限定 1MB,滚到 .1.2。论证轮转策略。
  4. (Hard)parent_command_id 让重试链可见:哪条命令产出了下一条命令消费的输入。
  5. (Hard) 把 JSONL 管进一个迷你 TUI,高亮最新非零退出。TUI 要有用需展示八个关键特性。

本节要点回顾

  1. 看不到真实输出的 Agent 在猜:反馈运行器把 stdout/stderr/退出/时长捕获进结构化记录,Agent 对事实反应非对预测。
  2. 七字段:command(精确 argv)、stdout_tail、stderr_tail、exit_code、duration_ms、started_at、agent_note。
  3. 截断确定性:50MB 毁循环;首尾截断带「截断 N 行」标记,无采样,要看的在尾部。
  4. 反馈 vs 遥测:遥测(第 23 节)给人跨时间审,反馈给本次运行下一轮;共享字段但不同文件不同保留。
  5. 无反馈即拒绝推进:exit_code 为 null 即拒绝声称成功——无退出无进展。
  6. 成功幻觉/级联的直接解药:真实退出码进循环,Agent 无法自欺;第 06 节「400 变观察字符串」在工作台层的落地。
  7. 写时脱敏非读时:Bearer/password/AKIA/slack token,盘上文件是攻击者够向的;季度审计脱敏模式。
  8. 轮转策略:1MB 上限、.1/.2 滚动、.5 丢弃;只读当前文件,运行时代价有界。
  9. 父命令 id 串重试链:command_id + parent_command_id;否则重试像独立成功、审计藏失败史。
  10. 薄包装挺过框架迁移:因为它拥有记录的形状;Claude Code Bash/LangGraph 节点/CI 日志都能用。

下一节,我们把反馈、范围、规则收口进验证门:任务结束前必跑的、失败即关闭的、签名留审计的最后一道闸,把「看起来不错」挡在 main 之外。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U