Skip to content
清晨的一缕阳光
返回

Prompt 版本管理与测试实战

Prompt 版本管理与测试实战

随着 Prompt 在 AI 应用中的广泛应用,如何管理 Prompt 版本、保证 Prompt 质量成为工程化的核心问题。本文将深入解析 Prompt 版本控制、A/B 测试、回归测试等工程化方法。

一、为什么需要 Prompt 版本管理

1.1 Prompt 开发的挑战

Prompt 开发痛点:
┌─────────────────────────────────────┐
│ 1. 迭代频繁                           │
│    - 每天都要调整优化                │
│    - 难以追溯历史版本                │
├─────────────────────────────────────┤
│ 2. 效果不稳定                         │
│    - 模型更新导致效果变化            │
│    - 难以定位问题原因                │
├─────────────────────────────────────┤
│ 3. 协作困难                           │
│    - 多人修改难以合并                │
│    - 缺乏评审流程                    │
├─────────────────────────────────────┤
│ 4. 测试缺失                           │
│    - 无法保证修改后效果              │
│    - 缺乏自动化测试                  │
└─────────────────────────────────────┘

1.2 版本管理的价值

二、Prompt 版本控制系统

2.1 基于 Git 的版本管理

目录结构设计

prompts/
├── README.md                 # Prompt 目录说明
├── CHANGELOG.md             # 变更日志
├── version.json             # 版本元数据

├── v1.0.0/                  # 版本目录
│   ├── system-prompt.txt    # 系统提示词
│   ├── user-prompt.txt      # 用户提示词
│   └── metadata.json        # 版本元数据

├── v1.1.0/
│   ├── system-prompt.txt
│   ├── user-prompt.txt
│   └── metadata.json

└── latest/                  # 指向最新版本
    ├── system-prompt.txt -> ../v1.1.0/system-prompt.txt
    └── user-prompt.txt -> ../v1.1.0/user-prompt.txt

metadata.json 示例

{
  "version": "1.1.0",
  "created_at": "2026-05-10T10:00:00+08:00",
  "author": "zhangsan",
  "description": "优化 Few-Shot 示例,提升分类准确率",
  "changes": [
    "增加了 3 个负面样本示例",
    "调整了输出格式要求",
    "优化了边界条件说明"
  ],
  "test_results": {
    "accuracy": 0.95,
    "latency_avg": 1.2,
    "token_usage_avg": 850
  },
  "model_version": "gpt-4-0125-preview",
  "tags": ["classification", "few-shot"]
}

2.2 Prompt 差异对比

使用 diff 工具对比版本

# 对比两个版本的差异
diff -u prompts/v1.0.0/system-prompt.txt prompts/v1.1.0/system-prompt.txt

# 输出示例:
--- prompts/v1.0.0/system-prompt.txt
+++ prompts/v1.1.0/system-prompt.txt
@@ -5,6 +5,10 @@
 你是一个专业的文本分类助手。
 
 请根据以下规则对文本进行分类:
+1. 仔细分析文本的语义和情感
+2. 注意识别反讽和隐喻表达
 3. 输出格式必须为 JSON
+4. 如果无法确定类别,返回"unknown"
 
 示例:
 Input: "这个产品很好用"

使用 Git 管理 Prompt

# 初始化 Git 仓库
cd prompts
git init

# 添加初始版本
git add v1.0.0/
git commit -m "feat: 初始版本 v1.0.0"

# 创建新版本
git add v1.1.0/
git commit -m "feat: 优化 Few-Shot 示例 v1.1.0"

# 查看历史
git log --oneline

# 对比版本
git diff v1.0.0 v1.1.0

2.3 Prompt 注册中心

实现 Prompt 注册表

# prompt_registry.py
import json
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
from datetime import datetime

@dataclass
class PromptVersion:
    version: str
    created_at: str
    author: str
    description: str
    model_version: str
    tags: List[str]
    test_results: Dict[str, float]

class PromptRegistry:
    def __init__(self, prompts_dir: str = "prompts"):
        self.prompts_dir = Path(prompts_dir)
        self.registry_file = self.prompts_dir / "registry.json"
        self._load_registry()
    
    def _load_registry(self):
        """加载注册表"""
        if self.registry_file.exists():
            with open(self.registry_file, 'r', encoding='utf-8') as f:
                self.registry = json.load(f)
        else:
            self.registry = {"prompts": {}}
    
    def register(self, name: str, version: PromptVersion):
        """注册新版本"""
        if name not in self.registry["prompts"]:
            self.registry["prompts"][name] = {"versions": []}
        
        self.registry["prompts"][name]["versions"].append(asdict(version))
        self.registry["prompts"][name]["latest"] = version.version
        self._save_registry()
    
    def _save_registry(self):
        """保存注册表"""
        with open(self.registry_file, 'w', encoding='utf-8') as f:
            json.dump(self.registry, f, indent=2, ensure_ascii=False)
    
    def get_versions(self, name: str) -> List[PromptVersion]:
        """获取所有版本"""
        if name not in self.registry["prompts"]:
            return []
        
        return [
            PromptVersion(**v) 
            for v in self.registry["prompts"][name]["versions"]
        ]
    
    def get_latest(self, name: str) -> Optional[str]:
        """获取最新版本号"""
        if name not in self.registry["prompts"]:
            return None
        return self.registry["prompts"][name].get("latest")
    
    def load_prompt(self, name: str, version: str = None) -> Dict[str, str]:
        """加载 Prompt 内容"""
        if version is None:
            version = self.get_latest(name)
        
        if version is None:
            raise ValueError(f"Prompt '{name}' not found")
        
        prompt_dir = self.prompts_dir / name / version
        if not prompt_dir.exists():
            raise ValueError(f"Version {version} not found")
        
        prompt = {}
        for file in prompt_dir.glob("*.txt"):
            with open(file, 'r', encoding='utf-8') as f:
                prompt[file.stem] = f.read()
        
        return prompt

三、Prompt A/B 测试

3.1 A/B 测试框架

测试流程

graph LR
    A[流量] --> B{分流器}
    B -->|50%| C[版本 A]
    B -->|50%| D[版本 B]
    C --> E[结果收集]
    D --> E
    E --> F[效果对比]
    F --> G{决策}
    G -->|A 优 | H[回滚到 A]
    G -->|B 优 | I[升级到 B]

实现 A/B 测试

# ab_testing.py
import random
from typing import Dict, List, Callable
from dataclasses import dataclass
from datetime import datetime

@dataclass
class TestResult:
    version: str
    success_count: int
    failure_count: int
    avg_latency: float
    avg_tokens: float
    metrics: Dict[str, float]

class PromptABTester:
    def __init__(self, registry: PromptRegistry):
        self.registry = registry
        self.test_results: Dict[str, List[TestResult]] = {}
    
    def run_test(
        self,
        prompt_name: str,
        version_a: str,
        version_b: str,
        test_cases: List[Dict],
        evaluator: Callable,
        traffic_split: float = 0.5,
        iterations: int = 100
    ) -> Dict[str, TestResult]:
        """
        运行 A/B 测试
        
        Args:
            prompt_name: Prompt 名称
            version_a: 版本 A
            version_b: 版本 B
            test_cases: 测试用例列表
            evaluator: 评估函数
            traffic_split: 流量分配比例
            iterations: 迭代次数
        """
        results = {
            version_a: TestResult(
                version=version_a,
                success_count=0,
                failure_count=0,
                avg_latency=0,
                avg_tokens=0,
                metrics={}
            ),
            version_b: TestResult(
                version=version_b,
                success_count=0,
                failure_count=0,
                avg_latency=0,
                avg_tokens=0,
                metrics={}
            )
        }
        
        latencies = {version_a: [], version_b: []}
        tokens = {version_a: [], version_b: []}
        
        for _ in range(iterations):
            for test_case in test_cases:
                # 分配流量
                if random.random() < traffic_split:
                    version = version_a
                else:
                    version = version_b
                
                # 加载 Prompt
                prompt = self.registry.load_prompt(prompt_name, version)
                
                # 执行测试
                start_time = datetime.now()
                response = self._execute_prompt(prompt, test_case)
                latency = (datetime.now() - start_time).total_seconds()
                
                # 评估结果
                is_success = evaluator(response, test_case)
                
                # 统计结果
                result = results[version]
                if is_success:
                    result.success_count += 1
                else:
                    result.failure_count += 1
                
                latencies[version].append(latency)
                tokens[version].append(response.get('token_usage', 0))
        
        # 计算平均值
        for version in [version_a, version_b]:
            result = results[version]
            result.avg_latency = sum(latencies[version]) / len(latencies[version])
            result.avg_tokens = sum(tokens[version]) / len(tokens[version])
            result.metrics['accuracy'] = (
                result.success_count / 
                (result.success_count + result.failure_count)
            )
        
        return results
    
    def _execute_prompt(self, prompt: Dict, test_case: Dict) -> Dict:
        """执行 Prompt(调用 LLM API)"""
        # 实际项目中调用 LLM API
        pass
    
    def print_comparison(self, results: Dict[str, TestResult]):
        """打印对比结果"""
        print("\n=== A/B 测试结果 ===\n")
        
        versions = list(results.keys())
        result_a = results[versions[0]]
        result_b = results[versions[1]]
        
        print(f"{'指标':<15} {versions[0]:<15} {versions[1]:<15} {'提升':<10}")
        print("-" * 60)
        print(f"{'准确率':<15} {result_a.metrics['accuracy']:.2%}  "
              f"{result_b.metrics['accuracy']:.2%}  "
              f"{(result_b.metrics['accuracy'] - result_a.metrics['accuracy']):.2%}")
        print(f"{'平均延迟':<15} {result_a.avg_latency:.2f}s  "
              f"{result_b.avg_latency:.2f}s  "
              f"{((result_b.avg_latency - result_a.avg_latency) / result_a.avg_latency):.2%}")
        print(f"{'平均 Token':<15} {result_a.avg_tokens:.0f}  "
              f"{result_b.avg_tokens:.0f}  "
              f"{((result_b.avg_tokens - result_a.avg_tokens) / result_a.avg_tokens):.2%}")

3.2 测试用例设计

构建测试数据集

# test_cases.py
from typing import List, Dict

class PromptTestCases:
    """Prompt 测试用例构建器"""
    
    @staticmethod
    def build_classification_test_cases() -> List[Dict]:
        """构建分类任务测试用例"""
        return [
            {
                "id": "case_001",
                "input": "这个产品很好用,推荐购买",
                "expected": {"category": "positive", "confidence": 0.9},
                "difficulty": "easy"
            },
            {
                "id": "case_002",
                "input": "一般般吧,没什么特别的",
                "expected": {"category": "neutral", "confidence": 0.8},
                "difficulty": "medium"
            },
            {
                "id": "case_003",
                "input": "说是很好用,结果用了一次就坏了",
                "expected": {"category": "negative", "confidence": 0.85},
                "difficulty": "hard"  # 反讽表达
            },
            # ... 更多测试用例
        ]
    
    @staticmethod
    def build_extraction_test_cases() -> List[Dict]:
        """构建信息抽取测试用例"""
        return [
            {
                "id": "extract_001",
                "input": "张三,男,1990 年出生,北京市朝阳区",
                "expected": {
                    "name": "张三",
                    "gender": "",
                    "birth_year": 1990,
                    "location": "北京市朝阳区"
                },
                "difficulty": "easy"
            },
            # ... 更多测试用例
        ]

四、回归测试

4.1 回归测试框架

实现回归测试

# regression_testing.py
import json
from typing import List, Dict
from pathlib import Path

class PromptRegressionTester:
    """Prompt 回归测试器"""
    
    def __init__(self, registry: PromptRegistry):
        self.registry = registry
        self.test_suite_dir = Path("test_suites")
    
    def create_test_suite(self, prompt_name: str, test_cases: List[Dict]):
        """创建测试套件"""
        suite_dir = self.test_suite_dir / prompt_name
        suite_dir.mkdir(parents=True, exist_ok=True)
        
        # 保存测试用例
        with open(suite_dir / "test_cases.json", 'w', encoding='utf-8') as f:
            json.dump(test_cases, f, indent=2, ensure_ascii=False)
        
        # 保存基准结果
        baseline_results = self._run_tests(prompt_name, test_cases)
        with open(suite_dir / "baseline.json", 'w', encoding='utf-8') as f:
            json.dump(baseline_results, f, indent=2, ensure_ascii=False)
    
    def run_regression_test(
        self, 
        prompt_name: str, 
        new_version: str
    ) -> Dict:
        """
        运行回归测试
        
        Returns:
            测试结果报告
        """
        suite_dir = self.test_suite_dir / prompt_name
        
        # 加载测试用例
        with open(suite_dir / "test_cases.json", 'r', encoding='utf-8') as f:
            test_cases = json.load(f)
        
        # 加载基准结果
        with open(suite_dir / "baseline.json", 'r', encoding='utf-8') as f:
            baseline = json.load(f)
        
        # 运行新版本测试
        new_results = self._run_tests(
            prompt_name, 
            test_cases, 
            version=new_version
        )
        
        # 对比结果
        report = self._compare_results(baseline, new_results)
        
        return report
    
    def _run_tests(
        self, 
        prompt_name: str, 
        test_cases: List[Dict],
        version: str = None
    ) -> List[Dict]:
        """运行测试"""
        results = []
        
        for test_case in test_cases:
            prompt = self.registry.load_prompt(prompt_name, version)
            response = self._execute_prompt(prompt, test_case)
            
            result = {
                "case_id": test_case["id"],
                "expected": test_case["expected"],
                "actual": response,
                "passed": self._evaluate(response, test_case)
            }
            results.append(result)
        
        return results
    
    def _compare_results(
        self, 
        baseline: List[Dict], 
        new_results: List[Dict]
    ) -> Dict:
        """对比测试结果"""
        baseline_passed = sum(1 for r in baseline if r["passed"])
        new_passed = sum(1 for r in new_results if r["passed"])
        
        baseline_total = len(baseline)
        new_total = len(new_results)
        
        # 找出回归的用例
        regressions = []
        for i, (b, n) in enumerate(zip(baseline, new_results)):
            if b["passed"] and not n["passed"]:
                regressions.append({
                    "case_id": b["case_id"],
                    "baseline_result": b,
                    "new_result": n
                })
        
        report = {
            "baseline": {
                "passed": baseline_passed,
                "total": baseline_total,
                "pass_rate": baseline_passed / baseline_total
            },
            "new": {
                "passed": new_passed,
                "total": new_total,
                "pass_rate": new_passed / new_total
            },
            "regressions": regressions,
            "regression_count": len(regressions),
            "passed": len(regressions) == 0
        }
        
        return report
    
    def print_report(self, report: Dict):
        """打印测试报告"""
        print("\n=== 回归测试报告 ===\n")
        
        print(f"基准版本:{report['baseline']['passed']}/{report['baseline']['total']} "
              f"({report['baseline']['pass_rate']:.2%})")
        print(f"新版本:  {report['new']['passed']}/{report['new']['total']} "
              f"({report['new']['pass_rate']:.2%})")
        print(f"\n回归用例数:{report['regression_count']}")
        
        if report['regressions']:
            print("\n回归用例详情:")
            for reg in report['regressions'][:5]:  # 只显示前 5 个
                print(f"  - {reg['case_id']}")

4.2 CI/CD 集成

GitHub Actions 示例

# .github/workflows/prompt-test.yml
name: Prompt 测试

on:
  push:
    paths:
      - 'prompts/**'
  pull_request:
    paths:
      - 'prompts/**'

jobs:
  regression-test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'
      
      - name: Install dependencies
        run: |
          pip install -r requirements.txt
      
      - name: Run regression tests
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
        run: |
          python tests/run_regression_tests.py
      
      - name: Upload test report
        uses: actions/upload-artifact@v3
        if: always()
        with:
          name: test-report
          path: test_reports/

五、实战案例

5.1 电商评论分类 Prompt 版本管理

项目结构

prompts/
└── ecommerce-classification/
    ├── registry.json
    ├── v1.0.0/
    │   ├── system-prompt.txt
    │   ├── user-prompt.txt
    │   └── metadata.json
    ├── v1.1.0/
    │   └── ...
    └── latest/
        └── ...

版本迭代记录

# CHANGELOG.md

## [1.1.0] - 2026-05-10
### 改进
- 增加反讽识别能力
- 优化边界条件处理
- 增加 unknown 类别

### 效果
- 准确率:92% → 95%
- 召回率:90% → 93%
- F1 分数:0.91 → 0.94

## [1.0.0] - 2026-05-01
### 初始版本
- 基础分类功能
- 支持正面、中性、负面三类

5.2 A/B 测试结果分析

测试报告

=== A/B 测试结果 ===

指标              v1.0.0          v1.1.0          提升
------------------------------------------------------------
准确率            92.00%          95.00%          +3.00%
平均延迟          1.15s           1.20s           +4.35%
平均 Token        780             850             +8.97%

结论:v1.1.0 准确率显著提升,虽然延迟和 token 略有增加,但整体效果更优
建议:升级到 v1.1.0

六、最佳实践

6.1 版本管理建议

  1. 语义化版本

    • 主版本号。次版本号。修订号(如 1.1.0)
    • 重大变更升级主版本号
    • 功能增加升级次版本号
    • 小修小补升级修订号
  2. 提交规范

    feat: 新增 XXX 功能
    fix: 修复 XXX 问题
    perf: 性能优化
    docs: 文档更新
    test: 测试用例更新
  3. 变更日志

    • 记录每次变更的原因
    • 记录效果对比数据
    • 记录测试覆盖率

6.2 测试建议

  1. 测试用例覆盖

    • 正常场景(80%)
    • 边界场景(15%)
    • 异常场景(5%)
  2. 评估指标

    • 准确率
    • 召回率
    • F1 分数
    • 平均延迟
    • Token 使用量
  3. 回归测试频率

    • 每次修改后必须运行
    • 每日定时运行(可选)
    • 模型更新后必须运行

七、总结

7.1 核心要点

  1. 版本管理

    • 使用 Git 管理 Prompt 版本
    • 建立注册中心追踪版本
    • 记录详细的变更日志
  2. A/B 测试

    • 科学分配流量
    • 设计合理的测试用例
    • 基于数据做决策
  3. 回归测试

    • 建立测试套件
    • 保存基准结果
    • CI/CD 自动化

7.2 工具推荐

工具用途链接
Git版本控制https://git-scm.com/
LangSmithPrompt 测试https://smith.langchain.com/
PromptLayerPrompt 管理https://promptlayer.com/
DSPyPrompt 优化https://dspy-docs.vercel.app/

参考资料


分享这篇文章到:

上一篇文章
MySQL 8.0 新特性
下一篇文章
打破教育困局,必须向着光明走去:倾听《十三邀》的深刻反思