手撕 LangChain Deep Agents 源码 (一):create_deep_agent 是如何"组装"出一个 AI 操作系统的
系列定位:这不是官方文档翻译,而是一个从零到懂的源码阅读笔记。我会用最通俗的语言,带你把 Deep Agents 的每一行代码都看个明明白白------保证看完之后比看源码之前头发还多(大概)。
源码版本 :
deepagents 0.5.9阅读前建议 :你需要对 LangChain 的BaseChatModel、BaseTool、LangGraph 有基本了解。如果你用过create_react_agent,那更好了------你会发现 Deep Agents 就是它的"究极进化形态"。
先说结论:create_deep_agent 到底干了什么?
一句话总结:
create_deep_agent是一个"装电脑的大师傅"。你给它一颗 CPU(LLM 模型)和一堆外设(工具),它帮你把主板、内存、硬盘、驱动程序全部装好,最后交付一台能直接用的电脑。
用代码说就是:
python
from deepagents import create_deep_agent
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
tools=[my_custom_tool],
memory=["/memory/AGENTS.md"],
skills=["/skills/project/"],
)
这一行看似简单的代码背后,create_deep_agent 默默做了这些事:
- 🔧 解析模型字符串,初始化 LLM 客户端
- 🧠 加载模型专属的"性格配置"(Harness Profile)
- 🛠️ 组装 8+ 个中间件(Middleware),形成完整的处理链
- 🤖 自动创建一个"通用子智能体"(General Purpose SubAgent)
- 📝 拼装系统提示词(System Prompt),包含 4 层叠加逻辑
- 📋 注入默认工具集(文件操作、Shell 执行、TODO 管理)
- 🔒 校验权限和中间件排除规则
- 🚀 最终调用
create_agent,设置 9999 的递归上限,交付成品
如果用装电脑来比喻,大概是这样:
| 步骤 | 装电脑比喻 | Deep Agents 实际行为 |
|---|---|---|
| 选 CPU | 选 i9 还是 R9? | 解析 model 参数,初始化对应 LLM |
| 刷 BIOS | 刷主板固件 | 加载 Harness Profile(模型专属调优) |
| 插内存条 | DDR5 插满 | 组装 Middleware 链(TodoList、FileSystem、Memory...) |
| 挂硬盘 | SSD 插 M.2 | 绑定 Backend(State/Filesystem/Sandbox) |
| 装外设驱动 | 显卡驱动 + 网卡驱动 | 注入工具(Tools)并绑定中间件 |
| 装 OS | 装 Windows/Linux | 拼装 System Prompt |
| 跑分 | 3DMark | 设置 recursion_limit=9999 交付 |
💡 如果面试官问你 :"
create_deep_agent和create_react_agent有什么区别?"你可以这么答:"
create_react_agent是一台裸机------给你 CPU 和工具,剩下的自己搞。create_deep_agent是一台整机------CPU、主板、内存、硬盘、驱动、操作系统全部装好,甚至还自带一个万能助理。你拿到手就能用。核心差异在于:Deep Agents 自动组装中间件栈、自动创建子智能体、自动匹配模型 Profile,让 Agent 从'能用'变成'好用'。"
源码目录结构:先认识一下"全家桶"
在正式开撕之前,我们先看一下 Deep Agents 的完整目录结构。
bash
deepagents/
├── __init__.py # 包入口,导出核心 API
├── _version.py # 版本号 0.5.9
├── graph.py # 🌟 核心入口!create_deep_agent 就在这里
├── _models.py # 模型解析工具(resolve_model)
├── _tools.py # 工具描述重写工具
├── _excluded_middleware.py # 中间件排除/校验逻辑
│
├── _api/ # 废弃警告等 API 级别工具
│ └── deprecation.py
│
├── backends/ # 🗄️ 存储后端(文件系统、状态、沙箱)
│ ├── protocol.py # 后端协议接口定义
│ ├── state.py # StateBackend(内存后端)
│ ├── filesystem.py # FilesystemBackend(本地文件系统)
│ ├── sandbox.py # SandboxBackend(沙箱后端)
│ ├── langsmith.py # LangSmith 远程后端
│ ├── store.py # StoreBackend
│ ├── composite.py # 组合后端
│ └── ...
│
├── middleware/ # 🔌 中间件(灵魂所在!)
│ ├── filesystem.py # 文件系统中间件(ls/read/write/edit/glob/grep)
│ ├── subagents.py # 子智能体中间件(task 工具)
│ ├── async_subagents.py # 异步子智能体中间件
│ ├── summarization.py # 对话摘要中间件(自动压缩上下文)
│ ├── memory.py # 记忆中间件(加载 AGENTS.md)
│ ├── skills.py # 技能中间件(加载 SKILL.md)
│ ├── patch_tool_calls.py # 修补悬挂工具调用
│ ├── _tool_exclusion.py # 工具排除中间件
│ └── _utils.py # 中间件公共工具
│
└── profiles/ # 🎭 模型配置档案
├── harness/ # Harness Profile(运行时行为配置)
│ ├── harness_profiles.py # Profile 核心定义
│ ├── _anthropic_sonnet_4_6.py # Claude Sonnet 4.6 专属配置
│ ├── _anthropic_opus_4_7.py # Claude Opus 4.7 专属配置
│ ├── _anthropic_haiku_4_5.py # Claude Haiku 4.5 专属配置
│ └── _openai_codex.py # OpenAI Codex 专属配置
└── provider/ # Provider Profile(模型构造配置)
├── provider_profiles.py
├── _openai.py
└── _openrouter.py
用装电脑来理解这个结构:
graph.py------ 总装车间。所有的零件在这里被组装成一台完整的电脑。middleware/------ 驱动程序。每个中间件都是一个独立的功能模块,像驱动一样被加载。backends/------ 硬盘。不管底下是 SSD 还是 HDD,对上层都是同一个"存取"接口。profiles/------ BIOS 固件。不同 CPU 有不同的出厂设置。
正片开始:逐行解剖 create_deep_agent
好,准备工作做完了。现在我们打开 graph.py,直接对着源码开始撕。沏杯茶,上菜了。
函数签名:每个参数都是什么?
python
def create_deep_agent(
# ===== 核心参数 =====
model: str | BaseChatModel | None = None,
# LLM 大模型。可以传字符串如 "anthropic:claude-sonnet-4-6",
# 也可以直接传 BaseChatModel 实例。None 则用默认模型(已废弃)
tools: Sequence[BaseTool | Callable | dict] | None = None,
# 用户自定义工具列表。可以是 BaseTool 实例、普通函数、或工具描述字典。
# 这些工具会追加到中间件提供的默认工具集(文件操作等)之后
# ===== 行为配置 =====
*,
system_prompt: str | SystemMessage | None = None,
# 自定义系统提示词。字符串会拼在 BASE 提示词前面;
# SystemMessage 形式会保留 Anthropic 的 cache_control 标记
middleware: Sequence[AgentMiddleware] = (),
# 用户自定义中间件序列。插入在基础栈之后、尾部栈之前
subagents: Sequence[SubAgent | CompiledSubAgent | AsyncSubAgent] | None = None,
# 子智能体列表。三种类型:
# SubAgent --- 声明式,系统自动编译
# CompiledSubAgent --- 预编译好的
# AsyncSubAgent --- 远程异步(LangSmith Deployments)
# 不传则自动创建一个 general-purpose 子智能体
# ===== 知识与记忆 =====
skills: list[str] | None = None,
# 技能目录路径列表。SkillsMiddleware 会从这些路径加载 SKILL.md,
# 并将内容注入系统提示词
memory: list[str] | None = None,
# 记忆文件路径列表。MemoryMiddleware 会读取这些文件(如 AGENTS.md),
# 并将内容注入到对话上下文中
# ===== 权限与存储 =====
permissions: list[FilesystemPermission] | None = None,
# 文件系统权限列表。控制 Agent 可以读写哪些目录,
# 未声明则默认允许当前工作目录
backend: BackendProtocol | BackendFactory | None = None,
# 存储后端。决定文件操作、状态、沙箱等底层实现。
# None 则自动根据环境选择(本地 vs 容器)
# ===== 高级控制 =====
interrupt_on: dict[str, bool | InterruptOnConfig] | None = None,
# 人机协作中断配置。键为工具名,值为 True(始终中断)、
# InterruptOnConfig(条件中断)。
# 匹配的工具执行前会暂停,等待人类确认后继续
response_format: ResponseFormat | type | dict | None = None,
# 结构化输出格式。可以是 Pydantic 模型、JSON Schema 等,
# 让 Agent 的最终回复符合指定格式
context_schema: type | None = None,
# 对话上下文的类型定义。用于向 Agent 提供运行时环境信息
# (如当前工作目录、用户信息等)
# ===== 持久化与调试 =====
checkpointer: Checkpointer | None = None,
# 检查点存储。用于保存和恢复对话状态,实现多轮对话持久化。
# None 则无持久化,每次启动都是全新对话
store: BaseStore | None = None,
# 长期记忆存储。跨对话的键值存储,LangGraph 的 BaseStore 实现。
# 用于 Agent 在不同对话间共享数据
debug: bool = False,
# 调试模式。开启后输出详细的中间件调用和状态转换日志
name: str | None = None,
# Agent 名称。用于 LangSmith 追踪和日志标识,
# None 则自动生成
cache: BaseCache | None = None,
# LLM 调用缓存。避免重复的 LLM 请求,节省 token 和延迟。
# None 则不缓存
) -> CompiledStateGraph:
"""创建一个深度智能体(Deep Agent),返回编译后的 LangGraph 状态图。"""
18 个参数,看起来是不是有点吓人?别慌,其实核心就 3 个:
| 参数 | 必要性 | 说明 | 装电脑比喻 |
|---|---|---|---|
model |
推荐 | 你选的"大脑" | 选哪颗 CPU |
tools |
可选 | 你给 AI 配的"手" | 插什么外设 |
system_prompt |
可选 | 你对 AI 说的"第一条指令" | 装 OS 时选什么语言包 |
剩下的都是高级配置,后面我们遇到了再细讲。
第一阶段:模型初始化 & Profile 匹配
python
# 如果没传 model,用默认的 Claude Sonnet 4.6(但会报警告)
if model is None:
warn_deprecated(
since="0.5.3",
removal="1.0.0",
message="Passing model=None is deprecated...",
)
model = _build_default_model()
else:
model = resolve_model(model)
这里有两层逻辑:
第一层 :model=None 是"不选 CPU"模式------帮你默认选 Claude Sonnet 4.6,但这个行为在 0.5.3 版本就被标记为废弃了,1.0.0 版本会彻底移除。就像你去装电脑不选配置,老板默认给你最高配------听起来很爽,但下个月就不这么干了,你得自己选。
第二层 :resolve_model(model) 是真正的"CPU 适配器"。如果你传的是字符串 "anthropic:claude-sonnet-4-6",它会:
- 调用 LangChain 的
init_chat_model创建模型实例 - 通过
ProviderProfile注册表应用提供商级别的初始化配置 - 返回一个配置好的
BaseChatModel实例
还是装电脑的比喻:resolve_model 就像你在装机单上写"i9-14900K",老板不光给你拿这颗 CPU,还顺带帮你选好配套的主板芯片组、确认散热器兼容性------不同品牌的 CPU(OpenAI、Anthropic、OpenRouter)有不同的"配套方案"。你写"R9 7950X",老板就给你换 AM5 主板;你写"i9-14900K",老板就给你上 LGA 1700。你不需要关心底层的针脚差异,老板全搞定。
接着是 Profile 匹配:
python
_profile = _harness_profile_for_model(model, _model_spec)
这行代码会根据你的模型自动匹配一个 HarnessProfile。比如你用的是 anthropic:claude-sonnet-4-6,系统就会加载 _anthropic_sonnet_4_6.py 里的配置。
那这个 Profile 里有什么呢?以 Claude Sonnet 4.6 为例:
python
_SYSTEM_PROMPT_SUFFIX = """
<use_parallel_tool_calls>
If you intend to call multiple tools and there are no dependencies between
the tool calls, make all of the independent tool calls in parallel...
</use_parallel_tool_calls>
<investigate_before_answering>
Never speculate about code you have not opened...
</investigate_before_answering>
<tool_result_reflection>
After receiving tool results, carefully reflect on their quality...
</tool_result_reflection>
"""
也就是说,Profile 会在你的系统提示词后面追加一段"性格补丁"------告诉 Claude 要并行调用工具、不要瞎猜代码、拿到工具结果要先想清楚下一步。相当于出厂前工程师偷偷刷了一段微码,让 CPU 跑得更稳。
继续装电脑的比喻:Profile 就像 BIOS 里针对不同 CPU 的微码更新。同样是这块主板,你插 i9 和插 R9,BIOS 会自动加载不同的微码------i9 的微码侧重功耗管理,R9 的微码侧重温度墙策略。你不需要手动刷,BIOS 自己会识别。换 CPU 不换主板,系统照样跑,这就是 Profile 的威力。
💡 如果面试官问你:"Deep Agents 怎么做到不同模型自动适配的?"
答案就是 Harness Profile。每个模型注册一个 Profile,里面包含模型专属的提示词后缀、工具描述覆盖、中间件排除规则。你切模型的时候不需要改一行提示词------Profile 自动帮你适配。这就是为什么你从 Claude 换到 GPT-4o,Agent 照样能正常工作。
第二阶段:子智能体处理(SubAgent 装配线)
这是整个函数最复杂的部分,也是 Deep Agents 和普通 Agent 的核心区别------它不只是一个人在战斗,而是一个团队。
换到"开公司"的比喻来说:普通的 create_react_agent 就是你一个人干所有事------写代码、写文档、改 Bug、发邮件,全是你的活。create_deep_agent 是你开了一家公司,手下有不同职能的员工,你负责指挥,他们负责干活。你是老板,不是打工人。
create_deep_agent 支持三种类型的子智能体:
| 类型 | 类名 | 开公司比喻 |
|---|---|---|
| 声明式子智能体 | SubAgent |
招了个实习生,公司帮你培训上岗 |
| 预编译子智能体 | CompiledSubAgent |
招了个老手,自带技能直接上班 |
| 异步子智能体 | AsyncSubAgent |
外包团队,远程协作 |
代码里是这样区分的:
python
for spec in subagents or []:
if "graph_id" in spec:
# AsyncSubAgent --- 外包团队
async_subagents.append(cast("AsyncSubAgent", spec))
continue
if "runnable" in spec:
# CompiledSubAgent --- 老手直接上班
inline_subagents.append(spec)
else:
# SubAgent --- 实习生,需要完整培训
# ... 一大堆处理逻辑 ...
对于声明式子智能体(SubAgent),系统会做以下"培训":
- 模型解析:子智能体可以有自己的大脑(不继承主智能体的模型)
- Profile 匹配:子智能体也匹配自己的 Harness Profile
- 权限继承:子智能体默认继承主智能体的权限,也可以自己定义
- 中间件组装:为子智能体组装一套独立的中间件栈
- 工具继承:默认继承主智能体的工具,也可以自定义
- Human-in-the-Loop 继承 :主智能体的
interrupt_on配置会传给子智能体
这段代码有一处特别精妙的设计------子智能体的中间件组装:
python
subagent_middleware: list[AgentMiddleware] = [
TodoListMiddleware(), # TODO 管理
FilesystemMiddleware(backend=backend, # 文件系统
custom_tool_descriptions=_subagent_profile.tool_description_overrides,
_permissions=subagent_permissions,
),
create_summarization_middleware(...), # 对话摘要
PatchToolCallsMiddleware(), # 修补悬挂工具调用
]
🔍 设计洞察 :子智能体的中间件比主智能体少!没有
SubAgentMiddleware(员工不需要再招自己的员工------不然套娃下去公司得破产),没有MemoryMiddleware(记忆由老板统一管理),没有HumanInTheLoopMiddleware(和客户的沟通由老板控制)。
但最精彩的还是"自动创建通用子智能体":
python
gp_profile = _profile.general_purpose_subagent or GeneralPurposeSubagentProfile()
if gp_profile.enabled is not False and not any(
spec["name"] == GENERAL_PURPOSE_SUBAGENT["name"] for spec in inline_subagents
):
# ... 自动创建 general-purpose 子智能体 ...
inline_subagents.insert(0, general_purpose_spec)
即使你没有显式传入任何子智能体,create_deep_agent 也会自动帮你创建一个名为 general-purpose 的通用子智能体 ,并且把它插到子智能体列表的第一位。
继续开公司的比喻:这就好比你刚注册了一家公司,连员工都还没招,工商局自动给你派了一个"万能助理"------写文档、查资料、整理文件,什么杂活都能干。你只需要把任务通过 task 工具扔给它就行。免费的,不要白不要。
如果你不想要这个自动创建的助理?可以通过 Profile 配置"裁员":
python
register_harness_profile(
"anthropic:claude-sonnet-4-6",
HarnessProfileConfig(
general_purpose_subagent={"enabled": False}
),
)
但你想想,免费的万能助理你裁掉干嘛?除非你确信自己一个人能扛下所有活。
💡 如果面试官问你:"Deep Agents 的子智能体和 AutoGen 的多 Agent 有什么区别?"
这题有深度。AutoGen 的多 Agent 是"平级对话"------大家坐在会议室里讨论。Deep Agents 的子智能体是"上下级关系"------主 Agent 派活,子 Agent 干活,干完了汇报。关键区别在于:Deep Agents 的子智能体由主 Agent 通过
task工具调度,主 Agent 控制权限、工具、中断策略,层级分明。这更接近真实公司的运作方式------老板不会和实习生平起平坐开会。
第三阶段:主智能体中间件栈组装
这是真正的"主板插内存条"环节。create_deep_agent 会按照严格的顺序组装中间件:
python
deepagent_middleware: list[AgentMiddleware] = [
TodoListMiddleware(), # 1. TODO 管理(最先加载)
]
if skills is not None:
deepagent_middleware.append( # 2. 技能加载(如果配置了)
SkillsMiddleware(backend=backend, sources=skills)
)
deepagent_middleware.append( # 3. 文件系统(核心中间件)
FilesystemMiddleware(backend=backend, ...)
)
if inline_subagents:
deepagent_middleware.append( # 4. 子智能体管理
SubAgentMiddleware(backend=backend, subagents=inline_subagents, ...)
)
if async_subagents:
deepagent_middleware.append( # 5. 异步子智能体
AsyncSubAgentMiddleware(async_subagents=async_subagents)
)
deepagent_middleware.extend([ # 6. 对话摘要 + 工具调用修补
create_summarization_middleware(model, backend),
PatchToolCallsMiddleware(),
])
# --- 用户自定义中间件插入点 ---
if middleware:
deepagent_middleware.extend(middleware)
# --- Profile 额外中间件 + 尾部中间件 ---
deepagent_middleware.extend(_profile.materialize_extra_middleware())
if _profile.excluded_tools:
deepagent_middleware.append(_ToolExclusionMiddleware(...))
deepagent_middleware.append(AnthropicPromptCachingMiddleware(...))
if memory is not None:
deepagent_middleware.append(MemoryMiddleware(...))
if interrupt_on is not None:
deepagent_middleware.append(HumanInTheLoopMiddleware(...))
完整中间件加载顺序图:
sql
┌─────────────────────────────────────────────────┐
│ BASE STACK(基础栈) │
│ │
│ 1. TodoListMiddleware --- TODO 列表管理 │
│ 2. SkillsMiddleware --- 技能加载(可选) │
│ 3. FilesystemMiddleware --- 文件系统操作 │
│ 4. SubAgentMiddleware --- 子智能体调度 │
│ 5. AsyncSubAgentMiddleware --- 异步子智能体(可选) │
│ 6. SummarizationMiddleware --- 对话自动摘要 │
│ 7. PatchToolCallsMiddleware --- 修补悬挂工具调用 │
├─────────────────────────────────────────────────┤
│ USER MIDDLEWARE(用户自定义中间件) │
│ 你的插件插在这里 ↑↑↑ │
├─────────────────────────────────────────────────┤
│ TAIL STACK(尾部栈) │
│ │
│ 8. Profile extra_middleware --- Profile 额外中间件│
│ 9. _ToolExclusionMiddleware --- 工具排除过滤 │
│ 10. AnthropicPromptCachingMiddleware --- 提示缓存 │
│ 11. MemoryMiddleware --- 记忆加载(可选) │
│ 12. HumanInTheLoopMiddleware --- 人机协作(可选) │
└─────────────────────────────────────────────────┘
还是装电脑的比喻:基础栈是出厂就焊在主板上的芯片(必须有的核心功能),用户插入点是主板上的 PCIe 插槽(你想加什么自己插),尾部栈是 BIOS 层面的优化(和性能/安全相关,最后生效)。
所以你写自定义中间件的时候,它在基础栈和尾部栈之间------在核心功能之后、在安全校验之前。位置恰到好处,不会挡住核心流程,也不会被安全策略漏掉。
有几个特别值得注意的中间件:
PatchToolCallsMiddleware ------ 它会检查消息历史,如果发现某个 AI 消息调用了工具但没有收到对应的工具结果(可能是因为上下文溢出导致工具结果被截断),就自动补一个"已取消"的回复,防止后续处理崩溃。
继续装电脑的比喻:这就像机箱里的"断电保护器"------如果某根线松了没插紧,保护器自动帮你把电路断开,防止短路烧主板。不是修复问题,而是确保不会因为一个小毛病导致整台电脑挂掉。这种"坏了我兜底"的设计,在大型系统里特别重要------你可以犯错,但系统不会因为你的错而崩。
AnthropicPromptCachingMiddleware ------ 这个中间件非常聪明:它无条件加载,但对非 Anthropic 模型自动静默跳过 (unsupported_model_behavior="ignore")。这样你切换模型时不需要改任何代码。
还是装电脑:这就像一个万能驱动------你插 NVIDIA 显卡它就干活,你插 AMD 显卡它就安静待着,不会报错也不会蓝屏。主打一个"有了更好,没有也不碍事"。
💡 如果面试官问你:"中间件的加载顺序为什么这么讲究?能随便调吗?"
不能。顺序就是优先级。
TodoListMiddleware在最前面,因为 TODO 管理是基础功能,其他中间件可能依赖它。HumanInTheLoopMiddleware在最后面,因为人机协作是最外层的安全网,必须在所有内部处理完成后才拦截。你随便调,可能就会出这种 bug:Agent 先问你要不要确认,再去做文件操作------顺序反了,确认了个寂寞。
第四阶段:中间件排除校验(安全检查)
组装完中间件栈之后,代码进行了一系列安全校验:
python
# 验证排除配置不会移除必需的脚手架中间件
_validate_excluded_middleware_config(
_profile,
required_classes=_REQUIRED_MIDDLEWARE_CLASSES,
required_names=_REQUIRED_MIDDLEWARE_NAMES,
)
# 应用排除规则
deepagent_middleware = _apply_excluded_middleware(
deepagent_middleware,
_profile,
matched_classes=_main_matched_classes,
matched_names=_main_matched_names,
)
# 验证每个排除规则都匹配到了实际的中间件
_verify_excluded_middleware_coverage(
_profile,
_main_matched_classes,
_main_matched_names,
required_classes=_REQUIRED_MIDDLEWARE_CLASSES,
required_names=_REQUIRED_MIDDLEWARE_NAMES,
)
系统定义了两组不可移除的脚手架中间件:
python
_REQUIRED_MIDDLEWARE = (
(FilesystemMiddleware, ()), # 文件系统中间件------所有文件工具都依赖它
(SubAgentMiddleware, ()), # 子智能体中间件------task 工具依赖它
)
🚧 设计原则 :
FilesystemMiddleware和SubAgentMiddleware被标记为"脚手架"------你可以移除摘要中间件、可以移除记忆中间件,但你不能移除文件系统和子智能体。因为一旦移除,核心功能就会静默失效,这比直接报错更危险。
校验逻辑分三步:
- 语法校验:排除配置中的名称不能为空、不能有下划线前缀、不能有冒号(防拼写错误)
- 脚手架校验 :不能排除
FilesystemMiddleware和SubAgentMiddleware(防核心功能丢失) - 覆盖率校验:每个排除规则都必须匹配到至少一个实际存在的中间件(防无效配置)
🔒 安全哲学:宁可报错让你修,也不能静默失效让你排查三天。这是优秀的库设计------把运行时的静默失败转化为构建时的显式错误。你的配置写错了,启动时就炸,而不是上线后跑了半个月才发现 Agent 怎么不读文件了。
继续装电脑的比喻:你不能把 CPU 供电线拔了然后问"为什么开不了机"。系统直接不让你拔------拔之前就报错提醒你"这根线是必须的"。你要是硬拔,电源直接罢工给你看。
第五阶段:System Prompt 拼装(四层叠加)
create_deep_agent 的系统提示词不是简单的"你是一个 AI 助手",而是一个四层叠加的结构:
sql
┌──────────────────────────────────┐
│ USER(用户自定义) │ ← 你传入的 system_prompt 参数
│ "你是一个开发专家..." │ 永远在最前面,优先级最高
├──────────────────────────────────┤
│ BASE / CUSTOM(基础 / Profile) │ ← BASE_AGENT_PROMPT 或 Profile 的 base_system_prompt
│ "You are a deep agent..." │ Profile 可以完全替换 BASE
├──────────────────────────────────┤
│ SUFFIX(Profile 尾部指令) │ ← Profile 的 system_prompt_suffix
│ "<use_parallel_tool_calls>..." │ 距离对话历史最近,模型最关注
└──────────────────────────────────┘
代码实现:
python
base_prompt = _apply_profile_prompt(_profile, BASE_AGENT_PROMPT)
if system_prompt is None:
final_system_prompt = base_prompt
elif isinstance(system_prompt, SystemMessage):
# 保留 SystemMessage 的 cache_control 标记
final_system_prompt = SystemMessage(
content_blocks=[*system_prompt.content_blocks,
{"type": "text", "text": f"\n\n{base_prompt}"}]
)
else:
# 字符串形式:USER + BASE/CUSTOM
final_system_prompt = system_prompt + "\n\n" + base_prompt
关键设计决策:
- USER 永远在最前面:你传入的指令不会被 Profile 覆盖
- SUFFIX 在最后面:模型对末尾内容关注度最高(这个有心理学研究支撑------人也是这样,最后说的话最容易被记住)
- 支持 SystemMessage 形式 :保留 Anthropic 的
cache_control标记,方便做提示缓存优化
用做菜来比喻:USER 是你点菜时说的"不要辣"(最优先,厨师必须听),BASE 是菜谱的基本做法(默认流程),SUFFIX 是装盘前厨师加的那把葱花(最后加的但最显眼,食客最先看到)。
💡 如果面试官问你:"为什么 USER prompt 放最前面,SUFFIX 放最后面?"
这是故意的设计。USER 是你的"硬性指令"------不管底层怎么变,你的要求不能被覆盖,所以放最前面,类似宪法。SUFFIX 是模型的"行为微调"------距离对话最近,模型最关注,相当于执行层面的战术指导。中间的 BASE 是"基本法"------兜底用的,你不出 system_prompt 的时候它就是全部规则。三层各有分工,互不干扰。
第六阶段:最终交付(调用 create_agent)
所有零件都装好了,最后一步------拧上最后一颗螺丝:
python
return create_agent(
model,
system_prompt=final_system_prompt,
tools=_tools,
middleware=deepagent_middleware,
response_format=response_format,
context_schema=context_schema,
checkpointer=checkpointer,
store=store,
debug=debug,
name=name,
cache=cache,
).with_config({
"recursion_limit": 9_999, # 🔥 递归上限设为 9999!
"metadata": {
"ls_integration": "deepagents",
"versions": {"deepagents": __version__},
"lc_agent_name": name,
},
})
注意那个 recursion_limit: 9_999 ------普通的 create_react_agent 默认递归限制通常是 25 或 50,而 Deep Agents 直接拉到了 9999。
继续装电脑的比喻:普通 Agent 就像一台设了 30 分钟自动休眠的电脑------你正干活呢,它突然"累了"不动了。Deep Agents 直接把休眠时间设到 9999 分钟------基本上等于"你别休眠了,给我一直跑,跑到任务完成或者没电为止"。
但别担心这会导致无限循环------SummarizationMiddleware 会在上下文接近溢出时自动压缩对话历史,确保 Agent 不会因为 token 用完而崩溃。就像电脑的虚拟内存------内存快满了自动把冷数据换到硬盘,确保系统不会 OOM。所以 9999 不是让你真的递归 9999 次,而是"别因为递归限制打断我,我自己有别的机制控制"。
💡 如果面试官问你:"递归上限设 9999 会不会导致死循环烧 token?"
不会。9999 是天花板,不是目标。Deep Agents 有
SummarizationMiddleware做上下文压缩,有PatchToolCallsMiddleware做断路保护。真正控制递归的是"任务完成"和"上下文预算",不是这个数字。设 9999 的意思是------"我不希望因为技术限制被迫中断,只在任务真正完成时停下"。
全流程总结:一张图看懂 create_deep_agent
scss
用户调用 create_deep_agent(model, tools, ...)
│
▼
┌─────────────┐
│ 1. 解析模型 │ resolve_model() → BaseChatModel 实例
└──────┬──────┘
▼
┌─────────────┐
│ 2. 匹配配置 │ _harness_profile_for_model() → HarnessProfile
└──────┬──────┘
▼
┌──────────────────────────────────────────┐
│ 3. 处理子智能体 │
│ ├── 解析用户传入的 subagents │
│ ├── 为每个子智能体组装中间件 │
│ └── 自动创建 general-purpose 子智能体 │
└──────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 4. 组装主中间件栈 │
│ 基础栈 → 用户中间件 → 尾部栈 │
└──────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 5. 安全校验 │
│ ├── 验证排除规则不会移除脚手架中间件 │
│ ├── 应用排除规则过滤中间件 │
│ └── 验证所有排除规则都有对应中间件 │
└──────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 6. 拼装 System Prompt │
│ USER → BASE/CUSTOM → SUFFIX │
└──────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────┐
│ 7. 交付成品 │
│ create_agent(...).with_config({ │
│ recursion_limit: 9_999, │
│ metadata: {ls_integration: "deepagents"}│
│ }) │
└──────────────────────────────────────────┘
本篇小结:三个值得记住的设计
1. Deep Agents 不只是一个 Agent,是一个 Agent 工厂
它帮你自动创建子智能体(general-purpose),就像开公司时系统自动给你配了一个万能助理。你不需要自己写子智能体的创建逻辑,系统帮你搞定了中间件、权限、工具继承。这种"开箱即用的团队"设计,是 Deep Agents 和普通 Agent 最大的区别。你一个人能干多少活?有了团队,产能直接翻倍。
2. Harness Profile 是"隐形的 BIOS 微码"
你看不到它,但它一直在工作。不同的模型有不同的"出厂调优"------Claude Sonnet 被告诉要"并行调工具",OpenAI Codex 被告诉要"像个自主的高级工程师"。这个机制让你切换模型时不需要改一行提示词代码,BIOS 自动适配。你插 i9 它就刷 i9 的微码,你插 R9 它就刷 R9 的微码,丝滑切换。
3. 脚手架中间件 = 不可拔的 CPU 供电线
很多框架把"附加功能"做成可选项,但 Deep Agents 把 FilesystemMiddleware 和 SubAgentMiddleware 设计为不可移除的脚手架 。这不是限制自由,而是防止你把"CPU 供电线"拔了还奇怪为什么电脑开不了机。宁可构建时报错,也不运行时静默失效。这个设计哲学值得学习------好的约束不是枷锁,是护栏。
下一篇预告
手撕 LangChain Deep Agents 源码 (二):System Prompt 的组装------四层叠加背后的"潜规则"
我们刚刚看到了 System Prompt 的四层结构(USER → BASE → CUSTOM → SUFFIX),但只是走马观花。下一篇我们深入拆解:
BASE_AGENT_PROMPT的完整内容------Deep Agents 默认给 Agent 塞了多少"出厂指令"?HarnessProfile如何通过base_system_prompt完全替换 BASE?system_prompt_suffix里的 XML 标签到底在告诉模型什么?_apply_profile_prompt的合并逻辑------当 CUSTOM 和 SUFFIX 同时存在时,谁覆盖谁?- Anthropic 的
cache_control标记如何影响 Prompt 缓存策略?
看完下一篇,你就知道为什么 Deep Agents 的 Agent 比"手写 prompt"的 Agent 聪明那么多------因为它的 System Prompt 根本不是一个人写的,而是一整条流水线组装出来的。
📖 源码阅读建议 :如果你想跟着看源码,建议从
graph.py的create_deep_agent函数开始,然后跳到_models.py看resolve_model,再看profiles/harness/harness_profiles.py理解 Profile 机制。中间件的细节留给后续篇章。💬 有什么问题? 欢迎在评论区讨论。如果你发现我哪里理解有误,或者有什么想让我深入讲的部分,直接说------源码面前,没有废话。