第2034篇:Few-Shot提示设计——样本选择的艺术与工程
2026/4/30大约 6 分钟
第2034篇:Few-Shot提示设计——样本选择的艺术与工程
适读人群:需要通过Few-Shot提示提升LLM输出稳定性的工程师 | 阅读时长:约17分钟 | 核心价值:掌握Few-Shot样本的设计原则和动态选择方法,让模型输出更可控
我们做了一个合同条款分类功能,需要把条款分为"付款条款"、"违约责任"、"知识产权"等8个类别。
用Zero-Shot提示的时候,分类准确率大概68%。加了3个示例(3-shot)之后,准确率跳到了89%。
多加了几个示例呢?10个示例——准确率91%。20个示例——反而下降到87%。
这个倒U型曲线告诉我:Few-Shot不是样本越多越好。
Few-Shot的工作原理
Few-Shot的本质是在推理时提供任务格式的样本,让模型理解你的期望输出格式和风格,不需要修改模型权重。
它和微调的区别:
微调(Fine-tuning):
- 修改模型参数
- 一次训练,长期受益
- 需要大量数据(通常500+条)
Few-Shot提示:
- 不修改参数
- 每次请求都带着样本(占用token)
- 只需要几个到几十个样本
- 效果不如微调,但零成本、立即可用样本设计的几个原则
原则1:质量远比数量重要
一个精心设计的示例,抵得上十个随意选的示例。
// 差的示例:不完整,没有推理过程
String badExample = """
输入:本合同有效期自签订之日起一年。
输出:付款条款
""";
// 好的示例:清晰展示分类逻辑
String goodExample = """
输入:本合同有效期自签订之日起一年,到期后如双方无异议则自动续签。
分析:这个条款讨论的是合同的有效期和续签方式,既不涉及付款,也不涉及违约,
属于合同执行期限类条款。
输出:合同期限
""";原则2:覆盖边界情况
不只是典型case,还要包含容易搞错的边界case:
// 覆盖典型case
String typicalExample = """
输入:乙方应在收到甲方发票后15个工作日内完成付款。
输出:付款条款
""";
// 覆盖容易混淆的边界case
String edgeCaseExample = """
输入:因乙方付款延迟导致的损失,按日利率0.05%计算违约金。
分析:虽然提到了付款,但核心是描述延迟付款的违约责任,应分类为违约责任而非付款条款。
输出:违约责任
""";动态Few-Shot:按相似度自动选择样本
样本库大了之后,不需要(也不应该)把所有样本都塞进prompt。按照用户当前输入的相似度,动态选择最相关的样本:
/**
* 动态Few-Shot样本选择器
* 根据输入内容,从样本库中选择最相似的k个样本
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class DynamicFewShotSelector {
private final EmbeddingModel embeddingModel;
private final FewShotSampleRepository sampleRepo;
/**
* 为给定输入选择最合适的k个Few-Shot样本
*/
public List<FewShotSample> selectSamples(
String input, String taskType, int k) {
// 1. 获取输入的向量表示
float[] inputEmbedding = embeddingModel.embed(input);
// 2. 从样本库中获取同类型的所有样本
List<FewShotSample> candidates = sampleRepo.findByTaskType(taskType);
if (candidates.size() <= k) {
return candidates;
}
// 3. 计算相似度,选出最相似的k个
return candidates.stream()
.map(sample -> {
float[] sampleEmbedding = sample.getEmbedding();
if (sampleEmbedding == null) {
// 按需计算并缓存embedding
sampleEmbedding = embeddingModel.embed(sample.getInput());
sample.setEmbedding(sampleEmbedding);
sampleRepo.save(sample);
}
double similarity = cosineSimilarity(inputEmbedding, sampleEmbedding);
return Map.entry(sample, similarity);
})
.sorted(Map.Entry.<FewShotSample, Double>comparingByValue().reversed())
.limit(k)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
/**
* 构建包含动态Few-Shot样本的Prompt
*/
public String buildPromptWithExamples(
String taskDescription,
String input,
String taskType,
int numExamples) {
List<FewShotSample> examples = selectSamples(input, taskType, numExamples);
StringBuilder promptBuilder = new StringBuilder();
promptBuilder.append(taskDescription).append("\n\n");
// 添加选择的示例
promptBuilder.append("以下是一些示例:\n\n");
for (int i = 0; i < examples.size(); i++) {
FewShotSample example = examples.get(i);
promptBuilder.append("示例").append(i + 1).append(":\n");
promptBuilder.append("输入:").append(example.getInput()).append("\n");
if (example.getChainOfThought() != null) {
promptBuilder.append("分析:").append(example.getChainOfThought()).append("\n");
}
promptBuilder.append("输出:").append(example.getExpectedOutput()).append("\n\n");
}
promptBuilder.append("现在请处理:\n");
promptBuilder.append("输入:").append(input).append("\n");
promptBuilder.append("输出:");
return promptBuilder.toString();
}
private double cosineSimilarity(float[] a, float[] b) {
double dotProduct = 0;
double normA = 0;
double normB = 0;
for (int i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
}
}样本库的维护
样本库需要持续维护,不然会越来越老化:
/**
* Few-Shot样本库管理
* 包含样本的增删改查和质量评估
*/
@Service
@RequiredArgsConstructor
@Slf4j
public class FewShotSampleManager {
private final FewShotSampleRepository sampleRepo;
private final ChatClient evaluatorModel;
/**
* 添加新样本(带质量检查)
*/
public FewShotSample addSample(AddSampleRequest request) {
// 质量检查:验证样本的输入输出是否一致
boolean isValid = validateSample(request);
if (!isValid) {
throw new InvalidSampleException("样本质量检查失败,输入和输出不一致");
}
FewShotSample sample = FewShotSample.builder()
.taskType(request.getTaskType())
.input(request.getInput())
.chainOfThought(request.getChainOfThought())
.expectedOutput(request.getExpectedOutput())
.addedBy(request.getAddedBy())
.addedAt(LocalDateTime.now())
.qualityScore(1.0) // 初始满分,后续通过实际效果调整
.build();
return sampleRepo.save(sample);
}
/**
* 验证样本质量:用LLM检查输入输出是否匹配
*/
private boolean validateSample(AddSampleRequest request) {
String validationPrompt = String.format("""
请判断以下示例是否正确:
任务类型:%s
输入:%s
期望输出:%s
判断这个输出对这个输入是否正确合理?
只回答"正确"或"错误":
""", request.getTaskType(), request.getInput(), request.getExpectedOutput());
String response = evaluatorModel.prompt()
.user(validationPrompt).call().content().trim();
return response.contains("正确");
}
/**
* 样本有效性分析:基于实际使用效果,淘汰低效样本
* 每周运行一次
*/
@Scheduled(cron = "0 0 2 * * 1") // 每周一凌晨2点
public void pruneIneffectiveSamples() {
List<FewShotSample> lowQualitySamples = sampleRepo
.findByQualityScoreLessThan(0.5);
for (FewShotSample sample : lowQualitySamples) {
log.info("淘汰低质量样本: id={}, qualityScore={}",
sample.getId(), sample.getQualityScore());
sampleRepo.delete(sample);
}
log.info("样本库清理完成,淘汰{}条低质量样本", lowQualitySamples.size());
}
/**
* 从模型实际预测中发现新的错误案例,自动加入样本库
* 这是持续改进Few-Shot效果的关键机制
*/
public void learnFromError(String input, String wrongOutput, String correctOutput, String taskType) {
// 把错误案例(加上正确答案)加入样本库
// 这样下次遇到类似输入,模型会参考到正确的处理方式
AddSampleRequest request = AddSampleRequest.builder()
.taskType(taskType)
.input(input)
.expectedOutput(correctOutput)
.chainOfThought(String.format("注意:之前的错误答案是[%s],正确答案是[%s]",
wrongOutput, correctOutput))
.addedBy("auto-learning")
.build();
try {
addSample(request);
log.info("从错误案例学习,已加入样本库: {}", input.substring(0, Math.min(50, input.length())));
} catch (Exception e) {
log.warn("从错误案例学习失败: {}", e.getMessage());
}
}
}测试不同Few-Shot配置的效果
/**
* Few-Shot配置的A/B测试
*/
@Service
@RequiredArgsConstructor
public class FewShotABTester {
private final DynamicFewShotSelector selector;
private final ChatClient chatClient;
/**
* 对比不同Few-Shot策略的效果
* strategies: [0-shot, 3-shot-random, 3-shot-similar, 5-shot-similar]
*/
public Map<String, Double> compareStrategies(
List<EvalCase> evalCases, String taskType) {
Map<String, Double> results = new LinkedHashMap<>();
// 策略1:Zero-shot
results.put("0-shot", evaluate(evalCases, 0, false, taskType));
// 策略2:3个随机样本
results.put("3-shot-random", evaluate(evalCases, 3, false, taskType));
// 策略3:3个相似样本
results.put("3-shot-similar", evaluate(evalCases, 3, true, taskType));
// 策略4:5个相似样本
results.put("5-shot-similar", evaluate(evalCases, 5, true, taskType));
return results;
}
private double evaluate(List<EvalCase> cases, int numShots,
boolean useSimilar, String taskType) {
int correct = 0;
for (EvalCase evalCase : cases) {
String prompt;
if (numShots == 0) {
prompt = evalCase.getInput();
} else if (useSimilar) {
prompt = selector.buildPromptWithExamples(
evalCase.getTaskDescription(),
evalCase.getInput(), taskType, numShots);
} else {
List<FewShotSample> randomSamples = getRandomSamples(taskType, numShots);
prompt = buildPromptWithFixed(evalCase.getInput(), randomSamples);
}
String response = chatClient.prompt().user(prompt).call().content();
if (response.trim().equals(evalCase.getExpectedOutput())) {
correct++;
}
}
return (double) correct / cases.size();
}
}Few-Shot不是填几个例子那么简单。样本的质量、多样性、边界覆盖,以及选择策略,每一个都会影响效果。
把Few-Shot样本管理当成一个工程系统来建设:有持续更新的样本库、基于向量相似度的动态选择、以及从错误案例中自动学习的机制。这样的Few-Shot系统,效果会随着时间自我优化。
