LangChain 系列·(九):Agent——让 AI 自己做决策

LangChain 系列 · 第八篇:Agent------让 AI 自己做决策

🎯 适合人群:已掌握 Tool 定义与 Tool Calling 机制,想让 LLM 自主规划并完成多步任务的工程师

⏱️ 阅读时间:约 30 分钟

💬 本文介绍 ReAct 模式的推理原理,以及 LangChain 的 AgentExecutor 如何将工具调用循环封装为可复用的 Agent


一、从工具调用到自主决策

第七篇介绍了 Tool Calling 的底层流程:LLM 输出工具调用请求 → 代码执行工具 → 结果返回 LLM → LLM 给出最终答案。这个流程是单轮的------LLM 调用一次工具,拿到结果,输出答案,结束。

但真实任务往往更复杂:

用户问:"帮我分析一下北京和上海最近一周的 AQI 数据,告诉我哪个城市空气更好,并给出可视化建议。"

完成这个任务需要:

  1. 查询北京最近一周 AQI
  2. 查询上海最近一周 AQI
  3. 对比数据,得出结论
  4. 生成可视化建议

这就需要 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_iterationsmax_execution_time 是必须设置的安全阀
agent_scratchpad 存放当前轮次内的中间推理步骤(工具调用 + 结果),每轮循环都会注入完整历史
chat_history 跨轮次的对话历史,用于支持多轮对话上下文

🎯 Agent 的强大之处在于"不确定的执行路径",而这也是它最大的风险。max_iterations、工具返回值的清晰度、verbose=True 的调试观察------这三件事在 Agent 开发的早期阶段缺一不可。


参考资料


下期预告

AgentExecutor 能处理线性的工具调用循环,但无法表达"等待人工审批"、"并行子任务"、"有状态的多步工作流"这类复杂场景。

第九篇《LangGraph:当 Agent 需要"记住状态"》 将介绍 LangGraph 的 StateGraph 模型------用节点、边、状态机的方式描述复杂工作流,支持条件路由、状态持久化和 Human-in-the-loop 中断恢复。

相关推荐
孟祥_成都1 小时前
前端唯一的护城河?结合 AI 将字节组件库 Headless 化后的感想~
前端·人工智能·react.js
数智工坊1 小时前
【经典RL算法】Q-Learning:强化学习的里程碑——从理论到收敛证明的完整解析
论文阅读·人工智能·深度学习·算法·transformer
数智大号1 小时前
赋能 AI PC 时代:TCL 华星超高迁 50 技术重构智能显示新范式
人工智能·重构
Bolt1 小时前
Codex CLI + 国产模型:一个零侵入的 AI 网关实践
人工智能·全栈
尽欢i1 小时前
前端大坑!文件切片上传后端总报错找不到文件名?
前端·javascript
财经资讯数据_灵砚智能1 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年5月11日
大数据·人工智能·python·信息可视化·自然语言处理
java1234_小锋1 小时前
Spring AI 2.0 开发Java Agent智能体 - 会话记忆(Chat Memory)
java·人工智能·spring
Sylvia33.1 小时前
世界杯数据链路解析:从球场传感器到终端推送的毫秒级架构
java·前端·python·架构
数字护盾(和中)1 小时前
终端安全破局:银狐木马防御的 EDR 核心能力详解
网络·人工智能·安全