本文只依据该仓库已公开的 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:
<span>@asynccontextmanager</span>
<span>async</span> <span>def</span> <span>lifespan</span>(<span>app: FastAPI</span>):
init_db()
settings.skills_root_dir.mkdir(parents=<span>True</span>, exist_ok=<span>True</span>)
sync_skills_from_disk()
skill_registry.reload_from_db()
get_agent()
<span>yield</span>
顺序含义很直白:先保证表存在 → 磁盘 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 = <span>dict</span>(
model=model,
tools=tools,
store=_store,
backend=_make_backend,
checkpointer=_checkpointer,
)
<span>if</span> skill_paths:
create_kw[<span>"skills"</span>] = [<span>"/skills/"</span>]
_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,而是:
<span>def</span> <span>_thread_id</span>(<span>session_id: <span>int</span></span>) -> <span>str</span>:
model_key = (settings.default_model <span>or</span> <span>""</span>).strip().lower()
<span>return</span> <span>f"<span>{session_id}</span>:<span>{model_key}</span>"</span>
原因写在注释里:切到 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 <span>clone</span> https://github.com/liuyanqun0815/langchain-deep-agent.git
<span>cd</span> langchain-deep-agent
<span>cp</span> .env.example .<span>env</span>
<span># 编辑 .env,填入你自己的 DEEPSEEK_API_KEY 等;不要提交真实密钥</span>
docker-compose up -d
docker-compose logs -f
按 README:前端 http://localhost ,后端 API http://localhost:8000 。
本地开发
<span># 后端</span>
<span>cd</span> backend
pip install -r requirements.txt
python run_dev.py <span># 已排除 skills/ 热重载</span>
<span># 前端</span>
<span>cd</span> frontend
npm install
npm run dev <span># /api 代理到 127.0.0.1:8000</span>
冒烟
curl http://127.0.0.1:8000/api/health
<span># 预期 {"status":"ok"}</span>
常见问题:模型 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 在支持该语法的编辑器中可直接渲染。)
对想落地 Agent 工作台的团队,本文提供了 SSE 事件协议、技能双轨和 Reasoner 兼容等实战细节,可快速避坑;但长期记忆尚未持久化,生产使用需自行增强。