前端转 AI 第 37 天:Few-shot 与 CoT - 让模型学会"推理"
昨日回顾
Day 36 讲了 Prompt 工程的方法论:四要素、System Prompt、六条原则、反模式。
但只讲了"怎么说",没讲"怎么让模型想"。
复杂问题,光说清楚还不够------还得让模型"分步思考"。
Day 37 目标:掌握 Few-shot / CoT / Self-Consistency / ReAct 四大推理技巧,让模型从"答对"到"会推理"。
一、为什么模型需要"推理技巧"
1. 一个测试
问一个简单的数学题:
一个商店进了一批苹果,第一天卖了一半加半个,
第二天卖了剩下的一半加半个,
第三天又卖了剩下的一半加半个,正好卖完。
问:原来有多少个苹果?
直接问模型:
python
response = client.chat.completions.create(
model="deepseek-chat",
messages=[{"role": "user", "content": "..."}],
)
输出:
原来有 7 个苹果。
对吗? 我们验证一下:
- 初始:7 个
- 第一天:7/2 + 0.5 = 4 个 → 剩 3 个
- 第二天:3/2 + 0.5 = 2 个 → 剩 1 个
- 第三天:1/2 + 0.5 = 1 个 → 剩 0 个 ✅
对了!但这个问题的正确答案是 7,模型蒙对的概率也不低。
再看一个更难一点的:
一个班级有 40 个学生,其中 60% 是女生。
女生中有 1/3 会弹钢琴。会弹钢琴的女生中有 1/4 参加过比赛。
问:参加过比赛的女生有多少人?
直接问,模型可能答"2 人"或"3 人"------但到底对不对不一定。
加上"让我们一步步思考":
一个班级有 40 个学生...
让我们一步步思考。
输出:
好的,让我一步步计算:
第一步:计算女生人数
40 × 60% = 24 人
第二步:计算会弹钢琴的女生
24 × 1/3 = 8 人
第三步:计算参加过比赛的女生
8 × 1/4 = 2 人
答案:2 人。
稳了。逻辑清晰,答案正确。
2. 差距在哪
关键差异:加了"让我们一步步思考"这一句。
这六个字值多少钱?
在 GSM8K(小学数学题数据集)上的测试:
| 方法 | 准确率 |
|---|---|
| 直接回答 | 17.9% |
| 加"一步步思考" | 78.7% |
准确率提升了 4 倍!
这就是 CoT 的威力。
3. 原因分析
为什么"一步步思考"这么有效?
核心原因: 大模型是"自回归生成"的 ------它每输出一个 token,就基于前面所有 token 来预测下一个。
这意味着:
- 输出越多:模型"思考时间"越长
- 显式推理:每一步的结果都成了后续计算的"上下文"
- 少走捷径:不会直接跳到答案,中间过程逼着它推
类比前端:
你写一个复杂函数:
js
// ❌ 直接写:容易漏边界
function calcPrice(order) {
return order.items.reduce((s, i) => s + i.price, 0);
}
// ✅ 先理清步骤再写:不易出错
function calcPrice(order) {
// 1. 校验输入
if (!order || !order.items) return 0;
// 2. 过滤无效商品
const validItems = order.items.filter(i => i.price > 0);
// 3. 求和
const total = validItems.reduce((s, i) => s + i.price, 0);
// 4. 应用折扣
const discount = order.coupon?.discount || 0;
return Math.max(0, total - discount);
}
先想清楚步骤,再写代码------这就是人类版的 CoT。
模型也一样。
二、Few-shot:给例子是最强的引导
1. 三种方式对比
Zero-shot(不给例子):
判断这个评论的情感:
"这个产品太差了,浪费钱"
One-shot(给一个例子):
判断情感,例如:
"东西不错,物流很快" → 正面
"这个产品太差了,浪费钱" →
Few-shot(给多个例子):
判断情感,例如:
"东西不错,物流很快" → 正面
"一般般吧,没惊喜" → 中性
"再也不会买了" → 负面
"这个产品太差了,浪费钱" →
效果对比:
| 方式 | 准确率 | Token 消耗 |
|---|---|---|
| Zero-shot | 70% | 低 |
| One-shot | 82% | 中 |
| Few-shot | 90% | 高 |
付出 token,收获准确率。
2. Few-shot 的三大要素
1. 例子的数量:1-3 个最合适
1 个 → 通常够用
3 个 → 覆盖多种情况
5+ 个 → 边际收益递减,浪费 token
2. 例子的多样性
❌ 都是同一类型:
"很好" → 正面
"不错" → 正面
"很棒" → 正面
模型学会"一切都正面"------因为它没看过其他类型。
✅ 覆盖多种情况:
"很好" → 正面
"一般" → 中性
"太差" → 负面
"有点贵但质量不错" → 偏正面
覆盖正/中/负 + 复杂情况。
3. 例子的格式要和实际输入一致
❌ 不一致:
示例输入:"今天天气不错"
示例输出:{"sentiment": "positive"}
实际输入:"这家餐厅的服务员态度非常恶劣,食物也难吃"
格式差异会让模型"跑偏"。
✅ 一致:
【示例】
输入:"今天天气不错"
输出:{"sentiment": "positive", "confidence": 0.95}
【示例】
输入:"这家餐厅的服务员态度非常恶劣,食物也难吃"
输出:
3. 实战:给"工单分类"加 Few-shot
需求:把用户工单分到正确的部门。
创建 day37/few_shot_demo.py:
python
"""Few-shot 演示:工单分类"""
import os
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
)
# ============ 零样本版本 ============
ZERO_SHOT_SYSTEM = """你是一个工单分类助手。
把用户工单分到以下部门之一:技术支持、售前咨询、投诉、财务、其他。
直接返回部门名称,不要解释。"""
# ============ Few-shot 版本 ============
FEW_SHOT_SYSTEM = """你是一个工单分类助手。
把用户工单分到以下部门之一:技术支持、售前咨询、投诉、财务、其他。
参考示例:
工单:"网站打不开,报 502 错误" → 技术支持
工单:"你们家产品支持私有化部署吗?" → 售前咨询
工单:"客服态度太差,我要投诉!" → 投诉
工单:"发票什么时候能寄到?" → 财务
工单:"你们公司成立几年了?" → 其他
工单:"登录后一直转圈,换了浏览器也不行" → 技术支持
工单:"想了解一下企业版的报价" → 售前咨询
直接返回部门名称,不要解释。"""
def classify(ticket: str, use_few_shot: bool = True) -> str:
system = FEW_SHOT_SYSTEM if use_few_shot else ZERO_SHOT_SYSTEM
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{"role": "system", "content": system},
{"role": "user", "content": f"工单:{ticket}"},
],
temperature=0,
)
return response.choices[0].message.content.strip()
if __name__ == "__main__":
test_tickets = [
"上传文件的时候提示'文件太大',怎么解决",
"想问下 5 人团队用什么套餐合适",
"已经付款 3 天了,订单还是'待发货',太让人失望了",
"需要开发票,抬头是个人",
"你们的 API 支持流式返回吗",
]
print(f"{'工单':<40} {'Zero':<10} {'Few':<10}")
print("=" * 70)
for t in test_tickets:
zero = classify(t, use_few_shot=False)
few = classify(t, use_few_shot=True)
print(f"{t[:38]:<40} {zero:<10} {few:<10}")
输出示例:
工单 Zero Few
======================================================================
上传文件的时候提示'文件太大',怎么解决 技术支持 技术支持
想问下 5 人团队用什么套餐合适 售前咨询 售前咨询
已经付款 3 天了,订单还是'待发货'... 投诉 投诉
需要开发票,抬头是个人 财务 财务
你们的 API 支持流式返回吗 技术支持 售前咨询 ← Few-shot 修正
注意最后一条: Zero-shot 判成"技术支持",但实际是售前咨询。Few-shot 给对了。
因为例子里的"支持私有化部署"让模型学到了"问能力=售前"的模式。
4. Few-shot 的"坑"
坑 1:例子有偏见
如果例子全是"技术支持":
工单:"登录不了" → 技术支持
工单:"密码忘了" → 技术支持
工单:"页面卡顿" → 技术支持
模型会倾向把一切归到"技术支持"。
解法:例子要均衡。
坑 2:例子和实际输入差异太大
例子里是长工单,实际是短工单------模型可能"照搬格式",输出一堆无关内容。
解法:例子要和实际输入同分布。
坑 3:例子太多
例子超过 5 个,边际收益陡降,还占用大量 token。
解法:3-5 个精心挑选的例子,胜过 10 个普通例子。
三、CoT:让模型"思考后再回答"
1. Zero-shot CoT:最简单的一招
什么都不用改,只在 Prompt 最后加一句:
Let's think step by step.
(让我们一步步思考。)
或者中文版:
请一步步思考后再给出答案。
测试数据(学术论文结果):
| 任务 | 直接回答 | 加 CoT |
|---|---|---|
| 数学题 | 17.9% | 78.7% |
| 常识推理 | 40% | 65% |
| 符号推理 | 20% | 60% |
一句话,翻倍效果。
2. Few-shot CoT:给带推理过程的例子
更强的版本------示例包含推理过程:
问:一个商店有 23 个苹果,卖了 20 个,又进货 6 个,现在有多少个?
答:让我一步步想:
- 初始:23 个
- 卖了 20 个后:23 - 20 = 3 个
- 进货 6 个后:3 + 6 = 9 个
- 答案:9 个
问:小明有 5 块钱,买了 3 个苹果每个 1.5 元,还剩多少钱?
答:
模型会模仿示例的格式,给出完整推理过程。
3. CoT 的最佳实践
1. 明确要求"分步"
请按以下步骤回答:
1. 理解问题
2. 提取已知条件
3. 逐步推导
4. 得出结论
或者更简洁:
请一步步思考。
2. 用"标记词"分隔
【思考过程】
...
【最终答案】
...
这样模型会分开"思考"和"结论",避免思考混进答案。
3. 复杂任务用"分步骤指令"
请按以下步骤分析这段代码:
步骤 1:梳理函数功能
步骤 2:检查边界条件
步骤 3:分析时间复杂度
步骤 4:给出优化建议
每一步"想什么"都说清楚。
4. CoT 的"坑"
坑 1:简单任务不需要 CoT
问"1+1等于几",让模型"一步步思考"是浪费。
判断标准:
- 简单查表 → 不用 CoT
- 多步推理 → 用 CoT
- 需要计算 → 用 CoT
坑 2:CoT 会泄露推理过程
如果输出给用户,可能显得啰嗦。
解法:
【思考过程】(内部推理,用户看不到)
...
【最终答案】
简洁的结论
然后在展示时只取"最终答案"部分。
坑 3:CoT 有时会"过度思考"
简单题给出一大堆推导,最后还可能"想歪了"。
解法:
- 限制温度 :
temperature=0 - 限制长度 :
max_tokens - 换模型:小模型不适合 CoT,用推理模型(如 DeepSeek R1)
四、Self-Consistency:让模型"多试几次"
1. 核心思想
同一个问题,让模型答 N 次,取出现次数最多的答案。
有点像"民主投票"。
问题:X 是多少?
第 1 次:答案是 7(推理过程 A)
第 2 次:答案是 7(推理过程 B)
第 3 次:答案是 8(推理过程 C)
第 4 次:答案是 7(推理过程 D)
第 5 次:答案是 7(推理过程 E)
→ 投票结果:7(4 票)
准确率提升数据(GSM8K):
| 方法 | 准确率 |
|---|---|
| CoT | 78.7% |
| Self-Consistency (N=5) | 85% |
| Self-Consistency (N=10) | 90% |
| Self-Consistency (N=40) | 93% |
代价: N 倍 token 消耗。
2. 实现 Self-Consistency
创建 day37/self_consistency.py:
python
"""Self-Consistency 演示"""
import os
from collections import Counter
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
)
def solve_with_cot(question: str) -> str:
"""单次 CoT 求解"""
response = client.chat.completions.create(
model="deepseek-chat",
messages=[
{
"role": "system",