From 5b1a7a430f0c71777a75826d91b806a103784f6d Mon Sep 17 00:00:00 2001 From: Shikui Wang Date: Fri, 23 Oct 2026 16:52:30 +0800 Subject: [PATCH] phase 6 part 1: eight cross-stack enterprise scenarios and scenarios index --- .revision-progress | 1 + catalog.json | 417 +++++++++++++++++- scenarios/approval-human-in-loop.md | 85 ++++ scenarios/content-moderation.md | 88 ++++ scenarios/customer-service-kb.md | 98 ++++ scenarios/data-sync.md | 92 ++++ scenarios/flash-sale.md | 84 ++++ scenarios/index.md | 22 +- scenarios/multi-tenant-saas.md | 99 +++++ scenarios/report-agent.md | 81 ++++ scenarios/ticket-system.md | 88 ++++ .../mongodb/scenarios/conversation-store.md | 2 +- 12 files changed, 1145 insertions(+), 12 deletions(-) create mode 100644 scenarios/approval-human-in-loop.md create mode 100644 scenarios/content-moderation.md create mode 100644 scenarios/customer-service-kb.md create mode 100644 scenarios/data-sync.md create mode 100644 scenarios/flash-sale.md create mode 100644 scenarios/multi-tenant-saas.md create mode 100644 scenarios/report-agent.md create mode 100644 scenarios/ticket-system.md diff --git a/.revision-progress b/.revision-progress index 5053edf..74ba764 100644 --- a/.revision-progress +++ b/.revision-progress @@ -12,3 +12,4 @@ phase5b done (frontend vite/webpack/fe-testing-perf) phase5c done (backend mongodb/elasticsearch/data-pipeline/nodejs) phase5d done (java-spring awareness, k8s, run_examples java support) phase5 done: all 43 stacks pass --strict (cs algorithms/network-os/web-security added) +phase6a done (8 cross scenarios) diff --git a/catalog.json b/catalog.json index 3ac720b..4331ab8 100644 --- a/catalog.json +++ b/catalog.json @@ -4097,7 +4097,9 @@ "backend.mongodb.document-model" ], "related": [ - "backend.mongodb.interview" + "backend.mongodb.interview", + "scenario.data-sync", + "scenario.customer-service-kb" ], "est_hours": 2, "week": [ @@ -9191,6 +9193,419 @@ "path": "research/index.md", "sections": [] }, + { + "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" + ], + "related": [ + "ai.langgraph.scenario.refund-approval", + "ai.function-calling.scenario.safe-business-tools", + "ai.agent-design.index" + ], + "est_hours": 3, + "week": [ + 11, + 12 + ], + "order": null, + "path": "scenarios/approval-human-in-loop.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, + { + "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" + ], + "related": [ + "ai.prompt-engineering.scenario.injection-defense", + "ai.agent-ops.scenario.agent-quality-gate", + "backend.mq.reliability" + ], + "est_hours": 2.5, + "week": [ + 13, + 14 + ], + "order": null, + "path": "scenarios/content-moderation.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, + { + "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" + ], + "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" + ], + "est_hours": 3, + "week": [ + 5, + 6, + 13, + 15 + ], + "order": null, + "path": "scenarios/customer-service-kb.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, + { + "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" + ], + "related": [ + "backend.elasticsearch.scenario.kb-search-sync", + "backend.data-pipeline.scenario.third-party-sync", + "backend.mongodb.scenario.conversation-store", + "backend.mq.reliability" + ], + "est_hours": 3, + "week": [ + 6, + 14 + ], + "order": null, + "path": "scenarios/data-sync.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, + { + "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" + ], + "related": [ + "backend.redis.scenario.stock-reservation", + "backend.mq.scenario.delayed-order-cancel", + "backend.redis.lock-ratelimit" + ], + "est_hours": 3, + "week": [ + 3, + 14, + 16 + ], + "order": null, + "path": "scenarios/flash-sale.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, + { + "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" + ], + "related": [ + "ai.vector-db.scenario.multi-tenant-index", + "ai.rag.scenario.permission-aware-kb", + "ai.llm-serving.scenario.multi-provider-gateway" + ], + "est_hours": 3, + "week": [ + 6, + 13, + 14 + ], + "order": null, + "path": "scenarios/multi-tenant-saas.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, + { + "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" + ], + "related": [ + "ai.agent-design.scenario.report-agent", + "cs.web-security.scenario.ai-app-security", + "backend.data-pipeline.scenario.third-party-sync" + ], + "est_hours": 3, + "week": [ + 10, + 13, + 14 + ], + "order": null, + "path": "scenarios/report-agent.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, + { + "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" + ], + "related": [ + "ai.langchain.scenario.ticket-triage", + "backend.postgresql.scenario.order-state-audit", + "backend.mq.scenario.delayed-order-cancel", + "frontend.react.scenario.long-task-ui" + ], + "est_hours": 3, + "week": [ + 3, + 11, + 14 + ], + "order": null, + "path": "scenarios/ticket-system.md", + "sections": [ + "业务背景与问题", + "约束", + "方案设计", + "关键代码", + "取舍", + "易错点", + "面试怎么讲" + ] + }, { "id": "scenarios.index", "title": "跨技术栈企业服务场景", diff --git a/scenarios/approval-human-in-loop.md b/scenarios/approval-human-in-loop.md new file mode 100644 index 0000000..99513dd --- /dev/null +++ b/scenarios/approval-human-in-loop.md @@ -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 加步骤名作为幂等键,恢复或重试都不会重复退款。超时由延迟任务以驳回恢复,每一步都写审计事件。” diff --git a/scenarios/content-moderation.md b/scenarios/content-moderation.md new file mode 100644 index 0000000..6a66929 --- /dev/null +++ b/scenarios/content-moderation.md @@ -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)。 + +## 取舍 + +| 决策 | 选择 | 原因 | +| --- | --- | --- | +| 审核方式 | 规则 + 模型 + 人工分层 | 兼顾速度、成本和准确率 | +| 发布时机 | 按风险分级 | 体验和安全的平衡 | +| 模型故障 | 降级为人工 | 不能因故障放行风险内容 | +| 标准变更 | 标注集回归 | 防止改一条规则引起大量误杀 | + +## 易错点 + +- 只看召回率(拦住多少违规),不看误杀率(误伤多少正常内容)。 +- 审核结果覆盖了用户后来编辑过的新内容(用内容哈希校验版本)。 +- 把用户内容直接拼进审核提示词,被注入。 +- 审核日志中保存了大量个人信息,没有脱敏和保留期限。 + +## 面试怎么讲 + +> “内容审核我做了三层:同步的敏感词和正则直接拦高危;异步的大模型按审核标准输出类别、风险和证据;中风险和不确定的进人工复审。发布按风险分级,模型故障时降级到人工而不是放行。审核标准作为版本化配置,修改后用标注集回归召回率和误杀率。申诉结论回流到标注集。” diff --git a/scenarios/customer-service-kb.md b/scenarios/customer-service-kb.md new file mode 100644 index 0000000..f3dc835 --- /dev/null +++ b/scenarios/customer-service-kb.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 里跑召回率、引用正确率和拒答正确率,任何改动比基线差就不合并。”(数字请用你自己项目的实测结果。) diff --git a/scenarios/data-sync.md b/scenarios/data-sync.md new file mode 100644 index 0000000..c4b0a92 --- /dev/null +++ b/scenarios/data-sync.md @@ -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、版本号作为外部版本,乱序和重复都不会让旧数据覆盖新数据;删除也同步。每天按分片哈希对账;换映射或嵌入模型时新建索引全量导入、回放增量、校验后切别名。监控同步延迟和积压。” diff --git a/scenarios/flash-sale.md b/scenarios/flash-sale.md new file mode 100644 index 0000000..587288e --- /dev/null +++ b/scenarios/flash-sale.md @@ -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 和数据库之间通过对账保证最终一致。” diff --git a/scenarios/index.md b/scenarios/index.md index ad0264e..bf87eff 100644 --- a/scenarios/index.md +++ b/scenarios/index.md @@ -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))。 diff --git a/scenarios/multi-tenant-saas.md b/scenarios/multi-tenant-saas.md new file mode 100644 index 0000000..dbc69b0 --- /dev/null +++ b/scenarios/multi-tenant-saas.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 里有跨租户访问测试。” diff --git a/scenarios/report-agent.md b/scenarios/report-agent.md new file mode 100644 index 0000000..0485638 --- /dev/null +++ b/scenarios/report-agent.md @@ -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 库。” diff --git a/scenarios/ticket-system.md b/scenarios/ticket-system.md new file mode 100644 index 0000000..23c6f46 --- /dev/null +++ b/scenarios/ticket-system.md @@ -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 用延迟消息,到期再检查状态决定是否升级。分类服务挂了就直接人工分派,不影响主流程。” diff --git a/topics/backend/mongodb/scenarios/conversation-store.md b/topics/backend/mongodb/scenarios/conversation-store.md index 22b6e93..2750e8b 100644 --- a/topics/backend/mongodb/scenarios/conversation-store.md +++ b/topics/backend/mongodb/scenarios/conversation-store.md @@ -8,7 +8,7 @@ tags: [对话记录, 文档建模, TTL, 归档, 变更流, 多租户] level: intermediate prerequisites: [backend.mongodb.document-model] est_hours: 2 -related: [backend.mongodb.interview] +related: [backend.mongodb.interview, scenario.data-sync, scenario.customer-service-kb] week: [14] ---