本课目录(23 节)
🎯 本节目标
学完这一节,你能:
- 写出结构化 Prompt(五要素框架),不再写"帮我整理下客户反馈"这种让模型抓瞎的话
- 用 Few-shot 示例 和 CoT 思维链 提升复杂任务准确率,还能讲清"为啥这样有效"
- 让模型稳定输出 JSON(这是 FDE 把 AI 从"聊天玩具"变成"工程系统"的关键)
提示词工程在 FDE JD 出现率 55%,是技术栈第一项。后面所有模块(RAG / Agent / 评估)都建立在它之上。
🍎 生活化类比
写 Prompt = 点外卖。
回想你点外卖填的那张单子,为啥要勾那么多选项?因为一句"我要吃饭"没法让店家知道你到底要啥。一份清楚的外卖单,有五块东西:
- 给谁下单(角色):给厨师下单,还是给跑腿小哥下单?
- 现在啥情况(背景):朋友聚餐?一个人吃?赶时间?
- 具体要什么(任务):两份番茄炒蛋、一份米饭
- 不要什么(约束):不要辣、不要香菜
- 怎么给你(输出格式):打包盒、要不要餐具
少一块会怎样?少"背景",店家不知道份量做多大;少"约束",可能做出你吃不了的味道;少"输出格式",直接给你端盘子里——可你在家没法用。写 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 句话回顾:
- Prompt 五要素 = 点外卖:角色 / 背景 / 任务 / 约束 / 输出格式,五块都给齐模型才稳
- Few-shot(看成品)和 CoT(写过程)分别解决"规则难描述"和"推理复杂",前提是用对地方
- JSON 输出三档约束:口头要求(松)→ response_format(中,生产首选)→ Function Calling(严)
下一节:进入模块 2,用低代码平台(Coze / Dify) 把这些能力快速做成能给客户用的应用——30 分钟交付。
🏋️ 配套自练
- Anthropic Prompt 互动教程 — 重点做第 6 章(思维链)和第 7 章(Few-shot),带答案(免费)
- PromptClaude 练习场 — 写 prompt 立即被 Claude 打分,看哪里能改进(免费、需 Anthropic Key)
- PromptEval 每日一练 — 5 个能力轨道,从基础到护栏,每天一道(免费)
🔗 延伸阅读
- Prompt Engineering Guide(promptingguide.ai)
- OpenAI Prompt Engineering 最佳实践
- Anthropic Prompt Engineering
← 上一节:第一次调通 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。
前置条件
- 到 console.groq.com 注册并创建免费 GROQ_API_KEY(无需信用卡)。
- Colab 或本地 Jupyter(Python 3.10+,全程零 GPU)。
- 安装课程助手包 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 模型内部已自带,显式要求可能画蛇添足。
自查问题
- 遇到「模型做不到」,为什么第一步永远是改指令,而不是先堆 few-shot 或上工具?
- few-shot 示例随每次请求重复发送,这笔 token 开销换来了什么?什么时候「一条清晰规则」比示例更划算?
- 把步骤 4 的 JSON 提示连跑 5 次并统计不合法输出的比例——这个比例如何解释「提示是地板,结构化输出是保障」?
随堂测
逐题作答,即时反馈
1. 在 Prompt 五要素中,“背景”和“任务”的区别是什么?以邮件分类器为例,下列哪项属于“任务”?
2. 关于 CoT(思维链)提升准确率的原理,下列符合讲义表述的是?
3. 要让模型稳定输出 JSON 并用于生产环境,讲义推荐的做法是?