Few-shot 设计的底层逻辑——为什么你的示例没起到应有的效果
Few-shot 设计的底层逻辑——为什么你的示例没起到应有的效果
适读人群:在用 Few-shot 做 Prompt 工程的开发者 | 阅读时长:约 11 分钟 | 核心价值:理解 Few-shot 失效的真正原因,掌握让示例真正起作用的设计方法
有个同事找我看他的 Prompt,说加了 Few-shot 但效果没提升,甚至感觉更差了。
我看了一眼他的示例,大概是这样:
示例1:
输入:这个产品真的很好用
输出:正面
示例2:
输入:不太满意这次的服务
输出:负面
示例3:
输入:还行吧
输出:中性他在做情感分析,任务本身没问题。但这三个示例有个根本性的缺陷:它们示范的都是模型本来就能做对的简单情况。
那些让模型出错的 case——带讽刺语气的正面评价、双重否定句、夹杂着投诉的五星好评——一个示例都没有。
这就是 Few-shot 设计里最常见、也最不被意识到的问题。
Few-shot 的本质是什么
在用之前先想清楚它的本质:Few-shot 是在告诉模型你的分类标准和边界,不是在教它做任务本身。
模型已经会做情感分析了。你提供的示例,真正的作用是在说:「在你不确定的时候,按照我给的这些例子来判断。」
所以示例的价值不在于数量,在于覆盖边界案例。
如果你的示例都是简单的、模型本来就能处理的,那 Few-shot 对你没有帮助。真正有价值的示例,是那些「没有示例模型会判断错,有了示例它能判断对」的 case。
我做的对比实验
任务:对用户评论做情感分类(正面 / 负面 / 中性)。
测试集:200 条评论,其中包含 40 条「难例」——讽刺语气、双重否定、夹杂投诉的好评、夸赞竞品来贬低本品的等。
版本 A:随机选取的 5 个示例
示例1 → 正面
示例2 → 负面
示例3 → 中性
示例4 → 正面
示例5 → 负面版本 B:针对边界 case 设计的 5 个示例
示例1(讽刺):"哇这服务真是太好了,等了三天终于发货" → 负面
示例2(双重否定):"不是说不好用,就是不太顺手" → 负面
示例3(混合情感):"客服态度很好,但产品质量差强人意" → 中性(有明确好有明确坏时取中性)
示例4(隐含否定):"上一款比这款强多了" → 负面
示例5(夸张正面):"有了它,感觉人生都不一样了哈哈哈" → 正面测试结果(Claude 3.5 Sonnet,temperature=0):
| 测试集 | 版本A准确率 | 版本B准确率 |
|---|---|---|
| 全部200条 | 91% | 95% |
| 难例40条 | 67% | 88% |
| 简单160条 | 99% | 98% |
难例准确率差了 21 个百分点。而在简单样本上,两个版本几乎没有差异——因为模型本来就能做对。
这个数据说明了什么?Few-shot 对简单 case 几乎没有影响,对边界 case 有决定性影响。你的示例设计不当,不是没效果,是在浪费这个工具。
示例质量:什么是好示例
除了覆盖边界 case,好的示例还有几个特征:
1. 输入输出之间有明确的映射逻辑
坏的示例:输入很长,输出很简单,看不出为什么这个输入导致那个输出。
好的示例:输入特征和输出之间的关系是清晰的,或者有 chain-of-thought 解释。
带思维链的示例(在复杂任务里效果更好):
输入:虽然价格有点贵,但质量真的没话说,以后还会回购
分析:价格是负面因素,但被"质量没话说"和"还会回购"的强正面信号压倒
输出:正面2. 输入要有代表性,不要太极端
每个示例的输入应该是真实场景中会出现的文本,不要用人工构造的「教科书案例」。模型见过的真实数据分布和你人工构造的示例之间有差距,太极端的示例反而会让模型困惑。
3. 示例之间要有多样性
5 个示例都是同一类型的输入,不如 5 个示例覆盖 5 种不同情况。多样性比重复更有价值。
示例数量:多少合适
这个问题没有固定答案,但有几个规律:
通常 3-8 个足够了。超过 10 个之后,每增加一个示例的边际收益很小,但会增加 token 消耗和出现示例冲突的风险。
越复杂的任务,越需要更多示例。简单分类 3-5 个,复杂的多步骤推理可能需要 5-10 个。
质量优先于数量。3 个精心设计的边界 case,胜过 10 个随机挑的简单示例。
一个实用的测试方法:把你现有的示例一个一个删掉,看准确率有没有变化。如果删掉某个示例后准确率没有下降,说明这个示例是冗余的,可以替换成更有价值的 case。
示例顺序:不是没影响
这一点很多人不知道:示例的顺序对结果有影响,且影响方向不稳定。
有一类现象叫「近因效应」(Recency Effect):排在最后的示例对模型的影响更大。如果你最后一个示例是负面的,模型在处理模糊 case 时可能会偏向输出「负面」。
我的建议:
- 把最重要的示例(覆盖最典型边界的)放在最后,利用近因效应
- 如果任务有明显的类别分布(比如正面占 60%),示例的分布也要大致反映这个比例
- 不要让所有同类示例聚在一起,穿插排列
可以做一个简单的顺序测试:把你现有的示例随机打乱顺序,运行多次,看准确率的方差。方差大说明你的示例对顺序敏感,需要优化示例设计减少这种不稳定性。
示例格式:格式本身在传达信息
模型会从示例的格式里学到「期望输出的风格」。
如果你的示例输出都是一个词,模型倾向于输出一个词。如果示例输出都是一段解释+结论,模型会模仿这个结构。
所以示例的格式要和你真正期望的输出格式完全一致。不要在示例里用 {"label": "positive"} 然后期望正式输出是 Positive,格式不一致会引入噪声。
一个常见错误是示例之间格式不统一:
# 错误示范
示例1输入:xxx
示例1输出:正面
Input: xxx
Output: 负面
[输入]xxx
[输出]中性这三种格式混在一起,模型不知道该遵循哪种,会出现格式不稳定的输出。
动态 Few-shot:更高级的用法
固定的示例是静态的。但有时候,最好的示例应该是根据当前输入动态选择的。
思路:建立一个「示例库」,对每个新输入,用向量相似度找到最相关的示例,再组成 Prompt。
from typing import List, Tuple
import numpy as np
class DynamicFewShotSelector:
def __init__(self, embedding_client, example_store):
self.embedding_client = embedding_client
self.example_store = example_store # 已有嵌入的示例库
def get_embedding(self, text: str) -> List[float]:
response = self.embedding_client.embeddings.create(
model="text-embedding-3-small",
input=text
)
return response.data[0].embedding
def cosine_similarity(self, a: List[float], b: List[float]) -> float:
a_arr = np.array(a)
b_arr = np.array(b)
return float(np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr)))
def select_examples(self, query: str, n: int = 5) -> List[dict]:
"""选取与当前输入最相关的n个示例"""
query_embedding = self.get_embedding(query)
# 计算与所有示例的相似度
similarities = []
for example in self.example_store:
sim = self.cosine_similarity(query_embedding, example['embedding'])
similarities.append((sim, example))
# 按相似度排序,取前n个
similarities.sort(key=lambda x: x[0], reverse=True)
selected = [ex for _, ex in similarities[:n]]
# 把最相似的放最后(利用近因效应)
selected.reverse()
return selected
def build_few_shot_prompt(self, query: str, n: int = 5) -> str:
examples = self.select_examples(query, n)
prompt_parts = []
for ex in examples:
prompt_parts.append(f"输入:{ex['input']}\n输出:{ex['output']}")
prompt_parts.append(f"输入:{query}\n输出:")
return "\n\n".join(prompt_parts)动态 Few-shot 在以下场景特别有用:
- 示例库很大(几十到几百个),无法全部塞进 Prompt
- 输入多样性高,不同类型的输入需要不同的示例来引导
- 有一个持续增长的「好案例库」,希望新示例自动被利用
一个快速自查清单
在你下次设计 Few-shot 之前,问自己这几个问题:
- 我的示例覆盖了哪些边界 case?哪些边界 case 没有覆盖?
- 如果去掉某个示例,准确率会下降吗?不会下降的示例是冗余的
- 示例的格式和期望输出的格式完全一致吗?
- 示例之间有足够的多样性吗?还是都是同一种类型?
- 示例顺序有没有利用近因效应?
做到这几点,你的 Few-shot 效果不会差。
