很多人第一次把 LangChain / DeepAgent 接到自己的产品里,会卡在同一处:模型能聊,但规划、工具、文件、会话管理散落在脚本里,同事没法点、没法管。公开仓库 liuyanqun0815/langchain-deep-agent 做的是另一件事——用 Vue 3 工作台把 DeepAgent 包成可运行工程,并且把「已经写完的」「只留了壳的」「设计里还没做的」分得很开。
本文只依据该仓库已公开的 README、docs/、前后端源码,不含本地私有文件或真实密钥。文中会明确标出:当前已实现、半成品/预留、未来计划;设计文档与代码不一致的地方也会直说。
一、要解决什么,以及我们选了什么
痛点
- 单页调 Chat Completions:没有规划、工具、文件系统的统一壳。
- 纯脚本跑 Agent:难给非开发同事试用,技能开关也难管。
- 只堆 Prompt:复杂任务靠「多写几句」撑不住。
选型(仓库现状)
| 层 | 选择 | 公开证据 |
|---|---|---|
| 前端 | Vue 3 + Vite + Naive UI | frontend/,路由 ChatView / SkillsView / ModelsView |
| 后端 | FastAPI + SQLModel + SQLite | backend/main.py、app/db/ |
| Agent | LangChain deepagents create_deep_agent | backend/app/agent/factory.py |
| 部署 | Docker Compose;本地用 run_dev.py | docker-compose.yml、backend/run_dev.py |
核心务实点有两个:一是产品化外壳(会话 / 消息 / 技能进 SQLite,界面可点);二是双轨扩展(skill.yaml Tools + SKILL.md Skills 同目录共存,一起喂给 DeepAgent)。
二、整体架构
graph TB
subgraph FE[前端 Vue 3 SPA]
CV[ChatView 聊天]
SV[SkillsView 技能]
MV[ModelsView 模型]
end
subgraph API[FastAPI 接口层]
R1["/api/sessions"]
R2["/api/skills"]
R3["/api/models"]
R4["/api/agent/config"]
AS[agent_service]
end
subgraph AG[Agent 运行时]
FAC[factory.create_deep_agent]
REG[skill_registry Tools]
BE[CompositeBackend]
CP[MemorySaver checkpointer]
ST[InMemoryStore]
end
subgraph DATA[持久化与磁盘]
DB[(SQLite sessions/messages/skills)]
SK["backend/skills/"]
UP["data/uploads/"]
end
CV --> R1
SV --> R2
MV --> R3
R1 --> AS
AS --> FAC
REG --> FAC
FAC --> BE
FAC --> CP
FAC --> ST
AS --> DB
SK --> REG
SK --> BE
UP --> BE
职责边界和目录是对齐的:routers/ 很薄,services/ 做业务编排,真正碰 DeepAgent 的只有 app/agent/。启动时 lifespan 会按固定顺序热身——这很重要,后面会细说。
三、一次请求的真实路径(含 SSE)
仓库里聊天不止「一次性 invoke」。sessions 路由已经挂了流式端点:POST /{session_id}/messages/stream 与带上传的 .../upload/stream,返回 StreamingResponse(..., media_type="text/event-stream"),内部调 agent_service.chat_stream / chat_stream_with_files。
sequenceDiagram
participant U as 用户
participant FE as Vue 前端
participant API as FastAPI sessions
participant Svc as agent_service
participant AG as DeepAgent
participant DB as SQLite
U->>FE: 输入消息(可附文件)
FE->>API: POST .../messages/stream
API->>Svc: chat_stream
Svc->>DB: 写入 user 消息
Svc-->>FE: SSE event=start
Svc->>AG: agent.stream(messages, thread_id)
loop 增量事件
AG-->>Svc: AI/Tool 消息增量
Svc-->>FE: inference_step / chunk
end
Svc->>DB: 写入 assistant(Reasoner 附 reasoning_content)
Svc-->>FE: SSE event=end
FE-->>U: 展示正文与推理框
启动热身(lifespan)
公开 backend/main.py:
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
settings.skills_root_dir.mkdir(parents=True, exist_ok=True)
sync_skills_from_disk()
skill_registry.reload_from_db()
get_agent()
yield
顺序含义很直白:先保证表存在 → 磁盘 skills 同步进 DB → 注册表按 DB 启用状态装 Tools → 预创建 Agent 单例。第一句聊天不会撞上冷启动拼装。
设计文档 vs 代码:SSE 这件事要诚实说
docs/TECHNICAL_DESIGN.md 第 9 节「风险与扩展」仍写着:
流式输出:若后续支持 SSE,需在 agent_service 中改为 yield……
但公开代码里 SSE 已经落地:chat_stream 会推 start → inference_step → chunk → end(失败时还有 error)。这是典型的「设计文档落后于实现」。写文章或二次开发时,以 agent_service.py + sessions 路由为准,不要被第 9 节误导成「流式还没做」。
四、深挖 factory:DeepAgent 是怎么装起来的
公开文件 backend/app/agent/factory.py 是整仓最值得读的一块。
4.1 模型选择
默认走 DeepSeek(ChatDeepSeek,deepseek-chat);其它模型名尝试 init_chat_model,再 fallback 到 ChatOpenAI。温度、max_tokens 来自 config.settings。
4.2 Tools + Skills 双通道注入
tools = skill_registry.get_tools()
skill_paths = _get_skill_paths_for_agent()
create_kw = dict(
model=model,
tools=tools,
store=_store,
backend=_make_backend,
checkpointer=_checkpointer,
)
if skill_paths:
create_kw["skills"] = ["/skills/"]
_agent_graph = create_deep_agent(**create_kw)
要点:
- Tools:来自注册表(磁盘
skill.yaml同步到 DB 后,按启用状态转成 LangChain Tool)。 - Skills:扫描
skills_root下含SKILL.md的子目录;只要存在至少一个,就传skills=["/skills/"]——这是后端虚拟根路径,不是宿主机绝对路径。 - store / checkpointer:
InMemoryStore+MemorySaver,进程内有效;重启即丢(长期记忆增强另说)。 - rebuild_agent():技能或配置变更后先
reload_from_db()再重建图,避免「UI 关了技能,Agent 还握着旧工具表」。
4.3 CompositeBackend:四块虚拟盘 + 本地 Shell
_make_backend 组装路由:
| 虚拟路径 | 后端 | 用途 |
|---|---|---|
/memories/ | StoreBackend(runtime) | DeepAgent 内置长期记忆读写 |
/skills/ | FilesystemBackend(skills_root) | 官方 SKILL.md 与资源 |
/uploads/ | FilesystemBackend(data/uploads) | 用户上传文件 |
| 默认 | LocalShellBackend(backend 根目录) | execute 跑本地 shell |
代码注释写得很清楚:LocalShellBackend 仅用于本地开发;生产应换沙箱(如 BaseSandbox)或 HITL 审批。另外给 shell 环境塞了 PYTHONUNBUFFERED=1,避免 Windows 上 Python 脚本 stdout 缓冲导致 execute 抓不到 print。
开发侧还有一个救命细节:run_dev.py 用 uvicorn --reload,并 reload_excludes 掉 skills/**。否则从本机路径添加技能、复制文件时,服务会误重启,聊天中途直接断。README 也专门提醒了 PowerShell 展开通配符的坑。
五、深挖 agent_service:invoke、SSE、Reasoner、上传路径
5.1 非流式 chat()
流程:读历史 → 转 LangChain 消息 → 写 user → agent.invoke → 抽推理步骤 → 写 assistant。thread_id 不是裸 session_id,而是:
def _thread_id(session_id: int) -> str:
model_key = (settings.default_model or "").strip().lower()
return f"{session_id}:{model_key}"
原因写在注释里:切到 deepseek-reasoner 时,旧线程状态里可能缺 reasoning_content,DeepSeek 会 400。按「会话 + 模型」隔离 checkpointer,避免状态污染。这是读源码才能看到的工程判断,不是 README 口号。
5.2 流式 chat_stream() 事件协议
对前端来说,协议比实现细节更重要:
| event | 含义 |
|---|---|
start | 开始,可显示「正在思考」 |
inference_step | 思考 / 工具调用 / 工具结果(去重后推送) |
chunk | 最终答案增量;tool_content 区分工具回显与最终结果 |
end | 落库完成,带 message_id、inference_steps |
error | 异常信息 |
实现上走 agent.stream(..., stream_mode="messages", subgraphs=True),兼容带 namespace 与不带 subgraphs 的两种事件元组形态。有 reasoning 或 tool_calls 的 content 进推理框,不抢最终答案 chunk;干净的 content 才按 delta 推 chunk。
5.3 DeepSeek-Reasoner
- 历史 assistant:若当前是 reasoner,从
metadata_.reasoning_content注入AIMessage.additional_kwargs。 - 本轮回复:
content入库为最终回答;reasoner 把reasoning_content写入 message metadata,供下一轮合规。
这不是「展示好看」的装饰,而是 API 契约:多轮 + 工具调用时,缺少 reasoning 字段会直接报错。
5.4 文件上传如何喂给 Agent
_augment_message_with_files 把「文件名 → 绝对物理路径」拼进 user_message,并附使用说明:纯文本用 read_file + 绝对路径;PDF/Word 优先走技能;execute 也用绝对路径,不要用虚拟路径。流式与非流式分别走 chat_stream_with_files / chat_with_files,底层复用同一套注入逻辑。
上传落盘路径形如 data/uploads/session_{id}/...,同时 CompositeBackend 把 uploads 挂到 /uploads/,两边都能摸到文件。
六、双轨 Skills:电钻和说明书
flowchart LR
subgraph Disk[backend/skills 目录]
Y[skill.yaml 子目录]
M[SKILL.md 子目录]
end
Y -->|sync + registry| T[tools 列表]
M -->|扫描存在则启用| S["skills=['/skills/']"]
T --> DA[create_deep_agent]
S --> DA
DA --> Out[可调用函数 + 渐进式说明书]
| Tools(本项目) | Skills(官方 SKILL.md) | |
|---|---|---|
| 清单 | skill.yaml / yml / json | SKILL.md(YAML frontmatter + 正文) |
| 作用 | 注册可调用函数 | 提供说明与步骤,按需读取 |
| 注入 | tools=[...] | skills=["/skills/"] |
| 示例 | 可按规范自建 python/http 工具 | 仓库已带 backend/skills/pdf/、doc/ |
同一 backend/skills/ 下两者可共存、同时生效。官方 Skills 遵循 Agent Skills 规范:启动只读 frontmatter,任务匹配后再加载全文(渐进式披露)。docs/SKILLS.md 还说明了从本地子目录或 GitHub skills/* 导入、以及启用/禁用走 DB 的流程。
七、状态表:已实现 / 半成品 / 未来
这一节是全文的「防踩坑地图」。只写公开文件能证实的事。
当前已实现
| 能力 | 证据 |
|---|---|
| Vue3 聊天 / 技能 / 模型三页 | frontend/src/router → ChatView、SkillsView、ModelsView |
| sessions / skills / models / agent_config API | backend/app/routers/ |
| lifespan:init_db → sync_skills → reload → get_agent | main.py |
| factory:tools + skills + CompositeBackend + checkpointer | factory.py |
chat invoke + chat_stream SSE(start/inference_step/chunk/end) | agent_service.py、sessions.py |
| DeepSeek-Reasoner 的 reasoning_content 历史注入与落库 | agent_service.py |
| thread_id 按 session + model 隔离 | _thread_id |
| 上传文件路径注入 + upload/stream | chat_*_with_files、sessions 路由 |
| 双轨 skills;pdf/doc 示例目录 | backend/skills/、docs/SKILLS.md |
| Docker Compose;开发 reload 排除 skills | docker-compose.yml、run_dev.py |
还在考虑或半成品
| 项 | 现状(公开代码) |
|---|---|
memory.py | 几乎全是注释:说明长期记忆目前靠 DeepAgent 内置 /memories/ + StoreBackend;模块本身预留给「以后从 DB/向量库检索再注入」 |
context.py | 仅 get_session_context_dir(session_id) 辅助函数,维护 session_{id} 目录路径 |
| 设计文档第 9 节 SSE | 文档仍写「若后续支持」;代码已实现——文档滞后 |
| 向量库记忆 | 设计文档 4.4 写明第一版可用 SQLite+文本或简单 embedding,独立向量库为后续选项;未看到完整向量检索实现 |
| Store / Checkpointer | 当前为进程内 InMemoryStore / MemorySaver,不是跨进程持久记忆方案 |
未来计划(设计提及,勿当成已做)
- 多用户鉴权,Session/Message 增加
user_id隔离(TECHNICAL_DESIGN §9)。 - 生产环境用沙箱替代
LocalShellBackend(factory 注释 + 设计安全边界)。 - 更丰富的长期记忆:从 DB / 向量库检索片段并注入上下文(
memory.py注释与设计 4.4)。
八、本地复现(无真实密钥)
Docker Compose(推荐)
git clone https://github.com/liuyanqun0815/langchain-deep-agent.git
cd langchain-deep-agent
cp .env.example .env
# 编辑 .env,填入你自己的 DEEPSEEK_API_KEY 等;不要提交真实密钥
docker-compose up -d
docker-compose logs -f
按 README:前端 http://localhost ,后端 API http://localhost:8000 。
本地开发
# 后端
cd backend
pip install -r requirements.txt
python run_dev.py # 已排除 skills/ 热重载
# 前端
cd frontend
npm install
npm run dev # /api 代理到 127.0.0.1:8000
冒烟
curl http://127.0.0.1:8000/api/health
# 预期 {"status":"ok"}
常见问题:模型 Key / 默认模型名不对;加技能后服务狂重启(没用 run_dev.py);SKILL.md 不在子目录根下导致 skills 通道未启用;PowerShell 展开 skills/* 通配符。
环境变量示例(占位符):
DEEPSEEK_API_KEY=sk-xxxxxxxx
LANGCHAIN_DEBUG=0
# LANGSMITH_API_KEY=...
# DATABASE_URL=sqlite:///./data/agent_app.db
九、读完源码后的判断
这仓的价值不在「又包了一层 Chat API」,而在三件事叠在一起:
- 产品壳完整:会话、技能开关、模型配置都有 API 和 Vue 页,能给别人点。
- Agent 装配诚实:factory 把 tools、SKILL.md、虚拟文件系统、shell、store、checkpointer 一次接清;Reasoner 与 thread 隔离这种坑也处理了。
- 边界写得清楚:Shell 仅限本地、记忆模块先留壳、设计里的鉴权/向量库还没当做成品卖——读代码时不会被 README 吹过头。
不足也很实在:长期记忆仍是 DeepAgent 文件系统路径为主,进程内 Store;SSE 文档未同步;上生产前必须换掉 LocalShell,并补鉴权与隔离。
若你也在把 DeepAgent 接到办公助手或内部工作台,直接看源码比看概念文有效:
(插图为配套示意图;发布掘金时可与 images/ 下 cover、architecture、request-flow、skills-dual 一并上传。Mermaid 在支持该语法的编辑器中可直接渲染。)