《Deep Agents 实战笔记》第 3 篇 | 基于 deepagents (Python) 官方文档 原文:https://docs.langchain.com/oss/python/deepagents/overview 参考:https://docs.langchain.com/oss/python/deepagents/customization

上一篇我们跑通了第一个深度研究 Agent:40 行代码,Agent 自己拆题、搜索、写报告。当时我留了个悬念——真正的"深度"来自你没传的那些参数。你没配文件系统,它有;你没定义子代理,它有;你没写压缩逻辑,它还是有。

这一篇我们就把 create_deep_agent 这层皮撕开,看看 harness 默认塞给你的到底是哪几样东西。读懂这五张牌,才算真正入门 Deep Agents。

一、先看全局:中间件栈才是 harness 的骨架

先说一个容易被忽略的事实:Deep Agents 的五大能力,不是五个独立模块,而是用同一套机制组织起来的——中间件(middleware)栈。

当你只写 create_deep_agent(model="openai:gpt-5.5") 时,框架按固定顺序组装了一串中间件:

flowchart TB
    A["1. FilesystemMiddleware<br/>文件系统:ls / read_file / write_file / edit_file ..."]
    B["2. SubAgentMiddleware<br/>子代理:提供 task 工具"]
    C["3. SummarizationMiddleware<br/>总结压缩:对话变长时自动收敛历史"]
    D["4. PatchToolCallsMiddleware<br/>修复中断恢复后悬空的工具调用"]
    E["5. 提示词缓存中间件<br/>Anthropic / Bedrock 上自动生效"]
    F["6. Harness profile 附加项与排除工具过滤"]
    A --> B --> C --> D --> E --> F
    OPT["可选插槽<br/>按需出现:TodoList / Skills / Memory / HITL / 异步子代理"]
    F -.->|"传入对应参数时插入"| OPT
    style A fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
    style B fill:#f4effb,color:#4a2170,stroke:#8b52c9
    style C fill:#fbf3e6,color:#6b4a12,stroke:#c99a3d
    style D fill:#f7f7f7,color:#333333,stroke:#999999
    style E fill:#fdeef0,color:#701d2a,stroke:#c94a5e
    style F fill:#eef4ff,color:#1a3a8b,stroke:#4a72d9
    style OPT fill:#eef4ff,color:#1a3a8b,stroke:#4a72d9

正在渲染图表…

这个设计的妙处在于插槽化:技能、记忆、人机协同(HITL)、任务规划,全是"传了参数才出现"的可选中间件。而且每个中间件实例都有 .name,你传入的实例如果和内置条目同名,会原位替换而不是重复叠加(这个行为要求 deepagents>=0.7)——这意味着你可以用几十行代码换掉框架的核心策略,比如自定义总结的触发时机。

下面按重要性挨个拆。

二、牌一:task 工具与子代理派发

五大能力里最"贵"的一张牌。harness 内置一个 task 工具,主代理调用它就能为隔离的、长时间运行的、多步的任务创建一次性子代理。执行流程是这样:

sequenceDiagram
    participant M as 主代理
    participant SM as SubAgentMiddleware
    participant SA as 子代理实例
    participant FS as 文件系统

    M->>SM: 调用 task 工具<br/>(任务描述 + 子代理名)
    SM->>SA: 创建全新代理实例<br/>mode 默认 isolated 独立上下文
    SA->>FS: 自主多轮干活<br/>搜索 / 读写文件 / 推理
    Note over SA: 中间过程全部留在<br/>子代理自己的上下文里
    SA-->>SM: 单次交接:最终报告
    SM-->>M: 返回紧凑结果摘要

正在渲染图表…

注意三个容易错过的细节:

  1. 默认还有个"通用子代理"。就算你不传 subagents 参数,框架也会自动配一个 general-purpose 子代理,这就是上一篇的研究 Agent 能自己拆题的原因。想彻底关掉,需要禁用该子代理且不传同步 subagents。
  2. 单次交接,无状态。子代理干完活只回传一份最终报告,不能往回连发多条消息。中间几十轮工具调用的原始输出全部留在隔离上下文里,用完即弃——这是上下文可控的关键。
  3. 文件系统是共享的。默认 StateBackend 下,子代理写的文件保留在 LangGraph 代理状态中,子代理结束后主代理和其他子代理还能读到。这是天然的"协作黑板",第 5 篇细讲。

自定义一个专用子代理也只是一个字典的事:

research_subagent = {
    "name": "research-agent",
    "description": "Used to research more in depth questions",
    "system_prompt": "You are a great researcher",
    "tools": [internet_search],
    "model": "openai:gpt-5.5",  # 可选覆盖,默认为主代理的模型
}
subagents = [research_subagent]

agent = create_deep_agent(
    model="google_genai:gemini-3.6-flash",
    subagents=subagents,
)

description 字段是主代理决定"什么时候派给谁"的唯一依据——写砸了,委派质量就砸了。官方文档专门有一节排错指南对比好描述与坏描述,戏剧性很强,第 5 篇展开。

三、牌二:内置文件系统工具集

默认装配的 FilesystemMiddleware 给了 Agent 一整套文件工具:

工具描述
ls列出目录中的文件及其元数据(大小、修改时间)
read_file读取文件内容并带行号,大文件支持 offset/limit 分页;图片、视频、音频、文档等还能以多模态内容块返回
write_file创建新文件,或覆盖已有文件
edit_file精确字符串替换(支持全局替换模式)
delete删除文件或递归删除目录,要求 deepagents>=0.7
glob按模式查找文件,例如 **/*.py
grep按内容搜索文件,支持仅文件名、带上下文、计数等多种输出模式
execute运行 shell 命令,仅沙箱类 Backend 可用

两个设计上的小心思值得点出来:

一是能力跟着 Backend 走。 你配置的 Backend 不支持 delete 或 execute 时,对应工具会自动从模型面前隐藏——不是报错,是模型根本看不到。声明式的能力裁剪,比运行时报错优雅得多。

二是它是其他能力的地基。 技能是文件、记忆是文件、被卸载的大工具结果是文件、跨线程的对话历史也是文件。第 4 篇讲 Backend 时你会看到,/large_tool_results/ 和 /conversation_history/ 这些路径都是 harness 的内部约定。

四、牌三:todo 规划——从 v0.7 起变成可选启用

任务规划让 Agent 在执行期间维护一个结构化任务清单(pending / in_progress / completed 三种状态,持久化在代理状态中)。一个值得注意的版本变化:从 v0.7 开始,任务规划只能选择性启用,更早的版本是默认包含的。

from deepagents import create_deep_agent
from langchain.agents.middleware import TodoListMiddleware

agent = create_deep_agent(
    model="google_genai:gemini-3.6-flash",
    middleware=[TodoListMiddleware()],
)

传一个 TodoListMiddleware(),Agent 就多出一个 write_todos 工具。这个改动我举双手赞成:简单任务上强推规划纯属浪费 token,模型会为了规划而规划。官方建议在三类场景开启——冗长复杂的多步任务、能力较弱需要明确问责工具的模型、以及需要从状态流实时展示进度的前端 UI。

五、牌四:总结压缩——长任务的救命机制

SummarizationMiddleware 在对话变长时自动压缩消息历史,让 Agent 在 token 限制内跑完长时间任务。默认参数够用,但当你的 Agent 跑数小时的作业时,调一下触发阈值是常规操作——用上一节说的同名替换机制:

from deepagents import create_deep_agent
from deepagents.backends import StateBackend
from deepagents.middleware import SummarizationMiddleware

backend = StateBackend()
model = "anthropic:claude-sonnet-4-6"

agent = create_deep_agent(
    model=model,
    middleware=[
        SummarizationMiddleware(
            model=model,
            backend=backend,
            trigger=("tokens", 100000),  # 对话超过 10 万 token 时进行摘要
            keep=("messages", 20),       # 原样保留最近的 20 条消息
        ),
    ],
)

trigger 还接受 ("fraction", ...) 形式(按模型上下文窗口百分比),多个阈值组合时是 OR 语义。keep 保证最近的消息不被压缩,Agent 手头正在干的事不受影响。

压缩解决的是"对话历史",还有一类膨胀源是巨大的工具结果——一次搜索返回两万字网页。Deep Agents 的解法是自动卸载(offload)到文件系统,上下文里只留引用,Agent 需要时分块读回。压缩和卸载配合子代理隔离,构成了长任务的上下文三道闸门:

flowchart LR
    RAW["原始上下文压力<br/>对话历史膨胀 + 工具结果巨大 + 子任务中间过程"]
    RAW --> G1
    G1["闸门一:卸载<br/>大工具结果落盘到文件系统<br/>上下文只留引用"]
    G1 --> G2
    G2["闸门二:总结压缩<br/>SummarizationMiddleware<br/>收敛历史消息"]
    G2 --> G3
    G3["闸门三:子代理隔离<br/>子任务的中间过程<br/>只回传结果摘要"]
    G3 --> CLEAN["主代理上下文保持精简<br/>长任务不崩"]
    style RAW fill:#fde8e8,color:#8b1a1a,stroke:#e05252
    style CLEAN fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a

正在渲染图表…

顺带一提,LangChain 的预构建中间件里还有重试、降级、PII 检测等,可以按需插进这个栈里——但那是第 13 篇走向生产时的话题了。

六、牌五:能力裁剪——include 与 exclude 的三板斧

把 harness 用到生产,你几乎一定会想"少给模型几个工具"。工具越少,模型选错的概率越低,提示词也越省。官方给了三条裁剪路径,粒度从粗到细:

第一板斧:整组隐藏——excluded_tools。 通过注册 Harness Profile,把模型面前的文件系统工具整体摘掉:

from deepagents import HarnessProfile, register_harness_profile

register_harness_profile(
    "anthropic:claude-sonnet-4-6",
    HarnessProfile(
        excluded_tools=frozenset(
            {"ls", "read_file", "write_file", "edit_file", "delete", "glob", "grep"}
        ),
    ),
)

第二板斧:白名单保留——FilesystemMiddleware(tools=...)。 不想全藏、只想留一部分时,直接传入自己定制的 FilesystemMiddleware 实例(tools 白名单要求 deepagents>=0.7):

from deepagents import create_deep_agent
from deepagents.middleware import FilesystemMiddleware

# 只读代理:write_file、edit_file、delete 和 execute 永远不会展示
agent = create_deep_agent(
    model="claude-sonnet-4-6",
    middleware=[
        FilesystemMiddleware(backend=backend, tools=["read_file", "ls", "glob", "grep"]),
    ],
)

两条硬规则:白名单里必须始终包含 read_file,否则创建代理时直接抛 ValueError;delete 和 execute 就算你写了,Backend 不支持也会被移除。

第三板斧:同名替换——middleware 参数。 传一个 .name 与内置条目匹配的实例(比如自定义的 SummarizationMiddleware、FilesystemMiddleware),原位替换默认实现。注意替换是完全替换——自定义 FilesystemMiddleware 时必须自己把 backend(和 permissions)传给你的实例,它不会继承 create_deep_agent 的同名参数。

还有一个刻意的安全设计:你可以用 excluded_tools 隐藏工具面,但不能通过 excluded_middleware 移除 FilesystemMiddleware 本身——它是技术栈里必需的脚手架,会被刻意拒绝。框架的立场很清楚:你可以决定模型看到什么,但不能拆掉地基。

最后给一张速查对比:

flowchart TB
    Q["想裁剪内置能力?"]
    Q --> P1["只藏工具不换逻辑"]
    Q --> P2["藏一部分留一部分"]
    Q --> P3["连行为一起换掉"]
    P1 --> W1["excluded_tools<br/>HarnessProfile 白名单之外全隐藏"]
    P2 --> W2["FilesystemMiddleware<br/>tools= 白名单<br/>必须含 read_file"]
    P3 --> W3["middleware= 传入同名实例<br/>原位替换默认实现<br/>需 deepagents&gt;=0.7"]
    W1 --> R["注意:excluded_middleware<br/>移除 FilesystemMiddleware 被刻意拒绝"]
    style W1 fill:#f0f7f0,color:#1d4a1d,stroke:#4a9e4a
    style W2 fill:#fbf3e6,color:#6b4a12,stroke:#c99a3d
    style W3 fill:#f4effb,color:#4a2170,stroke:#8b52c9
    style R fill:#fdeef0,color:#701d2a,stroke:#c94a5e

正在渲染图表…

七、本篇踩坑点

  • 同名替换是"替换"不是"合并":覆盖 FilesystemMiddleware 后忘了传 backend,文件工具会拿到错误的存储——替换实例必须完整配置好所需的所有设置;
  • 白名单漏了 read_file 直接抛 ValueError:创建阶段就会失败,不是运行时的隐蔽 bug;
  • 想让子代理也受裁剪约束? 传入自定义 FilesystemMiddleware 会替换主代理的默认实例,通用子代理会继承同样的限制;但声明式子代理不会继承,需要在它自己的 middleware 字段里单独配;
  • v0.7 前后行为差异大:todo 规划从默认包含变成可选启用、delete 工具是新加的、同名替换机制也是新加的——升级版本前先看 changelog,别拿旧经验套新版本。

下一篇预告

五大能力之首的文件系统,至今我们只把它当"一个能存东西的地方"。但它背后藏着整个 Deep Agents 最优雅的抽象:Backend。同一个 Agent API,怎么做到内存态、真实磁盘、跨线程云端存储随意切换,还能按路径路由——/memories/ 走长期记忆、/workspace/ 走磁盘、其他统统走内存?下一篇《Backend:把文件系统做成外接大脑》,我们把这个抽象层拆到底,并且手写一个接入自有存储的最小自定义 Backend。


参考资料: