Deep Agents 中间件栈深度解析:create_deep_agent 的装配流程与定制边界
【免费下载链接】deepagentsThe batteries-included agent harness.项目地址: https://gitcode.com/GitHub_Trending/de/deepagents
create_deep_agent()是 Deep Agents(libs/deepagents)面向开发者的核心装配入口:它负责解析模型与HarnessProfile、按固定顺序组装有序的AgentMiddleware栈,再把最终主栈交给 LangChain 的create_agent()运行。本文以openwiki/architecture/middleware-stack.md为骨架,结合 graph.py、_excluded_middleware.py、middleware 目录 与相关单元测试,逐层拆解主代理与子代理的中间件装配顺序、profile 排除与调用方替换规则、工具可见性与授权边界,以及声明式/编译式/异步三类子代理各自的构建与继承边界,帮助你掌握如何在不破坏内置脚手架的前提下安全地定制代理行为。
一、定位:create_deep_agent 是装配器,不是独立运行时
Deep Agents 不是一个全新的 Agent 运行时。它位于 LangChain 与 LangGraph 之上,是一个"开箱即用"的 opinionated harness(见 ARCHITECTURE.md 三层架构)。create_deep_agent()的职责可以概括为六步装配(对应 graph.py 中 create_deep_agent 的实现):
- 解析请求的模型(支持
provider:model字符串,或直接传入BaseChatModel实例),并解析适用的 provider / harness profile; - 解析 backend(文件、内存、技能与
execute行为落盘位置); - 组装主代理的中间件栈;
- 构建默认的 general-purpose 子代理与调用方传入的子代理;
- 拼接系统提示词(调用方指令 + 基础 prompt + profile 的 suffix);
- 调用 LangChain
create_agent(...)产出可运行的 agent 图。
最终透传给create_agent()的选项包括:system_prompt、tools、middleware、response_format、context_schema、checkpointer、store、debug、name、cache以及state_schema——模型/工具循环本身由 LangChain 拥有。组装完成后,create_deep_agent还会为返回的图设置recursion_limit: 9_999并附加 Deep Agents 相关的 tracing metadata。运行时边界(模型如何被调用、状态如何流转)详见 SDK 构造与执行。
DeepAgentState是图状态的基础:它继承 LangChain 的AgentState,并把messages字段换成DeltaChannel+_messages_delta_reducer,使 checkpoint 增长从 O(N²) 降为 O(N)(见 graph.py DeepAgentState 定义)。
二、中间件 vs 普通工具:请求时扩展边界的本质区别
中间件是请求时(request-time)的扩展边界。一个实现了wrap_model_call()的中间件会在每次 LLM 请求发出之前被调用,因此它可以改变即将发给模型的有效系统提示词、历史消息、工具列表,甚至写入跨轮的类型化状态(通过state_schema贡献状态字段)。LangChain 允许中间件在模型调用前、模型调用周围、工具执行周围以及状态准备阶段介入(ARCHITECTURE.md 中的说明)。
与之相对,tools=里传的普通可调用对象,只有模型主动选中它之后才会执行。它没有机会为任何一次模型请求做准备工作——无法改写工具清单,也无法注入提示词。因此判断标准很清晰:
- 需要每次请求生效的行为(提示词/工具注入、请求改写、跨轮状态、权限检查、压缩)→ 用中间件;
- 只需要一个自包含操作(模型决定调用时执行一次)→ 用普通工具。
具体到 Deep Agents,中间件栈承担了:向最终系统提示词注入 filesystem / memory / skills / subagent / human-in-the-loop 指令、在请求发出前增删工具、随上下文增长压缩与卸载历史、把类型化值写入图状态、以及在(内置)文件工具执行前实施文件系统权限。完整的中间件清单见 middleware catalog。
三、主代理装配流程与栈顺序
主代理中间件栈的成员资格是条件性的:skills、子代理形态、memory、文件系统权限、interrupt 配置、profile extras、已安装的 provider 集成以及 profile 排除项都会影响最终结果。下图为经过源码验证的装配流程与仅有的几个调用方定制决策点(可选条目在条件不满足时缺席):
核心带(core band)顺序
核心带按以下顺序装配(对应 graph.py 主栈构建段):
SkillsMiddleware——仅当传入skills时;FilesystemMiddleware——提供内置文件工具并实施permissions;SubAgentMiddleware——仅当存在同步内联子代理时(通常是因为自动添加了 general-purpose 子代理);- Deep Agents 的 summarization 中间件(实现类为
_DeepAgentsSummarizationMiddleware,公开名为SummarizationMiddleware); PatchToolCallsMiddleware;AsyncSubAgentMiddleware——仅当存在远端 async specs 时。
尾部带(tail band)顺序
核心带之后追加(graph.py 尾部段):
HarnessProfile.extra_middleware的实例化结果;- provider 提示词缓存中间件:
AnthropicPromptCachingMiddleware总是安装(对不支持的模型 no-op),Bedrock 与 Fireworks 变体仅在对应集成包(langchain-aws/langchain-fireworks)可导入时才追加,同样对不支持的模型忽略(见 _prompt_caching.py); MemoryMiddleware——仅当传入memory时;它带有cache_control断点,但只在请求模型为 Anthropic 时生效,因此可以无条件安全启用;HumanInTheLoopMiddleware——仅当解析后的 interrupt 映射非空时。
缓存中间件刻意放在 memory 之前:profile extras 先于缓存运行,而 memory 对系统提示词的修改发生在 Anthropic 缓存前缀之后,避免 memory 更新使缓存前缀失效(graph.py 注释)。
第一遍 profile 排除在尾部组装完成后运行;随后调用方middleware=被合并,第二遍排除再次运行;最后仅当 profile 声明了excluded_tools时才追加_ToolExclusionMiddleware。它必须位于栈尾:这样它看到的是接近最终的请求——工具生产中间件与调用方模型包装器都已运行完毕,被排除的工具名不可能被某个调用方包装器重新注入。
装配器还会把显式的state_schema与中间件贡献的 schema 合并,通过private_state_field_names()推导私有状态字段名(PrivateStateAttr注解,见 _state.py),并把这些字段赋值给SubAgentMiddleware——这决定了普通同步委派在穿越状态边界时能携带什么。
四、上下文管理:summarization 中间件的实际职责
默认的 summarization 组件远不止是"加一段提示词"。它在 summarization.py 中实现,工作分三步(见wrap_model_call,L1345-L1484):
- 截断旧的大工具参数:当 token 达到较低阈值时,仅对 keep 窗口之前的消息中
AIMessage.tool_calls的args进行缩短——典型对象是write_file的内容、edit_file的补丁、冗长的execute输出。实现上只针对write_file/edit_file两个工具名(L1033),截断后保留前 20 个字符并追加后缀; - 按阈值压缩历史:
TriggerClause支持tokens、messages、fraction三种触发条件(可 AND 组合),keep策略决定保留多少最近上下文; - ContextOverflowError 兜底重试:若阈值未触发但模型调用抛出了
ContextOverflowError,立即走压缩路径,用summary_message + 保留的近期消息重试模型调用,并通过_clip_overflow_tail把过大的保留尾部 batch 卸载到 per-TM 文件。
被逐出的历史会卸载(offload)到配置的 backend,存为/conversation_history/{session_id}.md(按 session id 追加 section);同时把_summarization_event(含cutoff_index、summary_message、file_path)作为私有状态写入SummarizationState,_summarization_session_id按图调用隔离历史文件。内联的data:媒体块(base64 或 percent-encoded,如内联 SVG)会被解码、按内容 hash 去重后上传到{artifacts_root}/conversation_history/media/{sha256[:16]}.{ext},消息改写为可寻址的<image url="..." />引用;上传或解码失败的块会被替换为<image error="failed_to_offload" />占位符并计入警告——卸载失败时日志会明确警告:更早的消息不可恢复。压缩提示词DEEPAGENTS_DEFAULT_SUMMARY_PROMPT专门附加了媒体引用标签的说明,确保总结模型保留这些路径引用。
默认阈值是模型感知的:compute_summarization_defaults()在模型 profile 提供max_input_tokens时使用("fraction", 0.85)触发 /("fraction", 0.10)保留;无 profile 时退化为更保守的("tokens", 170000)触发 /("messages", 6)保留。上下文管理的完整说明见 context management。
五、调用方中间件合并与 profile 排除规则
按 name 合并而非盲目追加
调用方中间件通过_apply_custom_middleware()(graph.py)按.name合并:
- 调用方条目的名字若仍存在于基础栈中 →原位替换,保留其原有位置;
- 新名字 → 插入到最后一个幸存的 core 成员之后,即位于 profile extras、提示词缓存、memory 与 approval 中间件之前;
- 第一遍排除发生在合并之前;第二遍排除用于移除"试图重新引入被排除名字或精确类"的条目。
这给了调用方一种受控的"替换内置行为"手段。例如:调用方可以传入同名的FilesystemMiddleware但使用更窄的tools=[...],从而彻底移除某个文件系统工具——仅靠tools=是做不到的(tools=只做加法)。同时,使用新名字的自定义中间件不会仅仅因为装在了主代理上就被复制进 general-purpose 子代理。
excluded_middleware 的校验与约束
HarnessProfile可以通过excluded_middleware从栈中减去条目,但受安全性与覆盖率检查约束(全部实现见 _excluded_middleware.py):
- 受保护脚手架:
FilesystemMiddleware与SubAgentMiddleware是被保护的脚手架(_REQUIRED_MIDDLEWARE,graph.py)。按类或按名字排除它们都会抛ValueError,而不是静默破坏内置的文件/权限行为或同步task处理器; - 类条目用精确
type而非isinstance:排除基类不会移除调用方的子类实例;字符串条目精确匹配AgentMiddleware.name——因此"SummarizationMiddleware"这个公共别名可以精准命中__name__不同的实现类_DeepAgentsSummarizationMiddleware(见 summarization.py 的 name 属性); - 名字歧义:一个字符串排除若在一个栈里匹配到多个不同的类,属于歧义配置,抛
ValueError,提示改用类形式排除; - 覆盖率校验:每个被允许的条目必须在某处命中。装配完成后若有条目未命中任何中间件,抛
ValueError——这能捕获拼写错误和过期 profile。
对主 profile 而言,匹配集合跨主代理栈 + 自动添加的 general-purpose 栈累积,两套栈都过滤完成后再做一次覆盖率检查——因此一条排除合法地只命中其中一个栈也是允许的。声明式子代理若解析到另一个 profile,则自己完成独立的校验、过滤与覆盖率检查。
工具可见性不是授权
excluded_tools会追加最终的_ToolExclusionMiddleware(_tool_exclusion.py):它在wrap_model_call里从模型请求中移除指定名字的工具,同时在wrap_tool_call边界上拒绝执行被排除的名字(返回"Error: {name} is not available."的ToolMessage,status 为"error")。这保持了"对外宣传的工具"与"实际可执行的工具"一致——但文档与代码注释都明确:这不是安全边界。
文件系统权限则由FilesystemMiddleware在其内置工具被调用时强制实施,而不是由 backend 强制。因此直接使用 backend 会绕过中间件层的权限规则(见 graph.py 中 permissions 参数的说明)。权限规则与审批行为的完整讨论见 permissions and human-in-the-loop。
六、三条独立的子代理路径
子代理形态在装配期即已确定(graph.py 子代理分拣段):
- spec 含
graph_id→AsyncSubAgent,交给AsyncSubAgentMiddleware; - spec 含
runnable→CompiledSubAgent; - 否则 → 声明式
SubAgent。
三种形态有着不同的构造与继承边界。
声明式子代理(declarative)
每个声明式 spec解析自己的模型与 harness profile,并构建独立栈:FilesystemMiddleware+ summarization +PatchToolCallsMiddleware;isolated-spec 的 skills 或 fork 继承父级 skills;profile extras;提示词缓存;围绕 spec 中间件的两遍排除;覆盖率校验;最终工具过滤。fork 在配置了 memory 时还会镜像顶层 memory。
继承规则很明确(SubAgent TypedDict 定义):spec 只有在省略某个字段时才继承顶层 tools、permissions 和interrupt_on;spec 自带的 permissions整体替换而非扩展父级规则。这些值解析完成后,若 interrupt 映射非空,则在编译声明式图时追加HumanInTheLoopMiddleware。父级的state_schema会传给这次编译;而调用方提供的已编译 runnable 与远端图则各自拥有自己的 schema 与审批配置。
上下文模式:
- 默认
isolated:子代理收到一个包含委派任务的HumanMessage,而不是父级对话; handoff作为isolated的遗留别名被接受(见_validate_subagent_mode,subagents.py);- 实验性
fork:子代理收到父级有效压缩历史加上任务 preamble(_FORK_TASK_PREAMBLE,防止子代理把父级委托消息误当成新请求),并用可选的子代理 addendum 重建父级提示词。声明式 fork不能定义独立 skills(会抛ValueError);它保留合格的私有状态通道,而 fork 的 compiled runnable 因 schema 不透明会被剥离私有键;fork 子代理调用task会被拒绝(返回_FORK_RECURSION_REFUSAL),防止递归委派——_ForkTaskToolMiddleware通过_deepagents_forked_context私有状态标记在调用期拦截而非删掉工具(见 subagents.py)。详见 subagents and skills。
general-purpose、compiled 与 async 代理
- general-purpose 自动添加:除非当前 profile 禁用它(
general_purpose_subagent=GeneralPurposeSubagentProfile(enabled=False)),或者某个内联同步 spec 已经占用了general-purpose这个名字,harness 会自动添加它(graph.py)。它的栈包含 filesystem、summarization、patching、可选 skills、profile extras、缓存、排除 pass 与最终工具过滤。它只继承覆盖其原有默认槽位的调用方中间件——而不是任意的主代理专属中间件(_gp_inheritable仅保留 name 命中原 GP 栈槽位的条目); - CompiledSubAgent 原样使用:不继承父级 state schema 或顶层审批规则,且必须返回含
messages键的状态(_return_command_with_state_update会校验)。完成时父级读取返回状态:若structured_response非None,JSON 序列化后作为ToolMessage内容返回;否则取最后一条非空AIMessage文本(Anthropic 偶尔会在成功的最后工具调用后输出空end_turn消息,因此回退逻辑会跳过空文本)。合格的非私有状态更新会被合并回父级; - AsyncSubAgent 后台任务:通过 Agent Protocol(LangGraph SDK)作为远端后台任务运行,兼容 LangGraph Platform / LangSmith Deployment 与自托管服务器。
AsyncSubAgentMiddleware(async_subagents.py)暴露五个工具——start_async_task、check_async_task、update_async_task、cancel_async_task、list_async_tasks——并在中间件状态(async_tasks字典,带_tasks_reducer)中返回与追踪 task ID,不阻塞父级task调用;图 schema 与审批行为属于远端图。认证由 SDK 通过LANGGRAPH_API_KEY/LANGSMITH_API_KEY/LANGCHAIN_API_KEY自动处理,自托管可经headers传入自定义鉴权;省略url时使用进程内 ASGI 传输(仅限ainvoke异步入口)。
七、安全变更与聚焦测试
顺序变更会改变模型看到的内容和可执行的工具。请测试装配后的完整栈,而不仅是中间件构造函数本身(test_subagent_middleware_init.py 就是一个围绕SubAgentMiddleware初始化的样例:验证默认system_prompt为空、工具仅task一个、构造注解可运行时解析)。
原文档给出的测试聚焦点:
- 替换 vs 插入:同名中间件原位替换、新名中间件插入 core 之后;
- 两遍排除:合并前第一遍、合并后第二遍;
- 最终请求/工具调用过滤:
_ToolExclusionMiddleware的请求侧过滤与工具调用侧拒绝; - 受保护脚手架与歧义名失败:排除
FilesystemMiddleware/SubAgentMiddleware抛ValueError、字符串排除命中多类抛ValueError; - 跨主代理与 general-purpose 的覆盖率:profile 级排除允许只命中其中一个栈;
- 声明式 / compiled / async / isolated / fork 各自路径:尤其私有状态处理、fork 的 prompt/history 构造、递归委派拒绝、结构化响应回退。
仓库的单元测试套件覆盖了中间件提供的文件系统与task工具;图级测试则覆盖 profile 排除行为与子代理栈装配。改造中间件栈时,以这些测试矩阵为回归基准,可以显著降低"模型侧行为漂移"与"工具静默消失"的风险。
八、定制决策速查
| 目标 | 手段 | 边界 |
|---|---|---|
| 每次模型请求前改写 prompt / 工具 / 历史 | 自定义中间件wrap_model_call | 放在middleware=中,按 name 与内置槽位合并 |
| 替换某个内置行为(如收窄文件工具) | 同名中间件替换 | 保留原位置;tools=无法移除内置工具 |
| 彻底拿掉内置工具 | 同名FilesystemMiddleware+ 更窄tools | 不破坏脚手架(不能排除类本身) |
| 按 profile 排除中间件 | HarnessProfile.excluded_middleware | 类条目精确 type、字符串精确 name;受保护脚手架/歧义/未命中均抛ValueError |
| 不让模型看到某些工具 | HarnessProfile.excluded_tools | 只是可见性过滤,非安全边界 |
| 文件访问控制 | permissions规则 | 仅作用于FilesystemMiddleware内置工具;直接使用 backend 会绕过 |
| 长对话压缩 | 内置 summarization(阈值可配) | 大参数截断 → 阈值压缩 → 溢出重试;历史 offload 到 backend |
| 委派子代理 | SubAgent(isolated/fork)、CompiledSubAgent、AsyncSubAgent | 各自拥有独立栈、schema 与审批边界 |
这套"核心脚手架 + 调用方插入 + profile 排除 + 尾部行为"的装配模型,是 Deep Agents 既能开箱即用、又能精确定制的根本原因:脚手架保证能力下限,排除与替换规则保证定制不越界,三态子代理保证隔离边界清晰。如需继续深入,可依次阅读 middleware catalog、context management、subagents and skills 与 permissions and human-in-the-loop。
【免费下载链接】deepagentsThe batteries-included agent harness.项目地址: https://gitcode.com/GitHub_Trending/de/deepagents
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考