第2031篇:Chain-of-Thought提示工程——让LLM展示推理过程的实战技巧
第2031篇:Chain-of-Thought提示工程——让LLM展示推理过程的实战技巧
适读人群:希望提升LLM输出质量的工程师 | 阅读时长:约18分钟 | 核心价值:掌握CoT及其变体的实用技巧,在不微调的情况下显著提升复杂任务的输出质量
有一个需求:让LLM分析一份合同,判断是否存在对甲方不利的条款。
第一版prompt非常简单:
分析这份合同,找出对甲方不利的条款。效果不稳定——有时候能找到关键问题,有时候明明有问题却没发现。
加上一句请一步一步分析之后,命中率从60%提升到了85%。
这就是Chain-of-Thought的基本效果。但CoT远不止这一个技巧。
为什么让模型"想一想"有效
直接给答案的模式:
输入 → 立即输出结果Chain-of-Thought的模式:
输入 → 展开推理步骤 → 基于推理得出结论为什么展开推理有效?从信息论角度看,让模型输出推理过程,相当于强制模型在得出答案之前处理更多的中间信息。这些中间token的生成,使得后续token(最终答案)能够利用更丰富的上下文。
一个反直觉的发现:不让模型展示推理过程,它的"计算资源"(token生成次数)全部用在生成答案上;让它展示推理,它先用更多token做"草稿",最后得出的答案往往更准确。
Zero-Shot CoT:最简单的CoT
不需要样本,只需要一句魔法短语:
@Service
@RequiredArgsConstructor
public class ContractAnalyzer {
private final ChatClient chatClient;
/**
* 使用Zero-Shot CoT分析合同风险
* 关键:在最后加"请一步一步分析"
*/
public ContractAnalysisResult analyze(String contractText) {
String prompt = """
以下是一份合同文本,请分析其中对甲方可能存在的风险条款。
合同内容:
%s
请一步一步分析每个主要条款,然后给出综合结论。
""".formatted(contractText);
String response = chatClient.prompt()
.system("你是一位专业的合同律师,擅长识别合同中的风险条款。")
.user(prompt)
.call()
.content();
return parseAnalysisResult(response);
}
}"请一步一步分析"、"让我们一步一步思考"、"请先思考,再给出答案"——这类提示语都能触发CoT行为。
Few-Shot CoT:提供示范推理过程
当Zero-Shot效果不稳定时,提供几个带推理过程的样本:
@Service
@RequiredArgsConstructor
public class RiskAssessmentService {
private final ChatClient chatClient;
private static final String FEW_SHOT_EXAMPLES = """
示例1:
输入:本合同有效期为一年,自签署之日起生效。
分析:
1. 确认合同类型:服务类合同
2. 检查有效期:一年,明确且合理
3. 生效条件:签署即生效,无需其他条件
4. 潜在风险:有效期较短,如需长期合作需注意续签
结论:低风险,建议关注续签时间节点。
示例2:
输入:甲方有权在不提前通知的情况下变更合同条款,乙方需在7日内书面确认。
分析:
1. 识别主体:甲方(客户方)、乙方(我方)
2. 关键权利:甲方单方面变更条款权
3. 对乙方影响:我方只有7天确认窗口,缺乏协商余地
4. 商业风险:合同条款可能随时改变,影响业务计划
5. 法律角度:单方变更条款协议在部分情况下可能不受法律保护
结论:高风险条款,建议删除或修改为"双方协商后变更"。
""";
/**
* 带Few-Shot示范的风险评估
*/
public RiskAssessment assess(String clause) {
String prompt = FEW_SHOT_EXAMPLES + "\n" +
"现在请分析以下条款:\n" + clause;
String response = chatClient.prompt()
.system("你是合同风险分析专家,请按照示例格式进行分析。")
.user(prompt)
.call()
.content();
return parseRiskAssessment(response);
}
}Few-Shot CoT的关键不只是给样本,而是给带有完整推理过程的样本。模型会学习推理的风格和深度,而不只是学习答案的格式。
强制结构化输出的CoT
有时候你不只需要推理结果,还需要机器可读的结构化输出:
@Service
@RequiredArgsConstructor
public class StructuredCoTService {
private final ChatClient chatClient;
/**
* 两阶段输出:先推理,再结构化
* 比直接要JSON更稳定
*/
public ContractRisk analyzeWithStructuredOutput(String clause) {
// 第一阶段:自由推理
String thinkingPrompt = """
分析以下合同条款的风险,请先进行详细分析,然后给出你的判断:
条款:%s
请先详细分析,考虑各种可能的情况。
""".formatted(clause);
String thinking = chatClient.prompt()
.user(thinkingPrompt)
.call()
.content();
// 第二阶段:基于推理提取结构化结论
String extractPrompt = """
基于以下分析结果,提取关键信息,严格按照JSON格式输出,不要输出其他内容:
分析过程:
%s
请输出如下格式的JSON:
{
"riskLevel": "HIGH/MEDIUM/LOW",
"riskType": "风险类型描述",
"specificRisks": ["具体风险1", "具体风险2"],
"recommendation": "修改建议"
}
""".formatted(thinking);
String jsonResponse = chatClient.prompt()
.user(extractPrompt)
.call()
.content();
return parseJsonResponse(jsonResponse);
}
}这个两阶段的方式比直接要"输出JSON并解释理由"更可靠。原因是:如果你要求模型同时推理和输出JSON,它往往会为了维持JSON格式而压缩推理深度。分开两步,各自发挥。
Tree-of-Thought:多路径探索
对于特别复杂的决策问题,可以让模型探索多个推理路径,然后选最好的:
@Service
@RequiredArgsConstructor
public class TreeOfThoughtService {
private final ChatClient chatClient;
/**
* Tree-of-Thought:生成多个解决方案,评估后选最优
* 适合:有多个可行解的决策问题
*/
public String solveWithToT(String problem, int numCandidates) {
// 步骤1:生成多个候选解法
String generationPrompt = """
问题:%s
请生成%d个不同的解决思路,每个思路要独立展开,
编号为方案1、方案2...请尽量让方案有差异化。
""".formatted(problem, numCandidates);
String candidates = chatClient.prompt()
.user(generationPrompt)
.call()
.content();
// 步骤2:评估每个方案的优缺点
String evaluationPrompt = """
以下是针对""%s""的%d个解决方案:
%s
请逐一评估每个方案的:
1. 可行性(0-10分)
2. 潜在风险
3. 实施成本
最后给出推荐方案和理由。
""".formatted(problem, numCandidates, candidates);
return chatClient.prompt()
.user(evaluationPrompt)
.call()
.content();
}
}自我一致性(Self-Consistency)
对于重要决策,可以让模型多次回答同一问题,取"多数答案":
@Service
@RequiredArgsConstructor
public class SelfConsistencyService {
private final ChatClient chatClient;
/**
* 自我一致性:多次采样,取最一致的答案
* 适合:答案有对错之分的问题(分类、是否判断)
* 原理:错误的推理路径是多样的,正确的路径往往收敛
*/
public String answerWithConsistency(String question, int sampleCount) {
List<String> answers = new ArrayList<>();
// 用较高temperature多次采样(每次推理路径不同)
for (int i = 0; i < sampleCount; i++) {
String answer = chatClient.prompt()
.user(question + "\n请一步一步分析,然后给出明确的结论。")
.options(ChatOptions.builder().temperature(0.8).build())
.call()
.content();
answers.add(extractConclusion(answer));
}
// 提取最常见的结论
return findMostCommonAnswer(answers);
}
private String extractConclusion(String fullResponse) {
// 提取最后的结论部分
// 实际实现需要更复杂的解析逻辑
String[] parts = fullResponse.split("结论:");
return parts.length > 1 ? parts[1].trim() : fullResponse;
}
private String findMostCommonAnswer(List<String> answers) {
// 统计最常见的答案
return answers.stream()
.collect(Collectors.groupingBy(a -> a, Collectors.counting()))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse(answers.get(0));
}
}CoT的几个反直觉发现
发现1:说"一步一步"比说"仔细想"效果好
"请仔细分析"效果不如"请一步一步分析"。具体的行为指令(step by step)比模糊的质量指令(仔细)更有效。
发现2:推理过程不需要正确,方向对了就有效
即使模型的推理过程中有一些小错误,只要大方向对,最终结论往往也对。CoT的价值是让模型"展开思考空间",不是要求每一步推理都完美。
发现3:CoT对大模型效果显著,对小模型效果有限
实测7B模型的CoT效果比70B差很多——小模型没有足够的参数来支撑复杂的推理链。对于小模型,直接给样本(Few-Shot)比让它自己推理更有效。
// 根据模型规模选择合适的提示策略
@Service
@RequiredArgsConstructor
public class AdaptivePromptStrategy {
public String buildPrompt(String question, String modelSize) {
return switch (modelSize) {
case "7B", "8B" -> {
// 小模型:直接给Few-Shot样本,不要求推理
yield buildFewShotPrompt(question);
}
case "72B", "70B" -> {
// 大模型:Chain-of-Thought效果好
yield buildCoTPrompt(question);
}
default -> question;
};
}
private String buildCoTPrompt(String question) {
return question + "\n\n请一步一步分析,先展开你的推理过程,再给出最终结论。";
}
private String buildFewShotPrompt(String question) {
return EXAMPLES + "\n现在回答:" + question;
}
}CoT不是一种技巧,而是一类技巧的统称。从最简单的"让我们一步一步思考",到复杂的Tree-of-Thought多路径探索,背后的原理都是一样的:给模型更多的"计算空间"来处理复杂问题。
找到适合你场景的CoT变体,往往比微调模型更快见效,而且成本几乎为零。
