Few-shot 示例怎么给:让大模型秒懂你的意图
上周有个做客服工单系统的朋友找我吐槽:同一个抽取订单信息的 prompt,接入模型后线上准确率忽高忽低,有时候输出的 JSON 里字段名带引号,有时候不带,有时候干脆在 JSON 前面加一句”好的,为您提取如下”。他把 system prompt 里的规则描述从三行加到二十行,情况没好转,token 账单倒是涨了一截。我看了他的 prompt,问题很典型:规则写得再细,模型也得靠”猜”去理解你说的”格式”到底长什么样;而只要给 2-3 个真实的 Input/Output 例子,模型立刻就知道该往哪个模子里套。这就是 few-shot prompting 的核心价值——它不是”教模型知识”(模型早就会了),而是用示例把输出格式和风格边界钉死,减少模型靠猜测拼凑答案的空间。选对 3-5 个有代表性的示例,往往比堆几百字的规则描述管用得多,而且更省 token。
什么时候必须用 few-shot
| 场景 | 是否需要 few-shot |
|---|---|
| 标准问答 / 总结 | 通常不需要,zero-shot 够用 |
| 自定义输出格式(如特殊 JSON 结构) | 需要,1-3 个示例即可 |
| 领域特定分类(自定义标签体系) | 需要,每类至少 1 个正例 |
| 风格模仿(语气、措辞) | 需要,3-5 个示例 |
| 实体抽取(自定义字段) | 强烈需要,示例覆盖边界情况 |
判断依据其实就一条:这个任务的”正确答案长什么样”,靠语言描述能不能说清楚? 能说清楚(比如”用一句话总结这段文字”)就别加示例,白白浪费 token;说不清楚(比如”输出的 JSON 里哪些字段可选、什么时候该填 null、枚举值到底用中文还是英文缩写”)就必须上示例,因为这些细节写成规则往往要绕好几层”如果……那么……否则……”,模型读起来反而费劲,还容易漏看某一条。
示例结构:Input-Output 对
最通用的 few-shot 格式是清晰的 Input / Output 标记:
EXAMPLES = [
{
"input": "订单 #A123 已于 2024-03-01 发货,预计 3 天到达。",
"output": '{"order_id":"A123","ship_date":"2024-03-01","eta_days":3}'
},
{
"input": "您的包裹 B456 正在运输中,大约需要 5 个工作日。",
"output": '{"order_id":"B456","ship_date":null,"eta_days":5}'
},
]
def build_few_shot_prompt(query: str) -> str:
parts = []
for ex in EXAMPLES:
parts.append(f"Input: {ex['input']}\nOutput: {ex['output']}")
parts.append(f"Input: {query}\nOutput:")
return "\n\n".join(parts)
注意:Output: 后面不加内容,让模型续写,这是续写触发比”请输出”指令更稳定的技巧。
这里多说一句底层原因:Chat 模型本质上还是在做”续写下一个 token”,只不过被指令微调过,学会了在看到类似对话轮次的结构时按角色扮演。当你在 prompt 里写满了”Input: xxx / Output: xxx”这种成对出现的模式,模型看到最后一个孤零零的 Output: 时,会强烈倾向于直接续写内容本身,而不是先寒暄一句”好的,这是结果”——因为前面所有示例里 Output: 后面从没出现过寒暄。反过来,如果你写”请按照上面的格式输出结果”这种指令句,模型会把它当成一个需要”回应”的请求,回应就容易带客套话。所以格式对齐的诀窍不是”讲清楚要求”,而是”让最后一步和前面的例子长得一模一样”,模型自然会顺着模式走下去。
我见过一个真实翻车案例:接的是某个海外模型的 API,prompt 里示例用的是 Input: / Output:,但请求体的 system message 里又额外写了一句”You must respond only with valid JSON”。结果模型偶尔会在 JSON 前面加一句 “Here is the valid JSON:“——因为这句强调像是在提醒模型”证明自己遵守了规则”,反而引诱它加一句表态。后来把这句删掉,只留示例本身,问题就消失了。这说明 few-shot 和额外的强调性指令有时会打架,示例的模式强度往往比一句孤立的指令更管用,冲突时以示例为准,删掉多余的指令噪音。
示例选择:质量远比数量重要
好示例的三个标准:
- 覆盖边界:至少包含一个含 null / 空值 / 特殊字符的边界案例
- 格式一致:所有示例的 Output 格式完全统一,包括空格和引号风格
- 短而典型:示例不要超过任务的典型复杂度,过长示例会浪费 token 且引导模型啰嗦
坏示例的常见问题:
# 反例:示例之间格式不一致(引发模型不稳定)
示例1 output: {"name": "张三"}
示例2 output: {name: '李四'} # 单引号!无引号键!
这种不一致在小规模测试时经常发现不了——你自己写示例的时候脑子里知道该用双引号,测两次也恰好都对。但线上量一大,模型会在两种格式之间随机摇摆,因为它从示例里学到的规则本身就是”两种写法都有可能对”。排查这类问题的笨办法但很有效:把你所有示例的 Output 一字不差地跑一遍 JSON.parse(或对应语言的解析函数),解析不过的示例本身就是错的,先把这一步做成单元测试,比事后调 prompt 效率高得多。
还有一个容易被忽略的坑是示例长度失衡。如果你的 3 个示例里有一个特别长(比如原文贴了一大段客户聊天记录),模型会误以为”这个任务通常输入都很长”,面对短输入时反而输出啰嗦、脑补细节。经验值:示例长度尽量控制在与真实请求同一量级,长的示例可以截取最关键的片段,而不是原样照搬。
示例数量与位置
┌─────────────────────────────────┐
│ System Prompt(角色 + 规则) │
├─────────────────────────────────┤
│ 示例 1: Input / Output │
│ 示例 2: Input / Output │
│ 示例 3: Input / Output │
├─────────────────────────────────┤
│ 当前任务 Input(真实请求) │
└─────────────────────────────────┘
- 示例放在 user message 里(不要放 system),更符合 Chat 模型的训练分布
- 数量建议:格式对齐 1-3 个,分类任务每类 1-2 个,上限约 8-10 个(超过边际效益下降且 token 贵)
- 排序:把最接近当前输入的示例放最后,模型注意力对末尾更敏感
“末尾更敏感”这个结论不是让你死记硬背,自己也能花十分钟验证。做法很简单:准备一批带标准答案的测试用例,固定一组示例,只调换顺序(比如把边界案例分别放在第一位和最后一位)跑两轮,对比准确率。我自己在一个分类任务上试过,同样的 3 个示例,把最容易混淆的那个边界案例放最后时,准确率比放第一位高出大约 8 个百分点——这个具体数字因任务和模型而异,但”排序影响准确率”这件事值得你在自己的场景里跑一遍,而不是照抄结论。
System prompt 和 few-shot 的分工也要分清楚:System 里放”角色定位 + 硬性规则”(比如”你是订单信息抽取助手,只输出 JSON,不要任何解释”),few-shot 放在 user message 里负责”给规则做示范”。有人图省事把示例也塞进 system,从实测效果看,多数 Chat 模型(尤其是走对话式微调的)对 user 轮次里出现的”历史对话”模式更敏感,因为这更贴近它训练时见过的真实多轮对话分布;塞进 system 反而有被模型当成”背景说明”一带而过的风险。
静态示例覆盖不了所有情况,可以用 embedding 检索动态注入:
from sentence_transformers import SentenceTransformer
import numpy as np
model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
example_embeddings = model.encode([ex["input"] for ex in EXAMPLE_POOL])
def get_top_k_examples(query: str, k: int = 3) -> list:
q_emb = model.encode([query])[0]
scores = np.dot(example_embeddings, q_emb)
top_k = np.argsort(scores)[::-1][:k]
return [EXAMPLE_POOL[i] for i in top_k]
这样每次请求都能拿到语义最近的示例,显著提升格式准确率。
这套动态检索方案上线前有几个坑你最好提前踩一遍:
- embedding 模型和任务语言要匹配:中文任务却用了英文语料训出来的 embedding 模型,检索出来的”最相似”示例经常牛头不对马嘴,因为语义空间本身就没对齐。中文场景优先选专门在中文语料上训练或微调过的模型,选型时以模型卡片标注的支持语言和评测集为准,别凭感觉猜。
- 示例池要去重和限流:
EXAMPLE_POOL如果是从历史日志里自动收集的,很容易混入内容几乎一样但措辞略有差异的重复样本,检索出来的 top-k 全是”同一类”,反而失去了覆盖边界情况的作用。上线前建议按语义相似度做一次聚类去重,每个簇里保留最典型的 1-2 条。 - 相似度分数要设阈值兜底:如果
query是一个完全没见过的新场景,np.dot算出来的最高分可能也很低,这时候硬塞进 top-k 反而误导模型。实践中可以给相似度设一个阈值,低于阈值就退回到一组固定的”通用示例”,而不是硬凑数。 - embedding 计算别放在请求路径上现算:
example_embeddings应该离线批量算好、缓存起来(存到向量库或者本地文件都行),线上请求只对当前query做一次 embedding 计算,否则每次请求都要重新编码整个示例池,延迟和成本都会明显上升。
并发场景下的注意事项:如果你的服务需要同时处理多个请求,SentenceTransformer 这类本地模型通常不是线程安全的,简单粗暴的做法是每个 worker 进程各自加载一份模型实例;更省资源的做法是把 embedding 服务单独拆成一个进程或轻量服务,用队列或者简单的 HTTP 接口对外提供编码能力,避免每个 worker 都占一份显存或内存。如果你用的是异步框架(比如 asyncio + httpx 调用 LLM API),检索这一步是纯 CPU/GPU 计算,记得用 run_in_executor 丢到线程池里跑,别在事件循环里同步阻塞,不然并发请求会互相卡队。
成本与效果的取舍
few-shot 不是免费的:每加一个示例,这段 token 都要在每次请求里重复计费。做个粗略估算——如果你的示例平均 100 token,加 3 个示例大概多花 300 token 的输入成本,按你接入的模型输入单价乘以日调用量,就能算出这笔”格式对齐税”到底值不值。这里给一个简单的判断表:
| 场景 | 建议 |
|---|---|
| 调用量小(日千次以内)、格式要求高 | 直接上 3-5 个静态示例,成本可以忽略 |
| 调用量大(日十万次以上)、格式相对固定 | 先用 few-shot 跑通,验证准确率后评估 fine-tuning,摊薄长期 token 成本 |
| 调用量大但输入场景差异大 | 动态检索示例,只在必要时注入,比固定塞 5 个示例更省 |
| 对准确率要求不极端 | 优先精简示例数量到 1-2 个,先测效果够不够,再加 |
常见问题
示例太多会不会让模型只会”背”示例? 会。超过 10 个示例后,模型倾向于过拟合示例格式,遇到稍有偏差的输入反而输出错误。控制在 5 个以内,配合清晰的规则说明更稳健。
示例中出现的字段,真实请求没有,模型会幻觉填值吗?
有这个风险。示例中如有可选字段,务必在至少一个示例的 Output 里显示 null 或省略该字段,明确告诉模型”此字段可为空”。
few-shot 和 fine-tuning 如何选择? few-shot 适合原型期和低频场景;如果同一任务每天调用超过万次且对准确率要求极高,考虑在少量标注数据上做 fine-tuning,推理时 token 消耗更少。
加了示例后模型输出反而变差了,是怎么回事? 排查顺序建议这样走:第一步检查示例本身是不是有格式错误(前面说的 JSON.parse 校验),第二步看示例和当前输入的任务类型是不是完全一致——我见过一次线上事故,是把”提取订单信息”的示例误用到了”提取用户投诉信息”的任务上,两者字段结构长得像,模型被旧示例带偏,输出了一堆订单相关的空字段。第三步检查示例总长度是不是把关键的当前输入”挤”到了 context 太靠前的位置——如果你的 system prompt、规则说明、几个长示例加起来已经占了大半个 context window,当前真正要处理的输入反而显得”不重要”,这时候适当精简示例或者把当前输入挪到最后强调的位置,效果通常会回升。
流式输出(stream)场景下 few-shot 还适用吗?
适用,而且更要注意格式收尾。流式场景下客户端通常是边收 token 边渲染,如果输出格式是 JSON,你需要保证模型不会在结构完整之前提前换行或者夹带解释性文字(比如生成到一半冒出一句”(以上是提取结果)”)。做法是在示例里明确演示”JSON 输出后立即结束”,不要有任何尾缀;同时在客户端做增量 JSON 解析时,用支持”部分 JSON”容错的解析器(比如允许未闭合括号先跳过渲染),而不是等收完整个流再一次性 JSON.parse,否则用户会盯着空白等好几秒才看到结果。
请求超时或者 429 限流了,是不是 few-shot 示例太长导致的? 两者有关联但不是直接因果。few-shot 示例变长会让单次请求的 prompt token 数变大,如果你接入的模型对单次请求有速率限制(常见的限制维度是 tokens-per-minute),示例越长,单位时间内能处理的请求数就越少,越容易撞到 429。排查时先看返回的错误信息里有没有明确写限流维度(是请求数超限还是 token 数超限),如果是 token 维度超限,第一反应就该是砍示例数量或者换成动态检索按需注入,而不是简单加重试。超时的情况通常和示例长度关系不大,更多是网络链路或模型自身排队导致,重试时记得用指数退避(比如首次等 1 秒,失败再等 2 秒、4 秒),别用固定间隔无脑重试,容易在网络抖动时把请求堆得更挤。
上线前自查清单
真要把 few-shot prompt 放到生产环境,建议照着这份清单过一遍,比事后线上排查省心得多:
- 所有示例的 Output 是否都能被目标格式的解析器(JSON/YAML/正则等)无错解析
- 示例之间的字段命名、引号风格、大小写是否完全一致
- 是否至少有一个示例覆盖了”空值/边界/异常输入”的情况
- 示例总 token 数是否在可接受的成本范围内,是否评估过动态检索的必要性
- 示例放的位置是 user message 还是 system——确认放对了地方
- 当前真实输入是否放在所有示例之后,且格式和示例保持一致(比如同样用
Input:前缀) - 是否用一批真实测试用例(至少覆盖 20-30 条,包含边界情况)跑过准确率,而不是凭感觉觉得”应该没问题”
延伸阅读:大模型应用开发模式 · 应用模式 Hub · Prompt 模板设计 · 思维链 CoT 怎么用