跳到正文
Elaine Blog
返回

怎样用示例和步骤教会模型完成任务

更新于:
Prompt Engineering

同样一句“登录不了”,可能是忘记密码,也可能是账号被别人改了密码。把规则写进 Prompt 后,模型仍可能混淆这两种情况。此时需要补的往往是判断边界,而不是更强烈的语气。

本篇继续使用工单分类:billing 表示账单诉求,security 表示疑似安全事件,general 表示明确的普通咨询,unknown 表示信息不足。我们先看示例怎样传递边界,再看复杂任务何时值得拆成步骤。所有样本及预测对照都是教学构造,不是模型实测。

示例是在补充边界,不是在装饰提示

先只给规则,让模型完成任务,常称为 zero-shot;再加入几组输入与期待输出,常称为 few-shot。只提供一组完整输入输出时也称为 one-shot;这些名称描述示例数量,不是不同的训练算法。示例的价值不在数量,而在于展示抽象规则容易产生分歧的位置。

例如“忘记密码,怎么重置”是 general、low,因为它没有提供账号遭入侵的证据;“我没操作却收到异地登录提醒”是 security、high。二者都包含登录问题,如果示例只展示特别简单的退款消息,就解释不了这条边界。

另一个有用对照是“订单 A100 申请退 99 元”和“这个订单帮我退款”。前者有明确编号和金额,后者两个字段都应为 null。这样,示例说明了“提取”与“补全”的差别。模型不用为了满足格式而编造缺失信息。

示例之间必须服从同一套规则。如果说明要求未知金额用 null,示例却写 0,模型会同时接收两种信号。把更多矛盾示例塞进上下文,通常不会自动得到更清楚的任务。每加一条示例,都应先检查它在澄清什么边界。

Few-shot 没有偷偷执行一次微调

一条示例由输入和期望输出组成。模型把这两部分当成上下文,后续位置通过 Attention 读取它们,利用训练中获得的模式识别能力完成当前任务。这通常称为 in-context learning(上下文学习)。普通推理调用没有反向传播,也没有因为本次示例而更新权重;下一次请求如果不带这些示例,就不能假定它仍然拥有这份任务约定。GPT-3 的 Few-shot 研究明确区分了上下文示例与梯度更新。

“利用上下文”是可观察的计算事实,但不能据此声称每个模型都在内部运行同一种分类算法。对博客读者更有用的观察是:换标签定义、换示例顺序、换输入表达,都可能改变结果,需要用同一组问题比较。

假设只给十条退款示例,没有安全、普通咨询或信息不足示例。它们会反复展示 billing 的输出模式,却不解释 billing 的边界。即使数量很多,也未必比“忘记密码”和“陌生登录”的两条对照更有帮助。示例选择至少要同时考虑相似性和覆盖面:相似案例提供局部参考,边界案例防止把表面相似当成同一类别。

动态选择示例时,也要保留这个原则。可以先按输入主题找相关示例,再保留必要的相邻类别对照;不能把保留测试集的答案检索进 Prompt,然后声称模型在测试集表现优秀。新增示例还会增加输入长度。例如规则 600 Token、当前消息 400 Token、每条示例 150 Token,放 4 条时共约 1600 Token,放 20 条则约 4000 Token,尚未计算消息模板和输出预算。这是教学预算,实际长度由目标 Tokenizer 计算。

长输入中若同时出现旧政策、新政策和历史结论,单纯把关键句放最后也不能彻底消除冲突。应在组装前选定当前有效版本,明确哪些是引用历史,去掉已失效且不需要比较的规则。位置安排可以帮助可读性,版本选择应由应用完成。

任务复杂以后,怎样安排中间步骤

现在客户说:“订单 A100 原价 199 元,用了 20 元券,实付 179 元,已经退过 50 元,我还想退 129 元。”一次分类需要抓住的是用户请求的 129 元,而不是最先出现的 199 元。如果提示只有“提取金额”,就连人工都不知道该抽哪一个。

先要求识别金额的语义角色,再选择 requested_amount,能把错误定位到更具体的位置。内部工作记录可以列出“原价 199、优惠 20、实付 179、已退 50、当前请求 129”;最终仍只返回约定五字段。这个中间记录是面向任务的可检查数据,不是模型内部计算过程的完整读数。

如果后来需要核算可退上限,应通过订单系统取得实付和累计退款,并由程序计算;不能把客户陈述直接当成账本。即使 179 - 50 = 129 恰好正确,也只验证了算术,没有验证输入金额是真的。

Chain-of-Thought(思维链)提示通过中间推导示例或步骤安排帮助解决一些多步问题,相关研究在算术、常识与符号推理任务上观察到了收益。它不意味着所有任务都应输出长篇推理,也不保证写出的理由忠实反映模型为何得出结论。原论文的实验范围应与当前工单任务分开看。

对本例,要求“引用用于分类的短原文”和“区分金额角色”比笼统要求“想得更深入”更容易检查。若模型服务提供专门的推理配置,应遵循它的接口;应用通常只需要结果与简短可核验依据,不需要索取完整内部思维链。

当一步输出确实是下一步必需的输入时,可以把一次大任务拆成多个调用:提取候选信息 → 对照原文校验 → 按业务规则分类。这样每一步都能独立查看,但会增加延迟和中间结果传递成本;前一步把陌生登录漏掉,后一步也可能跟着错。因此应保留必要原文,而不是只把有损摘要向后传递。

有些人会生成多个答案再投票。Self-consistency 的原始思路是采样多条推导路径,聚合最终答案;它在一些推理基准上有帮助,但共享同一条错误规则的多次生成仍可能一起错。研究说明不能被解释成“多数票就是真相”。对工单分类,先修复标签边界通常比额外调用五次更值得尝试。

把中间步骤写成可以核对的记录

“先提取,再判断”只有在中间结果可检查时才有意义。上面的多金额消息可以记录角色、原文片段和字符位置:requested 对应“129”,paid 对应“179”。检查位置能发现凭空编出的数字,检查角色才能发现“数字出现过,但抽错了对象”。

下面把两步放进程序。提取结果由作者预置,程序展示证据检查和确定性计算;它没有调用模型,不代表完成了自然语言金额抽取。完整文件见 evidence_steps.py下载

"""从已标注的证据位置演示金额角色选择;不假装正则或固定标注就是模型。"""
from decimal import Decimal

text = "订单 A100 原价 199 元,实付 179 元,已退 50 元,申请再退 129 元。"
# 这些角色与片段由作者标注。真实提取器可以是模型,但输出必须再核对。
annotations = [
    ("paid", "179"), ("already_refunded", "50"), ("requested", "129"),
]
records = []
for role, quote in annotations:
    # 本教学输入中各数值只出现一次;一般文本需要提取器提供精确位置。
    start = text.index(quote)
    records.append({"role": role, "start": start, "end": start + len(quote), "quote": quote})

for item in records:
    # 先检查切片一致;这只证明文字确实出现,不证明角色判断正确。
    if text[item["start"]:item["end"]] != item["quote"]:
        raise ValueError("证据位置与原文不一致")

requested = [item for item in records if item["role"] == "requested"]
if len(requested) != 1:
    raise ValueError("请求金额不唯一,需要澄清")
print("客户请求金额:", Decimal(requested[0]["quote"]))

# 后台事实单独给出,不能拿客户陈述充当账本。
ledger = {"paid": Decimal("179.00"), "refunded": Decimal("50.00")}
remaining = ledger["paid"] - ledger["refunded"]
print("账面剩余金额:", remaining)
# 数值相等仍不构成退款批准;资格、权限和状态还需业务流程核验。

这段程序刻意使用两套数据:annotations 表示客户说了什么,ledger 表示后台记录了什么。即使它们数值相同,也不能删除这层区分。实际项目中,客户可能说实付 179 元,而账本只有 159 元;程序必须保留冲突,不能让模型用一个流畅解释抹平差异。

否定、时间与多意图怎样改变判断

“不是账号被盗,是我忘了密码”含有“被盗”两个字,但它在否定范围内。关键词匹配会很容易误判。一个有用的边界示例应把完整句子保留下来,说明 general 的依据是普通密码重置,而不是简单教模型“遇到不是就忽略后文”。“不是只有我,其他人也收到陌生登录提醒”里的“不是”恰好没有否定风险。

时间也会改变当前任务。“昨天申请退款,今天决定换货”表达了请求变更,不应把昨天的意图当成当前退款申请。不过,如果客户在报告“昨天发生陌生登录,今天发现钱少了”,事件发生在过去不代表已经解决。本例以“疑似未解决的安全事件优先”为规则,不能把“现在”当作唯一高优先级线索。

多意图则需要明确选择。“账号被盗,订单也被扣款”在本例优先 security;“A100 退款、A101 换货”超出了单订单接口,应该进入澄清或拆单流程。不能为了凑出一个类别和一个编号而丢掉半个请求。Few-shot 展示的是已确定的处理策略,不能替业务负责人决定所有冲突。

设计示例时,可以成对改变一个条件:忘记密码与密码被他人修改,历史退款与当前退款,明确金额与金额未提供。这种对照使模型和读者都能看见决策依据。单纯换十个订单号,知识增量通常很小。

需要外部事实时,把推理与工具观察分开

“还有多少可退”既需要算术,也需要订单事实。若上下文没有账本,单纯延长推理无法得到真实累计退款额。模型可以提出查询订单的动作,由程序校验并执行,再把返回值交给后续判断。这个循环中,模型提出动作、工具实际执行、观察结果进入上下文,是三个不同步骤。

ReAct 一类方法将推理与外部动作交替组织。理解它时,关键是下一步会利用真实观察修正判断,而不是把“我要查询”这句话当成已经查到。固定三步流程通常可以直接写成工作流;只有下一动作确实依赖上一次结果时,才需要更自由的动作选择。原理与实验见 ReAct 论文

以工单为例,查询发现订单不存在,应进入澄清;查询发现订单已退款,应停止重复申请;查询超时则属于工具失败,不能总结为“无此订单”。循环必须规定最大调用数和停止条件,权限由工具实现控制。这里介绍的是如何组织任务,具体授权在第五篇展开。

多调用一定比一次调用好吗

考虑三个方案:一次直接分类、一次先输出简短证据再分类、两个调用分别提取与分类。第一种成本最低;第二种增加输出长度但仍在同一次生成中;第三种形成可独立验证的步骤,同时增加网络往返和错误传递。应先看失败究竟发生在哪里,再选方案。

假设每次调用平均输入 1000 Token、输出 100 Token。直接调用一次约处理 1100 Token,独立采样五次约为 5500 Token,尚未计算聚合调用。并行能缩短部分等待,却不会让总调用费用消失。这些是预算示意,不能当作具体模型的价格或速度。

Self-consistency 聚合的是多条路径的答案;自我检查则让模型依据标准重新审视一个候选。后者如果只问“你确定吗”,可能在没有新证据时把正确答案改错。更好的检查输入是明确规则、原文与候选,输出指出违反的具体条件;格式、数值运算和权限仍优先用程序验证。

动态示例也不是必须上向量库才能实现。开始时按已知业务主题选择几组已审核示例就够了;规模增长后再引入检索。无论如何选择,都要记录本次用了哪些示例,否则无法重建同一条请求。下一篇把这些方法串成从需求到完整 Prompt 的写作过程


分享这篇文章:

上一篇
Prompt 为什么能改变模型回答:从生成原理到上下文组织
下一篇
如何写好一个 Prompt:从需求到可用版本