跳转至

第2章 Few-shot 与模板

学习目标

  • 理解 Few-shot 学习原理及其与 Zero-shot、One-shot 的差异
  • 掌握 Prompt 模板设计方法与变量替换机制
  • 学会使用 Jinja2 构建可复用的 Prompt 模板
  • 掌握模板配置化管理与版本控制实践
  • 能够对比零/一/多例场景下的输出效果

问题导入

某团队开发了一个文本分类 Agent,需要把用户评论分为"正面/负面/中性"。最初他们用 Zero-shot 方式:

请判断以下评论的情感倾向(正面/负面/中性):
评论:这个手机拍照很清晰,但是电池不耐用。

模型有时返回"正面",有时返回"中性",甚至返回一段分析而不是标签,输出极不稳定。工程师尝试在 Prompt 里加很多形容词约束,效果仍不理想。

后来他们改为 Few-shot 方式,在 Prompt 中给出 3 个示例:

评论:屏幕显示效果好。 → 正面
评论:信号差,经常断线。 → 负面
评论:价格 2999 元,配 8GB 内存。 → 中性

输出立刻稳定为单个标签。这就是 Few-shot 的威力——示例比规则更有效。但随之而来的新问题是:示例如何管理?不同业务线如何复用?本章解决这些问题。

核心概念

1. Few-shot 示范原理

Few-shot Learning(少样本学习)指在 Prompt 中提供少量"输入-输出"示例,让模型通过模式识别理解任务格式与逻辑。其原理基于 LLM 在预训练阶段习得的"in-context learning"能力:

  • 格式锚定:示例固定输出格式,减少格式漂移
  • 模式迁移:示例展示任务模式,模型迁移到新输入
  • 边界澄清:正反例对比,明确分类边界

2. Zero-shot / One-shot / Few-shot 对比

方式 示例数 适用场景 优点 缺点
Zero-shot 0 简单任务、通用问答 Token 省、响应快 输出不稳定
One-shot 1 格式固定任务 轻量锚定格式 模式单一
Few-shot 2-10 复杂分类、格式严格 输出稳定、可控 占用 Token、需选例

经验法则:任务越复杂、输出格式要求越严,所需示例越多。但超过 10 个示例收益递减,反而稀释注意力。

3. 示例选择策略

  • 代表性:覆盖各类典型情况(正/负/中性各一)
  • 多样性:避免示例过于相似导致模型过拟合
  • 难度递进:简单示例在前,边界情况在后
  • 标签平衡:分类任务各类别示例数均衡

4. Prompt 模板设计

把 Prompt 抽象为"模板 + 变量",实现复用与配置化:

模板:你是一名{role}。请将以下{src_lang}翻译为{tgt_lang}:
{user_input}
变量:role=专业译员, src_lang=中文, tgt_lang=英文, user_input=...

5. Jinja2 模板引擎

Jinja2 是 Python 生态主流模板引擎,支持变量替换、条件判断、循环:

  • {{ variable }}:变量替换
  • {% if condition %}...{% endif %}:条件分支
  • {% for item in list %}...{% endfor %}:循环

6. 模板配置化管理

生产环境应将 Prompt 模板与代码解耦:

  • 模板存储为 .j2 或 .yaml 文件
  • 按业务线/版本分目录管理
  • 通过模板加载器动态读取
  • 支持版本控制与回滚

原理图

flowchart LR
    A[用户输入] --> B[模板引擎]
    C[模板文件 .j2] --> B
    D[变量配置 config.yaml] --> B
    E[Few-shot 示例库] --> B

    B --> F[渲染后 Prompt]
    F --> G[LLM 推理]
    G --> H[结构化输出]

    E --> E1[示例1 输入→输出]
    E --> E2[示例2 输入→输出]
    E --> E3[示例N 输入→输出]

    style B fill:#fff9c4
    style F fill:#c8e6c9
    style G fill:#bbdefb
    style H fill:#c8e6c9

模板引擎将模板文件、变量配置、Few-shot 示例库组合渲染为最终 Prompt,送入 LLM 推理。

示例代码

1. Few-shot 与 Zero-shot 效果对比

"""
第2章示例:Few-shot 与 Zero-shot 效果对比
"""
from openai import OpenAI

client = OpenAI()


def classify_zero_shot(text: str) -> str:
    """Zero-shot:无示例,输出不稳定"""
    prompt = f"请判断以下评论的情感倾向(正面/负面/中性),只返回标签:\n{text}"
    return _call(prompt)


def classify_few_shot(text: str) -> str:
    """Few-shot:提供示例,输出稳定"""
    prompt = """请判断评论的情感倾向,只返回标签(正面/负面/中性)。

示例:
评论:屏幕显示效果好。 → 正面
评论:信号差,经常断线。 → 负面
评论:价格 2999 元,配 8GB 内存。 → 中性

现在请判断:
评论:{text} →""".format(text=text)
    return _call(prompt)


def _call(prompt: str) -> str:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
    )
    return resp.choices[0].message.content


if __name__ == "__main__":
    test = "这个手机拍照很清晰,但是电池不耐用。"
    print("Zero-shot:", classify_zero_shot(test))
    print("Few-shot:", classify_few_shot(test))

2. Jinja2 模板系统实现

"""
基于 Jinja2 的 Prompt 模板系统
"""
from jinja2 import Environment, FileSystemLoader, select_autoescape
from openai import OpenAI
import yaml
from pathlib import Path

client = OpenAI()


class PromptTemplateManager:
    """Prompt 模板管理器:加载、渲染、调用"""

    def __init__(self, template_dir: str = "prompts"):
        self.env = Environment(
            loader=FileSystemLoader(template_dir),
            autoescape=select_autoescape(default=False),
            trim_blocks=True,
            lstrip_blocks=True,
        )

    def render(self, template_name: str, **variables) -> str:
        """渲染模板"""
        tpl = self.env.get_template(template_name)
        return tpl.render(**variables)

    def call(self, template_name: str, user_input: str, **variables) -> str:
        """渲染并调用 LLM"""
        prompt = self.render(template_name, user_input=user_input, **variables)
        resp = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[{"role": "user", "content": prompt}],
            temperature=0,
        )
        return resp.choices[0].message.content


# ========== 模板文件示例 prompts/translate.j2 ==========
# 你是一名{{ role }}。请将以下{{ src_lang }}翻译为{{ tgt_lang }}。
# 只输出译文,不要解释。
#
# 示例:
# {{ src_lang }}:你好世界 → {{ tgt_lang }}:Hello World
#
# 现在请翻译:
# {{ src_lang }}:{{ user_input }} → {{ tgt_lang }}:

# ========== 配置文件示例 prompts/config.yaml ==========
# translate:
#   role: 专业译员
#   src_lang: 中文
#   tgt_lang: 英文

class ConfigurablePromptManager:
    """配置化 Prompt 管理:模板 + 配置分离"""

    def __init__(self, template_dir: str, config_path: str):
        self.tpl_manager = PromptTemplateManager(template_dir)
        self.config = self._load_config(config_path)

    def _load_config(self, path: str) -> dict:
        with open(path, "r", encoding="utf-8") as f:
            return yaml.safe_load(f)

    def call_with_config(self, template_name: str, user_input: str,
                         config_key: str) -> str:
        """根据配置键调用模板"""
        variables = self.config[config_key]
        return self.tpl_manager.call(template_name, user_input, **variables)


if __name__ == "__main__":
    manager = PromptTemplateManager("prompts")
    result = manager.call(
        "translate.j2",
        user_input="今天天气真好",
        role="专业译员",
        src_lang="中文",
        tgt_lang="英文",
    )
    print(result)

3. Few-shot 示例库管理

"""
Few-shot 示例库:按业务线分类管理示例
"""
from dataclasses import dataclass


@dataclass
class FewShotExample:
    input: str
    output: str

    def render(self) -> str:
        return f"输入:{self.input}\n输出:{self.output}"


# 示例库:按任务分类
EXAMPLE_BANK = {
    "sentiment": [
        FewShotExample("屏幕显示效果好", "正面"),
        FewShotExample("信号差,经常断线", "负面"),
        FewShotExample("价格 2999 元,配 8GB 内存", "中性"),
    ],
    "intent": [
        FewShotExample("我想退款", "退换货"),
        FewShotExample("我的订单到哪了", "物流查询"),
        FewShotExample("这个手机有什么颜色", "商品咨询"),
    ],
}


def build_few_shot_prompt(task: str, user_input: str, n: int = 3) -> str:
    """动态构建 Few-shot Prompt"""
    examples = EXAMPLE_BANK.get(task, [])[:n]
    example_text = "\n".join(ex.render() for ex in examples)
    return f"""请根据示例判断任务类型,只返回标签。

{example_text}

输入:{user_input}
输出:"""

企业案例

标准化 Prompt 管理

某金融科技公司有 20+ 个 Agent,分别处理信贷咨询、风险评估、合规审查等任务。早期每个开发者各自维护 Prompt,导致:

  • 同一任务不同 Agent 的 Prompt 风格不一致
  • Prompt 修改后无法回滚,线上事故频发
  • 新人接手困难,不知道每个 Prompt 的设计意图

他们建立了标准化 Prompt 管理体系:

  1. 模板仓库:所有 Prompt 模板存放在 Git 仓库 prompts/ 目录,按业务线分目录
  2. 配置中心:变量配置存 YAML 文件,与代码解耦
  3. 示例库:Few-shot 示例统一管理,标注来源与有效性
  4. 版本控制:每次修改 Prompt 必须提 PR,附 A/B 测试结果
  5. 评审机制:Prompt 变更需经过业务 + 技术双评审

上线该体系后,Prompt 相关线上事故下降 85%,新人接手时间从 2 周缩短到 3 天。关键经验:

  • Prompt 是"代码资产",不是"文档",需纳入版本控制
  • 模板与配置分离,便于非技术人员调整业务参数
  • Few-shot 示例需定期清理失效样本

常见问题

Q1:Few-shot 示例给几个最好?

A:通常 3-5 个。分类任务建议每类 1-2 个;生成任务建议 2-3 个高质量示例。超过 10 个收益递减,且占用 Token。关键是示例的"代表性"而非"数量"。

Q2:示例是否会让模型过拟合?

A:会。如果示例过于相似或存在偏见,模型会对示例模式过拟合,无法泛化。解决:保证示例多样性,覆盖边界情况,定期用测试集评估。

Q3:Jinja2 模板和 Python f-string 有什么区别?

A:f-string 适合简单场景,但 Jinja2 支持:①条件分支(if);②循环(for);③模板继承与复用;④文件分离与配置化。生产环境推荐 Jinja2。

Q4:Few-shot 示例的顺序会影响结果吗?

A:会。模型对首尾位置更敏感(recency bias)。建议:简单示例在前,复杂/边界示例在后;同类示例不要连续排列。

面试题

  1. 简答题:Zero-shot、One-shot、Few-shot 的区别是什么?各自适用什么场景?

  2. 原理题:为什么 Few-shot 比 Zero-shot 更稳定?请从 in-context learning 角度解释。

  3. 设计题:设计一个 Few-shot Prompt,用于把自然语言转换为 SQL。需要哪些示例?如何选择?

  4. 实践题:Jinja2 模板中如何实现"根据用户语言动态加载不同 Few-shot 示例"?写出模板代码。

参考要点:使用 {% if lang == 'zh' %}...{% elif lang == 'en' %}...{% endif %} 条件分支。

  1. 分析题:以下 Few-shot 示例选择有什么问题?
    评论:好 → 正面
    评论:很好 → 正面
    评论:非常好 → 正面
    

    参考要点:示例过于相似、只覆盖正面、缺少负/中性、无边界情况、模型易过拟合。

练习题

  1. 对比实验:对同一批 20 条评论,分别用 Zero-shot 和 Few-shot 分类,统计准确率与格式稳定性差异。

  2. 模板开发:用 Jinja2 实现一个"多语言客服"Prompt 模板,支持中/英/日三种语言的 Few-shot 示例切换。

  3. 示例库建设:为一个"简历信息提取"任务构建 Few-shot 示例库,至少包含 5 个示例,覆盖不同字段(姓名、电话、技能、教育等)。

  4. 配置化管理:将第1章的客服 Prompt 改造为 Jinja2 模板 + YAML 配置的形式,实现业务参数与模板分离。

小结

Few-shot 与模板是 Prompt 工程从"手工作坊"走向"工程化"的关键一步。本章核心要点:

  • 示例比规则有效:Few-shot 通过模式示范稳定输出,3-5 个高质量示例通常最优。
  • 示例选择有讲究:代表性、多样性、难度递进、标签平衡,缺一不可。
  • 模板化是基础:用 Jinja2 将 Prompt 抽象为"模板 + 变量",实现复用。
  • 配置化管理:模板、变量、示例库分离存储,纳入版本控制,建立评审机制。
  • 避免过拟合:定期用测试集评估,清理失效示例,保证泛化能力。

下一章将学习结构化 Prompt,让模型输出可直接被程序解析的 JSON/XML 格式,打通 LLM 与业务系统的数据通道。