编码助手上下文管理:分层预算、检索压缩与注入实践
1. 上下文管理到底在管什么:一次答非所问暴露的四个层次
codebuddy 这类编码助手的上下文管理机制,说白了就是解决一个很朴素的问题:模型在某一刻“眼前”到底能看到哪些信息,以及这些信息以什么顺序、什么密度摆到它面前。很多人第一次接触这个概念,会把它等同于“聊天记录保存得好不好”,实际动手用一段时间就会发现,聊天记录只是最表层的一层,真正决定输出质量的,是项目结构、检索结果、工具返回值和历史对话这四类信息怎么被筛选、折叠、排序,最后塞进一个有限的窗口里。
我印象很深的一次,是让助手把一个工具方法从同步改成异步。它很利索地改了调用处的await,却把方法定义里的async漏了,跑起来直接报语法错误。单看对话历史,上下文是完整的,因为我上一句刚说过“把这个方法改成异步”;但它拿到的代码片段是检索出来的旧版本,方法定义所在文件根本没被召回到窗口里。这件事让我彻底改变了对上下文管理的理解:它不是“记住多少”的问题,而是“在正确的时刻拿到正确的那一块”的问题。窗口再大,拿错了块,照样白搭。
所以这篇内容想聊的,是一个稍微偏工程视角的拆解。适合三类人看:一是天天用编码助手、但总觉得它“时灵时不灵”的一线开发者;二是团队里负责搭内部编码规范、想把助手调教得更贴合项目的人;三是对这类工具内部机制好奇、想自己动手做点小工具的技术爱好者。我不打算把它写成产品说明书,而是按“它为什么这么设计、我实际怎么调、踩过哪些坑”的顺序往下走,能抄的配置直接给,能算的账直接算给你看。
先给一个整体判断:上下文管理机制的上限,决定了编码助手的能力上限。模型本身的推理能力是底座,但底座之上能发挥几成,几乎全看上下文喂得准不准。一个 70 分水平的模型配上精细的上下文管理,在具体项目里的表现,经常能压过一个 90 分模型配粗糙上下文。这个结论不是拍脑袋,后面几节拆完链路,你自己就能验证。
1.1 上下文不等于聊天记录:四层信息各有各的脾气
把上下文拆开看,我习惯分成四层,从稳定到易变依次是:全局层、项目层、会话层、即时层。
全局层是系统提示词和工具使用规范,基本不变,占用固定,属于“地基”。项目层是仓库结构、依赖关系、规则文件、索引出来的代码片段,体量大、变化慢,是编码助手区别于普通聊天机器人的关键。会话层是当前这轮对话的历史,包括你说过的话、它做过的工具调用和返回结果,增长最快、最容易失控。即时层是这一次请求临时拼进来的内容,比如你刚选中的那段代码、刚粘贴的报错、刚@的文件。
这四层的更新频率完全不同。全局层可能几周才动一次,项目层随代码提交而变,会话层每说一句话就膨胀,即时层则是一次性的。把它们混在一起管理,结果就是“该稳定的被频繁重算,该丢弃的一直占着位置”。我见过不少人抱怨助手“越聊越傻”,本质就是会话层把项目层和即时层挤出去了——窗口就那么大,历史对话越长,能留给代码检索的空间越小,这不是模型退化,是预算被吃光了。
理解这个分层之后,很多操作就有了依据。比如“什么时候该开新会话”,答案不是聊满多少轮,而是“当会话层开始挤占项目层预算的时候”。再比如“为什么它老是引用过时代码”,大概率是项目层的索引没跟上提交,而不是模型记性差。
1.2 一次跨文件修改,看清上下文是怎么漏的
回到开头那个异步改造的例子,我们把它拆细一点。一次跨文件修改,助手至少需要四类信息同时在场:目标方法的定义、所有调用点、相关的类型声明、项目的异步规范。少任何一块,改出来都有瑕疵。
实际发生的情况是:我选中的是调用方文件的一段代码,即时层就只塞了这一段;会话层里我说了“改成异步”,但没说是哪个方法;项目层检索按相似度召回,调用点文件因为包含方法名,得分高被召回,定义文件因为方法名只出现一次、周围注释多,被切块算法切散了,得分低没进来。于是它拿着“调用点 + 一句模糊指令”就动手了。
这里暴露的是上下文管理里最典型的三个失误:即时层给得太窄、会话层表述太模糊、项目层切块方式不适合代码。对应的修法也很直接:选中范围放大到方法签名,指令里带上方法全名,项目配置里把切块粒度调细、给定义文件加权。这些操作后面第 4 节会给出具体写法。我先想说明的是,上下文漏掉往往不是单一原因,而是几层同时掉链子,排查的时候要一层一层看,别一上来就怪模型。
1.3 为什么这件事值得单独拿出来讲:影响范围比想象中大
上下文管理做得好不好,影响的不只是“一次问答准不准”,它会顺着研发流程往下传。
对个人来说,最直接的是返工率。上下文给错,改出来的代码要重看、重改,来回两三次,时间就没了。对团队来说,是规范落地的一致性。同一个项目,A 同学用助手改出来的风格和 B 同学不一样,往往不是两人水平差,而是两人喂给助手的项目层信息不一样——一个喂了规则文件,一个没喂。再往上,是代码审查成本和安全边界。上下文里混进了不该带的敏感配置、内部地址,助手再把它写进代码或注释,问题就从“不好用”变成“不能用”了。
还有一层容易被忽略的是成本。上下文越长,token 消耗越大,响应越慢。一个团队每天几百次调用,上下文预算管理粗放一点,账单和等待时间都会明显上去。所以这不是一个“调参玩”的话题,它同时牵着质量、一致性、安全和成本四根线,值得认真对待。
2. 整体设计与取舍:为什么不能把整个仓库塞进去
理解了要管什么,接下来要回答“怎么管”。最朴素的想法是:窗口不是有 128K 甚至更大吗,直接把整个仓库塞进去不就行了。我早期也这么想过,实测下来行不通,原因不止一个。
首先是窗口再大也有天花板,而中等规模的项目动辄几十万行,塞不下。其次是塞进去不等于用得上,相关度低的内容会稀释注意力,模型在长上下文里找关键信息的能力并不是线性的,中间段的内容经常被忽略。再次是成本和延迟,每次请求都把全量代码带上,费用和响应时间都不可接受。最后是时效性,代码天天变,全量塞意味着每次都要重新计算,缓存基本失效。
所以主流的做法一定是“选一部分、压一压、排好序”,而不是“全都要”。这背后其实是一个经典的取舍:召回率、精确率、成本三者不可能同时拉满。召回拉高,塞得多,精确率和成本就崩;精确率拉高,漏掉关键信息的风险就上来。上下文管理机制的全部设计,本质上都是在这个三角里找一个适合当前场景的平衡点。
2.1 分层模型:全局、项目、会话、即时各自怎么定位
把这四层落到具体设计上,每一层的管理策略是不一样的,我整理成了一张对照表,方便你对照自己手头的工具看它在哪一层做得薄。
| 层级 | 典型内容 | 变化频率 | 管理策略 | 常见问题 |
|---|---|---|---|---|
| 全局层 | 系统提示、工具规范、安全约束 | 极低 | 固定注入,前缀缓存 | 规则写太啰嗦,白占预算 |
| 项目层 | 仓库结构、依赖图、代码片段、规则文件 | 中 | 增量索引 + 检索召回 | 索引滞后、切块不合理 |
| 会话层 | 历史问答、工具调用与返回 | 高 | 滑动窗口 + 摘要压缩 | 越聊越长,挤占其他层 |
| 即时层 | 选中代码、粘贴报错、@ 的文件 | 一次性 | 原样注入,优先靠前 | 选得太窄或太宽 |
这张表最有用的地方在于定位问题。当你发现助手“忘了前面说过的话”,问题在会话层;当你发现它“不知道项目里有这个工具类”,问题在项目层;当你发现它“连我选的这段代码都改错”,问题在即时层。分层定位比笼统地说“它不行”要高效得多。
我自己的习惯是,每换一个项目,先把项目层摸清楚:索引范围是什么、忽略规则怎么配、规则文件放哪。这三件事定下来,日常使用中八成的问题就不会发生。会话层和即时层是使用时随时调的,项目层则是“一次配好、长期受益”的投资。
2.2 预算怎么切:一笔可以算清楚的账
上下文窗口的预算分配,很多人凭感觉,其实可以算。假设窗口是 128K token,输出预留 16K,那么可用于输入的实际上是 112K 左右。这 112K 怎么分,我给一个我用下来比较稳的起始比例,你可以按项目调:
- 全局层:4K 到 6K,占 4% 到 5%。系统提示和工具规范,写清楚就行,别堆废话。
- 项目层检索:50K 到 60K,占 45% 到 55%。这是大头,代码片段、依赖信息都从这里出。
- 会话层:30K 到 40K,占 27% 到 36%。历史对话和工具结果,需要用摘要压。
- 即时层:8K 到 12K,占 7% 到 11%。选中的代码和临时粘贴内容。
- 安全余量:5K 左右,占 4% 到 5%。留给 token 计数误差和临时插入。
为什么项目层要占将近一半?因为编码任务的核心是“看懂代码”,而代码在项目层。会话层给太多,历史闲聊和失败的工具调用会大量占位;给太少,又容易出现“刚说过就忘”。30K 到 40K 是一个折中区间,超过这个量,摘要机制就该介入了。
这个比例不是死的。做重构类任务,项目层可以再往上加,会话层压到 20K;做多轮调试,会话层要留够,项目层相应减。关键是心里有这笔账,而不是等窗口爆了才发现历史记录把代码挤没了。第 4 节我会给一个更具体的计算示例,把 token 估算的方法也写出来。
2.3 为什么选“压缩加检索”,而不是“无限扩容”
有人会问,既然窗口在变大,为什么不干脆等窗口大到能装下一切。我的判断是,窗口变大解决的是“装得下”,解决不了“找得准”和“算得起”。检索和压缩这两个动作,本质上是在替模型做“预习”,把最相关的信息挑出来、把冗余信息折起来,这件事不会因为窗口变大而失去意义。
压缩的核心是信息密度。一段 200 行的日志,真正有用的可能就三行报错,全塞进去是浪费,摘要成“某文件某行报空指针,调用栈前三层如下”,信息量几乎不损失。检索的核心是相关性排序,把“可能用到”和“大概率用不到”分开,前者进窗口,后者留在索引里等着被二次召回。
这两件事配合起来,才让“小窗口干大项目”成为可能。我见过一些团队试图用超长窗口硬扛,短期看着省事,长期下来成本高、响应慢、关键信息被淹没,最后还是得回到检索加压缩的路子上。上下文管理的工程价值,恰恰在于它承认窗口有限,并且在有限里做优化,而不是假装无限。
3. 核心链路拆解:采集、压缩、检索、注入
这一节把上下文从“进仓库”到“进模型”的完整链路走一遍。我把它拆成四段:采集、压缩、检索、注入。四段对应四个容易出问题的环节,每一段我都会说清楚它在干什么、为什么这么干、哪里容易翻车。
先给一个全景:代码提交后,采集层把文件切块、建索引;检索层根据当前请求召回相关块;压缩层把会话历史和工具结果折叠成更短的表示;注入层把这几路内容按重要性排序,拼成最终 prompt。四段是流水线关系,任何一段掉链子,后面都补不回来。
3.1 采集层:切块方式决定了检索的天花板
采集层做的第一件事是切块,也就是把源文件切成一段一段可检索的单元。切得好不好,直接决定检索能不能命中。按固定行数切是最省事的做法,但代码不是散文,一个函数被从中间切开,检索出来就是半截,模型看到半截函数,改出来的东西大概率也是半截。
我比较推荐按语法结构切:函数、类、方法各成一块,块内保留完整签名和必要注释,块与块之间留少量重叠。重叠的作用是防止“调用点在 A 块末尾、定义在 B 块开头”这类跨块信息丢失。重叠行数不用多,5 到 10 行就够,太多会显著增加索引体积。
切块还要考虑文件类型差异。业务代码按函数切合适,配置文件按顶层键切更合适,文档类按标题层级切。一刀切的做法在混合仓库里会很难受。另外,忽略规则要配好,构建产物、依赖目录、大体积二进制、自动生成的代码,这些不该进索引。索引里混进几万行生成代码,检索质量会被明显拉低,这是我自己踩过的坑,后面第 5 节会细说。
采集层还有一件事是增量更新。全量重建索引在中小项目上还能忍,大项目上每次改一行就重建,时间全耗在这上面。增量更新的关键是记录文件指纹,只重算变化的文件,同时把依赖它的文件标记为待更新。这一块做得好不好,直接决定“改了代码之后助手多久能感知到”。
3.2 压缩层:把会话历史折叠成“交接文档”
会话层是最容易膨胀的一层。一轮问答、几次工具调用,token 就上去了,而且里面大量是重复和噪声:同一段报错被贴了三遍,一次失败的命令返回了满屏输出,助手自己解释了一大段其实没用的推理。压缩层要做的,就是把这些折叠成一份“交接文档”。
我理解的压缩有几个层次,从轻到重:去重、截断、摘要、结构化状态。去重最好理解,重复的报错和文件内容只留一份。截断针对长输出,比如命令返回 500 行,只保留头尾和匹配关键词的行。摘要是把一段对话压成几句话,保留结论和待办。结构化状态则更彻底,把“当前改到哪个文件、哪些函数已改、哪些还没改、下一步做什么”抽成一个小结构体,历史对话本身可以大量丢弃。
这四层里,结构化状态是最有价值的,也是最容易被忽略的。我的习惯是,在处理一个多步骤任务时,每隔一段就要求助手输出一份简短的进度清单:已完成、进行中、待办、已知约束。这份清单本身很短,但它是压缩后的“骨架”,后面即使历史被裁掉大半,靠这份清单也能接上。这跟工地交接班要写交接记录是一个道理,人走了,记录还在。
压缩也有风险,主要是丢关键约束。比如“这个方法不能改签名,有外部调用”,这种一句话的约束如果被摘要漏掉,后面改出来就是破坏性的。所以压缩策略里要有一条:用户显式给出的约束、否定指令、边界条件,不参与摘要,原样保留。这一条我建议直接写进项目规则文件里,让助手自己遵守。
3.3 检索层:关键词和语义各管一段
检索层要解决的问题是:当前这个请求,该从索引里捞出哪些块。主流做法是两路召回再融合:关键词召回和语义召回。
关键词召回靠的是精确匹配,方法名、类名、配置键、错误码,这些词一搜一个准,尤其适合代码。缺点是换个说法就搜不到,比如你问“登录校验在哪”,代码里写的是authenticate,纯关键词就可能漏。语义召回靠向量相似度,能处理“说法不同但意思相近”的情况,缺点是容易召回“看起来相关其实没用”的块,代码里大量结构相似的函数,语义上很容易混淆。
我的经验是,代码场景里关键词召回的权重应该更高,因为代码的命名本身就有很强的指示性,方法名往往就是最好的关键词。语义召回作为补充,用来兜住“你描述得比较口语化”的情况。两路结果的融合,简单的加权打分就够用,不必上太复杂的模型。
还有一个提升召回质量的技巧是查询改写。用户的原话往往很口语,比如“这个接口怎么老超时”,直接拿去做检索,命中率一般。可以先让模型把它改写成几个检索词:超时、timeout、重试、连接池、超时配置。用改写后的词去召回,效果会好不少。这个动作成本很低,但收益明显,我在好几个项目里都加上了。
检索层最怕的是索引滞后。代码已经改了,索引还是旧的,召回出来的是历史版本,模型基于旧代码改新代码,错误非常隐蔽。判断方法很简单:看它引用的代码片段和你本地文件是否一致。不一致就手动触发一次索引更新,或者检查增量更新是不是卡住了。
3.4 注入层:顺序和位置比你想的重要
检索出一堆内容之后,怎么摆进 prompt,是注入层的事。这里有个反直觉的点:内容不是塞进去就行,位置很关键。长上下文里,模型对开头和结尾的关注度明显高于中间,所以最重要的信息要放在两头。
我一般的排法是:系统规范和硬约束放最前,当前任务描述和即时层内容紧随其后,中间放检索到的代码块,靠后的位置放会话摘要和待办清单。这样模型一进来先看到“要干什么、有什么限制”,中间查代码,最后看“干到哪了”。如果任务特别复杂,我会把最关键的那段代码再在末尾重复一次,成本不高,命中率提升明显。
注入层还有一个细节是标注来源。每个代码块前面带上文件路径和行号范围,模型引用的时候就有据可依,你自己核对也方便。没有来源标注的代码块,模型很容易张冠李戴,把 A 文件的实现说成 B 文件的。这个习惯我从早期就养成了,后来发现它还有一个好处:出问题时能快速定位是哪个文件召错了。
最后是顺序稳定性。相同的内容,每次注入顺序都变,前缀缓存就用不上,成本和延迟都会上去。所以固定部分的顺序要尽量稳定,变化的部分往后放。这是工程细节,但对日常体验影响不小。
4. 实操配置:把上下文预算和索引范围捏在手里
前面讲的是机制,这一节讲怎么落地。不同工具的具体配置项名称不一样,但思路是通的:划定索引范围、设定预算、管理会话、手工干预。我下面给的配置样例是示意性的,字段名按你手头工具的实际文档调整,逻辑可以直接搬。
先说明一点,这些配置不是一次配完就完事。项目在变,任务类型在变,配置也要跟着调。我的做法是每个季度回头看一次,看看忽略规则是不是漏了新生成的目录,预算比例是不是还适合当前的任务结构。
4.1 索引范围:先把不该看的挡在外面
索引范围配置的核心是“白名单加黑名单”。白名单定大方向,黑名单兜底。下面是一份我常用的示意配置:
index: include: - "src/**" - "lib/**" - "config/*.yaml" exclude: - "**/node_modules/**" - "**/dist/**" - "**/build/**" - "**/*.min.js" - "**/generated/**" - "**/*.lock" max_file_size_kb: 512 chunk: strategy: syntax max_lines: 120 overlap_lines: 8几个参数值得展开说。max_file_size_kb设 512 是个经验值,超过这个体积的单个文件,要么是生成代码,要么是数据文件,进索引收益低、噪音大。max_lines设 120 是让一个块大致覆盖一个中等规模的函数,太大检索不精确,太小上下文碎片化。overlap_lines设 8 是为了跨块信息不丢,前面说过原因。
黑名单里我特意加了generated和lock。生成代码进索引,会把真正手写的代码挤下去;锁文件动辄上万行,纯属浪费。这两个是我早期没排除、后来被坑过才加上的。
配好之后要做一次验证:随便挑一个业务方法名去搜,看能不能召回定义和调用点。召回不全,说明切块或范围有问题;召回一堆无关内容,说明黑名单不够。这个验证花不了几分钟,但能省掉后面很多莫名其妙的错误。
4.2 预算参数:按任务类型切换档位
预算配置我习惯分档,不同任务用不同档位,比一套参数打天下要稳。
| 任务类型 | 项目层预算 | 会话层预算 | 即时层预算 | 说明 |
|---|---|---|---|---|
| 单文件小改 | 30K | 20K | 12K | 即时层给足,选中的代码要完整 |
| 跨文件重构 | 60K | 25K | 10K | 项目层拉满,依赖和调用点都要 |
| 多轮调试 | 40K | 45K | 8K | 会话层留够,报错和尝试过程要连着 |
| 代码审查 | 55K | 20K | 10K | 规则文件和目标代码为主 |
这张表的关键不是数字,而是任务类型和预算结构的对应关系。跨文件重构最怕“看不到调用点”,所以项目层给到 60K;多轮调试最怕“忘了试过什么”,所以会话层给到 45K。用错档位,表现就会明显下降——用单文件档去做重构,它改完这处漏那处;用调试档去做重构,历史对话占着位置,代码反而进不来。
切换档位这件事,手动切比自动判断可靠。自动判断听起来聪明,但判断错了你很难感知,手动切虽然多一步操作,至少心里有数。
4.3 会话管理:什么时候该开新会话
“什么时候开新会话”是使用频率最高的一个决策。我的判断标准有三条,满足任意一条就开新的:
- 任务切换。上一个任务已经收尾,新任务和它没有代码依赖,直接开新会话。旧会话里的历史对新任务全是噪声。
- 会话层开始挤占项目层。表现是助手开始引用更少的代码、更多地在“回忆”你说过的话。这时候继续聊,质量只会往下走。
- 出现方向性错误且纠正两次无效。同一件事解释两遍它还理解偏,说明上下文里有什么东西在误导它,与其继续掰,不如开新的、把关键信息重新干净地给一遍。
开新会话之前,有一个动作我强烈建议做:让助手先输出一份交接摘要,包括当前进度、已改文件、待办事项、已知约束。把这份摘要复制出来,开新会话后第一句就贴进去。这一步花不了一分钟,但能让新会话直接接上,不用从头讲一遍背景。这就是前面说的“结构化状态”的实际用法。
反过来,有些情况不该开新会话。比如正在追一个偶发 bug,前面十几次失败的尝试本身就是重要信息,开新的等于把排查过程清零。这种时候应该做的是压缩,而不是清空——让它把失败尝试归纳成“已排除的假设”,保留结论,丢掉过程。
4.4 手工干预:最被低估的一项能力
自动检索再聪明,也有召不准的时候。这时候手工干预的价值就出来了:明确指定看哪些文件、明确说不要看哪些、明确给出约束。
我常用的几种说法,效果比含糊描述好很多:
- “只看
src/service/order.ts和它的直接调用方,不要看测试文件。”——限定范围,减少噪声。 - “改之前先把相关函数的签名列出来,我确认后再动手。”——强制它先把上下文摊开,避免拿着半截信息就改。
- “以下三条是硬约束,不参与摘要:不改公开方法签名、不引入新依赖、错误码保持不变。”——把关键约束钉死。
- “先给我一份当前理解的要点清单,我看看有没有偏。”——在动手前校准上下文理解。
这几句话背后的逻辑是一样的:把隐式的上下文变成显式的。自动机制靠猜,手工干预靠说,说清楚了,猜错的概率就低。很多人觉得这样操作“麻烦”,但比起改错了再返工,前面多说一句其实更省时间。
还有一个技巧是用文件路径代替描述。说“订单服务那个类”不如说“src/service/order.ts里的OrderService”。路径和类名是精确的,描述是模糊的,精确信息进上下文,检索和定位都会准很多。
4.5 一次预算计算的实际演示
光说比例有点虚,拿一个具体场景算一遍。假设窗口 128K,输出预留 16K,可用输入 112K,任务是一次跨文件重构。
先估项目层需要多少。目标方法定义约 150 行,两个调用点各 80 行,相关类型声明 100 行,再加一点依赖信息,合计约 450 行。按代码平均每行 8 到 12 token 估,取 10,大约 4500 token。这是“必须看到”的部分。但检索不会这么精准,通常会多召回一些周边代码,按 3 倍冗余估,项目层实际占用约 13.5K。
再估会话层。前面几轮沟通加上一次失败的尝试,原始 token 可能在 20K 左右。做一次摘要压缩,压到 6K 到 8K,保留结论和约束。即时层这次是选中的方法签名加一段报错,约 2K。
加起来:全局层 5K + 项目层 13.5K + 会话层 7K + 即时层 2K = 27.5K,离 112K 还有很大余量。余量怎么办?两个用法:一是扩大检索范围,把间接依赖也召回,跨文件重构多看点没坏处;二是保留给工具调用,重构过程中要跑测试、看编译错误,这些返回结果会占不少空间。
这个例子说明一件事:大多数日常任务的上下文占用,远没有到窗口上限,真正的瓶颈不是“装不下”,而是“装的东西不对”。所以优化重点应该放在检索精度和注入顺序上,而不是一味追求更大窗口。反过来,如果你发现某个任务确实把窗口撑满了,那大概率是会话层没压缩,或者检索召回了大量无关内容,该去查这两处。
5. 问题排查实录:八种典型症状与对应处理
用得久了,遇到的问题会收敛成几类。我把它们整理成一张速查表,按“症状、可能原因、怎么查、怎么处理”四列排。这张表我自己贴在便签上,遇到问题先对照一遍,能省不少瞎试的时间。
| 症状 | 可能原因 | 排查方法 | 处理方式 |
|---|---|---|---|
| 答非所问,扯到无关文件 | 检索召回了相似但无关的代码 | 看它引用的文件路径对不对 | 缩小检索范围,加否定指令 |
| 忘了前面说过的约束 | 会话层被压缩或裁剪掉了 | 回看历史,确认约束是否还在 | 把约束写进规则文件,或重新声明 |
| 引用过时代码 | 索引滞后,增量更新没生效 | 对比引用片段和本地文件 | 手动触发重建索引 |
| 改一处漏一处 | 调用点没被召回 | 搜方法名,看调用点是否在索引里 | 扩大检索范围,手动 @ 调用文件 |
| 越聊越慢、越聊越贵 | 会话层无限膨胀 | 看每轮 token 消耗趋势 | 压缩历史,或开新会话 |
| 输出风格和项目不一致 | 项目规则文件没被注入 | 检查规则文件是否在索引范围内 | 把规则文件放到固定位置并纳入注入 |
| 长输出把窗口撑爆 | 工具返回结果没截断 | 看单次工具返回的行数 | 配置输出截断,只留头尾和关键词行 |
| 同一错误反复犯 | 失败尝试占满会话层,挤掉关键信息 | 看历史里失败尝试的占比 | 归纳成“已排除假设”,清掉过程细节 |
这张表里,我个人遇到最多的是第一行和第四行,本质是同一个问题:召回不准。下面挑几个展开说,讲讲具体的排查动作。
5.1 答非所问:先看它引用了什么,再决定怎么改
助手答非所问的时候,很多人的第一反应是重新描述一遍问题。这个动作有时候有用,但更高效的做法是先看它引用了哪些文件。大多数编码助手在回答里会标注来源,没标注的,从它提到的类名、方法名也能反推。
如果引用的文件明显不相关,问题在检索层,处理方式是缩小范围:明确说“只看某目录”,或者把不相关的目录加进忽略规则。如果引用的文件相关但版本旧,问题在索引层,处理方式是重建索引。如果引用的文件对、但理解偏了,问题在注入层或会话层,处理方式是重新组织指令,把关键约束提前。
这个顺序很重要:先定位是哪一层的问题,再动手。上来就重写 prompt,有时候碰巧好了,但你不知道 why,下次还会犯。定位到层,处理才有可复现性。
5.2 约束被遗忘:把“不参与摘要”的规矩立起来
“这个方法不能改签名”说了三遍还是被改,这种情况我遇到过不止一次。根因通常是会话层压缩时,这句约束被当成普通对话摘要掉了,或者摘得面目全非。
治标的办法是重新声明,并且加重语气,明确说“这是硬约束”。治本的办法是把它写进项目规则文件,让它在项目层固定注入,不参与会话压缩。规则文件里我一般会放几类内容:代码风格、禁改清单、依赖约束、错误处理约定。这些东西不随对话变化,放在项目层最合适。
还有一个细节:约束要写得可判定。“注意兼容性”这种话等于没说,“公开方法签名不得变更、已有错误码不得修改”才是可判定的。可判定的约束,模型才能遵守,你也能验证。
5.3 索引滞后:一个很容易被忽略的隐形坑
索引滞后是最隐蔽的问题之一。表现是助手说得头头是道,但引用的代码和你本地的不一致,改出来的东西编译不过,你还以为是它水平不行。
排查方法很土但有效:挑一段它引用的代码,去本地文件里搜一下。对不上,就是索引旧了。处理方式分两种:如果工具支持手动重建,直接重建;如果是增量更新卡住,看看是不是某个大文件一直在变、拖累了整条更新链路。
预防上,我建议把索引更新和代码提交挂钩,提交后自动触发。另外,切换分支之后要特别留意,分支切换会大范围改文件,增量更新如果处理不好,很容易出现一半新一半旧的状态。这种情况我一般直接全量重建,虽然慢一点,但省心。
5.4 成本失控:从 token 趋势里找原因
成本问题不是小事,尤其是团队共用额度的时候。表现是响应越来越慢,账单越来越高,但单看每次请求,好像也没干什么大事。
排查方法是看每轮对话的 token 消耗趋势。正常情况应该在一个区间内波动,如果呈单调上升,基本可以确定是会话层在膨胀。再细看,是历史对话占得多,还是工具返回占得多。前者靠摘要压缩,后者靠输出截断。
截断策略要注意别截掉关键信息。我的做法是保留头部若干行、尾部若干行,中间如果有关键词(比如 error、fail、exception)就保留匹配行,其余丢掉。这样既控制了长度,又不至于把报错信息截没。
5.5 风格不一致:规则文件的位置比内容还重要
同一个项目,不同人用助手改出来的代码风格不一样,这个问题我在几个团队里都见过。根因往往不是规则文件写得不好,而是规则文件没被稳定注入。
规则文件要放在固定路径,并且确保它在索引和注入范围内。有些工具支持自动读取特定文件名的规则文件,这种最省事;不支持的,就得靠每次会话开头手动带上,或者写进全局配置。位置固定了,注入才稳定,风格才一致。
内容上,规则文件别写太长。我见过把整个编码规范几百条全塞进去的,结果占了大量预算,真正关键的几条反而被淹没。挑十几条最常违反的、最影响一致性的写进去,效果比堆几百条好。
6. 踩坑之后的几条经验
写到这里,机制和操作都讲得差不多了。最后分享几条我个人踩坑之后沉淀下来的经验,不是教程,就是一些用久了形成的习惯,供你参考。
第一条,把上下文当工作台,别当仓库。工作台上只放当前要用的工具和材料,用完就收。仓库里什么都有,但你不能把仓库搬到工作台上干活。会话层就是工作台,项目索引是仓库,即时层是你手边正在用的那几样。分清楚这个关系,很多操作就顺了:该清的时候清,该查的时候查,别什么都往台面上堆。
第二条,重要的事情说三遍不如写一遍。约束、规范、禁改清单,说在对话里会被压缩掉,写进规则文件里才能稳定生效。我现在遇到“说了它不听”的情况,第一反应不是换个说法再说,而是想想这条能不能落到规则文件里。能落就落,落不了的,才靠对话强调。
第三条,任务边界要自己划,别指望它猜。助手不会知道你只想改这一个文件、不想动测试、不想碰配置。这些边界你要主动说,而且要用可判定的方式说。划边界这件事,花十秒钟,能省十分钟返工。
第四条,索引和会话都要定期“体检”。不用很频繁,每周花几分钟看看:索引覆盖是不是还合理,有没有新目录该排除;会话压缩是不是还在正常工作,有没有出现越压越乱。这两个地方出问题都很隐蔽,定期看一眼,比出了问题再查要省事。
第五条,跨文件任务先让它“画地图”。改动涉及三个以上文件的时候,我习惯先让它列出受影响的文件和函数,我确认之后再动手。这一步能暴露很多上下文缺失——它列不全,说明检索没到位;它列错了,说明召回了无关内容。地图画对了,后面的改动基本不会跑偏。
第六条,别迷信窗口大小。窗口从 32K 涨到 128K 再往上,体验的提升并不是线性的。真正决定体验的,还是那几件事:检索准不准、压缩狠不狠、注入顺序对不对、约束稳不稳。把这几件事做好,中小窗口也能干得漂亮;做不好,窗口再大也是浪费。
如果后续要扩展,我觉得有两个方向值得试:一是把团队里反复出现的上下文配置沉淀成模板,新项目直接套;二是把“交接摘要”这个动作固化下来,变成任务收尾的标准步骤。这两个都不复杂,但坚持做下去,日常使用中的摩擦会小很多。