Gemini 3.6 Flash 在代码补全里调用项目不存在的工具函数、或者退回旧写法,通常不是模型本身补不动,而是喂进去的上下文既杂又乱:整文件塞入、依赖没给出、调用方和类型定义被截断在窗口之外。可以先做三件事:把补全位置锁到一个函数范围内;只保留调用方、被调用方和数据结构定义三类片段;把这几类片段按「类型定义 → 被调用方 → 调用方 → 待补位置」的顺序拼接。然后用同一处补全跑两次对比,看输出里出现的不存在符号是否减少,再决定下一轮删掉哪些片段。
补全答非所问时,先怀疑上下文范围与顺序,而不是模型能力。适用场景:模型补出的函数体调用了项目里没有的工具函数、旧 API 或臆造参数。操作动作:把补全位置收到单个函数,只留调用方、被调用方、类型定义三类片段,按依赖顺序拼接后再喂。验证方式:固定提示骨架,平铺与排序各跑一次,记录输出中出现的不存在符号。风险边界:顺序调整只能减少歧义,无法弥补缺失的接口信息;若项目本身存在新旧两套写法,仍需人工指定用哪一套。
确认补全位置被截在哪个函数
先把要补的那一段单独摘出来,而不是把整个文件丢进去。整文件的问题在于:模型看到的无关函数多、真正相关的签名可能刚好被截在窗口边缘,于是它宁可自己编一个函数名,也不去用你没给出来的那个。
摘取时贴出补全位置前后各若干行原文,并显式标出两样东西:一是函数签名(参数类型、返回类型、是否 async),二是该位置已经引入的依赖(import、require、注入的 client)。可以用行号定位再打印,避免手工复制漏行:
# 先确认补全位置的行号范围,再打印前后若干行
nl -ba src/service/order.ts | sed -n '40,95p'
# 只保留目标函数及其上下文(示例范围,按实际情况调整)
sed -n '52,78p' src/service/order.ts
把打印结果整理成下面这种带标记的形式,作为后续所有实验的「待补位置」固定片段:
// src/service/order.ts L58-72
// 已导入:import { formatAmount } from '../lib/format';
// import type { Order, OrderItem } from '../types/order';
export async function buildOrderSummary(order: Order): Promise<string> {
// <TARGET> 只补这一段函数体
}
这一步的判断标准很直接:如果待补片段里出现的类型名在上下文中找不到定义,说明截取范围太小,需要补类型文件;如果待补片段周围还有三四个不相关的导出函数,说明截取范围太大,需要再收。两种情况都要在真正跑补全之前处理掉。
把上下文收窄到调用方和被调用方
收窄的目标不是「少给点」,而是「每给一段都有明确用途」。通常只需要三类片段,其余文件、历史版本代码、同目录下写法相近但已废弃的旧实现,一律剔除。
| 片段类别 | 回答的问题 | 取舍判断 |
|---|---|---|
| 谁调用这个函数(调用方) | 入参从哪来、返回值被怎么用、错误怎么处理 | 只保留 1-2 处真实调用点;测试文件一般不放进补全上下文 |
| 这个函数依赖什么(被调用方) | 可以调用哪些已有函数、函数名与签名是什么 | 只保留本次补全预计会调到的工具函数;整个 utils 目录不要整包塞入 |
| 涉及的数据结构定义 | 字段名、可空性、枚举取值 | 只保留被引用到的类型;无关的 interface 一并去掉 |
剔除时要留意旧代码。项目里常见新旧两套封装并存,如果两套都放进上下文,模型容易挑到已废弃的那一套。可以先确认当前分支里哪个封装还在被调用,只留那一个,必要时在提示里写一句「仅使用 xxxClient,不要使用旧版 legacy 封装」。
按依赖顺序组织上下文片段
顺序本身是一种提示。把类型定义放在最前面,模型先知道字段长什么样,再看到被调用函数有哪些,最后看到调用方怎么用,造函数名的概率会低一些。拼接顺序建议固定为:类型定义 → 被调用方 → 调用方 → 待补位置。
下面是拼接脚本骨架,路径按项目替换即可,输出直接复制到补全请求里:
# assemble_context.py —— 按依赖顺序拼接,路径手工填写
PARTS = [
("types", ["src/types/order.ts"]),
("callee", ["src/lib/format.ts"]),
("caller", ["src/api/handler.ts"]),
("target", ["snippets/order_target.ts"]), # 只含待补函数的片段
]
chunks = []
for tag, files in PARTS:
for path in files:
with open(path, encoding="utf-8") as f:
chunks.append(f"### [{tag}] {path}\n{f.read()}")
print("\n\n".join(chunks))
配套的提示骨架可以固定成这几句,约束部分不要省:
只补全 <TARGET> 标记处的函数体,不要改动签名,不要输出其他文件。
上下文按依赖顺序给出:数据结构定义 → 本函数可调用的已有函数 → 调用方 → 待补位置。
约束:
1) 只能调用上下文中已出现的函数与已导入的模块;
2) 不引入新的第三方库;
3) 字段名以类型定义为准,不要自行新增字段;
4) 若信息不足,在输出末尾另起一行写「缺少:...」,不要臆造函数名。
用同一处补全复跑对比
验证方式要能落到可观察的差异上,而不是凭感觉说「好像好一点」。做法是:同一处待补位置、同一份提示骨架,只改变上下文组织方式,跑两次并记录输出。
- 第一轮用平铺上下文:把相关文件按目录顺序整段拼在一起,不分类、不排序。
- 第二轮用排序上下文:按上一节的模板拼接。
- 两轮都把模型输出粘回编辑器,先不运行,直接看它调用了哪些符号。
- 对每个可疑符号在项目里搜索一次(例如
grep -rn "函数名" src/),确认是否存在、是否已废弃。
| 观察项 | 平铺上下文 | 排序上下文 |
|---|---|---|
| 调用了项目中不存在的函数名 | 记录具体名字与个数 | 记录具体名字与个数 |
| 使用了已废弃封装或旧字段名 | 记录出现位置 | 记录出现位置 |
| 是否按类型定义使用字段 | 核对字段名 | 核对字段名 |
| 是否遵守「缺少信息就写缺少」 | 看输出末尾 | 看输出末尾 |
判断标准保持保守:如果排序后仍出现不存在的函数名,说明缺的是被调用方片段,不是顺序问题,回到上一节补上对应文件;如果两轮表现接近,说明这次补全的关键信息本来就少,继续精简上下文的收益有限。
记录哪些片段被明显引用
跑完一两轮后,把每个片段拿出来核对一次:模型的输出里有没有用到它。这一步决定下一轮能删什么,避免上下文越堆越多又回到最初的整文件模式。
| 片段名 | 是否被输出引用 | 引用方式 | 下一轮处理 |
|---|---|---|---|
| types/order.ts | 是 | 用了 OrderItem.unitPrice 字段 | 保留 |
| lib/format.ts | 是 | 调用了 formatAmount | 保留,可只留该函数 |
| api/handler.ts | 否 | 未出现任何相关调用 | 可删除或只留一行调用示例 |
| utils/legacy.ts | 否 | 未被引用,但模型曾提到旧名 | 删除,并在提示中显式禁用 |
记录表的价值在于形成稳定习惯:每次补全前先想清楚三类片段在哪,补全后回填一次引用情况。连续几轮下来,同一模块的上下文清单基本可以复用,遇到新的「答非所问」时,先看是片段缺失、顺序错乱,还是旧代码混入,再决定改哪一处,而不是继续加大上下文。