Advanced Indexing: Parent-Child Documents, Summary Indexes, Contextual Retrieval, and the Trade-Offs of Tree Aggregation vs. Graph Retrieval
The same set of documents can support several index structures. Today implement parent-child and summary indexes, land contextual retrieval — a technique with a very good cost-to-benefit ratio — then explain exactly what problems tree-based recursive aggregation and graph retrieval each solve, what they cost, and when not to use them.
今日目标
- 能实现父子索引与摘要索引,并说明它们分别让哪一类问题变得可回答
- 能落地上下文检索:给每个块补上下文说明,并算清一次性建索引的成本
- 能说清树状递归聚合与图检索解决的是什么问题,以及在什么条件下它们的成本不划算
昨天把功夫下在查询进来之后,今天换一边:查询不动,改索引。读完回到页面顶部把三条目标勾掉。
小白版讲解
同一本书,可以配好几套目录
一本厚书的最后一般不止一个目录:前面是按章节排的目次,后面还有按主题排的索引。**书一个字没变,变的是你按什么给它建卡片。**找「第七章讲什么」翻目次最快,找「所有提到回收站的地方」翻主题索引最快。
检索也一样。前十天我们只建过一种索引:把文档切成块,每块进一次库,查询来了在这批块里找。它对「保留期是多少天」这类问题很好用。但另外三类问题它几乎没办法:
- 命中的块太小,模型看到的是半句话(上下文完整性);
- 块被切下来之后失去了归属,单看不知道说的是谁的保留期(块的自足性);
- 问题要的是全库的一个概括,而不是某几段原文(全局性问题)。
今天要做的,就是在同一批文档上再建几套目录,让这三类问题各自有地方去。索引结构变了,检索器一行不用改——这是本章所有对照实验能成立的前提。
那么问题来了:同一份语料建了三套索引,一个查询进来,凭什么决定走哪一套?
父子索引:检索单位和上下文单位可以不是一个东西
第四天讲切块时留了一句话:小块检索精度高,大块上下文完整,两者能不能都要。答案是能,办法就是让它们不是同一个东西。
按标题节点切成大块叫父块,父块再切成 200 字左右的小块叫子块。只有子块进索引,父块存在一边用 parentId 挂着;检索命中子块,装上下文时把它换成父块——就像卡片目录上写着「第 3 排第 2 格」,你按卡片找到的却是整本书。
子块(进索引,200 字) → 命中 → 按 parentId 换成父块(进上下文,整节)// 父块 = 一个标题节点;子块 = 把这一节再切成 200 字的小块
export function chunkParentChild(doc) {
const children = []
const parents = new Map()
doc.nodes.forEach((node, i) => {
const parentId = `${doc.docId}#p${String(i + 1).padStart(2, '0')}`
const heading = node.headingPath.join(' > ')
parents.set(parentId, { chunkId: parentId, text: `${heading}\n${node.text}` })
// 子块的块首同样拼上标题路径,跟平铺块保持同一套约定
for (const part of splitLong(node.text, 200, 40)) {
children.push({
docId: doc.docId,
chunkId: `${doc.docId}#k${String(children.length + 1).padStart(2, '0')}`,
text: `${heading}\n${part}`,
parentId, // 关键:子块知道自己属于哪个父块
})
}
})
return { children, parents }
}from dataclasses import dataclass
@dataclass
class Chunk:
doc_id: str
chunk_id: str
text: str
parent_id: str | None = None
def chunk_parent_child(doc) -> tuple[list[Chunk], dict[str, Chunk]]:
"""父块 = 一个标题节点;子块 = 把这一节再切成 200 字的小块"""
children: list[Chunk] = []
parents: dict[str, Chunk] = {}
for i, node in enumerate(doc.nodes, start=1):
parent_id = f"{doc.doc_id}#p{i:02d}"
heading = " > ".join(node.heading_path)
parents[parent_id] = Chunk(doc.doc_id, parent_id, f"{heading}\n{node.text}")
# 子块的块首同样拼上标题路径,跟平铺块保持同一套约定
for part in split_long(node.text, 200, 40):
children.append(
Chunk(
doc_id=doc.doc_id,
chunk_id=f"{doc.doc_id}#k{len(children) + 1:02d}",
text=f"{heading}\n{part}",
parent_id=parent_id, # 关键:子块知道自己属于哪个父块
)
)
return children, parents真正容易写错的不是切,是装。同一个父块常被好几个子块同时命中,照着名次挨个往上下文里塞,同一段会被重复塞三遍,600 token 的预算两块就见底。所以必须按父块去重,而且跳过重复父块要用「继续下一条」而不是「停止」——后面还有别的父块等着。
代价是索引条目变多(这份语料 134 块涨到 158 块)、每次装进上下文的东西变大:拿索引体积和上下文预算换块的完整性,不是白拿。
但要提醒一句:这份评估集测不出父子索引的收益。20 题里单文档题的召回率在所有配置下都是 100%,已经顶到天花板;父子索引补的「材料完整度」又不体现在「答案文档进没进上下文」这个二值指标上。所以它跟基线持平不等于它没用,只等于这把尺子量不了它——要量得换一把尺,比如让模型裁判去判「给的材料够不够答」。
摘要索引:先定位到哪本书,再翻到哪一页
第二套目录是给「文档很多、每篇很长」准备的:给每篇文档生成一段一百来字的摘要,只把摘要建成索引。查询先在摘要上检索,定位到最相关的三五篇,再在这几篇内部细检索。
好处很直接:一万篇文档的块级索引是几十万条,摘要索引只有一万条;生产里第二段甚至不用单独建索引,向量库加一个「文档号在这几个里面」的过滤条件就够。
但它在我们这份语料上是净亏的:基线召回率 93.8%,摘要索引 87.5%;多跳题从 75% 掉到 50%,答案文档平均名次从 2.44 退到 3.63。
第一段只留三篇,多跳题要的两篇里只要有一篇在摘要上看着不相关,就在第一段被砍掉,第二段再准也捞不回来。**它省下的算力全部由「第一段可能砍错」买单。**判据记成一句话:文档数多到「全库块级检索」本身成了瓶颈时,摘要索引才开始划算;在那之前它只是在给自己制造召回上限。
上下文检索:给每块补一句定位说明,然后老实看数字
第三套目录是最近传得最广的一个手法,做法只有一行:让模型读完整篇文档,给每一块写一句「它在全文里是哪一部分」,拼在块首再入库。
「保留期是 30 天」单独拎出来,谁也不知道说的是谁的保留期;补上「《网盘与文件管理》回收站:」之后,一个问「回收站能留多久」的查询就能对上它了。
// 缓存断点放在整篇文档之后:同一篇的第二块起只付缓存读取价
const res = await client.messages.create({
model: 'claude-sonnet-5',
max_tokens: 100,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: `全文:\n${doc.body}`, cache_control: { type: 'ephemeral' } },
{ type: 'text', text: `其中一段:\n${chunk.text}\n\n${INSTRUCTION}` },
],
},
],
})
// 块头只拼进「进索引的文本」,不要拼进「进上下文的文本」
const indexText = `${res.content[0].text.trim()}:\n${chunk.text}`# 缓存断点放在整篇文档之后:同一篇的第二块起只付缓存读取价
res = client.messages.create(
model="claude-sonnet-5",
max_tokens=100,
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": f"全文:\n{doc.body}",
"cache_control": {"type": "ephemeral"},
},
{"type": "text", "text": f"其中一段:\n{chunk.text}\n\n{INSTRUCTION}"},
],
}
],
)
# 块头只拼进「进索引的文本」,不要拼进「进上下文的文本」
index_text = f"{res.content[0].text.strip()}:\n{chunk.text}"顺序不能反:缓存按前缀匹配,整篇必须放最前面、块内容放后面;把每块都不同的内容放前面,前缀次次都变,缓存一次都不会命中。
现在说数字。第四天在纯 BM25 下量到过一条反直觉的结果:加块头之后命中率不变、索引 token 涨了一成、答案文档平均名次反而从 2.88 退到 3.25。解释是块头把同一篇的标题词摊到每一块,把逆文档频率自己拉低了,对关键词检索是稀释;当时留了一句「它的收益应该在向量侧」。今天还这笔账。
把上下文分成三个层次,在三条检索路线上各跑一遍(30 篇语料、20 题标准答案集、块级检索、600 token 预算、离线哈希向量):
| 检索路线 | 召回率 ①→②→③ | 答案文档平均名次 ①→②→③ | nDCG@10 ①→②→③ |
|---|---|---|---|
| 纯 BM25 | 93.8% → 93.8% → 93.8% | 2.44 → 2.44 → 2.44 | 0.5854 → 0.6753 → 0.7312 |
| 纯向量 | 81.3% → 81.3% → 87.5% | 3.75 → 2.94 → 3.00 | 0.4152 → 0.5088 → 0.6185 |
| 混合检索 | 93.8% → 93.8% → 93.8% | 2.44 → 2.44 → 2.44 | 0.5530 → 0.6438 → 0.7218 |
①是裸块,②是块首带标题路径的块,③是加了块头的块。索引 token 一路从 15638 涨到 18036 再涨到 22339。②对应第九天的配置,但标题路径只拼一次——第九天那份实现把它拼了两遍,所以本章的绝对数字与第八、九天的不严格可比,只能横着看本章内部的相对差异。
三条结论,一条比一条不像宣传语:
第一,向量那一路单独看是涨的,但这个涨不能拿去证明什么。纯向量一路召回率从 81.3% 涨到 87.5%,nDCG@10 从 0.5088 涨到 0.6185。问题在于离线模式下的「向量」是保留字面重合度的哈希向量——它涨,只说明块头给它添了更多可以字面命中的词,不说明真实 embedding 会因为拿到语义上下文而变准。这两件事长得很像,但不是一回事。
**第二,第四天那条名次退化没有复现。**纯 BM25 一路上三个层次的名次都是 2.44。差别在切法:第四天是固定长度 400 字硬切,块边界跟小节边界对不齐,块头会把不属于这一块的标题词塞进来;今天按标题节点切,块本身就落在一个小节里,块头补的信息跟块里已有的高度重合,既没帮忙也没帮倒忙。所以那条结论不是「块头对 BM25 必然有害」,而是「块头在块边界与结构不对齐时对 BM25 有害」——前提比结论重要。
**第三,混合检索这一路,块头买到的是排序质量,买不到召回率。**召回率停在 93.8%,答案文档平均名次也停在 2.44,只有 nDCG@10 从 0.6438 涨到 0.7218。指标选错,这一整段会得出相反的结论:只看 nDCG 以为涨了 12.1%,只看召回率以为一点用没有。
成本要分两笔记,这是本节最实用的一段。
一次性建索引:30 篇、134 块。不开缓存每块都要把整篇重读一遍,输入 103017 token;开了缓存,整篇每篇只写一次(17340),第二块起走缓存读取(60137),再加缓存不了的块正文与指令(25540)和输出(13400)。按占位单价算降约 29%,块切得越碎比例越高。
每次查询:块头一旦入库,就在每一次查询里被读两遍——先在重排里读,再在上下文里读,成本涨约 12.3%。而把一次性那笔摊到每次查询的一成以下,只需要 217 次查询。
**所以结论是反直觉的:建索引那笔是小钱,每次查询多出来的那几十个 token 才是长期账。**顺着这条账还能推出一个立刻能用的优化:块头只进索引、不进上下文。模型生成答案时不需要这句定位说明,它只对检索器有用。同一套索引只改这一个开关,指标一个不变,600 token 的预算里却多装进了 36 个 token(500 涨到 536)。
树状递归聚合:把「这份资料大概讲什么」变成可检索的
前面三套目录都在优化「找到那一段」。但有一类问题从根上就不是这个形状:
云梯科技这份知识库一共覆盖哪几个部门?每个部门大致在讲什么?
跑一遍就知道有多难:候选池取 20 条只覆盖 12 篇文档,全库却有 30 篇。答案分散在每一篇里,任何「取前 k 块」都凑不齐——它要的不是某几段原文,而是全库的一个概括。
树状递归聚合的思路是:既然概括不在语料里,那就把它造出来再入库。把全部块按向量聚类,每簇让模型写一段摘要;这些摘要作为新一层节点再聚类、再摘要,递归到顶。索引里于是同时存着叶子层的原文块和上面几层的摘要块:问细节的命中叶子,问概括的命中高层摘要——同一个索引,两种粒度。
代价在建索引:全库聚类要算一遍全部向量,每层每簇要调一次模型,语料一变就要重算受影响的那几支。判据是概括型问题占几成——一成不到就先别上。
图检索:擅长多跳与全局,贵在建图
评估集里有一道:生产数据库主备切换必须由谁书面审批,这个人叫什么名字。答案跨两篇文档,一篇写着「须平台组组长书面审批」,另一篇写着「平台组组长是周敏」。
这道题在全部五种索引结构下都失败,失败方式完全一样:那篇写着人名的文档在 20 条候选池里一次都没出现。查询里没有「周敏」这两个字,也没有任何跟那篇文档字面或语义接近的词。切法、块头、父子回填全都无效,它压根没被检索到。
图检索针对的就是这个缺口:先把实体和关系抽出来建成一张图,检索时沿边走一跳、两跳。第二篇文档不是靠相似度捞出来的,是靠一条边走过去的。
建图的成本远不止一次模型调用:实体要消歧、关系要去重、文档更新时受影响的子图要重算,还要多维护一套图存储(增量更新见第十三天)。门槛不在技术,在你有没有足够多「必须跨实体串联」的问题来摊掉它。
多模态一瞥,以及一个查询到底走哪一套
知识库里往往还有图表和截图。最省事的做法是把它们变成文字再入库:让多模态模型给每张图写一段描述,当作一个块建索引,元数据里留着原图路径。检索走文字这一路,整套索引、融合、重排一行不用改;答完按路径把原图附在引用旁边。给图和文各建一套向量空间再融合更讲究,但要换模型、重建索引,收益得先用第八天那套评估证明。
回到开头那个悬念:建了三套索引,查询进来走哪一套?
默认哪套都不走,走你原来那套。今天这五种结构召回率全部停在 93.8%,一种都没跑赢基线;唯一动了的是 nDCG@10(块头把它从 0.6438 抬到 0.7218),摘要索引还掉到了 87.5%。这不是说手法没用,而是说它们各自补的是一个特定短板,你的系统没有那个短板时,它们只带来成本。
所以顺序是:先用评估集看失败案例长什么样。块被切碎,上父子索引;块脱离上下文就不知道在说谁、且向量是主力路线,上上下文检索;概括型问题占比高,上树状聚合;跨实体串联占比高,上图检索。先有失败案例,再有索引结构。
真要同时挂几套,路由的判据也不是「哪套准」,而是问题的形状——昨天那个意图路由干的就是这件事。分类错了回落到默认那套,而不是并行全查一遍。
源码导读
动手实验
starter/ 原样跑起来时多数验收项都不达标:块头那一列跟标题路径一模一样、摘要索引两行完全相同、缓存那一笔是负数。五个练习点做完它们会一条条变对,全程 MOCK=1 不需要网络也不需要密钥。
- 实现父子切块:标题节点存成父块,节内再切 200 字子块并挂上归属,看表一块数变成 158。
- 实现上下文块头:离线走规则版、在线走模型,看表二第三列与第二列拉开差距。
- 把父块回填与去重补进装上下文那一步,看父子索引的上下文 token 与召回率变正常。
- 接上摘要索引的两段式检索,看表三两行拉开差距,想清楚召回率为什么是跌的。
- 算出提示词缓存那笔账,看表五省下的比例从负数变成约 29%,再改一次占位单价重跑看占比翻转。
面试题
今天 4 道题在下方题库区,侧重多种索引结构的适用面、上下文检索的成本收益、图检索的落地门槛。展开后先看「分析过程」再看要点——照着推导练比背要点管用。
检查清单与明日预告
- 能实现父子索引与摘要索引,并说明它们分别让哪一类问题变得可回答
- 能落地上下文检索:给每个块补上下文说明,并算清一次性建索引的成本
- 能说清树状递归聚合与图检索解决的是什么问题,以及在什么条件下它们的成本不划算
- 能说清「块头只进索引、不进上下文」为什么是白捡的收益
- 能说出本章的实验环境有哪两条边界,以及它们让哪个问题变得回答不了
- 实验的 5 条验收标准全部通过
- 4 道面试题不看要点也能答出至少 3 道
明天(D12)我们把检索包成工具接进 Agent 循环,让模型自己决定查不查、查几次、要不要推翻重来。顺序是有意的:今天反复出现的失败模式是「一次检索凑不齐答案」——那道审批人问题,五种索引结构全军覆没。索引结构解决不了的,只能靠多查几次,而这就得有人决定查什么、什么时候停。那个人是明天的主角。
Interview questions
Parent-child indexing and contextual retrieval both patch the same problem — chunks losing their context. What actually distinguishes them?父子索引和上下文检索都在补『块被切碎』这个问题,它们的差别到底在哪?
Common in ChinaCommon overseasIntermediate#indexing#contextual-retrieval#chunkingHow to reason about it · think before answering
- The hinge is which half of the pipeline each one fixes. Answering 'one is a chunking trick, the other adds a prompt' just describes implementations; the interviewer wants to know where each acts.
- Split the pipeline in two and ask separately: what does the retriever see, and what does the generator see. Parent-child changes the generation side — retrieval still runs on small chunks, but a hit is swapped for its parent. Contextual retrieval changes the retrieval side — the header exists so the chunk can be found at all, and the generator does not need it.
- Conclusion: parent-child fixes 'found it but can't read it'; contextual retrieval fixes 'readable but never found'. Neither changes what the other changes, so they compose.
- That difference also dictates which metric can see each one. Contextual retrieval moves rank, so recall and nDCG catch it. Parent-child moves 'is the evidence sufficient to answer', which a binary recall metric cannot see. Our 20-question set is already saturated at 100% on single-document questions, so parent-child comes out level with the baseline — that is the ruler failing, not the technique.
- That difference yields a free optimization: since the header only serves retrieval, keep it out of the context window. Leaving it in pays rent on every single query. Flipping that one switch in our lab freed 36 tokens inside a 600-token budget with every metric unchanged.
- The costs differ too. Parent-child costs index entries and a bigger context unit. Contextual retrieval costs one model call per chunk up front plus a permanently larger index. One is space; the other is time and space.
- Expect the follow-up 'why not both'. Look at the failure logs first: are you mostly seeing incomplete evidence, or nothing retrieved at all? Without the matching failure mode, neither is worth its price.
分析过程 · 先想清楚再作答
- 这题的题眼是『补的是哪一半』。答成『一个是切块技巧、一个是加提示词』就是在描述实现,面试官想听的是它们各自作用在检索管道的哪一段。
- 拆的办法是把管道分成两段问:检索时看到什么、生成时看到什么。父子索引改的是**生成侧**——检索单位还是小块,只是命中之后把上下文单位换成大块;上下文检索改的是**检索侧**——块头拼进去是为了让这一块能被检索到,模型生成时并不需要它。
- 结论:父子索引解决『找到了但看不全』,上下文检索解决『看得全但找不到』。前者不改变谁被检索到,后者不改变模型看到多少。它们正交,可以叠加。
- 这个差别还决定了它们各自要用什么指标去量:上下文检索动的是名次,用召回率和 nDCG 量得到;父子索引动的是『材料够不够答』,召回率这种二值指标量不出来。我们那份 20 题评估集单文档档已经 100% 饱和,父子索引在表里跟基线持平——那不是它没用,是尺子量不了它。
- 顺着这条差异能推出一个立刻能用的优化:既然块头只服务检索,就不该进上下文。它进了上下文就是在每一次查询里白占预算,而且这笔钱是长期的。我们的实验里把这个开关一改,五列指标一个不变,600 token 的预算里多装进了 36 个 token。
- 代价也不同:父子索引的代价是索引条目变多、每次装进上下文的东西变大;上下文检索的代价是一次性要给每块调一次模型,加上索引 token 永久变大。前者是空间,后者是时间加空间。
- 可预期的追问是『那我全都上』。答案是先看失败案例:日志里是『材料不完整』多,还是『压根没检索到』多。没有对应的失败模式就不该上,这两个手法都不是免费的。
Key points
- Parent-child acts on the generation side: retrieve small, swap in the parent for context. It fixes 'found but unreadable'.
- Contextual retrieval acts on the retrieval side: the header makes the chunk findable. It fixes 'readable but never found'.
- They are orthogonal and compose; keep the header in the index only, never in the context window.
- Parent-child costs more index entries and a larger context unit; contextual retrieval costs one call per chunk plus a permanently larger index.
- Pick based on the observed failure: incomplete evidence points to the former, zero retrieval to the latter.
答题要点
- 父子索引作用在生成侧:检索单位是小块,上下文单位换成父块,解决『找到了但看不全』。
- 上下文检索作用在检索侧:块头让块能被检索到,解决『看得全但找不到』。
- 两者正交可叠加;块头只该进索引不该进上下文,否则每次查询都在为它付钱。
- 父子索引的代价是索引条目与上下文单位变大;上下文检索的代价是一次性建索引调用加永久变大的索引。
- 选哪个看失败案例:材料不完整选前者,压根没检索到选后者。
Contextual retrieval needs one model call per chunk. How do you estimate that one-off cost, and what levers bring it down?上下文检索要给每个块调一次模型,这笔一次性成本怎么估?有哪些办法能压下来?
Common in ChinaCommon overseasDeep dive#contextual-retrieval#prompt-caching#costHow to reason about it · think before answering
- This checks whether you have actually done the arithmetic. Saying 'prompt caching makes it cheap' without knowing which line item it touches is a tell.
- Split the bill first: one-off = per-chunk input + output + full re-embedding; per-query = the header read twice, once by the reranker and once in the context. Keep them separate, because they scale with completely different things.
- The dominant term on the one-off side is how many times the same document is re-read. A doc split into n chunks is read n times. Prompt caching attacks exactly that: put the whole document first and mark it cacheable, pay a cache write once, then cache reads for the remaining n-1, typically an order of magnitude cheaper than input.
- Order matters. Caching is prefix-matched, so the document must come first and the chunk after. Put the varying part first and the prefix changes every call — zero cache hits. This is the most common way people get it wrong.
- Our measurement: 30 docs, 134 chunks. Without caching, 103017 input tokens; with caching, 17340 written plus 60137 read, cutting the one-off cost by roughly 29%. The finer the chunks, the bigger the saving, because re-reads multiply.
- The counter-intuitive part is the useful part: the one-off cost amortizes below 10% of per-query cost after about 217 queries. The lasting bill is the extra tokens every query carries (we measured +12.3%). So the first lever is not cheaper index building — it is keeping the header out of the context, keeping it short, and not generating it for the whole corpus indiscriminately.
- A bonus point: before spending any of it, confirm your evaluation setup can actually detect the benefit. In our offline harness the vector route contributed exactly zero unique answer documents, so it cannot answer whether headers help embeddings at all — an A/B run there hands you a wrong conclusion that looks numerically supported.
分析过程 · 先想清楚再作答
- 这题考的是你有没有真的算过账。只会说『用提示词缓存就便宜了』属于听过没做过——面试官会追问缓存到底省在哪一项上。
- 先把成本拆开:一次性 = 每块的输入 + 输出 + 全量 embedding;每次查询 = 块头在重排和上下文里各被读一遍。**这两笔要分开记**,因为它们随业务量的增长方式完全不同。
- 一次性那笔的主项是『同一篇文档被重复读了多少遍』。一篇切成 n 块就要读 n 遍,这是成本的大头。提示词缓存省的正是这一项:把整篇放在提示词最前面并标记为可缓存,第一块付一次缓存写入,后面 n-1 块只付缓存读取,而读取价通常比输入价低一个数量级。
- 顺序不能反:缓存按前缀匹配,整篇必须在前、块内容在后。把变化的块放前面,前缀次次都变,缓存一次都不会命中——这是最常见的翻车点。
- 我们的实测:30 篇、134 块,不开缓存输入 103017 token,开缓存后拆成写入 17340 加读取 60137,一次性成本降约 29%。**块切得越碎这个比例越高**,因为重复读的次数更多。
- 结论反直觉但很实用:一次性那笔是小钱,摊到 217 次查询就降到每次查询成本的一成以下;真正的长期账是每次查询多出来的那几十个 token(我们量到 +12.3%)。所以压成本的第一优先级不是压建索引,而是让块头别进上下文、别过长、别对全库无差别地生成。
- 最后一条是加分项:花这笔钱之前先确认你的评估环境**测得出**收益。我们的离线环境里向量路对召回的独立贡献实测为 0,所以它根本没法回答『块头对向量侧有没有用』——在这种环境里做的 A/B 会给你一个看起来有数字支撑的错误结论。
Key points
- Split into one-off (per-chunk input/output plus re-embedding) and per-query (header read by both reranker and generator).
- The one-off is dominated by re-reading each document n times; caching turns that into one write plus n-1 reads.
- Caching is prefix-matched: the full document must come first, the chunk after, or you get zero hits.
- Measured on 30 docs / 134 chunks, caching cut the one-off cost by about 29%, and finer chunks save more.
- The lasting cost is per query: keep headers out of the context window, keep them short, and generate them selectively.
答题要点
- 把账拆成一次性(每块的输入输出 + 全量 embedding)和每次查询(块头在重排与上下文里各读一遍)两笔。
- 一次性的大头是同一篇被重复读 n 遍;提示词缓存把它压成一次写入加 n-1 次读取。
- 缓存按前缀匹配,整篇必须放在提示词最前面,块内容在后,顺序反了一次都不会命中。
- 实测 30 篇 134 块,一次性成本降约 29%,块越碎省得越多。
- 长期账在每次查询:块头别进上下文、控制长度、只对真正需要的文档生成。
What kind of question actually requires graph retrieval? Give one concrete case where it is justified and one where it is not.什么样的问题必须上图检索?给一个该上的具体例子和一个不该上的例子。
Common in ChinaCommon overseasDeep dive#graph-rag#multi-hop#costHow to reason about it · think before answering
- This one tests whether you reach for tools you don't need. If the answer is 'multi-hop questions need a graph', the interviewer knows you haven't shipped one — multi-hop is necessary, nowhere near sufficient.
- Anchor the criterion on something observable: does the second required document share any lexical or semantic overlap with the query? If it does, ordinary hybrid retrieval will surface it and the hop is illusory. If it shares nothing, only a relation edge gets you there — that is graph territory.
- Justified case: 'who must sign off on a production failover, and what is that person's name?' One doc says the platform lead must approve; another says who the platform lead is. The second shares not one term with the query. Across all five index structures we tested, it never once appeared in a 20-item candidate pool — rechunking, headers and parent backfill all failed.
- Unjustified case: 'which process covers a capacity change, and how many working days ahead must the ticket be filed?' Also two documents, but both overlap the query lexically; hybrid retrieval ranked them second each, and one pass collected both. Building a graph for this buys a solved problem at several times the cost.
- Then state the cost, which is what makes the answer sound operational: graph building is not one extraction call. Entities need disambiguation, relations need dedup, updates force recomputing affected subgraphs, and you now run a graph store and its update pipeline.
- Expect 'what else could you do instead'. Hand multi-hop to agentic retrieval: let the model retrieve the intermediate entity first, then issue a second query with it. Near-zero build cost, paid back in latency and call count per query. Try that before you build a graph.
分析过程 · 先想清楚再作答
- 这题在考你会不会为了用而用。只要答案里出现『多跳问题就要上图检索』,面试官基本就知道你没落地过——多跳只是必要条件,远不是充分条件。
- 判据要落在一个可观察的现象上:**答案的第二篇文档和查询之间,有没有字面或语义上的重合**。有重合,普通的混合检索就能捞到它,多跳是假的;完全没有重合,只能靠一条关系边走过去,这才是图检索的领地。
- 该上的例子:问『生产库主备切换必须谁书面审批、这个人叫什么』。一篇写着须平台组组长审批,另一篇写着平台组组长是某人。第二篇跟查询一个词都不重合,我们在五种索引结构下测了一遍,它在 20 条候选池里一次都没出现过——换切法、加块头、父子回填全都无效。
- 不该上的例子:问『扩容要走哪个流程、最晚提前几个工作日提单』。同样跨两篇文档,但两篇都跟查询有明显字面重合,混合检索把它们分别排在第 2 名,一次检索就凑齐了。为它建图是拿几倍成本买一个已经解决的问题。
- 然后说代价,这一段决定了你像不像做过:建图不止一次抽取调用,实体要消歧、关系要去重、文档更新时受影响的子图要重算,还要多维护一套图存储和一套更新链路。
- 可预期的追问是『不上图检索还有什么办法』。答案是把多跳交给 Agentic 检索:让模型先查出中间实体,再拿这个实体发起第二次检索。它的一次性成本几乎为零,代价换成了每次查询的延迟与调用次数——先试这条,试不通再考虑建图。
Key points
- The test is not 'is it multi-hop' but 'does the second document overlap the query at all' — only zero overlap earns a graph.
- Justified: the approver question, where an intermediate entity is the only bridge and the second doc never enters the candidate pool.
- Not justified: a multi-hop question whose documents both overlap the query — hybrid retrieval collects them in one pass.
- Real graph cost is entity disambiguation, relation dedup, incremental subgraph recomputation and a whole extra store — not a single extraction call.
- Try two-pass agentic retrieval first; build the graph only when that fails.
答题要点
- 判据不是『是不是多跳』,而是『第二篇文档跟查询有没有字面或语义重合』——没有重合才轮得到图检索。
- 该上:审批人那类问题,中间实体是唯一的桥,第二篇文档在候选池里一次都不出现。
- 不该上:两篇都跟查询有重合的多跳题,混合检索一次就能凑齐。
- 建图的真实成本是实体消歧、关系去重、增量重算和一套额外的图存储,不是一次抽取调用。
- 先试 Agentic 检索的两次查询,走不通再考虑建图。
You have built three different indexes over the same corpus. How do you decide which one a query goes to?同一份语料建了三套索引,检索时你怎么决定走哪一套?
Common in ChinaCommon overseasIntermediate#index-routing#evaluation#architectureHow to reason about it · think before answering
- Whether this is an easy point or a lost one depends on whether you first ask 'do we actually need three?'. Jumping straight to routing accepts an unverified premise.
- Step one is admitting the answer is usually 'none of them — use the default'. Across 30 documents we measured five index structures and every one landed at 93.8% recall, none beating the baseline. The only metric that moved was nDCG@10, which headers lifted from 0.6438 to 0.7218, while the two-stage summary index fell to 87.5%. Each structure patches one specific weakness; without that weakness it is pure overhead.
- Step two is routing, and the criterion is not 'which index is more accurate' — that is an offline evaluation question, not something you know at request time. What you do have at request time is the shape of the question: detail-seeking, summarizing, or entity-chaining. Those map onto the chunk index, the tree-summary index and the graph index.
- Implementation is a lightweight intent classifier — the same one from the previous day's intent routing, no need to invent another. Carry the decision as request metadata so you can replay it later.
- Spell out the fallback: on a misclassification, fall back to the default index rather than fanning out across all three and fusing. Fan-out looks safe but multiplies latency and cost by the number of indexes, and the extra routes usually never make it into the context budget anyway.
- Expect 'how do you know the classifier is right'. Log every routing decision and replay the golden set periodically: run each question through all three indexes and check whether the classifier picked the best-scoring one. It is a standing offline job that needs no human labelling.
分析过程 · 先想清楚再作答
- 这题是送分还是丢分,取决于你有没有先反问一句『真的需要三套吗』。上来就答路由策略的人,默认了一个没被验证的前提。
- 第一步是承认多数情况下答案是『都不走,走默认那套』。我们在 30 篇语料上把五种索引结构各测一遍,**召回率全部停在 93.8%,没有一种跑赢基线**;唯一动了的是 nDCG@10(块头把它从 0.6438 抬到 0.7218),而两段式的摘要索引还掉到了 87.5%。每种结构补的都是一个特定短板,你没有那个短板时它只带来成本。
- 第二步才是路由,而判据不是『哪套准』——那是离线评估该回答的问题,不是运行时能知道的。运行时能拿到的只有**问题的形状**:细节型(答案落在某一段)、概括型(要全库的一个概括)、多跳型(要跨实体串联)。按形状分流,正好对应块级索引、树状聚合索引、图索引。
- 实现上就是一个轻量意图分类器,跟前一天的意图路由是同一套东西,不必再造一个。分类结果作为元数据带进请求,方便事后拿评估集回看分错了多少。
- 兜底策略要说清楚:分类错了**回落到默认那一套**,不要并行全查一遍再融合。并行看着稳,实际上把延迟和成本按索引套数翻倍,而多出来的那两路大概率一条都进不了上下文预算。
- 可预期的追问是『怎么知道分类器分对了』。答案是把路由决策记进日志,定期拿标准答案集回放:对每个问题分别走三套索引,看分类器选的那套是不是指标最好的那套。这是一个能持续跑的离线作业,不需要人工标注。
Key points
- First challenge the premise: all five index structures landed at the same 93.8% recall in our measurement, so an index without a matching weakness is pure cost.
- At request time the usable signal is question shape — detail, summary, or entity-chaining — mapping to chunk, tree-summary and graph indexes.
- Reuse the previous day's intent router for classification and record the routing decision as request metadata.
- Fall back to the default index on misclassification instead of fanning out and fusing, which multiplies latency and cost.
- Replay the golden set periodically to check whether the classifier picks the best-scoring index.
答题要点
- 先反问是否真需要三套:实测五种索引结构召回率全部持平在 93.8%,没有对应短板就是纯成本。
- 运行时的判据是问题的形状——细节型、概括型、多跳型,分别对应块级、树状摘要、图索引。
- 复用前一天的意图路由做分类,把路由决策记进请求元数据。
- 分类错了回落到默认索引,不要并行全查再融合——延迟和成本按套数翻倍。
- 用标准答案集定期回放,检验分类器选的那套是不是指标最好的那套。
Comments
Sign in to join the discussion
No comments yet — be the first.