提示工程与角色设定

读完能写出一个带角色设定、few-shot 示例和结构化输出要求的提示,并解释这三味「配料」分别让模型的输出好在哪。

为什么只记结论不足以掌握提示工程与角色设定

学习“提示工程与角色设定”时,第一步不是记住结论,而是冻结输入、上下文、版本和成功标准。只有这些条件明确,提示工程与角色设定的含义才不会随着样例变化。正文已有的概念说明要与结构图、运行轨迹和失败样本互相印证,不能只凭最终输出看似正确就宣布完成。

结构分析从同样一句吩咐,会下指令和不会下,天差地别开始:列出参与者、职责、连接方向、生命周期和所有权,再沿正常路径追踪数据或控制流。每一条边都要说明为什么存在、谁创建、谁消费、谁负责清理;如果边界被跨越,必须能在证据中找到第一处异常。

机制验证要把第一味:给小特发「岗位说明书」——角色设定写成可以执行的条件。正常样本证明主路径,恰好边界样本验证等号和空值,单故障样本只破坏一个假设。三类样本使用同一份观察指标,避免因为测试口径变化而把偶然结果误认成规律。

成本分析同时记录时间、空间、延迟、耦合、可维护性和不可逆操作。第二味与全貌:提示的三层结构不是一句“可能失败”,而是可复现输入、预期停点、实际轨迹、错误分类与清理步骤。任何自动重试都要有次数、预算和幂等边界。

方案比较不能只列优点。需要给出直接实现、当前方案和至少一个替代方案,逐项比较复杂度、扩展点、故障隔离和团队认知成本。当问题规模很小或变化轴稳定时,更简单的实现往往更好;模式与框架必须由真实变化压力证明。

实现阶段把大结论拆成可检查的中间产物:配置快照、结构清单、状态转移、输入输出样本、日志摘要和测试结果。每个产物带来源与生成命令,下一阶段只消费已通过门禁的版本,避免旧缓存或隐式默认值污染结论。

解释结果时必须区分相关性与因果性、接口承诺与实现细节、设计意图与运行事实。对“第三味:给几个范例,让它照葫芦画瓢——few-shot 示例”的判断要由独立证据支持,并明确适用范围;一旦输入分布、版本、硬件或组织边界改变,就重新运行最小实验。

复盘从首个分叉开始,而不是从最后一个报错倒推。先比较冻结输入,再比较第一份结构化中间产物,随后检查状态、约束和副作用。这样可以把复杂系统的排错范围收缩到一个阶段,避免在多个层次同时修改造成新的不确定性。

迁移到真实项目时,先选择一个最小但有代表性的切片,保存改造前基线,再逐步引入“提示工程与角色设定”中的机制。每一步只改变一个变量并保留回滚点;性能、正确性、安全性和可理解性至少各有一项可量化指标。

最终验收要求读者能脱离页面重新画出结构、口述关键链路、实现最小版本、构造一个反例并解释失败位置。若只能复述名词而不能预测中间状态,说明知识仍停留在识记层,需要回到图示和实验重新验证。

本页用、

、、、建立统一坐标。先预测这些概念在结构图和运行轨迹中的位置,再操作实验控件;如果结果与预测不一致,停止在首个分叉,不要用后续补丁掩盖早期错误。

权威目录与核心概念逐项对照

  • 提示工程与角色设定
  • 同样一句吩咐,会下指令和不会下,天差地别
  • 第一味:给小特发「岗位说明书」——角色设定
  • 第二味与全貌:提示的三层结构
  • 第三味:给几个范例,让它照葫芦画瓢——few-shot 示例
  • 第四味:让它按表格填,别写散文——结构化输出
  • 动手一:三味配料,每加一味输出好一截
  • 动手二:散文 vs JSON,程序提取天差地别

可复现的最小实现

先把决策记录写成机器可读结构:

{
  "unit": "提示工程与角色设定",
  "inputFrozen": true,
  "scenario": "normal | boundary | single-fault",
  "firstDivergence": null,
  "cleanupRequired": true
}

再用同一条执行链处理三类样本:

type Evidence = { stage: string; expected: string; actual: string };
 
function verify(sample: unknown, expected: readonly Evidence[]) {
  const trace = runFromCleanState(sample);
  return expected.find((item, index) => trace[index]?.actual !== item.expected);
}

最后保存回归门禁,禁止失败样本静默通过:

normal      -> complete, invariant preserved
boundary    -> complete or explicit rejection
singleFault -> stop at first divergence, no stale output

用统一评分解释实验结果:

Q=Ccorrect+Ctrace+CrecoverRresidualQ = C_{correct} + C_{trace} + C_{recover} - R_{residual} Ccoverage=NverifiedNofficialC_{coverage} = \frac{N_{verified}}{N_{official}} Rresidual=P(failure)×I(impact)R_{residual} = P(failure) \times I(impact) Accept=(Ccoverage0.90)(RresidualRbudget)Accept = (C_{coverage} \ge 0.90) \land (R_{residual} \le R_{budget})

本章回顾

  • 提示工程与角色设定必须绑定冻结输入和明确成功标准。
  • 同样一句吩咐,会下指令和不会下,天差地别必须能画成结构并沿边追踪责任。
  • 第一味:给小特发「岗位说明书」——角色设定要由正常、边界和单故障样本共同验证。
  • 第二味与全貌:提示的三层结构必须保存第一处偏离和清理重建步骤。
  • 第三味:给几个范例,让它照葫芦画瓢——few-shot 示例决定方案是否可以进入下一阶段。

术语表

同样一句吩咐,会下指令和不会下,天差地别

你对私人助理小特说:「把这句话整理一下——后天下午去广州,预算别超 600。」他可能给你回一长段热情的废话:「去广州玩呀,早茶别错过哦~」也可能干脆利落地列出「城市:广州,时间:后天下午」。同样一颗大脑,差别全在你怎么吩咐

这一章就讲:怎么把吩咐写好——给小特发一份「岗位说明书」、给他几个范例照着做、再要求他按固定格式交活。三招下去,他的输出就从「没法用」变成「精准、规范、程序能直接接」。

没学会会怎样?你只能拿到时好时坏、忽长忽短、夹着闲聊的回答,每次都得自己再收拾一遍——更别提让程序去自动处理它了。

第一味:给小特发「岗位说明书」——角色设定

你交代差事前,最该先说清楚的是:你希望它以什么身份、按什么规矩来答。这就是:一句「你是严谨的订票助理,只按事实回答、不确定就说不知道」,就把小特从「热情的话痨」框成了「靠谱的办事员」。

它装在提示最前面、独立的一层里(叫 system,系统设定)。它不回答任何具体问题,只定调——后面不管你问什么,它都照这份说明书的身份和规矩来答。你会在下一节的 Demo 里看到:光加这一味,输出就从跑题闲聊变得聚焦正事。

第二味与全貌:提示的三层结构

你以为「发给大脑的提示」就是你手打的那句话?其实不是。真正送进大脑的,是拼成的一整段:最底层是刚说的系统设定(角色),中间是few-shot 示例,最上面才是你这一轮的用户消息

下面这张图把三层一层层拼给你看,点播放,看一段提示是怎么组装出来的:

记住这个全貌:你能调的不只是「问什么」,还有「给它配什么角色、给它看什么范例」。后两层正是把输出从「能用」逼到「好用」的关键。

第三味:给几个范例,让它照葫芦画瓢——few-shot 示例

中间那层装的是:与其啰嗦地描述「我要城市、日期、方式……请用这种格式」,不如直接甩给它一个例子——「用户说『明天去上海』,你就回 {"city":"上海","date":"明天"}」。它一看就懂,照着这个格式和字段答。

这是「示范」胜过「讲道理」:一个好范例,抵得过一大段文字要求,还更稳。下一节 Demo 里你打开「few-shot」开关,会看到输出立刻对齐到范例的字段和格式。

第四味:让它按表格填,别写散文——结构化输出

最后一味,是要求模型。同一份订票需求,模型既可以写成一段散文「您是想后天坐高铁去广州对吧」,也可以吐一个 JSON {"city":"广州", ...}。对人来说散文更亲切,但对程序来说,散文是灾难——字段埋在句子里,只能靠正则去猜,换个措辞就崩。

JSON 则是程序的母语:一行 json.loads 就拿到一个字典,按 key 取值,稳。下一节第二个 Demo 会把「散文」和「JSON」摆一起,让你看程序从两者里提取字段的成功率天差地别。

动手一:三味配料,每加一味输出好一截

光读不如自己拨开关。下面这个 Demo 把「角色设定 / few-shot 示例 / 输出格式要求」做成三个开关,针对同一句固定的口语订票需求,每打开一味,下方「模型输出」就切到对应的示意输出,并给一个质量评级。

猜一猜:现在三个开关全关,模型在自由发挥。只打开「① 角色设定」一味,输出会变好多少?能直接被程序解析吗?先猜一个,再拨开关验证。

可交互三味配料:每加一味,模型输出好一截

用户口语需求(固定不变)

帮我订后天下午去广州的高铁票,预算别超 600。

已加配料:0 / 3(什么都没加,看大脑自由发挥)

模型输出(示意)差 · 跑题/啰嗦/没法解析
哈喽!去广州玩呀?广州可是个好地方,塔下夜景很赞,早茶也别错过~
要订高铁的话我建议你提前看看天气哦。需要我帮你推荐景点吗?😄

程序提取字段:✗ 解析脆弱 / 失败

没给任何约束,大脑自由发挥:闲聊跑题、没提取要点,程序完全没法用。

说明:以上输出为按规律预写的示意非实时调用模型——本章用 curated 输出演示每味配料的作用,真实模型每次输出会略有不同,但「加配料 → 输出更可控」的趋势一致。

你会看到一条清晰的阶梯:全关时跑题闲聊、没法用;加「角色」就聚焦了正事但还是散文;再加「few-shot」字段对齐了;三味全开才吐出干净的 JSON、程序一行就能解析。每味配料补的洞各不相同——角色管「别跑题」、few-shot 管「字段对齐」、格式要求管「能被程序读」。

动手二:散文 vs JSON,程序提取天差地别

第二个 Demo 把焦点放在最后一味。同一桩「抽取订票字段」的任务,用开关切换模型的两种输出:自由文本(散文)和 JSON 结构化。右侧「程序提取」面板会逐字段显示成功 ✓ 还是失败 ✗。

猜一猜:让模型写成散文「您是想后天坐高铁去广州,预算六百块上下」,程序想抠出 type=高铁budget=600 这两个字段,能稳定抠到吗?关掉开关切到自由文本试试。

可交互同一任务,两种输出:散文 vs JSON,程序提取天差地别

模型输出(示意)· JSON 结构化

{"city": "广州", "date": "后天", "type": "高铁", "budget": 600}

程序提取字段(data = json.loads(输出),按 key 取):

  • city(城市)"广州"
  • date(日期)"后天"
  • type(交通方式)"高铁"
  • budget(预算上限)600

稳定提取成功:4 / 4 字段

JSON 这一侧,一行 json.loads 就拿到一个 dict,按 key 取值,稳。这就是为什么要让小特「按表格填」。 两段输出均为预写示意(非实时模型)。

(关掉开关切到「自由文本」,看程序怎么抠瞎)

JSON 那侧四个字段全绿——json.loads 拿到字典直接按 key 取。散文那侧,「坐高铁」对不上约定值、「六百块上下」根本不是数字,抠取一片飘红。这就是为什么:凡是要交给程序处理的输出,必须让模型按结构化格式填,别让它写散文。

代码逐段拆解:把三味配料写成真实的 Python

讲了这么多,落到代码里到底长什么样?下面我们一步步把上面那个「订票需求抽取」的提示用 Python 写出来。聊天类 LLM 的接口几乎都长一个样:你传一个 messages 列表,每条是一个带 rolecontent 的字典。最朴素的提示就两条——一条 system(角色设定),一条 user(你的需求):

client = OpenAI()  # 任意兼容 OpenAI 风格的客户端
 
messages = [
    # 第一层:system —— 角色设定(小特的「岗位说明书」)
    {"role": "system", "content": "你是严谨的订票助理,只按事实回答,不确定就说不知道。"},
    # 第三层:user —— 你这一轮真正要它办的事
    {"role": "user", "content": "帮我订后天下午去广州的高铁票,预算别超 600。"},
]
 
resp = client.chat.completions.create(model="gpt-4o-mini", messages=messages)
print(resp.choices[0].message.content)

注意 systemuser 是两条独立的消息,靠 role 区分。角色设定必须放进 role: "system",而不是塞进 user 那句话里——这是把行为约束和具体请求分开的标准做法。

现在加第二味——few-shot 示例。做法很直接:在 system 之后、真正的 user 之前,塞进几对「示例 user → 示例 assistant」消息,假装这段对话之前已经发生过。模型看到这几个范例,就会照着范例里 assistant 的格式来答:

messages = [
    {"role": "system", "content": "你是严谨的订票助理,只按事实回答。"},
    # —— few-shot 示例:一对「示例输入 → 示例输出」,给模型打样 ——
    {"role": "user", "content": "明天去上海"},
    {"role": "assistant", "content": '{"city": "上海", "date": "明天", "type": "高铁", "budget": null}'},
    # —— 真正的用户消息(模型会照上面 assistant 的格式来答)——
    {"role": "user", "content": "帮我订后天下午去广州的高铁票,预算别超 600。"},
]

这就是 few-shot 的全部机巧:用范例代替啰嗦的描述。你没写一个字解释「请输出 city、date 字段」,但模型从那对示例里全看明白了。想更稳就多给一两对范例(few-shot 的 few 就是「几个」),但范例本身的质量要高、要覆盖典型情况。

第三味是结构化输出。光给范例还不够稳——得在 system明确要求只输出 JSON,很多 API 还提供 response_format 强制约束格式。两手都上,最保险。拿到回答后,用 json.loads 把它解析成 Python 字典:

import json
 
messages = [
    {"role": "system", "content": "你是订票信息抽取器。只输出 JSON,不要任何多余文字。"},
    {"role": "user", "content": "明天去上海"},
    {"role": "assistant", "content": '{"city": "上海", "date": "明天", "type": "高铁", "budget": null}'},
    {"role": "user", "content": "帮我订后天下午去广州的高铁票,预算别超 600。"},
]
 
resp = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=messages,
    response_format={"type": "json_object"},  # 强制模型输出合法 JSON
)
data = json.loads(resp.choices[0].message.content)  # → Python dict,按 key 取
print(data["city"], data["budget"])  # 广州 600

json.loads 把模型那段 JSON 文本变成真正的 Python 字典 data,于是 data["city"] 就稳稳地拿到「广州」。这正是动手二里 JSON 那侧「四字段全绿」的代码版本。

最后一招让这套提示能复用:把固定部分写成模板,变化部分用占位符,用 str.format 填进去。这样同一套角色 + few-shot,能套给无数条不同的用户需求:

# 把固定的「角色 + 输出要求」写成模板,{request} 是每次变化的用户需求
SYSTEM_PROMPT = "你是订票信息抽取器。只输出 JSON,不要多余文字。"
USER_TEMPLATE = "请抽取这条订票需求的字段:{request}"
 
def build_messages(request: str) -> list[dict]:
    return [
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": "明天去上海"},
        {"role": "assistant", "content": '{"city": "上海", "date": "明天"}'},
        {"role": "user", "content": USER_TEMPLATE.format(request=request)},
    ]
 
# 同一套模板,喂不同需求复用
msgs = build_messages("帮我订后天下午去广州的高铁票,预算别超 600。")

USER_TEMPLATE.format(request=...) 把用户那句口语填进固定模板。这一步看着小,却是工程化的关键:提示一旦稳定,就把它冻成模板,只让真正变化的部分(用户需求)走占位符——而不是每次手拼字符串。这就是把「写好一个提示」变成「能复用的提示函数」。

容易踩的坑

小结

  • 提示是三层结构拼成的一整段:系统设定(角色)+ few-shot 示例 + 用户消息——大脑读到的是整段,不是你单打的那句
  • 角色设定(放 system)给小特发岗位说明书、定调、管「别跑题」;写短而准,别堆长人设
  • few-shot 示例(塞在 system 与 user 之间的「示例 user→assistant」对)用范例代替啰嗦描述、管「字段对齐」;范例必须自身就是标准答案
  • 结构化输出response_format + 提示要求 + json.loads)管「程序能稳定解析」;光靠嘴说不够,要强制 + 锚定 + 兜底
  • 提示稳定后冻成模板,固定部分写死、变化部分用 format 占位符复用,把「写好一个提示」变成「能复用的提示函数」

讨论

评论区加载中…