上传图文混排 PDF,就内容(含论文插图、表格)提问,得到带引用、可溯源、多轮的答案。
不只是能跑的 RAG——而是一套可度量、并诚实暴露自身缺陷的中文 RAG 系统。
一眼结果(12 题黄金集,本机可复现):检索 Recall@5 0.58 → 0.79(hybrid) · 引用零幻觉 CitationPrecision 1.0 · 跨厂商 LLM 裁判 correctness 0.96 · 裁判人工校准 MAE 0 · 并三指标交叉印证一处检索缺陷。
- 🎯 可度量,不止能跑:12 题版本化黄金集 + 检索 / 答案 / LLM 裁判 / 人工校准四层评估,每个决策用数字背书。
- 🔍 混合检索:语义切分 → BM25 + 向量 + RRF(只用名次) → 可选交叉编码器重排。加 hybrid 后 Recall@5 0.58→0.79。
- 🖼 图也能被检索:论文图表经 caption-then-embed 进入同一检索空间,命中图则 VLM 看图作答。
- 🧩 接口隔离架构:六接口 + 唯一装配点,加一整套评估器零侵入主链路。
- 🔬 诚实:找到并刻画了自己系统的一处检索缺陷,还给出修法——不报喜藏丑。
flowchart LR
Doc[PDF / txt / md] --> C[语义切分]
Doc --> V[VLM 给图配文]
C --> R[(ChromaDB<br/>唯一真相源)]
V -->|caption-then-embed| R
Q[提问 / 追问] --> RW[历史感知改写]
RW --> H[Hybrid<br/>BM25+向量+RRF]
R --> H
H --> RR[可选 Rerank]
RR --> G[Generator<br/>DeepSeek / VLM 看图]
G --> A[答案 + 可溯源引用]
| 功能 | 界面 |
|---|---|
| 核心问答 + 可溯源引用 上传文档→提问→答案带 [n] 引用,点开折叠块即见检索原文 |
|
| 多模态:图表也能被检索 问图表相关问题,命中图以 Gallery 展示、VLM 看图作答 |
|
| 多轮追问 依赖上文的追问经历史感知改写后仍正确召回 |
core/interfaces.py 定义六个抽象基类(DocumentLoader / Chunker / Retriever / Generator / QueryRewriter / Evaluator);pipeline.py 只依赖接口、不 import 任何具体实现;config.py 是唯一装配点。换加载格式、分块策略、检索器、大模型、改写策略,只动 config 工厂分支,主流程一行不改。
最能说明问题的是
Evaluator——它是六接口之一,却不进 pipeline、是离线背书层。所以我加了一整套 LLM 裁判(跨厂商 + 多采样 + 校准),做的只是「挂一个新的 Evaluator 实现」,pipeline / config / interfaces 一行没改。架构的价值不是画得好看,是「加一大块功能核心代码零改动」这个能落地的事实。
展开:六接口清单 + 每个接口的多实现
每个接口背后都有 ≥2 个可换实现,可插拔不是口号(删任一备用实现 config 都会报错):
- Retriever ×4:dense(bge+Chroma)· keyword(BM25)· hybrid(RRF 融合)· rerank(交叉编码器)
- Chunker ×2:fixed · semantic(句界+段落硬边界)
- Generator ×3:template · llm(DeepSeek 开卷+引用编号)· vlm(命中图→kimi 看图作答)
- QueryRewriter ×2:noop(直通)· llm(历史感知改写)
- DocumentLoader:Pdf / Text / Auto 按格式分派
- Evaluator:retrieval · answer · judge(离线,不进 pipeline)
12 题人工标注黄金集(evaluators/golden.jsonl,单一真相源),逐级对比(top-5):
| 指标 | Dense | Hybrid | Rerank |
|---|---|---|---|
| HitRate@5 | 0.67 | 0.92 | 0.92 |
| Recall@5 | 0.58 | 0.79 | 0.79 |
| MRR | 0.52 | 0.74 | 0.72 |
| 延迟/查询 | ~6ms | ~8ms | ~885ms |
怎么读:hybrid 把 dense 漏掉的「研究方法 / 43 份 / 24 省份」这类精确术语/数字查询用 BM25 词面通道救回(dense rank - → hybrid rank 1),是定向救回、不是均匀拔高。而 rerank 在这 12 题上并不优于 hybrid(k=3 Recall 反低、k=5 持平且 MRR 略降),却贵约 100×——cross-encoder 的价值在首阶召回噪声大时才显现,本评估集未触发。这是真实测量,不为给 rerank 背书而修饰。
🔎 找到并刻画了一处检索缺陷:「社会网络分析得出了什么结论」在 dense 与 hybrid 下双双 miss(n=6/n=12 两次一致)——结论段讲「节点/贡献度/治理」却不含「结论」二字,抽象问法与具体发现间无词面锚、有语义鸿沟。被三指标交叉印证:检索 Recall miss + 答案 AnswerCoverage 漏点 + 裁判 correctness 低而 groundedness 满(「有据但答错」)。下一步:查询改写 / 章节感知召回。评估用于探测问题,不为刷分。
12 题、生产配置 rerank+semantic+llm,确定性 → LLM 判 → 人工校准逐层加强:
| 层 | 指标 | 结果 |
|---|---|---|
| 确定性 | CitationPrecision / AnswerCoverage | 1.0 / 0.92 |
| LLM 裁判 | Correctness / Groundedness / Stability | 0.96 / 1.0 / 1.0 |
| 人工校准 | MAE / ExactMatch | 0 / 1.0 |
- 确定性指标(免费常驻):CitationPrecision=1.0 即答案标的
[n]引用全部合法(零幻觉引用)。 - LLM-as-judge(opt-in):跨厂商裁判(DeepSeek 生成 / kimi 判,防运动员兼裁判)+ 多采样报方差 + 失败计
n_failed绝不静默虚高。 - 人工校准:没校准过的裁判分不许对外引用。MAE=0 即裁判与人工零误差一致。
诚实边界:MAE=0 一半是裁判靠谱、一半是评估集多为无歧义事实题,准确说是「事实型 QA 上零误差」而非「裁判万能」。
🖼 多模态:让图也能被检索
- 抽图:
get_image_rects + get_pixmap(clip=rect)按页面位置渲染,而非抽原始 xref——避免带翻转矩阵的图被镜像、VLM 读错字。 - 配文入库:每张图经 kimi 生成中文 caption,作为「文本」参与嵌入,于是图能被语义召回并标进引用。
- 稳健:caption 并发撞 VLM 账号上限(429),故并发卡上限内 + 退避重试 + 单图失败跳过不拖垮整篇。
- 看图作答:命中图块时把图喂回 kimi 看图作答,
VLMGenerator继承LLMGenerator复用引用编号——Generator签名不变、pipeline 零改动。
💬 多轮对话:历史感知查询改写
追问「它怎么用?」依赖上文,裸检索会召回垃圾。QueryRewriter 在进检索前用历史把追问改写成自洽独立 query,改写吸收了历史依赖,于是下游 retrieve/generate 仍是无状态单轮,pipeline.run(query,k) 一行不改。历史存 SQLite(按 session_id);首轮不调 LLM、改写失败回退原句,绝不拖垮检索。
🔗 可溯源:引用一键展开看依据
每个 [n] 在界面上可点击展开,露出该编号对应的检索原文(图块则是 caption)。原文检索时已随 Retrieved 在手、顺带回传,不额外检索;前端用 HTML <details> 折叠,零 JS。RAG 不黑箱的关键,就是把检索证据端到台前。
🔬 工程严谨:一路让数据说话,修了五个真 bug
每个都最小诊断到根因,不猜:
- 黄金集单一真相源静默失效:
GOLDEN_PATH指向被 gitignore 且不存在的data/golden.jsonl,四个 eval 脚本全 FileNotFound,而版本化的evaluators/golden.jsonl没人读。根因:换源重构只挪文件没改路径常量。 - 标注漂移致 recall 恒 0:chunk id=
sha1(source,start,end),加表格抽取后全文偏移平移→所有 id 变→旧标注全失效。按当前切块重标修复。深层:偏移型 id 使任何 parser 改动静默作废所有检索标注。 - chroma HNSW 崩:
delete(where=全删)触发 compactor 读残留半落盘 HNSW 段。改「删集合重建」。 - 裁判 temperature 契约错:kimi-k2.6 只允许 temperature=1,默认 0.6→18 次采样全 400→n_failed=6 全 0 分(兜底正确、未静默虚高)。改 1.0。
- eval_rerank 漏迁:两次重构都漏了它,仍硬编码 stale 标注 + 脆的清空。迁 load_golden + 硬化,与其余脚本对齐。
⚠️ 已知局限 / 路线图
局限(诚实标注):黄金集 n=12 统计量偏小、评估集多为无歧义事实题(judge Stability=1.0 部分源于此);结论型查询检索缺陷未修复;难表二维结构被线性化丢失;AnswerCoverage 为字面弱信号(已用裁判补);参考文献段落切不干净靠 rerank 压制。
路线图:难表→表格截图→VLM 看图(复用现有多模态管线,架构已定待实现)· 修结论型查询检索缺陷(查询改写/章节感知召回,用现有 eval 量 delta)· 鉴权 + Web 部署。
pip install -r requirements.txt
cp env.example .env # 填 DEEPSEEK_API_KEY(生成) / MOONSHOT_API_KEY(图caption+看图)
python api/main.py # 后端 FastAPI :8000
python app.py # 另开终端起 Gradio 前端首次运行自动下载 bge-small-zh-v1.5(嵌入)与 bge-reranker-base(重排)。
复现评估(黄金集随仓提供):
python scripts/eval_run.py # 检索:dense vs hybrid
python scripts/eval_rerank.py # 检索:三档对比 + 延迟
RUN_JUDGE=1 python scripts/eval_answer.py # 答案:确定性 + LLM 裁判
python scripts/eval_calibrate.py # 裁判人工校准(MAE)core/ interfaces(六ABC) · pipeline(只依赖接口) · config(工厂) · paths
chunkers/ fixed · semantic retrievers/ dense · keyword · hybrid · rerank
generators/ template · llm · vlm rewriters/ noop · llm(多轮)
ingest/ parser(文本/图/表) · captioner(VLM配文) · loaders(格式分派)
evaluators/ retrieval · answer · judge(跨厂商裁判+校准) · golden.jsonl(12题黄金集)
store/ metadata_db(SQLite:文档状态+会话历史)
api/ main · routes · schemas app.py Gradio(多轮+可溯源+命中图)
scripts/ eval_*(run/rerank/answer/label/calibrate) · verify_*(各模块独立验证)
MIT License · 一个可度量、诚实、能被追问的 RAG 工程实践