LangChain 智能体实战:构建技术学习规划 Agent

文章目录

  • 前言
  • [一. 理解 LangChain Agent](#一. 理解 LangChain Agent)
    • [1.1 什么是 Agent](#1.1 什么是 Agent)
    • [1.2 Agent 和普通聊天模型的区别](#1.2 Agent 和普通聊天模型的区别)
    • [1.3 Agent 和手动工具调用的区别](#1.3 Agent 和手动工具调用的区别)
  • [二. `create_agent()` 的基本使用](#二. create_agent() 的基本使用)
    • [2.1 实测环境](#2.1 实测环境)
    • [2.2 初始化模型](#2.2 初始化模型)
    • [2.3 创建最小 Agent](#2.3 创建最小 Agent)
    • [2.4 调用 Agent](#2.4 调用 Agent)
  • [三. 实战:技术学习规划 Agent](#三. 实战:技术学习规划 Agent)
    • [3.1 设计自己的工具](#3.1 设计自己的工具)
    • [3.2 创建学习规划 Agent](#3.2 创建学习规划 Agent)
    • [3.3 真实调用 Agent](#3.3 真实调用 Agent)
  • [四. 让 Agent 返回结构化结果](#四. 让 Agent 返回结构化结果)
    • [4.1 Agent 的 `response_format`](#4.1 Agent 的 response_format)
    • [4.2 定义 `StudyPlan`](#4.2 定义 StudyPlan)
    • [4.3 获取真实的结构化结果](#4.3 获取真实的结构化结果)
    • [4.4 `ToolStrategy` 的错误处理参数](#4.4 ToolStrategy 的错误处理参数)
  • [五. 流式观察 Agent 的执行过程](#五. 流式观察 Agent 的执行过程)
    • [5.1 为什么 Agent 更需要流式输出](#5.1 为什么 Agent 更需要流式输出)
    • [5.2 常用 `stream_mode`](#5.2 常用 stream_mode)
    • [5.3 使用 `updates` 模式](#5.3 使用 updates 模式)
  • [六. Agent 开发中的常见问题](#六. Agent 开发中的常见问题)
    • [6.1 Agent 为什么没有调用工具](#6.1 Agent 为什么没有调用工具)
    • [6.2 Agent 会不会无限调用工具](#6.2 Agent 会不会无限调用工具)
    • [6.3 工具越多越好吗](#6.3 工具越多越好吗)
    • [6.4 Agent 是否自动拥有长期记忆](#6.4 Agent 是否自动拥有长期记忆)
    • [6.5 有副作用的工具能否直接执行](#6.5 有副作用的工具能否直接执行)
  • [七. 总结](#七. 总结)

前言

在上一篇 LangChain 1.2 的学习中,我们已经掌握了消息、工具调用和结构化输出。通过手动编排工具调用,我们可以让模型生成工具名称与参数,再由 Python 程序执行工具,最后使用 ToolMessage 把结果传回模型。

但是,当任务需要连续调用多个工具时,手动编排很快会出现几个问题:

  1. 我们需要不断判断模型是否还要调用工具。
  2. 每一次工具执行都要维护对应的 tool_call_id。
  3. 需要自己组织模型、工具、消息和停止条件之间的循环。
  4. 如果还要增加流式输出、结构化结果和错误重试,代码会继续膨胀。

这正是 Agent 要解决的问题。

本期我们继续使用 LangChain 1.2.12,构建一个属于自己的"技术学习规划 Agent"。它可以查询某个技术主题的学习要点、计算每天的学习时间,并返回自然语言计划或结构化 StudyPlan。

本文中的模型调用、工具调用、流式事件和结构化输出都已经在本地使用 deepseek-v4-flash 重新执行。模型回答具有非确定性,重新运行时具体措辞可能发生变化,但消息类型、工具结果和结构化字段应与代码契约保持一致。

一. 理解 LangChain Agent

1.1 什么是 Agent

在大模型应用中,Agent 通常是一个以聊天模型为决策核心、可以根据任务选择工具并持续执行,直到满足停止条件的软件系统。

Agent 的重点不是"模型回答了一句话",而是"模型能够根据当前状态决定下一步行动,并利用行动结果继续推进任务"。

一个 Agent 通常涉及以下能力:

  • 理解用户目标。
  • 判断是否需要调用工具。
  • 从多个工具中选择合适的工具。
  • 生成工具参数。
  • 读取工具返回结果。
  • 决定继续调用工具,还是生成最终回答。

实际项目不一定同时具备复杂规划、长期记忆等所有能力,但"行动"是 Agent 与普通模型调用之间最明显的区别。

1.2 Agent 和普通聊天模型的区别

普通聊天模型调用通常只有一次输入和一次输出:

text 复制代码
用户问题 → 聊天模型 → AIMessage

Agent 则可能在模型和工具之间循环多次:
#mermaid-svg-RgymJM6p43O7vc5R{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-RgymJM6p43O7vc5R .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-RgymJM6p43O7vc5R .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-RgymJM6p43O7vc5R .error-icon{fill:#552222;}#mermaid-svg-RgymJM6p43O7vc5R .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-RgymJM6p43O7vc5R .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-RgymJM6p43O7vc5R .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-RgymJM6p43O7vc5R .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-RgymJM6p43O7vc5R .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-RgymJM6p43O7vc5R .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-RgymJM6p43O7vc5R .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-RgymJM6p43O7vc5R .marker{fill:#333333;stroke:#333333;}#mermaid-svg-RgymJM6p43O7vc5R .marker.cross{stroke:#333333;}#mermaid-svg-RgymJM6p43O7vc5R svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-RgymJM6p43O7vc5R p{margin:0;}#mermaid-svg-RgymJM6p43O7vc5R .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-RgymJM6p43O7vc5R .cluster-label text{fill:#333;}#mermaid-svg-RgymJM6p43O7vc5R .cluster-label span{color:#333;}#mermaid-svg-RgymJM6p43O7vc5R .cluster-label span p{background-color:transparent;}#mermaid-svg-RgymJM6p43O7vc5R .label text,#mermaid-svg-RgymJM6p43O7vc5R span{fill:#333;color:#333;}#mermaid-svg-RgymJM6p43O7vc5R .node rect,#mermaid-svg-RgymJM6p43O7vc5R .node circle,#mermaid-svg-RgymJM6p43O7vc5R .node ellipse,#mermaid-svg-RgymJM6p43O7vc5R .node polygon,#mermaid-svg-RgymJM6p43O7vc5R .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-RgymJM6p43O7vc5R .rough-node .label text,#mermaid-svg-RgymJM6p43O7vc5R .node .label text,#mermaid-svg-RgymJM6p43O7vc5R .image-shape .label,#mermaid-svg-RgymJM6p43O7vc5R .icon-shape .label{text-anchor:middle;}#mermaid-svg-RgymJM6p43O7vc5R .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-RgymJM6p43O7vc5R .rough-node .label,#mermaid-svg-RgymJM6p43O7vc5R .node .label,#mermaid-svg-RgymJM6p43O7vc5R .image-shape .label,#mermaid-svg-RgymJM6p43O7vc5R .icon-shape .label{text-align:center;}#mermaid-svg-RgymJM6p43O7vc5R .node.clickable{cursor:pointer;}#mermaid-svg-RgymJM6p43O7vc5R .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-RgymJM6p43O7vc5R .arrowheadPath{fill:#333333;}#mermaid-svg-RgymJM6p43O7vc5R .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-RgymJM6p43O7vc5R .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-RgymJM6p43O7vc5R .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RgymJM6p43O7vc5R .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-RgymJM6p43O7vc5R .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RgymJM6p43O7vc5R .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-RgymJM6p43O7vc5R .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-RgymJM6p43O7vc5R .cluster text{fill:#333;}#mermaid-svg-RgymJM6p43O7vc5R .cluster span{color:#333;}#mermaid-svg-RgymJM6p43O7vc5R div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-RgymJM6p43O7vc5R .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-RgymJM6p43O7vc5R rect.text{fill:none;stroke-width:0;}#mermaid-svg-RgymJM6p43O7vc5R .icon-shape,#mermaid-svg-RgymJM6p43O7vc5R .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-RgymJM6p43O7vc5R .icon-shape p,#mermaid-svg-RgymJM6p43O7vc5R .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-RgymJM6p43O7vc5R .icon-shape .label rect,#mermaid-svg-RgymJM6p43O7vc5R .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-RgymJM6p43O7vc5R .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-RgymJM6p43O7vc5R .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-RgymJM6p43O7vc5R :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
用户目标
模型节点
是否需要工具
最终回答
工具节点
ToolMessage

这里的模型节点负责决策,工具节点负责执行。工具结果不会自动成为最终答案,而是会作为新的上下文重新交给模型。

1.3 Agent 和手动工具调用的区别

对比项 手动工具调用 create_agent()
工具选择 应用程序读取 tool_calls 后自行分发 Agent 图中的工具节点自动分发
结果回传 手动构造 ToolMessage 自动写入消息状态
多轮循环 自己编写循环和停止判断 模型节点与工具节点自动循环
最终状态 通常自己维护消息列表 返回包含 messages 的状态字典
流式进度 需要额外封装 支持 updates、messages、custom 等模式
结构化结果 模型单独使用 with_structured_output() Agent 使用 response_format

所以,Agent 并没有改变工具调用的底层消息协议,而是把我们上一期手动实现的循环变成了一个可复用的图结构。

二. create_agent() 的基本使用

2.1 实测环境

本文使用的环境如下:

项目 实际版本
验证日期 2026-08-24
Python 3.13.12
LangChain 1.2.12
langchain-core 1.2.18
langchain-deepseek 1.0.1
LangGraph 1.1.6
模型 deepseek-v4-flash

需要安装的核心依赖如下:

bash 复制代码
pip install -U "langchain==1.2.12" "langchain-core==1.2.18" "langchain-deepseek==1.0.1" pydantic

API Key 仍然通过环境变量传入:

powershell 复制代码
$env:DEEPSEEK_API_KEY = "your-api-key"

2.2 初始化模型

首先创建本文使用的聊天模型:

python 复制代码
import os

from langchain_deepseek import ChatDeepSeek


model = ChatDeepSeek(
    model="deepseek-v4-flash",
    extra_body={"thinking": {"type": "disabled"}},
    temperature=0,
    timeout=60,
    max_retries=1,
    api_key=os.environ["DEEPSEEK_API_KEY"],
)

这里直接传入模型对象,而不是只写模型字符串。这样可以明确设置超时时间、重试次数和 API Key 来源。DeepSeek-V4-Flash 默认开启思考模式;本文需要使用 ToolStrategy 强制工具选择,而该组合在当前接口中不受支持,因此通过 extra_body 显式关闭思考模式。

2.3 创建最小 Agent

LangChain 1.x 使用统一的 create_agent() 入口创建 Agent:

python 复制代码
from langchain.agents import create_agent


agent = create_agent(
    model=model,
    tools=[],
    system_prompt="你是技术学习规划助手。",
    name="study_planner",
)

print("agent_type =", type(agent).__name__)
print("nodes =", sorted(agent.get_graph().nodes.keys()))

几个核心参数的作用如下:

参数 作用
model Agent 使用的聊天模型,可以传模型字符串或模型对象
tools Agent 可以调用的工具列表
system_prompt 规定 Agent 的身份、目标和行为边界
response_format 规定最终结构化结果的 Schema 或策略
middleware 在模型调用、工具调用等阶段插入中间件
checkpointer 保存图状态,用于多轮会话或恢复执行
name 为 Agent 图设置稳定名称

create_agent() 返回的不是某个旧版本的 AgentExecutor,而是 LangGraph 的 CompiledStateGraph。

我在本地打印 Agent 类型和图节点,实际结果如下:

text 复制代码
agent_type = CompiledStateGraph
nodes = ['__end__', '__start__', 'model']

因为这里传入的是空工具列表,所以最小图只有 __start__ → model → __end__。当我们在下一节真正绑定工具后,图中才会增加 tools 节点,以及 model → tools → model 的循环。

2.4 调用 Agent

Agent 的输入是一个状态更新,其中最常用的字段是 messages:

python 复制代码
result = agent.invoke({
    "messages": [
        {
            "role": "user",
            "content": "请帮我制定学习计划。",
        }
    ]
})

返回值是一个字典,result["messages"] 保存本次执行形成的完整消息轨迹。即使底层发生了多次模型调用和工具调用,我们也可以从这个列表中还原执行过程。

三. 实战:技术学习规划 Agent

3.1 设计自己的工具

  1. query_topic_profile:查询某个技术主题的前置知识、核心要点和实践任务。
  2. allocate_study_time:把总学习时间分配到指定天数。

先准备一份小型主题资料:

python 复制代码
TOPIC_PROFILES = {
    "LangChain Agent": {
        "prerequisites": ["消息对象", "工具调用", "结构化输出"],
        "core_points": [
            "create_agent 的统一入口",
            "模型与工具节点的循环",
            "messages 状态与 ToolMessage",
            "流式观察 Agent 执行进度",
        ],
        "practice": "实现一个可以查询学习资料并生成计划的 Agent",
    }
}

接下来定义两个工具:

python 复制代码
from langchain.tools import tool


@tool(parse_docstring=True)
def query_topic_profile(topic: str) -> dict:
    """查询技术主题的学习要点。

    Args:
        topic: 要学习的技术主题,例如 LangChain Agent。
    """
    return TOPIC_PROFILES.get(
        topic,
        {
            "prerequisites": [],
            "core_points": [f"理解 {topic} 的基本概念"],
            "practice": f"完成一个 {topic} 最小示例",
        },
    )


@tool(parse_docstring=True)
def allocate_study_time(total_minutes: int, days: int) -> dict:
    """把总学习时间平均分配到指定天数。

    Args:
        total_minutes: 总学习分钟数,必须大于 0。
        days: 学习天数,范围为 1 到 7。
    """
    if total_minutes <= 0:
        raise ValueError("total_minutes 必须大于 0")
    if not 1 <= days <= 7:
        raise ValueError("days 必须在 1 到 7 之间")

    base, remainder = divmod(total_minutes, days)
    daily_minutes = [
        base + (1 if index < remainder else 0)
        for index in range(days)
    ]
    return {
        "total_minutes": total_minutes,
        "days": days,
        "daily_minutes": daily_minutes,
    }

工具描述是模型选择工具的重要依据,所以 docstring 需要说明工具用途和参数含义。真正进入生产环境时,还要在工具内部完成参数校验、权限校验和异常处理,不能只依赖模型生成正确参数。

3.2 创建学习规划 Agent

把模型、工具和系统提示词交给 create_agent():

python 复制代码
agent = create_agent(
    model=model,
    tools=[query_topic_profile, allocate_study_time],
    system_prompt=(
        "你是技术学习规划助手。处理学习计划时,必须先调用 "
        "query_topic_profile 查询主题要点,再调用 "
        "allocate_study_time 计算每天的时间。"
        "工具返回什么就基于什么回答,不要虚构课程资料。"
    ),
    name="study_planner",
)

print("agent_type =", type(agent).__name__)
print("nodes =", sorted(agent.get_graph().nodes.keys()))

本地重新读取实战 Agent 的图节点,真实结果为:

text 复制代码
agent_type = CompiledStateGraph
nodes = ['__end__', '__start__', 'model', 'tools']

真实导出的图结构可以简化成下面这样:
#mermaid-svg-WeSnOQdH8KgskCei{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WeSnOQdH8KgskCei .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WeSnOQdH8KgskCei .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WeSnOQdH8KgskCei .error-icon{fill:#552222;}#mermaid-svg-WeSnOQdH8KgskCei .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WeSnOQdH8KgskCei .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WeSnOQdH8KgskCei .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WeSnOQdH8KgskCei .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WeSnOQdH8KgskCei .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WeSnOQdH8KgskCei .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WeSnOQdH8KgskCei .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WeSnOQdH8KgskCei .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WeSnOQdH8KgskCei .marker.cross{stroke:#333333;}#mermaid-svg-WeSnOQdH8KgskCei svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WeSnOQdH8KgskCei p{margin:0;}#mermaid-svg-WeSnOQdH8KgskCei .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WeSnOQdH8KgskCei .cluster-label text{fill:#333;}#mermaid-svg-WeSnOQdH8KgskCei .cluster-label span{color:#333;}#mermaid-svg-WeSnOQdH8KgskCei .cluster-label span p{background-color:transparent;}#mermaid-svg-WeSnOQdH8KgskCei .label text,#mermaid-svg-WeSnOQdH8KgskCei span{fill:#333;color:#333;}#mermaid-svg-WeSnOQdH8KgskCei .node rect,#mermaid-svg-WeSnOQdH8KgskCei .node circle,#mermaid-svg-WeSnOQdH8KgskCei .node ellipse,#mermaid-svg-WeSnOQdH8KgskCei .node polygon,#mermaid-svg-WeSnOQdH8KgskCei .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WeSnOQdH8KgskCei .rough-node .label text,#mermaid-svg-WeSnOQdH8KgskCei .node .label text,#mermaid-svg-WeSnOQdH8KgskCei .image-shape .label,#mermaid-svg-WeSnOQdH8KgskCei .icon-shape .label{text-anchor:middle;}#mermaid-svg-WeSnOQdH8KgskCei .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WeSnOQdH8KgskCei .rough-node .label,#mermaid-svg-WeSnOQdH8KgskCei .node .label,#mermaid-svg-WeSnOQdH8KgskCei .image-shape .label,#mermaid-svg-WeSnOQdH8KgskCei .icon-shape .label{text-align:center;}#mermaid-svg-WeSnOQdH8KgskCei .node.clickable{cursor:pointer;}#mermaid-svg-WeSnOQdH8KgskCei .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WeSnOQdH8KgskCei .arrowheadPath{fill:#333333;}#mermaid-svg-WeSnOQdH8KgskCei .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WeSnOQdH8KgskCei .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WeSnOQdH8KgskCei .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WeSnOQdH8KgskCei .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WeSnOQdH8KgskCei .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WeSnOQdH8KgskCei .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WeSnOQdH8KgskCei .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WeSnOQdH8KgskCei .cluster text{fill:#333;}#mermaid-svg-WeSnOQdH8KgskCei .cluster span{color:#333;}#mermaid-svg-WeSnOQdH8KgskCei div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WeSnOQdH8KgskCei .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WeSnOQdH8KgskCei rect.text{fill:none;stroke-width:0;}#mermaid-svg-WeSnOQdH8KgskCei .icon-shape,#mermaid-svg-WeSnOQdH8KgskCei .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WeSnOQdH8KgskCei .icon-shape p,#mermaid-svg-WeSnOQdH8KgskCei .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WeSnOQdH8KgskCei .icon-shape .label rect,#mermaid-svg-WeSnOQdH8KgskCei .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WeSnOQdH8KgskCei .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WeSnOQdH8KgskCei .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WeSnOQdH8KgskCei :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不需要工具
需要工具
工具结果写回消息状态
start
model
end
tools

model 节点有两条路径:如果模型返回最终回答,流程进入 __end__;如果模型返回 tool_calls,流程进入 tools,执行完成后再回到 model。

这里的系统提示词明确了三件事:

  • Agent 的角色是技术学习规划助手。
  • 生成计划前必须调用两个工具。
  • 最终回答必须基于工具结果,不能擅自编造资料。

系统提示词可以帮助模型理解业务目标,但它不是安全边界。对于删除数据、支付、发消息等有副作用的工具,仍然需要在应用层增加权限和人工确认。

3.3 真实调用 Agent

使用下面的输入发起调用:

python 复制代码
result = agent.invoke({
    "messages": [
        {
            "role": "user",
            "content": (
                "我想用 3 天学习 LangChain Agent,总共安排 270 分钟。"
                "请先查询学习要点,再计算每天时间,最后给出计划。"
            ),
        }
    ]
})

print("result_type =", type(result).__name__)
print("message_count =", len(result["messages"]))

本次真实运行返回:

text 复制代码
result_type = dict
message_count = 5

消息轨迹如下。为了方便阅读,我只移除了响应元数据,并将工具返回的 JSON 按字段换行;消息类型、工具名称、参数和结果均来自本次真实执行:

text 复制代码
1. HumanMessage: 我想用 3 天学习 LangChain Agent,总共安排 270 分钟。请先查询学习要点,再计算每天时间,最后给出计划。
2. AIMessage: tool_calls=[
    {'name': 'query_topic_profile', 'args': {'topic': 'LangChain Agent'}},
    {'name': 'allocate_study_time', 'args': {'total_minutes': 270, 'days': 3}}
]
3. ToolMessage(query_topic_profile): {
    "prerequisites": ["消息对象", "工具调用", "结构化输出"],
    "core_points": [
        "create_agent 的统一入口",
        "模型与工具节点的循环",
        "messages 状态与 ToolMessage",
        "流式观察 Agent 执行进度"
    ],
    "practice": "实现一个可以查询学习资料并生成计划的 Agent"
}
4. ToolMessage(allocate_study_time): {
    "total_minutes": 270,
    "days": 3,
    "daily_minutes": [90, 90, 90]
}
5. AIMessage: # LangChain Agent 3 天学习计划

最终 AIMessage 中的核心计划内容为:

text 复制代码
# LangChain Agent 3 天学习计划

**总时长:270 分钟 → 每天 90 分钟**

## 前置知识(建议先确认已掌握)
- 消息对象
- 工具调用
- 结构化输出

## 核心要点
1. `create_agent` 的统一入口
2. 模型与工具节点的循环
3. `messages` 状态与 `ToolMessage`
4. 流式观察 Agent 执行进度

## 每日安排

| 天数 | 时长 | 学习内容 |
|------|------|----------|
| 第 1 天 | 90 分钟 | 前置知识回顾 + `create_agent` 统一入口 |
| 第 2 天 | 90 分钟 | 模型与工具节点的循环、`messages` 状态与 `ToolMessage` |
| 第 3 天 | 90 分钟 | 流式观察 Agent 执行进度 + 完成实践项目 |

## 实践项目
实现一个可以查询学习资料并生成计划的 Agent。

> 说明:以上要点与实践项目均来自主题查询结果,每天 90 分钟为 270 分钟平均分配到 3 天的计算结果。

这次模型在同一个 AIMessage 中生成了两个工具调用。因为两个工具之间没有数据依赖,Agent 可以并行执行它们,然后把两个 ToolMessage 一起交给模型生成最终回答。
allocate_study_time query_topic_profile model 节点 Agent 图 用户 allocate_study_time query_topic_profile model 节点 Agent 图 用户 #mermaid-svg-VkNJGmZmqmxuHMX7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VkNJGmZmqmxuHMX7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VkNJGmZmqmxuHMX7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VkNJGmZmqmxuHMX7 .error-icon{fill:#552222;}#mermaid-svg-VkNJGmZmqmxuHMX7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VkNJGmZmqmxuHMX7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VkNJGmZmqmxuHMX7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VkNJGmZmqmxuHMX7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VkNJGmZmqmxuHMX7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VkNJGmZmqmxuHMX7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VkNJGmZmqmxuHMX7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VkNJGmZmqmxuHMX7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VkNJGmZmqmxuHMX7 .marker.cross{stroke:#333333;}#mermaid-svg-VkNJGmZmqmxuHMX7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VkNJGmZmqmxuHMX7 p{margin:0;}#mermaid-svg-VkNJGmZmqmxuHMX7 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VkNJGmZmqmxuHMX7 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-VkNJGmZmqmxuHMX7 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VkNJGmZmqmxuHMX7 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-VkNJGmZmqmxuHMX7 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-VkNJGmZmqmxuHMX7 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-VkNJGmZmqmxuHMX7 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-VkNJGmZmqmxuHMX7 .sequenceNumber{fill:white;}#mermaid-svg-VkNJGmZmqmxuHMX7 #sequencenumber{fill:#333;}#mermaid-svg-VkNJGmZmqmxuHMX7 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-VkNJGmZmqmxuHMX7 .messageText{fill:#333;stroke:none;}#mermaid-svg-VkNJGmZmqmxuHMX7 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VkNJGmZmqmxuHMX7 .labelText,#mermaid-svg-VkNJGmZmqmxuHMX7 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-VkNJGmZmqmxuHMX7 .loopText,#mermaid-svg-VkNJGmZmqmxuHMX7 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-VkNJGmZmqmxuHMX7 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-VkNJGmZmqmxuHMX7 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VkNJGmZmqmxuHMX7 .noteText,#mermaid-svg-VkNJGmZmqmxuHMX7 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-VkNJGmZmqmxuHMX7 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VkNJGmZmqmxuHMX7 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VkNJGmZmqmxuHMX7 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-VkNJGmZmqmxuHMX7 .actorPopupMenu{position:absolute;}#mermaid-svg-VkNJGmZmqmxuHMX7 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-VkNJGmZmqmxuHMX7 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-VkNJGmZmqmxuHMX7 .actor-man circle,#mermaid-svg-VkNJGmZmqmxuHMX7 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-VkNJGmZmqmxuHMX7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} par 查询主题资料 计算学习时间 3 天、270 分钟学习 Agent HumanMessage + 工具定义 两个 tool_calls topic=LangChain Agent 学习要点 total_minutes=270, days=3 90, 90, 90 两个 ToolMessage 最终 AIMessage 展示学习计划

四. 让 Agent 返回结构化结果

4.1 Agent 的 response_format

上一期我们使用 model.with_structured_output() 约束单次模型调用。Agent 则通过 create_agent() 的 response_format 参数规定最终响应结构。

LangChain 提供两种主要策略:

策略 原理 适用情况
ProviderStrategy 使用模型提供方的原生结构化输出能力 提供方和当前模型明确支持原生结构化输出
ToolStrategy 把 Schema 转换成特殊工具,通过工具参数返回结构化数据 模型支持工具调用,但原生结构化能力不可用或不稳定

这里使用 ToolStrategy

4.2 定义 StudyPlan

首先使用 Pydantic 定义单日任务和完整学习计划:

python 复制代码
from pydantic import BaseModel, Field


class DayPlan(BaseModel):
    """单日学习任务。"""

    day: int = Field(description="第几天,从 1 开始")
    focus: str = Field(description="当天学习重点")
    minutes: int = Field(description="当天学习分钟数")
    task: str = Field(description="当天需要完成的实践任务")


class StudyPlan(BaseModel):
    """技术学习计划。"""

    topic: str = Field(description="学习主题")
    total_minutes: int = Field(description="总学习分钟数")
    days: list[DayPlan] = Field(description="按顺序排列的每日计划")
    checkpoints: list[str] = Field(description="验收学习效果的检查点")

接下来创建结构化 Agent:

python 复制代码
from langchain.agents.structured_output import ToolStrategy


structured_agent = create_agent(
    model=model,
    tools=[query_topic_profile, allocate_study_time],
    system_prompt=(
        "你是技术学习规划助手。必须先查询主题资料并计算每日时间,"
        "再根据工具结果生成结构化学习计划。"
    ),
    response_format=ToolStrategy(
        schema=StudyPlan,
        tool_message_content="学习计划已按 StudyPlan 结构生成。",
        handle_errors=True,
    ),
    name="structured_study_planner",
)

tool_message_content 只改变结构化输出成功后写入消息历史的提示文本,不会改变最终的 structured_response。handle_errors=True 表示结构不满足 Schema 时,将错误反馈给模型并尝试重新生成。

4.3 获取真实的结构化结果

调用结构化 Agent:

python 复制代码
import json


result = structured_agent.invoke({
    "messages": [
        {
            "role": "user",
            "content": (
                "请为 LangChain Agent 生成 3 天学习计划,总时间 270 分钟。"
                "每天都要有学习重点和实践任务,最后给出验收检查点。"
            ),
        }
    ]
})

plan = result["structured_response"]
print("structured_response_type =", type(plan).__name__)
print(json.dumps(plan.model_dump(), ensure_ascii=False, indent=2))

本次使用 DeepSeek 实际运行后,返回类型为:

text 复制代码
structured_response_type = StudyPlan

完整结构化结果如下:

json 复制代码
{
  "topic": "LangChain Agent",
  "total_minutes": 270,
  "days": [
    {
      "day": 1,
      "focus": "前置基础:消息对象、工具调用与结构化输出",
      "minutes": 90,
      "task": "定义 2 个工具(如查询学习资料、计算学习时间),用结构化输出(Pydantic 模型)约束工具参数,并手动构造 HumanMessage / AIMessage / ToolMessage 走通一次工具调用链路。"
    },
    {
      "day": 2,
      "focus": "create_agent 统一入口与「模型---工具」循环",
      "minutes": 90,
      "task": "用 create_agent 把第 1 天的工具接入模型,观察模型节点与工具节点交替执行的循环过程,打印每一步的 messages 状态,确认 ToolMessage 被正确回填。"
    },
    {
      "day": 3,
      "focus": "messages 状态管理与流式观察执行进度",
      "minutes": 90,
      "task": "实现完整 Agent:可查询学习资料并生成学习计划;用流式输出观察 Agent 的思考与工具调用进度,并处理多轮追问(基于历史 messages 继续对话)。"
    }
  ],
  "checkpoints": [
    "能说清消息对象、工具调用、结构化输出三者的关系,并手写一次完整的工具调用链路",
    "能用 create_agent 注册工具并跑通「模型---工具」循环,解释循环的终止条件",
    "能说明 messages 状态如何累积、ToolMessage 在其中的作用",
    "能通过流式输出定位 Agent 卡在哪一步或调用了哪个工具",
    "最终 Agent 能根据用户需求查询学习资料并输出结构化的 3 天学习计划"
  ]
}

此时后端不需要再从一段 Markdown 文本中提取字段,可以直接读取:

python 复制代码
print(plan.topic)
print(plan.days[0].minutes)
print(plan.checkpoints)

4.4 ToolStrategy 的错误处理参数

handle_errors 支持多种配置方式:

配置 行为
True 捕获所有结构化输出错误,并使用默认提示让模型重试
False 不重试,直接把异常抛给应用程序
字符串 捕获错误后,把指定字符串作为反馈内容
异常类型 只捕获指定类型的异常
异常类型元组 只捕获元组中的异常类型
处理函数 接收异常并动态生成反馈字符串

例如:

python 复制代码
response_format = ToolStrategy(
    schema=StudyPlan,
    tool_message_content="学习计划结构生成成功。",
    handle_errors="计划字段不完整,请严格按照 StudyPlan 重新生成。",
)

这段代码展示的是错误处理配置方式,不作为独立运行案例。生产环境中还应该在 Agent 外层设置超时、重试上限和失败降级,避免模型在无法满足 Schema 时持续消耗 Token。

五. 流式观察 Agent 的执行过程

5.1 为什么 Agent 更需要流式输出

普通模型调用可能只等待一次模型响应,而 Agent 内部还可能包含多次模型调用和多个工具调用。如果一直等到 invoke() 完成,用户只能看到长时间的空白页面。

流式输出可以把 Agent 当前执行到哪一步及时展示出来,例如:

  • 模型正在选择工具。
  • 工具已经开始执行。
  • 某个工具返回了结果。
  • 模型开始生成最终回答。

5.2 常用 stream_mode

模式 输出内容 适用场景
values 每个步骤执行后的完整状态 状态快照、调试和持久化
updates 每个步骤发生变化的增量状态 展示 Agent 执行进度
messages 模型 Token 与节点元数据 聊天窗口打字机效果
custom 工具或节点主动发送的业务进度 文件处理进度、报告生成进度
checkpoints 检查点状态 工作流恢复,需要配置 checkpointer
tasks 任务开始、结束、结果和错误 任务生命周期监控
debug 更详细的任务与状态事件 开发阶段调试

对初学者来说,最容易理解的是 updates:它可以让我们看到模型节点和工具节点每一步更新了什么。

5.3 使用 updates 模式

下面对同一个学习规划 Agent 发起流式调用:

python 复制代码
request = {
    "messages": [
        {
            "role": "user",
            "content": (
                "我想用 2 天学习 LangChain Agent,总共安排 150 分钟。"
                "请查询学习要点并计算时间。"
            ),
        }
    ]
}

for chunk in agent.stream(request, stream_mode="updates"):
    for step, data in chunk.items():
        message = data["messages"][-1]
        if getattr(message, "tool_calls", None):
            names = [call["name"] for call in message.tool_calls]
            print(f"step={step}, tool_calls={names}")
        elif type(message).__name__ == "ToolMessage":
            print(
                f"step={step}, tool={message.name}, "
                f"result={message.content}"
            )
        else:
            print(f"step={step}, final={message.content}")

本次真实流式事件如下:

text 复制代码
step=model, tool_calls=['query_topic_profile', 'allocate_study_time']
step=tools, tool=query_topic_profile, result={"prerequisites": ["消息对象", "工具调用", "结构化输出"], "core_points": ["create_agent 的统一入口", "模型与工具节点的循环", "messages 状态与 ToolMessage", "流式观察 Agent 执行进度"], "practice": "实现一个可以查询学习资料并生成计划的 Agent"}
step=tools, tool=allocate_study_time, result={"total_minutes": 150, "days": 2, "daily_minutes": [75, 75]}
step=model, final=以下是你的 2 天 LangChain Agent 学习计划(共 150 分钟,每天 75 分钟):

## 前置知识
- 消息对象
- 工具调用
- 结构化输出

## 核心要点
1. `create_agent` 的统一入口
2. 模型与工具节点的循环
3. `messages` 状态与 `ToolMessage`
4. 流式观察 Agent 执行进度

## 时间分配

| 天数 | 学习时间 |
|------|---------|
| 第 1 天 | 75 分钟 |
| 第 2 天 | 75 分钟 |

## 实践任务
实现一个可以查询学习资料并生成计划的 Agent。

建议第 1 天先补齐前置知识并理解 `create_agent` 入口与模型/工具循环,第 2 天深入 `messages` 状态、`ToolMessage` 与流式观察,并动手完成实践任务。

本次重新运行时,模型生成工具调用和流式返回工具结果的顺序都是 query_topic_profile、allocate_study_time。不过两个工具可以并行执行,不能把本次观察到的先后顺序当成稳定契约。

原因是两个工具可以并行执行,完成顺序不保证与声明顺序一致。因此业务代码应该根据工具名称和 tool_call_id 识别结果,不能依赖列表位置或事件先后顺序。

六. Agent 开发中的常见问题

6.1 Agent 为什么没有调用工具

常见原因包括:

  • 工具 docstring 没有清楚说明使用场景。
  • 用户问题不需要外部数据,模型可以直接回答。
  • 系统提示词没有明确业务步骤。
  • 当前模型不支持工具调用。
  • 多个工具描述过于相似,模型无法稳定区分。

解决时应该先检查工具名称、参数 Schema 和 docstring,再观察 AIMessage.tool_calls,而不是盲目提高提示词长度。

6.2 Agent 会不会无限调用工具

Agent 会在模型不再返回 tool_calls 时结束,但错误的提示词、失败的工具和不满足要求的结果可能让模型反复尝试。

生产环境需要配置:

  • 请求超时。
  • 工具自身的重试上限。
  • Agent 图的递归或步骤限制。
  • Token 和费用预算。
  • 失败后的降级回答。

6.3 工具越多越好吗

不是。工具越多,模型需要理解和区分的描述越多,相似工具也更容易被选错。

比较稳妥的做法是按业务场景只暴露必要工具,并让每个工具职责单一、名称清晰。需要大量工具时,可以进一步使用动态工具选择或分层编排,而不是一次把所有工具交给模型。

6.4 Agent 是否自动拥有长期记忆

不是。一次 invoke() 返回的 messages 只代表本次状态。如果下一次请求没有继续传入历史消息,也没有配置 checkpointer 或外部存储,Agent 不会自动记住上一轮内容。

短期记忆、长期记忆和上下文治理属于 Agent 的后续能力,不能仅凭创建了 create_agent() 就认为已经具备。

6.5 有副作用的工具能否直接执行

涉及支付、删除数据、发送邮件、发布内容、修改权限等操作时,不能因为模型生成了工具调用就直接执行。

应用层至少应该增加:

  • 用户身份和权限校验。
  • 参数白名单与业务规则校验。
  • 危险操作二次确认。
  • 幂等控制。
  • 操作审计与失败回滚。

Agent 负责生成行动建议,业务系统仍然要掌握最终执行权。

七. 总结

本期我们完成了 LangChain 1.2 Agent 的第一轮学习,并真实运行了一个技术学习规划 Agent。

通过这篇文章,我们掌握了以下内容:

  1. Agent 是模型与工具之间持续执行的循环系统,不是一次普通聊天模型调用。
  2. LangChain 1.x 使用 create_agent() 作为统一创建入口。
  3. create_agent() 返回 CompiledStateGraph,核心节点是 model 和 tools。
  4. Agent 的输入和输出都围绕消息状态展开,完整过程可以通过 result["messages"] 还原。
  5. 一个模型响应可以同时请求多个工具,工具结果的完成顺序不一定等于声明顺序。
  6. response_format=ToolStrategy(...) 可以让 Agent 返回 Pydantic 结构化对象。
  7. stream_mode="updates" 可以实时观察模型决策、工具执行和最终回答。
  8. Agent 自动完成的是编排循环,权限、安全、超时、成本和失败降级仍然由业务代码负责。

到这里,我们已经从"手动执行一次工具调用"迈进了"让 Agent 根据目标自动选择并组合工具"。下一步可以继续学习 Agent 的中间件、上下文与记忆,让这套执行循环具备更完整的工程能力。

感谢大家的阅读,我们下期继续学习 LangChain!

参考资料

相关推荐
打工仔折腾 AI1 小时前
AI Agent 工程化实战(二):5 分钟跑通 Deep Agent 并逐层拆解底层链路
java·数据库·人工智能·后端·python·langchain·ai agent 实战
云和数据.ChenGuang1 小时前
Python map() 函数详解:从语法、原理到实战用法
开发语言·人工智能·python·langchain·1024程序员节
小七-七牛开发者2 小时前
当 Agent 接手更多工作,我们还能把好关吗?
ai·openai·agent·claude
Carson带你学Android2 小时前
Claude Duet 发布:又一个手机Agent可以用了!
agent·ai编程
阿里云大数据AI技术3 小时前
阿里云Flink + Fluss年度发布:Streaming Agent 与湖流一体
人工智能·flink·agent
古雨蓝枫3 小时前
DeepSeek Harness 插件开发进阶:从 Host 工具到 Client UI
ai·ai编程·deepseek·dsh
Sailing4 小时前
【封闭结束3】ReAct 循环:从一次 Function Call 到多步 Agent
人工智能·面试·agent
前端双越老师4 小时前
程序员 1v1 访谈系列第七期:失业半年所想到的
agent·ai编程·全栈
xifangge20254 小时前
把 DeepSeek 搬回自己电脑:Ollama+OpenWebUI+AnythingLLM 本地私有部署(含离线一键包)
人工智能·deepseek