Agent Loop 工程设计指南:从 ReAct 到 Claude Agent SDK,智能体核心循环完全拆解

“Loops 取代 Prompts” —— 这句话正在发生。
Anthropic Claude Code 负责人 Boris Cherny 在 2025 年 AI 工程师大会上说:“我不再直接给 Claude 写提示词了。我运行着一堆 Loop,让它们去决定什么时候调用 Claude、传什么参数、什么时候停。”
Docker 创始人 Solomon Hykes 说得更狠:“Agent 就是一个 LLM 在循环里搞破坏。”
同一个星期,两个不同背景的技术领袖,得出了同一个结论。过去我们纠结于怎么写 prompt 才能让模型一次输出更好的答案。现在的问题变了:你怎么设计一个控制系统,让模型自己跑完全程?
我会从 2022 年的 ReAct 论文拆起,然后拉出 Claude Agent SDK 和 OpenAI Agents SDK 的实际 Loop 实现来对比,最后聊上下文管理、成本控制、权限、Hook 这些你在生产环境一定会撞上的问题。
ReAct
2022 年 10 月,Google Research 和普林斯顿大学发表了 “ReAct: Synergizing Reasoning and Acting in Language Models”。核心思路简单到你会觉得”这不是显然的吗”:让 LLM 交替产出推理轨迹和行动指令,然后从环境里拿观察结果,形成一个闭环。
Thought → Action → Observation → Thought → Action → Observation → ...
实验数据很直白。HotpotQA(多跳问答)和 FEVER(事实验证)上,ReAct 的错误率比纯 Chain-of-Thought 低了近一半。原因也简单:模型终于可以在不确定的时候”查一下”了。
ReAct 解决了此前两种路线的各自短板:
| 方法 | 代表 | 优势 | 劣势 |
|---|---|---|---|
| 纯推理 | Chain-of-Thought | 逻辑链清晰 | 不能跟外部交互,容易幻觉 |
| 纯行动 | 直接调用工具 | 能拿外部信息 | 缺规划,动作瞎撞 |
| ReAct | Thought + Action | 推理引导行动,行动验证推理 | 实现更复杂 |
但论文和工程中间隔了一堆问题。论文证明”这种模式有效”,工程得回答:Loop 怎么启停、工具调用怎么编排、上下文炸了怎么压、出错了怎么恢复、怎么控制成本、怎么保证安全。
五阶段通用架构
把 ReAct 映射到工程实现,生产级 Agent Loop 包含五步:
- 接收输入:用户提示、System Prompt、工具定义、历史对话
- 状态评估:LLM 看当前状态,决定调工具还是直接返回文本
- 工具执行:跑 LLM 请求的一个或多个工具,收集结果
- 结果反馈:工具结果塞回对话历史,下一轮 LLM 能看到
- 循环或终止:重复 2-4,直到 LLM 返回纯文本——不含工具调用
有一个问题比上面五个步骤都重要:谁来决定循环停止?
ReAct 的设计是 LLM 自己判断。最自然——模型觉得”搞定了”就停。但阿里巴巴研究团队 2026 年的分析直接打了这个方案的脸:“LLM 的自我评估机制不可靠。它在主观感觉’差不多了’的时候退出,而不是客观上满足了验证标准的时候退出。”
所以 Ralph Loop、SGH(Structured Graph Harness)这些后来的范式,都不约而同地把”停止判断”从 LLM 手里拿走,交给外部验证。不过这个话题太大了,本文聚焦 SDK 层面的 Loop 实现。
Claude Agent SDK
Anthropic 在 2025 年底把 “Claude Code SDK” 改名叫 “Claude Agent SDK”,意味着从”代码辅助工具”正式变成”通用 Agent 框架”。它的 Loop 实现是目前最贴近 Claude Code 自身运行逻辑的版本。
Turn 机制
Claude Agent SDK 的核心概念是 Turn。一个 Turn = LLM 输出 → 工具执行 → 结果反馈。多个 Turn 串联就是整个执行过程:
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
import asyncio
async def run_agent():
async for message in query(
prompt="修复 auth 模块的测试失败",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Bash", "Glob", "Grep"],
max_turns=30,
effort="high",
),
):
if isinstance(message, ResultMessage):
if message.subtype == "success":
print(f"完成: {message.result}")
print(f"花费: ${message.total_cost_usd:.4f}")
else:
print(f"终止原因: {message.subtype}")
asyncio.run(run_agent())
典型的一次 bug 修复执行过程:
- Turn 1:Claude 调
Bash跑npm test,拿到 3 个失败用例 - Turn 2:Claude 调
Read读auth.ts和auth.test.ts - Turn 3:Claude 调
Edit修代码,再调Bash跑测试,全过了 - Final Turn:Claude 返回纯文本,Loop 结束
一个 Turn 内可以调多个工具。Turn 不是”一次调用一个工具”,而是”模型一次输出 → 这批工具全跑完 → 结果汇总反馈”。并行执行省下来的时间在复杂任务上非常可观。
五种消息类型
Loop 运行中 SDK 会 yield 出五种消息:
| 消息类型 | 触发时机 | 内容 |
|---|---|---|
| SystemMessage | 会话初始化、上下文压缩后 | 会话元数据、压缩边界标记 |
| AssistantMessage | 每次 Claude 响应后 | 文本内容 + 工具调用请求 |
| UserMessage | 工具执行完成后 | 工具返回的结果数据 |
| StreamEvent | 开启 partial messages 时 | 实时流式文本和工具输入 |
| ResultMessage | Loop 结束时 | 最终结果、token 用量、花费、session ID |
ResultMessage.subtype 直接告诉你任务怎么停的:"success" 正常完成,"error_max_turns" 打到上限,"error_max_budget_usd" 超出预算。应用层据此决定恢复、升级人工还是收工。
上下文窗口与自动压缩
Loop 跑起来后上下文膨胀得很快。System Prompt、工具定义、每轮的工具输入输出都在堆。复杂任务跑 20 个 Turn 很正常,上下文可能已经几十万 token 了。
Claude Agent SDK 的办法是自动压缩:上下文快满的时候,SDK 把旧消息压成摘要,保留最近的信息。压缩时触发 SystemMessage(subtype = compact_boundary)。
几个补充策略:
- Subagent 委托:每个 Subagent 独立上下文,完了只把结果摘要传回主 Agent。主 Agent 永远看不到 Subagent 内部的几十轮对话。
- 精简工具集:工具定义本身吃上下文。按需加载,别一股脑全挂上。
- 调 effort 级别:简单任务用
"low",省推理 token。
成本与权限
两个最实际的问题:这玩意儿烧了多少钱?它干了不该干的事没?
成本两层防护:
options = ClaudeAgentOptions(
max_turns=30, # 最大 Turn 数
max_budget_usd=0.50, # 最大花费
)
权限靠工具白名单 + 模式控制:
options = ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"], # 只读
disallowed_tools=["Bash", "Write", "Edit"], # 不准写
permission_mode="default", # 未列出的工具要审批
)
permission_mode 三个档:"default" 要人工审批,"acceptEdits" 自动放行文件编辑,"bypassPermissions" 全跳过(只在隔离环境用)。
Hook
Hooks 是 Loop 里的检查站,在应用进程里跑,不吃 LLM 上下文额度:
| Hook | 触发时机 | 典型用途 |
|---|---|---|
| PreToolUse | 工具执行前 | 校验参数、拦截危险命令 |
| PostToolUse | 工具执行后 | 审计输出、触发副作用 |
| UserPromptSubmit | 提交 Prompt 时 | 注入额外上下文 |
| Stop | Agent 完成时 | 验证结果、保存状态 |
| PreCompact | 上下文压缩前 | 归档完整对话记录 |
PreToolUse 可以直接短路整个 Loop:Hook 回调返回拒绝 → 工具不执行 → LLM 收到拒绝消息 → 换条路走。比输出以后再做过滤优雅得多。
OpenAI Agents SDK
OpenAI 走了一条不同的路。
Runner.run()
核心入口是 Runner.run()——一个封装好的隐式循环:
from agents import Agent, Runner
agent = Agent(
name="代码审查助手",
instructions="你是一个专业的代码审查员。阅读代码,指出潜在问题。",
tools=[read_file, search_code, lint_check]
)
result = Runner.run(agent, input="审查 src/auth.py 的安全性")
print(result.final_output)
内部循环逻辑和 Claude Agent SDK 本质一样:发 LLM → LLM 返回 → 执行工具 → 结果反馈 → 重复。区别在于它把循环封装在内部,开发者不碰中间消息,只拿最终结果。
Handoff
OpenAI Agents SDK 最特别的设计是 Handoff(交接)。当前 Agent 发现自己处理不了时,直接把控制权甩给另一个 Agent:
from agents import Agent, Runner
triage_agent = Agent(
name="分诊 Agent",
instructions="判断用户问题是技术问题还是账单问题,然后交给对应 Agent。",
handoffs=[tech_support_agent, billing_agent]
)
result = Runner.run(triage_agent, input="我的 API key 无法使用")
Handoff 是 Loop 层面的”跳转”:当前 Agent 的 Loop 暂停,新 Agent 接管同一个上下文继续跑。对比 Subagent 的”独立上下文 + 结果返回”,Handoff 传的是上下文本身,对话是连续的。
Guardrails
OpenAI 在 Loop 入口和出口加了 Guardrails:
from agents import Agent, InputGuardrail
async def check_input_safety(ctx, agent, input):
if contains_pii(input):
return GuardrailResult(tripwire_triggered=True, output="输入包含敏感信息")
return GuardrailResult(tripwire_triggered=False)
agent = Agent(
name="安全 Agent",
input_guardrails=[InputGuardrail(guardrail_function=check_input_safety)],
output_guardrails=[output_check]
)
Guardrails 在 Loop 外面跑,不占 LLM 上下文。和 Claude 的 Hook 思路不同:Guardrails 是”审批层”不是”拦截层”——它不参与 Loop 内部的决策,只在入口和出口把关。
两种 SDK 对比
| 维度 | Claude Agent SDK | OpenAI Agents SDK |
|---|---|---|
| 核心循环 | Turn-based,显式消息流 | Runner.run(),隐式循环 |
| 多 Agent | Subagent 委托,独立上下文 | Handoff 交接,上下文传递 |
| 工具管理 | 内建工具 + MCP + Custom Tool | 自定义 Tool + Function Calling |
| 安全控制 | Permission Mode + Hooks | Guardrails(Input/Output) |
| 上下文管理 | 自动压缩 + 手动压缩 | 靠外部管理 |
| 可观测性 | StreamEvent + Hooks 回调 | Tracing span |
| 执行控制 | max_turns + max_budget_usd | max_turns + handoff 链 |
| effort 调整 | 5 级(low → max) | 无对等机制 |
设计哲学差异很明显。Claude Agent SDK 把控制权交给你——你能监听每条消息、拦截每个工具调用、精确控制花费。OpenAI Agents SDK 追求简洁抽象——Runner.run() 搞定一切,Handoff 让多 Agent 编排更直观。
怎么选:
- 要精细控制、要看见中间过程、要在 Hook 层面拦截 → Claude Agent SDK
- 想快速搭多 Agent 协作、不想管底层循环细节 → OpenAI Agents SDK
几个绕不开的生产问题
成本
一个开放式任务(“改进这个代码库”)跑几十个 Turn 不稀奇。没预算上限的 Loop 就是烧钱机器。
max_budget_usd 和 max_turns 两个都得设,不是选一个。触发限制后你得决策:加钱继续、升人工、还是收工。
过早退出
我在前面说过,LLM 自己的”我觉得完成了”信不得。你得拿外部信号来判断:测试通过没?文件生成了没?API 返回对不对?
如果修测试的 Loop 在测试全通之前就停了——这不是 bug,是设计事故。
上下文静默退化
跑了 20 个 Turn 以后,模型可能开始忘掉最初的目标。对话历史里什么都有,但模型注意力散了,优先级乱了。
做法很简单:不要把上下文当记忆用。把决策日志、进展、发现写到文件系统里。每轮迭代从文件系统读当前状态,而不是翻看 50 页历史对话。
错误恢复
Loop 里的工具调用总会失败——超时、限流、文件不存在。Claude Agent SDK 的处理方式我很认可:直接把错误作为 UserMessage 塞给 LLM,让它自己决定重试还是换招。
我的经验是,LLM 通常能读懂错误信息并做出合理的调整。不需要给每种错误写专门的处理逻辑。把错误丢给模型看,它知道怎么办。
单 Agent 到多 Agent
单 Agent Loop 够应付大多数任务。但涉及多领域专业知识的复杂任务,或者能拆成并行子任务时,多 Agent 更高效。
Subagent(Claude 路线)
每个 Subagent 独立上下文窗口。跑完了只把结果摘要传回主 Agent。
什么时候用:任务能并行拆解、需要控制主 Agent 上下文膨胀。
Handoff(OpenAI 路线)
Agent A 的 Loop 暂停,Agent B 接管同一个上下文继续。
什么时候用:任务要顺序流水线处理、需要保持对话连贯。
选哪个
- 能并行拆 → Subagent
- 要顺序流 → Handoff
- 怕上下文爆炸 → Subagent
- 要对话连续性 → Handoff
大部分生产系统最后会混合用。但有一个原则别忘:单 Agent 能搞定的就别上多 Agent。Anthropic 在 Building Effective Agents 里说得够直白了——多数场景下,简单的 workflow 比复杂的 agentic loop 更稳。Loop 真正有价值的场景是那些没法预先穷举所有执行路径的任务。
workflow 是”我告诉你怎么做”,Agent Loop 是”我告诉你做什么,你自己想怎么干”。
你可以贴在显示器旁边的清单
- 能用确定性 workflow 就别用 Loop。Loop 是兜底,不是默认。
- 不要让 LLM 自己决定停。用测试、文件检查、API 校验定义”完成”。
max_turns和max_budget_usd两个都设。- 文件系统比上下文窗口更适合当记忆。文件不漂、不胀、换模型不会丢。
- 从只读报告模式开始。跑一周看 Agent 会做什么决定。95% 靠谱了再加写入权限。
- 每个写操作和网络调用都得过审批层,Hook 或 Guardrails 必须配。
- 每次 Turn 日志都记:输入、动作、结果、决策。调多轮状态机比调一次 prompt 难一个数量级。
Loop 的价值不在让模型变强,在让你不用盯着每一步。你告诉系统做什么,它自己琢磨怎么做。前提是你设计了一套能跑、能验、能停、能恢复的反馈闭环。
本文参考了 Anthropic Claude Agent SDK 官方文档、OpenAI Agents SDK 文档、ReAct 论文(Yao et al., 2022)、Anthropic “Building Effective Agents” 指南,以及掘金、博客园、CSDN、knightli.com 等社区的实战分析。