89 lines
4.5 KiB
Markdown
89 lines
4.5 KiB
Markdown
---
|
||
id: scenario.ticket-system
|
||
title: 工单系统:AI 自动分类与路由、状态机、SLA 超时升级和坐席协作
|
||
type: scenario
|
||
domain: cross
|
||
stack: [postgresql, mq, redis, langchain, gin, fastapi, react]
|
||
tags: [工单, 状态机, 自动分类, 路由, SLA, 延迟队列, 审计, 乐观锁]
|
||
level: intermediate
|
||
prerequisites: [backend.postgresql.index, ai.langchain.index]
|
||
est_hours: 3
|
||
related: [ai.langchain.scenario.ticket-triage, backend.postgresql.scenario.order-state-audit, backend.mq.scenario.delayed-order-cancel, frontend.react.scenario.long-task-ui]
|
||
week: [3, 11, 14]
|
||
---
|
||
|
||
# 工单系统:AI 自动分类与路由、状态机、SLA 超时升级和坐席协作
|
||
|
||
## 业务背景与问题
|
||
|
||
客服中心的工单靠人工分派:
|
||
|
||
1. 分类不准、分派慢,首次响应时间长;
|
||
2. 工单状态混乱(已关闭的又被改回处理中),没有操作记录;
|
||
3. 超过 SLA 没人知道;
|
||
4. 两个坐席同时处理同一工单,互相覆盖。
|
||
|
||
## 约束
|
||
|
||
- 分类错误会耽误处理,但人工可以纠正。
|
||
- 需要审计:谁在什么时候做了什么。
|
||
- AI 不可用时系统照常运行(降级为人工分派)。
|
||
|
||
## 方案设计
|
||
|
||
1. **状态机与审计**(PostgreSQL):`new → triaged → assigned → in_progress → waiting_customer → resolved → closed`,只允许合法转换;每次转换在同一事务中写事件表;`version` 字段做乐观锁。见[订单状态与审计](../topics/backend/postgresql/scenarios/order-state-audit.md)。
|
||
2. **AI 分类**(LangChain 结构化输出):类别、优先级、建议处理组、置信度、理由;置信度低于阈值进入“待人工分类”;坐席的纠正记录为评测数据。见[工单智能分诊](../topics/ai/langchain/scenarios/ticket-triage.md)。
|
||
3. **异步流程**:创建工单后发消息,分类服务消费(重试、死信);回写结果时校验状态和版本,不覆盖人工修改。
|
||
4. **SLA**:按优先级计算截止时间,投递延迟消息;到期时**再检查当前状态**,仍未处理才升级。见[延迟消息](../topics/backend/mq/scenarios/delayed-order-cancel.md)。
|
||
5. **坐席界面**(React):列表实时更新,显示 AI 分类理由,一键采纳或纠正;处理中用 Redis 软锁(带过期)提示其他人。见[长任务界面](../topics/frontend/react/scenarios/long-task-ui.md)。
|
||
6. **降级**:分类服务超时或不可用时,工单直接进入人工分派队列。
|
||
|
||
## 关键代码
|
||
|
||
状态转换 + 乐观锁 + 审计(一个事务中):
|
||
|
||
```sql
|
||
UPDATE tickets
|
||
SET status = 'assigned', assignee_id = $3, version = version + 1, updated_at = now()
|
||
WHERE id = $1 AND version = $2 AND status IN ('new', 'triaged');
|
||
-- 影响行数为 0:状态不允许或已被别人修改,返回 409 让前端刷新
|
||
|
||
INSERT INTO ticket_events (ticket_id, actor, action, from_status, to_status, payload)
|
||
VALUES ($1, $4, 'assign', 'triaged', 'assigned', jsonb_build_object('assignee', $3));
|
||
```
|
||
|
||
分类结果结构(示意):
|
||
|
||
```python
|
||
class Triage(BaseModel):
|
||
category: Literal["refund", "invoice", "account", "bug", "other"]
|
||
priority: Literal["P1", "P2", "P3"]
|
||
group: str
|
||
confidence: float = Field(ge=0, le=1)
|
||
reason: str = Field(max_length=200)
|
||
|
||
result = triage_chain.invoke({"title": t.title, "body": t.body}) # model.with_structured_output(Triage)
|
||
if result.confidence < 0.7:
|
||
route_to_manual_queue(t.id, suggestion=result)
|
||
```
|
||
|
||
## 取舍
|
||
|
||
| 决策 | 选择 | 原因 |
|
||
| --- | --- | --- |
|
||
| 分类方式 | 大模型结构化输出 + 置信度阈值 | 冷启动快;纠正数据积累后可训练小模型降成本 |
|
||
| 并发控制 | 乐观锁 + 软锁提示 | 冲突少,悲观锁会让坐席等待 |
|
||
| SLA | 延迟消息 + 到期检查,定时扫描兜底 | 及时且可靠 |
|
||
| 语言 | 工单核心用 Go(Gin),AI 分类用 Python | 发挥两者优势;全部 Python 也可以 |
|
||
|
||
## 易错点
|
||
|
||
- AI 分类结果覆盖了人工已修改的字段。
|
||
- 状态转换只靠前端隐藏按钮,数据库层没有约束。
|
||
- 延迟消息到期时不检查当前状态,已处理的工单也被升级。
|
||
- 审计日志和业务更新不在同一事务中。
|
||
|
||
## 面试怎么讲
|
||
|
||
> “工单系统用状态机约束转换,每次转换在同一事务中写审计事件,乐观锁防止坐席互相覆盖。AI 分类用结构化输出给出类别、优先级、建议组和置信度,低于阈值进入人工队列,纠正记录作为评测数据。SLA 用延迟消息,到期再检查状态决定是否升级。分类服务挂了就直接人工分派,不影响主流程。”
|