Few-shot与In-context Learning:不训练模型,让AI学会新技能
Few-shot与In-context Learning:不训练模型,让AI学会新技能
一、那个让预算部门皱眉的决定
2024年夏天,苏州某SaaS公司,产品经理张雨接到了一个任务:让AI学会公司特定的客服回复风格。
公司有一套固定的客服语气规范:
- 开头必须用"感谢您的反馈"
- 中间要引用具体的工单编号和时间
- 结尾要以"期待继续为您服务,祝您工作顺利!"收尾
- 语气要温和但专业,不能太口语化
- 技术术语必须用中文全称,不能用缩写
张雨找到了技术负责人赵磊,说想用AI来自动化客服回复,但需要让模型学会这套风格。
赵磊的第一反应:"那就微调吧,收集1000条标注数据,fine-tune一个模型。"
张雨皱眉:"微调要多少钱?要多久?"
赵磊查了一下:"用GPT-4微调,算数据标注的人工成本,怎么也得3-5万,还要2周时间。另外以后每次风格规范更新,都要重新微调……"
会议室沉默了。
第二天,赵磊带着一份方案重新出现在会议室:"我研究了一晚上,发现用Few-shot就能解决这个问题,成本几乎是零,今天就能上线。"
这篇文章,就是深入讲清楚为什么Few-shot能做到这件事,原理是什么,以及Java工程师如何用好它。
二、In-context Learning的本质:为什么给几个例子AI就能"学会"
这是Few-shot最让人困惑的地方:你没有修改模型的任何参数,只是在输入里给了几个例子,模型就能按照例子的风格/规则来输出。
这是怎么做到的?
2.1 大模型是"元学习者"
理解ICL的关键在于认识到:现代大模型经过大规模预训练后,本质上成了一个元学习者(Meta-learner)——它不只是学会了"语言",还学会了"如何从示例中学习"这种能力本身。
在预训练数据中,模型见过无数这样的模式:
问题类型:数学应用题
例子1:苹果3元一个,买5个多少钱?答:15元
例子2:香蕉2元一根,买8根多少钱?答:16元
新问题:橙子4元一个,买6个多少钱?模型在预训练时就见过海量的"给例子→解新题"的模式,所以当你在推理时给几个例子,它能识别出"这是在做示范,后面要我按照这个模式来"。
2.2 不是"学习",是"激活"
更准确的描述:Few-shot不是让模型学会新技能,而是让模型激活(activate)它已经具备的能力。
// 类比:Java的泛型和类型推断
// 你不需要告诉编译器"学会"如何处理Integer,
// 你只需要给足够的上下文,让它"推断"出正确的类型
List<String> list = new ArrayList<>(); // 类型推断:这是String的列表
list.add("hello"); // 编译器知道这里只能add String
// Few-shot类似:
// 你给几个"输入→输出"的例子
// 模型激活了它关于"这种输入风格→这种输出风格"的已有理解研究发现,如果你让模型处理一个任务,但故意把示例的标签弄错(比如把正面情感的句子标为"负面"),模型的输出几乎不受影响。
这证明了:Few-shot示例的作用不是"教导"模型新规则,而是指定格式和激活相关的行为模式。
2.3 In-context Learning的范式
ICL的完整概念包含了比Few-shot更广的范围:
Zero-shot(零样本):直接说任务描述,没有任何例子
"将以下句子翻译成英文:..."
One-shot(单样本):给1个例子
"输入:今天天气很好 输出:Today the weather is great
输入:..."
Few-shot(少样本):给2-8个例子
(通常3-5个效果最稳定)
Many-shot(多样本):给更多例子(利用长上下文窗口)
(近年来越来越流行,有时效果接近微调)三、零样本 vs 少样本 vs 多样本:横向对比
3.1 对比表格
| 维度 | Zero-shot | Few-shot (3-5个) | Many-shot (50+个) |
|---|---|---|---|
| Token成本 | 最低 | 中等 | 高 |
| 效果稳定性 | 一般 | 好 | 很好 |
| 对复杂任务 | 效果有限 | 明显提升 | 接近微调 |
| 更新维护 | 只改Prompt | 更新示例 | 更新示例库 |
| 对格式控制 | 较弱 | 较强 | 很强 |
3.2 实际效果数据
以"情感分类"任务为例(引用研究数据):
| 方法 | 准确率(GPT-3 175B) |
|---|---|
| Zero-shot | 67.1% |
| 1-shot | 71.3% |
| 4-shot | 76.9% |
| 8-shot | 78.1% |
| 微调(全量) | 83.2% |
关键观察:
- Few-shot(4-8个)显著优于Zero-shot
- 从1到4个样本,提升明显(+5.6%)
- 从4到8个样本,提升减少(+1.2%)——边际效益递减
- 和全量微调相比,仍有差距(约4-5%),但成本低了几个数量级
3.3 什么时候各种方式最合适
用Zero-shot的场景:
// 任务简单明确,大模型天然擅长
// 例如:语言翻译、格式转换、摘要
String zeroShotPrompt = "将以下JSON转换为Markdown表格:\n" + jsonContent;用Few-shot的场景:
// 需要特定风格、特定格式、特定领域约束
// 例如:公司定制化回复、特定数据提取格式
String fewShotPrompt = buildFewShotPrompt(examples, newInput);用微调的场景(Few-shot搞不定时):
- 任务高度专业化(医学影像报告、法律文书)
- 需要注入大量私有知识(公司内部规范、专有名词体系)
- 任务对准确率要求极高(医疗诊断辅助)
- 需要大幅度改变模型的行为风格(几乎重新定义模型的"人格")
四、Few-shot示例的选取原则:好例子决定成败
这是最被低估的工程细节。随便给几个例子和精心选择例子,效果差别极大。
4.1 原则一:相关性(Relevance)
示例应该和你要处理的输入语义相关。
// 任务:对IT工单做情感分类(紧急/普通/低优先级)
// 差的例子(和IT工单不相关):
// 例1:"我今天买了个苹果,味道很好。" -> 普通
// 例2:"我的电脑坏了!!!" -> 紧急
// 好的例子(和任务场景相关):
// 例1:"用户报告邮件系统无法登录,影响50名员工。" -> 紧急(影响范围大)
// 例2:"请问会议室预约系统什么时候能升级?" -> 低优先级(无业务影响)
// 例3:"VPN连接偶尔断开,不影响正常使用。" -> 普通(偶发性问题)相关性的Java实现原理(见第五节):动态选择最相关的示例。
4.2 原则二:多样性(Diversity)
示例要覆盖任务的不同"子类型",不要用多个几乎一样的例子。
// 坏:3个都是"严重故障"类的例子
// 示例1:"数据库宕机" -> 紧急
// 示例2:"服务器崩溃" -> 紧急
// 示例3:"所有用户无法访问" -> 紧急
// 结果:模型对"低优先级"的处理能力没有被激活
// 好:覆盖所有目标类别
// 示例1:紧急场景 × 2个不同类型
// 示例2:普通场景 × 2个不同类型
// 示例3:低优先级 × 1个4.3 原则三:质量(Quality)
每个示例都是高质量的"真实案例",不要为了凑数放质量差的例子。
关键发现:研究表明,使用1个高质量示例,效果往往优于使用5个质量一般的示例。
// 质量评估标准
@Service
public class ExampleQualityEvaluator {
public double evaluateExample(FewShotExample example) {
double score = 1.0;
// 检查1:输入是否有代表性?
// (不要用边界情况作为示例)
// 检查2:标签是否清晰无歧义?
// 如果有人看了这个例子,不同的人会有不同的理解,就是歧义的
// 检查3:输入和输出是否符合任务格式?
if (!example.getInput().matches(inputPattern)) score -= 0.3;
if (!example.getOutput().matches(outputPattern)) score -= 0.3;
// 检查4:示例长度是否合适?
// 太短(缺乏上下文)或太长(占用过多token)都不好
int inputLength = countTokens(example.getInput());
if (inputLength < MIN_EXAMPLE_LENGTH || inputLength > MAX_EXAMPLE_LENGTH) {
score -= 0.2;
}
return score;
}
}4.4 原则四:顺序(Order)
研究发现,示例的顺序影响模型输出(Recency Bias)。模型对最后几个示例更敏感。
实践建议:
- 把最典型、最高质量的示例放在最后(紧挨着要处理的输入)
- 如果有"特别难的情况",放在倒数第2个位置
- 开头放普通示例,让模型建立基本理解
五、动态Few-shot:从数据库检索最相关的示例(RAG+Few-shot)
静态Few-shot(每次都用同样的几个例子)的问题:
对于一个IT工单分类系统,你的工单可能有几十种不同的类型(网络问题、硬件问题、软件问题、账号问题……),用几个固定的例子无法覆盖所有情况。
动态Few-shot:根据当前输入,从示例库中检索最相关的示例,动态构建Few-shot Prompt。
5.1 架构图
用户输入
│
▼
[嵌入模型] → 查询向量
│
▼
[向量数据库] → 检索Top-K最相似示例
│
▼
[Few-shot Prompt构建]
│ 系统提示 + 动态选出的示例 + 用户输入
▼
[大模型] → 按照示例风格输出结果5.2 Java实现:动态Few-shot示例管理系统
@Service
public class DynamicFewShotService {
private final VectorStore vectorStore;
private final ChatClient chatClient;
private final EmbeddingModel embeddingModel;
@Value("${fewshot.examples-per-class:2}")
private int examplesPerClass;
@Value("${fewshot.max-total-examples:6}")
private int maxTotalExamples;
/**
* 动态Few-shot推理主方法
*/
public String inferWithDynamicFewShot(String input, String taskDescription) {
// 1. 检索最相关的示例
List<FewShotExample> relevantExamples = retrieveRelevantExamples(input);
// 2. 去重和多样性过滤(确保示例覆盖不同类别)
List<FewShotExample> diverseExamples = ensureDiversity(relevantExamples);
// 3. 构建动态Prompt
String prompt = buildFewShotPrompt(taskDescription, diverseExamples, input);
log.info("为输入「{}」选择了{}个示例: {}",
input.substring(0, Math.min(50, input.length())),
diverseExamples.size(),
diverseExamples.stream().map(FewShotExample::getId).collect(toList()));
// 4. 调用大模型
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
/**
* 从向量数据库检索最相关的示例
*/
private List<FewShotExample> retrieveRelevantExamples(String input) {
// 先检索更多候选,然后筛选
List<Document> candidates = vectorStore.similaritySearch(
SearchRequest.query(input)
.withTopK(maxTotalExamples * 3) // 检索3倍候选,留余地筛选
.withSimilarityThreshold(0.65)
);
return candidates.stream()
.map(doc -> FewShotExample.fromDocument(doc))
.filter(example -> example.getQualityScore() >= 0.7) // 过滤低质量示例
.collect(toList());
}
/**
* 确保示例多样性:每个类别最多examplesPerClass个,总数不超过maxTotalExamples
*/
private List<FewShotExample> ensureDiversity(List<FewShotExample> examples) {
Map<String, List<FewShotExample>> byLabel = examples.stream()
.collect(groupingBy(FewShotExample::getLabel));
List<FewShotExample> diverse = new ArrayList<>();
// 每个类别取最相似的N个
byLabel.forEach((label, labelExamples) -> {
labelExamples.stream()
.sorted(Comparator.comparingDouble(FewShotExample::getSimilarity).reversed())
.limit(examplesPerClass)
.forEach(diverse::add);
});
// 总数截断,保留最相似的
return diverse.stream()
.sorted(Comparator.comparingDouble(FewShotExample::getSimilarity).reversed())
.limit(maxTotalExamples)
// 重排序:最相关的放最后(利用Recency Bias)
.sorted(Comparator.comparingDouble(FewShotExample::getSimilarity))
.collect(toList());
}
/**
* 构建Few-shot Prompt
*/
private String buildFewShotPrompt(String taskDescription,
List<FewShotExample> examples,
String newInput) {
StringBuilder sb = new StringBuilder();
// 任务描述
sb.append(taskDescription).append("\n\n");
// 示例区
sb.append("以下是一些参考示例:\n\n");
for (int i = 0; i < examples.size(); i++) {
FewShotExample ex = examples.get(i);
sb.append(String.format("示例%d:\n", i + 1));
sb.append("输入:").append(ex.getInput()).append("\n");
sb.append("输出:").append(ex.getOutput()).append("\n\n");
}
// 新输入
sb.append("现在请处理以下输入(严格按照示例的格式和风格):\n");
sb.append("输入:").append(newInput).append("\n");
sb.append("输出:");
return sb.toString();
}
/**
* 添加新示例到示例库
*/
public void addExample(FewShotExample example) {
// 计算示例的嵌入向量
float[] embedding = embeddingModel.embed(example.getInput());
// 存储到向量数据库
Document doc = new Document(
example.getId(),
example.getInput(),
Map.of(
"output", example.getOutput(),
"label", example.getLabel(),
"quality_score", example.getQualityScore(),
"created_at", LocalDateTime.now().toString()
)
);
vectorStore.add(List.of(doc));
log.info("已添加新示例:id={}, label={}", example.getId(), example.getLabel());
}
}5.3 应用回到开头的故事:客服回复风格
@Service
public class CustomerServiceReplyService {
private final DynamicFewShotService dynamicFewShotService;
// 客服风格任务描述
private static final String TASK_DESCRIPTION = """
你是公司的客服助手,请按照以下规范回复客户工单:
1. 开头必须是"感谢您的反馈,"
2. 引用工单编号和时间
3. 结尾必须是"期待继续为您服务,祝您工作顺利!"
4. 语气温和专业
5. 技术术语使用中文全称
""";
public String generateReply(CustomerTicket ticket) {
String input = String.format(
"工单编号:%s\n提交时间:%s\n客户问题:%s",
ticket.getId(),
ticket.getCreatedAt(),
ticket.getContent()
);
return dynamicFewShotService.inferWithDynamicFewShot(input, TASK_DESCRIPTION);
}
/**
* 初始化:从历史优质客服回复中构建示例库
*/
@PostConstruct
public void initializeExampleLibrary() {
// 从数据库加载历史优质回复(人工筛选的高质量样本)
List<TicketReplyPair> qualityReplies = ticketRepository.findHighQualityReplies();
qualityReplies.forEach(pair -> {
FewShotExample example = FewShotExample.builder()
.id(UUID.randomUUID().toString())
.input(pair.getTicketContent())
.output(pair.getReplyContent())
.label(pair.getTicketType()) // 用于多样性过滤
.qualityScore(pair.getHumanRating() / 5.0)
.build();
dynamicFewShotService.addExample(example);
});
log.info("已初始化{}条客服示例", qualityReplies.size());
}
}六、Chain-of-Thought Few-shot:带推理链的示例
普通Few-shot给的是"输入→输出"示例。
Chain-of-Thought(CoT)Few-shot给的是"输入→推理过程→输出"示例。
6.1 为什么推理链能提升效果
普通Few-shot示例:
问题:小明有5个苹果,给了小红2个,又买了3个,现在有几个?
答案:6个
CoT Few-shot示例:
问题:小明有5个苹果,给了小红2个,又买了3个,现在有几个?
推理:小明初始有5个苹果。给了小红2个后:5-2=3个。又买了3个:3+3=6个。
答案:6个为什么CoT有效:
大模型是自回归生成的,每一步的生成都基于之前的内容。
如果模型在生成答案之前先生成了"中间推理步骤",这些中间步骤成为了后续生成的"上下文"。这个上下文引导模型沿着正确的推理路径走,减少了"短路到错误答案"的概率。
Java类比:
// 不用CoT:相当于直接return一个复杂计算结果
public int solve(int a, int b, int c) {
return a - b + c; // 容易出错
}
// CoT:相当于把中间计算步骤显式写出来,每步都有校验
public int solveWithSteps(int a, int b, int c) {
int afterGive = a - b; // 步骤1:给出之后的数量
System.out.println("Step 1: " + afterGive);
int afterBuy = afterGive + c; // 步骤2:买入之后的数量
System.out.println("Step 2: " + afterBuy);
return afterBuy;
}6.2 代码任务中的CoT Few-shot
对Java工程师来说,CoT在代码相关任务中特别有用:
String cotCodePrompt = """
任务:分析以下Java代码的时间复杂度
示例1:
代码:
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
sum += array[i][j];
}
}
分析过程:
- 外层循环:执行n次
- 内层循环:对每个外层循环执行n次
- 总操作次数:n × n = n²
- 没有其他影响复杂度的操作
时间复杂度:O(n²)
示例2:
代码:
int left = 0, right = n - 1;
while (left <= right) {
int mid = (left + right) / 2;
if (array[mid] == target) return mid;
else if (array[mid] < target) left = mid + 1;
else right = mid - 1;
}
分析过程:
- 每次循环将搜索范围减半
- 初始范围:n,1次后:n/2,2次后:n/4,...,k次后:n/2^k
- 循环结束条件:n/2^k = 1,即 k = log₂(n)
- 最多执行 log₂(n) 次循环
时间复杂度:O(log n)
现在请分析:
代码:
%s
分析过程:
""".formatted(targetCode);6.3 Zero-shot CoT(神奇的"让我们一步步想")
2022年的研究发现,对于没有示例的情况,只需要在Prompt末尾加一句"让我们一步步来思考"(Let's think step by step),模型的复杂推理能力就会显著提升。
// Zero-shot CoT
String zeroShotCoTPrompt = question + "\n\n让我们一步步来分析:";
// 用中文:
String chineseZeroShotCoT = question + "\n\n请一步步分析:";
// 或:
String chineseZeroShotCoT2 = question + "\n\n让我们逐步推导:";这再次证明了ICL的本质:这句话激活了模型"做推理"的行为模式。
七、示例数量:N个示例的收益递减
7.1 经验数据
1个示例:相比Zero-shot,平均提升约5-8%
2个示例:再提升3-5%
3-4个示例:再提升2-3%
5-8个示例:再提升1-2%(边际效益显著递减)
8+个示例:几乎没有提升,有时反而下降(Context太长,模型"注意力分散")一般建议:3-5个高质量示例是性价比最高的选择。
7.2 上下文长度的成本
更多示例 = 更多token = 更高API成本:
// 成本估算
@Service
public class FewShotCostEstimator {
public CostEstimate estimateCost(List<FewShotExample> examples,
int queryCount,
double pricePerMillionTokens) {
int examplesTokens = examples.stream()
.mapToInt(ex -> countTokens(ex.getInput() + ex.getOutput()))
.sum();
// 每次API调用都要发送所有示例
long totalExampleTokens = (long) examplesTokens * queryCount;
double cost = totalExampleTokens / 1_000_000.0 * pricePerMillionTokens;
return CostEstimate.builder()
.tokensPerCall(examplesTokens)
.totalTokens(totalExampleTokens)
.estimatedCostUSD(cost)
.recommendation(cost > 100 ? "考虑使用Prompt Caching减少重复token成本" : "成本合理")
.build();
}
}7.3 Prompt Caching:重复示例的成本优化
如果你的示例是固定的(静态Few-shot),每次API调用都要重新发送相同的示例,是浪费。
使用Prompt Caching可以缓存重复的前缀部分:
// Anthropic的Prompt Caching(Java伪代码)
@Service
public class CachedFewShotService {
public String inferWithCaching(String input) {
// 固定的示例部分(标记为可缓存)
String cachedSystemPrompt = buildSystemWithExamples(); // 包含固定示例
ChatRequest request = ChatRequest.builder()
.model("claude-3-5-sonnet-20241022")
.system(SystemBlock.builder()
.content(cachedSystemPrompt)
.cacheControl(CacheControl.EPHEMERAL) // 缓存这部分!
.build())
.user(input) // 只有这部分是变化的
.build();
// 第一次:缓存建立,正常计费
// 后续调用:缓存命中,成本降低约90%
return anthropicClient.chat(request).getContent();
}
}八、Few-shot的局限:什么情况下必须微调
8.1 Few-shot的上限
Few-shot有一个不可逾越的上限:模型必须在预训练中见过这类任务的一般形式。
如果任务是大模型从未见过的全新类型,Few-shot可以解释格式但无法注入能力本身。
典型场景:专业领域特定的推理能力
例子1(Few-shot能搞定):
任务:按公司模板写邮件
原理:写邮件是预训练中大量出现的任务,模型有能力,只需要示例指定格式
例子2(Few-shot搞不定,需要微调):
任务:根据病理报告图像特征判断癌症类型
原理:这需要专业的医学知识,不是通过几个示例能激活的能力8.2 什么时候必须微调
| 场景 | Few-shot能解决? | 原因 |
|---|---|---|
| 改变输出格式/风格 | 能 | 格式是模式问题 |
| 注入特定领域词汇 | 有限 | 词汇超出token限制 |
| 注入大量私有知识 | 不能 | 知识量超出上下文 |
| 改变基础推理方式 | 不能 | 改变能力需要微调 |
| 处理非常专业的任务 | 不能 | 需要专业能力积累 |
| 极高准确率要求 | 通常不能 | Few-shot有天花板 |
8.3 Few-shot+微调的组合方案
实际上,最好的方案往往是微调+Few-shot的组合:
- 对基础模型进行领域微调(注入专业知识、改变基础行为)
- 在微调后的模型上使用Few-shot(精细化输出格式和风格)
// 这个思路在RAG中体现为:
// 步骤1:微调让模型理解领域知识(如医疗术语)
// 步骤2:RAG提供具体的上下文和相关文档
// 步骤3:Few-shot定制输出格式
// 三者结合,能达到单一方法无法达到的效果九、实战:用Few-shot让AI学会特定领域的写作风格
把所有知识整合成一个完整的Spring AI实现案例。
9.1 需求:技术博客自动生成系统
假设你维护一个技术博客,每篇文章都有固定的风格:
- 开头必须用"作为Java开发者,你可能遇到过..."
- 使用代码注释来解释原理
- 每个重要知识点用"知识点小结"格式列出
- 结尾用"推荐阅读"列出相关资源
9.2 完整Spring AI实现
@Configuration
public class FewShotBlogConfig {
@Bean
public ChatClient chatClient(ChatModel chatModel) {
return ChatClient.builder(chatModel)
.defaultSystem("""
你是一个Java技术博客写作助手。
严格按照示例的风格、格式和结构来写文章。
不要偏离示例中展示的格式模式。
""")
.build();
}
}@Service
@Slf4j
public class TechBlogGenerationService {
private final ChatClient chatClient;
private final ExampleRepository exampleRepository;
private final EmbeddingModel embeddingModel;
private final VectorStore vectorStore;
/**
* 生成技术博客文章
*/
public BlogArticle generateBlogArticle(String topic, String keyPoints) {
// 1. 动态选择最相关的风格示例
List<BlogExample> examples = selectRelevantExamples(topic, 3);
// 2. 构建包含示例的Prompt
String prompt = buildBlogPrompt(examples, topic, keyPoints);
// 3. 生成文章(使用较低的temperature保持风格一致性)
String content = chatClient.prompt()
.user(prompt)
.options(OpenAiChatOptions.builder()
.temperature(0.3)
.maxTokens(3000)
.build())
.call()
.content();
// 4. 验证格式(检查必要的结构元素是否存在)
validateBlogFormat(content);
return BlogArticle.builder()
.topic(topic)
.content(content)
.generatedAt(LocalDateTime.now())
.examplesUsed(examples.stream().map(BlogExample::getId).collect(toList()))
.build();
}
private List<BlogExample> selectRelevantExamples(String topic, int count) {
// 向量检索最相关的博客示例
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(topic)
.withTopK(count * 2)
.withSimilarityThreshold(0.6)
);
return docs.stream()
.map(BlogExample::fromDocument)
.distinct()
.limit(count)
// 最相关的放最后(Recency Bias优化)
.sorted(Comparator.comparingDouble(BlogExample::getRelevanceScore))
.collect(toList());
}
private String buildBlogPrompt(List<BlogExample> examples,
String topic, String keyPoints) {
StringBuilder sb = new StringBuilder();
// 任务说明
sb.append("请写一篇关于「").append(topic).append("」的Java技术博客文章。\n\n");
sb.append("需要覆盖的关键知识点:\n").append(keyPoints).append("\n\n");
// Few-shot示例
sb.append("以下是风格参考示例,请严格按照这些示例的格式和写作风格:\n");
sb.append("=" .repeat(50)).append("\n\n");
for (int i = 0; i < examples.size(); i++) {
BlogExample ex = examples.get(i);
sb.append(String.format("【风格示例 %d:%s】\n\n", i + 1, ex.getTopic()));
sb.append(ex.getContent());
sb.append("\n\n").append("=".repeat(50)).append("\n\n");
}
// 生成指令
sb.append("现在,请按照以上示例的风格,写关于「")
.append(topic).append("」的文章:\n\n");
return sb.toString();
}
private void validateBlogFormat(String content) {
List<String> missingElements = new ArrayList<>();
if (!content.contains("作为Java开发者")) {
missingElements.add("开头格式");
}
if (!content.contains("知识点小结")) {
missingElements.add("知识点小结板块");
}
if (!content.contains("推荐阅读")) {
missingElements.add("推荐阅读板块");
}
if (!missingElements.isEmpty()) {
log.warn("生成的文章缺少格式元素: {}", missingElements);
// 可以选择重新生成或人工检查
}
}
}9.3 示例库维护策略
@Service
public class ExampleLibraryManager {
/**
* 反馈驱动的示例优化
* 当用户觉得某次生成质量特别好,把它加入示例库
*/
public void promoteToExample(BlogArticle article, double userRating) {
if (userRating >= 4.5) { // 只有高质量的才进示例库
BlogExample newExample = BlogExample.builder()
.id(UUID.randomUUID().toString())
.topic(article.getTopic())
.content(article.getContent())
.qualityScore(userRating / 5.0)
.createdAt(LocalDateTime.now())
.build();
// 检查与现有示例的重复性
if (!isDuplicate(newExample)) {
vectorStore.add(List.of(newExample.toDocument()));
log.info("优质生成案例已加入示例库: topic={}", article.getTopic());
}
}
}
/**
* 定期清理低质量示例
*/
@Scheduled(cron = "0 0 2 * * 0") // 每周日凌晨2点
public void purgeOutdatedExamples() {
List<BlogExample> allExamples = exampleRepository.findAll();
// 删除:质量分低于0.6的,或超过6个月未被使用的
allExamples.stream()
.filter(ex -> ex.getQualityScore() < 0.6 ||
ex.getLastUsed().isBefore(LocalDateTime.now().minusMonths(6)))
.forEach(ex -> {
vectorStore.delete(List.of(ex.getId()));
log.info("已清理过期示例: id={}", ex.getId());
});
}
}十、FAQ
Q1:Few-shot示例里的"错误"例子有用吗?
A:有用,但要谨慎。对比示例("这样不对,这样才对")可以帮助模型理解边界情况。但错误示例的数量要远少于正确示例(建议比例1:4),否则模型可能混淆。
Q2:用真实历史数据作为Few-shot示例有什么风险?
A:主要有两个风险:
- 隐私泄露:如果真实数据包含用户信息,放入Prompt后会被发送到API提供商的服务器
- 分布偏移:历史数据可能不代表未来的数据分布,导致示例与新输入不匹配
建议:对真实数据做脱敏处理,或用合成数据替代真实数据。
Q3:Few-shot能解决模型的语言风格问题吗(比如让模型说话更像人,不那么"AI腔")?
A:部分可以解决。Few-shot能影响输出风格,但如果模型在预训练中强化了"AI腔"(过度礼貌、结构化列点等),需要大量的高质量Few-shot示例,加上明确的风格指令,才能有效矫正。对于严重的风格问题,微调更有效。
Q4:Few-shot prompt的token消耗很高怎么办?
A:三个策略:
- 用Prompt Caching缓存固定的示例部分(Anthropic、OpenAI都支持)
- 压缩示例:保留关键输入输出,删除不必要的解释
- 分层方案:先用Zero-shot快速筛选,对不确定的结果再用Few-shot处理
Q5:如何验证Few-shot是否真的有效?
A:构建评估集做量化对比:
- 准备100个带标准答案的测试用例
- 分别测试Zero-shot和Few-shot的准确率
- 如果提升幅度大于统计误差(通常需要>3%的提升才有意义),则Few-shot有效
- 用不同的示例集测试,验证效果的稳定性(不同示例集的效果差距太大则示例质量有问题)
十一、总结
Few-shot不是魔法,而是利用了大模型"元学习"能力的工程技巧。
| 概念 | 本质 | 最大价值 |
|---|---|---|
| Zero-shot | 直接描述任务 | 简单任务,最低成本 |
| Few-shot | 示例激活行为 | 格式/风格定制,无需微调 |
| CoT Few-shot | 示例包含推理链 | 复杂推理任务 |
| 动态Few-shot | 检索最相关示例 | 覆盖多样任务类型 |
| Many-shot | 利用长上下文 | 接近微调的效果 |
Few-shot的核心工程价值:
在不花费微调成本(时间+金钱)的情况下,为模型提供"行为定制"的能力。对于格式定制、风格学习、领域适应,Few-shot往往是最快最经济的解决方案。
对于赵磊面临的客服回复风格问题:用30个精心挑选的优质客服回复作为示例库,结合动态Few-shot检索,一下午就能上线一个效果不亚于微调的系统,而且可以随时更新维护。
十二、Few-shot高级技巧:让示例发挥最大价值
12.1 示例格式的标准化
统一的示例格式让模型更容易识别示例和新输入之间的边界:
@Component
public class FewShotPromptFormatter {
// 方案一:XML标签格式(Claude特别擅长处理)
public String formatXMLStyle(List<FewShotExample> examples, String newInput) {
StringBuilder sb = new StringBuilder();
sb.append("<examples>\n");
for (FewShotExample ex : examples) {
sb.append("<example>\n");
sb.append("<input>").append(ex.getInput()).append("</input>\n");
sb.append("<output>").append(ex.getOutput()).append("</output>\n");
sb.append("</example>\n");
}
sb.append("</examples>\n\n");
sb.append("<input>").append(newInput).append("</input>\n");
sb.append("<output>"); // 让模型继续补全
return sb.toString();
}
// 方案二:角色扮演格式(对话类任务效果好)
public String formatDialogStyle(List<FewShotExample> examples, String newInput) {
StringBuilder sb = new StringBuilder();
for (FewShotExample ex : examples) {
sb.append("用户:").append(ex.getInput()).append("\n");
sb.append("助手:").append(ex.getOutput()).append("\n\n");
}
sb.append("用户:").append(newInput).append("\n");
sb.append("助手:");
return sb.toString();
}
// 方案三:Q&A格式(知识问答任务)
public String formatQAStyle(List<FewShotExample> examples, String newInput) {
StringBuilder sb = new StringBuilder();
for (FewShotExample ex : examples) {
sb.append("Q: ").append(ex.getInput()).append("\n");
sb.append("A: ").append(ex.getOutput()).append("\n\n");
}
sb.append("Q: ").append(newInput).append("\n");
sb.append("A:");
return sb.toString();
}
}不同任务推荐的格式:
- 客服/对话类:角色扮演格式
- 数据提取/分类:XML标签格式(格式清晰,易于解析输出)
- 知识问答:Q&A格式
- 代码生成:代码注释+函数签名格式
12.2 负例示范(Negative Examples)
除了"正确的示例",加入"错误的对比示例"可以更清晰地定义边界:
// 数据提取任务:告诉模型哪些格式是错误的
String negativeExamplePrompt = """
任务:从文本中提取手机号码,格式必须是11位数字
【正确示例】:
文本:"联系我:13812345678"
提取结果:13812345678
【正确示例】:
文本:"电话:+86 138-1234-5678"
提取结果:13812345678(去除+86和连字符,保留11位数字)
【错误示例(不要这样做)】:
文本:"联系我:13812345678"
错误结果:+86 138-1234-5678(不要改变格式,保持原始11位数字)
【错误示例(不要这样做)】:
文本:"没有手机号"
错误结果:null(应该返回空字符串"",而不是null)
现在请处理:
文本:"%s"
提取结果:
""".formatted(inputText);12.3 渐进式Few-shot(从简单到复杂)
示例的顺序按照复杂度递进,帮助模型"暖机":
// 代码复杂度分析任务
String progressiveFewShotPrompt = """
任务:分析Java代码的时间复杂度
// 级别1:简单循环(帮助模型建立基础理解)
代码:
for (int i = 0; i < n; i++) {
System.out.println(i);
}
分析:一个循环执行n次,每次O(1)操作。
时间复杂度:O(n)
// 级别2:嵌套循环
代码:
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
sum += a[i][j];
}
}
分析:外层循环n次,内层循环从i到n,平均n/2次。总操作数≈n²/2。
时间复杂度:O(n²)
// 级别3:带递归
代码:
int fib(int n) {
if (n <= 1) return n;
return fib(n-1) + fib(n-2);
}
分析:每次调用分裂成两个子问题,深度n,总节点数2^n。
时间复杂度:O(2^n)(未优化的朴素递归)
// 现在请分析:
代码:
%s
分析:
时间复杂度:
""".formatted(targetCode);12.4 温度感应式Few-shot(根据置信度决定示例数量)
@Service
public class AdaptiveFewShotService {
/**
* 先尝试Zero-shot,置信度低时自动增加示例
*/
public String adaptiveInfer(String input) {
// 第一轮:Zero-shot
String zeroShotResult = chatClient.prompt()
.user(taskDescription + "\n\n输入:" + input)
.call()
.content();
// 评估置信度(用模型的logprobs或启发式规则)
double confidence = estimateConfidence(zeroShotResult);
if (confidence >= 0.85) {
log.info("Zero-shot置信度:{},直接返回", confidence);
return zeroShotResult;
}
// 第二轮:加入3个示例
log.info("Zero-shot置信度不足({}),升级到Few-shot", confidence);
List<FewShotExample> examples = retrieveRelevantExamples(input, 3);
String fewShotResult = chatClient.prompt()
.user(buildFewShotPrompt(examples, input))
.call()
.content();
confidence = estimateConfidence(fewShotResult);
if (confidence >= 0.80) {
return fewShotResult;
}
// 第三轮:增加到6个示例 + CoT
log.info("Few-shot(3)置信度不足({}),升级到CoT Few-shot", confidence);
List<FewShotExample> cotExamples = retrieveCoTExamples(input, 6);
return chatClient.prompt()
.user(buildCoTFewShotPrompt(cotExamples, input))
.call()
.content();
}
private double estimateConfidence(String response) {
// 启发式置信度估计
// 包含"我不确定"、"可能"、"也许"等词语,置信度降低
long uncertaintyWords = Stream.of("我不确定", "可能", "也许", "大概",
"或许", "不太清楚", "不确定")
.filter(response::contains)
.count();
// 输出长度异常(太短或太长都降低置信度)
int length = response.length();
double lengthScore = (length > 50 && length < 1000) ? 1.0 : 0.7;
return Math.max(0, 1.0 - uncertaintyWords * 0.15) * lengthScore;
}
}十三、Few-shot在不同业务场景的具体应用
13.1 场景一:代码审查风格统一
// 让AI学会公司的代码审查风格
String codeReviewFewShot = """
你是团队的代码审查助手,按照以下示例的风格进行代码审查:
示例1:
待审查代码:
public List<User> getActiveUsers() {
List<User> allUsers = userRepository.findAll();
List<User> activeUsers = new ArrayList<>();
for (User user : allUsers) {
if (user.isActive()) {
activeUsers.add(user);
}
}
return activeUsers;
}
审查意见:
[性能] 建议使用数据库查询过滤替代全量加载:
```java
// 直接在SQL层面过滤,避免加载所有用户到内存
public `List<User>` getActiveUsers() {
return userRepository.findByActiveTrue();
}
```
[严重程度] 中等 - 数据量小时问题不大,数据量增长后会成为性能瓶颈。
[建议] 在UserRepository中添加findByActiveTrue方法,或者使用@Query注解编写HQL。
示例2:
待审查代码:
String sql = "SELECT * FROM users WHERE id = " + userId;
审查意见:
[安全] SQL注入漏洞!禁止直接拼接用户输入到SQL语句!
```java
// 正确做法:使用PreparedStatement或JPA参数化查询
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setLong(1, userId);
```
[严重程度] 高危 - 必须修复,否则不允许合并。
[建议] 通过代码审查阶段强制要求所有SQL必须参数化。
现在请审查以下代码:
%s
审查意见:
""".formatted(targetCode);13.2 场景二:需求文档转化为Jira Story
// 让AI学会把模糊需求转化为结构化的用户故事
String requirementsToStoryFewShot = """
任务:将需求描述转化为标准的用户故事格式
示例1:
原始需求:"用户要能搜索商品"
用户故事:
**标题**:商品关键词搜索
**故事**:作为 购物用户,我希望 通过关键词搜索商品,以便 快速找到我需要的商品。
**验收标准**:
- [ ] 用户可以在搜索框输入关键词(1-50个字符)
- [ ] 搜索结果按相关度排序展示
- [ ] 无结果时显示"未找到相关商品"提示
- [ ] 搜索响应时间不超过2秒
**优先级**:高
**故事点**:5
示例2:
原始需求:"用户可以分享商品给朋友"
用户故事:
**标题**:商品分享功能
**故事**:作为 购物用户,我希望 将感兴趣的商品分享给朋友,以便 帮助朋友发现好物并可能获得分享奖励。
**验收标准**:
- [ ] 每个商品详情页有"分享"按钮
- [ ] 支持复制链接、微信分享、微博分享三种方式
- [ ] 分享链接有效期30天
- [ ] 分享成功后有Toast提示
**优先级**:中
**故事点**:3
现在请转化以下需求:
原始需求:"%s"
用户故事:
""".formatted(rawRequirement);13.3 场景三:日志分析与告警描述
// 让AI学会分析错误日志,生成结构化的告警描述
String logAnalysisFewShot = """
任务:分析Java应用错误日志,生成清晰的告警描述
示例1:
错误日志:
```
2024-03-15 14:23:17.453 ERROR c.e.s.OrderService - 订单创建失败
java.sql.SQLTimeoutException: Statement timed out after 30000ms
at com.mysql.cj.jdbc.StatementImpl.executeQuery(StatementImpl.java:1195)
at c.e.r.OrderRepository.findByUserId(OrderRepository.java:45)
at c.e.s.OrderService.createOrder(OrderService.java:89)
```
告警描述:
**告警级别**:P2(业务影响,非核心流程)
**影响功能**:订单创建
**根本原因**:数据库查询超时(30s),OrderRepository.findByUserId方法性能不足
**用户影响**:用户在创建订单时可能遇到超时失败,约占当前流量的X%
**建议排查**:
1. 检查order表的用户ID索引是否存在
2. 查看当前数据库慢查询日志
3. 确认当前数据库连接池是否打满
**紧急程度**:需要在2小时内处理
现在请分析以下日志:
%s
告警描述:
""".formatted(errorLog);十四、构建企业级的Few-shot示例管理平台
大型团队使用Few-shot时,如何统一管理示例库是一个工程挑战。
14.1 示例管理平台架构
// 企业级示例管理系统的核心数据模型
@Entity
@Table(name = "few_shot_examples")
public class FewShotExampleEntity {
@Id
private String id;
private String businessDomain; // 业务域:客服/代码/文档/财务
private String taskType; // 任务类型:分类/生成/提取/分析
@Column(columnDefinition = "TEXT")
private String inputText;
@Column(columnDefinition = "TEXT")
private String outputText;
private double qualityScore; // 0-1,人工评分
private int usageCount; // 被使用次数
private double avgEffectivenessScore; // 使用效果的平均评分
private String createdBy; // 谁创建的
private LocalDateTime createdAt;
private LocalDateTime lastUsedAt;
private boolean isActive; // 是否启用
// 版本控制:更新示例时不删除,而是创建新版本
private String previousVersionId;
private int version;
// 标签,用于多维度检索
@ElementCollection
private List<String> tags;
}@Service
public class ExampleManagementService {
/**
* 示例的生命周期管理
*/
public ExampleUpdateResult updateExample(String exampleId,
String newOutput,
String reason) {
FewShotExampleEntity existing = exampleRepository.findById(exampleId)
.orElseThrow(() -> new ExampleNotFoundException(exampleId));
// 废弃旧版本(保留历史记录)
existing.setIsActive(false);
exampleRepository.save(existing);
// 创建新版本
FewShotExampleEntity newVersion = FewShotExampleEntity.builder()
.id(UUID.randomUUID().toString())
.businessDomain(existing.getBusinessDomain())
.taskType(existing.getTaskType())
.inputText(existing.getInputText())
.outputText(newOutput) // 更新后的输出
.qualityScore(existing.getQualityScore())
.previousVersionId(exampleId) // 指向上一版本
.version(existing.getVersion() + 1)
.createdBy(getCurrentUser())
.isActive(true)
.build();
// 重新计算嵌入(新输出可能影响检索结果)
updateEmbedding(newVersion);
exampleRepository.save(newVersion);
// 记录变更日志
changeLogService.record(
exampleId, newVersion.getId(), reason, getCurrentUser()
);
return new ExampleUpdateResult(newVersion.getId(), existing.getId());
}
/**
* A/B测试示例库版本:测试更新前后的效果差异
*/
public ABTestResult testExampleUpdate(String oldExampleId,
String newExampleId,
int testSampleSize) {
List<String> testInputs = loadTestInputs(testSampleSize);
double oldScore = evaluateWithExample(oldExampleId, testInputs);
double newScore = evaluateWithExample(newExampleId, testInputs);
return ABTestResult.builder()
.oldExampleId(oldExampleId)
.newExampleId(newExampleId)
.oldAverageScore(oldScore)
.newAverageScore(newScore)
.improvement(newScore - oldScore)
.recommendation(newScore > oldScore
? "建议升级到新版本"
: "新版本效果不佳,建议保留旧版本")
.build();
}
}