对 ReAct 范式的理解
何为 ReAct 范式
ReAct 即 Reasoning and Acting,意为“推理 + 执行”。让大模型推理和执行,也就是边思考边执行的意思。换言之,ReAct 范式不允许大模型一次性把方案做好再执行,而是走一步看一步,一直到任务结束。这十分符合人类做事的思维,毕竟计划常常赶不上变化,不必一开始就把所有的计划好再做。
一句话概括 ReAct 范式ReAct 范式要求模型在给出最终答案前,必须输出思考过程(Thought),并在需要时通过调用外部工具(Action),来获取真实反馈(Observation),直到推导出结论。
具体示例
场景设定
假设我们给 Agent 提供了两个外部函数(工具):
-
Search(query):可以联网搜索信息并返回网页摘要。 -
Calculator(expression):可以计算数学表达式。
用户的提问: “苹果公司现任 CEO 的年龄乘以 2 是多少?”
ReAct Agent 的运行过程
Agent 接收到问题后,会在后台开启一个循环,生成如下的交互流:
第一轮循环:
-
Thought(思考): 我需要先找出苹果公司现任的 CEO 是谁,然后查出他的年龄,最后将年龄乘以 2。我的第一步应该是搜索苹果现任 CEO。
-
Action(行动):
Search("苹果现任 CEO") -
Observation(观察): 外部 Python 代码执行搜索后,将结果强行塞回给大模型:“蒂姆·库克 (Tim Cook) 自 2011 年起担任苹果公司首席执行官。”
第二轮循环:
-
Thought(思考): 好的,CEO 是蒂姆·库克。接下来我需要知道他的年龄。
-
Action(行动):
Search("蒂姆·库克 年龄") -
Observation(观察): 代码再次执行搜索并返回:“蒂姆·库克出生于 1960 年 11 月 1 日,现年 65 岁。”
第三轮循环:
-
Thought(思考): 我现在知道他 65 岁了。最后一步,我需要计算 65 乘以 2 的结果。
-
Action(行动):
Calculator("65 * 2") -
Observation(观察): 外部计算器代码执行完毕并返回:“130”
第四轮循环(终止点):
-
Thought(思考): 我已经拿到了最终的计算结果,现在可以回答用户了。
-
Final Answer(最终输出): 苹果公司现任 CEO 蒂姆·库克的年龄乘以 2 是 130。
工程师视角的本质
这其实就是一个文本解析与循环控制的过程:
-
构造一个极其详细的 System Prompt,告诉大模型:“你必须严格按照
Thought:、Action:、Observation:的格式输出。” -
模型开始生成文本,当后端代码通过正则匹配到它输出了
Action:时,强制阻断大模型的继续生成。 -
服务端代码根据 Action 去调用本地的函数(比如发请求查维基百科,或者执行一段 Python 的
eval())。 -
拿到结果后,把结果拼接在
Observation:后面,把这一大段聊天历史重新发给大模型,触发它的下一轮思考。 -
直到模型输出了标志着结束的字符串(比如
Final Answer:),整个while循环break,并将最终结果返回给前端界面。
存在的问题与解决方案
文本解析失败
由于 LLM 输出的内容并不一定严格遵守 Prompt 要求的格式,后端代码如果采用的是正则匹配的方式,就有可能匹配失败。
解决方案
-
原生 Function Calling: 主流大模型(如 OpenAI、DeepSeek)在 API 层面直接支持了
tools参数。你不需要在 Prompt 里写正则规则,而是通过传递 JSON Schema 来定义工具的输入输出结构。模型经过专门的微调,会原生输出符合该 JSON 结构的指令,彻底告别正则解析。JSON Schema 是何意味第一代( ReAct 纯文本流): 大模型没有任何特殊功能,只是个纯文本生成器。我们通过 Prompt 逼迫它输出
Action: Search这样的纯文本格式。然后我们用正则去匹配这些特定的英文字符串。这种方式很脆弱,因为大模型可能会输出Action:Search(中文冒号),正则直接白给。 第二代(原生 Function Calling): 它是 API 级别的新特性。当你向 OpenAI 或 DeepSeek 发请求时,除了传 Prompt,还会传一个定义了工具结构的 JSON Schema。此时,大模型在底层不再生成带有Thought/Action的纯文本流。当它决定调用工具时,API 会直接在特殊的返回字段(如tool_calls)里,塞给你一个排版极其标准的 JSON 对象(比如{"name": "Search", "arguments": "{\"query\": \"苹果现任 CEO\"}"})。这个 API 的底层如何实现的?第一步:API 层的“暗箱操作”(偷偷修改 Prompt) 当你的后端代码把
tools(一个描述工具的 JSON 数组)发给 OpenAI 的 API 网关时,大模型本身是看不到这个 JSON 对象的。对于大模型来说,它实际看到的输入其实是类似这样的纯文本: “你是一个助手。你可以使用以下工具: 工具1:Search,参数规范:{"type": "string", "description": "搜索词"} 如果你要调用工具,你必须输出<|tool_call_start|>,然后输出工具名,然后输出 JSON 格式的参数,最后输出<|tool_call_end|>。 用户的提问是:苹果现任 CEO 是谁?” 第二步:模型层的“特殊标记词”微调(Special Tokens) 为了让模型能精准地输出工具调用指令,而不是继续跟你闲聊,OpenAI 和 DeepSeek 在训练模型时,往词表里硬塞了几个“特殊标记词(Special Tokens)”。 模型经过海量的微调训练,形成了一种肌肉记忆:只要它在预测下一个 Token 时,发现自己需要查资料,它就会极高概率地预测出<|tool_call_start|>这个特殊的 Token,紧接着开始一个字一个字地预测 JSON 字符串。 第三步:API 网关的“偷梁换柱”(拦截与解析)- OpenAI 的服务器在以流式(Stream)接收大模型蹦出来的 Token。
- 当服务器一旦监听到大模型吐出了
<|tool_call_start|>这个特殊标记词时,它立刻拦截这段输出,不把它作为普通的文字(content)返回给你。 - 服务器会在内存里把大模型接着吐出来的 JSON 字符串积攒起来。
- 等到模型吐出
<|tool_call_end|>时,OpenAI 的服务器会执行类似json.loads(字符串)的操作。 - 最后,OpenAI 的服务器重新组装一个标准的 HTTP 响应对象,把解析好的数据塞进一个叫
tool_calls的字段里,再通过网络返回给你的代码。
-
结构化输出与校验(Structured Outputs): 在 Python 生态中,可以利用 Pydantic( FastAPI 底层用于数据校验的核心库)来定义 Agent 的输出数据模型。结合 Instructor 或 LangChain 的 Output Parsers,如果大模型输出的 JSON 缺斤少两,代码会直接捕获异常,并将报错信息(如“缺少参数 X”)自动塞回给大模型,强制它重试并修正错误。
结构化输出校验(Pydantic)与正则匹配的本质区别?正则是被动的字符串筛选。而像 Pydantic 结合结构化输出(Structured Outputs),是在大模型生成 Token 的过程中,直接在底层施加了语法约束(Constrained Decoding)。模型根本无法生成不符合 JSON 格式的 Token。
生成 Token 时如何做到“底层语法约束”?大模型的本质就是一个一个词往后预测。现在的“结构化输出(Structured Outputs)”用了一种更底层的黑科技:Logit 掩码(Logit Masking / Constrained Decoding)。 它是这样做到的:
- 假设大模型的词表里有 10 万个 Token。正常情况下,预测下一个词时,这 10 万个词都有一定的概率(Logit)。
- 当你传入了一个严格的 JSON Schema(比如要求输出一个数字类型的
age字段)时,推理引擎会充当一个“严格的门卫”。 - 在预测下一个 Token 这一微秒间,门卫会检查当前 JSON 的生成状态。如果现在光标正处于
{"age":后面,门卫会强行把所有非数字 Token(比如字母、表情包、标点)的概率全部修改为 0。 - 大模型“被迫”只能从 0-9 的数字 Token 中选择概率最高的那一个。 结论:它不是事后校验,而是在大模型每一次开口说话(选词)的瞬间,直接捂住它的嘴,不让它说出不符合 JSON 语法的词。这种技术把格式错误率从 10% 左右直接降到了接近 0。
死循环
如果工具调用一直得不到结果,Agent 可能会陷入无限的 尝试 -> 失败 -> 再尝试同样方法 循环,浪费 Token。因此必须设置 max_steps 最大尝试次数。
解决方案
-
状态机与图结构编排(LangGraph/Workflow): 抛弃简单的
while循环,采用有向无环图(DAG)来管理 Agent 的状态。例如设定“最多重试 3 次,如果依然失败,则强制跳转到 Fallback 节点(如返回默认话术)或交由人工介入(Human-in-the-loop)”。 -
反思与打分机制(Reflection & Critique): 引入一个额外的轻量级大模型作为“监督者(Critic)”。主 Agent 每走一步,监督者就对其当前行为和上下文进行评估打分。如果监督者发现主 Agent 的“观察(Observation)”与之前的步骤高度重复,就会触发熔断机制,强制主 Agent 更改策略。
为什么要引入额外的轻量级模型作为“监督者(Critic)”?大模型和人一样,存在自我确认偏误(Confirmation Bias)。主模型在经过漫长的推理后,往往会坚信自己当前的思路是对的,即使它已经陷入了死胡同,也很难自己推翻自己。 主模型的调用成本非常昂贵。如果让主模型同时负责执行和自我反思,不仅消耗大量 Token,速度也很慢。交给一个独立的、成本更低的 Critic 模型(仅用作逻辑校验的判别器),可以提供独立的“第二视角”,且系统架构上可以解耦。
上下文长度爆炸
在真实场景中,每次循环都会把前几轮的 Thought 和 Observation 塞回 Prompt 里,很容易会撑爆大模型的 Token 上限。不仅导致 API 调用成本直线上升,还会让大模型出现“遗忘”或“注意力涣散”。
解决方案
-
滑动窗口与记忆摘要(Memory Management): 不再将全部历史记录一股脑扔给模型。对于短期记忆,只保留最近 轮对话(滑动窗口);对于长期记忆,在后台启动一个低成本的小模型(不阻塞主模型响应用户的实时提问),定期将旧的对话历史总结成一段高密度的摘要,用摘要替换掉冗长的原始记录。
-
RAG(检索增强生成)与向量数据库: 面对动辄数万字的外部文档材料,绝对不能直接放入 Prompt。而是需要先将文档切分为小块(Chunk),利用 Embedding 模型将其转化为向量,存入向量数据库。当 Agent 需要信息时,先通过语义搜索匹配出最相关的 3-5 个文档块,再将这几个精准的“切片”喂给大模型。