欢迎光临
我们一直在努力

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

在这里插入图片描述

文章目录

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

前言

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

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

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

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

    在这里插入图片描述

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

    一. 理解 LangChain Agent

    1.1 什么是 Agent

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

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

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

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

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

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

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

    用户问题 → 聊天模型 → 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

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

    pip install -U "langchain==1.2.12" "langchain-core==1.2.18" "langchain-deepseek==1.0.1" pydantic

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

    $env:DEEPSEEK_API_KEY = "your-api-key"

    2.2 初始化模型

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

    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:

    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 类型和图节点,实际结果如下:

    agent_type = CompiledStateGraph
    nodes = ['__end__', '__start__', 'model']

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

    2.4 调用 Agent

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

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

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

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

    3.1 设计自己的工具

  • query_topic_profile:查询某个技术主题的前置知识、核心要点和实践任务。
  • allocate_study_time:把总学习时间分配到指定天数。
  • 先准备一份小型主题资料:

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

    接下来定义两个工具:

    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():

    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 的图节点,真实结果为:

    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

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

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

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

    本次真实运行返回:

    result_type = dict
    message_count = 5

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

    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 中的核心计划内容为:

    # 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 定义单日任务和完整学习计划:

    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:

    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:

    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 实际运行后,返回类型为:

    structured_response_type = StudyPlan

    完整结构化结果如下:

    {
    "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 文本中提取字段,可以直接读取:

    print(plan.topic)
    print(plan.days[0].minutes)
    print(plan.checkpoints)

    4.4 ToolStrategy 的错误处理参数

    handle_errors 支持多种配置方式:

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

    例如:

    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 发起流式调用:

    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}")

    本次真实流式事件如下:

    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。

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

  • Agent 是模型与工具之间持续执行的循环系统,不是一次普通聊天模型调用。
  • LangChain 1.x 使用 create_agent() 作为统一创建入口。
  • create_agent() 返回 CompiledStateGraph,核心节点是 model 和 tools。
  • Agent 的输入和输出都围绕消息状态展开,完整过程可以通过 result["messages"] 还原。
  • 一个模型响应可以同时请求多个工具,工具结果的完成顺序不一定等于声明顺序。
  • response_format=ToolStrategy(…) 可以让 Agent 返回 Pydantic 结构化对象。
  • stream_mode="updates" 可以实时观察模型决策、工具执行和最终回答。
  • Agent 自动完成的是编排循环,权限、安全、超时、成本和失败降级仍然由业务代码负责。
  • 到这里,我们已经从“手动执行一次工具调用”迈进了“让 Agent 根据目标自动选择并组合工具”。下一步可以继续学习 Agent 的中间件、上下文与记忆,让这套执行循环具备更完整的工程能力。

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

    参考资料

    • LangChain Agents 官方文档
    • LangChain Structured output 官方文档
    • LangChain Streaming 官方文档
    • LangChain create_agent API Reference
    • DeepSeek API 文档
    • DeepSeek 思考模式说明
    赞(0)
    未经允许不得转载:171主机测评 » LangChain 智能体实战:构建技术学习规划 Agent
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址