LLM 提示词版本管理:像管理数据库迁移一样管理你的 Prompt 演进
LLM 提示词版本管理:像管理数据库迁移一样管理你的 Prompt 演进
一、Prompt 的迭代噩梦:当最新版本覆盖了旧版本后,你失去了回溯能力
你做了一个 Prompt 优化,把"请用简洁的语言回答"改成"请用不超过 50 个字的简洁语言回答"。效果看起来不错,更短了。两周后,用户反馈说回答太短、缺乏细节。你回想起之前的版本似乎更好,但你已经不记得原来的 Prompt 到底写了什么——它被覆盖了,没有 Git 记录(它存在数据库里),没有版本号。
更大的问题是 A/B 测试。你想同时跑 3 个版本的 Prompt,比较它们的回答质量。如果 Prompt 分散在代码、配置文件和数据库中,版本管理几乎不可能。你需要一套 Prompt 版本管理系统,和数据库迁移一样——可追溯、可回滚、可对比。
graph TB subgraph Prompt_Migration[Prompt 迁移系统] V1[prompt_v1: 初始版本<br/>2024-01-01] --> V2[prompt_v2: 增加指令<br/>2024-01-15] V2 --> V3[prompt_v3: 修改示例<br/>2024-02-01] V3 --> V4[prompt_v4: 当前版本<br/>2024-02-15] end subgraph Testing[测试流程] T1[单元测试] T2[A/B 测试<br/>V3 vs V4] T3[回归测试] end V4 --> T1 V3 --> T2 V4 --> T2 V4 --> T3 T2 -->|V4 胜出| D1[部署 V4] T2 -->|V3 胜出| D2[回滚到 V3] style V4 fill:#51cf66,color:#fff style D2 fill:#ffd43b,color:#000本文将设计一套 Prompt 版本管理方案,覆盖迁移文件结构、版本追踪和 A/B 测试集成。
二、Prompt 迁移的设计:借鉴数据库 Migration 的思路
数据库 Migration 的四个核心能力恰好是 Prompt 版本管理需要的:
- 版本编号:每个迁移文件有唯一的版本号(时间戳)
- 正向迁移(Up):应用 Prompt 变更
- 回滚(Down):回退到上一版本
- 状态追踪:记录当前使用的版本号
Prompt 的版本文件可以组织为:
prompts/ ├── migrations/ │ ├── 1704067200_initial_prompt.json │ ├── 1704153600_add_concise_instruction.json │ ├── 1704240000_update_examples.json │ └── 1704326400_current.json └── active_version.txt # 记录当前生效的版本每个迁移文件的结构:
{ "version": "1704153600", "description": "增加简洁性指令,限制回答不超过50字", "template": "你是一个{{role}}助手。请用不超过50个字简洁回答以下问题:\n{{question}}", "variables": ["role", "question"], "model_params": { "temperature": 0.3, "max_tokens": 100 }, "parent_version": "1704067200", "created_at": "2024-01-15T10:00:00Z" }三、完整的 Prompt 版本管理实现
// prompt-manager.ts import { readFileSync, writeFileSync, readdirSync, existsSync } from 'fs'; import { join } from 'path'; interface PromptVersion { version: string; description: string; template: string; variables: string[]; model_params: { temperature: number; max_tokens: number; [key: string]: any; }; parent_version: string | null; created_at: string; } class PromptManager { private baseDir: string; private versions: Map<string, PromptVersion> = new Map(); private activeVersion: string | null = null; constructor(baseDir: string) { if (!existsSync(baseDir)) { throw new Error(`Prompt directory does not exist: ${baseDir}`); } this.baseDir = baseDir; this.loadVersions(); this.loadActiveVersion(); } // 加载所有历史版本 private loadVersions() { const migrationsDir = join(this.baseDir, 'migrations'); if (!existsSync(migrationsDir)) return; const files = readdirSync(migrationsDir).filter(f => f.endsWith('.json')); for (const file of files) { const content = readFileSync(join(migrationsDir, file), 'utf-8'); const version: PromptVersion = JSON.parse(content); this.versions.set(version.version, version); } } // 加载当前生效版本 private loadActiveVersion() { const activeFile = join(this.baseDir, 'active_version.txt'); if (existsSync(activeFile)) { this.activeVersion = readFileSync(activeFile, 'utf-8').trim(); } else { // 默认使用最新版本 const sorted = this.getVersionHistory(); this.activeVersion = sorted[sorted.length - 1] || null; } } // 获取版本历史(按时间排序) getVersionHistory(): string[] { return Array.from(this.versions.keys()) .sort((a, b) => parseInt(a) - parseInt(b)); } // 获取当前生效的 Prompt getActivePrompt(): PromptVersion | null { if (!this.activeVersion) return null; return this.versions.get(this.activeVersion) || null; } // 切换到指定版本 activateVersion(version: string): boolean { if (!this.versions.has(version)) return false; this.activeVersion = version; writeFileSync(join(this.baseDir, 'active_version.txt'), version); return true; } // 创建新版本 async createVersion(template: string, description: string, variables: string[], modelParams: any): Promise<string> { const version = Date.now().toString(); const sorted = this.getVersionHistory(); const parentVersion = sorted.length > 0 ? sorted[sorted.length - 1] : null; const promptVersion: PromptVersion = { version, description, template, variables, model_params: modelParams, parent_version: parentVersion, created_at: new Date().toISOString(), }; const migrationsDir = join(this.baseDir, 'migrations'); const filename = `${version}_${description.replace(/[^a-z0-9]/gi, '_').toLowerCase().substring(0, 30)}.json`; writeFileSync(join(migrationsDir, filename), JSON.stringify(promptVersion, null, 2)); this.versions.set(version, promptVersion); this.activateVersion(version); return version; } // 渲染 Prompt(替换变量) render(variables: Record<string, string>, version?: string): string { const promptVersion = version ? this.versions.get(version) : this.getActivePrompt(); if (!promptVersion) { throw new Error('No prompt version found'); } let rendered = promptVersion.template; for (const [key, value] of Object.entries(variables)) { if (!promptVersion.variables.includes(key)) { console.warn(`Variable "${key}" not declared in prompt version ${promptVersion.version}`); } rendered = rendered.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value); } // 检查未填充的变量 const unfilled = rendered.match(/\{\{(\w+)\}\}/g); if (unfilled) { throw new Error(`Unfilled variables: ${unfilled.join(', ')}`); } return rendered; } // 对比两个版本的差异 diff(v1: string, v2: string): string { const p1 = this.versions.get(v1); const p2 = this.versions.get(v2); if (!p1 || !p2) return ''; const lines: string[] = []; if (p1.template !== p2.template) { lines.push('Template changed:'); lines.push(` Old: ${p1.template.substring(0, 100)}...`); lines.push(` New: ${p2.template.substring(0, 100)}...`); } if (JSON.stringify(p1.model_params) !== JSON.stringify(p2.model_params)) { lines.push('Model params changed:'); lines.push(` Old: ${JSON.stringify(p1.model_params)}`); lines.push(` New: ${JSON.stringify(p2.model_params)}`); } return lines.join('\n') || 'No differences'; } } // 使用示例 const manager = new PromptManager('./prompts'); // 创建新版本 const v2 = await manager.createVersion( '你是一个{{role}}助手。请用 {{style}} 的风格回答:{{question}}', '添加 style 变量支持不同回答风格', ['role', 'style', 'question'], { temperature: 0.5, max_tokens: 500 } ); // 使用当前版本渲染 const rendered = manager.render({ role: '技术', style: '简洁', question: '什么是 Docker?', }); // => "你是一个技术助手。请用 简洁 的风格回答:什么是 Docker?" // A/B 测试:对比两个版本 const v1Result = manager.render( { role: '技术', question: '什么是 Docker?' }, '1704067200' // v1 ); // => "你是一个技术助手。请回答:什么是 Docker?"四、实践中的权衡
版本粒度:建议"对业务有影响的 Prompt 变更"才创建新版本。仅调整温度参数从 0.3 到 0.5 的类型,可以复用同一版本(通过render的额外参数覆盖)。只有 template 变更或新增变量时才创建新版本。
存储方式:文件存储适合原型阶段(< 20 个版本)。当版本超过 50 个时,建议迁移到数据库,实现按条件搜索和统计。
回滚风险:回滚 Prompt 版本时,需要确保业务代码兼容旧的变量定义。如果新版本引入了新变量(如style),回滚到旧版本后代码可能传入style但 Prompt 中没有对应的{{style}}。建议在render中加警告而不是抛错。
不适用场景:
- Prompt 非常简单且不变(< 50 tokens):直接用环境变量管理即可
- 需要 A/B 测试流量分发的场景:Prompt 版本管理只做存储和切换,不负责流量分发
五、总结
Prompt 版本管理的核心价值是让 Prompt 的迭代变得可追溯、可回滚、可对比。文件 + 版本号的方案简单且有效。
落地路径:先建立一个prompts/目录,把所有 Prompt 从代码中提取出来;然后参照数据库 Migration 的文件命名模式,为每次变更创建新版本;最后实现activateVersion支持版本切换,集成到测试和发布流程中。
少即是多。Prompt 版本管理不需要复杂的平台——文件夹 + JSON + 版本号已经解决了 90% 的问题。