一个把「答案质量」当作一等公民的 chat-with-docs:混合检索(BM25 + dense → RRF 融合 → 可选 cross-encoder 重排 / HyDE 假设文档路)+ 接地引用(回答标注 [1][2] 并校验标号真实性)+ 内建忠实度评测(claim 级 LLM-as-judge:回答拆成原子论断逐条判定,score = 支撑数/总数,本地可审计),并把同一套管线延伸成「任意代码库 → RAG 知识语料」的生成器(rag-corpus skill)。
后端 FastAPI(SSE 流式)+ Streamlit 对话式前端;LLM/Embedding 走 OpenAI 兼容接口,默认智谱 GLM;无 key 时自动进入 MOCK 模式,零配置可跑。
| 模块 | 说明 |
|---|---|
| 混合检索 | BM25(词法,jieba)+ dense(语义余弦)双路并行,RRF 名次融合(对两路分数量纲不敏感);可选 BGE cross-encoder 精排(RERANK=1,失败自动回退 RRF 原序);可选 HyDE 假设文档第三融合路(HYDE=1,对症词法陷阱,见技术要点) |
| 多轮对话 + 查询改写 | 跟进问题(「那要等多久?」)先由 LLM 改写成独立问题,与原始问题双查询融合检索——改写救回纯指代题,改写丢主题词时原始路兜底,两路互为保险 |
| 接地引用 + 标注校验 | 回答附来源块;[n] 标号与真实引用集双向校验——越界标号(幻觉引用)与从未被引用的检索块都会被标出 |
| claim 级忠实度评测 | 每条回答生成后跑 LLM-as-judge:回答分解成原子论断逐条判定,score = 支撑数/总论断数(本地计算、可审计,不采信 LLM 拍的整体分);前端绿/红徽章 + 逐条 ✓/✗;judge 畸形输出宽容归一、不可用降级启发式(MOCK 走句子级近似 claim,公式对称) |
| 检索质量评测 | Recall@k / Hit@k / MRR / Precision@k 纯函数实现;评测集故意含转述题与词法陷阱样本,暴露真实短板而非自我安慰 |
| 持久化 | JSONL(零依赖:临时文件 + fsync + 原子替换,逐行容错坏行/坏字节)或 LanceDB(一个环境变量切换);重启免重复 embedding |
| 多项目语料 | 按 project:relpath 标注入库,sources 参数在两路检索同过滤;UI 侧栏「检索范围」选择器 |
| Provider 无关 + 零配置 | 换 OpenAI/DeepSeek/本地 vLLM 只改 base_url + 模型名;无 key 进入 MOCK 模式(确定性哈希向量 + 启发式评测),整条链路离线可演示,CI 无 secrets |
用户提问(可带多轮 history)
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 多轮改写:跟进问题 → 独立问题(LLM,失败/MOCK 透传) │ ← app/llm.py rewrite_query
│ 检索:[原始 + 改写 (+HyDE 假设文档)] 多查询 │ ← app/retrieval.py
│ → 各自 BM25+dense→RRF → 名次级 RRF 融合 │
│ → (可选)BGE 重排 → top_k 引用 │
└──────────────────────────────────────────────────────────────┘
│ context
▼
┌──────────────────────────────────────────────────────────────┐
│ 生成:LLM 流式回答(带历史 + [1][2] 引用标注) │ ← app/llm.py / main.py (SSE)
└──────────────────────────────────────────────────────────────┘
│ answer + context
▼
┌──────────────────────────────────────────────────────────────┐
│ 评测① 引用校验(毫秒级纯函数)→ event:meta 立刻发出 │ ← app/citations.py
│ 评测② 忠实度:claim 级 judge → supported/total → event:faith │ ← app/faithfulness.py
│ (尾包分级:快校验不被秒级 judge 绑架在同一尾包) │
└──────────────────────────────────────────────────────────────┘
+ 离线评测:忠实度 / 检索质量 / 多轮改写 / RAGAS 对照
(scripts/ 与 eval/ 产出本地报告,见「测试与评估」)
持久化:启动装载 → 仅全新安装种演示语料 → 灌库后落盘 ← app/persist.py
| 层 | 实现 | 代码 |
|---|---|---|
| 检索正确性 | RRF 与标准式一致;平局次序确定化(dense 名次 → bm25 名次 → chunk_id,不依赖容器迭代序);多查询融合的展示分即融合分(排序-分数不矛盾);top_k 双向夹取 | app/retrieval.py |
| 并发安全 | store 全部读写持 RLock(FastAPI sync 端点在线程池真并发共享实例);持久化在锁内做快照序列化 | app/store.py |
| 启动数据安全 | 仅「全新安装」才种演示语料;加载失败以空库服务且不落盘(一写就覆盖用户索引);坏行跳过后装回按位置重编号 chunk_id | app/persist.py · app/main.py lifespan |
| 失败降级 | 重排失败回退 RRF 原序;改写失败检索原问题;HyDE 失败跳过该路;judge 失败回退句子级启发式;生成中途失败显式发 event: error(与用户主动停止可区分);流开始前失败返回结构化 502 |
app/rerank.py · app/llm.py · app/main.py |
| LLM 调用层 | 进程内复用客户端(连接池)+ 显式超时/重试;流式用 with 块关连接;embedding 自动分批 ≤32 条/请求(智谱单次 64 条上限,带真实回归测试);响应缺向量 fail-fast | app/llm.py |
| 输入守卫 | 上传扩展名白名单 + 2 MiB 上限 + UTF-8→gb18030 严格解码(二进制 422 拒绝);向量维度一致性校验(换 embedding 模型混库即报错,杜绝静默垃圾相似度) | app/main.py · app/store.py |
| 前端安全 | 语料侧字符串(引用原文/来源/判分理由)全量 html.escape——unsafe_allow_html 只包自产 markup;失败轮次不进对话历史 |
ui/streamlit_app.py |
app/
config.py # 环境驱动配置(.env / RERANK / PERSIST_* / SEED_DEMO)
store.py # 混合存储(BM25 + dense 余弦 + RLock)+ 项目过滤 + save/load
persist.py # 持久化:JSONL(原子写+逐行容错)/ LanceDB(可选)
llm.py # Provider 无关 LLM 客户端 + query 改写 + MOCK fallback
ingestion.py # 滑窗切分 + ingest_files 批量入库(一次 embed/一次重建)
retrieval.py # hybrid_retrieve + multi_query_retrieve(双查询融合)
rerank.py # 可选 BGE 重排(惰性加载、失败回退、分数回写)
citations.py # 引用标注校验(纯函数)
faithfulness.py # claim 级忠实度分解(judge 逐条判定 + 句子级启发式 fallback)
retrieval_metrics.py # Recall/Precision/Hit/MRR(纯函数)
schemas.py / main.py # Pydantic 模型 / FastAPI(SSE、结构化错误、上传守卫)
ui/streamlit_app.py # 对话式前端(可打断生成、错误/停止可区分、检索范围过滤)
skills/rag-corpus/ # 项目→语料 skill(SKILL.md + 6 个规则文件,含程序化质量门 G1–G10)
eval/ # 评测集:忠实度 / 检索 / 多轮 + RAGAS 对照脚本
scripts/ # ingest_corpus.py + 评测脚本(支持任意项目语料)
tests/ # 单元 + API 集成测试(TestClient,离线;计数见「测试与评估」)
data/sample.md # 演示语料(虚构 NovaCloud 帮助文档,14 节)
要求 Python ≥ 3.10。
pip install -r requirements.txt
cp .env.example .env # (可选)填 LLM_API_KEY;不填走 MOCK,零配置可跑
python -m app.main # 后端 :8000(启动装载持久化索引,全新安装才种演示语料)
streamlit run ui/streamlit_app.py # 前端 :8501打开前端即是对话式界面:灌文档(可选)→ 提问 → 追问(「那要等多久?」)→ 看流式回答 + 忠实度徽章 + 引用校验 + 改写说明。生成中可随时停止,已生成部分保留。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /ingest |
灌文本 {text, source};失败返回结构化 502 |
| POST | /ingest/file |
上传 .txt/.md(白名单 + 2 MiB 上限;UTF-8 优先、gb18030 兜底,均失败 422) |
| POST | /ingest/batch |
批量入库 {project, files:[{path,text}]};source 存 project:path |
| GET | /sources |
项目列表与块数(检索范围选择器数据源) |
| DELETE | /ingest/project/{name} |
删除某项目全部块(重灌防重复;chunk_id 重编号) |
| POST | /query |
SSE:citations → token 流 → meta(引用+引用校验+改写/HyDE,毫秒级先行)→ faith(claim 级忠实度,judge 完成即发);失败时 error 事件(与用户停止可区分);body 可带 history 与 sources(项目过滤) |
| POST | /eval |
批量忠实度评测 |
| GET | /health |
状态(mock / chunks / rerank / persist) |
pytest # 82 项:切分/overlap、RRF/融合/平局、维度守卫、
# 坏行重编号、并发冒烟、rerank 契约、claim 级判分公式、
# HyDE 降级契约、SSE 事件序与错误契约、
# lifespan 数据安全、上传编码兜底…
python scripts/run_eval.py # 忠实度评测(claim 级 judge,模式自动标注;
# 评测集含有据/编造/混合三种样本)
python scripts/run_retrieval_eval.py # 检索质量(Recall/Hit/MRR/Precision + 多轮改写对照
# + HyDE A/B 对照)
python scripts/run_retrieval_eval.py --corpus <目录> --evalset <目录>/evalset.json # 任意自定义语料
python eval/ragas_eval.py # RAGAS 标准对照(pip install ragas langchain-openai)评测报告(
RESULTS*.md/RETRIEVAL_RESULTS*.md)为本地运行时产物,不入库——数字永远可由上述命令在你自己的环境复现;评测脚本的模式标注(MOCK/启发式/真实)与指标聚合逻辑由测试离线覆盖。
CI(.github/workflows/ci.yml):ruff + mypy 静态检查,pytest 跑 3.10–3.13 全矩阵;显式 LLM_API_KEY="" 强制 MOCK,零 secrets。
deep-rag 不只吃文档——用 rag-corpus skill 把任意代码仓库转成 RAG 优化的语料(模块手册 manual/*.md + 问答层 faq.md + 检索评测集 evalset.json),然后直接提问你的项目:
# ① 生成语料:在目标项目里对 Claude Code 说「用 rag-corpus 把这个项目做成知识库」
# (skill 正本在 skills/rag-corpus/,可复制到 ~/.claude/skills/ 对任意项目使用;
# 生成走「清点 → 分区全量阅读 → 合并 → 程序化质量门 G1–G10」流程)
# 产出 <项目>/deeprag-corpus/:manual/*.md + faq.md + evalset.json + manifest.json
# ② 入库(服务运行中)
python scripts/ingest_corpus.py <项目>/deeprag-corpus --project <项目名> --api http://localhost:8000
# ③ 验证检索质量(配了真实 key 则用真实 embedding)
python scripts/run_retrieval_eval.py --corpus <项目>/deeprag-corpus --evalset <项目>/deeprag-corpus/evalset.json --project <项目名>多项目入库后,UI 侧栏「检索范围」可按项目过滤。自举验证:对 deep-rag 自身完整跑过这套管线(手册锚点均经程序化门禁校验)——生成的语料目录与评测报告同属本地运行产物,按惯例不入库,任何项目都能用上述命令自行复现。
- 密钥仅存
.env(已 gitignore);api_key字段repr=False,不出现在日志与调试输出 - 上传:扩展名白名单 + 2 MiB 上限 + 严格解码——二进制/坏编码文件 422 显式拒绝,绝不乱码入库
- 前端把语料内容视为不可信输入:所有动态字符串过
html.escape再进 HTML 模板 - 持久化:临时文件 + fsync +
os.replace原子替换;逐行容错坏行/坏字节;启动加载失败时空库服务且不写盘(保护用户索引不被覆盖) - 容器:非 root 用户运行 +
HEALTHCHECK探活;.dockerignore把.env、本地工作区挡在构建上下文外 - 可选重排的版本坑:FlagEmbedding(1.4) 需
transformers<5——5.x 移除prepare_for_model后打分抛错并静默回退 RRF 原序(重排失效而非崩溃,符合降级契约但等于白开) - 公网部署须知:默认无鉴权且 CORS 全开(演示定位)——公网部署并填真实 key 时请置于鉴权/反向代理之后,或使用 MOCK 模式
- 检索主干:BM25+dense 混合(RRF)+ 可选重排 + 引用接地 + SSE 流式
- 质量链路:忠实度评测 + 引用标注校验 + 检索质量评测 + RAGAS 标准对照
- 多轮:query 改写 + 双查询融合(改写前后对照评测)
- 持久化:JSONL 默认 / LanceDB 可选,重启免重复 embedding
- 项目 → 知识库管线:rag-corpus skill + 多项目入库/来源过滤 + 任意语料评测
- 加固轮:SSE 错误契约、store 并发锁、向量维度守卫、启动数据安全、上传白名单、CI 静态检查
- claim 级忠实度分解(score = supported/total 本地计算;混合样本实证整体打分的稀释盲区后与 RAGAS 对照对齐)
- HyDE 词法陷阱检索增强(先跑 rerank/HyDE 排除实验定位病灶再实现第三融合路;A/B 见检索评测 HyDE 段)
- 流式校验的尾包分级:引用校验先行(meta)+ judge 异步补发(faith);句级流式 judge 经成本论证否决(论证见上)
- RRF 名次融合:每个片段按名次贡献
1/(k + rank),对 BM25 分数与余弦分数的量纲差异完全免疫;实现上预计算 cid→rank 字典避免list.index()的 O(N²),平局次序显式策略化(dense 名次 → bm25 名次 → chunk_id),可复现、可解释。 - claim 级忠实度而非整体打分:混合回答(有据句 + 编造句同段)会被有据句稀释——整体打分对此系统性失明,与 RAGAS 标准对照时出现过「整体打分满分、标准对照不及格」的真实样本。改为单次结构化调用做分解 + 逐条判定,
score = 支撑数/总论断数由本地计算(可审计),与 RAGAS 的数学一致但延迟不翻倍;MOCK 降级为句子级近似 claim,两条路径公式对称。混合样本上的对照结论一致,具体数字由run_eval.py/ragas_eval.py本地复现。 - HyDE 前先做排除实验:词法陷阱(查询词在不相关章节逐字共现)是 query 侧问题——排除实验证明 cross-encoder 重排救不了(正确块不在候选池,BGE 反而更偏爱逐字共现的干扰块),于是按 rerank 同款开关范式实现 HyDE 假设文档第三融合路(答案侧词汇替代问题侧陷阱词):词法陷阱样本从零命中救回到 top-k 内,整体均值亦有提升;代价同样如实记录——个别题的正确块名次后移(仍在 top-k)。逐题 A/B 由
run_retrieval_eval.py的 HyDE 段产出,本地可复现。「先测量、先排除便宜方案、再对症实现」比 HyDE 这个名词本身更有区分度。 - SSE 尾包分级:毫秒级的引用校验(纯函数)曾与秒级的忠实度 judge(实测单次 ~2.5s)阻塞在同一个 meta 尾包里——拆成
meta(校验先行)+faith(judge 完成即发)两个事件后,快校验立刻可见。句级流式 judge 经成本论证否决(每句一次带全量 context 的判定 × 5–15 句、同步生成器需线程化、MOCK 离线不可演示;完整决策记录在本地 OPTIMIZATIONS.md 工作文件,按评测产物惯例不入库)。 - 改写 + 双查询融合而非只信改写:LLM 改写偶尔丢主题词(曾观测到把「错误」改写成「系统或设备」),融合让改写路与原始路互为兜底——改写有效则提升,改写失效则原始路救回。
- MOCK 模式是一等工程决策:确定性哈希假向量让「共享 token 的文本相似度更高」,零配置下检索链路有语义可演示;CI 全矩阵无 secrets 跑真实代码路径;所有评测产物如实标注 MOCK/启发式/真实三模式。
- 评测集故意包含难题:转述题(无词面重叠)、纯指代跟进、词法陷阱(查询词在不相关章节逐字共现)——能区分好坏检索的评测才是评测。
- chunk_id 是位置下标的显式契约:删除/装回都会重编号,引用只活在单次请求内;这换来了
getO(1) 直取与无需外部 ID 服务的简单性。 - 持久化的取舍:JSONL 零依赖 + 原子替换 + 逐行容错,demo→中型语料够用;检索侧内存余弦在万级 chunk 内正确且快,换 ANN 索引的时机是「暴力扫描变慢」,由数据驱动而非提前设计。
单容器(FastAPI:8000 + Streamlit:7860),对外 7860。
docker build -t deep-rag .
docker run -p 7860:7860 -e LLM_API_KEY=你的智谱key deep-ragHF Spaces(Docker Space,push 仓库后在 Settings 加 LLM_API_KEY secret)与 Render(Web Service 指向 7860)均可直接跑;容器内已含就绪门(API 健康后才起 UI)与存活监督(API 退出即重启容器)。