10-Prompt版本管理
要点
- Prompt 不是一次写定的产品——模型会升级、业务会变、边界情况会冒出来
- 没有版本管理的 Prompt 等于盲改:改了不知道效果变好还是变差,回滚没有依据
- Prompt 版本管理和代码版本管理的区别:Prompt 的效果不可编译期检查,需要运行时度量
- 三种版本管理方案:Git + 常量、数据库存储、管理平台——复杂度递增
- 灰度发布和 A/B 测试是 Prompt 版本管理的核心能力
- 回滚机制要有——新版本出问题时要能快速退回到上一个稳定版本
1. 为什么需要版本管理
看一个真实场景。
V1 版本的 Prompt 上线一个月,效果稳定。产品说:「用户反馈回答太短了,能不能让回答更详细一些?」
于是改了一版 V2,加了「请给出详细的回答」之类的指令。上线后发现:回答确实变长了,但 Token 消耗翻了一倍,成本超预算。
需要回滚到 V1。但如果 Prompt 是直接写在代码里的字符串常量,回滚意味着改代码、重新部署。更糟的情况:V1 的代码已经被覆盖,找不到原来的版本了。
这就是 Prompt 版本管理要解决的问题:
- 每个版本都有记录——能追溯哪个版本是什么时候上线的、改了什么
- 可以回滚——新版本出问题,能快速退回到上一个稳定版本
- 可以灰度——新版本先在 10% 流量上验证,没问题再全量
- 可以对比——不同版本的效果数据可以放在一起对比
2. Prompt 版本管理 vs 代码版本管理
Prompt 版本管理和代码版本管理有很多相似之处,但也有关键区别。
| 维度 | 代码版本管理 | Prompt 版本管理 |
|---|---|---|
| 变更粒度 | 文件、函数、行 | 整段 Prompt 文本 |
| 验证方式 | 编译、单元测试、集成测试 | 运行时测试集 + 效果评估 |
| 回滚代价 | 重新部署 | 切换版本号(可能不需要部署) |
| 灰度能力 | Feature Flag、A/B 测试 | 类似的机制,但粒度是 Prompt 版本 |
| 效果度量 | 通过/不通过 | 需要定义评估指标(准确率、满意度、成本) |
最大的区别在于:代码可以编译期检查,Prompt 不行。代码里写了一个未定义变量,编译器会报错。Prompt 里改了一个词,效果变好还是变差,只有运行时才知道。
所以 Prompt 版本管理需要额外的能力:效果评估和对比。
3. 方案一:Git + 常量
最简单的方案:Prompt 写在代码文件里,用 Git 管理版本。
// src/services/prompt/versions/v3/order-query.ts
export const ORDER_QUERY_PROMPT_V3 = `你是一个订单查询助手。
// ... V3 版本的 Prompt 内容
`// src/services/prompt/order-query.ts
import { ORDER_QUERY_PROMPT_V3 } from './versions/v3/order-query'
// 当前使用的版本
const CURRENT_VERSION = 'v3'
const PROMPTS: Record<string, string> = {
v1: ORDER_QUERY_PROMPT_V1,
v2: ORDER_QUERY_PROMPT_V2,
v3: ORDER_QUERY_PROMPT_V3,
}
export function getCurrentPrompt(): string {
return PROMPTS[CURRENT_VERSION]
}优点:简单,和代码版本管理统一。 缺点:改 Prompt 需要改代码、重新部署。不能做到运行时切换版本。
适合场景:Prompt 变更不频繁、团队规模小、不需要灰度。
4. 方案二:数据库存储
把 Prompt 存在数据库里,运行时读取。改 Prompt 不需要改代码、不需要部署。
// 数据库表设计
// CREATE TABLE prompt_versions (
// id INTEGER PRIMARY KEY AUTOINCREMENT,
// prompt_key TEXT NOT NULL, -- 例如 'order-query'
// version TEXT NOT NULL, -- 例如 'v3'
// content TEXT NOT NULL, -- Prompt 内容
// status TEXT NOT NULL DEFAULT 'draft', -- draft / active / archived
// created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
// created_by TEXT,
// UNIQUE(prompt_key, version)
// )
// src/services/prompt/prompt-store.ts
import { db } from '@/lib/db'
type PromptVersion = {
id: number
promptKey: string
version: string
content: string
status: 'draft' | 'active' | 'archived'
createdAt: string
createdBy: string
}
export class PromptStore {
// 获取当前激活的 Prompt
async getActive(promptKey: string): Promise<string | null> {
const row = await db.query(
'SELECT content FROM prompt_versions WHERE prompt_key = ? AND status = ?',
[promptKey, 'active']
)
return row[0]?.content ?? null
}
// 创建新版本
async createVersion(params: {
promptKey: string
version: string
content: string
createdBy: string
}): Promise<void> {
await db.query(
'INSERT INTO prompt_versions (prompt_key, version, content, status, created_by) VALUES (?, ?, ?, ?, ?)',
[params.promptKey, params.version, params.content, 'draft', params.createdBy]
)
}
// 激活某个版本(同时把其他版本归档)
async activateVersion(promptKey: string, version: string): Promise<void> {
await db.query(
'UPDATE prompt_versions SET status = ? WHERE prompt_key = ? AND status = ?',
['archived', promptKey, 'active']
)
await db.query(
'UPDATE prompt_versions SET status = ? WHERE prompt_key = ? AND version = ?',
['active', promptKey, version]
)
}
// 获取历史版本列表
async listVersions(promptKey: string): Promise<PromptVersion[]> {
return db.query(
'SELECT * FROM prompt_versions WHERE prompt_key = ? ORDER BY created_at DESC',
[promptKey]
)
}
// 回滚到指定版本
async rollback(promptKey: string, version: string): Promise<void> {
await this.activateVersion(promptKey, version)
}
}使用:
// src/services/prompt/order-query.ts
import { PromptStore } from './prompt-store'
const store = new PromptStore()
// 带缓存的版本——避免每次请求都查数据库
let cachedPrompt: string | null = null
let cacheExpiry = 0
async function getOrderQueryPrompt(): Promise<string> {
const now = Date.now()
if (cachedPrompt && now < cacheExpiry) {
return cachedPrompt
}
const prompt = await store.getActive('order-query')
if (!prompt) throw new Error('No active prompt version for order-query')
cachedPrompt = prompt
cacheExpiry = now + 60_000 // 缓存 1 分钟
return prompt
}优点:运行时切换版本,不需要重新部署。支持历史版本查询和回滚。 缺点:需要数据库,需要管理界面(否则只能靠 SQL 操作)。
5. 灰度发布
新版本上线不应该直接全量——先在 10% 流量上跑一段时间,确认效果没问题再全量。
// src/services/prompt/canary.ts
/**
* 判断当前请求是否走新版本(灰度)
*/
function shouldUseCanary(promptKey: string, percentage: number): boolean {
// 简单的灰度策略:按用户 ID 哈希取模
// 同一个用户的请求总是走同一个版本,避免体验不一致
return Math.random() < percentage
}
async function getPromptWithCanary(promptKey: string): Promise<string> {
const canaryPercentage = await getCanaryPercentage(promptKey)
if (canaryPercentage > 0 && shouldUseCanary(promptKey, canaryPercentage)) {
const canaryVersion = await getCanaryVersion(promptKey)
return store.getVersion(promptKey, canaryVersion)
}
return store.getActive(promptKey)
}
// 灰度配置也存在数据库或配置中心
// CREATE TABLE prompt_canary (
// prompt_key TEXT PRIMARY KEY,
// canary_version TEXT NOT NULL,
// canary_percentage REAL NOT NULL DEFAULT 0, -- 0-1
// updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
// )灰度发布的关键指标:
- 效果指标:新版本的任务完成率、用户满意度
- 成本指标:Token 消耗、平均响应时间
- 安全指标:错误率、异常输出比例
灰度期间需要持续监控这些指标。如果新版本指标明显差于稳定版本,需要快速回滚。
6. A/B 测试
A/B 测试比灰度更进一步:同时运行两个版本,对比效果差异。
// src/services/prompt/ab-test.ts
type ABTestConfig = {
promptKey: string
variantA: string // 版本 A 的 version
variantB: string // 版本 B 的 version
splitRatio: number // B 版本的流量比例,0-1
startDate: string
endDate: string
}
async function runABTest(config: ABTestConfig, userId: string): Promise<string> {
// 按用户 ID 分流,确保同一用户总是走同一个版本
const hash = hashCode(userId)
const bucket = (hash % 100) / 100
const version = bucket < config.splitRatio ? config.variantB : config.variantA
// 记录分流日志——用于后续分析
await logABTestAssignment({
promptKey: config.promptKey,
userId,
assignedVersion: version,
timestamp: new Date().toISOString(),
})
return store.getVersion(config.promptKey, version)
}
// 分析 A/B 测试结果
async function analyzeABTest(promptKey: string) {
const assignments = await db.query(
'SELECT * FROM ab_test_assignments WHERE prompt_key = ?',
[promptKey]
)
// 按版本分组统计
const statsA = await computeMetrics(promptKey, 'v1')
const statsB = await computeMetrics(promptKey, 'v2')
return {
variantA: { version: 'v1', ...statsA },
variantB: { version: 'v2', ...statsB },
// 统计显著性检验
significance: computeSignificance(statsA, statsB),
}
}A/B 测试的关键:分流要稳定(同一用户始终走同一版本),样本量要够(否则结果不可信)。
7. 版本管理的 Hono 接口
把版本管理能力暴露成 API,供管理界面调用:
// src/routes/admin/prompts.ts
import { Hono } from 'hono'
import { PromptStore } from '@/services/prompt/prompt-store'
const store = new PromptStore()
const adminApp = new Hono()
// 获取所有 Prompt 列表
adminApp.get('/prompts', async (c) => {
const prompts = await db.query(
'SELECT DISTINCT prompt_key FROM prompt_versions ORDER BY prompt_key'
)
return c.json({ prompts })
})
// 获取某个 Prompt 的所有版本
adminApp.get('/prompts/:key/versions', async (c) => {
const key = c.req.param('key')
const versions = await store.listVersions(key)
return c.json({ versions })
})
// 创建新版本
adminApp.post('/prompts/:key/versions', async (c) => {
const key = c.req.param('key')
const { version, content } = await c.req.json()
await store.createVersion({
promptKey: key,
version,
content,
createdBy: c.get('user').id,
})
return c.json({ ok: true })
})
// 激活某个版本
adminApp.post('/prompts/:key/activate', async (c) => {
const key = c.req.param('key')
const { version } = await c.req.json()
await store.activateVersion(key, version)
return c.json({ ok: true })
})
// 回滚
adminApp.post('/prompts/:key/rollback', async (c) => {
const key = c.req.param('key')
const { version } = await c.req.json()
await store.rollback(key, version)
return c.json({ ok: true })
})
// 设置灰度
adminApp.put('/prompts/:key/canary', async (c) => {
const key = c.req.param('key')
const { version, percentage } = await c.req.json()
await db.query(
'INSERT OR REPLACE INTO prompt_canary (prompt_key, canary_version, canary_percentage) VALUES (?, ?, ?)',
[key, version, percentage]
)
return c.json({ ok: true })
})8. 版本管理的几个实践建议
建议一:版本号要有意义
v1.0.0 — 初始版本
v1.1.0 — 修改了输出格式约束
v1.1.1 — 修复了一个错别字
v2.0.0 — 重写了整个 Prompt
不要用 final、final2、really_final 这种命名。
建议二:每次修改都要记录原因
await store.createVersion({
promptKey: 'order-query',
version: 'v1.2.0',
content: newContent,
createdBy: 'user-123',
})
// 同时记录修改原因——可以存在另一张表
await db.query(
'INSERT INTO prompt_changelog (prompt_key, version, change_reason) VALUES (?, ?, ?)',
['order-query', 'v1.2.0', '用户反馈回答太短,增加详细程度要求']
)没有修改原因的版本记录,后续追溯会很痛苦。
建议三:回滚要能快速执行
回滚不是「改代码重新部署」——应该是「调一个 API 切换版本号」。如果回滚需要 30 分钟,那在这 30 分钟里线上一直在用有问题的 Prompt。
建议四:不要把 Prompt 只存在数据库里
数据库可能丢数据、可能被误操作。Prompt 版本应该同时存在 Git 和数据库里:
- Git 作为 source of truth——代码审查、版本追溯
- 数据库作为运行时存储——快速读取、动态切换
每次在管理界面修改 Prompt 时,同步创建一个 Git commit。
总结
回顾这一节的要点:
- Prompt 需要版本管理——模型升级、业务变化、边界情况都会触发修改
- Prompt 版本管理 vs 代码版本管理:关键区别在于 Prompt 效果需要运行时度量
- 三种方案:Git + 常量(简单)、数据库存储(运行时切换)、管理平台(完整能力)
- 灰度发布和 A/B 测试是核心能力
- 回滚要能快速执行,不能依赖重新部署
- 实践建议:有意义的版本号、记录修改原因、快速回滚、Git + 数据库双写
下一篇讲 Prompt 测试集——怎么度量 Prompt 质量,怎么知道改完之后效果变好还是变差。