想象你走进一座藏书百万的图书馆,咨询台的老张正在接待读者。有人问:“《红楼梦》里贾宝玉的玉是怎么来的?”老张不会傻乎乎地把整座图书馆的书翻一遍,而是先在目录卡片柜里查找“红楼梦”“贾宝玉”“玉”这些关键词,锁定三五本可能相关的书,再快速翻到对应章节,圈出关键词附近的段落,最后组织成一句话回答给你。整个过程不超过一分钟。 这就是检索式问答系统(Retrieval QA)的日常:先粗筛,再精读,最后给答案。老张的大脑就是一个问答系统,图书馆的藏书就是知识库,目录卡片柜就是倒排索引,他那套“什么时候该给哪本书加分”的经验就是 BM25 算法。本文就从老张的一天讲起,带你把检索式问答系统的每个环节搭一遍,从流程框架到代码实现,一条龙讲透。

岗位关联

检索式问答是智能客服、知识库助手的核心技术底座。掌握它的原理与实现,是踏入 NLP 与 RAG 工程的第一块敲门砖,也是面试和实战中几乎绕不开的硬通货。

目录
  1. 可视化演示
  2. 系统总览
  3. 知识库构建
  4. 倒排索引
  5. 核心算法
  6. 答案抽取
  7. 工程优化
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示检索式问答系统核心流程:查询分词、倒排索引匹配、BM25评分排序及答案抽取,切换模式观察各阶段细节。

本节配有交互动效(进入视口后自动加载,约 3KB)。

检索式问答流程 用户查询经分词后进入倒排索引,快速定位相关文档 BM25算法对候选文档评分排序,选取Top-K结果 从Top-K文档中抽取答案片段返回用户 BM25(D,Q) = Σ IDF(qi) · f(qi,D)·(k₁+1) / (f(qi,D)+k₁·(1-b+b·|D|/avgdl))

系统总览

老张处理每个咨询问题时,都会不自觉地拆成三步:听到问题后先在心里猜测“答案可能藏在哪几本书里”,接着从书架上抽出那几本翻阅,最后锁定一个句子念给读者听。他从来不会试图背下整座图书馆,因为他知道那不是高效的服务方式。

检索式问答系统(Retrieval QA)正是这个思维方式的工程化。整个系统由两大部分组成:检索器(Retriever) 和 阅读器(Reader)。检索器像老张的那一步“猜想”,面对用户问题,它用倒排索引在百万级文档中快速筛出几十篇候选文档;阅读器接过候选集,像老张翻阅书页一般,从中精读并抽取答案片段。两个阶段各司其职,一个管广度和速度,一个管深度和精度。

这种“先粗筛、后精读”的两阶段设计,在工程上极其优雅:粗筛阶段用廉价算法把候选空间从百万缩小到百,精读阶段再用复杂模型在百级规模上精挑细选。整套系统的知识库可以是 FAQ 列表、产品手册、政策文件甚至是爬取的网页正文,这也解释了为什么智能客服和知识库助手普遍采用这一架构。

老张的一天就是两阶段流水线的缩影。当你在搭建自己的问答系统时,请先记住这个骨架:检索器保证“找得到”、阅读器保证“答得对”,后续所有优化都是在这两步上添砖加瓦。

A=Reader(Retriever(q,D))\mathcal{A} = \mathrm{Reader}\left( \mathrm{Retriever}(q, \mathcal{D}) \right)
这个公式把整个问答流程压缩成一行:答案等于阅读器对检索器产出的候选文档做进一步处理的结果。理解它,就理解了检索式 QA 的骨架。
  1. 文档集合 \(\mathcal{D}\)知识库中所有文档的集合,相当于图书馆的全部藏书。
    D={d1,d2,…,dm}\mathcal{D} = \{d_1, d_2, \dots, d_m\}
  2. 用户问题 \(q\)用户输入的一句话,是检索的起点,相当于读者向老张提的问题。
    qq
  3. 检索器 \(\mathrm{Retriever}\)负责从全量文档中快速筛出少量候选文档,等于是老张在目录柜里翻卡片。
    Retriever(q,D)→{dc1,dc2,…,dct}\mathrm{Retriever}(q, \mathcal{D}) \rightarrow \{d_{c1}, d_{c2}, \ldots, d_{ct}\}
  4. 阅读器 \(\mathrm{Reader}\)对候选文档做细粒度阅读并抽取答案,等于是老张翻开书页找到那句关键的话。
    A=Reader ⁣(Retriever(q,D))\mathcal{A} = \mathrm{Reader}\!\left( \mathrm{Retriever}(q, \mathcal{D}) \right)

知识库构建

每本新书到达图书馆后,老张都要先检查缺页、污损,涂掉书中的批注,再给书贴上索书号、录入系统、摆上对应书架。如果书太厚,他还会拆成上中下三册,方便读者搬运。这套“入库流程”看似琐碎,却直接决定了后面检索的体验。

知识库构建对应着四个环节:数据采集、文档清洗、内容切分、索引存储。采集阶段从业务系统、FAQ、网页、PDF 中抽取原始文本;清洗阶段去掉 HTML 标签、乱码、页眉页脚、重复段落;切分阶段把长文档切成适当大小的块;最后把这些块写入存储并建立索引。很多新手只关注后面的算法,却忘了知识库的质量才是检索上限的根源。

切分尤其关键。切得太粗,一篇文档里混着多个主题,检索时会干扰打分;切得太细,答案被拦腰截断,抽取阶段无米下锅。业界常用“固定长度 + 重叠窗口”的办法:设定每块长度 S,相邻块保留 δ 的重叠,既能控制块数,又能保住语义连贯性。切分后的块数直接决定索引规模和检索耗时,这是一个需要结合业务反复试验的超参数。

知识库的构建就像给图书馆的每一本书做精心打理:清洗让书页干净,切分让搬运轻松,存储让取用快速。做不好这一步,后面再强的算法也像是让老张面对一堆乱糟糟的散页,再厉害的管理员也难为无米之炊。

N=⌈LS⌉N = \left\lceil \frac{L}{S} \right\rceil
一块长 L 的文档,按每块 S 的长度切分,需要约 L/S 个块。向上取整确保最后剩下的小尾巴也有地方安放。
  1. 文档长度 \(L\)表示一篇原始文档的字符总数,等价于一本书的总页数。
    LL
  2. 切分大小 \(S\)设定的每个文本块的目标长度,相当于把一本书拆成的每册厚度。
    SS
  3. 向上取整 \(\lceil \cdot \rceil\)当 L 不能被 S 整除时,最后不足一块的部分也占一个块。
    ⌈LS⌉\left\lceil \frac{L}{S} \right\rceil
  4. 块数 \(N\)切分后得到的文本块总数,每块就是一个独立的检索单元。
    NN

倒排索引

老张最依赖的宝贝是一排目录卡片柜。每张卡片按关键词首字母排序,卡片上写着“这个关键词出现在第几排书架的第几本书”。读者问“神经网络有什么应用”,老张走到 N 字头的抽屉,抽出“神经网络”卡片,立刻知道有 12 本书相关,然后直奔那 12 本。他不需要把整个图书馆的书都翻一遍。

目录卡片柜的本质就是倒排索引(Inverted Index)。它把“文档 → 词项”的正向关系,反转成“词项 → 文档列表”的逆向关系。结构上包含两部分:词典(Dictionary) 保存所有出现过的词项,每个词项后面挂着一个倒排列表(Posting List),记录了包含该词的所有文档 ID 及词频。正排索引是一本本书的书名页,倒排索引则是按关键词组织的卡片柜。

构建倒排索引的流程并不复杂:对每篇文档分词,统计词项,然后向词典中对应词项的倒排列表追加文档 ID。由于同一词项在一篇文档中只记录一次用于 DF 计算,同时还要保存词频 TF 供后续 BM25 打分使用,实践中倒排列表通常存储 (docId, tf) 二元组。索引构建完成后支持增量更新,新文档入库时只需把新词项挂到已有词典即可,开销极小。

有了倒排索引,检索从“逐本翻书”变成“翻卡片”,速度提升几个数量级。老张的卡片柜让他面对“神经网络”这样的问题也能面不改色,这就是倒排索引带给检索系统的底气。

DFt=∣{d∈D:t∈d}∣\mathrm{DF}_t = \left|\{ d \in \mathcal{D} : t \in d \}\right|
词项 t 的文档频率 DF,等于“包含词项 t 的文档数量”。老张看卡片上写了几本书,就是在数 DF。
  1. 词项 \(t\)一个关键词,如“神经网络”,对应卡片柜里的一张卡片。
    tt
  2. 文档集合 \(\mathcal{D}\)知识库中全部文档的集合,即整个图书馆的书目。
    D\mathcal{D}
  3. 条件 \(t \in d\)表示“词项 t 出现在文档 d 中”,等价于这张卡片的记录里包含这本书。
    t∈dt \in d
  4. 集合计数 \(\left|\{\cdot\}\right|\)花括号构造一个满足条件的集合,竖线表示取集合大小,结果就是卡片上列了书的数量。
    ∣{d∈D:t∈d}∣\left|\{ d \in \mathcal{D} : t \in d \}\right|

核心算法

老张的卡片柜虽然能锁定“可能有关系的书”,但 12 本书里哪本最相关,他心里有一杆秤:第一,关键词在书里出现得越多,书越相关;第二,如果这个词是“的”“了”这种满书架都有的词,那加分就得大打折扣;第三,内容很厚的大部头里出现一次关键词,参考价值远不如薄册子里出现一次。

这杆秤的数学化版本就是 BM25 算法,它是业界最经典的检索排序函数之一。BM25 的设计可以拆成三块:IDF 权重衡量词项在整个语料中的稀有程度,越稀有越值钱;词频项刻画关键词在文档中出现的密度,但采用饱和函数防止一词出现百次就得分爆表;长度归一化则惩罚那些因为篇幅长而“天然词频高”的大部头文档。三个因子相乘再对查询中的所有词求和,就得到文档与查询的相关性得分。

BM25 之所以经久不衰,是因为它用极小的计算代价捕捉了“相关性”的核心直觉。老张靠经验打分能服务一个图书馆,BM25 让搜索引擎和问答系统在千万文档面前也能保持同样的判断力。参数 k1 和 b 需要根据语料做小范围网格搜索调优,但默认值(k1=1.5,b=0.75)在大多数场景下已经是可靠的起点。

score(D,Q)=∑i=1nln⁡ ⁣(1+N−n(qi)+0.5n(qi)+0.5)⋅f(qi,D)⋅(k1+1)f(qi,D)+k1⋅(1−b+b⋅∣D∣avgdl)\mathrm{score}(D,Q)=\sum_{i=1}^{n}\ln\!\left(1+\frac{N-n(q_i)+0.5}{n(q_i)+0.5}\right)\cdot\frac{f(q_i,D)\cdot(k_1+1)}{f(q_i,D)+k_1\cdot\left(1-b+b\cdot\dfrac{|D|}{\mathrm{avgdl}}\right)}
这个公式看起来吓人,其实就是在做三件事:看词有多稀有(IDF),看词在文档中出现得多频繁(词频饱和),看文档是不是“注水大长篇”(长度归一)。三者的乘积加总,就是 BM25 对文档的最终打分。
  1. 对所有查询词求和 \(\sum_{i=1}^{n}\)把查询里每个词单独算分再加总,相当于把“神经网络”“应用”每张卡片都翻一遍分数再相加。
    ∑i=1n\sum_{i=1}^{n}
  2. 文档总数 \(N\) 与文档频率 \(n(q_i)\)N 是知识库全部文档数,n(q_i) 是包含查询词 q\_i 的文档数,两者共同决定 ID“稀有度”。
    ln⁡ ⁣(1+N−n(qi)+0.5n(qi)+0.5)\ln\!\left(1+\frac{N-n(q_i)+0.5}{n(q_i)+0.5}\right)
  3. 词频 \(f(q_i,D)\)查询词在文档中出现的次数,它让包含关键词次数多的文档获得更高分。
    f(qi,D)⋅(k1+1)f(qi,D)+k1⋅(⋯ )\frac{f(q_i,D)\cdot(k_1+1)}{f(q_i,D)+k_1\cdot(\cdots)}
  4. 长度比 \(|D|/\mathrm{avgdl}\)文档长度除以语料平均长度。比值越大,说明越是“大部头”,分母越大,得分被压得越狠。
    ∣D∣avgdl\frac{|D|}{\mathrm{avgdl}}
  5. 参数 \(k_1\)控制词频的饱和速度。k\_1 越大,词频带来的收益衰减得越慢,词频高低的影响持续更久。
    k1∈[1.2,2.0]k_1 \in [1.2, 2.0]
  6. 参数 \(b\)控制长度归一化的强度。b=0 时完全忽略文档长度,b=1 时长度影响拉满,常见取值为 0.75。
    b∈[0,1]b \in [0, 1]

答案抽取

检索器从十万篇文档里筛出 10 篇候选后,老张拿起其中一本翻到目标章节,用荧光笔圈出两句最像答案的话。他圈句子的标准有三条:这句话包含的关键词够多吗?这句话在文档中的位置重要吗?这句话的类型和问题匹配吗(比如问价格就该圈出含数字的句子)?

答案抽取(Answer Extraction) 模块就是把这三条标准变成可计算的打分函数。它先对候选文档做候选生成——按句子或窗口切出候选片段,排除太短或太长的垃圾片段;然后计算每个候选的特征值:与查询词的词面重叠数、在原文中的位置得分、句子的类型特征(长度、是否含数字、是否以动词开头等);最后用 加权求和 的方式把多个特征融合成最终答案分。得分最高的候选片段即为系统给出的答案。若最高分仍低于阈值,系统会拒答而不是硬答,这在客服场景中尤为重要——乱答一句比承认不知道更伤用户体验。

划定阈值、宁可拒答也不硬答,这是老张在服务多年后总结出来的经验。答案抽取模块把“阅读并圈重点”这件看似主观的事,变成了可解释、可调参的工程实现。当你看到系统给出一个精准答案时,背后正是三个特征在暗中投票。

AnsScore(c)=λ1 ϕoverlap(c)+λ2 ϕpos(c)+λ3 ϕtype(c)\mathrm{AnsScore}(c) = \lambda_1\, \phi_{\mathrm{overlap}}(c) + \lambda_2\, \phi_{\mathrm{pos}}(c) + \lambda_3\, \phi_{\mathrm{type}}(c)
一个候选片段 c 的答案分,是“词面重叠、位置、类型”三个特征的加权和。权重 λ 告诉模型哪个特征更可信,等同于老张心里给三把尺子分配的重要程度。
  1. 候选片段 \(c\)从候选文档中切出的一个句子或一个连续片段,是老张用荧光笔框出来的那个句子。
    cc
  2. 重叠特征 \(\phi_{\mathrm{overlap}}\)候选片段与查询词重合的词的数量,重叠越多说明候选越紧扣问题。
    ϕoverlap(c)=∣words(q)∩words(c)∣\phi_{\mathrm{overlap}}(c) = \left| \mathrm{words}(q) \cap \mathrm{words}(c) \right|
  3. 位置特征 \(\phi_{\mathrm{pos}}\)候选片段在原文中所处位置的得分,越靠前或靠后的越有价值。
    ϕpos(c)=11+log⁡(pos(c)+1)\phi_{\mathrm{pos}}(c) = \frac{1}{1 + \log(\mathrm{pos}(c) + 1)}
  4. 类型特征 \(\phi_{\mathrm{type}}\)片段是否满足问句期望的答案类型,比如“多少钱”倾向数字句子。
    ϕtype(c)∈{0,1}\phi_{\mathrm{type}}(c) \in \{0, 1\}
  5. 权重 \(\lambda_1, \lambda_2, \lambda_3\)三个特征各自的信任系数,权重越大说明该特征对最终判断越重要。
    λ1+λ2+λ3=1\lambda_1 + \lambda_2 + \lambda_3 = 1

工程优化

老张每天下班前会翻看桌上的意见卡,记录今天哪些问题没被答好、哪些书的查找速度太慢、哪些读者常在同一个坑里打转。一周下来,他把高频翻车的问题汇总,调整卡片柜的分类方式,把热门书挪到更近的书架。这不是一次性的改进,而是每周循环的功课。

问答系统的工程优化同样是一个闭环:离线评估 → 在线评测 → 性能优化 → 持续迭代。离线阶段用带标注的数据集回放查询,计算 MAP、MRR、Recall@K 等指标,快速验证算法改动是否有效;在线阶段对真实流量做 A/B 实验,观察点击率、首答命中率、用户满意度;性能层面则关注检索延迟,尝试用缓存、索引压缩、 ANN 加速等手段压时间;最终把线上积累的坏例清洗成新的训练数据,进入下一轮迭代。这一循环一旦转起来,系统质量就会像老张的服务口碑一样稳步爬升。

除了 MAP,MRR 关注第一个正确答案出现的位置,Recall@K 关注候选集是否覆盖了正确答案,三个指标从不同角度刻画检索质量,实践中配合使用。老张的意见卡是人工视角的评估闭环,MAP 等指标则是算法视角的自动化评估闭环——两者缺一不可,共同驱动系统从“能用”走向“好用”。

MAP=1∣Q∣∑j=1∣Q∣AP(qj)\mathrm{MAP} = \frac{1}{|Q|} \sum_{j=1}^{|Q|} \mathrm{AP}(q_j)
MAP(平均精度均值)是对每个查询算出 AP,再对所有查询取平均。它同时惩罚“没找到”和“排错位”,是评估排序质量的黄金指标。
  1. 查询集合 \(Q\)离线评估用的一组带标准答案的测试问题,相当于意见卡上记录的真实读者问题。
    Q={q1,q2,…,qm}Q = \{q_1, q_2, \dots, q_m\}
  2. 单个查询的平均精度 \(\mathrm{AP}(q_j)\)对第 j 个查询,把每个正确文档出现位置处的精确率取平均,惩罚“正确答案排太靠后”。
    AP(qj)=1mj∑k=1mjPrecision(Rjk)\mathrm{AP}(q_j) = \frac{1}{m_j} \sum_{k=1}^{m_j} \mathrm{Precision}(R_{jk})
  3. 对所有查询取平均 \(\frac{1}{|Q|}\sum_{j=1}^{|Q|}\)将每个查询的表现平等看待,避免热门查询掩盖冷门查询的劣势。
    1∣Q∣∑j=1∣Q∣\frac{1}{|Q|}\sum_{j=1}^{|Q|}

AI 应用场景

智能客服与 FAQ 自动化

阶段:算法推理与反馈优化

解决的问题:传统人工客服需逐条翻阅手册,响应慢且成本高;检索式问答系统把客服知识库变成秒级问答,用户提问即可获得标准答复,大幅降低人工坐席压力。

具体案例:某电商平台将退货政策、物流说明等文档清洗后切块入库,构建倒排索引并部署 BM25 检索。用户输入“怎么申请退款”,系统 0.1 秒内召回 5 篇候选,抽取“退货流程”相关句子,自动回复附带申请链接。若置信度低于阈值则转接人工。

企业知识库问答助手

阶段:数据工程与检索管线搭建

解决的问题:企业内部文档散落在 Wiki、邮件、项目文档中,员工检索效率低;检索式 QA 统一建库并提供问答入口,把“找文档”变成“问答案”。

具体案例:某互联网公司搭建研发知识助手,将上千篇技术方案按标题和章节切块,用倒排索引 + BM25 实现内部搜索。新员工问“线上服务如何发布”,系统从部署文档中抽取操作步骤,并附上原文链接,入职上手时间缩短 30%。

Python 演示

python95 行
import mathimport refrom collections import defaultdict  class Tokenizer:    def cut(self, text):        return re.findall(r'[\w\u4e00-\u9fff]+', text.lower())  class BM25:    def __init__(self, docs, k1=1.5, b=0.75):        self.k1 = k1        self.b = b        self.doc_tokens = {}        self.N = 0        self.avgdl = 0.0        self.index = defaultdict(list)        self.idf_cache = {}        self._build(docs)     def _build(self, docs):        tk = Tokenizer()        total_len = 0        for doc_id, text in docs.items():            toks = tk.cut(text)            self.doc_tokens[doc_id] = toks            total_len += len(toks)            seen = set()            for t in toks:                if t not in seen:                    self.index[t].append(doc_id)                    seen.add(t)        self.N = len(docs)        self.avgdl = total_len / max(self.N, 1)     def idf(self, t):        if t not in self.idf_cache:            df = len(self.index.get(t, []))            self.idf_cache[t] = math.log(                1.0 + (self.N - df + 0.5) / (df + 0.5)            )        return self.idf_cache[t]     def score(self, query, doc_id):        s = 0.0        toks = self.doc_tokens[doc_id]        dl = len(toks)        for t in set(Tokenizer().cut(query)):            if t not in self.index:                continue            tf = toks.count(t)            tf_part = tf * (self.k1 + 1.0) / (                tf + self.k1 * (1.0 - self.b + self.b * dl / self.avgdl)            )            s += self.idf(t) * tf_part        return s  def extract_answer(doc_text, query):    sents = re.split(r'[。!?!?]', doc_text)    q_toks = set(Tokenizer().cut(query))    scored = []    for i, st in enumerate(sents):        if len(st) < 2:            continue        overlap = len(q_toks & set(Tokenizer().cut(st)))        pos_score = 1.0 / (1.0 + i * 0.1)        scored.append((overlap * 1.0 + pos_score * 0.05, st.strip()))    if not scored:        return ''    return max(scored, key=lambda x: x[0])[1]  def main():    docs = {        'd1': '倒排索引是信息检索系统中重要的数据结构。它通过词项映射到文档ID列表。',        'd2': 'BM25是一种经典的排序函数。它基于词频和文档长度计算相关性。',        'd3': '问答系统通常由检索器和阅读器组成。检索器负责筛选候选文档。',        'd4': '深度学习在问答系统中应用广泛。BERT等模型擅长抽取答案片段。',        'd5': '评估指标包括MAP和MRR。MAP衡量排序质量,MRR关注第一名位置。'    }    bm = BM25(docs)    query = '什么是BM25'    ranked = sorted(docs, key=lambda d: bm.score(query, d), reverse=True)    print('检索排序:')    for i, d in enumerate(ranked):        print('  %d. %s  score=%.4f' % (i + 1, d, bm.score(query, d)))    best = ranked[0]    ans = extract_answer(docs[best], query)    print('抽取答案:', ans)  if __name__ == '__main__':    main()
目的:演示倒排索引构建、BM25 打分排序和启发式答案抽取的完整流程,跑通后即可在真实知识库上替换文档集使用。
  1. self.index[t].append(doc_id)把包含词 t 的文档 ID 追加到倒排列表,实现“词 → 文档”映射。
  2. self.idf_cache[t] = math.log(1.0 + (self.N - df + 0.5) / (df + 0.5))计算并缓存 IDF 权重,文档频率越低、词越稀有,权重越高。
  3. tf_part = tf * (self.k1 + 1.0) / (tf + self.k1 * (1.0 - self.b + self.b * dl / self.avgdl))BM25 词频饱和与长度归一化的核心实现。
  4. ranked = sorted(docs, key=lambda d: bm.score(query, d), reverse=True)对全部文档按 BM25 得分降序排列,得到检索排序结果。
  5. ans = extract_answer(docs[best], query)从得分最高的文档中抽取与查询关键词重叠最多的句子作为答案。

常见误区

误区“用户输入几个词,我在知识库里一搜,找到包含关键词最多的文档直接返回不就行了?为什么要搞 BM25 排序那么复杂?”

正解关键词匹配只能解决“有没有”的问题,解决不了“哪个更相关”的问题。就像老张翻卡片找出的 12 本书,如果不打分排序,读者可能拿到一本只提到一次“神经网络”的小说。BM25 通过 IDF、词频饱和和长度归一化,对候选文档排出正确优先级,这是检索质量的命脉。

为何常见:刚入门时只看到倒排索引“秒级定位”的威力,误以为定位就是全部,忽略了排序才是决定用户体验的关键环节。

误区“k1=1.5、b=0.75 是公认的默认值,任何语料下都表现很好,调参纯属浪费时间。”

正解默认值只是经验起点而非万能答案。短文本语料中,词频本来就不高,k1 可以适当调大;文档长度差异悬殊时,b 需要接近 1;反之如果长度都很均匀,b 则可以调小。正确做法是在小验证集上做网格搜索,观察 MAP 变化,通常 5×5 次搜索即可找到更优组合。

为何常见:看到论文和开源库都写默认值,产生“默认即最优”的错觉,忽略了参数与语料分布之间的耦合关系。

误区“反正 BM25 很快,我一次召回 1 万篇文档再精排,覆盖率高,准确率肯定比召回 100 篇更高。”

正解候选集过大有两个坏处:一是噪音篇目增多,BM25 本来是为“少而精”的候选设计,候选过多会让精读阶段被无关内容干扰;二是延迟和算力成本陡增。实践中的常识是召回 50~200 篇候选即可,Recall@K 在 K=100 时通常已经接近饱和。

为何常见:把“覆盖更多”和“答案更准”混为一谈,忽略了排序模型在规模扩大时信噪比下降的事实。

误区“把文档切成 100 字的小块,检索粒度更细,命中更精准,答案模块也更容易定位。”

正解切块太小会切断语义完整性,使一句话变成半句,答案抽取时无话可抽。建议切块大小为 200~500 字并保留 10%~20% 重叠,让每块至少包含一个完整语义单元。块大小应当依据文档类型和答案长度反向设计,而不是一味求小。

为何常见:直觉上“更小的单元 = 更精确的定位”,却没考虑语义的连续性和答案的完整性。

误区“离线评估时我只看 top1 准不准,Top1 对了就说明系统很好,MAP 之类的不重要。”

正解Top1 准确率只反映“最好的情况下”的表现。真实用户可能改一两个字再问一次,如果正确答案总排在第 5 位,Top1 永远是错的,但 MRR 和 MAP 能捕捉到这种“差一口气”的问题。排序指标引导系统朝“强者更强”的方向优化,是离线评估不可或缺的维度。

为何常见:Top1 指标直观好汇报,但单点指标掩盖了排序能力的缺陷,无法指导系统性优化。

面试题

Q1. 检索式问答系统中,检索器负责什么任务?

单选题 · easy

  • A.从文档库中筛选候选文档← 正确答案
  • B.训练深度神经网络模型
  • C.对用户身份进行认证
  • D.自动生成全新的回答

答案:A。检索器像老张翻卡片柜,负责把用户问题和知识库比对,快速筛出少量候选文档。训练模型是算法工程师的事,认证是账号体系的事,生成全新回答是生成式模型的事,都不属于检索器职责。

Q2. 倒排索引的“倒排”指的是什么关系?

单选题 · mid

  • A.从词项映射到文档列表← 正确答案
  • B.从文档列表映射到词项
  • C.文本存储顺序的反转
  • D.神经网络反向传播

答案:A。正排索引是“文档 → 词”,倒排索引把方向倒过来,变成“词 → 文档列表”,这是它支持快速定位的关键。B 描述的是正排索引,C 是文本处理,D 是深度学习术语,都与“倒排”无关。

Q3. BM25 中参数 b 的作用是什么?

单选题 · mid

  • A.控制词频饱和的增长速度
  • B.控制文档长度归一化的强度← 正确答案
  • C.控制 IDF 权重的平滑程度
  • D.控制查询分词的长度限制

答案:B。b 取值范围 0~1,b=0 时完全忽略文档长度,b=1 时长度归一化强度拉满,默认 0.75。控制词频饱和的是 k1,A 错;IDF 平滑由分子分母的 0.5 常数项实现,C 错;D 与 BM25 无关。

Q4. 下列哪些属于检索式问答系统的合理切分策略?

多选题 · mid

  • A.按固定字符数切分文本← 正确答案
  • B.保留段落结构的切分← 正确答案
  • C.相邻块之间保留重叠← 正确答案
  • D.删除所有常见停用词

答案:ABC。固定长度切分便于控制块数,保留段落能维持语义完整,重叠窗口可以避免答案被切断,三者都可取。删除停用词是检索预处理环节而非切分策略,且“删除所有”过于绝对,D 错。

Q5. 下列哪一项是问答系统的离线评估指标?

单选题 · easy

  • A.MAP 平均精度均值← 正确答案
  • B.CPU 内存占用率
  • C.HTTP 响应状态码
  • D.数据库连接数

答案:A。MAP 平均精度均值是检索排序质量的核心离线指标,同时考查查找能力与排序能力。CPU 内存、HTTP 状态码、数据库连接数都是工程运维指标,与答案质量评估无关。

Q6. 答案抽取阶段的输入是什么?

单选题 · mid

  • A.整个知识库的全量文档
  • B.检索器返回的候选文档← 正确答案
  • C.用户的历史聊天记录
  • D.模型训练时的测试集

答案:B。两阶段流水线中,答案抽取(阅读器)的输入是检索器筛出的候选文档,输出是答案片段。全量文档输入会带来巨大开销,历史记录和测试集都不在推理链路中,A、C、D 错。

Q7. 关于 BM25 算法,下列说法正确的是?

多选题 · hard

  • A.包含 IDF 权重的计算← 正确答案
  • B.包含文档长度的归一化← 正确答案
  • C.参数 b 控制词频饱和曲线
  • D.词频越高总得分一定越高

答案:AB。BM25 的 IDF 项衡量词项稀有度,长度归一化项惩罚长文档,A、B 正确。控制词频饱和的是 k1 而非 b,C 错。虽然词频项单调递增,但 IDF 可能为负,若语料中一半以上文档都含该词,总贡献为负,D“一定越高”说法错误。

Q8. 多个候选片段评分接近且内容高度重叠时,应如何处理?

单选题 · mid

  • A.合并重叠片段并取最高分← 正确答案
  • B.全部无过滤地返回给用户
  • C.随机选取其中一个片段
  • D.直接丢弃全部候选片段

答案:A。重叠片段往往是在描述同一句话,合并后再打分能消除冗余并稳定答案。全部返回会让用户看到重复内容,随机选取或全丢弃都会丢失信息,B、C、D 都不合理。

Q9. MRR 指标中,答案出现在第 3 位时的倒数贡献值是?

单选题 · hard

  • A.三分之一值的贡献← 正确答案
  • B.三倍权重的贡献
  • C.没有任何贡献值
  • D.固定一半的贡献值

答案:A。MRR(平均倒数排名)是对每个查询取第一个正确答案所在位置的倒数。第 3 位命中则贡献 1/3。位置越靠后贡献越小,这体现了“答案越靠前越好”的设计思想。B、C、D 都与倒数排名的定义不符。

Q10. 检索式问答系统的主要组成模块包括哪些?

多选题 · easy

  • A.知识库与文档切分模块← 正确答案
  • B.倒排索引与检索模块← 正确答案
  • C.答案抽取与打分模块← 正确答案
  • D.显卡驱动与散热模块

答案:ABC。知识库切分、倒排索引检索、答案抽取是检索式 QA 的三大支柱模块,对应着“存得好、找得快、答得准”。显卡驱动和散热属于硬件运维范畴,不是问答系统组件,D 错。