第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 管理体系:
- 模板仓库:所有 Prompt 模板存放在 Git 仓库
prompts/目录,按业务线分目录 - 配置中心:变量配置存 YAML 文件,与代码解耦
- 示例库:Few-shot 示例统一管理,标注来源与有效性
- 版本控制:每次修改 Prompt 必须提 PR,附 A/B 测试结果
- 评审机制: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)。建议:简单示例在前,复杂/边界示例在后;同类示例不要连续排列。
面试题¶
-
简答题:Zero-shot、One-shot、Few-shot 的区别是什么?各自适用什么场景?
-
原理题:为什么 Few-shot 比 Zero-shot 更稳定?请从 in-context learning 角度解释。
-
设计题:设计一个 Few-shot Prompt,用于把自然语言转换为 SQL。需要哪些示例?如何选择?
-
实践题:Jinja2 模板中如何实现"根据用户语言动态加载不同 Few-shot 示例"?写出模板代码。
参考要点:使用
{% if lang == 'zh' %}...{% elif lang == 'en' %}...{% endif %}条件分支。
- 分析题:以下 Few-shot 示例选择有什么问题?
评论:好 → 正面 评论:很好 → 正面 评论:非常好 → 正面参考要点:示例过于相似、只覆盖正面、缺少负/中性、无边界情况、模型易过拟合。
练习题¶
-
对比实验:对同一批 20 条评论,分别用 Zero-shot 和 Few-shot 分类,统计准确率与格式稳定性差异。
-
模板开发:用 Jinja2 实现一个"多语言客服"Prompt 模板,支持中/英/日三种语言的 Few-shot 示例切换。
-
示例库建设:为一个"简历信息提取"任务构建 Few-shot 示例库,至少包含 5 个示例,覆盖不同字段(姓名、电话、技能、教育等)。
-
配置化管理:将第1章的客服 Prompt 改造为 Jinja2 模板 + YAML 配置的形式,实现业务参数与模板分离。
小结¶
Few-shot 与模板是 Prompt 工程从"手工作坊"走向"工程化"的关键一步。本章核心要点:
- 示例比规则有效:Few-shot 通过模式示范稳定输出,3-5 个高质量示例通常最优。
- 示例选择有讲究:代表性、多样性、难度递进、标签平衡,缺一不可。
- 模板化是基础:用 Jinja2 将 Prompt 抽象为"模板 + 变量",实现复用。
- 配置化管理:模板、变量、示例库分离存储,纳入版本控制,建立评审机制。
- 避免过拟合:定期用测试集评估,清理失效示例,保证泛化能力。
下一章将学习结构化 Prompt,让模型输出可直接被程序解析的 JSON/XML 格式,打通 LLM 与业务系统的数据通道。