Skip to content

Prompt Engineering 进阶:Chain-of-Thought、Few-shot、ReAct 模式,如何用 Prompt 替代微调

问题

面试中常见这样的问题:

"你如何设计 Prompt 来让模型完成复杂推理任务?Chain-of-Thought 和 Few-shot 有什么区别?什么时候应该用 ReAct 而不是普通 Prompt?如果老板说『别微调了,搞个 Prompt 就上线』,你会怎么回答?"

这些问题的背后,本质上是考察对 Prompt Engineering 从"写清楚提示词"到"系统化引导模型行为"的认知升级。

三大核心模式

1. Chain-of-Thought(CoT)—— 让模型学会"先想再说"

核心思想:在 Prompt 中加入"Let's think step by step",引导模型输出中间推理步骤,再给出最终答案。

Why it works:LLM 本质上是一个"下一个 token 预测器",直接让它输出答案,它会跳过推理过程,导致逻辑跳跃。CoT 相当于给模型一个"思考的脚手架"——让概率分布在推理路径上逐步收敛,而不是一步跳到最终答案。

python
# Zero-shot CoT 示例
prompt = """
Q: 小明有 12 个苹果,给了小红 3 个,又从超市买了 5 个,现在比原来多几个?
A: Let's think step by step.
"""

# 模型输出:
# 1. 小明原来有 12 个苹果
# 2. 给了小红 3 个,剩下 12 - 3 = 9 个
# 3. 又买了 5 个,变成 9 + 5 = 14 个
# 4. 原来 12 个,现在 14 个,多了 14 - 12 = 2 个
# 答案:2

底层原理:CoT 有效不是因为模型真的"学会了推理",而是因为中间步骤把推理路径拆成了多个小步骤,每个步骤的 token 预测难度大幅降低。Wei et al. (2022) 在 GSM8K 上验证:Zero-shot 准确率 10.4% → CoT 58.4%,提升 5 倍。核心原因是分步降低了每一步的条件熵

进阶变体

  • Self-Consistency:对同一个问题多次采样 CoT 推理,取多数答案。数学推理任务上可以再提 5-10 个点。原理:单一 CoT 路径可能落入局部最优,多次采样 + 投票能平滑掉随机波动。
  • CoT-SC(Self-Consistency with CoT):采样 → 多数投票 → 输出。OpenAI 的 Let's Verify Step by Step 论文证明了这一点。在 GSMA8K 上,CoT-SC 把准确率从 58% 推到 72%。
python
# Self-Consistency 伪代码
def solve_with_self_consistency(prompt, n=5, temperature=0.7):
    responses = [llm.generate(
        prompt + " Let's think step by step.",
        temperature=temperature
    ) for _ in range(n)]
    answers = [extract_answer(r) for r in responses]
    vote = Counter(answers)
    # 调试用:打印投票分布
    print(f"Vote distribution: {vote.most_common()}")
    return vote.most_common(1)[0][0]

踩坑经验:temperature 设太高(>0.7)会导致 CoT 路径发散,设太低(<0.1)又失去多样性。实操中推荐 temperature=0.3~0.5,采样 5 次,取多数。另外,数字推理场景中 LLM 的"四则运算"经常出错,建议加 Format 约束让模型输出中间算式,由后端解析器做实际计算——这叫"CoT + 计算委托"。

2. Few-shot —— 用示例教会模型"上下文学习"

核心思想:在 Prompt 中给出 2-5 个问答示例,让模型在当前对话上下文中学会模式。

适用场景:分类、格式转换、命名实体识别等结构化输出任务。

底层原理(In-Context Learning):Few-shot 不是"训练"了模型,而是激活了预训练阶段见过的模式。每个示例相当于在 attention 计算中为特定输出格式提供了"锚点"。示例越多,attention 越收敛到目标分布。但示例数量超过某个阈值(通常 5-10 个)后收益递减,因为上下文窗口有限,注意力被稀释。

python
# Few-shot 示例 —— 情感分类
prompt = """
文本:这部电影太棒了,看得我热泪盈眶。
情感:正面

文本:剧情拖沓,演员演技尴尬。
情感:负面

文本:服务员态度很差,但菜的味道还不错。
情感:混合

文本:{{user_input}}
情感:
"""

关键参数

  • 示例数量(k):k=2 通常就够了,太多会浪费 token。实测:k=1 准确率 78%,k=3 准确率 92%,k=7 准确率 93%,边际收益骤降。
  • 示例顺序:越靠近 query 的示例影响越大。因为 attention 对近端 token 的权重更高。实战:把最难分的示例放在最后。
  • 示例多样性:覆盖不同类别,避免分布偏差。例如情感分类,2 正 2 负 1 混合,不要 4 正 1 负。

踩坑经验:Few-shot 中如果示例本身的标签有噪声(比如一条负面情感被标成了正面),模型会学到这个错误模式。线上场景中,务必将 few-shot 示例纳入 CI 校验——每次模型更新都跑一遍示例集,确保输出格式不变。我见过一个事故:GPT-4 升级后,Few-shot 情感分类的格式从"正面/负面/混合"变成了"Positive/Negative/Mixed",导致下游 NER 解析全部报错,回滚花了 2 小时。

3. ReAct —— 推理 + 行动循环

核心思想:让模型交替输出"推理"和"行动",行动调用外部工具,观察结果后再推理,形成闭环。

这是 Agent 系统的核心框架。ReAct 论文(Yao et al., 2023)在 HotPotQA 和 ALFWorld 上验证了:ReAct 比 CoT(仅推理,无行动)和 Act-only(仅行动,无推理)分别高出 22% 和 34%。原因是推理修正行动,行动反馈推理,形成正反馈循环。

时序流程

                    ┌─────────────────────────────────┐
                    │         用户输入 Query            │
                    └────────────┬────────────────────┘

                    ┌─────────────────────────┐
                    │  Thought: 分析当前状态    │
                    │  "需要先查天气数据"       │
                    └────────────┬─────────────┘

                    ┌─────────────────────────┐
                    │  Action: 调用工具        │
                    │  get_weather(city, date) │
                    └────────────┬─────────────┘

                    ┌─────────────────────────┐
                    │  Observation: 工具返回   │
                    │  {"temp": 28, ...}      │
                    └────────────┬─────────────┘

                    ┌─────────────────────────┐
                    │  Thought: 分析结果       │
                    │  "28°C 多云,适合跑步"     │
                    └────────────┬─────────────┘

                    ┌─────────────────────────┐
                    │  Action: 再查辅助信息     │
                    │  check_running_advice()  │
                    └────────────┬─────────────┘
                    (循环直到条件满足)

                    ┌─────────────────────────┐
                    │  Final Answer: 综合输出   │
                    └─────────────────────────┘

生产环境 ReAct Agent 实现(带错误处理)

python
import json
from typing import Dict, List, Optional

class ReActAgent:
    def __init__(self, llm, tools: Dict, max_steps: int = 5):
        self.llm = llm
        self.tools = tools  # {"get_weather": func, "search": func, ...}
        self.max_steps = max_steps
        self.history: List[Dict] = []

    def run(self, query: str) -> str:
        prompt = self._build_prompt(query)
        step = 0
        while step < self.max_steps:
            response = self.llm.generate(prompt)

            # 解析输出
            if "Final Answer:" in response:
                return response.split("Final Answer:")[-1].strip()

            action = self._parse_action(response)
            if not action:
                # 模型没输出 Action,强制回退
                prompt += "\nObservation: 你没有输出 Action,请重新思考并输出 Action\n"
                step += 1
                continue

            try:
                tool_func = self.tools.get(action["name"])
                if not tool_func:
                    observation = f"Error: 工具 {action['name']} 不存在,可用工具: {list(self.tools.keys())}"
                else:
                    observation = tool_func(**action["input"])
            except Exception as e:
                observation = f"Error: 工具调用失败: {str(e)}"

            self.history.append({
                "step": step,
                "thought": self._extract_thought(response),
                "action": action,
                "observation": observation
            })
            prompt += f"\nObservation: {observation}\n"
            step += 1

        # 超过最大步数,强制返回
        return f"Agent 达到最大步数 {self.max_steps},未完成完整推理"

    def _parse_action(self, response: str) -> Optional[Dict]:
        """解析 Action 块,支持 JSON 和文本两种格式"""
        if "Action:" not in response:
            return None
        try:
            lines = response.split("\n")
            name = None
            input_str = None
            for line in lines:
                if line.startswith("Action:"):
                    name = line.split("Action:")[-1].strip()
                elif line.startswith("Action Input:"):
                    input_str = line.split("Action Input:")[-1].strip()
            if name and input_str:
                return {"name": name, "input": json.loads(input_str)}
        except:
            pass
        return None

生产环境中的三大坑

  1. 死循环陷阱:ReAct Agent 经常在"我回答不了 → 我查一下 → 查不到 → 再查"之间循环。必须设置 max_steps 和重复检测(连续 3 步查同一个工具 + 同一个参数就打断)。
  2. 工具幻觉:模型会编造不存在的工具名。比如虚构一个 get_weather_by_city 而不调用真实的 get_weather。固定解法:在 system prompt 中列出精确的工具名,并在解析层做白名单校验,命中白名单外的工具抛异常并重试。
  3. Observation 太长截断:工具返回的 JSON 太大会溢出上下文窗口。实战中遇到 ES 返回 200 条结果,Observation 占了 15K token,下一步的 Thought 直接被截没了。解法:对 Observation 做长度截断 + 摘要(str(obs)[:2000] + "...")。

核心问题:什么时候用 Prompt 替代微调?

现实场景中,很多团队面临一个选择:是花几周微调一个模型,还是花几天设计一套 Prompt + RAG 系统?

80% 场景不需要微调

在 GPT-4 / Claude 3.5 级别的大模型上,80% 的垂直场景可以通过精心设计的 Prompt + RAG 解决:

场景推荐方案原因
客服问答Prompt + RAG知识更新快,无需重训模型
文本分类Few-shot Prompt示例足够,模型本身理解能力强
代码生成CoT + 约束 Prompt标准任务,模型已预训练大量代码
数据提取结构化 Prompt + JSON Schema比微调更容易迭代
搜索意图识别Few-shot + CoT 组合先用 CoT 推理意图,再用 Few-shot 输出格式
工具调用ReAct + 工具白名单工具可动态增删,无需重新训练

什么时候才需要微调?

  • 模型需要学会特定行为模式(如严格遵守某种格式、拒绝回答某些问题)
  • 需要降低推理成本(用更小的模型替代大模型,微调后达到同等效果)
  • 领域知识非常特殊静态(如法律条文理解、医疗诊断辅助)

真实案例:搜索系统

某电商团队的搜索意图识别模块,一开始用 GPT-4 + Few-shot Prompt 上线,准确率 94%。之后用 GPT-4 的蒸馏数据微调一个 7B 模型,准确率 95.5%,推理成本降低 80%。

LTV 曲线:0-2 周用 Prompt 快速验证 → 2-4 周收集真实数据 → 4-8 周微调小模型上线 → 8 周后删掉大模型 Prompt 调用。这是被验证的最佳实践路径。

三种模式对比表

维度CoTFew-shotReAct
核心能力推理能力格式学习工具调用 + 推理闭环
适用场景数学推理、逻辑分析分类、结构化输出Agent、多步骤任务
需要外部工具
Token 消耗中(需输出推理步骤)中(需提供示例)高(多轮循环)
延迟低-中高(取决于步骤数)
稳定性中(依赖模型推理能力)高(示例固定)中-低(容易跑偏)
面试常见追问Self-Consistency 如何选温度示例顺序为什么重要死循环怎么处理
可以和哪个组合+Few-shot、+Self-Consistency+CoT(CoT-Few-shot)+CoT(内部推理步骤)

Prompt 工程化的三大局限

面试官不会只问好处,还会问局限。

  1. Token 成本:长 Prompt 每次推理都消耗大量 token。如果一个 Prompt 包含 5000 token,每秒 10 次请求,一天就是 432 万 token。成本不可忽视。拿 GPT-4 算:432 万 token × $0.03/1K = $129.6/天,一个月 $3888。如果用微调后的 7B 模型,推理成本降到 $0.5/天,一个月 $15。
  2. 上下文窗口限制:Few-shot 示例越多,占用的窗口越大。如果示例太多,可能挤占输入内容的空间。长上下文窗口(如 128K、200K)也带来了延迟问题——attention 计算是 O(n²),10K→100K token,延迟从 0.5s 涨到 8s。
  3. 一致性问题:同样的 Prompt 在不同模型版本上的行为可能不同。OpenAI 的 GPT-4-0613 到 GPT-4-1106 就有明显的输出风格变化。我遇到过:GPT-4-0613 上 Few-shot 输出 JSON 格式完美,升级到 GPT-4-1106 后开始输出 Markdown 格式的 JSON,导致下游解析全挂。解决方案:在 CI 中跑 Prompt 回归测试,每次模型版本更新都跑一遍,而不是等线上报警。

实战组合策略

真实面试中,祥哥你可能会被问到:"一个复杂的客服 Agent,你会怎么设计它的 Prompt?"

我的回答框架:

系统 Prompt:
  角色定义(你是客服 Agent,用友好语气)
  工具列表(严格白名单,每个工具名精确)
  行为约束(不要编造信息,不要猜测用户意图)
  输出格式(JSON,带 confidence 字段)
  限制条件(max_steps=5,异常时返回 fallback 话术)

用户 Query:
  [用户输入]

Few-shot 示例(2-3 个):
  示例 1: 简单查询 → 直接调用工具
  示例 2: 复杂查询 → CoT 推理 → 调多个工具 → 聚合答案
  示例 3: 异常情况(工具返回空)→ 回退 + 转人工

CoT 引导:
  Let's work through this step by step.

ReAct 循环:
  Thought → Action → Observation → ...

这样同时用了 CoT(推理)、Few-shot(格式和模式)、ReAct(工具调用),三者不是互斥的,而是可以组合的。

进阶方向

如果面试官继续追问,展示你了解这些:

  • Meta-Prompting:让 LLM 自己优化 Prompt。DSPy 框架(Khattab et al., 2024)实现了这个思路,通过声明式编程自动搜索最优 Prompt。实战:DSPy 可以自动选择合适的 few-shot 示例数量和顺序,将分类任务准确率从 92% 提升到 95%。
  • Prompt Compression(LLMLingua):用更小的模型将长 Prompt 压缩到 20-30%,在效果几乎不变的情况下节省 token。实测:5K token 的 Prompt 压缩到 1.2K,准确率只降 0.3%,每百万 token 从 $15 降到 $3.6。
  • Prompt 版本管理:Prompt 应该像代码一样纳入 CI/CD 流程。非侵入式检查(如输出格式校验、正则匹配)和 A/B 测试是工程落地的关键。推荐:用 SQLite 存每条 Prompt 的版本号 + 输出样本 + 人工评分,做效果追踪。

总结

  • CoT 解决"模型不会推理"的问题,适合复杂推理,原理是分步降低 token 预测难度
  • Few-shot 解决"模型不知道我要什么格式"的问题,适合结构化输出,靠 attention 锚点激活预训练模式
  • ReAct 解决"模型不会调用外部工具"的问题,适合 Agent 系统,核心是推理闭环 + 错误处理
  • 三种模式可以组合(CoT-Few-shot-ReAct),不是互斥的
  • Case by case:先 Prompt 上线,用数据驱动决定是否微调
  • Prompt 工程化要考虑成本、窗口限制和一致性,并引入版本管理和元优化
  • 生产环境要关注死循环、工具幻觉、Observation 截断三大坑

参考:Chain-of-Thought (Wei et al., 2022)、ReAct (Yao et al., 2023)、DSPy、OpenAI Prompt Engineering Guide、LLMLingua、LLMLingua2


下一篇预告:Function Calling / Tool Use —— 大模型如何调用外部 API?

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。