想象你走进一座藏书上亿册的超级图书馆。书架从地下一层延伸到天台,而你只有一个问题:“中国的首都是哪里?”你当然不会自己逐层翻找,而是径直走向读者问询台。这个问询台之所以高效,不是因为某位员工记忆力超群,而是因为三个人各司其职:快搜员凭“直觉”秒速抱回一摞可能相关的书,资深审读员逐页精读后筛出真正有用的几页,最后综合写作专家用自己的话把答案写给你。这三个人之间的分工与协作,正是开放域问答系统(Open-domain QA)的核心架构——检索(Retrieval)、重排(Reranking)与生成(Generation)。 本文会带你拆开这个问询台的每个岗位:快搜员如何练出语义直觉(密集检索器),审读员如何精准把关(重排与融合),写作专家如何组织语言(生成式问答),馆长如何考核服务质量(评估指标),高峰期如何不排队(工程部署),以及培训教材和书架如何打理(数据与预处理)。读完你就能理解:为什么搜索引擎顶部会出现答案卡片,为什么企业客服机器人能秒回长尾问题,以及你自己如何动手搭建一套问答系统。

岗位关联

学习开放域问答系统设计,是进入检索增强生成(RAG)、企业知识库、搜索引擎问答等一线应用场景的核心技能,也是 NLP 算法工程师从“调模型”走向“搭系统”的关键一步。

目录
  1. 可视化演示
  2. 系统整体架构
  3. 密集检索器设计
  4. 重排与融合
  5. 生成式问答
  6. 评估指标体系
  7. 工程部署优化
  8. 数据与预处理
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示开放域QA的四阶段流水线(查询→密集检索→阅读理解→答案生成),文档库Top-K检索过程,以及密集检索(语义向量相似度)与稀疏检索(BM25词项匹配)的对比。

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

开放域QA流水线 QRY 查询:用户问题输入 RET 检索:DPR从文档库检索Top-K相关段落 ANS 答案:阅读器从段落中抽取最终答案 answer = Reader(Retriever(query, K)) 两阶段检索-阅读架构是开放域QA的核心范式。

系统整体架构

回到那座超级图书馆。问询台之所以高效,关键不是某个人有多强,而是三个人形成了流水线:快搜员负责缩小范围,审读员负责精挑细选,写作专家负责组织语言。任何一环掉链子,整条流水线都会卡在最弱的岗位上。开放域问答面对的问题一模一样——文档库大到模型根本“读不完”,所以主流架构先把任务拆成两段:先检索,再阅读/生成。检索阶段像快搜员,用最快速度从亿级文档中捞回几百个候选;阅读/生成阶段像写作专家,只对着这几百个候选认真作答。

具体到工程落地,完整流水线包含离线和在线两类组件。离线侧负责构建文档库:爬取原始网页、清洗噪音、切分段落、用编码器把段落向量化并建索引。在线侧先做查询理解,包括拼写纠错、同义改写、意图识别,再把“召回 → 重排 → 生成”三段式串起来。各模块之间通过标准接口衔接,可以独立替换和升级——今天换一个更强的检索器,明天换一个更大的生成模型,都不会牵一发动全身。

从概率角度看,整个系统其实是在估计“给定问题和文档库,答案出现的概率”。这个概率被分解成“先找到相关文档、再从文档中生成答案”两步的加权和:如果检索器根本没召回答案所在的文档,那么生成器再强也拿不到零分。

P(a∣q,D)=∑d∈R(q)Pret(d∣q)⋅Pgen(a∣q,d)\displaystyle P(a \mid q, \mathcal{D}) = \sum_{d \in \mathcal{R}(q)} P_{\text{ret}}(d \mid q) \cdot P_{\text{gen}}(a \mid q, d)
这个公式把问答拆成了“检索 × 生成”的因果链:检索概率为 0 的文档,对最终答案概率没有任何贡献。所以开放域问答圈有一句行话——检索决定上限,生成决定下限。
  1. 查询与文档库q 表示用户提出的问题,D 表示整个文档集合,相当于图书馆里从上到下所有藏书。
    q∈Query,D={d1,d2,…,dN}\displaystyle q \in \text{Query},\quad \mathcal{D} = \{d_1, d_2, \dots, d_N\}
  2. 检索子集R(q) 是检索器从全库中召回的相关文档子集,就像快搜员抱回来的那摞可能相关的书。
    R(q)={d∈D∣S(q,d)>θ}\displaystyle \mathcal{R}(q) = \{d \in \mathcal{D} \mid S(q, d) > \theta\}
  3. 概率连乘答案概率等于“文档被召回的概率”乘以“给定文档生成答案的概率”,再对所有召回文档求和。

密集检索器设计

快搜员为什么这么快?因为他从不逐字读书,而是凭“直觉”判断一本书和问题的关系。你问“猫咪该怎么养”,他能一秒想起角落里那本《养猫指南》——尽管书名里根本没有“猫咪”两个字。这种跳过字面、直达含义的能力,就是密集检索要复刻的直觉。传统的关键词检索(如 BM25)像查字典:问题里的词必须和文档里的词一模一样才加分,遇到“猫”和“猫咪”、“首都”和“北京”这种同义表达就彻底失灵。

密集检索(Dense Retrieval)用神经网络把查询和文档各编码为一个低维稠密向量(Embedding)。向量可以理解为文本的“语义指纹”:语义相近的文本,指纹在空间里也靠得近。最常见的实现是双塔编码器——一个查询塔只编码问题,一个文档塔只编码段落,两塔把各自的输入压成同一个语义空间里的向量。打分时用余弦相似度衡量两个向量的方向是否一致。

线上部署时,文档塔的向量全部离线算好,存入专门的向量索引(如 FAISS、HNSW)。查询向量一到,用近似最近邻搜索代替全库线性扫描,把召回从“遍历上亿本书”变成“对数级跳转”,毫秒级返回 top-k 候选。这就像快搜员脑子里有一套精确到书架格子的地图,而不是真的去数每一本书。

快搜员的“直觉”不是天生的,他经历过大量“看问题、翻对书”的对比学习训练,才建立起“什么话题对应什么书”的敏感度。这个训练过程,我们在“数据与预处理”一节会详细展开。

S(q,d)=cos⁡(eq,ed)=eq⋅ed∥eq∥⋅∥ed∥\displaystyle S(q, d) = \cos(\mathbf{e}_q, \mathbf{e}_d) = \frac{\mathbf{e}_q \cdot \mathbf{e}_d}{\|\mathbf{e}_q\| \cdot \|\mathbf{e}_d\|}
余弦相似度把两个向量的“方向一致程度”压缩到 [-1, 1]:越接近 1,说明查询和文档的语义越接近。分母的模长归一化保证长文档不会因为“词多”就天然得分高。
  1. 嵌入向量e_q 和 e_d 分别是查询塔与文档塔输出的稠密向量,它们是文本经过神经网络压缩后的数字指纹。
    eq=Encq(q),ed=Encd(d)\displaystyle \mathbf{e}_q = \text{Enc}_q(q), \qquad \mathbf{e}_d = \text{Enc}_d(d)
  2. 点积分子两个向量逐元素相乘后求和,衡量它们在各个维度上的“对齐程度”。
    eq⋅ed=∑ieq,i⋅ed,i\displaystyle \mathbf{e}_q \cdot \mathbf{e}_d = \sum_{i} e_{q,i} \cdot e_{d,i}
  3. 模长归一把点积结果除以两个向量模长的乘积,让分数不受文本长度影响,只反映方向相似度。
    ∥eq∥=∑ieq,i2\displaystyle \|\mathbf{e}_q\| = \sqrt{\sum_{i} e_{q,i}^2}

重排与融合

快搜员抱回来二十本书,你不可能把二十本都直接扔给写作专家——信息太多反而干扰判断。这时资深审读员登场:他戴上眼镜,把“问题”和“每本书”放在一起逐页对照,快速淘汰不相关的书,最后只留下两三本。审读员的技术原型是重排器(Reranker),最典型的是交叉编码器(Cross-Encoder):把“问题 + 候选段落”拼成一个输入,让 Transformer 同时看到两边,输出一个精确的相关性分数。

代价是每个候选都要独立过一遍完整模型,计算量比双塔检索大几十倍。所以系统设计成“粗召回 + 精排”两段式:第一段用便宜快速的检索器把候选从亿级压到几十个,第二段再用昂贵的交叉编码器把几十个压到两三个。用速度换精度,这是工程上最经典的杠杆。

现实系统往往不止一个快搜员:BM25 字面召回一路、密集检索语义召回一路,有时还叠加知识图谱一路。多路结果怎么合并?直接比分数不公平,因为各路的分数分布天差地别。更常用的做法是 RRF(Reciprocal Rank Fusion,倒数排名融合)——不看分数看名次,把每个文档在各路的排名倒数相加,名次越靠前贡献越大。

审读员只负责“筛”,不负责“写”。经过他把关,几十个候选被压缩成最可靠的一两段,写作专家看到的材料越精炼,最后写崩的概率就越低。

RRF(d)=∑r∈R1k+rankr(d)\displaystyle \text{RRF}(d) = \sum_{r \in \mathcal{R}} \frac{1}{k + \text{rank}_r(d)}
这个公式像“多评委投票”:每个通道相当于一位评委,评委只按名次打分。常数 k 用来平滑名次差异,通常取 60。融合后按总分重新排序,把各路最有共识的文档顶到最前面。
  1. 排名位置rank_r(d) 表示文档 d 在第 r 路检索结果中的名次,名次越靠前,倒数值越大。
    rankr(d)∈{1,2,…,∣Dr∣}\displaystyle \text{rank}_r(d) \in \{1, 2, \dots, |\mathcal{D}_r|\}
  2. 倒数加权1 除以 (k + 名次),把“名次”平滑地转换成“得分”,常数 k 调节名次之间的差距敏感度。
    1k+rankr(d),k=60(常用值)\displaystyle \frac{1}{k + \text{rank}_r(d)}, \qquad k = 60 \text{(常用值)}
  3. 多路累加对每一路召回到的同一篇文档,把它的名次得分累加起来,再按总分做全局重排。

生成式问答

材料备齐,写作专家登场。他和审读员最大的区别是:不摘抄原文,而是理解后用流畅的话组织答案。这样即便检索到的段落表述晦涩,他也能给出通俗回复,甚至能处理“这两个概念有什么区别”这类需要推理的问题。这类模型在技术上叫序列到序列(Seq2Seq)架构,代表作有 BART、T5 以及各类大语言模型。输入是“问题 + 检索段落”拼接后的 token 序列,输出是一段答案 token 序列。

与之相对的是抽取式阅读器:直接从命中段落里“剪”出答案片段,答案中每个字都必须来自原文。抽取式更稳妥但缺乏灵活性,生成式更自然但容易“自由发挥”。工业界常用“检索 + 生成”的组合,也就是今天大热的检索增强生成(RAG)。

生成式模型的训练目标,是让给定输入 x 时输出答案 y 的每个词都尽可能接近真实答案。解码时常用 Beam Search 同时维护多条候选序列,最后挑整体概率最高的一条。它最大的隐患是幻觉(Hallucination)——模型会写出“读起来通顺但文档里根本没有”的内容。工业系统的应对手段包括:强制模型只能基于检索段落作答、要求答案附带引用出处、对找不到依据的问题直接拒答。

写作专家偶尔也会“编故事”,所以问询台立了条铁规矩:答案必须搭配出处,找不到出处的宁可回答“不知道”。这条规矩,是生成式问答从“炫技”走向“可用”的分水岭。

P(y∣x)=∏t=1TP(yt∣y<t,x;θ)\displaystyle P(y \mid x) = \prod_{t=1}^{T} P(y_t \mid y_{<t}, x; \theta)
答案的整体概率 = 每个词在“看到前文和输入”条件下的概率连乘。写作专家每写下一个词,都要回头看看自己刚写了什么、以及检索材料里说了什么。
  1. 序列展开答案 y 由 T 个词组成,整体概率等于每个词的条件概率连乘。
    P(y∣x)=∏t=1TP(yt∣y<t,x;θ)\displaystyle P(y \mid x) = \prod_{t=1}^{T} P(y_t \mid y_{<t}, x; \theta)
  2. 条件依赖生成第 t 个词时,模型依赖前面已经生成的所有词 y_<t,以及原始输入 x。
    yt∼P(⋅∣yt−1,…,y1,x)\displaystyle y_t \sim P(\cdot \mid y_{t-1}, \dots, y_1, x)
  3. 极大似然训练训练时找一组参数 θ,让真实答案序列的对数概率最大化,等价于最小化交叉熵损失。
    θ∗=arg⁡max⁡θ∑(x,y)∈Tlog⁡P(y∣x;θ)\displaystyle \theta^* = \arg\max_{\theta} \sum_{(x, y) \in \mathcal{T}} \log P(y \mid x; \theta)

评估指标体系

馆长怎么判断问询台干得好不好?不是看员工忙不忙、回答长不长,而是定期做满意度调查:答案对不对、全不全、检索准不准。一套好的指标体系,能把“感觉还不错”变成可量化的改进方向。答案层面最常用的两个指标是 EM 与 F1。EM(Exact Match,精确匹配率)要求预测答案与标准答案逐字完全一致,极其严格;F1 则把两个答案都拆成词元,计算重叠部分的精确率与召回率,再做调和平均,允许“部分答对”。

检索层面关注Recall@k:检验真实答案所在的段落是否出现在召回的前 k 名里。这个指标决定了系统的上限——如果答案段落压根没被召回,后面的重排与生成再强也无力回天。另一个常用指标是 MRR(平均倒数排名):看答案段落被排在第几名,名次越靠前分数越高。

自动指标也不是万能的。同义改写会让 EM 误判,比如“北京天气”和“北京的气象”表达同一意思却算全错。所以每隔一段时间要人工抽检,评估“流畅、忠实、有用”三个维度,用抽样结论校准自动指标——满意度调查从来不是一道单选题。

F1=2×P×RP+R,P=∣pred∩gold∣∣pred∣,R=∣pred∩gold∣∣gold∣\displaystyle F_1 = \frac{2 \times P \times R}{P + R}, \qquad P = \frac{|\text{pred} \cap \text{gold}|}{|\text{pred}|}, \qquad R = \frac{|\text{pred} \cap \text{gold}|}{|\text{gold}|}
F1 是精确率 P 和召回率 R 的调和平均。它对“偏科”很敏感:一个系统如果只顾精确、不顾召回,F1 会被明显拉低。就像满意度调查既看“答对的比例”,也看“该答的全没全答”。
  1. 精确率 P预测答案中,有多大比例的词元与标准答案重合——答出来的内容里有多少是对的。
    P=∣pred∩gold∣∣pred∣\displaystyle P = \frac{|\text{pred} \cap \text{gold}|}{|\text{pred}|}
  2. 召回率 R标准答案中,有多大比例被预测答案覆盖——该答的内容里答出了多少。
    R=∣pred∩gold∣∣gold∣\displaystyle R = \frac{|\text{pred} \cap \text{gold}|}{|\text{gold}|}
  3. 调和平均把 P 和 R 综合成一个分数。与算术平均不同,调和平均对偏科更严厉。
    F1=2×P×RP+R\displaystyle F_1 = \frac{2 \times P \times R}{P + R}

工程部署优化

问询台平时运转流畅,可一到午休高峰,读者排起长队,等超过十分钟就会转身离开。线上问答系统最硬的约束同样是延迟预算,业界常用 p99 衡量:99% 的请求必须在指定时间内(比如 500ms)返回。端到端时延由三环节叠加:检索(向量库近邻查询,毫秒级)、重排(交叉编码器逐候选打分,几十毫秒)、生成(自回归逐字输出,几百毫秒)。生成通常是大头,但检索和重排决定了候选质量,三者都逃不过预算管理。

工程上有几把常用手术刀。检索端用 HNSW 索引和向量量化压缩内存,离线预计算全部文档向量;重排端做模型蒸馏,把大交叉编码器的知识压进小模型,或者把交叉编码器特征蒸馏回双塔;生成端用 KV Cache 缓存历史注意力、用增量解码一次只推新词;再把向量库和模型分片部署到多台机器,横向扩展吞吐。

全局层面还有两层缓存:热门问题的最终答案直接缓存,高频文档的检索结果缓存。命中时整个流水线可以跳过,响应时间从几百毫秒降到几十毫秒。这就像把常见问题提前贴在墙上,读者一抬头就看见答案,完全不用排队。

高峰期排队的智慧不是让每个人都跑得更快,而是“热门问题提前贴墙、冷门难题才走全流程”。延迟预算不是束缚,而是倒逼架构做减法的指挥棒。

Ttotal=Tret+Trerank+Tgen≤Tbudget\displaystyle T_{\text{total}} = T_{\text{ret}} + T_{\text{rerank}} + T_{\text{gen}} \leq T_{\text{budget}}
总时延是三个环节的累加,必须压在预算以内。p99 意味着你还要考虑最差情况——99% 的请求都达标还不够,剩下 1% 的慢请求同样影响用户体验。
  1. 各阶段时延总时延等于检索、重排、生成三个环节的耗时之和,任何一环超时都会拖垮整体。
    Ttotal=Tret+Trerank+Tgen\displaystyle T_{\text{total}} = T_{\text{ret}} + T_{\text{rerank}} + T_{\text{gen}}
  2. 预算约束系统要求绝大多数请求(通常以 p99 衡量)满足延迟上限。
    Ttotal≤Tbudget,p99(Ttotal)<500ms\displaystyle T_{\text{total}} \leq T_{\text{budget}}, \qquad \text{p99}(T_{\text{total}}) < 500\text{ms}
  3. 逐项优化每次优化的收益可以逐项相加——压检索用 ANN 索引,压重排用蒸馏,压生成用 KV Cache。
    ΔT=ΔTret+ΔTrerank+ΔTgen\displaystyle \Delta T = \Delta T_{\text{ret}} + \Delta T_{\text{rerank}} + \Delta T_{\text{gen}}

数据与预处理

问询台再好的分工,也需要两样基础:整齐的书架和优质的培训教材。书架乱七八糟,快搜员再敏锐也会漏书;教材错误百出,新人学出来的直觉也会跑偏。数据质量,决定了整套系统的天花板。文档库构建的第一步是清洗:去除重复网页、广告噪音、低质内容,统一编码格式。接着是段落切分:把长文档切成 100-200 词的语义段落,段落之间保持少量重叠,防止答案被拦腰截断。这些切好的段落就是向量索引的基本单位,也是后续检索和重排的输入粒度。

训练检索器还需要问答对数据。正样本是“问题 + 答案所在段落”,负样本则大有讲究——随机负样本太简单,模型学不到精细边界。困难负样本挖掘专门挑“与问题高度相似但不含答案”的段落来锤炼模型:比如问“中国的首都是哪里”,某个谈论中国经济的段落就是很好的困难负样本。数据增强方面,常用同义改写和回译:把问答对翻译成另一种语言再翻译回来,获得措辞不同的新样本。

这些做法在数学上对应对比学习的目标:让正样本对的相似度尽量高,同时把负样本的相似度压下去。损失函数 InfoNCE 用指数形式放大正负样本的差距,温度系数控制训练的“火药味”。

书架整理得越整齐、教材批注得越用心,问询台的整体水平就越高。很多项目调模型效果不明显,最后靠仔细清洗数据、挖好负样本就显著提升——数据预处理,永远是开放域问答里性价比最高的杠杆。

L=−log⁡eS(q,d+)/τeS(q,d+)/τ+∑j=1meS(q,dj−)/τ\displaystyle \mathcal{L} = -\log \frac{e^{S(q, d^{+}) / \tau}}{e^{S(q, d^{+}) / \tau} + \sum_{j=1}^{m} e^{S(q, d^{-}_j) / \tau}}
这个损失可以理解为“在一堆候选中认出正确答案”的难度。分子是问题与正确段落的相似度,分母还要加上所有错误段落的相似度——正确段落必须在一群“长得像的干扰项”中脱颖而出。
  1. 正样本指数项分子是问题 q 与正样本段落 d⁺(含答案的段落)的相似度,经温度缩放后取指数。
    eS(q,d+)/τ\displaystyle e^{S(q, d^{+}) / \tau}
  2. 负样本分母项分母累加所有负样本的指数项,构成一个“竞争对手池”,让正确段落必须胜过所有干扰项。
    ∑j=1meS(q,dj−)/τ\displaystyle \sum_{j=1}^{m} e^{S(q, d^{-}_j) / \tau}
  3. 温度系数τ 调节分布的尖锐程度——τ 越小,分布越尖锐,模型越聚焦困难负样本。
    τ∈(0,1],τ→0⇒更强调难负样本\displaystyle \tau \in (0, 1], \qquad \tau \to 0 \Rightarrow \text{更强调难负样本}
  4. 整体取负对数把分数转成损失值,最小化它等价于让正样本相似度尽量高于所有负样本。
    L=−log⁡eS(q,d+)/τeS(q,d+)/τ+∑j=1meS(q,dj−)/τ\displaystyle \mathcal{L} = -\log \frac{e^{S(q, d^{+}) / \tau}}{e^{S(q, d^{+}) / \tau} + \sum_{j=1}^{m} e^{S(q, d^{-}_j) / \tau}}

AI 应用场景

企业知识库智能问答客服

阶段:检索增强生成(RAG)全流程——文档库构建、密集检索、重排、生成。

解决的问题:传统客服依赖人工翻阅 FAQ 和工单,响应慢、无法覆盖长尾问题、夜间无人值守。开放域 QA 架构把企业文档切成段落建索引,用户提问后自动召回相关段落并生成带引用的回答,让“离职后如何重置 VPN 密码”这类冷门问题也能秒回。

具体案例:某 SaaS 公司把 2000 份产品文档和工单历史建成知识库,内部数据显示:平均响应时间从 1 小时降到 5 秒,人工抽检答中率 82%,且每条回答都附原文出处,用户可一键核对。

搜索引擎的答案卡片

阶段:检索召回 + 重排 + 答案抽取/生成。

解决的问题:传统搜索返回十条蓝色链接,用户需要逐个点击判断;对“北京是哪个国家的首都”这类事实型问题,答案卡片直接把答案亮在结果顶部,大幅提升一次点击满意率。

具体案例:主流搜索引擎对事实类查询展示答案卡片,流程是:网页库向量检索 → 交叉编码器重排 → 从 top 段落中抽取短句并附来源链接,整个过程在几百毫秒内完成,覆盖了从百科到新闻的实时文档库。

法律/医疗领域的证据可追溯问答

阶段:受限检索 + 引用溯源 + 拒答策略。

解决的问题:法律和医疗领域不允许“编答案”,幻觉会带来严重后果。系统强制“答案必须来自检索证据”,在库中找不到依据时直接拒答。

具体案例:法律助手在判例库中回答“竞业限制协议最长年限是几年”,检索到《劳动合同法》相关条款后生成答案并附法条引用;对“我这份合同条款是否有效”这类超出检索证据范围的问题,系统回复“未检索到足够依据”,宁可不说也不乱说。

Python 演示

python103 行
"""开放域问答(OpenQA)迷你流水线演示 用一套可离线运行的简化实现,演示开放域问答的核心流程:1. 构建文档库(一组短段落)2. 检索召回(用 TF-IDF 向量近似“密集检索”的语义匹配)3. 重排(按召回分数取 top-k,模拟粗排到精排的收缩)4. 抽取式回答(用关键词定位答案片段) 真实系统会使用神经网络双塔编码器 + 交叉编码器 + 生成式模型(如 RAG),此处用传统方法让读者在任意环境跑通流程。""" import mathimport refrom collections import Counter DOCS = [    "北京是中国的首都,也是中国北方的政治文化中心。",    "上海是中国的经济金融中心,位于长江入海口。",    "向日葵是一种常见的植物,它的花盘会跟着太阳转动。",    "秋天树叶变黄,是因为叶绿素逐渐分解。",    "Python 是一种解释型编程语言,语法简洁、生态丰富。",    "长城是古代中国修建的宏伟军事防御工程。",    "西瓜是夏天常见的水果,水分充足、口感清甜。",    "量子计算利用量子叠加与纠缠进行并行计算。",] STOP = {"的", "是", "了", "也", "在", "和", "与", "或", "等", "吗", "呢"}  def tokenize(text):    """把文本切成词粒:中文按连续汉字切分,英文按单词切分。"""    return re.findall(r"[\u4e00-\u9fa5A-Za-z0-9]+", text)  def tfidf_vec(tokens, n_docs, df):    """把词序列转成 TF-IDF 加权向量(字典表示,模拟嵌入向量)。"""    total = len(tokens)    cnt = Counter(tokens)    vec = {}    for tok in tokens:        vec[tok] = (1 + math.log(cnt[tok])) * math.log((n_docs + 1) / (df.get(tok, 0) + 1) + 1)    return vec  def build_stats(docs):    """统计每篇文档的 token 集合与各 token 的文档频率。"""    n = len(docs)    df = Counter()    for doc in docs:        for tok in set(tokenize(doc)):            df[tok] += 1    return n, df  def cos_sim(a, b):    """余弦相似度:衡量查询向量与文档向量的方向一致性。"""    dot = sum(a.get(tok, 0) * b.get(tok, 0) for tok in set(a) | set(b))    na = math.sqrt(sum(v * v for v in a.values()))    nb = math.sqrt(sum(v * v for v in b.values()))    return dot / (na * nb + 1e-9)  def retrieve(query, docs, k=3):    """检索阶段:把查询与每个文档算相似度,返回 top-k 候选。"""    n, df = build_stats(docs)    qv = tfidf_vec(tokenize(query), n, df)    scored = []    for i, doc in enumerate(docs):        dv = tfidf_vec(tokenize(doc), n, df)        scored.append((cos_sim(qv, dv), i))    scored.sort(reverse=True)    return scored[:k]  def extract_answer(query, passage):    """抽取式回答:定位查询关键词在段落中的位置,截取答案片段。"""    qtoks = [tok for tok in tokenize(query) if tok not in STOP]    ptoks = tokenize(passage)    for i, tok in enumerate(ptoks):        if tok in qtoks:            prev = ptoks[i - 1] if i > 0 else ""            start = i if (not prev or prev in STOP) else i - 1            end = min(len(ptoks), i + 5)            return "".join(ptoks[start:end])    return passage[:15]  def main():    query = "北京是哪个国家的首都?"    print("用户问题:", query)    print("\n=== 阶段一:检索召回 top-k(按相似度排序)===")    candidates = retrieve(query, DOCS, k=3)    for score, idx in candidates:        print(f"  相似度 {score:.4f}  {DOCS[idx]}")    best_idx = candidates[0][1]    print("\n=== 阶段二:抽取式回答 ===")    answer = extract_answer(query, DOCS[best_idx])    print("  答案:", answer)  if __name__ == "__main__":    main()
目的:用纯标准库实现一个“检索 + 抽取式回答”的迷你开放域问答管道,展示检索、重排、答案生成三阶段的完整流程,无需安装任何深度学习框架即可运行。
  1. def tokenize(text):把中文和英文文本切成词粒,是后续所有向量化计算的基础设施。
  2. vec[tok] = (1 + math.log(cnt[tok])) * math.log((n_docs + 1) / (df.get(tok, 0) + 1) + 1)TF-IDF 加权:出现频率高但只出现在少数文档中的词,权重更高,更能代表主题。
  3. def cos_sim(a, b):余弦相似度对应正文中的语义打分公式,衡量两个向量的方向一致性。
  4. def retrieve(query, docs, k=3):检索阶段的核心:计算查询与每个文档的相似度,取 top-k 作为候选,对应快搜员抱书。
  5. scored.append((cos_sim(qv, dv), i))每条候选都带上分数和原文编号,方便后续按名次取回段落。
  6. def extract_answer(query, passage):在命中段落里用关键词定位答案起点,模拟“阅读器”的抽取逻辑。
  7. if __name__ == "__main__":以脚本方式运行时自动执行演示流程,输出检索排名和最终答案。

常见误区

误区我以为开放域问答的检索和传统搜索一样,只要问题里出现过的词在文档里出现就能召回,没出现就没办法。

正解字面匹配(BM25、TF-IDF)确实只能处理词面重合,但密集检索把查询和文档映射到语义向量空间,“猫”和“猫咪”、“中国”和“中华”即使字面不同也能靠向量距离匹配上。真实系统通常两者混合:稀疏检索保证可解释性和冷门词覆盖,密集检索补足语义泛化能力。

为何常见:因为最早接触搜索引擎时,“关键词命中”是最直观的体验,容易把“检索”窄化为“字符串匹配”。

误区既然后面的重排和生成模型很聪明,那我干脆把 top-k 设成 100,把所有可能相关的段落都喂进去,答案一定更准。

正解候选太多会带来三方面问题:重排延迟随候选数线性增长;生成模型在长上下文中更容易被噪声带偏,“信息淹没”反而降低准确率;检索混入的无关段落会直接污染生成的答案。实践上一般召回 20-100 个候选进重排,重排后只留 1-5 个进生成。

为何常见:直观上认为“给模型的信息越多越好”,忽视了噪声对生成模型的影响,以及延迟预算这条工程红线。

误区GPT 这类大模型知识量很大,直接问它就行了,为什么还要费劲做检索和索引?

正解模型参数里的知识有截止时间、会过时,也无法覆盖企业私有文档和实时资讯。检索增强生成(RAG)把“答案从外部文档来”变成硬约束:知识可实时更新、可溯源、可做权限控制。这正是开放域问答的主流形态,而不是让模型“背答案”。

为何常见:聊天机器人体验流畅,让人误以为模型“无所不知”,忽略了参数化知识在事实准确性和时效性上的固有短板。

误区评估时我只看 EM(精确匹配率),它最严格、最客观,所以分数高就代表系统靠谱。

正解EM 要求预测与标准答案逐字完全一致,同义改写会被误判为错误,方差很大。业界实际用 EM + token 级 F1 + 检索侧 Recall@k 组合评估,再辅以人工抽检流畅度与忠实度。单一硬指标看似客观,实际上会误导优化方向。

为何常见:单一数值指标看起来简单直观,容易被误当作“金标准”,忽略了它过于严格和脆弱的缺陷。

误区测试集上 F1 和 EM 刷得很高,部署后用户应该也会满意。

正解离线集与线上真实分布存在偏差:线上问题更长、更口语化,文档库持续更新,还要同时满足 p99 延迟和硬件成本约束。上线前要做压测、缓存命中率评估、线上 AB 实验,用真实反馈持续迭代,离线指标只是入场券。

为何常见:实验室评估环境相对固定,容易让人忽视工程约束和真实流量的多样性。

面试题

Q1. 开放域问答系统的两阶段核心流水线是什么?

单选题 · easy

  • A.分词之后再去做词性标注
  • B.先检索召回再阅读生成← 正确答案
  • C.先聚类然后做文本分类
  • D.先翻译再生成摘要

答案:B。开放域问答面对的是亿级规模的文档库,任何模型都无法直接读完所有内容。主流架构分两步:先用快速检索器召回少量候选段落,再用阅读器(抽取)或生成器(生成)产出答案。这就像图书馆里快搜员先抱回几本书,写作专家再组织语言回答问题。

Q2. 密集检索器用什么方式表示文档和查询?

单选题 · easy

  • A.低维稠密的语义向量← 正确答案
  • B.高维稀疏的词频表
  • C.字符 n 元文法
  • D.依存句法分析树

答案:A。密集检索(Dense Retrieval)用神经网络编码器把文本映射为低维稠密向量(Embedding),语义相近的文本向量距离也近。“猫”和“猫咪”字面不同但向量相近。词频表和 n 元文法是字面匹配,句法树关注语法结构而非语义相似度,都不是密集检索的表示方式。

Q3. 双塔编码器中“双塔”指的是哪两部分?

单选题 · mid

  • A.自注意力层和前馈层
  • B.查询编码塔与文档编码塔← 正确答案
  • C.词嵌入层与位置编码层
  • D.编码器塔与解码器塔

答案:B。双塔(Two-Tower)结构包含两个独立编码器:查询塔只编码用户问题,文档塔只编码文档段落,两者输出向量在同一个语义空间中比较相似度。自注意力/前馈是 Transformer 内部组件,词嵌入与位置编码是输入表示层,编码器-解码器则是生成式模型的架构。

Q4. 检索器召回候选后,为什么还需要一个重排阶段?

单选题 · mid

  • A.为了进一步减少网络流量
  • B.因为粗召回精度不够高← 正确答案
  • C.为了统一候选长度格式
  • D.为了提升向量索引速度

答案:B。检索器为了速度采用双塔和 ANN 索引,精度有限,top-k 里常混入不相关段落。重排阶段用交叉编码器等更精细的模型逐一对“问题+候选”打分,精度高但速度慢,正好与“粗召回”互补——粗检保速度、精排保精度。这对应问询台里审读员的角色。

Q5. 生成式问答中“幻觉”指的是什么?

单选题 · mid

  • A.模型编造与文档不符的内容← 正确答案
  • B.模型训练时产生随机噪声
  • C.检索器返回了空的结果集
  • D.缓存失效导致的延迟抖动

答案:A。生成模型是概率式“写”答案,可能出现“读起来流畅但与检索文档事实不符”的内容,这就是幻觉。缓解手段包括强制基于检索段落生成、引用溯源、对无依据问题拒答。训练噪声、空结果和缓存抖动都与幻觉不是一回事。

Q6. 对比学习 InfoNCE 损失中,温度系数 τ 的主要作用是什么?

单选题 · extreme

  • A.避免相似度分母出现零
  • B.调节概率分布的锐利程度← 正确答案
  • C.提升模型推理的整体速度
  • D.限制负样本的采样数量

答案:B。τ 除以相似度后,通过指数运算影响 softmax 分布的集中程度:τ 越小分布越尖锐,模型越强调把正样本从困难负样本中“挤”出来;τ 越大分布越平缓,训练越平滑。它的作用不是防除零、提速或限采样量,而是控制优化信号的聚焦程度。

Q7. 线上问答系统控制 p99 延迟,最有效的手段是?

单选题 · hard

  • A.把检索 top-k 数量调得更大
  • B.无限加大推理时的批次大小
  • C.热门结果缓存与模型蒸馏← 正确答案
  • D.换用参数更大的生成模型

答案:C。p99 延迟预算要求绝大多数请求在时限内返回。热门问题查缓存直接命中可跳过全链路,模型蒸馏把大模型压缩为小模型降低推理耗时,两者直击时延瓶颈。增大 top-k 和 batch、换更大模型都会让延迟变得更糟,与优化目标背道而驰。

Q8. 开放域问答中,检索阶段常用的评估指标有哪些?

多选题 · easy

  • A.前 k 名检索召回率← 正确答案
  • B.平均倒数排名指标← 正确答案
  • C.模型训练显存占用率
  • D.网络传输带宽大小

答案:AB。检索评估关注“真实答案所在段落是否被召回”。Recall@k(A)看前 k 名是否包含答案段落,MRR(B)看重排后答案段落的平均名次——名次越靠前分数越高。显存占用和带宽是工程资源指标,衡量的是成本和吞吐,不评价检索准确度。

Q9. 训练密集检索器时,常见的负样本构造方式有?

多选题 · mid

  • A.批次内随机负采样← 正确答案
  • B.困难负样本挖掘策略← 正确答案
  • C.对全库做暴力全量枚举
  • D.人工逐条标注所有文档

答案:AB。批次内负采样(A)利用同批次里其他问题的正样本充当负样本,几乎零成本;困难负样本挖掘(B)专门挑“高相似但无答案”的段落,让模型学到精细边界。全库枚举在训练时计算量不可行,人工标注所有文档成本极高,都不是实际可行的做法。

Q10. 相比纯大模型直接作答,检索增强生成(RAG)的优势有哪些?

多选题 · hard

  • A.外部知识可以实时更新← 正确答案
  • B.显著减少事实性幻觉← 正确答案
  • C.完全消除所有推理开销
  • D.无需维护任何文本索引

答案:AB。RAG 把答案锚定在检索到的文档上:知识库更新即答案更新(A),且要求模型基于证据作答、可追溯引用,从而减少幻觉(B)。但检索、索引、重排都有计算成本,推理开销并未消除;答案也依赖索引系统,不存在“无需索引”的免费午餐。