Prompt Engineering进阶:Chain-of-Thought、Few-Shot、Self-Consistency的工程实践
Prompt Engineering进阶:Chain-of-Thought、Few-Shot、Self-Consistency的工程实践
适读人群:Java后端工程师、AI应用开发者 | 阅读时长:约18分钟 | 依赖:Spring AI 1.0、OpenAI API
开篇故事
我做过一个财务数据分析的AI助手,需要根据用户上传的Excel报表回答问题,比如"今年Q3的净利润同比增长了多少"。
第一版我用的是最直接的方式:把表格数据塞进Prompt,然后问LLM。结果模型经常算错,特别是涉及多步计算的时候——先算增量,再算增长率,两步下来经常有数字错误。
我最早以为是数学能力问题,但仔细想想,GPT-4的数学能力不至于这么差。后来我读到了Chain-of-Thought(思维链)的论文,有个说法让我印象深刻:大模型在一步输出中处理复杂推理时,就像让人类在脑子里一步完成多位数乘法——做不到。但如果让它"边算边写",一步一步列出来,准确率就会大幅提升。
我把Prompt改成了CoT格式,要求模型"先列出已知数据,再逐步计算,最后给出结论",准确率从71%直接跳到了92%。这个改动让我对Prompt Engineering刮目相看,也开始系统性地把这些技术整理成工程化的实现。
一、核心问题分析
Prompt Engineering的本质,是通过精心设计输入,引导模型更好地调用它已经具备的能力。它不会让模型变聪明,但会让它"更专注地聪明"。
常见Prompt问题分类:
三种技术的适用场景:
- Chain-of-Thought:适合需要多步推理的任务,如数学计算、逻辑分析、数据解读
- Few-Shot:适合输出格式复杂或任务定义难以用语言描述的场景,如信息抽取、分类打标
- Self-Consistency:适合准确率要求极高的关键决策,通过多次采样+投票降低随机错误
二、原理深度解析
2.1 Chain-of-Thought工作原理
普通Prompt的问题在于,模型需要在单次前向传播中同时"理解问题"和"得出答案",这对复杂推理任务来说太难了。
CoT的做法是让模型先生成推理步骤,再给出答案。由于Transformer的自回归特性,每生成一个token时都能看到之前生成的所有内容,中间步骤本质上为最终答案的生成提供了"工作空间"。
Zero-shot CoT:直接在Prompt末尾加"请逐步思考",简单有效。
Few-shot CoT:提供几个有完整推理过程的示例,让模型模仿推理风格,效果更强。
2.2 Self-Consistency原理
Self-Consistency是CoT的增强版。核心思想:对同一个问题,用高temperature多次采样(通常5-20次),得到多个不同的推理路径,最终取答案中的多数。
理论依据:单次生成可能因为随机性走向错误的推理路径,但正确的推理路径通常比错误路径更多。多数投票能过滤偶发错误。
代价:需要调用LLM N次,成本是单次的N倍。只适合精度要求极高、可以承受更高成本的场景。
三、完整代码实现
3.1 Prompt模板管理系统
@Component
public class PromptTemplateManager {
private final Map<String, PromptTemplate> templates = new HashMap<>();
@PostConstruct
public void loadTemplates() {
// Chain-of-Thought模板
templates.put("financial_cot", new PromptTemplate("""
你是一个专业的财务分析师。请分析以下财务数据并回答问题。
财务数据:
{financial_data}
问题:{question}
请按以下步骤回答:
第一步:列出解题所需的关键数据
第二步:写出计算公式
第三步:代入数据逐步计算
第四步:得出结论并解读含义
请开始分析:
"""));
// Few-Shot信息抽取模板
templates.put("contract_extraction_fewshot", new PromptTemplate("""
从合同文本中提取关键信息,输出JSON格式。
示例1:
合同文本:甲方:北京科技有限公司,乙方:上海贸易股份有限公司,合同金额:人民币500万元,
有效期:2024年1月1日至2024年12月31日。
输出:{"party_a": "北京科技有限公司", "party_b": "上海贸易股份有限公司",
"amount": "500万元", "currency": "人民币",
"start_date": "2024-01-01", "end_date": "2024-12-31"}
示例2:
合同文本:委托方(甲方):广州制造厂,受托方(乙方):深圳设计院,
服务费用:USD 8000,合同期限:6个月,自签署之日起计算。
输出:{"party_a": "广州制造厂", "party_b": "深圳设计院",
"amount": "8000", "currency": "USD",
"start_date": null, "end_date": null, "duration": "6个月"}
现在请处理:
合同文本:{contract_text}
输出:
"""));
// 风险评估CoT模板
templates.put("risk_assessment_cot", new PromptTemplate("""
你是一名资深风控专家。请对以下业务场景进行风险评估。
业务信息:
{business_info}
评估维度:合规风险、财务风险、操作风险、声誉风险
请按照以下结构逐步分析:
【维度分析】对每个风险维度,说明:1)存在哪些风险点 2)风险等级(高/中/低) 3)依据是什么
【综合判断】综合以上维度,总体风险评级是什么
【处置建议】针对高风险项,给出3条具体的缓解措施
开始分析:
"""));
}
public String render(String templateName, Map<String, String> variables) {
PromptTemplate template = templates.get(templateName);
if (template == null) {
throw new IllegalArgumentException("模板不存在: " + templateName);
}
return template.render(variables);
}
}3.2 Chain-of-Thought执行器
@Service
public class ChainOfThoughtExecutor {
private final ChatClient chatClient;
private final PromptTemplateManager templateManager;
public ChainOfThoughtExecutor(ChatClient.Builder builder,
PromptTemplateManager templateManager) {
this.chatClient = builder.build();
this.templateManager = templateManager;
}
/**
* 标准CoT执行:单次调用,要求逐步推理
*/
public CoTResult execute(String templateName,
Map<String, String> variables) {
String prompt = templateManager.render(templateName, variables);
long start = System.currentTimeMillis();
String rawOutput = chatClient.prompt(prompt)
.options(ChatOptions.builder()
.temperature(0.2) // CoT使用低temperature保持逻辑稳定
.build())
.call()
.content();
long elapsed = System.currentTimeMillis() - start;
// 从CoT输出中提取最终答案(最后一段或"结论"之后的内容)
String finalAnswer = extractFinalAnswer(rawOutput);
return new CoTResult(rawOutput, finalAnswer, elapsed);
}
/**
* 从CoT输出中提取最终答案
*/
private String extractFinalAnswer(String cotOutput) {
// 查找"结论"、"综合判断"、"第四步"等关键词后的内容
String[] markers = {"结论:", "综合结论:", "综合判断:", "最终答案:",
"第四步:", "因此,", "所以,"};
for (String marker : markers) {
int idx = cotOutput.lastIndexOf(marker);
if (idx >= 0) {
return cotOutput.substring(idx + marker.length()).trim();
}
}
// 没找到标记,返回最后一个自然段
String[] paragraphs = cotOutput.split("\n\n");
return paragraphs[paragraphs.length - 1].trim();
}
@Data
@AllArgsConstructor
public static class CoTResult {
private String fullReasoning; // 完整推理过程
private String finalAnswer; // 提取的最终答案
private long latencyMs;
}
}3.3 Self-Consistency执行器
@Service
public class SelfConsistencyExecutor {
private static final Logger log = LoggerFactory.getLogger(SelfConsistencyExecutor.class);
private final ChatClient chatClient;
private final PromptTemplateManager templateManager;
@Value("${sc.sample-count:7}")
private int sampleCount; // 采样次数,建议奇数,5/7/9
@Value("${sc.temperature:0.8}")
private double temperature; // 高temperature增加多样性
public SelfConsistencyExecutor(ChatClient.Builder builder,
PromptTemplateManager templateManager) {
this.chatClient = builder.build();
this.templateManager = templateManager;
}
/**
* Self-Consistency:多次采样 + 多数投票
*/
public ScResult execute(String templateName, Map<String, String> variables) {
String prompt = templateManager.render(templateName, variables);
List<String> samples = new ArrayList<>();
// 并行采样(使用CompletableFuture提升效率)
List<CompletableFuture<String>> futures = IntStream.range(0, sampleCount)
.mapToObj(i -> CompletableFuture.supplyAsync(() ->
chatClient.prompt(prompt)
.options(ChatOptions.builder()
.temperature(temperature)
.build())
.call()
.content()
))
.collect(Collectors.toList());
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
futures.forEach(f -> {
try {
samples.add(f.get());
} catch (Exception e) {
log.warn("采样失败: {}", e.getMessage());
}
});
// 从每个样本中提取答案(简化为最后一段)
List<String> extractedAnswers = samples.stream()
.map(this::extractAnswer)
.collect(Collectors.toList());
// 多数投票
String majorityAnswer = vote(extractedAnswers);
double confidence = calculateConfidence(extractedAnswers, majorityAnswer);
log.info("Self-Consistency采样{}次,置信度{:.2f},最终答案:{}",
samples.size(), confidence, majorityAnswer);
return new ScResult(majorityAnswer, confidence, extractedAnswers, samples);
}
/**
* 语义相似度投票(处理答案表述不一致的情况)
*/
private String vote(List<String> answers) {
// 简单版本:统计完全相同的答案
Map<String, Long> counts = answers.stream()
.collect(Collectors.groupingBy(a -> normalizeAnswer(a),
Collectors.counting()));
return counts.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(answers.get(0));
}
private String normalizeAnswer(String answer) {
// 去除空白、标点,取前50字作为去重键
String normalized = answer.replaceAll("[\\s,。!?、]", "");
return normalized.length() > 50 ? normalized.substring(0, 50) : normalized;
}
private double calculateConfidence(List<String> answers, String majorityAnswer) {
long majorityCount = answers.stream()
.filter(a -> normalizeAnswer(a).equals(normalizeAnswer(majorityAnswer)))
.count();
return (double) majorityCount / answers.size();
}
private String extractAnswer(String text) {
String[] paragraphs = text.split("\n\n");
return paragraphs[paragraphs.length - 1].trim();
}
@Data
@AllArgsConstructor
public static class ScResult {
private String majorityAnswer;
private double confidence;
private List<String> allAnswers;
private List<String> fullSamples;
}
}3.4 Few-Shot示例动态选择器
@Service
public class DynamicFewShotSelector {
private final UnifiedEmbeddingService embeddingService;
private final List<FewShotExample> examplePool;
public DynamicFewShotSelector(UnifiedEmbeddingService embeddingService) {
this.embeddingService = embeddingService;
this.examplePool = loadExamplesFromDb(); // 从数据库加载示例库
}
/**
* 根据当前输入,动态选择最相似的K个示例
* 比静态Few-Shot效果更好,尤其是示例库很大时
*/
public List<FewShotExample> selectExamples(String input, int k) {
float[] inputVec = embeddingService.embed(input);
return examplePool.stream()
.map(example -> {
float[] exampleVec = example.getEmbedding();
double similarity = cosineSimilarity(inputVec, exampleVec);
return Map.entry(example, similarity);
})
.sorted(Map.Entry.<FewShotExample, Double>comparingByValue().reversed())
.limit(k)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
/**
* 构建包含动态示例的Prompt
*/
public String buildFewShotPrompt(String taskDescription,
String input,
int shotCount) {
List<FewShotExample> examples = selectExamples(input, shotCount);
StringBuilder prompt = new StringBuilder(taskDescription).append("\n\n");
for (int i = 0; i < examples.size(); i++) {
FewShotExample ex = examples.get(i);
prompt.append("示例").append(i + 1).append(":\n");
prompt.append("输入:").append(ex.getInput()).append("\n");
prompt.append("输出:").append(ex.getOutput()).append("\n\n");
}
prompt.append("现在请处理:\n输入:").append(input).append("\n输出:");
return prompt.toString();
}
private double cosineSimilarity(float[] a, float[] b) {
double dot = 0, normA = 0, normB = 0;
for (int i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-10);
}
private List<FewShotExample> loadExamplesFromDb() {
// 实际项目从数据库加载
return new ArrayList<>();
}
}四、效果评估与优化
财务分析任务(100道计算题,混合单步和多步):
| Prompt策略 | 准确率 | 平均延迟 | Token消耗 |
|---|---|---|---|
| 直接问答 | 71.3% | 320ms | 约200 |
| Zero-shot CoT | 86.8% | 580ms | 约450 |
| Few-shot CoT (3-shot) | 91.2% | 720ms | 约700 |
| Self-Consistency (7次) | 95.1% | 3100ms | 约3500 |
| Self-Consistency (5次) | 93.7% | 2200ms | 约2500 |
信息抽取任务(合同关键信息,200个样本):
| Prompt策略 | 字段准确率 | 格式合规率 |
|---|---|---|
| 直接抽取(无示例) | 76.4% | 61.3% |
| 静态Few-Shot (3-shot) | 88.9% | 94.7% |
| 动态Few-Shot (3-shot) | 93.2% | 97.8% |
动态Few-Shot(根据当前输入选择最相似的示例)比静态Few-Shot高了约4个点,主要体现在边界情况的处理上——对于和训练示例差异较大的合同格式,动态选择能找到更合适的参考。
五、踩坑实录
坑1:CoT让模型"自我说服"走向错误答案
有一次模型在推理过程中犯了一个小错误,但随后的步骤全都基于这个错误继续推演,最终得出了一个"逻辑自洽但完全错误"的结论。这就是CoT的一个风险:中间步骤的错误会级联放大。解法是在CoT结尾加一个验证步骤:"请验证你的计算结果是否合理,如发现错误请重新计算。"这个自我验证步骤能捕获约60%的中间错误。
坑2:Self-Consistency的并发调用触发了API限流
7次并发调用LLM直接把API的rate limit打到了,特别是在有多个用户同时使用的场景。解决方案是改用顺序调用加短间隔(每次调用后等100ms),并且对Self-Consistency做缓存——同一个问题的查询结果缓存5分钟,相同问题不重复采样。
坑3:Few-Shot示例的质量比数量更重要
我早期放了20个示例在Prompt里,以为示例越多越好。但有些示例质量较差(我自己随手写的),反而干扰了模型的判断。换成5个高质量的示例(由业务专家审核过的标准案例)之后,效果比20个差示例好很多。现在我的经验是:3-5个精选示例 > 10-20个普通示例。
六、总结
CoT、Few-Shot、Self-Consistency三种技术各有侧重,但都服务于同一个目标:让模型在特定任务上更稳定、更准确地发挥它已有的能力。
工程化使用时,我建议的优先级是:先用Zero-shot CoT(改动最小,效果立竿见影);如果还不够,加Few-shot示例;如果准确率要求极高,且成本可以接受,再叠加Self-Consistency。不要一开始就全都用上,成本-效果要仔细衡量。
