[sdk] 02 - Deep Agents Middleware

了解 Deep Agents的中间件的思想与实现。

  1. LangChain DeepAgents 速通指南(一)—— 一文详解DeepAgents核心特性
  2. LangChain DeepAgents 速通指南(二)—— Summarization中间件为Agent作记忆加减法
  3. 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 有 PreToolUsePostToolUse 等事件点;
    • LangChain Middleware 则有 before_agentbefore_modelafter_modelwrap_tool_call 等。

两边本质上都在做“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

TodoListMiddlewareSummarizationMiddlewareHumanInTheLoopMiddleware、model/tool call limit、fallback 等

Link: https://github.com/langchain-ai/langchain/tree/master/libs/langchain_v1/langchain/agents/middleware

Deep Agents 专用 Middleware deepagents FilesystemMiddlewareSubAgentMiddlewareSkillsMiddlewareMemoryMiddlewarePatchToolCallsMiddleware
模型/Provider 专用 Middleware 各 integration package AnthropicPromptCachingMiddlewareBedrockPromptCachingMiddleware、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 就有 grepread_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_agent no longer includes TodoListMiddleware by default, the write_todos tool, todos state 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.
        )
    ],
)

 

以上,便足以理解中间件的设计理念。

 

posted @ 2026-08-18 17:42  郝壹贰叁  阅读(14)  评论(0)    收藏  举报