智能体项目复盘 · deepagents 篇:1 主 3 子架构、HITL、RAGFlow 接入,以及 16 个真实踩坑
💡 前面几篇把大模型的基础盘、RAG 调参、低代码平台和私有化部署都过了一遍,但那些内容有个共同上限:一个 Agent 干所有事。
这一篇讲的是另一条路------多智能体。课件是尚硅谷大模型智能体课程的 04-多智能体项目_deepagents,5 天时间从 create_deep_agent() 一行代码,一路做到"主智能体 + 3 个子智能体 + FastAPI + WebSocket + Vue 前端 + MySQL 查询 + RAGFlow 知识库"的完整深度搜索项目。
但真正值得写的不是它能跑起来,而是中间那 16 个坑:为什么并发必须用 ContextVar 而不是全局变量、为什么流式解析要先过滤中间件节点、为什么教案里把 RAGFlow 说成"字节开源"、为什么子代理套子代理这条路走不通。这些内容才是一个项目从 Demo 到能上线的距离。
1. 先把三层关系对齐:LangChain、LangGraph、DeepAgents
这是看这类课程最容易迷路的第一步:三个东西都叫 agent 框架,但它们不在同一层。
课件给的分层说法我认为是全篇最清晰的一句:
| 层 | 项目 | 角色 | 类比 | 管什么 |
|---|---|---|---|---|
| Framework | LangChain | 提供"做动作"的积木 | Spring | 模型适配、Prompt 模板、工具、@tool |
| Runtime | LangGraph | 提供"跑流程"的引擎 | SpringMVC 容器 | 状态图、节点、边、checkpointer、Store |
| Harness | DeepAgents | 负责组织与调度 | SpringBoot Starter | 规划、子代理、虚拟文件系统、长期记忆、HITL |
text
你的业务代码 --> DeepAgents(Harness) --> LangChain(Framework) + LangGraph(Runtime) --> 大模型 API
简单理解:LangChain 负责"会做事",LangGraph 负责"按流程做事",DeepAgents 负责"把复杂的事拆给一队人做"。
课件用的版本锁得很死,这套组合是配套演进的,跨版本很容易炸:
| 依赖 | 版本 |
|---|---|
deepagents |
0.4.3 |
langchain |
1.2.10 |
langgraph |
1.0.9 |
langgraph-checkpoint |
4.0.0 |
langgraph-prebuilt |
1.0.8 |
langchain-openai |
1.1.10 |
fastapi / uvicorn |
0.129.2 / 0.41.0 |
ragflow-sdk |
0.24.0 |
| 前端 | Vue 3.5.24 + TS 5.9 + Vite(被 override 成 rolldown-vite@7.2.5) |
Q:为什么不直接用 LangGraph 手搓一个?
因为 DeepAgents 帮你内置了四件你迟早要自己写的事。课件把它总结为四大核心能力:
text
① write_todos --> 规划与任务分解(配套 read/update/delete_todos)
② 文件系统工具集 --> ls / read_file / write_file / edit_file,用来把上下文卸载到文件,防溢出
③ task 工具 --> 生成子代理 + 上下文隔离
④ LangGraph Store --> 跨 thread 的长期记忆
其中 ② 是最容易被低估的一条。长任务真正的瓶颈不是模型不聪明,是上下文塞不下------把中间结果写进文件,只把路径留在对话里,是这套框架最实用的设计。
2. 多智能体:什么时候该上,什么时候不该上
课件在这一节写得非常克制,这很难得------大部分教程会直接开始教你建 5 个 agent。
先给对比:
| 维度 | 单体 Agent | 多智能体 |
|---|---|---|
| 上下文 | 全部堆在一个窗口,注意力被稀释 | 每个子代理只看自己该看的,上下文隔离 |
| 提示词 | 越长越互相打架 | 每个角色一份短提示词 |
| 拆解方式 | 靠模型自己排序 | 分而治之 |
| 成本 | 线性 | Token 指数级失控 |
| 可调试性 | 尚可 | 非确定性叠加,调试噩梦 |
课件因此给了两条很硬的规则:
三条入场铁律:① 问题极度开放;② 存在领域冲突(不同角色对同一问题的最优解不同);③ 需要多方向并行。
一句工程铁律:不建全链路追踪,绝不上线。
第二条我自己踩过,深表认同------多智能体的 bug 长得不像 bug,它长得像"模型今天心情不好"。
架构模式上,业界三条路也顺便理清了:
text
层级式 Orchestrator-Workers:主代理派活、收结果 --> DeepAgents 走这条
网 络式 Network :任意两两通信 --> AutoGen 走这条
混 合式 :角色分工 + 局部协商 --> CrewAI 走这条
Q:所以 deepagents 到底是不是"多智能体框架"?
更准确的说法是:它是一个层级式(指挥官模式)的多智能体脚手架 。子代理在框架里被统一暴露成一个叫 task 的工具,主代理发出的调用长这样:
json
{"name": "task", "args": {"subagent_type": "网络搜索助手", "description": "..."}}
也就是说,"调用子代理"这件事被降维成了"调用一个工具"。这是 DeepAgents 最重要的一个设计取舍:简单、可组合,但也正是这个设计决定了它的一个硬限制------见下一节。
3. 核心 API:一个 create_deep_agent 就够了
课件里 90% 的示例都从这一个函数出发。把它的参数认清,框架就通了。
python
from deepagents import create_deep_agent
deep_agent = create_deep_agent(
model=llm,
tools=[internet_search],
subagents=[],
system_prompt="""你是一个专家级的研究员!...""",
)
实际用到的全部参数(来自课件的《DeepAgents 使用总结》):
| 参数 | 作用 | 备注 |
|---|---|---|
model |
模型实例 | init_chat_model(model=..., model_provider="openai", temperature=0.1) |
system_prompt |
人设与规则 | 项目里全部外置到 prompts.yml |
tools |
主代理自己的工具 | |
subagents |
子代理列表 | 见下 |
checkpointer |
状态存储 | HITL 必需,示例用 InMemorySaver() |
interrupt_on |
人工介入白名单 | |
backend |
文件/记忆的落点 | 缺省走 LangGraph state(虚拟 FS) |
store |
跨 thread 长期记忆 | |
middleware |
中间件 | 限流、自定义工具拦截 |
skills |
Agent Skills 目录 | 必须先配 FilesystemBackend |
子代理两种写法:
python
# 写法 1:字典(项目里三个子代理都是这个,每个只有 8 行)
weather_agent = {
"name": "weather_helper",
"description": "...",
"system_prompt": "...",
"tools": [],
}
# 写法 2:兼容层------把 LangGraph 图 / LangChain agent 包装进来
from deepagents import CompiledSubAgent
sub_agent = CompiledSubAgent(name="graph_agent", description="处理所有的业务逻辑!!", runnable=compile_graph)
写法 2 有个强制约束:图的 State 必须包含 messages,且带 add_messages reducer,否则消息合并直接报错。
Q:流式输出怎么解析?
这是课件标注"重点"的部分,也是最容易被劝退的部分。astream() 的 chunk 只有四种语义:
text
A 模型决定调工具 --> B 工具返回结果 --> C 模型决定调子代理(name=='task') --> D 最终回复
而节点名只有 model 和 tools 两个 key。但要先加一道过滤,否则会中途崩掉:
python
if not state or "messages" not in state:
continue # 否则会被 {'TodoListMiddleware.after_model': None} 之类崩掉
框架内置的 TodoListMiddleware、PatchToolCallsMiddleware 会往流里塞自己的节点,不认识的 key 一律跳过是多智能体流式解析的通用保命符。
Q:HITL(人工确认)怎么接?
三个硬条件,缺一个就静默失败:
| 条件 | 说明 |
|---|---|
必须配 checkpointer |
状态要能挂起 |
必须传 thread_id |
且二次调用线程 id 必须完全一致 |
二次 invoke 只能传 Command(resume={"decisions": [...]}) |
不能再把 query 传进去 |
decisions 支持 approve / reject / edit(edit 要带 edited_action: {name, args}),并且数组顺序必须与被拦截的顺序一一对应 。课件注释很坦白:InMemorySaver 仅用于测试,生产建议换 Redis checkpointer。
Q:子代理能再套子代理吗?
不能。课件专门写了 CEO→CTO→Coder 的三层委派实验,结论是当前版本不支持子代理再生成子代理 ,字典式 subagent 硬写 subagents 键也没用(原注释:"没有这个属性,硬写的!dict 底层不识别")。多层级只能靠提示词强行约束。
这一条直接决定了架构上限:你能做的是两层(1 主 N 子),不是树。
4. 项目全貌:一个"深度搜索"产品的完整链路
前面是零件,这里是怎么装起来。项目定位:企业知识 + 业务数据库 + 联网搜索三源合一的深度研究报告助手。
text
Vue 前端(5173) --HTTP POST /api/task {query, thread_id}--> FastAPI(8000) --立即返回 started--> 前端
│ │
└────────────── WS /ws/{thread_id} 长连接 ◄────────────────────┘
│
asyncio.create_task(run_deep_agent)
│
主智能体(项目经理人设:只规划/委派/汇总,不自己查数据)
├── task --> network_search_agent (Tavily)
├── task --> database_query_agent (MySQL 三工具)
└── task --> knowledge_base_agent (RAGFlow)
│
monitor 单例 + contextvars 取当前 thread_id --> 定向 send_json 回对应 socket
接口层一共 5 个,设计得很朴素但都是必要的:
| 接口 | 方法 | 作用 |
|---|---|---|
/api/task |
POST | 提交任务,立即返回 {status:"started", thread_id},内部 asyncio.create_task |
/api/upload |
POST | multipart 上传到 updated/session_{thread_id}/ |
/api/download?path= |
GET | 下载产物,带路径穿越校验 |
/api/files?path= |
GET | 递归列文件,按 mtime 倒序 |
/ws/{thread_id} |
WS | 事件回传,收 ping 回 pong,前端断线 3 秒自动重连 |
Q:为什么"提交任务"和"拿结果"要分成两条通道?
因为一个深度搜索任务要跑几分钟。HTTP 请求只能立刻返回"已受理",过程事件(tool_start / assistant_call / task_result)靠 WebSocket 推。这里有个很值得记住的实现细节:
并发隔离只能靠
ContextVar。 全局变量会串台(A 会话的文件写进 B 会话的目录),而threading.local在 asyncio 单线程模型下完全失效------因为从头到尾就一个线程。
api/context.py 整个文件就是这段论证。同理,ConnectionManager.loop 要延迟到 @app.on_event("startup") 里才 set_loop():同 loop 用 create_task,跨线程/跨 loop 必须 asyncio.run_coroutine_threadsafe,否则报"协程在错误的循环中运行"。
数据侧:MySQL + RAGFlow
| 项 | 选型 | 细节 |
|---|---|---|
| 业务库 | MySQL 8 pharma_db |
drugs 10 条 / inventory 每药 3 批次 / sales_records,外键 ON DELETE CASCADE |
| 访问方式 | 裸 mysql.connector,不是 ORM |
list_sql_tables / get_table_data / execute_sql_query |
| 返回格式 | CSV 文本 | 首行列头 + 逗号分隔 |
| RAG | RAGFlow (ragflow-sdk 0.24.0) |
检索与生成全在服务端完成,本项目不做切块、不做向量检索 |
| Embedding | text-embedding-v3@Tongyi-Qianwen |
建库时显式指定 |
| 向量存储 | Elasticsearch 8.11.3(可选 Infinity) | 部署时需手工把 es 镜像标签改成这个版本号,否则触发长时间重拉 |
两个反直觉但很实用的决定:
一是 SQL 结果转 CSV 再喂给模型。 模型对 CSV 的字段对齐理解比 JSON 稳,尤其在列数多的时候------JSON 里字段名重复占 token,模型反而更容易把值和列对错位。
二是 RAG 完全外包。 整个项目里没有一行 chunk/embedding 代码,ragflow_tools.py 只有两个 @tool:get_assistant_list() 拼出"助手名称;功能介绍;关联知识库",create_ask_delete(chat_name, question) 走 create_session → session.ask(stream=True) → delete_sessions。Agent 只负责"选哪个助手 + 问什么问题"。
这带来一个直接后果,课件在 prompts.yml 里把它写成了硬规则:知识库名和 description 必须写准,因为主代理完全靠 description 决定调用哪个助手;同时子代理被要求"保留检索的全部信息,不需要进行概括性总结",防止子代理自作主张压缩上下文。
5. 16 个坑清单
这一节是我认为这份课件最值钱的地方,全部来自代码注释和运行报错,不是编的。
| # | 坑 | 后果 / 解法 |
|---|---|---|
| 1 | 流式解析没过滤中间件节点 | {'TodoListMiddleware.after_model': None} 直接崩,加 continue 守卫 |
| 2 | HITL 二次调用又传了 query | 必须只传 Command(resume=...),且 thread_id 完全一致 |
| 3 | 字典 subagent 里写 subagents 键 |
底层不识别,静默失效,多层委派走不通 |
| 4 | LangGraph 图的 State 少了 messages+add_messages |
合并报错 |
| 5 | 在错误的 event loop 上跑 WS 协程 | 用 run_coroutine_threadsafe,loop 延迟到 startup 设置 |
| 6 | 用全局变量 / threading.local 存会话 id |
串台 / 完全失效,只能用 ContextVar |
| 7 | 模型自己编路径(/workspace、/mnt/data、/home/user) |
path_utils.py 一张 12 行场景表兜底清洗;Windows 无盘符要当相对路径;session_abc/session_abc/ 要拍平 |
| 8 | os.getenv("tavily_api_key") 大小写不匹配 |
取到 None,day01 示例真有这个 bug,day04/05 才改对 |
| 9 | rawflow/rag_config.py 的 .env 路径算错一层 |
分支永不命中,全靠 load_dotenv() 回退掩盖 |
| 10 | execute_sql_query 无只读约束,表名用 f-string 拼 |
模型可执行任意 SQL。生产必须上只读账号 + 语句白名单 |
| 11 | PDF 转换走 win32com + 本机装了 Word |
Linux/Mac 直接不可用;requirements 里的 xhtml2pdf/weasyprint 是没接线的备胎 |
| 12 | 前端 http://localhost:8000 与 http://127.0.0.1:8000 混用;/outputs/... 是死链(后端 app.mount 已注释) |
真实下载走 /api/download |
| 13 | 提示词自相矛盾:一处要求"必须把工作目录告诉子代理",另一处说"不需要把文件地址告诉子智能体" | 模型行为不可预测;人设写成"空调公司"但数据是医药库,角色-数据错配直接影响子代理选择 |
| 14 | read_file_content 的 instruction 参数收下后完全未用 |
典型"为提示词而写的假参数",装饰器签名与函数体脱节 |
| 15 | SKILL 放太多 | 原注释:"不是越多越好,7-10,多了模型也不会调用(摆烂),千万不要放重复功能的 SKILL(选择困难)";且文件夹名必须等于 SKILL.md 的 name |
| 16 | 限流中间件没配 | ModelCallLimitMiddleware(thread_limit, run_limit, exit_behavior="error"/"end") + @wrap_tool_call 自定义中间件是控制 Token 失控的唯一实用手段 |
第 10 条值得单独强调:给 Agent 一个能执行任意 SQL 的工具,等价于把数据库读写权限交给了提示词注入。 这是所有 text-to-SQL 项目的通病,课件原样留着当教学,生产环境绝对不能照搬。
6. 顺手做个事实核查:教案里有几处是错的
这一节不在课程大纲里,是我核对时发现的。写出来是因为如果你照抄教案,会在面试或者文档里翻车。
| 教案说法 | 实际情况 |
|---|---|
| "RAGFlow 是字节跳动开源的 RAG 框架" | 不对。RAGFlow 是 InfiniFlow 项目的开源产品 |
| 把 Infinity 解释成"字节开源的高分辨率图像生成模型" | 完全串了。在 RAGFlow 语境里,Infinity 是它自带的可选向量/文档引擎(与 Elasticsearch 二选一) |
| 把 Valkey 说成"缓存、消息队列" | Valkey 是 Redis 协议的分叉 KV 存储,在 RAGFlow 里做队列/缓存,但不是消息队列产品 |
模型名 qwen3-32b、qwen-max、qwen2.5-14b-instruct 混用 |
都是走 DashScope 的 OpenAI 兼容模式 https://dashscope.aliyuncs.com/compatible-mode/v1,model_provider="openai" |
另外,课程代码的 .env 与教案 md 里是明文 API Key (DashScope、Tavily、RAGFlow 三套)。本文已全部脱敏成 ‹REDACTED-API-KEY›。这里顺带说一句:课程资料是会被反复下载和转发的,明文密钥写进 .env 示例就等于已经泄露了 ,真实项目里这类文件必须进 .gitignore,用 .env.example 占位。
最后总结
- DeepAgents 的定位要认准 :它不是新的 Agent 框架,是 LangChain 之上、LangGraph 之上的 Harness 层。它的价值是"组织",不是"能力"。
- 一句话理解 :
模型负责思考,LangChain 负责做事,LangGraph 负责跑流程,DeepAgents 负责把活分给一队人。 - 多智能体是成本换来的确定性:上下文隔离、职责单一,代价是 Token 指数级失控和非确定性调试。三条入场铁律(问题足够开放 / 存在领域冲突 / 需要并行)不满足就别上。
- 架构上限是两层 :当前版本子代理不能再生成子代理。想搭"树",得自己在
task工具之上重做。 - 最实用的三个内置能力 :虚拟文件系统(上下文卸载)、
interrupt_on(HITL)、CompositeBackend(短期 state + 长期 Store + 本地文件混用)。 - 工程真正的难点不在 Agent,在周边 :event loop 归属、
ContextVar并发隔离、模型编路径的清洗表、WS 通道设计------这三样占了本项目一半以上的 bug。 - 笔者(后端 & 架构)的判断是:这套代码可以直接当"多智能体入门 Demo"看,但不能当"生产模板"抄。 具体不抄三条:任意 SQL 工具、
InMemorySaver、Windows-only 的 Word 转 PDF。真要上生产,先补的是全链路追踪和只读数据通道,而不是再加两个子代理。
下一篇讲一个完整的落地项目------掌柜智库,看 RAG 在真实业务系统里怎么切文档、怎么设计那 24 个 .prompt 文件。
参考资料 & 致谢
4 RAGFlow - GitHub(InfiniFlow)
9\] \[尚硅谷大模型智能体线上速成班 V2.0 · Day02《多智能体项目 deepagents》课件与源码
注:文中所有 API Key 已脱敏;版本号、端口、表结构等配置均来自课程交付代码,属 2026 年课程快照,实际依赖请以你本地
pip freeze为准。