如果一段流程要交给模型执行,顺序建议是:先把提示词结构、输出字段和验收校验固定下来,再让模型接手。判断标准很直接:如果一条输出无法被脚本判定为通过或不通过,那么就不应该让它进入下游流程。GPT-6.1 Terra 这类模型适合承担“读输入、做归类、写理由”这一段,而字段拼装、数值计算、写入数据库、发通知,通常更适合交给脚本。把提示词写得细,作用不只是让模型更好,更是让校验函数有明确可校验的对象;验收标准落在脚本侧,模型侧只负责产出结构化结果,以及“不确定就说不确定”。
适合场景:流程步骤固定、输入格式相对稳定、单步输出能被字段化描述。操作动作:先列出步骤归属(人工、脚本或模型),再写带字段约束的提示词,并把验收标准实现成校验函数,最后用历史样本回放对比人工结论。验证方式:看校验函数的失败率、失败原因分布,以及回放对照表中判定差异的条数。风险边界:涉及金额、权限、对外承诺的步骤不建议只由模型判定;校验规则本身也需要随业务规则变更同步维护,否则会误判或漏判。
把流程步骤逐条标注归属
先把现有流程拆成单步,每一步只回答一个问题,再逐个标注类型和归属方。判断类的典型特征是“需要理解自然语言、边界模糊”;生成类的特征是“要有可读文本”;纯计算类的特征是“有唯一正确答案、可复算”。下面是常见拆分方式,实际列数按你的流程增减。
| 步骤 | 类型 | 归属方 | 判断依据 |
|---|---|---|---|
| 解析用户提交内容 | 纯计算类 | 脚本 | 格式固定,正则或 schema 校验即可 |
| 判断诉求类别 | 判断类 | 模型 | 表述多样,规则枚举难以覆盖 |
| 生成回复草稿 | 生成类 | 模型 | 需要成段可读文本 |
| 计算金额或时长 | 纯计算类 | 脚本 | 结果可复算,模型不该参与 |
| 决定是否越权操作 | 判断类 | 人工(模型只给建议) | 后果不可逆,需人工确认 |
| 写入工单、发通知 | 纯计算类 | 脚本 | 属于副作用,由代码控制 |
标注完成后,把“模型”那一列合并起来看:如果模型输出要跨两个以上脚本步骤才能被使用,通常说明字段拆得不够细,或者某一步本来就该由脚本完成。验证方式很简单:对每一步问一句“这一步出错时,能不能用一段代码判定它错了”,答不出来的步骤先不要交给模型。
写一版带字段约束的提示词模板
模板的作用是把输出结构提前固定,而不是把语气写得更像人。建议固定五段:角色、任务、输入占位、输出字段列表、禁止输出内容。输入占位用明确的标记名,便于脚本替换和后续日志回放。
角色:你是流程中的“分类与说明”环节,只输出结构化结果,不做对外承诺。
任务:阅读 <INPUT> 中的内容,判断诉求类别并给出理由。
输入:
<INPUT>
{{raw_text}}
</INPUT>
<RULE>{{category_enum}}</RULE>
输出:仅返回一个 JSON 对象,字段如下,不得增删:
action: 字符串,取值必须是 <RULE> 中列出的枚举之一
reason: 字符串,10-200 字,说明判断依据,引用原文片段
confidence: 0 到 1 之间的小数
ref_id: 字符串,格式为 2-3 位大写字母加连字符加 4-8 位数字,如 AB-1234
need_human: 布尔值,证据不足时为 true
禁止输出:
- 任何 JSON 之外的说明文字、前后缀、Markdown 围栏
- 输入中未出现的事实、编号、金额、时间
- 结论性承诺语句(如“保证”“一定”“已经处理”)
- 空字段、null、占位符如“待补充”
若证据不足,令 need_human 为 true,confidence 不高于 0.5,并在 reason 中写明缺什么。这段模板里真正起作用的是三处:枚举来自外部注入而不是模型自由发挥;ref_id 有明确格式以便校验;“证据不足”有专门出口,避免模型被迫编造。替换项只有 {{raw_text}} 和 {{category_enum}},其余属于固定骨架,改动时应走版本管理并记录变更原因。
把验收标准落成可执行的校验函数
校验函数放在调用模型的脚本里、解析 JSON 之后、写库之前。它只做机械判断,不判断语义好坏;语义好坏留给回放环节。下面是一个通用骨架,字段名按上一节调整。
import re, json
REQUIRED = {"action", "reason", "confidence", "ref_id", "need_human"}
ALLOWED = {"approve", "reject", "hold"}
def validate(raw: str) -> tuple[bool, list[str], dict | None]:
errs = []
try:
out = json.loads(raw)
except json.JSONDecodeError:
return False, ["not_json"], None
if not isinstance(out, dict):
return False, ["not_object"], None
missing = REQUIRED - out.keys()
if missing:
errs.append("missing:" + ",".join(sorted(missing)))
if out.get("action") not in ALLOWED:
errs.append("bad_enum")
c = out.get("confidence")
if not isinstance(c, (int, float)) or not (0 <= c <= 1):
errs.append("bad_confidence")
r = out.get("reason") or ""
if not (10 <= len(r) <= 200):
errs.append("bad_reason_len")
if re.search(r"<[^>]+>", r):
errs.append("reason_html")
if not re.fullmatch(r"[A-Z]{2,3}-\d{4,8}", str(out.get("ref_id", ""))):
errs.append("bad_ref_id")
if not isinstance(out.get("need_human"), bool):
errs.append("bad_flag")
return (not errs), errs, out不通过时建议只在三种处理里选一种,选择依据写进代码注释,不要临场决定:
- 同轮重试一次:把 errs 列表拼回提示词再调一次。适用于错误集中在格式、字段缺失、围栏未去掉这类机械问题,且重试预算明确为一次,避免无限循环放大成本。
- 规则兜底或转人工:直接置 need_human 为 true 并进人工队列。适用于业务允许 hold、错误属于证据不足或判定分歧的情况,宁可慢一步也不要把不确定结果写进下游。
- 整批中止并告警:同一错误在多条记录上重复出现时停止本批。通常说明是提示词或解析层改动引入的回归问题,继续跑只会污染数据;此时应先定位版本差异,再决定是否重放。
用历史样本回放对比人工结果
上线前最值得做的一步,是拿已经有人工结论的历史样本,用同一版提示词跑一遍,逐条对照。抽样方式建议按业务分层:每个类别取几条、覆盖长文本和短文本、覆盖当时被人工判为“需要补充信息”的样本——这类样本最能暴露模型是否会硬猜。样本量不追求大,先取几十条跑通流程,人工结论必须是当时归档的原始结论,不要事后重判,否则对比没有意义。
差异类型建议固定几类,便于统计和归因:结构差异(字段缺失、类型错误、多余文本)、事实差异(ref_id 指向不存在的记录、理由中出现输入里没有的信息)、判定差异(结论与人工不同但理由成立)、表达差异(理由过长、含禁用词、可读性差)。结构差异和事实差异应优先修提示词与校验函数;判定差异需要回到业务规则上确认到底谁对;表达差异一般不影响流程,可以放到后面处理。
| 样本ID | 人工结论 | 模型结论 | 差异类型 | 是否可接受 | 处理动作 |
|---|---|---|---|---|---|
| 样本-001 | reject | reject | 无 | 是 | 保留为回归用例 |
| 样本-002 | hold | approve | 判定差异 | 否 | 补充枚举说明后重跑 |
| 样本-003 | approve | 字段缺失 | 结构差异 | 否 | 加严 ref_id 约束并重试一次 |
| 样本-004 | reject | reason 含原文未出现的时间 | 事实差异 | 否 | 提示词增加禁止编造条款,改校验函数拦截 |
回放结果只有一个用途:决定“能不能接手、接手哪几步”。如果判定差异集中在某一类诉求上,就把这一类继续留给人;如果结构差异反复出现在同一字段,就回去改提示词的字段说明和校验函数,而不是加一句“请严格输出 JSON”。验证方式是把这份对照表作为回归基线存档,后续每次改提示词或换模型版本,都用同一批样本重跑,看新增的差异落在哪一类。