3.4 Prompt 模板与变量化
承前
在上一节中,我们深入探讨了 System Prompt 的设计原则。我们学会了如何通过角色设定、行为约束、输出格式规约等手段,为模型构建一个稳定的"人格"与"工作环境"。System Prompt 解决的是"模型应该成为什么样的人"的问题,而在实际开发中,我们还会面临另一个工程挑战:如何让 Prompt 在不同场景下高效复用、灵活组装、可追溯地迭代。这就是本节要讨论的 Prompt 模板化与变量化。
3.4.1 从硬编码到模板化:为什么需要 Prompt 模板?
在开发 AI 应用时,很多开发者最直觉的做法是直接在代码中拼接 Prompt 字符串:
# ❌ 硬编码 —— 难以维护
prompt = "你是一个客服助手。请回答用户问题:" + user_question这种方式在原型验证阶段看似快速高效,但随着项目规模增长,一系列问题会逐渐暴露:
- 不可复用:每个业务场景都要重新编写一遍 Prompt,大量重复劳动。
- 难以测试:Prompt 和业务逻辑耦合在一起,无法独立验证 Prompt 质量。
- 版本混乱:修改一次 Prompt 就要改代码、走测试、重新部署,迭代成本极高。
- 团队协作差:非技术人员(如产品经理、领域专家)无法参与 Prompt 优化,因为 Prompt 被埋没在代码里。
为了理解模板化的价值,我们可以用一个生活中的类比来帮助理解。
信件模板类比。想象一下,你在办公室工作,每天需要向不同客户发送催款邮件。你当然可以每次都从零开始写——但更合理的做法是:先设计一个通用模板,预留出"客户姓名""欠款金额""截止日期"等可变字段,每次发信时只需填入对应的变量值即可。更进一步,如果客户是 VIP,模板会自动加上一段致谢语;如果客户在海外,模板会切换为英文版本。
Prompt 模板化做的是完全一样的事情。我们将 Prompt 中固定不变的结构作为模板骨架,将随场景变化的动态内容抽象为变量,用专门的模板引擎来渲染最终文本。核心价值链如下:
编写一次 → 复用多次 → 独立测试 → 版本管理 → A/B测试 → 持续优化核心思想:将 Prompt 从代码中分离出来,作为独立的、可管理的"资产"来对待。
3.4.2 f-string 模板化:最轻量的入门方案
Python 3.6+ 引入的 f-string 是最简单的字符串格式化方式,零依赖、语法直观,适合快速原型开发和简单场景。
基础用法:
# 定义模板:用花括号 {} 标记变量位置
template = """
你是一个{role},请根据以下信息完成任务:
任务:{task_description}
输入:{user_input}
要求:{requirements}
"""
# 渲染使用:传入变量值,生成最终 Prompt
prompt = template.format(
role="资深Python代码审查员",
task_description="审查代码安全性",
user_input="""
def login(username, password):
query = f"SELECT * FROM users WHERE name='{username}' AND pwd='{password}'"
return db.execute(query)
""",
requirements="重点检查SQL注入、XSS等安全漏洞"
)f-string 的优缺点可以归纳如下:
| 优点 | 缺点 |
|---|---|
| 零依赖,Python 内置 | 不支持条件判断、循环等复杂逻辑 |
| 语法简单,学习成本低 | 难以处理嵌套数据结构 |
| 适合快速原型 | 缺少安全防护,用户输入可能注入花括号 |
在实际项目中,一个推荐的做法是将模板集中存储在独立的配置文件或字典中,与业务代码分离:
# 将所有 Prompt 模板集中管理在同一个字典中
PROMPT_TEMPLATES = {
"code_review": """
你是一个{role},请审查以下代码:
【代码】
```{language}
{code}【审查重点】
请以{format}格式输出审查结果。 """, "translation": """ 将以下{source_lang}文本翻译为{target_lang}:
【原文】
要求:
- 保持{style}风格
- {domain_constraint} """, "summarization": """ 请用不超过{max_words}字总结以下内容,保留{key_points}个关键要点:
【原文】 {content} """ }
渲染函数:根据模板名称和参数生成 Prompt
def render_prompt(template_name, **kwargs): template = PROMPT_TEMPLATES[template_name] # 取出模板字符串 return template.format(**kwargs) # 用传入的参数填充变量
这种集中管理的模式让 Prompt 的维护变得清晰——所有模板都在一处,修改时一目了然。
---
#### 3.4.3 Jinja2 高级模板化:面向生产环境
当 Prompt 的复杂度上升——需要根据条件动态切换内容、遍历列表生成多条规则、复用通用的格式片段——f-string 就力不从心了。这时,Python 生态中最成熟的模板引擎 **Jinja2** 成为了生产环境的首选。
Jinja2 在 Prompt 工程中的核心能力包括:条件渲染(if/else)、循环遍历(for)、宏定义(可复用的模板片段)、过滤器(对变量进行格式化处理)。
##### 条件渲染
条件渲染让一个模板能根据不同参数"自动变形",就像信件模板根据客户类型切换措辞:
```python
from jinja2 import Template # 从 jinja2 导入 Template 类
template = Template("""
你是一个{{ role }}助手。
{% if language == "zh" %}
请用中文回答,语气友好亲切。
{% elif language == "en" %}
Please respond in English with a professional tone.
{% else %}
请根据用户的语言偏好回答。
{% endif %}
{% if debug_mode %}
【调试信息】
模型:{{ model_name }}
温度:{{ temperature }}
{% endif %}
用户问题:{{ question }}
""")
# 渲染:传入所有变量,Jinja2 会根据条件自动选择分支
prompt = template.render(
role="技术",
language="zh",
debug_mode=True,
model_name="gpt-4",
temperature=0.7,
question="如何优化数据库查询性能?"
)循环与列表处理
当需要在 Prompt 中嵌入多条规则或历史对话时,循环是最自然的表达方式:
template = Template("""
你是一个代码审查助手。请根据以下规则审查代码:
{% for rule in rules %}
{{ loop.index }}. {{ rule.name }}:{{ rule.description }}
{% endfor %}
历史对话记录:
{% for msg in history %}
[{{ msg.role }}]: {{ msg.content[:100] }}{% if msg.content|length > 100 %}...{% endif %}
{% endfor %}
请审查以下代码:
```{{ language }}
{{ code }}
~~~
""")
# 传入列表数据,Jinja2 自动遍历渲染
prompt = template.render(
rules=[
{"name": "安全检查", "description": "检查SQL注入、XSS等安全漏洞"},
{"name": "性能检查", "description": "检查N+1查询、不必要循环等"},
{"name": "风格检查", "description": "检查是否符合PEP 8规范"},
],
history=[
{"role": "user", "content": "帮我审查这段代码"},
{"role": "assistant", "content": "好的,请提供代码"},
],
language="python",
code="def foo():\n pass"
)宏定义与复用
宏类似于编程语言中的函数——定义一次,多次调用:
template = Template("""
{% macro code_block(code, language="python") %}
```{{ language }}
{{ code | trim }}
~~~
{% endmacro %}
{% macro example(title, input, output) %}
**示例 - {{ title }}**
输入:{{ input }}
输出:{{ output }}
{% endmacro %}
你是一个{{ role }}。
{{ example("情感分析", "这个产品太棒了!", "正面") }}
{{ example("情感分析", "用了两天就坏了,垃圾!", "负面") }}
现在请分析以下文本:
{{ code_block(user_text) }}
""")过滤器
过滤器用于对变量进行格式化处理,类似于数据处理管道:
template = Template("""
请将以下文本翻译为{{ target_lang }}:
原文:
{{ text | trim | replace('\\n', ' ') }}
{% if glossary %}
术语表:
{% for term, translation in glossary.items() %}
- {{ term }} -> {{ translation }}
{% endfor %}
{% endif %}
要求:字数不超过{{ text | wordcount * 1.5 | int }}字
""")f-string 还是 Jinja2? 可以参考以下决策树:
需要条件/循环/过滤?
├── 是 → Jinja2
└── 否 → 是否需要复杂嵌套?
├── 是 → Jinja2
└── 否 → 是否需要团队协作?
├── 是 → Jinja2(模板文件独立管理)
└── 否 → f-string(简单快捷)3.4.4 Chat Template:多模型兼容的通用格式
随着开源大模型生态的繁荣,我们经常需要在不同的模型之间切换。而不同的模型使用不同的对话格式——OpenAI 用 JSON 数组,Llama 用特殊标签,ChatML 用另一套标签。手动适配这些格式既繁琐又容易出错。
Chat Template 是 Hugging Face Transformers 库引入的解决方案:它用一个 Jinja2 模板将结构化的消息列表(system/user/assistant)自动转换为目标模型期望的字符串格式。
from transformers import AutoTokenizer # 导入分词器
# 加载模型对应的分词器
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3-8B-Instruct")
messages = [
{"role": "system", "content": "你是一个代码助手"},
{"role": "user", "content": "写一个快速排序函数"}
]
# 自动转换为 Llama-3 期望的格式
formatted_prompt = tokenizer.apply_chat_template(
messages,
tokenize=False, # 返回字符串而非 token id
add_generation_prompt=True # 末尾添加助手回复的起始标记
)
print(formatted_prompt)
# 输出: <|begin_of_text|><|start_header_id|>system<|end_header_id|>
# 你是一个代码助手<|eot_id|>...你也可以自定义 Chat Template,以适配非标准模型或自定义格式:
# 查看模型自带的 Chat Template
print(tokenizer.chat_template)
# 自定义一个简洁的中文格式模板
custom_template = """
{% for message in messages %}
{% if message['role'] == 'system' %}
[系统] {{ message['content'] }}
{% elif message['role'] == 'user' %}
[用户] {{ message['content'] }}
{% elif message['role'] == 'assistant' %}
[助手] {{ message['content'] }}
{% endif %}
{% endfor %}
[助手]
"""
tokenizer.chat_template = custom_template # 替换为自定义模板3.4.5 Prompt 版本管理
Prompt 不是写完就一劳永逸的——它需要持续迭代、对比效果、必要时回滚。这就要求我们像管理代码一样管理 Prompt 版本。
版本管理最佳实践包含四个方面:
- 文件命名规范:使用语义化版本号,如
code_review_v1.2.0.py - Git 管理:将模板文件纳入版本控制,每次修改附带清晰的 commit message
- 变更日志:记录每次修改的原因、预期效果和实际表现
- 元数据标注:每个 Prompt 标注作者、日期、适用模型、评估分数
# prompts/v1/code_review.py
"""
Prompt 版本: 1.0.0
创建日期: 2025-07-01
作者: 张三
适用模型: GPT-4, Claude 3.5 Sonnet
评估分数: 准确率 92%, 用户满意度 4.3/5
变更记录:
- 1.0.0: 初始版本
- 1.0.1: 优化了输出格式要求
- 1.1.0: 增加了语言特定的检查规则
"""
CODE_REVIEW_V1 = "你是一个专业的代码审查助手,版本 v1.1.0。..."
# prompts/v2/code_review.py
CODE_REVIEW_V2 = "你是一个资深代码审查专家,版本 v2.0.0。..."
# 使用时通过版本号选择
from prompts import CODE_REVIEW_V1, CODE_REVIEW_V2
def get_prompt(version="v2"):
prompts = {"v1": CODE_REVIEW_V1, "v2": CODE_REVIEW_V2}
return prompts.get(version, CODE_REVIEW_V2)更进一步,可以用 dataclass 将版本元数据结构化:
from dataclasses import dataclass
from datetime import datetime
@dataclass
class PromptVersion:
name: str # Prompt 名称
version: str # 语义化版本号
template: str # 模板内容
author: str # 作者
created_at: datetime # 创建时间
model_target: list # 适用的模型列表
metrics: dict # 评估指标
changelog: list # 变更记录
def render(self, **kwargs):
from jinja2 import Template
return Template(self.template).render(**kwargs)
# 使用
code_review_v1 = PromptVersion(
name="code_review",
version="1.0.0",
template="你是一个{{ role }}...",
author="张三",
created_at=datetime.now(),
model_target=["gpt-4", "claude-3.5-sonnet"],
metrics={"accuracy": 0.92, "user_satisfaction": 4.3},
changelog=["初始版本", "优化输出格式"]
)3.4.6 A/B 测试:用数据驱动 Prompt 优化
当有两个候选 Prompt 版本时,凭直觉判断哪个更好是不可靠的。更科学的做法是 A/B 测试:将一部分请求分配给版本 A,另一部分分配给版本 B,收集指标数据后做统计判断。
import hashlib
from dataclasses import dataclass, field
from typing import Dict, List
@dataclass
class PromptExperiment:
"""Prompt A/B 测试实验"""
name: str # 实验名称
variants: Dict[str, str] # {"A": prompt_a, "B": prompt_b}
traffic_split: Dict[str, float] = field(
default_factory=lambda: {"A": 0.5, "B": 0.5}
)
results: Dict[str, List[dict]] = field(
default_factory=lambda: {"A": [], "B": []}
)
def assign_variant(self, user_id: str) -> str:
"""根据用户 ID 的哈希值分配实验组
哈希保证同一用户始终看到同一版本"""
hash_val = int(hashlib.md5(user_id.encode()).hexdigest(), 16)
normalized = (hash_val % 10000) / 10000.0 # 归一化到 0~1
cumulative = 0
for variant, ratio in self.traffic_split.items():
cumulative += ratio
if normalized <= cumulative:
return variant
return list(self.traffic_split.keys())[-1]
def record_result(self, variant: str, metrics: dict):
"""记录一次请求的结果指标"""
self.results[variant].append(metrics)
def get_prompt(self, user_id: str) -> str:
"""获取用户对应的 Prompt 版本"""
variant = self.assign_variant(user_id)
return self.variants[variant], variant
# 使用示例
experiment = PromptExperiment(
name="代码审查Prompt优化",
variants={
"A": "你是一个代码审查助手。请审查以下代码:{code}",
"B": """你是一个资深代码审查专家,拥有10年经验。
请从以下维度审查代码:
1. 安全性 2. 性能 3. 可读性 4. 最佳实践
代码:{code}"""
},
traffic_split={"A": 0.5, "B": 0.5}
)
# 为不同用户分配不同版本
for user_id in ["user_001", "user_002", "user_003"]:
prompt, variant = experiment.get_prompt(user_id)
print(f"用户 {user_id} -> 版本 {variant}")A/B 测试的关键是:样本量要足够大,否则统计结果不可靠。一般建议每组至少收集数百次请求的结果后再做决策。