引用注入:解析 @ 文件、目录、URL 与图片,并把注入量说清楚
别让 Agent 自己翻遍整个仓库:实现 @ 引用语法,把文件、目录、网页地址解析成结构化的注入内容,处理二进制与超大文件,并在终端显式打印这一次注入了多少字符、占掉多少预算。
今日目标
- 能实现一套引用语法的解析与展开,覆盖文件、目录、网址三类来源
- 能为注入内容设定预算并在超限时按可解释的规则裁剪
- 能说清主动注入与让模型自己调工具检索之间的取舍
第二周从今天开始,主题从「它能不能干活」换成「它能不能少走弯路」。读完回到页面顶部把三条目标勾掉。
小白版讲解
指路:把资料放到他桌上,比让他翻遍整栋楼便宜
第一周那个新人现在能干活了。但你有没有注意到他每次是怎么开始的:先在仓库里搜一遍关键词,再列一遍目录,再打开三四个文件,最后才动手。
而很多时候你早就知道该看哪个文件。你只是没告诉他。
前七天的 mca 就是这样。你问它「divide 哪里有问题」,它会先 glob 找文件、再 grep 搜关键词、再 read_file 读一遍——三四轮工具调用,每一轮都是一次完整的网关往返,而这三四轮换来的东西,你本来一句话就能给它。
今天要做的就是这一句话:@ 引用。你写 解释一下 @src/calc.js 里的 divide 哪里有问题,程序在发请求之前就把那个文件的内容放进上下文。本实验里这一句话的效果是零次工具调用就给出了结论——同一个问题,第一周要三四轮。
省下来的不只是延迟。每一轮工具往返都要把整个消息数组重发一次,所以省掉两轮的收益是复利的。
但这件事有代价,而且代价就是今天后半段的全部内容:你替它决定了看什么。 决定错了,它拿着一份不相关的材料很自信地答错;材料太大,它把预算用在了噪音上;材料悄悄被截断了,它看不出来。所以今天真正要立的规矩不是语法,而是一条纪律——每一次注入都要报账。
引用语法的解析:转义、空格、通配符与不存在的路径
语法只有一个符号:@ 后面跟路径、通配符或者网址。五行正则就能写完——然后你会在一周内收到四类 bug,因为它要处理的是人随手打出来的东西。
四种情况一个都不能漏:
- 邮箱不是引用。
eric@example.com里那个 @ 前面是字母。判据是「@ 必须在开头或者紧跟空白」,不是「文本里有没有 @」。少了这一条,用户每写一次邮箱地址,Agent 就去读一个不存在的路径并报一次错。 - 要能转义。
\@src表示我真想说这几个字,别去读文件。 - 路径里有空格就得能引起来:
@"src/my file.js"。引号没闭合时当成没写完,不猜。 - 标点会粘在后面,而且分两种。 中文标点是硬终止符:
看看 @src/calc.js。还有…里那个句号必须停下解析,因为路径里不会出现它。但 ASCII 的点不能当终止符——calc.js自己就带一个点,只能在末尾出现时剥掉。斜杠两张表都不进,它是目录的标记,要留着。
/** 硬终止符:中日韩标点,路径里不会有它们,而人写中文一定紧贴着写 */
const TERMINATORS = new Set(['。', ',', '、', ';', ':', '!', '?', ')', '」'])
/** 只在末尾剥掉的字符。ASCII 的点不能当终止符:calc.js 自己就带一个 */
const TRAILING = new Set(['.', ',', ';', ':', '!', '?', ')', ']', '}', '"', "'"])
export function parseReferences(text: string): Reference[] {
const refs: Reference[] = []
for (let i = 0; i < text.length; i += 1) {
if (text[i] !== '@') continue
if (text[i - 1] === '\\') continue // 转义
// 这一行就是「邮箱不是引用」:@ 必须在开头或者紧跟空白
if (i > 0 && !/\s/.test(text[i - 1] as string)) continue
const parsed = readTarget(text, i + 1)
if (!parsed) continue
refs.push({ kind: classify(parsed.target), target: parsed.target, start: i, end: parsed.end })
i = parsed.end - 1
}
return refs
}TERMINATORS = set("。,、;:!?)」")
TRAILING = set(".,;:!?)]}\"'")
def parse_references(text: str) -> list[Reference]:
"""@ 必须在开头或紧跟空白,否则它是邮箱、装饰器或用户名的一部分。"""
refs: list[Reference] = []
i = 0
while i < len(text):
if text[i] != "@" or (i and text[i - 1] == "\\"):
i += 1
continue
if i and not text[i - 1].isspace():
i += 1
continue
parsed = read_target(text, i + 1)
if parsed is None:
i += 1
continue
target, end = parsed
refs.append(Reference(kind=classify(target), target=target, start=i, end=end))
i = end
return refs解析结果里保留每条引用在原文里的位置。为什么要留:用户的原话一个字都不改。 原话里那个 @src/calc.js 原样留着——它对模型是一个指针,模型回答时会引用它,用户翻历史时也能对上自己写过的东西。
至于不存在的路径:不崩、不注入、给一行人话,而且要说清你在哪儿找过。本实验的提示是「找不到(在 work/repo 或 当前目录 里都没有这个路径)」。为什么要说找过哪些地方——因为引用有两个根:用户心里的「这个文件」可能相对他让 Agent 改的那个仓库,也可能相对他启动 mca 的目录。两处都找,但必须把实际解析到哪个根打出来,猜对了不说,等于让他下次继续猜。
目录引用要展开成什么:文件树还是文件内容
这是今天第一个真正的设计决定,而且很容易做错。
@src/ 该注入什么?把这个目录下所有文件的内容都塞进去,听起来「信息更全」。本实验的起点代码就是这么写的,跑一次你就会看到那行报账变成几千字符——而用户真正关心的那个文件,可能因为预算不够被挤掉了。
本课的口径是:目录只给文件树,不给内容。
理由一句话:目录是「范围」,不是「材料」。 用户写 @src/ 表达的是「相关的东西在这一片」,不是「这一片每个字我都要你读」。给它一张清单,它自己会挑要读的那一两个——而它已经有 read_file 了。
你 > 看看 @src/ 和 @work/repo/README.md 还有 @https://example.com/doc
引用注入 3 个来源,共 345 字符(约 203 token,占引用预算 1%)
✔ src/ 72 字符 · 约 44 token
✔ work/repo/README.md 105 字符 · 约 59 token
✔ https://example.com/doc 168 字符 · 约 100 token顺带定另一条:通配符(@src/** 这种)算第三类形态,它会展开成很多个文件来源。这些来源要和「用户逐个点名的」区分开——分级在下面裁剪那一节要用。
文件本身的注入格式也有一个小决定:带行号,而且格式和 read_file 的输出一模一样。 模型不该学两种「文件长什么样」的格式;它看到行号就会用行号说话,而 edit_file 与 read_file 用的也是同一套行号。
网址引用的三件事:抓取、正文提取、失败降级
网址是三类来源里最麻烦的一类,因为它涉及一个你控制不了的对方。三件事各有一条纪律。
抓取要有超时、大小上限、内容类型检查,三样都不能省。 没超时,一轮对话会挂在一个不响应的服务器上;没大小上限,一个几十兆的页面在你算预算之前就已经进内存了(所以要边读边截,不是读完再截);不看内容类型,你会把一个 PDF 或者一张图的字节当成文本注进去。
正文提取做到「能用」就够,不要引第三方库。 目标不是完美还原文章结构,而是「把导航、脚本、样式去掉,留下能读的段落」。三步:
export function extractText(html: string): string {
// 顺序很重要:先整块删掉 script 与 style,
// 否则第二步把标签换成空白之后,脚本代码会变成正文
const withoutBlocks = html
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
const withBreaks = withoutBlocks
// 块级标签换成换行,段落感能留住一点
.replace(/<\/(p|div|li|h[1-6]|tr)>/gi, '\n')
.replace(/<[^>]+>/g, ' ')
return collapse(decodeEntities(withBreaks))
}BLOCK_END = re.compile(r"</(p|div|li|h[1-6]|tr)>", re.I)
TAG = re.compile(r"<[^>]+>")
def extract_text(html: str) -> str:
"""先整块删 script 与 style,再把标签换成空白——顺序反了脚本会变成正文。"""
body = re.sub(r"<script.*?</script>", " ", html, flags=re.S | re.I)
body = re.sub(r"<style.*?</style>", " ", body, flags=re.S | re.I)
body = BLOCK_END.sub("\n", body)
body = TAG.sub(" ", body)
return collapse(unescape(body))顺序是这段代码唯一的技术含量:先删整块,再删标签。反过来写,script 里的代码会变成正文的一部分——而模型会当真,然后在回答里引用你的埋点代码。
失败要降级成一句人话,不是崩掉整轮。 抓不到就说抓不到、为什么;抓到了但抽不出正文(多半是靠前端渲染的页面),就说清楚并建议用户直接把内容贴进来。不要硬猜:一个空壳页面注进去比不注进去更糟,因为模型会以为自己看过了。
顺带说一句离线:本实验在 MOCK=1 下不发任何网络请求,网址引用走一个内置样例页。抽正文那段逻辑跑的还是同一份代码,所以这条链路在没网的机器上也能验。
二进制与超大文件:拒绝比截断更诚实
两种坏材料,处理方式都是拒绝,而不是「尽力而为」。
二进制好理解:读到空字节基本就是二进制,把乱码注进去纯属浪费预算(判据和第三天的 read_file 一致)。
超大文件值得多说一句,因为「截断一下不就好了」听起来太自然了。
不好。 你截断一个日志文件,模型拿到的是首尾各一段,而中间那句关键报错正好在被截掉的地方——而它看不出自己被骗了,会基于不完整的材料给出一个很自信的结论。拒绝并给出下一步(「请用 read_file 带 offset 分段读,或者只引用其中一段」),反而让它有机会拿到真正需要的那一段。
那单源上限那里为什么又允许截断?因为那是已经通过大小检查的文件,量级可控(预算的一半),而且截断处会明确写「中间截掉了多少字符、怎么补读」。两种处理的分界线是「差一点」还是「差一个数量级」。
四种坏情况在本实验里各有一行提示,都带 ✘,都不注入:
引用注入 0 个来源,共 0 字符(约 0 token,占引用预算 0%)
✘ src/nope.js 找不到(在 work/repo 或 当前目录 里都没有这个路径)
✘ work/fixtures/logo.png 看起来是二进制文件(4 字节),没有注入
✘ work/fixtures/huge.txt 太大了(254 KB,上限 195 KB),没有注入。请用 read_file 带 offset 分段读
✘ src/*.rs 这个通配符没有匹配到任何文件注入量要显式告诉用户:字符数、估算 token、占预算的比例
现在是今天要立的那条纪律。
上下文里的每一段东西都在花钱、也在挤别的东西。而注入这件事的特点是用户看不见它:他写了三个 @,屏幕上就那一行字,实际可能注进去了两万字符。等他发现,往往是因为「怎么这么慢」或者「怎么答得这么离谱」。
所以:每一路注入都要自己报账。 报三个数——字符数、估算 token、占本路预算的百分之几。本实验那行 引用注入 1 个来源,共 252 字符(约 86 token,占引用预算 1%) 就是全部。
三个实现决定:
- 报账打在终端,不打给模型。 这些数字是给人做决策用的,塞给模型只是浪费预算——和第三天「工具结果的 meta 不回灌」是同一个道理。
- token 只估算,不精算。 本实验用中英分系数(中日韩字符按一个字一个 token,其余按四个字符一个 token)。它一定不准,真正的计数要靠 tokenizer;这里要的只是一个量级,好让那行字有意义。第十二天会用网关回传的真实用量把系数校准一遍。
- 预算只管自己这一路。 总预算怎么在系统提示、指令文件、记忆、历史、引用之间分配,是第十二天的题目。今天只要有这一路的账,第十二天才有东西可分。
裁剪规则也要可解释,因为它会真的丢东西。本课的三条,顺序就是优先级:
export function applyBudget(sources: Source[], budget = REF_BUDGET_CHARS): BudgetResult {
const singleCap = Math.floor(budget * 0.5)
// 一、每条先服从单源上限:超了从中间截断,保留首尾并写清截了多少
const staged = sources.map((source) =>
source.content.length <= singleCap ? measure(source) : measure(truncate(source, singleCap))
)
// 二、总量还超就丢,但只丢二级来源,而且从最后一条开始丢。
// 一级是用户逐个点名的,宁可截断也要留一段——悄悄丢掉他点名的文件
// 是最坏的行为,他会以为模型看过了
let total = staged.reduce((sum, s) => sum + s.chars, 0)
for (let i = staged.length - 1; i >= 0 && total > budget; i -= 1) {
const source = staged[i] as Measured
if (source.tier !== 'derived') continue
total -= source.chars
staged[i] = { ...source, content: '', chars: 0, dropped: true }
}
return summarize(staged, budget)
}def apply_budget(sources: list[Source], budget: int = REF_BUDGET_CHARS) -> BudgetResult:
"""一级来源宁可截断也要留;只丢二级,而且从最后一条开始丢。"""
single_cap = budget // 2
staged = [
measure(s if len(s.content) <= single_cap else truncate(s, single_cap)) for s in sources
]
total = sum(s.chars for s in staged)
for i in reversed(range(len(staged))):
if total <= budget:
break
if staged[i].tier != "derived":
continue
total -= staged[i].chars
staged[i] = replace(staged[i], content="", chars=0, dropped=True)
return summarize(staged, budget)三条规则背后是同一个判断:用户明确点名的东西优先级最高。 分级就是这么来的——逐个写出来的文件与网址是一级,通配符或目录带出来的是二级。而且丢了也要报数(那行报账末尾会多一句「另有 N 个来源因预算不够被丢掉」),静默丢弃是今天的红线。
还有一条结构上的决定,第十二天会付利息:注入内容是单独一条消息,不拼进用户那句话里。 拼在一起有三个损失——模型不再引用用户写的那个名字、用户翻历史看不到自己说过的话、压缩上下文时只能整条丢(而理想的做法是丢掉注入、保留原话)。
主动注入与让模型自己搜:什么时候该省这一步
最后回答一个绕不开的问题:既然它自己有 glob、grep、read_file,为什么还要 @ 引用?
因为两者的成本结构不一样:
| 主动注入(@ 引用) | 让模型自己检索 | |
|---|---|---|
| 往返次数 | 0 | 通常 2–4 轮 |
| 谁决定看什么 | 用户 | 模型 |
| 决定错了 | 拿着不相关材料很自信地答 | 多花几轮,但通常能自己找到 |
| 适合 | 用户知道该看哪儿 | 用户也不知道该看哪儿 |
判据就是最后一行:用户知道该看哪个文件,就注入;用户也在找,就让模型自己搜。
反过来说,两种情况不该用注入:一是你其实不确定该看哪个文件,凭感觉贴了五个进去——那不叫指路,那叫把噪音塞进上下文,而且挤掉的可能正是有用的那一份;二是材料会在对话过程中变化(比如一个正在被改的文件),注入的是一份快照,而工具读到的是当下——这种情况必须让它自己读。
顺带说一句今天标题里的图片:引用一张截图在解析层和文件没区别(多一类形态而已),但它要走的是多模态那条消息通路,与文本注入完全是两件事。那是第十九天的题目,今天的实现遇到图片会走「二进制文件」那条路直接拒绝——这不是偷懒,是让它在能力就位之前不要假装可以。
源码导读
动手实验
今天挖了五个练习点,其中三个是「看着更周到、实际更糟」的陷阱:目录连内容一起注入、超大文件截断而不是拒绝、预算不够时从最前面丢(还会丢掉用户点名的来源)。起点代码原样跑是十二项里过七项。
今天不需要子进程也不需要网络:@ 引用这条链路的每一步都能直接调用,网页抓取在 MOCK=1 下走内置样例页。二进制与超大文件这两份素材,自检会自己造在 work/fixtures 下。
- 实现引用解析器,识别文件、目录、网址三类并保留原文位置;用
/refs 试试 @**/*.js 与 eric@example.com确认邮箱没被当成引用。 - 实现展开器:文件带行号、目录只给清单、通配符展开成多个二级来源;跑一次
@src/确认注入里一个字节的文件内容都没有。 - 加注入预算与裁剪规则,超限时优先保留被明确点名的来源,被丢掉的要报数。
- 在终端打印本次注入的来源清单与字符数、估算 token、占预算比例;四种坏情况(不存在、二进制、超大、通配符没命中)各给一行人话。
- 跑自检:
MOCK=1 SELFTEST=1 pnpm start应该打印12/12 通过。解析结果、注入字符数、估算 token、裁剪后的总量都是可复现的;会话 id 与真实网页内容不是。
验收看五条勾:自检 12/12 通过;三类引用都被解析并显示注入量与占预算比例;@src/ 只注入清单;四种坏情况各有一行人话提示且都不注入;材料到位之后那一轮零次工具调用就给出结论,消息数组里是「注入一条 + 用户原话一条」两条。
面试题
今天三道题,考的是上下文注入的治理判断,不是「怎么读文件」:
- 实现文件引用注入,你会怎么处理目录、二进制和超大文件?
- 注入的上下文超过预算怎么裁?裁剪规则要不要让用户可见?
- 什么时候该主动把内容塞进上下文,什么时候该让模型自己调工具去取?
完整的中英题干、分析过程与答题要点见本课面试题库的第八天。第一题最容易答浅——三种材料对应三种不同的处理(清单、拒绝、拒绝并给下一步),能把「为什么拒绝比截断诚实」说清楚的人不多。
检查清单与明日预告
- 能说出解析 @ 引用要处理的四种情况,尤其是「邮箱不是引用」的判据
- 知道目录为什么只给文件树,以及「范围」与「材料」的区别
- 能说出抓网页的三件套,以及正文提取为什么要先删整块再删标签
- 能解释为什么超大文件拒绝比截断诚实,以及截断在哪种情况下才可以
- 能说出裁剪的三条规则,以及为什么静默丢弃是红线
- 知道注入内容为什么要单独一条消息,而不是拼进用户那句话
- 能说清主动注入与让模型自己搜的判据,以及两种不该注入的情况
明天是 D9《项目指令文件:三层加载、导入展开与 system prompt 的合并顺序》。今天解决的是「这一次看什么」,明天解决的是「每一次都该知道什么」——你的代码风格、测试怎么跑、哪些目录别碰,这些话不该每次对话都说一遍,该写进文件让它自己读。顺序上先做引用再做指令文件,是因为引用是一次性的、可控的,指令文件是常驻的、会互相覆盖的:先用简单的那一路把「注入要报账」这条纪律立起来,明天那三层合并才有账可对。
面试题库
实现文件引用注入(比如 @ 语法),你会怎么处理目录、二进制文件和超大文件?Implementing file-reference injection (an @ syntax, say), how would you handle directories, binary files, and very large files?
国内高频海外高频基础#context-injection#file-handling分析过程 · 先想清楚再作答
- 这题最容易答浅:三种情况各说一句「跳过就好」就完了。区分度在于你能不能对每一种给出**不同的**处理,并且说清为什么不同。
- 怎么拆:先问「模型拿到这份材料之后会做什么」。目录的答案是「它会挑一两个文件读」,那就给它清单;二进制的答案是「它会试着理解乱码」,那就一个字节都不给;超大文件的答案最关键——它会**基于被截断的材料给出很自信的结论**,而它看不出自己被骗了。
- 所以三种处理是:目录只给文件树不给内容(目录是「范围」不是「材料」);二进制直接拒绝,判据是读到空字节;超大文件也拒绝,但要在提示里给出下一步——用分段读的工具带偏移量取,或者只引用其中一段。
- 「超大文件为什么不截断」是这题的真正分水岭。截断一个日志,模型拿到首尾各一段,中间那句关键报错正好在被截掉的地方。拒绝反而让它有机会拿到真正需要的那一段。**能截断的只有已经通过大小检查的文件**——量级可控,而且截断处要写清截了多少、怎么补读。分界线是「差一点」还是「差一个数量级」。
- 还要主动讲检查顺序:先看文件大小(不用读文件),再读字节判二进制,最后才转文本。倒过来写,一个两百兆的视频会先被完整读进内存。安全上还有一条不变量:解析出来的绝对路径必须落在允许的根里面,字符串里查不查两个点都不可靠。
- 可预期的追问:图片呢?解析层它和文件没区别,但它要走多模态那条消息通路,是另一件事。能力没就位之前就按二进制拒绝——**让它别假装可以**,比返回一堆乱码有用。
How to reason about it · think before answering
- The easy failure is answering just skip them for all three. The signal is giving each a different treatment and explaining why they differ.
- How to break it down: ask what the model will do with the material. For a directory it will pick one or two files to read, so give it a listing. For a binary it will try to interpret garbage, so give it nothing. For an oversized file the answer is the important one: it will draw a confident conclusion from truncated material without noticing anything is missing.
- So: directories inject a file listing only, because a directory is a scope, not material; binaries are rejected, detected by a null byte; oversized files are also rejected, but the message must carry the next action — read it in ranges with an offset, or reference only the relevant part.
- Why not truncate the oversized file is the real dividing line. Truncating a log gives the model the head and tail while the one crucial error line sits in the removed middle. Rejecting gives it a path to the part it actually needs. Truncation is acceptable only for files that already passed the size check, where the magnitude is bounded and the cut is annotated with how much was removed and how to fetch it. The line is between off by a little and off by an order of magnitude.
- Volunteer the check order too: size first without reading, then bytes to detect binary, and only then decode text. Reversed, a two-hundred-megabyte video is fully loaded into memory first. One security invariant as well: the resolved absolute path must stay inside an allowed root, since string-scanning for dot-dot is never reliable.
- Likely follow-up: what about images? Parsing treats them like files, but they travel on the multimodal message path, which is a different feature. Until that path exists, reject them as binary — making the tool not pretend beats returning garbage.
答题要点
- 三种材料三种处理,判据是「模型拿到它之后会做什么」
- 目录只给文件清单不给内容:目录是范围不是材料,模型自己会挑文件读
- 二进制读到空字节直接拒绝;超大文件也拒绝,但要给出分段读的下一步
- 拒绝比截断诚实:被截掉的中间段模型看不出来,会给出很自信的错结论
- 检查顺序是大小、字节、文本;解析出的绝对路径必须落在允许的根里面
Key points
- Three kinds of material, three treatments, decided by what the model would do with each
- Directories inject a listing, not contents: a directory is a scope, and the model will pick files itself
- Binaries are rejected on a null byte; oversized files are rejected too, with a ranged-read next step
- Rejecting beats truncating: the model cannot see the removed middle and answers confidently anyway
- Check size, then bytes, then decode; and the resolved absolute path must stay inside an allowed root
注入的上下文超过预算怎么裁?裁剪规则要不要让用户可见?When injected context exceeds its budget, how do you trim it, and should the trimming rules be visible to the user?
国内高频海外高频进阶#context-budget#trimming分析过程 · 先想清楚再作答
- 这题在考「你有没有想过丢东西的后果」。答「按长度截到预算以内」的人漏掉了最要紧的一半:**丢哪一条比丢多少更重要。**
- 怎么拆:先给来源分级。用户逐个点名的(一个具体文件、一个网址)是一级;通配符或目录带出来的是二级。分级依据是「用户有没有明确指望它在里面」——这一条决定了后面所有规则。
- 然后是三条规则,顺序就是优先级:一级来源永不静默丢弃,宁可截断也要留一段并说清截了多少;单个来源不超过预算的一半,否则一个大文件就能把别的挤光;总量还不够就丢二级,从最后一条开始丢。实现上就是「先各自服从单源上限,再从尾部往前丢二级」两遍扫描。
- 第二问的答案是明确的**要可见**,而且理由不是「体验好」,是「不可见的裁剪会让模型和用户各自基于不同的事实说话」。用户以为那五个文件都在上下文里,模型只看到两个,于是它的回答在用户看来毫无道理。所以报账要打三个数:字符数、估算 token、占本路预算的百分比;被丢掉的要单独报数。**静默丢弃是红线。**
- 还有一条实现纪律值得讲:报账打在终端,不打给模型——那些数字是给人做决策的,塞给模型只是又花一遍预算。同理,注入内容要作为**单独一条消息**而不是拼进用户那句话,这样压缩上下文时可以只丢注入、保留原话。
- 可预期的追问:那总预算怎么在系统提示、指令文件、记忆、历史、引用之间分?那是另一层的问题,而它的前提正是「每一路都自己报账」——没有各路的账,总预算无从分配。分配策略通常是给固定的那几路(系统提示、指令文件)留死额度,剩下的按「越近越优先」给历史与注入。
How to reason about it · think before answering
- This tests whether you have thought about the consequence of dropping things. Answering truncate to fit misses the important half: which source you drop matters more than how much.
- How to break it down: tier the sources. Anything the user named individually — a specific file, a URL — is first tier; anything a glob or directory pulled in is second tier. The criterion is whether the user explicitly expected it to be there, and it drives every rule that follows.
- Then three rules, in priority order: never silently drop a first-tier source, truncate it instead and say how much was cut; cap any single source at half the budget, or one large file crowds out everything else; if the total still exceeds, drop second-tier sources starting from the last. Implementation is two passes — enforce the per-source cap, then drop from the tail.
- The answer to the second half is a clear yes, and not for user-experience reasons: invisible trimming makes the model and the user reason from different facts. The user believes all five files are in context, the model sees two, and its answer looks unreasonable. So report three numbers — characters, estimated tokens, and share of that lane's budget — and report dropped sources separately. Silent dropping is the red line.
- One more implementation discipline: the accounting goes to the terminal, not to the model, since those numbers exist for human decisions and would just spend budget again. Likewise, inject the material as its own message rather than concatenating it into the user's sentence, so compaction can drop the injection while keeping the original words.
- Likely follow-up: how do you split the total budget across system prompt, instruction files, memory, history, and injections? That is a layer up, and its precondition is exactly this per-lane accounting. Typical policy: reserve fixed allowances for the stable lanes and allocate the rest to history and injections with a recency preference.
答题要点
- 先给来源分级:用户逐个点名的是一级,通配符与目录带出来的是二级
- 三条规则:一级永不静默丢弃(宁可截断)、单源不超预算一半、超限从尾部丢二级
- 裁剪必须可见,否则模型与用户会基于不同的事实说话
- 报账三个数:字符数、估算 token、占本路预算比例;被丢掉的单独报数
- 报账打给人不打给模型;注入自成一条消息,便于压缩时只丢注入保留原话
Key points
- Tier the sources first: individually named ones are first tier, glob- or directory-derived ones second
- Three rules: never silently drop first tier, cap any single source at half the budget, drop second tier from the tail
- Trimming must be visible, or the model and the user end up reasoning from different facts
- Report three numbers — characters, estimated tokens, share of the lane budget — and count dropped sources separately
- Accounting goes to the human, not the model; keep injections as their own message so compaction can drop them selectively
什么时候该主动把内容塞进上下文,什么时候该让模型自己调工具去取?When should you push content into the context yourself, and when should you let the model fetch it with tools?
国内高频海外高频深入#context-strategy#retrieval分析过程 · 先想清楚再作答
- 这题在考架构判断,而且它没有唯一答案——所以答「都要有」不加条件的人会被追问到底。区分度在于你能不能给出一条可执行的判据。
- 怎么拆:把两者的成本结构摊开对比。主动注入是零次往返、用户决定看什么;让模型自己检索通常是两到四轮往返、模型决定看什么。而每一轮往返都要把整个消息数组重发一次,所以省掉两轮的收益是复利的。
- 判据就落在「谁知道该看哪儿」:**用户知道该看哪个文件就注入,用户也在找就让模型自己搜。** 这一条几乎能覆盖全部情况,而且它解释了为什么两条路径必须并存,而不是选一条。
- 然后要主动说两种**不该**注入的情况,这是这题的深水区:一、你其实不确定该看哪个文件,凭感觉贴了五个进去——那不叫指路,那叫把噪音塞进上下文,而且挤掉的可能正是有用的那一份;二、材料会在对话过程中变化(比如一个正在被改的文件),注入的是一份快照而工具读到的是当下,这种情况必须让它自己读,否则它会拿着旧内容做判断。
- 生产视角补一条:注入过的东西要在系统提示里说明「已经贴进来的文件不要再读一遍」。少了这句,它有不小的概率把同一个文件再读一次——同一份内容在上下文里出现两遍,既花钱又容易让它在两份之间纠结。
- 可预期的追问:那检索类的东西(向量检索、代码索引)算哪一类?算「让模型自己取」的加强版——区别只在检索质量,判据没变。真正的取舍仍然是「谁更知道该看哪儿」,而检索的价值恰恰在于用户和模型都不知道的时候。
How to reason about it · think before answering
- This tests architectural judgment and has no single right answer, so answering both, unconditionally invites follow-ups until you break. The signal is producing an actionable criterion.
- How to break it down: compare the cost structures. Pushing content costs zero round trips and lets the user decide what to look at. Letting the model retrieve usually costs two to four round trips and lets the model decide. Since every round trip resends the whole message array, saving two of them compounds.
- The criterion lands on who knows where to look: push when the user knows which file matters, retrieve when the user is also searching. That covers almost every case and explains why both paths must coexist rather than one replacing the other.
- Then volunteer the two cases where you should not push, which is the deep end: first, when you are not actually sure which file matters and you paste five in on instinct — that is not guidance, that is noise, and it may crowd out the one useful source; second, when the material changes during the conversation, such as a file being edited, since an injection is a snapshot while a tool read is the present state, and the model will otherwise reason from stale content.
- A production note: tell the model in the system prompt not to re-read files that were already pasted in. Without that line it has a real chance of reading the same file again, and the same content appearing twice both costs money and makes it hesitate between the two copies.
- Likely follow-up: where does retrieval — vector search, a code index — fit? It is a stronger version of let the model fetch, differing only in retrieval quality, so the criterion is unchanged. Its real value shows up precisely when neither the user nor the model knows where to look.
答题要点
- 对比成本结构:注入零往返、用户决定;检索两到四轮、模型决定,而每轮都重发全部历史
- 判据一句话:用户知道该看哪个文件就注入,用户也在找就让模型自己搜
- 两种不该注入:自己也不确定就贴一堆(是噪音)、材料会在对话中变化(注入是快照)
- 注入过的内容要在系统提示里说明不用再读一遍,否则同一份内容会出现两遍
- 向量检索与代码索引属于「让模型自己取」的加强版,判据不变
Key points
- Compare cost structures: pushing is zero round trips and user-decided, retrieval is two to four and model-decided, and every trip resends all history
- One criterion: push when the user knows which file matters, retrieve when the user is searching too
- Two cases not to push: pasting several files on instinct, and material that changes mid-conversation since an injection is a snapshot
- Tell the model not to re-read what was already pasted, or the same content shows up twice
- Vector search and code indexes are a stronger form of model-side retrieval, and the criterion does not change