大模型应用的Prompt版本管理:像管理代码一样管理Prompt

0 阅读1分钟

软件工程师对代码版本管理已经习以为常——每次改动都有 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 版本管理不是高级需求,而是基本工程卫生。核心要做到:

  1. Prompt 文件化:从代码中提取出来,独立管理
  2. 变更可追溯:每次改动都有记录(Git 或注册表)
  3. 变更可回滚:能在5分钟内回到任意历史版本
  4. 效果可对比:A/B 测试验证新版本是否真的更好

一旦你的 Prompt 变成了可版本化管理的资产,迭代速度和可靠性都会显著提升。