Few-shot与CoT思维链
> **本文适合谁**
Few-shot 与 CoT 思维链:让模型推理更准确
本文适合谁
已经会写基础 Prompt、但在分类/推理类任务上效果不稳定的开发者。本文告诉你为什么给几个例子能显著提升效果(Few-shot),以及为什么让模型"先想再答"能减少错误(CoT)——两种技术的原理和使用场景都有详细说明。
在需求分类这类任务中,直接提问的效果往往不稳定。以 Jira 需求自动打标签为例,当 Prompt 中没有任何示例时,模型对边界情况容易判断错误——"重构登录模块以支持 OAuth2",到底是技术债还是功能需求?模型在没有判断标准的情况下,只能依赖自身的通用理解,导致准确率偏低。
加入几个精心挑选的示例后,准确率会显著提升。例子的作用不是让模型记住更多知识,而是把特定的判断逻辑直接传递给它。
1.1 从 Zero-shot 到 Few-shot
这三个概念的区别在于提供给模型的参考数量。
Zero-shot(零样本提示,即不给任何例子直接提问):直接提问,不给任何例子。适合简单任务,比如翻译、摘要。但对于有特定判断标准、输出格式要求严格的任务,Zero-shot 很不稳定。
One-shot:给一个例子。比 Zero-shot 好,但一个例子覆盖不了太多情况。
Few-shot:给 3 到 5 个例子。这是最常用的,也是效果提升最明显的。
1.1.1 Few-shot 为什么有效:In-Context Learning 的机制
要理解 Few-shot 为什么有效,需要理解一个概念:In-Context Learning(上下文学习)。
大语言模型在训练完成后,参数就固定了——你不是在"重新训练"它。但当你在 Prompt 里提供几个例子时,模型会在推理阶段临时调整自己的行为。这个过程不会修改任何模型参数,只靠 Prompt 本身就完成了"学习"。
这是怎么做到的?训练 LLM 时,模型见过海量的文本,其中包含大量"模式 + 续写"的结构:新闻文章后面有类似的新闻、代码注释后面有对应的实现、例题解析后面有类似题目的解法。模型学会了"看到几个例子之后,按照相同的模式继续"。这个能力在规模足够大的模型里自然涌现,是大模型的核心特性之一。
所以 Few-shot 的本质是:你提供的例子激活了模型内部与该任务相关的知识分布,让模型在当前对话中暂时"切换"到这个任务的判断框架。例子不是在教模型新知识,而是在告诉模型"用你已有知识中的哪个部分来回答"。
这个理解有一个重要推论:Few-shot 对模型"已经懂"的知识有效,对模型从未见过的全新领域效果有限。例子无法让模型凭空学会它训练数据里完全没有的东西。
以需求分类为例,加入 Few-shot 后的 Prompt 结构如下:
system_prompt = """你是一名项目管理助手,负责对研发需求进行分类。
分类规则:
- 功能需求:新增用户可见的功能
- 性能优化:提升系统速度、吞吐量、降低延迟
- Bug 修复:修复已知的错误行为
- 技术债:重构、升级依赖、改善代码结构,不改变外部行为
示例:
需求:用户可以通过手机号注册账号
分类:功能需求
需求:登录接口 P99 延迟超过 2 秒,需要优化
分类:性能优化
需求:修复用户头像上传失败的问题
分类:Bug 修复
需求:将 Spring Boot 从 2.x 升级到 3.x
分类:技术债
只输出分类结果,不要解释。"""
例子的选取至关重要。不要选简单的典型案例,要选边界情况和容易混淆的情况。"重构登录模块以支持 OAuth2"这种,既有重构(技术债)又有新功能(OAuth2),需要在例子里明确体现判断标准。
例子的质量远比数量重要。5 个精挑细选的例子,比 20 个随便堆的例子效果好得多。
Zero-shot、Few-shot 与 CoT 三种方式的输入结构对比
1.2 CoT:让模型先想再答
Few-shot 解决了"告诉模型应该输出什么"的问题,但对于需要推理的任务,光靠例子不够,还需要让模型把推理过程显式地写出来。
这就是 Chain of Thought(思维链,缩写 CoT,让模型把推理过程像解题步骤一样逐步写出来)。
原理很简单:让模型在给出答案之前,先把思考步骤一步一步写出来。
1.2.1 CoT 为什么有效:自回归生成的数学基础
要真正理解 CoT 为什么有效,需要了解 LLM 的生成机制。
大语言模型是**自回归(Autoregressive)**的:它每次只生成一个 Token,下一个 Token 的概率分布由前面所有 Token 共同决定。从数学上看,生成序列 $t_1, t_2, ..., t_n$ 的联合概率是:
$$P(t_1, t_2, ..., t_n) = P(t_1) \cdot P(t_2|t_1) \cdot P(t_3|t_1,t_2) \cdot ... \cdot P(t_n|t_1,...,t_{n-1})$$
这意味着:已经生成的每一个 Token,都会影响后续 Token 的概率分布。
当你让模型直接回答"这道题答案是多少"时,模型在一次前向传播中就必须把所有推理压缩进去。但当你让模型先写推理步骤时,中间步骤的 Token(比如"首先计算 5-2=3")会影响后续 Token 的生成方向,相当于给模型提供了"草稿纸"——每一步推理结果都成为下一步推理的条件概率输入。
第二个原因是错误可见性:写出推理步骤后,如果某个中间步骤出错,模型在生成后续 Token 时有机会感知到矛盾并自我修正;而直接给答案时,这种纠错机会就完全没有了。
这两个机制共同解释了为什么 CoT 在复杂推理任务上效果显著,而在简单事实问答上效果不明显——简单问题本来就不需要多步推理,强制加推理步骤反而可能引入噪音。
1.2.2 "Let's think step by step" 为什么有效
2022 年谷歌的一篇论文(Large Language Models are Zero-Shot Reasoners)发现,在 Prompt 末尾加上 "Let's think step by step",在数学推理任务上准确率能提升几十个百分点。中文版是"让我们一步一步思考"。
这句话触发的机制和上面说的一样:强迫模型不要直接跳到答案,而是先把推理链条写出来。
Zero-shot CoT:不给例子,只加"一步一步思考"。对于模型本身有能力推理但容易偷懒的任务很有效。
Few-shot CoT:给的例子里包含推理过程,不只是输入输出对,而是"输入 → 推理步骤 → 输出"。这是效果最好的组合,但写起来也最费劲。
1.3 一个实际例子:代码 Bug 分析
以下对比展示了 CoT 的效果差异。假设需要分析一段代码为什么会抛出 ConcurrentModificationException。
不用 CoT 的 Prompt(验证目的:观察没有推理步骤时模型给出的答案质量):
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{
"role": "user",
"content": """以下 Java 代码为什么会抛出 ConcurrentModificationException?
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String item : list) {
if (item.equals("b")) {
list.remove(item);
}
}
直接给出原因。"""
}
]
)
这个 Prompt 大概率会得到一个正确但笼统的答案:"因为在遍历 ArrayList 的同时修改了它"。
用 Few-shot CoT 的 Prompt(验证目的:观察加入推理步骤模板后,模型是否能给出完整的因果链分析):
system_prompt = """你是一名 Java 调试专家。分析代码问题时,请按以下步骤思考:
1. 确认异常类型和触发时机
2. 追踪相关操作的执行顺序
3. 找到根本原因
4. 给出修复方案
示例:
问题代码:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.isEmpty()) list.remove(s); // 直接用 list.remove
}
分析过程:
步骤1:NullPointerException?不是。ConcurrentModificationException?是的,在迭代器遍历过程中触发。
步骤2:迭代器内部维护一个 modCount,记录列表被修改的次数。list.remove() 会让 modCount +1,但迭代器的 expectedModCount 没有更新。
步骤3:下一次调用 it.next() 时,迭代器检测到 modCount != expectedModCount,抛出异常。根本原因:绕过迭代器直接修改了列表。
步骤4:把 list.remove(s) 改成 it.remove(),通过迭代器自身的 remove 方法删除,会同步更新 expectedModCount。
现在分析以下问题:"""
user_prompt = """问题代码:
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
for (String item : list) {
if (item.equals("b")) {
list.remove(item);
}
}"""
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.1
)
用了 Few-shot CoT 之后,模型会照着例子的推理步骤走,解释 for-each 循环背后其实也是迭代器,分析 modCount 的变化,最后给出用 Iterator.remove() 或 removeIf() 修复的方案,而不是给一句"因为并发修改"了事。
1.4 什么时候用 Few-shot,什么时候用 CoT
理解了两种技术的底层机制,选择依据就变得清晰了。
Few-shot 解决的问题是:告诉模型用什么标准判断、输出什么格式。
Few-shot 适合的场景:
任务有固定的输出格式。比如信息提取,每次都要输出一个结构化的 JSON,给几个例子让模型照着格式走。
分类任务。判断标准不是通用的,而是业务特有的,需要用例子传递判断逻辑。
需要特定风格。比如让模型的回复风格和团队的 Wiki 保持一致,给几篇 Wiki 做示范。
CoT 解决的问题是:给模型足够的"计算空间"来完成多步推理。
CoT 适合的场景:
复杂推理。比如分析一个系统设计方案有哪些潜在风险,需要从多个维度思考。
数学计算或逻辑推导。直接给答案错误率很高,让它写推理过程能大幅降低出错率。
多步骤任务。任务本身就有先后依赖关系,比如先分析需求,再拆分子任务,再评估优先级。
判断标准的一个简洁版本:如果问题的核心难点是"模型不知道用什么规则",用 Few-shot;如果核心难点是"模型需要多步推导才能得出答案",用 CoT;如果两者都有,就组合使用。
1.5 Few-shot + CoT:最常用的组合
两个技术经常组合使用。给几个包含推理步骤的例子(Few-shot CoT),比单独用任何一个都强。
先试 Zero-shot,如果效果不稳定,加 Few-shot;如果涉及复杂推理,再加 CoT。不需要一上来就把所有技巧全堆上去,Prompt 越复杂维护成本越高。
下一章讲 ReAct,那是 Agent 能够调用工具、操作外部世界的基础模式。
1.6 Prompt 速查卡片
| 技巧 | 解决什么问题 | 典型触发条件 | 示例 |
|---|---|---|---|
| Zero-shot | 模型本身能力足够的简单任务 | 通用翻译、简单摘要 | 直接提问 |
| Few-shot | 需要特定判断标准或输出格式 | 业务分类、特定格式提取 | 提供 3-5 个示例 |
| CoT | 多步推理任务,直接回答容易出错 | 数学题、复杂逻辑分析 | "让我们一步一步思考" |
| Few-shot CoT | 需要特定标准 + 多步推理 | 复杂业务判断 + 需要解释 | 示例中包含推理步骤 |
好的 Few-shot vs 差的 Few-shot:
差的做法:随机选几个典型例子作为示范。
好的做法:专门选边界情况和容易混淆的案例——这些才是模型最需要判断标准的地方。5 个精挑细选的例子,比 20 个随便堆的例子效果好得多。