跳到正文

05. 使用 create_agent 构建 Agent Loop

学习目标

完成本章后,你应该能够:

  • 说明 create_agent 在 LangChain Agent 中扮演的角色。
  • 区分直接 tool calling 和 Agent Loop。
  • 使用模型、工具和 system prompt 构建一个最小 Agent。
  • 理解 Agent 如何在模型调用和工具执行之间循环。
  • 按步骤定位 Agent 调用失败的问题。

从模型到 Agent

直接调用模型时,你通常只得到一次模型回复。即使模型请求了工具,仍然需要你自己读取 tool_calls、执行工具、把结果组织回上下文,再决定是否继续调用模型。

create_agent 是更高层的 Agent harness。它把模型、工具和 system prompt 组合起来,负责驱动“模型思考、请求工具、执行工具、把工具结果交回模型、生成最终回复”的循环。

这并不意味着 Agent 变成了魔法。工具仍然是普通 Python 函数,模型仍然可能选错工具或生成错误参数。create_agent 只是帮你管理常见的执行流程。

运行示例

运行本章示例:

bash
uv run python examples/04_agent_cli.py

示例创建聊天模型:

python
model = ChatOllama(
    model=config.ollama_model,
    base_url=config.ollama_base_url,
    temperature=0,
)

然后交给 create_agent

python
agent = create_agent(
    model=model,
    tools=[add, multiply],
    system_prompt="...",
)

用户输入会以 message 形式传入:

python
agent.invoke({"messages": [{"role": "user", "content": user_input}]})

Agent 返回的结果中包含完整消息列表,示例只打印最后一条 AI message 的内容。

直接 tool calling 与 create_agent 的区别

直接 tool calling 更接近底层机制:你能清楚看到模型原始内容、工具调用请求和工具执行结果。它适合学习、调试和掌握工具调用协议。

create_agent 更适合构建实际 Agent:它封装了工具执行循环,让你把注意力放在工具设计、系统提示词和用户体验上。你不需要为每一轮都手写“检查 tool_calls、执行工具、追加 tool message、再次调用模型”的样板代码。

两者的关系可以这样理解:

  • 直接模型调用:模型只负责生成下一条回复或工具请求。
  • 直接 tool calling:程序手动执行模型请求的工具。
  • create_agent:LangChain 帮你组织模型、工具和中间消息,直到得到最终回复。

调试重点

调试 Agent 时,不要只看最终答案。建议按这个顺序排查:

  1. 先运行上一章的直接 tool calling 示例,确认模型能产生结构化工具调用。
  2. 确认工具函数能独立运行,例如 add.invoke({"a": 2, "b": 3})
  3. 检查 system prompt 是否明确要求算术使用工具,最终回答保持简短。
  4. 打印 Agent 返回的 messages,查看中间是否出现工具调用和工具结果。
  5. 如果本地模型不支持工具调用,先换支持工具调用的 Ollama 模型,再调整 prompt。

当 Agent 输出异常时,优先判断问题发生在哪一层:模型没有请求工具、工具参数错误、工具执行失败,还是工具结果返回给模型后没有被正确总结。

Agent Loop 的消息变化

一次成功的工具调用通常会经历下面的消息变化:

  1. 用户消息进入 Agent。
  2. 模型返回一个 AI 消息,其中包含 tool call 请求。
  3. 程序执行工具,生成 tool message。
  4. tool message 被追加到上下文。
  5. 模型再次读取上下文,生成最终回答。

这就是为什么 Agent 调试必须看中间消息。如果只看最终回答,你无法判断模型是否真的调用了工具,也无法判断工具结果是否正确进入上下文。

代码阅读重点

阅读 examples/04_agent_cli.py 时,重点看:

  • create_agent(model=model, tools=[...], system_prompt=...)
  • agent.invoke({"messages": [...]})
  • _final_message_content() 如何取出最后一条消息
  • try/except 如何把工具调用失败转成中文提示

这个示例把工具执行循环交给 LangChain 管理。你不再手动遍历 tool_calls,但你仍然要设计清楚工具和系统提示。

实验练习

  • 给 Agent 增加一个 subtract 工具。
  • 把系统提示里的“算术必须使用工具”删掉,观察模型是否还会调用工具。
  • 输入一个多步计算问题,例如 (5 + 7) * (9 - 3),观察模型是否会拆分工具调用。

自测问题

  • create_agent 相比直接 model.invoke() 多做了什么?
  • 为什么 examples/03_tools_cli.py 仍然有价值?
  • 如果 Agent 没有调用工具,你会先改系统提示还是先验证底层 tool calling?

深入理解:Agent Loop 的停止条件

Agent loop 不可能无限运行。它必须有停止条件。常见停止方式包括:

  • 模型返回最终回答,不再请求工具。
  • 达到最大迭代次数。
  • 工具调用失败,程序中断或返回错误。
  • 外部策略判断风险过高,要求人工确认。
  • 用户取消任务。

教学示例里,create_agent 帮你处理大部分循环细节。但在真实系统里,你仍然要关心循环是否可能失控。例如模型反复调用同一个检索工具、反复请求参数错误的工具、或者在没有新信息时继续推理。

如果 Agent 行为开始不可控,通常不是“再写一句 prompt”就能解决,而是需要增加运行策略:最大步数、工具白名单、重试次数、人工确认和日志。

create_agent 适合的任务形态

create_agent 适合“模型主导下一步”的任务:

  • 用户问题不确定,需要模型判断是否查资料。
  • 工具有几个,但调用顺序不固定。
  • 失败后可以让用户重试。
  • 状态主要是短期消息历史。

它不适合把强约束业务流程完全交给模型。例如“必须先校验权限,再查询余额,再确认用户意图,再执行扣款”,这种流程应该由程序或 LangGraph 显式表达,模型只能参与其中某些节点。

排查树

create_agent 没有按预期工作时,可以按下面路径排查:

  1. 直接调用模型是否正常?
  2. bind_tools 直接工具调用是否正常?
  3. 工具函数独立运行是否正常?
  4. create_agent 的 system prompt 是否要求使用工具?
  5. Agent 返回的 messages 里是否出现 tool call?
  6. tool result 是否进入后续模型输入?
  7. 最终回答是否正确引用工具结果?

这个顺序能把问题拆开:先验证模型和工具能力,再验证 Agent loop。

工程建议

在真实项目里,不要只把 Agent 当作一个函数调用。至少要围绕它建立:

  • 输入校验。
  • 工具调用日志。
  • 最大步骤数。
  • 错误分类。
  • 评估样例。
  • 人工接管路径。

这会让 Agent 从“演示能跑”变成“失败可解释、可恢复、可改进”。

完整示例

python
from __future__ import annotations

import sys
from pathlib import Path

sys.path.append(str(Path(__file__).resolve().parents[1]))

from langchain.agents import create_agent
from langchain_core.messages import AIMessage
from langchain_core.tools import tool
from langchain_ollama import ChatOllama

from examples.common.config import load_config, ollama_client_kwargs
from examples.common.ollama_check import assert_model_available, assert_ollama_running


@tool
def add(a: int, b: int) -> int:
    """Add two integers and return the sum."""
    return a + b


@tool
def multiply(a: int, b: int) -> int:
    """Multiply two integers and return the product."""
    return a * b


def _final_message_content(agent_response: dict) -> str:
    messages = agent_response.get("messages", [])
    if not messages:
        return ""

    final_message = messages[-1]
    if isinstance(final_message, AIMessage):
        return str(final_message.content)
    return str(getattr(final_message, "content", final_message))


def main() -> None:
    config = load_config()
    try:
        assert_ollama_running(config.ollama_base_url)
        assert_model_available(config.ollama_base_url, config.ollama_model)
    except RuntimeError as exc:
        print(f"错误:{exc}")
        return

    model = ChatOllama(
        model=config.ollama_model,
        base_url=config.ollama_base_url,
        client_kwargs=ollama_client_kwargs(),
        temperature=0,
    )
    agent = create_agent(
        model=model,
        tools=[add, multiply],
        system_prompt=(
            "You are a concise arithmetic assistant. Use the provided tools for arithmetic "
            "instead of doing calculations mentally. Give the final answer briefly in Chinese."
        ),
    )

    print("LangChain create_agent 工具示例。输入 `exit` 或 `quit` 退出。")
    while True:
        user_input = input("\n你:").strip()
        if user_input.lower() in {"exit", "quit"}:
            break
        if not user_input:
            continue

        try:
            response = agent.invoke({"messages": [{"role": "user", "content": user_input}]})
        except Exception as exc:  # Local model tool calls can fail because of malformed args.
            print(f"错误:Agent 执行失败:{exc}")
            print("建议:先运行 examples/03_tools_cli.py 检查模型是否能稳定产生工具调用。")
            continue

        print(f"AI:{_final_message_content(response)}")


if __name__ == "__main__":
    main()