训练数据里混入多级目录路径,模型才肯照你的 monorepo 约定生成引用
要让模型稳定输出符合你 monorepo 约定的引用路径,训练数据里必须显式混入多级目录路径,而不是只给文件名或扁平结构。模型不会自动推断你的目录规范,它只会从样本里统计路径模式和命名之间的共现规律。
微调私有代码库模型时,大部分人栽在同一个坑里:喂进去的数据全是函数签名、docstring、调用示例,唯独缺了文件在仓库里的真实位置。结果模型生成的 import 语句是 from utils import helper,而你的仓库实际路径是 packages/shared-utils/src/helpers/string_helper.ts。不是模型笨,是它压根没见过这种路径长什么样。
目录路径是最高优先级的约束信号
先给结论:在代码微调数据里,文件路径的权重应该和函数签名本身一样高,甚至在某些任务里更高。
原因很直接。一个函数的逻辑、参数、返回值,模型从预训练阶段就已经见过大量类似模式。但你的 monorepo 目录约定是私有信息,模型在预训练语料里找不到。唯一能让它学会的途径,就是在微调样本里反复出现「函数 → 完整路径」的映射。
具体怎么做。每条训练样本至少包含三个字段:
{
"file_path": "packages/billing-service/src/domain/invoice/calculate_tax.ts",
"function_name": "calculateTax",
"context": "// 引用该函数的正确方式\nimport { calculateTax } from '@billing/domain/invoice/calculate_tax';"
}
file_path 必须是仓库内的真实相对路径,从 monorepo 根目录开始,包含包名、模块名、子目录层级。不要简化成 calculate_tax.ts,也不要只写 domain/invoice。模型需要看到完整的多级路径,才能学会你的包命名规则(比如 @billing/ 前缀)和目录层级之间的对应关系。
我在一个私有项目上做过对比。第一版训练数据只包含函数名和调用示例,模型生成 import 时正确率约 31%。第二版给每条样本加上完整 file_path 和对应的 import 语句,同样训练量下正确率拉到 87%。差别就一个字段。
用「引用任务」构造样本,而不是只做代码补全
第二个关键结论:如果你只训练模型做代码补全(给上文补下文),它学不会主动生成正确的引用路径。你需要专门构造一批「给定函数描述或签名,生成引用语句」的样本。
具体做法是把每个函数的 docstring 或签名作为输入,把正确的 import 语句作为输出:
# 训练样本格式
input_text = """\
// 函数签名
export function calculateTax(amount: number, region: TaxRegion): TaxResult
// 生成引用该函数的 import 语句
"""
output_text = """\
import { calculateTax } from '@billing/domain/invoice/calculate_tax';
"""
这类样本强制模型建立「函数 → 路径」的直接关联。代码补全样本里,import 语句通常出现在文件头部,模型很容易把注意力放在函数体逻辑上,路径信息被稀释。而引用任务把路径变成唯一的输出目标,梯度信号集中。
比例上,我建议引用任务样本占总样本量的 20%–30%。剩下 70%–80% 保留正常的代码补全,但每一条代码补全样本的开头或上下文中都要带上正确的文件路径注释,比如:
// file: packages/billing-service/src/domain/invoice/calculate_tax.ts
// 以下代码位于该文件中
export function calculateTax(amount: number, region: TaxRegion): TaxResult {
// ...
}
这样模型在补全代码时也能持续感知到「当前文件在仓库中的位置」。
负样本与路径变体:让模型知道什么是不对的
很多团队只给正样本,模型能学会该用什么,但分不清不该用什么。尤其当仓库里有多个同名函数时,比如 calculateTax 在 billing 包和 reporting 包里各有一个,模型必须根据上下文区分。
构造负样本的方式是在训练数据里加入「错误路径 → 正确路径」的修正样本:
{
"incorrect_import": "import { calculateTax } from '@reporting/utils/tax';",
"correct_import": "import { calculateTax } from '@billing/domain/invoice/calculate_tax';",
"instruction": "修正 import 路径,确保引用的是 billing 包中的 calculateTax"
}
这类修正样本让模型学会路径冲突时的判别规则。实际效果上,它比单纯增加正样本更能降低幻觉式路径生成。
另一个容易被忽略的点是路径变体。一个函数可能被多种方式引用:相对路径、绝对路径、别名路径。你的团队可能只允许其中一种,但模型需要知道哪些变体是禁止的。在训练数据里,对同一条路径可以生成 2–3 个变体样本,其中只有符合规范的那条标记为正样本,其余标记为负样本。
目录约定本身要写进系统提示或数据前缀
如果你的微调数据里混入了不同包、不同模块的函数,模型需要知道全局的目录规则。把这些规则直接写进每一条训练样本的前缀,比单独放在 system prompt 里更有效,因为微调阶段模型对数据内部的规律敏感度高于外部指令。
示例前缀:
仓库目录约定:
- 包名格式:packages/<service-name>/src/<domain>/<module>/
- import 使用包别名:@<service-name>/<domain>/<module>/<file-without-extension>
- 禁止相对路径引用跨包模块
- 禁止使用 src 路径直接引用
根据以上约定,完成以下任务:
把这个前缀加在每一条样本的输入部分。微调后模型在推理时,只要在上下文里看到类似的约定描述,就能沿用训练时学到的路径模式。这比只靠 model card 里写说明靠谱得多。
数据规模与配比参考
给出几组实际数字。我在一个约 480 个函数、目录层级 4–6 层的 monorepo 上做微调,最终有效样本量是:
- 引用任务样本:1,200 条(正样本 900 + 负样本 300)
- 代码补全样本:3,600 条(每条含文件路径注释)
- 路径修正样本:400 条
- 总计约 5,200 条,训练 3 个 epoch
这个量级对于 7B 参数的模型已经足够学会私有目录约定。如果你用的是更小的模型(1B–3B),建议把引用任务样本占比提高到 35%–40%,因为小模型更需要强信号。
常见问题
为什么模型训练完后还是偶尔生成错误的路径?
检查你训练数据里是否覆盖了所有实际会用到的目录层级。如果某个子包在训练数据里只出现过一两次,模型对它的路径模式掌握就不牢。解决办法是统计每个包的样本分布,确保每个包至少有 50–80 条包含完整路径的样本。另外确认推理时的系统提示或上下文里是否给出了目录约定描述,没有的话模型只能靠猜。
要不要把文件路径拆成 token 级标签单独训练?
不需要。把路径作为文本的一部分直接训练即可,模型能自己学到路径组成规律。拆 token 打标签属于过度工程,实际收益不明显,还增加数据管线复杂度。你只需要保证路径格式一致,不要有的带 src/ 有的不带。
多级路径应该从仓库根开始写,还是从包根开始写?
从仓库根开始写,也就是 packages/billing-service/src/domain/invoice/calculate_tax.ts 这种完整形式。模型需要看到包之间的边界才能理解 monorepo 结构。只从包根写起会丢失「哪些目录是包、哪些是包内模块」的信息。