本文同步更新于公众号:AI开发的后端厨师,本文完整代码开源github:https://github.com/windofbarcelona/all-agentic-architectures-golang/tree/main/03_react
本文同步更新于公众号:AI开发的后端厨师,本文完整代码开源github:https://github.com/windofbarcelona/all-agentic-architectures-golang/tree/main/03_react
本文同步更新于公众号:AI开发的后端厨师,本文完整代码开源github:https://github.com/windofbarcelona/all-agentic-architectures-golang/tree/main/03_react

当面对“iPhone制造商的现任CEO的母校是哪所?”这类需要串联多个信息节点的“多跳问答”时,传统的、试图一次性规划所有步骤的智能体架构往往显得笨拙且脆弱。ReAct架构的提出,正是为了解决此类动态、多步骤的复杂推理问题。它并非一个具体的框架,而是一种设计范式,其核心在于让智能体像人类一样,在“思考(Reason)”与“行动(Act)”之间动态交替,基于环境反馈实时调整策略。本文将从核心理念出发,结合代码级的工作流实现,深入剖析ReAct的优势、陷阱及其在实战中的应用。

一、核心定义:从静态规划到动态交互的范式转变

ReAct 代表了 Reasoning(推理)Acting(行动) 的交错循环。它让智能体摆脱了预先制定完整“计划”的束缚,转而采用一种试错式、增量式的问题解决策略。

  • 传统静态规划模式:智能体(或开发者)预先推断出完成任务所需的所有步骤(如:搜索A -> 解析结果得到B -> 计算C),然后按序执行。一旦中间步骤出错或环境变化,整个链条可能断裂。
  • ReAct动态交互模式:智能体只规划下一步。它基于当前所有已知信息(初始问题+历史行动与观察)进行“思考”,决定一个最有可能推进任务的“行动”,执行后“观察”结果,并将新信息纳入下一轮“思考”。这形成了一个 Think -> Act -> Observe 的自主循环。

其本质是将大模型的内部推理过程外显化、结构化,并将每次推理都与一次对外部世界(工具)的具体干预绑定,从而实现对复杂任务的探索性求解。

二、工作流与代码实战:拆解ReAct循环的每一个环节

一个最简化的ReAct智能体工作流可以用以下伪代码逻辑清晰表达。其核心循环体现了“思考-行动-观察”的紧密耦合。

2.1 高层工作流图示与解析

[复杂任务/目标输入]
        |
        v
[思考步骤 (Think)]
|-- 分析当前上下文(任务 + 历史)
|-- 决定下一步最佳行动(或判断任务完成)
|-- 生成结构化输出(思考文本 + 行动指令)
        |
        v
[行动步骤 (Act)]
|-- 解析思考步骤中的行动指令
|-- 调用对应的工具(Tool)并传入参数
        |
        v
[观察步骤 (Observe)]
|-- 获取工具返回的结果(或错误信息)
|-- 将结果格式化为文本观察
        |
        v
[循环判断]
|-- 观察是否表明任务已完成?
|-- 若未完成,将[思考+行动+观察]加入上下文,返回“思考步骤”
|-- 若完成,进入最终合成阶段
        |
        v
[最终响应合成]

2.2 代码级实现拆解

以下是基于Eino框架的React流程编排代码,完整代码参考文章顶部的github

func GetToolUseRunnable() (compose.Runnable[map[string]any, *schema.Message], error) {
	sg := compose.NewGraph[map[string]any, *schema.Message](compose.WithGenLocalState(func(ctx context.Context) *state {
		return &state{Messages: make([]*schema.Message, 0)}
	}))
	ctx := context.Background()
	model, err := GetModel()
	if err != nil {
		return nil, err
	}
	tools := GetBaiDuMapTool(ctx, []string{MapServer})
	toolNode, err := compose.NewToolNode(ctx, &compose.ToolsNodeConfig{
		Tools: tools,
	})
	if err != nil {
		return nil, err
	}
	toolsInfo, err := genToolInfos(ctx, tools)
	if err != nil {
		return nil, err
	}
	model, err = model.WithTools(toolsInfo)
	if err != nil {
		return nil, err
	}
	modelPreHandle := func(ctx context.Context, input []*schema.Message, state *state) ([]*schema.Message, error) {
		state.Messages = append(state.Messages, input...)

		return state.Messages, nil
	}
	toolsNodePreHandle := func(ctx context.Context, input *schema.Message, state *state) (*schema.Message, error) {
		if input == nil {
			return state.Messages[len(state.Messages)-1], nil // used for rerun interrupt resume
		}
		state.Messages = append(state.Messages, input)
		return input, nil
	}

	// toolsNodePostHandle := func(ctx context.Context, input *schema.Message, state *state) (*schema.Message, error) {
	// 	state.Messages = append(state.Messages, input)
	// 	return input, nil
	// }

	modelPostBranchCondition := func(ctx context.Context, sr *schema.StreamReader[*schema.Message]) (endNode string, err error) {
		if isToolCall, err := firstChunkStreamToolCallChecker(ctx, sr); err != nil {
			return "", err
		} else if isToolCall {
			return "ToolsNode", nil
		}
		return compose.END, nil
	}

	makeAnswerTemplate := DraftCodeTemplate()
	sg.AddChatTemplateNode("MakeAnswerTemplate", &makeAnswerTemplate, compose.WithNodeName("MakeAnswerTemplate"))
	sg.AddChatModelNode("MakeAnswerModel", model, compose.WithNodeName("MakeAnswerModel"), compose.WithStatePreHandler(modelPreHandle))
	sg.AddToolsNode("ToolsNode", toolNode, compose.WithNodeName("ToolsNode"), compose.WithStatePreHandler(toolsNodePreHandle))
	sg.AddChatModelNode("Synthesis", model, compose.WithNodeName("Synthesis"), compose.WithStatePreHandler(modelPreHandle))

	sg.AddEdge(compose.START, "MakeAnswerTemplate")
	sg.AddEdge("MakeAnswerTemplate", "MakeAnswerModel")
	//sg.AddEdge("MakeAnswerModel", "ToolsNode")
	if err = sg.AddBranch("MakeAnswerModel", compose.NewStreamGraphBranch(modelPostBranchCondition, map[string]bool{"ToolsNode": true, compose.END: true})); err != nil {
		return nil, err
	}
	sg.AddEdge("ToolsNode", "MakeAnswerModel")
	compileOpts := []compose.GraphCompileOption{compose.WithMaxRunSteps(20)}
	reflectionRunnable, err := sg.Compile(context.Background(), compileOpts...)
	return reflectionRunnable, err
}

关键实现说明:

  1. 提示词工程(插入点A):提示词必须清晰定义Thought/Action/Action Input/Final Answer的格式,并包含可用工具列表及其描述。这是引导模型进行规范化ReAct推理的关键。
  2. 输出解析(插入点B):必须稳健地解析LLM的回复,即使它没有严格遵循格式。通常使用正则表达式或启发式方法进行提取。
  3. 工具执行与安全(插入点C):在执行工具前,必须验证参数的类型和范围,防止注入攻击。工具返回的原始数据(如JSON、HTML)需要被处理成简洁的文本observation

三、应用场景、优势与核心挑战

3.1 典型应用场景

  • 多跳问答与复杂推理:如“现任特斯拉CEO在创立PayPal之前参与创立的公司,其最新股价是多少?” 此类问题需要顺序查找(CEO -> 早期创业公司 -> 股票代码 -> 股价),ReAct能动态管理这一链条。
  • 交互式环境导航:如操作图形用户界面(GUI)或命令行,每一步操作的结果(页面变化、命令输出)决定了下一步操作。ReAct非常适合这种状态依赖型任务
  • 需验证与纠错的研究任务:例如,让智能体研究一个主题。它可能先搜索一个概览,发现矛盾信息,然后决定搜索更权威的来源进行验证,整个过程是动态调整的。

3.2 核心优势

  1. 极强的环境适应性:ReAct智能体不依赖预先设定的完美路径,能够根据每一步的观察结果即时调整后续策略,适应动态或不确定的环境。
  2. 将推理过程透明化:外显的Thought使调试和解释智能体的决策过程成为可能。开发者可以直观看到它是如何“想”的,从而优化提示词或工具。
  3. 对复杂问题的分解能力:通过迭代,ReAct能自然地分解和攻克需要多个依赖步骤的复杂问题,而无需开发者手动拆分。

3.3 核心劣势与工程挑战

  1. 延迟与成本的线性增长:每个Think-Act循环通常对应1-2次LLM调用。对于一个需要5步才能解决的问题,其延迟和API成本是单次查询的5-10倍。
  2. 循环与发散风险:智能体可能陷入无意义的思考-行动循环(如反复搜索相同关键词),或在某个子问题上徘徊不前。必须设置最大步数(max_steps) 和设计有效的提示词来引导其“终结”任务。
  3. 对提示词和工具描述的极高依赖性:模糊的工具描述或鼓励冗余思考的提示词会直接导致系统低效甚至失败。提示词需要精心调试,以鼓励简洁、有效、目标导向的思考

四、总结与进阶讨论

核心结论:

  1. ReAct是解决动态、多步骤、状态依赖型复杂任务的强大范式,其核心价值在于将推理与行动循环耦合,实现了对环境反馈的实时适应
  2. 实现一个可用的ReAct智能体,工程重点在于构建鲁棒的循环控制逻辑、设计引导高效推理的提示词,以及确保工具调用的安全与稳定。它比简单的函数调用(Function Calling)架构更为灵活,但也复杂得多。
  3. ReAct的性能和成本直接受循环步数影响。在实际应用中,常需要结合任务规划器进行“宏观”步骤缩减,或在提示词中嵌入强约束,以控制成本。

开放讨论:

  1. 在您实现的ReAct智能体中,如何设计提示词以最有效地平衡“思考深度”与“行动效率”,避免模型陷入过度推理或无效行动?是否有特定的提示词模式(如Chain-of-Thought变体)被证明特别有效?
  2. 如何为ReAct智能体设计有效的“故障恢复”机制? 例如,当工具连续返回错误或观察结果与预期严重不符时,除了简单的重试,智能体应如何被引导至替代解决方案或安全地承认失败?
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐