上下文窗口越大,代码质量反而越差
上周用 Cursor 0.42.3 重构一个老项目的订单模块,我干了件蠢事——把整个 3000 行的 order-service.ts 直接拖进 Composer,让它帮我整理。结果它吭哧吭哧生成完,我一看,中间 500 行的退款状态机逻辑没了。直接没了。
也不是注释掉了,就是凭空消失了。
我愣了半天才反应过来:它觉得那是冗余代码,给我“优化”掉了。
靠。
最近朋友圈和即刻上全是 AI 编程的内容,各种“百万 token 上下文”的宣传,Claude 3.5 Sonnet 出了 200K,Gemini 1.5 Pro 直接干到 1M。但说实话,我去年踩过的坑告诉我一个事:窗口再大,你不做结构化分块,出来的代码就是一坨。不是比喻,是真的能跑但没法看的那种。
先说个数据
去年 12 月我们组内部做了个实验,挺扎心的。
同一个需求文档,三组人用不同的方式喂给 AI 编程工具(当时用的是 Claude 3.5 Sonnet 20241022):
- A 组:直接把整个项目代码 + 需求文档一股脑塞进去,靠模型自己的长上下文消化。大概塞了 12000 行。
- B 组:手动挑相关文件,控制在 5000 行以内。
- C 组:用结构化分块策略,按模块-文件-函数的层级切,每次只喂相关的上下文,大概 1500-2000 行。
结果:A 组产出的代码首次通过率 31%,B 组 47%,C 组直接到 68%。
更离谱的是 A 组出了 3 次 bug——AI 把不同模块的变量名搞混了,生成的代码编译能过、TypeScript 类型检查也不报错,但逻辑完全错误。其中有一个是把 order.refundAmount 和 payment.refundAmount 当成同一个字段用了,线上跑了三天才发现。这种 bug,查起来想死。
长上下文窗口不是银弹,它更像一个巨大的桌子。你把所有东西都摊在上面,找东西反而更慢。
我踩过最惨的坑
2024 年 11 月初,我在做一个电商系统的订单模块重构。那时候刚用上 Claude 的 200K 上下文,心想牛逼了,直接把整个订单模块(大概 8000 行)加上相关的用户模块、支付模块的接口定义全丢进去,让 AI 帮我重构。
第一次生成出来,看着挺像那么回事。跑起来也没报错。TypeScript 编译过了,ESLint 也没告警。
上线两天后,财务那边炸了。
有一批订单的退款金额算错了。用户买了两件商品,用了一张满减券,结果退款的时候按照商品原价退了,没扣掉优惠券分摊的部分。大概多退了 3 万 2 千多块钱。
排查了一整天。最后发现是 AI 在处理长上下文时,把旧版退款逻辑(2023 年的版本,按商品原价退)和新版逻辑(2024 年 3 月改的,按实付金额退)混在一起了。两个版本的代码都在上下文窗口里——一个在文件底部的注释掉的代码块里,一个在正式逻辑里。AI 没有正确判断哪个是当前生效的版本,它“自作主张”取了个中间值。
等等,这里我要更正一下——不是取了中间值,准确说是它在计算退款基数时用了新版逻辑,但在处理优惠券分摊时又跳回了旧版逻辑的 fullRefund 分支。两个版本的代码片段在 embedding 空间里靠得比较近,模型就当成同一套逻辑来理解了。
教训:上下文不是越多越好,噪音太多反而干扰判断。尤其是注释掉的旧代码,必须删干净再喂。
后来我改了策略,每次只喂当前模块的核心文件,加上明确的接口契约作为边界。再也没出过这种问题。
结构化分块到底怎么做
经过这小半年各种项目的折腾,我总结了一套还算实用的分块策略。不保证是最优解,但起码在我自己的项目里能用。分享出来大家批判。
第一层:项目级分块
别把整个项目的文件树丢给 AI。这玩意儿看到 200 个文件会懵的。
我现在的做法是维护一个 project-context.md,大概 200-300 行,放在项目根目录。包含这些东西:
- 项目技术栈和整体架构(5-10 行就够了)
- 目录结构和每个目录的职责(用树形结构,每个目录一行说明)
- 核心数据流(用文字描述,别画图,AI 看不懂 Mermaid 的细节)
- 关键约束条件(比如“禁止直接操作数据库,必须走 Repository 层”、“所有金额计算用 `decimal.js`,禁止用浮点数”)
这个文件相当于给 AI 的“项目说明书”,每次对话都带上。实测能让 AI 的上下文理解准确率提升 30% 左右——这是我自己记的,不一定严谨,但体感很明显。
第二层:模块级分块
这个是最关键的一层。我的规则很粗暴:一个模块的上下文不超过 3000 行。
为什么是 3000?不是拍脑袋。
我们组用不同大小的上下文做过对比测试,数据大概是这样:
- 1000 行以内:AI 理解准确率 85%+,但经常缺少跨模块依赖信息
- 1000-3000 行:准确率 75-85%,依赖信息基本完整
- 3000-5000 行:准确率降到 60-70%,开始出现幻觉,变量名混淆
- 5000 行以上:准确率断崖式下跌,而且生成的代码风格开始不一致——前面用 `async/await`,后面突然冒出 `.then()` 链
具体操作上,我会给每个模块维护一个 module-context.md,包含这个模块的核心接口定义、依赖关系、数据模型。然后实际喂给 AI 的时候,用这个文件 + 当前要修改的 2-3 个核心文件。
举个例子,改订单模块的退款逻辑时,我给 AI 的是:
1. module-context.md(订单模块的接口定义和依赖,大概 400 行)
2. refund-service.ts(退款服务,要改的文件,800 行)
3. payment-adapter.ts(支付适配器的接口定义,只给接口,不给实现,150 行)
总共 1350 行。AI 处理起来很轻松,而且不会乱猜支付模块的内部实现。
第三层:函数级分块
这个是从一个 GitHub 项目学到的思路,好像是叫 code-to-context,Star 不多但思路很对:用 AST 工具把代码文件解析成函数级别的结构化数据。
比如一个 2000 行的 service 文件,我会先用 tree-sitter 写的脚本把它拆成这样的 JSON:
{
"file": "order-service.ts",
"summary": "订单核心业务逻辑",
"functions": [
{
"name": "createOrder",
"signature": "(params: CreateOrderDTO): Promise<Order>",
"summary": "创建订单,包含库存校验、价格计算、优惠券核销",
"dependencies": ["inventoryService.checkStock", "couponService.validate"],
"lines": "45-120"
}
]
}嗯...这个比较复杂,展开说一下。
这个结构化的索引文件大概只有原文件的 1/5 大小,但包含了 AI 需要的核心信息——函数名、签名、依赖关系、功能摘要。实际用的时候,先让 AI 看这个索引,它自己判断需要哪些函数的完整代码,我再按需提供。
这个做法的好处是,AI 的“注意力”被引导到真正相关的代码上,而不是在 2000 行里大海捞针。 我觉得这可能是整套策略里最有用的一步。
一个意外的发现
做这套分块策略的过程中,我发现一个有意思的事情。
分块越细,AI 越敢说“不知道”。
以前我把整个模块丢进去,AI 几乎从来不会说它需要更多信息。它会硬着头皮猜。猜错了也不告诉你。但用了结构化分块后,AI 会主动说“这个函数依赖了 couponService.validate,能给我那个模块的接口定义吗?”
这其实是个好事。AI 的“自信”往往和上下文的信息密度成反比——信息越杂乱,它越容易产生虚假的确定感。据我了解,这跟 transformer 的注意力机制有关,信息噪声大的时候模型倾向于“平滑”处理,反而掩盖了不确定性。
工具推荐
说几个我在用的工具,都是免费的:
- **repomix**:把整个项目打包成一个 AI 友好的文本文件。但我不建议直接用全部内容,而是用它生成项目概览,大概 `--overview-only` 模式。挺好用的。
- **tree-sitter**:做 AST 解析,生成函数级别的结构化索引。支持 TypeScript、Python、Go,覆盖面够用了。
- 我自己写了个小脚本,基于 tree-sitter 自动生成模块的 `module-context.md`,每次 `git commit` 的时候通过 `pre-commit` hook 更新。代码很糙,大概 200 行不到,但能用。回头整理一下放 GitHub,估计得下个月了。
最后说两句
AI 编程现在最大的瓶颈不是模型能力,而是我们怎么组织信息喂给它。
长上下文窗口给了我们一个错觉,以为可以偷懒不用做信息筛选了。但实际上,信息的质量远比数量重要。 这句话我说过很多次了,但每次踩坑都有新的体会。
我现在带的新人,第一件事就是教他们怎么给 AI 准备上下文,而不是怎么用 AI。这个习惯养成了,效率提升比换什么模型都明显。我们组有个实习生,来的时候啥都不会,但上下文组织得特别好,一个月之后产出快赶上两年经验的了。
你们在实际项目里是怎么处理长上下文的?有没有踩过类似的坑?或者有更好的分块策略?评论区聊聊,我最近正好在优化这块的流程,想听听大家的经验。
#AI编程 #长上下文窗口 #结构化分块 #研发效能 #工程实践
Edit:没想到这么多兄弟有同感。评论区有人提到用 RAG(检索增强生成)做代码检索的思路,把代码库向量化之后按语义检索相关片段再喂给 AI,这个方向很有意思。我下周试试用 LanceDB 做本地向量索引,到时候回来更新效果。还有老哥推荐了 aider 的 /map 命令,说能自动做函数级上下文映射,我也去看看。
读者评论 2