phase 6 part 1: eight cross-stack enterprise scenarios and scenarios index
This commit is contained in:
1 parent
691f738f5f
commit
5b1a7a430f
12 files changed
+1145
-12
No files matched your search
@@ -0,0 +1,85 @@
|
||||
---
|
||||
id: scenario.approval-human-in-loop
|
||||
title: 审批流 + 人工介入的 Agent:高风险操作暂停审批、断点续跑、超时和幂等执行
|
||||
type: scenario
|
||||
domain: cross
|
||||
stack: [langgraph, function-calling, postgresql, redis, fastapi, vue]
|
||||
tags: [人机协作, interrupt, 检查点, 审批, 幂等, 超时, 审计]
|
||||
level: advanced
|
||||
prerequisites: [ai.langgraph.index, ai.function-calling.index]
|
||||
est_hours: 3
|
||||
related: [ai.langgraph.scenario.refund-approval, ai.function-calling.scenario.safe-business-tools, ai.agent-design.index]
|
||||
week: [11, 12]
|
||||
---
|
||||
|
||||
# 审批流 + 人工介入的 Agent:高风险操作暂停审批、断点续跑、超时和幂等执行
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
AI 客服 Agent 可以办理退款、改地址、补发商品。业务要求:
|
||||
|
||||
1. 金额超过阈值或命中风险规则的操作必须人工审批;
|
||||
2. 审批可能几小时后才完成,期间服务可能重启;
|
||||
3. 审批通过后只能执行一次(不能重复退款);
|
||||
4. 审批超时自动拒绝并通知用户;
|
||||
5. 全程可审计:谁发起、模型建议了什么、谁批准、实际执行了什么。
|
||||
|
||||
## 约束
|
||||
|
||||
- Agent 不能自己“决定”跳过审批,不能依赖提示词约束。
|
||||
- 审批人在后台系统操作,不在对话中。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **流程建模(LangGraph)**:理解意图 → 查订单 → 计算方案 → 风险判断 → [需审批] `interrupt` 暂停 → 执行 → 通知。风险判断是**代码规则**(金额、频次、用户等级),不是模型判断。
|
||||
2. **持久化**:检查点存数据库(开发 SQLite、生产 PostgreSQL),`thread_id` 与审批单关联,重启后从检查点恢复。
|
||||
3. **审批单**:暂停时创建审批记录(方案、模型理由、风险命中项、截止时间)推送到后台;审批后用 `Command(resume=...)` 恢复对应线程。
|
||||
4. **幂等执行**:执行节点调用退款接口时带幂等键(`thread_id + 步骤名`),恢复或重试不会重复退款。
|
||||
5. **超时**:延迟任务到期检查审批状态,未处理则以“驳回(超时)”恢复。
|
||||
6. **前端**:用户端显示“已提交审核”,状态变化推送更新;审批后台展示对话摘要和方案。
|
||||
7. **审计**:每步写事件表,包括模型输出、工具参数和结果。
|
||||
|
||||
## 关键代码
|
||||
|
||||
审批节点(示意;可运行版本见 [examples/ticket-flow](../topics/ai/langgraph/examples/ticket-flow/),含 SqliteSaver、interrupt、崩溃恢复和幂等 Mailer 的测试):
|
||||
|
||||
```python
|
||||
from langgraph.types import Command, interrupt
|
||||
|
||||
def risk_check(state: RefundState) -> dict:
|
||||
return {"need_approval": state["amount"] > 500 or state["user_refunds_30d"] >= 3} # 规则写在代码里
|
||||
|
||||
def approval(state: RefundState) -> dict:
|
||||
decision = interrupt({"ticket": state["ticket_id"], "amount": state["amount"], "reason": state["plan_reason"]})
|
||||
return {"approved": decision["approved"], "approver": decision["by"]}
|
||||
|
||||
def execute(state: RefundState) -> dict:
|
||||
if state["need_approval"] and not state.get("approved"):
|
||||
return {"result": "rejected"}
|
||||
key = f"{state['thread_id']}:refund" # 幂等键
|
||||
return {"result": refund_api(state["order_id"], state["amount"], idempotency_key=key)}
|
||||
|
||||
def on_approval(thread_id: str, approved: bool, by: str) -> None: # 审批后台回调
|
||||
graph.invoke(Command(resume={"approved": approved, "by": by}),
|
||||
config={"configurable": {"thread_id": thread_id}})
|
||||
```
|
||||
|
||||
## 取舍
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 是否需要审批 | 代码规则 | 模型可能被注入或误判 |
|
||||
| 流程引擎 | LangGraph(interrupt + 检查点) | 与 Agent 天然结合;Temporal 等工作流引擎更重 |
|
||||
| 恢复方式 | 审批回调触发 resume | 比轮询及时 |
|
||||
| 执行保障 | 幂等键 + 状态检查 | 恢复和重试都可能重复执行节点 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 把“是否需要审批”交给模型判断。
|
||||
- `interrupt` 所在节点恢复时会**从头重新执行**,节点内 interrupt 之前的副作用会重复——副作用放在 interrupt 之后的节点,或保证幂等。
|
||||
- 审批超时没有处理,线程永远挂起。
|
||||
- 审批人看到的信息不足,只能盲批。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “高风险操作我用 LangGraph 的 interrupt 做人工审批。是否需要审批由代码规则决定,不交给模型。暂停时创建审批单,检查点存数据库,服务重启不丢;审批回调用 Command(resume) 恢复线程。执行退款用 thread_id 加步骤名作为幂等键,恢复或重试都不会重复退款。超时由延迟任务以驳回恢复,每一步都写审计事件。”
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
id: scenario.content-moderation
|
||||
title: 内容审核:规则 + 模型的分层审核、异步流水线、人工复审、申诉和效果度量
|
||||
type: scenario
|
||||
domain: cross
|
||||
stack: [mq, redis, postgresql, prompt-engineering, agent-ops, fastapi, react]
|
||||
tags: [内容审核, 分层过滤, 敏感词, 大模型审核, 人工复审, 异步, 召回率, 误杀率]
|
||||
level: intermediate
|
||||
prerequisites: [backend.mq.index, ai.prompt-engineering.index]
|
||||
est_hours: 2.5
|
||||
related: [ai.prompt-engineering.scenario.injection-defense, ai.agent-ops.scenario.agent-quality-gate, backend.mq.reliability]
|
||||
week: [13, 14]
|
||||
---
|
||||
|
||||
# 内容审核:规则 + 模型的分层审核、异步流水线、人工复审、申诉和效果度量
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
社区产品允许用户发帖、评论,同时 AI 助手会生成回复。监管和平台要求:
|
||||
|
||||
1. 违规内容(色情、暴力、诈骗广告、个人信息泄露)要及时拦截;
|
||||
2. 纯人工审核成本高、速度慢;纯关键词误杀多(“打击诈骗”被当成诈骗);
|
||||
3. AI 生成的内容同样要审核;
|
||||
4. 用户对误判可以申诉。
|
||||
|
||||
## 约束
|
||||
|
||||
- 发帖体验不能明显变慢。
|
||||
- 高风险内容宁可误杀;低风险内容宁可放过后再处理。
|
||||
- 审核标准会随政策变化。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **分层审核**:
|
||||
- 第一层(同步,毫秒级):敏感词(AC 自动机 / 词库服务)、黑名单 URL、正则(手机号、身份证号)→ 命中高危直接拒绝;
|
||||
- 第二层(异步,秒级):大模型按审核标准分类,输出结构化结果(类别、风险等级、证据片段、理由);
|
||||
- 第三层(人工):中风险和模型不确定的内容进入人工复审队列,按风险和时效排序。
|
||||
2. **发布策略**:低风险先发布(“先发后审”),中风险“仅自己可见”直到审核完成,高风险拒绝。
|
||||
3. **异步流水线**:发帖写库后发消息,审核服务消费(重试、死信、幂等:按内容 id + 内容哈希);结果回写并通知。
|
||||
4. **AI 生成内容**:输出前过第一层;流式输出时按句缓冲检测,命中即中止并替换为提示语。
|
||||
5. **审核标准即配置**:审核提示词和类别定义版本化管理;修改后用标注集回归(召回率、误杀率)通过才上线。
|
||||
6. **申诉与反馈**:申诉进入人工复审;复审结论回流到标注集,用于评测和改进规则。
|
||||
7. **防对抗**:谐音、拆字、图片中的文字(OCR);提示词注入(帖子内容里写“忽略审核规则”),审核提示词中把用户内容作为数据隔离。
|
||||
|
||||
## 关键代码
|
||||
|
||||
审核结果结构与决策(示意):
|
||||
|
||||
```python
|
||||
class ModerationResult(BaseModel):
|
||||
category: Literal["ok", "porn", "violence", "fraud", "pii", "other"]
|
||||
risk: Literal["low", "medium", "high"]
|
||||
evidence: list[str] = Field(max_length=5) # 命中的原文片段,方便复审
|
||||
reason: str
|
||||
|
||||
def decide(rule_hit: RuleHit | None, model: ModerationResult | None) -> str:
|
||||
if rule_hit and rule_hit.level == "high":
|
||||
return "reject"
|
||||
if model is None: # 模型超时 / 不可用:进入人工,不自动放行中高风险
|
||||
return "manual_review"
|
||||
if model.risk == "high":
|
||||
return "reject"
|
||||
if model.risk == "medium" or (rule_hit and rule_hit.level == "medium"):
|
||||
return "manual_review"
|
||||
return "publish"
|
||||
```
|
||||
|
||||
审核提示词中隔离用户内容(防注入)的写法见[提示词注入防护](../topics/ai/prompt-engineering/scenarios/injection-defense.md);评测和门禁见 [Agent 质量门禁](../topics/ai/agent-ops/scenarios/agent-quality-gate.md)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 审核方式 | 规则 + 模型 + 人工分层 | 兼顾速度、成本和准确率 |
|
||||
| 发布时机 | 按风险分级 | 体验和安全的平衡 |
|
||||
| 模型故障 | 降级为人工 | 不能因故障放行风险内容 |
|
||||
| 标准变更 | 标注集回归 | 防止改一条规则引起大量误杀 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 只看召回率(拦住多少违规),不看误杀率(误伤多少正常内容)。
|
||||
- 审核结果覆盖了用户后来编辑过的新内容(用内容哈希校验版本)。
|
||||
- 把用户内容直接拼进审核提示词,被注入。
|
||||
- 审核日志中保存了大量个人信息,没有脱敏和保留期限。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “内容审核我做了三层:同步的敏感词和正则直接拦高危;异步的大模型按审核标准输出类别、风险和证据;中风险和不确定的进人工复审。发布按风险分级,模型故障时降级到人工而不是放行。审核标准作为版本化配置,修改后用标注集回归召回率和误杀率。申诉结论回流到标注集。”
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
id: scenario.customer-service-kb
|
||||
title: 客服知识库问答:从文档接入到带引用的流式回答、权限、评测和转人工
|
||||
type: scenario
|
||||
domain: cross
|
||||
stack: [rag, vector-db, elasticsearch, fastapi, vue, redis, data-pipeline, agent-ops]
|
||||
tags: [RAG, 混合检索, 权限, 流式输出, 评测, 转人工, 引用]
|
||||
level: intermediate
|
||||
prerequisites: [ai.rag.index, python.fastapi.index]
|
||||
est_hours: 3
|
||||
related: [ai.rag.scenario.permission-aware-kb, backend.elasticsearch.scenario.kb-search-sync, frontend.vue.scenario.ai-chat-stream, ai.agent-ops.scenario.agent-quality-gate]
|
||||
week: [5, 6, 13, 15]
|
||||
---
|
||||
|
||||
# 客服知识库问答:从文档接入到带引用的流式回答、权限、评测和转人工
|
||||
|
||||
> 跨技术栈的综合场景,串联前端、后端、AI 和运维。BOSS直聘 2026-10-23 的调研中,Agent(22 个岗位)、RAG(17 个)是出现最多的要求,这是最常见的落地形态,也适合作为作品集项目。
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
一家 SaaS 公司的客服每天处理大量重复问题(退货、发票、账号、错误码)。要做一个 AI 客服:
|
||||
|
||||
1. 回答必须基于公司文档,不能编造,要给出引用来源;
|
||||
2. 不同客户(租户)、不同部门的文档权限不同;
|
||||
3. 文档经常更新,更新后很快生效;
|
||||
4. 回答不了或用户不满意时转人工,并把上下文交给坐席;
|
||||
5. 需要证明效果:上线前和每次改动后都要有数据。
|
||||
|
||||
## 约束
|
||||
|
||||
- 首 token 延迟 2 秒以内,回答流式输出。
|
||||
- 模型调用有成本预算。
|
||||
- 数据不能出公司(或只能使用指定的模型服务)。
|
||||
- 团队:1 名前端、1 名后端、你(全栈 + AI)。
|
||||
|
||||
## 方案设计
|
||||
|
||||
| 层 | 方案 | 专题 |
|
||||
| --- | --- | --- |
|
||||
| 文档接入 | 连接器增量拉取 → 解析(PDF / Word / 网页,表格单独处理)→ 按标题层级分块 → 嵌入 → 写入;outbox + 队列 + 幂等消费者,每日对账 | [数据管道](../topics/backend/data-pipeline/index.md)、[RAG 切分](../topics/ai/rag/01-pipeline-chunking.md) |
|
||||
| 检索 | ES BM25 + 向量 kNN 两路召回,RRF 融合,重排取前 5;租户和部门权限作为 filter | [ES 混合检索](../topics/backend/elasticsearch/scenarios/kb-search-sync.md)、[权限感知知识库](../topics/ai/rag/scenarios/permission-aware-kb.md) |
|
||||
| 生成 | 只依据资料回答、资料不足时拒答,输出带引用编号;结构化输出(答案、引用、置信度、是否建议转人工) | [提示词与结构化输出](../topics/ai/prompt-engineering/index.md) |
|
||||
| 接口 | FastAPI SSE 流式接口,事件:`citations`、`token`、`done`、`error`;断开时取消生成 | [FastAPI 流式接口](../topics/python/fastapi/scenarios/streaming-chat-api.md) |
|
||||
| 前端 | Vue 对话组件,增量渲染 Markdown(安全清洗),引用可点击看原文,“没解决”按钮转人工 | [Vue AI 对话流](../topics/frontend/vue/scenarios/ai-chat-stream.md)、[AI 应用安全](../topics/cs/web-security/scenarios/ai-app-security.md) |
|
||||
| 缓存与限流 | 通用问题语义缓存(按租户隔离、文档更新时失效);按用户限流 | [Redis](../topics/backend/redis/index.md) |
|
||||
| 转人工 | 置信度低 / 用户点击 / 反复追问时创建工单,附对话摘要和检索资料 | [工单系统](ticket-system.md) |
|
||||
| 评测与监控 | 标注集(问题 → 应引用文档 → 参考答案),CI 中跑召回率@K、引用正确率、拒答正确率;线上记录 Bad Case、成本、延迟 | [Agent 质量门禁](../topics/ai/agent-ops/scenarios/agent-quality-gate.md) |
|
||||
|
||||
## 关键代码
|
||||
|
||||
接口骨架(示意;完整实现见第 5~6 周项目和各专题示例):
|
||||
|
||||
```python
|
||||
@app.post("/chat")
|
||||
async def chat(req: ChatRequest, user: User = Depends(current_user)):
|
||||
async def events():
|
||||
hits = await retrieve(req.question, acl=user.acl, k=20) # 两路召回 + 权限过滤
|
||||
top = await rerank(req.question, hits, k=5)
|
||||
if not top or top[0].score < MIN_SCORE:
|
||||
await create_ticket(user, req, reason="no_context")
|
||||
yield sse("done", {"answer": "抱歉,资料中没有找到相关内容,已为你转人工。", "handoff": True})
|
||||
return
|
||||
yield sse("citations", [c.public() for c in top]) # 先发引用,前端先展示来源
|
||||
async for token in llm.stream(build_prompt(req.question, top, history=req.history)):
|
||||
yield sse("token", {"text": token})
|
||||
yield sse("done", {"handoff": False})
|
||||
return StreamingResponse(events(), media_type="text/event-stream")
|
||||
```
|
||||
|
||||
评测门禁(CI 中运行,比基线差则失败):
|
||||
|
||||
```python
|
||||
report = evaluate(dataset="eval/kb_v3.jsonl", retriever=retrieve, generator=answer)
|
||||
assert report.recall_at_5 >= baseline.recall_at_5
|
||||
assert report.citation_accuracy >= 0.9
|
||||
assert report.refusal_accuracy >= baseline.refusal_accuracy # 该拒答的要拒答
|
||||
```
|
||||
|
||||
## 取舍
|
||||
|
||||
| 决策 | 选择 | 放弃的 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| 检索 | 混合检索 + 重排 | 只用向量 | 错误码、型号等精确词召回差 |
|
||||
| 框架 | 主流程手写,部分组件用 LangChain | 全部用框架 | 便于调试和讲清原理 |
|
||||
| 低代码 | Dify 做 POC,正式版写代码 | 直接上线 Dify | 权限、评测、定制 UI 受限,见 [POC 到生产](../topics/ai/lowcode/scenarios/poc-to-production.md) |
|
||||
| 缓存 | 只缓存无个人信息的通用问题 | 所有问题都缓存 | 防止串数据 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 没有评测集,靠感觉调参。
|
||||
- 权限只在检索后过滤,导致结果不足或泄露。
|
||||
- 文档删除、权限变更没有同步。
|
||||
- 回答渲染成 HTML 时没有清洗。
|
||||
- 转人工时没有带上下文,用户要从头再说一遍。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “这是我做的客服知识库问答。文档通过增量同步进 ES,同时存文本和向量;查询时 BM25 和向量两路召回、RRF 融合、重排取前 5,租户和部门权限作为 filter。生成只依据资料、带引用编号,资料不足就拒答并转人工。接口是 FastAPI 的 SSE,先发引用再流式发答案。我维护了一份评测集,CI 里跑召回率、引用正确率和拒答正确率,任何改动比基线差就不合并。”(数字请用你自己项目的实测结果。)
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
id: scenario.data-sync
|
||||
title: 数据同步:业务库到搜索引擎 / 向量库 / 报表库,outbox 与 CDC、幂等、顺序、对账和重建
|
||||
type: scenario
|
||||
domain: cross
|
||||
stack: [data-pipeline, postgresql, mongodb, elasticsearch, vector-db, mq]
|
||||
tags: [数据同步, outbox, CDC, 变更流, 幂等, 顺序, 对账, 全量重建]
|
||||
level: advanced
|
||||
prerequisites: [backend.data-pipeline.index, backend.mq.index]
|
||||
est_hours: 3
|
||||
related: [backend.elasticsearch.scenario.kb-search-sync, backend.data-pipeline.scenario.third-party-sync, backend.mongodb.scenario.conversation-store, backend.mq.reliability]
|
||||
week: [6, 14]
|
||||
---
|
||||
|
||||
# 数据同步:业务库到搜索引擎 / 向量库 / 报表库,outbox 与 CDC、幂等、顺序、对账和重建
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
业务数据在 PostgreSQL(订单、知识库文档)和 MongoDB(对话记录),需要同步到:ES(全文检索)、向量库(RAG)、报表库(报表 Agent)。现状是业务代码里“写完数据库顺手写 ES”:
|
||||
|
||||
1. 写 ES 失败时数据库已提交,两边不一致;
|
||||
2. 同一条数据快速修改两次,ES 中留下的是旧版本;
|
||||
3. 删除没有同步,搜索出已下架的商品;
|
||||
4. 修改映射或换嵌入模型时不知道怎么重建。
|
||||
|
||||
## 约束
|
||||
|
||||
- 允许秒级到分钟级的延迟。
|
||||
- 不能明显增加业务写入的耗时。
|
||||
- 下游可以从源头完全重建(源头是唯一可信的数据)。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **捕获变更**(二选一或组合):
|
||||
- **outbox 模式**:业务事务中同时写 outbox 表(事件类型、实体 id、版本号),后台进程投递到消息队列 → 保证“数据库提交 ⇔ 有事件”;
|
||||
- **CDC**:读取数据库日志(PostgreSQL 逻辑复制 / Debezium,MongoDB 变更流),业务代码零侵入。
|
||||
2. **消费与写入**:
|
||||
- 消息只带 id 和版本,消费者回源读取最新数据再写下游(避免消息里的旧快照覆盖新数据);
|
||||
- 下游文档 id = 业务主键,写入带外部版本号,旧版本被拒绝;
|
||||
- 删除事件 → 删除下游文档(或标记删除);
|
||||
- 同一实体的事件按实体 id 分区,保证顺序;
|
||||
- 失败重试、死信、告警。
|
||||
3. **对账**:每天比较源头与下游的数量和版本(按分片哈希),差异自动修复。
|
||||
4. **全量重建**:新建索引 / 集合 → 全量导入(记录开始时间)→ 回放该时间之后的增量 → 校验 → 切换别名 → 保留旧索引回滚。
|
||||
5. **监控**:同步延迟(事件产生到写入下游的时间)、积压量、失败数、对账差异。
|
||||
|
||||
## 关键代码
|
||||
|
||||
outbox(同一事务中写业务数据和事件):
|
||||
|
||||
```sql
|
||||
BEGIN;
|
||||
UPDATE documents SET body = $2, version = version + 1, updated_at = now() WHERE id = $1;
|
||||
INSERT INTO outbox (aggregate, aggregate_id, event, version)
|
||||
SELECT 'document', id, 'updated', version FROM documents WHERE id = $1;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
消费者:回源 + 外部版本号(Elasticsearch Python 客户端,示意):
|
||||
|
||||
```python
|
||||
def handle(event: dict) -> None:
|
||||
doc = repo.get(event["aggregate_id"]) # 回源读取最新数据
|
||||
if doc is None or doc.deleted:
|
||||
es.options(ignore_status=404).delete(index="kb", id=event["aggregate_id"])
|
||||
return
|
||||
es.index(index="kb", id=doc.id, document=to_search_doc(doc),
|
||||
version=doc.version, version_type="external") # 版本小于等于已有版本时 ES 返回冲突,直接忽略
|
||||
```
|
||||
|
||||
基础组件的可运行示例:[增量同步管道](../topics/backend/data-pipeline/examples/incremental-sync/)、[MongoDB 变更流](../topics/backend/mongodb/examples/conversations/)、[ES 别名切换](../topics/backend/elasticsearch/examples/hybrid-search/)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 决策 | outbox | CDC |
|
||||
| --- | --- | --- |
|
||||
| 侵入性 | 需要改业务代码 | 业务零侵入 |
|
||||
| 事件语义 | 业务事件,含义清晰 | 行级变更,需要转换 |
|
||||
| 运维 | 简单 | 需要部署和维护 CDC 组件、处理日志保留 |
|
||||
| 适合 | 中小团队、事件少 | 表多、多个下游 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 业务代码中双写(数据库 + ES),没有事务保证。
|
||||
- 消息中带完整快照,乱序时旧快照覆盖新数据。
|
||||
- 忘记同步删除和权限变更。
|
||||
- 重建期间的增量没有回放,切换后丢数据。
|
||||
- 没有同步延迟监控,下游落后几小时才发现。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “我们把‘写库顺手写 ES’改成了 outbox:业务事务里同时写 outbox 表,后台投递到队列,保证提交和事件一致。消费者只拿 id 回源读最新数据,用业务主键作为文档 id、版本号作为外部版本,乱序和重复都不会让旧数据覆盖新数据;删除也同步。每天按分片哈希对账;换映射或嵌入模型时新建索引全量导入、回放增量、校验后切别名。监控同步延迟和积压。”
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
id: scenario.flash-sale
|
||||
title: 秒杀 / 高并发抢购:流量削峰、Redis 原子扣库存、异步下单、防超卖和防刷
|
||||
type: scenario
|
||||
domain: cross
|
||||
stack: [redis, mq, postgresql, gin, nginx, vue]
|
||||
tags: [秒杀, 高并发, 库存, Lua, 削峰, 异步下单, 幂等, 限流, 防刷]
|
||||
level: advanced
|
||||
prerequisites: [backend.redis.index, backend.mq.index]
|
||||
est_hours: 3
|
||||
related: [backend.redis.scenario.stock-reservation, backend.mq.scenario.delayed-order-cancel, backend.redis.lock-ratelimit]
|
||||
week: [3, 14, 16]
|
||||
---
|
||||
|
||||
# 秒杀 / 高并发抢购:流量削峰、Redis 原子扣库存、异步下单、防超卖和防刷
|
||||
|
||||
> 经典的后端面试场景题。你的主线是 AI 应用,但“高并发怎么设计”几乎每个后端 / 全栈面试都会问。
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
电商平台做限量 1000 件的秒杀,预计瞬间几十万请求:
|
||||
|
||||
1. 上次活动数据库被打垮,整个商城不可用;
|
||||
2. 出现超卖(卖出 1003 件);
|
||||
3. 同一用户用脚本抢到多件;
|
||||
4. 用户付款超时后库存没有释放。
|
||||
|
||||
## 约束
|
||||
|
||||
- 活动时间固定,流量可预期。
|
||||
- 不能影响商城的其他功能。
|
||||
- 库存数据最终以数据库为准。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **前端和网关削峰**:静态页面走 CDN;按钮到点才可点击、点击后置灰;验证码或答题打散请求;Nginx / 网关按 IP 和用户限流。
|
||||
2. **独立部署**:秒杀服务独立,和主站隔离,挂了不影响主站。
|
||||
3. **预热**:活动前把库存加载到 Redis。
|
||||
4. **原子扣减**:Redis Lua 脚本一次完成“检查是否已购买 → 检查库存 → 扣减 → 记录用户”,避免超卖和重复购买。
|
||||
5. **异步下单**:扣减成功后发消息,下单服务按数据库能承受的速率消费;数据库层 `UPDATE stock SET n = n - 1 WHERE id = ? AND n > 0` 再兜底一次,订单表用户 + 活动唯一约束保证幂等。
|
||||
6. **结果查询**:前端轮询“排队中 / 成功 / 失败”。
|
||||
7. **超时释放**:订单创建时投递延迟消息,到期未支付则取消订单、库存加回 Redis 和数据库。
|
||||
8. **防刷**:登录态 + 设备指纹 + 风控规则;隐藏真实下单地址(到点才下发带签名的令牌)。
|
||||
|
||||
## 关键代码
|
||||
|
||||
Redis Lua 原子扣减(可以直接在 Redis 中运行):
|
||||
|
||||
```lua
|
||||
-- KEYS[1]=库存 key KEYS[2]=已购用户集合 ARGV[1]=用户 id
|
||||
if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then
|
||||
return -1 -- 已经抢过
|
||||
end
|
||||
local stock = tonumber(redis.call('GET', KEYS[1]) or '0')
|
||||
if stock <= 0 then
|
||||
return 0 -- 售罄
|
||||
end
|
||||
redis.call('DECR', KEYS[1])
|
||||
redis.call('SADD', KEYS[2], ARGV[1])
|
||||
return 1 -- 成功,随后发消息异步下单
|
||||
```
|
||||
|
||||
预占库存与超时释放的完整讨论见 [Redis 库存预占](../topics/backend/redis/scenarios/stock-reservation.md)、[延迟消息取消订单](../topics/backend/mq/scenarios/delayed-order-cancel.md)。
|
||||
|
||||
## 取舍
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 扣库存位置 | Redis 原子扣减 + 数据库兜底 | 数据库扛不住瞬时流量 |
|
||||
| 下单 | 异步(消息队列) | 削峰,按数据库能力消费 |
|
||||
| 一致性 | 最终一致 + 对账 | Redis 和数据库之间可能短暂不一致 |
|
||||
| 用户体验 | 排队中 + 轮询结果 | 换取系统稳定 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 先查库存再扣减(两步操作),并发下超卖。
|
||||
- Redis 扣减成功但消息发送失败,库存“丢失”:需要补偿(本地消息表 / 对账)。
|
||||
- 数据库下单没有唯一约束,消息重复消费产生重复订单。
|
||||
- 超时取消时没有检查订单是否已支付。
|
||||
- 秒杀服务和主站共用数据库连接池。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “秒杀我分层削峰:CDN 静态页、按钮控制和验证码打散、网关限流;秒杀服务独立部署。库存提前加载到 Redis,用 Lua 脚本原子完成‘是否已购、库存检查、扣减、记录用户’,杜绝超卖和重复购买。扣减成功后发消息异步下单,数据库用条件更新再兜底,订单表唯一约束保证幂等;未支付由延迟消息超时释放库存。Redis 和数据库之间通过对账保证最终一致。”
|
||||
+12
-10
@@ -8,15 +8,17 @@ stack: []
|
||||
|
||||
# 跨技术栈企业服务场景
|
||||
|
||||
每个场景按“业务背景与问题 → 约束 → 方案设计 → 关键代码 → 取舍 → 易错点 → 面试怎么讲”组织,综合多个技术栈。
|
||||
每个场景按“业务背景与问题 → 约束 → 方案设计 → 关键代码 → 取舍 → 易错点 → 面试怎么讲”组织,综合多个技术栈,并链接到各专题中更细的场景和可运行示例。面试中的“系统设计题”“讲一个你做过的项目”都可以从这里取材。
|
||||
|
||||
> 建设中,计划包含:
|
||||
| 场景 | 主要技术栈 | 对应周 |
|
||||
| --- | --- | --- |
|
||||
| [客服知识库问答](customer-service-kb.md) | RAG、ES 混合检索、FastAPI SSE、Vue、评测 | 5、6、13、15 |
|
||||
| [工单系统](ticket-system.md) | PostgreSQL 状态机、MQ、LangChain 分类、React | 3、11、14 |
|
||||
| [审批流 + 人工介入的 Agent](approval-human-in-loop.md) | LangGraph interrupt、检查点、幂等 | 11、12 |
|
||||
| [报表生成 Agent](report-agent.md) | Text-to-SQL、语义层、RLS、评测 | 10、13、14 |
|
||||
| [秒杀 / 高并发抢购](flash-sale.md) | Redis Lua、MQ、数据库兜底 | 3、14、16 |
|
||||
| [多租户 SaaS](multi-tenant-saas.md) | RLS、租户上下文、配额、成本分摊 | 6、13、14 |
|
||||
| [内容审核](content-moderation.md) | 规则 + 模型 + 人工、异步流水线 | 13、14 |
|
||||
| [数据同步](data-sync.md) | outbox、CDC、ES / 向量库、对账、重建 | 6、14 |
|
||||
|
||||
- 客服知识库问答(`customer-service-kb`)
|
||||
- 工单系统(智能分类与流转)(`ticket-system`)
|
||||
- 审批流 + 人工介入的 Agent(`approval-human-in-loop`)
|
||||
- 报表生成 Agent(自然语言查数)(`report-agent`)
|
||||
- 秒杀 / 高并发抢购(`flash-sale`)
|
||||
- 多租户 SaaS(`multi-tenant-saas`)
|
||||
- 内容审核(`content-moderation`)
|
||||
- 数据同步(数据库到搜索 / 向量库)(`data-sync`)
|
||||
各技术栈内的单栈场景见各专题的 `scenarios/` 目录([专题总览](../topics/index.md))。
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
id: scenario.multi-tenant-saas
|
||||
title: 多租户 SaaS:数据隔离方案、租户上下文、配额与限流、AI 成本分摊和租户级配置
|
||||
type: scenario
|
||||
domain: cross
|
||||
stack: [postgresql, vector-db, redis, fastapi, gin, llm-serving, k8s]
|
||||
tags: [多租户, 数据隔离, 行级安全, 租户上下文, 配额, 限流, 成本分摊, 噪声邻居]
|
||||
level: advanced
|
||||
prerequisites: [backend.postgresql.index, ai.vector-db.index]
|
||||
est_hours: 3
|
||||
related: [ai.vector-db.scenario.multi-tenant-index, ai.rag.scenario.permission-aware-kb, ai.llm-serving.scenario.multi-provider-gateway]
|
||||
week: [6, 13, 14]
|
||||
---
|
||||
|
||||
# 多租户 SaaS:数据隔离方案、租户上下文、配额与限流、AI 成本分摊和租户级配置
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
AI 客服做成了 SaaS,服务几百家企业客户:
|
||||
|
||||
1. 一个开发者漏写了租户条件,A 公司看到了 B 公司的工单(严重事故);
|
||||
2. 一个大客户批量导入文档,向量化任务占满资源,其他客户的问答变慢(噪声邻居);
|
||||
3. 模型费用无法按客户核算,定价没有依据;
|
||||
4. 大客户要求数据物理隔离、使用自己的模型密钥。
|
||||
|
||||
## 约束
|
||||
|
||||
- 大多数客户规模小,单独部署成本太高。
|
||||
- 少数大客户愿意为隔离付费。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **隔离分级**:
|
||||
| 级别 | 方式 | 适合 |
|
||||
| --- | --- | --- |
|
||||
| 共享表 + `tenant_id` + 行级安全 | 成本最低 | 大多数客户 |
|
||||
| 每租户独立 schema / 向量集合 | 中等 | 中型客户 |
|
||||
| 独立数据库 / 独立部署 | 最高 | 有合规要求的大客户 |
|
||||
通过“租户 → 数据源”的路由表决定访问哪里,代码对上层透明。
|
||||
2. **租户上下文**:网关从令牌中解析租户,放入请求上下文;数据访问层自动注入(ORM 全局过滤 + PostgreSQL RLS 兜底);缓存 key、向量检索 filter、对象存储路径、日志都带租户 id。见[多租户向量索引](../topics/ai/vector-db/scenarios/multi-tenant-index.md)。
|
||||
3. **防噪声邻居**:按租户限流(Redis);后台任务队列按租户公平调度(每个租户的并发上限);大批量导入走单独队列。
|
||||
4. **配额与计费**:每次模型调用记录租户、模型、输入 / 输出 token、成本;按套餐设置月度额度,超额降级到小模型或拒绝;报表给销售和财务。见[多模型网关](../topics/ai/llm-serving/scenarios/multi-provider-gateway.md)。
|
||||
5. **租户级配置**:提示词、模型选择、品牌样式、功能开关按租户配置;支持客户自带模型密钥(加密存储,按租户使用)。
|
||||
6. **测试**:跨租户访问的自动化测试(用 A 的令牌访问 B 的每类资源,必须 404 / 403)放进 CI。
|
||||
|
||||
## 关键代码
|
||||
|
||||
租户上下文 + RLS(FastAPI + PostgreSQL,示意):
|
||||
|
||||
```python
|
||||
tenant_ctx: ContextVar[str] = ContextVar("tenant_id")
|
||||
|
||||
@app.middleware("http")
|
||||
async def tenant_middleware(request: Request, call_next):
|
||||
claims = verify_jwt(request.headers.get("authorization"))
|
||||
token = tenant_ctx.set(claims["tenant_id"])
|
||||
try:
|
||||
return await call_next(request)
|
||||
finally:
|
||||
tenant_ctx.reset(token)
|
||||
|
||||
@contextmanager
|
||||
def tenant_conn():
|
||||
with pool.connection() as conn, conn.transaction():
|
||||
conn.execute("SELECT set_config('app.tenant_id', %s, true)", (tenant_ctx.get(),)) # RLS 策略读取它
|
||||
yield conn
|
||||
|
||||
def cache_key(*parts: str) -> str:
|
||||
return ":".join(["t", tenant_ctx.get(), *parts]) # 缓存 key 带租户,防止串数据
|
||||
```
|
||||
|
||||
跨租户访问测试:
|
||||
|
||||
```python
|
||||
@pytest.mark.parametrize("path", ["/tickets/{id}", "/docs/{id}", "/conversations/{id}"])
|
||||
def test_cross_tenant_access_denied(path, tenant_a_token, tenant_b_resources):
|
||||
r = client.get(path.format(id=tenant_b_resources[path]), headers=auth(tenant_a_token))
|
||||
assert r.status_code in (403, 404)
|
||||
```
|
||||
|
||||
## 取舍
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 隔离方式 | 分级(共享为主,大客户独立) | 平衡成本和合规 |
|
||||
| 租户过滤 | 应用层自动注入 + 数据库 RLS 兜底 | 一层失误还有另一层 |
|
||||
| 资源公平 | 租户级限流和任务并发上限 | 防噪声邻居 |
|
||||
| 计费 | 按 token 记录到租户 | 定价有依据 |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 只依赖开发者记得写 `WHERE tenant_id = ?`。
|
||||
- 缓存 key、向量检索、文件路径漏了租户维度。
|
||||
- 异步任务中丢失租户上下文(ContextVar 不会自动跨进程 / 跨消息传递,需要在消息中显式携带)。
|
||||
- 管理后台的“超级管理员”接口没有审计。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “多租户我做了分级隔离:大多数客户共享表加 tenant_id,数据库层用行级安全兜底;有合规要求的大客户独立库,通过路由表透明切换。租户从令牌解析放进上下文,数据访问层自动注入,缓存 key、向量检索、文件路径都带租户。防噪声邻居靠租户级限流和任务并发上限;每次模型调用按租户记 token 和成本,支撑套餐定价。CI 里有跨租户访问测试。”
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
id: scenario.report-agent
|
||||
title: 报表生成 Agent:自然语言查数、安全的 Text-to-SQL、结果校验、图表和成本控制
|
||||
type: scenario
|
||||
domain: cross
|
||||
stack: [agent-design, function-calling, postgresql, data-pipeline, web-security, react, agent-ops]
|
||||
tags: [Text-to-SQL, 报表, 只读账号, SQL 校验, 语义层, 行级安全, 评测]
|
||||
level: advanced
|
||||
prerequisites: [ai.agent-design.index, backend.postgresql.index]
|
||||
est_hours: 3
|
||||
related: [ai.agent-design.scenario.report-agent, cs.web-security.scenario.ai-app-security, backend.data-pipeline.scenario.third-party-sync]
|
||||
week: [10, 13, 14]
|
||||
---
|
||||
|
||||
# 报表生成 Agent:自然语言查数、安全的 Text-to-SQL、结果校验、图表和成本控制
|
||||
|
||||
## 业务背景与问题
|
||||
|
||||
运营和商家希望用一句话查数据(“上个月各渠道的退款率,按周看趋势”)。早期原型直接让模型写 SQL 执行:
|
||||
|
||||
1. 生成的 SQL 有时口径错误,结果看起来合理但是错的;
|
||||
2. 有一次生成了没有租户条件的查询,看到了其他商家的数据;
|
||||
3. 复杂查询拖慢了业务主库;
|
||||
4. 一个问题调用模型多次,成本不可控。
|
||||
|
||||
## 约束
|
||||
|
||||
- 数据来自同步过来的订单库(见[数据同步](data-sync.md))。
|
||||
- 只读;结果要能解释(口径、SQL)。
|
||||
|
||||
## 方案设计
|
||||
|
||||
1. **语义层**:数据团队以 YAML 定义指标(退款率 = 退款订单数 / 支付订单数)和维度(渠道、周);模型优先选择“指标 + 维度 + 过滤条件”,由代码生成 SQL;覆盖不了的才让模型写 SQL。
|
||||
2. **SQL 安全**(见 [AI 应用安全](../topics/cs/web-security/scenarios/ai-app-security.md)):只读账号、只授权报表视图、连接只读副本;SQL 解析后只允许单条 SELECT 和白名单表;**租户隔离由数据库行级安全(RLS)保证**,会话变量由代码设置,模型无法绕过;强制 LIMIT、语句超时。
|
||||
3. **Agent 流程**:理解 → 澄清(时间范围不明确时先问)→ 选指标 / 生成 SQL → 校验 → 执行 → 结果检查(空结果、异常值)→ 图表配置和文字结论。见[报表 Agent](../topics/ai/agent-design/scenarios/report-agent.md)。
|
||||
4. **可解释**:附口径说明和 SQL(可折叠),用户可“报告错误”进入 Bad Case 库。
|
||||
5. **成本**:模型调用次数上限(中间件)、常见问题缓存、简单问题路由到小模型(见[多模型网关](../topics/ai/llm-serving/scenarios/multi-provider-gateway.md))。
|
||||
6. **评测**:问题 → 期望结果(不是期望 SQL),比较执行结果;CI 中运行。
|
||||
|
||||
## 关键代码
|
||||
|
||||
数据库层的租户隔离(PostgreSQL RLS):
|
||||
|
||||
```sql
|
||||
ALTER TABLE rpt_orders ENABLE ROW LEVEL SECURITY;
|
||||
CREATE POLICY tenant_isolation ON rpt_orders
|
||||
USING (tenant_id = current_setting('app.tenant_id'));
|
||||
GRANT SELECT ON rpt_orders TO report_reader; -- 报表账号只读,且不是表的所有者(所有者默认不受 RLS 限制)
|
||||
```
|
||||
|
||||
执行模型生成的 SQL(示意):
|
||||
|
||||
```python
|
||||
def run_report_sql(sql: str, tenant_id: str) -> list[tuple]:
|
||||
check_select_only(sql, allowed_tables={"rpt_orders", "rpt_refunds"}) # 用 SQL 解析库(如 sqlglot)做结构校验,不用正则
|
||||
with readonly_pool.connection() as conn, conn.transaction():
|
||||
conn.execute("SET LOCAL statement_timeout = '5s'")
|
||||
conn.execute("SELECT set_config('app.tenant_id', %s, true)", (tenant_id,)) # 事务内有效,由代码设置
|
||||
return conn.execute(f"SELECT * FROM ({sql}) AS q LIMIT 1000").fetchall()
|
||||
```
|
||||
|
||||
## 取舍
|
||||
|
||||
| 决策 | 选择 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 生成方式 | 语义层优先,SQL 兜底 | 口径一致、可控 |
|
||||
| 租户隔离 | RLS + 会话变量 | 不依赖模型写对条件,子查询 / CTE 也覆盖 |
|
||||
| 执行位置 | 只读副本 | 不影响主库 |
|
||||
| 评测方式 | 比较执行结果 | 同一问题有多种正确 SQL |
|
||||
|
||||
## 易错点
|
||||
|
||||
- 让模型自己写租户条件。
|
||||
- 用正则判断 SQL 是否只读(注释、多语句可绕过)。
|
||||
- 报表账号是表的所有者,RLS 不生效(需要 `FORCE ROW LEVEL SECURITY` 或换账号)。
|
||||
- 结果为空时模型“编”一个结论。
|
||||
- 只评测 SQL 能否执行,不评测结果是否正确。
|
||||
|
||||
## 面试怎么讲
|
||||
|
||||
> “报表 Agent 的核心是不信任模型生成的 SQL。常见指标走语义层由代码生成 SQL;必须让模型写时,解析后只允许单条 SELECT 和白名单表,租户隔离交给 PostgreSQL 的行级安全,会话变量由代码设置,模型绕不过去;只读账号连只读副本并设超时。评测集比较执行结果而不是 SQL 文本,每个回答附口径和 SQL,用户一键报错进 Bad Case 库。”
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
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 用延迟消息,到期再检查状态决定是否升级。分类服务挂了就直接人工分派,不影响主流程。”
|
||||
Reference in new issue
Block a user