阶段4(下):LangChain 1.x、提示词与注入防护、模型侧(路由/vLLM/LoRA)、AI 编程工具与验证、Dify/Coze 专题
This commit is contained in:
1 parent
caaec830b3
commit
7a32d2c9a6
38 files changed
+3059
-67
No files matched your search
@@ -0,0 +1,92 @@
|
||||
---
|
||||
id: ai.langchain.scenario.ticket-triage
|
||||
title: 工单自动分类服务:结构化输出、批处理、重试降级和人工复核
|
||||
type: scenario
|
||||
domain: ai
|
||||
stack: [langchain, fastapi, postgresql]
|
||||
tags: [结构化输出, 批处理, 重试, 降级, 人工复核, 成本]
|
||||
level: intermediate
|
||||
prerequisites: [ai.langchain.lcel-structured-agent]
|
||||
est_hours: 2.5
|
||||
related: [ai.langchain.interview, ai.llm-serving.index]
|
||||
week: [6, 11]
|
||||
---
|
||||
|
||||
# 工单自动分类服务:结构化输出、批处理、重试降级和人工复核
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
客服每天收到约 5000 张工单,人工分派到退款、技术、物流、投诉等 8 个队列,平均分派耗时 20 分钟。希望用大模型自动分类、判断紧急程度、提取订单号。试运行问题:
|
||||
|
||||
1. 模型偶尔输出“类别:退款”这样的自然语言,解析失败;
|
||||
2. 模型给出了不存在的类别“售后”;
|
||||
3. 高峰期模型接口限流,工单堆积;
|
||||
4. 历史 50 万张工单要回填分类,逐条调用太慢、太贵;
|
||||
5. 分类错误的工单被分到错误的队列,没人发现。
|
||||
|
||||
## 约束
|
||||
|
||||
- 分派延迟目标:1 分钟内。
|
||||
- 分类结果要可追溯(模型版本、提示词版本)。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **结构化输出**:用 Schema 定义输出(类别枚举、紧急程度、订单号格式),优先使用模型的原生结构化输出(`with_structured_output`),并用 pydantic 校验;校验失败重试一次,仍失败进入“待人工分派”队列。
|
||||
2. **置信度和人工复核**:输出中包含置信度(或用规则判断:订单号缺失、类别为“其他”),低置信度进入人工复核;每天抽检 2% 自动分类的工单。
|
||||
3. **重试和降级**:限流 / 超时用指数退避重试;主提供方持续失败时切换备用提供方;都失败时进入人工队列,不阻塞工单创建。
|
||||
4. **批处理**:历史回填走离线任务,用 `batch` 控制并发(或厂商的批处理接口,通常更便宜,以厂商文档为准),按天分片、断点续跑。
|
||||
5. **可追溯**:每条结果记录模型名、提示词版本、原始输出、耗时。
|
||||
|
||||
## 关键代码
|
||||
|
||||
```python
|
||||
class Triage(BaseModel):
|
||||
category: Literal["refund", "tech", "logistics", "complaint", "invoice", "account", "presale", "other"]
|
||||
urgency: Literal["low", "normal", "high"]
|
||||
order_id: str | None = Field(default=None, pattern=r"^A\d{4,}$")
|
||||
confidence: float = Field(ge=0, le=1)
|
||||
|
||||
|
||||
classifier = (
|
||||
prompt | primary_model.with_structured_output(Triage)
|
||||
).with_retry(stop_after_attempt=2).with_fallbacks([prompt | backup_model.with_structured_output(Triage)])
|
||||
|
||||
|
||||
async def triage(ticket: Ticket) -> Triage | None:
|
||||
try:
|
||||
result = await classifier.ainvoke({"title": ticket.title, "body": ticket.body[:2000]})
|
||||
except Exception:
|
||||
await queue.put_manual(ticket.id, reason="模型失败")
|
||||
return None
|
||||
if result.confidence < 0.7 or result.category == "other":
|
||||
await queue.put_review(ticket.id, result)
|
||||
await save_result(ticket.id, result, model=MODEL_NAME, prompt_version=PROMPT_VERSION)
|
||||
return result
|
||||
|
||||
|
||||
# 历史回填:控制并发
|
||||
results = await classifier.abatch(inputs, config={"max_concurrency": 8}, return_exceptions=True)
|
||||
```
|
||||
|
||||
基础写法的可运行示例:[examples/lc-basics](../examples/lc-basics/)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 选择 | 优点 | 缺点 |
|
||||
| --- | --- | --- |
|
||||
| **大模型分类(本方案)** | 新增类别只改提示词和 Schema;能同时提取信息 | 有成本和延迟;需要评测和复核 |
|
||||
| 传统文本分类模型 | 便宜、快 | 需要标注数据训练,类别变化要重新训练 |
|
||||
| 关键词规则 | 简单可解释 | 覆盖率低 |
|
||||
| 规则 + 大模型 | 明确的用规则(便宜),模糊的用模型 | 两套逻辑要维护 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 让模型自由输出类别名,再用字符串匹配。
|
||||
- 模型失败时阻塞工单创建:分类应该是异步的,失败可降级为人工。
|
||||
- 回填时并发过高触发限流,或者没有断点续跑,失败后从头再来。
|
||||
- 模型自报的置信度并不可靠,要和评测集的实际准确率对照校准。
|
||||
- 换模型或改提示词后没有跑评测集。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “工单分类我用结构化输出,类别是枚举、订单号有格式校验,失败重试一次,再失败进人工队列,不阻塞工单创建。主提供方限流时自动切换备用提供方。低置信度和‘其他’类进入人工复核,每天再抽检 2%。历史回填用批处理控制并发、断点续跑。每条结果记录模型和提示词版本,改动前后跑评测集对比准确率。”
|
||||
Reference in new issue
Block a user