ARTICLE · 1332838
源码视角:Claude Code / Codex 是怎么把上下文压下去的
📚 验证成本转移系列 · 第4/10篇
10 篇讲透一件事:AI 把写代码的成本压到近乎零,但软件工程的总成本没降,只是从「生产」转移到了「验证」
✅ 第1篇:被误读的一年:AI 让开发变快还是变慢,这个问题本身就有问题
✅ 第2篇:瓶颈转移:从「写不出」到「审不过来」
✅ 第3篇:Harness Engineering:2026 年最重要但最少被讨论的词
⏳ 第4篇:源码视角:Claude Code / Codex 是怎么把上下文压下去的
⏳ 第5篇:移动端特写:超大代码库里的 AI Coding,难在哪
⏳ 第6篇:重构的真相:代码一行没动,问题却解决了
⏳ 第7篇:写代码退居其次之后,工程师站在哪
⏳ 第8篇:从 Vibe 到 Agentic:一套方法体系的生长史
⏳ 第9篇:AI 审 AI:当审查本身也交给机器
⏳ 第10篇:收尾:2026 年,一个移动端开发者该带走什么
📰 科技要闻
• 一位 05 后写下个人数码编年史,称只想回应自己当时的感受与记忆,只因那时满腔少年心气、对一切充满好奇、对未来满怀憧憬。
• 「本周看什么」栏目盘点最近值得一看的 9 部作品,其中电影《戴高乐之战》发布本周新预告并确认引进(11月2日)。
• 在洛杉矶气温升至 90 多度、需躲避酷热之际,UV 追踪设备 Gem 让作者重新思考阳光对健康而不只是对外表的影响。
一、灶台就那么大:上下文为什么是核心问题
把上下文窗口想成后厨那张备料台:做一桌席,每道工序都要摊开料碟、锅和写着“盐放过了”的便签,台面就那么大,越往后越没地方。只剩两条路——把早先的碟子推下台,或换张大台、每道工序把碟子重新端一遍。前者丢东西,后者累死人。长任务里天天在发生。
换成工程话:一次跨十几个文件的重构不是一轮对话,是几十轮。每轮请求都要把系统提示、历史来回、工具回显、读过的文件重新打包发过去;一次 cat 或 grep 的回显就是几千 token,吃窗口的是它,不是你那句 prompt。轮次线性堆上去,窗口迟早见底,外面那层调度壳子(Harness)就得拍板:截断,还是滑窗。
截断的代价很隐蔽:模型忘了开头定的接口约定、忘了哪个文件动过,于是重复改、改回旧逻辑、往错处下刀,擦屁股的还是你。全量重喂或换个标称更大的窗口,账单和首 token 延迟一起涨,钱花在反复搬同一批旧内容上;模型对长文中段的注意力本来就衰减,多花钱买回来的仍是个会漏的脑子。
我的判断:大窗口是止痛药,不是解法。拿窗口尺寸当 Harness 的本事夸,等于拿灶台大小夸厨子。
所以决定长任务能不能跑到底的,不是窗口标称多少 K,而是每一步让模型看见什么、扔掉什么、怎么交接。用大窗口硬扛?我不用;把力气花在裁剪和交接上?值得,这是整条链子最硬、也最容易被跳过的一块。裁剪当然也会失真、会剪错,这笔账后面算。本篇要拆的两种策略,说到底都在答同一件事:怎么让台面上只留当下要用的那几样。
策略 A:流水线式,每段只带够用的上下文
把它想成中央厨房的切配台。洗菜的只见要洗的菜,切配的只拿到洗好的菜加一张配菜单,炒菜的只接一配料碗——没人把整冰箱堆到自己台面上。让这条线不断掉的从来不是记忆,是那张单和那只碗:几克、什么刀口、送哪个灶,字段写死,下一道不必猜上一道当时在想什么。流水线式上下文就是这意思:不指望模型记住全程,只保证每段交接时把该带的带齐。
落到实现上,先把长任务切成固定阶段——定位、改码、跑测、复盘——每段开工重新组装一份最小上下文,只放本段输入;段末不传对话历史,传一个字段固定的结构化对象。我自己的交接单长这样(示意,不是任何产品的源码):
{ "stage": "patch", "inputs": ["Foo.java:42"], "facts": {"root": "NPE@init"}, "next": "test", "drop": ["chat_history"]}这里 drop 和 facts 一样重要:显式声明丢掉什么,才没人偷偷把历史又塞回来。
我愿意用它,是因为三件事终于可预期:上下文体积有上限、成本算得清、某段崩了只重跑那一段而不是整轮重来。排查尤其省事——你能精确复现模型当时看见了什么,比「整段塞进去祈祷别截断」踏实得多。
但它不该是默认答案。阶段是预先定死的,碰上路径不确定的活(偶现 crash 得在定位和验证之间来回跳)立刻显笨;更麻烦的是交接单本身就是瓶颈,字段没设计到,那条信息永久丢了,后面再聪明也补不回。我的判断很直接:阶段能枚举清楚、形态稳定的批量活——仓库迁移、按模板生成、批量格式化——用它收益最大;探索性任务别硬套,那是策略 B 的地盘。
策略 B|一路累积,到点交一次班
拿值夜班交接理解最顺。上一班八小时里的每一条告警、每一次试探,不可能全口述给下一个人;真交班就一张纸:3 床术后平稳、两点有台定时重启、那条报警是误报别管。拿着它,下一班无缝接上,整晚的监控流水直接归档。摘要式上下文就是这个动作:任务不切段,历史照常往上堆,堆到窗口快满、或刚跨过一个里程碑,把这一大段压成一份能揣着走的短记忆,原稿扔掉,带摘要继续跑。Claude Code 的 compact 是典型——不是清空重来,是让模型写一份「到目前为止」交给下一个自己。
我愿意为它的灵活买单。探索性调试、啃没文档的遗留代码,事先根本不知道该在哪儿划线,流水线硬切只会把线索切碎;摘要不挑时机,到点就压,反倒兜得住。但我不迷信:摘要是模型自己写的有损压缩,而写摘要的那个「自己」,恰恰不知道后面会用到什么。当时无关的一个变量名、一次跳过的 corner case,压完就没了;更糟的是原稿已丢,想回溯都没底稿。策略 A 丢信息是明着丢,B 是悄悄丢,后者更难查。
所以我给摘要打了个补丁:不许自由发挥写散文,逼它按硬事实字段交班,试探过程、废话、自我纠正这些软叙述一律压掉。
# HANDOFF / 只留硬事实goal: 一句话目标changed: file:line, file:lineblocking: 报错原文,别复述rejected: 方案 + 否决原因next: 下一步一件事粒度上我吃过亏:压太狠,一句「已修复登录态」就让它跑去改缓存,改错地方还理直气壮。摘要里必须留 file:line、报错原文这种能核对的锚点——可被验证的才叫短记忆,否则只是读着通顺的幻觉。至于压缩率,别信现成数字,按自己的任务试出来才算数。
流水线还是错题本:先问你敢不敢赌那一步
把这事想成备考。流水线是按章节复习:每章只带本章的公式清单,章末填一张交接卡交给下一章,写清哪些结论已定、从哪接手。可控、可预算、崩在哪章一眼定位;代价是题目一跨章节,卡上没记的那个知识点就断了。摘要式是一本错题本:一路做一路压进去,翻到哪页都能接着想,不预设顺序;但压缩有损——你当时为什么选错、草稿上那步被划掉的试算,最后往往只剩一个正确答案。
落到 Harness,差别不在省不省 token,在输入可不可预期。流水线每段喂什么、花多少、失败落在哪段都能写死、能断言;需求一改切分就得跟着改,这才是它"灵活度低"的来源,不是笨。摘要不预设任务形状,长任务能一直滚;但 handoff 等于让模型给自己的历史写读书笔记——漏一个约束,后面全建立在缺了它的历史上,越往后越难对账。失真会累积,不是一次性事故。
想给"摘要失真有多常见"找量化结论:本篇要点未指定 arXiv 编号,未能在 arXiv 核到该编号,我不编数字撑场面。按工程经验,选择就三行:
我的判断:别把摘要当默认。handoff 没有第二份原始记录可校验,写错了甚至不知道从哪步开始错;那张笨交接卡至少能断言、能回滚。上下文工程里"可验证"比"省 token"值钱。真混着用,只压过程、别压约束——这是我会守的线。
📱 落到 Android:按改动文件裁上下文
把整个 module 塞进上下文,等于出差把整个衣柜拖上飞机:行李箱就那么大(窗口),多带的每件都是超重费(token)。我按「这次要办的事」收拾——改动的文件是主角,带全文;它 import 的类算配件,只带一跳;配件怎么用,带张接口清单(类签名)就够,函数体那点走线没必要拆开。
落到代码就是个裁剪器:吃 git diff 吐出来的改动文件,吐一个拼好的上下文串。三步,骨架如下,解析细节我省略了。
// skeleton: changed + deps + sigs// ... resolve() / decl omittedclass CtxTrimmer( private val proj: Project) { fun build( changed: List<KtFile> ): String { val sb = StringBuilder() // 1. changed: full text changed.forEach { sb.append(it.text) } // 2. direct deps: one hop val deps = changed .flatMap { it.imports } .map(proj::resolve) .distinct() // 3. deps: signatures only deps.forEach { val s = signature(it) sb.append(s) } return sb.toString() } private fun signature( c: KtClass ): String { // header + method decls val decls = c.methods .joinToString("\n") { it.decl } return c.header + decls }}我的判断:别当银弹。裁到只剩签名,AI 看得见fun pay(),看不见里面怎么扣款,碰上要改方法内部的活就会改错地方——粒度的账就在这儿:剪太狠省了 token,把判断依据也一起剪没了。我的做法是签名留一跳、顶多两跳,再往外整段换摘要。但论「省 token 又不丢主线」,它比无脑塞整个 module 强得多,是我实测收益最直接的一处改动。
踩坑:摘要会失真,裁过头会改错地方
整理错题本这事我干过:把一道压轴题的解法浓缩成一句「用韦达定理」,当时觉得够狠够省。期末再翻,四个字还在,可为什么设而不求、哪一步最容易翻车,全想不起来了。handoff 摘要就是这种有损压缩——它留得住「做过什么」,压掉的偏偏是「为什么这么改」。下一个 agent 拿着结论没有理由,最省事的做法就是照结论再推一遍,于是把当初特意绕开的 deprecated API 又请了回来。我吃过这亏,现在只把摘要当省 token 的便签,绝不当事实用。
裁剪是另一把刀。为了让窗口小一点,把「看着无关」的基类、接口、资源文件一并砍掉,AI 少读了 8k token,转头在父类里本该复用的方法上手写了一遍。这类错最阴:能编译、读着还挺顺,review 一眼扫过去看不出来,等跑起来才发现改在了错地方。
粒度怎么定?我不拿行数或 token 数当尺子,卡「一跳依赖」:当前改动文件显式 import 到的工程内源码,原样留、不摘要;二阶以外才压成类签名。命不中就当没这一跳,宁可漏给也别递归乱抓。三个入参依次是改动文件、java 根目录、工程包名:
import refrom pathlib import Pathdef one_hop_deps(chg, root, pkg): """一跳依赖:只跟当前文件的显式 import。""" hits = set() pat = r'^import\s+([\w.*]+)' txt = Path(chg).read_text() fs = re.findall(pat, txt, re.M) for fqn in fs: if not fqn.startswith(pkg): continue if fqn.endswith('.*'): continue rel = fqn.replace('.', '/') f = Path(root, rel + '.kt') if f.exists(): hits.add(str(f)) return hits值不值得?值得,但有条线:能重新读回原文的,随便压;压了就再也还原不回来的决策依据——某次为何绕开某 API、某个线上 bug 的成因——我宁可多烧几千 token 原样揣着。我的判断是宁可裁保守点,也别让 AI 在错地方写出「看着很对」的代码:后者排查起来的成本,比省下的那点上下文贵一个数量级。
收尾:上下文工程是 Harness 里最值钱的一块
出远门收拾行李,箱子就那么大。新手比的是谁箱子大,老手比的是带什么、扔什么——同样 20 寸,有人三天够用还走得轻快,有人塞满用不上的东西,到地方翻箱倒柜找不着充电线。长任务的上下文窗口就是这只箱子:决定你能不能走到终点的,从来不是窗口标称多少 K,而是每一轮往里装什么、到点丢什么、怎么把有用的交到下一轮手上。
把这个系列往前翻,模型能力趋同之后差距只能从工程层找(第 3 篇说的 Harness),而工程层里最不显眼的恰恰是这块。它不像工具调用、不像跑测试那样有个「看得见的功能」,改好了没人夸,改坏了却让后面每一步都多付代价:窗口撑爆、账单往上走、改错地方还理直气壮。前面拆的流水线、handoff 摘要、一跳裁剪,说到底都在答同一件事——让模型每一步只看见该看的。
我的判断:这是 Harness 里最容易被低估、但杠杆最长的一块。换 prompt 技巧常常只对一类任务生效,裁剪和交接策略改对一次,之后每一轮请求都省、每一次改动都更准,收益是复利式往后滚的。也别把它神化成「求一个最优压缩率」:我不追那个数字,我要的是喂进去的东西可预期、能断言、能回溯——可验证比省 token 值钱,这是整篇我守的线。
落到我们这行尤其如此。Android 一次改动往往跨好几个文件,还得编译、装真机才验得动,上下文喂错一次的返工成本比后端高一截。所以在「验证成本转移」这条链上,我会先把上下文工程补起来:它不产出功能,却决定了前面所有收益能不能兑现。















































































