前言
前面讲了 Agent 相关的核心概念和工作架构。那么有哪些核心的设计让 Agent 能顺利且准确地完成任务呢?这次就学几个关键的核心设计来加深对 Agent 的理解。
定义
ReAct(Reasoning and Acting) 是当前AI智能体(Agent)领域的一个核心范式。
- 本质:“思考-行动-观察”主循环
- 目标:解决传统大模型在复杂、多步任务中容易产生的“幻觉”、逻辑错误和无法与外界交互的问题。
- 核心流程:
-
推理:智能体根据当前任务和目标,分析现状,规划下一步应该做什么(例如:“要回答这个问题,我需要先查询今天的天气”)。
-
行动:智能体执行上一步规划的动作,通常是调用一个外部工具或API(例如:调用天气查询API)。
-
观察:智能体接收行动执行后的结果(例如:API返回“北京,晴,25℃”)。
-
循环:基于新的观察结果,再次进入“推理”步骤,直至任务完成。
-
Coding
其实,在 LLM 工程中我一直信奉 "万物皆靠Prompt"的真理。所以 ReAct 的第一步便是 Prompt的编写。我们打开 Langsmith-Hub 搜索 ”ReAct“,其中的 ”llm-react/react“就是一个符合 ReAct 范式的 Prompt 模板。
我们不难发现,Prompt 将问题的求解分成了几个步骤:
-
Question:原始问题
-
Thought:首次思考需要做什么?
-
Action:根据思考采取一定的行动(Function Calling)
- Action Input:行动输入
- Action output:行动输出
- Observation:行动结果
-
Thought:继续思考 Observation 是否足以回答问题。如果不足:
- Thought -> Action 持续循环
- 这个思考/行动可以重复N次,直到足以回答问题
-
Thought:终于可以得到答案了
-
Final Answer:最终答案
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 页面和链接列表
- 核心流程:
-
查询意图分析:接收自然语言查询,可自动推断最佳搜索参数(如时间范围、主题领域)。
-
多源并行检索:同时向 20+ 权威数据源(包括新闻、学术、金融等)发起请求,确保信息覆盖的广度和质量。
-
内容提取与清洗:这是其关键价值所在。它会自动剥广告、导航栏、脚本等噪音,提取出最纯净的正文,直接省去了开发者自己写爬虫和解析HTML的繁琐工作。
-
语义相关性打分与排序:不仅基于关键词匹配,更利用语义理解技术对结果进行重排序,将最相关、质量最高的片段排在前面。
-
结构化输出:返回JSON格式
titleurlcontent(已清洗的文本)score(相关性分数)include_answer=true:true 可直接获得由AI生成的答案摘要- ...
-
TavilySearch 的使用需要在 官网 申请 API_KEY,并在本地配置:
- 方法1:.env文件配置(推荐)
TAVILY_API_KEY = "tvly-xxx"
- 方法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 按照上面说的流程逐个解决最终解决了这个问题。
思考🤔
我们发现 create_react_agent 依赖 langchain_classic 模块,但是我们也知道 Langchain 1.0 范式并不鼓励继续使用 langchain_classic 兼容包。在新的范式下发现一个create_agent接口,但是这个接口好像并不能旧接口一样很好体现 ReAct 流程。那么问题来了?
- 通常不是说 0.x 的设计是黑盒的,更集中化的;1.0 新范式是更开放,更透明的。但是按 create_agent 对 ReAct 的体现,好像反而更黑盒了,这个怎么解释?
- 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 的核心设计思想?
-
完全透明:每一步都清晰可见
- 提示词构造:
build_react_prompt函数 - 响应解析:
parse_llm_response函数 - 循环逻辑:
ReActAgent.invoke方法中的 while 循环 - 状态管理:
history列表跟踪所有消息
- 提示词构造:
-
可组合性:每个组件都是独立的
- LLM 可以替换为任何
Runnable - 工具只需实现
BaseTool接口 - 解析逻辑可以自定义替换
- LLM 可以替换为任何
-
无魔法:没有任何隐藏行为
- 没有
AgentType枚举 - 没有黑盒的
AgentExecutor - 循环控制完全由开发者掌握
- 没有