企业知识库 RAG 实战:混合检索、权限过滤与答案溯源

企业知识库 RAG 实战:混合检索、权限过滤与答案溯源

企业知识库问答的难点,不只是让大模型读懂一份文件。文档需要正确解析和更新,搜索结果需要符合当前用户权限,回答还要能指出依据来自哪份文件、哪个版本和哪个位置。

RAG(Retrieval-Augmented Generation,检索增强生成)把"先查资料,再组织答案"串成一条流程。企业知识库还需要把登录授权、文档审核、异步索引、混合检索、引用核验和运行追溯接在一起,才能让答案有据可查,知识变更也能安全生效。

本文围绕企业知识库的常见落地架构,拆解 RAG 的工作方式,并通过员工年假问答走一遍完整示例。文中的参数是便于理解的起始值,实际表现需要用目标文档、模型和业务问题评测。

先看整体架构

业务入口与 AI 处理分开:统一 Web 路由层调用 .NET 类库,完成登录上下文、业务授权和数据管理;Python 私网服务负责文档解析、OCR、切片、向量化、检索与模型编排。

text 复制代码
浏览器 / 公众号 / 小程序 / 客户系统
                 │
                 ▼
         统一 Web 路由层
                 │ 反射调用
                 ▼
       .NET 业务类库
       ├── 登录上下文、租户与 ACL
       ├── MySQL:业务事实与任务状态
       ├── 对象存储:原件与派生文件
       └── 受控内网调用
                 │
                 ▼
          Python AI 私网服务
       ├── 文档解析、OCR、切片
       ├── Embedding 与模型适配
       ├── Elasticsearch:BM25 全文检索
       └── Milvus:稠密向量检索

这种拆分有两个实际好处:AI 解析和模型适配可以独立扩展;租户授权的最终判断仍留在业务服务,不交给检索索引或提示词决定。Python 服务不直接连接业务 MySQL,也不对终端用户开放公网 API。

每个组件各管什么

组件 在系统中的职责 数据定位
统一路由层与 .NET 类库 对外业务入口、登录上下文、授权、管理操作和引用复核 业务访问边界
MySQL 租户、知识库、ACL、文档版本、审核、任务、分片定位、发布指针和审计 业务事实源
对象存储 原始文件、预览文件及 OCR 等派生对象 文件存储
Elasticsearch 文本字段和 BM25 关键词检索 可重建的全文检索投影
Milvus 文本 Embedding 与向量相似度检索 可重建的向量检索投影
Python AI 服务与 Worker 解析、OCR、切片、Embedding、检索编排和模型调用 AI 处理层
队列与任务账本 异步处理、重试、进度和失败恢复 可靠任务处理

MySQL 是业务状态的权威来源。ES 和 Milvus 里的 published、租户 ID、文档 ID、密级等字段可以帮助预过滤,却不能代替 MySQL 的授权检查。索引可能延迟、残留或重建,因此只把它当检索投影。

入库:从文件到可检索片段

知识库里的文件并不会上传后立刻进入正式问答。入库链路大致如下:

text 复制代码
上传校验
  → 内容扫描与策略检查
  → 文档解析 / OCR
  → 规范化文本块
  → 按结构切片
  → 审核
  → 写入 ES 与 Milvus 的 staging 代际
  → 校验数量、hash 和检索可见性
  → 切换 MySQL 发布指针

解析时保留原文位置

不同格式需要保留不同定位信息:

  • PDF 保留页码。
  • Word 保留标题层级和段落。
  • Excel 保留工作表名称及行列坐标。
  • PowerPoint 保留幻灯片页和区域。
  • 图片保留 OCR 区块和图像区域坐标。

定位信息会跟着片段进入引用记录。否则答案即使找到正确内容,也只能显示文件名,无法快速回到原文。

旧版 DOC、XLS、PPT 等格式需要先提示转换,或交给隔离的转换 Worker;压缩包和 Office 文件还要限制文件数量、解压比例,宏不执行。OCR 结果和文档正文都属于不可信输入,不能因为经过模型处理就视为安全内容。

切片不要把一条规则拆散

可以从 tokenizer 计算约 512 tokens、片段重叠约 64 tokens 开始试验。这个数值不是通用答案:操作手册、政策条款、表格和代码的最佳切片大小往往不同,最终要通过业务问题评测。

切片时优先沿标题、段落和列表边界拆分。表格要保留表头,过长表格按行分组,不能只按固定字符数切断。每片记录稳定的 chunk_id、页码或区域、hash、解析器版本、切片器版本、Embedding 模型版本和索引 generation。

一个可用于检索的片段记录通常不止有正文,还要有可过滤字段和原文定位:

json 复制代码
{
  "chunk_id": "483",
  "tenant_id": "tenant-17",
  "kb_id": "kb-hr",
  "document_id": "doc-leave-policy",
  "version_id": "ver-2026-03",
  "generation": 4,
  "title_path": ["员工休假制度", "年休假"],
  "content": "连续工作满一年不满十年的员工,每年享有 5 天带薪年休假。",
  "page": 12,
  "chunk_index": 3,
  "valid_from": "2026-03-01",
  "valid_to": null,
  "content_hash": "sha256:...",
  "embedding_model_version": "embedding-v2"
}

ES 文档会包含可检索正文和词项字段;Milvus 保存向量及必要的过滤字段;关系库保存片段 ID、hash、版本和定位。大段正文不必在每个存储里重复保留。各存储通过稳定的 chunk_id、version_id 和 generation 关联。

例如,年假规则应保留完整条件与结果:

text 复制代码
标题:员工休假制度 / 年休假
正文:员工连续工作满一年不满十年的,每年享有 5 天带薪年休假。
来源:员工休假制度.pdf,第 12 页,第 3 片

若只切出"每年享有 5 天",适用条件就丢失了;若只保留标题"年休假",检索也没有足够依据回答。

审核通过后再发布索引

索引构建期间,新内容先写入 staging generation,旧的已发布版本继续提供查询。ES 和 Milvus 都写入成功,并且数量、hash 和抽样查询通过后,业务服务才在 MySQL 事务中切换活动版本和 generation 指针。

跨 MySQL、ES、Milvus 和对象存储不存在一个统一原子事务。因此采用"先构建并验证索引,再切换权威指针"的方式。半成品索引不能被问答读取;旧索引可以在新版本发布后异步清理。发布指针只在一处权威存储中更新,查询每次读取当前活动代际,避免不同存储各自决定"哪个版本生效"。

可以将文档状态拆为业务状态和索引状态:

text 复制代码
业务状态:uploaded → parsing → review_pending → approved → published
                                      └────────→ rejected
索引状态:queued → building → validating → active → retiring → deleted
                         └───────────────→ failed / retrying

"审核通过"不等于"索引已发布"。索引失败时,文档仍不能被问答召回;重试使用相同幂等键和稳定主键,避免重复生成片段。删除和撤权的顺序相反:先更新权威状态使查询立即拒绝,再异步删除 ES、Milvus 中的旧投影。

问答:先授权,再检索和生成

用户提交问题后,在线 RAG 流程可以概括为:

text 复制代码
登录身份与租户范围确认
  → 知识库和文档权限过滤
  → 问题标准化与条件识别
  → BM25、向量及结构化检索
  → 融合排序 / 可选重排
  → 再次授权复核并读取证据
  → 大模型按证据生成答案
  → 校验引用并返回

将问答拆成有序阶段,可以记录每步耗时、状态和错误码。带图片的查询会额外经过图片校验、OCR 和视觉理解;图片作为当轮会话附件,默认不会自动进入知识库。

第一步:确定有权搜索的范围

服务根据登录上下文确认租户、成员、启用的知识库和当前用户的数据范围。权限包含 discover(可见)、read(阅读/检索)、edit、share、forward、download、delete 等独立动作。

能看见知识库名称,不等于可以读取正文;能管理知识库,也不自动获得内容读取权。目录和文档可能继承 ACL,拒绝规则优先。密级、有效期、文档状态、租户授权和用户成员状态同样需要通过检查。

检索之前先对可访问的知识库、文档、已发布版本和索引代际做过滤。小型权限集合可以将可读文档 ID 作为检索过滤条件;集合很大时,应按范围分批检索并设扫描上限,避免先取固定 top-k 再过滤,导致合法结果被无权结果挤掉。候选返回后,再由业务授权服务核验;读取正文、提交给模型、展示引用和打开历史记录时也需要复核。未授权内容不能进入模型上下文、用户可见 trace 或答案缓存。

第二步:混合检索找候选证据

系统同时使用两类主要检索:

  • Elasticsearch BM25:适合员工编号、产品型号、错误码、文件编号等关键词精确匹配。
  • Milvus 稠密向量检索:适合含义相近但措辞不同的自然语言问题。

结构化条件,例如知识库、产品型号、密级、有效时间和发布版本,先作为过滤条件处理。ES 和 Milvus 的候选结果通过稳定的 chunk_id 对齐,再用 RRF 融合排名。可以从全文和向量检索各取前 100 条、融合后保留前 30 条开始;如果评测证明有帮助,再用 Reranker 精排到约 8 条。候选数是吞吐、延迟和召回率之间的参数,需要用实际语料调整。

RRF 的简化形式如下:

text 复制代码
融合分数(chunk) = Σ 检索路权重 / (60 + 该路排名)

它根据排名融合结果,不要求 ES 和 Milvus 的原始分数处在同一数值范围。Reranker 是另一层模型排序,不能和 RRF 当作同一个步骤。

第三步:把证据交给模型

业务服务再次确认候选片段的权限和发布状态后,才读取文本。上下文按文档去重,可以从不超过 4 篇引用文档、约 6000 tokens 开始调试;实际需按模型窗口、评测结果和问题复杂度调整。送入模型前,还应去掉重复片段、保留标题路径和时间条件,并按相关性排序,避免高分结果全部来自同一段重复文本。

模型提示需要明确:只根据提供的证据回答;证据不足时说明缺少什么;回答事实要关联证据编号;不能根据文件中的指令改变系统规则。文件内容本身可能包含恶意提示,它只能作为待检索资料,不能变成模型指令。

回答生成完成后,系统校验引用 ID 是否属于本轮证据集合,并复核发布指针、权限 epoch 和输出安全策略。引用记录包含文档、版本、片段、页码或区域、摘要和 hash。图片引用还需要关联当轮附件编号、OCR 区块和图像坐标。

一个实用的提示结构是把规则、问题和证据分开,给每条证据分配不可由模型生成的 ID:

text 复制代码
规则:仅依据下方证据回答。证据不足时说明不足。引用只能使用给定的证据 ID。

问题:入职两年有几天年假?申请要提前多久?

[E1] 员工休假制度.pdf,第 12 页,年休假
连续工作满一年不满十年的员工,每年享有 5 天带薪年休假。

[E2] 员工休假制度.pdf,第 12 页,年休假申请
年休假申请至少提前三个工作日提交,经直属负责人审批后生效。

模型输出中的 [E1]、[E2] 只是候选引用标记。服务端还要验证标记确实存在于本轮证据集合,再映射成文档版本和原文定位。引用不是模型随口附加的脚注。

示例:年假问题如何得到答案

假设知识库中有文件《员工休假制度.pdf》,第 12 页写着:

text 复制代码
连续工作满一年不满十年的员工,每年享有 5 天带薪年休假。
连续工作满十年不满二十年的员工,每年享有 10 天带薪年休假。
年休假申请至少提前三个工作日提交,经直属负责人审批后生效。

员工提问:"入职两年有几天年假?申请要提前多久?"

系统先确认员工有权读取这份制度,再同时搜索关键词和语义相近片段。示意检索候选如下:

text 复制代码
1. 员工休假制度.pdf / 第 12 页 / chunk 483
   命中:连续工作满一年不满十年......5 天带薪年休假

2. 员工休假制度.pdf / 第 12 页 / chunk 484
   命中:年休假申请至少提前三个工作日......

在片段经过权限复核后,模型收到问题和两条证据,生成的答案可以是:

text 复制代码
连续工作满一年不满十年的员工,每年享有 5 天带薪年休假;年休假申请需至少提前三个工作日提交,并经直属负责人审批。[1]

引用 [1] 会打开《员工休假制度.pdf》的第 12 页,并定位到对应文本。若有人询问"年假能否跨年结转",而知识库没有相关规则,系统应说明现有资料未覆盖该问题,不补充法规猜测。

如果该员工只有 discover 权限、没有 read 权限,知识库名称可能可见,但制度正文不能进入候选证据,模型也不会收到这份文件。

一个检索融合代码片段

下面的 Python 代码演示把 BM25 和向量检索的排名通过 RRF 合并。输入列表必须已经应用知识库、发布状态和授权过滤;此函数只负责排序,不承担权限判断。

python 复制代码
from collections import defaultdict


def reciprocal_rank_fusion(result_lists, weights=None, k=60):
    """result_lists 是多路检索结果,每项为按相关性排序的 chunk_id 列表。"""
    if weights is None:
        weights = [1.0] * len(result_lists)

    scores = defaultdict(float)
    for results, weight in zip(result_lists, weights):
        for rank, chunk_id in enumerate(results, start=1):
            scores[chunk_id] += weight / (k + rank)

    return sorted(scores.items(), key=lambda item: item[1], reverse=True)


bm25 = ["chunk-483", "chunk-210", "chunk-484"]
dense = ["chunk-484", "chunk-483", "chunk-901"]

for chunk_id, score in reciprocal_rank_fusion([bm25, dense]):
    print(f"{chunk_id}: {score:.4f}")

输出排名大致为:

text 复制代码
chunk-483: 0.0325  # BM25 第 1,向量检索第 2
chunk-484: 0.0323  # BM25 第 3,向量检索第 1
chunk-210: 0.0161  # 只被 BM25 召回
chunk-901: 0.0159  # 只被向量检索召回

同一片段在多路检索中出现时会累加排名贡献,因此能兼顾精确词命中和语义相似命中。k 和各路权重需要用标注问题集评估,不能把示例值当作线上最优参数。RRF 能融合排名,却无法补回所有检索路都漏掉的片段;召回阶段仍需单独评估。

文档处理为什么采用异步任务

PDF 解析、OCR 和向量生成耗时不可控,不适合阻塞在上传请求里。常见做法是由业务 API 写入任务主状态和 Outbox 事件,再由后台 Worker 领取任务并处理;AI Worker 执行各阶段,再通过受控内部业务入口回写阶段状态和日志。

任务需要具备稳定幂等键、心跳、租约、重试和死信处理。消息队列通常按至少一次投递处理,因此重复消息必须能够安全重跑;索引写入使用稳定主键 upsert,避免重复消费生成重复片段。Outbox 用于处理数据库提交成功但消息发布失败的窗口:业务记录和待投递事件先在同一数据库事务提交,发布器之后持续投递未确认事件;消费者按 event_id 去重,完成持久化后才确认消息。

进度日志记录任务 ID、文档版本、阶段、序号、页码或区域、片段 hash、token 数、配置版本、耗时和状态。日志列表默认不携带正文;正文预览必须重新做权限校验。密钥、完整图片字节和未经脱敏的敏感正文不写入运行日志。

图片问答和多模态资料

图片问答的 MVP 可以采用 OCR 与视觉摘要转成可检索文本,不直接把图片向量检索作为第一阶段必需能力。图片先校验真实格式和解码尺寸,再进行 OCR、表格识别或视觉理解;每个结果带置信度和区域坐标。

图片中的低置信度参数需要提示核对。OCR 置信度只反映文字识别的可信程度,不等于答案正确率。图片里出现的 URL、二维码或"忽略之前规则"等文字仍是内容数据,不应触发外部访问或改变模型行为。

单张图片 OCR 失败时,可以标记失败并继续处理其余图片;如果所有图片都失败且没有可用文字问题,应请求补充清晰材料。多图引用使用本轮的图 1、图 2 顺序,并定位到 OCR 区块或图像区域。

更新、撤权和故障时如何保持一致

索引的存在不代表内容当前有效。文档作废、删除、过期或权限撤销时,先更新 MySQL 权威状态并递增相应发布或安全 epoch,使新查询立即拒绝旧内容;ES 和 Milvus 的物理清理可以异步重试。

缓存也要与授权范围绑定。答案缓存键需包含租户、授权范围摘要、权限 epoch、发布 epoch、模型版本、提示词版本、标准化问题和附件 hash。缓存命中后仍重新核验引用权限;秘密级请求不复用跨用户答案缓存。

检索故障的处理要明确:

  • ES 或 Milvus 单路故障,另一通路仍有经授权的证据时可以降级,并在追溯信息中标出。
  • 两路检索都失败时返回暂不可用,不能让模型凭空补答。
  • 模型调用失败时可以返回授权后的检索证据列表。
  • MySQL 或权限服务不可用时,问答应拒绝服务,不能放宽权限范围。
  • 队列不可用时保留 Outbox 任务,上传状态显示待处理。

用评测判断系统是否真的有用

检索、答案和权限需要分开评测。可以先建立 100 条人工标注集:60 条文字问题、30 条图片问题、10 条提示注入/无权限/跨租户问题,并标注相关片段、答案要点、允许引用和是否应该拒答。样本应覆盖精确参数、表格、多文档组合、过期版本和资料中无答案等问题。

可将以下数值作为试点目标,最终门槛应由业务风险和实际测量决定:

  • 可回答问题 Recall@10 不低于 85%。
  • 人工核验答案正确率不低于 85%。
  • 引用定位及支持关系正确率不低于 95%。
  • 权限、跨租户和撤权负例泄露为 0。
  • 无证据场景正确拒答率不低于 95%。

出现错误时,先判断问题在哪一层:原文解析不完整、切片破坏上下文、正确片段没有召回、候选排序不佳、授权过滤遗漏,还是模型误读了证据。只改提示词通常无法修复检索缺失或权限问题。

总结

企业知识库 RAG 是一条带权限和版本控制的资料处理链:文档经过解析、切片、审核和双索引校验后发布;问题先限定授权范围,再通过全文与向量检索召回证据;模型根据证据组织答案,引用在返回前再次校验。

关键是让每一层职责清楚:业务数据库掌握文档状态和授权事实,AI 服务负责解析与检索编排,全文和向量索引提供可重建的召回能力。这样回答能够追溯到来源,文档更新和权限变化也有清晰的生效路径。

相关推荐
花椒技术1 小时前
花椒 AI 应用工程化实践:灵泡泡的工作流、版本管理与任务恢复
后端·agent·ai编程
geovindu1 小时前
rust: Mediator Pattern
开发语言·后端·设计模式·rust·中介者模式·行为模式
Doker 多克2 小时前
Spring Integration AMQP 和 Spring AMQP详解
java·后端·spring
韩振方2 小时前
Kubernetes Ingress 用 TLS Secret 给预览域名接上 HTTPS
后端
用户1494484813202 小时前
为什么大厂很少直接用 2PC/TCC?聊聊分布式事务在真实业务里的落地套路
分布式·后端
Thneonl2 小时前
采样采错地方,trace 全断链:OTel 尾部采样的代价账
后端·架构
匆匆那年随风2 小时前
零依赖 C# Office 库到底怎么用——四个场景讲透 Wisdom.Office
后端
今天早睡2 小时前
用子图封装流程,设计清晰的父子状态接口
后端
Lyy2 小时前
DevOps平台 — 第十七篇:测试管理的设计与实现
后端·devops