前言

在上一篇我们搭建了一个 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(智元界) 原创发布