ReAct框架把大语言模型的推理能力和外部工具调用绑在同一个循环里,让模型先产出思考轨迹再决定动作,随后把执行结果喂回上下文继续下一轮。这种结构解决了纯推理不可执行、纯执行无规划的断层问题。理解它的内部实现,重点不在模型本身,而在循环调度与文本解析这两层胶水代码。

循环主体的状态机设计
在ReAct的开源实现中,最外层通常是一个带有最大步数限制的while循环。框架维护一份scratchpad列表,用来累积每一轮模型生成的Thought、Action以及环境返回的Observation。循环开始时会检查步数计数器,如果超过阈值就强制退出并抛出超时错误,避免模型在错误路径上无限调用工具。
每一轮迭代首先把scratchpad拼接到系统提示后面,形成完整的few-shot加动态上下文,然后调用LLM接口。拿到回复文本后,先用正则或特定的分隔符切分出Thought和Action字段。若解析失败,框架一般会构造一条Observation提示模型“格式错误,请按模板输出”,并把这条提示写回scratchpad,相当于给模型一次自我纠正的机会。
当成功解析出Action且对应的工具存在时,循环进入执行分支:把参数传给工具函数,捕获返回值,格式化成Observation文本。这一步之后并不立即清空中转区,而是把Action和Observation依次追加,使下一轮模型能看到完整决策链路。这样的状态机设计把“思考-行动-观察”三段式固化为可预测的流转,也方便在中间插入日志或人类确认节点。
提示模板与输出解析的实现细节
ReAct源码里最容易被忽略的是提示模板的构造方式。模板通常包含任务描述、工具列表以及若干示范对话,示范中严格遵循“Thought: …nAction: …nObservation: …”的顺序。代码层面,这些示范被放在一个常量字符串里,运行时通过字符串替换把用户问题填到对应槽位,再拼接scratchpad形成最终请求体。
输出解析器往往用一个简单的状态机或正则表达式来提取内容。例如用Action:s*(w+)s*[(.*)]来抓取工具名与参数。由于大模型偶尔会输出多余换行或解释,健壮的实现会先做截断,只取第一个Action之前的内容作为Thought,再单独解析动作。如果模型给出了Final Answer,解析器就中止循环,直接返回答案文本,不再调用工具。
这种解析逻辑对格式非常敏感,因此在源码中常能看到针对常见错误的后处理:比如把中文冒号替换成英文冒号,或者把单引号参数包成标准JSON。理解了这部分,我们在定制自己的智能体时,就能通过调整模板和解析正则,支持并行工具调用或结构化返回,而不必受限于原始ReAct的串行文本协议。
工具调度与错误隔离机制
工具调度模块在ReAct中通常以字典或注册表形式存在,key是工具名,value是可调用对象。循环体拿到Action后,会先查表确认工具存在,不存在就生成“未知工具”的Observation。存在时则使用try-except包裹调用,任何异常都被转成字符串Observation返回给模型,而不是让整个进程崩溃,这保证了Agent在脏数据或网络抖动下仍能继续推理。
下面是一个简化的Python片段,展示调度与错误隔离的核心写法:
tools = {
'search': lambda q: do_search(q),
'calc': lambda expr: eval(expr)
}
def run_action(name, arg):
if name not in tools:
return 'Observation: 工具 ' + name + ' 不存在'
try:
result = tools[name](arg)
return 'Observation: ' + str(result)
except Exception as e:
return 'Observation: 执行出错,' + str(e)
# 在循环中调用
resp = run_action('calc', '1/0')
print(resp)
从这段源码风格可以看出,ReAct把不确定性完全推给模型处理:工具层只负责执行和报错,模型在下一次Thought中自行判断是否需要换工具或修正参数。这种松散耦合让框架容易扩展新能力,只要往注册表加函数即可,不需要改动循环主体。但也要求提示模板里写清每个工具的输入输出,否则模型容易误用。
综合来看,ReAct的Reason加Act循环并不神秘,本质是用文本协议驱动的状态机加工具注册表。掌握其源码里的循环边界、解析容错与调度隔离三点,我们就能在已有库上做二次开发,比如加入向量检索记忆、限制单工具频次或实现多轮人类介入,而不必重复造轮子。
ReActagent_loopLLM_reasoning修改时间:2026-08-18 05:10:25