
你们团队的 prompt 放在哪?如果是"散落在各个文件里的字符串常量",那这篇文章是写给你的。
一、三类真实生产事故
先来三个不虚构的场景:
事故 1:一行改动,全线降级
某 AI 客服系统,有工程师在周五下午悄悄把系统 prompt 里的一句话从"你是一个专业客服助手"改成了"你是一个友好的助手"。周一早上,QA 发现对话质量全面下滑------但没有人知道是这行改动导致的,因为 prompt 只在源码里,git blame 也得先知道去哪找。排查花了两天。
事故 2:多人协作覆盖
团队里产品经理直接改了 prompt 文件,工程师同时在做技术优化,两份修改在合并时产生冲突,随手解冲突选了错误的版本。上线后用户反馈变差,但 diff 记录被覆盖,无法还原。
事故 3:模型升级引发 prompt 失效
从 GPT-4 升级到某新版模型后,原有 prompt 里的几个 few-shot 示例格式不再被新模型正确理解。想回滚 prompt 到模型升级前的版本------发现根本没有版本历史,只能手动对比代码 commit 来猜。
这三个事故的共同根源:prompt 没有被作为一等公民管理。它们散落在代码字符串里、配置文件里、数据库某个 text 字段里,没有版本、没有历史、没有回滚路径。
二、Prompt 版本管理的 4 个核心需求
在设计解决方案之前,先明确需求边界。生产环境的 prompt 版本管理,本质上要解决 4 个问题:
需求 1:可追溯的历史记录
每次 prompt 修改都应该有:
- 修改时间
- 修改人
- 修改内容(diff)
- 修改原因(可选但推荐)
这不是"最好有",而是出了问题必须有。一个没有历史记录的 prompt,出事后只能靠猜。
需求 2:环境隔离与版本标签
Prompt 需要像代码一样走 dev → staging → production 的流程。具体实现上通常是"版本标签":
movie-critic-prompt
├── v1 (已废弃)
├── v2 [staging]
├── v3 [production] ← 当前生产版本
└── v4 (草稿)
这样你可以在 staging 测试 v4 而不影响生产的 v3。
需求 3:快速回滚能力
生产出问题时,回滚 prompt 应该是 1 分钟内能完成的操作------不是"修改代码 → PR → Review → 合并 → 部署"这个 20 分钟起步的流程。
这要求 prompt 的存储和应用代码解耦:应用在运行时动态读取 prompt,而不是编译时硬编码。
需求 4:变更归因能力
当 A/B 测试某个 prompt 变体时,你需要能把业务指标(转化率、用户评分、LLM 输出质量)和具体 prompt 版本关联起来。这是"哪个 prompt 更好"问题的答案基础。
三、三种方案对比:Git、数据库、专用 Registry
方案 A:Git + 代码目录
最简单的方案:把所有 prompt 放在一个专门的目录,用 git 管理。
prompts/
customer-service/
system-prompt.txt # 主 prompt
system-prompt.v1.txt # 手动备份(不推荐)
summarizer/
main.txt
main.md
加载方式(Node.js):
typescript
import fs from 'fs/promises';
import path from 'path';
// 约定:PROMPT_VERSION 环境变量控制版本目录
const PROMPT_DIR = process.env.PROMPT_DIR ?? './prompts';
export async function loadPrompt(name: string): Promise<string> {
const filePath = path.join(PROMPT_DIR, `${name}.txt`);
return fs.readFile(filePath, 'utf-8');
}
优点:
- 零额外基础设施
- git log/blame/diff 原生可用
- 对小团队够用
缺点:
- 回滚 = 改代码 + 部署,不是运行时操作
- 没有环境标签概念,staging/production 版本靠分支或手动
- 无法和 LLM trace(哪次调用用了哪个版本)关联
- 多人同时修改时的协作体验差
适用场景:≤3 人的团队,prompt 变更频率低(每周 1-2 次),没有 A/B 测试需求。
方案 B:自建轻量 Prompt Registry(数据库)
当团队规模和 prompt 变更频率增加,Git 方案开始出现摩擦,这时候自建一个轻量的 Prompt Registry 是性价比最高的升级路径。
数据库 Schema 设计(PostgreSQL):
sql
-- 主表:prompt 条目
CREATE TABLE prompts (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, -- 唯一标识符,如 "customer-service-system"
version INTEGER NOT NULL, -- 单调递增版本号
content TEXT NOT NULL, -- prompt 正文(支持模板变量)
model_hint VARCHAR(100), -- 建议使用的模型(可选)
config JSONB DEFAULT '{}', -- temperature 等超参数
labels TEXT[] DEFAULT '{}', -- ["production", "staging"] 等
hash CHAR(64) NOT NULL, -- SHA-256(content) 防篡改
created_by VARCHAR(255),
created_at TIMESTAMPTZ DEFAULT NOW(),
notes TEXT, -- 修改原因
UNIQUE(name, version)
);
-- 索引:按 name + label 快速查找当前版本
CREATE INDEX idx_prompts_name_labels ON prompts USING GIN (labels);
CREATE INDEX idx_prompts_name_version ON prompts (name, version DESC);
读取 API(Node.js + pg):
typescript
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
interface PromptRecord {
id: number;
name: string;
version: number;
content: string;
config: Record<string, unknown>;
labels: string[];
}
// 获取指定 label 的最新版本
export async function getPrompt(
name: string,
label: string = 'production'
): Promise<PromptRecord | null> {
const { rows } = await pool.query<PromptRecord>(
`SELECT * FROM prompts
WHERE name = $1 AND $2 = ANY(labels)
ORDER BY version DESC
LIMIT 1`,
[name, label]
);
return rows[0] ?? null;
}
// 回滚:把指定版本的 labels 更新为 production
export async function rollbackPrompt(
name: string,
targetVersion: number
): Promise<void> {
await pool.query('BEGIN');
try {
// 移除当前 production 标签
await pool.query(
`UPDATE prompts
SET labels = array_remove(labels, 'production')
WHERE name = $1 AND 'production' = ANY(labels)`,
[name]
);
// 赋予目标版本 production 标签
await pool.query(
`UPDATE prompts
SET labels = array_append(labels, 'production')
WHERE name = $1 AND version = $2`,
[name, targetVersion]
);
await pool.query('COMMIT');
} catch (err) {
await pool.query('ROLLBACK');
throw err;
}
}
写入/发布流程(CI 脚本):
typescript
// scripts/publish-prompt.ts
import crypto from 'crypto';
interface CreatePromptParams {
name: string;
content: string;
labels?: string[];
modelHint?: string;
config?: Record<string, unknown>;
notes?: string;
createdBy?: string;
}
export async function createPromptVersion(params: CreatePromptParams): Promise<number> {
const {
name, content, labels = ['staging'],
modelHint, config = {}, notes, createdBy
} = params;
const hash = crypto.createHash('sha256').update(content).digest('hex');
// 获取下一个版本号
const { rows: [{ max_version }] } = await pool.query(
'SELECT COALESCE(MAX(version), 0) as max_version FROM prompts WHERE name = $1',
[name]
);
const nextVersion = (max_version as number) + 1;
const { rows: [record] } = await pool.query(
`INSERT INTO prompts (name, version, content, model_hint, config, labels, hash, notes, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
RETURNING id, version`,
[name, nextVersion, content, modelHint, config, labels, hash, notes, createdBy]
);
console.log(`✅ Created ${name} v${record.version} [${labels.join(', ')}]`);
return record.version;
}
这套方案的回滚从 20 分钟缩短到 30 秒 :调用 rollbackPrompt('customer-service-system', 5) 即完成,应用下次请求就会拿到 v5 的 prompt。
方案 C:使用 Langfuse(推荐用于需要可观测性的团队)
Langfuse 是目前 prompt 管理领域功能最完整的开源平台,支持自托管,prompt management 功能免费。与方案 B 相比,它额外提供:
- Prompt Experiments:在 UI 里直接运行多版本 A/B 测试
- Trace 关联:每次 LLM 调用自动关联 prompt 版本,方便归因
- Zero-latency 缓存:本地缓存 + 后台刷新,不增加请求延迟
Python SDK 使用示例:
python
from langfuse import Langfuse
langfuse = Langfuse()
# 创建新版本(同名 prompt 自动递增版本)
langfuse.create_prompt(
name="customer-service-system",
type="text",
prompt="""你是一个专业的客服助手,负责处理用户的订单问题。
回复规范:
1. 语气专业且友好
2. 对于退款问题,必须先确认订单号
3. 不确定的信息不要猜测,引导用户联系人工
用户问题:{{user_query}}""",
labels=["staging"],
config={"temperature": 0.3}
)
# 获取 production 版本(带本地缓存)
prompt = langfuse.get_prompt(
"customer-service-system",
label="production",
cache_ttl_seconds=300 # 5 分钟缓存,减少网络请求
)
# 编译(填充模板变量)
compiled = prompt.compile(user_query="我的订单什么时候发货?")
Node.js/TypeScript 示例:
typescript
import { LangfuseClient } from "@langfuse/client";
const langfuse = new LangfuseClient();
// 获取并编译 prompt
const prompt = await langfuse.getPrompt(
"customer-service-system",
undefined, // version --- undefined 表示用 label 查找
{ label: "production", cacheTtlSeconds: 300 }
);
const messages = prompt.compile({
user_query: "我的订单什么时候发货?"
});
// 直接传给 OpenAI/Claude SDK
const response = await openai.chat.completions.create({
model: "gpt-4o",
messages,
...prompt.config // temperature 等超参数也从 Langfuse 读取
});
// trace 自动关联:langfuse 会记录用了哪个 prompt 版本
回滚操作(通过 UI 或 API):
bash
# 通过 Langfuse API 把 v3 设为 production
curl -X PATCH "https://cloud.langfuse.com/api/public/v2/prompts/customer-service-system" \
-u "$LANGFUSE_PUBLIC_KEY:$LANGFUSE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"version": 3,
"labels": ["production"]
}'
一条 curl,30 秒内完成回滚,不需要修改代码或重新部署。
四、方案对比表
| 维度 | Git + 代码 | 自建 DB | Langfuse | LangSmith | PromptLayer |
|---|---|---|---|---|---|
| 版本历史 | ✅ git log | ✅ 自定义 | ✅ UI + API | ✅ UI + API | ✅ 自动捕获 |
| 环境标签 | ⚠️ 靠分支 | ✅ 自定义 | ✅ labels | ✅ tags | ⚠️ 基础 |
| 运行时回滚 | ❌ 需重部署 | ✅ SQL | ✅ API/UI | ✅ API/UI | ✅ |
| Trace 关联 | ❌ | ❌ | ✅ 内置 | ✅ 内置 | ⚠️ 基础 |
| A/B 测试 | ❌ | 需自建 | ✅ Experiments | ✅ | ⚠️ 基础 |
| 本地缓存 | ✅ 文件系统 | 需自建 | ✅ zero-latency | ✅ | ⚠️ |
| 自托管 | ✅ | ✅ | ✅ (Docker) | ❌ | ❌ |
| 成本 | 免费 | 工程成本 | 免费/自托管 | $39+/月 | $39+/月 |
| 适合规模 | ≤3人小团队 | 中型团队 | 任意规模 | 企业 | 小-中型 |
五、Prompt Diff 与语义回归检测
版本管理不只是"存档",更重要的是帮助你判断"这次改动安全吗"。
5.1 文本 Diff
最基础的是文本 diff,检查 prompt 文本的具体改动:
typescript
import { diffLines, Change } from 'diff';
export function promptDiff(oldContent: string, newContent: string): string {
const changes: Change[] = diffLines(oldContent, newContent);
return changes.map(change => {
if (change.added) return `+ ${change.value.trimEnd()}`;
if (change.removed) return `- ${change.value.trimEnd()}`;
return ` ${change.value.trimEnd()}`;
}).join('\n');
}
// 使用示例
const diff = promptDiff(
`你是一个专业客服助手,负责处理订单问题。`,
`你是一个友好的助手,帮助用户解决各种问题。`
);
console.log(diff);
// - 你是一个专业客服助手,负责处理订单问题。
// + 你是一个友好的助手,帮助用户解决各种问题。
5.2 语义回归测试(LLM-as-Judge)
文本 diff 只能看到"改了什么",不能判断"改得好不好"。对于开放性输出,目前工程界的主流做法是 LLM-as-Judge:用另一个模型评估新旧 prompt 的输出质量。
python
import asyncio
from anthropic import Anthropic
from typing import NamedTuple
client = Anthropic()
class PromptEvalResult(NamedTuple):
score: float # 0.0 - 1.0
reasoning: str
winner: str # "old" | "new" | "tie"
JUDGE_PROMPT = """你是一个 LLM 输出质量评估专家。
请对比以下两个版本的 prompt 对同一个用户问题的输出,判断哪个更好。
用户问题:{user_query}
版本 A(旧)的输出:
{old_output}
版本 B(新)的输出:
{new_output}
评估维度:
1. 准确性(是否回答了问题)
2. 专业性(是否符合客服场景要求)
3. 安全性(是否有不当内容)
请以 JSON 格式回复:
{{"winner": "A" | "B" | "tie", "score_a": 0-10, "score_b": 0-10, "reasoning": "..."}}"""
async def eval_prompt_versions(
test_cases: list[str],
old_prompt: str,
new_prompt: str
) -> list[PromptEvalResult]:
results = []
for query in test_cases:
# 并行获取两个版本的输出
old_response, new_response = await asyncio.gather(
get_llm_output(old_prompt, query),
get_llm_output(new_prompt, query)
)
# LLM-as-Judge 评判
judge_response = client.messages.create(
model="claude-opus-4-5",
max_tokens=500,
messages=[{
"role": "user",
"content": JUDGE_PROMPT.format(
user_query=query,
old_output=old_response,
new_output=new_response
)
}]
)
verdict = parse_judge_response(judge_response.content[0].text)
results.append(verdict)
return results
async def get_llm_output(system_prompt: str, user_query: str) -> str:
response = client.messages.create(
model="claude-haiku-4-5",
max_tokens=300,
system=system_prompt,
messages=[{"role": "user", "content": user_query}]
)
return response.content[0].text
实践中,建议维护一个 golden test set(黄金测试集):20-50 个代表性的用户输入,涵盖正常场景、边界情况、潜在风险。每次发布新 prompt 版本前,必须通过这套测试,胜率 ≥ 70% 才允许提升到 production。
六、与 CI/CD 集成
把 prompt 发布纳入 CI/CD,是从"手动管理"到"工程化管理"的关键一步。
6.1 目录结构约定
prompts/
customer-service/
system.txt # 当前 staging 草稿
system.v3.txt # production 快照备份(可选)
test-cases.json # 黄金测试集
metadata.json # 元数据
summarizer/
main.txt
test-cases.json
metadata.json 示例:
json
{
"name": "customer-service-system",
"description": "客服系统主提示词",
"owner": "team-customer-service",
"model_hint": "claude-haiku-4-5",
"config": { "temperature": 0.3, "max_tokens": 500 },
"min_win_rate": 0.70
}
6.2 GitHub Actions 流水线
yaml
# .github/workflows/prompt-publish.yml
name: Prompt Version Publish
on:
push:
paths:
- 'prompts/**/*.txt'
- 'prompts/**/*.json'
branches:
- main
jobs:
validate-and-publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # 获取上一个 commit 以便 diff
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Detect changed prompts
id: changed
run: |
# 找出本次 push 修改了哪些 .txt 文件
CHANGED=$(git diff --name-only HEAD~1 HEAD -- 'prompts/**/*.txt')
echo "changed_files=$CHANGED" >> $GITHUB_OUTPUT
- name: Run regression tests
if: steps.changed.outputs.changed_files != ''
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
run: |
for file in ${{ steps.changed.outputs.changed_files }}; do
echo "Testing $file..."
node scripts/test-prompt.mjs --file "$file" --label staging
done
- name: Publish to Langfuse staging
if: success()
env:
LANGFUSE_SECRET_KEY: ${{ secrets.LANGFUSE_SECRET_KEY }}
LANGFUSE_PUBLIC_KEY: ${{ secrets.LANGFUSE_PUBLIC_KEY }}
run: |
for file in ${{ steps.changed.outputs.changed_files }}; do
node scripts/publish-prompt.mjs --file "$file" --label staging
done
echo "✅ Prompts published to staging. Manual promotion to production required."
注意:CI 只发布到 staging,从 staging 提升到 production 需要人工确认(或额外的自动化审批步骤)。这是一个关键的安全门控。
6.3 测试脚本
javascript
// scripts/test-prompt.mjs
import { readFile } from 'fs/promises';
import path from 'path';
import { Langfuse } from 'langfuse';
import { evalPromptVersions } from './eval-prompt.mjs';
const [, , ...args] = process.argv;
const fileArg = args[args.indexOf('--file') + 1];
const promptDir = path.dirname(fileArg);
const promptName = path.basename(promptDir);
const content = await readFile(fileArg, 'utf-8');
const metadata = JSON.parse(
await readFile(path.join(promptDir, 'metadata.json'), 'utf-8')
);
const testCases = JSON.parse(
await readFile(path.join(promptDir, 'test-cases.json'), 'utf-8')
);
// 获取当前 production 版本
const langfuse = new Langfuse();
const productionPrompt = await langfuse.getPrompt(
metadata.name, undefined, { label: 'production' }
);
console.log(`🔍 Testing ${metadata.name}: new vs production...`);
const results = await evalPromptVersions(
testCases.map(tc => tc.input),
productionPrompt.prompt,
content
);
const winCount = results.filter(r => r.winner === 'new').length;
const winRate = winCount / results.length;
console.log(`📊 Win rate: ${(winRate * 100).toFixed(1)}% (threshold: ${metadata.min_win_rate * 100}%)`);
if (winRate < metadata.min_win_rate) {
console.error(`❌ FAILED: New prompt win rate ${(winRate * 100).toFixed(1)}% < threshold ${metadata.min_win_rate * 100}%`);
process.exit(1);
}
console.log(`✅ PASSED: New prompt meets quality threshold`);
七、迁移路线:从硬编码到 Registry
已有项目怎么迁移?别想着"一次性重构",那会让你在会议室里拿着一叠 PPT 却没法交付。推荐分 3 步走:
第一步:统一收口(1-2 天)
不改任何 prompt 内容,只把所有散落的 prompt 字符串挪到统一目录,建立一个 loadPrompt(name) 函数作为全局入口。
typescript
// 改造前:prompt 散落在业务代码里
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: '你是一个专业客服助手...' }, // ← 硬编码
{ role: 'user', content: userQuery }
]
});
// 改造后:通过统一入口读取
const systemPrompt = await loadPrompt('customer-service-system');
const response = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userQuery }
]
});
这一步完成后,你已经获得了"知道所有 prompt 在哪"的能力。
第二步:加版本控制(3-5 天)
选择方案 B(自建 DB)或方案 C(Langfuse),把所有 prompt 导入进去,加上历史记录和环境标签。
python
# 批量导入现有 prompt(迁移脚本)
import glob
import os
for filepath in glob.glob('prompts/**/*.txt', recursive=True):
name = filepath.replace('prompts/', '').replace('.txt', '').replace('/', '-')
content = open(filepath).read()
langfuse.create_prompt(
name=name,
type='text',
prompt=content,
labels=['production'], # 所有现有 prompt 标记为 production
notes=f'Initial import from {filepath}'
)
print(f'✅ Imported {name}')
第三步:建立黄金测试集(1 周)
这是最花时间但最值钱的一步:为每个重要 prompt 整理 20-50 个测试用例,涵盖正常场景和边界情况。有了测试集,后续每次修改都能用数据说话。
八、选型决策树
你的团队有多少人在维护 prompt?
├── ≤3 人,prompt 改动 ≤ 每周 2 次
│ └── 【Git + 代码目录】足够用,别过度工程化
│
├── 4-15 人,需要 staging/production 隔离,有 A/B 测试需求
│ ├── 想要 LLM trace 关联 → 【Langfuse(自托管)】
│ └── 只需要版本管理 → 【自建轻量 Registry(PostgreSQL)】
│
└── 大型团队,需要审批流程、RBAC、合规审计
├── 可接受 SaaS 费用 → 【LangSmith / Humanloop】
└── 必须自托管、成本敏感 → 【Langfuse Enterprise / 自建】
一个常见的错误:刚开始用 LLM 就直接上 LangSmith,39/月 的费用还能接受,但你花在配置上的时间值 39 的 10 倍。在你真正需要 trace 关联之前,自建方案足够应对 80% 的场景。
总结
Prompt 版本管理不是"最佳实践",是生产必需品。当你的 LLM 应用出了问题,没有版本历史就是在黑暗中摸墙。
本文给出的三套方案各有适用场景:
- 小团队:Git + 代码目录,零成本,马上能用
- 中型团队:自建 PostgreSQL Registry,30 秒回滚,可控成本
- 需要可观测性:Langfuse,prompt 版本与 LLM trace 天然关联
无论选哪套,最重要的一步是建立黄金测试集------它是你判断"这次 prompt 改动是好是坏"的唯一可靠依据。
记住:Prompt 是一等公民,不是字符串常量。
本文代码均已在 Node.js 20 + Python 3.11 + Langfuse 3.x 环境验证。自建 Registry 方案完整代码见 GitHub: 示例仓库链接
关键词:prompt 版本管理、prompt 工程化、LLM 工程实践、prompt registry、Langfuse、生产 AI 应用