第 73 章

Workbench + 评估框架:Prompt Generator / Datadog 监控 / 系统性 Eval 方法论

第七十三章:Few-Shot 与 Chain-of-Thought 的科学:从原理到工程实践

73.1 为什么需要示例与推理链

大语言模型的能力边界由两个因素共同决定:模型本身的参数量与训练数据,以及推理时的上下文输入质量。前者是固定的,后者是工程师可以主动干预的变量。Few-Shot Prompting 和 Chain-of-Thought(CoT)正是两类最经过实证验证的上下文干预手段。

Few-Shot 的核心假设是:模型在见到任务的输入-输出示例之后,能够推断出任务的隐式规则,并将其应用到新样本上。 这与人类学习类比推理的机制高度相似。

Chain-of-Thought 的核心假设是:将推理步骤显式化,能够帮助模型在生成最终答案之前,通过中间步骤"想清楚"问题。 这本质上是将模型的计算资源从"直接输出"重新分配到"逐步推导"。

这两者并非互斥,实践中最强的 Prompt 往往是两者的结合:Few-Shot CoT,即提供带有推理过程的示例。

73.2 Few-Shot 的科学基础

73.2.1 In-Context Learning 的机制

研究者对 In-Context Learning(ICL)的工作原理至今仍有争论,但有几点已形成共识:

  1. 格式学习(Format Learning)优先于内容学习(Content Learning):模型从示例中学到的最重要的信息是"任务的输入-输出格式长什么样",而非"具体哪个输入映射到哪个输出"。Brown et al.(2020) 的实验表明,即使使用随机错误的标签,Few-Shot 仍然比 Zero-Shot 表现更好,这支持了格式学习的假说。

  2. 示例激活相关表示(Activation of Relevant Representations):示例的存在会激活模型参数中与当前任务相关的知识子集,引导模型进入正确的"任务模式"。

  3. 分布对齐(Distribution Alignment):示例帮助模型理解当前任务的输出分布特征,例如"答案应该是一个三位数的整数"或"回复应该使用正式商务语体"。

73.2.2 示例数量的权衡

示例数量 适用场景 风险
0-shot 任务描述足够清晰,或模型对该任务有强大的先验 格式不稳定,输出多样
1-shot Token 预算有限,任务相对标准 单一示例可能引入偏见
3-5 shot 大多数结构化任务的黄金区间 消耗更多上下文
10+ shot 高度复杂或罕见的任务类型 收益递减,可能超出上下文窗口

研究表明,对于大多数任务,3-5 个高质量示例的效果与 20 个低质量示例相当甚至更好。示例质量远比数量重要。

73.2.3 示例选择策略

示例选择是 Few-Shot 工程中最被低估的环节。以下是四种主要策略:

策略一:随机采样(Random Sampling)

从示例池中随机选取,实现简单,但质量不稳定。适合作为基线。

策略二:相似度检索(Similarity-Based Retrieval)

将测试样本嵌入为向量,从示例池中检索最相似的 K 个示例。这是目前生产环境中最常用的策略。

from anthropic import Anthropic
import numpy as np

client = Anthropic()

def cosine_similarity(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def retrieve_similar_examples(query_embedding, example_pool, k=5):
    """
    从示例池中检索最相似的 K 个示例
    example_pool: [{"input": ..., "output": ..., "embedding": [...]}]
    """
    similarities = [
        (cosine_similarity(query_embedding, ex["embedding"]), ex)
        for ex in example_pool
    ]
    similarities.sort(key=lambda x: x[0], reverse=True)
    return [ex for _, ex in similarities[:k]]

策略三:多样性采样(Diversity-Based Sampling)

确保示例覆盖任务的不同子类型,防止示例集合过于同质化。可以使用 K-means 聚类后从每个类中采样一个代表性示例。

策略四:难度梯度(Difficulty Gradient)

从简单到复杂排列示例,让模型先看到容易的样本建立基础理解,再看到复杂样本。

73.2.4 示例格式一致性

格式一致性是 Few-Shot 成功的关键约束,且这一点经常被忽视。

不一致的格式示例(错误示范):

示例1:
输入: 苹果
输出: 水果

示例2:
Input: 香蕉
Output: Fruit

示例3:
Q: 西瓜属于什么类别?
A: 这是一种水果。

一致的格式示例(正确示范):

示例1:
输入: 苹果
类别: 水果

示例2:
输入: 香蕉
类别: 水果

示例3:
输入: 西瓜
类别: 水果

格式一致性检查清单:

73.3 Chain-of-Thought 的科学基础

73.3.1 为什么推理链有效

Wei et al.(2022) 在论文《Chain-of-Thought Prompting Elicits Reasoning in Large Language Models》中首次系统性证明了 CoT 的有效性。核心发现包括:

  1. 涌现行为:CoT 的效果在模型参数量超过约 100B 时才显著涌现。对于较小的模型,CoT 甚至可能有负面效果。对于 Claude 这样的大型模型,CoT 几乎总是有益的。

  2. 任务依赖性:CoT 对需要多步推理的任务(数学、逻辑推断、代码调试)效果显著;对单步任务(情感分类、命名实体识别)效果有限,甚至增加了不必要的复杂度。

  3. 计算分配:CoT 将 Transformer 的 KV-cache 注意力机制引导到中间推理步骤上,相当于给模型提供了"草稿纸"。

73.3.2 CoT 的主要变体

Zero-Shot CoT

在 Prompt 末尾加入"让我们一步一步地思考"(Let's think step by step),这是成本最低的 CoT 实现方式。

def zero_shot_cot_prompt(question: str) -> str:
    return f"""{question}

请一步一步地分析这个问题,然后给出最终答案。"""

Few-Shot CoT

提供带有推理过程的示例,这是效果最强的变体。

示例:
问题:一个工厂有 240 名工人,其中 1/3 是女性。在女性工人中,有 40% 持有大学学位。
持有大学学位的女性工人有多少?

推理过程:
第一步:计算女性工人数量
女性工人 = 240 × (1/3) = 80 人

第二步:计算持有大学学位的女性工人数量
有学位的女性 = 80 × 40% = 32 人

最终答案:32 人

自洽性 CoT(Self-Consistency CoT)

对同一问题生成多条推理链(使用较高温度),然后通过投票选择最常见的答案。这显著提升了数学和逻辑推理的准确率,代价是 API 调用成本倍增。

from collections import Counter

def self_consistency_cot(question: str, n_samples: int = 5) -> str:
    """
    生成多条推理链,通过多数投票得出最终答案
    """
    answers = []
    
    for _ in range(n_samples):
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=1024,
            temperature=0.7,  # 较高温度增加多样性
            messages=[{
                "role": "user",
                "content": f"{question}\n\n请一步一步思考,最后在 <answer> 标签中给出答案。"
            }]
        )
        # 提取答案
        text = response.content[0].text
        if "<answer>" in text:
            answer = text.split("<answer>")[1].split("</answer>")[0].strip()
            answers.append(answer)
    
    # 多数投票
    if answers:
        counter = Counter(answers)
        return counter.most_common(1)[0][0]
    return "无法确定"

树状思维(Tree of Thoughts, ToT)

探索多条并行推理路径,在每个步骤评估哪条路径更有前途,类似树搜索。适合需要回溯的复杂规划任务。

73.3.3 CoT 的工程实现要点

在 Claude 的生产部署中,以下几点至关重要:

1. 推理链放在 Assistant 预填充中

# 通过预填充强制 Claude 先进行推理
response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=2048,
    messages=[
        {"role": "user", "content": "分析以下合同条款是否存在法律风险:..."},
        {"role": "assistant", "content": "让我系统地分析这份合同的各个风险维度:\n\n**第一步:识别关键条款**\n"}
    ]
)

2. 结构化推理与答案分离

def structured_cot_prompt(task: str) -> str:
    return f"""任务:{task}

请按以下格式回答:

<thinking>
在这里进行详细的逐步分析。检查所有相关因素,考虑不同的可能性。
</thinking>

<answer>
在这里给出简洁的最终答案。
</answer>"""

3. 推理链的验证

推理链本身可能包含错误。在高风险场景(财务、医疗、法律)中,应当将推理链作为额外的可审计信息,而不是盲目信任其正确性。

73.4 Few-Shot CoT 的组合实践

73.4.1 构建高质量示例库

# 示例库的数据结构设计
example_schema = {
    "id": "ex_001",
    "task_type": "financial_analysis",
    "difficulty": "medium",
    "input": "...",
    "reasoning": "第一步:...\n第二步:...",
    "output": "...",
    "embedding": [...],  # 用于相似度检索
    "quality_score": 0.95,  # 人工标注的质量分
    "created_at": "2025-01-15",
    "tags": ["revenue", "yoy_growth"]
}

73.4.2 动态示例组装

def build_few_shot_cot_prompt(
    user_query: str,
    task_instruction: str,
    example_pool: list,
    query_embedding: list,
    n_examples: int = 3
) -> list:
    """
    动态构建 Few-Shot CoT Prompt
    """
    # 检索最相关的示例
    similar_examples = retrieve_similar_examples(
        query_embedding, example_pool, k=n_examples
    )
    
    # 按难度从低到高排序
    similar_examples.sort(key=lambda x: x.get("difficulty_score", 0.5))
    
    # 构建示例文本
    examples_text = ""
    for i, ex in enumerate(similar_examples, 1):
        examples_text += f"""
示例 {i}:
输入:{ex['input']}

分析过程:
{ex['reasoning']}

输出:{ex['output']}
---"""
    
    system_prompt = f"""{task_instruction}

以下是一些参考示例,展示了正确的分析思路:

{examples_text}

请按照相同的分析框架处理用户的请求。"""
    
    return [
        {"role": "user", "content": system_prompt + f"\n\n现在请处理:\n{user_query}"}
    ]

73.4.3 Few-Shot vs Fine-tuning 的决策树

任务类型?
├── 格式化/结构化输出 → Few-Shot 通常足够
├── 需要领域专业知识 → 考虑 RAG + Few-Shot
├── 需要特定风格/语气 → Fine-tuning 更合适
└── 需要高精度推理 → Few-Shot CoT 或 Fine-tuning + CoT

数据量?
├── < 100 个高质量样本 → Few-Shot
├── 100-1000 个样本 → 评估 Fine-tuning 成本收益
└── > 1000 个样本 → Fine-tuning 可能显著提升效果

延迟要求?
├── < 1 秒 → Zero-Shot 或 1-Shot(最短上下文)
├── 1-5 秒 → 3-5 Shot CoT
└── > 5 秒可接受 → Self-Consistency CoT

73.5 常见陷阱与调试技巧

73.5.1 示例偏见(Example Bias)

当所有示例都属于同一类别时,模型会产生明显的分类偏见。解决方案:确保示例集合在标签分布上与真实测试集相近,或明确在指令中说明"注意保持输出平衡"。

73.5.2 示例顺序效应(Recency Bias)

模型对序列末尾的示例赋予更高权重(Recency Bias)。在分类任务中,这会导致模型倾向于预测最后一个示例的标签。

缓解策略

73.5.3 推理链的幻觉(Hallucinated Reasoning)

模型可能生成听起来合理但实际错误的推理链。这在以下情况下尤为危险:推理链本身没有被人工审阅,而是直接驱动下游动作。

防御措施

  1. 要求模型在推理链中引用来源或说明推理依据
  2. 对关键中间步骤设置验证节点(独立调用 Claude 验证)
  3. 将高置信度阈值作为触发自动执行的条件

73.5.4 上下文窗口管理

大量示例会占用宝贵的上下文窗口。在 Claude 的 200K Token 窗口中,这通常不是瓶颈,但在需要同时处理长文档的场景中需要谨慎规划 Token 预算。

def estimate_token_budget(
    system_prompt: str,
    examples: list,
    user_query: str,
    target_output_length: int = 500
) -> dict:
    """简单的 Token 预算估算"""
    # 粗略估算:英文约 4 字符/token,中文约 1.5-2 字符/token
    system_tokens = len(system_prompt) // 3
    example_tokens = sum(
        len(ex.get("input", "") + ex.get("output", "") + ex.get("reasoning", "")) // 3
        for ex in examples
    )
    query_tokens = len(user_query) // 3
    
    total_input = system_tokens + example_tokens + query_tokens
    
    return {
        "system_tokens": system_tokens,
        "example_tokens": example_tokens,
        "query_tokens": query_tokens,
        "total_input": total_input,
        "remaining_for_output": 200000 - total_input,
        "is_feasible": total_input + target_output_length < 200000
    }

73.6 生产部署的工程考量

73.6.1 示例缓存策略

频繁检索相似示例会带来嵌入计算开销。在生产环境中:

import hashlib
from functools import lru_cache

@lru_cache(maxsize=1000)
def get_query_embedding_cached(query: str) -> tuple:
    """带缓存的嵌入计算(将 list 转为 tuple 以支持 LRU 缓存)"""
    # 调用嵌入模型 API
    embedding = compute_embedding(query)
    return tuple(embedding)

73.6.2 A/B 测试框架

不同的示例选择策略效果差异显著,需要通过 A/B 测试来量化。

import random

class PromptABTester:
    def __init__(self, strategies: dict):
        """
        strategies: {"strategy_name": strategy_function}
        """
        self.strategies = strategies
        self.results = {name: [] for name in strategies}
    
    def run_experiment(self, query: str, ground_truth: str):
        strategy_name = random.choice(list(self.strategies.keys()))
        prompt = self.strategies[strategy_name](query)
        
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=512,
            messages=[{"role": "user", "content": prompt}]
        )
        
        output = response.content[0].text
        score = evaluate_output(output, ground_truth)
        self.results[strategy_name].append(score)
        
        return {"strategy": strategy_name, "output": output, "score": score}
    
    def get_summary(self) -> dict:
        return {
            name: {
                "mean_score": sum(scores) / len(scores) if scores else 0,
                "n_samples": len(scores)
            }
            for name, scores in self.results.items()
        }

73.6.3 示例库的维护

生产环境中的示例库需要持续维护:


小结

Few-Shot 和 Chain-of-Thought 是 Prompt 工程中经过最多实证研究支撑的两类技术。Few-Shot 的核心在于高质量示例的选择与格式一致性;CoT 的核心在于将推理过程显式化,为模型提供"草稿纸"。两者结合形成的 Few-Shot CoT 是复杂推理任务的黄金标准。

从工程角度看,示例库的构建、动态检索、格式规范化、以及持续的 A/B 测试构成了一套完整的 Few-Shot 工程实践体系。理解这些技术的工作原理不仅帮助工程师做出更好的设计决策,也能在出现问题时快速定位根因。

本章评分
4.7  / 5  (3 评分)

💬 留言讨论