Agent 速成笔记 · 第 4 章 智能体经典范式构建
Agent 速成笔记 · 第 4 章 智能体经典范式构建
源:Datawhale《Hello-Agents》第 4 章 | 定位:工程实现 | 一句话:不靠框架、从零手写 ReAct、Plan-and-Solve、Reflection 三种范式,把「LLM 推理 + 外部工具」装配成一个能跑起来的智能体。
0. 一章速览(30 秒)
- 一句话:三大经典范式的差别,本质上是「思考与行动的组织方式」不同——ReAct 每一轮交替做一点,Plan-and-Solve 先做完整计划再逐条执行,Reflection 做完之后回头自我批判并改写。
- 本章解决什么问题:给定一个 LLM 客户端和若干工具,如何用最少的抽象把它们编排成一个能自主决策、能纠错、有终止条件的循环;以及三种编排方式各自解决什么、代价是什么、什么场景该选哪一个。
- 必须记住的 5 个点:
- 三种范式的骨架都是
LLM 客户端 + 工具注册表 + 提示词模板 + 主循环,差别只在主循环的形状。 - 提示词模板是机制的载体:格式约束写在提示词里,解析逻辑写在代码里,两者必须成对设计。
- 每个循环都必须有一个硬终止条件(
max_steps/max_iterations),否则会无限烧钱。 - 循环的上下文(
history)是智能体「记忆」的最朴素形态:ReAct 存 Action/Observation,Plan-and-Solve 存步骤结果,Reflection 存执行稿与反思稿。 - 选型看任务的性质:信息在外部且路径未知选 ReAct,路径确定但步骤多选 Plan-and-Solve,质量优先且允许慢选 Reflection;后两者可以叠加在 ReAct 之上。
- 三种范式的骨架都是
- 为什么要「重复造轮子」:框架替你挡下了输出格式解析、工具调用失败重试、防死循环这三件脏活;亲手处理它们,是从框架使用者变成创造者的分界线。
1. 公共基础设施:LLM 客户端与工具(4.1)
三种范式共享同一套底座。先把它建好,后面每一节就只剩「主循环怎么写」这一个问题。
1.1 HelloAgentsLLM:把模型调用收拢成一个方法
- 是什么:一个薄封装类,屏蔽模型服务的差异。构造时按「传入参数优先、否则读环境变量」的顺序取
LLM_MODEL_ID、LLM_API_KEY、LLM_BASE_URL、LLM_TIMEOUT(默认 60 秒),三者缺一就抛ValueError。 - 怎么运作:对外只暴露一个
think(messages, temperature=0)方法,内部用OpenAI(api_key, base_url, timeout)建客户端,调用chat.completions.create(..., stream=True),把流式 chunk 里的chunk.choices[0].delta.content逐段收集拼成完整字符串返回。注意它显式跳过了chunk.choices为空的 chunk,并捕获异常返回None——调用方必须判空。 - 为什么重要:只要服务兼容 OpenAI 接口,同一份代码就能在官方服务、第三方服务、本地部署之间切换,这是全书实战章节的公共依赖。
- 工程要点:默认
temperature=0,因为智能体需要确定性输出而非创造力——格式错一个字符,下游正则就解析不出来。配置统一走.env文件(load_dotenv()加载),避免密钥硬编码。
1.2 工具:三要素与工具执行器
工具(Tool)是智能体与外部世界交互的「手和脚」。 一个定义良好的工具必须包含三个要素:
- 名称(Name):简洁唯一的标识符,供智能体在
Action中调用,例如Search。 - 描述(Description):一段自然语言说明「这个工具是干什么的、什么时候该用它」。这是三要素中最关键的部分,因为 LLM 完全依赖这段文字来判断该选哪个工具——描述写得含糊,工具就等于不存在。
- 执行逻辑(Execution Logic):真正干活的函数。
当工具多于一个时,需要 ToolExecutor 做统一注册与调度,它内部就是一个 Dict[str, Dict[str, Any]],提供三个能力:
registerTool(name, description, func):注册,重名时警告并覆盖;getTool(name):按名取回执行函数;getAvailableTools():把所有工具格式化成- 名称: 描述的多行字符串,这就是要塞进提示词{tools}占位符的内容。
为什么重要:getAvailableTools() 把「工具清单」和「提示词模板」解耦——新增工具只需注册,提示词自动更新,智能体的能力就扩展了。这是最小可用的插件机制。
以本章的 search 工具为例,它还体现了一个值得学的细节:工具内部要做「智能解析」。SerpApi 返回的 JSON 字段很多,工具不把原始 JSON 丢给 LLM,而是按 answer_box_list → answer_box.answer → knowledge_graph.description → organic_results 前三条摘要的优先级挑最精确的答案返回——工具的职责是降低 LLM 的输入噪声。工具内部异常也要兜住,返回 "搜索时发生错误: {e}" 这类自然语言文本,而不是让异常冒泡中断整个循环。
1.3 分层骨架(本章代码的组织方式)
本章所有实现都遵循同一套五层结构,记住它比记住任何一段代码都有用:
| 层 | 职责 | 本章实现 |
|---|---|---|
| Client | 屏蔽模型服务差异 | HelloAgentsLLM.think() |
| Tool | 定义能力 + 注册调度 | search() + ToolExecutor |
| Prompt | 约束 LLM 输出结构 | *_PROMPT_TEMPLATE |
| Agent | 状态持有 + 主循环 | ReActAgent / PlanAndSolveAgent / ReflectionAgent |
| Loop | 循环边界与终止 | max_steps / max_iterations |
分层的收益在于:换模型只动 Client,加能力只动 Tool,改行为只动 Prompt,三种范式的差异被压缩到 Agent 层内部的那几十行循环里。
2. ReAct:思考-行动-观察循环(4.2)
2.1 是什么,为什么需要它
ReAct(Reason + Act)由 Shunyu Yao 等人在 2022 年提出,正式发表于 ICLR 2023。它把「推理(Reasoning)」与「行动(Acting)」显式地结合起来,形成「思考-行动-观察」的循环。
理解它价值的最好方式是看它要取代什么:ReAct 之前的方法分两类,一类是「纯思考」型,如思维链(Chain-of-Thought),推理很漂亮但无法与外部世界交互,容易产生事实幻觉;另一类是「纯行动」型,模型直接输出要执行的动作,但缺乏规划和纠错能力。ReAct 的判断是——思考与行动相辅相成:思考使行动有目的性,行动的观察结果又反过来修正思考。
它最适用的三类场景:需要外部知识的任务(实时天气、新闻、股价、专业领域检索)、需要精确计算的任务(交给计算器而不是让 LLM 心算)、需要与 API 交互的任务(操作数据库、调用业务服务)。
2.2 机制拆解:三种字段 + 一个循环
每轮交互有三个固定字段:
- Thought(思考):智能体的「内心独白」——分析当前情况、分解任务、制定下一步计划、或复盘上一步结果。不执行任何操作,纯文本。
- Action(行动):决定采取的具体动作,格式是
工具名[工具输入],例如Search['华为最新款手机'];结束信号是Finish[最终答案]。 - Observation(观察):执行 Action 后工具返回的结果(搜索摘要、API 返回值等),由代码产生而非 LLM 产生。
形式化地看,在每个时间步 \(t\),策略(也就是大语言模型 \(\pi\))依据原始问题 \(q\) 和之前所有「行动-观察」对 \((a_1,o_1),\dots,(a_{t-1},o_{t-1})\) 生成当前的思考与行动:
随后环境中的工具 \(T\) 执行该行动并返回观察结果:
新的 \((a_t,o_t)\) 对被追加进历史,循环继续,直到模型在 Thought 中判断任务完成。
一个极易被忽略的要点:LLM 每轮只能看到文本。它「记得」自己调用过什么工具,靠的不是任何内部状态,而是被拼回提示词的 history 字符串。上下文里累积的 Thought/Action/Observation 轨迹,就是记忆的最朴素形态。
2.3 提示词模板:机制的真正来源
ReAct 的稳定性几乎完全由提示词决定。模板承担四件事,缺一不可。
请注意,你是一个有能力调用外部工具的智能助手。
可用工具如下:
{tools}
请严格按照以下格式进行回应:
Thought: 你的思考过程,用于分析问题、拆解任务和规划下一步行动。
Action: 你决定采取的行动,必须是以下格式之一:
- `{{tool_name}}[{{tool_input}}]`:调用一个可用工具。
- `Finish[最终答案]`:当你认为已经获得最终答案时。
Question: {question}
History: {history}
这段在干什么:用一个模板同时完成角色设定(「有能力调用外部工具的智能助手」)、工具清单注入({tools} 由 getAvailableTools() 填)、输出格式规约(Thought/Action 两行,Action 只有两种合法形态)、动态上下文注入({question} 是用户问题,{history} 是累积的 Action/Observation 文本)。其中格式规约最重要——正因为格式被强制固定,代码才能用正则在几十行内可靠地把 LLM 的意图翻译成函数调用。
注意 {{tool_name}} 使用了双花括号,这是为了让 str.format() 输出单花括号字面量,属于典型的模板转义坑。
2.4 解析器:从纯文本里抠出结构化动作
LLM 返回的是纯文本,需要两级解析,都用正则实现。
def _parse_output(self, text: str):
"""第一级:切分 Thought 与 Action"""
thought_match = re.search(r"Thought:\s*(.*?)(?=\nAction:|$)", text, re.DOTALL)
action_match = re.search(r"Action:\s*(.*?)$", text, re.DOTALL)
thought = thought_match.group(1).strip() if thought_match else None
action = action_match.group(1).strip() if action_match else None
return thought, action
def _parse_action(self, action_text: str):
"""第二级:从 'Search[华为最新手机]' 拆出工具名与输入"""
match = re.match(r"(\w+)\[(.*)\]", action_text, re.DOTALL)
if match:
return match.group(1), match.group(2)
return None, None
这段在干什么:_parse_output 用「匹配到 Action: 之前」切出 Thought,用「匹配到文本末尾」切出 Action,两级都用了 re.DOTALL 让 . 能跨行;_parse_action 用 (\w+)\[(.*)\] 把 Search[华为最新手机] 拆成工具名 Search 和工具输入 华为最新手机。解析失败时返回 (None, None) 而不抛异常,由调用方决定怎么兜。
2.5 主循环:每轮五步与终止条件
ReActAgent 初始化时持有 llm_client、tool_executor、max_steps=5,以及一个 history 列表(每次 run() 开始都重置)。run(question) 的循环体固定为五步:
- 格式化提示词:取工具描述 +
"\n".join(self.history),填入模板; - 调用 LLM:
messages = [{"role": "user", "content": prompt}]——整个 ReAct 只用一条 user 消息,不靠多轮对话,全靠 prompt 自带历史; - 解析输出:
_parse_output得到thought, action;若解析不出 action,打印警告并break; - 执行动作:若
action.startswith("Finish"),用re.match(r"Finish\[(.*)\]", action)取出最终答案并return;否则_parse_action拆出工具名和输入,getTool取函数执行得到observation。工具不存在时不抛错,而是生成"错误:未找到名为 '{tool_name}' 的工具。"作为 observation; - 整合结果:把
Action: {action}和Observation: {observation}两条追加进self.history,进入下一轮。
终止条件有两个:模型主动输出 Finish[...],或步数达到 max_steps(示例值 5)后返回 None。max_steps 是防死循环的安全阀——没有它,一个反复搜索同一个词的智能体会一直烧 API。
一次真实运行记录展示了完整链路:第 1 步模型判断「这些信息在我的知识库之外」,调用 Search[华为最新手机型号及主要卖点] 拿到三条搜索结果;第 2 步根据结果直接输出 Finish[...] 给出答案——两步收敛。轨迹里「先意识到自己不知道,再动手查」正是 ReAct 相对纯思考范式的关键收益。
2.6 特点、局限与调试
三个主要优点:
- 高可解释性:
Thought链把每一步的心路历程摊开——为什么选这个工具、下一步打算做什么,全都可读。 - 动态规划与纠错能力:走一步看一步,上一步搜索结果不理想,下一步就改搜索词重试。这是一次性生成完整计划的范式做不到的。
- 工具协同能力:LLM 负责运筹帷幄(规划推理),工具负责解决具体问题(搜索计算),二者结合突破了单一 LLM 在知识时效性、计算准确性上的固有天花板。
四个固有局限(同时也是工程必须提前设计的点):
- 对 LLM 自身能力强依赖:推理、指令遵循、格式化输出三项能力任何一项不够,都会导致 Thought 规划错误或 Action 格式非法,流程直接中断。
- 执行效率问题:循序渐进的代价是多次串行调用 LLM,每轮都带网络延迟与成本,复杂任务总耗时和费用都会上去。
- 提示词脆弱性:整个机制的稳定运行建立在模板之上,模板中任何微小变动甚至用词差异都可能改变 LLM 行为;且并非所有模型都能持续稳定遵循格式。
- 可能陷入局部最优:步进式决策缺乏全局长远规划,可能因眼前的 Observation 选了看似正确、长远并非最优的路径,甚至原地打转。
五个调试动作,按排查顺序排列:打印每次调用前的完整提示词(追溯决策源头)→ 解析失败时打印 LLM 原始未处理文本(区分是模型没守格式还是自己的解析逻辑有误)→ 验证 tool_input 是否符合工具期望、observation 格式是否能被模型理解 → 在提示词里加一两个完整的 Thought-Action-Observation 成功示例(Few-shot Prompting)→ 换更强的模型或调 temperature(通常设 0 保证确定性)。
3. Plan-and-Solve:先规划后执行(4.3)
3.1 机制拆解:把「想」和「做」解耦
Plan-and-Solve Prompting 由 Lei Wang 等人在 2023 年提出(arXiv:2305.04091),动机是解决思维链在处理多步骤复杂问题时容易「偏离轨道」的问题。它与 ReAct 的根本区别在于:ReAct 把思考和行动融合在每一步,Plan-and-Solve 把整个流程解耦为两个阶段。
- 规划阶段(Planning Phase):接收完整问题后,第一件事不是解决它,而是把它分解成一个清晰、分步骤的行动计划。这个计划本身就是一次 LLM 调用的产物。
- 执行阶段(Solving Phase):拿到完整计划后,严格按照计划逐条执行。每一步都可能是独立的 LLM 调用,直到所有步骤完成,最后一步的结果就是最终答案。
形式化表达更清楚地显示了「解耦」:
规划模型只吃问题、产出 \(n\) 步计划 \(P=(p_1,\dots,p_n)\);执行模型每解一步,输入是「原始问题 + 完整计划 + 之前所有步骤的结果」这三样。注意 \(s_i\) 的输入里有 \(q\) 和 \(P\)——每一步都知道自己在整个计划中的位置和最终目标,这正是它比裸思维链更稳的原因。
适用场景的共同特征是结构性强、可被清晰分解:多步数学应用题、需整合多个信息源的报告撰写(先规划引言/数据源A/数据源B/总结的结构)、代码生成(先构思函数与模块结构)。
3.2 规划阶段:用格式约束换取解析稳定性
规划阶段的关键设计决策是:让 LLM 直接输出 Python 列表,而不是自然语言计划。提示词需明确角色、任务和输出格式范例。
你是一个顶级的AI规划专家。你的任务是将用户提出的复杂问题分解成一个由多个简单步骤组成的行动计划。
请确保计划中的每个步骤都是一个独立的、可执行的子任务,并且严格按照逻辑顺序排列。
你的输出必须是一个Python列表,其中每个元素都是一个描述子任务的字符串。
问题: {question}
请严格按照以下格式输出你的计划,```python与```作为前后缀是必要的:
```python
["步骤1", "步骤2", "步骤3", ...]
这段在干什么:用「顶级 AI 规划专家」做角色设定激发专业能力,用「每个步骤都必须独立可执行、严格按逻辑顺序」约束计划的粒度与次序,最后用 Python 列表 + 代码围栏的格式要求,把后续解析从「理解自然语言」降级为「读一个字面量」——这是 Plan-and-Solve 工程上最省事的一步。
```python
try:
plan_str = response_text.split("```python")[1].split("```")[0].strip()
plan = ast.literal_eval(plan_str)
return plan if isinstance(plan, list) else []
except (ValueError, SyntaxError, IndexError) as e:
print(f"解析计划时出错: {e}")
return []
这段在干什么:先按 python 和 两个标记切出围栏内的字符串,再用 ast.literal_eval 安全求值成 Python 列表(不用 eval,因为它只接受字面量、不执行任意代码)。异常被兜住并返回空列表,PlanAndSolveAgent.run 检测到空计划就终止任务,而不是继续跑一个畸形的流程。解析失败时打印原始响应同样是关键调试信息。
3.3 执行器与状态管理
执行器的提示词目标与规划器完全不同:不分解问题,而是在已有上下文的基础上专注解决当前一步。因此它必须注入四样东西:原始问题(确保始终了解最终目标)、完整计划(了解当前步骤的位置)、历史步骤与结果(当前步骤的直接输入)、当前步骤(明确现在要解决哪一个)。
Executor.execute(question, plan) 维护一个 history 字符串,循环遍历计划:
history = ""
for i, step in enumerate(plan):
prompt = EXECUTOR_PROMPT_TEMPLATE.format(
question=question, plan=plan,
history=history if history else "无",
current_step=step)
response_text = self.llm_client.think(messages=[{"role": "user", "content": prompt}]) or ""
history += f"步骤 {i+1}: {step}\n结果: {response_text}\n\n"
final_answer = response_text # 循环结束后,最后一步的响应就是最终答案
这段在干什么:这是整节最核心的部分——状态管理。每完成一步就把「步骤描述 + 结果」追加进 history,下一步的提示词里带上它。执行器提示词还要求「仅输出该步骤的最终答案,不要输出任何额外的解释」,所以累积的历史是一份干净的「已知量清单」,不会混入废话。水果店苹果题的运行日志正好验证了这条信息链:步骤 2 用到了步骤 1 的结果 15,步骤 3 用到步骤 2 的结果 30,最后一步得出 70。
外层 PlanAndSolveAgent 本身不含复杂逻辑,只做协调(Orchestrator):初始化 Planner 和 Executor,run() 里先 plan()、为空则终止,再 execute() 输出最终答案。这体现了「组合优于继承」——两个组件可以独立替换或测试。
3.4 优势与代价
优势是结构性与稳定性:全局计划一次成型,目标一致性高,不会在中间步骤迷失方向;计划是结构化列表,可被代码检查甚至人工审核;执行阶段的提示词比 ReAct 简单得多,对模型能力的要求更宽容。
代价是「计划错了全盘皆输」:本章实现中计划是静态的——一次性生成、不可修改,执行阶段没有任何中途纠偏机制。一旦某步切错或前提不成立,整条链只能继续错下去,而 ReAct 至少能靠 Observation 当场改主意。这正是工程上常需要额外「动态重规划」设计的原因。
4. Reflection:执行-反思-优化(4.4)
4.1 机制拆解:事后自我校正循环
前两个范式的共同点是:任务一旦完成,流程就结束。Reflection 的出发点是——无论行动轨迹还是最终答案,都可能存在谬误或待改进之处。它引入一个「事后(post-hoc)」的自我校正循环,让智能体像人一样审视自己的工作。
思想源自人类学习过程(写完初稿要校对、解完题要验算),在 Shinn Noah 等人 2023 年提出的 Reflexion 框架中得到系统体现(NeurIPS 2023)。核心是简洁的三步循环:
- 执行(Execution):用熟悉的方法(ReAct 或 Plan-and-Solve)完成任务,产出初稿;
- 反思(Reflection):用独立的、或带特殊提示词的 LLM 实例扮演「评审员」,从四个维度审查初稿——事实性错误(是否与常识/已知事实相悖)、逻辑漏洞(推理是否不连贯或矛盾)、效率问题(是否有更直接简洁的路径)、遗漏信息(是否忽略了关键约束或方面);产出一段结构化的反馈(Feedback),指出具体问题和改进建议;
- 优化(Refinement):把初稿和反馈一起作为新上下文再调 LLM,要求它据此修正,生成修订稿。
形式化地,设 \(O_i\) 为第 \(i\) 轮输出(\(O_0\) 是初始输出):
循环可重复多次,直到反思不再发现新问题,或达到迭代次数上限。
与前两种范式相比,Reflection 的三个独特价值:它提供了内部纠错回路——不再完全依赖外部工具的反馈(ReAct 的 Observation),因而能修正更高层次的逻辑与策略错误;它把一次性执行变成持续优化过程,显著提升复杂任务的成功率与答案质量;它天然构建了一份短期记忆——智能体不仅知道最终答案,还记得自己是如何从有缺陷的初稿迭代过来的。这份记忆还可以是多模态的,允许反思和修正文本以外的输出(代码、图像等),是后续多模态智能体的基础。
4.2 Memory:迭代的前提是记得住
反思需要上下文,但把全部历史直接塞进提示词会引入大量冗余。 因此本章设计了一个短期记忆模块,负责存储每一次「执行-反思」循环的完整轨迹:
records:一个列表,按顺序存{"type": ..., "content": ...},type只有'execution'或'reflection'两种;add_record(record_type, content):追加一条记录;get_trajectory():核心方法,把所有记录「序列化」成连贯文本,execution 记录渲染成--- 上一轮尝试 (代码) ---,reflection 渲染成--- 评审员反馈 ---,这一整段可直接插入后续提示词;get_last_execution():倒序查找最近一条 execution 记录,返回最新「初稿」供反思使用,没有则返回None。
这几十行代码就是智能体记忆模块的最小完整形态:写入(add_record)、按类型读取(get_last_execution)、序列化为提示词(get_trajectory)。往后第 8、9 章要解决的上下文管理、记忆检索问题,都是这个原型的复杂化。
4.3 三个角色的提示词
Reflection 是本章唯一需要多个角色提示词协同的范式,三份模板分别对应三个阶段。
- 初始执行提示词:设定「资深 Python 程序员」,要求代码包含完整函数签名、文档字符串、遵循 PEP 8,并直接输出代码不要解释。它只负责出稿,不负责质量。
- 反思提示词(机制的灵魂):设定「极其严格的代码评审专家和资深算法工程师,对代码的性能有极致的要求」,并把审查焦点收窄到算法效率一个维度——要求分析时间复杂度、思考是否存在算法上更优的方案、给出具体可行的改进建议(例如用筛法替代试除法),并且明确规定「如果代码在算法层面已经达到最优,才能回答无需改进」。
- 优化提示词:设定「资深 Python 程序员,正在根据评审专家的反馈优化代码」,输入是原始任务 + 上一轮代码 + 评审反馈,要求输出新版本,仍须满足函数签名/文档字符串/PEP 8。
这三份提示词最值得学的地方是两处设计:其一,把审查维度收窄并显式给出改进方向(筛法),避免「评审员」产出「代码可以更好」这类无法执行的空话;其二,把终止条件写进提示词本身——只有当评审员承认已达最优才会输出「无需改进」四个字,代码就靠这个词判断收敛。
4.4 主循环与终止条件
ReflectionAgent 初始化时持有 llm_client、Memory() 实例和 max_iterations=3。run(task) 的流程是:
- 初始执行:用
INITIAL_PROMPT_TEMPLATE调一次 LLM 拿到initial_code,add_record("execution", ...)写入记忆; - 进入迭代循环(
for i in range(max_iterations),每轮三步):- 反思:
get_last_execution()取最新代码,用REFLECT_PROMPT_TEMPLATE生成feedback,add_record("reflection", feedback); - 检查停止:
if "无需改进" in feedback:则 break——这是软终止条件; - 优化:把 task、上一轮代码、feedback 一起送进
REFINE_PROMPT_TEMPLATE,得到refined_code,add_record("execution", refined_code);
- 反思:
- 循环外
get_last_execution()取最终代码返回。
因此终止条件有两个,与 ReAct 形成对照:ReAct 是「模型输出 Finish 或步数用尽」,Reflection 是「反思文本里出现『无需改进』或迭代次数用尽」。共同点是必须有硬上限兜底,因为 LLM 完全可能一直认为「还能更好」。
4.5 案例:从 O(n√n) 到 O(n log log n)
任务设定是「编写一个 Python 函数,找出 1 到 n 之间所有的素数」。选它的理由是三个条件同时满足:存在明确的优化路径(初稿很可能是简单但低效的实现)、反思点清晰(时间复杂度高、重复计算)、优化方向明确。
实际轨迹完整展示了阶梯式提升:
- 初始执行生成素数筛选代码;
- 第 1 轮反思精准指出时间复杂度为 O(n·√n),瓶颈在于每个数都要试除,并建议改用埃拉托斯特尼筛法(Sieve of Eratosthenes),复杂度 O(n log(log n));优化阶段成功实现筛法;
- 第 2 轮反思肯定筛法的效率,还额外提及两个更高级的方向——分段筛法(Segmented Sieve)适用于 n 很大但内存有限的场景,奇数筛法(Odd Number Sieve)利用「除 2 以外素数都是奇数」把空间复杂度减半;但最终判定「对于大多数应用场景这些改进并非必需」,输出「无需改进」,触发终止。
这个案例说明的重点不是算法,而是机制:一次有效的「批判」是优化的前提,而批判的质量完全由反思提示词的角色设定和审查维度决定。智能体的价值不止于修错,更在于驱动方案在质量和效率上实现阶梯式提升。
4.6 成本收益分析:以成本换质量
三项主要成本:模型调用开销增加——每轮迭代至少额外两次 LLM 调用(一次反思、一次优化),多轮时成本成倍增长;任务延迟显著提高——Reflection 是严格串行过程,每轮优化必须等上一轮反思完成,不适合高实时性场景;提示工程复杂度上升——要为执行、反思、优化三个阶段分别设计和调试提示词。
两项核心收益:解决方案质量跃迁——把「合格」的初稿迭代成「优秀」的终稿,从功能正确到性能高效、从逻辑粗糙到逻辑严谨;鲁棒性与可靠性增强——内部自纠错循环能发现并修复逻辑漏洞、事实性错误、边界情况处理不当等问题。
结论:Reflection 是典型的「以成本换质量」策略,适合对质量、准确性、可靠性要求极高、对实时性要求宽松的场景——生成关键业务代码或技术报告、科学研究中的复杂逻辑推演、决策支持系统。反之,需要快速响应或「大致正确」即足够的场景,轻量的 ReAct 或 Plan-and-Solve 性价比更高。
另一种提升性价比的工程手段:分工使用不同模型——用更强的模型做反思,用更快的模型做执行。
5. 三范式对比与选型
5.1 机制对照
| 维度 | ReAct | Plan-and-Solve | Reflection |
|---|---|---|---|
| 思考与行动组织 | 每步交替、融合 | 两阶段解耦 | 执行后再审再改 |
| 循环形状 | 思考→行动→观察 | 规划→逐条执行 | 执行→反思→优化 |
| LLM 调用次数 | 每步 1 次,串行 | 1 次规划 + n 次执行 | 初始 1 次 + 每轮至少 2 次 |
| 上下文累积内容 | Action + Observation | 各步骤结果 | 历次执行稿 + 反思稿 |
| 终止条件 | Finish 或 max_steps |
计划执行完 | 「无需改进」或 max_iterations |
| 内生纠错能力 | 有(靠观察反馈) | 无(计划静态) | 有(靠自我批判) |
5.2 选型规则
- 环境多变、答案在外部世界、路径事先未知 → 选 ReAct。它的核心优势是环境适应性与动态纠错能力,是处理探索性、需要外部工具输入任务的首选。
- 任务结构清晰、纯内部推理密集、逻辑路径基本确定 → 选 Plan-and-Solve。核心优势是结构性与稳定性,避免多步推理中途跑偏。
- 质量、准确性、可靠性要求极高,且允许慢 → 选 Reflection。核心价值是显著提升解决方案质量。
- 组合使用:Reflection 不是替代品,而是可外挂在前两者之上的优化层(ReAct + Reflection、Plan-and-Solve + Reflection)。判断标准是:先用前者解决「怎么把任务做完」,再用 Reflection 解决「怎么把结果做好」。
6. 失败模式与工程处理
本章反复出现的三个坑,都是「让 LLM 输出结构化文本」必然带来的问题。手写一遍才能知道框架替你挡了什么。
| 失败模式 | 触发原因 | 工程处理 |
|---|---|---|
| 解析失败 | 模型没按格式输出;模板被改动 | 解析函数返回 None 不抛异常;打印原始输出定位;加 Few-shot 示例;temperature=0 |
| 死循环 / 原地打转 | 步进决策缺全局规划;反复调同一工具 | 硬终止 max_steps=5;提示词里明确「信息足够时必须用 Finish 输出」 |
| 幻觉动作 | 模型编造不存在的工具名或参数 | getTool 查不到时不中断,把「工具不存在」作为 observation 回灌,让模型自纠 |
| 计划错误 | 规划阶段切错步骤且无法修改 | 当前实现无解;需额外设计动态重规划机制 |
| 反思不收敛 | 评审员总能挑出新问题 | 提示词限定「已达最优才能回答无需改进」;硬终止 max_iterations=3 |
另外两个易被忽略的坑:工具数量膨胀——工具增到几十上百个时描述会挤爆上下文,且模型选择准确率下降,需要工程上做工具检索与分组;工具返回噪声——原始 API JSON 直接回灌会淹没关键信息,工具内应先做优先级解析再返回自然语言。
7. 高频考点 & 易错点速查
- ReAct 的完整形式化:\((th_t,a_t)=\pi(q,(a_1,o_1),\dots,(a_{t-1},o_{t-1}))\),\(o_t=T(a_t)\);\(o_t\) 由工具产生,\(th_t\) 和 \(a_t\) 由模型产生。
- 三范式在「思考与行动的组织方式」上的本质区别:ReAct 交替融合、Plan-and-Solve 阶段解耦、Reflection 事后迭代——这是本章所有对比题的答题主轴。
- 提示词与解析器必须成对设计:提示词里的格式约束(
Thought:/Action:/python 列表)和代码里的正则、ast.literal_eval是同一个协议的两端,改一端必须改另一端。 - 正则解析为何脆弱:模型输出不可控、微小措辞差异即可破坏匹配、无法覆盖多行与嵌套;兜底手段是 Few-shot、格式约束强化、换更强模型、
temperature=0。 ast.literal_eval而非eval:只安全求值字面量,不执行任意代码——这是解析 LLM 输出的标准做法。- 两类终止条件并存:软条件(模型自述完成:「
Finish」/「无需改进」)+ 硬上限(max_steps/max_iterations),缺软条件则流程永远跑满,缺硬上限则可能失控。 - 三次 LLM 调用是 Reflection 的单位成本:一轮迭代 = 反思 + 优化,成本与质量同向增长。
Memory是最小可用的短期记忆:写入、取最新、序列化进提示词三件事,是后续记忆专题的起点。- 易混点:Plan-and-Solve 的「执行阶段」不是一次调用,而是逐步调用、逐步累积历史;ReAct 的 history 里不存 Thought,只存 Action 和 Observation。
- 选型判断题的抓手:看信息在内部还是外部、路径是否确定、质量与延迟哪个优先;混合范式的标准答法是「以 ReAct 或 Plan-and-Solve 为骨架,外挂 Reflection 做质量提升」。
- 章末习题考点映射:① 三范式组织方式差异 / 智能家居选型 / 混合架构;② 正则解析的脆弱性与更鲁棒方案;③ 工具扩展、工具选择失败处理、工具数达 50~100 时的组织与检索;④ 静态计划与动态重规划、与 ReAct 的场景对比、分层规划;⑤ 双模型分工、更智能的终止条件、多维度反思;⑥ 提示词结构差异、角色设定的影响、Few-shot 效果;⑦ 客服智能体的范式组合、工具清单、决策与话术兼顾、风险应对。
8. 与前后章节的衔接
本章的输入来自第 3 章——那里交付的是「智能体的大脑」:Transformer 架构、与 LLM 交互的方法及其能力边界;本章给它装上「手和脚」(工具),产出三个可运行的最小智能体。三篇奠基论文 ReAct(Yao 2023 ICLR)、Plan-and-Solve(Wang 2023)、Reflexion(Shinn 2023 NeurIPS)是本章的理论锚点。输出分两路向后:一路是 ToolExecutor 与「工具描述决定工具选择」,指向工具与函数调用工程专题;另一路是 Memory 这个最小记忆模块与 Reflection 的迭代轨迹,指向记忆机制与上下文管理。下一节先转向低代码平台与轻代码构建智能体的方案,把本章的手写能力映射到工程化工具上。

浙公网安备 33010602011771号