🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)

24 阅读8分钟

写在前面:前面我们手搓过一个 Agent——那 338 行的 agent.py,从主循环到工具分发到沙箱校验,一行行自己写。今天这节课换了个思路:别人已经把大部分搭好了,我们只写自己想改的那部分。 readme 开门见山——"复杂的 Agent,全部从头实现比较麻烦。DeepAgent 半成品的 Agent 框架,提供了基础的 Agent 模型,可以快速实现复杂的 Agent。" 这篇文章讲两件事:DeepAgents 到底替你做了哪些活,以及那个只写了骨架的中间件文件,藏着什么设计思想。以下代码来自课堂真实文件,框架背景均已核对官方文档。


一、三层抽象:从散件到整机

readme 用三句话,把 LangChain 生态的三层定位讲清楚了:

"LangChain 是给你一堆 AI 开发积木, LangGraph 搭建复杂工作的底层蓝图 DeepAgent 大幅度降低复杂 Agent 的开发门槛,适合快速落地复杂 Agent 应用。"

这个递进关系,可以拿组装电脑来类比:

层次类比你拿到什么
LangChain一堆散装配件主板、CPU、内存、电源——零件都齐,怎么装看你
LangGraph装机图纸走线怎么走、扩展槽怎么规划——结构由你设计
DeepAgents品牌整机已经装好了,开机能用,你想升级再加卡

readme 后面还有两句更精炼的定位:

"跳过重复的底层基建,直接聚焦 Agent 的业务逻辑与能力迭代,是 LangGraph 生态面向生产落地的高阶封装方案。"

"跳过重复的底层基建" —— 这七个字是 DeepAgents 存在的全部理由。

想想前面那个 agent.py:主循环、工具分发、finish_reason 判断、子 Agent 启动、上下文隔离……这些活每个 Agent 都要干一遍,而且每次都差不多。 自己写不是不行,是重复。

用装机比喻说——你每次攒机都要自己拧一遍所有螺丝,太累了。 有人直接把整机装好给你,你只需要决定装什么软件。


二、DeepAgents 送了什么:四件"底层基建"

readme 列了它的核心能力:

"状态管理 state,循环路由,持久化执行能力(底层)—— 任务规划,长期记忆,子 Agent 调度,上下文压缩等核心能力。"

注意这句话里有个分层:

层次能力谁提供的
底层状态管理、循环路由、持久化执行LangGraph(前面学过)
上层任务规划、长期记忆、子 Agent 调度、上下文压缩DeepAgents 直接送

这四件"上层能力",正是手搓 Agent 时最费劲的部分。逐个对照着看看:

1. 任务规划

官方文档里,DeepAgents 内置了一个 task 工具,用于派发任务;同时提供待办清单能力,让 Agent 在动手前先把复杂目标拆成可执行步骤 $TRAE_REF。

社区教程里有个很直观的例子——你跟它说"帮我写一个博客系统",它会自动拆成:

1. 设计数据库
2. 写后端 API
3. 写前端页面
4. 部署上线

这种"先列清单再干活"的机制,解决的是 Agent 的一个老毛病:跑到一半忘了目标。 任务一复杂、步骤一多,模型很容易在中间绕晕——有了待办清单,它每一步都能回头看看"还有哪几项没做完"。

2. 子 Agent 调度

这条 readme 也提到了,官方文档的说法是——内置 task 工具让主智能体可以为隔离的、长期运行的、多步骤或并行的任务创建临时子智能体 $TRAE_REF。

子智能体带来三个好处,文档里列得很清楚 $TRAE_REF:

好处含义
全新的上下文每次调用都创建一个带自身上下文的新 Agent 实例
自主执行子 Agent 独立运行直到完成
单一交接完成后把结果交回主 Agent

这三条,跟前面 Harness 那节课讲 Sub Agents 的三个理由完全对得上——上下文隔离、并行执行、只回传结论。区别只在于:那时候是我们自己写 run_subagent 函数,现在是框架内置一个 task 工具。

自己写 vs 框架内置——这就是"底层基建"和"业务逻辑"的分界线。

3. 上下文压缩

官方文档提到:当上下文窗口变长时,自动摘要功能会介入 $TRAE_REF。

这不就是前面 Memory 那节课学的"总结记忆"吗——聊到一定长度就把老消息压缩成摘要。 当时我们自己写 trimMessages、自己调 js-tiktoken 算 token、自己判断什么时候触发总结。现在它是默认行为。

4. 长期记忆与可插拔存储

这条最"工程化"。文档里说,存储后端可以选——内存状态、本地磁盘、用于跨线程持久化的 LangGraph Store、用于隔离代码执行的沙箱(Modal、Daytona、Deno),甚至可以通过组合路由把多个后端拼起来,或者实现自己的自定义后端 $TRAE_REF。

"可选后端"这个设计很值得琢磨。

它把"存哪里"变成了一个配置项,而不是写死在代码里:

开发阶段  → 内存(重启就没了,但够用)
单机部署  → 本地磁盘
生产环境  → LangGraph Store(跨线程持久化)
跑代码    → 沙箱(Modal / Daytona / Deno)

同一套 Agent 代码,换个后端配置就能从开发环境搬到生产环境。 这是"面向生产落地"这句话的具体含义。


三、中间件:整机上预留的"插槽"

框架替你干了大部分活,但总有些活是只有你知道该怎么干的——比如你想记录每次模型调用的次数、想给日志加个前缀、想在特定条件下拦一下。

这就是中间件(Middleware)的用途。LangChain 官方文档对它的定位是:提供一种更精细地控制 Agent 内部行为的方式 $TRAE_REF,用于:

用途举例
追踪行为日志、分析、调试
转换输入输出改写 prompt、控制工具选择、格式化输出
增强健壮性重试、降级、提前终止
加约束限流、护栏、PII(敏感信息)检测

回到装机比喻——中间件就是主板上预留的那些插槽。 整机已经能用,但你想加块显卡、装个采集卡,插上去就行,不用换主板。

课堂的骨架代码

middleware-test.mjs 短得可以全文贴出来:

// 中间件
import "dotenv/config"
import {z} from "zod"
import { ChatOpenAI } from "@langchain/openai";
import {
    createAgent,// 创建Agent
    createMiddleware,// 创建中间件
    HumanMessage,// 人类消息
    AIMessage,// 人工智能消息
} from "@langchain";

const model = new ChatOpenAI({
    model: process.env.MODEL_NAME,
    apiKey: process.env.OPENAI_API_KEY,
    configuration:{
        baseURL: process.env.OPENAI_BASE_URL,
    },
    temperature: 0,
});
// 日志 中间件 模型调用次数统计
// request 中间 response
const loggingMiddleware = createMiddleware({

});

const agent = createAgent({
    model,
    tools:[],
    systemPrompt:"你是一个助手。",
    middleware:[
        loggingMiddleware,
    ]
});

看到 createMiddleware({ }) 里面是空的,别急着说"这没写完"——这个空壳子本身就在讲一件重要的事。

它说明中间件的结构是:创建一个对象、填进钩子函数、挂到 Agent 上。

代码里那三行注释,才是这份文件的精华:

// 日志 中间件 模型调用次数统计
// request 中间 response

三个信息:

注释透露的设计意图
"日志 中间件"这个中间件要干的事:记录日志
"模型调用次数统计"具体目标:数一数模型被调了几次
"request 中间 response"拦截的位置:请求之前、响应之后

"request 中间 response" —— 这就是中间件的本质。它不是一个功能,它是"插在流程中间的一段代码"。

钩子挂在哪:看懂 Agent 主循环

要理解中间件能插手的位置,得先看 Agent 的主循环长什么样。官方文档的描述是——调用模型、让模型选择要执行的工具、当它不再调用工具时结束 $TRAE_REF。

    ┌──────────────────────────────────┐
    │                                  │
    ▼                                  │
[调用模型] → 要调工具吗? ──是──→ [执行工具] ──┘
    │
    否
    ▼
  [结束]

中间件就在这些步骤的前后插钩子。官方文档把钩子分成两类 $TRAE_REF:

类型钩子特点
节点式beforeModel / afterModel在模型调用前后执行
包裹式wrap_model_call / wrap_tool_call包住每次调用

包裹式钩子有个很有意思的能力——你可以决定真正要执行的那个 handler 被调用几次。

文档的原话是:你可以让它被调用零次(短路)、一次(正常流程)或多次(重试逻辑)$TRAE_REF。

这三种可能性,对应三个非常实用的场景:

调用次数效果用途
零次短路,不真的执行缓存命中直接返回、命中规则直接拦截
一次正常流程日志、监控
多次重复执行重试(失败了再来一次)

第二种正好对应课堂注释里说的"模型调用次数统计"——包住调用,数一次,再放行。

顺便一提,LangChain v1 里 beforeModel / afterModel 是替代了旧版 pre_model_hook / post_model_hook 的新写法 $TRAE_REF——框架自己也在迭代,钩子的名字会变,但"在流程中间插一段"这个思想不会变。

一个容易被忽略的细节

官方文档里有一句话,我读完觉得挺关键——中间件不是独立的运行时:钩子就跑在 createAgent 编译出来的那个 LangGraph 里。 你可以把整个 Agent(连同它的中间件)当成一个节点或子图,丢进更大的 StateGraph 里,所有中间件钩子照样会执行 $TRAE_REF。

这句话把前面几节课的知识串起来了。

还记得 LangGraph 那节课吗——我们学过 StateGraph、addNode、条件边、子图。当时用来编排自己的流程。现在:

更大的 StateGraph(你自己编排的流程)
├── node: 分类节点
├── node: 这个 Agent(createAgent 出来的)
│         └── 内部:主循环 + 中间件钩子
└── node: 其他处理节点

你编排的图和框架内部的图,是同一种东西,可以互相嵌套。

这就是"是 LangGraph 生态的高阶封装方案"的技术含义——DeepAgents 和 LangGraph 不是两个体系,是同一棵树上的不同高度。

还有个实用细节:官方文档明确说 HumanInTheLoopMiddleware 是按每个工具的 .name 来匹配的 $TRAE_REF。意思是——人类审批(Human-in-the-loop)这种能力,在 v1 里就是配置一个内置中间件。 前面 Harness 那节课我们手写过 interrupt 逻辑,现在是 middleware: [humanInTheLoopMiddleware({ interruptOn: { send_email: true } })] 一行配置。

关于那个导入路径

课堂文件里是从 "@langchain" 导入这几个函数的,而官方 JavaScript 文档的示例用的是 langchain $TRAE_REF。

这类入口路径会随版本和安装方式变化,以你本地实际安装的包为准——遇到导入报错,先看一眼 package.json 装的是哪个,再去查对应版本的文档。这不是什么大坑,但确实是新手最容易卡住的地方之一。


四、这份骨架代码,其实信息量很大

最后把 createAgent 那段再读一遍:

const agent = createAgent({
    model,
    tools:[],
    systemPrompt:"你是一个助手。",
    middleware:[
        loggingMiddleware,
    ]
});

四个参数,正好勾勒出一个最小 Agent 的骨架:

参数作用课堂值
model用哪个模型DeepSeek(走 OpenAI 兼容接口)
tools能用哪些工具空数组
systemPrompt系统提示词"你是一个助手。"
middleware挂哪些中间件一个(空壳的)日志中间件

tools: [] 是个很有意思的状态。

我们知道 Agent = LLM + Harness,工具是 Harness 的核心成员。这里工具是空的——意味着这个 Agent 目前只会聊天,没有手脚。

而它依然是个合法可跑的 Agent。这说明:

工具是"能力",中间件是"机制"——可以先有机制、后加能力。

先把日志中间件挂上、把骨架跑通、确认钩子能被触发,再去加工具——这是一种很务实的调试顺序。 等工具加了一堆再排查"为什么日志没打出来",就麻烦多了。

代码里注释掉的 HumanMessage、AIMessage 也印证了这一点——它们是从 "@langchain" 一起导进来的,大概是为后面构造测试消息准备的(手动喂一条 HumanMessage 进去,看中间件有没有被触发)。

一个"能跑的最小骨架 + 一个待填充的钩子" ——这就是学习一个新框架最标准的第一步。


五、三层各管什么:一张选择表

把三层抽象和中间件的关系整理成一张表:

你想干的事该在哪一层动手
换个数据源、加个解析器LangChain 组件
设计流程图(分支、循环、并行)LangGraph
记日志、数调用次数中间件
改 prompt、控制工具选择中间件
加重试、降级、限流中间件
加人类审批、敏感信息过滤中间件(内置)
任务规划、子 Agent、上下文压缩DeepAgents 已内置

观察一下最下面一行——那些最费劲的能力,现在成了"默认就有"。

而中间件占据了中间那几行——"每个项目都需要但每个项目都不一样"的那部分。 框架不能替你决定日志打成什么格式、什么时候该重试、哪些操作要人工审批——这些是你项目的个性,所以留给你插。

这就是"半成品"的精髓:把共性替你做完,把个性的接口留出来。


PS:这节课最有意思的地方在于那个"空中间件"——createMiddleware({}) 里什么都没有,但它精确地标出了"你可以在这里插手"。框架的价值不只是替你干活,还在于它在哪里留了口子。看一个框架,先看它给你留了哪些钩子,往往比看它有哪些功能更能理解它的设计思路。