一句话里带两个诉求被分错,不能直接归因于标签体系。更常见的顺序是:先确认模型返回结构到底能不能装下两个标签,再确认标签定义之间是否互斥清晰,最后才怀疑句子太长把两个诉求压成了一个。Xing4.0-29B-Intent 这类意图模型,如果上线时只按单标签调用、提示词里也只让它输出一个 intent,那它给一个结果是“按约定执行”,不是“判断失误”。建议先用一小批带两个诉求的真实句子,把原始返回抓下来,再决定改标签、改提示词还是改调用方式。
处理顺序建议固定为三步:先抓一次调用的原始返回,确认返回字段是单个字符串还是可承接数组;再按(人工标签、模型标签)成对统计错误,看是否集中在少数类别之间互相吞并;最后才对长句做切分对比。标签体系的问题通常表现为固定的几对类别反复混淆;输出结构的问题表现为无论几个诉求都只返回一个值。两种原因的改法完全不同,先在日志层面区分开,再动标签定义。
先把带两个诉求的句子挑出来做成小测试集
抱怨“它又分错了”没有排查价值,先把它变成可复现的样本。取样来源建议就地取材:近一两周的客服工单首句、在线会话里用户的第一条消息、被人工改判过的意图记录。这三类来源都自带真实表达习惯,比人工造句更接近线上分布。
筛选标准可以写得很机械:一句话里出现两个及以上诉求动词(问价、退款、改地址、开发票、取消、催发货、查进度),或者出现并列连接词(另外、还有、同时、顺便、以及)后再接一个诉求,或者一句话里有两个问号。命中任一条件就进集合。收集 20~30 条即可,先求覆盖典型场景,不求统计显著。
标注纪律要写清楚,否则测试集本身不可信:一条句子有两个独立诉求就标两个标签;同一个诉求的两种说法只标一个;标注时不考虑模型能不能输出多标签,只按业务上应该怎么分流来标;每条都写一句依据,便于后面复核。
id,sentence,label_1,label_2,basis
01,我想问下这个套餐多少钱 另外昨天买的能退吗,price_inquiry,refund_request,问价+退货两个动作
02,订单能改地址吗 还有能不能开发票,order_change,invoice,改单+开票
03,这个月流量超了怎么算钱 顺便帮我查下余额,price_inquiry,balance_query,计费咨询+余额查询
04,会员怎么取消自动续费 另外退款多久到账,member_cancel,refund_request,订阅操作+退款时效
05,你们几点上班 我这单一直没发货,service_hours,logistics,咨询+催发货
06,价格能便宜点吗 我朋友说可以退差价,price_inquiry,refund_request,议价+退差价
07,发票怎么开 抬头写错了能重开吗,invoice,invoice,同一诉求两种问法 只标一个
08,我要退货 运费谁出,refund_request,refund_request,退货含运费 合并为一个诉求
09,账号登不上去了 密码也忘了怎么改,account_login,password_reset,登录+改密
10,这个型号有货吗 没有的话什么时候补,stock_query,stock_query,同一诉求追问
11,能开发票吗 另外能不能寄到公司地址,invoice,order_change,开票+改收货地址
12,我昨天下的单还没到 能改配送时间吗,logistics,order_change,催单+改配送
13,退款到哪了 顺便问一下下次还能用优惠券吗,refund_progress,coupon_query,退款进度+券规则
14,怎么开发票 开完能发邮箱吗,invoice,invoice,同一诉求两步
15,我想换货 颜色选错了 运费怎么算,exchange,exchange,换货含运费
16,套餐能不能升级 降档会不会有违约金,plan_change,plan_change,同一诉求双向
17,这单能加急吗 加钱也行,logistics,price_inquiry,加急+付费意愿
18,我的优惠券没用上 能补吗 还有有效期多久,coupon_query,coupon_query,同一诉求多问
19,客服电话多少 我想人工处理退款,contact_human,refund_request,转人工+退款
20,订单能拆开发票吗 两张抬头不一样,invoice,invoice,同一诉求变体
21,东西坏了能换吗 保修期从什么时候算,exchange,warranty,换货+保修
22,我想取消订单 钱会退回来吗,cancel_order,refund_request,取消+退款确认
23,会员到期了还能看吗 续费有优惠吗,member_status,member_renew,状态+续费
24,地址填错了能改吗 已经付款了会不会发走,order_change,order_change,同一诉求含两个担心这份样例可以直接存成 CSV,作为后续对比的固定基线。其中 07、08、10、14、15、16、18、20、24 是刻意留的对照组:它们看着像两个诉求,业务上其实只有一个,用来防止后面一看到两个动词就切分。
核对模型实际返回的是单标签还是可多标签
这一步只做一件事:拿到一次调用的原始返回,逐字段看。不要看前端展示,不要看封装后的业务对象,要看 HTTP 响应体本身。建议在网关或调用侧加一条临时日志,把请求体和响应体原样落盘。
# 通用接入骨架 具体字段名下以你环境实际部署为准
import json, requests
payload = {
'model': 'xing-4.0-29b-intent',
'messages': [
{'role': 'system', 'content': '你是意图识别服务。输出 JSON,字段 intents 为数组,每项含 label 与 evidence;最多三个标签;无法判断时返回空数组。'},
{'role': 'user', 'content': '我想问下这个套餐多少钱 另外昨天买的能退吗'}
],
'temperature': 0
}
resp = requests.post(ENDPOINT, headers=HEADERS, json=payload, timeout=30)
raw = resp.text # 先落盘 再做业务解析
open('raw_resp.jsonl','a').write(raw + '\n')
print(resp.status_code, raw)落盘后看三件事:第一,返回体里意图字段是字符串还是数组;第二,数组是模型自己输出的,还是被服务端 schema 截成了一个;第三,提示词里有没有明确要求“最多 N 个标签”和“空数组”的兜底。
判断结论以实际行为为准:如果提示词要求数组、schema 也是数组,返回仍然只有一个元素,并且换几条双诉求句子都这样,那更可能是标签定义或训练偏好偏向单标签,需要看标签互斥性;如果提示词从头到尾只让它输出一个 intent,那问题出在输出结构和调用约定,改标签没有用。可以先在这里下结论,再去动标签表。
按标签对统计错在哪些类别之间
把测试集跑一遍,每条样本记录人工标签集合和模型标签集合,然后展开成(人工标签,模型标签)成对计数。只看总体准确率会掩盖问题,成对统计才能看出是哪几类在互相吞并。
统计口径:一条样本人工标了两个标签、模型只给一个,就记两条——命中的那对记 (A,A),漏掉的那对记 (B,A) 或 (A,B),表示“B 被并进了 A”。
| 人工标签 | 模型标签 | 次数 | 典型句子 | 观察 |
|---|---|---|---|---|
| refund_request | price_inquiry | 待填 | 价格能便宜点吗 我朋友说可以退差价 | 议价与退差价边界模糊 |
| invoice | order_change | 待填 | 能开发票吗 另外能不能寄到公司地址 | 开票改地址同属售后办事 |
| refund_progress | refund_request | 待填 | 退款到哪了 顺便问一下下次还能用优惠券吗 | 退款子类被父类吸收 |
| logistics | order_change | 待填 | 我昨天下的单还没到 能改配送时间吗 | 催单与改单同话术 |
| coupon_query | price_inquiry | 待填 | 我的优惠券没用上 能补吗 | 优惠类并入价格类 |
跑完后按次数排序,取前三对作为重点。这三对通常就是标签定义需要写清边界的对象:要么补一句“退款进度与退款申请分开计”“开票与改地址分开计”这类互斥说明,要么确认业务上本来就该合并——如果业务分流到同一个队列,那模型合并未必是错,是标注口径需要统一。
# 展开成对统计 pandas 骨架
df['human'] = df['human_labels'].str.split('|')
df['pred'] = df['model_labels'].str.split('|')
pairs = []
for _, r in df.iterrows():
for h in r['human']:
if h in r['pred']:
pairs.append((h, h))
else:
for p in r['pred']:
pairs.append((h, p))
import collections
for k, v in collections.Counter(pairs).most_common(10):
print(k, v)把长句按诉求切分后再送一次做对比
切分规则可先保守:按句号、问号、分号切;再按“另外、还有、同时、顺便、以及、然后”这类连接词切;切完去掉语气词和空串,保留原句编号和顺序号,便于和整句结果对齐。
import re
CONN = r'(另外|还有|同时|顺便|以及|然后)'
def split_claim(text, sep='|'):
parts = re.split(r'[。?;?!;]', text)
out = []
for p in parts:
p = p.strip()
if not p:
continue
for q in re.split(CONN, p):
q = q.strip()
if q and q not in ('另外','还有','同时','顺便','以及','然后'):
out.append(q)
return out
print(split_claim(sep='|' in 'x'), split_claim('我想问下这个套餐多少钱 另外昨天买的能退吗'))
# 期望得到两个片段 每片单独调用一次意图接口对比时同一批 20~30 条样本要保留两张结果:整句送入的模型标签、切分后逐片送入再合并的标签。对比列固定为:人工标签、整句结果、切分后结果、是否修正、新引入的问题。
切分带来的新问题要提前写明,否则修正会被高估:代词丢失(“这个”“它也”“那单”离开原句后指代不明);共享上下文丢失(订单号、手机号只在第一句出现,第二句单独送就缺槽位);连接词本身承载语义(“顺便”暗示这是次要诉求,切掉后主次关系消失);过度切分把同一诉求拆成两片,反而把单标签样本变成两个标签。建议切分前后都跑,按“整句错、切分对”和“整句对、切分错”两个方向分别计数,再决定是否长期启用切分。
多诉求场景改成两次调用再合并结果
如果模型确实不具备稳定输出多标签的能力,最省事的做法不是继续调标签,而是把多诉求拆成两次调用:第一次做开放式诉求抽取,第二次把每个诉求归到封闭标签集。这样模型每次只回答一件事,标签体系不用改。
调用顺序建议:先抽取后归类。抽取阶段的提示词只要原文和“列出这段话里用户提出的每一个诉求,一条一行”,不加标签约束;归类阶段把每个诉求单独送一次,附上完整标签定义,要求返回一个标签加一个置信度或证据片段。
# 合并规则骨架
PRIORITY = ['cancel_order', 'refund_request', 'refund_progress',
'exchange', 'order_change', 'invoice',
'logistics', 'price_inquiry', 'coupon_query']
CONFLICT = {('cancel_order','refund_request'), ('exchange','refund_request')}
def merge(items):
labels = []
for it in items: # items: [{'label':..., 'text':...}]
if it['label'] and it['label'] not in labels:
labels.append(it['label'])
for a, b in CONFLICT:
if a in labels and b in labels:
return {'primary': a, 'secondary': [x for x in labels if x != a],
'need_human': True}
labels.sort(key=lambda x: PRIORITY.index(x) if x in PRIORITY else 99)
return {'primary': labels[0] if labels else None,
'secondary': labels[1:],
'need_human': len(labels) > 2}
print(merge([{'label':'price_inquiry'}, {'label':'refund_request'}]))合并规则要写死几条:同一标签去重,保留第一次出现的证据文本;主次按业务分流优先级排序,排在前面的当主意图;超过两个标签或命中冲突对时标记 need_human,转人工复核,不要让合并逻辑自己“猜”。上面 CONFLICT 里的组合可按你所在业务的处理规则调整——取消与退款、换货与退款这类往往不能同时成立,机器合并容易给出自相矛盾的结果。
验证方式也简单:用前面那批样本重跑,检查每条样本是否至少拿到了人工标注里的主标签,以及 need_human 的比例是否在可接受范围。代价是调用次数随诉求条数线性增加,长尾多诉求句子会变慢,这条边界要在选型时一起考虑。