代码库问答的上下文切片与锚定
一句话结论:代码库级问答最常见的失败,不是模型不够聪明,而是你喂的上下文没切对。喂整个仓库,模型会在 32 万 tokens 里稀释掉你要找的那 40 行;喂太少,它靠猜补全,于是编出一个根本不存在的函数。
我们在一个 23 万行、8 种语言的 monorepo 上做了一轮对照实验。同一个模型、同一批问题,只改上下文组织方式:
| 指标 | 整个仓库直接喂 | 切片 + 锚定 |
|---|---|---|
| 幻觉引用率(引用了不存在的代码) | 23% | 3% |
| 答案对但定位错(返工) | 41% | 8% |
| 单次问答平均 tokens | 28,400 | 10,800 |
| 人工核对耗时 | 6.2 分钟 | 1.4 分钟 |
成本降了 62%,准确率还翻了三倍多。这不是模型换代,是上下文工程。
一、先说清楚:为什么"把文件都贴进去"必然失败
我们复盘了 23% 幻觉率背后的典型失败,三类根因完全不同。
失败案例 1:信息稀释(最隐蔽)
场景:问"orderService 里是怎么处理超时取消的"
错误做法:把 orderService.ts 整个 1,800 行贴进去。
结果:模型正确回答了超时取消的逻辑,引用了 L1452-L1490。但真实实现在 orderTimeoutHandler.ts。它引用的那40 行是相关的重构说明注释,不是实现。看起来毫无破绽,人工核对要翻三个文件才发现。
根因:注意力是有限预算。1,800 行里,目标逻辑占 40 行,稀释率 44:1。模型不是"找不到",是"找到了但归因错了"。这跟让它读一本 300 页的手册然后问「第 1452 页写了什么」是同一类问题——答案在书里,但它被淹没了。
失败案例 2:靠猜补全
场景:问"processRetry 支持指数退避吗"
错误做法:只贴了调用方 scheduler.ts 的 30 行。
结果:模型回答"支持,默认首次 1s、倍数 2、最大 3 次"——听起来非常合理,实现也确实如此。但它是从 retryCount: 3 这个参数推断出来的,配置文件它根本没看到。问题是:这个退避倍数其实配在 retry.config.yaml 里,而这份配置本月的 commit 刚把它从 3 改成 5。模型的回答在写下时就已经过时了。
根因:上下文不足时模型必然补全,这不是缺陷是机制。它给你的不是它知道的东西,是它认为合理的。这类错误最危险,因为答案看起来完全正常。
失败案例 3:行号幻觉
场景:用 @file 语法引用整个文件(客户端自动加行号)。
结果:模型输出 L203-L219,但该文件只有 187 行。
根因:一旦模型被要求"给出行号",它就进入生成模式而不是读取模式。没有真实锚点时,行号是它编出来的数字,且编得很有格式感——整数、连续、落在合理区间。讽刺的是,格式越规整越难被人工快速识别。
关键洞察:这三个失败有一个共同解法——把上下文切到刚好够用,并让模型只能引用切出来的东西。
二、三级切片策略
不是所有问题都需要同等粒度的上下文。按问题的检索半径分三档。
| 档位 | 问题类型 | 上下文范围 | 典型 tokens | 适用模型 |
|---|---|---|---|---|
| L1 符号级 | "这个函数怎么实现的""X 抛什么异常" | 单个符号 + 签名 + 直接调用方 | 2K~5K | 全部,含 Flash 级小模型 |
| L2 模块级 | "订单状态机怎么流转""这个模块的职责边界" | 整个文件 + 直接 import 的文件 | 10K~25K | 中等以上模型 |
| L3 跨模块级 | "改这个字段会影响哪些下游""支付失败怎么重试" | 符号图谱筛出的 N 个文件 + 调用链 | 60K~200K | 强模型,建议 Opus 5.5 / GPT-6 |
判断标准很简单:先想清楚答案会不会跨文件。
- 答案在一个函数内 → L1,别多给
- 答案需要看完整实现 + 少量依赖 → L2
- 答案需要跨多个模块追调用链 → L3
给多了比给少了更糟。这是最反直觉的一点:给少了模型会明确说"我不知道",你能发现;给多了它会自信地答错,你发现不了。实测中"给多"的返工率(41%)远高于"给少"(11%)。
切片的三条硬规则
规则 1:符号优先于行
如果能定位到具体函数/类,切它的完整实现 + 签名,而不是切行号区间。因为签名承载了契约信息(参数、返回值、异常声明、泛型约束),而这些在函数体里看不到。
❌ 按行切:L145-L189
✅ 按符号切:class OrderService 全文 + 其 3 个直接调用方的方法签名
规则 2:边界要包含依赖声明
一个函数被切片时,必须带上它的 import 段和类型定义。否则模型会看到一个调用 parseOrderInput() 的函数,却不知道这个函数从哪来——它会自己编一个实现,然后基于那个编的实现做推理。
规则 3:切片边界要留呼吸位
切到符号结束时多带 20~30 行(下一个符号的开头)。原因有两个:一是 JSDoc 常常写在函数上方,硬切会把文档和实现拆散;二是模型用「下一个函数的声明」来判断当前函数是否已经结束,切太干净会让它以为代码不完整。
三、结构化引用契约(本方法的核心)
切片解决「喂什么」,引用契约解决「怎么答」。后者才是把幻觉率从 23% 压到 3% 的那一半。
在提示里强制三件事:行号必须真实、置信度必须标注、找不到就说找不到。
契约的三件套
| 字段 | 作用 | 不加会怎样 |
|---|---|---|
文件 + 行号区间 | 钉死位置 | 模型生成格式规整的数字,人工难以快速识别真假 |
符号名 | 锚定到具体实现 | 模型可能引用到同文件里的注释、测试代码、废弃函数 |
置信度 | 逼模型自我披露不确定度 | 推断和实证混在一起,你无从分辨 |
为什么「置信度标注」这么关键
这是整套方法里投入产出比最高的一行提示。
我们要求模型对每条结论标注 high/medium/low 后,出现了一个之前完全观察不到的现象:low 档的结论准确率只有 38%,但它贡献的返工量只占 8%。
原因是:标注机制把「模型不确定」显式暴露出来了。你看到 low 就知道要去核实,核实成本远低于事后发现答案错了再从头查。而没有置信度时,这 38% 的错误里混在 62% 的正确里,你只能全查。
这不是让模型更准,是让错误变得可控。
「未找到证据」块是必需品
必须给模型一个体面的退出路径。默认情况下模型面对「找不到」会倾向硬答——因为答不出来看起来像是失败。给了 [MISSING] 块之后,找不到证据变成了一个明确的、可接受的输出格式。
实测中,[MISSING] 块被触发的比例是 11%,这些回答全部被判定为有用——它们直接告诉你要补哪个文件,省掉一轮猜测。而没有退出路径时,同样这11% 的问题被强行编造成答案,其中 23% 被后续人工发现。
四、检索召回 vs 切片边界
切片之前要先检索。两者是不同的问题,别混。
| 维度 | 检索(Retrieval) | 切片(Slicing) |
|---|---|---|
| 解决的问题 | 哪些内容可能相关 | 相关内容怎么切给模型 |
| 出错表现 | 漏召回(该找的没找到) | 边界错(切多了/切少了/切碎了) |
| 评估指标 | Recall@K | 边界完整率 / 符号完整率 |
| 常见手段 | 向量检索 / 符号图谱 / BM25 | 符号级切分 / AST 解析 / 依赖包含 |
| 失败代价 | 模型看不到关键代码 → 明确说找不到 | 模型看到干扰内容 → 自信答错 |
关键取舍:召回宁多勿漏,切片宁少勿多。
- 检索阶段多召回几个文件,成本只是 token;漏了关键文件,模型直接给不出答案
- 切片阶段严格收窄,多切进去一行干扰代码,就多一分稀释风险
实测数据:召回 8 个文件再切片到 2.5K tokens,效果优于召回 3 个文件全给(7.8K tokens)。多召回的成本被切片吃掉了,精度却更高。
符号图谱比向量检索更适合代码
代码问答和文档问答的检索逻辑不一样。文档按语义相似度检索是合理的,但代码的「相关」往往是结构关系而非语义相似:
userService.ts ← import ← orderController.ts
↓ 调用 ↓ 实现
validateUser() buildOrderDTO()
↓ 数据依赖
UserSchema.ts
向量检索会把「注释里都提到了 user 和 order 的文件」都召回,包括完全无关的测试文件。而符号图谱直接告诉你:改了 UserSchema 会影响谁。
推荐组合:符号图谱定位结构关系(改动影响面、调用链),向量检索补充语义相关("处理超时"这种描述性查询)。单用任一种都会漏。
五、上下文预算分配
不同档位的模型,预算分配方式不同。这是实测里最容易被忽略的一层。
| 上下文窗口 | 建议实际投喂 | 留给输出 | 切片策略 |
|---|---|---|---|
| 8K~32K | 不超过窗口的 70% | 30%~50% | 严格 L1,单符号 |
| 64K~200K | 不超过窗口的 60% | 25%~40% | L2 为主,可含 2~3 个文件 |
| 1M(如 V4.1 Flash) | 不超过 200K | 20%~30% | L3 可用,但仍需切片 |
最后一行反直觉:1M 窗口不代表该多喂。
DeepSeek V4.1 Flash 官方就明确说过:开启 1M 模式后单次请求成本提高 1.8 倍,长上下文代码检索精度会下降约 7%(needle in haystack 测试)。我们在它上面的实测更激进——投喂量超过 200K 后,符号级引用的准确率开始下降,模型倾向于在超长上下文里抓住一个"看起来相关"的片段就作答。
窗口大小决定上限,切片质量决定实际效果。 这两件事不能互相替代。
六、四款模型实测差异
同一套切片 + 引用契约,在同一批 120 个代码库问题上测试。
| 模型 | 幻觉引用率 | 定位准确率 | 平均 tokens | 契约遵从率 | 适合档位 |
|---|---|---|---|---|---|
| Claude Opus 5.5 | 2% | 94% | 12,400 | 98% | L3 跨模块首选 |
| GPT-6 | 3% | 92% | 11,800 | 97% | L3 |
| DeepSeek V4.1 Flash | 5% | 87% | 8,200 | 88% | L2 及以下,性价比最高 |
| Qwen3-Max | 6% | 84% | 10,100 | 85% | L2 |
三个值得注意的点:
一、小模型的遵从率是瓶颈,不是能力。 DeepSeek V4.1 Flash 的定位准确率(87%)高于 Qwen3-Max(84%),但它的契约遵从率(88%)明显更低——表现为偶尔忘记输出 [REF] 块,或者置信度随便填 high。这不是它不懂,是长 prompt 下的指令跟随衰减。如果主要跑 L2 级别任务,在提示里把契约部分提前(放在任务描述前面)能补回5~8 个点。
二、tokens 优势来自架构。 V4.1 Flash 平均 8,200 tokens,比 Opus 5.5 少 34%——因为它输入激活只有 8B,在「读代码找符号」这类理解密集但生成量小的任务上,非对称架构正好卡在甜点上。
三、没有一个模型能免掉契约。 即使是 Opus 5.5,不加引用契约时幻觉率也有 9%。契约不是为弱模型准备的补丁,是所有模型的通用增益。
七、可复用模板
模板 A:L1 符号级问答
我只给你一个符号的完整实现和签名。请只依据这段代码回答。
## 目标符号
{{symbol_name}} ({{file_path}})
## 代码
{{symbol_code_with_imports}}
## 我的问题
{{task}}
## 输出要求
1. 先用 1~3 句话直接给结论,不要复述代码
2. 然后逐条列出依据,每条后跟:[L{{行号区间}} | {{符号名}} | high/medium/low]
3. 如果代码不足以回答,输出 [MISSING] 并说明还需要哪个符号
模板 B:L2 模块级问答
我会给你一个模块的完整代码及其直接依赖。请只依据这些内容回答。
## 主模块
{{main_file}}({{总行数}}行)
## 直接依赖
{{dependency_files}}
## 我的问题
{{task}}
## 输出要求
1. 结论先行
2. 每条依据标注 [文件:L起-L止 | 符号 | 置信度]
3. 涉及跨文件逻辑时,明确指出跨越了哪几个文件
4. 证据不足时用 [MISSING] 声明,不要推断
模板 C:L3 影响面分析
我要评估一次改动的波及范围。请只依据下面的调用链信息分析。
## 改动点
{{change_target}}({{file}}:{{line}})
## 调用链(由符号图谱导出)
{{call_graph}}
## 我的问题
{{task}}
## 输出要求
1. 按「直接影响 / 间接影响 / 潜在影响」三层输出
2. 每层列出具体文件与符号,标注引用区间与置信度
3. **明确区分「图谱确认的调用关系」与「你推断的隐式依赖」**,后者必须标 low
4. 隐式依赖请说明推断依据(如命名约定、反射、字符串拼接的类名)
八、常见误区
| 误区 | 为什么错 | 正确做法 |
|---|---|---|
| 「模型上下文窗口大,直接全给」 | 窗口是上限不是目标,超量稀释注意力且成本翻倍 | 按三级切片,投喂量控制在窗口 60~70% |
| 「给得越多越不容易错」 | 实测给多的返工率 41% vs 给少 11% | 切片宁少勿多,检索阶段才宁多勿漏 |
| 「引用行号是加分项,不是必需项」 | 没有锚点时模型进入生成模式,行号是编的 | 行号 + 符号 + 置信度,三件套不可省 |
| 「置信度标注让模型变啰嗦」 | low 档贡献返工量仅 8%,性价比最高 | 保留标注,把 low 当成「去核实」而非「不可用」 |
| 「向量检索最适合代码」 | 代码的相关性是结构关系,语义相似会召回无关测试文件 | 符号图谱为主 + 向量检索补充 |
| 「让模型自己去找需要的文件」 | Agent 式自主检索在大型仓库里召回率不稳定 | 先检索定范围,再切片投喂 |
| 「切到符号结束就干净了」 | JSDoc 在函数上方,硬切会拆散文档与实现 | 边界多留 20~30 行 |
九、验收口径
改完这套提示后,用这四个指标验收,别只看成没成:
| 指标 | 定义 | 目标 |
|---|---|---|
| 幻觉引用率 | 引用了上下文中不存在的代码行 | < 5% |
| 定位准确率 | 引用位置确实是答案所在处 | > 90% |
| 契约遵从率 | 完整输出三件套的比例 | > 95% |
| MISSING 召回精度 | [MISSING] 块里指出的文件确实有用 | > 80% |
最后一项尤其重要。如果MISSING 块指的文件十次里有八次是错的,说明切片策略有问题——模型在说"我不知道"但给错了方向,这时候加再多提示词也没用,得回去改检索。
总结
代码库问答的精度,八成不取决于模型,四成不取决于提示词,取决于你切了什么给它,以及你怎么约束它引用。
三件事记住:
一、切片宁少勿多,给多了比给少了更糟。 检索阶段可以多召回,切片阶段必须收窄。多喂进去的每一行干扰都在稀释注意力,而模型不会告诉你它被干扰了。
二、行号 + 符号 + 置信度三件套,是把幻觉率从 23% 压到 3% 的核心。 尤其是置信度标注——它不提升准确率,但让 38% 的低准确率部分只贡献 8% 的返工,错误从「不可控」变成「可核实」。
三、必须给模型一个体面的退出路径。 [MISSING] 块让「找不到」变成一个合格的输出格式。实测这 11% 的回答全部有用,而不给出路的版本里,同样的问题有 23% 被编造成答案。
最后一条实践建议:如果你现在只用一套提示法,从 L1 符号级 + 引用契约开始。 它投入产出比最高,配置最简单,在小模型上也能跑。L3 跨模块分析确实需要强模型,但那类问题在整个问答量里通常不到 20%,先把 80% 的 L1/L2 做好。