你在 staging 环境测试了一个新的 JSON 输出格式要求,更新了 prompt 模板,测试通过后部署到生产。结果生产环境的某些 Agent 实例仍在使用旧模板(因为它们从 Redis 缓存里读取 prompt,而缓存没有被刷新),输出了旧格式的 JSON,导致下游解析失败。或者在 LangGraph 里,有 3 个节点都在调用「代码审查」功能,每个节点的开发者各自维护了一份 prompt 模板,一次功能迭代时只有其中一个节点的模板被更新了,三个节点现在使用三个略微不同版本的 prompt,输出质量参差不齐,bug 极难复现(取决于哪个节点处理了请求)。
常见原因
1. Prompt 模板分散在多个文件或代码位置,没有集中管理
每个 Agent 类里都硬编码了自己的 system prompt,整个项目有 15 处 SYSTEM_PROMPT = """...""" 的定义,功能迭代时只改了其中几处,其余的静默过期。
怎么判断:在代码库里搜索 system_prompt、SYSTEM_PROMPT、system=""" 等关键词,统计定义数量。如果有 5 个以上的独立定义,就缺乏集中管理。
2. Prompt 存在数据库或 Redis 里,但没有版本号
Prompt 被存储在一个可以随时修改的数据库记录里,没有版本号和生效时间戳。更新 prompt 后,不同的 Agent 实例在缓存失效时间不同的情况下,会在一段时间内使用不同版本的 prompt。
怎么判断:查看存储 prompt 的数据库表或 Redis key,是否有 version、updated_at、hash 等字段记录版本信息。
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 里对比回滚前后的输出质量,确认问题已解决。