“代码又变了,你让 Agent 改一下这个接口。”
这句话听起来很平常,但真正在大型生产代码库里跑过 LLM 辅助开发的人,都知道后面会发生什么:Agent 自信地给出一个看似合理的修改方案,但它引用的函数签名是三个月前的版本;它新增的依赖和项目现有架构格格不入;它甚至不知道这个模块已经被重构过两次。于是你花了大半个小时审查它的输出,发现还不如自己动手改。
这就是 LLM 在生产代码库中的“漂移”。
我最近在几个中大型项目里系统性地处理这个问题,核心思路只有一个:不要指望模型“记得住”代码库,而是要让代码库的状态在每次生成前都被强制同步进模型的工作上下文。这篇文章会讲清楚漂移到底是什么、它从哪里来、我在工程上怎么拦截它,以及实践中的完整配置和排错经验。
1. 什么是 LLM 在生产代码库中的漂移
先给一个可操作的定义。
这里的“漂移”不是指模型参数变化导致行为漂移(那是模型版本升级的话题),而是指模型在生成代码时,所使用的代码库认知,与代码库的真实当前状态之间出现了偏差。这种偏差会随着时间积累,最终让模型输出看起来“专业但过时”,甚至完全脱离项目实际。
我给你几个真实场景:
- 一个 Controller 类在两周前被拆分成了两个类,但 Agent 还在按旧结构生成调用代码。
- 配置文件里的数据源从 MySQL 切换到了 PostgreSQL,但 LLM 生成的 SQL 还是 MySQL 方言。
- 项目团队约定所有对外接口都走统一的响应包装类
ApiResponse<T>,但模型生成的代码直接返回裸对象。 - 一个工具类从
commons-utils模块迁移到了shared-kernel模块,Agent 引用了已经删除的 Maven 依赖。
这些问题单独看都不严重,但叠加在一起,会让 AI 辅助开发的“省心”完全变成“费心”。你省下了写代码的时间,却多出了审查、修改、回滚的时间。净收益变成负数。
2. 漂移的三种类型和根因
我把生产环境里最常见的漂移归纳成三类,方便后续排查时对症下药。
2.1 上下文漂移(Context Drift)
这是最基础也最常见的一种。LLM 的上下文窗口是有限的,代码库却是持续变化的。当项目超过一定规模,就不可能把全部代码塞进一次对话里。于是模型只能依赖:
- 你手动粘给它的代码片段
- 对话历史里早期提到的内容
- 它自己的预训练知识(非常过时)
问题是,对话历史也有长度限制,而且早期的信息会在长对话中被稀释。当你在一轮很长的 AI 辅助开发对话中反复修改需求时,模型可能把最早几轮里的旧设计当成当前设计。这就是上下文漂移。
2.2 架构漂移(Architecture Drift)
这类漂移更隐蔽。代码库的架构约束、分层规则、设计模式约定,通常不会写进每个类的注释里,也不会单独出现在某个文件中。模型如果只看局部代码,很容易生成“能跑但不符合架构”的代码。
举例来说,一个遵循领域驱动设计的项目里,Application Service 不应该直接依赖 Infrastructure 层的 Repository 实现。但如果模型只看到了一个 Service 类的片段,它可能直接 new 一个 Repository 实现类,完全绕过接口抽象。代码编译能过,测试能过,但架构评审一定被打回。
2.3 依赖与配置漂移(Dependency & Config Drift)
生产代码库的依赖关系、配置项、环境变量、构建脚本,都是“活”的信息。模型很容易在生成代码时使用一个项目里根本不存在的库,或者引用一个已经改名的配置键。
这类漂移的典型特征是:IDE 里飘红,mvn compile直接报错,但模型的回答看起来却很有道理。因为从通用知识角度,它的建议是对的;从当前项目角度,它引用的坐标已经失效。
3. 为什么简单的“喂上下文”方案不管用
很多人第一反应是:把代码库压缩成文本喂给模型不就行了?我试过,这条路走不通。
首先,生产代码库动辄几十万行代码,即使只提取关键文件,token 消耗也非常惊人。一个大型项目的核心模块可能就有几万行,远超主流模型的有效上下文窗口。
其次,全量喂代码会引入大量噪声。模型需要从几万行代码里自行判断哪些与当前任务相关,这种“大海捞针”式操作,在长上下文下的准确率并不理想。你喂得越多,模型反而更容易被无关代码干扰。
最后,代码库是动态的。你构建的上下文快照,在下一次提交后就过期了。如果每次都在手工准备上下文,那还不如直接自己写代码。
所以问题变成了:如何让 LLM 每次生成代码时,都能拿到与当前任务相关、且是最新的代码库状态?
4. 防漂移的整体架构设计
经过几个项目的实践,我总结出一个五层防漂移架构。它不是某个单一工具,而是一套工程流程:
第一层:索引层(Index Layer)
对代码库建立结构化索引,让代码库的当前状态可被查询。索引可以基于静态分析工具生成,也可以基于向量数据库构建。
第二层:上下文检索层(Retrieval Layer)
根据当前任务,只检索与任务最相关的代码片段、配置文件和架构约束说明。这一层的目标是“按需取用”,而不是全量喂入。
第三层:约束注入层(Constraint Injection Layer)
把项目的架构规范、编码约定、接口封装要求以显式规则注入提示词。不要让模型自己去猜。
第四层:验证层(Validation Layer)
对模型生成的代码进行自动校验,包括编译检查、静态检查、测试运行、依赖检查。校验失败则自动反馈给模型重新生成。
第五层:反馈闭环层(Feedback Loop Layer)
把每一次审查中发现的问题,沉淀为新的约束规则或索引修正,持续优化前三层。
这个架构的核心理念是:不对抗模型的天性,而是改造输入和校验流程。模型没有记忆,我们就替它建立可查询的记忆;模型不擅长遵守隐性规范,我们就按显式规范喂进去;模型输出不可靠,我们就用自动化手段强制验证。
5. 环境准备与基础配置
防漂移落地需要的环境并不复杂,但在开始前要明确版本和工具选择。以下是我的推荐配置思路,具体版本以你项目实际为准:
5.1 基础工具链
- Python 3.10 以上,用于搭建索引和自动化脚本
- 你项目本身的构建工具(Maven、Gradle、npm、pip 等)
- 代码检索工具或向量数据库,推荐按项目规模选择
- LLM API 或本地模型推理服务
5.2 项目背景信息文件
我强烈建议在每个项目根目录维护一个AGENTS.md或PROJECT_CONTEXT.md文件,这是成本最低但收益最高的一步。里面记录:
# 项目架构说明 - 分层结构:Controller -> Application -> Domain -> Infrastructure - 所有对外接口必须返回 ApiResponse<T> 包装 - 业务逻辑禁止写在 Controller 层 # 常用命令 - 构建:mvn clean package - 测试:mvn test - 代码检查:mvn checkstyle:check # 注意事项 - 数据库访问必须走 MyBatis Mapper,禁止直接使用 JdbcTemplate - 新模块必须注册到根 pom.xml 的 module 列表这个文件的目的是让模型在每次任务开始时都有一个“项目上下文锚点”,而不是依赖预训练知识猜测。
5.3 配置项规划
在实际项目中,我通常把防漂移配置拆成两部分:
- 静态配置:项目名称、模块路径、构建命令、编码规范,写入项目上下文文件。
- 动态配置:当前分支、最近提交、变更文件列表,在每次任务开始时通过命令动态获取。
动态信息示例:
git branch --show-current git diff --name-only HEAD~1 git log --oneline -56. 核心流程拆解:从任务到完成的全链路
我以“让 Agent 修改一个现有接口并补充单元测试”为例,拆解防漂移流程。
6.1 任务定义与目标确认
明确告诉模型要做什么、改哪个接口、验收标准是什么。这一步非常关键,因为模糊的需求会放大漂移。
6.2 检索相关代码
根据任务描述,从代码索引中检索:
- 接口所在的类文件
- 该接口的调用方
- 相关联的 DTO、VO、Mapper
- 项目上下文文件中的相关规范
6.3 注入约束与生成
将检索结果、项目上下文文件、本次任务的专门约束一起发送给模型,要求它只基于给定上下文生成代码。
6.4 自动验证
对生成结果执行:
- 编译检查
- 静态代码检查
- 单测运行
- 依赖冲突检查
6.5 人工审查与反馈
最后仍然需要人工审查,但审查重点从“检查每一行代码”变成“检查设计决策是否符合业务意图”。把发现的问题回填到约束文件中,形成持续改进。
7. 完整示例代码实现
这一部分我给出一个最小可用的“检索->生成->验证”示例。示例用 Python 编写,假设代码库是 Java Maven 项目。
7.1 代码索引构建脚本
文件路径:scripts/build_index.py
""" 代码索引构建脚本 功能:扫描 Java 源码,生成类名、方法签名、文件路径的结构化索引 JSON """ import os import json import re from pathlib import Path INDEX_FILE = "code_index.json" def extract_class_info(file_path: Path): """从 Java 文件中提取类名、方法名等基础信息""" content = file_path.read_text(encoding="utf-8", errors="ignore") class_match = re.search(r"class\s+(\w+)", content) if not class_match: return None class_name = class_match.group(1) methods = re.findall(r"(public|private|protected)\s+[\w<>,\[\]\s]+\s+(\w+)\s*\(", content) return { "file": str(file_path), "class": class_name, "methods": [m[1] for m in methods], "content_preview": content[:500] } def build_index(root_dir: str): index = [] for dirpath, _, filenames in os.walk(root_dir): if "target" in dirpath or "node_modules" in dirpath: continue for filename in filenames: if filename.endswith(".java"): file_path = Path(dirpath) / filename info = extract_class_info(file_path) if info: index.append(info) with open(INDEX_FILE, "w", encoding="utf-8") as f: json.dump(index, f, ensure_ascii=False, indent=2) print(f"索引完成,共 {len(index)} 个 Java 文件") if __name__ == "__main__": build_index("src")这个脚本的核心是生成一个轻量级 JSON 索引,而不是把所有代码都变成向量。对于中小型项目,基于关键字检索就已经足够;对于大型项目,可以在这个基础上接入向量检索。
7.2 上下文检索与提示词组装脚本
文件路径:scripts/build_prompt.py
""" 任务上下文组装脚本 功能:根据任务描述检索相关代码,组装 LLM 提示词 """ import json import sys INDEX_FILE = "code_index.json" CONTEXT_FILE = "AGENTS.md" def load_index(): with open(INDEX_FILE, "r", encoding="utf-8") as f: return json.load(f) def load_project_context(): try: with open(CONTEXT_FILE, "r", encoding="utf-8") as f: return f.read() except FileNotFoundError: return "" def search_related_code(index, task_description: str, top_k: int = 5): """ 基于关键字匹配检索相关代码文件 生产环境可替换为向量检索或 tree-sitter 结构检索 """ keywords = [kw.lower() for kw in task_description.split() if len(kw) > 2] scored = [] for item in index: haystack = f"{item['class']} {' '.join(item['methods'])}".lower() hit_score = sum(1 for kw in keywords if kw in haystack) if hit_score > 0: scored.append((hit_score, item)) scored.sort(key=lambda x: x[0], reverse=True) return [item for _, item in scored[:top_k]] def build_prompt(task_description: str) -> str: index = load_index() context = load_project_context() related = search_related_code(index, task_description) prompt_sections = [] prompt_sections.append("你是一名经验丰富的开发工程师。请严格基于以下项目上下文和代码信息完成本次任务,不要引用你自己训练数据中的过时信息。") prompt_sections.append("\n## 项目全局上下文\n") prompt_sections.append(context if context else "(未提供,请参考代码推断)") prompt_sections.append("\n## 与任务相关的现有代码\n") for item in related: prompt_sections.append(f"### 文件:{item['file']}\n") prompt_sections.append(f"类名:{item['class']}\n") prompt_sections.append(f"方法:{', '.join(item['methods'])}\n") prompt_sections.append(f"代码预览:\n{item['content_preview']}\n") prompt_sections.append(f"\n## 本次任务\n{task_description}") prompt_sections.append("\n## 输出要求") prompt_sections.append("1. 只修改与任务直接相关的文件,不重构无关代码。") prompt_sections.append("2. 遵守项目现有编码规范和架构约束。") prompt_sections.append("3. 生成内容包含修改的文件路径和完整代码。") return "\n".join(prompt_sections) if __name__ == "__main__": task = sys.argv[1] if len(sys.argv) > 1 else "修改 UserController 中的 getUserInfo 接口" print(build_prompt(task))运行示例:
python scripts/build_prompt.py "修改 OrderController 的 createOrder 接口增加超时字段"这个脚本会输出一个结构化提示词,其中包含项目全局上下文和检索到的相关代码。
7.3 编译与静态检查验证脚本
文件路径:scripts/validate_code.sh
#!/bin/bash # 功能:对 Maven 项目执行编译和常见静态检查 # 用法:./validate_code.sh [模块名] MODULE=$1 echo "========== 编译检查 ==========" if [ -n "$MODULE" ]; then mvn -pl "$MODULE" -am compile -DskipTests else mvn compile -DskipTests fi COMPILE_STATUS=$? if [ $COMPILE_STATUS -ne 0 ]; then echo "编译失败,代码存在语法或依赖问题" exit 1 fi echo "========== 静态检查 ==========" mvn checkstyle:check 2>/dev/null || echo "checkstyle 未配置或检查未通过,请关注" echo "========== 单元测试 ==========" mvn test -Dtest=*Test -DfailIfNoTests=false7.4 完整接入示例
在真实项目里,我把这三个脚本串成一个流水线:
#!/bin/bash # 文件路径:scripts/ai_task_pipeline.sh # 功能:LLM 辅助开发防漂移流水线 TASK="$1" echo "===== 1. 获取当前分支和最近变更 =====" git branch --show-current git diff --name-only HEAD~1 | head -20 echo "===== 2. 重建代码索引 =====" python scripts/build_index.py echo "===== 3. 生成上下文提示词 =====" python scripts/build_prompt.py "$TASK" > /tmp/llm_prompt.txt echo "===== 4. 手动复制提示词给模型,将输出保存到 /tmp/llm_output.txt =====" echo "提示词已生成:/tmp/llm_prompt.txt" echo "模型输出请保存到:/tmp/llm_output.txt" echo "===== 5. 执行验证 =====" ./scripts/validate_code.sh8. 运行结果与效果验证
以我实际处理过的一个订单模块修改为例,运行流水线后看到的效果是这样的:
- 索引阶段:扫描出 8 个与 Order 相关的 Java 文件,包括 Controller、Service、Mapper、VO。
- 检索阶段:根据任务描述匹配到 4 个高相关文件,准确命中了真正需要修改的
OrderServiceImpl.java和OrderVO.java。 - 生成阶段:模型输出只修改了目标接口,没有触碰无关代码。
- 验证阶段:
mvn compile一次通过,单测全部通过。
重点要看三个指标:
指标一:编译通过率
如果流水线每次生成的代码都能一次通过编译,说明上下文同步基本有效。
指标二:无关修改率
观察模型输出中是否包含“与任务无关的重构”。漂移严重的模型会顺手改掉它认为不合理但实际上运行良好的代码。
指标三:人工审查耗时
防漂移方案的核心价值是降低人工审查成本。如果审查时间没有下降,说明方案还需要优化。
真实项目里的数据是:接入流程后,Agent 生成代码的一次通过率从不足 50% 提升到 80% 左右,审查时间大约缩短一半。这不是模型变聪明了,而是它拿到的是「当前的」代码库状态。
9. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 检索结果全是无关文件 | 关键字匹配太粗糙,任务描述与代码命名不一致 | 查看search_related_code的匹配关键字 | 改用向量检索或 tree-sitter 结构检索,或人工补充检索关键字 |
| 编译通过但架构违规 | 项目上下文文件中缺少架构约束说明 | 查看AGENTS.md是否完整 | 补充分层规范和接口封装要求 |
| 模型忽略给定上下文 | 提示词中未强调“只基于给定信息” | 检查提示词结构 | 在提示词开头明确要求“不要引用训练数据中的旧信息” |
| 文件索引过期 | 代码变更后未重建索引 | 检查索引文件时间戳 | 在 Git hook 或 CI 中自动触发索引重建 |
| 验证耗时太长 | 每次任务都跑全量测试 | 查看测试执行日志 | 按变更模块缩小测试范围,只跑相关模块的测试 |
| 上下文文件过时 | AGENTS.md维护滞后 | 定期 review 上下文文件 | 把上下文文件变更作为 Code Review 的一部分 |
10. 最佳实践与工程建议
在多个项目里落地这套流程后,我总结出几条值得坚持的原则。
10.1 把“项目知识”显式化
代码库中最大的漂移源不是模型不知道代码,而是模型不知道团队约定。每一条架构规范、编码约定、命名规则,都应该写成显式文本,而不是让模型从代码里猜。维护一份高质量的项目上下文文件,比调任何提示词都有效。
10.2 检索优先于全量扫描
不要试图把整个代码库塞进上下文。生产代码库的规模决定了这种做法迟早会失败。构建一个轻量索引,按任务只检索相关文件,既节约 token 又降低干扰。我见过很多团队在向量检索上重度投入,但对于大多数中小型项目,基于文件名、类名、方法名的关键字检索已经足够。
10.3 验证是防漂移的最后一道防线
无论提示词优化得多好,模型输出依然可能出错。编译检查、静态检查、测试运行这三道关卡必须有。其中编译检查是最低要求。如果你的 Agent 生成代码后连编译都不验证,那“漂移”就不是偶然,而是必然。
10.4 建立反馈闭环
每次人工审查发现的问题,都应该沉淀为规则。这个反馈不一定要自动化,最简单的方式就是更新项目上下文文件。比如审查中发现模型经常生成 MySQL 专属语法(而项目是 PostgreSQL),就把“数据库方言必须使用 PostgreSQL”写进约束文件。核心就是让每次返工都有价值。
10.5 安全边界与权限控制
在使用 LLM 处理生产代码时,尤其是让 Agent 自动执行命令或修改文件时,要注意权限边界。不要把具备写入权限的 Agent 直接接到生产仓库上。推荐的流程是:Agent 生成代码和补丁,人工在本地分支审查后合并,通过 CI 验证再合入主干。涉及数据库变更、密钥配置这类高风险操作,必须额外设置人工确认环节。
11. 生产环境接入时的进阶建议
如果要在团队层面推广这套防漂移流程,还需要考虑:
11.1 代码索引的增量更新
对于每天都在变化的项目,全量重建索引成本逐渐变高。建议监听 Git 提交事件,只增量更新变更文件对应的索引条目。
11.2 上下文文件的分层管理
大型项目可能有多个子模块,每个子模块有不同的规范。此时不建议一个巨大的全局上下文文件,而是拆成:
- 根目录
AGENTS.md:全局通用规范 - 各模块
MODULE.md:模块专属约束
检索时先读全局规范,再按任务范围加载对应模块规范。
11.3 与 CI/CD 流程结合
防漂移验证脚本不应该只在本地运行。把编译检查、静态检查和测试执行接入 CI,在 Agent 提交 Pull Request 时自动触发。这样即使开发者忘记本地验证,CI 也会拦截漂移代码。
11.4 模型选择与成本控制
防漂移的效果和模型能力有一定关系。处理复杂代码库任务时,推理能力更强的模型理解上下文更准确,但成本也更高。一个可行的策略是:简单任务用轻量模型,复杂架构调整用强模型。任务分类可以通过文件变更范围和任务描述的关键字来判断。
12. 一个完整的可复制模板
最后,我给出一份可以直接复制到项目里使用的目录结构,方便你把这套方案落地到实际项目:
project-root/ ├── AGENTS.md # 项目上下文约束文件 ├── scripts/ │ ├── build_index.py # 代码索引构建 │ ├── build_prompt.py # 上下文检索与提示词组装 │ ├── validate_code.sh # 编译与静态检查 │ └── ai_task_pipeline.sh # 整体流水线入口 └── code_index.json # 生成的索引文件(可加入 .gitignore)实际业务里,这套架构最大的价值不在于某个脚本写得有多好,而在于它把“模型可能会漂移”这件事当成了默认前提来处理。模型不是可靠的内存,代码库也不是一成不变的,把两者之间的同步制度化、自动化,才是在生产代码库里安全使用 LLM 的正确姿势。下一步可以考虑的方向,是把这套流程接入团队的 MCP Server 或 Agent 编排框架里,让上下文检索和验证成为 Agent 的自动工具调用,而不是人工执行脚本。