Appearance
Prompt 工程的工程化最佳实践
"Prompt 是新的代码。但很多团队还在像写 commit message 一样写 Prompt。"
如果你的产品里有任何 LLM 调用,Prompt 就是一种会影响生产的产物,必须当代码对待。这一篇不讲"怎么写好 Prompt"——网上太多了,而是讲怎么把 Prompt 工程化地管起来。
工程化 Prompt 的五件事
- 把 Prompt 从代码里抽出来,模板化;
- 输入做变量替换、约束输出格式;
- 版本管理 + diff 评审;
- 灰度 / A/B / Fallback;
- 监控 + 回放。
一、把 Prompt 从代码里抽出来
最常见的反模式:
ts
// ❌ 反模式
const res = await llm.chat([
{ role: 'system', content: '你是客服助手,请友好地回答用户...' },
{ role: 'user', content: question },
]);Prompt 散在业务代码里,改一个字就要改代码、过 review、走发布。建议改成:
src/
prompts/
customer-service/
v1.md # Prompt 内容
v1.meta.yaml # 模型、温度、版本、负责人
v1.eval.ts # 这个版本的回归测试样本v1.md:
md
---
input:
- question: string
- history: ChatMessage[]
output:
format: markdown
max_tokens: 500
---
你是 Aaron 商城的客服助手。回答规则:
1. 一律使用中文,礼貌但不啰嗦。
2. 不知道的事情说"我去帮您确认",不要编造。
3. 引用商品时用 [商品名](url) 的格式。
历史对话:
{{#each history}}
- {{role}}: {{content}}
{{/each}}
用户问题:{{question}}加载时只是 loadPrompt('customer-service@v1')。改 Prompt = 改这个文件。
二、约束输出格式
让 LLM 返回结构化数据时,永远 用三层保险:
1. Prompt 里明确写 schema → 软约束
2. 用 JSON Schema / Zod 验证 → 硬约束
3. 失败重试一次(让模型看错误信息再来) → 容错ts
import { z } from 'zod';
const Article = z.object({
title: z.string().max(60),
tags: z.array(z.string()).max(5),
summary: z.string().max(200),
});
async function extractArticle(html: string) {
const prompt = `
请从下面 HTML 中提取文章信息,严格按以下 JSON 输出,不要任何额外解释:
${JSON.stringify(zodToJsonSchema(Article))}
HTML:
${html}
`;
for (let i = 0; i < 2; i++) {
const raw = await llm.chat(prompt, { responseFormat: 'json' });
try {
return Article.parse(JSON.parse(raw));
} catch (e) {
if (i === 1) throw e;
// 给模型一次纠正机会
prompt += `\n\n上一轮你的输出无法解析:${e.message},请严格按 schema 重新输出。`;
}
}
}很多时候 responseFormat: 'json' + Schema 校验比反复调 Prompt 可靠得多。
三、版本管理:把 Prompt 当数据库迁移
每次 Prompt 改动新建版本,不要在原版本上覆盖。原因:
- 上线后发现新版回归,可以一键回滚;
- A/B 时两个版本要同时存在;
- 监控数据按版本归类。
我建议的目录约定:
prompts/
customer-service/
v1.md # 2026-01-10 初版
v2.md # 2026-02-03 加入退货流程
v3.md # 2026-03-15 改用 chain-of-thought
current.md # symlink → v3.mdcurrent.md 是默认版本,环境变量或灰度规则可以覆盖到具体版本。
四、灰度与 A/B
至少给每个 Prompt 调用配两个旋钮:
ts
async function chat(question: string, userId: string) {
const variant = pickVariant(userId, {
'customer-service@v3': 0.9,
'customer-service@v4-experimental': 0.1, // 10% 流量
});
const result = await llm.chat(loadPrompt(variant), { question });
// 上报指标
reportMetric({
promptVersion: variant,
userId,
latencyMs: result.latency,
tokenCount: result.usage.total_tokens,
rating: null, // 等用户反馈再补
});
return result;
}pickVariant 用 userId 哈希保证同一用户始终落到同一变体——避免用户来回看到不同风格。
五、Fallback
LLM 比传统服务更容易出问题:超时、限流、模型暂时变蠢、output 解析失败。一定要有降级链:
ts
const providers = [
{ name: 'claude-sonnet', fn: claudeSonnet },
{ name: 'gpt-4o', fn: gpt4o },
{ name: 'deepseek-v3', fn: deepseek },
];
for (const p of providers) {
try {
return await withTimeout(p.fn(prompt), 8_000);
} catch (err) {
log.warn(`${p.name} failed: ${err.message}`);
}
}
throw new Error('all providers failed');六、监控指标
每次 LLM 调用至少记录:
| 字段 | 用途 |
|---|---|
prompt_version | 按版本聚合质量 |
provider / model | 比较各 provider 表现 |
latency_ms | 性能监控 |
tokens_in / tokens_out | 成本监控 |
parse_ok | 输出格式是否能解析 |
user_rating | 用户反馈(thumbs up/down) |
tags | 业务维度,比如 query 类型 |
接到任意时序数据库(ClickHouse / Datadog / Grafana),就有了 Prompt 维度的 dashboard。
七、回放(Eval)
Prompt 改动前后必须跑回归测试。最简单的版本:
ts
// prompts/customer-service/v3.eval.ts
export const cases = [
{
name: '咨询退货',
input: { question: '我买的鞋子尺码不对,怎么退?', history: [] },
assert: (output: string) => {
expect(output).toContain('退货');
expect(output.length).toBeLessThan(500);
expect(output).not.toContain('我不知道');
},
},
// ... 通常 20-50 条覆盖核心场景
];跑一次 = 把所有 case 喂给当前版本,记录通过率。新版本必须通过率不低于旧版本 才能 promote。
更高级的玩法是用 LLM-as-Judge——再调一次模型让它打分,但这就是另一篇了。
八、组织上的小建议
Prompt 的修改 PR 必须有人 review。 它不是 README,它能改产品行为。建议规范:
- PR title 用
prompt(customer-service): bump v3 to v4开头; - diff 要展示前后对比 + 至少一条 eval 失败截图;
- merge 后默认走 1% 灰度,观察 24h 没问题再 promote 100%。
九、不要做的事
- ❌ 把 API key 写进 Prompt(被泄漏的概率比代码大很多);
- ❌ Prompt 里嵌业务密文 / PII(日志会泄漏);
- ❌ 让用户输入直接拼到 system prompt(Prompt Injection 是真问题);
- ❌ "线上有问题先临时改 Prompt"——必须走版本流程,否则三个月后没人知道为什么是这样。
