一、什么是 Prompt Engineering?为什么它如此重要?

1.1 重新理解 Prompt

很多初学者把 Prompt Engineering 理解为"写好一段话让 AI 回答",但作为 AI Application Engineer,你需要从更深的层面理解它。

LLM 的本质是一个条件概率生成器 —— 给定前文(prompt),它会基于学到的概率分布,逐 token 地生成最可能的后续内容。所以 Prompt Engineering 的核心问题其实是:如何构造输入序列,使得模型的概率分布偏向我们期望的输出空间?

打一个你容易理解的比方:假设你要给一位能力很强、但对你的具体需求一无所知的新同事布置任务。你说的话越模糊,他交付的结果偏差就越大;你说的话越精准、给的参考越具体,他就越能命中你的期望。Prompt Engineering 就是这个"精准沟通"的技术。

1.2 Prompt 在 AI 应用开发中的位置

在实际的 AI 应用架构中,Prompt 并不是用户随手写的一句话,而是一个工程化的模块。典型的 prompt 结构包含:

System Prompt(系统指令)    → 定义模型的角色、行为边界、输出格式
Context(上下文)           → 提供背景信息、相关数据、历史对话
User Input(用户输入)       → 实际的问题或任务
Output Specification(输出规范) → 期望的格式、长度、风格

这四个部分共同构成了发送给 LLM 的完整 prompt。作为 AI Application Engineer,你写的大部分代码其实都在动态组装这个 prompt。


二、Clear Instructions(清晰指令)

2.1 核心原理

Clear Instructions 是 Prompt Engineering 最基础也最重要的技术。它的原理很直觉:你给模型的指令越明确、越具体,模型输出的确定性就越高,随机漂移的空间就越小。

从概率的角度理解:模糊的 prompt 会让模型在多个可能的输出方向上都有较高的概率,导致输出不可控。而清晰的 prompt 会大幅收窄可能的输出分布,让模型更集中地生成你想要的内容。

2.2 六大实操原则

原则一:指定角色和身份(Role Setting)

# ❌ 模糊的 prompt
prompt = "帮我分析这段代码的问题"

# ✅ 清晰的 prompt
prompt = """你是一位拥有10年经验的 Python 后端工程师,专精代码审查。
请从以下维度分析这段代码的问题:
1. 逻辑正确性
2. 性能瓶颈
3. 安全隐患
4. 代码风格与可维护性"""

为什么指定角色有效?因为 LLM 在训练数据中见过大量"某角色在某场景下的表达模式"。当你指定角色时,模型会倾向于激活与该角色相关的知识和表达方式。这不是玄学,而是条件概率 P(output | role, task) 与 P(output | task) 的分布差异。

原则二:明确输出格式(Output Format)

# ❌ 模糊的格式要求
prompt = "列出学习 Python 的资源"

# ✅ 精确指定格式
prompt = """请推荐 5 个学习 Python 的在线资源。
对每个资源,请按以下 JSON 格式输出:
{
  "name": "资源名称",
  "url": "链接",
  "level": "beginner | intermediate | advanced",
  "focus": "主要学习方向",
  "reason": "推荐理由(一句话)"
}
请以 JSON 数组形式返回所有结果。"""

这在 AI 应用开发中极其重要。你的代码需要解析模型的输出,如果输出格式不稳定,你的 parser 就会频繁出错。指定 JSON、XML、Markdown 表格等结构化格式,是保证应用稳定性的关键。

原则三:提供约束条件(Constraints)

prompt = """请为一款面向日本市场的健康饮品写一段广告文案。

约束条件:
- 字数:100-150个日文字符
- 语调:温暖、亲切,面向30-40岁女性
- 必须包含:产品名"朝のめぐみ"
- 不得包含:与竞品的直接比较、夸大的健康声明
- 格式:一个主标题 + 一段正文"""

约束条件就像给模型画了一个"输出框",模型只能在框内发挥。没有约束,模型可能输出 500 字也可能输出 50 字,可能用正式语气也可能用口语 —— 这种不确定性在生产环境中是灾难性的。

原则四:使用分隔符隔离内容(Delimiters)

prompt = """请将以下用三重反引号包裹的用户评论分类为"正面"、"负面"或"中性"。

用户评论:
\```
这家餐厅的拉面味道还不错,但是等了40分钟实在太久了。下次可能会考虑其他店。
\```

请只输出分类结果,不需要解释。"""

分隔符的作用是防止 prompt 注入。如果用户输入的内容包含"请忽略以上指令"之类的攻击性文本,分隔符可以帮助模型区分"这是指令"和"这是要处理的数据"。在 AI 应用安全中,这是必备的防护手段。

原则五:将复杂任务分步骤(Step-by-Step Task Decomposition)

prompt = """请按以下步骤分析这篇新闻文章:

步骤 1:用一句话总结文章的核心事件
步骤 2:识别文章中提到的所有人物及其角色
步骤 3:判断文章的情感倾向(正面/负面/中性),并给出依据
步骤 4:基于以上分析,生成 3 个关键标签

文章内容:
---
{article_text}
---

请按步骤编号依次输出结果。"""

这个原则的背后逻辑是:LLM 的 autoregressive 特性意味着前面的输出会影响后面的生成。当你让模型先总结、再分析、最后判断时,前面步骤的输出就成了后面步骤的"context",帮助模型逐步深入。

原则六:给出"做什么"而不仅是"不做什么"

# ❌ 只说不做什么
prompt = "回答用户的问题,不要编造信息,不要太长,不要太短"

# ✅ 明确说做什么
prompt = """回答用户的问题。
- 只基于提供的文档内容回答
- 如果文档中没有相关信息,回复"根据现有资料无法回答此问题"
- 回答长度控制在 2-3 个段落
- 在回答末尾引用具体的文档段落作为出处"""

这是因为 LLM 的概率生成机制对"不要做 X"的处理不如"请做 Y"可靠。告诉模型"不要想大象",它反而会给大象相关的 token 更高的权重。

2.3 Clear Instructions 的代码实践

在实际开发中,你会把这些原则组织成模板:

def build_analysis_prompt(user_text: str, language: str = "zh") -> str:
    """
    构建一个结构化的文本分析 prompt。
    注意:这就是 AI Application Engineer 的日常工作之一 ——
    将 prompt engineering 原则封装成可复用的函数。
    """
    system_prompt = f"""你是一位专业的文本分析助手。
你的任务是对用户提供的文本进行结构化分析。
输出语言:{"中文" if language == "zh" else "English"}"""

    user_prompt = f"""请分析以下文本,按指定的 JSON 格式输出结果。

待分析文本:
---
{user_text}
---

输出格式:
{{
  "summary": "一句话摘要",
  "sentiment": "positive | negative | neutral",
  "confidence": 0.0-1.0,
  "key_entities": ["实体1", "实体2"],
  "topics": ["话题1", "话题2"]
}}

要求:
1. summary 不超过 50 个字
2. confidence 为你对 sentiment 判断的置信度
3. key_entities 最多提取 5 个
4. 只输出 JSON,不要添加任何其他文字"""

    return system_prompt, user_prompt

三、Few-Shot Prompting(少样本提示)

3.1 核心原理

Few-shot prompting 是在 prompt 中给模型提供几个"输入→输出"的示例,让模型从示例中推断出你想要的模式,然后将这个模式应用到新的输入上。

这项技术的理论基础来自 2020 年 GPT-3 论文《Language Models are Few-Shot Learners》。研究发现,大型语言模型不需要微调(fine-tuning),仅仅通过在 prompt 中提供少量示例,就能"学会"新任务。这种能力被称为 In-Context Learning(上下文学习)

为什么这行得通?从 Transformer 的 attention 机制角度理解:模型在处理新输入时,会通过 self-attention 去"关注"前面的示例。它会发现示例中输入和输出之间的映射模式(比如格式、风格、逻辑规则),然后在生成新输出时复制这个模式。本质上,few-shot prompting 是在利用 attention 机制进行模式匹配和迁移

3.2 Shot 的分类

Zero-shot:  不给示例,直接给任务指令
One-shot:   给 1 个示例
Few-shot:   给 2-5 个示例(最常用)
Many-shot:  给更多示例(通常 10+ 个,在 context window 足够大时使用)

3.3 实战示例:情感分析

# Zero-shot(零样本)
zero_shot_prompt = """判断以下评论的情感倾向。

评论:这家店的服务态度真的很差,等了一个小时才上菜。
情感:"""
# 模型可能输出"负面",也可能输出"消极"、"不好"、"negative" —— 格式不可控

# Few-shot(少样本)
few_shot_prompt = """判断评论的情感倾向。

评论:今天天气真好,心情特别愉快!
情感:positive

评论:这个产品质量一般般,没什么特别的。
情感:neutral

评论:快递太慢了,包装还破了,非常失望。
情感:negative

评论:这家店的服务态度真的很差,等了一个小时才上菜。
情感:"""
# 模型现在非常明确地知道:输出只能是 positive / neutral / negative

看到区别了吗?Few-shot 的三个示例同时传达了多层信息:输出只有三个选项(positive/neutral/negative)、输出是英文标签而不是中文描述、每个标签对应什么程度的情感。这些"规则"不需要你用文字描述,模型从示例中自动推断出来了。

3.4 Few-Shot 的进阶技巧

技巧一:示例的多样性比数量更重要

# ❌ 差的 few-shot:示例太相似
bad_examples = """
输入:苹果 → 类别:水果
输入:香蕉 → 类别:水果
输入:橙子 → 类别:水果
"""

# ✅ 好的 few-shot:覆盖不同类别和边界情况
good_examples = """
输入:苹果 → 类别:水果
输入:胡萝卜 → 类别:蔬菜
输入:三文鱼 → 类别:海鲜
输入:番茄 → 类别:蔬菜(注:虽然有时被归为水果,但在烹饪分类中属于蔬菜)
"""

那个"番茄"的示例非常关键 —— 它展示了模型应该如何处理边界情况,并且示范了可以添加注释说明。

技巧二:示例顺序会影响结果

研究表明,few-shot 示例的顺序对输出有显著影响。一般建议把与当前输入最相似的示例放在最后(最靠近实际问题的位置),因为 LLM 的 attention 机制对近距离的内容有更强的关注(recency bias)。

技巧三:在代码中动态选择示例

import numpy as np
from typing import List, Tuple

class DynamicFewShotSelector:
    """
    动态 few-shot 示例选择器。
    核心思路:根据用户输入,从示例库中选择最相关的示例。
    这是 AI 应用开发中的高频模式 —— 不要硬编码示例,要动态检索。
    """

    def __init__(self, examples: List[Tuple[str, str]], embedding_func):
        """
        参数:
            examples: [(input_text, output_text), ...] 示例库
            embedding_func: 将文本转为向量的函数
        """
        self.examples = examples
        self.embedding_func = embedding_func
        # 预计算所有示例的 embedding
        self.example_embeddings = [
            embedding_func(inp) for inp, _ in examples
        ]

    def select(self, query: str, k: int = 3) -> List[Tuple[str, str]]:
        """
        选出与 query 最相似的 k 个示例。
        使用余弦相似度作为相似性度量。
        """
        query_embedding = self.embedding_func(query)

        # 计算 query 与每个示例的余弦相似度
        similarities = []
        for emb in self.example_embeddings:
            cos_sim = np.dot(query_embedding, emb) / (
                np.linalg.norm(query_embedding) * np.linalg.norm(emb)
            )
            similarities.append(cos_sim)

        # 取 top-k 最相似的示例
        top_k_indices = np.argsort(similarities)[-k:]

        # 返回示例,注意:最相似的放在最后(利用 recency bias)
        return [self.examples[i] for i in top_k_indices]

    def build_prompt(self, query: str, k: int = 3) -> str:
        """组装完整的 few-shot prompt"""
        selected = self.select(query, k)

        prompt_parts = []
        for inp, out in selected:
            prompt_parts.append(f"输入:{inp}\n输出:{out}")

        prompt_parts.append(f"输入:{query}\n输出:")

        return "\n\n".join(prompt_parts)

这个模式叫做 Dynamic Few-Shot Selection,是 RAG(Retrieval-Augmented Generation)的一个子应用。在生产环境中,你的示例库可能有上千条,每次请求时动态选择最相关的 3-5 条作为 few-shot 示例。

3.5 Few-Shot 的局限性

Few-shot 并非万能。它有几个明显的局限:第一,占用 token 数量 —— 每个示例都消耗 context window 空间,也增加 API 费用;第二,对复杂推理任务效果有限 —— 如果任务需要多步推理,光给输入输出的示例不够,模型需要看到推理过程(这就是 Chain-of-Thought 要解决的问题);第三,示例质量至关重要 —— 错误的示例会"教坏"模型。


四、Chain-of-Thought(思维链)

4.1 核心原理

Chain-of-Thought (CoT) prompting 是 2022 年由 Google Brain 的 Jason Wei 等人提出的技术。它的核心思想非常简单但非常有效:让模型在给出最终答案之前,先输出中间推理步骤。

为什么这样做能提升效果?这要回到 LLM 的 autoregressive 本质。LLM 是逐 token 生成的,每个新 token 的生成都依赖于前面已经生成的所有 token。当模型直接输出答案时,它需要在"一步"内完成所有推理,这个推理全部发生在模型的前向传播中(也就是神经网络的内部计算)。但模型的单步计算能力是有限的。

而当你让模型先写出推理步骤时,这些步骤会成为后续生成的 context,相当于给模型提供了"外部工作记忆"。模型可以在每一步聚焦于一个子问题,然后把结果写下来,作为下一步的输入。这就像人类解数学题时在草稿纸上写中间步骤一样。

4.2 基础 CoT 示例

# ❌ 不使用 CoT(直接要答案)
prompt_no_cot = """
餐厅账单是 180 元,需要加 10% 的服务费,三个人平分。
每人需要付多少钱?
"""
# 模型可能直接给出答案,但复杂数学问题时容易出错

# ✅ 使用 CoT(要求展示推理过程)
prompt_with_cot = """
餐厅账单是 180 元,需要加 10% 的服务费,三个人平分。
每人需要付多少钱?

请一步一步地思考这个问题,展示你的推理过程,最后给出答案。
"""
# 模型输出:
# 1. 原始账单:180 元
# 2. 服务费:180 × 10% = 18 元
# 3. 总计:180 + 18 = 198 元
# 4. 每人:198 ÷ 3 = 66 元
# 答案:每人需要付 66 元

4.3 CoT 的三种实现方式

方式一:Zero-Shot CoT(最简单)

只需在 prompt 末尾加一句神奇的话:

prompt = f"""
{your_question}

Let's think step by step.
"""

这句"Let’s think step by step"是 2022 年论文《Large Language Models are Zero-Shot Reasoners》中验证过的最有效的 zero-shot CoT 触发器。它为什么有效?因为训练数据中大量的推理文本(教材、论坛解答、论文)都包含类似的逐步推理模式,这句话触发了模型对这些模式的回忆。

方式二:Few-Shot CoT(结合示例)

prompt = """请解决以下数学问题。

问题:小明有 5 个苹果,他给了小红 2 个,然后妈妈又给了他 3 个。小明现在有几个苹果?
推理过程:
- 初始:小明有 5 个苹果
- 给了小红 2 个后:5 - 2 = 3 个
- 妈妈给了 3 个后:3 + 3 = 6 个
答案:6 个

问题:一个图书馆有 3 层,每层有 4 个书架,每个书架有 5 层隔板,每层隔板放 8 本书。图书馆一共有多少本书?
推理过程:
- 每个书架的书:5 层 × 8 本 = 40 本
- 每层楼的书:4 个书架 × 40 本 = 160 本
- 整个图书馆:3 层 × 160 本 = 480 本
答案:480 本

问题:{new_question}
推理过程:"""

注意这里的示例不仅展示了"输入→答案",还展示了完整的推理链路。这比普通的 few-shot 多了一个关键信息:推理的方式和格式

方式三:Structured CoT(工程化 CoT)

在 AI 应用开发中,你经常需要模型的推理过程是结构化、可解析的:

prompt = """你是一个医疗分诊助手。根据患者描述,进行初步分诊评估。

请严格按以下结构输出你的分析:

<reasoning>
<symptom_extraction>
[从描述中提取关键症状,列出每个症状]
</symptom_extraction>

<severity_assessment>
[评估每个症状的严重程度:轻微/中等/严重]
</severity_assessment>

<possible_conditions>
[基于症状组合,列出可能的病症,按可能性从高到低排列]
</possible_conditions>

<urgency_decision>
[综合判断:常规就诊/尽快就诊/紧急就医]
</urgency_decision>
</reasoning>

<final_answer>
[分诊建议(简洁版)]
</final_answer>

患者描述:{patient_description}"""

这种结构化 CoT 的价值在于:你的代码可以分别解析 reasoning 和 final_answer,把推理过程存入日志用于审计和调试,而只把 final_answer 展示给终端用户。

4.4 CoT 的代码实践:构建一个带推理链的分析器

import json
import re
from dataclasses import dataclass
from typing import Optional


@dataclass
class ReasoningResult:
    """封装 CoT 输出的数据结构"""
    reasoning_steps: list[str]   # 推理步骤
    final_answer: str            # 最终答案
    confidence: float            # 置信度
    raw_output: str              # 模型原始输出


class ChainOfThoughtAnalyzer:
    """
    一个带有 Chain-of-Thought 推理能力的分析器。
    演示了如何在 AI 应用中工程化地使用 CoT。
    """

    def __init__(self, llm_client, model: str = "claude-sonnet-4-20250514"):
        self.client = llm_client
        self.model = model

    def analyze(self, question: str) -> ReasoningResult:
        """
        对问题进行 CoT 分析。
        关键设计:将推理过程和最终答案分离,方便下游处理。
        """
        prompt = self._build_cot_prompt(question)

        response = self.client.messages.create(
            model=self.model,
            max_tokens=2000,
            messages=[
                {"role": "system", "content": self._system_prompt()},
                {"role": "user", "content": prompt}
            ]
        )

        raw_output = response.content[0].text
        return self._parse_response(raw_output)

    def _system_prompt(self) -> str:
        return """你是一个严谨的分析助手。
在回答任何问题之前,你必须先展示完整的推理过程。
你的输出必须严格遵循指定的格式。"""

    def _build_cot_prompt(self, question: str) -> str:
        return f"""请分析以下问题。

问题:{question}

请按以下格式输出:

<reasoning>
步骤1: [第一步分析]
步骤2: [第二步分析]
...(根据需要添加更多步骤)
</reasoning>

<confidence>
[0.0 到 1.0 之间的数字,表示你对答案的置信度]
</confidence>

<answer>
[最终答案]
</answer>"""

    def _parse_response(self, raw: str) -> ReasoningResult:
        """
        解析模型的结构化输出。
        这就是为什么我们需要固定格式 —— 为了可靠地解析。
        """
        # 提取推理步骤
        reasoning_match = re.search(
            r'<reasoning>(.*?)</reasoning>', raw, re.DOTALL
        )
        reasoning_text = reasoning_match.group(1).strip() if reasoning_match else ""

        steps = [
            line.strip() for line in reasoning_text.split('\n')
            if line.strip() and line.strip().startswith('步骤')
        ]

        # 提取置信度
        conf_match = re.search(
            r'<confidence>(.*?)</confidence>', raw, re.DOTALL
        )
        try:
            confidence = float(conf_match.group(1).strip()) if conf_match else 0.5
        except ValueError:
            confidence = 0.5

        # 提取最终答案
        answer_match = re.search(
            r'<answer>(.*?)</answer>', raw, re.DOTALL
        )
        final_answer = answer_match.group(1).strip() if answer_match else raw

        return ReasoningResult(
            reasoning_steps=steps,
            final_answer=final_answer,
            confidence=confidence,
            raw_output=raw
        )

4.5 CoT 的使用时机

CoT 并不是所有场景都需要的。它最适合以下场景:需要多步推理的数学和逻辑问题、需要综合多个因素做判断的决策问题、复杂的文本理解和推理任务,以及需要可解释性的应用场景(医疗、法律、金融)。

而对于以下场景,CoT 反而可能是多余的:简单的事实检索(“法国的首都是什么”)、创意写作和内容生成、简单的格式转换或翻译。在这些场景中使用 CoT 会浪费 token(增加成本和延迟),而且不会显著提升质量。


五、三种技术的组合使用

在实际的 AI 应用中,这三种技术几乎总是组合使用的。下面是一个综合示例,展示一个"客户邮件自动回复系统"的 prompt 设计:

def build_email_reply_prompt(
    customer_email: str,
    customer_history: str,
    product_info: str
) -> tuple[str, str]:
    """
    综合运用 Clear Instructions + Few-Shot + CoT 的实战示例。
    场景:电商客服自动回复系统。
    """

    system_prompt = """你是"TechShop"的高级客服代表。
你的目标是:准确理解客户问题,提供有帮助的回复,维护品牌形象。

核心规则:
1. 始终保持礼貌和专业
2. 如果涉及退款,需要先验证订单信息
3. 如果问题超出你的处理范围,引导客户联系人工客服
4. 回复长度控制在 100-200 字"""  # ← Clear Instructions

    user_prompt = f"""请根据客户邮件生成回复。

=== 客户历史 ===
{customer_history}

=== 产品信息 ===
{product_info}

=== 参考示例 ===

【示例1】
客户邮件:我上周买的蓝牙耳机左耳没声音了,能换一个吗?
思考过程:
- 问题类型:产品质量问题
- 购买时间:一周内,在保修期
- 处理方式:应该提供换货服务
- 需要信息:订单号,以便查询
回复:您好!很抱歉听到您的耳机出现了问题。一周内的产品质量问题我们可以为您免费更换。麻烦您提供一下订单号,我会尽快为您安排换货流程。如有其他问题,随时联系我们!

【示例2】
客户邮件:你们能不能把我的订单地址改成大阪市?我下周要出差。
思考过程:
- 问题类型:订单修改(地址变更)
- 关键因素:需要确认订单是否已发货
- 处理方式:如果未发货可以修改,已发货需要拦截或转寄
- 需要信息:订单号,以便查询发货状态
回复:您好!地址变更没问题,不过需要先确认您的订单发货状态。请提供您的订单号,如果还未发货我们可以直接修改地址;如果已发货,我会帮您联系物流进行转寄。

=== 当前客户邮件 ===
{customer_email}

请先进行思考分析,然后生成回复。按以下格式输出:

思考过程:
[你的分析]

回复:
[给客户的回复]"""  # ← Few-Shot + CoT 组合

    return system_prompt, user_prompt

在这个例子中,Clear Instructions 定义了角色、规则和约束;Few-Shot 提供了两个覆盖不同场景的示例,展示了期望的分析方式和回复风格;CoT 要求模型先分析再回复,确保回复有据可依。三者协同工作,构成了一个可靠的生产级 prompt。


六、关键要点总结

Clear Instructions 解决的是"模型知道你要什么"的问题。它通过精确的指令和约束,收窄模型的输出空间,是所有 prompt engineering 的基础。

Few-Shot 解决的是"模型知道你要什么样的"的问题。它通过示例展示期望的模式,利用 LLM 的 in-context learning 能力,让模型"模仿"你给出的模式。

Chain-of-Thought 解决的是"模型能想清楚"的问题。它通过外化推理过程,给模型提供"思考的空间",显著提升复杂推理任务的准确性。

作为 AI Application Engineer,你需要根据具体场景灵活组合这三种技术,找到效果、成本和延迟之间的最佳平衡点。