软件工程师对代码版本管理已经习以为常——每次改动都有 commit、有 diff、可以回滚。但对 Prompt,大多数团队还处于"随手改,改完就忘了"的状态。
Prompt 是 LLM 应用最核心的逻辑,理应像代码一样严格管理。本文讲如何建立 Prompt 版本管理体系。
为什么要管理 Prompt
先确认问题的严重性:
场景1:Prompt 改了,效果变差,但不知道哪次改动导致的 你上周改了系统 Prompt 的一句话,本周客服满意度下降了。但你不知道是那句话的问题、还是别的原因。如果有 Prompt 版本记录,可以立刻回滚到上周版本验证。
场景2:多人修改 Prompt,互相覆盖 产品经理说"把语气改得更亲切一点",工程师说"加上不要编造信息的约束",两人都直接改了生产环境的 Prompt,但彼此都不知道对方改了什么。
场景3:A/B 测试两个 Prompt,但没有记录 测了两个版本,记录了结果,但没有记录每个版本的具体 Prompt 内容。事后想重建那个效果好的版本,找不到了。
这些场景都是真实发生的工程问题。
Prompt 版本管理的三个层次
层次一:最简单 - Git 管理 Prompt 文件
把 Prompt 从代码里提取出来,存在独立文件中,用 Git 管理:
your-project/
├── prompts/
│ ├── system/
│ │ ├── rag_assistant.md
│ │ ├── code_reviewer.md
│ │ └── customer_service.md
│ ├── templates/
│ │ ├── rag_query.jinja2
│ │ └── tool_use.jinja2
│ └── README.md
Prompt 文件格式(带元数据):
---
name: rag_assistant
version: 1.3.0
description: RAG 客服助手系统提示词
author: 张三
last_updated: 2026-05-02
model_tested: gpt-4o, gpt-4o-mini
---
# System Prompt
你是{{company_name}}的智能客服助手。
## 工作原则
1. 只基于提供的文档内容回答问题
2. 如果文档中没有相关信息,明确说明"根据现有资料,我暂时无法回答这个问题"
3. 回答要简洁、准确、有帮助
4. 遇到涉及价格、合同、法律的问题,引导用户联系人工客服
## 回答格式
- 简单问题:直接回答,2-3句话
- 复杂问题:分点说明
- 涉及操作步骤:用有序列表
## 禁止行为
- 不得编造任何信息
- 不得评价竞争对手
- 不得讨论政治、宗教等敏感话题
代码中加载 Prompt:
import yaml
from pathlib import Path
from string import Template
import re
def load_prompt(name: str, variables: dict = None) -> str:
"""加载 Prompt 文件,支持变量替换"""
prompt_dir = Path(__file__).parent / "prompts"
# 找到对应文件
for ext in [".md", ".txt", ".jinja2"]:
filepath = prompt_dir / "system" / f"{name}{ext}"
if filepath.exists():
break
else:
raise FileNotFoundError(f"Prompt 文件不存在: {name}")
content = filepath.read_text(encoding="utf-8")
# 去掉 YAML 前言
if content.startswith("---"):
_, _, content = content.split("---", 2)
content = content.strip()
# 变量替换
if variables:
for key, value in variables.items():
content = content.replace(f"{{{{{key}}}}}", str(value))
return content
# 使用
system_prompt = load_prompt("rag_assistant", {
"company_name": "我们公司"
})
层次二:进阶 - Prompt 注册表
对于大型项目,建立一个 Prompt 注册表,统一管理:
import sqlite3
import hashlib
from datetime import datetime
from typing import Optional
class PromptRegistry:
"""Prompt 版本注册表"""
def __init__(self, db_path: str = "prompts.db"):
self.conn = sqlite3.connect(db_path)
self._init_db()
def _init_db(self):
self.conn.execute("""
CREATE TABLE IF NOT EXISTS prompts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
version TEXT NOT NULL,
content TEXT NOT NULL,
hash TEXT NOT NULL,
description TEXT,
author TEXT,
created_at TEXT NOT NULL,
is_active BOOLEAN DEFAULT 0,
metadata TEXT -- JSON 格式的额外信息
)
""")
self.conn.execute("""
CREATE TABLE IF NOT EXISTS prompt_experiments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
experiment_name TEXT NOT NULL,
prompt_name TEXT NOT NULL,
prompt_version TEXT NOT NULL,
variant TEXT, -- 'control' 或 'treatment'
start_date TEXT,
end_date TEXT,
results TEXT, -- JSON
winner TEXT
)
""")
self.conn.commit()
def register(
self,
name: str,
content: str,
version: str = None,
description: str = "",
author: str = "",
metadata: dict = None,
) -> str:
"""注册新 Prompt 版本"""
content_hash = hashlib.sha256(content.encode()).hexdigest()[:12]
# 自动生成版本号(如果未指定)
if version is None:
existing = self.get_latest_version(name)
if existing:
parts = existing.split(".")
parts[-1] = str(int(parts[-1]) + 1)
version = ".".join(parts)
else:
version = "1.0.0"
# 检查是否有内容相同的版本(幂等)
existing = self.conn.execute(
"SELECT version FROM prompts WHERE name=? AND hash=?",
(name, content_hash)
).fetchone()
if existing:
print(f"内容相同的版本已存在: {name} v{existing[0]}")
return existing[0]
import json
self.conn.execute("""
INSERT INTO prompts (name, version, content, hash, description, author, created_at, metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (
name, version, content, content_hash,
description, author,
datetime.utcnow().isoformat(),
json.dumps(metadata or {})
))
self.conn.commit()
print(f"已注册: {name} v{version} (hash: {content_hash})")
return version
def activate(self, name: str, version: str):
"""激活某个版本(标记为当前生产版本)"""
# 先取消所有版本的激活状态
self.conn.execute(
"UPDATE prompts SET is_active=0 WHERE name=?", (name,)
)
# 激活指定版本
self.conn.execute(
"UPDATE prompts SET is_active=1 WHERE name=? AND version=?",
(name, version)
)
self.conn.commit()
print(f"已激活: {name} v{version}")
def get_active(self, name: str) -> Optional[str]:
"""获取当前激活版本的内容"""
row = self.conn.execute(
"SELECT content FROM prompts WHERE name=? AND is_active=1",
(name,)
).fetchone()
return row[0] if row else None
def get_version(self, name: str, version: str) -> Optional[str]:
"""获取特定版本"""
row = self.conn.execute(
"SELECT content FROM prompts WHERE name=? AND version=?",
(name, version)
).fetchone()
return row[0] if row else None
def get_history(self, name: str) -> list:
"""获取版本历史"""
rows = self.conn.execute(
"SELECT version, hash, description, author, created_at, is_active FROM prompts WHERE name=? ORDER BY created_at DESC",
(name,)
).fetchall()
return [
{"version": r[0], "hash": r[1], "description": r[2],
"author": r[3], "created_at": r[4], "is_active": bool(r[5])}
for r in rows
]
def get_latest_version(self, name: str) -> Optional[str]:
"""获取最新版本号"""
row = self.conn.execute(
"SELECT version FROM prompts WHERE name=? ORDER BY created_at DESC LIMIT 1",
(name,)
).fetchone()
return row[0] if row else None
def diff(self, name: str, version1: str, version2: str) -> str:
"""对比两个版本的差异"""
import difflib
content1 = self.get_version(name, version1) or ""
content2 = self.get_version(name, version2) or ""
diff = difflib.unified_diff(
content1.splitlines(keepends=True),
content2.splitlines(keepends=True),
fromfile=f"{name} v{version1}",
tofile=f"{name} v{version2}",
)
return "".join(diff)
层次三:完整方案 - 配合 A/B 测试
class PromptExperimentManager:
"""Prompt A/B 测试管理器"""
def __init__(self, registry: PromptRegistry):
self.registry = registry
def start_experiment(
self,
experiment_name: str,
prompt_name: str,
control_version: str, # 现有版本
treatment_version: str, # 新版本
traffic_split: float = 0.5, # 50% 流量给新版本
):
"""启动 A/B 实验"""
import json
self.registry.conn.execute("""
INSERT INTO prompt_experiments
(experiment_name, prompt_name, prompt_version, variant, start_date, results)
VALUES
(?, ?, ?, 'control', ?, '{}'),
(?, ?, ?, 'treatment', ?, '{}')
""", (
experiment_name, prompt_name, control_version, datetime.utcnow().isoformat(),
experiment_name, prompt_name, treatment_version, datetime.utcnow().isoformat(),
))
self.registry.conn.commit()
# 保存流量分配配置
self._traffic_config = {
prompt_name: {
"control": control_version,
"treatment": treatment_version,
"split": traffic_split,
"experiment": experiment_name,
}
}
def get_prompt_for_request(self, prompt_name: str, user_id: str) -> tuple[str, str]:
"""根据用户 ID 决定给哪个版本(确保同一用户始终看到同一版本)"""
config = self._traffic_config.get(prompt_name)
if not config:
# 没有实验,用激活版本
return self.registry.get_active(prompt_name), "production"
# 用 user_id 哈希决定版本(确保一致性)
hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
use_treatment = (hash_val % 100) / 100 < config["split"]
if use_treatment:
version = config["treatment"]
variant = "treatment"
else:
version = config["control"]
variant = "control"
content = self.registry.get_version(prompt_name, version)
return content, variant
def record_outcome(
self,
experiment_name: str,
variant: str,
metric: str,
value: float,
):
"""记录实验结果"""
import json
row = self.registry.conn.execute(
"SELECT results FROM prompt_experiments WHERE experiment_name=? AND variant=?",
(experiment_name, variant)
).fetchone()
if row:
results = json.loads(row[0])
if metric not in results:
results[metric] = []
results[metric].append(value)
self.registry.conn.execute(
"UPDATE prompt_experiments SET results=? WHERE experiment_name=? AND variant=?",
(json.dumps(results), experiment_name, variant)
)
self.registry.conn.commit()
实际使用示例
# 初始化
registry = PromptRegistry("prompts.db")
# 注册第一个版本
v1 = registry.register(
name="customer_service",
content="你是一个客服助手。请回答用户问题。",
description="初始版本",
author="张三",
)
registry.activate("customer_service", v1)
# 新版本(改进了格式要求)
v2 = registry.register(
name="customer_service",
content="""你是一个专业的客服助手。
## 回答原则
- 简洁准确
- 友好礼貌
- 不能编造信息
""",
description="增加了格式约束和礼貌要求",
author="李四",
)
# 查看 diff
print(registry.diff("customer_service", v1, v2))
# 做 A/B 测试
exp_manager = PromptExperimentManager(registry)
exp_manager.start_experiment(
"customer_service_v2_test",
"customer_service",
control_version=v1,
treatment_version=v2,
traffic_split=0.2, # 20% 用户看新版本
)
# 在应用中使用
user_id = "user_12345"
prompt_content, variant = exp_manager.get_prompt_for_request("customer_service", user_id)
print(f"用户 {user_id} 看到的版本: {variant}")
# 记录结果(比如用户评分)
exp_manager.record_outcome("customer_service_v2_test", variant, "user_rating", 4.5)
最佳实践
1. Prompt 变更要 Code Review 和代码一样,Prompt 的修改也应该走 PR 流程,由至少一人审核。
2. 变更要有测试用例 每次修改 Prompt 前,准备好5-10个测试问题,修改后回归测试,确认没有退化。
3. 生产变更要有回滚方案 激活新版本前,确认可以快速回滚(注册表模式天然支持)。
4. 记录变更动机
description 字段要写清楚"为什么改",而不只是"改了什么"。
总结
Prompt 版本管理不是高级需求,而是基本工程卫生。核心要做到:
- Prompt 文件化:从代码中提取出来,独立管理
- 变更可追溯:每次改动都有记录(Git 或注册表)
- 变更可回滚:能在5分钟内回到任意历史版本
- 效果可对比:A/B 测试验证新版本是否真的更好
一旦你的 Prompt 变成了可版本化管理的资产,迭代速度和可靠性都会显著提升。