Agent来了0x03:ReAct 范式

0 阅读7分钟

前言

前面讲了 Agent 相关的核心概念和工作架构。那么有哪些核心的设计让 Agent 能顺利且准确地完成任务呢?这次就学几个关键的核心设计来加深对 Agent 的理解。

定义

ReAct(Reasoning and Acting) 是当前AI智能体(Agent)领域的一个核心范式。

  • 本质:“思考-行动-观察”主循环
  • 目标:解决传统大模型在复杂、多步任务中容易产生的“幻觉”、逻辑错误和无法与外界交互的问题。
  • 核心流程:
    1. 推理:智能体根据当前任务和目标,分析现状,规划下一步应该做什么(例如:“要回答这个问题,我需要先查询今天的天气”)。

    2. 行动:智能体执行上一步规划的动作,通常是调用一个外部工具或API(例如:调用天气查询API)。

    3. 观察:智能体接收行动执行后的结果(例如:API返回“北京,晴,25℃”)。

    4. 循环:基于新的观察结果,再次进入“推理”步骤,直至任务完成。

image.png

Coding

其实,在 LLM 工程中我一直信奉 "万物皆靠Prompt"的真理。所以 ReAct 的第一步便是 Prompt的编写。我们打开 Langsmith-Hub 搜索 ”ReAct“,其中的 ”llm-react/react“就是一个符合 ReAct 范式的 Prompt 模板。

image.png

我们不难发现,Prompt 将问题的求解分成了几个步骤:

  1. Question:原始问题

  2. Thought:首次思考需要做什么?

  3. Action:根据思考采取一定的行动(Function Calling)

    • Action Input:行动输入
    • Action output:行动输出
    • Observation:行动结果
  4. Thought:继续思考 Observation 是否足以回答问题。如果不足:

    • Thought -> Action 持续循环
    • 这个思考/行动可以重复N次,直到足以回答问题
  5. Thought:终于可以得到答案了

  6. Final Answer:最终答案

image.png

Prompt

那我们根据以上模板写一个中文模板(当然也可直接使用 hub 加载在线 Prompt)。

template = ('''
    '尽你所能回答以下问题。如果能力不够你可以使用以下工具:'
    '{tools}
    使用以下格式:'
    'Question: 你必须回答的输入问题'
    'Thought: 你应该始终思考该做什么'
    'Action: 要采取的行动,应该是 [{tool_names}]之一'
    'Action Input: 行动的输入'
    'Observation: 行动的结果'
    '... (这个想法/行动/行动输入/观察可以重复N次)'
    'Thought: 我现在知道最终答案了'
    'Final Answer: 原始输入问题的最终答案'
    '开始!'
    'Question: {input}'
    'Thought: {agent_scratchpad}'
    '''
)
prompt = PromptTemplate.from_template(template)

Agent

# 初始化Agent
agent = create_react_agent(llm, tools, prompt)
# 构建AgentExecutor
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    handle_parsing_errors=True,
    verbose=True)

# 执行AgentExecutor
agent_executor.invoke({"input": """在中国,目前市场上玫瑰花的一般进货价格是多少?大概是多少钱一支?\n如果我在此基础上加价5%,应该如何定价?"""})

TavilySearch

TavilySearch 是一个专为L LM 和 Agent 设计的“机器友好”型搜索引擎API

  • 目标:直接为AI模型输送干净、结构化、可立即消费的信息流
  • 与传统搜索引擎不同:传统搜索返回给人看的 HTML 页面和链接列表
  • 核心流程:
    1. 查询意图分析:接收自然语言查询,可自动推断最佳搜索参数(如时间范围、主题领域)。

    2. 多源并行检索:同时向 20+ 权威数据源(包括新闻、学术、金融等)发起请求,确保信息覆盖的广度和质量。

    3. 内容提取与清洗:这是其关键价值所在。它会自动剥广告、导航栏、脚本等噪音,提取出最纯净的正文,直接省去了开发者自己写爬虫和解析HTML的繁琐工作。

    4. 语义相关性打分与排序:不仅基于关键词匹配,更利用语义理解技术对结果进行重排序,将最相关、质量最高的片段排在前面。

    5. 结构化输出:返回JSON格式

      • title
      • url
      • content(已清洗的文本)
      • score(相关性分数)
      • include_answer=true:true 可直接获得由AI生成的答案摘要
      • ...

image.png

TavilySearch 的使用需要在 官网 申请 API_KEY,并在本地配置:

  1. 方法1:.env文件配置(推荐)
    • TAVILY_API_KEY = "tvly-xxx"
  2. 方法2:bash文件配置
    • export TAVILY_API_KEY="tvly-xxx"
    • 配置好后命令行执行 source ~/.zshrc 生效

示例代码:

from dotenv import load_dotenv
from langchain_tavily import TavilySearch

# 用于加载本地环境配置,因为 TavilySearch 的使用需要在官方申请 API_KEY
load_dotenv()

search = TavilySearchResults(max_results=3)

Running

显然,Agent 按照上面说的流程逐个解决最终解决了这个问题。 image.png

image.png

思考🤔

我们发现 create_react_agent 依赖 langchain_classic 模块,但是我们也知道 Langchain 1.0 范式并不鼓励继续使用 langchain_classic 兼容包。在新的范式下发现一个create_agent接口,但是这个接口好像并不能旧接口一样很好体现 ReAct 流程。那么问题来了?

  1. 通常不是说 0.x 的设计是黑盒的,更集中化的;1.0 新范式是更开放,更透明的。但是按 create_agent 对 ReAct 的体现,好像反而更黑盒了,这个怎么解释?
  2. 1.0 新范式下怎么实现 类似 create_react_agent 功能呢?因为我需要很好地体现 ReAct 流程。

范式升级

  • create_react_agent:高级封装,虽然从 Log 日志上看特别能体现 ReAct 流程。但是具体操作实际还是黑盒的。一旦想微调内部某些逻辑,就非常困难,需要去继承和重写。
    • 0.x 时代的设计哲学:“框架即解决方案”,拿来即用
  • create_agent:初始组件。将模式的定义权从框架移交给了开发者,它不再提供“成品”,而是提供了更精密的“零件”和“组装说明书”。
    • 1.0 时代的设计哲学:“框架即组件”,提倡 “显式优于隐式” 。所有上层执行过程透明。

新范式下 ReAct 代码

你想要 ReAct ?那就遵循 ”显式优于隐式“原则,显式地组合出 ReAct 所需的组件。

Coding

class CustomReActAgent:
    """纯 Runnable 实现的 ReAct Agent"""
    
    def __init__(self, llm: Runnable, tools: List[BaseTool], max_iterations: int = 5):
        self.llm = llm
        self.tools = {tool.name: tool for tool in tools}
        self.max_iterations = max_iterations
        self.prompt_template = build_react_prompt(tools)
    
    def invoke(self, input_text: str, config: Optional[RunnableConfig] = None) -> Dict[str, Any]:
        """执行 ReAct 循环"""
        history = []  # 存储对话历史
        iterations = 0
        
        print(f"🤔 问题: {input_text}")
        print("-" * 50)
        
        while iterations < self.max_iterations:
            iterations += 1
            
            # 构建提示
            prompt = self.prompt_template.format_messages(
                input=input_text,
                history=history
            )
            
            # 调用 LLM
            response: ChatResult = self.llm.invoke(prompt, config=config)
            llm_output = response.content
            
            print(f"🔁 第 {iterations} 轮思考:")
            print(f"   LLM 原始输出:\n   {llm_output}")
            
            # 解析响应
            parsed = parse_llm_response(llm_output)
            
            if parsed["thought"]:
                print(f"   思考: {parsed['thought']}")
            
            # 检查是否是最终答案
            if parsed["final_answer"]:
                print(f"✅ 最终答案: {parsed['final_answer']}")
                return {
                    "output": parsed["final_answer"],
                    "iterations": iterations,
                    "history": history
                }
            
            # 执行工具调用
            if parsed["action"] and parsed["action"] in self.tools:
                tool = self.tools[parsed["action"]]
                print(f"   行动: 调用工具 '{parsed['action']}',输入: {parsed['action_input']}")
                
                try:
                    # 执行工具
                    tool_result = tool.invoke(parsed["action_input"])
                    print(f"   观察: {tool_result}")
                    
                    # 更新历史
                    history.extend([
                        AIMessage(content=llm_output),
                        ToolMessage(
                            content=str(tool_result),
                            tool_call_id=f"call_{iterations}"
                        )
                    ])
                    
                except Exception as e:
                    error_msg = f"工具调用失败: {str(e)}"
                    print(f"   ❌ {error_msg}")
                    history.extend([
                        AIMessage(content=llm_output),
                        ToolMessage(content=error_msg, tool_call_id=f"call_{iterations}")
                    ])
            else:
                error_msg = f"无效行动: {parsed.get('action')}"
                print(f"   ⚠️  {error_msg}")
                history.append(AIMessage(content=llm_output))
                history.append(ToolMessage(
                    content=f"错误: 无法识别行动 '{parsed.get('action')}',可用行动: {list(self.tools.keys())}",
                    tool_call_id=f"call_{iterations}"
                ))
            
            print("-" * 30)
        
        # 达到最大迭代次数
        return {
            "output": f"达到最大迭代次数({self.max_iterations})仍未解决问题",
            "iterations": iterations,
            "history": history
        }

创建使用:

# 创建 ReAct Agent
    agent = CustomReActAgent(llm=llm, tools=tools, max_iterations=3)

分析

这个实现是不充分实现体现了 LangChain 1.0 的核心设计思想?

  1. 完全透明:每一步都清晰可见

    • 提示词构造:build_react_prompt函数
    • 响应解析:parse_llm_response函数
    • 循环逻辑:ReActAgent.invoke方法中的 while 循环
    • 状态管理:history列表跟踪所有消息
  2. 可组合性:每个组件都是独立的

    • LLM 可以替换为任何 Runnable
    • 工具只需实现 BaseTool接口
    • 解析逻辑可以自定义替换
  3. 无魔法:没有任何隐藏行为

    • 没有 AgentType枚举
    • 没有黑盒的 AgentExecutor
    • 循环控制完全由开发者掌握

源码

github