工单提取与路由分类不同,猜测型提取器猜错比不猜更糟。邮件格式带来的签名块、引用历史、机密声明等噪音必须先剥离,再逐轮提取而非拼接全文。
这不是工单路由,也不是打标签。路由是将整个工单归类到一个队列中;而提取是从行文中抽取出命名字段,让工程师不用读四段话才能找到是哪个版本坏了。这两种方式的失败模式截然相反:路由器猜错了通常还有用,而提取器猜错了还不如没有。
输入的不是工单,是会话
无论你的工单系统把描述字段叫什么,其中的内容通常是一封邮件。这意味着它带着邮件所携带的一切:签名档、移动客户端页脚、公司保密声明、自动回复尾巴,以及——在第一次回复之后——迄今为止整个会话的引用历史。
每一种都会以特定方式污染提取结果。签名档中包含的电话号码和职位头衔看起来就像字段。保密声明中经常出现"error"这个词,足以造成干扰。引用历史意味着在第三次回复时,模型提取的是客户周一提到的、随后已更正过的版本号。在做任何操作之前先剔除引用部分和尾巴部分,并按轮次提取而不是对拼接后的正文整体提取——这与在线程中分离新文本和引用历史的机制相同,省略这一步的失败后果也一样。
然后在有轮次顺序的情况下进行提取,并让后一轮的字段逐字覆盖更早一轮。客户写"抱歉,是 4.2.1 不是 4.2.0"时只更正了一个字段,而一个在每次更新时都从头重新提取整个工单的流水线,会愉快地丢失仅在第一条消息中出现的那三个字段。
大部分字段都是缺失的
这个 schema 的残酷真相是:一个典型的一次接触工单包含其十个字段中的两到三个,而不是十个。复现步骤缺失的频率远远高于出现的频率。版本要么缺失,要么就是"latest"这个词。环境信息缺失。期望行为几乎从不说明,因为客户认为这是不言自明的。
一个被要求填充十字段 schema 的模型会填充十个字段。它会把客户的叙述转化为祈使句来写出看似合理的复现步骤,结果读起来完全像真正的步骤,但任何人都无法复现。这是该流水线可能产生的最具破坏性的输出,因为工程师会在上面花一个小时,然后才得出客户糊涂了的结论。
三种防御手段,都是结构性的而非措辞问题。让每个字段都允许为空,并在指令中说明 null 是预期值。为每个非空字段要求一个证据跨度,并验证它是该轮次的逐字子字符串——合成出来的步骤无法产生这样的证据跨度。用显式的逐字段来源区分声明值和推断值,这样模型被允许推导的字段会被标记为 derived,可以从任何必须字面的内容中过滤掉。
是步骤呈现还是叙述?编号或项目符号的祈使句序列是步骤。"我在结账过程中它就死了"是叙述,将其转化就是捏造。给这个字段配一个伴生枚举——numbered_list、prose_sequence、narrative_only、absent——让叙述情况流入澄清模板而不是流向工程师。
"Latest"不是版本。"The new one"也不是。保持 version_text 原样,仅在字符串可解析时才解析为真实版本;根据工单日期和你的发布历史推导出的解析值是一种推断,必须标记为推断。
错误字符串是最有价值的字段。逐字的错误信息或堆栈帧是你问题跟踪器和日志的连接键。按精确跨度提取,不做规范化、不折叠大小写、不截断,并且提取全部而不是只提取第一个。
两种严重程度,只有一种是客户的
客户用形容词、大写字母和感叹号表达紧急程度。你的严重程度标尺是关于爆炸半径和解决方案可用性的定义。这是两个不同的量,而被问及"严重程度"的模型会悄悄地把第一个伪装成第二个返回,这就是队列最终按人们有多愤怒来排序的原因。
逐字提取 customer_severity_text——他们用过的词,包括大写字母——并将其作为证据保留。另外,如果你想要一个评估后的严重程度,就将其定义为基于工单实际陈述内容的评分标准:有多少用户受到影响、是否有解决方案、是否正在丢失数据、是否可复现。将评分标准作为枚举定义而不是标签来输入,这样模型是在描述之间选择,而不是在"high"和"critical"这些意味着模型学到的任何意思的词之间选择。
永远把这两个字段放在不同的列中。它们之间的差距本身就是真实有用的信息——一个冷静描述全部数据丢失的客户,或者一个关于 cosmetic 问题的紧急升级,都值得看到——而一个合并的字段会毁掉这一切。
预期和实际存在于一个句子中
规范的 bug 报告把预期行为和实际行为放在不同的段落中。真实的工单把两者放在一个从句里:"发票总额显示为零,而应该显示各行项目的总和"。把它拆成两个字段就是提取,这是这里少数几个模型真正物有所值的地方之一,因为拆分取决于"should"、"expected"、"instead of"或"but"的两侧各是句子哪一半——以及顺序并非固定这一事实。
两种模式需要显式处理。仅 actual 是常见情况:"发票总额显示为零",期望值隐含在产品的契约中。让 expected 为空;它不在工单里,而编造它意味着断言产品应该做什么,这是关于你系统的声明而不是从文档中提取。仅 expected 也会发生,在措辞为 bug 的功能请求中,值得检测,因为这通常意味着工单归错类了。
当两个字段来自同一个句子时,给它们相同的证据跨度。这看起来是冗余的,但这正是让审查者一眼看出这是对一个从句的判断而不是两个独立发现的东西。
{
"type": "object",
"properties": {
"product": { "type": ["string", "null"] },
"component": { "type": ["string", "null"] },
"version_text": { "type": ["string", "null"] },
"version": { "type": ["string", "null"] },
"environment": { "type": ["string", "null"] },
"customer_severity_text": { "type": ["string", "null"] },
"assessed_severity": {
"enum": ["data_loss_or_outage", "blocked_no_workaround",
"degraded_with_workaround", "cosmetic", "insufficient_information"]
},
"steps_kind": { "enum": ["numbered_list", "prose_sequence",
"narrative_only", "absent"] },
"steps": { "type": ["array", "null"], "items": { "type": "string" } },
"expected": { "type": ["string", "null"] },
"actual": { "type": ["string", "null"] },
"frequency": { "enum": ["always", "intermittent", "once", "unstated"] },
"error_strings": { "type": "array", "items": { "type": "string" } },
"field_sources": {
"type": "object",
"additionalProperties": { "enum": ["stated", "inferred"] }
},
"evidence": {
"type": "object",
"additionalProperties": { "type": "string" }
}
},
"required": ["assessed_severity", "steps_kind", "frequency",
"error_strings", "field_sources", "evidence"]
}
assessed_severity 没有 null,并将 insufficient_information 作为真实值包含在内,这是刻意设计的:可空枚举会鼓励模型跳过决策,而显式的"工单没有提供足够信息"是一个可路由的答案。frequency 的处理方式相同。所有真正关于文档内容本身的都是可空的;所有关于文档分类的都有"无法判断"的值。
把工单拆分成轮次,剔除引用历史、签名和法律页脚。把剔除的文本作为独立列保留。
针对 schema 在 temperature 0 下按轮次提取,使用严格结构化输出以防止枚举值漂移成自由字符串。
验证每个证据值都是该轮次空格规范化后的子字符串。丢弃任何证据校验失败的字段并记录丢弃——不要修复它,因为失败的证据检查正是你正在度量的信号。
向前合并轮次:对每个字段,最新的非空值胜出,并用其轮次索引保留被取代的值。
按 steps_kind 路由。absent 和 narrative_only 进入自动化澄清请求,命名具体缺失的字段;其余进入队列。
严格 schema 强制在提供商之间并不统一——对约束解码的支持、接受的 JSON Schema 子集以及字段无法填充时的行为都存在差异,所以一个产生相同形状的备用模型是工作量而不是一行配置。通过一个 API 路由意味着当主模型不可用时,schema、重试和逐工单成本核算都留在同一个地方。相关阅读:各提供商的 structured output 支持。
两个相邻的提取值得在同一轮次中运行,因为它们共享被剔除的文本:客户粘贴在正文某处的订单号,以及附带记录中存在的任何时间戳,后者有其自身的问题。
Extracting Order Numbers Referenced Inside a Support Ticket
Extracting Speaker Turns From a Chat Transcript
Handling a Required Field That Is Missing From the Source Document