
一个context-mode走过的坑让我把AI编程的上下文管理思路彻底换了一套。说白了之前用AI辅助写代码时最烦的不是模型能力不够而是它经常“看不懂”我的项目——要改某个函数它不知道这个函数在哪被调用要排查一个报错它抓不住关键文件明明给它塞了一堆代码回答还是飘的。后来我做了这套叫context-mode的东西核心就一句话按任务类型动态决定给模型喂什么上下文。这个机制能做的事很简单它把“上下文”从一刀切的“全量塞入”改成了“按需组装”。比如改bug时走精确模式只拉报错栈关联的代码链路跨文件重构时走探索模式结合调用关系检索影响面写新功能时走全量模式把相关模块的骨架和接口契约都带上。它解决的核心问题有两个一是上下文窗口不够用二是上下文脏导致模型被无关代码带偏。适合谁看最近在用AI写代码、又发现提示词越长效果越差的开发者以及想在团队里把AI辅助编程工具落地得更稳的工程负责人这篇文章里讲的东西应该对你们有帮助。1. 为什么AI编程最大的瓶颈不是模型而是上下文管理1.1 我踩过的“上下文失控”的坑先说说我在没做context-mode之前日常用AI改代码的几个典型翻车现场。第一个场景是排查线上报错。我习惯把报错信息直接扔给AI然后附上相关文件的全部代码。问题是一个稍微复杂的Go服务单个文件动不动就四五百行我再把依赖的model层、repository层也贴进去提示词直接破了1.5万token。模型倒是能看但它的注意力被大量无关的内容稀释了——比如某个文件里有五六个函数模型把报错定位到了完全无关的一个又比如上下文里有旧版本的接口定义跟当前代码冲突模型给出的修复方案直接调用了不存在的方法。第二个场景是跨文件改需求。比如要在一个Web服务里加一个“导出报表”的接口涉及handler、service、dao、model四层。我不可能把四层的代码全贴完只能挑两三个关键文件贴进去。结果模型改出来的service层代码跟dao层的真实方法签名对不上还得我来回纠错好几轮。每纠一轮就要重新贴一遍代码上下文窗口又爆一次。第三个场景是模型“失忆”。上下文窗口一长模型对前面提到的约束条件就开始选择性遗忘。比如我在开头说了“不要改这个文件的公共接口”到了让它生成实现时它照样给你新增了一个方法浑然不觉。这三个问题其实是同一个根源上下文内容的数量和结构没有跟任务的实际需求对齐。AI编程助手不比人它没有“读整个项目”的长期记忆它的“理解”全部建立在这一次请求喂进去的内容上。所以喂什么、怎么喂、喂多少几乎决定了它的输出质量上限。1.2 context-mode是什么给AI配一个“上下文管理员”我后来想明白一件事人改代码时也不会把整个项目从头到尾读一遍。改一个函数的实现我会先看函数定义、调用方、依赖的模块排查报错我会沿着堆栈一路看下去而不是打开所有文件。AI也应该是这样只是它需要一种机制来主动完成“看哪里”的决策这就是context-mode要解决的问题。context-mode的定位是一个上下文组装与路由层放在AI编程工具不管是Copilot、ChatGPT还是自研的代码助手和项目源码之间。它做四件事把项目源码拆分成可检索、可引用的上下文单元根据用户当前的任务类型判断该加载哪些上下文单元按预算裁剪和排序避免窗口溢出把组装好的上下文交给AI并且告诉AI这些上下文之间的关系。听起来像个RAG检索增强生成但又不完全是。RAG通常解决的是“从知识库里找答案”但代码场景里的上下文比这细得多——它要理解文件之间的依赖关系、函数调用链、符号的定义与使用位置。所以我在实现时除了用向量检索嵌入片段还用了AST抽象语法树和依赖图来做更结构化的筛选。这套机制的实质是把“让AI自己猜哪些代码重要”变成“由系统先按规则筛选再让AI在筛选结果内推理”。效果非常明显——同一批任务模型输出被采纳的比率大概提升了一倍来回修改的轮次也明显变少了。2. context-mode核心设计把上下文切成块再按模式重组2.1 上下文单元的分层模型做context-mode第一件事是想清楚“上下文”到底由哪些部分组成。我当时把代码相关的上下文分成了四层仓库层项目的构建文件、依赖清单、目录结构、README、环境变量说明。这一层决定了模型对项目整体架构的理解但在具体任务里通常不需要全量加载。文件层单个文件的整体内容、文件的职责注释、import列表、导出符号。适用于小文件或核心入口文件的整体加载。符号层函数、结构体、接口、常量、变量的定义以及它们的调用关系。这是代码检索中最常用的单元。变更层git diff、最近提交记录、当前分支与主分支的差异。排查问题时非常有价值常常能直接暴露“这行代码是最近谁改的、为什么改成这样”。我在实现时建了一个轻量的索引器先用AST解析每个文件的符号定义和引用关系生成一张符号表再按函数和类型把文件切成“语义块”每个块是一个独立上下文单元最后生成一个项目依赖图保存“哪些文件引用了哪些符号”。这四层设计有一个好处不同任务天然对应不同层的组合。比如“修改一个函数实现”核心是符号层需要函数定义及其直接依赖“新增一个接口”核心是文件层加上接口相关的符号层而“排查构建失败”则必须看仓库层的构建配置和变更层。context-mode的“模式”就是按这些组合来设计的。2.2 三种基础模式的选型逻辑我最终把context-mode收敛成三个基础模式再通过权重参数调整。第一种是精确模式precision mode。适用场景修改已有代码、修复bug、解答“这个函数做了什么”之类的问题。这种模式下上下文只包含目标函数定义、被它调用的函数定义递归一层、调用它的上游函数回收两层、相关接口定义。数据量最小通常控制在2000 token以内最大化信噪比。代价是模型可能看不到全貌如果问题实际出在某个依赖的依赖里就得靠检索层把那个依赖拉进来。第二种是探索模式exploration mode。适用场景跨文件重构、需求变更影响分析、“我要改这个可能影响到哪些地方”。这种模式下除了目标符号还会加载调用链上所有相关的文件片段、相关模块的测试用例、涉及到的数据模型定义。token预算通常在4000~6000目的是让模型看到影响面而不只是修改点本身。第三种是全量模式full mode。适用场景新模块开发、技术方案设计、从零写一个功能。这种模式下会加载相关目录的多个文件骨架文件头、声明、接口签名而非全部实现、项目规范文档、现有类似模块的实现参考。它不追求完整贴入所有代码而是搭一个“架子”让模型知道该往哪里填以及填的时候要遵守哪些约定。这里有个很重要的取舍逻辑全量模式不等于无限塞代码。我最初试过把整个目录所有文件全贴进去效果很差——窗口爆了模型还会把旧代码当新代码用。全量模式的关键是“骨架规范样例”而不是“全量复制”。2.3 为什么有效信噪比和注意力的数学现实我后来用一个简单的实验验证了这套思路。同一个任务“给用户模块增加软删除功能”分别用两种方式对同一个模型提问一种是把用户模块所有文件共约1.2万token全部塞进去另一种是用context-mode的全量模式输出约3500 token的骨架和关键实现引用。结果是前者的修改建议有3处直接用了不存在的函数后者只出现了1次并且后者被我直接落地的比例更高。其实道理也简单。Transformer模型的注意力机制会随序列长度增加而变得分散有效上下文长度往往远小于它的窗口上限。这就像你在一个嘈杂的会议室里听人讲话人越多越难抓住重点。喂给模型的内容里如果只有20%是相关的那它要拿出80%的“注意力预算”去过滤噪音最后的推理质量自然打折。context-mode本质上做了一件事提高上下文里的信号密度把模型有限的注意力集中到真正相关的代码上。3. 实操过程一步步搭出一套可用的context-mode3.1 环境与数据准备先建索引我这里的实现语言是Python但核心思路跟语言无关。第一步是建立项目源码的索引需要两个基础组件AST解析器我用的是Python自带的ast模块和tree-sitter和依赖收集器。先看一段用tree-sitter解析Go代码、提取函数定义的简化代码from tree_sitter import Language, Parser GO_LANGUAGE Language(build/my-languages.so, go) parser Parser(GO_LANGUAGE) def extract_functions(source_bytes): tree parser.parse(source_bytes) root tree.root_node functions [] def walk(node): if node.type function_declaration: if node.children: name_node node.child_by_field_name(name) if name_node: body node.child_by_field_name(body) func_text source_bytes[node.start_byte:node.end_byte].decode() body_text source_bytes[body.start_byte:body.end_byte].decode() if body else functions.append({ name: name_node.text.decode(), signature: func_text.split({)[0].strip(), body: body_text, start_byte: node.start_byte, end_byte: node.end_byte }) for child in node.children: walk(child) walk(root) return functions这里signature就是函数的完整签名包括参数和返回值body是函数体。我保存这两个字段是因为在精确模式下可以只贴签名不贴函数体在全量模式下再贴函数体。这样同一个符号在不同模式下有不同粒度的呈现方式。依赖收集器我用了pyright的import resolver但更简单的做法是解析每个文件的import语句再跟项目文件路径做映射。比如一个Go文件import了errors但这属于标准库不需要作为上下文单元如果import了internal/user那就要把internal/user目录下的相关文件加入候选检索范围。索引生成之后我把它存成JSON或SQLite每个文件一个条目里面包含路径、函数列表含签名和函数体、类型定义、import依赖、文件的标记性注释。这一步是后续所有模式的地基索引质量直接决定了检索效果。3.2 上下文预算分配token不是无限的有了索引下一步是给每种模式定token预算。我用的估算函数是tiktoken比字符数准确得多。当然也可以用更快的近似len(text) // 4按英文平均每4个字符一个token估算误差不大但中文场景建议len(text) // 2左右。我当时的预算分配表是这样模式总预算指令提示检索上下文备份余量精确模式3000 token5002200300探索模式6000 token6005000400全量模式9000 token8007800400这个预算不是拍脑袋定的。模型的实际有效窗口比标称窗口要小我一般按标称窗口的75%作为硬上限。假设模型标称8k那我就只用到6k标称16k就用到12k。剩下的余量给模型留作“思考空间”避免输出到一半被截断。然后我写了一个简单的组装器按优先级把上下文单元塞进预算里def assemble_context(candidate_units, budget): selected [] used 0 # 按优先级排序目标符号 直接依赖 调用方 测试引用 for unit in sorted(candidate_units, keylambda u: u[priority]): estimated_tokens len(unit[content]) // 4 if used estimated_tokens budget: # 超预算时只保留签名部分如果unit有signature字段 if unit.get(signature) and len(unit[signature]) // 4 budget - used: selected.append({ role: unit[role], content: unit[signature], note: 由于预算限制函数体已省略只提供签名 }) used len(unit[signature]) // 4 continue selected.append({role: unit[role], content: unit[content]}) used estimated_tokens return selected, used这个“只保留签名”的降级策略特别有用。对于大函数模型靠签名和附带的注释也能推测出它的作用只有签名还不够时再把函数体按行切成两半分两次检索补全。3.3 检索层BM25和Embedding混合召回光有索引还不行用户的需求通常是自然语言描述的比如“改一下登录接口的密码校验逻辑”系统得先从代码库里把“登录接口”和“密码校验”对应的代码找出来。这里我用的是混合检索BM25关键词检索 Embedding向量检索然后做结果融合。import bm25s from sentence_transformers import SentenceTransformer # 假设 index_units 是所有上下文单元的列表unit_texts 是其文本 bm25_index bm25s.BM25() bm25_index.index(bm25s.tokenize(unit_texts)) model SentenceTransformer(moka-ai/m3e-base) def hybrid_search(query, top_k10): bm25_results bm25_index.retrieve(bm25s.tokenize([query]), ktop_k)[0] query_vec model.encode(query, normalize_embeddingsTrue) scores [] for idx, unit in enumerate(index_units): vec unit[embedding] if embedding not in unit: vec model.encode(unit[semantic_title], normalize_embeddingsTrue) scores.append((idx, (bm25_results.get(idx, 0) * 0.4) (vec query_vec * 0.6))) # 按融合分数排序 scores.sort(keylambda x: x[1], reverseTrue) return [index_units[idx] for idx, _ in scores[:top_k]]这里的融合公式我用了最简单的加权求和BM25权重0.4向量权重0.6。之所以不全部用向量检索是因为代码里的符号名比如CreateUser、validatePassword是强特征关键词命中非常精准向量模型反而不一定能抓住这种精确匹配。而向量检索的优势在于语义泛化比如用户说“校验登录密码”而代码里函数叫verifyCredential关键词匹配不到向量却能关联上。两者混合的召回率实测是最稳的。检索的chunk切分粒度我调整了两次。第一次按固定200行切效果很差——一个函数被切成两半向量搜索时语义丢了关键词也容易错过。后来改成按AST边界切函数定义单独一个块类型定义单独一个块文件顶部注释单独一个块。这样每个块都是语义完整的检索出来的结果才能直接被放进上下文。3.4 模式路由从用户任务到上下文策略组装上下文前系统要决定走哪个模式。我一开始写了个纯规则分类器用关键词判断包含“报错”“异常”“日志”“panic”就归为精确模式包含“新增”“实现”“设计”“重构”就归为全量模式或探索模式。后来发现规则太脆弱比如“实现一个登录接口”既算新功能又依赖大量已有代码不该简单归到全量模式。最终我改成让模型自己判断但加了约束。具体做法是在一次很小的请求里丢给AI请判断这个任务对应的上下文加载模式precision/exploration/full 只输出模式名称和一个JSON对象包含任务标的文件或函数名不要多余解释。 任务描述{user_input}这个辅助请求的token消耗很小一次约100~200 token但返回的target_file和target_function能直接命中索引准确率比规则高不少。如果后续请求失败才降级到规则匹配。路由结果的格式像这样{ mode: exploration, target_file: internal/user/handler.go, target_function: Login, related_files: [internal/user/service.go] }拿到这个结果后组装器会按模式加载对应的上下文单元。精确模式就取Login函数定义、它调用的service方法、以及handler里引用到它的上层探索模式再加service.go整体骨架和相关测试用例全量模式就再加用户模块所有文件的关键类型定义和项目规范。4. 三套模式的实际效果同一模型不同结果4.1 三个场景的对比实录我在一个Go后端项目上跑了对比。项目大概3万行代码模块划分清晰用的模型是同一个只看context-mode不同策略下的表现。场景一是“登录接口密码校验逻辑修改要求兼容老用户”。精确模式选了handler.Login、service.Login、model.User的数据结构带了密码相关的字段注释还拉取了service.Login的单元测试。模型给出的方案直接指出了老用户密码哈希格式不一致的问题建议在service层做兼容分支完全没碰接口签名。全量模式对照组把整个user目录贴进去下模型反而开始建议改User表的字段定义方向就跑偏了。场景二是“新增一个导出用户列表CSV的接口”。这属于探索模式。我把handler层获取用户列表的逻辑、service层的分页模型、以及model层User的字段列表放进去再附带一个已有的导出接口做参考只需要签名不需要实现。模型产出的代码风格跟现有代码高度一致连命名习惯都对上了基本没做修改就合入。场景三是“重构user模块的数据库访问层从gorm换成sqlc”。这里必须用全量模式。我把所有dao文件的接口定义、db连接对象的结构定义、现有查询方法的签名列表全部放进去再加一份项目里“如何定义新查询”的规范说明。模型给出的sqlc查询定义和映射代码基本可用。如果只给两三个dao文件全文模型很容易在没看到全局的情况下重构到一半炸掉。4.2 三个关键衡量指标我做了一套简单的指标来衡量效果不只凭感觉。第一是首次采纳率即AI第一次给出的代码有多少比例被我直接用或仅微调后合入。没上context-mode时这个数字大概在四成左右上了之后提高到七成以上。这是最直观的信号。第二是平均修改轮次。一次bug修复任务之前平均要跟AI来回对话三到五轮因为每轮它都答不到点子上现在基本一轮或两轮就能给出正确方案因为第一轮它就拿到了最关键的上下文。第三是token消耗。全量模式虽比精确模式费token但综合计算下来修复一个bug的总token开销反而下降了约四成——因为之前每多一轮对话就要重新贴一遍代码现在一次到位。这笔账算下来非常划算。5. 常见问题与排查技巧实录5.1 检索召回不到关键代码怎么办最常遇到的坑是检索层压根没把目标函数找出来。我排查了两次发现罪魁祸首是chunk切分得太碎或者函数没有签名注释向量检索和关键词都抓不住。解决办法是给索引建立“别名表”。比如代码里的函数叫CalcPrice用户描述可能是“算价格”“费用计算”“amount”那就需要把这类语义别名跟符号关联起来。我维护了一个简单的同义词映射表从历史交互里不断积累效果逐渐变好。还有一个技巧是检索失败时自动降级不检索函数级而是检索文件级把文件级命中结果的前几个函数块全部丢给模型让模型自己判断哪个是目标。5.2 上下文预算还是超了怎么办即使有预算控制探索模式有时候还是塞不进去。我加了一个压缩策略当某个上下文单元比如一个文件块超出剩余预算时按“提取骨架”的逻辑处理——保留文件的import列表、类型定义和函数签名丢弃所有函数体。这样模型能知道这个文件有哪些能力、函数签名长什么样但牺牲了实现细节。需要看实现时由模型提出请求再走一次精确检索补上。这个“先骨架、后细节”的策略非常好用几乎就是人读代码的顺序。一开始就把什么都塞进去模型容易晕让它先看到结构再按需拉细节效果稳定很多。5.3 模型被无关代码带偏了有一次探索模式检索出来的上下文里混入了一个老版本的User结构定义代码里已经没有这个结构了但旧索引还没清理模型按旧结构写的代码一编译就报错。排查下来发现是我索引更新时机的问题——只监听保存文件事件没监听git分支切换导致旧分支的索引残留。修复方式是索引跟git分支绑定切换分支后必须重建索引。另外每次组装上下文时我会让组装器输出“排除令牌”如果某个符号在AST里没有找到定义说明它是过期的要静默剔除。这相当于给上下文加了一个编译期检查。5.4 多语言项目怎么处理一个项目混合Go、Python、TypeScript很常见。AST解析必须分语言注册parser依赖图也要分语言处理。难点是跨语言的调用关系比如Python后端调Go微服务代码层面没有直接引用。我现在的做法是对跨语言调用只做路径匹配——把“调微服务API”的代码块和“微服务的路由定义”建立映射但这种映射要手工维护自动化程度还不高算是这个方案的已知局限。如果项目跨越的前后端模块非常复杂我更推荐按目录直接限定检索范围比如“这次任务只检索backend和contracts目录”这个前置约束能省掉大量无效检索。6. 关于扩展的一点经验最后分享一个我用了很久的小技巧context-mode不仅能做“检索组装”还能反向做“何时检索”。也就是说第一轮先让模型根据已有上下文给出一个“我需要看哪些文件”的列表然后系统按这个列表去检索。这个“模型引导检索”的方式对探索式问题特别有效因为它把用户的模糊需求先转化成了明确的文件路径。我目前正在把context-mode往团队协作的方向推进核心是沉淀每个任务的上下文组合模式——比如“修bug”和“写测试”的上下文组合规律把它保存成团队模板。有了模板新人用AI写代码也能直接用最合理的上下文不用自己摸索。这个东西后续能扩展的空间还很大比如结合CI流水线自动给每个PR生成上下文快照或者按模块历史改动的频率动态调权重。不过现阶段这三模式一检索的组合已经帮我省下了大量“跟AI来回解释”的时间。如果你也在被AI编程的上下文问题困扰不妨先手工拆一拆自己最常做的三类任务看看它们分别需要什么样的上下文再决定要不要上这套机制。