
文章目录
- 前言
- [一. 理解 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的错误处理参数)
- [4.1 Agent 的 `response_format`](#4.1 Agent 的
- [五. 流式观察 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 把结果传回模型。
但是,当任务需要连续调用多个工具时,手动编排很快会出现几个问题:
- 我们需要不断判断模型是否还要调用工具。
- 每一次工具执行都要维护对应的
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 和普通聊天模型的区别
普通聊天模型调用通常只有一次输入和一次输出:
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 设计自己的工具
query_topic_profile:查询某个技术主题的前置知识、核心要点和实践任务。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。
通过这篇文章,我们掌握了以下内容:
- 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!