上下文构建与代码索引:让模型只看该看的东西
这一篇单独拎出“给模型喂什么”这个问题:如何在一个动辄上万文件的代码库里,选出几 KB~几十 KB 的上下文给模型看,又尽量不漏掉关键信息。
为什么“上下文选择”比模型本身还重要?
在 IDE 场景下,模型的输入窗口再大也是有限的:
- 当前文件几百行代码;
- 一堆相关的类型/接口定义;
- 周边调用方/被调用方的片段;
- 再加上指令和系统提示,很容易就把 prompt 撑满。
如果不做选择,常见问题有:
- 给了大量无关代码,真正相关的片段挤不进来;
- 上下文太“稀释”,模型产生的建议变成泛泛而谈;
- 成本和延迟被无谓放大。
所以一个实用的 AI IDE 助手,通常会把很大一块工程精力放在“上下文构建”和“代码索引”上。
第一层筛选:基于编辑器状态的“近场上下文”
最容易拿到的一层上下文来自编辑器状态本身:
当前文件:
- 光标所在函数/类的完整定义;
- 这个函数/类前后若干行;
- 同一文件里相关的辅助函数。
用户操作历史:
- 最近打开/编辑的几个文件;
- 当前选中的代码块;
- 终端/输出面板里最近的错误信息。
这些信息往往可以不用任何额外索引,直接从 IDE 壳和 LSP 拿到。
它们构成了模型的“第一圈视野”:就像开发者下意识先看当前文件和最近改动的地方一样。
第二层筛选:基于语义和结构的项目级索引
仅靠“当前打开的几个文件”通常还不够,尤其是:
- 需要理解某个接口所有实现;
- 需要知道一个类型在项目里“真实是怎么被用的”;
- 需要沿着调用链往上或往下看几层。
这就需要一层项目级索引,常见做法包括:
符号索引:
- 把所有函数/类/方法/变量的定义位置、类型信息、引用关系存起来;
- 快速回答“某个符号在哪里定义/被用过”。
文档/注释索引:
- 把注释和文档内容按文件/段落切块;
- 支持按关键字或语义检索。
语义向量索引(Embedding Index):
- 把代码片段(函数/类/文件片段)编码成向量;
- 给定一个查询(用户问题、当前函数片段),找“语义上最接近”的若干块。
这三类索引可以结合使用:
- 符号索引用于“精确跳转”;
- 向量索引用于“发散找相关”;
- 文档索引用于补充高层语义。
切块策略:按什么粒度把代码喂给索引?
做语义索引之前,必须先决定“怎么切代码”:
- 按文件切:实现简单,但单块往往太大,信息密度不均匀;
- 按函数/类切:比较自然,适合作为“语义单位”;
- 按逻辑片段切:例如按注释/空行分组,适合脚本类代码。
常见的实践是:
- 优先以函数/方法为单位切块;
- 对于特别长的函数,再按逻辑片段二次切分;
- 对于工具/配置类文件,可以按段落切。
切块时还需要存一些元数据:
- 所在文件路径、语言类型;
- 所属类/模块/命名空间;
- 依赖/被依赖的符号列表。
这些信息后面会帮助“从向量相似度约束回语言结构”。
构造检索查询:不是简单把选中代码塞进去
当需要为某个任务构造上下文时,通常不会直接把“当前代码”丢给向量索引,而是:
根据任务类型构造不同的“查询向量”:
- 解释/重构某个函数:用这个函数本身 + 几行上下文作为查询;
- 生成测试:用被测函数签名 + 关键分支片段;
- 帮忙修 bug:用错误堆栈 + 出错行附近代码。
结合符号信息做过滤:
- 只在同一语言/同一模块下检索;
- 只考虑包含特定符号/类型的切块。
这样可以让检索出来的代码既“语义相近”,又“结构上合理”。
Prompt 结构:如何把上下文拼装给模型?
选出了相关代码之后,还需要把它们以合理的形式塞进 prompt 里。
常见的一些做法:
给每个片段加上简单的头信息,比如:
1// File: src/services/user.ts 2// Function: createUser 3...按“距离任务的相关性”排序:
- 最相关的函数/类型放在前面;
- 辅助的工具函数/配置放在后面;
- 不再相关但可能有用的,干脆不放。
为模型标出“关注点”:
- 明确指出“下面这段是你要修改/解释的代码”;
- 把“仅供参考的上下文”和“目标代码”分开。
这样一来,模型看到的不是一大坨散乱代码,而是一个“结构化的案卷”:
既知道“事实材料”是什么,也知道“这次你要干什么”。
截断与压缩:在有限窗口里做取舍
即使有了索引和排序,往往还是会遇到“相关上下文太多”的情况。
这时候就要在信息量和窗口大小之间做权衡:
优先保留:
- 目标函数/类的完整实现;
- 最近修改/与当前改动强相关的代码;
- 关键接口/类型定义。
可以适度压缩或省略:
- 已经很标准的库调用细节;
- 一长串和本次任务关联不大的样板代码;
- 重复出现的模式(可以用一句话概括)。
有些系统会尝试对上下文做自动摘要,例如:
- 把长文档/注释先用模型压成几句要点,再放进主 prompt;
- 对长函数里和本次任务无关的分支只保留签名或一句描述。
和日常编码体验的连接
理解了上下文构建和索引的大致思路,再回头看 IDE 里的 AI 助手,会更容易解释很多现象:
- 有时它看起来“很懂当前文件”,但对项目某个角落一问三不知 —— 可能就是那一块没被选进上下文;
- 有时它会给出和某个工具函数高度一致的写法 —— 多半是检索阶段把这个函数片段喂给它了;
- 当你刚改完某个文件,建议突然变得贴切了 —— 极有可能是索引刚更新完,新的片段能被召回。
从工程角度看,“让模型只看该看的东西”往往比“再换一个更大的模型”更有性价比,也更可控。