一个面向个性化学习的智能助手:循环 ReAct agent(RAG 问答 + 自主规划)+ 系统级学习闭环(掌握度追踪 + SM-2 间隔重复),并配套完整的评估管线与工程化脚手架(并发/重试/限流/可观测)。
后端 FastAPI + LangGraph + Chroma;前端 Vue3 + Pinia + Vite;大模型与向量/重排使用阿里云 DashScope(通义千问)。
| 模块 | 说明 |
|---|---|
| 循环 ReAct agent | LangGraph agent↔tools 循环图 + should_continue 条件路由 + 原生函数调用(bind_tools)+ recursion_limit 优雅收尾(已流出的末轮内容不丢、通常为空时给明确兜底文案,收尾照常持久化) |
| RAG 对话 | BM25+向量混合检索(RRF 融合)+ cross-encoder 重排 + 查询改写(多查询/HyDE)+ 回答引用溯源;分层贡献经五臂消融量化(见 eval) |
| 工具错误隔离 | 工具失败 / 模型幻觉出不存在的工具名,都回 error ToolMessage 让 agent 观察后自纠,而非击垮整轮 |
| 真流式 | LangGraph astream 逐 token 推送 SSE,支持中止;客户端断开时部分回答仍持久化(记忆不断档) |
| 对话记忆 | 会话级滑动窗口 + 超阈值自动摘要(失败推进水位防级联),持久化 |
| 自适应学习计划 | 结构化 JSON 计划;对话中可让 AI 自主调用规划(react→plan 工具);更新进度时漂移/过期门控条件重规划(在轨不 churn);端点与工具共用同一编排(无漂移) |
| 系统级学习闭环 | 计划 → 出题 → 自动批改 → 掌握度更新 → SM-2 间隔重复 → 重规划。对话 agent 可自主调工具参与其中:检索 / 规划 / 出题 / 批改 / 复习提醒 / 掌握度查询(闭环也经 REST 端点编排) |
| 可评估 | golden 集 + LLM-as-judge 双跑双判 + 工具路由行为用例 + 多轮指代用例 + 五臂检索消融 + 两臂成本(token/调用/延迟),可回归 |
前端(Vue3 + Pinia + Vite)
ChatWindow(真流式打字机 + 引用 chips)· UploadPanel · PlanView(计划/掌握度/测验)
│
│ fetch + SSE(text/event-stream,可中止)
▼
后端(FastAPI)
对话编排 chat_service
load_memory(窗口 + 摘要)→ build_agent → graph.astream → SSE
Agent = LangGraph 循环 ReAct(agent ↔ tools,原生函数调用)
agent 节点 ──tool_calls──▶ tools
▲ · knowledge_search(BM25+向量+RRF+重排)
└────观察回灌──── · make_learning_plan / get_due_reviews
· get_mastery / make_quiz / grade_quiz
should_continue:无 tool_calls → END;工具失败/幻觉工具名 → error 观察 → 自纠
recursion_limit 优雅收尾(兜底文案 + 持久化)
学习闭环(REST 编排):/plan → /quiz → /quiz/grade → 掌握度 → SM-2 → /update_progress
评估 eval:golden + LLM-as-judge 双跑双判(baseline 裸 RAG vs 真实 agent)
持久化:SQLite(SQLAlchemy async)· 配置:pydantic-settings
│
▼
DashScope(Qwen Chat + Embeddings + Rerank)
注:检索的「改写 / BM25+向量 / RRF / 重排」全部发生在
knowledge_search工具内部,不是图里的独立节点;记忆加载与流式推送由chat_service编排,不在 agent 图内。agent 共 6 个工具:knowledge_search/make_learning_plan/get_due_reviews/get_mastery/make_quiz/grade_quiz(后四个让 agent 在对话里自主驱动学习闭环)。
| 层 | 实现 | 代码 |
|---|---|---|
| 循环与兜底 | agent↔tools 循环 + should_continue 条件路由 + recursion_limit 触发时优雅收尾并持久化(如末轮已有累积内容则连同说明收尾;真实拓扑下通常为空,走明确兜底文案) |
agents/react_agent.py · services/chat_service.py |
| 工具错误隔离 | 工具失败 / 幻觉工具名(查表未命中)都回 error ToolMessage,agent 观察→再决策 |
react_agent.py tool_node |
| 流式 | graph.astream(stream_mode="messages") → SSE;客户端断开时把问题+部分回答落库(记忆不断档) |
services/chat_service.py |
| 记忆 | 会话级滑动窗口 + 超阈值 LLM 摘要(带超时与水位推进),持久化(带多轮回归测试) | memory/conversation_memory.py |
| 可观测 | RequestID 结构化日志;EvalTracer(token/LLM 调用/工具调用/延迟)经 SSE meta 透出 |
core/middleware.py · eval/tracer.py |
| 韧性 | 统一 LLM 调用层:并发闸(同步+异步双 Semaphore)+ 按错误分类的退避重试(429/5xx 重试、401/400 不重试)+ 单调用超时,覆盖 agent 主路径/结构化输出/摘要/裁判/漂移判分全部调用点(查询改写亦有超时,失败降级为原 query 检索);工具调用超时与内部各段预算的算术自洽(外层 ≥ 改写+抽取+生成之和) | models/llm.py · react_agent.py |
| 护栏 | 身份网关(X-API-Key → user_id 映射,不信任请求体)+ 令牌桶限流(前置到鉴权之前,401 爆破同样限速)+ 上传安全(分块读+预检)+ 检索片段「不可信资料」前置框定 + 统一异常 | core/security.py · core/rate_limit.py · core/errors.py |
| 可评估 | 双跑双判(baseline vs 真实 agent)+ LLM-as-judge + 工具路由行为用例 + 多轮指代用例 + 检索消融 + 两臂成本追踪 | eval/run.py · eval/golden.py |
backend/
app/
main.py # FastAPI 入口(路由/中间件/异常)
core/ # settings / logging / middleware / errors / security / rate_limit / structured
models/ # LLM 单例与统一调用层 / Embedding 单例
schemas/ # Pydantic 请求/响应与结构化输出模型
db/ # SQLAlchemy async 模型与会话
repositories/ # 仓库模式(替换内存 dict)
memory/ # 对话记忆(窗口+摘要)
rag/ # document_loader / vector_store / hybrid_retriever / reranker / query_transform
agents/ # react_agent(LangGraph 循环 ReAct)
learning/ # srs(SM-2 间隔重复 + 统一 UTC 时间基准)
services/ # 业务编排(chat / learning / planner 规划服务)
eval/ # golden 集 + LLM-judge 评估管线 + 消融 + tracer
eval_corpus/ # 评测语料(虚构简历,见 eval 段)
data/ # uploads / vector_db / app.db(运行时,不入库)
frontend/
src/ api.js · App.vue · stores/ · components/
cd backend
cp .env.example .env
# 编辑 .env 填入你的 DASHSCOPE_API_KEY(阿里云 DashScope 控制台获取)cd backend
python -m venv venv && venv\Scripts\activate # Windows
pip install -r requirements.txt # 或 requirements-dev.txt(含测试)
uvicorn app.main:app --reload
# 访问 http://127.0.0.1:8000/docscd frontend
npm install
npm run dev # http://localhost:3000| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /health |
健康检查 |
| POST | /upload |
上传文档建库(uuid 重命名+白名单+分块读大小预检) |
| GET | /documents |
列出知识库文档 |
| POST | /chat |
流式 RAG 对话(SSE:session / token / citations / meta / done / error) |
| POST | /plan |
生成结构化学习计划(与 make_learning_plan 工具共用编排) |
| POST | /update_progress |
更新进度并自适应重规划(漂移/过期门控) |
| POST | /quiz |
出题并保存为服务端「活动测验」,响应不含答案 |
| POST | /quiz/grade |
批改:请求只带作答,题取服务端(答案不可偷看/伪造);一次性消费——成功后活动测验删除,重复/并发提交只有一个生效(404 no_active_quiz),防掌握度/SM-2 重放污染 |
| GET | /mastery |
学习看板(掌握度/到期复习/薄弱知识点) |
| GET | /reviews |
今日到期复习队列 |
错误统一返回 {code, message, request_id}(含 X-Request-ID 头),不泄露堆栈。
pytest # 单测 194 例(分片/RRF/掌握度/SM-2/校验/消毒/记忆/限流/漂移门控/工具隔离/信任边界/失败路径/批改消费…)
python -m app.eval.run # RAG 评估(双跑双判 + 行为/多轮用例,需先建库,见 eval 段)
python -m app.eval.run --ablation # 五臂检索消融(hit@k + 空结果计数 + 延迟 + LLM 调用成本)- 密钥仅存
.env(已 gitignore),源码零硬编码 - 上传:uuid 重命名防路径穿越 + 扩展名白名单 + Content-Length 预检 + 分块读上限(超限 413 早于全量读内存)
- 身份网关(调用方 + 身份合一):
REQUIRE_API_KEY=true时,X-API-Key经API_KEY_USERS映射绑定user_id(不信任请求体,映射即唯一白名单)——数据端点统一current_user_id依赖,防越权访问 agent 的按用户分域工具。dev(默认关闭)回落请求体user_id,便于本地联调。鉴权按项目规模选用 API-key 网关而非 JWT/RBAC,避免半接线的 token 鉴权制造假安全面 - 测验信任边界:标准答案只存服务端(active_quiz),
/quiz响应剔除 answer/explanation、/quiz/grade只收作答——答案不可提前偷看、不可由客户端伪造刷掌握度;批改为一次性消费(原子删除活动测验,重复提交 404) - 生产 fail-closed:
APP_ENV非dev时,启动期断言鉴权映射与 DASHSCOPE_API_KEY 就绪,否则拒绝启动——避免默认部署被打穿或带病上线 - 限流前置:令牌桶挂在鉴权之前且只在 key 为已映射合法 key 时按 key 聚合,其余按 IP——401 爆破路径同样限速,伪造/轮换 X-API-Key 头无法绕过(默认内存桶,
RATE_LIMIT_*可调;超限 429) - 检索注入框定:检索片段以「不可信资料」前置声明传入模型(明确资料内指令性内容一律视为数据),配轻量注入启发式,降低间接提示注入面
- CORS 收敛为白名单;全局异常处理不向客户端泄露内部堆栈
- 依赖 CVE 审计进 CI(pip-audit,失败即阻塞);唯一豁免项留档:chromadb PYSEC-2026-311(自托管 chroma server 未认证端点的 RCE,上游暂无修复版)——本项目默认嵌入式 PersistentClient 不暴露该面,server 模式为可选内网部署
横向扩展能力落地为可插拔后端:默认走本地零依赖后端(开箱即跑),配置对应环境变量即切换分布式后端,支持 uvicorn --workers N / 多副本。每个有状态组件都有明确的单进程→分布式切换点:
| 组件 | 默认(零依赖,单进程) | 横向扩展(配 env 启用) |
|---|---|---|
| 令牌桶限流 | 进程内 dict(asyncio.Lock,带桶数上限与淘汰) |
REDIS_URL → Redis Lua 原子令牌桶(register_script 自愈 NoScript,跨 worker/副本一致) |
| 向量库 | Chroma PersistentClient,data/vector_db/{user}/{kb}/ 目录隔离(client 按目标缓存复用) |
CHROMA_URL → Chroma Server HttpClient,按 collection=sha1(user/kb) 隔离(多 worker 共享写安全) |
| BM25 索引缓存 | 进程内 LRU(BM25_CACHE_MAX_SIZE,命中率先注入 /health) |
BM25_CACHE_TTL_SEC>0 → TTL 过期重建,多 worker 在窗口内最终一致(零依赖,无需广播) |
| 元数据库 | SQLite + WAL + busy_timeout(单写者) | SQLITE_URL=postgresql+asyncpg://... → Postgres(多 worker 并发写安全) |
| LLM 并发足迹 | 不限 | LLM_CONCURRENCY>0 → 同步/异步双 Semaphore 限 in-flight,防 DashScope 429 雪崩 |
横向扩展部署示例(多 worker):
REDIS_URL=redis://redis:6379/0 \
CHROMA_URL=http://chroma:8000 \
SQLITE_URL=postgresql+asyncpg://user:pass@postgres:5432/learning \
LLM_CONCURRENCY=8 \
uvicorn app.main:app --workers 4可选依赖(仅启用对应后端时安装):redis(REDIS_URL 限流)、asyncpg(Postgres)。Chroma Server 用 chromadb 自带 HttpClient,无需额外依赖。
边界说明:分布式后端(Redis / Chroma Server / Postgres 适配器)已实现并由单测覆盖契约(后端路由 + Lua + TTL + driver 检测 + Semaphore);多 worker 端到端实测需对应中间件环境。默认单进程开箱即跑,横向扩展能力配 env 即生效。
完整部署步骤(docker-compose + 数据迁移须知 + 分布式限流验证)见
docs/scaling.md。
- 工程地基:配置外置(pydantic-settings)· 中间件 / 统一异常 / 结构化日志 / 健康检查
- 对话主干:循环 ReAct agent · 真流式 SSE · SQLite 持久化 · 会话记忆(窗口 + 摘要)
- 顶配召回:BM25+向量混合(RRF)+ 重排 + 查询改写,回答带来源引用;评估管线就位
- 学习闭环:计划 / 出题 / 批改(一次性消费)/ 掌握度 / SM-2 / 门控重规划 + 看板
- 前端工作台:三栏布局 · 流式打字机 · 明暗双主题 · 键盘可访问性
- 测试与文档:离线单测 + HTTP 集成测 + 端到端验证
- 加固轮:工具错误隔离(含幻觉工具名)· 失败路径闭环 · 限流与批改的信任模型 · eval 方法论(有效 N / 配对 Δ / 五臂消融 / 多轮用例)
- 循环 ReAct agent:
ChatTongyi.bind_tools+ LangGraphagent↔tools循环图(should_continue条件路由 +tools→agent回边)+recursion_limit兜底;循环结构支持多步推理轨迹,行为证据见行为用例与多轮用例。 - 规划即工具:tutor agent 自主调用规划工具
make_learning_plan;planner为结构化输出服务,带「漂移/过期门控」(在轨不重规划);端点与工具共用generate_grounded_plan单一编排,两个入口行为一致。 - 工具错误隔离(含幻觉工具名):工具失败与模型幻觉出不存在的工具名都回 error
ToolMessage,agent 观察→再决策(换查询/换工具/致歉),两类失败的自愈均有离线测试覆盖。 - 失败路径工程:recursion_limit 触发优雅收尾并持久化(末轮已有内容连同说明收尾,通常为空时给明确兜底文案);客户端断开把问题+半截回答落库(记忆不断档);统一 LLM 调用层把并发闸/分类重试/超时覆盖到全部调用点(agent 主路径/结构化输出/摘要/裁判/漂移判分),各层失败路径均有实现与测试覆盖。
- agent 驱动学习闭环:除检索/规划外,agent 还能自主调
get_due_reviews/get_mastery/make_quiz/grade_quiz——在对话里观察掌握度、提醒到期复习、出题、批改并写回 SM-2 排期。 - 真 token 流式:
graph.astream(stream_mode="messages")→ SSE,前端fetch + ReadableStream增量渲染打字机,可中止;同 chunk 双载荷(过渡语+工具调用)也能正确分桶。 - 顶配召回(消融量化):BM25(jieba)+向量 → RRF 融合 → DashScope
gte-rerank-v2重排 → 多查询+HyDE 改写,回答带来源引用;分层贡献由五臂消融量化(hit@k + 空结果计数 + 延迟 + LLM 成本,python -m app.eval.run --ablation可复跑)。 - 结构化输出容错:
with_structured_output对大 schema 会因函数调用 JSON 偶发损坏(系统性 0/4 失败),改用自研invoke_structured(JSON 提示 +json_repair+ Pydantic 校验 + 把校验错误回灌 prompt 的 self-refine 重试)解决。 - 评估管线(双跑双判 + 行为/多轮/消融):golden 集 + LLM-as-judge,每题同时跑 baseline 与真实 agent;两臂同温度、各臂对自己实际使用的 context 判分、拒答感知判分(正确拒答≠幻觉)、裁判失败剔除不混 0 分、有效 N<2 不判显著;行为用例断言工具路由,多轮用例断言指代接力。
- 系统级学习闭环:结构化计划 → 出题 → 自动批改(客观精确匹配/数值等价/主观 LLM 判分)→ 掌握度 EMA(首评直接初始化)→ SM-2 间隔重复(半对=勉强通过的显式通过线)→ 自适应重规划;全程 UTC 统一时间基准。
- 工程规范:Settings(.env) · 结构化日志+RequestID · 统一异常(不泄露堆栈) · 上传安全 · SQLite 持久化(CAS 乐观锁 + 并发首评竞态恢复 + 批改一次性消费) · 单测 194 例 · conftest 全局态复位 · 依赖锁定。
每题双跑双判 ×N 跑(EVAL_RUNS,默认 3):baseline(裸 RAG:hybrid_search → 单次生成)对照 agent(/chat 同款 ReAct 循环),LLM-as-judge 打分报 mean±std + 显著性(逐题:|Δ|>两臂 pooled std,按失败剔除后的有效 run 数判定,有效 N<2 标 n/a;聚合级:逐题配对 Δ 检验 |mean Δ|>2×SE(Δ),题间共同方差经配对抵消)+ 两臂成本(token / LLM 调用 / 工具调用 / 延迟)。被测臂终态异常(超时/401)与裁判失败同样剔除计 fail_rate,不击穿整轮、不混 0 分(题层整臂全失败亦按臂剔除);结果逐题增量落盘。
golden 集 4 类:fact(单跳)/ multihop(多跳综合)/ unanswerable(资料外,测幻觉与拒答)/ adversarial(诱导确认伪事实)。语料为 backend/eval_corpus/fictional_resume.md(虚构人物,无真实 PII;unanswerable 题逐题核对过语料确无该信息)。行为用例覆盖全部 6 工具路由(含出题→批改两步链)+ 注入鲁棒;多轮用例验证指代接力与「指代+资料外」的正确拒答;另有五臂检索消融。
评估产物(report.md / report.json / ablation.md)为本地运行时生成,不入库;聚合与显著性逻辑由
tests/test_eval_scoring.py离线回归覆盖。运行前需先构建评测知识库(默认
probe/probe):cd backend && python -c "from app.rag.document_loader import load_document; from app.rag.vector_store import build_vector_store; build_vector_store(load_document('eval_corpus/fictional_resume.md'), user_id='probe', kb_id='probe')"