Skip to content

Repository files navigation

mm-docqa · 多模态文档问答助手

上传图文混排 PDF,就内容(含论文插图、表格)提问,得到带引用、可溯源、多轮的答案。
不只是能跑的 RAG——而是一套可度量、并诚实暴露自身缺陷的中文 RAG 系统。

一眼结果(12 题黄金集,本机可复现):检索 Recall@5 0.58 → 0.79(hybrid) · 引用零幻觉 CitationPrecision 1.0 · 跨厂商 LLM 裁判 correctness 0.96 · 裁判人工校准 MAE 0 · 并三指标交叉印证一处检索缺陷。

核心问答演示


目录


30 秒看懂

  • 🎯 可度量,不止能跑: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[答案 + 可溯源引用]
Loading

项目演示

功能 界面
核心问答 + 可溯源引用
上传文档→提问→答案带 [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

每个都最小诊断到根因,不猜:

  1. 黄金集单一真相源静默失效:GOLDEN_PATH 指向被 gitignore 且不存在的 data/golden.jsonl,四个 eval 脚本全 FileNotFound,而版本化的 evaluators/golden.jsonl 没人读。根因:换源重构只挪文件没改路径常量。
  2. 标注漂移致 recall 恒 0:chunk id=sha1(source,start,end),加表格抽取后全文偏移平移→所有 id 变→旧标注全失效。按当前切块重标修复。深层:偏移型 id 使任何 parser 改动静默作废所有检索标注。
  3. chroma HNSW 崩:delete(where=全删) 触发 compactor 读残留半落盘 HNSW 段。改「删集合重建」。
  4. 裁判 temperature 契约错:kimi-k2.6 只允许 temperature=1,默认 0.6→18 次采样全 400→n_failed=6 全 0 分(兜底正确、未静默虚高)。改 1.0。
  5. 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 工程实践

About

可度量的中文多模态文档 RAG:语义切分+混合检索(BM25+向量+RRF)+交叉编码器重排,接口隔离架构,含检索/答案/LLM裁判/人工校准四层评估

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages