10-Prompt版本管理

要点

  • Prompt 不是一次写定的产品——模型会升级、业务会变、边界情况会冒出来
  • 没有版本管理的 Prompt 等于盲改:改了不知道效果变好还是变差,回滚没有依据
  • Prompt 版本管理和代码版本管理的区别:Prompt 的效果不可编译期检查,需要运行时度量
  • 三种版本管理方案:Git + 常量、数据库存储、管理平台——复杂度递增
  • 灰度发布和 A/B 测试是 Prompt 版本管理的核心能力
  • 回滚机制要有——新版本出问题时要能快速退回到上一个稳定版本

1. 为什么需要版本管理

看一个真实场景。

V1 版本的 Prompt 上线一个月,效果稳定。产品说:「用户反馈回答太短了,能不能让回答更详细一些?」

于是改了一版 V2,加了「请给出详细的回答」之类的指令。上线后发现:回答确实变长了,但 Token 消耗翻了一倍,成本超预算。

需要回滚到 V1。但如果 Prompt 是直接写在代码里的字符串常量,回滚意味着改代码、重新部署。更糟的情况:V1 的代码已经被覆盖,找不到原来的版本了。

这就是 Prompt 版本管理要解决的问题:

  1. 每个版本都有记录——能追溯哪个版本是什么时候上线的、改了什么
  2. 可以回滚——新版本出问题,能快速退回到上一个稳定版本
  3. 可以灰度——新版本先在 10% 流量上验证,没问题再全量
  4. 可以对比——不同版本的效果数据可以放在一起对比

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

不要用 finalfinal2really_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 质量,怎么知道改完之后效果变好还是变差。