面向长篇中文小说(~500 万字)的检索增强问答工具。包含四个部分:多跳检索 Agent(loop)、单轮 RAG 流水线(fast)、前后端 Web 产品(system)、评测体系(evaluate)。
解决的问题很直接:模型单靠训练数据回答长文本里的问题,会记错、会编造;全文又超出上下文窗口。方案是检索增强加多步推理——先检索再回答,复杂问题拆成子问题逐个查证,把确认的结论沉淀成笔记供后续使用。
tech/
├── loop/ 多跳检索 Agent(CLI,核心算法)
├── fast/ 单轮 Top-K RAG 流水线(速度优先)
├── system/ 前后端 Web 产品(复用 loop,生产部署栈)
└── evaluate/ 评测体系(消融实验 + AnonyRAG 基准)
算法层只有两条线:
LLM 工具调用 + 检索工具(BM25 / 向量 / 章节 / 窗口 / 笔记)
├── loop:多跳 Agent —— 规划 → 检索 → 综合,25 轮上限,笔记跨会话记忆
└── fast:单轮 Top-K —— 问题拆分 → 双路召回 → RRF 融合 → 重排 → 受证据生成
本地向量检索:BAAI/bge-base-zh-v1.5(FAISS)+ rank-bm25(jieba)+ bge-rerankerWeb 版(system)在上面叠加了产品层:
浏览器 → nginx(:80) → Django(WSGI 控制面: auth/会话/文件)
→ SSE 网关(FastAPI/uvicorn: Redis Streams 事件流)
→ PostgreSQL / Redis / MinIO / Prometheus
Celery worker(宿主机,需 CUDA embedding)异步执行 loop 多跳问答评测层(evaluate)用消融实验量化 loop 各特性的贡献,另外跑了 AnonyRAG 基准:
消融实验:C0 完整 Loop vs C1 无向量 vs C2 无规划 vs C3 单次检索 vs C4 单轮 RAG
→ 34 题,LLM 事实覆盖判分 → results/report.md 失败案例分析
AnonyRAG:匿名实体推理基准 → 验证检索-推理还原真实身份的能力核心设计是防幻觉加多跳推理:模型必须先检索再回答,多要素问题拆成子问题逐个逼近,每步结论沉淀为笔记,下次提问按语义召回相关笔记注入上下文。
| 特性 | 说明 |
|---|---|
| 强制检索铁律 | System Prompt 明确"即使认为知道答案也必须用工具查证",对抗训练数据幻觉 |
| 规划引导 | 用户问题后注入"先规划再检索",多跳问题第一轮就给出方案而不是乱试 |
| 多步分解 | 多要素问题(人物/时间/事件/次数)先拆子问题逐个检索再综合,工具上限 25 轮 |
| 检索后汇报发现 | 每步检索后用一两句话说明查到什么,中间结论可见,留在上下文供后续推理 |
| 进展即记笔记 | 子问题查清立即 take_notes 沉淀;跨会话按问题向量召回相关笔记(最多 10 条) |
| Debug 状态机 | 3 个暂停点(请求工具 / 工具执行完 / 返回文本),Enter 继续 / R 重试 / Esc 退出,全局开关一键关闭 |
| retry 快照回退 | 暂停点 R 回到本轮开始前快照重演,失败不污染上下文 |
| 审查模块 | 二次独立 LLM 核对回答事实(Cited/Unsupported),当前待用(ENABLE_REVIEW) |
工具集:
| 工具 | 作用 |
|---|---|
bm25_search(keyword, top_k) |
关键词定位(jieba + BM25),返回临时表 |
vector_search(description, top_k) |
语义定位(bge 向量 + FAISS) |
get_chapter(chapter_number) |
取整章全部块 |
window_search(table_id, center, before, after) |
在检索结果表上展开前后窗口 |
take_notes(notes) |
沉淀笔记到 SQLite,跨会话记忆 |
无 agent 循环,速度更快,实现更简。
- 按"第 N 章"切分文本,建 FAISS 向量索引 + BM25 关键词索引(索引可复用)。
- 历史问答拆分问题(支持指代/省略/复合),双路召回后 RRF 融合,bge-reranker 重排。
- 第一轮直接检索并审查,失败才进入 HyDE / 查询改写 / 增强检索。
- LLM 用 DeepSeek 官方
deepseek-v4-flash,Embedding 与 reranker 强制 CUDA。
前后端分离的 Web 版,复用 loop 的多跳问答能力,加用户体系、文件检索与流式交互。提供本地生产部署栈(Docker Compose 一键起全套)。
技术栈:
- 后端:Django 5.2 + DRF + JWT(simplejwt),PostgreSQL + Redis + MinIO(Docker),Celery 异步任务
- SSE 网关:独立 FastAPI 服务(
system/gateway/),从 Redis Streams 按游标推送事件 - 入口:nginx(唯一对外入口
:80),SSE 路由关闭缓冲、调大超时 - 前端:Vite + React 19 + TypeScript,
react-markdown渲染回答
两种交互模式:
| 模式 | 触发条件 | 行为 |
|---|---|---|
| 普通 AI 对话(chat) | 会话未绑定文件 | 无工具、无检索,直接 LLM 对话 |
| 文件检索问答(rag) | 会话绑定文件 | Loop 多跳 + 检索绑定文件的索引回答 |
后端能力:
| 能力 | 说明 |
|---|---|
| 用户认证 | 注册 / 登录 / me / 登出,JWT Bearer token |
| API Key 加密存储 | OpenRouter key 以 AES-GCM 密文存 UserProfile,前端只见掩码,运行时解密、不随请求传明文 |
| 会话与消息 | 多会话,Loop 问答完成后自动写入会话(刷新后可恢复) |
| 会话绑定文件 | ChatSession.files ManyToMany,PATCH /api/sessions/{id} 绑定(仅当前用户已解析完成的文件),create_run 按绑定文件分流 rag/chat 模式 |
| 文件自动建索引 | 上传后 Celery 异步建索引(BM25 + FAISS),进度经 Redis 推送,not_started → indexing → ready/error,每文件隔离索引目录 |
| Loop 问答 API | POST /api/runs 创建任务 → Celery 异步执行 loop 多跳引擎 |
| SSE 流式 | 独立 FastAPI 网关 GET /api/runs/{id}/stream;Redis Streams 事件总线;id: 字段让浏览器断线自动带 Last-Event-ID 续传(不重放) |
前端能力:
| 能力 | 说明 |
|---|---|
| 登录 / 注册 | JWT 持久化到 localStorage,刷新自动恢复会话 |
| 多会话侧边栏 | 新建 / 切换 / 重命名 / 删除 |
| 文件管理 | 上传 txt(自动解析,进度轮询)、预览(增量加载)、重命名、删除 |
| 绑定文件 | 输入框左侧 + 按钮 → 菜单勾选已解析文件;绑定文件显示在输入框上方 |
| Loop 对话 | 提问 → SSE 流式回答;工具阶段以 > 引用块显示,最终答案 markdown 渲染 |
| API Key 设置 | 掩码显示已存 key,支持替换 / 清空 |
| 主题 | 浅色 / 深色切换 |
量化 loop 各特性的贡献,以及 loop 与普通 RAG 的差距。
| 配置 | 隔离变量 |
|---|---|
| C0_full | 完整 Loop(规划 + 5 工具 + 笔记) |
| C1_no_vector | 禁用 vector_search |
| C2_no_plan | 不注入规划引导 |
| C3_one_search | 只允许一次定位检索 |
| C4_fast | 普通单轮 Top-K RAG |
结果(34 题,答案覆盖 reference 全部核心事实点即通过):
| 难度 | C0 完整 | C1 无vector | C2 无规划 | C3 单次检索 | C4 fast |
|---|---|---|---|---|---|
| 简单 (20) | 100% | 100% | 95% | 95% | 55% |
| 普通 (10) | 90% | 100% | 80% | 50% | 40% |
| 困难 (4) | 100% | 25% | 50% | 50% | 25% |
几条结论:
- 多跳 Agent 明显强于单轮 RAG:简单 95–100% vs 55%,普通 80–100% vs 40%,困难 50–100% vs 25%。
- 多跳检索对普通题很关键:单次检索(C3)普通题掉到 50%。
- vector_search 的价值主要在困难题:完整 Loop 100% vs 25%,语义检索帮模型拼齐散落的多事实证据。
匿名实体推理基准,验证模型在给定匿名实体 ID 时能否还原真实身份。数据/索引/测试集在 evaluate/anonyrag_data/,分标准档(anonyrag_testset.json)和最难档(anonyrag_hard50.json,实体数最多的 50 题)。复用 system 的 run_loop_query 无头调用,用实体映射精确匹配打分(golden「实体ID=名字」)。
hard50 结果:
| 指标 | 结果 |
|---|---|
| 整题通过率 | 18/50 = 36% |
| 实体级命中率 | 328/429 = 76% |
| 未通过题分布 | 部分命中 28 题 / 完全答错 4 题 |
通过率不高,但拆开看,问题不在"不会",在完整性:多数未通过题是答对了大部分实体、漏了一两个,而 Anonymity Reversion 要求全部命中才判通过。实体级 76% 对整题 36% 的差距说明瓶颈在枚举的收尾,和消融实验里"枚举/聚合是已知边界"的结论一致。标准档结果见 anonyrag_data/anonyrag_results.json,hard50 评分版见 anonyrag_hard50_scored.json。
在 system 基础上做的生产化改造,不推翻原有选型,只在边界补健壮性。核心矛盾:LLM 调用是瓶颈,Web 层要做到不占线程、不雪崩、可横向扩展。
| 能力 | 实现 | 解决什么 |
|---|---|---|
| SSE 异步网关 | 独立 FastAPI 服务(system/gateway/)+ Redis Streams 事件流 |
长连接不占 WSGI 线程;Streams 游标 + Last-Event-ID 断线续传,修掉重连重放 bug |
| Celery worker 并行化 | 多 solo worker 进程(LLAGENT_WORKERS) |
Windows 无 prefork,多进程实现并行;并发问答从 1 → N |
| 任务健壮性 | acks_late=True, max_retries=3 + 幂等(run 已 COMPLETED 跳过、消息去重) |
任务失败自动重试不丢回答 |
| 每用户并发上限 | Redis 原子 INCR/DECR 计数,create_run 超限返回 429 |
单用户连发 N 个问答不打爆上游 LLM |
| 队列限流 | broker 积压 ≥ 阈值返回 503 | 突发流量被挡在入口 |
| Redis 职责分离 | 缓存 / broker / 事件流分 DB(生产规划) | 高并发下三者互不干扰 |
| DB 连接池 + 分区 | CONN_MAX_AGE / PgBouncer + 按月分区(生产规划) |
防连接堆积、表膨胀 |
| nginx 统一入口 | 静态 + 路由 + SSE 缓冲关 + 超时调大 | 连接承载、负载均衡、TLS |
几个实现细节(完整方案见 .docs/高并发改造方案.md):
- 事件流用 Redis Streams:tasks 写
XADD,网关XRANGE按游标轮询(xread(block=)在本环境会挂起,弃用)。 - SSE 断线续传:网关每条事件带
id:,浏览器原生 EventSource 重连自动带Last-Event-ID从断点继续,不重放、不漏。 - 限流完整生命周期:acquire → 超限回滚 429 → worker 完成释放 → 计数归零。
逐环节压测生产栈,全程不调 GPU、不调真实 API key(全链路用本地 mock LLM + 假 key)。详见 system/loadtest/README.md。
| 脚本 | 压什么 | 方式 |
|---|---|---|
loadtest.py --nginx |
nginx 静态/health 吞吐 | Python 并发 httpx |
loadtest.py --crud |
Django CRUD(登录/会话/文件) | 并发 httpx + mock 账号 admin |
loadtest.py --sse |
SSE 网关并发长连接承载 | asyncio + 预置假事件流 |
fullchain.py |
全链路(nginx→Django→Celery→mock LLM→Redis→网关→SSE) | mock_llm + 假 key 用户 |
结果:
| 环节 | 压测内容 | 结果 |
|---|---|---|
| A. nginx | GET /health / 静态页 |
115 / 117 req/s,0 错误 |
| B. Django CRUD | 会话 / 文件列表 | 85 / 83 req/s,0 错误 |
| C. SSE 网关 | 100 并发长连接 | 100/100 挂住(3.2s) |
| D. 全链路 | 并发 POST /api/runs(20 并发 × 60) |
429×58 / 201×2(限流生效) |
| E. 全链路 | SSE 消费抽查 | completed=True |
| 扩展性 | 1 worker → 2 worker | 成功创建 1 → 3 |
几点结论:
- Web 层不是瓶颈(nginx/Django 吞吐充足、零错误)。
- 异步网关的价值被实测证明:100 并发 SSE 长连接不占线程,同步 WSGI 的话早把 worker 耗尽了。
- 限流确实拦住过载:429 是设计行为——每用户 in-flight 上限保护上游 LLM,worker 完成后计数归零。
- 扩容杠杆是 worker:加 worker 吞吐线性提升,瓶颈在 LLM 调用耗时。
压测过程还发现并修了 3 个 bug(mock 延迟误解析、SSE 测试脚本卡死、in-flight 计数泄漏),见 loadtest/README 第四节。
环境要求:Python 3.11(tech/.venv,uv 管理)、支持 CUDA 的 NVIDIA GPU(loop/fast 的 embedding 强制 CUDA)、Docker Desktop(system 生产栈)、OpenRouter API Key(loop/system 的 LLM)。
:: loop(CLI 多跳 Agent)
cd tech\loop
..\.venv\Scripts\python.exe main.py
:: fast(单轮 RAG)
cd tech
.venv\Scripts\python.exe fast\main.py
:: system(Web 生产栈)
system\backend\run_system.bat :: compose up + 本机 worker
:: 访问 http://localhost,登录 admin / 123456 → 设置填 OpenRouter API Key
:: → 上传小说(等解析完成)→ 绑定文件 → 提问评测和压测:
:: 评测
cd tech\evaluate
..\.venv\Scripts\python.exe generate_testset.py
..\.venv\Scripts\python.exe runner.py :: C0–C3 消融
..\.venv\Scripts\python.exe run_fast.py :: C4
..\.venv\Scripts\python.exe judge.py --reuse :: 打分 → report.md
:: 压测(全链路需 mock_llm + mock worker)
cd tech\system\loadtest
..\..\.venv\Scripts\python.exe loadtest.py --all --concurrency 100 --requests 1000 --sse-concurrency 100
..\..\.venv\Scripts\python.exe fullchain.py --concurrency 20 --runs 60tech/
├── loop/ 多跳检索 Agent(CLI,核心算法)
│ ├── main.py CLI 入口
│ └── utils/ loop.py(对话循环) / utils.py(工具/检索/索引/SQLite)
├── fast/ 单轮 Top-K RAG 流水线
│ ├── main.py CLI + 模块入口
│ └── utils/ qsplit / hyde / qrewrite / recall / rerank / reason
├── system/ Web 产品 + 生产部署栈
│ ├── backend/ Django + DRF + Celery(apps: accounts/chat/documents/rag/loop)
│ ├── gateway/ 独立 FastAPI SSE 网关(Redis Streams 游标续传)
│ ├── frontend/ Vite + React 19 + TS(useSSE/useLoopChat hooks)
│ ├── nginx/ nginx 入口配置(SSE 缓冲关 + 超时调大)
│ ├── prometheus/ Prometheus 配置
│ ├── loadtest/ 压测:mock_llm / loadtest(三层) / fullchain(全链路) / README
│ └── docker-compose.yml 生产栈编排(nginx/Django/gateway/pg/redis/minio/prometheus)
├── evaluate/ 评测体系
│ ├── runner.py 消融跑批(C0–C3)
│ ├── run_fast.py C4 fast
│ ├── judge.py LLM 事实覆盖评分
│ ├── anonyrag_*.py AnonyRAG 基准
│ └── results/ 消融结果 + report.md
├── .dockerignore
└── README.MD 本文件
| 层 | 技术 |
|---|---|
| LLM | DeepSeek v4-flash(OpenRouter,可通过 LLAGENT_LLM_BASE_URL 覆盖为本地 mock / 本地推理) |
| Embedding / Rerank | BAAI/bge-base-zh-v1.5、bge-reranker-base(本地 CUDA) |
| 向量 / 关键词检索 | FAISS + rank-bm25(jieba) + RRF 融合 |
| Web 后端 | Django 5.2 + DRF + JWT(simplejwt)+ Celery |
| SSE 网关 | FastAPI + uvicorn + Redis Streams |
| 存储 | PostgreSQL + Redis + MinIO |
| 入口 / 部署 | nginx + Docker Compose |
| 前端 | Vite + React 19 + TypeScript + react-markdown |
| 可观测 | Prometheus + Grafana |
| 评测 | LLM-as-judge 事实覆盖 + AnonyRAG 精确匹配 |