LangChain 系列 · 第八篇:Agent------让 AI 自己做决策
🎯 适合人群:已掌握 Tool 定义与 Tool Calling 机制,想让 LLM 自主规划并完成多步任务的工程师
⏱️ 阅读时间:约 30 分钟
💬 本文介绍 ReAct 模式的推理原理,以及 LangChain 的 AgentExecutor 如何将工具调用循环封装为可复用的 Agent
一、从工具调用到自主决策
第七篇介绍了 Tool Calling 的底层流程:LLM 输出工具调用请求 → 代码执行工具 → 结果返回 LLM → LLM 给出最终答案。这个流程是单轮的------LLM 调用一次工具,拿到结果,输出答案,结束。
但真实任务往往更复杂:
用户问:"帮我分析一下北京和上海最近一周的 AQI 数据,告诉我哪个城市空气更好,并给出可视化建议。"
完成这个任务需要:
- 查询北京最近一周 AQI
- 查询上海最近一周 AQI
- 对比数据,得出结论
- 生成可视化建议
这就需要 LLM 能够自主规划步骤、反复调用工具、基于中间结果决定下一步 ,而不是一次性给出答案。这种能力,就是 Agent。
Agent 与直接调用工具链的本质区别在于:工具链的执行路径是代码写死的,Agent 的执行路径由 LLM 在运行时动态决定。
Tool Chain (fixed path):
Input → Tool A → Tool B → Tool C → Output
(always in this order)
Agent (dynamic path):
Input → LLM decides → Tool A → LLM decides → Tool C → LLM decides → Output
(path changes based on intermediate results)
二、ReAct:Agent 的推理模式
ReAct (Reasoning + Acting)是目前最主流的 Agent 推理模式,由 Google Research 在 2022 年提出。它的核心思想是:让 LLM 交替进行"思考"和"行动",每一步行动的结果都作为下一步思考的输入。
2.1 ReAct 循环
ReAct Loop:
┌─────────────────────────────────────────────────────┐
│ │
│ Thought: "I need to find Beijing's AQI first" │
│ │ │
│ ▼ │
│ Action: call get_aqi(city="Beijing") │
│ │ │
│ ▼ │
│ Observation: "Beijing AQI past 7 days: [85,92...]" │
│ │ │
│ ▼ │
│ Thought: "Now I need Shanghai's AQI" │
│ │ │
│ ▼ │
│ Action: call get_aqi(city="Shanghai") │
│ │ │
│ ▼ │
│ Observation: "Shanghai AQI past 7 days: [55,61...]"│
│ │ │
│ ▼ │
│ Thought: "I have both datasets, can now compare" │
│ │ │
│ ▼ │
│ Final Answer: "上海空气更好,平均 AQI 58 vs 北京 88" │
│ │
└─────────────────────────────────────────────────────┘
每轮循环包含三个部分:
- Thought(思考):LLM 推理当前状态,决定下一步做什么
- Action(行动):调用某个工具,传入具体参数
- Observation(观察):工具的返回结果,作为下一轮思考的输入
循环持续直到 LLM 认为已有足够信息,直接输出 Final Answer(最终答案)。
2.2 为什么叫"Reasoning + Acting"
ReAct 的关键创新在于:思考和行动不是分离的。传统方式要么让 LLM 一次性规划所有步骤,要么直接行动不思考。ReAct 将推理链(Chain of Thought)和工具调用交织在一起,每一步的工具结果都会修正后续的推理方向。
这也意味着 ReAct Agent 具备一定的自我纠错能力:如果某次工具调用返回了错误或意外结果,LLM 的下一个 Thought 会基于这个结果重新规划。
三、create_tool_calling_agent:构建 Agent
LangChain 提供了 create_tool_calling_agent 函数,将 ReAct 模式封装为一个标准的 Runnable。
3.1 最小可运行示例
python
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.agents import create_tool_calling_agent, AgentExecutor
load_dotenv()
# --- 定义工具 ---
@tool
def get_weather(city: str) -> str:
"""查询指定城市的当前天气,返回天气描述字符串。
Args:
city: 城市中文名,如"北京"、"上海"
"""
data = {
"北京": "晴,22°C,湿度 35%",
"上海": "多云,26°C,湿度 72%",
"深圳": "阵雨,29°C,湿度 88%",
}
return data.get(city, f"未找到 {city} 的天气数据")
@tool
def calculate(expression: str) -> str:
"""计算数学表达式,返回计算结果。
Args:
expression: 数学表达式,如 "2 ** 10"、"(3 + 5) * 12 / 4"
"""
try:
result = eval(expression, {"__builtins__": {}})
return str(result)
except Exception as e:
return f"计算失败:{e}"
tools = [get_weather, calculate]
# --- 构建 Agent ---
# Agent 的 Prompt 必须包含 MessagesPlaceholder("agent_scratchpad")
# agent_scratchpad 是 Agent 存放中间推理步骤(工具调用 + 结果)的位置
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个智能助手,能够查询天气和进行数学计算。根据用户的问题,选择合适的工具完成任务。"),
MessagesPlaceholder(variable_name="chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"), # 必须有
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# create_tool_calling_agent 返回一个 Runnable
# 它负责:解析 LLM 输出 → 识别工具调用 → 格式化为 AgentAction
agent = create_tool_calling_agent(llm, tools, prompt)
# AgentExecutor 负责:运行工具调用循环,直到 Agent 输出 Final Answer
executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # 打印每一步的推理过程
max_iterations=10, # 最多循环 10 次,防止无限循环
)
# --- 运行 ---
result = executor.invoke({"input": "北京今天适合户外运动吗?另外 2 的 15 次方是多少?"})
print(result["output"])
运行时 verbose=True 会输出完整的 ReAct 推理过程:
> Entering new AgentExecutor chain...
Invoking: `get_weather` with `{'city': '北京'}`
晴,22°C,湿度 35%
Invoking: `calculate` with `{'expression': '2 ** 15'}`
32768
北京今天天气晴朗,22°C,湿度仅 35%,非常适合户外运动。
2 的 15 次方等于 32768。
> Finished chain.
3.2 agent_scratchpad 的作用
agent_scratchpad 是 Agent Prompt 中的关键占位符,它存放 Agent 每一步的中间状态:
agent_scratchpad 的内容(随每轮循环累积):
Round 1:
AIMessage(tool_calls=[{name: "get_weather", args: {"city": "北京"}}])
ToolMessage(content="晴,22°C,湿度 35%", tool_call_id="...")
Round 2:
AIMessage(tool_calls=[{name: "calculate", args: {"expression": "2 ** 15"}}])
ToolMessage(content="32768", tool_call_id="...")
Round 3:
(no tool_calls) → Final Answer
每一轮,AgentExecutor 都会将完整的 agent_scratchpad 注入 Prompt,让 LLM 看到所有历史推理步骤,从而做出正确的下一步决策。
四、AgentExecutor 详解
AgentExecutor 是 LangChain 提供的 Agent 运行时,负责管理 ReAct 循环的执行。
4.1 核心参数
python
executor = AgentExecutor(
agent=agent,
tools=tools,
# 循环控制
max_iterations=15, # 最大循环次数,超出后强制停止
max_execution_time=60.0, # 最大执行时间(秒),超出后强制停止
# 错误处理
handle_parsing_errors=True, # LLM 输出格式错误时自动重试
handle_tool_errors=True, # 工具执行出错时将错误信息返回给 LLM
# 输出控制
verbose=True, # 打印每步推理过程(开发阶段建议开启)
return_intermediate_steps=True, # 在返回结果中包含中间步骤
)
4.2 查看中间步骤
开启 return_intermediate_steps=True 后,可以拿到 Agent 每一步的调用详情:
python
executor = AgentExecutor(
agent=agent,
tools=tools,
return_intermediate_steps=True,
)
result = executor.invoke({"input": "北京和上海哪个城市今天更热?"})
# result["intermediate_steps"] 是一个列表,每个元素是 (AgentAction, tool_output)
for step in result["intermediate_steps"]:
action, observation = step
print(f"工具:{action.tool}")
print(f"参数:{action.tool_input}")
print(f"结果:{observation}")
print("---")
print("最终答案:", result["output"])
输出:
工具:get_weather
参数:{'city': '北京'}
结果:晴,22°C,湿度 35%
---
工具:get_weather
参数:{'city': '上海'}
结果:多云,26°C,湿度 72%
---
最终答案:上海今天更热,气温 26°C,比北京高 4°C。
4.3 流式输出
Agent 运行时间往往较长,使用流式输出可以让用户看到实时进度:
python
# astream_events 返回 Agent 执行过程中的所有事件
async def run_agent_streaming(user_input: str):
async for event in executor.astream_events(
{"input": user_input},
version="v2",
):
kind = event["event"]
# 工具调用开始
if kind == "on_tool_start":
print(f"\n[调用工具] {event['name']}({event['data']['input']})")
# 工具调用结束
elif kind == "on_tool_end":
print(f"[工具结果] {event['data']['output'][:100]}...")
# LLM 流式输出最终答案
elif kind == "on_chat_model_stream":
chunk = event["data"]["chunk"]
if chunk.content:
print(chunk.content, end="", flush=True)
import asyncio
asyncio.run(run_agent_streaming("北京今天天气怎样?"))
五、带记忆的 Agent:多轮对话
默认的 AgentExecutor 是无状态的------每次 invoke 都是独立对话,不记得上一轮说了什么。通过 chat_history 注入对话历史,可以让 Agent 支持多轮对话:
python
from langchain_core.messages import HumanMessage, AIMessage
chat_history = []
def chat_with_agent(user_input: str) -> str:
result = executor.invoke({
"input": user_input,
"chat_history": chat_history,
})
answer = result["output"]
# 更新对话历史
chat_history.append(HumanMessage(content=user_input))
chat_history.append(AIMessage(content=answer))
return answer
# 多轮对话
print(chat_with_agent("北京今天天气怎么样?"))
print(chat_with_agent("那适合穿什么衣服?")) # Agent 能记住上一轮的北京天气
print(chat_with_agent("上海呢?")) # 能理解"上海呢"是在问上海天气
💡
chat_history在 Prompt 的MessagesPlaceholder("chat_history", optional=True)位置注入,与agent_scratchpad的区别是:chat_history是跨轮次的历史,agent_scratchpad是当前轮次内的中间步骤。
六、自定义 Agent 行为
6.1 工具选择的引导
通过 System Prompt 可以引导 Agent 的工具选择策略:
python
prompt = ChatPromptTemplate.from_messages([
(
"system",
"你是一个专业的数据分析助手。\n\n"
"工具使用原则:\n"
"- 需要实时数据时,优先调用工具,不要凭记忆回答\n"
"- 能用一次工具解决的,不要调用两次\n"
"- 工具返回数据后,先验证数据合理性再给出结论\n"
"- 如果工具调用失败,告知用户原因,不要编造数据\n"
),
MessagesPlaceholder(variable_name="chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
6.2 限制工具调用范围
有时希望不同场景使用不同的工具子集:
python
# 全量工具列表
all_tools = [search_tool, calculate_tool, send_email_tool, query_db_tool]
# 只读场景:不允许发邮件和写数据库
readonly_executor = AgentExecutor(
agent=create_tool_calling_agent(llm, [search_tool, calculate_tool], prompt),
tools=[search_tool, calculate_tool],
)
# 完整权限场景
full_executor = AgentExecutor(
agent=create_tool_calling_agent(llm, all_tools, prompt),
tools=all_tools,
)
七、常见问题与调试
问题一:Agent 陷入无限循环
现象:Agent 反复调用同一个工具,始终无法给出最终答案。
根本原因:工具返回的结果没有解答 LLM 的疑问,或 LLM 误判需要继续调用。
python
# ✅ 防护措施一:设置 max_iterations
executor = AgentExecutor(agent=agent, tools=tools, max_iterations=8)
# ✅ 防护措施二:设置最大执行时间(秒)
executor = AgentExecutor(agent=agent, tools=tools, max_execution_time=30.0)
# ✅ 防护措施三:工具返回值提供明确的"完结信号"
@tool
def search(query: str) -> str:
"""搜索信息"""
results = do_search(query)
if not results:
return "未找到相关信息,请换一个关键词重试或直接回答用户"
return "\n".join(results)
问题二:工具被错误调用
现象:LLM 选择了不合适的工具,或传入了错误的参数格式。
排查方法 :开启 verbose=True 查看 LLM 的推理过程,定位是哪一步出现判断错误。
python
# ❌ 工具描述过于笼统
@tool
def query(q: str) -> str:
"""查询数据"""
# ✅ 描述明确说明适用场景
@tool
def query_product_info(product_id: str) -> str:
"""查询指定商品的详细信息(名称、价格、库存)。
仅适用于查询商品数据,不适用于查询订单或用户信息。
Args:
product_id: 商品 ID,格式为 'PROD-' 加 6 位数字,如 'PROD-001234'
"""
问题三:中间步骤过多,Token 消耗大
现象:Agent 完成一个任务需要 10+ 轮工具调用,Token 成本极高。
python
# ✅ 让工具尽可能一次返回完整信息,减少往返次数
@tool
def get_city_report(city: str) -> str:
"""获取城市的综合报告,包含天气、AQI、温度、湿度。一次返回所有数据。"""
# 合并多个数据源,避免 LLM 多次调用不同工具
weather = get_weather(city)
aqi = get_aqi(city)
return f"城市:{city}\n天气:{weather}\nAQI:{aqi}"
问题四:handle_parsing_errors 的使用场景
python
# 当 LLM 输出格式偶尔不符合预期时(如工具参数格式错误)
# handle_parsing_errors=True 会将错误信息返回给 LLM,让它重新生成
executor = AgentExecutor(
agent=agent,
tools=tools,
handle_parsing_errors=True, # 解析失败时自动重试,不抛出异常
)
# 也可以传入自定义的错误提示信息
executor = AgentExecutor(
agent=agent,
tools=tools,
handle_parsing_errors="请检查工具调用格式,确保参数类型正确后重试。",
)
八、完整实战示例:城市信息助手
python
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool, ToolException
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
from langchain.agents import create_tool_calling_agent, AgentExecutor
load_dotenv()
# --- 工具定义 ---
@tool
def get_weather(city: str) -> str:
"""查询城市当前天气,返回气温、天气状况、风力。
Args:
city: 城市中文名
"""
data = {
"北京": {"temp": 22, "desc": "晴", "wind": "东南风 3 级", "humidity": 35},
"上海": {"temp": 26, "desc": "多云", "wind": "东风 2 级", "humidity": 72},
"广州": {"temp": 31, "desc": "阵雨", "wind": "南风 4 级", "humidity": 88},
"成都": {"temp": 20, "desc": "阴", "wind": "无持续风向", "humidity": 80},
}
if city not in data:
raise ToolException(f"不支持城市 '{city}',支持:{', '.join(data.keys())}")
d = data[city]
return f"{city}:{d['desc']},{d['temp']}°C,{d['wind']},湿度 {d['humidity']}%"
@tool
def get_aqi(city: str) -> str:
"""查询城市当前空气质量指数(AQI)及等级。
Args:
city: 城市中文名
"""
data = {
"北京": {"aqi": 88, "level": "良"},
"上海": {"aqi": 52, "level": "优"},
"广州": {"aqi": 45, "level": "优"},
"成都": {"aqi": 112, "level": "轻度污染"},
}
if city not in data:
raise ToolException(f"不支持城市 '{city}'")
d = data[city]
return f"{city} AQI:{d['aqi']}({d['level']})"
@tool
def calculate(expression: str) -> str:
"""计算数学表达式。
Args:
expression: 合法的 Python 数学表达式,如 "(22 + 26) / 2"
"""
try:
result = eval(expression, {"__builtins__": {}})
return f"{expression} = {result}"
except Exception as e:
raise ToolException(f"表达式非法:{e}")
tools = [get_weather, get_aqi, calculate]
# --- Agent 构建 ---
prompt = ChatPromptTemplate.from_messages([
(
"system",
"你是一个专业的城市信息助手,能够查询天气、空气质量,并进行数据分析。\n"
"在给出结论之前,确保已收集了足够的数据。\n"
"如果工具返回错误,如实告知用户,不要编造数据。"
),
MessagesPlaceholder(variable_name="chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder(variable_name="agent_scratchpad"),
])
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True,
max_iterations=10,
handle_tool_errors=True,
return_intermediate_steps=True,
)
# --- 多轮对话 ---
chat_history = []
def chat(user_input: str) -> str:
result = executor.invoke({
"input": user_input,
"chat_history": chat_history,
})
answer = result["output"]
chat_history.append(HumanMessage(content=user_input))
chat_history.append(AIMessage(content=answer))
return answer
print(chat("对比北京和上海今天的天气和空气质量,哪个城市更适合户外活动?"))
print("---")
print(chat("两个城市的平均气温是多少?")) # 记住上下文,直接计算
九、AgentExecutor 的局限
AgentExecutor 适合线性的工具调用循环,但在以下场景力不从心:
| 场景 | AgentExecutor 的问题 |
|---|---|
| 需要人工审批再继续 | 无法在循环中途暂停等待人工输入 |
| 多个子任务并行执行 | 工具调用是串行的,无法并发 |
| 需要分支逻辑(if/else) | 无法表达复杂的条件路由 |
| 长任务需要保存进度 | 无法持久化中间状态,中断就全部丢失 |
| 循环次数不确定 | max_iterations 是硬限制,不够灵活 |
这些局限正是 LangGraph 要解决的问题------下一篇将介绍如何用 LangGraph 构建有状态、可分支、可暂停的复杂 Agent 工作流。
十、总结
| 概念 | 核心要点 |
|---|---|
| Agent | LLM + 工具 + 循环;执行路径由 LLM 在运行时动态决定,而非代码写死 |
| ReAct 模式 | Thought → Action → Observation 循环;推理和行动交织,支持基于中间结果自我纠错 |
| create_tool_calling_agent | 将 ReAct 逻辑封装为 Runnable,需要 Prompt 中包含 agent_scratchpad 占位符 |
| AgentExecutor | Agent 的运行时,管理工具调用循环;max_iterations 和 max_execution_time 是必须设置的安全阀 |
| agent_scratchpad | 存放当前轮次内的中间推理步骤(工具调用 + 结果),每轮循环都会注入完整历史 |
| chat_history | 跨轮次的对话历史,用于支持多轮对话上下文 |
🎯 Agent 的强大之处在于"不确定的执行路径",而这也是它最大的风险。
max_iterations、工具返回值的清晰度、verbose=True的调试观察------这三件事在 Agent 开发的早期阶段缺一不可。
参考资料
- LangChain Agent 概念文档
- create_tool_calling_agent API
- ReAct 论文:Synergizing Reasoning and Acting in Language Models
- AgentExecutor 参数说明
下期预告
AgentExecutor 能处理线性的工具调用循环,但无法表达"等待人工审批"、"并行子任务"、"有状态的多步工作流"这类复杂场景。
第九篇《LangGraph:当 Agent 需要"记住状态"》 将介绍 LangGraph 的 StateGraph 模型------用节点、边、状态机的方式描述复杂工作流,支持条件路由、状态持久化和 Human-in-the-loop 中断恢复。