1. 为什么你的 DeepAgents 跑起来像“黑盒”很多人第一次接触 DeepAgents注意力都放在create_deep_agent()那一行调用上觉得把模型、工具、提示词塞进去就能跑。结果一旦 Agent 行为不符合预期——比如该规划的时候不规划、该派子代理的时候自己硬扛、对话轮次一多就失控——就完全不知道从哪下手。问题往往不在模型而在 Middleware中间件这一层。DeepAgents 的核心架构可以理解为“LangGraph 状态图 中间件栈”的组合。LangGraph 负责把 Agent 的执行拆成节点和边Middleware 则在这些节点前后插入逻辑决定 Agent 什么时候规划、什么时候读写文件、什么时候派发子代理、什么时候终止。换句话说Middleware 是 DeepAgents 的“行为开关”而AgentMiddleware就是写这些开关的基类。这篇内容面向已经能跑通最简 Agent、但想搞清楚执行链路和配置骨架的读者。我会从AgentMiddleware的 Hook 机制切入结合 LangGraph 的节点执行顺序给出一份可复制的config.toml和settings.json骨架再带你验证 Middleware 到底有没有生效。全程本地可跑不需要复杂环境。2. 前置准备TaoToken 接入与依赖安装在写 Middleware 之前得先有一个能稳定调用的模型入口。我这边习惯用 TaoToken 做统一接入它的 API 兼容 OpenAI 风格配置成本低适合拿来跑 DeepAgents 这种需要多轮调用的场景。第一步去控制台创建一个 API Key。地址是https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite创建后复制保存后面配置文件里要用。第二步安装依赖。DeepAgents 依赖 LangChain 和 LangGraph建议用虚拟环境python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install deepagents langchain langgraph langchain-openai第三步设置环境变量。TaoToken 的 API 地址是https://taotoken.net/api注意这里不加 UTM 参数直接作为 base_url 使用export TAOTOKEN_API_KEY你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你更习惯用配置文件管理可以跳过环境变量直接看下一节的config.toml和settings.json。两种方式选一种即可不要混用导致覆盖。提示模型名建议先用gpt-4o-mini这类轻量模型验证 Middleware 逻辑确认 Hook 触发顺序正确后再换成更强的模型跑真实任务能省不少调试成本。3. 可复制配置config.toml 与 settings.json 骨架DeepAgents 本身没有强制要求配置文件格式但把模型、Middleware 开关、限制参数抽出来能让调试清晰很多。下面这份config.toml是我实测下来比较顺手的骨架字段都对应到具体的 Middleware 行为# config.toml [model] provider openai name gpt-4o-mini base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY temperature 0.2 [agent] system_prompt You are a helpful assistant with planning ability. max_steps 25 [middleware.todo_list] enabled true auto_plan true [middleware.filesystem] enabled true root ./agent_workspace [middleware.sub_agent] enabled true max_sub_agents 3 [middleware.summarization] enabled true trigger_message_count 30 [middleware.human_in_the_loop] enabled false [middleware.custom.logging] enabled true max_model_calls 10对应的settings.json用于运行时覆盖适合在 CI 或不同机器上切换参数{ model: { name: gpt-4o-mini, base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY }, agent: { max_steps: 25 }, middleware: { todo_list: { enabled: true, auto_plan: true }, filesystem: { enabled: true, root: ./agent_workspace }, sub_agent: { enabled: true, max_sub_agents: 3 }, summarization: { enabled: true, trigger_message_count: 30 }, human_in_the_loop: { enabled: false }, custom: { logging: { enabled: true, max_model_calls: 10 } } } }关键字段说明middleware.todo_list.auto_plan控制是否让 Agent 自动调用write_todos做规划middleware.filesystem.root是虚拟文件系统的落盘目录middleware.sub_agent.max_sub_agents限制子代理派发数量防止递归失控middleware.custom.logging.max_model_calls是自定义中间件的调用上限超过就jump_to: end。把这些配置读进代码的方式很简单用tomllibPython 3.11或tomliimport tomllib from pathlib import Path def load_config(path: str config.toml) - dict: with open(path, rb) as f: return tomllib.load(f) cfg load_config() print(cfg[middleware][custom][logging])4. 配置骨架如何映射到 AgentMiddleware配置文件只是外壳真正决定行为的是AgentMiddleware的 Hook。DeepAgents 的中间件栈按执行顺序排列Main Agent 默认包含PatchToolCallsMiddleware、TodoListMiddleware、FilesystemMiddleware、SubAgentMiddleware、SummarizationMiddleware、HumanInTheLoopMiddleware、PromptCachingMiddleware、MemoryMiddleware。SubAgent 的栈是精简版没有TodoListMiddleware和SubAgentMiddleware因为子代理不能再派子代理。AgentMiddleware提供 6 个 Hook分两类。Node-style 的四个按时间点触发before_agent整个生命周期只跑一次before_model每次调模型前跑after_model每次模型返回后跑after_agent结束时跑一次。Wrap-style 的两个包裹调用wrap_model_call包住每次模型调用wrap_tool_call包住每次工具调用。执行顺序可以这样理解Agent 启动 →before_agent→ 进入循环 →before_model→wrap_model_call包裹模型 → 模型返回 →after_model→ 如果有工具调用 →wrap_tool_call包裹工具 → 回到循环 → 循环结束 →after_agent。把配置映射过来todo_list.enabled对应是否挂载TodoListMiddlewarefilesystem.root传给FilesystemMiddleware的初始化参数sub_agent.max_sub_agents传给SubAgentMiddlewarecustom.logging.max_model_calls传给自定义的LoggingMiddleware。下面是一个把配置转成中间件列表的骨架from deepagents import create_deep_agent from deepagents.middleware import TodoListMiddleware, FilesystemMiddleware, SubAgentMiddleware from langchain.agents.middleware import SummarizationMiddleware def build_middleware(cfg: dict): mw [] m cfg[middleware] if m[todo_list][enabled]: mw.append(TodoListMiddleware(auto_planm[todo_list][auto_plan])) if m[filesystem][enabled]: mw.append(FilesystemMiddleware(rootm[filesystem][root])) if m[sub_agent][enabled]: mw.append(SubAgentMiddleware(max_sub_agentsm[sub_agent][max_sub_agents])) if m[summarization][enabled]: mw.append(SummarizationMiddleware( trigger_message_countm[summarization][trigger_message_count] )) return mw注意顺序TodoListMiddleware要放在SubAgentMiddleware前面因为规划逻辑应该在派发子代理之前生效。如果你把限制类中间件放在日志中间件后面一旦限制先jump_to日志就不会执行了这是踩过的坑。5. 验证 Middleware 是否生效配置写完不代表生效得用具体动作验证。最直接的方式是写一个自定义LoggingMiddleware在before_model和after_model里打印状态然后跑一个会触发工具调用的任务。from typing import Any from deepagents import create_deep_agent from langchain.agents.middleware import AgentMiddleware, AgentState from langgraph.runtime import Runtime from langchain.messages import AIMessage class LoggingMiddleware(AgentMiddleware): def __init__(self, max_calls: int 10): super().__init__() self.max_calls max_calls self.call_count 0 def before_model(self, state: AgentState, runtime: Runtime) - dict[str, Any] | None: self.call_count 1 print(f[LOG] 第 {self.call_count} 次模型调用消息数: {len(state[messages])}) if self.call_count self.max_calls: return { messages: [AIMessage(content达到最大调用次数终止。)], jump_to: end, } return None def after_model(self, state: AgentState, runtime: Runtime) - dict[str, Any] | None: last state[messages][-1] has_tools hasattr(last, tool_calls) and last.tool_calls print(f[LOG] 模型返回has_tool_calls{has_tools}) return None def get_weather(city: str) - str: Get the weather for a given city. return fIts always sunny in {city}! agent create_deep_agent( modelopenai:gpt-4o-mini, tools[get_weather], system_promptYou are a helpful assistant., middleware[LoggingMiddleware(max_calls5)], ) result agent.invoke( {messages: [{role: user, content: What is the weather in Paris?}]} ) print( 最终回复 ) for msg in result[messages]: if getattr(msg, type, None) ai: print(msg.content)运行后你应该看到类似输出[LOG] 第 1 次模型调用消息数: 2 [LOG] 模型返回has_tool_callsTrue [LOG] 第 2 次模型调用消息数: 4 [LOG] 模型返回has_tool_callsFalse 最终回复 The weather in Paris is always sunny!如果before_model只打印了一次说明模型没有触发工具调用检查get_weather的 docstring 是否清晰如果after_model里has_tool_calls一直是 False可能是模型没理解工具用途。验证TodoListMiddleware是否生效可以给一个多步任务观察输出里是否出现write_todos的调用记录。验证FilesystemMiddleware看./agent_workspace目录下有没有生成文件。6. 本篇常见错误排查错误一在before_model里直接改state[messages]却不返回。这样修改不会生效因为 LangGraph 靠返回值合并状态。正确做法是返回{messages: [新消息]}。错误二jump_to值写错。只有end是 LangGraph 内置终止节点写exit或stop会报节点不存在。这个错误在日志里表现为图执行异常不容易一眼看出。错误三混淆before_agent和before_model的执行频率。before_agent整个生命周期只跑一次适合初始化数据库连接、加载配置before_model每次调模型都跑适合做动态检查。如果你把计数器放在before_agent里会发现它永远只加一次。错误四Middleware 顺序错误。限制类中间件要放在日志类前面否则限制触发jump_to后后面的日志中间件不会执行。同理TodoListMiddleware要放在SubAgentMiddleware前面。错误五自定义 Middleware 的name与默认栈冲突。如果自定义中间件的.name和默认中间件同名会替换默认实例而不是追加。想追加就换个名字想替换就保持同名。排查时建议打开 LangGraph 的调试日志或者在wrap_model_call里打印request内容能看到实际传给模型的完整消息列表比猜要快得多。7. 下一步从配置骨架到真实任务跑通最小示例后你可以把config.toml里的human_in_the_loop打开观察 Agent 在关键步骤暂停等待确认的行为也可以把max_sub_agents调大给一个需要拆解的任务看子代理如何被派发。如果想让 Agent 长期跑编码类任务建议了解一下 Coding Plan 的额度方案地址是https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite适合需要持续调用模型的场景。Middleware 的调试本质上是观察 LangGraph 状态图的流转。把每个 Hook 的输入输出打印出来对照执行顺序图很快就能定位问题。配置骨架只是起点真正的行为逻辑还是写在AgentMiddleware的子类里。