写在前面:前面我们手搓过一个 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。
社区教程里有个很直观的例子------你跟它说"帮我写一个博客系统",它会自动拆成:
markdown
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 短得可以全文贴出来:
javascript
// 中间件
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 上。
代码里那三行注释,才是这份文件的精华:
javascript
// 日志 中间件 模型调用次数统计
// request 中间 response
三个信息:
| 注释 | 透露的设计意图 |
|---|---|
| "日志 中间件" | 这个中间件要干的事:记录日志 |
| "模型调用次数统计" | 具体目标:数一数模型被调了几次 |
| "request 中间 response" | 拦截的位置:请求之前、响应之后 |
"request 中间 response" ------ 这就是中间件的本质。它不是一个功能,它是"插在流程中间的一段代码"。
钩子挂在哪:看懂 Agent 主循环
要理解中间件能插手的位置,得先看 Agent 的主循环长什么样。官方文档的描述是------调用模型、让模型选择要执行的工具、当它不再调用工具时结束 $TRAE_REF。
css
┌──────────────────────────────────┐
│ │
▼ │
[调用模型] → 要调工具吗? ──是──→ [执行工具] ──┘
│
否
▼
[结束]
中间件就在这些步骤的前后插钩子。官方文档把钩子分成两类 $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、条件边、子图。当时用来编排自己的流程。现在:
yaml
更大的 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 那段再读一遍:
javascript
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({}) 里什么都没有,但它精确地标出了"你可以在这里插手"。框架的价值不只是替你干活,还在于它在哪里留了口子。看一个框架,先看它给你留了哪些钩子,往往比看它有哪些功能更能理解它的设计思路。