第 6 章 Agent 主循环
本章要解决的问题
Agent 的核心不是模型而是循环------思考、行动、观察。这个循环怎么设计才不会死循环、不浪费 token?
章节大纲
- 6.1 Agent Loop 架构:Think → Act → Observe
- 6.2 规划器 Planner 与执行器 Executor 的协作
- 6.3 停止条件、最大步数与防死循环
- 6.4 可观测性:日志、追踪(Langfuse/OpenTelemetry)
- 🛠 解决方案:Agent 卡死/循环/乱步骤的 6 大根因与修复
6.1 Agent Loop 架构:Think → Act → Observe
6.1.1 循环是 Agent 的心脏
第 1 章说过:Agent = LLM + 工具 + 循环。前两章讲了模型和工具,本章讲循环------把两者串起来的引擎。

图 1:Agent Loop 循环
markdown
┌──────────────────────────────────────────┐
│ │
▼ │
┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ Think │───►│ Act │───►│ Observe │───────┘
│ 思考 │ │ 行动 │ │ 观察 │
│ 决定下一步│ │ 调用工具 │ │ 读取结果 │
└─────────┘ └─────────┘ └─────────┘
│
▼ (判断:任务完成了吗?)
回答用户 / 继续循环
- Think(思考):模型看当前上下文,决定"直接回答"还是"调用某个工具"。
- Act(行动):执行工具调用(查询、计算、操作)。
- Observe(观察):把工具结果放回上下文。
- 再 Think:基于新信息继续决策,直到任务完成。
6.1.2 最小循环实现
python
# 示例代码:演示最小 Agent 循环,省略了 API 异常重试和详细日志
import json, time
from openai import OpenAI
client = OpenAI(base_url="https://api.deepseek.com", api_key="<你的Key>")
def execute_tool(call):
"""调用第 5 章的 safe_call_tool 执行工具,返回 JSON 字符串结果。"""
from ch05_tools import safe_call_tool # 第 5 章定义的工具执行函数
name = call.function.name
arguments = call.function.arguments
result = safe_call_tool(name, arguments)
return json.dumps(result, ensure_ascii=False)
def agent_loop(user_query, tools, max_steps=5, timeout=120):
messages = [{"role": "user", "content": user_query}]
start = time.time()
for step_no in range(max_steps): # 步数上限
if time.time() - start > timeout: # 超时上限
return "超时,返回当前结果"
# Think:模型决策(回答 or 调工具)
resp = client.chat.completions.create(
model="deepseek-chat",
messages=messages,
tools=tools,
tool_choice="auto",
temperature=0.2,
)
msg = resp.choices[0].message
messages.append(msg) # 保留模型决策(含 tool_calls)
# 判断:模型没有继续工具调用 → 认为任务完成或进入澄清/结束
if not msg.tool_calls:
return msg.content
# Act + Observe:执行每个工具调用并回填结果
for call in msg.tool_calls:
result = execute_tool(call) # 第5章的安全执行
messages.append({
"role": "tool",
"tool_call_id": call.id,
"content": result, # 观察结果
})
# 回到循环顶部 → 再 Think
return "已达最大步数,返回当前结果"
这是全书最核心的一段代码------第 10-18 章的 9 大设计模式,本质都是在"怎么组织这个循环"上做文章(提示链是固定的循环、规划是带计划的循环、反思是带检查的循环......)。
6.1.3 循环的两种推进风格
| 风格 | 特点 | 适用 |
|---|---|---|
| 串行单步(本章) | 一次一个决策,上下文累积 | 默认,最稳 |
| 并行多步 | 一次多个工具调用(parallel_tool_calls) | 子任务独立时(第 12 章) |
串行单步是基线,并行是优化------先跑通串行,再按需并行。
6.2 规划器 Planner 与执行器 Executor 的协作
6.2.1 两种循环形态:单循环 vs 两段式
| 形态 | 结构 | 特点 | 呼应 |
|---|---|---|---|
| 单循环(ReAct 式) | 思考+行动交错,无独立计划 | 灵活、探索性强 | 第 15 章 ReAct |
| 两段式(Plan-and-Execute) | 先 Planner 出计划,再 Executor 执行 | 有计划清单、不易迷路 | 第 15 章 PnE |

图 2:Planner-Executor 两段式
6.2.2 Planner 与 Executor 的分工
diff
Planner(规划器) Executor(执行器)
- 看目标 + 上下文 - 按计划逐项执行
- 生成任务清单(带依赖) - 每步调用工具
- 任务完成/偏离时重规划 - 汇报每步结果
python
def two_phase_agent(goal, max_steps=10):
# 阶段一:Planner 出计划
plan = planner.generate_plan(goal) # 第15章的 JSON 计划
results = []
# 阶段二:Executor 执行
for step in plan["steps"]:
result = executor.execute_step(step) # 单步 = 一次工具调用
results.append(result)
if not check_on_track(goal, plan, results):
plan = planner.replan(goal, plan, results) # 偏离 → 重规划
return synthesize(goal, results)
什么时候用哪种:任务路径可预知 → 两段式(省、稳);任务需要探索 → 单循环(灵活)。混合式(计划外带探索)是生产最优(第 15 章 15.3 详述)。
6.3 停止条件、最大步数与防死循环
6.3.1 循环必须回答"何时停"
没有停止条件的循环 = 无限烧钱的定时炸弹。 三个停止条件,组合使用:

图 3:循环三停止条件
| 停止条件 | 说明 | 优先级 |
|---|---|---|
| 任务完成 | 模型判断无需再调工具(tool_calls 为空),或进入澄清/结束状态 | 最高(正常退出) |
| 用户终止 | 用户显式取消/终止任务 | 高(外部干预) |
| 最大步数 | 步数上限(max_steps) | 兜底(防失控) |
| 超时 | 总时长上限 | 兜底(防慢拖) |
python
import time
def agent_loop(user_query, max_steps=5, timeout=120):
start = time.time()
for step_no in range(max_steps): # 步数上限
if time.time() - start > timeout: # 超时上限
return "超时,返回当前结果"
if user_cancelled(): # 用户终止(外部信号)
return "用户已取消任务"
msg = think(user_query) # 单步执行:思考/行动/观察
if not msg.tool_calls: # 任务完成或进入澄清
return msg.content
return "达最大步数" # 步数兜底
6.3.2 防死循环的三类场景与对策
| 死循环形态 | 表现 | 对策 |
|---|---|---|
| 重复调用同一工具 | 同一个调用反复出现 | 调用历史去重(相同签名不重复执行) |
| 无效往返 | 工具结果不变,模型仍继续 | "无进展检测":连续 N 步结果无变化 → 终止 |
| 左右摇摆 | 计划 A ↔ 计划 B 反复切换 | 重规划次数上限 + 历史计划去重 |

图 4:防死循环三对策
python
def no_progress_detector(history, threshold=3):
"""连续 N 步没有新信息 → 判定无进展"""
if len(history) < threshold:
return False
recent = [h["result"] for h in history[-threshold:]]
return len(set(recent)) == 1 # 结果完全相同 → 无进展
6.3.3 工具失败后的恢复策略
工具调用不是"调一次就成功"------网络超时、参数错误、服务不可用都可能发生。需要分层恢复:
| 失败次数 | 策略 | 说明 |
|---|---|---|
| 1 次 | 重试 | 指数退避(1s/2s/4s) |
| 2~3 次 | 降级 | 换备用工具或返回"暂时无法处理" |
| 连续 5 次 | 熔断 | 停用该工具,转人工或终止任务 |
python
tool_fail_counts = {} # 工具名 → 连续失败次数
def handle_tool_failure(tool_name, error):
tool_fail_counts[tool_name] = tool_fail_counts.get(tool_name, 0) + 1
if tool_fail_counts[tool_name] >= 5:
return f"工具 {tool_name} 连续失败已熔断,请转人工处理"
return f"工具 {tool_name} 执行失败:{error},将重试或降级"
6.3.4 成本护栏:循环的 token 预算
循环每步都调用模型,成本随步数线性增长。两层护栏:
- 步数硬上限(max_steps 5~20 按任务复杂度)。
- token 预算上限(累计超限强制终止,呼应第 24 章 G5 预算封顶)。
6.4 可观测性:日志、追踪
6.4.1 为什么循环必须可观测
Agent 循环是"多步、有状态、可能出错"的过程------没有观测,出了问题是黑盒,无法定位是哪一步出错(呼应第 21 章排错)。生产级 Agent 的标配:关键步骤都留痕。
6.4.2 每步留痕的数据结构
python
def log_step(step_no, phase, model_input, model_output, tool_result, cost):
return {
"step": step_no,
"phase": phase, # think / act / observe
"input_tokens": ..., "output_tokens": ...,
"cost_usd": ...,
"model": "deepseek-chat",
"latency_ms": ...,
"trace_id": current_trace_id,
}
6.4.3 追踪工具选型
| 工具 | 特点 | 适用 |
|---|---|---|
| Langfuse | LLM 专用追踪/评测/成本面板,开源可自部署 | 推荐首选 |
| LangSmith | LangChain 生态,功能全 | 已用 LangChain |
| OpenTelemetry | 通用可观测标准 | 与现有监控体系统一 |
| 自研日志 | 零依赖,但缺面板 | 快速起步 |
最小起步:先自研 JSON 日志(每步一条),跑起来后再接 Langfuse------日志先行,面板后补。
🛠 解决方案:Agent 卡死/循环/乱步骤的 6 大根因与修复
常见问题
- "Agent 卡死不动":工具超时未处理,循环阻塞。对策:单工具超时 + 超时回填错误信息(第 5 章)。
- "死循环烧钱":无停止条件/无进展检测。对策:三停止条件 + 无进展检测(6.3.2)。
- "乱步骤(先做后置步骤)":无计划约束,单循环自由发挥。对策:换两段式(Planner 先出计划,第 15 章)。
- "上下文越滚越长,最后爆窗":每步结果全量累积。对策:历史裁剪/摘要(第 3 章)+ 工具结果精简回填。
- "步数不够任务没做完":max_steps 设小。对策:按任务复杂度调大(第 24 章表四 3~20),但要先查根因------频繁"步数不够"说明任务拆解有问题。
- "线上出问题定位不到":无观测。对策:每步留痕 + 追踪(6.4),trace 到每次调用。
解决方案速查表
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 卡死不动 | 工具超时 | 超时分层 + 错误回填 |
| 死循环 | 无停止条件 | 三条件 + 无进展检测 |
| 乱步骤 | 无计划 | 两段式 Planner |
| 上下文爆 | 结果全量累积 | 裁剪 + 精简回填 |
| 步数不够 | 任务拆解差 | 查根因 + 调步数 |
| 定位不到 | 无观测 | 每步留痕 + 追踪 |
实战提示
- 先想清楚停止条件再写循环:任务完成判定、最大步数、超时,缺一不可。
- 无进展检测是省钱利器:连续几步结果没变化就停,避免无效往返。
- 每步留痕是标配:trace 到每次调用,出问题 5 分钟定位。
- 单循环先跑通,再优化:并行、两段式都是优化项,基线是串行单循环。
- 循环的每一步都要过第 5 章的安全执行:工具调用应始终走注册表 + 校验。