为什么要检索:幻觉、知识截止与长上下文的代价,以及一个纯关键词的最小 RAG
先弄清楚检索增强生成到底替模型解决了哪三个具体毛病,再把它和微调、超长上下文放在一起比成本与适用面,最后不碰任何向量,用 BM25 打分写出第一个能回答问题的最小系统。
今日目标
- 能用一句话说清检索增强生成解决的是知识时效、事实归因和上下文成本这三个问题,并举出一个不该用它的场景
- 能画出检索增强生成的五个环节:切块、建索引、检索、组装上下文、生成,并说明每个环节可能怎么坏掉
- 能手写 BM25 打分函数并解释词频饱和与文档长度归一化这两个修正各自在防什么
今天不装向量库、不调 embedding 接口、不引任何框架,用三百行左右的代码把一条完整的问答链路跑通。读完并做完实验之后,回到页面顶部把这三条目标逐一勾掉。
小白版讲解
开卷考试为什么比背书靠谱,也更便宜
想象两种考试。第一种是闭卷:你把整本教材背下来,考场上凭记忆答题。背得再熟也有三个问题——教材去年修订过一版你不知道;记不清的地方你会下意识地"顺"出一个看着挺像的答案;改卷老师问你"这条依据在哪一页",你答不上来。第二种是开卷:你不背书,只练一件事——听到题目,快速判断该翻哪一页,把那一页读懂,再照着写答案,并在答案后面标上页码。
这门课从头到尾只讲第二种考试怎么考好。它的正式名字叫检索增强生成(retrieval-augmented generation,简称 RAG):用户提问时,先去一个外部的资料库里把最相关的几段捞出来,连同问题一起交给模型,让模型只依据这几段作答。模型还是那个模型,变的是它答题时手边有什么。
开卷比闭卷强在哪?强在三件很实在的事上。资料库改一行,下一次提问立刻生效,不用重新训练任何东西;答案能指回具体哪一段,出了问题可以追溯;每次只把相关的那几段搬上桌,而不是把整个图书馆搬上桌,token 账单差着量级。这三件事分别对应知识时效、事实归因、上下文成本——今天要建立的第一个判断就是这三条,后面十三天的所有工作都是在把这三条做扎实。
这个类比会贯穿整门课,先把词对上号:语料是馆藏,切块是把整本书拆成一条条词条,索引是卡片目录,检索是照着目录去书架上取书,重排是图书管理员在你取回的一摞书里再帮你挑一遍,引用是论文末尾的参考文献,而拒答,是查遍了目录也没查到时老老实实说一句"查不到",而不是自己编一条。
模型的三个毛病,正好是三笔账
先把"模型不够用"这句空话拆成三个能验证的现象。
第一个毛病是知识截止。 模型的知识来自训练数据,训练结束之后发生的事它一概不知。你问它上周刚改的公司报销政策,它只能拿旧版本回答,而且语气跟回答别的问题一样自信。这不是模型"笨",是它根本没有渠道知道这件事。
第二个毛病是幻觉。 模型的工作方式是预测下一个词最可能是什么,"最可能"和"真的"是两回事。当它没有把握时,输出的不是"我不知道",而是一段看起来非常像正确答案的文字——格式对、术语对、语气笃定,只有内容是编的。这比第一个毛病危险得多:知识截止是明确的边界,幻觉没有边界。
第三个毛病是没法归因。 就算这次答对了,你也不知道它凭什么答对,没法把答案指回一份可以核对的原始材料。对个人使用这只是不方便;对企业知识库、客服、法务这类场景,"依据在哪"往往比"答得对不对"更要紧——没有依据的正确答案,在流程上是不能用的。
这三个毛病对应三笔账:知识更新的账、错误的账、token 的账。今天开始的每一个技术选择,最后都要落回这三笔账上算一遍。
三条路:检索、微调、把整本书塞进去
让模型知道它原本不知道的事,工程上有三条路,各有各的成本曲线。
第一条是微调:用你的数据继续训练模型,把知识"焊"进参数里。它擅长的是教模型一种风格或格式——按公司口径写周报、按固定 schema 输出字段。它不擅长灌事实:数据一变就要重训,周期以天计,而且训完依然没法归因。
第二条是把整本书塞进上下文:现在动辄几十万 token 的窗口,小型知识库确实可以整个塞进去。优点是几乎没有工程量,缺点是每一次提问都要为全部材料付一次钱,延迟也随输入长度上升。更麻烦的是材料一多,模型在长上下文里定位关键信息的稳定性会下降——你会看到它"读了但没读到"。
第三条就是检索:材料放在外面,每次只取相关的几段。改材料等于改文件,立刻生效;成本只跟取回来的那几段有关,跟库的总量几乎无关;每一段都带编号,答案可以标出处。代价是你要自己建一套检索系统,而且这套系统本身会出错——接下来十三天处理的基本都是这个"会出错"。
选哪条不是三选一,而是看你缺的是哪样东西。缺"怎么说"选微调,缺"知道什么"选检索,两者经常一起用。至于什么时候不该用检索:如果你的任务根本不依赖外部事实,比如把一段文字改写得更简洁、把代码从一种风格改成另一种,硬塞检索只会引入噪声、增加延迟和成本。判据是这一句:这个任务的答案会不会因为一份文档改了而改变。 不会,就不需要检索。
五个环节,五种坏法
一个检索增强生成系统拆开只有五个环节,从左到右串成一条链:
原始文档 → ① 切块 → ② 建索引 → ③ 检索 → ④ 组装上下文 → ⑤ 生成 → 带出处的回答新手最容易犯的错,是把这条链当成一个黑箱,出了问题只会说"模型答得不好"。实际上每一环都有自己典型的坏法,而且症状很不一样:
- 切块坏了:一条完整的规则被从中间切断,前半句在这一块、后半句在下一块。检索命中了其中一块,但那一块单独看是不完整甚至误导的。症状是答案"半对"。
- 建索引坏了:解析没做干净,表格串了行、页眉页脚混进正文。症状是检索结果里出现一眼就不相干的东西。
- 检索坏了:正确答案根本没进候选。这是最致命的一种,因为后面所有环节都无能为力——材料里没有的东西,模型只能编。症状是答案跑题或一本正经地胡说。
- 组装上下文坏了:材料取对了,但拼提示词时顺序乱了、被截断了、或者忘了写"只能依据下面的资料回答"。症状是模型无视材料,用先验知识作答。
- 生成坏了:材料齐全、指令清楚,模型还是漏读了一条,或者把两段材料的数字张冠李戴。症状是引用编号和内容对不上。
这张表的用法是:排查时从右往左看,修复时从左往右修。 从右往左是因为你最先看到的是生成结果;从左往右是因为左边的错会被右边放大,检索没捞到的东西,再好的提示词也救不回来。第 8 天会把这套排查变成可量化的指标,今天先建立"分环节归因"这个习惯。
把关键词检索做扎实:BM25 逐项拆解
现在动手做第③环。今天完全不用向量,用一个 1990 年代就定型、到今天仍然是所有检索系统标配的算法:BM25。
从最朴素的想法出发:一篇文档里,问题中的词出现得越多,它越可能相关。这就是词频。但光有词频有个大漏洞——"的""我们""公司"这类词到处都是,它们的出现次数完全不能说明相关性。所以要给每个词一个权重,衡量它有多"稀有",这就是逆文档频率:一个词只出现在 2 篇文档里,它一旦命中就非常有说服力;出现在 28 篇里,那基本等于没说。
先做分词。中文没有空格,不能像英文那样按空白切。零依赖场景下最划算的做法是二元组:把"回收站"切成"回收""收站"两个词元,查询里出现同一个词时也切成同样两个,不用词典就能对上。代价是词元数量约等于字数,索引大一倍——对三十篇语料完全无所谓。
// 中文按二元组切,英文数字按词切;标点与空白当分隔符
export function tokenize(text) {
const tokens = []
const segments = text.toLowerCase().match(/[一-鿿]+|[a-z0-9]+/g) ?? []
for (const seg of segments) {
if (/^[一-鿿]+$/.test(seg)) {
if (seg.length === 1) {
tokens.push(seg)
continue
}
for (let i = 0; i + 1 < seg.length; i += 1) tokens.push(seg.slice(i, i + 2))
} else if (seg.length > 1) {
// 单个字母或数字几乎没有区分度,丢掉能省不少索引
tokens.push(seg)
}
}
return tokens
}import re
SEGMENT = re.compile(r"[一-鿿]+|[a-z0-9]+")
CJK = re.compile(r"^[一-鿿]+$")
def tokenize(text: str) -> list[str]:
"""中文按二元组切,英文数字按词切;标点与空白当分隔符"""
tokens: list[str] = []
for seg in SEGMENT.findall(text.lower()):
if CJK.match(seg):
if len(seg) == 1:
tokens.append(seg)
continue
tokens.extend(seg[i : i + 2] for i in range(len(seg) - 1))
elif len(seg) > 1:
# 单个字母或数字几乎没有区分度,丢掉能省不少索引
tokens.append(seg)
return tokens有了词元就能建倒排索引:不是"文档里有哪些词",而是反过来,"每个词出现在哪些文档里、各出现几次"。检索时只需要把查询里几个词的候选文档取出来打分,不必扫全库——这就是卡片目录的价值。
接下来是 BM25 本体。完整公式长这样:
score(D, Q) = Σ idf(t) · ( f(t,D) · (k1 + 1) ) / ( f(t,D) + k1 · (1 - b + b · |D| / avgdl) )
t∈Q
idf(t) = ln( 1 + (N - n(t) + 0.5) / (n(t) + 0.5) )看着吓人,拆开只有三块。
第一块是逆文档频率 idf。 N 是文档总数,n(t) 是包含这个词的文档数。词越稀有,分子越大,权重越高。外面套一层 ln 是为了压缩量纲,加的那个 1 是为了防止 n(t) 超过一半文档时算出负分。这一项自带停用词效果:无需维护停用词表,"的"这种词自己就会被压到接近 0。
第二块是词频饱和,由 k1 控制。 注意分子分母里都有 f,这个分式在 f 增大时会趋近于一个上界,而不是无限增长。它防的是什么?防一篇文章把"回收站"写五十遍就霸榜。写五十遍确实比写五遍更相关,但绝不该相关十倍。k1 越小,饱和得越快;调到 0.2 时,词出现两次和出现二十次的得分几乎一样。
第三块是长度归一化,由 b 控制。 长文档天生更容易蒙中关键词——把一本书的全文当成一篇文档,它几乎能命中任何查询。这一项用"本文长度除以平均长度"把超长的文档按比例压回去。b 等于 0 时完全不看长度,b 等于 1 时完全按长度惩罚,0.75 是长期实践下来的折中值,也是几乎所有实现的默认值。
// idf:一个词出现在越少的文档里,命中它越有说服力
export function idf(index, term) {
const n = index.postings.get(term)?.size ?? 0
return Math.log(1 + (index.docCount - n + 0.5) / (n + 0.5))
}
export function scoreDoc(index, docId, queryTerms, { k1 = 1.2, b = 0.75 } = {}) {
const len = index.lengths.get(docId) ?? 0
const norm = 1 - b + (b * len) / index.avgLength // 长度归一化项,跟具体的词无关,先算好
let score = 0
for (const term of queryTerms) {
const f = index.postings.get(term)?.get(docId) ?? 0
if (f === 0) continue
score += (idf(index, term) * (f * (k1 + 1))) / (f + k1 * norm)
}
return score
}import math
def idf(index, term: str) -> float:
"""一个词出现在越少的文档里,命中它越有说服力"""
n = len(index.postings.get(term, {}))
return math.log(1 + (index.doc_count - n + 0.5) / (n + 0.5))
def score_doc(index, doc_id: str, query_terms: list[str], k1: float = 1.2, b: float = 0.75) -> float:
length = index.lengths.get(doc_id, 0)
norm = 1 - b + b * length / index.avg_length # 长度归一化项,跟具体的词无关,先算好
score = 0.0
for term in query_terms:
f = index.postings.get(term, {}).get(doc_id, 0)
if f == 0:
continue
score += idf(index, term) * (f * (k1 + 1)) / (f + k1 * norm)
return score最后一环是组装上下文。取回前三段之后,拼提示词时有三件事必须做,少一件今天的实验就会出现对应的翻车现象:给每段材料一个可引用的编号、把引用规则写进指令、明确允许拒答。第三条最容易漏,而它恰恰是幻觉的主要闸门。
export function buildPrompt(question, passages) {
const system = [
'你是云梯科技的知识库助手。只能依据下面提供的资料回答问题。',
'资料里找不到答案时,只回复这一句:资料中没有找到能支撑回答的内容。',
'每一句结论后面都要用方括号标出它来自哪一篇,格式如 [doc-006]。',
'如果两段资料互相矛盾,把两种说法都列出来并指出各自的更新日期,不要自己挑一个。',
].join('\n')
const context = passages
.map((p) => `[${p.id}](更新于 ${p.updated})${p.title}\n${p.body}`)
.join('\n\n---\n\n')
return { system, user: `资料:\n\n${context || '(无)'}\n\n问题:${question}` }
}def build_prompt(question: str, passages: list[dict]) -> tuple[str, str]:
system = "\n".join(
[
"你是云梯科技的知识库助手。只能依据下面提供的资料回答问题。",
"资料里找不到答案时,只回复这一句:资料中没有找到能支撑回答的内容。",
"每一句结论后面都要用方括号标出它来自哪一篇,格式如 [doc-006]。",
"如果两段资料互相矛盾,把两种说法都列出来并指出各自的更新日期,不要自己挑一个。",
]
)
context = "\n\n---\n\n".join(
f"[{p['id']}](更新于 {p['updated']}){p['title']}\n{p['body']}" for p in passages
)
return system, f"资料:\n\n{context or '(无)'}\n\n问题:{question}"第一版为什么不上向量
到这里你可能会问:现在满世界都在讲向量检索,为什么第一天要写一个三十年前的算法?
三个理由。第一,可解释。BM25 的每一分都能拆到具体哪个词贡献了多少,排序不对时你能当场看出是词频的问题还是稀有度的问题。向量检索给你一个 0.83 的相似度,出了问题你连从哪儿下手都不知道。第二,零依赖:不需要 embedding 接口、不需要向量库、不需要 GPU,两分钟跑完,随时能在没有网络的笔记本上复现。第三,也是最重要的——它是基线。
基线不是"凑合用的版本",基线是一把尺子。后面十三天你会加向量、加混合检索、加重排、加查询改写,每一样都要花钱、加延迟、增加故障点。凭什么说它值得?只有一个答案:跟基线比,指标涨了多少、延迟涨了多少、钱涨了多少。三笔账缺一笔都不算数。
而且 BM25 有向量做不到的地方:精确匹配。文档编号、错误码、人名、型号这类词,向量往往会把它们"理解"成语义相近的另一个,BM25 只认字面。这就是为什么第 9 天讲混合检索时,关键词这一路不会被丢掉,而是和向量并排跑再融合——今天写的这套东西会一直活到最后一天。
今天的实验里你会看到基线的两个真实短板:一个问题的答案分散在两篇文档里时,BM25 只能捞到其中一篇(这是第 12 天要处理的多跳);两篇文档对同一件事给出了不同结论时,它照单全收、并不知道该信谁(这是第 6 天要处理的材料冲突)。看到短板才是今天的收获,因为接下来每一天,都是在补其中一块。
源码导读
动手实验
实验目录里已经放好了一份三十篇的虚构语料——一家叫「云梯科技」的公司的产品手册、内部制度、技术文档、客服问答和会议纪要。这份语料后面十三天都会用同一份,所以值得花五分钟翻一翻,尤其是 doc-005 那张排版乱掉的表格和 doc-024 那篇跟产品手册对不上的客服问答,它们是故意留在那儿的。
starter/ 里挖了四个练习点:分词、逆文档频率、BM25 打分、分数阈值过滤。原样跑起来会看到每个问题都是"一个词都没命中"——那是因为默认的分词按空白切,对中文完全无效,这就是你的第一个任务。没有 API key 也能全程离线做完,离线模式只把最后的"生成"换成模板拼接,前四个环节都真实执行。
- 先读一遍
corpus/里的三五篇文档,确认自己知道语料里有什么、没有什么,这决定了你能问出什么样的问题。 - 补完分词与倒排索引,再跑一次,看到检索结果从「一个词都没命中」变成带分数的前三名。
- 把打分换成完整的 BM25,取前三条拼进提示词,跑通一个完整问答,看到回答里带出
[doc-006]这样的编号。 - 问 b08 和 b09 两个语料里没有答案的问题,观察一个被分数阈值挡住、另一个照编不误,想清楚这两层分别该由谁负责。
- 把
K1和B各调一次重跑基线,对比前三名的变化,最后保留默认参数跑出的baseline.json作为后续十三天的参照物。
面试题
今天 4 道题在下方题库区,侧重检索增强生成与微调、长上下文的边界,BM25 的公式含义,以及最小系统里每个环节的失败模式。展开后先看"分析过程"再看要点——照着推导练,比背要点管用。标注"国内高频 / 海外高频"方便按目标市场取舍。
检查清单与明日预告
- 能用一句话说清检索增强生成解决的是知识时效、事实归因和上下文成本这三个问题,并举出一个不该用它的场景
- 能画出检索增强生成的五个环节:切块、建索引、检索、组装上下文、生成,并说明每个环节可能怎么坏掉
- 能手写 BM25 打分函数并解释词频饱和与文档长度归一化这两个修正各自在防什么
- 能说清为什么"排查从右往左、修复从左往右",并用它定位过一个真实的答错案例
- 实验的 5 条验收标准全部通过,
baseline.json已经生成并留存 - 4 道面试题不看要点也能答出至少 3 道
明天(D2)我们给这套系统装上另一只眼睛:embedding 与向量检索。今天的 BM25 只认字面,你换个说法问同一件事它就抓瞎;向量能把"意思相近"变成"坐标相近",正好补上这一块。先学关键词再学向量是刻意的顺序——如果一上来就是向量,你会误以为检索就等于算相似度,从此再也想不起来还有精确匹配这回事;而先有了基线,明天每一项改进都能当场量出来涨了多少。
面试题库
什么时候该用检索增强生成,什么时候该微调,什么时候直接把文档塞进上下文就够了?When should you use retrieval-augmented generation, when should you fine-tune, and when is stuffing the documents into the context window good enough?
国内高频海外高频基础#rag-basics#fine-tuning#long-context分析过程 · 先想清楚再作答
- 这题几乎每场都问,区分度不在能不能背出三条定义,而在你会不会给一条判据。只说「RAG 适合动态知识、微调适合特定风格」的人一抓一大把,面试官等的是下一句。
- 先给一条能当场套用的判据:模型缺的是「知道什么」还是「怎么说」。缺知识走检索,缺风格与输出格式走微调,这一刀切下去能分掉八成场景。
- 再拿三笔账把三条路排开:知识更新的代价(改文件立刻生效 / 重训以天计 / 改文件立刻生效)、单次成本(只付取回的几段 / 只付推理 / 每次都付全量材料)、能不能归因(能 / 不能 / 能但材料一多定位会飘)。
- 把上下文直塞的适用边界说清楚:材料总量小、更新不频繁、对单次成本不敏感的场景它最划算,因为工程量近乎为零。一旦材料涨到几百篇,或者同一批材料每天要被问上万次,成本曲线立刻反超。
- 最后主动补一句「什么时候都不该用检索」——任务的答案不依赖任何外部文档时(改写、翻译、格式转换),加检索只会引入噪声、延迟和成本。能主动划出不该用的边界,比会背适用场景更能证明你做过。
- 可预期的追问:能不能既微调又检索?答案是可以,而且常见——微调管输出格式与拒答口径,检索管事实,两者解决的不是同一个问题。
How to reason about it · think before answering
- This question shows up in almost every loop. The differentiator is not reciting three definitions, it is offering a decision rule the interviewer can reuse.
- Lead with the rule: is the model missing knowledge, or missing a way of speaking? Missing knowledge means retrieval; missing style or output shape means fine-tuning. That single cut covers most cases.
- Then line up the three options against three costs: cost of updating knowledge, cost per request, and whether the answer can be traced back to a source. Retrieval updates by editing a file, fine-tuning takes a retraining cycle, and long-context pays for the whole corpus on every call.
- Give long-context its fair case: when the corpus is small, changes rarely, and request volume is low, stuffing it in is the cheapest engineering decision you can make. It stops being cheap once the corpus grows or the same material is queried thousands of times a day.
- Close by naming when none of this applies: if the answer does not depend on any external document (rewriting, translating, reformatting), retrieval only adds noise, latency and cost.
- Expected follow-up: can you do both? Yes, and it is common. Fine-tuning controls format and refusal behaviour, retrieval supplies the facts.
答题要点
- 一条判据:缺「知道什么」用检索,缺「怎么说」用微调。
- 检索改文件即时生效、可归因、成本只跟取回的几段有关,代价是要自己建一套会出错的检索系统。
- 微调擅长固化风格与输出格式,不擅长灌事实:数据一变就要重训,而且没法归因。
- 长上下文直塞在小型、低频、少变的语料上最划算,材料变多或调用量变大之后成本与定位稳定性都会恶化。
- 任务答案不依赖外部文档时三条路都不该用,直接调模型。
Key points
- One rule: retrieval for missing knowledge, fine-tuning for a missing way of speaking.
- Retrieval updates instantly by editing files, supports citation, and costs scale with the retrieved passages rather than the corpus.
- Fine-tuning is good at locking in style and output schema, poor at loading facts, and offers no traceability.
- Long-context stuffing wins when the corpus is small, stable and queried infrequently; it loses on cost and on locating facts once the corpus grows.
- If the answer does not depend on any document, use none of them.
BM25 里的词频饱和与文档长度归一化分别在解决什么问题?把 k1 和 b 都设成 0 会发生什么?In BM25, what problems do term-frequency saturation and document length normalisation each solve? What happens if you set both k1 and b to zero?
国内高频海外高频进阶#bm25#ranking#information-retrieval分析过程 · 先想清楚再作答
- 这题考的是你有没有真的读过公式,而不是有没有调过库。判据很明确:能不能把 k1 和 b 各自对应到公式里的哪一项,并说出去掉之后会被什么样的文档钻空子。
- 先说朴素词频的两个漏洞:一是重复刷词,一篇文章把关键词写五十遍就能霸榜;二是长文占便宜,文档越长越容易蒙中查询里的词。这两个漏洞正好对应两个修正。
- k1 管第一个漏洞。分子分母里都有词频 f,所以词频涨上去之后整个分式趋近一个上界而不是线性增长——写五十遍确实比写五遍相关,但绝不该相关十倍。k1 越小饱和越快。
- b 管第二个漏洞。归一化项是 1 减 b 加上 b 乘以本文长度除以平均长度,b 等于 0 时完全不看长度,b 等于 1 时完全按长度比例惩罚,0.75 是长期折中的默认值。
- 回到题干那个陷阱:k1 设成 0 会让分式退化成常数,词出现一次和一百次得分完全一样,等于只剩「有没有出现过」的布尔匹配;b 设成 0 则长度信息彻底消失。两个一起设成 0,BM25 就退化成对逆文档频率求和,跟词频再无关系。
- 可预期的追问:那逆文档频率去掉行不行?答案是不行,去掉之后「的」「我们」这类高频词会淹没一切——而且要顺带说明 BM25 因此天然不需要停用词表,这一句最能体现你读懂了公式。
How to reason about it · think before answering
- This checks whether you have actually read the formula rather than merely called a library. The test is whether you can map k1 and b onto specific terms and name the failure each one prevents.
- Start with the two holes in raw term frequency: keyword stuffing lets one document dominate by repeating a word, and long documents win by accident because they contain more words overall.
- k1 closes the first hole. Term frequency appears in both numerator and denominator, so the ratio approaches a ceiling instead of growing linearly. Fifty mentions are more relevant than five, but not ten times more relevant. A smaller k1 saturates sooner.
- b closes the second. The normalisation factor is one minus b plus b times document length over average length: at b equal to zero length is ignored entirely, at one it is fully penalised, and 0.75 is the conventional compromise.
- Now the trap in the question: k1 equal to zero collapses the ratio to a constant, so one occurrence scores the same as a hundred and matching becomes boolean. b equal to zero removes length entirely. Set both to zero and BM25 degenerates into a plain sum of inverse document frequencies.
- Expected follow-up: can you drop the IDF term? No. Without it, ubiquitous words drown everything else, and it is precisely IDF that lets BM25 work without a stopword list.
答题要点
- 词频饱和由 k1 控制,防的是重复刷词:词频涨大后得分趋近上界而非线性增长。
- 长度归一化由 b 控制,防的是长文档靠词多蒙中查询,用本文长度比平均长度把它压回去。
- k1 设 0 会退化成布尔匹配,词出现一次和一百次同分;b 设 0 则完全不考虑文档长度。
- 两者都设 0 时 BM25 只剩逆文档频率求和,等于放弃了词频信息。
- 逆文档频率是第三块,让稀有词权重更高,也让 BM25 天然不需要停用词表。
Key points
- k1 controls saturation and prevents keyword stuffing: the score approaches a ceiling rather than growing linearly with frequency.
- b controls length normalisation and stops long documents from winning by sheer word count.
- Setting k1 to zero degenerates the scorer into boolean matching; one occurrence scores the same as a hundred.
- Setting b to zero removes document length from the equation entirely; both at zero leaves only a sum of IDF terms.
- IDF is the third component: it up-weights rare terms and removes the need for a stopword list.
一个检索增强生成系统答错了,你怎么定位是检索的锅还是生成的锅?A retrieval-augmented generation system gave a wrong answer. How do you determine whether retrieval or generation is at fault?
国内高频海外高频进阶#debugging#failure-modes#evaluation分析过程 · 先想清楚再作答
- 题眼在「怎么定位」,不在「有哪些原因」。答成一串可能原因的罗列就输了,面试官想听的是一个有先后顺序、能落到具体动作的排查流程。
- 先给最省时间的第一步:把这次检索出来的几段原文原样打印出来,自己读一遍。正确答案不在里面就是检索的锅,在里面而模型没用上才是生成的锅。这一步三十秒,能省掉大半天的瞎猜。
- 然后把链路展开成五个环节——切块、建索引、检索、组装上下文、生成——并给出「排查从右往左、修复从左往右」这条口径:从右往左是因为你最先看到的是生成结果,从左往右是因为左边的错会被右边放大。
- 补充几个能把环节钉死的症状:答案「半对」多半是切块把一条完整规则切断了;检索结果里混着一眼不相干的东西多半是解析没做干净;模型无视材料用先验知识作答,通常是提示词里少了「只能依据资料回答」;引用编号和内容对不上,那是生成侧漏读或串了行。
- 最后落到工程做法:这套排查要能重复做,就必须把每次请求的检索结果、进上下文的段落、最终回答一起记下来,否则线上出问题时你根本复现不了。到了要批量做的时候,就得换成一批固定问题加指标,而不是一条条人工看。
- 可预期的追问:如果检索确实没捞到,改提示词有没有用?答案是没用——材料里没有的东西,再好的指令也只能换一种编法。这句话最能证明你分清了两层。
How to reason about it · think before answering
- The question asks how you localise the fault, not what the possible causes are. Listing causes loses; the interviewer wants an ordered procedure that ends in concrete actions.
- Give the cheapest first step: print the retrieved passages verbatim and read them. If the correct answer is not in there, retrieval is at fault. If it is in there and the model ignored it, generation is at fault. Thirty seconds, and it removes most of the guesswork.
- Then lay out the five stages — chunking, indexing, retrieval, context assembly, generation — with the rule: diagnose right to left, fix left to right. You see the generated answer first, but an error on the left is amplified by everything to its right.
- Add symptoms that pin down a stage: half-correct answers usually mean a rule was split across chunks; obviously irrelevant hits usually mean dirty parsing; the model ignoring the supplied material usually means the prompt never said it must; citation numbers that do not match their content point at generation.
- Land it in engineering terms: to run this procedure repeatedly you must log the retrieved hits, the passages that entered the context, and the final answer together, otherwise production issues are unreproducible. At scale this becomes a fixed question set with metrics rather than case-by-case reading.
- Expected follow-up: if retrieval missed the document, will prompt tuning help? No. Nothing in the prompt can conjure material that was never supplied.
答题要点
- 第一步永远是把检索出来的原文打印出来读一遍,判断正确答案在不在里面。
- 把链路拆成切块、建索引、检索、组装上下文、生成五个环节,排查从右往左、修复从左往右。
- 用症状钉环节:半对多半是切块问题,混入无关结果多半是解析问题,无视材料多半是提示词缺约束,引用与内容对不上是生成问题。
- 检索没捞到时改提示词没有意义,材料里没有的东西模型只能编。
- 要能重复排查就必须把检索结果、进上下文的段落和最终回答一起记录下来。
Key points
- Always start by printing the retrieved passages and checking whether the correct answer is present at all.
- Split the pipeline into chunking, indexing, retrieval, context assembly and generation; diagnose right to left, fix left to right.
- Use symptoms to pin the stage: half-correct answers point at chunking, irrelevant hits at parsing, ignored material at the prompt, mismatched citations at generation.
- If retrieval missed the document, prompt changes cannot help; the material simply is not there.
- Log retrieved hits, the passages that entered the context, and the final answer together, or production failures are unreproducible.
上下文窗口已经做到上百万 token 了,检索这一步会被淘汰吗?Context windows are now in the millions of tokens. Does that make the retrieval step obsolete?
国内高频海外高频深入#long-context#cost#system-design分析过程 · 先想清楚再作答
- 这是一道立场题,容易答成非黑即白。判断你有没有做过的地方在于:会不会区分「技术上能不能塞进去」和「工程上该不该每次都塞」,只谈前者的答案一听就是纸上谈兵。
- 先承认对方有道理的部分:窗口变大确实吃掉了检索的一部分场景。几十篇文档、更新不频繁、调用量不大的内部工具,直接全塞是最省事的选择,为它建一套检索系统是过度设计。
- 再给三条它吃不掉的理由。第一是成本:材料是按次计费的,同一份材料被问一万次就要付一万次,而检索只付取回的那几段;预填充缓存能缓解但不能消除,缓存也有有效期和命中率。
- 第二是规模:企业知识库动辄几十万篇,再大的窗口也塞不下,检索是唯一的入口。第三是归因与权限:答案要指回具体某一段,以及不同的人只能看到自己有权访问的材料——这两件事必须在把材料喂给模型之前完成,窗口再大也不解决。
- 还要补一条经验事实:材料变多之后,模型在长上下文里定位关键信息的稳定性会下降,出现「读了但没读到」。所以「全塞」并不总是等于「效果更好」,很多时候少而准反而更好。
- 可预期的追问:那检索的形态会不会变?会——窗口变大之后,取回的块可以更大、条数可以更多,重排与压缩的压力变小,检索从「精挑几句」变成「粗筛一批」。趋势是检索的粒度变粗,不是检索消失。
How to reason about it · think before answering
- This is a position question and it is easy to answer as a binary. The signal is whether you separate what fits technically from what is worth paying for on every request.
- Concede the valid half first: bigger windows genuinely absorb part of the use case. For an internal tool over a few dozen stable documents with low traffic, stuffing everything in is the right call and building a retrieval stack would be over-engineering.
- Then give three reasons it does not absorb the rest. Cost is the first: context is billed per request, so the same corpus is paid for on every one of ten thousand queries, whereas retrieval only pays for the passages it returns. Prompt caching softens this but does not remove it.
- Scale is the second: enterprise corpora run to hundreds of thousands of documents and no window holds them. Attribution and access control are the third: pointing an answer at a specific passage, and showing each user only what they are permitted to see, both have to happen before the material reaches the model.
- Add the empirical point: as the supplied material grows, models become less reliable at locating the one relevant fact inside it. More context is not automatically better; fewer and more precise passages often win.
- Expected follow-up: does retrieval change shape? Yes. Larger windows allow bigger chunks and more of them, which relieves pressure on reranking and compression. Retrieval gets coarser, it does not disappear.
答题要点
- 先区分「能不能塞进去」和「该不该每次都塞」,前者是技术问题,后者是成本问题。
- 小规模、低频、少变的语料确实可以直接全塞,为它建检索系统是过度设计。
- 检索不会被淘汰的三个理由:按次计费的成本、几十万篇塞不下的规模、必须在喂给模型之前完成的归因与权限过滤。
- 材料越多,模型定位关键信息的稳定性越差,全塞不等于效果更好。
- 趋势是检索粒度变粗——块更大、条数更多、重排压力变小,而不是检索消失。
Key points
- Separate whether it fits from whether it is worth paying for on every request.
- Small, stable, low-traffic corpora can legitimately be stuffed whole; building retrieval for them is over-engineering.
- Three reasons retrieval survives: per-request cost, corpora too large for any window, and attribution plus access control that must happen before the model sees the material.
- More supplied context reduces the reliability of locating a single fact, so stuffing everything is not automatically better.
- The trend is coarser retrieval — bigger chunks, more of them, less reranking pressure — not the removal of retrieval.
评论
登录后即可参与讨论
还没有评论,来说第一句。