很多做 LLM 应用的同学,都会遇到一个经典场景:
一条长文本,要求模型一次性抽一堆字段,还要严格 JSON 输出。
规则写得越来越长,few-shot 越堆越多,但效果总是"差点意思"。
这篇文章讲一个常见的场景:电商客服聊天记录抽取,聊聊如何把"单步抽取"重构为"多步推理管线",以及中间踩过的坑和优化经验。
一、场景设定:从客服对话中抽订单与问题信息
需求:对电商客服对话做结构化抽取,目标字段大致如下:
订单号商品名称数量退款/退货意向问题类型(物流/质量/错发/售后等)情绪/满意度(可选)
原始对话可能是这样:
客:上个月我在你们家买的那台黑色扫地机器人(订单号 2024-09-123456),用了不到一周就开始卡死不动了。
客:昨天快递终于送到了,但包装都烂了,感觉像旧的。
客:我想退货或者换新。
客:上次问你们的客服,说给我登记了,结果一直没人联系。
目标是抽出若干"事件",比如:
- 一个是"商品质量问题+退货意向"
- 一个是"物流包装问题"
- 等等。
如果让模型一次性输出一个大 JSON 数组,同时填所有字段,很快会遇到:
- 长对话、上下文复杂,模型要同时做:
- 事件检测 + 订单识别 + 商品归因 + 问题分类 + 情绪判断
- 规则一多,样例一堆,LLM 会"选择性失明":
- 有时候字段全填,有时候只填一半
- 有时候乱编订单号/商品名
- 一句对话里有多个事件(质量 + 物流 + 客服体验),拆分经常乱
这是典型的"任务过载"。
二、为什么要改成多步?------先找"事件",再填细节
核心思路很简单:
把"从对话 → 结构化记录"拆成两步:
Step1:事件检测 & 粗标注
Step2:单事件细粒度抽取
Step1:事件检测(Conversation Event Detection)
只解决一个问题:这段对话里有哪些"服务事件"?
例如,针对一整段对话,输出:
json
[
{
"事件原文": "上个月我买的那台黑色扫地机器人,用了一周就卡死不动了。",
"粗略订单标识": ["2024-09-123456"],
"粗略问题类型": ["商品质量问题"],
"是否退款意向": false
},
{
"事件原文": "昨天快递终于送到了,但包装烂了,感觉像旧的。",
"粗略订单标识": ["2024-09-123456"],
"粗略问题类型": ["物流/包装问题"],
"是否退款意向": false
},
{
"事件原文": "我想退货或者换新。",
"粗略订单标识": ["2024-09-123456"],
"粗略问题类型": ["售后/退换货"],
"是否退款意向": true
}
]
特点:
- 不要求模型在这里就把"订单号格式""问题子类别""满意度评分"弄得很精确;
- 只要求事件切得对 ,并且给出粗粒度标签 :
- 这条话是关于哪一个订单;
- 粗略属于哪个问题大类;
- 有没有直白的退货/退款意向。
Step2:单事件细粒度抽取(Event-Level Structuring)
对 Step1 检出的每个事件,再单独跑一遍模型,抽出完整字段:
json
[
{
"订单号": ["2024-09-123456"],
"商品名称": ["黑色扫地机器人"],
"数量": ["1"],
"问题类型": ["商品质量问题"],
"退款意向": ["退货或换新"],
"情绪": ["不满"],
"时间信息": ["上个月", "不到一周后"],
"客服处理评价": ["上次说登记了,但一直没人联系"]
}
]
每次 Step2 的上下文只是一小段"事件原文" + Step1 的粗略信息 →
模型任务简单得多,也更容易通过规则+示例牢牢控住字段。
三、Step1 提示词设计:只做"事件级别"的事
给 LLM 的系统提示大致是:
- 你是客服对话分析助手;
- 只做一件事:识别对话中的服务事件并粗标注;
- 输出一个 JSON 数组,每个元素代表一个事件。
字段设计(Step1)
事件原文:对话中与该事件直接相关的那几句(可以跨两三句)粗略订单标识:能明显标识订单的片段(完整订单号或"上次买的黑色扫地机器人"等)粗略问题类型:粗粒度标签,如:商品质量、物流/包装、售后/退换货、客服体验是否退款意向:true/false
关键规则示例
- 拆分规则 :
"商品有问题,而且快递也很慢"→ 至少两个事件:质量问题 + 物流问题"我想退货或者换新"→ 单独一个"售后/退换货"事件
- 时间上下文保留 :
- 如果一句话里既有时间又有问题描述,要把时间一起截进
事件原文,方便 Step2 做时间归因; - 但像"半年前买的,现在才发现问题",就要在 Step2 再区分"购买时间 vs 问题发现时间"。
- 如果一句话里既有时间又有问题描述,要把时间一起截进
few-shot 重点
- 主要教模型三件事:
- 一句多事要拆开;
- 事件原文要带上与该事件强相关的时间/订单/商品线索;
- 否定场景要识别(比如"本来以为要退货,后来发现自己搞错了",是否退款意向= false)。
四、Step2 提示词设计:像做"字段标注",而不是"阅读理解"
Step2 的系统提示可以写得很"工程化":
- 你已经拿到一个具体的服务事件片段 + 一些粗略信息;
- 你不需要再关心整段对话的其他内容;
- 只围绕这个片段填下面这些字段:
字段示例:
订单号:严格匹配对话中出现的订单号格式,不编造商品名称:原文出现的商品名,如果模糊就用片段中最接近商品描述的短语数量:明确提到"买了两台"、"一共三件"等才填问题类型:在已有大类上再做一下细化(如"质量问题-机器卡死")退款意向:归一化为无 / 退货 / 换新 / 退款等时间信息:按你业务需要,可能拆成"购买时间/问题发生时间/联系客服时间"等情绪:粗分"满意/一般/不满/愤怒"等
关键约束
- 不编造 :没有就
[]或"无";不要根据"常识"乱补订单号、下单时间; - 时间归属 :
- "上个月买的" → 购买时间
- "用了不到一周就坏了" → 问题发生时间
- "昨天又来找你们" → 本次联系客服时间
- 多值拆分 :
- 一个事件中如果既有购买时间又有问题发生时间,可以分别放到对应字段,必要时拆成多个记录。
Step2 few-shot 重点
- 用几个精心设计的示例覆盖难点:
- 购买时间 vs 问题时间;
- 一句话里多个时间点;
- 含模糊订单标识("上次的那单")但没有明确订单号的情况;
- 情绪"转折":例如"虽然问题解决了,但过程太糟糕"。
五、效果对比:多步 vs 单步
在一个小规模测试集上(几十条真实或合成客服对话),常见的结果是:
- 单步抽取 :
- 简单样本 OK,复杂样本一旦时间/问题/情绪混在一起,很容易:
- 只提一个时间,忽略另外两个;
- 把"购买时间"误当成"问题发生时间";
- 情绪字段经常"我行我素",要么全空,要么各种主观补充。
- 简单样本 OK,复杂样本一旦时间/问题/情绪混在一起,很容易:
- 多步抽取 :
- Step1 只做"哪儿有事 + 大概什么事",容错空间大得多;
- Step2 围绕一个短片段做字段填空,更像传统 IE 任务,规则能落地,few-shot 更好使;
- 整体召回率(尤其是复杂样本中的问题事件召回)显著提升;
- 错误更容易定位:
- 事件没检出 → 调 Step1;
- 字段乱填 → 调 Step2。
六、对提示词工程师和 AI 开发者的几点建议
-
能拆就拆,别让一个 prompt 做所有事
- 大模型强在"复杂推理能力",但不是"全活一口气干完"的意思。
- 把任务拆成"事件检测 → 单事件结构化 → 聚合/后处理",往往整体质量和可控性都更好。
-
提示词不是越长越好,关键在"分角色"
- Step1 的角色:事件侦探(在哪儿发生了什么事)
- Step2 的角色:数据标注员(为这个事件填表)
- 各自的规则和 few-shot 都要紧贴这个角色的职责,而不是全贴在一个 prompt 里。
-
few-shot 要为"最常见的错误模式"服务
- 时间归属错,就设计专门的"边界示例";
- 拆分错,就设计"一个句子多个事件"的重点示例;
- 方案/情绪乱填,就来一两个"错误 vs 正确"的对照示例。
-
多步 ≠ 多开销,而是换取"可维护性"
- 是的,多步意味着更多 API 调用;
- 但如果单步方案需要各种后处理脚本、人工规则修补,整体成本经常更高。
- 尤其在高要求场景(金融、医疗、客服质检)中,"可解释 + 可迭代"往往比"省几次 API 调用"更重要。
如果你现在手上也有一个"超级长提示词 + 一堆规则 + 一堆样例"的抽取任务,不妨试着问自己三个问题:
- 我能不能先只做"事件检测",晚一点再管字段?
- 哪些错误其实是"事件没切好"导致的,而不是模型不会填字段?
- 如果把最常错的 20% 场景拆出来做一个 Step2,会不会简单很多?
这三个问题,基本就是从单步到多步重构的起点。