不同版本的 Prompt 模板互相打架

多 Agent 系统或多环境部署中,不同版本的 prompt 模板并存,同一个任务在生产环境和暂存环境使用了不同的指令,导致输出格式或行为不一致,难以复现和调试。本文分析模板漂移根因并给出版本化管理方案。

你在 staging 环境测试了一个新的 JSON 输出格式要求,更新了 prompt 模板,测试通过后部署到生产。结果生产环境的某些 Agent 实例仍在使用旧模板(因为它们从 Redis 缓存里读取 prompt,而缓存没有被刷新),输出了旧格式的 JSON,导致下游解析失败。或者在 LangGraph 里,有 3 个节点都在调用「代码审查」功能,每个节点的开发者各自维护了一份 prompt 模板,一次功能迭代时只有其中一个节点的模板被更新了,三个节点现在使用三个略微不同版本的 prompt,输出质量参差不齐,bug 极难复现(取决于哪个节点处理了请求)。

常见原因

1. Prompt 模板分散在多个文件或代码位置,没有集中管理

每个 Agent 类里都硬编码了自己的 system prompt,整个项目有 15 处 SYSTEM_PROMPT = """...""" 的定义,功能迭代时只改了其中几处,其余的静默过期。

怎么判断:在代码库里搜索 system_promptSYSTEM_PROMPTsystem=""" 等关键词,统计定义数量。如果有 5 个以上的独立定义,就缺乏集中管理。

2. Prompt 存在数据库或 Redis 里,但没有版本号

Prompt 被存储在一个可以随时修改的数据库记录里,没有版本号和生效时间戳。更新 prompt 后,不同的 Agent 实例在缓存失效时间不同的情况下,会在一段时间内使用不同版本的 prompt。

怎么判断:查看存储 prompt 的数据库表或 Redis key,是否有 versionupdated_athash 等字段记录版本信息。

3. 多环境(dev/staging/prod)用了不同的 prompt 文件,但没有 diff 追踪

staging 环境的 prompts/review.txt 和 prod 环境的 prompts/review.txt 内容不同,但没有工具对比两者的差异。开发者以为 staging 验证的就是 prod 将运行的,实际上不是。

怎么判断:比较 staging 和 prod 环境的 prompt 文件(或数据库记录),检查内容是否一致。diff 命令或 git 的跨分支比较是最快的方法。

4. Prompt 里引用了环境变量或外部数据,不同环境值不同

Prompt 模板里有 {COMPANY_NAME}{API_VERSION} 这样的占位符,不同环境的配置文件里这些变量的值不同,导致最终 prompt 在不同环境下有微妙的差异。

怎么判断:在 prompt 模板里搜索所有占位符 {...},检查它们在所有环境的实际取值是否一致。

5. 热更新了生产环境的 prompt,但没有记录更改日志

为了快速修复一个生产问题,直接在 prod 数据库里改了 prompt,没有同步更新代码库。下次代码部署时,代码库里的旧版 prompt 会覆盖生产环境的热修复版本,问题复现。

怎么判断:对比生产数据库里当前的 prompt 与代码库里 prompts/ 目录下的内容,检查是否有「生产领先于代码库」的情况。

6. A/B 测试的 prompt 变体没有被正确标记,混入了主流量

为了测试新版 prompt,在 5% 的流量里用了实验性模板。实验结束后,回收代码时误删了 A/B 测试的 feature flag,导致实验性 prompt 变成了 100% 流量的 prompt。

怎么判断:检查所有 prompt 变体的 feature flag 或 experiment ID 是否都有明确的开关,以及开关的当前状态是否符合预期。

最短修复路径

Step 1:建立集中的 Prompt Registry,统一管理所有 prompt

# prompts/registry.py — 唯一的 prompt 定义中心
from dataclasses import dataclass
import hashlib

@dataclass(frozen=True)
class PromptTemplate:
    name: str
    version: str          # 语义版本,如 "2.1.0"
    content: str
    
    @property
    def hash(self) -> str:
        return hashlib.sha256(self.content.encode()).hexdigest()[:8]
    
    def render(self, **kwargs) -> str:
        return self.content.format(**kwargs)

class PromptRegistry:
    _templates: dict[str, PromptTemplate] = {}
    
    @classmethod
    def register(cls, name: str, version: str):
        def decorator(fn):
            content = fn()
            template = PromptTemplate(name=name, version=version, content=content)
            cls._templates[name] = template
            return template
        return decorator
    
    @classmethod
    def get(cls, name: str) -> PromptTemplate:
        if name not in cls._templates:
            raise KeyError(f"Prompt '{name}' 未在 Registry 里注册")
        return cls._templates[name]

# 所有 prompt 在这里定义,禁止在其他文件里硬编码 prompt 字符串
@PromptRegistry.register("code_review", version="2.1.0")
def code_review_prompt():
    return """你是一个严格的代码审查员。
请按照以下格式输出 JSON:
{"issues": [...], "score": 0-100, "summary": "..."}
不要在 JSON 前后加任何文字。"""

Step 2:在 Trace 里记录使用的 prompt 版本和 hash

def create_llm_span(
    agent_id: str,
    prompt_name: str,
    rendered_prompt: str,
    langfuse_client
):
    template = PromptRegistry.get(prompt_name)
    
    span = langfuse_client.generation(
        name=f"{agent_id}:{prompt_name}",
        input=rendered_prompt,
        metadata={
            "prompt_name": prompt_name,
            "prompt_version": template.version,
            "prompt_hash": template.hash,  # 用于快速比对版本
        }
    )
    return span

Step 3:用 git 管理 prompt 文件,禁止直接修改数据库

# 目录结构
prompts/
  code_review/
    v2.1.0.txt   # 当前版本
    v2.0.0.txt   # 上一版本(保留用于回滚)
    CHANGELOG.md  # 每次 prompt 变更的说明

# 禁止直接修改 prod 数据库里的 prompt
# 所有 prompt 变更必须通过 PR -> CI -> 部署流程
# CI 检查:确认 staging 和 prod 分支的 prompt 文件一致
diff <(git show staging:prompts/code_review/current.txt) \
     <(git show main:prompts/code_review/current.txt)

Step 4:部署前自动 diff prompt 变更

# scripts/check_prompt_drift.py
import os, hashlib

def check_all_prompts():
    """对比代码库里的 prompt 与生产数据库里的 prompt,发现漂移。"""
    drift_found = False
    
    for prompt_name, template in PromptRegistry._templates.items():
        # 从生产数据库读取当前实际使用的 prompt
        prod_content = prod_db.get_prompt(prompt_name)
        prod_hash = hashlib.sha256(prod_content.encode()).hexdigest()[:8]
        
        if prod_hash != template.hash:
            print(f"DRIFT: {prompt_name}")
            print(f"  代码库版本: {template.version} ({template.hash})")
            print(f"  生产版本:   unknown ({prod_hash})")
            drift_found = True
    
    if drift_found:
        raise SystemExit("发现 prompt 漂移,请在部署前同步 prompt 版本")

if __name__ == "__main__":
    check_all_prompts()

Step 5:为 A/B 测试 prompt 使用明确的 feature flag

from functools import lru_cache

def get_prompt_for_request(prompt_name: str, user_id: str) -> PromptTemplate:
    """根据 feature flag 选择 prompt 版本。"""
    # 检查是否在实验组里
    if feature_flags.is_enabled("code_review_v3_test", user_id=user_id):
        return PromptRegistry.get(f"{prompt_name}_v3_experiment")
    
    # 默认使用正式版本
    return PromptRegistry.get(prompt_name)

# 实验结束后,只需关闭 feature flag,不需要修改 prompt 代码
# feature_flags.disable("code_review_v3_test")

预防建议

  • 所有 prompt 定义集中在 PromptRegistry 里,禁止在 Agent 代码里硬编码 prompt 字符串
  • 每个 prompt 模板必须有语义版本号,版本号变更触发 changelog 更新
  • 在 Trace/LangSmith/Langfuse 里记录每次 LLM 调用使用的 prompt 版本和 hash,方便按版本分析质量差异
  • Prompt 变更和代码变更一起走 PR 流程,不允许直接修改生产数据库里的 prompt
  • 部署前自动 diff prompt 变更,确认 staging 验证的 prompt 与将要部署到 prod 的一致
  • A/B 测试的 prompt 变体必须用 feature flag 控制,实验结束后关闭 flag,不要删除代码
  • 每隔 1-2 个月对 prod 环境的 prompt 和代码库做一次全量对账,发现「生产领先」的情况及时修复

常见问答 (FAQ)

Q: 把 prompt 存在代码库里(文本文件)vs 存在数据库里,哪个更推荐? A: 对于「不需要运营人员在生产环境实时修改」的 prompt,代码库更推荐:版本控制自带,PR 流程保证质量,git blame 可以追溯改动原因。对于「需要非技术人员快速修改」的 prompt(如营销文案、客服话术),数据库更推荐,但必须加版本号字段和完整的变更审计日志。

Q: LangSmith 和 Langfuse 的 Prompt Management 功能能解决这个问题吗? A: 能解决一部分:LangSmith 的 Prompt Hub 和 Langfuse 的 Prompt Management 都提供了 prompt 的版本控制和 A/B 测试功能,可以在 UI 里管理 prompt 版本并跟踪各版本的性能。但它们不能防止开发者在代码里绕过这些工具直接硬编码 prompt。需要通过代码规范(如 lint 规则禁止在特定目录外定义 prompt 字符串)来强制所有 prompt 都通过统一的管理工具。

Q: 如何处理「大多数环境相同,只有少数字段不同」的环境差异 prompt? A: 用「base prompt + 环境 overlay」的组合模式:base prompt 存在代码库里(环境无关),环境特定的差异存在环境配置文件里(如 prompts/env_overrides/prod.yaml)。在渲染 prompt 时,先取 base prompt,再用环境 overlay 覆盖特定字段。这样 base prompt 的版本控制是统一的,环境差异也是显式的、可追踪的。

Q: 如何快速回滚到上一个版本的 prompt? A: 在 PromptRegistry 里保留前一个版本的模板(如 code_review_v2_0_0),回滚时只需把当前版本的别名指向旧版本。如果 prompt 存在数据库里,用数据库的版本号字段和「active 版本指针」来切换,不需要重新部署代码。回滚后在 LangSmith 里对比回滚前后的输出质量,确认问题已解决。

相关阅读

标签: #AI 编程 #Agents #排查