上下文构建与代码索引:让模型只看该看的东西

这一篇单独拎出“给模型喂什么”这个问题:如何在一个动辄上万文件的代码库里,选出几 KB~几十 KB 的上下文给模型看,又尽量不漏掉关键信息。

在 IDE 场景下,模型的输入窗口再大也是有限的:

  • 当前文件几百行代码;
  • 一堆相关的类型/接口定义;
  • 周边调用方/被调用方的片段;
  • 再加上指令和系统提示,很容易就把 prompt 撑满。

如果不做选择,常见问题有:

  • 给了大量无关代码,真正相关的片段挤不进来;
  • 上下文太“稀释”,模型产生的建议变成泛泛而谈;
  • 成本和延迟被无谓放大。

所以一个实用的 AI IDE 助手,通常会把很大一块工程精力放在“上下文构建”和“代码索引”上。

最容易拿到的一层上下文来自编辑器状态本身:

  • 当前文件:

    • 光标所在函数/类的完整定义;
    • 这个函数/类前后若干行;
    • 同一文件里相关的辅助函数。
  • 用户操作历史:

    • 最近打开/编辑的几个文件;
    • 当前选中的代码块;
    • 终端/输出面板里最近的错误信息。

这些信息往往可以不用任何额外索引,直接从 IDE 壳和 LSP 拿到。
它们构成了模型的“第一圈视野”:就像开发者下意识先看当前文件和最近改动的地方一样。

仅靠“当前打开的几个文件”通常还不够,尤其是:

  • 需要理解某个接口所有实现;
  • 需要知道一个类型在项目里“真实是怎么被用的”;
  • 需要沿着调用链往上或往下看几层。

这就需要一层项目级索引,常见做法包括:

  • 符号索引

    • 把所有函数/类/方法/变量的定义位置、类型信息、引用关系存起来;
    • 快速回答“某个符号在哪里定义/被用过”。
  • 文档/注释索引

    • 把注释和文档内容按文件/段落切块;
    • 支持按关键字或语义检索。
  • 语义向量索引(Embedding Index)

    • 把代码片段(函数/类/文件片段)编码成向量;
    • 给定一个查询(用户问题、当前函数片段),找“语义上最接近”的若干块。

这三类索引可以结合使用:

  • 符号索引用于“精确跳转”;
  • 向量索引用于“发散找相关”;
  • 文档索引用于补充高层语义。

做语义索引之前,必须先决定“怎么切代码”:

  • 按文件切:实现简单,但单块往往太大,信息密度不均匀;
  • 按函数/类切:比较自然,适合作为“语义单位”;
  • 按逻辑片段切:例如按注释/空行分组,适合脚本类代码。

常见的实践是:

  • 优先以函数/方法为单位切块;
  • 对于特别长的函数,再按逻辑片段二次切分;
  • 对于工具/配置类文件,可以按段落切。

切块时还需要存一些元数据:

  • 所在文件路径、语言类型;
  • 所属类/模块/命名空间;
  • 依赖/被依赖的符号列表。

这些信息后面会帮助“从向量相似度约束回语言结构”。

当需要为某个任务构造上下文时,通常不会直接把“当前代码”丢给向量索引,而是:

  • 根据任务类型构造不同的“查询向量”:

    • 解释/重构某个函数:用这个函数本身 + 几行上下文作为查询;
    • 生成测试:用被测函数签名 + 关键分支片段;
    • 帮忙修 bug:用错误堆栈 + 出错行附近代码。
  • 结合符号信息做过滤:

    • 只在同一语言/同一模块下检索;
    • 只考虑包含特定符号/类型的切块。

这样可以让检索出来的代码既“语义相近”,又“结构上合理”。

选出了相关代码之后,还需要把它们以合理的形式塞进 prompt 里。
常见的一些做法:

  • 给每个片段加上简单的头信息,比如:

    1// File: src/services/user.ts
    2// Function: createUser
    3...
    
  • 按“距离任务的相关性”排序:

    • 最相关的函数/类型放在前面;
    • 辅助的工具函数/配置放在后面;
    • 不再相关但可能有用的,干脆不放。
  • 为模型标出“关注点”:

    • 明确指出“下面这段是你要修改/解释的代码”;
    • 把“仅供参考的上下文”和“目标代码”分开。

这样一来,模型看到的不是一大坨散乱代码,而是一个“结构化的案卷”:
既知道“事实材料”是什么,也知道“这次你要干什么”。

即使有了索引和排序,往往还是会遇到“相关上下文太多”的情况。
这时候就要在信息量和窗口大小之间做权衡:

  • 优先保留:

    • 目标函数/类的完整实现;
    • 最近修改/与当前改动强相关的代码;
    • 关键接口/类型定义。
  • 可以适度压缩或省略:

    • 已经很标准的库调用细节;
    • 一长串和本次任务关联不大的样板代码;
    • 重复出现的模式(可以用一句话概括)。

有些系统会尝试对上下文做自动摘要,例如:

  • 把长文档/注释先用模型压成几句要点,再放进主 prompt;
  • 对长函数里和本次任务无关的分支只保留签名或一句描述。

理解了上下文构建和索引的大致思路,再回头看 IDE 里的 AI 助手,会更容易解释很多现象:

  • 有时它看起来“很懂当前文件”,但对项目某个角落一问三不知 —— 可能就是那一块没被选进上下文;
  • 有时它会给出和某个工具函数高度一致的写法 —— 多半是检索阶段把这个函数片段喂给它了;
  • 当你刚改完某个文件,建议突然变得贴切了 —— 极有可能是索引刚更新完,新的片段能被召回。

从工程角度看,“让模型只看该看的东西”往往比“再换一个更大的模型”更有性价比,也更可控。