DeepAgent 实战:SSE 已上线,长期记忆还是半成品

138 阅读8分钟

很多人第一次把 LangChain / DeepAgent 接到自己的产品里,会卡在同一处:模型能聊,但规划、工具、文件、会话管理散落在脚本里,同事没法点、没法管。公开仓库 liuyanqun0815/langchain-deep-agent 做的是另一件事——用 Vue 3 工作台把 DeepAgent 包成可运行工程,并且把「已经写完的」「只留了壳的」「设计里还没做的」分得很开。

本文只依据该仓库已公开的 README、docs/、前后端源码,不含本地私有文件或真实密钥。文中会明确标出:当前已实现、半成品/预留、未来计划;设计文档与代码不一致的地方也会直说。

封面:智能助手对接任务规划、技能、长期记忆与工具


一、要解决什么,以及我们选了什么

痛点

  • 单页调 Chat Completions:没有规划、工具、文件系统的统一壳。
  • 纯脚本跑 Agent:难给非开发同事试用,技能开关也难管。
  • 只堆 Prompt:复杂任务靠「多写几句」撑不住。

选型(仓库现状)

层选择公开证据
前端Vue 3 + Vite + Naive UIfrontend/,路由 ChatView / SkillsView / ModelsView
后端FastAPI + SQLModel + SQLitebackend/main.py、app/db/
AgentLangChain deepagents create_deep_agentbackend/app/agent/factory.py
部署Docker Compose;本地用 run_dev.pydocker-compose.yml、backend/run_dev.py

核心务实点有两个:一是产品化外壳(会话 / 消息 / 技能进 SQLite,界面可点);二是双轨扩展(skill.yaml Tools + SKILL.md Skills 同目录共存,一起喂给 DeepAgent)。


二、整体架构

三层架构示意:Vue 前端 → FastAPI 接口层 → 数据库与技能注册

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)

请求链路:用户发消息 → FastAPI → Agent 服务 → DeepAgent → 写入并回复

仓库里聊天不止「一次性 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)

要点:

  1. Tools:来自注册表(磁盘 skill.yaml 同步到 DB 后,按启用状态转成 LangChain Tool)。
  2. Skills:扫描 skills_root 下含 SKILL.md 的子目录;只要存在至少一个,就传 skills=["/skills/"]——这是后端虚拟根路径,不是宿主机绝对路径。
  3. store / checkpointer:InMemoryStore + MemorySaver,进程内有效;重启即丢(长期记忆增强另说)。
  4. 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:电钻和说明书

工具 Tools 与技能 Skills 双轨汇入同一 DeepAgent

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 / jsonSKILL.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 APIbackend/app/routers/
lifespan:init_db → sync_skills → reload → get_agentmain.py
factory:tools + skills + CompositeBackend + checkpointerfactory.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/streamchat_*_with_files、sessions 路由
双轨 skills;pdf/doc 示例目录backend/skills/、docs/SKILLS.md
Docker Compose;开发 reload 排除 skillsdocker-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」,而在三件事叠在一起:

  1. 产品壳完整:会话、技能开关、模型配置都有 API 和 Vue 页,能给别人点。
  2. Agent 装配诚实:factory 把 tools、SKILL.md、虚拟文件系统、shell、store、checkpointer 一次接清;Reasoner 与 thread 隔离这种坑也处理了。
  3. 边界写得清楚:Shell 仅限本地、记忆模块先留壳、设计里的鉴权/向量库还没当做成品卖——读代码时不会被 README 吹过头。

不足也很实在:长期记忆仍是 DeepAgent 文件系统路径为主,进程内 Store;SSE 文档未同步;上生产前必须换掉 LocalShell,并补鉴权与隔离。

若你也在把 DeepAgent 接到办公助手或内部工作台,直接看源码比看概念文有效:

github.com/liuyanqun08…

(插图为配套示意图;发布掘金时可与 images/ 下 cover、architecture、request-flow、skills-dual 一并上传。Mermaid 在支持该语法的编辑器中可直接渲染。)