大模型会"说"但不会"做"——它能生成文本但无法查数据库、调 API、执行代码。AI Agent 就是给大模型加上手脚:让它能推理(Reasoning)、行动(Acting)、观察结果(Observation),形成闭环。本文从 ReAct 框架讲起,用 LangChain 实现一个能调用工具的 Agent。
Agent 的核心循环
一个 Agent 的运行逻辑可以抽象为:
循环:
1. 思考 (Thought): 分析当前状态,决定下一步
2. 行动 (Action): 调用一个工具
3. 观察 (Observation): 获取工具返回的结果
4. 判断: 是否已经得到最终答案?
- 是 → 输出结果,结束
- 否 → 回到步骤 1
这就是 ReAct(Reasoning + Acting)框架的核心。它由 Yao et al. 在 2022 年提出,思路很朴素但效果显著。
ReAct 的 Prompt 结构
ReAct 通过精心设计的 Prompt 让 LLM 按照 Thought → Action → Observation 的格式输出:
Answer the following questions as best you can. You have access to the following tools:
search: Search the web for information
calculator: Do math calculations
Use the following format:
Question: the input question
Thought: reason about what to do
Action: the tool name
Action Input: the input to the tool
Observation: the result of the tool
... (repeat Thought/Action/Observation as needed)
Thought: I now know the final answer
Final Answer: the answer
Begin!
Question: 2024年美国GDP是多少万亿美元?换算成人民币是多少?
Thought: 我需要先搜索2024年美国GDP数据,然后用计算器做汇率换算。
Action: search
Action Input: 2024年美国GDP总量
Observation: 2024年美国GDP约为28.78万亿美元。
Thought: 得到了GDP数据,现在需要乘以汇率。假设汇率约7.2。
Action: calculator
Action Input: 28.78 * 7.2
Observation: 207.216
Thought: I now know the final answer
Final Answer: 2024年美国GDP约为28.78万亿美元,按7.2汇率换算约为207.2万亿人民币。
LLM 每次只生成到 Observation 之前停止,由框架执行工具并填入 Observation,再把完整文本送回 LLM 继续生成。
工具定义
工具(Tool)是 Agent 的手脚。定义一个工具需要三要素:
- 名称:让 LLM 知道有哪些工具可用
- 描述:让 LLM 理解什么时候该用这个工具
- 输入格式:让 LLM 知道怎么构造输入
from langchain.tools import Tool, tool
from langchain_core.tools import StructuredTool
from pydantic import BaseModel, Field
# 方式1:用 @tool 装饰器(最简单)
@tool
def search_web(query: str) -> str:
'''搜索互联网获取实时信息。当需要查询最新数据、新闻、事实时使用。'''
# 实际实现:调用搜索 API
import requests
resp = requests.get(f"https://api.search.example/search?q={query}")
return resp.json()["answer"]
# 方式2:结构化工具(复杂输入)
class WeatherInput(BaseModel):
city: str = Field(description="城市名称")
date: str = Field(description="日期,格式 YYYY-MM-DD", default="today")
@tool(args_schema=WeatherInput)
def get_weather(city: str, date: str = "today") -> str:
'''查询指定城市的天气预报。'''
return f"{city}在{date}的天气:晴,25°C"
# 方式3:Tool 对象(最灵活)
def run_python_code(code: str) -> str:
'''执行 Python 代码并返回结果'''
try:
result = eval(code)
return str(result)
except Exception as e:
return f"Error: {e}"
python_tool = Tool(
name="python_executor",
func=run_python_code,
description="执行 Python 表达式。用于数学计算、数据处理等。输入应为合法的 Python 表达式。",
)
工具描述(description)的质量直接影响 Agent 的表现。描述要说清楚:
- 这个工具做什么
- 什么时候该用它
- 输入是什么格式
LangChain Agent 实现
用 LangChain 实现一个完整的 ReAct Agent:
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_react_agent
from langchain import hub
from langchain.tools import tool
import os
os.environ["OPENAI_API_KEY"] = "your-key"
os.environ["OPENAI_BASE_URL"] = "https://api.your-provider.com/v1"
# === 定义工具 ===
@tool
def search_knowledge_base(query: str) -> str:
'''从内部知识库搜索信息。适用于查询公司产品、政策、技术文档等内部信息。'''
# 模拟知识库搜索
kb = {
"退货政策": "自签收之日起7天内可无理由退货,商品需保持原包装完好。",
"VIP折扣": "VIP用户享受全场9折优惠,部分商品可叠加满减活动。",
"配送时间": "同城次日达,跨省2-5个工作日。偏远地区可能延迟。",
}
for key, value in kb.items():
if key in query or any(c in query for c in key):
return value
return "未找到相关信息"
@tool
def query_order(order_id: str) -> str:
'''查询订单状态。输入订单编号,返回订单详情。'''
# 模拟订单查询
orders = {
"ORD20240601": "已发货,预计6月3日送达,快递单号SF1234567890",
"ORD20240530": "已签收,6月1日14:32签收",
}
return orders.get(order_id, f"订单 {order_id} 不存在")
@tool
def calculator(expression: str) -> str:
'''计算数学表达式。输入一个合法的数学表达式字符串。'''
try:
return str(eval(expression))
except Exception as e:
return f"计算错误: {e}"
# === 构建 Agent ===
llm = ChatOpenAI(model="gpt-4o", temperature=0)
tools = [search_knowledge_base, query_order, calculator]
# 获取 ReAct prompt 模板
prompt = hub.pull("hwchase17/neact")
# 创建 ReAct Agent
agent = create_react_agent(llm, tools, prompt)
# AgentExecutor 负责执行循环
agent_executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # 打印推理过程
max_iterations=5, # 最大循环次数,防止无限循环
handle_parsing_errors=True, # 自动处理输出格式错误
)
# === 运行 ===
result = agent_executor.invoke({
"input": "我的订单ORD20240601到哪了?另外VIP用户有什么优惠?"
})
print(result["output"])
运行输出(verbose=True 时):
> Entering new AgentExecutor chain...
Thought: 用户问了两个问题:订单状态和VIP优惠。我先查订单。
Action: query_order
Action Input: ORD20240601
Observation: 已发货,预计6月3日送达,快递单号SF1234567890
Thought: 订单信息拿到了,现在查VIP优惠。
Action: search_knowledge_base
Action Input: VIP折扣优惠
Observation: VIP用户享受全场9折优惠,部分商品可叠加满减活动。
Thought: 两个问题都有答案了。
Final Answer: 您的订单 ORD20240601 已发货,预计6月3日送达,
快递单号 SF1234567890。关于VIP优惠,VIP用户享受全场9折,
部分商品还可以叠加满减活动。
Function Calling
OpenAI 的 Function Calling 是比 ReAct Prompt 更原生的方案。它不是通过 Prompt 让 LLM 输出特定格式的文本,而是在 API 层面支持工具调用:
from langchain_openai import ChatOpenAI
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
# 使用 Function Calling 的 Agent
llm = ChatOpenAI(model="gpt-4o", temperature=0)
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个有用的客服助手。尽可能帮助用户解决问题。"),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
# create_tool_calling_agent 使用 Function Calling 而非 ReAct Prompt
agent = create_tool_calling_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = agent_executor.invoke({"input": "帮我查一下 ORD20240601"})
Function Calling 的优势:
- 更可靠:不依赖 LLM 精确输出特定文本格式
- 并行调用:支持一次返回多个工具调用
- 结构化输入:参数以 JSON 传递,不是纯文本解析
ReAct Prompt 的优势:
- 模型无关:任何 LLM 都能用(不需要模型支持 Function Calling)
- 推理过程可见:Thought 部分暴露了 LLM 的推理链
- 灵活:可以自定义 Prompt 格式
自己实现一个最小 Agent
不依赖 LangChain,用纯 OpenAI API 实现一个最小的 ReAct Agent:
import openai
import json
client = openai.OpenAI()
# 工具定义
tools_schema = [
{
"type": "function",
"function": {
"name": "search",
"description": "搜索信息",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"}
},
"required": ["query"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "数学计算",
"parameters": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "数学表达式"}
},
"required": ["expression"]
}
}
},
]
# 工具实现
def execute_tool(name: str, args: dict) -> str:
if name == "search":
return f"搜索结果:关于「{args['query']}」的信息..."
elif name == "calculate":
return str(eval(args["expression"]))
return "未知工具"
def run_agent(user_input: str, max_steps: int = 5) -> str:
messages = [
{"role": "system", "content": "你是一个有用的助手。"},
{"role": "user", "content": user_input},
]
for step in range(max_steps):
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools_schema,
tool_choice="auto",
)
msg = response.choices[0].message
# 如果没有工具调用,说明得到了最终答案
if not msg.tool_calls:
return msg.content
# 执行工具调用
messages.append(msg)
for tool_call in msg.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
print(f" [Tool] {fn_name}({fn_args})")
result = execute_tool(fn_name, fn_args)
print(f" [Result] {result}")
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": result,
})
return "达到最大步数,未能得出结论。"
# 测试
answer = run_agent("今年全球智能手机出货量是多少?换算成每秒出货多少台?")
print(f"\n最终答案: {answer}")
Agent 的局限
Agent 很强大但不是银弹:
- 延迟高:每次循环都要调一次 LLM,多步推理可能要 10-30 秒
- 成本高:每次工具调用都消耗 token,复杂任务可能需要很多轮
- 不稳定:LLM 可能输出格式错误、选错工具、陷入死循环
- 安全风险:如果工具能执行代码或操作数据库,需要严格的权限控制
生产环境中的 Agent 需要额外考虑:
- 设置 max_iterations 防止无限循环
- 工具执行结果做截断,避免超长 context
- 对工具输入做校验和沙箱隔离
- 记录完整的推理链用于审计
- 失败时的降级策略(fallback 到人工)
Agent 是 LLM 应用从"生成文本"走向"执行任务"的关键一步。理解 ReAct 框架和工具调用的原理,是构建更复杂 AI 应用的基础。