把企业散落在 PDF、Word、表格、网页里的文档,变成一个能语义检索、能多跳推理、能追问出处的知识入口——不是「文档搜索框」,而是「读得懂、引得出、也敢说不知道」的企业知识层。
产品案例 · 企业级 AI 应用 · 2026

| 项 | 内容 |
|---|---|
| 类型 | 产品案例 · 企业级 AI 应用(LLM 知识库) |
| 场景 | 企业级文档理解、语义检索、智能推理 |
| 交互形态 | 上传文档 → 自然语言提问 → 带原文引用的回答 → 多轮追问 |
| 输入 | 非结构化文档(PDF / Word / Excel / PPT / 网页)与口语化问题 |
| 输出 | 结论 + 引用来源(可点回原文)+ 置信/拒答判定 |
| 技术栈 | FastAPI 服务端 · React 管理后台 · 向量库 + 全文索引 · LLM 推理 |
| 部署形态 | 私有化部署,文档与向量数据不出企业内网 |
| 管理后台 | 文档库管理、切块预览、检索调试、问答效果评测 |
企业里真正的知识问题不是「没文档」,是文档在里面,但没人找得到、也没人敢信。典型四条:
这个产品的目标就是把上面四条一次性接住:理解文档 → 找到依据 → 给出可溯源的回答,找不到依据就明说找不到。
文档入库 → 解析与结构化切块 → 向量化 + 全文索引
→ 混合召回 → 融合排序 → 重排精选 → 上下文组装
→ LLM 生成带引用的回答 → 溯源校验 → 输出 or 拒答
用户的体感是一句话:「问它,它给你答案,还告诉你这句话是从第几页第几段来的。」
向量召回擅长「意思相近」,但企业问答里大量存在精确匹配刚需:编号、型号、金额、条款号、专有名词。这些恰好是向量最弱的地方——把「GB/T 1234」和「GB/T 1235」映射到几乎同一个位置。
所以不做单一检索,做混合召回:向量检索 + 关键词检索(BM25)各出一路候选,再用 RRF(Reciprocal Rank Fusion)把两路按排名倒数融合。关键点是不做分数归一化——两路的相似度量纲根本不同,硬归一化等于引入一个假的权重;RRF 只吃排名,天然免疫这个问题。
混合召回为了保召回率,候选池是刻意放宽的(比如 20 条),但全塞进上下文既贵又吵——噪声片段会把模型带偏。
解法是加一层 Cross-Encoder 重排:把每个候选片段和问题成对送进模型打分,而不是像向量那样各算各的再比距离。代价是慢,所以只对 Top-N 做,用它把 20 条压到 3–5 条真正相关的。召回宽松、精排严格,这两步的分工不能混。
最省事的做法是按固定字数切——结果是表格被切断、条款被拆到两半、标题和正文分家。检索出来全是残句。
改成结构感知切块:先按文档自身的层级(标题 / 小节 / 表格 / 列表)切,只在层级内部再按长度兜底;同时给每个块带上「所属章节路径」的元信息,让一个片段在检索时还能知道自己「从哪来」。块大小也不是一个常数——长制度条文和短 FAQ 需要的粒度不同。
这是整个项目里最反直觉的一条。
直觉上的解法是「提示词里写上:不确定就说不确定」。但工程上立刻会撞到一堵墙:你没有一个可靠手段去检测它此刻是不是在编。 模型编造的条款读起来和真的条款完全一样,语气一样笃定,格式一样规范。指望模型自我报告,等于把质检交给被质检的人。
所以不从「检测」下手,从流程下手,让它想编也编不出来:
产品的态度是:一个会说「我不知道」的知识库,比一个什么都能答的聊天框有用。
企业知识库天然是多租户的:HR 的制度、研发的设计文档、财务的台账,不该互相看见。
风险在于如果只在界面上过滤,等于把不该看的文档召回到了上下文里再藏起来——模型照样可能把内容说出去。
做法是权限前置到检索:过滤条件(部门 / 角色 / 密级 / 文档标签)在查询阶段就进检索过滤,不允许越权的块进入候选池。模型看不到的东西,它就不可能说出来。
没有评测集,改切块参数、换模型、调 Top-K 全是玄学——这次感觉好了,换个问题又崩了。
所以后台里带一套评测能力:固定一批真实问题 + 人工标注的标准答案/来源文档,每次改动跑一遍,对比召回命中率和回答准确率。先能量化,才谈得上优化。
┌─────────────── React 管理后台 ───────────────┐
│ 文档库管理 · 切块预览 · 检索调试 · 评测看板 │
└────────────────────┬───────────────────────────┘
│ HTTP
┌──────────────▼──────────────┐
│ FastAPI 服务端 │
├─────────────────────────────┤
入库链路 │ 解析器 → 结构切块 → 向量化 │
│ 索引写入(向量 + 全文) │
├─────────────────────────────┤
查询链路 │ 查询改写 → 混合召回 → RRF │
│ → Cross-Encoder 重排 │
│ → 上下文组装 → LLM 生成 │
│ → 引用校验 → 输出 / 拒答 │
└──────┬───────────────┬──────┘
│ │
向量数据库(embedding) 全文索引(BM25)
│ │
┌────┴───────────────┴────┐
│ PostgreSQL / 对象存储 │
└─────────────────────────┘
| 模块 | 职责 |
|---|---|
| 文档解析 | 多格式归一化,扫描件 OCR,表格与层级结构保留 |
| 结构切块 | 按文档层级切分,块携带章节路径与来源元信息 |
| 混合召回 | 向量检索 + BM25 并行,各自返回排名候选 |
| 融合排序 | RRF 按排名融合,不做跨量纲分数归一化 |
| 精排 | Cross-Encoder 对候选成对打分,压缩到最终上下文 |
| 生成与溯源 | 约束引用来源、反查引用合法性、低置信走拒答 |
| 权限控制 | 检索阶段过滤,越权内容不进入候选池 |
| 阶段 | 做什么 | 为什么这么做 |
|---|---|---|
| 查询处理 | 口语化问题归一化 / 改写 | 用户问法 ≠ 文档写法,需要先拉平 |
| 混合召回 | 向量路 + 关键词路并行,各取 Top-N | 语义覆盖 + 精确命中,互补而非替代 |
| RRF 融合 | 按排名倒数求和重排 | 绕过不同检索器的分数量纲不可比问题 |
| 重排 | Cross-Encoder 精排,保留 3–5 条 | 保召回的前提下把噪声挤出上下文 |
| 上下文组装 | 片段 + 来源元信息一并给模型 | 让模型「知道自己在引用什么」 |
| 生成 + 校验 | 生成后反查引用是否真实存在 | 出处对不上就重生成 / 降级 |
| 兜底 | 分数不足 → 拒答 | 宁可说不知道,不产出猜测 |