基于 RAG 的多模态文档问答产品,让非技术人员也能用自然语言从海量 PDF 中精准获取信息。
| 痛点 | 现状 | 本方案 |
|---|---|---|
| 信息检索效率低 | 财报动辄上百页,手动翻找特定数据耗时 10-30 分钟 | 自然语言提问,秒级返回答案 + 原文页码 |
| 图表信息难以利用 | PDF 中的柱状图、趋势图无法被关键词搜索命中 | 多模态解析自动生成图表描述,纳入语义检索 |
| 传统搜索精度差 | 关键词匹配无法理解"同比增长""环比下降"等语义 | 向量检索 + Rerank 重排序,理解业务语义 |
| 缺乏可信溯源 | 通用大模型回答无法验证来源 | 每条回答附带文件名 + 页码 + 页面截图证据链 |
- 分析师/研究员:需要从多份财报中快速提取关键指标
- 产品经理/运营:需要理解业务数据但不熟悉专业术语
- 企业知识库管理员:需要为团队搭建内部文档问答系统
用户:去年第三季度研发投入最高的子公司是哪家?
系统:根据财报数据,XX科技在2024年Q3研发投入为5.2亿元,位居首位。
📎 来源:XX集团2024年报.pdf · 第47页
[页面截图] [原文片段] [图表描述]
┌─────────────────────────────────────────────────────┐
│ Web Demo (Streamlit) │
│ macOS 风格 UI · 实时问答 · 证据链展示 │
└──────────────────────┬──────────────────────────────┘
│
┌──────────────────────▼──────────────────────────────┐
│ RAG Query Engine │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Embedding │ │ Vector │ │ Rerank │ │
│ │ (bge-m3) │→│ Search │→│ (bge- │→ LLM 生成 │
│ │ 1024-dim │ │ Cosine │ │ reranker)│ ecnu-max │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────▲──────────────────────────────┘
│
┌──────────────────────┴──────────────────────────────┐
│ Document Processing Pipeline │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ PyMuPDF │ │ Image │ │ Semantic │ │
│ │ 文本提取 │→│ Vision │→│ Chunking │→ JSON │
│ │ + 图片 │ │ Caption │ │ 语义切片 │ │
│ │ + 截图 │ │ ecnu-plus│ │ ≤1500字符 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────┘
| 决策 | 选择 | 理由 |
|---|---|---|
| 模型服务 | ECNU LLM Open Platform API | OpenAI 兼容协议,一套 SDK 覆盖 chat/embedding/vision/rerank,切换成本低 |
| 向量存储 | numpy 内存计算 | 文档量级 < 1万 chunk,无需引入 Milvus/Pinecone 等外部向量库,降低部署复杂度 |
| PDF 解析 | PyMuPDF + Vision caption | 轻量无需 GPU,图片通过 API 生成描述而非本地 OCR,平衡效果与成本 |
| Rerank 策略 | 两阶段检索(向量召回 → Rerank 精排) | 向量检索召回率高但排序粗糙,Rerank 补充交叉注意力提升 precision@k |
| 前端方案 | Streamlit | 零前端代码出 Demo,适合 PM 快速验证产品形态,非工程化部署方案 |
| Embedding 离线化 | 独立脚本批量构建 + 缓存文件 | 解耦计算与展示,支持断点续存,避免 API 故障时丢进度 |
| 语义切片 | 段落→句号→字符三级兜底 | 确保 chunk ≤ 1500 字符,避免超长文本触发 API 限制 |
| 维度 | 本项目 | 通用 RAG 框架(LangChain/LlamaIndex) | 企业级产品(通义千问文档问答) |
|---|---|---|---|
| 多模态图片理解 | ✅ 图片 caption 纳入检索 | ❌ 需额外集成 | ✅ 内置 |
| Rerank 精排 | ✅ 两阶段检索 | ✅ 内置 | |
| 回答溯源 | ✅ 文件名 + 页码 + 页面截图 | ✅ 支持 | |
| 部署门槛 | 低(API 调用,无 GPU) | 中(需配置多个组件) | 低(SaaS) |
| 可定制性 | 高(代码完全可控) | 高 | 低(黑盒) |
| 成本 | 按 API 调用计费 | 免费 + 自付模型费用 | 按量计费 |
# 安装依赖(Python 3.11+)
uv sync
# 配置 API
cp .env.example .env
# 编辑 .env 填入 ECNU API Key.env 配置:
LOCAL_API_KEY=your_ecnu_api_key
LOCAL_BASE_URL=https://chat.ecnu.edu.cn/open/api/v1
LOCAL_TEXT_MODEL=ecnu-plus # vision 模型,用于图片 caption
LOCAL_CHAT_MODEL=ecnu-max # 生成模型,用于 RAG 回答
LOCAL_EMBEDDING_MODEL=ecnu-embedding-small
LOCAL_RERANK_MODEL=ecnu-rerank# 1. PDF 解析(文本提取 + 图片 caption + 语义切片 + 页面截图)
uv run python core/fitz_pipeline_all.py
# 2. 构建 embedding 缓存(离线批量,支持断点续存)
uv run python scripts/build_embeddings.py
# 3. 启动 Web Demo
uv run streamlit run app/main.py# RAG 评测(20 条标注集,输出 Recall/引用/回答正确率)
uv run python scripts/eval_rag.py
uv run python scripts/eval_rag.py --skip-llm # 只测检索,不调 LLM
# 重新切片(切片逻辑变更时,无需重跑 PDF 解析)
uv run python scripts/rechunk.py
# Rerank 对比实验
uv run python scripts/eval_rerank.py
# API 健康检查
uv run python scripts/health_check.py将 PDF 文件放入 datas/ 目录,支持嵌套文件夹结构。
按段落边界切分 chunk,三级兜底确保每条 chunk ≤ 1500 字符:
- 按双换行(段落边界)拆分
- 合并过短段落(< 200 字符)
- 按单换行 → 句号 → 字符强制截断拆分过长段落
- 单次运行内:SHA256 内容哈希去重,避免同 PDF 内重复图片多次调用 Vision API
- 跨运行:磁盘缓存 (
caches/image_caption_cache.json),相同图片直接命中缓存
Streamlit 回答附带完整证据链:
- 页面截图(PDF 页面渲染为 PNG)
- 原文片段(命中的 chunk 文本)
- 图片描述(Vision 模型生成的 caption)
scripts/build_embeddings.py:批量构建 embedding(batch_size=50),每 batch 成功后立即保存- 支持 Ctrl+C 中断 + 自动断点续存
- Streamlit 只读缓存,秒级启动
构建 20 条标注评测集(eval_set.json),覆盖 5 种题型:数值提取、趋势分析、业务策略、跨文档对比、拒答测试。每条标注目标公司、关键词、期望答案。通过 scripts/eval_rag.py 自动化评测。
| 指标 | Phase 4 基线 | Phase 3 迭代后 | 变化 |
|---|---|---|---|
| Evidence Recall@5 (向量) | 80% | 95% | +15% |
| Evidence Recall@5 (Rerank) | 75% | 90% | +15% |
| 跨文档 Recall (向量) | 0/3 (0%) | 3/3 (100%) | +100% |
| 跨文档 Recall (Rerank) | 0/3 (0%) | 3/3 (100%) | +100% |
| Citation Accuracy | 100% | 100% | 持平 |
| Answer Correctness | 95% | 90% | -5% |
第一步:基线评测(Phase 4)
运行评测脚本,发现系统瓶颈:
- 跨文档对比题("对比千味央厨和伊利股份的应收账款")全部召回失败(0/3)
- 根因:单向量检索自然坍缩到最相似的文档簇,"千味央厨"主导了 embedding 语义
- 年报精确页码未命中:检索能找到年报但不是目标页码
第二步:LLM 驱动查询规划(Phase 3)
用 LLM 替代关键词匹配,实现语义级查询理解:
- LLM 分析问题语义,判断是否需要拆分("对比千味和伊利的应收账款"→需要拆分)
- 提取实体(公司名),生成优化后的子查询
- 子查询自动扩展相关关键词(如"应收账款 管理 政策 账期 坏账")
- 效果:跨文档向量 Recall 从 0% 提升到 100%
第三步:Rerank 多样性约束
Rerank 模型对合并后的结果倾向单公司排序:
- 新增
ensure_company_diversity(),强制每家公司至少保留 1 个 chunk - 效果:跨文档 Rerank Recall 从 0% 提升到 100%
第四步:拒答 Prompt 优化
在 Prompt 中增加规则:数据不足时回答"未在当前资料中找到依据",避免编造数据。
| 题型 | 数量 | 向量 Recall | Rerank Recall |
|---|---|---|---|
| 数值提取 | 5 | 5/5 (100%) | 4/5 (80%) |
| 趋势分析 | 5 | 4/5 (80%) | 4/5 (80%) |
| 业务策略 | 5 | 5/5 (100%) | 5/5 (100%) |
| 跨文档对比 | 3 | 3/3 (100%) | 3/3 (100%) |
| 拒答测试 | 2 | 2/2 (100%) | 2/2 (100%) |
| 问题 | 原因 | 改进方向 |
|---|---|---|
| 伊利股份2024年营业总收入 | 年报在 top-5 但非目标页码(p7 vs p13) | 年报 chunks 加权 |
| 伊利奶粉业务营收同比增长 | LLM 输出 2.53%,实际应为 7.53% | 检索结果中数据源冲突 |
| 伊利股份2022年营收增长 | 检索返回研报而非 2022 年报 | 实际财报优先级提升 |
| 千味央厨2022年Q1营收 | 检索结果中无直接季度数据 | 扩大检索范围 |
| 中恒电气2024年营收增长 | LLM 给出数字而非拒答 | 拒答 prompt 进一步优化 |
| 阶段 | 目标 | 状态 |
|---|---|---|
| Phase 1 | 接入 ECNU API,统一模型服务 | ✅ 完成 |
| Phase 2 | Streamlit Demo + 产品化包装 | ✅ 完成 |
| Phase 2.5 | 语义切片 + Embedding 离线化 + 证据链展示 | ✅ 完成 |
| Phase 3 | 多文档对比问答(LLM 查询规划 + 分公司检索 + Rerank 多样性) | ✅ 完成 |
| Phase 4 | 自动化评测体系(20 条标注集 + Bad Case 驱动迭代) | ✅ 完成 |
| Phase 5 | 产品文档完善 + 简历表达优化 | ✅ 完成 |
迭代逻辑:Phase 4 先做评测基线 → 发现跨文档检索是瓶颈 → Phase 3 针对性解决 → 重新评测验证效果。用数据驱动迭代,而非凭直觉优化。
spark_multi_rag/
├── app/
│ └── main.py # Streamlit Web Demo(只读缓存)
├── core/ # RAG 核心逻辑
│ ├── fitz_pipeline_all.py # PDF 解析(语义切片 + 图片去重 + 截图)
│ ├── rag_from_page_chunks.py # RAG 检索 + Rerank + 问答
│ ├── get_text_embedding.py # 文本向量化
│ └── extract_json_array.py # JSON 结构提取
├── image_utils/ # 图片分析工具
│ ├── async_image_analysis.py # Vision caption(异步)
│ └── caption_cache.py # 图片 caption SHA256 磁盘缓存
├── scripts/
│ ├── build_embeddings.py # 离线批量构建 embedding(batch=50, 断点续存)
│ ├── rechunk.py # 重新切片(无需重跑 PDF 解析)
│ ├── eval_rag.py # RAG 评测脚本(20 条标注集 + 指标计算)
│ ├── eval_rerank.py # Rerank 对比实验
│ └── health_check.py # API 健康检查
├── notebooks/ # Jupyter Notebook
│ ├── data_process.ipynb # 数据预处理
│ ├── model_finetune.ipynb # Qwen2.5-7B LoRA 微调
│ └── Qwen3_14B_finetune.ipynb # Qwen3-14B LoRA 微调
├── docs/
│ ├── plan.md # 产品迭代规划
│ ├── JD.md # 目标岗位分析
│ ├── resume_project.md # 简历项目经历
│ ├── eval_report.md # 评测报告(Markdown)
│ └── eval_report.json # 评测完整结果(JSON)
├── eval_set.json # 20 条标注评测集(开源)
├── datas/ # 原始 PDF 及测试集(gitignore)
├── caches/ # 缓存目录(自动创建)
│ ├── embeddings.npy # embedding 向量缓存
│ ├── chunks.json # chunk 列表缓存
│ ├── image_caption_cache.json # 图片 caption 缓存
│ └── page_images/ # 页面截图
└── .env # 环境变量(gitignore)
| 组件 | 方案 |
|---|---|
| PDF 解析 | PyMuPDF + ECNU Vision (ecnu-plus, 语义切片 + 图片去重) |
| Embedding | ECNU Embedding (bge-m3, 1024-dim, 批量离线构建) |
| Rerank | ECNU Rerank (bge-reranker-v2-m3, Cohere 兼容) |
| 生成模型 | ECNU Chat (ecnu-max, DeepSeek-V4-Flash) |
| 视觉模型 | ECNU Vision (ecnu-plus, Qwen3.6-27B) |
| 向量存储 | numpy (cosine similarity) |
| 前端 | Streamlit + macOS glassmorphism CSS |