07-Few-shot示例设计
要点
- 前面六节都是在用文字告诉模型该怎么做——但有时候「给模型看一个例子」比「写一段解释」更有效
- Few-shot 是在 Prompt 里放一两个输入输出对,让模型从示例中学到格式、风格和判断标准
- 示例的选择比数量更重要:两个好示例胜过十个普通示例
- 示例需要覆盖正常情况和边界情况,但不能互相矛盾
- 动态示例选择:根据用户输入从示例库中检索最相关的示例,而不是固定写死
1. 为什么 Few-shot 有效
先看一个对比。任务是把用户的自然语言翻译成 SQL。
Zero-shot(没有示例)
const prompt = `
将用户的自然语言翻译成 SQL 查询。
数据库表结构:
- users(id, name, email, created_at)
- orders(id, user_id, amount, status, created_at)
用户输入:最近 7 天注册了多少用户
SQL:
`模型可能输出:
SELECT COUNT(*) FROM users WHERE created_at >= NOW() - INTERVAL 7 DAY大部分时候这是对的。但如果用户的表述更复杂一些:
用户输入:上个月消费超过 1000 元的用户有几个
模型可能会在 amount 的单位上犯错(元 vs 分),或者在时间范围上算错。
Few-shot(加两个示例)
const prompt = `
将用户的自然语言翻译成 SQL 查询。
数据库表结构:
- users(id, name, email, created_at)
- orders(id, user_id, amount, status, created_at)
注意:amount 单位是分,需要除以 100 才是元。
示例:
输入:「最近 3 天注册的用户数」
SQL:SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE - INTERVAL '3 days'
输入:「所有已取消订单的总金额(以元为单位)」
SQL:SELECT SUM(amount) / 100.0 FROM orders WHERE status = 'cancelled'
输入:上个月消费超过 1000 元的用户有几个
SQL:
`两个示例起到了三个作用:
- 格式对齐——模型知道 SQL 该怎么写、时间函数用什么语法
- 单位提醒——示例里展示了
/ 100.0的转换,模型会复用这个模式 - 边界提示——「已取消订单」暗示 status 字段有枚举值
这就是 Few-shot 的价值:用具体的例子告诉模型「这样做是对的」,比用文字解释规则更直接。
2. 示例的数量
一个常见的问题是:放几个示例合适?
经验规律:
| 示例数量 | 适用场景 |
|---|---|
| 0 个(Zero-shot) | 任务简单明确,模型本身就能做好 |
| 1-2 个 | 需要对齐格式或风格 |
| 3-5 个 | 任务有一定复杂度,需要覆盖多种情况 |
| 5 个以上 | 任务非常复杂,或者需要覆盖很多边界情况 |
超过 5 个示例时,要警惕两个问题:
- Token 消耗——每个示例都会占用上下文窗口。示例太长太多,留给用户输入和模型输出的空间就不够了
- 示例之间可能矛盾——示例越多,两个示例暗示不同处理方式的概率越高,模型会困惑
示例数量的判断标准
放示例之前先问自己:这个示例是否在教模型一件它本来不知道的事?
- 如果任务本身很直观(比如翻译),不需要示例
- 如果任务需要特定的格式或领域知识(比如 SQL 方言、内部 API 调用规范),需要示例
- 如果任务的边界情况容易出错(比如金额单位转换),需要覆盖边界情况的示例
3. 示例的选择
示例的质量比数量重要。好的示例应该满足:
标准一:示例要典型
示例应该代表最常见的输入类型,而不是特殊情况。
// ❌ 示例太极端
const examples = [
{ input: '帮我退掉那个我三年前买的、中间退过两次后来又买回来的东西', output: '...' },
]
// ✅ 示例典型
const examples = [
{ input: '退订单 ORD-001', output: '{"intent": "refund", "orderId": "ORD-001"}' },
{ input: '查一下 ORD-002 到哪了', output: '{"intent": "query_order", "orderId": "ORD-002"}' },
]标准二:示例要覆盖关键差异
如果任务有几种不同的处理方式,每种至少给一个示例。
// ✅ 覆盖不同意图
const examples = [
{ input: '退订单 ORD-001', output: '{"intent": "refund"}' }, // 退款
{ input: 'ORD-002 到哪了', output: '{"intent": "query_order"}' }, // 查询
{ input: '取消 ORD-003', output: '{"intent": "cancel"}' }, // 取消
{ input: '这个质量太差了', output: '{"intent": "complaint"}' }, // 投诉
]如果只给「退款」的示例,模型遇到「投诉」就不知道怎么处理。
标准三:示例之间不矛盾
// ❌ 示例矛盾
const examples = [
{ input: '退订单', output: '{"action": "refund"}' }, // 输出 action 字段
{ input: '查订单', output: '{"intent": "query"}' }, // 输出 intent 字段
]
// ✅ 示例一致
const examples = [
{ input: '退订单', output: '{"intent": "refund"}' },
{ input: '查订单', output: '{"intent": "query"}' },
]字段名不一致会让模型困惑——它不知道该用 action 还是 intent。
标准四:示例要简短
示例越长,Token 消耗越大,而且模型更容易从短示例中学到核心模式。
// ❌ 示例太长
const examples = [
{
input: '你好,我想问一下,就是我之前下了一个订单,订单号好像是 ORD-001 吧,然后我想知道这个订单现在是什么状态了,能不能帮我查一下',
output: '{"intent": "query_order", "orderId": "ORD-001"}',
},
]
// ✅ 示例简短
const examples = [
{ input: '查 ORD-001 的状态', output: '{"intent": "query_order", "orderId": "ORD-001"}' },
]4. 示例在 Prompt 中的组织方式
方式一:输入输出交替
const prompt = `
意图分类。
输入:退订单 ORD-001
输出:{"intent": "refund", "orderId": "ORD-001"}
输入:ORD-002 到哪了
输出:{"intent": "query_order", "orderId": "ORD-002"}
输入:${userInput}
输出:
`最常见的方式,简单直接。
方式二:用角色消息包装
const messages = [
{ role: 'system', content: '你是一个意图分类助手。' },
// 示例用 user-assistant 对表示
{ role: 'user', content: '退订单 ORD-001' },
{ role: 'assistant', content: '{"intent": "refund", "orderId": "ORD-001"}' },
{ role: 'user', content: 'ORD-002 到哪了' },
{ role: 'assistant', content: '{"intent": "query_order", "orderId": "ORD-002"}' },
// 实际输入
{ role: 'user', content: userInput },
]这种方式更接近模型训练时的对话格式,有些场景下效果更好。但要注意:示例中的 assistant 消息会占用对话轮次,太多的话需要截断。
方式三:XML 标签分隔
const prompt = `
意图分类。
<example>
<input>退订单 ORD-001</input>
<output>{"intent": "refund", "orderId": "ORD-001"}</output>
</example>
<example>
<input>ORD-002 到哪了</input>
<output>{"intent": "query_order", "orderId": "ORD-002"}</output>
</example>
<input>${userInput}</input>
<output>
`XML 标签让示例的结构更清晰,模型更容易区分示例和实际输入。适合示例比较复杂的场景。
5. 动态示例选择
固定的示例有两个问题:
- 示例和用户输入不相关——用户问退款,示例里却没有退款的案例
- Token 浪费——固定示例不管用不用得到,都占上下文
更好的方式是动态选择:根据用户输入,从示例库中检索最相关的示例。
// src/services/prompt/example-selector.ts
type Example = {
input: string
output: string
tags: string[] // 用于匹配的标签
}
const EXAMPLE_LIBRARY: Example[] = [
{ input: '退订单 ORD-001', output: '{"intent": "refund"}', tags: ['refund'] },
{ input: '查 ORD-002 的状态', output: '{"intent": "query_order"}', tags: ['query'] },
{ input: '取消 ORD-003', output: '{"intent": "cancel"}', tags: ['cancel'] },
{ input: '商品质量太差了', output: '{"intent": "complaint"}', tags: ['complaint'] },
{ input: '发错货了怎么办', output: '{"intent": "complaint"}', tags: ['complaint'] },
{ input: '能改收货地址吗', output: '{"intent": "other"}', tags: ['other'] },
]
// 简单的标签匹配
function selectExamples(userInput: string, maxCount: number = 2): Example[] {
// 根据关键词匹配标签
const keywordTagMap: Record<string, string> = {
'退': 'refund', '退款': 'refund', '退回': 'refund',
'查': 'query', '状态': 'query', '到哪': 'query',
'取消': 'cancel',
'差': 'complaint', '投诉': 'complaint', '错': 'complaint',
}
const matchedTags = new Set<string>()
for (const [keyword, tag] of Object.entries(keywordTagMap)) {
if (userInput.includes(keyword)) {
matchedTags.add(tag)
}
}
// 按匹配度排序,取 top N
const scored = EXAMPLE_LIBRARY.map((ex) => ({
example: ex,
score: ex.tags.filter((t) => matchedTags.has(t)).length,
}))
return scored
.sort((a, b) => b.score - a.score)
.slice(0, maxCount)
.map((s) => s.example)
}
// 使用
function buildPrompt(userInput: string): string {
const examples = selectExamples(userInput, 2)
const exampleText = examples
.map((ex) => `输入:${ex.input}\n输出:${ex.output}`)
.join('\n\n')
return `
意图分类。
${exampleText}
输入:${userInput}
输出:
`
}更复杂的场景可以用 embedding 做语义相似度匹配——这就是 RAG 的思路,第 14 章会讲。
6. Few-shot 的常见陷阱
陷阱一:示例太多,Token 爆了
// ❌ 10 个示例,每个 100 字 → 1000 字纯示例
const prompt = `
${examples.map(e => `输入:${e.input}\n输出:${e.output}`).join('\n\n')}
输入:${userInput}
输出:
`示例占了大部分上下文,留给用户输入和模型输出的空间不够。
解决方案:控制示例数量在 2-3 个,用动态选择提高示例的相关性。
陷阱二:示例暗示了错误的模式
// ❌ 示例里的订单号都是 ORD-001,模型可能以为所有订单号都是 ORD-001
const examples = [
{ input: '退 ORD-001', output: '{"orderId": "ORD-001"}' },
{ input: '查 ORD-001', output: '{"orderId": "ORD-001"}' },
]模型可能从示例中学到「订单号总是 ORD-001」,而不是「订单号从输入中提取」。
解决方案:示例里的具体值要有变化。
陷阱三:示例和规则矛盾
const prompt = `
## 规则
金额单位是分,输出时要转换成元。
## 示例
输入:退款 100 元
输出:{"amount": 100} // ❌ 示例没有做转换,和规则矛盾
`模型不知道该听规则的还是听示例的。
解决方案:确保示例和规则一致。如果规则说「转换单位」,示例里的金额也要转换。
7. 一个完整的 Few-shot 示例
// src/services/prompt/sql-generator.ts
const SQL_EXAMPLES = [
{
input: '最近 7 天注册了多少用户',
output: "SELECT COUNT(*) FROM users WHERE created_at >= CURRENT_DATE - INTERVAL '7 days'",
},
{
input: '消费超过 1000 元的用户',
output: 'SELECT DISTINCT user_id FROM orders WHERE amount > 100000',
},
{
input: '上个月退款的订单数',
output: "SELECT COUNT(*) FROM orders WHERE status = 'refunded' AND created_at >= CURRENT_DATE - INTERVAL '30 days'",
},
]
const SQL_GENERATOR_PROMPT = `你是一个 SQL 查询生成助手。
## 数据库表结构
- users(id, name, email, created_at)
- orders(id, user_id, amount, status, created_at)
- amount 单位是分(integer)
- status 可选值:pending / shipped / completed / cancelled / refunded
## 注意事项
- 时间函数使用 PostgreSQL 语法
- 金额以元为单位输入时,在 SQL 中需要乘以 100 转换成分
## 示例
${SQL_EXAMPLES.map((e) => `输入:「${e.input}」\nSQL:${e.output}`).join('\n\n')}
## 任务
将用户输入翻译成 SQL 查询。只输出 SQL,不要解释。
`
export async function generateSQL(userInput: string): Promise<string> {
const response = await callModelAPI({
model: 'gpt-4o',
messages: [
{ role: 'system', content: SQL_GENERATOR_PROMPT },
{ role: 'user', content: userInput },
],
temperature: 0,
})
return response.choices[0].message.content.trim()
}总结
回顾这一节的要点:
- Few-shot 用具体示例教模型格式、风格和判断标准,比文字解释更直接
- 示例数量:2-3 个覆盖主要情况即可,不超过 5 个
- 示例质量:典型、覆盖关键差异、互相不矛盾、简短
- 三种组织方式:输入输出交替、角色消息包装、XML 标签分隔
- 动态示例选择比固定示例更高效——根据用户输入检索最相关的示例
- 常见陷阱:示例太多爆 Token、示例暗示错误模式、示例和规则矛盾
下一篇讲 Chain of Thought 使用边界——什么时候该让模型「想一想」,什么时候不该。