Prompt 工程教程:五要素法 + Few-shot + CoT + JSON 输出

约 60 分钟L1L2T1

课程来源

[来源] FDE 实战课(fde-teach)

[收录说明] 本课未随文署名,按课程线署名。

本课目录(23 节)

🎯 本节目标

学完这一节,你能:

  • 写出结构化 Prompt(五要素框架),不再写"帮我整理下客户反馈"这种让模型抓瞎的话
  • 用 Few-shot 示例 和 CoT 思维链 提升复杂任务准确率,还能讲清"为啥这样有效"
  • 让模型稳定输出 JSON(这是 FDE 把 AI 从"聊天玩具"变成"工程系统"的关键)

提示词工程在 FDE JD 出现率 55%,是技术栈第一项。后面所有模块(RAG / Agent / 评估)都建立在它之上。

🍎 生活化类比

写 Prompt = 点外卖。

回想你点外卖填的那张单子,为啥要勾那么多选项?因为一句"我要吃饭"没法让店家知道你到底要啥。一份清楚的外卖单,有五块东西:

  1. 给谁下单(角色):给厨师下单,还是给跑腿小哥下单?
  2. 现在啥情况(背景):朋友聚餐?一个人吃?赶时间?
  3. 具体要什么(任务):两份番茄炒蛋、一份米饭
  4. 不要什么(约束):不要辣、不要香菜
  5. 怎么给你(输出格式):打包盒、要不要餐具

少一块会怎样?少"背景",店家不知道份量做多大;少"约束",可能做出你吃不了的味道;少"输出格式",直接给你端盘子里——可你在家没法用。写 Prompt 也一样,五块都给齐,模型才稳。

📖 概念讲解

Prompt 五要素框架(点外卖版)

把点外卖那五块,翻译成写 Prompt:

点外卖Prompt 五要素邮件分类举例
给谁下单角色你是客服邮件分类专家
现在啥情况背景我们是卖电子产品的电商
具体要什么任务把每封邮件归到 5 类之一
不要什么约束只能归一类,不确定归"其他"
怎么给你输出格式JSON: {category, confidence}

没接触过 JSON 的同学记一句大白话:JSON 就是一种"机器能读的填表格式"——像表格一样把字段名和值配对写清楚({"category": "投诉", "confidence": 0.9})。为啥不让模型直接说人话?因为下游程序(分类统计、转 Excel、触发工单)要按字段名精确取值,"人话"没法稳定拆,JSON 可以。这就是本节末尾"稳定输出 JSON"的意义。

最容易搞混的是背景和任务。一句话记住:背景是状态,任务是动作。"我们是电商"是背景(陈述现状),"请分类"是任务(动词开头)。背景告诉模型"在什么场景下判断",任务告诉模型"具体产出什么"。

Few-shot:给学徒看几个成品

复杂任务,光描述规则模型常常学不会。这时别再费劲解释了——直接拿几个成品给它看。

就像师傅带徒弟:你口头描述"什么叫好的木匠活",徒弟半天听不懂;但你塞给他三把做好的椅子,他照着刨,立马就会了。模型也一样,这招叫 Few-shot(少样本):在 Prompt 里塞 3-5 个"输入→输出"的成品例子,它会照着模式模仿。

例子1: "这手机太好用了!" → positive
例子2: "质量差,后悔买" → negative
例子3: "还行吧,一般般" → neutral
现在判断: "续航惊人但拍照糊" → ?

为什么是 3-5 个,不是 1 个也不是 30 个?

  • 太少(1-2 个):模型只见过一次,没抓准规律,可能蒙
  • 正好(3-5 个):模型看几遍就总结出套路,稳定模仿
  • 太多(>10 个):一是费钱(每个例子都算 token),二是模型反而会"死记例子",换个稍不同的说法就懵(过拟合)

CoT(思维链):让模型把计算过程写出来

数学题、逻辑推理、多步决策这类任务,你直接问答案,模型容易蒙错。让它先把过程写出来再给答案,准确率能从 50% 跳到 90%+。

类比:考试时把计算过程写出来,vs 心算直接写答案。心算错一步整道题全错;写过程的话,每步都能检查,而且写着写着思路就顺了。

# 不用 CoT(容易蒙错)
prompt = "小明有 5 个苹果,吃了 2 个,又买了 3 个,现在有几个?"

# 用 CoT(准确率高)
prompt = """小明有 5 个苹果,吃了 2 个,又买了 3 个,现在有几个?
请一步步思考:
步骤1:
步骤2:
最终答案:"""

但说实话:模型并没有真的"在思考"。 它本质是个"字接字"的语言模型,CoT 只是逼它先输出一段推理文字。重点是:模型生成下一个字时,前面所有字(包括它自己刚写的推理)都会被一起算进去当输入。所以"先写过程 → 再下结论"这个顺序,会让最终结论落在更合理的字上。看起来像思考,本质是"输出文字 → 这些文字又帮下一句输出得更准"的滚雪球效应。

什么时候用 CoT?推理类任务(数学、逻辑、多步决策)。分类、抽取这种简单任务别用,白白多花 token 还变慢。

结构化 JSON 输出:三种"约束程度"

企业应用里,模型输出不能是一段话,必须是代码能解析的 JSON。三种做法,按"管得多严"排序:

方法 1:口头要求(松,可能违约) —— 在 Prompt 里写一句"请输出 JSON"

system = "你是数据抽取器。输出必须是合法 JSON,不要任何解释。格式:{name, age, city}"
user = "张三今年 28 岁,住在上海"
# 大部分时候输出 JSON,偶尔会调皮加一句"好的,这是结果:"

像口头约定:大多数时候守约,偶尔违约。简单任务够用,但你不能 100% 信它。

方法 2:response_format(中,基本不违约) —— 调用时强制 JSON 模式

response = client.chat.completions.create(
    model="glm-4.7-flash",
    messages=[...],
    response_format={"type": "json_object"}  # 像签合同:必须输出 JSON
)

像签合同:模型底层被强制走 JSON 模式,基本不会违约。生产环境首选。

方法 3:Function Calling(严,直接锁死格式) —— 定义一个"工具",让模型按工具的参数 schema 输出。像装了把锁:模型必须按你定义的字段名、类型填,一点不差。模块 3 详讲,现在知道有这招就行。

怎么选? 小练习用方法 1;做产品先上方法 2;要跟其他系统对接、字段必须严丝合缝,上方法 3。

🛠️ 动手实操:做一个邮件分类器(分三步加难度)

我们做 FDE 入门最经典的小项目——客服邮件分类器,但分三步迭代:先写五要素版,再加 Few-shot 提准确率,最后用 JSON 强制输出。你会直观看到"每加一层,效果稳一点"。

Step 0:确认上节的 chat() 还能用

确保 llm.py 还在 ~/fde-learn 目录里(上节封装的函数)。这节站在它肩膀上。

Step 1:五要素版(基础分类)

新建 email_classifier.py:

from llm import chat
import json

# 五要素版的 system(行尾【】标了对应哪个要素,方便对照)
SYSTEM_V1 = """你是客服邮件分类专家。【角色】

我们是卖电子产品的电商,每天收几百封客户邮件。【背景】

请把每封邮件归到下面 5 类中的 1 类:【任务】
- complaint:投诉(不满、索赔、情绪激动)
- inquiry:咨询(问功能、价格、流程)
- return:退货退款
- technical:技术问题(bug、报错、不能用)
- other:其他

每封只能归一类,不确定归 other。【约束】

输出 JSON: {"category": "类别", "confidence": 0.0-1.0, "reason": "一句话理由"}【输出格式】"""

def classify(email: str) -> dict:
    """分类一封邮件"""
    result = chat(email, system=SYSTEM_V1)
    try:
        return json.loads(result)
    except json.JSONDecodeError:
        return {"category": "other", "confidence": 0.0, "reason": "解析失败"}

if __name__ == "__main__":
    test_emails = [
        "我上周买的耳机充不进电,要求换货!",   # 期望 return / technical
        "请问你们的企业版支持多少人同时用?",   # 期望 inquiry
        "服务太差了!等了三天没人理,要投诉!", # 期望 complaint
    ]
    for email in test_emails:
        print(f"邮件: {email}")
        print(f"分类: {classify(email)}\n")

运行 python email_classifier.py。

✅ 大概率分类对。但遇到边界邮件(比如"产品不能用,要求退款")会左右摇摆——这时候该 Few-shot 上场。

Step 2:加 Few-shot(让边界邮件也稳)

把 SYSTEM 换成 V2,在五要素末尾追加几个"输入→输出"的成品例子:

# Few-shot 版:五要素 + 3 个成品例子
SYSTEM_V2 = SYSTEM_V1 + """

参考下面 3 个例子,照这个模式分类:
例子1: "耳机用了三天就坏了,充不进电,要求换货" → {"category": "return", "confidence": 0.9, "reason": "明确要求换货"}
例子2: "你们的 Pro 版和 Max 版有啥区别?" → {"category": "inquiry", "confidence": 0.95, "reason": "询问产品差异"}
例子3: "app 一直闪退,根本登录不进去" → {"category": "technical", "confidence": 0.9, "reason": "报 bug 不能用"}"""

def classify_v2(email: str) -> dict:
    """Few-shot 版分类"""
    result = chat(email, system=SYSTEM_V2)
    try:
        return json.loads(result)
    except json.JSONDecodeError:
        return {"category": "other", "confidence": 0.0, "reason": "解析失败"}

# 把 main 里的 classify(email) 换成 classify_v2(email) 再跑一遍

✅ 边界邮件会明显稳——这就是"师傅带徒弟看几个成品"的效果。3 个例子足够,加到 10 个反而会让模型只认这几个特定说法。

Step 3:JSON 强制输出(让解析不再失败)

Step 1/2 偶尔会解析失败(模型加了废话)。从源头解决:在 llm.py 末尾追加一个 chat_json() 函数,用方法 2 强制 JSON。

# 加到 llm.py 末尾
def chat_json(prompt: str, system: str = "你是中文助手", model: str = "glm-4.7-flash") -> str:
    """
    强制 JSON 输出版 chat。
    response_format={"type": "json_object"} 让模型底层走 JSON 模式,
    不会再加"好的,这是结果:"这种废话。
    """
    client = OpenAI(
        api_key=os.getenv("GLM_API_KEY"),
        base_url="https://open.bigmodel.cn/api/paas/v4/"
    )
    response = client.chat.completions.create(
        model=model,
        messages=[
            {"role": "system", "content": system},
            {"role": "user", "content": prompt}
        ],
        response_format={"type": "json_object"}  # 签合同:必须 JSON
    )
    return response.choices[0].message.content

然后 email_classifier.py 把 from llm import chat 改成 from llm import chat_json,classify_v2 里调用换成 chat_json,再跑一遍。

✅ 这次解析失败几乎不再出现。生产环境永远用 response_format 或 Function Calling,别赌模型自觉。

⚠️ 常见错误

问题原因解决
模型不输出 JSON,带一堆解释只口头要求升级到 response_format={"type": "json_object"}(方法 2)
JSON 解析失败模型加了 ```json 标记用 result.strip('\').replace('json\n','')` 清洗,或上方法 2
分类不准 / 左右摇摆规则模糊加 3-5 个 Few-shot 例子,把边界说清
简单任务用 CoT 反而慢不需要推理却加了"一步步思考"分类、抽取别用 CoT,只推理任务用
Few-shot 加 10+ 个更差过拟合 + 费 token砍到 3-5 个,挑边界例子

✅ 自测题

<summary>1. 五要素里,"背景" 和 "任务" 有啥区别?各举一个邮件分类里的例子。</summary>

背景 是"现在啥情况"(陈述现状)。例如:"我们是电商,每天收几百封邮件"。 任务 是"具体要模型干啥"(动词开头)。例如:"把每封邮件归到 5 类之一"。 一句话:背景是状态,任务是动作。

</details> <summary>2. Few-shot 为什么 3-5 个最好?给 30 个会怎样?</summary>

太少(1-2 个)模型没抓准规律;3-5 个模型看完能总结出套路,稳定模仿;太多(30 个)一是费钱(每个例子都算 token),二是模型容易"死记例子",换个稍不同的输入就懵(过拟合)。

</details> <summary>3. CoT 提升准确率的真正原理是什么?模型真的"在思考"吗?</summary>

不是真思考。模型本质是"字接字"的语言模型,CoT 只是逼它先生成一段推理文字。重点是:模型每次生成下一个字时,前面所有的字(包括它自己刚生成的推理)都会被一起算进去当输入。所以"先写过程 → 再下结论"这个输出顺序,会让最终结论落在更合理的字上。看起来像思考,本质是"输出文字 → 这些文字又帮助下一句输出得更准"的滚雪球效应。

</details>

🚀 实战小项目:结构化信息抽取

选一份真实文档(简历 / 产品说明 / 合同条款),用 五要素 Prompt + JSON 输出,抽取结构化字段。验收:对 10 份文档,字段准确率 ≥ 80%(自己人工核对)。进阶:准确率不够的字段,加 Few-shot 例子补强。

📚 总结 & 下一节

3 句话回顾:

  1. Prompt 五要素 = 点外卖:角色 / 背景 / 任务 / 约束 / 输出格式,五块都给齐模型才稳
  2. Few-shot(看成品)和 CoT(写过程)分别解决"规则难描述"和"推理复杂",前提是用对地方
  3. JSON 输出三档约束:口头要求(松)→ response_format(中,生产首选)→ Function Calling(严)

下一节:进入模块 2,用低代码平台(Coze / Dify) 把这些能力快速做成能给客户用的应用——30 分钟交付。

🏋️ 配套自练

🔗 延伸阅读


← 上一节:第一次调通 API | 下一节:Coze 与 Dify 低代码 →


动手实验

实验改编自 ai-engineer-notebooks(MIT License)第 01 章 Prompting Fundamentals(00-prompting-basics.ipynb)· github.com/calmrocks/ai-engineer-notebooks 运行方式:Colab 或本地 Jupyter 任选;模型用 Groq 免费 API(OpenAI 兼容),换成任何 OpenAI 兼容端点(含公司内部网关)只需改 base_url 与 key。

实验目标

用四组前后对照实验验证四种零成本提示技巧:任务/格式/约束说清楚、few-shot 示例、指定输出格式、先推理后作答。对应本课时讲义「提示词是模型行为最便宜的杠杆」与「任务-格式-约束三要素」——每个技巧都要观察到输出形态或路由结果的真实变化,而不是凭感觉调 prompt。

前置条件

  1. 到 console.groq.com 注册并创建免费 GROQ_API_KEY(无需信用卡)。
  2. Colab 或本地 Jupyter(Python 3.10+,全程零 GPU)。
  3. 安装课程助手包 aien(自带 groq 客户端与 setup 助手)。

实验步骤

步骤 1:安装与初始化

%pip install -q "git+https://github.com/calmrocks/ai-engineer-notebooks.git@v0.1.0"

from aien import setup
client, MODEL = setup()  # 读取 GROQ_API_KEY(Colab Secrets 或环境变量),返回 OpenAI 兼容客户端

步骤 2:技巧一——任务、格式、约束一次说清

同一条商品评论,模糊提问 vs 明确指令,对比输出形态:

review = ("Bought the standing desk. Assembly took two people and the "
          "instructions were useless, but it's rock solid and the motor "
          "is whisper quiet. Shipping was a week late though.")

def ask(prompt):
    r = client.chat.completions.create(
        model=MODEL, max_tokens=200,
        messages=[{"role": "user", "content": prompt}])
    return r.choices[0].message.content

print(ask(f"What do you think of this review?\n\n{review}"))  # 模糊:一段要人肉解析的散文
print(ask(
    "Extract, from the product review below, the aspects mentioned and the "
    "sentiment for each. Output one line per aspect as `aspect: sentiment` "
    "where sentiment is positive, negative, or mixed. Only aspects the "
    "review actually mentions.\n\n" + review))  # 明确:逐行 aspect: sentiment

预期:第二个版本输出稳定、逐行可解析,而且输出更短——格式约束同时是质量控制和成本控制。大多数「模型做不到」其实是「我没说清要什么」,改指令永远是第一步。

步骤 3:技巧二——few-shot 用示例锁定边界规则

「取消后仍被扣款 + 不认识的登录设备」按团队约定应路由 TRUST(欺诈)而非 BILLING,这种边界约定用一句话讲不清,一个示例就够:

SYSTEM = "Route each ticket to exactly one team. Reply with only the team name."
teams = "Teams: BILLING, PLATFORM (outages/API), DEVICE (hardware), TRUST (fraud/abuse)."
FEWSHOT = ("Ticket: My invoice is higher than my plan.\nTeam: BILLING\n"
           "Ticket: Charges I don't recognize plus a login from another country.\nTeam: TRUST\n"
           "Ticket: The API returns 503 for every request.\nTeam: PLATFORM\n")
ticket = ("I cancelled last month but was charged again, and I don't "
          "recognize the device that logged in.")

def route(ticket, shots=""):
    r = client.chat.completions.create(
        model=MODEL, max_tokens=10,
        messages=[{"role": "system", "content": f"{SYSTEM} {teams}"},
                  {"role": "user", "content": f"{shots}Ticket: {ticket}\nTeam:"}])
    return r.choices[0].message.content.strip()

print("zero-shot:", route(ticket))                 # 大概率猜 BILLING
print("few-shot :", route(ticket, shots=FEWSHOT))  # 示例教会了 TRUST 约定

步骤 4:技巧三——指定输出格式,并看清它的天花板

bug = ("App crashes on launch after the 4.2 update, but only on Android 13. "
       "Started yesterday. My coworker on iOS is fine.")
print(ask("Summarize this bug report as JSON with keys: title (short string), "
          "platform (string), severity (one of: low, medium, high). "
          "Output only the JSON, no other text.\n\n" + bug))

同一提示连跑 5 次:偶尔会混进 ```json 围栏或「Here's the JSON:」前缀。格式约束能拿 90 分,程序要稳定解析还需要 JSON mode / tool calling 这类 API 级保障——提示是地板,结构化输出是保障。

步骤 5:技巧四——先推理后作答

q = ("A team of 3 reviews 90 tickets. Two reviewers handle 25 each; the third "
     "takes the rest. If the third reviewer works twice as fast and clears 8 "
     "tickets an hour, how many hours does the third reviewer need?")
print(ask(q + " Reply with just the number."))  # 直接猜,容易错
print(ask(q + " Think step by step, then end with 'ANSWER: <number>'."))  # 先推理,ANSWER: 可解析

注意代价:推理本身也是要花钱的输出 token,简单分类/抽取任务别加;现代 reasoning 模型内部已自带,显式要求可能画蛇添足。

自查问题

  1. 遇到「模型做不到」,为什么第一步永远是改指令,而不是先堆 few-shot 或上工具?
  2. few-shot 示例随每次请求重复发送,这笔 token 开销换来了什么?什么时候「一条清晰规则」比示例更划算?
  3. 把步骤 4 的 JSON 提示连跑 5 次并统计不合法输出的比例——这个比例如何解释「提示是地板,结构化输出是保障」?

随堂测

逐题作答,即时反馈

0 / 3 已作答
  1. 1. 在 Prompt 五要素中,“背景”和“任务”的区别是什么?以邮件分类器为例,下列哪项属于“任务”?

  2. 2. 关于 CoT(思维链)提升准确率的原理,下列符合讲义表述的是?

  3. 3. 要让模型稳定输出 JSON 并用于生产环境,讲义推荐的做法是?