前言
在上一篇我们搭建了一个 AI 对话应用的基础骨架。这一篇聚焦一个更关键的问题:如何让模型的输出稳定可靠?
很多人以为写 Prompt 就是"把问题说清楚",但在生产环境中,你需要的是一个可测试、可版本管理、可 A/B 对比的 Prompt 系统。而不是散落在代码各处的字符串拼接。
1. Prompt 的原子组件
任何 Prompt 都可以拆解为四个基础组件。理解它们,你就掌握了设计任意 Prompt 的能力。
┌─────────────────────────────────────────────────┐
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 角色 │ │ 指令 │ │ 上下文 │ │
│ │ (WHO) │→│ (WHAT) │→│ (INFO) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────┐ │
│ │ ┌──────────┐ ┌────────────────────────┐ │ │
│ │ │ 示例 │ │ 输出格式 │ │ │
│ │ │ (DEMO) │→│ (FORMAT) │ │ │
│ │ └──────────┘ └────────────────────────┘ │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
组件 1:角色(WHO)
角色不只是"你是一个助手",而是能力边界 + 行为规范:
# 差的角色定义
"你是一个 AI 助手。"
# 好的角色定义
"你是一个资深的 Python 后端工程师,精通 FastAPI 和 SQLAlchemy。
你习惯写出有类型注解、有单元测试、遵循 PEP 8 的代码。
你的回答应该简洁务实,只有在必要时才给出完整代码。
不要替用户做决定,而是列出选项并说明利弊。"
好的角色定义让模型输出质量提升一个档次——它激活了预训练中对应的能力分布。
组件 2:指令(WHAT)
指令要遵循单一职责原则,一条指令只做一件事:
# 差的指令(一次要求太多)
"分析这段代码,找出 bug,优化性能,然后重写它,再写测试。"
# 好的指令(分步)
"请按以下步骤分析这段代码:
Step 1: 找出所有潜在的 bug,按严重程度排序
Step 2: 分析性能瓶颈
Step 3: 给出修复建议(不需要重写代码)"
组件 3:上下文(INFO)
上下文决定了模型回答的准确性。越具体越好:
# 差的上下文
"帮我写一个用户注册接口。"
# 好的上下文
"项目背景:电商后台管理系统
技术栈:FastAPI + SQLAlchemy + PostgreSQL
用户角色:普通用户,用邮箱注册
注册流程:邮箱验证 → 设置密码 → 完善个人信息
关键约束:密码至少 8 位含大小写字母和数字,邮箱唯一"
组件 4:输出格式(FORMAT)
永远明确告诉模型你想要的输出格式:
# 差的格式要求
"分析这段文本的情感。"
# 好的格式要求
"分析这段文本的情感,按以下 JSON 格式输出:
{
"sentiment": "positive | negative | neutral | mixed",
"confidence": 0.0 ~ 1.0,
"aspects": [
{"entity": "电池", "sentiment": "负面", "detail": "续航时间短"}
]
}
不要输出任何其他文字,只输出 JSON。"
2. 结构化模板设计模式
有了原子组件,下一步是组件组装。以下是经过线上验证的三种模式。
模式 1:Markdown 模板
适合多维度分析任务,用 Markdown 标题自然分隔:
# 角色
你是一个资深代码审查员,擅长发现安全漏洞和性能问题。
# 任务
审查以下代码,从三个维度分析:
1. 安全性:是否存在注入、XSS、CSRF 等风险?
2. 性能:有没有 N+1 查询、内存泄漏等隐患?
3. 可维护性:命名、结构、注释是否合理?
# 代码
```python
def get_users():
return db.query("SELECT * FROM users")
输出格式
每个维度给出:
- 严重程度:高/中/低
- 问题描述
- 修复建议
### 模式 2:XML 标签模板
适合需要精确分隔不同类型内容的场景。LLM 对 XML 结构有天然的理解能力,比 Markdown 更可靠:
```xml
你是一个翻译助手。将用户输入从英文翻译为中文。
要求:技术文档风格,术语准确,保持原文格式。
Transformer architecture enables parallel processing
Transformer 架构实现了并行处理
Transformer 是专有名词不翻译,architecture 译为"架构"
{user_text}
模式 3:JSON 结构化模板
适合需要程序化解析的场景,Prompt 本身也是 JSON:
{
"system": "你是一个情感分析引擎。",
"instruction": "分析用户评论的情感倾向和关键实体。",
"constraints": [
"只输出 JSON,不要任何说明文字",
"负面情感必须包含具体原因"
],
"output_schema": {
"sentiment": "string: positive|negative|neutral",
"entities": ["array of {name, sentiment, detail}"]
},
"input": "{user_review}"
}
3. Few-shot 与 CoT 最佳实践
Few-shot 的设计原则
Few-shot 不是随便举几个例子就行。以下是关键原则:
原则 1:覆盖正向、负向、边界
示例 1(正向):
输入:"这个产品太棒了,我每天都在用"
输出:{"sentiment": "positive", "score": 0.95}
示例 2(负向):
输入:"客服态度很差,等了半小时没人理"
输出:{"sentiment": "negative", "score": 0.88}
示例 3(边界-中性):
输入:"还行吧,一般般"
输出:{"sentiment": "neutral", "score": 0.45}
示例 4(边界-混合):
输入:"产品不错但价格太贵了"
输出:{"sentiment": "mixed", "score": 0.55}
每个边界情况都有示例,模型就不会把"还行吧"误判为 positive。
原则 2:示例格式必须与预期输出完全一致
如果输出是 JSON,示例也必须是 JSON。如果输出是 Markdown,示例也要用 Markdown。模型会模仿格式而不是只模仿内容。
原则 3:2-5 个示例效果最好
| 示例数 | 效果 | 说明 |
|---|---|---|
| 0 | 差 | 纯 Zero-shot,不稳定 |
| 1-2 | 一般 | 能理解任务,但边界模糊 |
| 3-5 | 最佳 | 覆盖主要情况,准确率最高 |
| >5 | 递减 | 边际效益递减,浪费 token |
Chain-of-Thought(思维链)
Zero-shot CoT——最简单但最有效的方法:在 prompt 末尾加"请逐步思考"。
问题:一个长方形的长是 12 米,宽是 8 米,求面积。
请逐步思考。
→ 模型输出:
1. 长方形面积公式:面积 = 长 × 宽
2. 面积 = 12 × 8 = 96
3. 所以面积是 96 平方米。
就这么简单。在 MATH 数据集上,Zero-shot CoT 将准确率从 37% 提升到 78%。
Few-shot CoT——给出完整的推理步骤示例:
问:小明有 5 个苹果,小红给了小明 3 个,小明又吃了 2 个,还剩几个?
答:小明原来有 5 个。小红给了 3 个后,有 5 + 3 = 8 个。
吃了 2 个后,剩下 8 - 2 = 6 个。所以答案是 6。
问:书店有 120 本书,卖了 45 本,又进了 30 本,现在有多少本?
答:
结构化 CoT——让思维链以结构化方式输出:
{
"reasoning_steps": [
"步骤1:理解问题——求剩余书本数",
"步骤2:初始数量 = 120",
"步骤3:卖出 45 本 → 120 - 45 = 75",
"步骤4:进货 30 本 → 75 + 30 = 105"
],
"final_answer": 105,
"verification": "反向验证:105 + 45 - 30 = 120 ✓"
}
这比自由文本的思维链更好——因为每一步都可追踪、可调试。
4. 生产级 Prompt 管理
问题
当你有几十条 Prompt 时,会面临这些痛苦:
- Prompt 散落在代码里,改一个要到处搜索
- 分不清"翻译不对"是 Prompt 问题还是模型问题
- 新 Prompt 上线了,效果是变好还是变差了?
解决方案:Prompt 版本管理
# prompts/__init__.py
# 所有 Prompt 集中管理,支持版本切换和 A/B 测试。
import hashlib
import json
from typing import Optional, Any
class PromptTemplate:
# Prompt 模板,支持版本管理和变量注入。
def __init__(self, name: str, version: str, template: dict):
self.name = name
self.version = version
self.template = template # 结构化 Prompt 字典
def render(self, **kwargs) -> list[dict]:
# 渲染为 messages 格式。
rendered = []
for key in ["system", "context", "examples", "instruction"]:
if key in self.template:
content = self.template[key]
# 变量替换
for k, v in kwargs.items():
content = content.replace(f"{{{k}}}", str(v))
if key == "system":
rendered.append({"role": "system", "content": content})
# 其他 key 合并到 user message
# 构建最终的 user message
user_parts = []
for key in ["context", "examples", "instruction"]:
if key in self.template:
user_parts.append(self.template[key])
user_content = "
".join(user_parts)
for k, v in kwargs.items():
user_content = user_content.replace(f"{{{k}}}", str(v))
rendered.append({"role": "user", "content": user_content})
return rendered
class PromptRegistry:
# Prompt 注册中心,管理所有 Prompt 的版本。
def __init__(self):
self._prompts: dict[str, dict[str, PromptTemplate]] = {}
def register(self, name: str, version: str, template: dict):
if name not in self._prompts:
self._prompts[name] = {}
self._prompts[name][version] = PromptTemplate(name, version, template)
def get(self, name: str, version: Optional[str] = None) -> PromptTemplate:
if version:
return self._prompts[name][version]
# 返回最新版本
versions = sorted(self._prompts[name].keys())
return self._prompts[name][versions[-1]]
# 定义所有 Prompt
registry = PromptRegistry()
# 翻译 Prompt v1
registry.register("translator", "v1.0", {
"system": "你是一个翻译助手。将输入翻译成{target_lang}。",
"instruction": "翻译以下内容:
{text}",
})
# 翻译 Prompt v2(改进版)
registry.register("translator", "v2.0", {
"system": "你是一个专业的翻译助手,精通技术文档翻译。要求:术语准确,保持原文格式,不要意译。",
"context": "源语言:{source_lang}
目标语言:{target_lang}
领域:{domain}",
"instruction": "原文:
{text}
翻译:",
})
# 情感分析 Prompt
registry.register("sentiment", "v1.0", {
"system": "你是一个情感分析引擎。",
"instruction": (
"分析以下评论的情感倾向。
"
"输出 JSON:
"
'{
'
' "sentiment": "positive | negative | neutral | mixed",
'
' "confidence": 0.0~1.0,
'
' "reasons": ["原因1", "原因2"]
'
"}
"
"评论:
{text}"
),
})
使用方式:
# 使用 Prompt 模板
prompt = registry.get("translator", "v2.0")
messages = prompt.render(
source_lang="英文",
target_lang="中文",
domain="technology",
text="Hello world",
)
# 调用 LLM
reply = chat_sync(messages)
A/B 测试
import hashlib
def select_version(user_id: str, name: str, registry: PromptRegistry) -> str:
# 根据用户 ID 一致性哈希选择 Prompt 版本。
versions = list(registry._prompts[name].keys())
if len(versions) == 1:
return versions[0]
hash_val = hashlib.md5(f"{name}:{user_id}".encode()).hexdigest()
idx = int(hash_val[:8], 16) % len(versions)
return versions[idx]
# 统计两个版本的效果
metrics = {"v1.0": {"calls": 0, "success": 0}, "v2.0": {"calls": 0, "success": 0}}
不要同时对所有用户切换版本。用小流量验证新版本效果,确认提升后再全量。
Prompt 监控指标
prompt_monitoring = {
"parse_success_rate": 0.98, # 结构化输出解析成功率
"empty_response_rate": 0.01, # 空回复率
"avg_latency_ms": 1200, # 平均延迟
"avg_output_tokens": 350, # 输出 token 数
"refusal_rate": 0.005, # 拒绝回答率
}
这些指标应该按 Prompt 版本分别统计,对比基线偏离时发出告警。
5. 基于上篇应用的实战
在上篇的 ai-chat-app 基础上集成 Prompt 管理:
# prompts.py——新的 Prompt 管理模块
from prompts import registry
# 为我们的聊天应用设计 Prompt
registry.register("chat_assistant", "v1.0", {
"system": "你是一个有用的 AI 助手。回答简洁准确。",
"instruction": "{user_input}"
})
registry.register("chat_assistant", "v2.0", {
"system": # 你是 Zyentor AI 助手,一个专注于 AI 开发者社区的智能助手。 能力边界: - 回答 AI 技术问题(编程、模型、工具) - 解释技术概念 - 提供代码示例和最佳实践 行为规范: - 回答简洁,优先用列表和代码块 - 不知道就说不知道,不要编造 - 涉及安全问题时优先提醒风险 - 不超过 500 字,
"instruction": "{user_input}"
})
# 在 main.py 中使用
# from prompts import registry, select_version
# session_id = body.get("session_id", "default")
# version = select_version(session_id, "chat_assistant", registry)
# prompt = registry.get("chat_assistant", version)
# messages = prompt.render(user_input=user_message)
# # 再合并历史消息...
6. 常见陷阱与调试方法
陷阱 1:指令被上下文淹没
现象:长上下文中,核心指令被模型忽略。
原因:模型对 prompt 中部的内容关注度最低(U 型注意力——开头和结尾记得最牢,中间容易被忽略)。
解决:核心指令放在 prompt 的开头(system message)和结尾(最后一条 user message),首尾呼应。
陷阱 2:格式约束不生效
现象:模型输出 JSON 但偶尔会加多余的说明文字。
解决:组合使用以下技术:
1. System Prompt 明确约束:你的输出必须是合法的 JSON
2. User Prompt 强调:只输出 JSON,不要 markdown 代码块
3. Few-shot 示例:给出纯 JSON 示例
4. 后处理兜底:正则提取 JSON
import re, json
def extract_json(text: str) -> dict:
# 从模型输出中提取 JSON。
# 尝试直接解析
try:
return json.loads(text)
except json.JSONDecodeError:
pass
# 尝试提取 ```json ... ``` 块
match = re.search(r'```(?:json)?\s*
?([\s\S]*?)
?```', text)
if match:
try:
return json.loads(match.group(1))
except json.JSONDecodeError:
pass
# 尝试提取第一个 { ... } 或 [ ... ]
match = re.search(r'(\{[\s\S]*\}|\[[\s\S]*\])', text)
if match:
try:
return json.loads(match.group(1))
except json.JSONDecodeError:
pass
# 最后尝试:修复常见格式错误
fixed = text
fixed = fixed.replace("'", '"') # 单引号→双引号
fixed = re.sub(r',(\s*[}\]])', r'', fixed) # 去除末尾逗号
fixed = re.sub(r'//[^
]*', '', fixed) # 去除注释
try:
return json.loads(fixed)
except json.JSONDecodeError:
raise ValueError("无法从模型输出中提取 JSON")
陷阱 3:Few-shot 导致模板固化
现象:模型严格复制示例风格,不会变通。
原因:Few-shot 示例太少或太同质化。
解决:在示例中展示多样性。如果示例都是"短回答",模型就会倾向于短回答。混入不同长度、不同风格的示例。
7. 调试框架
当 Prompt 效果不好时,按这个流程排查:
Prompt 效果不好?
├─ 模型不理解任务?
│ ├─ 指令是否清晰 → 让另一个人读一遍
│ ├─ 角色是否明确 → 角色越具体越好
│ └─ 需要 Few-shot 吗?→ 加 2-3 个示例
├─ 输出格式不对?
│ ├─ 格式要求明确吗?→ 给出精确 Schema
│ ├─ 需要 Function Calling 吗?
│ └─ 后处理兜底写了吗?
├─ 内容质量不高?
│ ├─ 上下文够具体吗?
│ ├─ 需要 CoT 吗?→ 加"请逐步思考"
│ └─ 需要换更强的模型吗?
└─ 生产不稳定?
├─ 有版本管理吗?
├─ 有监控指标吗?
└─ 有 A/B 测试吗?
总结
Prompt 工程的核心,是把"写提示词"这件事从艺术变成工程。
| 层级 | 做法 |
|---|---|
| L1 - 单条 Prompt | 掌握四个原子组件:角色、指令、上下文、格式 |
| L2 - 模板模式 | 选用合适的结构:Markdown、XML、JSON |
| L3 - 示例设计 | Few-shot 覆盖正/负/边界,CoT 让推理可见 |
| L4 - 生产管理 | 版本管理 + A/B 测试 + 监控指标 |
下一篇我们将在此基础上,深入多轮对话管理——上下文记忆、System Prompt 注入、历史截断策略,这些都是生产级 AI 应用必须解决的实际问题。
本文是 《AI 应用开发实战》系列 的第 2 篇。
系列目录:
1. ✅ 从零搭建你的第一个 AI 应用
2. ✅ Prompt 工程实战 ← 你在这里
3. 📝 多轮对话——对话管理与记忆
4. 📝 从零实现 RAG 系统
5. 📝 AI Agent——工具调用与自主决策本文由 Zyentor(智元界) 原创发布