基于 LangGraph + FastAPI + ChromaDB + SqliteSaver + Rerank 的多用户学习助手,支持用户画像持久化、意图分类、知识点问答、学习路径推荐、长对话摘要压缩。
| 能力 | 实现方式 | 示例 |
|---|---|---|
| 知识点问答 | RAG 检索增强生成(BGE Embedding + ChromaDB + BGE-Reranker 精排 + Query 改写) | "LangGraph 是什么" → 改写检索语句 → 检索 Top-10 → Rerank Top-3 → 整理回复 |
| 学习资源推荐 | LLM 读取用户画像,推荐下一步学习路线 | "推荐一些学习资源" → 基于画像推荐 |
| 学习进度更新 | LLM 从对话中提取进度,写入用户画像 | "Python 学完了" → 更新 topics["Python"]="已掌握" |
| 闲聊兜底 | 意图分类无法匹配时走 chat 路径 | "你好" → 友好的学习助手对话 |
| 更新后串联推荐 | 先更新画像 → 再基于新画像推荐下一步 | "Python 学完了,下一步学什么" → 提取进度 → 推荐 |
| 长对话摘要压缩 | 滑动窗口 + 增量摘要(Summary Node) | 超 10 条消息自动触发,旧对话压缩为摘要 |
POST /chat
{"user_id": "同学01", "message": "LangGraph是什么"}
│
▼
┌───────────────────────┐
│ main.py │
│ FastAPI + Pydantic │
│ ChatRequest 校验 │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ graph.py │
│ LangGraph 图编排 │
│ StateGraph + │
│ SqliteSaver 持久化 │
└───────────┬───────────┘
│
START → init_profile → classify_intent
│
┌─────────┬───────────────┼───────────────┬─────────┐
▼ ▼ ▼ ▼ ▼
recommend search update chat (fallback)
│ │ │ │
│ │ ▼ │
│ │ recommend │
│ │ │ │
└─────────┴───────────────┴───────────────┘
│
▼
respond ──(msg > 10?)── summarize
│ │
▼ ▼
END END
│
▼
{"reply": "LangGraph 是 LangChain 推出的图引擎框架..."}
四个意图通过 LLM 意图分类器路由,update → recommend 串联实现"先记进度再推荐"。 超过 10 条消息自动触发 summarize 节点,旧消息压缩为摘要,最近 6 条保留原文。
用户口语提问
│
▼
Query 改写(LLM:口语 → 正式检索语句)
│
▼
BGE Embedding 向量化 → ChromaDB 检索(Top-10)
│
▼
BGE-Reranker 精排(Top-10 → Top-3)
│
▼
Prompt 组装 → DeepSeek 生成
知识库位于 docs/,包含 6 份 Agent 开发核心文档:
- LangGraph 入门
- MCP 协议详解
- Agent 架构模式(FC / ReAct / Plan-Execute / Multi-Agent)
- RAG 全链路
- Python 异步编程
- Prompt 工程
首次 import knowledge_base.py 时自动构建,之后持久化在 chroma_db/,不再重复入库。
| 组件 | 技术 | 用途 |
|---|---|---|
| Web 框架 | FastAPI + Uvicorn | HTTP 服务 + 前端页面 |
| 图引擎 | LangGraph (StateGraph + SqliteSaver) | 多节点编排 + 多用户状态持久化 |
| 大模型 | DeepSeek Chat API | 意图分类 + 资源推荐 + 答案生成 + 闲聊 + 摘要压缩 |
| 向量数据库 | ChromaDB (PersistentClient) | Agent 知识库存储与检索 |
| Embedding | 硅基流动 BAAI/bge-large-zh-v1.5 | 文本向量化 |
| 精排 | 硅基流动 BAAI/bge-reranker-v2-m3 | 检索结果重排序(Top-10 → Top-3) |
| 前端 | 原生 HTML/CSS/JS(终端霓虹风) | 可视化聊天界面 |
study_assistant/
├── main.py # FastAPI 入口,GET /(前端页面)+ POST /chat(API)+ GET /debug/{user_id}
├── graph.py # LangGraph 图编排:7 节点 + SqliteSaver + 条件边(>10 条路由到 summarize)
├── state.py # AssistantState 类型定义(TypedDict + Annotated,含 summary 字段)
├── tools.py # 节点函数:classify_intent / recommend / search / update / chat / summarize_conversation
├── knowledge_base.py # 自建 RAG:BGE 向量化 + ChromaDB + BGE-Reranker 精排
├── env_loader.py # 环境变量加载 + OpenAI client 单例
├── requirements.txt # Python 依赖列表
├── .gitignore # Git 忽略规则
├── docs/ # Agent 开发知识库文档(6 份 .txt)
├── chroma_db/ # ChromaDB 向量库持久化数据(gitignore)
├── checkpoints.sqlite # SqliteSaver checkpoint 数据库(gitignore)
└── static/
└── index.html # 前端页面(终端霓虹风聊天界面)
# 1. 安装依赖
pip install -r requirements.txt
# 2. 配置 .env(放在 study_assistant/ 目录下或上级目录均可)
# DEEPSEEK_API_KEY=your_key
# SILICONFLOW_API_KEY=your_key
# 3. 启动服务
cd study_assistant
uvicorn main:app --reload --port 8001
# 4. 打开浏览器
http://127.0.0.1:8001
# 5. 或通过 API 测试
curl -X POST http://127.0.0.1:8001/chat \
-H "Content-Type: application/json" \
-d '{"user_id": "同学01", "message": "LangGraph是什么"}'启动后访问 http://127.0.0.1:8001/docs 查看 Swagger 文档。
返回前端聊天页面。
请求:
{
"user_id": "同学01",
"message": "LangGraph是什么"
}响应:
{
"reply": "LangGraph 是 LangChain 推出的图引擎框架,专门用于构建有状态的、多步骤的 AI Agent 工作流..."
}user_id决定 SqliteSaver 的thread_id,不同用户画像隔离,重启不丢失- 多次调用同一
user_id,画像自动累积 - 对话超 10 条消息自动触发摘要压缩
调试接口,查看用户当前状态(消息数、画像、摘要内容)。
| 坑 | 原因 | 修复 |
|---|---|---|
目录名 study-assistant 含连字符 |
Python 模块名不能含 -,import 报 SyntaxError |
重命名为 study_assistant |
auto_tool_call.py import 触发天气助手交互循环 |
原文件缺 if __name__ == "__main__" 保护,顶层 while 循环直接跑 |
加 guard 包裹 |
rag_pipeline.py load_env 路径写死 |
"../.env" 基于 CWD 而非文件所在目录 |
改用 __file__ + os.path.dirname 动态计算 |
| RAG 路径返回 500 | ask_rag() 返回 (answer, sources) 元组,AIMessage 要求纯 str |
创建独立 knowledge_base.py,ask() 只返回字符串 |
| MemorySaver 首次调用无画像 | 首次 invoke 时 user_profile 为空,下游节点读取报错 |
新增 init_profile 节点:首次设 DEFAULT_PROFILE,后续返回 {} 不覆盖 |
recommend 回复生硬 |
只能推荐不能更新画像;update_profile 只能更新不能推荐 |
图串联 update → recommend → respond,两个节点各司其职 |
| 知识库回答"根据现有资料无法确定" | 旧知识库是公司文档(产品手册/离职制度),不含 Agent 知识 | 新建 6 份 Agent 开发文档,切换为独立知识库 |
| SiliconFlow API Key 未加载 | knowledge_base.py 只检查了 DEEPSEEK_KEY 就跳过 .env 加载 |
条件改为 if not API_KEY or not SILICON_KEY |
| 前端界面全英文 | 初始版本从项目1模板迁移 | 全部中文化:提示语、按钮、错误信息、默认用户ID |
| 坑 | 原因 | 修复 |
|---|---|---|
| MemorySaver 重启丢失对话 | MemorySaver 纯内存,服务重启后多轮对话历史全部消失 | 迁移到 SqliteSaver,手动 sqlite3.connect(check_same_thread=False) + SqliteSaver(conn),避免 from_conn_string 的 contextmanager 陷阱 |
| chat 只取 messages[-1] | 之前认为"同一会话的上下文在 messages 里,LLM 知道历史",实际 LLM 无状态,每次调用只看到传入的消息 | 改为传入 summary + 最近 6 条消息,summary 承载压缩后的长期记忆 |
OpenAI 422 错误:role human 不合法 |
LangChain HumanMessage.type 为 "human",但 OpenAI API 只接受 system/user/assistant/tool | 在 chat() 构建 messages 时显式映射 role = "user" if m.type == "human" else "assistant" |
from_conn_string 不能模块级用 |
from_conn_string 是 @contextmanager,with 退出后自动 closing(conn),模块级持久化不可用 |
手动 sqlite3.connect() + SqliteSaver(conn),连接生命周期由模块控制 |
为什么用 LangGraph 而非手写 Agent 循环? 本项目有 5 个节点 + 条件分支 + 节点串联(update→recommend),手写 while+if 会变成意大利面。LangGraph 声明式定义图结构,新增节点只需加 node + edge,不碰已有逻辑。
为什么用 SqliteSaver 而非 MemorySaver? MemorySaver 零配置适合 demo,但纯内存导致服务重启后所有对话历史和用户画像丢失。SqliteSaver 数据落盘到 checkpoints.sqlite,重启后通过 thread_id 恢复状态,实现生产级持久化。手动管理 sqlite3 连接生命周期(而非 from_conn_string 的 contextmanager),保证模块级单例可用。
为什么引入 Summary Node? 长对话场景下全量传 messages 有两个极端:只取 messages[-1] 丢失短期上下文("金鱼记忆"),全量传导致 token 随对话线性增长最终超限。Summary Node 采用滑动窗口 + 增量摘要策略:超过 10 条消息触发 LLM 压缩,旧消息压为 ≤200 字摘要,最近 6 条保留原文。增量合并(旧摘要 + 新对话 → 新摘要)保证每次压缩成本恒定。
为什么 init_profile 返回 {} 而非覆盖已有画像?
SqliteSaver 会在每次 invoke 后持久化 State。init_profile 只在首次调用时设默认值;后续调用时 SqliteSaver 已恢复画像,返回 {} 表示"不动这个字段"。如果每次都返回 DEFAULT_PROFILE,会把用户积累的画像覆盖掉。
为什么 respond 节点单独存在?
所有业务节点(recommend/search/update/chat)只负责写 state["reply"] 字符串,不碰 messages。respond 节点作为统一出口,将 reply 转为 AIMessage 写入对话历史。好处是新增节点只需关心回复内容,不用重复 AIMessage 转换逻辑,且多轮对话历史由 Annotated[list, add_messages] 自动累加。
为什么 update 后自动串联 recommend?
用户说"学完了,下一步学什么"包含两层意图:记录进度 + 请求推荐。图编排 update → recommend 让两个节点各司其职——update_profile 提取进度写入画像,recommend_resources 基于最新画像推荐下一步。函数职责单一,图负责编排。
为什么自建 knowledge_base.py 而非复用 rag_lab?
rag_lab 的知识库是公司文档,不适合学习助手场景。且 ask_rag() 返回 (answer, sources) 元组,与本项目 AIMessage 需要的纯字符串不兼容。自建模块解耦两个项目,各自独立演进。
为什么意图分类单独一个 LLM 调用? 五种意图(recommend/ask/update/chat)语义差异大,用关键词匹配太脆弱。LLM 分类器(temperature=0)准确率接近 100%,且新增意图只需改 prompt 不用改代码。