提示与上下文工程实操:模板、few-shot、格式约束
硬件前提
纯 API。这一篇决定你后面所有场景的质量上限,比换模型更值钱。
一个可复用的上下文骨架
按这个顺序拼,稳定且利于缓存:
[system] 角色 + 硬性规则 + 输出格式 + 拒答条件
[资料区] 检索到的片段,每段带来源标记
[示例区] 2~3 个「输入 → 期望输出」示例
[历史] 最近 N 轮(更早的换成摘要)
[本轮] 用户问题 + 一句格式重申
要点:固定部分在前,变动部分在后;关键约束在末尾重申一次(长上下文里中段最容易被忽略)。
提示写法:五条实操规则
- 说清「要什么」和「不要什么」:「只根据资料回答;资料里没有就回答『资料未提及』」比「请准确回答」有效得多。
- 给结构,不给形容词:「分 3 点,每点不超过 40 字」优于「简洁明了」。
- 拆任务:抽取 → 判断 → 生成,三步分开调用。一步到位的长提示最容易在中途跑偏。
- 让它可核查:要求「每个结论标注来源片段编号」,随后你可以程序化校验编号是否真的存在。
- 别用否定式堆叠:一堆「不要…不要…」会互相干扰,改成「只做 X」。
few-shot:给几个、给什么样的
| 任务类型 | 示例数 | 注意 |
|---|---|---|
| 分类 / 抽取 / 格式转换 | 2~5 | 必须包含边界样本和「空结果」样本,否则它会硬凑 |
| 风格模仿 | 2~3 | 示例风格要一致,混风格等于没给 |
| 复杂推理 | 1~2 | 给推理过程容易带偏(它开始模仿格式而非结论),谨慎用 |
示例的格式一致性比数量重要:示例用了 JSON,输出就会是 JSON;示例里有一个错格式,就会污染输出。
格式约束:从弱到强
| 强度 | 做法 | 可靠性 |
|---|---|---|
| 弱 | 提示里写「请输出 JSON」 | 常出多余文字、注释、代码围栏 |
| 中 | 给严格 schema + 示例 + 停止串 | 多数情况可用,仍需解析容错 |
| 强 | 使用 API 的结构化输出 / 工具调用模式 | 高,字段级保证 |
| 最强 | 强约束 + 解析失败自动重试一次(把报错回喂) | 生产可用 |
解析侧永远要写容错:剥掉代码围栏、截取第一个合法对象、字段缺失时重试而不是崩溃。
import json, re
def parse_json(text):
text = re.sub(r"^```(?:json)?|```$", "", text.strip(), flags=re.M).strip()
start, end = text.find("{"), text.rfind("}")
if start == -1 or end == -1:
raise ValueError("no json object")
return json.loads(text[start:end + 1])
上下文预算:先算再塞
塞资料之前,先分配额度(示例值,按你的窗口调整):
| 部分 | 预算 |
|---|---|
| system + 工具说明 | 5%~10% |
| 资料片段 | 40%~60%(宁少勿多) |
| 历史 | 20%~30%(超出换成摘要) |
| 预留输出 | 15%~25% |
塞得多不等于答得好:无关片段会主动制造错误答案。加资料后如果质量下降,先减资料而不是加模型。
常见失败与排查
| 现象 | 原因 | 处理 |
|---|---|---|
| 输出带解释文字,解析失败 | 只给了弱约束 | 上结构化输出 + 解析容错 + 重试 |
| 它开始自问自答 | 示例里出现了类似模式 | 检查示例,删掉多余轮次 |
| 忽略某条规则 | 规则太多或在中段 | 规则砍到 5 条以内,关键规则放末尾 |
| 资料给了不用 | 片段太远/太多 | 减少片段、把最相关放最后、要求标注来源 |
| 换模型就崩 | 提示过拟合某个模型 | 保留提示回归测试集,换模型必跑 |
完成标志
你有一份提示模板文件(版本化)+ 一组回归样例(输入 → 期望输出),改提示或换模型时跑一遍就知道有没有变差。没有回归样例,所有提示优化都是碰运气。