怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

这一页比其他项目页更容易被问穿,先看这段 RAG 的量化指标(召回率、准确率)需要标注数据才能算,没真做过的话这些数字编不圆——面试官会问你标了多少条、谁标的、标注一致性怎么保证。所以:这三个模块比任何模块都更需要你真的动手做过,哪怕只是拿几百份文档搭个本地的检索问答。没做过的话,八股和场景两页就够了。
项目背景设定 企业服务 SaaS 的客户知识库问答,Java + Spring Boot + 向量库 + Elasticsearch,多租户。客户的文档在他们自己的系统里(Wiki / 网盘 / 工单库),需要增量同步过来建索引。
为什么选这三个模块 RAG 系统的质量上限由检索决定,模型再强也救不回错误的资料。而检索质量又主要由入库阶段决定——分块切坏了,后面所有调优都是徒劳。所以这三块的顺序就是排查问题的顺序:先看入库、再看检索、最后看生成。共同点是每一块都有可验证的判据,不是「我用了向量数据库」能糊过去的。

模块一:文档入库与分块治理

  1. 文档入库与分块治理(语义边界切分 + 结构路径前缀 + 整份重建 + 同步对账)★★★
    简历这样写 知识库入库与分块流水线(Spring Boot + 向量库 + 增量同步 + 内容哈希对账):按文档自身结构(标题层级、段落、表格行)切分而非固定字数,每个块拼上结构路径前缀参与向量化(让「该功能需要企业版授权」这类片段脱离上下文后语义仍完整);更新采用按文档 ID 整份删除重建而非按块增量(分块数量会随内容变化,增量会留下参与检索的孤儿块);同步用稳定外部 ID 做映射不用路径标题,并以内容哈希做定期对账。改造后「文档已更新但答案仍是旧内容」的问题定位到孤儿块并消除,对账每轮发现的不一致条目稳定在个位数并可自动修复。
    展开完整拆解
    为什么要这么设计

    第一版是照着教程做的:按固定字数切块、加一点重叠、向量化入库。用测试文档跑得很好,接了真实客户的知识库之后问题成片出现。

    一是切出来的块没有语义。「该功能需要企业版授权」被单独切成一块,检索到了也没用——不知道「该功能」指什么。更隐蔽的是指代关系断裂:段落里的「上述配置」「它」跨块之后彻底失去指向,这类块在库里是噪音,还会挤占 top-k 的位置。

    二是表格被切烂了。按字数切把表头和数据行分开,切完的数据行完全无法理解——一行数字没有列名。而企业知识库里恰恰有大量表格(价格表、参数对照、权限矩阵)。

    三是文档更新之后旧内容还在。这是最严重的一个,也是最难查的。更新逻辑是「按块 ID 覆盖」,但客户改了内容之后分块数量变了——原来 10 块现在 8 块,覆盖了前 8 个,后面 2 个旧块留在库里继续参与检索。表现是「文档明明改了,Agent 还在答旧的」,而你去查库能看到新内容确实在,于是完全找不到原因。

    四是同步用文档路径做标识。客户重命名了一个目录,整批文档被判定为「旧的删除 + 新的新增」,全量重新向量化,成本和延迟都爆了,而且中间有一段时间检索不到。

    所以第二版重做了四件事:按语义边界切分给每个块补上结构路径前缀更新改成整份重建同步改用稳定外部 ID 并加对账。其中第三条是纯粹的正确性修复,另外三条都是质量提升。

    最值得说的判断是第二条:与其把切分算法做得更精细,不如先把丢掉的上下文补回去。结构路径前缀的成本极低(几个字),但它把「该功能需要企业版授权」变成了「产品手册 > 权限管理 > 数据导出:该功能需要企业版授权」,语义立刻完整。这条改动的收益比调切分参数大得多。

    整体链路
    同步(增量) │ ├─ 用稳定外部 ID 做映射,不用路径 / 标题 │ 客户重命名目录不会被判定为删除加新增 │ ├─ 变更判定:外部 mtime 不可信(有系统改内容不改 mtime) │ 以内容哈希为准 ─▶ 哈希变了才重建 │ ├─ 权限元数据一并同步 │ 客户内部文档本身有权限(某目录只有 HR 可见) │ 不同步权限就会出现「越权在租户内部发生」 │ └─ 逐条记录处理结果:成功 / 跳过 / 失败 + 原因 只记任务级状态是不够的,失败会被静默吞掉 解析 │ ├─ 按类型走不同解析器,产出统一的结构化中间表示 │ 标题层级 / 段落 / 列表 / 表格 / 代码块 │ ├─ 解析产出为空或疑似乱码 ─▶ 拦下告警,不往下走 │ 格式变化(Word 换 PDF)会静默产出空内容 │ └─ 元数据与批注单独存,不参与检索 注入常藏在批注和不可见字符里 分块(按语义边界,不按固定字数) │ ├─ 优先用文档自身结构:标题 ─▶ 段落 ─▶ 列表项 │ ├─ 单段过长才在段内按句子边界二次切 │ ├─ 表格整块保留;过大则按行切但每行都带表头 │ ├─ 相邻块留少量重叠,避免关键句正好落在边界 │ └─ 每块拼结构路径前缀参与向量化 文档名 + 章节路径 + 正文,让块脱离上下文后仍可理解 写入(整份重建,不按块增量) │ ├─ 按文档 ID 删除全部旧块 ─▶ 写入全部新块 │ 分块数量会变,按块覆盖会留下孤儿块继续参与检索 │ ├─ 删与写在同一逻辑事务内,失败则整份回滚 │ 避免出现「删了没写成」的空洞 │ └─ 写入后校验块数与预期一致 对账(定期) │ ├─ 比对源系统清单与库内清单:缺失 / 多余 / 内容哈希不一致 │ ├─ 缺失与哈希不一致 ─▶ 触发重建(可自动) │ └─ 多余(源已删除)─▶ 清理并记录 不自动改数的只有一种情况:块数异常偏离,先告警人工看
    分步拆解
    1. 同步标识必须用稳定外部 ID。路径和标题都是客户随时会改的东西,用它们做主键,一次重命名就是一次全量重建。这和企业服务里组织架构同步的坑是同一个——那边是部门路径,这边是文档路径,本质都是「用可变属性做标识」。
    2. 变更判定以内容哈希为准,不信外部 mtime。有些文档系统改内容不更新修改时间,靠 mtime 做增量会永久漏掉这份文档的更新。哈希的成本只是读一遍内容,值得。
    3. 解析失败要拦下,不能静默继续。格式变化导致提取出空内容是很常见的,如果流程不报错继续往下走,结果是库里有一份空文档,检索永远召不回而且没人知道为什么。产出为空、长度骤减、乱码字符占比过高,三个条件任一命中就拦。
    4. 按语义边界切,不按固定字数切。技术文档和知识库文章通常结构化得不错,标题就是天然的语义边界。固定字数是「不看内容」的切法,必然切在错误的位置。
    5. 表格必须特殊处理。整块保留,或者按行切但每行都带上表头。这一条不做,所有涉及表格的问题都答不对,而价格、参数、权限这类高频问题恰恰都在表格里。
    6. 结构路径前缀是性价比最高的一条。把「文档名 > 章节 > 小节」拼在块的开头参与向量化。成本是每块多几个 token,收益是解决了一大类「片段脱离上下文无意义」的问题。
    7. 更新必须整份重建,这是正确性问题不是优化。按块增量在分块数量变化时会留下孤儿块,而孤儿块会继续参与检索,产生「答旧内容」的现象且极难定位。整份重建的代价是重复向量化未变的块,但换来的是确定性。
    8. 删与写要在一个逻辑事务里。删成功写失败会留下空洞——文档在源里存在,在库里消失了。如果向量库不支持事务,就用「先写新块打上新版本号、切换指针、再删旧版本」这种双版本切换的做法。
    9. 权限元数据要一起同步并在检索时生效。客户内部文档本身有权限,忽略它就会出现普通员工通过问答拿到 HR 文档内容。这是实际项目里最容易被客户安全评审拦下的点,比跨租户隔离更容易漏,因为它发生在租户内部。
    10. 对账是必需的,不是加分项。同步链路一定会丢,定期比对源清单和库内清单,按内容哈希发现不一致。缺失和哈希不一致可以自动修复(重建那一份是幂等的、无副作用的);只有块数异常偏离时才告警人工,因为那可能意味着解析器出了问题,自动重建会把错误的解析结果固化下来。
    关键决策与取舍

    整份重建 vs 按块增量,我们选了整份重建。增量在理论上更省——只有变化的段落需要重新向量化。但它要求「块与源内容的对应关系稳定」,而这个前提在真实文档上不成立:用户在中间插入一段,后面所有块的边界都变了,块 ID 和内容的对应关系全乱。为了维护这个对应关系要引入内容指纹匹配、块级 diff,复杂度很高而且一旦算错就是孤儿块。整份重建的代价是重复计算未变部分的向量(有成本但可预期),换来的是确定性——「库里只有当前版本的块」这个性质是靠删除全部旧块保证的,不依赖任何算法正确。这个取舍的原则和库存那边一样:宁可多做一点确定的工作,不要引入一个可能算错的优化。

    分块参数按文档类型分开配,不用一套打天下。同一个知识库里的 API 文档、政策条款、FAQ、工单记录,合理的切法完全不同——FAQ 天然一问一答就是一块,API 文档按接口切,政策条款按条款编号切。一套参数必然在某类文档上很差。代价是配置变多、需要人判断文档类型(我们用路径规则加人工标注结合)。但这个成本是一次性的,而切错的代价是持续的。

    对账发现不一致后允许自动修复,这和交易系统的做法不同,要说清为什么。交易对账发现偏差绝不自动改数,因为改错方向会造成二次资损。但知识库的重建是幂等且无副作用的——重新解析入库一份文档,最坏结果是还是错的,不会产生新的损害。所以这里可以自动修。唯一的例外是块数异常偏离时不自动修,因为那通常意味着解析器坏了,自动重建会把错误结果固化到库里,反而掩盖问题。「什么情况下允许自动修复」的判据是「修错了会不会产生不可逆后果」,这个判据比「要不要自动修」这个问题本身更值得说。

    踩过的坑一:重叠区导致同一段内容被召回两次。相邻块有重叠,用户问的内容正好在重叠区,两个块都被召回,top-k 里有两条几乎相同的内容,白占了一个位置。修法是在检索后做去重——按内容相似度合并高度重叠的候选,保留结构路径更精确的那个。这个坑说明重叠是有代价的,不是免费的保险

    踩过的坑二:解析器升级后没重建,新旧格式的块混在库里。换了 PDF 解析库,提取质量明显变好,但只对新增文档生效,历史文档还是旧解析结果。表现是「老文档答得差、新文档答得好」,排查了很久才想到是解析器版本问题。修法是给每个块记录解析器版本,并且支持按版本批量重建。这条和「模型版本要记录」是同一个道理——任何会影响产出的组件版本都要跟着数据存

    没做的部分:没做图片和扫描件的 OCR 入库。技术上可行,但 OCR 的错误率会引入一类很难处理的问题——识别错的文字看起来是正常内容,检索能召回,答案就是错的,而且无法察觉。当时的结论是宁可不支持,也不要引入不可察觉的错误源。如果要做,必须给 OCR 内容打上低可信标记并在答案里提示。

    数字是怎么测的

    分块质量怎么测,这是最容易被追问的地方。它没有现成指标,我们用的是可判定的代理指标:抽样一批块,人工标注「这个块脱离原文之后是否可理解」,算可理解比例。标注标准要写清(有明确主体、无未解析的指代、如果是表格行则带表头),否则不同人标出来不一致。报数字时必须说明抽了多少块、谁标的、标注一致性怎么校准——我们的做法是两人独立标一批,比对分歧再统一标准。这套说明比数字本身更能证明你真做过。

    孤儿块问题怎么验证修好了:这是正确性问题,用构造用例。造一份文档,第一版切成 N 块,改动内容让它切成 N-2 块,重新入库后断言库里该文档的块数正好是 N-2,且不存在属于该文档的旧版本块。这是个可以静态检查的性质,写成断言最可靠,不需要统计。

    对账不一致条目数:每轮对账发现的绝对条目数(个位数),不报一致率。理由和交易对账一样——一致率的分母(文档总数)会让分子看起来微不足道,反而掩盖问题,而且一被追问统计口径就说不清。还要区分三类分别报:缺失、多余、哈希不一致,它们指向不同的成因。

    同步延迟:从客户在源系统保存文档,到该文档能被检索命中的端到端时长。要说清同步任务的触发周期,否则这个数字没有意义——如果是每 10 分钟扫一次,那延迟的下限就是 10 分钟,报「平均 5 分钟」反而说明统计有问题。

    入库成功率:按「处理条数 / 成功 / 跳过 / 失败」四个数报,不要只报成功率。「跳过」这一类最需要单独看——它包含了「哈希未变所以跳过」(正常)和「解析为空所以跳过」(异常),混在一起就丢掉了诊断价值。我们踩过的坑就是这两种都被计为跳过,异常被正常淹没了。

    面试追问
    Q:结构路径前缀会不会污染向量?前缀在每个块里都出现,会不会让所有块都变相似? A:会有影响,所以前缀的粒度要控制。如果前缀只有文档名,那同一份文档的所有块都带同样的前缀,确实会让它们互相变相似,降低区分度。我们的做法是前缀带到最细的章节层级——「产品手册 > 权限管理 > 数据导出」,这样同一文档不同章节的块前缀是不同的,反而增加了区分度。另外前缀要短,占比控制在整块的一小部分,正文仍然是主要信号。更彻底的做法是把结构信息同时作为元数据字段存一份用于过滤,前缀只保留最必要的一两级——过滤是精确的,不受向量相似度影响。我们两个都做了:前缀用于让语义完整,元数据用于精确过滤。这个追问很好,它指向了「加信息也可能减信息」这个真实的权衡。
    Q:整份重建,如果一份文档特别大,重建期间检索会不会查不到? A:会,这是整份重建的固有窗口,必须处理。我们用双版本切换:先把新块以一个新的版本号写进去(此时新旧共存但检索只过滤当前版本),全部写成功后原子地切换该文档的当前版本号,再异步删除旧版本的块。这样检索侧看到的始终是一个完整的版本,没有空窗。代价是重建期间该文档的块占用双倍存储,以及需要一个版本号字段参与检索过滤。这个模式和搜索服务用别名切换做索引重建是同一个思路,只是粒度从整个索引降到了单份文档。如果向量库不支持按字段过滤,退化方案是接受短暂窗口但把重建放在低峰期,并且对大文档做拆分——不过我认为能做双版本就不要接受窗口,因为「客户刚改完文档就来验证结果」恰恰是最高频的场景。
    Q:客户文档的权限元数据同步过来了,但客户那边权限变了怎么办?总不能实时同步。 A:这里有个重要的判断:权限变更必须比内容变更同步得更及时,因为它的失效方向是不安全的。内容晚同步十分钟,用户看到旧答案,损害有限;权限晚同步十分钟,用户可能看到不该看的内容,这是安全事故。所以我们把两者拆成了不同的同步通道——内容走定时增量,权限走事件驱动(客户系统有权限变更的 webhook 就订阅,没有就用更高频的轮询只拉权限元数据,不拉内容,成本低得多)。另外加了一道检索时的实时校验:对高敏感目录的文档,命中后再调一次客户的权限接口确认,牺牲一点延迟换确定性。取舍原则是「按失效后果的严重程度决定同步的及时性」,而不是统一用一个同步周期。这条判断在企业服务的组织架构同步里也一样适用——人员离职要立刻生效,部门改名可以慢一点。
    Q:你说不做 OCR 是因为错误不可察觉,那能不能加个置信度过滤? A:可以缓解但不解决。OCR 引擎给的置信度对「识别成了另一个合法词」这种错误恰恰不敏感——把「不支持」识别成「支持」,字形接近,置信度可能很高,而这个错误的后果是答案完全反了。置信度能过滤掉的是「识别成乱码」这类明显失败,而那类本来也不会造成误导。所以如果要做,我的方案是三条一起上:只对确实必要的文档做(不要全量 OCR)、给 OCR 来源的块打标记并在检索时降权答案里明确提示「该内容来自图片识别,建议核对原件」。第三条是关键——既然错误无法在系统内被检测,就把判断权交还给用户,而不是假装它是可信的。这也是我们处理所有「不可察觉错误源」的通用做法:不能检测就必须标注。

模块二:混合检索与权限过滤

  1. 混合检索与权限过滤(双路召回按排名融合 + 过滤前置到查询条件 + 重排候选控制 + 查询改写)★★★
    简历这样写 混合检索链路(向量库 + Elasticsearch + 排名融合 + 交叉重排 + 查询改写):向量召回补语义、关键词召回补精确匹配(型号、错误码这类在向量空间几乎没有区分度),两路结果按排名融合而不做分数归一化(两边分数不同量纲,直接加权会被其中一路的分布主导);租户与文档权限过滤下推到两路各自的查询条件,不做检索后过滤(后过滤会让 top-k 被无权限文档占满,筛完变零结果);对「召回到但排名靠后」的情况引入重排并严格限制候选数(它在首 token 延迟的关键路径上)。上线后「用原文能召回、用用户问法召回不到」这类问题由查询改写覆盖,零结果率明显下降。
    展开完整拆解
    为什么要这么设计

    第一版是纯向量检索,最省事。上线后三类问题反复出现,而且每一类都不是「调阈值」能解决的。

    一是精确匹配全废。用户问「ERR_5032 是什么错误」,检索返回了一堆讲错误处理的通用文档,唯独没有那条错误码的说明。原因是错误码、产品型号、版本号这类标识在向量空间里几乎没有区分度——ERR_5032ERR_5039 的向量几乎一样。企业知识库里这类查询占比很高,纯向量方案在这上面是结构性的短板,不是调参能修的。

    二是权限过滤放在了检索之后。先检索 top 20,再按权限筛掉没权限的,剩下几条给模型。问题是某些查询的 top 20 全是别的部门的文档,筛完变成零结果,用户看到「没找到相关内容」,但他自己有权限的那份文档明明存在,只是排在第 30 位。这是个很隐蔽的 bug,因为它只在特定权限组合下出现。

    三是用户表述和文档表述的距离。用户说「导不出来数据」,文档写「数据导出功能的权限要求」。语义相关但字面和句式差得远,向量相似度不够高,排在了第八位而 top-k 是 5。用文档原句去检索能召回,用用户的问法召回不到——这个现象是诊断的关键,它说明索引没问题、召回能力也在,差的是查询侧。

    所以第二版做了四件事:加关键词召回并按排名融合把权限过滤下推到查询条件对排序不准的情况加重排加查询改写缩短表述距离。四条各自解决上面一个问题,其中第二条是正确性修复,其余是质量提升。

    整体链路
    查询预处理 │ ├─ 抽取硬标识:错误码 / 型号 / 版本号 / 工单号 │ 抽到了就作为关键词路的必须匹配项,不依赖向量 │ ├─ 查询改写:用小模型把口语问法改写成接近文档语言的表达 │ 原问题与改写后各检索一次取并集(不是替换,避免改写跑偏) │ └─ 改写失败或超时 ─▶ 降级为只用原问题,不阻塞主链路 双路并行召回(必须并行,串行会直接加到首 token 延迟上) │ ├─ 向量路:语义相似 │ 过滤条件下推:租户 + 用户可见范围 + 文档当前版本 │ ├─ 关键词路:精确匹配(BM25) │ 过滤条件同样下推,两路的过滤口径必须一致 │ └─ 两路各取若干候选 融合(按排名,不按分数) │ ├─ 两路分数量纲不同:向量是相似度、关键词是词频统计 │ 直接加权求和会被分布更宽的那一路主导 │ ├─ 只看各自的排名位次做融合,不需要做分数归一化 │ 工程上更稳:换向量模型或调 BM25 参数都不用重调权重 │ └─ 融合后按内容相似度去重 重叠区导致的近似重复块在这里合并,避免白占 top-k 重排(条件触发,不是默认开启) │ ├─ 触发条件:融合后候选数超过阈值,或场景标记为高精度要求 │ ├─ 候选数严格设上限 ─▶ 它在首 token 延迟的关键路径上 │ 初检取 100 条全丢给重排是常见的性能事故 │ └─ 重排模型超时 ─▶ 降级为直接用融合结果,不阻塞 出口 │ ├─ 按上下文预算取 top-k 并对每段截断 │ 塞太多不只是贵,还会稀释注意力让效果变差 │ ├─ 最高相关度低于阈值 ─▶ 不进模型,直接走拒答 │ └─ 记录本次召回明细(供排查与评测回放)
    分步拆解
    1. 先抽硬标识,这一步在检索之前。查询里出现错误码、型号、工单号这类有格式的标识,用正则抽出来作为关键词路的必须匹配条件。不要指望向量或者模型来处理这类精确匹配,规则又快又准。
    2. 权限过滤必须下推到两路各自的查询条件里。这是正确性问题:后过滤会让 top-k 被无权限文档占满导致假零结果。而且两路的过滤口径必须完全一致——如果向量路过滤了关键词路没过滤,就是个越权通道。我们的做法是把过滤条件构造收敛成一个方法,两路都调它。
    3. 租户和用户身份从认证态取,不接受任何上层传入。和工具参数那条一样的原则。过滤条件不能出现在任何可以被绕过的地方。
    4. 两路必须并行。它们都在模型之前,串行执行的耗时会全额加到首 token 延迟上。这是纯粹的工程细节但影响很直接。
    5. 融合按排名不按分数,这一条值得单独讲。向量相似度和 BM25 分数不在同一量纲,前者通常在有限区间内、后者理论上无上界。直接加权求和的结果会被分布更宽的那一路主导,而且换个向量模型或者调一下 BM25 参数,权重就得重调。按排名位次融合不需要归一化,工程上稳定得多。
    6. 融合后要去重。分块的重叠区会导致近似重复的块同时被召回,两条几乎一样的内容白占了 top-k 里的两个位置。按内容相似度合并,保留结构路径更精确的那条。
    7. 查询改写是取并集不是替换。改写有可能跑偏(把用户的意思改错了),如果直接替换原问题,跑偏就没救了。两个都检索取并集,成本增加不多但保住了下限。改写超时或失败要能降级为只用原问题,不能因为一个增强环节挂了让主链路不可用。
    8. 重排是条件触发不是默认开启。它的价值场景很明确:「正确答案在前 20 但不在前 3」。如果正确答案压根不在前 20,重排帮不上忙,该回去修召回。默认开启只是无谓地增加延迟。
    9. 重排候选数要设硬上限。初检取 100 条全丢给重排模型,延迟会非常难看。而且重排在关键路径上,它慢首 token 就慢。
    10. 相关度不足就不进模型。这道闸同时省钱、降延迟、消除编造可能,是整条链路上性价比最高的一个判断。
    11. 召回明细要落日志。每次检索召回了哪些块、各自的两路排名和融合位次、重排前后的顺序变化。没有这份记录,「为什么没召回到」这类问题只能靠猜,而这是最高频的排查需求。
    关键决策与取舍

    融合选按排名而不按分数,代价是丢掉了分数携带的强度信息。按分数融合理论上更精细——一个相似度 0.95 的结果和 0.75 的结果应该有区别,而按排名它们只是第一和第二。但实际上这个精细度换不来稳定性:两路量纲不同、分布不同、而且分布会随语料变化,调出来的权重在换模型或者换客户之后就不适用了。我们选了「稍微粗糙但不需要维护」的方案。如果某个场景确实需要用到强度信息,做法是在融合之后用重排来精细排序,而不是回去调融合权重——把「精确排序」这个职责交给专门做这件事的组件,比在融合层调参更可控

    查询改写用小模型而不是大模型。改写是个受约束的短任务(把口语变成术语),小模型完全够用,而它在关键路径上,延迟差异直接体现在首 token 上。代价是改写质量偶尔差一些——但因为我们是取并集不是替换,改写差的时候原问题那一路还在,下限有保障。这个「用并集给不可靠的增强环节兜底」的模式值得记,它让我们敢用更快更便宜的组件。

    重排做成条件触发,而不是所有查询都走。全量开启的话平均延迟会明显上升,而收益只体现在一部分查询上(排序不准的那些)。判断依据是「召回够但排序不准」这个特征——我们用融合后候选数和分数分布来估计:候选很多且头部分数接近,说明区分不开,值得重排;候选很少或者头部分数明显领先,重排的意义不大。另外高精度场景(比如涉及金额和政策条款的问题)无条件开启,这类问题答错代价高,值得多花几百毫秒。

    踩过的坑一:权限过滤下推之后零结果率反而上升了。下推是对的,但下推之后过滤掉的文档不再参与召回,导致某些用户可召回的文档总量很少,达不到 top-k。表现是「权限少的用户经常查不到东西」。这不是 bug 而是真实情况的暴露——他确实没有那些文档的权限。但产品上需要区分「没有相关内容」和「有内容但你没权限」,前者建议换问法,后者应该提示「可联系管理员申请权限」。我们的做法是额外做一次不带权限过滤的计数查询(只取数量不取内容),如果无权限的命中数大于零就给出申请权限的提示。注意只能返回「存在但无权限」这个事实,不能返回任何内容片段或标题,否则又是泄露。

    踩过的坑二:改写把专有名词改坏了。用户问的是产品里一个自定义功能名,改写模型不认识,把它「修正」成了一个常见词,两路都召回不到。修法是改写前先做专有名词保护——把从术语表匹配到的词标记为不可改写,改写后校验这些词是否还在,不在就丢弃改写结果。这一条的通用教训是:任何「自动修正」环节都要有一个「不许动的白名单」。

    没做的部分:没做基于用户点击和反馈的排序学习。方向是对的(搜索服务那边就是这么做的),但企业知识库的单客户流量太小,攒不够可用的行为数据,而跨客户共享行为数据既有隐私问题也不成立(不同客户的文档和术语完全不同)。这是「同样的技术在不同数据规模下不适用」的例子,值得在面试里说——它显示你判断方案的依据是数据条件而不是技术先进性。

    数字是怎么测的

    召回质量用标注集测,这里必须说清标注成本。做法是准备一批真实问题,人工标注每个问题的「正确答案所在的块」,然后算命中率(正确块是否出现在 top-k 里)命中位次规模要诚实——我们标了两三百条,不是几万条,因为这是人工标注。面试时我会直接说数量级,「标了两百多条」比含糊地说「大量测试」可信得多

    命中率和命中位次要分开报,因为它们指向不同的修法。命中率低是召回问题(去修分块、加关键词路、改写),命中位次靠后是排序问题(上重排、调融合)。只报一个「准确率」把两件事混在一起,就丢掉了诊断价值,而且面试官一追问「那你怎么知道该修哪一块」就答不上。

    混合检索的收益要按查询类型拆开报。整体提升可能不明显,但在含硬标识的查询上提升会很大——因为纯向量在这类查询上几乎是全废。我们的做法是把标注集按「含标识」和「纯自然语言」分成两组分别算。不拆开报会严重低估这个改动的价值,也说不清它到底解决了什么。

    零结果率:线上直接统计,但要区分三种零结果——真的没有相关内容、有内容但相关度低于阈值被拦、有内容但用户无权限。三者的处理完全不同(补文档 / 调阈值或改写 / 提示申请权限),混在一个指标里没法行动。

    延迟要按段拆开量。查询改写、两路召回(并行取最大值)、融合去重、重排,四段各自的耗时。因为它们全在首 token 的关键路径上,哪一段慢就直接体现在用户感知上。我们的口径是分段报而不报一个总数,这样才能说清优化空间在哪。

    权限过滤正确性不靠统计,靠红队用例。构造多用户多权限组合的固定用例,断言每个用户召回的块集合严格属于他的可见范围。这类断言必须进回归且不允许失败,因为它防的是最严重的一类事故。

    面试追问
    Q:既然关键词检索对精确匹配这么有效,为什么不干脆只用关键词,省掉向量那一路? A:因为两者失败的方向是互补的,去掉任何一路都会留下一整类召不回的查询。纯关键词的短板是「换个说法就召不回」——用户说「导不出来数据」,文档写「数据导出功能受权限限制」,字面重合度很低,BM25 给的分数不足以进 top-k。而企业知识库里用户用口语提问是常态,文档用术语书写是常态,这个表述距离恰恰是最普遍的情况。反过来纯向量的短板是标识类精确匹配。所以问题不是「哪个更好」,是「这两类查询在你的流量里各占多少」——我们拆开统计过,两类都是可观的比例,那就必须都要。如果某个场景确实只有一类(比如纯错误码查询工具),只用关键词是完全合理的。我会强调判断依据是流量构成而不是技术优劣
    Q:你说不能返回「有内容但你没权限」的细节,那计数本身算不算泄露?用户能通过反复试探来推断存在什么文档。 A:这个质疑是对的,计数确实是一个信息泄露通道,理论上可以做存在性探测。我们的处理是三条:一是只返回布尔而不返回精确计数(「有相关内容但你无权访问」而不是「有 3 条」),减少信息量;二是对这个提示做频率限制,同一用户短时间内多次触发就不再提示,防止批量探测;三是把触发记录进审计,异常频繁的探测行为可以事后发现。但我要承认这是个权衡而不是完美方案——完全不提示最安全,代价是用户会以为知识库里没有这份文档从而反复换问法或者去问人工,产品上不可接受。所以我们选择了「泄露一个布尔位换取可用性」,并且把这个决定写进了安全评审文档。面试时我倾向于这样答:明确说出这是个已知的权衡、量化了泄露的信息量、并且有补偿措施,比声称没有问题更可信。
    Q:重排条件触发,那评测的时候是按触发后的效果算还是按整体算?会不会挑好看的数字? A:两个都要报,而且要说清口径,这正是容易做手脚的地方。整体指标是对业务负责的数字(所有查询的平均命中位次和延迟),分组指标是对技术判断负责的数字(触发组和未触发组分别的效果)。只报触发组的提升是不诚实的,因为它掩盖了「触发判断本身准不准」这个问题——如果触发条件设计得不好,可能该触发的没触发,那么整体指标就不会改善。所以我们额外看一个指标:未触发组里有多少查询其实应该触发(用标注集反查那些命中位次靠后但没触发重排的)。这个数字衡量的是触发条件的召回率。我认为面试里主动说出「这个指标可以怎么被美化,所以我们额外看了什么」是很有说服力的,它说明你不是在汇报成绩而是在管理系统。

模块三:答案可溯源与拒答

  1. 答案可溯源与拒答(引用结构化校验 + 关键事实回查 + 三道拒答闸 + 来源版本回显)★★★
    简历这样写 答案可信度保障链路(Spring Boot + 引用校验 + 事实回查 + 拒答降级):要求模型输出结构化引用(片段 ID 而非自然语言描述),服务端校验引用是否真实存在与是否包含对应表述——引用之所以有价值是因为它可被代码校验,不只是给用户看;把答案中的数字、型号、日期抽出与召回原文做字面回查,对不上即标记;拒答做成入口拦(相关度不足不进模型)、类型前置(合规敏感直接转人工)、出口替换(无有效引用则整段替换话术)三道闸,且话术必含替代路径;回答附来源文档与更新时间,让业务方自行识别引用了旧版本。上线后编造型错误在回归用例中由每轮可复现降至未再复现,「答案引用旧版本」问题从依赖用户投诉转为可自查。
    展开完整拆解
    为什么要这么设计

    第一版有 RAG 但没有校验,认为「有资料在上下文里模型就不会编」。这个假设错了,三种情况下它照样编。

    一是检索没召回到,但模型还是答了。相关度很低的几个块进了上下文,模型从里面找不到答案,就用训练时的常识补了一个。而它的语气和答对时完全一样,用户没有任何办法判断。这是最危险的一类。

    二是问题很具体而资料很笼统。用户问「这个套餐的保修期是几个月」,资料只写了「按标准政策执行」。模型不会说「资料里没写」,它会填一个常见的数字。这类错误特别隐蔽,因为答案的形式完全正确

    三是把多个片段的信息串了。A 产品的名称配了 B 产品的参数,两个片段都在上下文里、每个数字都能找到出处,但组合起来是错的。

    还有一个不是幻觉但同样致命的问题:业务方改了文档,但答案还是旧的,而没有任何迹象能让人发现。他们只能靠用户投诉才知道,而投诉之前已经错了很多次。

    所以第三个模块的核心思路是:不追求让模型不出错,而是让出错能被代码检测出来,且检测不出来的时候默认行为是安全的。三个手段对应上面三类:引用做成结构化且服务端校验(解决第一类,无有效引用就不放行)、关键事实回查(解决第二三类,数字和标识必须能在原文找到)、三道拒答闸(兜住所有检测到问题的情况)。第四个问题用来源与版本回显解决。

    最值得说的判断是关于引用的:引用的价值不在于给用户看,而在于它可以被代码校验。如果只是让模型在答案末尾写一句「参考了产品手册」,那是装饰,无法验证。改成输出片段 ID 之后,服务端可以检查这个 ID 是否在本次召回结果里、这个片段是否真的包含答案里的说法——引用从此变成了一个可执行的断言。这个转变是整个模块能成立的基础。

    整体链路
    入口拦(第一道拒答,最省钱也最可靠) │ ├─ 召回最高相关度低于阈值 ─▶ 直接拒答,不调模型 │ 没有依据就不给它编造的机会 │ └─ 合规敏感类型(承诺 / 法务 / 赔付)由轻量分类前置 ─▶ 转人工 生成(要求结构化引用) │ ├─ 上下文里每个片段带唯一 ID │ ├─ 输出约定:每个论断附引用的片段 ID 列表 │ 要的是 ID,不是「参考了产品手册」这种自然语言描述 │ 因为前者可校验,后者只是装饰 │ └─ 结构化输出失败 ─▶ 容错解析 ─▶ 重试一次 ─▶ 降级为纯列表展示 出口校验(三类,全部纯代码可做) │ ├─ 1 引用存在性 │ 引用的 ID 是否在本次召回结果里(模型会编 ID) │ ├─ 2 引用相符性 │ 被引片段是否真的包含该论断的关键词 │ 不做语义判断,只做字面覆盖检查,宁可漏判不要误判 │ └─ 3 关键事实回查 抽出答案里的数字 / 型号 / 日期 / 金额 逐个在召回原文里做字面查找,找不到即标记 这一类是最典型也最有害的幻觉,而它恰好最好查 判定与出口 │ ├─ 全部通过 ─▶ 正常返回,附来源文档名与更新时间 │ ├─ 部分论断无有效引用 ─▶ 删除该论断,保留其余(不整段丢弃) │ ├─ 核心论断无有效引用 / 关键事实回查失败 ─▶ 整段替换为拒答话术 │ 注意是替换不是追加:拼在编造答案后面用户只读前半段 │ └─ 拒答话术三件套 说清没查到什么 + 给替代路径(转人工 / 建工单)+ 给相近内容参考 来源与版本回显 │ ├─ 答案下方列出:文档名 · 章节 · 最后更新时间 │ └─ 业务方一眼能看出引用的是旧版本 问题从「等用户投诉」变成「自己看出来」
    分步拆解
    1. 入口拦是三道闸里最重要的一道。相关度不足直接拒答,不进模型。它同时带来三个收益:省了模型调用成本、降了延迟、从根上消除了编造的可能。后面两道只是补漏,这一道是主力。
    2. 引用必须是片段 ID,不能是自然语言描述。这是整个模块的关键设计。ID 可以校验存在性、可以定位原文做相符性检查;「参考了产品手册」无法验证,等于没有。
    3. 引用存在性校验要做,因为模型真的会编 ID。它会输出一个格式正确但本次召回里不存在的片段编号。这个校验只是一次集合查找,成本可以忽略。
    4. 相符性校验只做字面覆盖,不做语义判断。检查论断里的关键词是否出现在被引片段里。刻意做得保守——宁可漏判不要误判,因为误判会把正确答案拦下来,那个代价更难接受。做语义判断需要再调一次模型,成本和不确定性都上去了,不值得。
    5. 关键事实回查是性价比最高的检测。把答案里的数字、型号、日期、金额抽出来,在召回原文里做字面查找。纯代码、无成本、而且抓的正是最有害的一类幻觉——编数字。用户拿着一个编造的保修期去操作,后果是实际的。
    6. 数字回查要处理格式差异。原文写「三个月」答案写「3 个月」、原文「1,200 元」答案「1200元」,字面查不到但其实是对的。要先做归一化再比对,否则误判率会很高,高到这个校验没法用。这一步的工作量比想象的大,但不做就等于没做。
    7. 部分失败要做部分裁剪,不要整段丢弃。答案有三个论断,其中一个没有有效引用,删掉那一个保留其余两个。整段丢弃会把本来有用的信息也扔了,用户体验反而更差。只有核心论断失败才整段替换。
    8. 出口校验失败是替换不是追加。把「以上内容可能不准确」拼在一段编造的答案后面,用户只会读前面那段。必须整段替换掉。
    9. 拒答话术必须三件套齐全。只说「抱歉没找到」是最差的实现,用户会重复问三遍然后转人工骂人。给出替代路径和相近内容之后,拒答的体验可以比一个错误答案更好。
    10. 来源回显要带更新时间,这一条的性价比最高。业务方看到「依据:某文档,更新于三个月前」,而他上周才改过,立刻就知道同步有问题。它把一个需要排查的问题变成了一眼能看出的问题,而实现成本只是多返回两个字段。
    11. 所有校验结果要记录,作为质量指标的来源。引用校验失败率、事实回查失败率、拒答率分类计数——这些是线上质量最直接的信号,比等用户反馈早得多。
    关键决策与取舍

    相符性校验选了「字面覆盖」这个粗糙方案,而不是用模型做语义判断。字面检查会漏——答案用同义词改写了原文的说法,字面对不上但语义正确,这种情况检查不出问题(漏判)。用模型判断能覆盖同义改写,但代价是多一次调用(延迟和成本)、而且判断本身也可能错。我们的取舍是接受漏判、坚决避免误判:漏判的后果是「一个错误答案没被拦下」,这还有关键事实回查作为第二道;误判的后果是「正确答案被替换成拒答」,用户会觉得系统很笨,而且这类问题很难被发现(没人会投诉「你本来能答对却说不知道」)。在两类错误里选择容忍哪一类,判据是「哪一类能被后续环节兜住、哪一类会静默积累」

    数字回查的归一化做到什么程度,我们止步于常见格式。中文数字、千分位、货币符号、百分号、日期的几种常见写法都做了;但没做单位换算(原文「一年」答案「12 个月」)和范围推断(原文「3 到 5 天」答案「4 天」)。理由是这些需要理解语义,一旦引入就又回到了模型判断的路上,而且越复杂的归一化越容易反过来产生误判。没覆盖的部分我们的处理是让它走漏判(不标记),而不是尝试用不可靠的方法去覆盖。

    拒答阈值不做全局统一,按问题类型分档。涉及金额、政策、承诺的问题阈值调高(宁可多拒),一般性的功能咨询阈值调低(多答一些)。统一阈值必然是个折中,对高风险问题太松、对普通问题太严。代价是要维护类型分类,我们用轻量分类加关键词规则结合,分类错的成本可接受(错到高档只是多拒一次)。

    踩过的坑一:事实回查的误判率一开始高到不可用。没做格式归一化,原文「三个月」答案「3 个月」直接判定为编造,正常答案被大量拦下,拒答率飙升。上线半天就回滚了。教训是检测类逻辑上线前必须先在离线日志上跑一遍看误判率,不能直接上线路。我们后来的做法是新增任何校验规则都先跑「影子模式」——只记录不生效,看一周的误判情况再决定是否启用。

    踩过的坑二:要求引用之后答案质量反而下降了。模型为了满足「每个论断都要有引用」,开始只说原文里明确写了的话,答案变得零碎、缺少必要的组织和衔接。修法是区分「事实性论断」和「组织性表述」——只要求前者带引用,后者(「下面分三种情况」「需要注意的是」)不要求。约定清楚之后质量恢复了。这个坑说明约束加得太死会挤压掉有价值的能力,要给它留出不需要引用的表达空间。

    没做的部分:没做答案的自动置信度评分(给用户一个百分比)。原因是这个分数会被用户当成可信度依据,而我们没有能力让它准确——引用校验通过只说明「有出处」,不说明「理解正确」。给一个不可靠的置信度分数,比不给更糟,因为它会诱导用户放松判断。我们的替代方案是回显来源让用户自己核对,把判断权交回去而不是给一个假的确定性。

    数字是怎么测的

    编造型错误的验证用构造用例,不用统计。做法是准备一批「知识库里确实没有答案」的问题(比如问一个不存在的套餐的保修期),断言系统走拒答而不是给出一个数字。报法是「这批用例由每轮可复现降至未再复现」——因为这是断言型测试,通过或不通过,不适合报百分比。用「可复现 / 未复现」这个口径比编一个准确率诚实得多,而且面试官能立刻理解你在测什么。

    校验规则的误判率必须单独测,这是最容易出事的地方。方法是影子模式:新规则只记录不生效,跑一周线上流量,人工抽查被标记的样本里有多少是误判。误判率的口径要说清——分母是被规则标记的数量,不是总请求量。我们的教训就是没测这个直接上线,半天就回滚了,所以这段经历本身比数字更值得讲。

    拒答率要按三道闸分别统计。入口拦(相关度不足)、类型前置(合规转人工)、出口替换(校验失败)。三个数字的诊断意义完全不同:入口拦占比高说明知识库覆盖不够,出口替换占比高说明模型在编造,类型前置占比高说明用户问的敏感问题多。混在一个「拒答率」里就丢掉了全部诊断价值。

    拒答的正确性也要测,不能只测拒答率。要区分该拒而拒(正确)、该拒未拒(漏,最危险)、不该拒而拒(误拒,影响体验)。用标注集算后两类的比例。只看拒答率高低是没意义的——拒答率 50% 可能是知识库太差也可能是阈值太严,必须配合正确性一起看。

    来源回显的效果不用技术指标衡量,用行为指标。我们看的是「业务方主动报告文档同步问题的次数」——回显上线后这个数字上升了,说明他们真的在看并且能自己发现问题。这是个反直觉的正向指标:报告的问题变多是好事,因为之前那些问题一直存在只是没被发现。这个视角在面试里值得说,它显示你关心的是问题被发现的效率而不是问题数量的账面下降。

    面试追问
    Q:引用校验只做字面覆盖,那模型只要把原文的词都抄进答案里就能通过校验,是不是很容易被绕过? A:确实能,这个校验挡不住「用对的词说错的话」——把原文的关键词都用上但组合成一个错误的论断。所以我从来没把它当成正确性保证,它的定位是「过滤掉完全无依据的输出」,也就是那些引用了不存在的片段、或者引用的片段和论断毫不相关的情况。这类恰恰是最常见的编造形态。至于「词都对但组合错」,靠的是关键事实回查那一层(数字和标识必须能在原文找到),以及最终的来源回显让用户核对三层各挡一类,没有任何一层是完备的。我会明确说这一点——如果声称某一层能保证正确性,面试官一构造反例就穿了,而承认每层的边界并说清它们如何互补,才是这套设计真正的样子。而且从工程角度看,一个成本接近零、能挡住最常见形态的粗糙检查,价值远大于一个精确但昂贵的检查。
    Q:入口拦掉的那些查询,你怎么知道是真的没有答案,而不是检索没做好? A:不能只靠相关度这一个信号判断,所以我们对入口拒答做了抽样复核的闭环。做法是把被入口拦掉的查询按周抽样,人工去知识库里找一遍——如果找到了答案,说明是检索问题(该召回没召回),这条就成为一个高价值的评测用例,去修分块或者查询改写;如果确实没有,那就是知识覆盖问题,转给知识运营去补文档。两种结论对应两个完全不同的团队和动作,所以这个区分很重要。另外有个便宜的辅助信号:同一个问题被多个不同用户问到且都被拦掉,那大概率是知识缺口而不是偶发的检索失败,这类会被优先排进补文档的队列。入口拒答不是终点而是一个待分析的队列,这个认知比阈值调到多少更重要——如果拒答之后没人看,那知识库永远不会变好。
    Q:为什么不给用户一个置信度分数?很多产品都这么做。 A:因为我们没有能力让这个分数准确,而一个不准的置信度分数比不给更有害。它有害在于会改变用户的行为——看到「置信度 92%」用户就不去核对了,而这个 92% 实际上只反映了「引用校验通过」,完全不反映「理解是否正确」。我们相当于用一个假的确定性换掉了用户本来会有的警惕。更根本的问题是,模型对自己置信度的估计本身就不可靠,它在编造时和答对时同样自信;而基于校验结果算出来的分数只能反映校验覆盖到的那几个维度。所以我们的替代方案是回显来源和更新时间,把判断权交回给用户——给他可核对的原始依据,而不是一个他无法验证的数字。如果一定要给某种信号,我倾向于给离散的定性标记(比如「依据充分 / 依据有限,建议核对原文」)而不是连续的百分比,因为定性标记不会给人虚假的精确感。

没有匹配的内容,换个关键词试试。

项目拆解 · 企业知识库检索增强(后端)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据