[sdk] 02 - Deep Agents Middleware
了解 Deep Agents的中间件的思想与实现。
- LangChain DeepAgents 速通指南(一)—— 一文详解DeepAgents核心特性
- LangChain DeepAgents 速通指南(二)—— Summarization中间件为Agent作记忆加减法
- LangChain DeepAgents 速通指南(三)—— 让Agent告别混乱:Tool Selector与Todo List中间件解析
从输入参数理解 Deep Agent
Deep Agents 是建立在
create_agent()之上的 opinionated harness,它不引入新的 runtime。
create_deep_agent() 做的事情,本质上就是:先替你把一套成熟 Harness 组装好,再交给 create_agent()。
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, state_schema=state_schema if state_schema is not None else DeepAgentState, ).with_config( { "recursion_limit": 9_999, "metadata": { "ls_integration": "deepagents", "lc_versions": {"deepagents": _lc_version()}, "lc_agent_name": name, }, } )
| Middleware | 作用 | 类比 |
|---|---|---|
SkillsMiddleware |
按需加载专业 Skill | 员工的专业手册 |
FilesystemMiddleware |
给 Agent 文件读写能力 | 办公桌 / 文件柜 |
SubAgentMiddleware |
增加 task(),可以委派 Subagent |
找专业同事干活 |
SummarizationMiddleware |
Context 太长时压缩历史 | 整理会议纪要 |
PatchToolCallsMiddleware |
修补中断/异常造成的不完整 tool call | 修复工作记录 |
AsyncSubAgentMiddleware |
后台异步跑 Subagent | 把任务交出去后台做 |
| 自定义 Middleware | 你自己的规则 | 公司内部制度 |
| Provider-specific | 针对 Claude/GPT 等做适配优化 | 针对不同员工调整工作方式 |
| Tool filtering | 隐藏某些不该看到的 Tool | 权限菜单 |
| Prompt caching | 缓存稳定 Prompt,省 token/延迟 | 常用资料不用反复加载 |
MemoryMiddleware |
加载长期记忆 | 员工长期档案 |
| HITL | 敏感操作暂停等人批准 | 经理审批 |
挂载中间件的三层设计
近期的代码,把中间件分为了三层:
- Base:定义“Deep Agent 本身怎么工作”
- User:给你插公司/业务自己的规则
- Tail:最后做 provider 适配、工具过滤、cache、memory、HITL 等外围控制
Claude Code Hook 和 LangChain/Deep Agents Middleware,底层思想高度相似:都是在 Agent 生命周期的关键节点插入确定性逻辑。
-
- Claude Code 的 Hook 有
PreToolUse、PostToolUse等事件点; - LangChain Middleware 则有
before_agent、before_model、after_model、wrap_tool_call等。
- Claude Code 的 Hook 有
两边本质上都在做“Agent 核心逻辑不改,但在关键时间点插入控制”。
用户需求
“分析我们收购 ABC 公司的风险,结合互联网新闻和公司内部投委会资料。”
得到如下类似:
Main Deep Agent Subagents: - news-agent - internal-research-agent Tools: - web_search() - company_rag() - customer_database()
但不是什么人都可以随便查数据,权限管理在这里:
class BankPermissionMiddleware: # 每一次准备调用模型之前自动执行 def before_model(...): user = current_user() if user.department != "Investment": # 伪代码:把敏感能力从模型可见范围删除 hide_tool("investment_committee_rag") log("What tools this user is allowed to see")
实现样貌
agent = create_deep_agent( # ① 主 Agent 的大脑 model=main_model, # ② Main Agent 自己直接能用的 tools tools=[ web_search, company_rag, customer_database, ], # ③ Main Agent 可以委派的专业 Subagents subagents=[ news_sub_agent, internal_research_sub_agent, ], # ④ 你自己加的 Middleware middleware=[ BankPermissionMiddleware(),
AuditMiddleware(), ], # ⑤ Main Agent 的工作说明 system_prompt=""" You are the main corporate research agent. Analyse acquisition risks and coordinate specialist researchers. """, )
然后,挂载的顺序就成了如下所示:
Deep Agents 自动加入
────────────────────
SkillsMiddleware
FilesystemMiddleware
SubAgentMiddleware
SummarizationMiddleware
PatchToolCallsMiddleware
...
↓
你传进来的
────────────────────
BankPermissionMiddleware
AuditMiddleware
↓
Deep Agents 自动加入
────────────────────
PromptCachingMiddleware
MemoryMiddleware
HITL Middleware
...
| 中间件类型 | 主要在哪里实现 | 例子 |
|---|---|---|
| 通用 Agent Middleware | langchain |
Link: https://github.com/langchain-ai/langchain/tree/master/libs/langchain_v1/langchain/agents/middleware |
| Deep Agents 专用 Middleware | deepagents |
FilesystemMiddleware、SubAgentMiddleware、SkillsMiddleware、MemoryMiddleware、PatchToolCallsMiddleware |
| 模型/Provider 专用 Middleware | 各 integration package | AnthropicPromptCachingMiddleware、BedrockPromptCachingMiddleware、Fireworks caching 等 |
基本明白了,之后就是对一些典型的中间件以及如何自定义中间件,什么时候需要自定义才进行细节上的学习。
SummarizationMiddleware
若自定义中间件参数
custom_summarization = SummarizationMiddleware( # -------------------------------------------------------- # 谁负责做 summary? # -------------------------------------------------------- model=SUMMARY_MODEL, # -------------------------------------------------------- # 被 Context 移出去的原始历史存在哪里? # -------------------------------------------------------- backend=backend, # -------------------------------------------------------- # 什么时候开始 Summary? # # ("fraction", 0.70) # # = 大约达到模型 Context Window 的 70% 时触发。 # # Deep Agents 默认在模型 profile 可用时大约是 85%。 # # 我这里故意提前到 70%: # # 原因: # Deep Research / Agent 工作中还可能突然出现: # - 很大的 Tool Result # - PDF # - Search Result # # 留 30% buffer 会比较从容。 # -------------------------------------------------------- trigger=("fraction", 0.70), # -------------------------------------------------------- # Summary 后最近多少消息保持原样? # # 这里保留最近 20 条。 # # 原因: # 最近几轮通常代表 Agent 当前正在做的事情, # 如果也压缩了,很容易丢掉“现场状态”。 # -------------------------------------------------------- keep=("messages", 20), # -------------------------------------------------------- # 最重要的自定义: # # 决定“怎么总结”。 # # 这里使用我们上面: # # 官方 Prompt # + # Jeffrey-style summary rules # # 合成后的 Prompt。 # -------------------------------------------------------- summary_prompt=CUSTOM_SUMMARY_PROMPT, # -------------------------------------------------------- # 给 Summary Model 的旧历史最多准备多少 token。 # # 默认值目前较保守。 # # 我的总结比较强调: # - 因果关系 # - 参数 # - 未决问题 # # 因此这里允许它看到更多待总结内容。 # # 代价: # summary call 会更贵一些。 # -------------------------------------------------------- trim_tokens_to_summarize=12_000, # -------------------------------------------------------- # 一个很实用的高级优化: # # Agent 的 Tool Call 参数有时候特别巨大。 # # 比如: # # write_file(content="一大篇文件...") # execute(command="...") # edit_file(patch="非常大的 patch...") # # 这些旧参数长期留在 Context 里非常浪费。 # # 所以: # # Context 到 50% 时, # 可以先对“老的巨大 tool arguments”做轻量截断, # 还没有必要立即做完整 Summary。 # -------------------------------------------------------- truncate_args_settings={ # 50% Context 就开始清理旧的大 Tool 参数 "trigger": ("fraction", 0.50), # 最近 20 条消息不要碰 "keep": ("messages", 20), # 老 Tool argument 最多留 2000 个字符 "max_length": 2000, # 被截断的位置明确做标记 "truncation_text": "...(旧 Tool 参数已截断)", }, )
发生了什么
到 SummarizationMiddleware
它的核心 hook 是:wrap_model_call(...)
模型马上要被调用
↓
SummarizationMiddleware 拦一下
↓
检查 Context
↓
再决定怎么调用 Model
invoke │ ├─ SummarizationMiddleware # 在 main model真正收到 1-101 messages 之前,第一次拦截,M1~M81的内容被归档,并生成 总结的版本 .md文件。 │ ↓ │ Model Call #1 │ ↓ │ task() │ ↓ │ Subagent # research agent ... 这中间可能有试几次的tool call。这之后 --> Main State 1 │ ↓ │ ├─ SummarizationMiddleware │ ↓ │ Model Call #2 │ ↓ │ search() # 有可能吃饱了撑的,"Subagent给了源码结论,但我还需要搜索一份官方文档确认。" --> Main State 2 │ ├─ SummarizationMiddleware │ ↓ │ Model Call #3 # --> Main State 3 │ └─ Final Answer
Main State 1
M1 ... M101 M102 = task(...) # Main Agent 发出的 Tool Call M103 = ToolMessage(...) # Subagent 最终研究结果
Main State 2
M1
...
M101
M102 = task(...)
M103 = ToolMessage(...)
M104 = AIMessage(tool_call=web_search)
M105 = ToolMessage(search result)
Main State 3
M106 = AIMessage(final answer)
总结不代表数据彻底丢失
未来 Agent 如果突然发现:
“等等,摘要说之前讨论过某个具体参数,但没保留数字。”
它还有机会:
read_file( "/conversation_history/session_xxx.md" )
重新找原文。Deep Agents 官方把这个设计称为 in-context summary + filesystem preservation。
Deep Agents 的设计不是“总结完自动把旧细节重新塞回来”,而是“总结里保留一个可追溯入口,模型需要时再主动去 filesystem 里检索原文”。
Deep Agents 自带的 virtual filesystem 就有 grep、read_file 等工具,而且官方专门说明 offloaded/summarized 内容可以被 Agent 重新搜索和读取。
Summary
→ LLM 在当前推理时发现信息不足
→ LLM 决定调用 filesystem tool
→ 有针对性地找回一小段原文
更准确地说,它是:Summary + 原始历史的可追溯入口
现在回头看会特别明显:
2023 年大家表面上在追 GPT-4,另一批人已经在提前做今天所谓的 Agent Architecture / Memory / Context Engineering / Harness 了
2023 early Reflexion “把经验压缩成可复用的文字记忆” ↓ 2023 Apr Generative Agents “完整 memory stream + 高层 reflection + 按需 retrieval” ↓ 2023 Oct MemGPT “Working Context ≈ RAM External Memory ≈ Disk” ↓ 2024~2025 Agent 越来越长时间运行 Context Engineering 成为显式问题 ↓ 现在的 Deep Agents Summary + Filesystem Preservation + 需要时 grep/read_file 找回原文
Tool Selector 与 Todo List 中间件
Tool Selector正是为了解决这一痛点而生。它是一个覆写了 wrap_model_call钩子函数的中间件。其核心机制是:在每次调用主模型之前,ToolSelector中间件会基于当前的对话消息列表及用户问题,对全部工具列表进行一次智能预筛选,只保留与当前任务最相关的一小部分工具。
工程上经常需要在每一次 Model Call 前后做点事情:
准备调用模型
↓
修改 prompt
修改 tools
换模型
重试
缓存
记录 token
检查权限
↓
真正调用模型
↓
检查/修改返回结果
怎么实现的呢?
教学伪代码。
class LLMToolSelectorMiddleware(AgentMiddleware): def wrap_model_call(self, request, handler): # 1. 先拿到当前所有 tools all_tools = request.tools # 2. 用一个 selector LLM 挑出相关 tools selected_tools = select_relevant_tools( user_query=request.messages, tools=all_tools, ) # 3. 新的request只有必要的tool。 new_request = request.override( tools=selected_tools ) # 4. 再真正调用主模型 return handler(new_request)
当然,调用 “前与后” 都可以用。这是这里只用到了“调用前”。
def wrap_model_call(self, request, handler): print("模型调用前") response = handler(request) print("模型调用后") return response
如何调用?
agent = create_agent( model=model, tools=[tool_1, tool_2, tool_3, tool_4, calculate], middleware=[ LLMToolSelectorMiddleware( model=model, max_tools=2, always_include=["tool_1"], # 不计入 max_tools ), ], )
Todo List 中间件 又是什么鬼?
Planning意图与write_todo工具强绑定
以额外工具的形式,向Agent注入一个名为 write_todo 的工具。这个工具只是专注 “写” 这个行为 in a Proper Format。
用户问题
↓
Main LLM
│
├─ 这是不是复杂任务?
├─ 如果是,应该怎么拆?
└─ 应该先做什么?
↓
产生一个 Tool Call
↓
write_todos(...)
触发过程:
response = model.invoke( messages=[ system_prompt, user_message ], tools=[ write_todos, get_financials, search_web, ... ] )
response的内容:
ToolCall( name="write_todos", args={ "todos": [ { "content": "分析财务情况", "status": "in_progress" }, { "content": "分析核心业务和增长动力", "status": "pending" }, { "content": "分析竞争格局", "status": "pending" }, { "content": "评估估值和主要风险", "status": "pending" }, { "content": "形成综合投资判断", "status": "pending" } ] } )
- 七月底发生的事情,v0.7.0
create_deep_agentno longer includesTodoListMiddlewareby default, thewrite_todostool,todosstate channel, and todo-planning prompt are now absent.- 简单的说 就是 过去是系统默认,现在需要用户手动添加一下。
from langchain.agents.middleware import TodoListMiddleware MY_PLANNING_PROMPT = """ You have access to write_todos. Use it only for genuinely multi-step tasks. When creating a plan: 1. Each todo must describe one independently verifiable objective. 2. Do not create vague todos such as "research more". 3. Prefer 3-7 todos. 4. Put only one todo in_progress at a time. 5. Mark a todo completed only when supporting evidence has been obtained. 6. If you lack domain knowledge, create an information-gathering todo first instead of inventing an expert plan. 7. Revise the remaining plan whenever new evidence changes the problem. """ agent = create_deep_agent( model=model, middleware=[ TodoListMiddleware( system_prompt=MY_PLANNING_PROMPT # 自定义 Planning prompt. ) ], )
以上,便足以理解中间件的设计理念。

浙公网安备 33010602011771号