阶段4(下):LangChain 1.x、提示词与注入防护、模型侧(路由/vLLM/LoRA)、AI 编程工具与验证、Dify/Coze 专题

This commit is contained in:
Shikui Wang committed 2026-10-23 16:15:33 +08:00
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%。历史回填用批处理控制并发、断点续跑。每条结果记录模型和提示词版本,改动前后跑评测集对比准确率。”