Reference Injection: Parsing @ Files, Directories, URLs, and Images, and Accounting for What Got Injected
Don't make the agent scan the whole repo itself: implement @-reference syntax that parses files, directories, and web addresses into structured injected content, handle binary and oversized files, and print in the terminal exactly how many characters were injected and how much budget that used.
Today's Goals
- Implement parsing and expansion for a reference syntax covering files, directories, and URLs
- Set a budget for injected content, and prune it by an explainable rule when it goes over
- Explain the trade-off between proactive injection and letting the model call a tool to retrieve it itself
Week two starts today, and the theme shifts from "can it work" to "can it avoid detours." When you finish, scroll back up and tick the three goals off.
Plain-Language Walkthrough
Pointing the way: putting the documents on their desk beats making them search the building
Week one's new hire can work now. But notice how they start every time: search the repository for a keyword, list a directory, open three or four files, and only then begin.
And very often you knew which file mattered all along. You simply did not say.
The first seven days' mca was like that. Ask "what is wrong with divide" and it will glob for files, grep for keywords, read_file to read — three or four tool calls, each a full gateway round trip, buying something you could have handed over in one sentence.
Today builds that sentence: @ references. Write explain what is wrong with divide in @src/calc.js and the program puts that file's content into the context before sending the request. In the lab, that sentence produces a conclusion with zero tool calls — the same question took three or four turns in week one.
The saving is more than latency. Every tool round trip resends the whole message array, so skipping two turns compounds.
But it has a cost, and that cost is the whole second half of today: you decided what it looks at. Decide wrongly and it answers confidently from irrelevant material; make the material too large and it spends the budget on noise; let the material be quietly truncated and it cannot tell. So today's real discipline is not the syntax but this: every injection must be accounted for.
Parsing the reference syntax: escaping, spaces, wildcards and missing paths
The syntax has one symbol: @ followed by a path, a wildcard or a URL. Five lines of regex write it — and then four classes of bug arrive within a week, because it handles what people type casually.
Four cases, none skippable:
- An email address is not a reference. In
eric@example.comthe character before @ is a letter. The criterion is "@ must be at the start or right after whitespace," not "the text contains an @." Without it, every email address a user writes sends the Agent to read a nonexistent path and report an error. - Escaping must work.
\@srcmeans I really meant those characters, do not read a file. - Paths with spaces must be quotable:
@"src/my file.js". An unclosed quote counts as unfinished; do not guess. - Punctuation sticks to the end, in two kinds. CJK punctuation is a hard terminator: in a Chinese sentence the full stop must end parsing, because paths never contain it. But an ASCII dot cannot be a terminator —
calc.jscontains one — so it is only stripped when trailing. A slash is on neither list; it marks a directory and must stay.
/** Hard terminators: CJK punctuation, absent from paths and written flush against text */
const TERMINATORS = new Set(['。', ',', '、', ';', ':', '!', '?', ')', '」'])
/** Stripped only when trailing. An ASCII dot cannot terminate: calc.js contains one */
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 // escaped
// This line is "an email is not a reference": @ must start the text or follow whitespace
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]:
"""@ must start the text or follow whitespace, else it belongs to an email or a handle."""
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 refsThe parse result keeps each reference's position in the original text. Why: the user's own words are not altered by one character. The @src/calc.js stays in their sentence — it is a pointer for the model, which cites it when answering, and the user can match it when scrolling back.
As for a missing path: do not crash, do not inject, print one plain sentence, and say where you looked. The lab's message reads "not found (no such path under work/repo or the current directory)." Why name the places searched — because references have two roots: the "this file" in the user's head may be relative to the repository the Agent is editing, or to the directory where they started mca. Search both, but print which root actually resolved; guessing right without saying so leaves them guessing next time.
What a directory reference expands into: a file tree or file contents
This is today's first real design decision, and an easy one to get wrong.
What should @src/ inject? Stuffing in the contents of every file under it sounds "more complete." The starter does exactly that, and one run shows the accounting line jumping to thousands of characters — while the file the user actually cared about may have been squeezed out by the budget.
This course's rule: a directory gives a file tree, not contents.
The reason in one sentence: a directory is a scope, not material. Writing @src/ says "the relevant thing is somewhere in here," not "read every character of this area." Give it a list and it picks the one or two to read — and it already has read_file.
you > look at @src/ and @work/repo/README.md and @https://example.com/doc
Injected 3 sources, 345 characters total (about 203 tokens, 1% of the reference budget)
v src/ 72 chars - about 44 tokens
v work/repo/README.md 105 chars - about 59 tokens
v https://example.com/doc 168 chars - about 100 tokensOne more rule alongside: a wildcard (@src/**) is a third form, expanding into many file sources. Those must be distinguished from the ones the user named individually — the tiering matters in the pruning section below.
The file injection format has a small decision too: include line numbers, in exactly the format read_file outputs. The model should not learn two "what a file looks like" formats; seeing line numbers it speaks in line numbers, and edit_file and read_file use the same numbering.
Three things about URL references: fetching, text extraction, graceful failure
URLs are the most troublesome of the three kinds, because they involve a party you do not control. Each of the three things has a discipline.
Fetching needs a timeout, a size cap and a content-type check, all three. Without a timeout, a turn hangs on an unresponsive server; without a size cap, a page of tens of megabytes is in memory before you compute the budget (so cap while reading, not after); without a content-type check, you inject the bytes of a PDF or an image as text.
Text extraction only needs to be usable; do not add a third-party library. The goal is not perfect structural fidelity but "strip navigation, scripts and styles, keep readable paragraphs." Three steps:
export function extractText(html: string): string {
// Order matters: delete script and style blocks entirely first,
// or step two turns tags into whitespace and script code becomes body text
const withoutBlocks = html
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
const withBreaks = withoutBlocks
// Block-level tags become newlines so some sense of paragraphs survives
.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:
"""Delete script and style blocks first, then tags - reversed, code becomes body text."""
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))Order is this code's only subtlety: delete blocks first, then tags. Reversed, code inside script becomes body text — and the model takes it seriously and quotes your analytics snippet in its answer.
Failure degrades to a plain sentence, not a crashed turn. Cannot fetch: say so and why. Fetched but no body extractable (usually a client-rendered page): say so and suggest pasting the content directly. Do not guess: injecting an empty shell is worse than injecting nothing, because the model believes it has read the page.
An offline note: under MOCK=1 the lab sends no network requests, and URL references use a built-in sample page. The extraction logic runs the same code, so the path is verifiable on a machine with no network.
Binary and oversized files: refusing is more honest than truncating
Two kinds of bad material, both handled by refusal, not "best effort."
Binary is easy: reading a null byte basically means binary, and injecting garbage wastes budget (the criterion matches day three's read_file).
Oversized files deserve a sentence, because "just truncate it" sounds too natural.
It is not fine. Truncate a log file and the model gets a head and a tail while the critical error line sits exactly in the removed middle — and it cannot tell it was misled, so it concludes confidently from incomplete material. Refusing with a next step ("read it in slices with read_file and an offset, or reference just one section") gives it a chance to obtain the part it actually needs.
Then why is truncation allowed at the per-source cap? Because that file already passed the size check, its magnitude is controlled (half the budget), and the cut point clearly states how many characters were removed and how to read the rest. The dividing line is "slightly over" versus "an order of magnitude over."
The lab prints one line per bad case, all marked with an x and none injected:
Injected 0 sources, 0 characters total (about 0 tokens, 0% of the reference budget)
x src/nope.js not found (no such path under work/repo or the current directory)
x work/fixtures/logo.png looks like a binary file (4 bytes), not injected
x work/fixtures/huge.txt too large (254 KB, cap 195 KB), not injected. Use read_file with an offset
x src/*.rs this wildcard matched no filesReport the injected volume: characters, estimated tokens, percentage of budget
Now the discipline of the day.
Everything in the context costs money and squeezes something else out. And injection has a particular property: the user cannot see it. They wrote three @s and see one line on screen, while twenty thousand characters may have gone in. They discover it when things are slow or the answer is wildly off.
So: every injection path reports its own numbers — characters, estimated tokens, and what percentage of that path's budget was used. The lab's line — Injected 1 source, 252 characters total (about 86 tokens, 1% of the reference budget) — is the whole thing.
Three implementation decisions:
- The accounting goes to the terminal, not to the model. These numbers help a human decide; sending them to the model just wastes budget — the same reasoning as day three's "tool result meta is not fed back."
- Tokens are estimated, not counted. The lab uses split coefficients (one token per CJK character, one per four characters otherwise). It is definitely inaccurate; real counting needs a tokenizer, and all we want here is a magnitude that makes the line meaningful. Day twelve calibrates the coefficients against real usage returned by the gateway.
- The budget covers only this path. How the total is divided between the system prompt, instruction files, memory, history and references is day twelve's subject. Today only produces this path's ledger, so day twelve has something to divide.
The pruning rules must be explainable, because they really do discard things. This course's three, in priority order:
export function applyBudget(sources: Source[], budget = REF_BUDGET_CHARS): BudgetResult {
const singleCap = Math.floor(budget * 0.5)
// 1. Each source obeys the per-source cap first: over it, cut from the middle,
// keeping both ends and stating how much was removed
const staged = sources.map((source) =>
source.content.length <= singleCap ? measure(source) : measure(truncate(source, singleCap))
)
// 2. Still over the total: drop, but only derived sources, starting from the last.
// Tier-one sources were named by the user; truncate rather than drop them - silently
// dropping a named file is the worst behavior, since the user assumes it was read
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:
"""Tier-one sources are truncated rather than dropped; drop derived ones from the end."""
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)One judgment underlies all three rules: what the user named explicitly has the highest priority. That is where the tiering comes from — files and URLs written out individually are tier one, those brought in by a wildcard or directory are tier two. And drops must be reported (the accounting line ends with "N more sources dropped for lack of budget"); silent discarding is today's red line.
One structural decision that pays interest on day twelve: injected content is its own message, not spliced into the user's sentence. Splicing loses three things — the model stops citing the name the user wrote, the user cannot see their own words when scrolling back, and compaction can only drop the whole thing (whereas ideally you drop the injection and keep the words).
Proactive injection versus letting the model search: when to skip the step
Finally, the unavoidable question: since it has glob, grep and read_file, why @ references at all?
Because the cost structures differ:
| Proactive injection (@) | Letting the model retrieve | |
|---|---|---|
| Round trips | 0 | usually 2-4 |
| Who decides what to look at | the user | the model |
| If the decision is wrong | it answers confidently from irrelevant material | a few extra turns, but it usually finds it |
| Suited to | the user knows where to look | the user does not know either |
The criterion is the last row: inject when the user knows which file matters; let the model search when the user is looking too.
Conversely, two situations where injection is wrong: one, you are not actually sure which file matters and pasted five in on a hunch — that is not pointing the way, that is stuffing noise into the context, possibly squeezing out the useful one; two, the material changes during the conversation (a file currently being edited), where an injection is a snapshot while a tool read is the present — there it must read for itself.
A word on images in today's title: referencing a screenshot is no different at the parsing layer (one more form), but it must travel the multimodal message path, an entirely different matter from text injection. That is day nineteen's subject, and today's implementation refuses images down the "binary file" path — not laziness, but not pretending to a capability that is not in place.
Source Reading
Hands-On Lab
Today leaves five exercises, three of which are "looks more thorough, is actually worse" traps: injecting directory contents, truncating oversized files instead of refusing, and dropping from the front when over budget (which discards sources the user named). The starter passes seven of twelve unmodified.
No child processes and no network today: every step of the @ path is directly callable, and web fetching uses a built-in sample page under MOCK=1. The binary and oversized fixtures are created by the self-test under work/fixtures.
- Implement the reference parser recognizing files, directories and URLs while keeping original positions; run
/refs try @**/*.js and eric@example.comand confirm the email was not treated as a reference. - Implement the expander: files with line numbers, directories as a listing only, wildcards expanding into several tier-two sources; run
@src/and confirm not one byte of file content is injected. - Add the injection budget and pruning rules, preserving explicitly named sources first and reporting anything dropped.
- Print this turn's source list with characters, estimated tokens and percentage of budget; give one plain sentence for each of the four bad cases (missing, binary, oversized, wildcard with no match).
- Run the self-test:
MOCK=1 SELFTEST=1 pnpm startshould print 12/12 passed. Parse results, injected characters, estimated tokens and the pruned total are reproducible; the session id and real web content are not.
Acceptance is five ticks: the self-test prints 12/12 passed; all three reference kinds parse and show volume and budget percentage; @src/ injects a listing only; the four bad cases each give a plain sentence and inject nothing; and once the material is in place that turn reaches a conclusion with zero tool calls, with the message array holding one injection message plus the user's own words.
Interview Questions
Today's three questions test governance judgment over context injection, not "how to read a file":
- Implementing file reference injection, how do you handle directories, binaries and oversized files?
- When injected context exceeds the budget, how do you prune? Should the pruning rules be visible to the user?
- When should content be pushed into the context proactively, and when should the model fetch it with a tool?
Full bilingual prompts, analyses and key points are in this course's day-eight question bank. Question one is the easiest to answer shallowly — three kinds of material map to three different handlings (a listing, refusal, refusal with a next step), and few can explain why refusing is more honest than truncating.
Checklist and Tomorrow
- I can name the four cases @ parsing must handle, especially the criterion for "an email is not a reference"
- I know why a directory gives only a file tree, and the difference between scope and material
- I can name the three requirements for fetching a page, and why extraction deletes blocks before tags
- I can explain why refusing an oversized file is more honest than truncating, and when truncation is acceptable
- I can state the three pruning rules and why silent discarding is a red line
- I know why injected content is its own message rather than spliced into the user's sentence
- I can state the criterion between proactive injection and model-driven search, plus the two cases where injection is wrong
Tomorrow is D9, "Project Instruction Files: Three-Layer Loading, Import Expansion, and the System Prompt Merge Order." Today solved "what to look at this once"; tomorrow solves "what it should know every time" — your code style, how tests are run, which directories not to touch. Those should not be repeated in every conversation; they belong in a file it reads itself. References come before instruction files because references are one-off and controllable while instruction files are permanent and override each other: use the simple path to establish the accounting discipline, so tomorrow's three-layer merge has a ledger to reconcile against.
Interview questions
Implementing file-reference injection (an @ syntax, say), how would you handle directories, binary files, and very large files?实现文件引用注入(比如 @ 语法),你会怎么处理目录、二进制文件和超大文件?
Common in ChinaCommon overseasBasic#context-injection#file-handlingHow 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?注入的上下文超过预算怎么裁?裁剪规则要不要让用户可见?
Common in ChinaCommon overseasIntermediate#context-budget#trimmingHow 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
答题要点
- 先给来源分级:用户逐个点名的是一级,通配符与目录带出来的是二级
- 三条规则:一级永不静默丢弃(宁可截断)、单源不超预算一半、超限从尾部丢二级
- 裁剪必须可见,否则模型与用户会基于不同的事实说话
- 报账三个数:字符数、估算 token、占本路预算比例;被丢掉的单独报数
- 报账打给人不打给模型;注入自成一条消息,便于压缩时只丢注入保留原话
When should you push content into the context yourself, and when should you let the model fetch it with tools?什么时候该主动把内容塞进上下文,什么时候该让模型自己调工具去取?
Common in ChinaCommon overseasDeep dive#context-strategy#retrievalHow 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
答题要点
- 对比成本结构:注入零往返、用户决定;检索两到四轮、模型决定,而每轮都重发全部历史
- 判据一句话:用户知道该看哪个文件就注入,用户也在找就让模型自己搜
- 两种不该注入:自己也不确定就贴一堆(是噪音)、材料会在对话中变化(注入是快照)
- 注入过的内容要在系统提示里说明不用再读一遍,否则同一份内容会出现两遍
- 向量检索与代码索引属于「让模型自己取」的加强版,判据不变