本页目录
企业知识库问答系统(生产级 RAG)
基于 LangChain 的企业知识库问答:上传文档 → 自动向量化入库 → 混合检索 + reranker 重排 → 带引用溯源的流式回答,附带 ragas 四指标量化评估与消融诊断报告。
与姊妹项目 research-assistant(LangGraph 多智能体)互补:一个吃 Agent 能力,一个吃 RAG 能力。
核心亮点(区别于「能跑个 RAG demo」)
| 能力 | 实现 | 量化证据 |
|---|---|---|
| 检索质量优化 | BM25(jieba)+向量混合召回 → 智谱 cross-encoder 重排 | context_recall 0.975(vs 纯向量 0.925) |
| 可量化评估 | ragas 四指标 + 20 题 golden set + 三模式消融 | 见 eval/REPORT.md |
| 防幻觉 | 材料外拒答 prompt + 【材料N】引用溯源 + 低温 | faithfulness 0.848,库外问题正确拒答 |
| 增量入库 | 文件 MD5 缓存,新增/修改/删除三路径幂等 | 重复 ingest 零 embedding 调用 |
| 多轮对话 | condense-question:追问先改写成独立问题再检索 | 「那企业版呢?」正确召回并回答 |
| 生产化 | FastAPI + SSE 流式、Docker、pytest 143 项(零 API 调用) | make test 23s 全绿 |
架构
┌─────────────── 数据层(Ingest)────────────────┐
│ 上传(md/txt) → MarkdownHeaderTextSplitter │
│ 结构感知分块(带章节面包屑) → embedding-3 │
│ → Chroma 持久化 → 增量缓存(MD5,三路径幂等) │
└────────────────────┬───────────────────────────┘
▼
┌─────────────── 检索层(Retrieve)──────────────┐
│ 提问 → [追问改写 condense-question] │
│ → EnsembleRetriever(BM25×0.4 + 向量×0.6) 召回8 │
│ → 智谱 rerank 重排 → top-4 带出处材料 │
│ (可开关 ENABLE_RERANK,评估消融用) │
└────────────────────┬───────────────────────────┘
▼
┌─────────────── 生成层(Generate)──────────────┐
│ 防幻觉 prompt + 【材料N】引用 → glm-4 流式 │
│ → 会话历史 sqlite 持久化(thread_id 隔离) │
│ → SSE: progress / sources / token / done │
└────────────────────────────────────────────────┘
┌─────────────── 评估层(Eval,独立)────────────┐
│ golden set 20 题 → 三模式跑批 → ragas 四指标 │
│ → 诊断报告(哪个环节拖后腿 → 优化方向) │
└────────────────────────────────────────────────┘
快速开始
cd portfolio-projects/knowledge-base-qa
cp .env.example .env # 填入 ZHIPUAI_API_KEY
make install # 或 pip install -r requirements.txt
make ingest # 8 个示例文档入库(增量,重复跑秒回)
make run # http://localhost:8001 打开前端
CLI 验证:
python cli.py query "试用期多久" # 裸向量召回
python cli.py retrieve "P0 工单响应时限" # 完整检索管线
python cli.py compare "报销时限" # 三模式并排对比
Docker:
docker compose up -d # 首次构建后 http://localhost:8001
评估(打真实 API,三模式约 30 分钟):
make eval # 或 python eval/run_eval.py --modes rerank --limit 5 冒烟
API
| 路由 | 说明 |
|---|---|
POST /api/upload |
上传 md/txt(白名单文件名+5MB+UTF-8 校验)→ 增量入库 → 索引热更新 |
POST /api/ask |
SSE 流式问答:progress / sources / token / done / error 五事件 |
GET /api/health |
健康检查(模型/rerank 开关/库内块数) |
GET / |
极简前端(上传 + 对话 + 引用面板 + 模式切换) |
设计决策(面试可讲的部分)
为什么结构感知分块? 企业制度文档强结构(章/节/条款),纯字符切分会把条款和所属章节切散。先按标题切出带层级 metadata 的节、超长节再字符兜底,每个 chunk 自带「员工手册 > 考勤 > 迟到处理」面包屑——检索命中即知精确出处,引用溯源直接可用。
为什么混合检索? 向量吃语义(「工资的八成」≈「转正工资的 80%」)但对精确词弱(「帆修」「P0」这类 token 会糊);BM25 恰好相反。加权 RRF 融合互补。中文坑:BM25 默认按空格分词,必须配 jieba,否则整句变一个 token 直接失效。
为什么 reranker 且做成可开关? cross-encoder 逐对精算 query×候选相关性,排序质量高于 bi-encoder 距离;只对召回后 8 条重排,成本可控。做成开关让评估层能跑消融——量化出 reranker 价值(context_recall +5pp)而不是拍脑袋说「加了更好」。
实测坑:智谱 rerank 分数饱和。 多数相关候选精确返回 1.0,同分时 API 排序不稳定,会打乱混合检索排好的头部。解法:本地按(分数降序,上游排名升序)稳定排序,rerank 只在真有区分度时改变顺序。评估还发现只开混合不开重排是负优化(BM25 词面噪声让 faithfulness 从 0.80 掉到 0.75,重排压掉噪声后回升到 0.85)——「混合检索必须配 reranker」是这个项目跑出来的结论,不是背的。
为什么多轮记忆不用 LangGraph Checkpointer? 研究助手是多节点图需要图状态检查点;本项目生成链是线性的,一张 sqlite messages 表就够。追问检索难题(「那企业版呢?」单独检索召不回)用 condense-question 解:glm-4-flash 先把追问改写成独立问题再检索。用对工具的复杂度也是生产判断力。
ragas 0.4.3 兼容坑。 它硬 import 已被 langchain-community sunset 删除的 chat_models.vertexai,import 即崩;ragas 只做 isinstance 判断不实例化,注入空壳 stub 即可(ragas_compat.py)。judge 超时产生的 NaN 样本从聚合剔除并注明,不让脏数据污染均值。
评估结果摘要
| 指标 | vector | hybrid | rerank(生产默认) |
|---|---|---|---|
| context_recall | 0.9250 | 0.9000 | 0.9750 |
| context_precision | 0.8333 | 0.8796 | 0.8583 |
| faithfulness | 0.8028 | 0.7488 | 0.8483 |
| answer_relevancy | 0.6058 | 0.6431 | 0.6524 |
逐指标诊断与后续优化方向见 eval/REPORT.md。
项目结构
├── src/kb_qa/
│ ├── config.py # pydantic-settings 配置中心(全部可 env 覆盖)
│ ├── loader.py # 结构感知分块 + 溯源 metadata
│ ├── ingest.py # 增量入库(MD5 缓存三路径)
│ ├── retriever.py # 混合检索 KBRetriever(三模式)
│ ├── rerank.py # 智谱 rerank(降级+稳定排序)
│ ├── generate.py # 防幻觉 prompt + 流式生成 + 追问改写
│ ├── history.py # sqlite 会话历史
│ ├── service.py # SSE 事件流编排
│ └── ragas_compat.py # ragas vertexai stub
├── api/ # FastAPI(upload / ask / health)
├── static/index.html # 极简前端
├── eval/ # golden set + ragas runner + 诊断报告
├── tests/ # pytest 143 项(LLM 全 mock + OCR 本地真跑,零 API 调用)
└── Dockerfile / docker-compose.yml / Makefile
课程能力映射
| 本项目模块 | 来自课程 | 升级点 |
|---|---|---|
| 增量入库缓存 | rag-09 src_hash 模式 | 补齐修改/删除路径,幂等 |
| 混合检索 | rag-06 BM25+RRF 玩具版 | EnsembleRetriever 生产版 + jieba |
| reranker | rag-06 token 命中率 | 真 cross-encoder API + 饱和坑处理 |
| 防幻觉+引用 | rag-05 | 系统 prompt + SSE sources 事件 |
| 评估 | rag-08 自制 mini-RAGAS | ragas 四指标 + 消融 + 诊断报告 |
| 分块 | rag-04 / framework-03 | MarkdownHeaderTextSplitter 结构感知 |
| FastAPI/SSE/Docker | research-assistant | 骨架复用,新增文件上传与索引热更新 |
生产运维(LLMOps)
本项目经 ops-lessons(LLMOps 生产运维课程,13 课)升级,从「能跑的 RAG」推进到「运维就绪的生产服务」。四个维度的能力:
| 维度 | 能力 | 代码 | 课程 |
|---|---|---|---|
| 可观测性 | 结构化日志(trace_id 贯穿) | src/kb_qa/observability.py |
L01 |
| 全链路追踪(Langfuse + 降级) | src/kb_qa/tracing.py |
L02 | |
| 线上评估闭环(抽样+点踩队列) | src/kb_qa/online_eval.py + POST /api/feedback |
L03 | |
| 安全 | API 鉴权(key)+ 限流(滑动窗口) | src/kb_qa/auth.py |
L04 |
| Prompt 注入攻击测试集 | eval/attack_set.json + eval/run_attack.py |
L05 | |
| 输入输出守护栏(纵深防御) | src/kb_qa/guardrails.py |
L06 | |
| 集成 | MCP Server(知识库成标准工具) | mcp_server.py |
L08 |
| 性能成本 | 语义缓存(同义命中) | src/kb_qa/semantic_cache.py |
L10 |
| 压测基线(QPS/P95) | loadtest/run_loadtest.py |
L11 | |
| 成本/质量选型报告 | eval/run_cost_eval.py |
L12 |
完整的生产上线检查清单见 ops-lessons/L13(36 项,每项指向代码证据)。
一句话:能回答「上线之后怎么知道好不好、怎么防攻击、怎么被集成、怎么控成本」——这是「做过 demo」和「运维过生产 AI 服务」的分界线。
多模态文档智能(v3)
本项目经 doc-intelligence-lessons(多模态文档智能课程,10 课)升级,从「只能吃纯文本」推进到「能吃扫描件/表格/图表、引用可回溯到原文档页码与区域」的多模态文档智能系统 v3。
真实企业知识库不是纯 md/txt——是扫描合同、带合并单元格的制度表、只把数字画进图里的经营简报。文本 RAG 在这些内容上有天花板:扫描页抽出空文本、表格被拍平丢结构、图表整个丢弃。v3 补齐这块。
五层架构(解析 → 理解 → 索引 → 生成 → 溯源)
⑤ 溯源层 引用 = 文档名+页码+区域(bbox),可回原图核对 ← L06
④ 生成层 按 element_type 路由:image→现场看图/table→结构化 ← L04,L05
③ 索引层 描述索引入统一 Chroma,metadata 带类型 ← L05
② 理解层 图表→VLM 描述 / 扫描→OCR / 表格→结构化(成本-精度路由)← L02-L04
① 解析层 PDF→Element(type, page, bbox) 流,版面感知分类 ← L01
─────────────────────────────────────────────────────────
入口扩展:语音(L07,enable_voice)ASR→主链路→TTS
能力与开关(全部默认关闭,任一关掉回退纯文本行为)
| 能力 | 代码 | 课程 | 开关(默认) | 关掉时降级 |
|---|---|---|---|---|
| 版面感知解析 | src/kb_qa/doc_parser.py |
L01 | enable_multimodal_ingest(off) |
只吃 md/txt(现状) |
| 表格结构化 | doc_parser.py |
L02 | table_format(markdown) |
— |
| 置信度 OCR | src/kb_qa/ocr.py |
L03 | ocr_engine(off) |
扫描页抽空(现状) |
| 图表 VLM 理解 | src/kb_qa/vision.py |
L04 | enable_image_caption(off) |
图表元素不入库 |
| 多模态检索 | service.py(描述入统一索引,检索层零改动) |
L05 | 随 enable_image_caption |
纯文本检索 |
| 页码+区域引用 | src/kb_qa/citation.py/generate.py |
L06 | 随多模态入库 | chunk 文本引用(现状) |
| 语音入口 | src/kb_qa/voice.py/api/main.py |
L07 | enable_voice(off) |
端点 404 |
🎯 两条设计主线:①成本-精度——本地 OCR/RapidOCR 打头阵(免费),低置信度页才升级 glm-4v 直读;图表描述入库缓存(按内容哈希去重),命中后才现场看图。钱花在刀刃上。②溯源——多模态材料(表格数字/图表读数/OCR 文本)都是「转换的产物」,转换就可能错,所以引用必须能一键回到原图核对。这是作品集「可信度三部曲」的第三步:数字可复算(research-assistant 代码解释器)→ 来源可回访(浏览器证据链)→ 引用可回溯(本项目页码+区域)。
收益(对照 text-only 基线,毒文档集实测)
| 题型 | text-only 基线 | v3 全开 | 说明 |
|---|---|---|---|
| 纯文本 | 100% | 100% | 防退化:多模态升级不伤老能力 |
| 表格题 | 0% | ✅ | 结构化表示进上下文 |
| 扫描题 | 0% | ✅ | OCR 填充文本层 |
| 图表题 | 0% | ✅ | VLM 读图入索引 |
毒文档集(4 类杀手页 PDF + 15 道 golden 题)见 data/multimodal_docs/,生成脚本可复现。逐机制收益表见 eval/run_multimodal_eval.py。诚实标注:ragas 在多模态下能验证「答案忠于描述」,但验证不了「描述忠于原图」——这个盲区靠 L04 的 VLM 描述抽查兜底。
一句话:kb-qa 经历三个版本——能跑的 RAG(v1)→ 运维就绪(v2)→ 多模态文档智能(v3)。扫描件、表格、图表都能吃,引用回溯到页码和区域,每个机制默认关、可降级,共 143 个测试(含多模态新增)全绿。