你大概率已经遇到过这个问题让 LLM 在项目里连续开发一段时间后刚开始还像模像样的代码慢慢开始走样。函数命名风格漂了项目里根本不存在的依赖被引进来本该写在services/层的业务逻辑被塞进 HTTP 路由甚至开始调用一个早就废弃的内部 API。这不是模型“变笨了”也不是开发者没有写好提示词。这是一种普遍存在于生产代码库中的现象我把它叫做LLM 漂移LLM Drifting。漂移的本质不是模型能力下降而是约束衰减。当一次任务只改一个文件时模型几乎不会出错当 Agent 需要连续修改十几个文件、跨越多个模块、依赖大量项目上下文时早期给它施加的规则被一步步稀释最终输出的代码看起来“没什么语法问题”却处处违背项目约定。这篇文章会从漂移的本质讲起说明为什么光靠“写一个更长的 System Prompt”解决不了问题然后给出一套可落地的三层防线输入约束、输出约束、自动验证。文末附带可以直接复制的项目规范模板、基于 Python 的守卫脚本和 CI Gate 配置你可以直接拿去部署在现有项目里。1. 生产代码库中的 LLM 漂移到底是什么先给出一个明确定义LLM 漂移是指模型在持续修改一个代码库的过程中对项目约束的遵守程度随着上下文复杂度上升而下降的现象。约束包括但不限于约束类型表现风格约束命名方式从snake_case漂移到camelCase引号风格混乱依赖约束引入了requirements.txt之外的第三方库且没有告知架构约束跨层调用Controller 直接写 ORM 查询Service 里拼 SQLAPI 约束调用了旧版内部接口或自己发明了不存在的函数签名流程约束跳过了迁移脚本、测试、错误处理等强制流程举一个具体场景。你的项目里约定所有model层只负责数据映射不写业务逻辑。前三次修改中LLM 严格遵守这个约定。但在第四次任务中用户要求“从订单表查询超过 30 天未支付的订单”LLM 直接在models/order.py里写了一个query_expired_orders()方法内部拼接 SQL。生成时模型可能觉得“这样更方便”但它打破了整个项目的分层边界。这就是漂移的典型模式它不是一个突发的错误而是一系列微小偏离的累积。单独看每一次改动代码都能运行但从项目整体看技术债正在以一种比人类协作者更快的速度累积。还有一个容易混淆的点漂移不特指“大模型幻觉”。幻觉是模型生成了与事实不符的内容而漂移是模型偏离了此刻正在处理的上下文约束。幻觉是输入理解问题漂移更多是约束保持问题。两者经常同时出现但解决思路完全不同幻觉靠检索增强漂移靠过程约束和自动化验证。2. 为什么光靠 Prompt 工程控制不住漂移这是很多团队的直觉反应模型不听规则是不是我 Prompt 写得不够好于是把 System Prompt 越写越长从 500 字写到 2000 字又从 2000 字写到 5000 字。结果是规则列得越多模型遵守得越不稳定。原因主要有三个。第一上下文窗口的注意力是稀疏的。即使模型支持 128K 甚至 200K 上下文它对长文本中“细节规则”的注意力也是有限的。当任务核心需求集中在“改好某个函数”时模型倾向于把注意力分配给最近的代码片段而不是 8000 字之前的规范段落。规则写得再多也抵不过任务本身对注意力的挤压。第二长对话中的早期约束会被后续内容稀释。LLM Agent 通常通过多轮工具调用完成任务每一轮都会追加新的工具返回结果、新的代码片段。规则被推到很靠前的位置后续的注意力渐渐被新内容覆盖。研究发现即使模型能“看到”完整上下文它对早期信息的引用置信度仍然会随着对话轮数下降。第三缺失“输出验证”环节。Prompt 工程只能影响生成概率无法保证输出满足规则。如果你要求“不要 import 任何新的第三方库”模型仍然可能生成import pandas因为这一步在概率上太自然了。没有验证环节违规输出就具备同等概率进入代码库。这就是为什么很多 LLM Agent 项目会引入“自检”环节让模型生成后检查自己的输出。但自检同样依赖模型自身的判断对于它自己刚犯的错误自检往往不够敏感。更可靠的防线是外部验证不依赖模型自觉而是用确定性的代码去检查输出。光靠 Prompt 能解决“模型不知道规则”的问题但解决不了“模型知道规则却依然违反”的问题。后者必须靠工程手段兜底。3. 防漂移的三层防线输入约束、输出约束、自动验证针对漂移发生的不同阶段可以设计三层防线。这三层缺一不可完整覆盖从生成前、生成中、生成后的全流程。3.1 第一层输入约束输入约束解决的是“模型不知道规则”和“模型上下文太杂”的问题。具体做法包括建立项目级的规则文件如AGENTS.md统一描述项目结构、技术栈、架构边界、代码风格。控制每次任务注入的上下文范围只把真正相关的模块和规则放进 Prompt避免“上下文中塞满无关文件”。使用 RAG 对项目文档做检索而不是把所有文档全部灌入上下文。按需获取减少注意力稀释。用 Few-shot Examples 把“正例”和“反例”放在离生成目标更近的位置。输入约束的目标不是“让模型更聪明”而是“让模型更容易看到关键规则”。规则位置越靠前、越贴近任务描述、越简短被遵守的概率越高。3.2 第二层输出约束输出约束解决的是“模型自由发挥空间过大”的问题。具体做法包括对需要结构化返回的结果使用 JSON Schema 或 Pydantic 模型约束例如让 LLM 输出“变更总结”时强制包含字段而不是自由文本。在工具调用场景中对函数参数做严格校验超出 Schema 的字段直接拒绝。给 LLM 提供“禁止使用”清单并在生成前把它转换成代码层面可检查的规则。如果使用 MCP 等工具协议只暴露白名单内的工具模型无法调用未授权的操作。输出约束的本质是把一部分“自由裁量权”从模型手中拿回来。模型仍然可以生成代码但它在格式、字段、工具选择上的自由度被限制住了。3.3 第三层自动验证自动验证解决的是“违规输出进入代码库”的问题。具体做法包括写一个守卫脚本Guard Script对 LLM 生成的代码做静态检查比如 AST 解析、依赖扫描、占位符检测、命名规范校验。接入 CI在 PR 阶段自动跑 lint、类型检查、单元测试。对 LLM 生成的变更跑一次“diff 审查”对比生成前后的代码差异重点检查是否发生了越界改动。建立小的 eval 集定期回归测试把历史典型任务重新喂给模型验证它是否仍然遵守规则。自动验证与前两层不同它不依赖模型的表现而是用确定性的规则兜底。无论模型怎么生成只要输出不满足验证条件就不能合入代码库。这一层是防漂移的关键兜底。4. 环境准备与最小落地框架下面开始进入实操。先说明本文示例的运行环境操作系统Linux / macOS / Windows 均可Python3.10 及以上代码库Git 管理使用 Pull Request 工作流LLM 接入任意支持 OpenAI 兼容 API 的模型或本地部署的 LLM 推理服务示例项目结构如下my-service/ ├── AGENTS.md # 项目规则文件 ├── guard/ │ └── check_llm_output.py # 守卫脚本 ├── src/ │ └── my_service/ │ ├── models/ │ ├── services/ │ └── api/ └── tests/这套方案不依赖特定 LLM 框架。如果你已经在使用 LangGraph、Spring AI 等 LLM 编排框架只需要把“守卫脚本”作为工具调用之后的一个强制步骤接入即可如果还没有引入编排框架也可以直接作为 Post-processing 脚本使用。5. 核心代码实现规范注入、守卫脚本与 CI Gate这一部分通过四个步骤从规则文件、静态检查、CI 集成到结构化输出完整实现一套防漂移机制。5.1 第一步建立机器可读取的项目规则文件首先在项目根目录创建AGENTS.md这个文件会被注入到每次 LLM 生成请求的 System Prompt 中。内容不要贪多只写“模型必须遵守的硬约束”。# 项目规则AGENTS.md 该文件会被自动注入到 LLM 生成请求中。 所有代码生成必须遵守以下规则不得例外。 ## 技术栈 - 语言Python 3.11所有函数必须带类型注解 - Web 框架FastAPI - ORMSQLAlchemy 2.x Alembic 迁移 - 数据校验Pydantic v2 ## 目录边界 - 路由层api/只做参数解析和响应组装 - 业务逻辑必须写在 services/ 目录 - 模型层models/禁止写业务逻辑禁止在模型层调用外部 HTTP 接口 - 新功能必须包含对应的单元测试 ## 依赖管理 - 禁止引入 requirements.txt 之外的第三方依赖 - 如果确实需要新依赖必须先在文档中说明理由经评审通过后加入 ## 代码风格 - 使用 ruff 默认规则 - 禁止使用 TODO / FIXME 占位符 - 错误处理必须显式不允许大面积裸 try/except这里最关键的设计是每个规则都要“可检查”。不要写“请保持代码优雅”这种无法验证的句子写成“禁止在模型层调用外部 HTTP 接口”守卫脚本才有实现空间。5.2 第二步编写 ASCII 守卫脚本接下来创建一个 Python 守卫脚本作用是在代码合入前扫描改动内容找出常见的漂移信号。# 文件路径guard/check_llm_output.py AI 生成代码守卫扫描 PR 变更拦截常见漂移信号。 用法 python guard/check_llm_output.py --root src import argparse import ast import re import sys from pathlib import Path # 允许的第三方依赖白名单按项目实际情况维护 ALLOWED_IMPORTS { fastapi, sqlalchemy, pydantic, uvicorn, alembic, } # 禁止出现在待合并代码中的占位符 PLACEHOLDER_RE re.compile( r\b(TODO|FIXME|NotImplementedError)\b, re.MULTILINE, ) # 禁止出现在模型层的外部调用 MODELS_DIR models FORBIDDEN_CALLS_IN_MODELS {requests, httpx, openai, aiohttp} def check_syntax(path: Path) - list[str]: try: ast.parse(path.read_text(encodingutf-8)) except SyntaxError as exc: return [f{path}: 语法错误 - {exc.msg} (line {exc.lineno})] return [] def check_imports(path: Path, content: str, tree: ast.AST, issues: list[str]) - None: for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: root_module alias.name.split(.)[0] if root_module not in ALLOWED_IMPORTS and not root_module.startswith(my_service): issues.append( f{path}:{node.lineno} 新依赖 {alias.name} 不在白名单中 请先在 AGENTS.md 中说明理由并经过评审 ) elif isinstance(node, ast.ImportFrom): root_module (node.module or ).split(.)[0] if root_module not in ALLOWED_IMPORTS and not root_module.startswith(my_service): issues.append( f{path}:{node.lineno} 新依赖 {node.module} 不在白名单中 ) def check_models_boundary(path: Path, content: str, issues: list[str]) - None: if MODELS_DIR not in path.parts: return for keyword in FORBIDDEN_CALLS_IN_MODELS: if keyword in content: issues.append( f{path}: 模型层禁止直接调用外部服务检测到 {keyword} ) def check_placeholders(path: Path, content: str, issues: list[str]) - None: for match in PLACEHOLDER_RE.finditer(content): issues.append(f{path}: 占位符 {match.group()} 不允许出现在待合并代码中) def check_typed_functions(path: Path, tree: ast.AST, issues: list[str]) - None: # 简化版只检查项目中是否全部函数都有返回注解 # 实际项目中可以依赖 mypy 或 pyright这里做一个轻量提醒 for node in ast.walk(tree): if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): if node.returns is None and not node.name.startswith(_): issues.append( f{path}:{node.lineno} 函数 {node.name} 缺少返回类型注解 ) def check_file(path: Path) - list[str]: issues: list[str] [] issues.extend(check_syntax(path)) if issues: return issues content path.read_text(encodingutf-8) tree ast.parse(content) check_imports(path, content, tree, issues) check_models_boundary(path, content, issues) check_placeholders(path, content, issues) check_typed_functions(path, tree, issues) return issues def main() - int: parser argparse.ArgumentParser(descriptionAI 生成代码守卫) parser.add_argument(--root, defaultsrc, help要扫描的代码目录) args parser.parse_args() root Path(args.root) if not root.exists(): print(f目录不存在: {root}) return 2 all_issues: list[str] [] for path in sorted(root.rglob(*.py)): all_issues.extend(check_file(path)) if all_issues: print(检测到以下漂移信号请修正后重新提交) for issue in all_issues: print(f - {issue}) return 1 print(OK: 未发现明显漂移信号。) return 0 if __name__ __main__: raise SystemExit(main())这个脚本重点检查四类漂移语法错误、白名单外依赖、模型层越界调用、TODO 占位符。在一个真实项目里你还可以加入ruff check、mypy、pytest的结果对比把守卫生效范围扩大。触发方式是直接扫描代码目录。实际 CI 中更应该把扫描范围限制在“本次 PR 变更的文件”避免历史存量问题阻塞新代码合入。可以把命令行参数从--root改成能接收 Git diff 文件列表的形式。为了更贴近 PR 场景也可以直接用git diff获取变更文件git diff --name-only origin/main...HEAD -- *.py | while read file; do python guard/check_llm_output.py --root $file done上面的脚本是扫描目录的如果需要按文件列表运行可以把--root参数改成接收文件路径列表或者用git diff把变更文件导出后再扫描。5.3 第三步在 CI 中接入 AI Gate有了守卫脚本下一步是把它接入 CI。以下是一个 GitHub Actions 的示例核心思想是每次 PR 变更都先跑守卫脚本再跑静态检查和单元测试。# 文件路径.github/workflows/ai-gate.yml name: ai-gate on: pull_request: types: [opened, synchronize] jobs: check-llm-output: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkoutv4 - name: Setup Python uses: actions/setup-pythonv5 with: python-version: 3.11 - name: Install dependencies run: | pip install ruff mypy pytest - name: Run LLM output guard run: | python guard/check_llm_output.py --root src - name: Run ruff run: | ruff check src tests - name: Run mypy run: | mypy src - name: Run tests run: | pytest --tbshort -q这个 Job 的价值在于“把 AI 生成的代码和其他代码一视同仁”。不管代码是人工写的还是 LLM Agent 生成的只要无法通过 gate就不能合入。CI 本身不关心代码来源它只关心输出质量。如果你的团队已经使用自定义的 GitLab CI 或其他 CI 平台思路完全相同在流水线中加入一个ai-gate阶段作为合并前的强制检查项。5.4 第四步用结构化输出约束 LLM 的 PR 总结漂移不只体现在代码本身也体现在模型的“配套产物”上。例如 PR 总结、变更说明、风险评估如果这些内容也由 LLM 生成格式漂移同样会扰乱协作流程。一种做法是使用 JSON Schema 约束 LLM 的输出让它在“固定的框架内”生成总结。下面是一个面向“PR 变更总结”的 JSON Schema 示例{ $schema: http://json-schema.org/draft-07/schema#, type: object, required: [summary, changed_files, risks], properties: { summary: { type: string, maxLength: 200, description: 本次变更的简要说明 }, changed_files: { type: array, items: { type: string }, minItems: 1 }, risks: { type: array, items: { type: object, required: [level, description], properties: { level: { type: string, enum: [low, medium, high] }, description: { type: string } } } } } }配合 OpenAI 兼容接口时可以在请求中声明response_format为 JSON Schema 类型。如果你的 LLM 接入层还不支持response_format也可以在拿到自由文本后用一个 JSON 解析器加校验器做后处理。# 文件路径guard/validate_pr_summary.py 校验 LLM 生成的 PR 总结是否符合 JSON Schema import json import sys import jsonschema from jsonschema import validate SCHEMA { $schema: http://json-schema.org/draft-07/schema#, type: object, required: [summary, changed_files, risks], properties: { summary: {type: string, maxLength: 200}, changed_files: { type: array, items: {type: string}, minItems: 1, }, risks: { type: array, items: { type: object, required: [level, description], properties: { level: { type: string, enum: [low, medium, high], }, description: {type: string}, }, }, }, }, } def main() - int: raw sys.stdin.read() try: data json.loads(raw) except json.JSONDecodeError as exc: print(fJSON 解析失败: {exc}) return 1 try: validate(data, SCHEMA) except jsonschema.ValidationError as exc: print(fSchema 校验失败: {exc.message}) return 1 print(PR 总结格式 OK) return 0 if __name__ __main__: raise SystemExit(main())使用方式echo {summary: feat: 增加订单过期清理任务, changed_files: [src/my_service/services/order_cleaner.py], risks: [{level: low, description: 新增后台任务不影响现有接口}]} | python guard/validate_pr_summary.py结构化输出不是为了让生成结果“好看”而是为了让下游流程可以稳定消费。有了固定的 JSON 结构CI 里的自动化脚本、文档生成器、消息通知组件都能直接对接这也间接防止了 LLM 在“配套产物”上的自由发挥。6. 运行结果与效果验证整套方案落地后可以从两个层面验证效果。层面一守卫脚本的单点验证。先制造一个明显违规的测试文件# 文件路径src/my_service/models/bad_example.py 故意制造违规的示例文件 import requests # 不在白名单中 def query_orders(): # 模型层直接调用外部服务违反目录边界 resp requests.get(https://internal-api.example.com/orders) return resp.json() # TODO: 这里缺少异常处理运行守卫脚本python guard/check_llm_output.py --root src/my_service/models预期输出检测到以下漂移信号请修正后重新提交 - src/my_service/models/bad_example.py:50 新依赖 requests 不在白名单中请先在 AGENTS.md 中说明理由并经过评审 - src/my_service/models/bad_example.py: 模型层禁止直接调用外部服务检测到 requests - src/my_service/models/bad_example.py:52 占位符 TODO 不允许出现在待合并代码中如果你只看表面可能会误以为守卫脚本只是一个“静态检查工具”。但实际上它改变了 LLM 生成代码的闭环当 Agent 的每一次输出都可能在 CI 里被拦截时你需要在 Prompt 中告诉它“所有生成结果必须通过守卫脚本”模型会逐步学会在生成阶段主动规避这些违规风险。这样一来验证就不再只是“事后兜底”而是反向影响生成行为。层面二PR 流程的效果观察。建议设置一个为期两周的观察期重点看三个指标CI 一次通过率LLM 生成的 PR 在第一次提交时就能通过全部检查的比例。评审返工率因“代码风格不符”“架构越界”“依赖未声明”等原因被打回的比例。移入存量代码库的漂移信号数量比如 TODO 占位符、越界调用等可以用守卫脚本定期全量扫描一次。两周后你会发现真正被拦下来的往往不是“写错了”的代码而是“写得太自由”的代码。这也印证了文章开头判断LLM 的问题不是不会写代码而是太擅长在没有约束的情况下“即兴发挥”。7. 常见问题与排查思路问题现象可能原因排查方式解决方案守卫脚本扫描整个src目录历史存量问题阻塞了 CI把历史代码和本次 PR 变更混在一起检查先看错误来自哪个文件确认是否属于本次变更改为基于git diff的文件列表执行守卫脚本只检查本次变更的文件模型经常引入白名单之外的依赖AGENTS.md中的依赖规则表达不够清晰或 Prompt 中没有强调查看 System Prompt 中规则注入的位置和长度把依赖白名单单独抽成一段“硬约束”放在 Prompt 靠近任务描述的显著位置JSON Schema 校验经常失败输出格式不稳定且没有启用结构化输出能力查看原始输出与 Schema 的差异确认是否有换行、多余字段优先使用 API 的response_format参数如果模型不支持则在后处理解析失败时自动重试一次某些规则被 LLM 反复违反规则本身模糊或与任务目标存在冲突对照AGENTS.md与实际生成代码找出冲突点改写成“可检查”的规则配合守卫脚本强制拦截形成硬记忆守卫脚本出现误报白名单覆盖不全比如内部包名没加进ALLOWED_IMPORTS查看具体的告警信息判断属于新依赖还是内部包维护白名单时把项目内部模块的根目录名也加进去这里最值得提醒的一点是规则文件不是越全越好。如果 80% 的规则在历史上从未被违反过说明它们对当前模型没有边际约束力反而可能挤占注意力资源。建议从“最近一周实际发生过的漂移问题”中提炼规则每个规则都要能对应到一个具体的失败场景。8. 工程最佳实践与安全边界8.1 先从小范围试点开始不要第一天就把整条 CI 防线接入所有 PR。建议先选择一个改动频率高、生成代码占比高的服务作为试点接入守卫脚本运行一周观察误报率和拦截效果再逐步扩展到其他项目。8.2 规则文件要有版本管理AGENTS.md是整个方案的“宪法”它必须和代码一样接受评审和版本管理。每次修改规则时观察后续两周内模型违反率的变化。如果某个规则被添加后模型仍然反复违反说明这条规则没有被有效传递需要改表达方式或加强验证手段。8.3 人类评审不可省略守卫脚本能拦截“规则明确的漂移”但拦截不了“语义层面的错误”。LLM 可能生成一个逻辑上完全错误、但风格完全合规的算法。因此所有 LLM 生成的代码必须经过人工评审后才允许合并。这是安全底线不能自动化替代。8.4 每次生成请求都应携带上下文指纹为了让“漂移”可追踪建议在每次 LLM 生成请求中记录上下文指纹提交了哪些文件、注入了哪些规范、调用了哪些工具。这一信息可以写入提交信息或在AGENTS.md头部维护一个context_hash字段。一旦后续发现漂移可以回溯是哪一次生成、哪一次规则变化导致的。8.5 用 eval 集做长期回归建议每两周从历史任务中抽取 10 到 20 个典型任务组成一个小的 eval 集统一用当前 Prompt 和规则体系跑一遍计算“规则违反率”。如果违反率上升说明最近的规则调整或模型升级引入了新的漂移风险。# 文件路径guard/run_eval.py 轻量 eval 回归统计 eval 集中每条任务生成结果的规则违反数 import subprocess import sys from pathlib import Path def run_eval(): eval_dir Path(eval_case) total_violations 0 case_count 0 for case_file in sorted(eval_dir.glob(*.md)): case_count 1 print(f运行用例: {case_file.name}) result subprocess.run( [python, guard/check_llm_output.py, --root, src], capture_outputTrue, textTrue, ) # 实际项目中应调用 LLM 生成代码后再执行守卫脚本 # 这里仅演示回归框架 if 检测到 in result.stdout: total_violations 1 print(f\n完成: {case_count} 个用例, 漂移用例数 {total_violations}) return 0 if total_violations 0 else 1 if __name__ __main__: raise SystemExit(run_eval())eval 集的价值不是提升单次生成质量而是让你对“规则系统整体是否在退化”保持敏感。即使模型版本升级只要违反率没有显著上升就说明防线仍然有效。8.6 关注工具链路的安全边界如果 Agent 有权限修改代码、执行命令一定要在工具调用层建立最小权限原则。守卫脚本只是代码质量防线工具权限边界是另一层安全防线。建议不要给 Agent 直接执行生产环境命令的权限。所有 Agent 操作默认进入隔离环境运行结果经过审核后再同步到主分支。使用 MCP 这类工具协议时只开放必要的工具操作前记录调用日志。9. 结尾从“防漂移”走向“可控生成”这套方案不需要引入复杂的 LLM 编排框架也不依赖特定模型能力。它真正改变的是团队对待 LLM 生成代码的视角从“模型应该自觉遵守规则”转变到“我们用工程手段确保规则被遵守”。落地的最小路径是创建一个AGENTS.md写入硬约束写一个 200 行以内的守卫脚本接入 CI然后坚持两周。你大概率会在第二周看到一次明显的变化LLM 生成的代码不再“随机试探边界”而是主动收敛到项目既有的轨道上。这就是把 LLM 从“有天赋的新人”变成“懂规矩的协作者”的关键一步。下一步值得继续深入的方向有三个一是把规则文件推广到更多项目形成组织级的规范库二是把 eval 集扩展到覆盖业务核心链路让回归测试粒度更细三是探索在 Agent 中间步骤中增加实时校验让漂移在“发生过程中”就被纠正而不是等到 PR 阶段才被发现。