1. 本章目标 #
到第 38 章为止,我们已经把「自己攒零件」这条路走完了一整圈:用 create_agent 装工具和护栏(第 8~10 章)、用 Store 做长期记忆(第 16 章)、用 LangGraph 显式编排审批流(第 19~28 章)、用 LangSmith 建评测闭环(第 29~38 章)。
现在换一条路:官方已经把这些零件攒成了一台整机——deepagents。
一句话先说清它是什么:Deep Agents 是一个「智能体驾驭层」(agent harness),内核仍是第 2 章那个「模型 ↔ 工具」循环,但预装了虚拟文件系统、子智能体、长期记忆、上下文压缩、人工审批这些「真实任务才会遇到」的能力。
本章不教怎么配置它(那是第 40 章之后的事),只做三件事:
一、把
create_deep_agent拆开,看清它和create_agent到底差在哪。(答案很反直觉:图的形状几乎没变,变的是工具面) 二、用实测数据说清这些内置能力的代价。(简单问题上,输入 token 会涨 7.4 倍) 三、给出一张选型判断表:什么时候上 harness,什么时候退回create_agent或自己写图。
学完你应能:
- 说清 framework(框架)/ runtime(运行时)/ harness(驾驭层) 三层分工,以及 LangChain、LangGraph、Deep Agents 各在哪一层
- 用
get_graph()打开一个 deep agent,指出它比create_agent多了什么、少了什么 - 解释为什么「装了 9 个内置工具」却「图只多了 1 个节点」
- 说清 Deep Agents 的内置能力装在哪里(不在系统提示词里,这一点实测结果和多数人的猜测相反)
- 估算内置能力的固定成本,并判断某个业务场景值不值得付
- 区分 v0.7 里默认开启和需要显式开启的能力
- 用一张判断表决定新需求该用
create_deep_agent、create_agent还是自己写图
前置依赖: 第 2 章(Agent 循环与消息轨迹)、第 9 章(create_agent 深入)、第 10 章(Middleware 与 HITL)、第 19 章(create_agent 就是一张 LangGraph 图)。其中第 19 章是本章的直接前提——本章 §3 用的全是第 19 章那套「打印图」的手法,只是这次拆的对象换成了 deep agent。
参考文档:
- Deep Agents Overview
- Runtimes, frameworks, and harnesses
- Comparison with Claude Agent SDK
- Customize Deep Agents
建议阅读顺序: §2 是概念地基,五分钟能读完。§3 是全章重点,务必动手跑一遍——里面的探测代码全部是纯本地操作,不花一分钱,但看完你对 harness 的理解会和看之前完全不同。§4 有两组真实调用的对照实验(约几分钱),结论直接支撑最后的判断表。§5、§6 是能力清单,可以当速查表用。§7 是和 Claude Agent SDK 的对比,不做选型时可以跳过。§8 是本章要带走的东西。
本章验证环境:deepagents 0.7.12、langchain 1.3.18、langchain-core 1.6.1、langgraph 1.2.11、langchain-deepseek 1.1.0,模型为 deepseek-v4-flash。版本很关键:deepagents 在 0.7 改过默认行为(§6 会实测),0.6 及更早跑出来的结果和本章不一样。
先装依赖:
pip install -U deepagents
# 或
uv add deepagents注意 deepagents 会一并升级 langchain 与 langchain-core(本章环境里从 langchain 1.3.14 升到了 1.3.18)。它是独立的库,不是 langchain 的子模块,所以 import 路径是 deepagents 而不是 langchain.deepagents。
2. 三层分工:framework、runtime、harness #
在拆代码之前,先把名字理顺。这一节的内容对应官方 Runtimes, frameworks, and harnesses。
很多人第一次看到 Deep Agents 的反应是:「LangChain、LangGraph 已经够多了,又来一个?」——这个困惑来自把三者放在同一层比较。实际上它们是上下三层,谈不上谁替代谁。
第 19 章 §6 已经理过 LangChain 和 LangGraph 的关系(用的类比是「电器 vs 电路」)。Deep Agents 是在那之上又加的一层:
| 层 | 它提供什么 | 官方定义 | 本教程里的代表 |
|---|---|---|---|
| Harness(驾驭层) | 预置工具、预置提示词、子智能体 | 「有主张的、开箱即用的框架」 | Deep Agents(第 39~52 章) |
| Framework(框架) | 抽象与集成:模型接口、工具协议、Agent 循环、middleware | 「让你更容易上手的抽象层」 | LangChain create_agent(第 2、9 章) |
| Runtime(运行时) | 持久执行、流式、HITL、状态持久化、底层编排控制 | 「在生产环境跑 Agent 的工具」 | LangGraph(第 19~28 章) |
三层的关系是建在之上,不是并列可选:
Deep Agents(harness:预置工具 + 预置提示词 + 子智能体)
↑ 建在之上
LangChain create_agent(framework:模型 / 工具 / 循环 / middleware 抽象)
↑ 建在之上
LangGraph(runtime:持久执行 / 流式 / HITL / 状态持久化)同一层里才有竞品。 官方文档在每层都列了别家的产品,这张表对理解「Deep Agents 到底是个什么东西」很有帮助:
| 层 | 同层的其他选择 |
|---|---|
| Harness | Claude Agent SDK、Manus,以及各种编码 CLI |
| Framework | Vercel AI SDK、CrewAI、OpenAI Agents SDK、Google ADK、LlamaIndex |
| Runtime | Temporal、Inngest 等持久执行引擎 |
看到这张表就清楚了:Deep Agents 的对标物不是 LangChain,而是 Claude Agent SDK(§7 会展开对比)。拿 Deep Agents 和 LangChain 比「谁更好」,就像拿「装修好的样板房」和「建材市场」比——它们解决的是不同阶段的问题。
官方给出的三层选用时机,可以收成一句话记:
| 层 | 什么时候选它 | 一句话记法 |
|---|---|---|
| Harness | 任务复杂、多步、跑得久、要自主决策 | 「我不想操心它怎么管上下文」 |
| Framework | 想快速上手、想统一团队的写法 | 「我要按业务精确组装」 |
| Runtime | 要底层控制、要长时间有状态的工作流 | 「流程是我定的,不能让模型决定」 |
官方还给了一张能力对照表,说明同一件事在三层各叫什么。这张表很实用,因为前面 38 章学的概念在 Deep Agents 里大多改了名字:
| 能力 | LangGraph 里叫 | LangChain 里叫 | Deep Agents 里叫 | 本教程章节 |
|---|---|---|---|---|
| 短期记忆 | 短期记忆(state + checkpointer) | 短期记忆 | StateBackend |
第 11、42 章 |
| 长期记忆 | 长期记忆(Store) |
长期记忆 | 长期记忆(AGENTS.md) |
第 16、45 章 |
| 技能 | —(没有对应物) | 多智能体 skills | Skills(SKILL.md) |
第 18、44 章 |
| 子智能体 | 子图(subgraph) | 多智能体 subagents | Subagents(task 工具) |
第 18、47 章 |
| 人机协同 | interrupt |
HITL middleware | interrupt_on 参数 |
第 10、26、48 章 |
| 流式 | 流式 | Agent 流式 | 流式(含 stream.subagents) |
第 27、49 章 |
这张表说明前面 38 章一点没白学。 Deep Agents 没有发明新机制,它是把你已经会的东西换了个更省事的接口——interrupt_on={"refund": True} 底下就是第 26 章那个 interrupt。
一句话总结这一节:Deep Agents 不是「更高级的 LangChain」,而是「装修好的 LangChain」。 家具都摆好了,好处是拎包入住,代价是家具按别人的习惯摆的——想改,得先知道它摆了什么。§3 就去看它到底摆了什么。
3. 拆开 create_deep_agent #
这一节完全沿用第 19 章的手法:不看文档,直接打印对象。文档会告诉你「它有虚拟文件系统和子智能体」,但只有打印出来,你才知道这些能力具体以什么形式存在——而这决定了它们花多少钱、怎么调试、怎么改。
本节所有代码都是纯本地操作,不调用模型,不花 token。
3.1. 它返回的还是 CompiledStateGraph #
先问最基础的问题:create_deep_agent 返回的是什么类型的对象?
第 19 章 §2.1 用 __mro__(Method Resolution Order,方法解析顺序)查过 create_agent 的继承链。这里对两者同时查一遍,直接对比:
# 从 .env 读取 API Key(本节不调模型,但 create_agent 初始化模型时会检查 Key 是否存在)
from dotenv import load_dotenv
# LangChain 的 Agent 构造函数,前 38 章一直在用
from langchain.agents import create_agent
# 把普通函数变成工具的装饰器
from langchain.tools import tool
# Deep Agents 的入口函数,注意包名是 deepagents,不是 langchain.deepagents
from deepagents import create_deep_agent
# LangGraph 的「编译后状态图」类型,用来做类型判断
from langgraph.graph.state import CompiledStateGraph
# override=True 让 .env 覆盖系统同名环境变量,避免读到旧 Key
load_dotenv(override=True)
# 一个最简单的业务工具,两个 Agent 都用它,保证对比时只有一个变量
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气。"""
# 演示用假数据,真实场景这里会调天气 API
return f"{city}:晴,26℃"
# 本章统一用这个模型,方便和第 19 章的数据对照
MODEL = "deepseek:deepseek-v4-flash"
# A 组:普通 Agent
plain = create_agent(model=MODEL, tools=[get_weather])
# B 组:deep agent,参数写法几乎一模一样
deep = create_deep_agent(model=MODEL, tools=[get_weather])
# 打印两者的真实类型,看是不是同一种对象
print("create_agent type:", type(plain).__name__)
print("create_deep_agent type:", type(deep).__name__)
# 打印 deep agent 的完整继承链
print("deep MRO:", [c.__name__ for c in type(deep).__mro__])
# 确认它也是 LangGraph 的编译图
print("deep 是 CompiledStateGraph 吗:", isinstance(deep, CompiledStateGraph))运行输出:
create_agent type: CompiledStateGraph
create_deep_agent type: CompiledStateGraph
deep MRO: ['CompiledStateGraph', 'Pregel', 'PregelProtocol', 'Runnable', 'ABC', 'Generic', 'object']
deep 是 CompiledStateGraph 吗: True两者返回的是同一种对象,MRO 一字不差。
把这行 MRO 和第 19 章 §2.1 打印出来的那行摆在一起看,是完全一样的:CompiledStateGraph → Pregel → PregelProtocol → Runnable → ...。这意味着第 19 章那些结论对 deep agent 全部成立:
| 第 19 章的结论 | 对 deep agent 是否成立 |
|---|---|
| 它就是一张编译好的 LangGraph 图 | 成立(isinstance 返回 True) |
有 invoke / stream / batch,能进 LCEL 管道 |
成立(MRO 里有 Runnable) |
可以用 checkpointer + thread_id 做持久化 |
成立(create_deep_agent 直接收 checkpointer 参数) |
可以 add_node("agent", deep) 塞进另一张图当节点 |
成立(依据同样是 Runnable) |
get_graph() 能打印结构 |
成立(下一节就用) |
这是本章第一个重要结论:Deep Agents 没有另起一套运行时。 它和
create_agent一样都编译成 LangGraph 图,所以第 19~28 章学的图、状态、checkpoint、interrupt 全部照用。「学 Deep Agents 要不要先忘掉 LangGraph」这个问题,答案是恰恰相反——LangGraph 是它的地基。
3.2. 打开图:只多了一个节点 #
既然也是图,那就用第 19 章 §2.2 那套办法打印它的结构。重点看两个东西:nodes(有哪些节点)和 edges(怎么连)。每条边的 conditional 属性区分两种边——False 是实线(无条件必走),True 是虚线(由路由函数运行时决定)。
读之前先猜一下: 一个预装了文件系统、子智能体、上下文压缩的 harness,图里会有多少个节点?多数人会猜十几个。
# 沿用 §3.1 的 plain / deep 两个 Agent
def show(name: str, agent):
"""打印一个 Agent 的图结构(纯本地操作,不调模型)。"""
# 拿到图的结构描述对象
g = agent.get_graph()
# 打印分组标题
print(f"\n=== {name} ===")
# 节点数量是本节的关键指标
print(f"节点数: {len(g.nodes)}")
# 列出所有节点名
print("nodes:", list(g.nodes.keys()))
# 遍历所有边,看整张图怎么连
for e in g.edges:
# conditional=True 用虚线,表示走不走由路由函数决定
arrow = "-.->" if e.conditional else "-->"
# 打印这条边的走向
print(f" {e.source} {arrow} {e.target}")
# A 组:普通 Agent,作为对照基准
show("A. create_agent", plain)
# B 组:deep agent
show("B. create_deep_agent", deep)运行输出:
=== A. create_agent ===
节点数: 4
nodes: ['__start__', 'model', 'tools', '__end__']
__start__ --> model
model -.-> __end__
model -.-> tools
tools -.-> model
=== B. create_deep_agent ===
节点数: 5
nodes: ['__start__', 'model', 'tools', 'PatchToolCallsMiddleware.before_agent', '__end__']
PatchToolCallsMiddleware.before_agent --> model
__start__ --> PatchToolCallsMiddleware.before_agent
model -.-> __end__
model -.-> tools
tools -.-> model5 个节点,比 create_agent 只多了 1 个。
这个结果值得停下来想一想。图的主干完全没变,还是那个「模型 ↔ 工具」循环:
create_agent: __start__ → model ⇄ tools → __end__
create_deep_agent: __start__ → Patch.before_agent → model ⇄ tools → __end__唯一多出来的 PatchToolCallsMiddleware.before_agent,按官方 customization 文档的说明,职责是「修复消息历史里悬空的工具调用」——一次运行被中断后恢复、或者模型吐出畸形的工具调用参数时,它负责把消息序列补成合法的。它挂的钩子是 before_agent(每次运行开始时执行一次),所以插在 __start__ 和 model 之间。
注意它是实线边:__start__ --> Patch.before_agent --> model,两条都是无条件的。用第 19 章 §3 的话说,这是结构性保证——每次运行必然先过一遍消息修复,没有绕过的路径。
那问题就来了:虚拟文件系统、子智能体、上下文压缩这些能力,都在哪?
官方文档说 bare stack(只传一个 model 时的中间件栈)包含 FilesystemMiddleware、SubAgentMiddleware、SummarizationMiddleware、PatchToolCallsMiddleware 和提示缓存中间件。可图里只看到 Patch 一个。其余几个中间件为什么没有留下节点?
第 19 章 §2.3 给过答案的一半:middleware 只在自己真正实现了的钩子上插节点。 当时验证的是 before_model / after_model 这两个钩子。而 LangChain 1.x 的 middleware 还有另一类钩子——wrap_model_call 和 wrap_tool_call,它们是包装器而不是节点,所以不会在图上留下痕迹:
# 导入 deepagents 的两个核心内置中间件
from deepagents.middleware.filesystem import FilesystemMiddleware
from deepagents.middleware.subagents import SubAgentMiddleware
# 要检查的六种钩子:前四种是「节点型」,后两种是「包装型」
HOOKS = ("before_agent", "before_model", "after_model", "after_agent",
"wrap_model_call", "wrap_tool_call")
# 逐个检查这两个中间件各自实现了哪些钩子
for cls in (FilesystemMiddleware, SubAgentMiddleware):
# cls.__dict__ 只含该类自己定义的成员,不含从父类继承来的
own = [h for h in HOOKS if h in cls.__dict__]
print(f"{cls.__name__:22s} {own}")运行输出:
FilesystemMiddleware ['wrap_model_call', 'wrap_tool_call']
SubAgentMiddleware ['wrap_model_call']对上了。 这两个中间件只实现了包装型钩子,一个节点都不插:
| 中间件 | 实现的钩子 | 钩子类型 | 在图上留痕吗 |
|---|---|---|---|
PatchToolCallsMiddleware |
before_agent |
节点型 | 留(多一个节点) |
FilesystemMiddleware |
wrap_model_call + wrap_tool_call |
包装型 | 不留 |
SubAgentMiddleware |
wrap_model_call |
包装型 | 不留 |
这就解释了「装了一堆能力、图却几乎没变」:Deep Agents 的能力主要不是靠改图的形状实现的,而是靠在 wrap_model_call 里改「发给模型的东西」。
那它改了什么?答案在下一节。
顺带说一个调试上的影响: 第 19 章教的「Agent 行为异常先打印图」这一招,对 deep agent 基本失效——它的图永远是那 5 个节点,看不出任何业务信息。排查 deep agent 要换另一招:看工具面和消息轨迹(§3.3、§3.4),或者直接上 LangSmith Trace。
3.3. 真正的变化:工具面从 1 个变成 10 个 #
既然能力不在图的形状里,那就去看模型实际看到了什么。
Agent 的行为由两样东西决定:系统提示词(告诉模型该怎么做)和工具面(模型能调什么)。先看工具面。
tools 节点里装的是一个 ToolNode,它有个 tools_by_name 字典,记录了所有注册进来的工具:
# 沿用 §3.1 的 plain / deep 两个 Agent
def tool_names(agent):
"""列出 tools 节点里注册的所有工具名。"""
# 取出名为 tools 的节点
node = agent.get_graph().nodes.get("tools")
# 没有工具时这个节点不存在,直接返回空
if node is None:
return []
# node.data 是 ToolNode 实例,tools_by_name 是「工具名 -> 工具」的字典
return sorted(getattr(node.data, "tools_by_name", {}))
# 对比两者注册的工具
for label, ag in (("create_agent", plain), ("create_deep_agent", deep)):
names = tool_names(ag)
# 打印数量和清单
print(f"{label:18s} ({len(names)}个): {names}")运行输出(为方便阅读手动折了行):
create_agent (1个): ['get_weather']
create_deep_agent (10个): ['delete', 'edit_file', 'execute', 'get_weather',
'glob', 'grep', 'ls', 'read_file', 'task', 'write_file']你传进去 1 个工具,它给了模型 10 个。 多出来的 9 个正是那些内置能力的真身:
| 内置工具 | 干什么 | 属于哪组能力 | 详见 |
|---|---|---|---|
ls |
列目录,带文件大小和修改时间 | 虚拟文件系统 | 第 42 章 |
read_file |
读文件,带行号,支持 offset / limit 分页 |
虚拟文件系统 | 第 42 章 |
write_file |
新建或覆盖文件 | 虚拟文件系统 | 第 42 章 |
edit_file |
按精确字符串替换改文件 | 虚拟文件系统 | 第 42 章 |
delete |
删文件或递归删目录 | 虚拟文件系统 | 第 42 章 |
glob |
按 **/*.py 这类模式找文件 |
虚拟文件系统 | 第 42 章 |
grep |
搜文件内容,多种输出模式 | 虚拟文件系统 | 第 42 章 |
execute |
在沙箱里跑 shell 命令 | 代码执行 | 第 43 章 |
task |
派一个子智能体去干活 | 委派 | 第 47 章 |
看着像什么?像一个命令行编码助手的工具面。 这不是巧合:Deep Agents 的设计出发点就是「让智能体像人一样用文件系统管理自己的工作」——大段搜索结果先 write_file 存下来,需要时再 read_file 读回,而不是一直堆在对话历史里。这就是官方说的「上下文卸载(context offloading)」,第 46 章会专门讲。
这里有个坑要提前说。 上面这 10 个工具是注册在 ToolNode 里的,但发给模型的并不是 10 个。把「模型实际收到的工具列表」也打印出来对比,就能看出差别:
# 需要一个假模型来截获「实际发给模型的工具列表」,全程不联网
from typing import Any, Sequence
# BaseChatModel 是所有聊天模型的基类,继承它就能造一个假模型
from langchain_core.language_models import BaseChatModel
from langchain_core.messages import AIMessage
from langchain_core.outputs import ChatGeneration, ChatResult
from langchain.tools import tool
from deepagents import create_deep_agent
# 用一个模块级字典记录截获到的内容
CAP: dict[str, Any] = {}
class SpyModel(BaseChatModel):
"""假模型:只记录收到了什么,不发任何网络请求。"""
@property
def _llm_type(self) -> str:
# 基类要求实现的标识属性,随便给个名字
return "spy"
def _generate(self, messages, stop=None, run_manager=None, **kwargs) -> ChatResult:
# 记录这一轮收到的完整消息列表
CAP.setdefault("calls", []).append(messages)
# 返回一个固定的假回答,让 Agent 循环能正常收尾
return ChatResult(
generations=[ChatGeneration(message=AIMessage(content="ok"))]
)
def bind_tools(self, tools: Sequence[Any], **kwargs: Any):
# Agent 会调这个方法把工具绑给模型,我们借机记录工具清单
CAP["tools"] = [
# 供应商内置工具是字典形式,自定义工具是对象形式,两种都要兼容
(t.get("name") or t.get("type")) if isinstance(t, dict)
else getattr(t, "name", type(t).__name__)
for t in tools
]
# 同时把工具对象本身留一份,§3.4 和 §6 要读它们的说明书
CAP["toolobjs"] = list(tools)
# 返回 self 即可,我们不需要真的绑定
return self
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气。"""
return f"{city}:晴,26℃"
# 用假模型建一个 deep agent
spy_agent = create_deep_agent(model=SpyModel(), tools=[get_weather])
# 跑一次,触发 bind_tools 和 _generate
spy_agent.invoke({"messages": [{"role": "user", "content": "hi"}]})
# 对比两个来源的工具清单
node_tools = sorted(spy_agent.get_graph().nodes["tools"].data.tools_by_name)
model_tools = CAP["tools"]
print(f"ToolNode 里注册的 ({len(node_tools)}个): {node_tools}")
print(f"实际发给模型的 ({len(model_tools)}个): {model_tools}")
# 用集合差集找出被拦下的工具
print("注册了但没给模型的:", sorted(set(node_tools) - set(model_tools)))运行输出:
ToolNode 里注册的 (10个): ['delete', 'edit_file', 'execute', 'get_weather', 'glob',
'grep', 'ls', 'read_file', 'task', 'write_file']
实际发给模型的 (9个): ['ls', 'read_file', 'write_file', 'edit_file', 'delete',
'glob', 'grep', 'task', 'get_weather']
注册了但没给模型的: ['execute']execute 被拦下了。 原因是默认后端是 StateBackend(一个存在 LangGraph 状态里的虚拟文件系统),它不支持执行 shell 命令。官方文档明确说 execute 只在沙箱后端下可用,并且「后端不支持的工具会自动对模型隐藏」。
这个机制的意义是:它不会给模型一个用不了的工具——否则模型会反复尝试调 execute 然后反复失败。第 43 章配沙箱时,execute 才会真正出现在模型的工具面里。
调试提醒:数工具面要看
bind_tools收到的列表,不要看ToolNode。 这两个数字在默认配置下就差 1 个(execute);配了permissions或 harness profile 的excluded_tools之后,差得更多。用错了来源,你会以为模型能调某个工具,实际它根本看不见。
3.4. 反直觉的发现:内置提示词是空的 #
工具面的问题清楚了。再看另一半:系统提示词。
按常理推测,一个「预置提示词」的 harness(官方原话就是 harness 提供 Predefined tools、Prompts、Subagents)应该塞了一大段内置指令,教模型怎么规划、怎么用文件系统、什么时候派子智能体。
用上一节的 SpyModel 把真正发给模型的消息截下来,看看那段内置提示词长什么样:
# 复用 §3.3 定义的 SpyModel、CAP、get_weather
from langchain.agents import create_agent
from deepagents import create_deep_agent
def peek(label: str, agent):
"""跑一次,打印模型收到的消息类型序列和 system 内容。"""
# 每次清空,避免上一轮的记录混进来
CAP.clear()
# 触发一次调用,SpyModel 会把消息记下来
agent.invoke({"messages": [{"role": "user", "content": "hi"}]})
# 取第一次模型调用收到的消息列表
msgs = CAP["calls"][0]
print(f"\n=== {label} ===")
# 先看消息类型序列:有没有 SystemMessage
print("消息序列:", [type(m).__name__ for m in msgs])
# 把所有 SystemMessage 挑出来
sysmsgs = [m for m in msgs if type(m).__name__ == "SystemMessage"]
# 用 repr 打印,空字符串和不存在能区分开
print("SystemMessage 正文:", repr(sysmsgs[0].content) if sysmsgs else "(没有这条消息)")
# A 组:普通 Agent,不传 system_prompt
peek("A. create_agent(不传 system_prompt)",
create_agent(model=SpyModel(), tools=[get_weather]))
# B 组:deep agent,同样不传 system_prompt
peek("B. create_deep_agent(不传 system_prompt)",
create_deep_agent(model=SpyModel(), tools=[get_weather]))
# C 组:deep agent,这次传一句自己的提示词
peek("C. create_deep_agent(传 system_prompt)",
create_deep_agent(model=SpyModel(), tools=[get_weather],
system_prompt="你是企业知识库助手。"))运行输出:
=== A. create_agent(不传 system_prompt) ===
消息序列: ['HumanMessage']
SystemMessage 正文: (没有这条消息)
=== B. create_deep_agent(不传 system_prompt) ===
消息序列: ['SystemMessage', 'HumanMessage']
SystemMessage 正文: ''
=== C. create_deep_agent(传 system_prompt) ===
消息序列: ['SystemMessage', 'HumanMessage']
SystemMessage 正文: '你是企业知识库助手。'B 组那行是本章最反直觉的一处:create_deep_agent 确实多插了一条 SystemMessage,但它的正文是空字符串。
C 组进一步确认:你传进去的 system_prompt 被原样放进那条消息,前后没有拼接任何内置指令。
所以「预置提示词」这个说法要修正:在 0.7.12 版本、默认配置下,Deep Agents 没有往系统提示词里塞任何东西。 那它靠什么教模型用那 9 个工具?
答案是工具的说明书——也就是每个工具的 description 和参数 schema。把两边的工具面按字符数称一下重量:
# 复用 §3.3 的 SpyModel、CAP、get_weather
import json
from langchain.agents import create_agent
from deepagents import create_deep_agent
def weigh(label: str, agent):
"""统计工具面的总字符数:description + 参数 schema。"""
CAP.clear()
# 跑一次,让 bind_tools 把工具对象记下来
agent.invoke({"messages": [{"role": "user", "content": "hi"}]})
print(f"\n=== {label} ===")
total = 0
# CAP["toolobjs"] 里是 §3.3 的 SpyModel 存下来的工具对象
for t in CAP["toolobjs"]:
# 工具名
name = getattr(t, "name", "?")
# 给模型看的说明书正文
desc = getattr(t, "description", "") or ""
# 参数 schema 也要一起发给模型,所以也算进成本
try:
schema = json.dumps(t.args_schema.model_json_schema(), ensure_ascii=False)
except Exception:
schema = ""
# 这个工具占的字符数
n = len(desc) + len(schema)
total += n
print(f" {name:12s} description={len(desc):5d} schema={len(schema):5d}")
print(f" 工具面总字符数: {total}")
weigh("A. create_agent", create_agent(model=SpyModel(), tools=[get_weather]))
weigh("B. create_deep_agent", create_deep_agent(model=SpyModel(), tools=[get_weather]))运行输出:
=== A. create_agent ===
get_weather description= 10 schema= 154
工具面总字符数: 164
=== B. create_deep_agent ===
ls description= 206 schema= 262
read_file description= 1220 schema= 599
write_file description= 321 schema= 447
edit_file description= 357 schema= 817
delete description= 362 schema= 282
glob description= 809 schema= 845
grep description= 557 schema= 1825
task description= 1419 schema= 566
get_weather description= 10 schema= 154
工具面总字符数: 11058164 字符 vs 11058 字符,差 67 倍。 这就是那些内置能力真正的存放位置。
两个最重的工具值得单独看。read_file 的说明书有 1220 字符,里面写满了使用纪律:
Reads a file from the filesystem. ...
Usage:
- By default, it reads up to 100 lines starting from the beginning of the file.
Use `offset`/`limit` to page through large files instead of reading them whole.
- Results are returned with line numbers starting at `offset` + 1 ...
Never include these line-number prefixes when editing.
- Speculatively batch multiple `read_file` calls in one response when several files may be useful.
- Large tool results may be offloaded to a file; the tool message gives the path.
Read that path here, paging with `offset`/`limit`.
- Images (`.png`, `.jpg`, etc.), audio, video, and PDFs return multimodal content blocks.
- Always read a file before editing it.注意倒数第三行:「大的工具结果可能被卸载到文件里,工具消息会给你路径」——这就是上下文卸载机制对模型的交代方式。以及最后一行「改文件前一定要先读」,这是防止模型瞎改的行为约束。这些本该写在系统提示词里的话,全都写在工具说明书里。
task 工具的 1419 字符则是整个子智能体机制的说明书:
Launch an ephemeral subagent to handle a complex, multi-step task.
Available agent types and the tools they have access to:
- general-purpose: General-purpose agent for researching complex questions, ...
This agent has access to all tools as the main agent.
Specify subagent_type to select the agent. Usage notes:
- Launch multiple agents concurrently when their tasks are independent,
using a single message with multiple tool calls.
- Each invocation is stateless by default: the agent sees only the prompt you give it
and returns a single final report. ...
- The agent's report is not shown to the user; relay a summary yourself.
- Tell the agent whether to create content, analyze, or only research, ...
- If an agent's description says to use it proactively, do so without waiting to be asked.第 18 章讲多智能体时,「子智能体上下文隔离」、「无状态、只回一份最终报告」、「独立任务可以并行扇出」这些机制都是我们自己用 LangGraph 拼出来的。这里它们变成了一段工具说明书——机制没变,只是从「你写代码实现」变成了「官方写好一段话告诉模型」。
为什么把内置知识放工具说明书里,而不是系统提示词里? 有一个很实际的好处:
| 放系统提示词 | 放工具说明书 | |
|---|---|---|
你传的 system_prompt |
要和内置的拼接,可能互相干扰 | 完全属于你,不被污染 |
| 某个工具被禁用时 | 提示词里的相关段落还在,白花 token | 说明书跟着工具一起消失 |
| 提示缓存 | 混了你的内容,缓存前缀更容易失效 | 工具定义是稳定的,更容易命中缓存 |
中间那条最实用:§3.3 里 execute 因为后端不支持而被隐藏,它那段说明书也一起没了。能力和它的说明书是绑在一起进出的,不会出现「工具没了、教它用工具的话还在」这种浪费。
本节结论:Deep Agents 的「内置能力」= 9 个内置工具 + 1.1 万字符的工具说明书。 不是新的运行时,不是更复杂的图,也不是一大段内置人设。看清这一点,你就能预判它的成本(§4)、知道该怎么裁剪(第 41 章的
excluded_tools)、以及为什么它对模型的能力有要求(工具面太大,弱模型会挑错工具)。
4. 对照实验:这些内置能力要花多少钱 #
§3 全是本地探测。这一节真实调模型,回答一个绕不开的问题:11058 字符的工具面,代价是什么?
本节两组实验共约十几次模型调用,用 deepseek-v4-flash 花费不到一分钱。
先准备公用代码。关键是 stats 函数:它从消息的 usage_metadata 里累计 token——这是唯一可靠的成本数据来源,比自己估算准得多:
# 计时用
import time
from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain.tools import tool
from deepagents import create_deep_agent
load_dotenv(override=True)
MODEL = "deepseek:deepseek-v4-flash"
# 全局列表,记录工具的真实调用顺序(第 19 章 §3.1 的埋点技巧)
CALLS = []
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气。"""
# 埋点:只有工具真被执行才会追加,模型嘴上说查了不算
CALLS.append(f"get_weather({city})")
# 五个城市的假数据,供后面的对比报告任务使用
data = {"北京": "晴,26℃", "上海": "多云,24℃", "广州": "雷阵雨,30℃",
"深圳": "阴,29℃", "杭州": "晴,27℃"}
return f"{city}:{data.get(city, '晴,25℃')}"
def stats(result):
"""从返回的消息里累计模型调用次数与 token 用量。"""
inp = out = n = 0
# 遍历所有消息,只有 AIMessage 带 usage_metadata
for m in result["messages"]:
u = getattr(m, "usage_metadata", None)
if u:
# 有用量数据说明这是一次真实的模型调用
n += 1
# 累加输入和输出 token
inp += u.get("input_tokens", 0)
out += u.get("output_tokens", 0)
return n, inp, out
def run(label: str, agent, question: str):
"""跑一次并打印全部观测指标。"""
# 清空工具埋点
CALLS.clear()
# 计时开始
t0 = time.perf_counter()
# 真实调用模型,这一步产生费用
r = agent.invoke({"messages": [{"role": "user", "content": question}]})
# 计时结束
dt = time.perf_counter() - t0
n, inp, out = stats(r)
print(f"\n--- {label} ---")
print(f"模型调用={n}次 输入={inp} tokens 输出={out} tokens 耗时={dt:.2f}s")
print("工具调用:", CALLS)
# 状态里有哪些键,deep agent 会多一个 files
print("state 的键:", sorted(r.keys()))
# 如果虚拟文件系统里有文件,列出来
if r.get("files"):
print("虚拟文件系统:", list(r["files"]))
# 只打印回答的前 120 字,避免输出太长
print("回答:", r["messages"][-1].content[:120].replace("\n", " "))
return inp, out4.1. 简单问题:输入 token 涨了 7.4 倍 #
第一组实验用最普通的问题:「北京天气怎么样?」——一个完全不需要文件系统和子智能体的任务。两边的正确行为都是「调一次 get_weather,然后回答」。
# 复用上面定义的 run 函数和 get_weather 工具
Q = "北京天气怎么样?"
# A 组:普通 Agent
a_in, a_out = run("create_agent", create_agent(model=MODEL, tools=[get_weather]), Q)
# B 组:deep agent,其他条件完全相同
b_in, b_out = run("create_deep_agent", create_deep_agent(model=MODEL, tools=[get_weather]), Q)
# 算一下输入 token 放大了多少倍
print(f"\n输入 token: {a_in} -> {b_in}(放大 {b_in / a_in:.1f} 倍)")运行输出:
--- create_agent ---
模型调用=2次 输入=784 tokens 输出=74 tokens 耗时=1.75s
工具调用: ['get_weather(北京)']
state 的键: ['messages']
回答: 北京目前是晴天,气温26℃,天气不错,适合外出活动。
--- create_deep_agent ---
模型调用=2次 输入=5835 tokens 输出=85 tokens 耗时=1.96s
工具调用: ['get_weather(北京)']
state 的键: ['files', 'messages']
回答: 北京今天天气晴朗,气温 26℃,很适合外出活动 ☀️。祝你有愉快的一天!
输入 token: 784 -> 5835(放大 7.4 倍)同样的问题、同样的工具调用、同样质量的回答,输入 token 从 784 涨到 5835。
这 5051 个多出来的 token,就是 §3.4 那 11058 字符工具面的代价。而且注意:这是固定成本,每一轮模型调用都要重发一遍。第 19 章 §3.5 讲过 Agent 循环的 token 是逐轮累积的——工具面越大,每一轮都要多背这个包袱,轮数越多亏得越多。
还有一个细节值得注意:state 的键从 ['messages'] 变成了 ['files', 'messages']。 那个 files 就是虚拟文件系统的存放处(默认的 StateBackend 把文件存在 LangGraph 状态里)。这一次它是空的,因为这个任务没用上——但工具面的钱已经付了。
把这组数据整理一下:
| 指标 | create_agent |
create_deep_agent |
差异 |
|---|---|---|---|
| 模型调用次数 | 2 次 | 2 次 | 一样 |
| 输入 tokens | 784 | 5835 | 7.4 倍 |
| 输出 tokens | 74 | 85 | 差不多 |
| 耗时 | 1.75s | 1.96s | 略慢 |
| 工具调用 | 1 次 | 1 次 | 一样 |
| 回答质量 | 正确 | 正确 | 一样 |
这组实验是 §8 判断表的核心依据:在简单任务上,Deep Agents 的内置能力全是纯开销。 一个只需要「问答 + 调几个工具」的场景,用
create_deep_agent等于每轮多付 5000 个 token 买一堆用不上的工具。
4.2. 要求落盘的任务:create_agent 只能说「我做不到」 #
那反过来呢?换一个真正需要那些内置能力的任务,差距会倒向哪一边?
任务设计成明确要求产出文件:查五个城市的天气、整理成对比报告、保存为 report.md。
# 复用 §4 开头定义的 run 函数和 get_weather 工具
Q = ("分别查询北京、上海、广州、深圳、杭州五个城市的天气,"
"整理成一份对比报告,保存为 report.md 文件。")
# A 组:普通 Agent,它没有任何文件工具
run("create_agent", create_agent(model=MODEL, tools=[get_weather]), Q)
# B 组:deep agent,它有 write_file
run("create_deep_agent", create_deep_agent(model=MODEL, tools=[get_weather]), Q)运行输出(回答部分做了截断):
--- create_agent ---
模型调用=2次 输入=1132 tokens 输出=893 tokens 耗时=5.74s
工具调用: ['get_weather(北京)', 'get_weather(广州)', 'get_weather(上海)',
'get_weather(深圳)', 'get_weather(杭州)']
state 的键: ['messages']
回答: 五个城市的天气数据已全部获取完成。以下是整理好的报告内容:
需要说明的是,我当前可用的工具中不包含文件写入功能(仅有天气查询工具),
因此无法直接为您生成并保存 `report.md` 文件。我先把完整的报告内容整理如下,
您可以直接复制保存:...
--- create_deep_agent ---
模型调用=3次 输入=10074 tokens 输出=1204 tokens 耗时=8.32s
工具调用: ['get_weather(广州)', 'get_weather(上海)', 'get_weather(深圳)',
'get_weather(北京)', 'get_weather(杭州)']
state 的键: ['files', 'messages']
虚拟文件系统: ['/report.md']
回答: 已完成五城市天气查询与对比报告整理,文件已保存为 `report.md`。...create_agent 老老实实说「我没有文件写入功能,无法保存」,然后把内容贴在回答里让用户自己复制。它的行为完全正确——工具面里确实没有 write_file,这是它的能力边界,不是它不听话。
create_deep_agent 则真的写出了文件:state["files"] 里出现了 /report.md。这个文件存在 LangGraph 状态里,配上 checkpointer 后能跨轮次保留,下一轮对话里模型可以 read_file 读回来接着改。
对照这一组:
| 指标 | create_agent |
create_deep_agent |
|---|---|---|
| 输入 tokens | 1132 | 10074 |
| 输出 tokens | 893 | 1204 |
| 耗时 | 5.74s | 8.32s |
| 产出文件 | 做不到 | /report.md |
| 任务是否完成 | 未完成(只给了文本) | 完成 |
这里的差别不是「快慢」或「贵贱」,而是「能不能做」。 给 create_agent 加提示词、换更强的模型、多给几次机会,都没用——它的工具面里没有写文件的能力,就像第 19 章 §3.4 说的「图里没有那条边」,调提示词创造不出来。
有两个观察顺带说明一下:
一、两边都在一条消息里并行调了 5 次 get_weather。 这是模型的并行工具调用能力(第 8 章),和用不用 harness 无关。所以 create_agent 只花了 2 次模型调用就查完 5 个城市。
二、create_deep_agent 多花了一次模型调用(3 次 vs 2 次)。 多出来的那次是「写完文件后再组织一段回复」。它的活确实更多——查数据、写文件、报告结果,比只查数据多一步。
4.3. 两组实验合起来看 #
把 §4.1 和 §4.2 并排放,结论就很清楚了:
| 简单问题(§4.1) | 要求落盘(§4.2) | |
|---|---|---|
create_agent |
784 tokens,任务完成 | 1132 tokens,任务失败 |
create_deep_agent |
5835 tokens,任务完成 | 10074 tokens,任务完成 |
| 该选谁 | create_agent |
create_deep_agent |
没有哪个更好,只有匹配不匹配。 判断依据也很直接:
你的任务需要「产出物」和「过程管理」吗?
- 需要(写文件、存中间结果、拆子任务、跑很久)→ Deep Agents 的内置能力是刚需,那 5000 token 花得值
- 不需要(问答、查询、单步操作)→ 那 5000 token 是纯浪费,用
create_agent
第 19 章有一句结论放在这里同样适用,只是换了一层:「把已经确定的事情交给模型决定,等于为不确定性付费」;这里是「给用不上的能力付固定成本,等于为可能性付费」。可能性本身没错,但要确认它真的会发生。
5. 四组内置能力总览 #
§3、§4 已经把「机制」和「代价」看清了。这一节补全「有哪些」,作为整个阶段七的地图。
官方 Overview 把 Deep Agents 的内置能力分成四组。本阶段第 41~48 章就是照这四组展开的,所以这张表也是你的学习路线:
| 能力组 | 管什么 | 包含 | 对应章 |
|---|---|---|---|
| Execution environment(执行环境) | 智能体在哪儿动手 | 工具与 MCP、虚拟文件系统、路径权限、沙箱与解释器、流式 | 41~43、49 |
| Context management(上下文管理) | 智能体知道什么、能撑多久 | Skills、Memory、摘要与上下文卸载、提示缓存 | 44~46 |
| Delegation(委派) | 大任务怎么拆 | 任务规划(write_todos)、子智能体(task) |
47 |
| Steering(操控) | 人怎么在运行时刹车 | 人工审批(interrupt_on)、中断与恢复 |
48 |
把 §3.3 打印出来的 9 个内置工具按这四组归位,能力和工具的对应关系就一目了然了:
执行环境 ── ls / read_file / write_file / edit_file / delete / glob / grep ← 虚拟文件系统
└─ execute ← 仅沙箱后端(默认被隐藏)
上下文管理 ─ (无独立工具,靠 wrap_model_call 在后台压缩历史、卸载大结果)
委派 ────── task ← 派子智能体
└─ write_todos ← v0.7 起需显式开启(§6)
操控 ────── (无独立工具,靠 interrupt_on 在工具调用前插入中断)注意「上下文管理」和「操控」这两组没有对应的工具。 它们不是模型主动调的能力,而是 harness 在背后替你做的事——摘要在 wrap_model_call 里自动触发,审批在 after_model 里自动拦截。模型甚至不知道它们存在,这也是为什么 §3.3 的工具面里看不到它们。
各组的关键点先建立印象,细节留给对应章节:
| 能力 | 一句话定位 | 和前面哪章是同一件事 |
|---|---|---|
| 虚拟文件系统 | 智能体的「工作台」,大结果先落盘再按需读回 | 新增能力,第 42 章 |
| 后端(backends) | 文件到底存哪:状态里 / 本地磁盘 / Store / 按路径分流 |
第 11 章 state、第 16 章 Store |
| 权限(permissions) | 声明式规则控制哪些路径可读可写,首条命中 | 类似第 10 章护栏,但作用于文件 |
| Skills | SKILL.md 装领域知识,启动只读摘要、用到才读全文 |
第 18 章 Skills |
| Memory | AGENTS.md 装长期规范,每次必加载 |
第 16 章长期记忆 |
| 摘要与卸载 | 历史太长自动压缩,工具结果太大自动落盘 | 第 10 章 SummarizationMiddleware |
| 子智能体 | task 工具派临时智能体,上下文隔离、只回一份报告 |
第 18 章 Subagents |
| 任务规划 | write_todos 维护结构化任务清单 |
新增能力,第 47 章 |
| 人工审批 | interrupt_on 在指定工具调用前暂停等人 |
第 10 章 HITL、第 26 章 interrupt |
这张表右列的意义是:阶段七不是从零开始的新知识。 九项能力里有六项你已经手写过一遍,Deep Agents 只是把它们变成了一个参数。真正的新东西只有虚拟文件系统(及其后端与权限)和任务规划。
6. 哪些能力默认开、哪些要自己开 #
§3.3 那份工具清单里有个东西没出现:write_todos。
这不是漏了,而是 deepagents 在 0.7 改了默认行为。官方文档写得很明确:「从 v0.7 起任务规划改为仅按需开启(opt-in only)。在更早的版本里,任务规划中间件是默认包含的。」
这类「默认值变了」的改动最容易踩坑:照着旧教程或旧博客写代码,发现模型死活不用 write_todos,却查不出原因——因为不报错,工具根本就不存在。所以这一节实测一遍,把「默认给什么、加了参数给什么」的边界划清楚。
四种配置一起对比。这段代码仍是纯本地操作,不花 token:
from dotenv import load_dotenv
# 任务规划中间件,注意它来自 langchain 而不是 deepagents
from langchain.agents.middleware import TodoListMiddleware
from langchain.tools import tool
from deepagents import create_deep_agent
load_dotenv(override=True)
MODEL = "deepseek:deepseek-v4-flash"
@tool
def get_weather(city: str) -> str:
"""查询指定城市的天气。"""
return f"{city}:晴,26℃"
# 一个高风险工具,用来给 interrupt_on 一个审批对象
@tool
def refund(order_id: str, amount: float) -> str:
"""给指定订单退款。"""
return f"订单 {order_id} 已退款 {amount} 元"
def tools_of(agent):
"""列出 tools 节点里注册的所有工具名。"""
node = agent.get_graph().nodes.get("tools")
return sorted(getattr(node.data, "tools_by_name", {})) if node else []
# 四种配置,每种只改一个变量,方便看清各自的影响
cases = {
# 基准:什么可选参数都不传
"① 默认": create_deep_agent(model=MODEL, tools=[get_weather]),
# 只加任务规划中间件
"② 加 TodoListMiddleware": create_deep_agent(
model=MODEL, tools=[get_weather], middleware=[TodoListMiddleware()]
),
# 只加人工审批:拦住 refund 这个工具
"③ 加 interrupt_on": create_deep_agent(
model=MODEL, tools=[get_weather, refund], interrupt_on={"refund": True}
),
# 只加一个自定义子智能体
"④ 加自定义 subagent": create_deep_agent(
model=MODEL,
tools=[get_weather],
subagents=[{
# 子智能体的名字,模型靠它来指定派谁
"name": "weather-reporter",
# 描述会出现在 task 工具的说明书里,模型靠它判断该不该派
"description": "专门写天气播报稿",
# 子智能体自己的系统提示词
"system_prompt": "你是天气播报员。",
# 子智能体能用的工具,可以比主智能体少
"tools": [get_weather],
}],
),
}
for label, ag in cases.items():
nodes = list(ag.get_graph().nodes.keys())
t = tools_of(ag)
print(f"\n=== {label} ===")
print(f"节点({len(nodes)}): {nodes}")
print(f"工具({len(t)}): {t}")运行输出(为方便阅读手动折了行):
=== ① 默认 ===
节点(5): ['__start__', 'model', 'tools', 'PatchToolCallsMiddleware.before_agent', '__end__']
工具(10): ['delete', 'edit_file', 'execute', 'get_weather', 'glob', 'grep', 'ls',
'read_file', 'task', 'write_file']
=== ② 加 TodoListMiddleware ===
节点(6): ['__start__', 'model', 'tools', 'PatchToolCallsMiddleware.before_agent',
'TodoListMiddleware.after_model', '__end__']
工具(11): ['delete', 'edit_file', 'execute', 'get_weather', 'glob', 'grep', 'ls',
'read_file', 'task', 'write_file', 'write_todos']
=== ③ 加 interrupt_on ===
节点(6): ['__start__', 'model', 'tools', 'PatchToolCallsMiddleware.before_agent',
'HumanInTheLoopMiddleware.after_model', '__end__']
工具(11): ['delete', 'edit_file', 'execute', 'get_weather', 'glob', 'grep', 'ls',
'read_file', 'refund', 'task', 'write_file']
=== ④ 加自定义 subagent ===
节点(5): ['__start__', 'model', 'tools', 'PatchToolCallsMiddleware.before_agent', '__end__']
工具(10): ['delete', 'edit_file', 'execute', 'get_weather', 'glob', 'grep', 'ls',
'read_file', 'task', 'write_file']三条结论,逐个说。
一、write_todos 确实要自己开(②)。 加了 TodoListMiddleware 之后,工具从 10 个变 11 个,多的正是 write_todos;同时图上多了一个 TodoListMiddleware.after_model 节点。这两处变化互相印证:它是一个「既加工具、又插节点」的中间件。什么时候值得开,官方给的场景是:任务特别长、模型能力较弱需要一个明确的自我问责工具、或者前端要展示任务进度。第 47 章会细讲。
二、interrupt_on 会改变图的形状(③)。 多出来的节点是 HumanInTheLoopMiddleware.after_model——和第 19 章 §2.3 打印出来的一模一样。这不是巧合:interrupt_on 底下就是第 10 章那个 HumanInTheLoopMiddleware,钩子是 after_model(它要审的是「模型提出的工具调用」,所以必须等模型答完才动手)。
这条印证了 §2 那张能力对照表:同一件事,create_agent 里叫「传一个 middleware」,Deep Agents 里叫「传一个 interrupt_on 参数」,底层是同一个东西。 你在第 10 章学的审批策略、第 26 章学的 Command(resume=...) 恢复方式,在这里全部照用。
三、加子智能体既不加工具、也不插节点(④)。 节点数和工具数和默认配置完全一样。那自定义的 weather-reporter 去哪了?
在 task 工具的说明书里。把两种配置下 task 的说明书里「可选智能体类型」那几行打印出来对比就清楚了(用 §3.3 的 SpyModel 截获工具对象):
# 复用 §3.3 定义的 SpyModel、CAP 和 get_weather
from deepagents import create_deep_agent
def agent_types_in_task(agent):
"""打印 task 工具说明书里列出的可选子智能体类型。"""
CAP.clear()
# 跑一次,触发 bind_tools
agent.invoke({"messages": [{"role": "user", "content": "hi"}]})
for t in CAP["toolobjs"]:
# 只看 task 这个工具
if getattr(t, "name", "") == "task":
for line in t.description.splitlines():
# 说明书里用 "- 名字: 描述" 的格式列出可选类型
if line.startswith("- "):
# 截断长行,避免输出太宽
print(" ", line[:90])
print("=== 默认(只有内置的 general-purpose)===")
agent_types_in_task(create_deep_agent(model=SpyModel(), tools=[get_weather]))
print("\n=== 加了 weather-reporter 之后 ===")
agent_types_in_task(create_deep_agent(
model=SpyModel(),
tools=[get_weather],
subagents=[{
"name": "weather-reporter",
"description": "专门写天气播报稿",
"system_prompt": "你是天气播报员。",
"tools": [get_weather],
}],
))运行输出(每行都做了截断,省略号是我加的):
=== 默认(只有内置的 general-purpose)===
- general-purpose: General-purpose agent for researching complex questions, ...
- Launch multiple agents concurrently when their tasks are independent, ...
- Each invocation is stateless by default: the agent sees only the prompt you give it ...
- The agent's report is not shown to the user; relay a summary yourself.
- Tell the agent whether to create content, analyze, or only research, ...
- If an agent's description says to use it proactively, do so without waiting to be asked.
- When only general-purpose is available, use it for any complex, context-heavy task; ...
=== 加了 weather-reporter 之后 ===
- general-purpose: General-purpose agent for researching complex questions, ...
- weather-reporter: 专门写天气播报稿
- Launch multiple agents concurrently when their tasks are independent, ...
- Each invocation is stateless by default: the agent sees only the prompt you give it ...
- The agent's report is not shown to the user; relay a summary yourself.
- Tell the agent whether to create content, analyze, or only research, ...
- If an agent's description says to use it proactively, do so without waiting to be asked.
- When only general-purpose is available, use it for any complex, context-heavy task; ...两组输出只差一行:你写的那句 description 原样变成了说明书里的 - weather-reporter: 专门写天气播报稿,其余七行(general-purpose 和六条使用纪律)一字未改。
这个机制很值得记住,因为它直接决定了怎么写子智能体的描述:
description不是给人看的注释,而是模型决定「派不派、派给谁」的唯一依据。 写「专门写天气播报稿」,模型才知道遇到播报需求该派它;写成「子智能体 1」,这个子智能体基本不会被用到。
这也解释了 §3.4 那个发现的实际价值:因为子智能体是「task 说明书里的一行」,所以加子智能体几乎不改变系统结构——不新增节点、不新增工具,只是让那段说明书长一点。第 18 章我们用 LangGraph 手写多智能体时,每个专家都是一个真实的图节点;这里它们变成了一份「可选清单」,由模型在运行时决定派谁。两种做法的取舍(结构性保证 vs 灵活性),正是第 19 章 §3 讨论过的那件事,第 47 章会重新拿出来讲。
把这一节的结论收成一张速查表:
| 能力 | 默认状态 | 怎么开 | 会改图形状吗 |
|---|---|---|---|
| 虚拟文件系统(7 个工具) | 默认开 | — | 不会 |
子智能体 task + general-purpose |
默认开 | — | 不会 |
| 摘要与上下文卸载 | 默认开 | — | 不会 |
| 提示缓存 | 默认开(仅 Anthropic / Bedrock 生效) | — | 不会 |
execute(shell) |
关(后端不支持时隐藏) | 换沙箱后端 | 不会 |
任务规划 write_todos |
关(v0.7 起) | 传 TodoListMiddleware |
会(+1 节点) |
| 人工审批 | 关 | 传 interrupt_on |
会(+1 节点) |
| Skills | 关 | 传 skills= |
不会 |
| Memory | 关 | 传 memory= |
不会 |
| 文件权限 | 关(不配就全放开) | 传 permissions= |
不会 |
| 自定义子智能体 | 关 | 传 subagents= |
不会 |
最后一列有个规律值得记:只有「节点型钩子」的能力会改图形状(write_todos 的 after_model、审批的 after_model、Patch 的 before_agent)。其余能力都走 wrap_model_call 这条路,改的是「发给模型的内容」而不是「图的走向」。
倒数第三行「文件权限:不配就全放开」要特别注意。 官方对权限规则的说明是「按声明顺序、首条命中;如果没有任何规则匹配,操作被允许」。也就是说默认配置下,智能体对整个虚拟文件系统有完整读写权限。用 StateBackend(文件只存在内存状态里)时这没什么风险;一旦换成 FilesystemBackend 落到真实磁盘上,就必须认真配 permissions——第 43 章专门讲这件事。
7. 和 Claude Agent SDK 比 #
§2 提过,Deep Agents 的同层竞品是 Claude Agent SDK。既然要做选型判断,就得知道这两家的取舍差在哪。
本节内容对应官方 Comparison with Claude Agent SDK。注意这是官方自己写的对比,立场偏向 Deep Agents,官方也在页尾注明了「本对比撰写于 2026 年 4 月 16 日,产品若有变化请提 issue」。所以下表适合用来理解两者的设计取向,不适合当作最终的产品评测结论。
| 维度 | Deep Agents | Claude Agent SDK |
|---|---|---|
| 智能体跑在哪 | 沙箱内,或沙箱外远程执行命令 | 只能在沙箱内 |
| 执行后端 | 可插拔:本地、虚拟文件系统、远程沙箱、自定义 | 沙箱的本地文件系统 |
| 模型供应商 | 任意(Anthropic、OpenAI、Google 等 100+) | 仅 Claude(Anthropic、Bedrock、Vertex、Azure) |
| 按模型调优 | harness profile(beta)声明式打包 | 在每个调用点写代码配置 |
| 部署 | LangSmith 托管,或 langgraph build 自建镜像 |
自建,服务端 / 认证 / 流式都要自己写 |
| 多租户 | 内置:线程隔离、按用户分配沙箱、RBAC | 自己实现 |
| 许可证 | MIT | MIT(Claude Code 本身闭源) |
最值得理解的是第一行那个架构差异,它决定了后面几行:
模式 A(两家都支持):智能体跑在沙箱里,直接操作沙箱的本地文件
┌──── 沙箱 ────┐
│ 智能体 + 文件 │
└──────────────┘
模式 B(只有 Deep Agents 支持):智能体跑在长生命周期容器里,把沙箱当成一个工具
┌── 容器 ──┐ ┌── 远程沙箱 ──┐
│ 智能体 │ ──网络─→ │ 执行命令 │
└──────────┘ └──────────────┘模式 B 的价值在多租户上:一个服务要同时服务很多用户时,模式 A 得给每个用户起一个沙箱、记住哪个沙箱属于谁、用完再销毁——这套「按用户开销毁沙箱」的管理代码,用 Claude Agent SDK 就得自己写。Deep Agents 把它做进了 harness,配一下就行。官方还提到 Deep Agents 已经在 OpenSWE 和 LangSmith Fleet 上跑生产。
官方给的选择建议,我原样转述(这是对比页的 Summary 段):
- 选 Deep Agents:想要模型与基础设施的灵活性、内置的多租户部署,以及「托管和自建之间切换不用改代码」
- 选 Claude Agent SDK:已经深度投入 Anthropic 生态,并且愿意自己搭 API、认证和多租户层
对本教程的读者,还有一条更实际的理由:Deep Agents 和你前 38 章学的东西是同一套生态。 LangSmith 观测(第 29~38 章)、LangGraph 图编排(第 19~28 章)、create_agent 的 middleware(第 10 章)在 Deep Agents 里全部能继续用——§3.1 那行 MRO 已经证明了这一点。换成 Claude Agent SDK,这些积累就得重新对接一遍。
顺带说一个容易搞混的地方:Deep Agents 支持所有主流模型,但不代表所有模型跑起来效果一样。 §3.4 实测的工具面有 11058 字符、9 个内置工具,弱模型面对这么大的工具面容易挑错工具。官方在 Models 页给了「建议模型」清单(经过实测推荐的那几个)。本章用
deepseek-v4-flash跑本地实验够用,但真要上生产,选模型这件事得按那份清单认真对一遍——第 40 章会展开。
8. 选型判断表 #
这是本章要带走的东西。现在你手里有三种做法,遇到新需求按下面的顺序判断。
为什么需要一张表? 第 19 章 §5 说过一次:凭感觉选,结果往往是「哪个我更熟就用哪个」。现在选项从两个变成三个,这个问题更严重了——刚学完 Deep Agents 的人容易什么都用 create_deep_agent(毕竟一行就能跑),而 §4.1 那 7.4 倍的输入 token 是实打实的成本。
8.1. 该用 Deep Agents 的信号 #
命中任意一条就可以考虑 Deep Agents。这几条的共同点是:任务有「过程」和「产出物」,不只有「一问一答」。
| # | 信号 | 典型表述 | 为什么 create_agent 不够 |
|---|---|---|---|
| 1 | 要产出文件或中间产物 | 「查完资料写成一份报告存下来」 | 工具面里没有写文件的能力(§4.2 实测:它只能说「我做不到」) |
| 2 | 一次任务要跑很多步、很久 | 「把这个仓库的文档全部审一遍」 | 历史会撑爆上下文,摘要与卸载得自己搭 |
| 3 | 中间结果很大,不能全堆在对话里 | 「每次搜索返回几万字,要挑有用的」 | 上下文卸载得自己实现(第 46 章) |
| 4 | 要把子任务隔离出去并行做 | 「五个子课题分别调研,最后汇总」 | 第 18 章手写过,工作量不小 |
| 5 | 要带一套稳定的领域规范或工作流 | 「所有报告都按公司模板和口径写」 | Skills / Memory 得自己搭(第 44~45 章) |
| 6 | 任务是开放式的,步骤不能预先列全 | 「排查这个线上问题,原因未知」 | 这正是 harness 的设计目标:自主决策 |
第 6 条是和第 19 章那张表方向相反的信号,值得对照着记:
| 需求性质 | 该用什么 | 依据 |
|---|---|---|
| 步骤确定,且要能向审计证明 | 自己写图(第 19~28 章) | 结构性保证,打印图就能查 |
| 步骤不确定,要模型自己规划 | Deep Agents | 预置的规划、文件系统、子智能体 |
| 介于两者之间:流程固定,某一步需要理解力 | 图套 Agent(第 19 章 §5.4) | 外层确定性,内层塞一个 Agent |
「要能向审计证明」和「让模型自己规划」是一对矛盾,因为自主性越高,可证明性越低。这也是为什么放款、退款这类动作不该交给一个自由发挥的 deep agent——第 19 章 §3 那组实验的结论在这里依然成立。
8.2. 反过来:这些情况别用 Deep Agents #
| 情况 | 说明 |
|---|---|
| 就是「问答 + 调几个工具」 | §4.1 实测:输入 token 白涨 7.4 倍,行为一模一样 |
| 单次调用要求低延迟、成本可控 | 固定工具面每轮都要重发,对高频接口不划算 |
| 流程是确定的业务规则 | 用图(第 19 章),别让模型自由发挥 |
| 用的是能力较弱或上下文窗口小的模型 | 9 个内置工具会挤占窗口,也更容易挑错工具 |
| 只是想加重试、限流、脱敏、审批 | 这些 create_agent 的 middleware 都有现成的(第 10 章) |
团队还没摸清 create_agent |
先把 framework 层用熟,再上 harness |
倒数第二条要强调一下:「我需要人工审批」不是上 Deep Agents 的理由。 第 10 章的 HumanInTheLoopMiddleware 就能做,create_deep_agent 的 interrupt_on 底下也是同一个东西(§6 已经用图证明了)。为了一个审批功能付 7.4 倍的 token,不值。
8.3. 判断流程 #
新需求来了
│
▼
流程里有「必须执行、不能跳过」的步骤,且要能向审计交代吗?
│
├─ 有 ─────────────────────────────────────► 自己写图(第 19~28 章)
│ (需要理解力的那步塞一个 Agent 进去)
▼ 没有
任务需要产出文件 / 跑很久 / 处理大中间结果 / 拆子任务吗?
│
├─ 需要 ───────────────────────────────────► create_deep_agent(第 40 章起)
│
▼ 不需要
用 create_agent + middleware(第 9~10 章)
│
▼
不够用了?
│
├─ 卡在「没有文件系统 / 上下文管不住 / 要拆子任务」 ──► 换 create_deep_agent
│
└─ 卡在「流程控制不住、要可证明」 ─────────────────► 换成自己写图最后那个分叉是这张图最重要的部分。 同样是「create_agent 不够用了」,往哪个方向走取决于卡在什么上:
| 卡点 | 往哪走 | 第 19 章 / 本章的依据 |
|---|---|---|
| 「它没法把结果存下来」 | Deep Agents | §4.2:工具面里没有写文件能力 |
| 「跑十几步之后它就忘了前面」 | Deep Agents | 摘要与卸载是内置的(第 46 章) |
| 「它偶尔会跳过风控那一步」 | 写图 | 第 19 章 §3.2:删掉提示词约束后 5/5 被绕过 |
| 「工具执行完必须接一个确定性步骤」 | 写图 | 第 19 章 §3.4:tools 只有一条出边回 model |
这两类卡点的方向是相反的:一类要「更自主」,一类要「更受控」。 用错方向会很难受——给一个需要审计的退款流程套 deep agent,等于在自主性上加码,问题只会更严重。
8.4. 三者可以叠着用 #
和第 19 章 §5.4 一样,选型不是三选一。因为 create_deep_agent 返回的也是 CompiledStateGraph(§3.1 已证明),它同样能当图的一个节点。
所以最完整的生产架构长这样:
┌──────────── LangGraph 图(业务流程,确定性,可审计)────────────┐
│ │
│ 参数校验 ──► 风控检查 ──┬──► create_deep_agent 调研并写报告 ──► 人工审批 ──► 归档
│ (纯代码) (纯代码) │ (自主、多步、要产出物) (interrupt) (纯代码)
│ └──► 拒绝 │
└────────────────────────────────────────────────────────────────────────────┘各层的分工很清楚:
- 图负责「必须发生」和「不能发生」——风控不通过,deep agent 根本不会被调用,连模型的钱都不用付
- deep agent 负责那个说不清步骤的环节——调研、整理、产出文件
create_agent在这个架构里也有位置:某个环节只需要「调两个工具答一句话」,用它比 deep agent 省 7 倍 token
一句话记:图管「不许」,harness 管「不知道怎么办」,framework 管「知道该干什么、按我说的干」。
9. 实用约定与坑 #
| 约定 | 说明 |
|---|---|
先确认 deepagents 版本 |
0.7 改过默认行为,write_todos 不再默认开(§6) |
数工具面看 bind_tools,不看 ToolNode |
两者默认就差一个 execute(§3.3) |
| deep agent 行为异常,别只打印图 | 它永远是那 5 个节点,要看工具面和消息轨迹(§3.2) |
子智能体的 description 当提示词写 |
它是模型决定派不派的唯一依据(§6) |
| 简单问答别上 deep agent | 输入 token 白涨 7.4 倍(§4.1) |
落到真实磁盘前先配 permissions |
不配规则等于全放开(§6) |
| 要审计的步骤仍然放图里 | harness 提高的是自主性,不是可证明性(§8.1) |
| 尽早打开 LangSmith Tracing | deep agent 步数多,靠 print 排障很快就不够用了 |
常见坑按「会静默出错」和「会明确报错」分开列——前一类才真正危险:
一、静默失效(不报错,行为悄悄不一样)
| 现象 | 原因 | 处理 |
|---|---|---|
模型从不使用 write_todos |
v0.7 起它默认不存在,不是模型不听话 | 显式传 TodoListMiddleware(§6) |
模型从不调 execute |
默认 StateBackend 不支持执行,该工具对模型隐藏 |
换沙箱后端(第 43 章) |
| 自定义子智能体一次都没被派过 | description 写得太笼统,模型无法判断何时该派 |
把 description 当提示词认真写(§6) |
以为自己的 system_prompt 被内置提示词覆盖了 |
实测内置提示词是空的,你传的会原样使用 | 不必为此改写提示词(§3.4) |
| 简单场景下账单莫名变高 | 11058 字符的工具面每轮都重发 | 换 create_agent,或用 excluded_tools 裁剪(第 41 章) |
| 文件写了,下一轮却读不到 | 默认 StateBackend 是线程内的,没配 checkpointer 就不跨轮次 |
配 checkpointer,或换持久后端(第 42 章) |
二、会明确报错
| 现象 | 原因 | 处理 |
|---|---|---|
ModuleNotFoundError: No module named 'deepagents' |
它是独立库,不随 langchain 安装 |
pip install -U deepagents |
安装后 langchain 版本被动升级 |
deepagents 对 langchain / langchain-core 有下限要求 |
正常现象,本章环境从 1.3.14 升到 1.3.18 |
移除 FilesystemMiddleware 时报错被拒 |
它是 Deep Agents 栈的必需脚手架,官方明确禁止整体移除 | 用 excluded_tools 只隐藏工具(第 41 章) |
| 模型报「不支持工具调用」 | Deep Agents 强依赖 tool calling | 换一个支持工具调用的模型(第 40 章) |
三、概念性误解
| 误解 | 纠正 |
|---|---|
| 「Deep Agents 是 LangChain 的升级版,该全面替换」 | 不同层:harness vs framework,按任务性质选(§2、§8) |
「用了 Deep Agents 就用不上 LangGraph / create_agent 了」 |
它就是 CompiledStateGraph,能当图的节点(§3.1、§8.4) |
| 「harness 提供『预置提示词』,所以内置了一大段人设」 | 实测内置提示词为空,知识在工具说明书里(§3.4) |
| 「装了这么多能力,图一定很复杂」 | 只多 1 个节点,能力靠 wrap_model_call 实现(§3.2) |
| 「Deep Agents 更自主,所以更适合放款、退款这类流程」 | 恰恰相反,自主性越高越难证明(§8.1) |
| 「支持所有模型,那随便挑一个都行」 | 大工具面对弱模型不友好,要按官方建议清单选(§7) |
口诀:
framework 管「按我说的干」,runtime 管「不许乱来」,harness 管「你自己想办法」。 需求属于哪一类,就用哪一层。
10. 练习 #
分两组:前四题是机制验证,跑一遍就知道有没有真看懂,而且全部不花 token;后四题需要真实调用或动手设计。
机制验证(零成本)
- 打开你自己的 deep agent:用 §3.2 的
show函数打印一个create_deep_agent的图,再打印第 9 章那个多工具办公助理的图。两张图的节点差几个?把差异解释清楚。 - 预判工具数:在跑代码之前先猜——
create_deep_agent(model=..., tools=[a, b, c])传 3 个业务工具,ToolNode里会有几个工具?发给模型的是几个?跑一遍验证,差异出在哪个工具上。 - 验证 opt-in:用 §6 的四种配置跑一遍,确认
write_todos默认不存在。然后再加一个PIIMiddleware("email", strategy="redact")(第 10 章),看它会不会插节点——先用第 19 章 §2.3 那张钩子表预判,再跑。 - 称一下自己工具的重量:用 §3.4 的
weigh函数,统计你在第 8 章或第 15 章写的业务工具包的工具面字符数,和内置的 11058 字符比一比。如果你的业务工具本身就有好几千字符,create_deep_agent的固定成本占比反而没那么夸张——算算实际比例。
能力建设
- 复现成本对照:跑一遍 §4.1 的实验,换成你自己常用的模型和一个真实业务工具。记录输入 token 的放大倍数,再按你的日均请求量估算一天的成本差。
- 找一个只有 deep agent 能做的任务:仿照 §4.2,设计一个任务让
create_agent明确回答「我做不到」,而create_deep_agent能完成。不要用「保存文件」这个现成答案,试试「把中间结果存下来,第二轮对话接着改」(需要配checkpointer)。 - 选型练习:给下面五个需求各判断该用
create_agent、create_deep_agent还是自己写图,并说明命中了 §8.1 / §8.2 的哪一条—— (a)内部文档问答机器人;(b)把一个季度的工单导出来做归因分析并出报告;(c)员工报销审批,超 5000 元需总监签字;(d)给定一个模糊的线上故障描述,自主排查并写复盘;(e)客服助手,能查订单、能退款,退款需人工确认。 - (扩展)图套 deep agent:把 §8.4 那张架构图实现出来——外层用 LangGraph 做「参数校验 → 风控 → deep agent 产出报告」,风控不通过直接拒绝。跑一遍黑名单用户,确认 deep agent 一次都没被调用(参照第 19 章 §5.4 的做法,那里用的是
create_agent,这次换成create_deep_agent)。
11. 本章小结 #
这一章没教怎么配置 Deep Agents,做的是拆开一层封装 + 建立选型判断:
- 三层分工要分清:runtime(LangGraph)管持久执行与底层控制,framework(LangChain
create_agent)管抽象与集成,harness(Deep Agents)管预置工具、提示词和子智能体。它们是上下三层,不是竞品——Deep Agents 的同层对手是 Claude Agent SDK。 create_deep_agent返回的还是CompiledStateGraph,MRO 和create_agent一字不差。所以第 19~28 章学的图、状态、checkpoint、interrupt 全部照用,它也能当另一张图的节点。- 图只多了 1 个节点(
PatchToolCallsMiddleware.before_agent,负责修复悬空的工具调用)。装了文件系统、子智能体、上下文压缩,图的主干却完全没变。 - 原因是钩子类型不同:
FilesystemMiddleware和SubAgentMiddleware只实现wrap_model_call/wrap_tool_call这类包装型钩子,不插节点。只有节点型钩子(before_agent/after_model等)才会改图的形状。 - 真正的变化在工具面:你传 1 个工具,模型拿到 9 个(内置 7 个文件工具 +
task)。ToolNode里注册的是 10 个,多的那个execute因为默认后端不支持执行而对模型隐藏——数工具面要看bind_tools,不要看ToolNode。 - 最反直觉的一处:内置系统提示词是空的。 不传
system_prompt时那条SystemMessage的正文是'',传了就原样使用,不做任何拼接。 - 内置知识全装在工具说明书里:工具面从 164 字符涨到 11058 字符(67 倍)。
read_file的 1220 字符里写着「改文件前必须先读」「大结果会被卸载到文件、路径在工具消息里」,task的 1419 字符是整套子智能体机制的说明书。这样做的好处是工具被禁用时说明书跟着一起消失,不会白花 token。 - 代价是固定成本:简单问题上实测输入 token 从 784 涨到 5835(7.4 倍),而行为、工具调用、回答质量完全一样。而且这是每轮都要重发的,轮数越多亏得越多。
- 但换来了做不到变做得到:要求「保存为 report.md」时,
create_agent明确回答「我没有文件写入功能,无法保存」,deep agent 真的在state["files"]里写出了/report.md。这不是快慢之差,是能不能做之差——加提示词、换强模型都补不上。 - v0.7 改了默认值:
write_todos从默认内置改成要显式传TodoListMiddleware。这类改动不报错,照旧教程写代码只会发现「模型死活不用这个工具」。 interrupt_on底下就是第 10 章那个 HITL middleware:加了它,图上会多一个HumanInTheLoopMiddleware.after_model节点,和第 19 章打印出来的一模一样。所以「我需要人工审批」不是上 Deep Agents 的理由。- 加子智能体既不加工具也不插节点,只是在
task的说明书里多一行。所以子智能体的description是模型决定「派不派、派给谁」的唯一依据,要当提示词认真写。 - 选型口诀:需求要「产出物 + 过程管理 + 自主规划」→ Deep Agents;需求是「问答 + 调工具」→
create_agent;需求要「必须发生、能向审计证明」→ 自己写图。同样是「create_agent不够用」,卡在「存不下结果」就往 harness 走,卡在「流程控不住」就往图走——两个方向相反,走错会更糟。 - 三者可以叠着用:外层图管「不许」,中间 deep agent 管「不知道怎么办」,
create_agent管「按我说的干」。因为 deep agent 也是CompiledStateGraph,add_node就能把它塞进图里。
下一章开始动手:装依赖、配模型和搜索工具,跑通第一个 deep agent,并亲眼看它自动写文件、派子智能体。