Dayward AI
Week 2 · D12About 4 hours

Context Compression: How to Count Tokens, What to Compress and What to Keep, and How to Verify Nothing Was Lost

Context always fills up eventually: first calibrate a local token estimator against real usage, implement segment-summary compression, pin down which message categories must be kept verbatim, then verify that compression didn't lose key information, using a set of probes that ask about early facts.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Implement a calibratable token estimator, and explain when estimation versus exact counting each applies
  2. Design a compression strategy, and explain which message categories must be kept verbatim and why
  3. Check with a verifiable method whether compression lost key information

For four days we kept adding to the context: references, instructions, checklists, memory. Today we take something out for the first time. When you finish, scroll back up and tick the three goals off.

Plain-Language Walkthrough

Only today's files on the desk

The new hire's desk is crowded now: the material you handed over, the team handbook, the checklist, the handover notes, and the dozen files opened in the last two hours, all spread out. The desk is finite, and one more sheet means removing one.

The question is which one. The lazy approach pulls the bottom sheets — older is lower, which sounds reasonable. But the oldest sheet may be exactly what you said this morning: this repository's build command is not pnpm build. Remove it and they spend the afternoon running the wrong command before asking you why it fails.

The correct approach is archiving: put that stack of read files away, but first write one line on a sticky note — "the facts confirmed in those files are..." The desk is clear and the conclusions remain.

That is context compaction: replacing a growing history with a short summary rather than discarding it. Day six's usage limit stops and tells you to compact — today builds the other half: not only stopping but making room.

Boundaries first: today is implementation only. What belongs in the context is a strategy question, and this platform's five-day context engineering course covers it end to end. Today answers three engineering questions: how to count, what to compress and what to keep, and how to prove nothing was lost.

The third is the most skipped, and it is the only one that lets you enable compaction in production with confidence: get the first two wrong and you see "the answers are a bit off"; skip the third and you cannot even tell that the wrongness came from compaction.

Learn to count before learning to save: character estimates, tokenizers, real usage

To save you must first count. There are three ways to count tokens, each with its use, and mixing them causes errors.

One: exact counting, running the model's own tokenizer. The only truly accurate one. But this course connects to any OpenAI-compatible gateway, behind which may be any model from anyone — the token table is not unique. Installing a tokenizer package gives only an illusion of exactness: precise arithmetic over someone else's tokenization.

Two: real usage, reading the gateway's returned usage. The prompt_tokens in that final usage event is what this request actually cost. It is accurate, and it is after the fact: judging "will this exceed" needs a number before sending.

Three: local estimation. Simple in shape, two terms added:

TextText
tokens ~= a x CJK character count + b x other character count

Day eight's accounting line used exactly this, with a as 1 and b as one quarter — roughly one token per CJK character, roughly one per four Latin characters. Enough to print "about 86 tokens," and no further.

Their division in one sentence: estimation decides (before a request, should we compact), real usage reconciles (after a request, how good was the estimate), exact counting settles (when you actually bill by token, which is day twenty).

Calibrate the estimator with returned usage

Since every request returns real usage, the coefficients should not be guessed but regressed.

The method collects samples in passing: before sending, join the message array into text and record it; when usage returns, pair "CJK character count, other character count" with "real token count" as one sample. With three samples, solve a least-squares fit — two unknowns, two normal equations.

Collect in passing, never send extra requests to measure. Dedicated probe requests cost money and measure a different kind of text; what you want to estimate well is the kind your program actually sends.

src/context/tokens.ts
fit(minSamples = 3): FitResult {
  const before = meanAbsPercentError(this.samples, active)
  if (this.samples.length < minSamples) return notEnough(before, this.samples.length)
 
  // The six sums of the two normal equations: sum cjk^2, cjk*rest, rest^2, cjk*t, rest*t
  let sxx = 0, sxy = 0, syy = 0, sxt = 0, syt = 0
  for (const s of this.samples) {
    sxx += s.cjk * s.cjk
    sxy += s.cjk * s.rest
    syy += s.rest * s.rest
    sxt += s.cjk * s.tokens
    syt += s.rest * s.tokens
  }
 
  const det = sxx * syy - sxy * sxy
  // A relative criterion, not an absolute one: with many samples, sxx*syy is itself huge
  if (Math.abs(det) < 1e-6 * Math.max(1, sxx * syy)) return flatFallback(this.samples, before)
 
  const coeff = {
    cjk: clamp((sxt * syy - syt * sxy) / det),
    rest: clamp((syt * sxx - sxt * sxy) / det),
  }
  return { coeff, samples: this.samples.length, errorBefore: before,
    errorAfter: meanAbsPercentError(this.samples, coeff), degenerate: false, note: 'calibrated' }
}

The only subtlety is that det check: a determinant near zero means the samples' CJK-to-other ratio barely varies (most commonly all-Latin text), and the two coefficients are then mathematically inseparable. Forcing a solution yields an absurd pair (often one positive and one negative), worse than no calibration, because it looks calibrated. So that branch must admit it cannot solve and fall back to one uniform per-character coefficient.

The self-test calibrates on five texts with different ratios and prints these two lines:

TextText
v calibrated on 5 samples: coefficients CJK 0.50 / other 0.51, error 55.6% -> 2.6%
v an all-ASCII sample judged unsolvable: the CJK-to-other ratio is too uniform to separate the two coefficients; falling back to one per-character coefficient

Both lines are reproducible offline, but offline "real usage" comes from the script (derived from character counts), so the error drops flatteringly; against a real gateway not one line of code changes, the regression yields different coefficients, and the error will not be that low.

One last discipline: the calibration result is one process-level table shared by all accounting, or the same text is reported as two different numbers in two places.

When to trigger: by proportion, not by turn count

The obvious criterion is turns: compact after twenty. It is wrong — there is no stable relationship between turns and occupancy. One grep matching two thousand lines eats half the window in a turn; ten turns of "change a line, run a test" may not reach ten percent. Turn-based triggering produces two behaviors only: compacting too early on short conversations and too late on long results (remembering to compact after already exceeding).

The correct criterion is proportion: compact when the estimate exceeds seventy percent of the total budget.

Which requires a "total budget" table. Each path reported its own ledger over the past days; today is where they are totaled, and the only place in the course that discusses dividing the total. The idea in one sentence: give the permanent paths fixed allowances and leave the rest to the ones that grow.

PathNatureAllowanceOwner
System instructionspermanent, fixed length3%day one
Project instruction filespermanent, fixed length10%day nine
Memorypermanent, slowly growing5%day eleven
Reference injectionone-off, controllable20%day eight
Compaction summariesaccumulate, must be capped7%today
Conversationgrows40%every turn
Tool resultsgrows fastest15%every tool call

Fixed allowances are not about saving money but about naming who overran: with one total you know only that it is full; with per-path allowances you know which path to compact. The lab's /budget prints this table, listing even the empty paths with their allowances.

The trigger sits at seventy rather than ninety percent for a practical reason: compaction itself takes room — the summarization request resends the conversation being compacted, and the summary occupies space too. Compacting at ninety percent risks the compaction request itself exceeding the limit.

The self-test pins this as an assertion: the same fourteen messages, the same turn count, differing only in tool result length —

TextText
v same 14 messages, same turns: the long one triggers at 1368 tokens, the short one does not at 176

What to compress: consecutive tool round trips

What can be compressed is not homogeneous: within the same history, a thousand tokens of tool results and a thousand tokens of user requirements differ by an order of magnitude in compression value.

Consecutive tool round trips are most worth compressing, for three increasingly practical reasons:

One, they occupy the most. Reading back hundreds of numbered lines, a test output of over a thousand characters — tool results are almost always the largest block.

Two, their information has already been digested. The model read the file and said "divide has no divide-by-zero guard," and that conclusion is in the assistant message right after; keeping the original only pays twice for one conclusion.

Three, they can be re-fetched. The file is still on disk and the command can run again. The worst case of compressing it is reading it again when needed; compressing a user requirement loses that sentence forever — they will not repeat it, they assume you remember.

So priority follows "can it be re-fetched," not recency. That transfers directly: compress the re-fetchable (files, search results, web bodies) first and the unrecoverable (the user's words, one-time responses, random ids) last.

What to keep: an explicit keep list

What to keep must be a whitelist: state which categories survive verbatim, and the rest is compressible. Enumerating "what may be compressed" will eventually miss a newly introduced message type, and the consequence is silent information loss — harder to spot than day eight's red line: a lost injection leaves an accounting line, a lost compaction leaves nothing.

The keep list has four entries:

  1. System instructions. They are this program's persona and rules; compressing them changes the program.
  2. The todo checklist. It is the sole record of "how far I have got," and summarizing it makes the model redo finished work. The checklist is day ten's mechanism, but the keep list must reserve its place first: the lab's criterion is a prefix match, so any checklist starting with those characters is automatically protected.
  3. Unfinished tool calls. Fewer tool results than calls means the previous turn was interrupted. A summary line saying "called run_command" cannot replace the missing result — what the model needs is to fill it in.
  4. The most recent few groups. Everything the model is currently doing is in them. A summary is a conclusion; right now it needs the original. Compressing them typically makes it repeat work just done.

And one rule harder than the list: an assistant message with tool calls and all its tool results are inseparable — the same origin as day seven's fork rule; splitting them yields a message array where a tool was requested with no result, making the next request invalid outright. So the first step is not selecting messages but grouping:

src/context/compact.ts
export function groupMessages(messages: Message[], options = DEFAULT_COMPACT_OPTIONS): Group[] {
  const groups: Group[] = []
  for (let i = 0; i < messages.length; i += 1) {
    const message = messages[i] as Message
    if (message.role === 'assistant' && message.toolCalls?.length) {
      // Swallow the consecutive tool messages that follow into this group
      let end = i + 1
      while (end < messages.length && messages[end]?.role === 'tool') end += 1
      const results = end - (i + 1)
      // Fewer results than calls means the previous turn was interrupted: keep this group verbatim
      groups.push({ start: i, end, keep: results < message.toolCalls.length ? 'unfinished' : 'compressible' })
      i = end - 1
      continue
    }
    groups.push({ start: i, end: i + 1,
      keep: message.role === 'system' ? 'system' : options.pinned(message) ? 'pinned' : 'compressible' })
  }
  return groups
}

After grouping and marking the keepers, the remaining consecutive groups form the compressible region; that region is then split at user messages — a user message opens a new piece of work, so splitting by work keeps a summary from blending two things into one sentence.

How to compress: have the model write a structured summary

Only now does the model take the stage, and its hands must be tied first.

A free-form summary reads smoothly and is lethal: it turns "divide has already been changed to throw" into "discussed how to fix the divide-by-zero problem" — an action became a topic. Next turn the model does not know it acted, so it acts again; and this time old_string no longer matches, so it starts suspecting someone else changed the file.

So the prompt fixes four headings and the model only fills in content:

TextText
Confirmed facts:
Changes already made:
Unfinished items:
Key paths and commands:

Their division: facts are conclusions that can be cited directly, changes are actions that must not be redone, unfinished items are the entry point for the next step, and paths and commands are the keys for re-fetching originals — that last line is what makes a compressed tool result genuinely re-fetchable. The prompt also says "content must come from the original; write none rather than inventing": once a summary speculates, the rest of the conversation rests on a sentence nobody said.

A fixed format makes it checkable, and that is today's most important safeguard:

Put each summary back in place, one message replacing a segment. Day eight's groundwork pays off here: injected material was its own message and never spliced into the user's sentence, so compaction can drop the injection and keep the words.

Finally, add a line to the system prompt: "the conversation history may have been compacted; messages starting with this phrase are summaries." Without it the model treats a summary as something the user just said, or keeps asking which file you meant.

How to verify: use early facts as probes

How do you know nothing was lost? "Looks fine" is not an answer — compaction fails silently: the model will not say "I lost a fact," it will confidently carry on with a wrong or invented one, and you notice several turns later from a strange result.

The verifiable method is probes: before compacting, record a few facts that exist only in the early context, and look for them afterwards.

The criterion is the key. The criterion is whether the fact is still in the context, not whether the model answers correctly. The latter is unreliable: a correct answer may be a lucky guess and a wrong one this turn's bad luck. Whether that sentence is in the context is deterministic, reproducible, and the only thing compaction can be held responsible for.

The self-test makes this a controlled experiment, because only a control proves anything:

TextText
v compaction: 14 messages -> 7, estimate 1368 -> 571 tokens, both summaries carry all four headings
v both probes are still findable in the compacted context: pnpm build:calc, divide by zero
v switching to "keep only the last three groups": the estimate drops to 358 tokens, but the fact "pnpm build:calc" is gone

The same session, the same keep list, with only "write a summary" replaced by "just discard": it saves even more tokens, and the early fact is gone. That is the entire difference between compaction and truncation, and what that extra request buys.

Two reminders. One: probes are best pinned by the user — sentences like "remember: the build command is..." are natural probes. Two: real mode deserves a second layer that actually asks the probe questions. The two layers mean different things: the first verifies compaction, the second verifies the model, and only the first is something you can fix.

Source Reading

Hands-On Lab

🧪 D12 lab: a context compaction mechanism with calibrated estimation, triggering and verification, plus a manual command

Code location: labs/my-coding-agent-21days/day-12-context-compaction

Today leaves five exercises, four of which are "looks simpler, is worse" traps: reporting coefficients without calibrating, one message per group (producing tool results with no matching call), triggering by turn count, and using a summary as soon as it arrives. The starter passes six of fifteen unmodified, all offline.

  1. Implement the two-coefficient estimator and least-squares calibration, see the line "error 55.6% down to 2.6%," and confirm an all-ASCII sample is judged unsolvable.
  2. Change triggering to proportion, and confirm with two histories of fourteen messages each that "the long one triggers and the short one does not."
  3. Implement grouping and the keep list: assistant messages bound to their tool results, with system instructions, the pinned checklist, unfinished calls and the last three groups excluded from the compressible region.
  4. Implement segment summaries with a format safeguard: one missing heading abandons the whole compaction, touching not one byte.
  5. Run MOCK=1 SELFTEST=1 pnpm start to see 15/15 passed, then use /compact in the REPL and check the two probes; the self-test's numbers are reproducible, while turns that run real tests in the REPL fluctuate by tens of tokens.

Acceptance is five ticks: the self-test prints 15/15 passed; calibration error drops markedly and a uniform sample refuses a forced solution; two histories of equal length differ in triggering; after compaction the message count and estimate both drop, summaries carry all four headings and no orphaned tool results remain; and both probes survive compaction while the truncation variant loses one fact.

Interview Questions

Today's three questions test engineering judgment about tokens and compaction, not "what is a context window":

  1. Without a tokenizer, how do you estimate tokens? How large is the error, and when can estimation not be used?
  2. Which message categories must compaction keep? Which are safest to compress?
  3. How do you prove a compaction lost no key information?

Full prompts, analyses and key points are in this course's day-twelve question bank. Question three discriminates most — most people get as far as "let the model judge," and few can state that the criterion is whether the context contains it, not whether the model answers correctly.

Checklist and Tomorrow

  • I can state the division between estimation, real usage and exact counting, and why this course adds no tokenizer
  • I can hand-write the two-coefficient least-squares calibration and explain why a near-zero determinant must not be forced
  • I can give the reason proportion beats turn count, and why the trigger does not sit at ninety percent
  • I can draw the seven-path budget table and state what fixed allowances really buy
  • I know compaction priority follows "can it be re-fetched," not recency
  • I can recite the four keep-list entries and the hard rule that an assistant message and its tool results are inseparable
  • I can explain why summaries are structured, why a failed format abandons everything, and what the probe criterion is

Tomorrow is D13, "Ask Before Acting: a Structured Question Tool, Read-Only Exploration Mode, and Plan Approval." Today solved what to do when the desk is full; tomorrow solves putting less on the desk in the first place: ask what to change, explore read-only, produce an approvable plan, and only then act. Compaction comes before plan mode because plan mode lengthens the context considerably — it adds question, exploration and plan segments, all three of which are the least appropriate to compress. Settle "what to compress and what to keep" today so those three segments have somewhere to sit tomorrow.

Interview questions

  • Without pulling in a tokenizer, how would you estimate the token count of a context? How large is the error, and when must you not rely on an estimate?不引入分词器,你怎么估算一段上下文有多少 token?误差有多大,什么时候不能用估算?
    Common in ChinaCommon overseasBasic#token-counting#calibration

    How to reason about it · think before answering

    1. This looks like a trick question but it really tests whether you separate three different numbers: the estimate, the reported usage, and an exact count. Answering only four characters per token will not survive the follow-ups.
    2. How to break it down: ask what the number is for. Decisions — should I compact before this request — can only use an estimate, because the decision happens before the call. Reconciliation uses the usage the gateway returns, which is accurate but only available afterwards. An exact count is needed only when you bill by token.
    3. Give the shape of the estimator: one coefficient for CJK characters plus one for everything else. The important move is not hardcoding those coefficients — the prompt_tokens in each response tells you what that text really cost, so a handful of (text, real count) samples plus one least-squares fit (two unknowns, two normal equations) recovers the pair. Collect samples along the way; never fire extra requests just to measure.
    4. Be honest about the error: the estimate is fine for prose and clearly optimistic for code, since indentation, brackets, snake_case names and the quotes and commas in JSON all split into more tokens — and code is most of what a coding agent carries. So use the estimate only for decisions with headroom, never to approach the limit. A trigger at seventy percent leaves the remaining thirty to absorb the estimator's own error.
    5. One detail that shows you actually built it: when the samples all have the same script mix (all-ASCII, say), the two coefficients are mathematically inseparable and the determinant approaches zero. You must admit it cannot be solved and fall back to a single per-character coefficient. Forcing a solution typically yields one positive and one negative coefficient — worse than no calibration, because it looks calibrated.
    6. Likely follow-up: why not just install a tokenizer library? Because an exact count is bound to a specific encoding table, and a gateway-agnostic program faces any model from any vendor. You would compute precisely — over someone else's tokenization. When you truly need precision, use the official tokenizer or counting endpoint of the model you are actually calling.

    分析过程 · 先想清楚再作答

    1. 这题看着像脑筋急转弯,其实在考「你分不分得清估算、真实用量、精确计数」。只答一个「四个字符一个 token」就结束的人,接下来一定接不住追问。
    2. 怎么拆:先问「这个数拿来干什么」。做决策(这次要不要先压一压)只能用估算,因为决策发生在请求**之前**;对账要用网关回传的 usage,它准但**是事后的**;真要按 token 出账单才需要精确计数。三种数各有各的时机,混用就会出错。
    3. 估算器的形状要给出来:中日韩字符与其余字符两项系数相加。而关键的一步是**别把系数拍死**——每次请求末尾的用量回传就是「这段文本真实是多少 token」,把若干条(文本, 真实值)当样本解一次最小二乘(两个未知数、两条正规方程)就能回归出这一对系数,样本顺路从每一轮请求收,不额外发请求去测。
    4. 误差要老实说:估算对自然语言够用,**对代码明显偏乐观**——缩进、括号、下划线命名、JSON 里成串的引号逗号都会被切成更多 token。而代码恰恰是 Coding Agent 上下文里最多的东西。所以估算只能用于有余量的决策,绝不能拿它去逼近上限:触发线留在七成,那三成余量里就包含了估算自己的误差。
    5. 还有一个能显出你真写过的细节:样本的中英比例太单一时(比如全是英文),两个系数在数学上分不开,行列式接近 0。这时候必须**承认解不出来**,退回一个统一的每字符系数——硬解常常给出一正一负的荒唐系数,比不校准更糟,因为它看起来像是校准过的。
    6. 可预期的追问:为什么不干脆装一个分词器包?因为精确计数必须绑定具体的编码表,而一个能换网关的程序面对的是任何一家的任何一个模型,分词表根本不唯一——装了包你算得很准,但算的是别家的分词。真要精确就用你所用模型官方的分词器或计数接口。

    Key points

    • Three numbers, three jobs: estimate for decisions before the call, reported usage for reconciliation after, exact counts only for billing
    • The estimator is two terms: one coefficient for CJK characters, one for everything else
    • Fit the coefficients by least squares against the real usage, sampling along the way rather than firing probe requests
    • The estimate is optimistic on code, so use it only where there is headroom and leave the trigger a thirty percent margin
    • When the samples share one script mix the determinant collapses; admit it and fall back to a single coefficient instead of forcing a solve

    答题要点

    • 三个数三种用途:估算做决策(请求之前)、回传用量对账(请求之后)、精确计数才用来结算
    • 估算式是两项相加:中日韩字符数与其余字符数各一个系数
    • 系数用 usage 回传的真实值做一次最小二乘回归,样本顺路收集,不额外发请求去测
    • 误差对代码偏乐观,所以估算只用于有余量的决策,触发线留三成余量吸收误差
    • 样本比例单一时行列式接近 0,必须承认解不出来并退回统一系数,不能硬解
  • When compacting context, which kinds of messages must be kept verbatim, and which are the safest to compress away?做上下文压缩时,哪几类消息必须原样保留?压掉哪些最安全?
    Common in ChinaCommon overseasIntermediate#context-compaction#keep-list

    How to reason about it · think before answering

    1. Two things separate answers here: whether your keep list is a whitelist, and what your compression priority is ordered by. Drop the oldest is the common mistake — age has nothing to do with value.
    2. Keeping first: it must be a whitelist. State which classes survive verbatim, and only what is left over is compressible. Enumerating what may be compressed will eventually miss a newly introduced message type, and the consequence is silent information loss — harder to notice than a trimmed injection, because compaction prints no accounting line.
    3. Four classes, each with a concrete failure mode: the system prompt (drop it and you have a different program); the todo list (the only record of how far the work got, and without it the agent redoes finished steps); unfinished tool calls (fewer tool results than tool calls means the previous turn was interrupted, and a summary line saying it called a tool cannot replace the missing result); and the last few groups (the current task lives there, a summary is a conclusion while the model needs the raw text, and compressing them makes it repeat work it just did).
    4. Then what to compress, with one transferable criterion: order by whether it can be fetched again, not by age. Files are still on disk and commands can be rerun, so long runs of tool round trips are the best target — they are the largest block, their information was already digested by the assistant message right after them, and they can be re-read on demand. Conversely, what the user said, one-shot responses from external systems, and randomly generated ids cannot be recovered, so compress them last.
    5. One structural rule outranks the list: an assistant message with tool calls and all of its tool results form an indivisible group. Split them and you get a message array that requested tools without results, which makes the next request invalid outright. So the first step of compaction is grouping, not picking; then mark the keepers, and only contiguous runs of what remains are compressible, split into segments at user messages.
    6. Likely follow-up: how do you keep the summary itself from making things worse? Fix the headings in the prompt — confirmed facts, changes already made, unfinished work, key paths and commands — and let the model only fill them in. A free-form summary turns already edited that file into discussed how to fix it: the action becomes a topic, and the next turn the model does not know it already acted.

    分析过程 · 先想清楚再作答

    1. 这题的区分度在两处:你的清单是不是**白名单**,以及你压缩的优先级是按什么排的。答「压最旧的」是最常见的错——年龄和价值没有关系。
    2. 先说保留:必须写成白名单,先说清哪几类原样留下、剩下的才是可压区。反过来列举「哪些可以压」,迟早会漏掉一类新出现的消息,而漏掉的后果是**静默丢信息**——比注入被裁剪更难发现,因为压缩不会给你一行报账。
    3. 清单四条,每条都有一个具体的失效现象:系统指令(压掉等于换了个程序);待办清单(那是「我做到哪一步」的唯一记录,压掉之后它会重做已经做完的事);未完成的工具调用(工具结果条数少于调用条数,说明上一轮被打断,摘要里写一句「调用了某个工具」替代不了那条缺失的结果);最近几组(模型正在做的那件事全在这里,摘要是结论而它此刻需要原文,压掉的现象是它开始重复刚做过的事)。
    4. 再说压什么,判据是一句可以迁移的话:**按「能不能重新取」排优先级,不是按新旧排。** 文件还在磁盘上、命令还能再跑一次,所以连续的工具往返最值得压——它占得最多、信息已经被紧随其后的助手消息消化过、而且需要时能重新读一遍。反过来,用户说过的话、外部系统的一次性响应、随机产生的 id 取不回来,最后压。
    5. 还有一条比清单更硬的结构规则:**一条带工具调用的助手消息与它的全部工具结果是一个不可分割的整体。** 拆开会得到「请求了工具却没有结果」的消息数组,下一轮请求直接不合法。所以压缩的第一步不是挑消息,是分组;分完组再标保留项,剩下的连续几组才是可压区,可压区再按用户消息切段。
    6. 可预期的追问:怎么保证摘要本身不添乱?提示里写死小标题(已确认的事实 / 已做过的改动 / 未完成的事情 / 关键路径与命令),让模型只填内容。自由发挥的摘要会把「已经改过某个文件」写成「讨论了如何修复」——**动作变成了话题**,下一轮它就不知道自己动过手了。

    Key points

    • The keep list must be a whitelist: define what survives verbatim, and only the remainder is compressible
    • Four classes always survive: system prompt, todo list, unfinished tool calls, and the last few groups
    • An assistant message with tool calls plus all of its results is indivisible, or the next request becomes invalid
    • Prioritize by whether it can be fetched again, not by age: tool round trips first, user statements last
    • Constrain the summary with fixed headings so actions taken do not degrade into topics discussed

    答题要点

    • 保留清单必须是白名单:先定原样保留的类别,剩下的才是可压区,否则会静默丢信息
    • 四类必留:系统指令、待办清单、未完成的工具调用、最近几组
    • 带工具调用的助手消息与它的全部工具结果不可分割,拆开会让下一轮请求不合法
    • 压缩优先级按「能不能重新取」排,不按新旧排:工具往返先压,用户的话最后压
    • 摘要要用固定小标题约束,防止把「做过的动作」写成「讨论过的话题」
  • How do you prove that a context compaction did not lose critical information?怎么证明一次上下文压缩没有丢掉关键信息?
    Common in ChinaCommon overseasDeep dive#compaction-verification#probes

    How to reason about it · think before answering

    1. This is the question with the most signal, because most candidates stop at let the model judge or eyeball the summary. The word prove is the hinge: you need a reproducible criterion, not a feeling.
    2. How to break it down: say why proof is required. Compaction fails silently — the model never says it lost a fact, it confidently continues with a wrong or invented one, and you infer the loss several turns later from a strange result. Unobservable failures have to be surfaced by an active check.
    3. The technique is probes: before compacting, record a few facts that exist only in the early context — a user saying remember: the build command is … is a natural probe — and look for them afterwards. The criterion is whether the fact is still in the context, not whether the model answers correctly. The latter is a random variable: a correct answer may be a lucky guess and a wrong one may be this turn's noise, and testing a deterministic mechanism with a random variable proves nothing.
    4. Go one step further: a single run proves little, so build a control. Same conversation, same keep list, but replace write a summary with keep only the last few groups and drop the rest. Both save a similar number of tokens, yet the truncating side loses the early fact. That contrast is the actual evidence that compaction differs from truncation, and the justification for the extra summarization request.
    5. Two engineering backstops: the summary must be checkable — every fixed heading present, and if one is missing the whole compaction is abandoned with the context untouched, since a corrupted context is far worse than an uncompacted one and the loss is irreversible once the originals leave the array. And implement it in two phases, collecting every segment summary before rebuilding the array once, so abandoning has a clean path.
    6. Likely follow-up: what about production? Run both layers — the deterministic check above, plus actually asking the probe question. They test different things: the first tests compaction, the second tests the model, and only the first is something you can fix. Above both sits an evaluation set: run the same task before and after compaction and compare pass rates.

    分析过程 · 先想清楚再作答

    1. 这题是今天最有区分度的一道,因为大多数人只能答到「让模型自己判断」或者「人工看一眼摘要」。题眼在「证明」两个字:你要给出一个**可复现的判据**,而不是一种感觉。
    2. 怎么拆:先说清为什么必须证明。压缩的失败是**静默的**——模型不会说「我丢了一条事实」,它会很自信地拿一条错的、或者凭空补的事实继续干活,你要在几轮之后从一个奇怪的结果里倒推。不可观测的失败必须靠主动检查暴露。
    3. 做法是探针:压缩之前记下几条只有早期上下文里才有的事实(用户说过的「记住:构建命令是……」这类话天然就是探针),压缩之后回头查一遍。而**判据是「这条事实还在不在上下文里」,不是「模型答得对不对」**——后者是随机变量:答对可能是猜对的,答错也可能是这一轮的运气。用随机变量去验一个确定的机制,验不出任何东西。
    4. 更进一步:单看一次压缩说明不了什么,要有**对照**。同一段会话、同一份保留清单,把「写摘要」换成「只留最近几组直接扔」,两者省下的 token 差不多,但截断式那一边早期那条事实就没了。这个对照才是「压缩」与「截断」区别的证据,也是你多发一次摘要请求的理由。
    5. 工程上还要有两条兜底:一是摘要必须**可校验**——固定小标题一个都不许少,缺了就整次放弃、上下文一个字都不动(压坏的上下文比没压的糟得多,而且不可逆,原文已经不在数组里了);二是实现上先把全部段落的摘要都拿到手,再一次性重建数组,这样「放弃」才有干净的退路。
    6. 可预期的追问:那真实环境里怎么办?两层一起上——第一层是上面这个确定性检查,第二层是把探针问题真的问一遍模型。两层验的不是同一件事:第一层验压缩,第二层验模型,**而只有第一层是你能修的**。再往上还有一层是第二十天的基准集:把「压缩前后同一个任务的通过率」当指标跑一遍。

    Key points

    • Compaction fails silently, so it must be surfaced by an active check rather than by looking fine
    • Use probes: record facts that exist only in the early context, then look for them after compacting
    • The criterion is whether the fact is still in the context, not whether the model answers it correctly
    • Include a control: the same conversation truncated instead of summarized saves similar tokens but loses the fact
    • The summary must be checkable; if the format fails, abandon the whole compaction and touch nothing

    答题要点

    • 压缩的失败是静默的,必须靠主动检查暴露,不能靠「看起来还行」
    • 探针:压缩前记下只有早期上下文才有的事实,压缩后回头查一遍
    • 判据是「事实还在不在上下文里」,不是「模型答得对不对」——后者是随机变量
    • 要有对照:同一段会话换成截断式压缩,省下的 token 差不多但那条事实丢了
    • 摘要必须可校验,格式不合格就整次放弃、上下文一个字都不动(先全拿到再重建)

Comments