Chain-of-Thought 在工程中的应用——不只是让模型想一步一步
Chain-of-Thought 在工程中的应用——不只是让模型想一步一步
有段时间我们团队在做一个合同风险识别的功能,任务是让 AI 扫描合同条款,识别出对甲方不利的风险点。
最开始的实现很简单:把合同文本扔给模型,问"这份合同有哪些风险点",结果输出一堆不痛不痒的通用条款,既没有针对性,准确率也低得可怜。
后来我换了一种方式——在 Prompt 里加了一句话:"请先分析合同的基本类型和适用法律,然后逐条分析关键条款的含义,最后再给出风险判断。"
同样的模型,同样的合同,识别出的风险点数量直接翻倍,而且更加精准,误报率也明显下降。
这就是 Chain-of-Thought 在工程里的实际价值——不是什么高深理论,就是让模型在给出答案之前先做功课。
CoT 的本质:为什么它有效
Chain-of-Thought(思维链)的核心思想很直观:当你要求模型给出推理过程而不只是答案时,模型的准确率会显著提升。
原始论文(Wei et al., 2022)里有一个很有说服力的对比:
普通 Prompt:
问:Roger 有 5 个网球,他又买了 2 罐,每罐 3 个,现在他有多少个?
答:11 个
CoT Prompt:
问:Roger 有 5 个网球,他又买了 2 罐,每罐 3 个,现在他有多少个?请一步步说明计算过程。
答:Roger 一开始有 5 个网球。2 罐球,每罐 3 个,共 2 × 3 = 6 个。
总共 5 + 6 = 11 个。所以答案是 11 个。数字简单所以看不出差异,但问题变复杂之后,没有中间推理过程的模型很容易在关键步骤出错,而 CoT 迫使模型把每步结果"显式化",每步都可以被验证。
为什么有效的深层原因:Transformer 的注意力机制在生成每个 token 时参考前面所有 token 的信息。当中间推理步骤作为 token 存在于上下文中,后续的推理可以"站在"这些中间步骤的基础上进行,而不是凭空跳跃到结论。换句话说,中间步骤是 Transformer 的"工作内存"。
这不是巧合,这是架构决定的。
显式 CoT vs 零样本 CoT
工程上常见两种 CoT 实现方式,搞清楚区别能帮你选对场景。
零样本 CoT(Zero-Shot CoT)
最简单的方式,就是在 Prompt 末尾加一句话。最著名的是"Let's think step by step",中文场景我们用"请一步步分析"或者"请先说出你的推理过程,再给出答案"。
// Zero-Shot CoT 的 Prompt 写法
String prompt = """
你是一名合同法律顾问。
以下是需要审查的合同条款:
%s
请一步步分析:
1. 首先识别这是什么类型的合同条款
2. 分析该条款对各方的权利义务
3. 识别其中可能的法律风险
4. 最后给出你的评估结论
""".formatted(clauseText);这种方式几乎零成本,不需要准备示例,直接引导模型产生推理链。
显式 CoT(Few-Shot CoT)
给模型提供带有完整推理过程的示例,让模型学习"应该怎么想":
// Few-Shot CoT:提供带推理过程的示例
String systemPrompt = """
你是一名合同法律顾问,专门识别合同中的风险条款。
分析示例:
条款:「本合同签订后,如乙方未能按时交付,每延误一天,乙方须支付合同总额 0.5% 的违约金,
且甲方有权单方面解除合同并要求乙方退还全部预付款」
分析过程:
- 条款类型:违约责任条款(针对乙方)
- 权利义务:甲方享有解除权和追偿权;乙方承担双重风险(违约金 + 退款)
- 风险识别:
* 0.5%/天的违约金标准明显偏高,超过法定上限可能被法院调整
* "按时交付"未明确定义,存在争议空间
* 甲方单方面解除权无前置条件限制,权利过重
* 退还预付款与违约金并行,可能构成重复惩罚
- 风险等级:高风险,不建议签署,需要重新谈判
现在请用相同的分析框架来分析以下条款:
""";两种方式的选择依据
我的经验是这样的:
- 任务逻辑比较通用(数学计算、逻辑推理、代码分析):零样本 CoT 通常够用,模型对这类任务本来就有较强的推理能力
- 任务有领域特定性(法律审查、医疗诊断、特定业务规则):用 Few-Shot CoT,给模型提供领域内的推理范式
- 输出格式有严格要求:一定要用 Few-Shot CoT,光靠指令要求格式容易跑偏
CoT 真正有提升的场景
这个问题很关键,因为 CoT 不是万灵药,用错了反而更烂。
有明显提升的场景
多步推理任务:需要根据中间结果做判断的问题。比如:给定财务报表,判断企业是否有偿债风险——需要先算各项财务比率,再对比行业标准,再做综合判断。
需要排除干扰项的任务:比如代码 bug 分析,直接问 bug 在哪容易给错误答案,但让模型先逐行理解代码逻辑,再定位异常,效果明显好得多。
领域知识密集型任务:比如合同分析、医学诊断,这类任务需要模型显式地"展开"领域知识来论证,而不是靠直觉跳跃。
逻辑蕴含和推断:给定一组前提,推导结论。零样本下模型经常跳过中间步骤直接给结论,且结论经常有逻辑漏洞。
没有明显提升或反而变差的场景
简单事实查询:"北京是哪个省的省会"——加 CoT 只是浪费 Token,模型不需要推理来回答这个问题。
情感分析:标准的情感分类任务(正面/负面),CoT 有时候反而让模型"想太多",把明显的正面情感分析成"有细微的负面因素"。
创意生成类任务:写文章、生成广告词,强制推理步骤会压制模型的创意发挥。
CoT 对 Token 消耗的影响
这是工程决策中绕不过去的问题。
CoT 会显著增加输出 Token 数量。我们做过一个测试,对合同条款分析任务:
- 直接回答:平均输出 ~150 Token
- Zero-Shot CoT:平均输出 ~450 Token(3 倍)
- Few-Shot CoT:平均输出 ~600 Token(4 倍)
输出 Token 通常比输入 Token 贵,所以 CoT 带来的成本增加是线性的。
怎么处理这个权衡?
策略一:选择性使用 CoT。对用户感知敏感的简单问题(比如意图识别、简单分类),不加 CoT;对结果正确性要求高的复杂任务,加 CoT。
策略二:压缩 CoT 输出。在 Prompt 里要求模型用更简洁的格式输出推理过程:
String prompt = """
分析以下合同条款的风险,请用简短的要点格式(每点不超过 20 字)列出推理步骤,最后给出结论。
%s
""".formatted(clause);策略三:用小模型做 CoT,大模型做最终判断。复杂推理链用便宜的模型展开,然后把完整推理过程喂给更贵的模型做最终决策。这在准确率和成本之间找到了一个平衡点。
Spring AI 中实现 CoT 的工程化方案
光说不练是假把式,来看实际怎么写。
基础配置
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.1 # CoT 任务通常用低温度,保证推理一致性CoT Prompt 模板设计
我设计了一个 CoT Prompt 模板系统,支持不同类型的推理任务:
@Component
public class CoTPromptTemplateFactory {
/**
* 零样本 CoT 模板 - 通用推理任务
*/
public PromptTemplate zeroShotCoT() {
return new PromptTemplate("""
请分析以下问题,并按照以下步骤给出回答:
步骤说明:
1. 理解问题:识别问题的核心要求
2. 分解问题:将复杂问题拆分为子问题
3. 逐步推理:针对每个子问题给出分析
4. 整合结论:汇总得出最终答案
问题:{question}
请开始你的分析:
""");
}
/**
* 结构化 CoT 模板 - 需要结构化输出的场景
*/
public PromptTemplate structuredCoT(String domainContext) {
return new PromptTemplate("""
你是一名 """ + domainContext + """
对于以下问题,请严格按照以下 JSON 格式输出你的分析过程和结论:
{
"reasoning_steps": [
{
"step": 1,
"thought": "这一步的思考内容",
"conclusion": "这一步得出的结论"
}
],
"final_answer": "最终答案",
"confidence": "high/medium/low",
"uncertainty_factors": ["影响置信度的因素"]
}
问题:{question}
""");
}
/**
* Few-Shot CoT 模板 - 领域专业任务
*/
public PromptTemplate fewShotCoT(List<CoTExample> examples, String taskDescription) {
StringBuilder sb = new StringBuilder();
sb.append(taskDescription).append("\n\n");
sb.append("以下是分析示例:\n\n");
for (int i = 0; i < examples.size(); i++) {
CoTExample example = examples.get(i);
sb.append("示例 ").append(i + 1).append(":\n");
sb.append("输入:").append(example.getInput()).append("\n");
sb.append("分析过程:\n").append(example.getReasoningProcess()).append("\n");
sb.append("结论:").append(example.getConclusion()).append("\n\n");
}
sb.append("现在请分析:\n{question}");
return new PromptTemplate(sb.toString());
}
}
// CoT 示例数据类
@Data
@AllArgsConstructor
public class CoTExample {
private String input;
private String reasoningProcess;
private String conclusion;
}合同风险分析的完整实现
@Service
@Slf4j
public class ContractRiskAnalysisService {
private final ChatClient chatClient;
private final CoTPromptTemplateFactory templateFactory;
// 预设的 Few-Shot 示例
private static final List<CoTExample> CONTRACT_EXAMPLES = Arrays.asList(
new CoTExample(
"乙方应在合同签订后 30 个工作日内交付全部成果,如未按时交付,每逾期一天按合同金额的 1% 计算违约金",
"""
1. 条款类型识别:交付时限 + 违约责任条款
2. 权利义务分析:
- 乙方义务:30 个工作日内完成交付
- 违约金标准:1%/天
3. 风险评估:
- 1%/天的违约金极高,30 天即超过合同总额,违反法律规定的公平原则
- 法院通常会将违约金调整至实际损失的 1.3 倍以内
- "工作日"未明确定义(哪个国家的工作日?是否包含节假日?)
""",
"高风险:违约金条款明显不公平,需重新谈判降低违约金比例,建议改为 0.1%/天,同时补充工作日的明确定义"
)
// 可以添加更多示例
);
public ContractRiskAnalysisService(ChatClient.Builder builder,
CoTPromptTemplateFactory templateFactory) {
this.chatClient = builder.build();
this.templateFactory = templateFactory;
}
public ContractAnalysisResult analyzeClause(String clauseText, AnalysisMode mode) {
long startTime = System.currentTimeMillis();
String response;
switch (mode) {
case ZERO_SHOT -> {
PromptTemplate template = templateFactory.zeroShotCoT();
response = callModel(template, clauseText);
}
case FEW_SHOT -> {
PromptTemplate template = templateFactory.fewShotCoT(
CONTRACT_EXAMPLES,
"你是一名专业的合同法律顾问,专注于识别对委托方不利的合同风险条款。"
);
response = callModel(template, clauseText);
}
case STRUCTURED -> {
PromptTemplate template = templateFactory.structuredCoT("合同法律顾问。");
response = callModel(template, clauseText);
}
default -> throw new IllegalArgumentException("未知的分析模式: " + mode);
}
long duration = System.currentTimeMillis() - startTime;
log.info("合同分析完成,模式:{},耗时:{} ms", mode, duration);
return ContractAnalysisResult.builder()
.clauseText(clauseText)
.analysisMode(mode)
.rawAnalysis(response)
.analysisTimeMs(duration)
.build();
}
private String callModel(PromptTemplate template, String question) {
Prompt prompt = template.create(Map.of("question", question));
return chatClient.prompt(prompt).call().content();
}
public enum AnalysisMode {
ZERO_SHOT, FEW_SHOT, STRUCTURED
}
}自动选择 CoT 策略
在实际系统中,我做了一个简单的路由逻辑,根据问题复杂度自动选择是否启用 CoT:
@Service
public class AdaptiveCoTService {
private final ChatClient chatClient;
// 判断是否需要 CoT 的关键词和模式
private static final List<String> COMPLEX_TASK_INDICATORS = Arrays.asList(
"分析", "评估", "推断", "计算", "比较", "原因", "为什么", "如何", "策略",
"风险", "利弊", "步骤", "方案", "诊断", "预测"
);
private static final List<String> SIMPLE_TASK_INDICATORS = Arrays.asList(
"是什么", "叫什么", "多少", "哪里", "哪个", "什么时候", "谁"
);
public String askWithAutoCoT(String question) {
boolean needCoT = assessComplexity(question);
if (needCoT) {
log.debug("问题复杂度评估:需要 CoT,问题:{}", question);
return askWithCoT(question);
} else {
log.debug("问题复杂度评估:直接回答,问题:{}", question);
return askDirect(question);
}
}
private boolean assessComplexity(String question) {
// 简单的启发式规则
// 实际场景可以用专门的分类模型或更复杂的规则
long complexIndicators = COMPLEX_TASK_INDICATORS.stream()
.filter(question::contains)
.count();
long simpleIndicators = SIMPLE_TASK_INDICATORS.stream()
.filter(question::contains)
.count();
// 问题长度也是参考因素
boolean isLongQuestion = question.length() > 100;
return complexIndicators > simpleIndicators || isLongQuestion;
}
private String askWithCoT(String question) {
return chatClient.prompt()
.user(question + "\n\n请一步步分析,然后给出结论。")
.call()
.content();
}
private String askDirect(String question) {
return chatClient.prompt()
.user(question)
.call()
.content();
}
}一个反直觉的实验
我做过一个对比实验,想看看 CoT 到底在哪类问题上最有价值。我选了三类任务:
任务 A:数学应用题(多步计算)
- 无 CoT 准确率:61%
- Zero-Shot CoT:84%
- Few-Shot CoT:91%
任务 B:情感分析(三分类:正面/中性/负面)
- 无 CoT 准确率:82%
- Zero-Shot CoT:80%(略微下降)
- Few-Shot CoT:83%(基本持平)
任务 C:合同条款风险评估(高/中/低风险)
- 无 CoT 准确率:54%
- Zero-Shot CoT:71%
- Few-Shot CoT:88%
结论很清晰:
- 需要多步推理的任务,CoT 提升巨大
- 简单分类任务,CoT 几乎没有提升,有时候反而引入噪声
- 领域专业任务,Few-Shot CoT 远好于 Zero-Shot CoT,因为模型需要领域特定的推理范式
这个结论直接影响了我们的产品设计——我们不是全量开启 CoT,而是针对不同的功能模块单独配置。
结构化 CoT 输出的工程价值
CoT 最烦人的一个问题是:推理过程是自然语言,后续程序很难解析。如果你的系统需要对推理结果做进一步处理,可以强制输出结构化的 CoT:
@Service
public class StructuredCoTService {
private final ChatClient chatClient;
private final ObjectMapper objectMapper;
public RiskAnalysisResult analyzeWithStructuredCoT(String contractClause) {
String systemPrompt = """
你是合同风险分析专家。
分析给定合同条款,严格以 JSON 格式输出,不要有任何额外文字:
{
"clause_type": "条款类型",
"parties_affected": ["受影响的当事方"],
"reasoning_chain": [
{"step": 1, "analysis": "分析内容", "finding": "发现"},
{"step": 2, "analysis": "分析内容", "finding": "发现"}
],
"risk_level": "HIGH/MEDIUM/LOW",
"risk_factors": ["风险因素1", "风险因素2"],
"recommendation": "建议行动"
}
""";
String response = chatClient.prompt()
.system(systemPrompt)
.user("分析条款:" + contractClause)
.call()
.content();
try {
return objectMapper.readValue(response, RiskAnalysisResult.class);
} catch (JsonProcessingException e) {
log.error("JSON 解析失败,原始响应:{}", response);
// 降级处理:返回一个包含原始文本的结果
return RiskAnalysisResult.fallback(contractClause, response);
}
}
}
@Data
@Builder
public class RiskAnalysisResult {
private String clauseType;
private List<String> partiesAffected;
private List<ReasoningStep> reasoningChain;
private String riskLevel;
private List<String> riskFactors;
private String recommendation;
private boolean isFallback;
private String rawResponse;
@Data
@AllArgsConstructor
public static class ReasoningStep {
private int step;
private String analysis;
private String finding;
}
public static RiskAnalysisResult fallback(String input, String rawResponse) {
return RiskAnalysisResult.builder()
.clauseType("解析失败")
.rawResponse(rawResponse)
.isFallback(true)
.build();
}
}结构化 CoT 的好处是既保留了完整的推理链(供调试和审计),又可以在程序中直接消费分析结果。这对于需要把 AI 分析结果录入数据库、触发工作流的场景特别有用。
实战建议
最后说几个工程化的经验:
Temperature 要调低。CoT 任务通常需要严谨的逻辑推理,temperature 设到 0.1-0.3 比较合适,高温度会让推理链更发散,中间结论的可靠性下降。
System Prompt 和 User Prompt 职责要分清。CoT 的格式要求放在 System Prompt 里,领域知识和示例也放 System Prompt,User Prompt 只放具体要分析的内容。这样用户消息可以复用,避免重复发送大量 Prompt 内容。
给 CoT 输出设置最大 Token 限制。在 API 调用层设置 max_tokens,防止模型在推理上滔滔不绝,消耗大量不必要的 Token。我通常给 CoT 分析任务设 1000-1500 Token 的上限,足够表达清楚推理过程了。
生产环境一定要记推理日志。CoT 最大的工程价值之一就是推理过程可审计,把这个优势发挥出来,对模型推理质量的持续改进很有帮助。
