1. 项目概述:从零构建你的第一个ReAct Agent
作为一名长期奋战在AI应用开发一线的工程师,我深刻理解初学者在构建智能Agent时的困惑。今天,我将带你用LangChain框架实现一个完整的ReAct Agent,这个项目不仅能处理多步骤推理任务,还能动态调用工具解决问题。不同于传统的线性处理流程,Agent能够自主决策执行路径,就像一位经验丰富的助手,知道什么时候该查资料、什么时候该计算。
1.1 Agent与链的本质区别
很多刚接触LangChain的开发者容易混淆Agent和Chain的概念。简单来说,Chain就像固定路线的公交车,必须按照预设站点依次停靠;而Agent则是自驾游,可以根据路况随时调整路线。具体差异体现在:
- 执行方式:Chain严格按照定义好的顺序执行,而Agent会根据中间结果动态选择下一步动作
- 错误处理:Chain遇到错误通常会中断,Agent则可能尝试其他解决方案
- 适用场景:Chain适合流程固定的任务,Agent更适合开放性问题
在我们的水果价格计算示例中,Agent需要自主决定先查询价格再计算总额,这种灵活的决策能力正是其价值所在。
1.2 工具:Agent的能力扩展包
工具(Tool)是Agent与外界交互的桥梁。想象你给助手配备了一个多功能工具箱:
- calculate工具:相当于计算器
- ask_fruit_unit_price工具:相当于价格查询手册
开发Agent的核心工作之一就是打造这些工具。好的工具应该具备:
- 明确的输入输出规范
- 详细的文档说明(模型靠这个理解工具用途)
- 安全的执行环境(特别是涉及eval等危险操作时)
重要提示:生产环境中直接使用eval极其危险!本文仅为演示用途,实际项目务必使用更安全的替代方案如ast.literal_eval或专用数学解析库。
2. 项目结构与核心实现
2.1 项目目录布局
标准的LangChain项目通常采用以下结构:
code复制react-agent-demo/
├── tools.py # 工具定义
├── agent.py # Agent核心配置
├── main.py # 入口文件
└── .env # 环境配置
这种结构将不同职责的代码分离,便于维护和扩展。接下来我们深入每个核心文件。
2.2 工具定义详解(tools.py)
工具是Agent能力的基石。我们使用@tool装饰器将普通Python函数转化为Agent可调用的工具:
python复制from langchain_core.tools import tool
@tool
def calculate(expression: str) -> float:
"""执行数学表达式计算 - 使用Python语法,支持加减乘除和括号运算
示例:
- 输入:"(3 + 5) * 2" → 输出:16.0
- 输入:"10.5 / 3" → 输出:3.5
注意:仅支持基本数学运算,不支持函数调用或变量赋值
"""
return eval(expression)
@tool
def ask_fruit_unit_price(fruit: str) -> str:
"""查询指定水果的单价(元/公斤),支持中英文名称
当前支持的水果:
- 苹果/Apple:10元/公斤
- 香蕉/Banana:6元/公斤
其他水果默认:20元/公斤
示例:
- 输入:"苹果" → 输出:"苹果单价是10元/公斤"
- 输入:"banana" → 输出:"香蕉单价是6元/公斤"
"""
fruit = fruit.strip().lower()
price_map = {
"apple": 10,
"苹果": 10,
"banana": 6,
"香蕉": 6
}
default_price = 20
price = price_map.get(fruit, default_price)
return f"{fruit}单价是{price}元/公斤"
工具开发最佳实践
-
文档字符串(Docstring)要详细:这是模型理解工具用途的主要依据,应包括:
- 功能描述
- 输入输出示例
- 注意事项
-
参数类型注解必须准确:LangChain会根据这个生成工具调用schema
-
工具命名要有意义:使用动词+名词形式,如"calculate_price"比"tool1"更易理解
-
输入验证必不可少:特别是涉及外部调用的工具,要验证参数合法性
2.3 Agent核心配置(agent.py)
这是整个项目的大脑,负责整合LLM、工具和推理逻辑。
python复制import os
import dotenv
from langchain.agents import AgentExecutor, create_react_agent
from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from tools import calculate, ask_fruit_unit_price
# 加载环境变量
dotenv.load_dotenv()
# ReAct提示词模板
REACT_PROMPT = PromptTemplate.from_template('''
你是一个智能助手,需要回答用户的问题。你可以使用以下工具:
{tools}
请严格按照以下格式响应:
Question: 需要回答的问题
Thought: 你的思考过程
Action: 要执行的动作,必须是[{tool_names}]中的一个
Action Input: 动作的输入
Observation: 动作执行结果
...(这个Thought/Action/Action Input/Observation循环可以重复多次)
Thought: 我现在知道最终答案了
Final Answer: 对原始问题的最终回答
开始!
Question: {input}
Thought:{agent_scratchpad}
''')
def create_agent() -> AgentExecutor:
"""创建并配置ReAct Agent执行器"""
llm = ChatOpenAI(
openai_api_key=os.getenv("OPENAI_API_KEY"),
openai_api_base=os.getenv("OPENAI_API_BASE"),
model=os.getenv("AI_MODEL", "deepseek-chat"),
temperature=0.1, # 低随机性保证结果稳定
max_tokens=1000 # 防止无限循环
)
tools = [calculate, ask_fruit_unit_price]
agent = create_react_agent(
llm=llm,
tools=tools,
prompt=REACT_PROMPT
)
return AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=10 # 限制最大迭代次数
)
关键配置解析
-
LLM参数调优:
temperature=0.1:推理任务需要高确定性max_tokens=1000:防止无限输出- 推荐使用性价比高的模型如DeepSeek,生产环境可考虑GPT-4
-
AgentExecutor安全设置:
max_iterations=10:避免无限循环verbose=True:开发阶段开启详细日志
-
提示词工程技巧:
- 明确输出格式要求
- 提供清晰的工具描述
- 强调逐步推理的重要性
2.4 入口程序(main.py)
python复制from agent import create_agent
if __name__ == '__main__':
agent = create_agent()
questions = [
"3公斤苹果和2公斤香蕉的总价格是多少?",
"5公斤橙子多少钱?",
"买2公斤苹果和3公斤香蕉,打9折后多少钱?"
]
for question in questions:
print(f"\n问题:{question}")
result = agent.invoke({"input": question})
print(f"答案:{result['output']}")
这个入口脚本展示了如何批量测试不同场景的问题。实际项目中,你可以将其改造为API服务或交互式命令行工具。
3. 运行与调试
3.1 环境准备
首先确保安装所需依赖:
bash复制pip install langchain langchain-openai langchain-core python-dotenv
创建.env配置文件:
code复制OPENAI_API_KEY=your_api_key_here
OPENAI_API_BASE=https://api.deepseek.com/v1
AI_MODEL=deepseek-chat
3.2 典型执行流程分析
让我们解剖一个完整的Agent推理过程:
问题:"3公斤苹果和2公斤香蕉的总价格是多少?"
执行日志:
code复制> Entering new AgentExecutor chain...
我需要计算总价格。首先,我应该找到苹果和香蕉的单价。
Thought: 我需要找到苹果的单价
Action: ask_fruit_unit_price
Action Input: 苹果
Observation: 苹果单价是10元/公斤
Thought: 现在我需要找到香蕉的单价
Action: ask_fruit_unit_price
Action Input: 香蕉
Observation: 香蕉单价是6元/公斤
Thought: 我已经知道了两种水果的价格。苹果是10元/公斤,香蕉是6元/公斤。现在我需要计算总成本:3公斤苹果和2公斤香蕉。
Action: calculate
Action Input: 3 * 10 + 2 * 6
Observation: 42
Thought: 我现在知道最终答案了
Final Answer: 3公斤苹果和2公斤香蕉的总价格是42元。
> Finished chain.
这个流程展示了ReAct模式的核心特点:
- 模型自主决定先查询单价
- 依次获取苹果和香蕉价格
- 最后进行计算并返回结果
3.3 常见问题排查
在实际开发中,你可能会遇到以下典型问题:
问题1:Agent陷入无限循环
- 解决方案:
- 设置
max_iterations参数 - 检查工具描述是否清晰
- 降低temperature值
- 设置
问题2:工具选择错误
- 症状:Agent调用了不合适的工具
- 修复方法:
- 优化工具文档字符串
- 在Prompt中明确各工具的适用场景
问题3:参数格式错误
- 表现:工具接收到不符合预期的输入
- 改进措施:
- 加强工具输入验证
- 在Prompt中提供参数示例
4. 高级扩展与实践建议
4.1 工具扩展方向
- 安全计算工具:
python复制import ast
import operator as op
def safe_calculate(expr: str) -> float:
"""安全计算数学表达式"""
allowed_operators = {
ast.Add: op.add,
ast.Sub: op.sub,
ast.Mult: op.mul,
ast.Div: op.truediv
}
def eval_node(node):
if isinstance(node, ast.Num):
return node.n
elif isinstance(node, ast.BinOp):
return allowed_operators[type(node.op)](
eval_node(node.left),
eval_node(node.right)
)
else:
raise ValueError(f"不支持的表达式: {node}")
try:
tree = ast.parse(expr, mode='eval').body
return eval_node(tree)
except Exception as e:
return f"计算错误: {str(e)}"
- 网络API工具:
python复制import requests
@tool
def get_weather(city: str) -> str:
"""获取指定城市的当前天气情况"""
try:
response = requests.get(
f"https://api.weather.com/v1/city/{city}/now",
timeout=5
)
data = response.json()
return f"{city}天气:{data['condition']}, 温度{data['temp']}℃"
except Exception as e:
return f"获取天气失败: {str(e)}"
4.2 Agent优化策略
- 自定义提示词:根据你的领域调整ReAct模板
- 记忆机制:添加对话历史记忆
- 工具路由:实现更智能的工具选择逻辑
- 验证层:在Agent和工具之间添加输入验证
4.3 生产环境注意事项
-
安全性:
- 禁用危险操作如eval
- 限制API调用频率
- 实施输入过滤
-
性能优化:
- 缓存常用工具结果
- 设置合理的超时时间
- 监控token使用量
-
可观测性:
- 记录完整的推理过程
- 收集工具使用统计
- 实现健康检查接口
5. 项目总结与进阶路线
通过这个项目,我们实现了一个具备基本推理能力的ReAct Agent。虽然示例简单,但已经包含了构建智能Agent的核心要素:
- 工具定义与封装
- ReAct推理流程实现
- LLM与工具的协同工作
- 执行过程监控与调试
要进一步提升Agent能力,建议从以下方向深入:
- 多工具协作:实现需要多个工具配合的复杂任务
- 长期记忆:添加向量数据库存储历史信息
- 动态工具加载:支持运行时添加新工具
- 验证与审计:建立完整的执行日志系统
记住,构建优秀的Agent是一个迭代过程。从简单用例开始,逐步扩展功能,持续优化提示词和工具设计,最终你将打造出真正强大的智能助手。