
做 AI 代码助手性能优化这块快两年了从最初“能跑就行”的 demo到后来扛住线上真实请求最大的感悟就一句话瓶颈往往不在模型本身而在你如何看待上下文和处理推断过程。很多团队一开始觉得“效果不行就换更大的模型”实测下来发现大模型带来的显存压力和延迟增长反而把产品体验拖垮了。今天这篇就围绕我自己在项目里的重构经历讲讲上下文窗口优化和推断加速这两条主线到底怎么落地哪些做法有效哪些是弯路。先说结论面向代码场景的助手对延迟敏感度极高。用户敲完代码一两秒没反应就开始怀疑工具坏了。所以你的优化目标不是把某个指标刷到好看而是让“从请求发出到有用的代码补全返回”这条路径尽可能短、尽可能稳。要做到这一点上下文怎么管理、KV Cache怎么处理、推断过程怎么加速、缓存怎么复用每一项都得抠细节。1. 性能瓶颈到底出在哪先讲清楚“卡”在哪里1.1 代码助手的延迟构成拆解如果你把一个代码补全请求从发出到返回拆开看通常包含这几段耗时网络传输与网关处理请求排队等待输入 Token 的预填充Prefill阶段输出 Token 的逐字生成Decode阶段结果后处理与回传其中真正大头在模型本身的 Prefill 和 Decode。Prefill 阶段要并行处理整个输入序列计算密集但好在可以批量并行Decode 阶段每个 Token 都要依赖前一个 Token 的结果串行执行这也是为什么长输出的响应时间会近似线性增长。我复盘了线上数据发现一个很典型的场景某个请求携带了约 8000 Token 的上下文代码库片段、历史对话、相关文件模型需要先生成约 300 Token 的补全结果。整个请求的耗时分布里Prefill 占了 70% 以上。原因很直接——上下文越长Prefill 需要处理的矩阵乘法规模越大而且这个请求的 batch 又小GPU 利用率很低。这就是第一个核心认知在单请求逐条推理的模式下长上下文对性能的杀伤力远大于长输出。不少团队把优化重点放在 Decode 的流式输出上反而忽略了 Prefill 阶段那几秒的等待其实用户的体感已经从第一帧就开始计时了。1.2 显存墙、带宽墙与“算力充足但速度很慢”的假象实际调优过程中我还遇到过一个很迷惑的现象GPU 算力看起来完全够用但推断速度就是上不去。用监控工具一看SM 占用率不到 20%显存带宽却接近打满。这是因为 Decode 阶段每个 Token 只需要读取整个模型权重和 KV Cache做一次矩阵向量乘本质上是访存密集而非计算密集。模型越大、上下文越长KV Cache 占用的显存和访问带宽就越高Decode 速度就被卡在带宽上。这也是为什么“堆算力卡”不一定能线性提速。你换了张更强的新卡计算能力翻倍但如果显存带宽没有同步大幅提升Decode 瓶颈依旧存在。所以做优化时别只盯着 FLOPS还要看带宽利用率。另一个隐蔽问题是显存墙。上下文窗口拉长之后KV Cache 会吃掉大量显存。以某个开源代码模型的配置为例它的层数较多、头数较多单个请求 16K 上下文的 KV Cache 就能占据接近 2GB 显存。如果并发请求数一高显存直接撑爆要么 OOM要么触发反复的显存换入换出整体吞吐反而下降。1.3 为什么“直接换大模型”常常是伪优化很多产品经理会提一个需求“效果不好换个更大的模型吧。”但更大模型意味着权重参数量增加、推理耗时变长、KV Cache 占用更多。如果不先做工程侧的优化即使换了模型延迟和成本双双失控体验照样不达标。我自己在项目里有段时间被线上延迟逼急了尝试切换到一个参数规模更大的候选模型结果在相同上下文长度下首 Token 延迟高了一倍以上显存占用暴涨单机并发能力直接减半。后来我做了上下文压缩和缓存优化之后小模型的延迟反而比之前大模型原始状态更低。这里我想强调的是模型能力是一个维度工程效率是另一个维度两者必须同时抓先做工程优化再评估模型升级顺序不能反。2. 上下文窗口优化让模型看该看的而不是看所有2.1 全量塞入的弊端与“上下文瘦身”的大方向初期版本里代码助手的上下文管理很简单把当前文件、相关文件、历史对话、项目说明一股脑全塞进 Prompt。这样做的代价很快显现——请求 Token 数飙升Prefill 变慢而且关键信息被海量无关内容淹没模型生成质量反而不稳定甚至出现“该遵循的约束忘了”的情况。后来我定下一条原则上下文不是越多越好而是越准越好。要做上下文瘦身核心思路是分级和取舍。比如当前编辑文件附近的代码属于“核心上下文”必须完整保留项目里其他相关文件属于“扩展上下文”可以只保留关键函数签名和注释历史对话中较旧的轮次属于“背景上下文”压缩成摘要即可。实际操作中我维护了一个预算机制给核心、扩展、背景三个层级分配不同的 Token 配额比如 2K / 6K / 4K每次构建 Prompt 时按层级填充超出配额的部分降级处理。这个机制上线后上下文平均长度从 8000 多压到 4000 左右Pre-fill 时间明显下降效果还更稳定了。2.2 三种可落地的上下文裁剪策略策略一滑动窗口裁剪适合流式对话和增量代码分析。维护一个按时间或位置排序的窗口窗口内的内容完整保留窗口外的内容逐步丢弃或压缩。比如某次代码修改相关的会话我们只保留最近 20 轮对话和当前改动涉及的代码块再往前的讨论直接折叠成一句摘要。该策略的优点是实现简单、延迟稳定缺点是可能丢失“很久之前提到但依然有效的关键约束”。策略二结构化摘要压缩对要丢弃的内容先用一个轻量模型或规则方法生成结构化摘要把“结论”和“待办”保留下来。这里要注意摘要不能只写一句话最好拆成目标、约束、已做决策、遗留问题四段式。我实验过四段式摘要被后续轮次有效引用的概率远高于自由文本摘要。虽然摘要本身会消耗一点预处理时间但省下的 Prefill 时间足以覆盖成本。策略三检索增强式上下文构建不依赖用户手动指定哪些文件相关而是基于当前光标位置和对话意图从项目代码库中检索最相关的几个片段。这部分可以借助向量化的代码索引也可以用简单的关键词与符号匹配。检索式构建的好处是上下文始终聚焦缺点是第一次建立索引有成本而且检索质量会直接影响效果。建议检索模块做成可以随时关闭的开关方便线上做 Ablation 测试。2.3 KV Cache 的精打细算共享前缀与缓存复用上下文窗口优化不止是减少输入 Token还包括对 KV Cache 的管理。每个请求都会计算 Prompt 对应的 Key 和 Value 矩阵。如果两个请求的前缀高度相似它们的 KV Cache 完全可以复用。代码场景天然适合这么做——同一个项目的请求经常携带相同的“系统提示词项目说明”这些前缀占了几千 Token。我落地了两层 KV Cache 复用第一层固定前缀缓存。将系统提示词、全局项目规范、用户偏好等固定内容拼成一个固定前缀单独计算好 KV 并常驻显存。每次请求只要命中这一前缀直接跳过这一段 Prefill。第二层检索缓存。在固定前缀后加入“相关代码片段”的公共部分比如多个请求会引用同一个核心模块时命中后可以直接续算后面的对话内容。这块的收益非常明显。因为固定前缀在我们场景里往往占整个上下文的 30% 到 50%缓存之后 Prefill 时间平均降了 25% 左右。需要提醒的是KV Cache 极耗显存做缓存时一定要记得设过期策略否则缓存堆积会把显存吃满得不偿失。2.4 一个可参考的上下文构建流程我在实际工程里把上下文构建拆成了四个步骤顺序很重要采集收集当前请求涉及的输入包括当前文件、选中代码、错误信息、历史对话。过滤与分级用规则判断哪些是核心哪些可压缩哪些直接不进入 Prompt。例如无关注释、空行、超长日志都应该被过滤。压缩与摘要对扩展级和历史级内容执行结构化摘要注意保留关键符号名和调用链。组装与预算校验按模板组装成最终 Prompt校验 Token 预算超出部分按优先级丢弃或进一步压缩。这个流程写成一个独立的中间件后不仅优化了延迟调试也方便很多——每次请求都能输出一张“上下文构成卡片”告诉我们哪些内容占了多少 Token。上线之后我发现不少用户会主动配合这个机制把代码拆成更清晰的小函数因为这样上下文裁剪的准确率会更高。3. 推断加速算法选型从投机解码到量化与批处理3.1 投机解码用“小模型先写大模型审核”的思路投机解码Speculative Decoding是我重点尝试的一个方向原理说起来并不复杂用一个轻量的草稿模型快速生成候选 Token 序列再交给主模型进行并行验证验证通过的 Token 直接复用不通过的则回退重来。因为验证过程可以一次处理多个 TokenDecode 阶段原本逐字串行的方式变成了“小步快跑批量确认”整体吞吐能提升不少。落地时有两个关键点。第一草稿模型和主模型的词表必须一致否则需要额外的映射层代价会抵消收益。第二草稿模型的接受率决定了加速效果。如果草稿模型太弱生成的候选大部分被驳回反而浪费算力。经验法则是草稿模型至少要有主模型七成以上的“对话和补全能力”速度还要快三倍以上这个组合才划算。我在某次实验中用主模型和一个参数量小得多的同系列草稿模型搭配输出速度提升了大约 1.8 倍而生成质量在基准测试上没有明显下降。不过要注意投机解码不适合所有场景如果用户请求主要以短输出为主比如简单补全几个 token草稿模型的预生成成本会占不到便宜建议做一次耗时分布统计再决定是否开启。3.2 量化压缩INT8 与 INT4 的取舍之道量化是另一个绕不开的话题。把模型权重从 FP16 压缩到 INT8 或 INT4可以显著减少显存占用同时让访存带宽压力下降解码速度因此变快。但我必须泼一盆冷水代码生成任务对量化精度比一般对话更敏感因为代码里的符号、数字、缩进稍有偏差就可能导致编译失败或语义错误。我的实践经验是把量化级别和上下文长度强关联起来。短上下文、低并发时优先保留 FP16保证质量长上下文、高并发时切到 INT8显存省下来之后可以用更大的批处理。INT4 我只推荐在内部评测不太意的场景使用或者和投机解码联合使用让草稿模型扛精度风险主模型用 INT4 放大吞吐。另外要留意有些推理框架的量化算子并没有针对代码场景做优化量化的本地加速收益可能是假的。建议上线前做一次逐算子 benchmark对比同一批代码补全请求在量化前后的耗时和通过率。3.3 动态批处理与算子融合吃满硬件红利批处理是拉升吞吐最直接的手段。多用户请求并发到达时动态批处理会把长度相近的请求拼成一个 batch一次前向计算应对多个请求。这个做法对 Prefill 阶段的收益尤其明显因为矩阵运算的本质就是批量并行。踩坑提示批处理不是越大越好。当 batch 内请求的上下文长度差异过大时按最长序列补齐会让大量算力浪费在 padding 上。我做了个简单策略根据上下文长度把请求分成几个桶不同桶用不同批处理线程处理同时设置每批最大 Token 总数而不仅是最大请求数。这样明显提升了资源利用率。算子融合就是另一层优化了。常规的做法是把 Attention 里的 Q、K、V 投影合并成一个大的矩阵乘把 LayerNorm 融合进前一个算子减少中间张量的读写开销。多数成熟的推理库已经内置了这些融合但如果你在用自研推理管线建议重点检查两个算子GEMM 和 Attention。这两块的中间显存读写最频繁也最值得融合。3.4 三大加速算法的收益对比与选型建议加速手段主要收益代价与风险适用场景投机解码降低 Decode 延迟提升输出吞吐需要额外草稿模型接受率不稳时收益为负长输出补全、多轮生成量化减少显存占用提升带宽效率可能导致代码生成质量下降上下文偏长、并发偏高的场景动态批处理提高吞吐摊薄 Prefill 成本内存碎片和调度复杂度上升高并发在线服务算子融合减少访存开销降低单请求延迟实现复杂度高部分融合收益有限自研推理管线、极端延迟敏感场景这里面我最想强调选型顺序先做 KV Cache 复用再做上下文压缩这两项几乎零风险和零额外硬件成本却能拿到最稳定的收益之后才是量化和投机解码这类“改变生成方式”的手段。最后再考虑批处理和算子级优化因为它们对调度层的影响最大需要配合压测回归。4. 工程落地从算法到系统的不止一步4.1 合理的架构分层接入层、调度层、推理层算法能够跑起来和能够稳定跑在线上是两码事。我把整个系统分成了三层接入层负责请求校验、用户鉴权、上下文采集与压缩。这一层可以做成无状态服务方便横向扩容。调度层维护请求队列、KV Cache 索引、批处理调度策略。它需要感知当前显存余量、缓存命中和各推理实例的负载。推理层执行真正的模型前向计算同时暴露指标比如 Prefill 耗时、Decode 耗时、缓存命中率等。这个分层的好处是每层可以独立优化。比如我们在调整批处理策略时只改调度层逻辑推理层完全不动后来 KV Cache 复用升级也只动了调度层。项目接手的人不会因为一次优化就搞不清全貌。4.2 缓存策略设计显存、磁盘与冷热数据KV Cache 存在显存里时访问速度最快但容量有限。对于代码助手这种场景固定前缀缓存可以常驻显存检索缓存则要有淘汰策略。我用的是类似 LRU 的思路但过期时间更激进——超过 10 分钟不命中的缓存优先淘汰因为代码项目的上下文变化快过了太久再命中可能反而干扰生成。同时我也把上下文构建过程中的检索结果做成了磁盘缓存。重复请求同一个项目片段时不需要反复执行向量检索和文件读取直接读缓存拼接 Prompt。这部分磁盘缓存命中率能到 40% 以上单请求的上下文准备时间减少了近一半。一种可行的存储分配方案显存里放最热的固定前缀和最近常用的项目片段磁盘里放全量检索缓存两者通过调度层联动。要严格限制显存缓存总量预留至少 20% 的余量给突发请求否则一旦显存打满触发换页反而会让延迟恶化。4.3 关键性能指标与监控体系优化做得再好没有监控就是盲人摸象。我们最终建立了以下监控指标每个指标都对应一项用户体验或资源成本TTFT首 Token 延迟用户发出请求到收到第一个 Token 的时间。这个指标直接决定“工具响应快不快”的主观感受。TPOT每输出 Token 耗时可以换算成“每秒生成多少 Token”影响长补全体验。Prefill 占比若 Prefill 耗时占总耗时比例过高说明上下文可能过于冗长或者缓存命中率不足。缓存命中率监控各级缓存的命中情况这是定位 Prefill 慢的第一线索。显存水位提示是否需要扩容或优化缓存策略。我还给监控配了一个简易看板里面最核心的一张图是“TTFT 与上下文长度的散点图”。从这个图可以直观看到上下文从 2K 涨到 8K 时延迟是如何爬升的。任何上下文压缩优化上线后都能马上从这个图看到分布是否左移。4.4 压测到底怎么测才不会被“压”倒很多团队压测做得太理想化全用长度相同、内容相似的请求结果线上完全不是一回事。我做压测时坚持三原则使用真实或接近真实的请求分布而不是单一的固定长度。混合不同类型请求短补全、长补全、多轮对话、代码解释按线上比例混合。压测长时间运行不仅看平均延迟还要看长尾和显存稳定性。部分优化刚开始没问题跑几个小时后缓存膨胀性能就开始劣化。有一次我们发现一个量化方案在 30 分钟压测中表现很好但追上 3 小时之后每分钟延迟逐步上升查下来是量化算子在特定显存布局下产生了内存碎片。如果只压 10 分钟这个问题根本不会被发现。所以压测时间一定要够长最好持续到显存水位进入稳态之后再记录最终性能数据。5. 实测效果与避坑清单5.1 优化前后的数据对比指标优化前优化后说明平均上下文 Token 数82004100通过分级裁剪与摘要压缩实现Prefill 平均耗时1.8s0.9s受益于 KV Cache 复用和瘦身首 Token 延迟 P502.1s1.1s整体请求链路同步优化单实例并发承载数612显存占用降低批处理层优化长补全输出速度18 token/s30 token/s投机解码与量化叠加效果上面这组数据来自一个模拟项目用同一模型在相同硬件环境下对比。可以看到收益最明显的是上下文压缩和 KV Cache 复用阶段这两个操作不需要改模型权重纯粹靠工程手段就能把延迟打下来一半。量化与投机解码进一步提升了吞吐但对少数极端代码片段会有轻微质量下降所以我在配置中把它们设为按需开启。5.2 高频踩坑问题速查问题现象根因分析解决办法显存 OOM 反复出现KV Cache 缓存策略太激进没有设置过期加容量上限和过期时间预留 20% 余量上下文压缩后生成效果变差摘要丢失了关键符号名和函数签名摘要模板强制保留标识符和调用关系投机解码开启后速度反而更慢草稿模型接受率太低候选大部分被驳回换更强的草稿模型或对输出类型分流批处理吞吐上不去请求长度差异太大padding 浪费严重按上下文长度分桶限制批量总 Token 数量化后代码缩进和括号异常INT4 精度损失在代码场景被放大优先使用 INT8代码敏感场景保持 FP165.3 一些可以“偷”的工程经验最后分享几个很小的但很有用的经验。第一个上下文压缩时不要只压缩旧对话也要压缩“当前文件的历史版本”。很多助手会把用户反复修改的代码片段完整保留其实保留最新版本加一句“在此之前用户进行了多次修改”就够了能省不少 Token。第二个投机解码里的草稿模型不一定要单独跑一个实例。你可以让它和主模型共享同一块 GPU但用更低的显存优先级。只要调度层控制好并发草稿模型引入的额外延迟通常小于它带来的加速。第三个对于极短输出请求可以绕过部分加速逻辑。比如如果用户只是要补全一个变量名预处理阶段的摘要压缩其实没必要执行直接走最短路径返回延迟反而更低。这个“按请求类型分路径”的思路很多通用框架不会替你设计值得自己实现。第四个所有优化都要做 A/B 回归尤其是代码补全的正确率。我见过有人把延迟从 2 秒降到 1 秒但同个测试集上编译通过率掉了 15 个百分点这种优化上线就是事故。所以每次改动都要配一份质量回归集里面放典型代码场景和对应产出结果跑完再看延迟收益。结尾与其追求极致参数不如先把链路理顺我做这些优化之前一直在纠结该用哪个量化位宽、该不该上投机解码。后来发现真正让体验质变的是把上下文管理和缓存复用这两件基础事做好。它们不炫但每一步都扎扎实实地减少了重复计算和无效计算。算力就像钱花在刀刃上才是关键。如果看完这篇你第一件事是把当前助手的上下文构建流程画出来看看到底有多少 Token 是重复和无用的那这次分享就没白写。后面等 KV Cache 复用的命中数据积累差不多了我会再写一篇关于缓存淘汰策略和显存动态调度的经验到时候我们再继续聊。