# AI Wingman 设计文档 **版本**: 1.0 **生成日期**: 2026-06-15 **作者**: Claude(满载跑批产出) **状态**: 设计阶段,待评审 **关联**: [[阶段2-3-任务]] §3 / [[阶段4-5-规划]] §4.1.2 --- ## 📌 1. 概述 ### 1.1 什么是 Wingman **Wingman**(僚机) = 坐席工作台的 AI 辅助系统,在坐席处理会话时**实时**提供: - 草稿回复(坐席打字 → AI 实时给草稿) - 自动摘要(会话结束 → AI 200 字摘要) - 知识推荐(对话中识别关键字 → 推 FAQ) - 排查步骤(员工描述问题 → AI 给 step-by-step) **目标**: 减少坐席重复劳动 50%,提升响应速度 30%。 ### 1.2 与现有 AI 的区别 | 维度 | 现有 AI(企微机器人) | Wingman | |---|---|---| | 用户 | 员工 | 坐席 | | 触发 | 员工提问 | 坐席打字 / 结束会话 / 关键字 | | 输出 | 完整回复给员工 | 草稿 / 摘要 / 步骤 给坐席审 | | 集成 | 企微 1 对 1 | 坐席工作台 右侧栏 | | 作用 | 自助解决 | 辅助坐席 | ### 1.3 关键指标 | 指标 | 目标 | |---|---| | 草稿采纳率 | ≥ 50% | | 摘要准确率 | ≥ 80% | | 知识命中率 | ≥ 30% | | 排查步骤有效率 | ≥ 60% | | 响应延迟 P95 | ≤ 1.5 秒 | --- ## 📌 2. 架构 ### 2.1 系统架构 ``` ┌─────────────────────────────────────────────────────────┐ │ 坐席浏览器 (frontend-agent) │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 对话区 │ │ 右侧栏 │ │ 标注面板 │ │ │ │ (主) │ │ (Wingman) │ │ (阶段4) │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ └─────────┼─────────────────┼─────────────────┼───────────┘ │ HTTP/WS │ WS(实时推送) │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ FastAPI 后端 │ │ ┌──────────────────────────────────────────────┐ │ │ │ WingmanService (ai_wingman.py) │ │ │ │ ├── draft_reply() 草稿回复 │ │ │ │ ├── summarize() 自动摘要 │ │ │ │ ├── recommend_knowledge() 知识推荐 │ │ │ │ └── troubleshoot() 排查步骤 │ │ │ └────────────────┬─────────────────────────────┘ │ │ │ │ │ ┌────────────────▼─────────────────────────────┐ │ │ │ DifyClient (dify_client.py) │ │ │ │ - 流式 / 阻塞 / 异步 统一封装 │ │ │ └────────────────┬─────────────────────────────┘ │ │ │ │ │ ┌────────────────▼─────────────────────────────┐ │ │ │ Redis 缓存 + 限流 + 配额 │ │ │ └──────────────────────────────────────────────┘ │ └────────────────────┬────────────────────────────────────┘ │ HTTPS ▼ ┌─────────────────────────────────────────────────────────┐ │ Dify 平台 (企微 AI 机器人已在用) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 工作流 A │ │ 工作流 B │ │ 工作流 C │ │ │ │ 草稿回复 │ │ 摘要生成 │ │ 排查步骤 │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 2.2 数据流 #### 2.2.1 草稿回复(实时) ``` [坐席打字] → debounce 300ms → 调 WingmanService.draft_reply(conv_id, last_employee_msg, context) → Dify 流式生成 → 推 WS 推前端 → 右侧栏显示 3 条草稿 → 坐席点"采用" → 草稿填入输入框 ``` #### 2.2.2 自动摘要(异步) ``` [坐席点"结单"] → 后端结单逻辑 → 触发 WingmanService.summarize(conv_id) → 异步任务(BackgroundTasks) → 调 Dify 摘要工作流 → 存 `conversations.summary` → 推 WS 通知"摘要已生成" → 坐席可编辑确认 ``` #### 2.2.3 知识推荐(被动) ``` [员工发消息] → 消息路由服务(message_router) → 检测关键字(VPN / 密码 / Outlook 等) → 命中 → 调 WingmanService.recommend_knowledge(keyword) → 推 WS 推 5 条 FAQ → 坐席右侧栏显示 ``` #### 2.2.4 排查步骤(主动) ``` [坐席点"AI 排查"] → 弹窗选问题分类 → 输入员工描述 → 调 WingmanService.troubleshoot(category, description) → 调 Dify 排查工作流 → 推 5-7 步 step-by-step → 坐席可发给员工(企微应用消息) ``` ### 2.3 部署架构 - **Dify**: 已有(企微 AI 机器人用) - **Wingman 工作流**: 新建(3 个工作流) - **Redis 缓存**: 已有,加 `wingman:draft:{conv_id}:{msg_hash}` key - **WS**: 已有 `ws_manager`,扩展推送 `wingman_event` 类型 --- ## 📌 3. 后端实现 ### 3.1 数据模型 ```python # backend/app/models/wingman.py from sqlalchemy import Column, String, Integer, DateTime, JSON, ForeignKey from .base import Base class WingmanDraft(Base): """草稿回复(短期存储,坐席可重生成)""" __tablename__ = "wingman_drafts" id = Column(Integer, primary_key=True) conv_id = Column(String, ForeignKey("conversations.id")) agent_id = Column(String, ForeignKey("agents.id")) # 触发消息(员工最后一条) trigger_message_id = Column(String, ForeignKey("messages.id")) # 草稿内容(3 条) drafts = Column(JSON) # ["draft1", "draft2", "draft3"] # 上下文 context_messages = Column(JSON) # 最近 10 条消息 # 状态 accepted_index = Column(Integer, nullable=True) # 坐席点了第几条(0/1/2) created_at = Column(DateTime) expires_at = Column(DateTime) # 5 分钟后过期 class WingmanSummary(Base): """会话摘要""" __tablename__ = "wingman_summaries" id = Column(Integer, primary_key=True) conv_id = Column(String, ForeignKey("conversations.id"), unique=True) summary = Column(String(2000)) # AI 生成 edited_summary = Column(String(2000)) # 坐席改后 final_summary = Column(String(2000)) # 最终(edited or summary) agent_id = Column(String, ForeignKey("agents.id")) model = Column(String) # 用的模型 created_at = Column(DateTime) updated_at = Column(DateTime) class WingmanKnowledgeHit(Base): """知识库命中记录(供阶段 4 分析)""" __tablename__ = "wingman_knowledge_hits" id = Column(Integer, primary_key=True) conv_id = Column(String) message_id = Column(String) knowledge_id = Column(Integer) agent_id = Column(String) helpful = Column(Integer, nullable=True) # 坐席反馈 created_at = Column(DateTime) ``` ### 3.2 Alembic 迁移 ```python # 013_add_wingman.py def upgrade(): op.create_table("wingman_drafts", ...) op.create_table("wingman_summaries", ...) op.create_table("wingman_knowledge_hits", ...) op.create_index("idx_wingman_drafts_conv", "wingman_drafts", ["conv_id"]) op.create_index("idx_wingman_summaries_conv", "wingman_summaries", ["conv_id"]) op.create_index("idx_wingman_hits_conv", "wingman_knowledge_hits", ["conv_id"]) ``` ### 3.3 Dify 客户端 ```python # backend/app/services/dify_client.py import httpx from typing import AsyncIterator from app.config import settings class DifyClient: def __init__(self): self.base_url = settings.DIFY_BASE_URL # https://dify.servyou-it.com/v1 self.api_key = settings.DIFY_API_KEY self.timeout = settings.DIFY_TIMEOUT # 默认 30s async def chat_messages( self, workflow_id: str, query: str, user: str, inputs: dict = None, stream: bool = True, ) -> AsyncIterator[dict] | dict: """流式 / 阻塞 调 Dify 工作流""" url = f"{self.base_url}/workflows/run" headers = {"Authorization": f"Bearer {self.api_key}"} body = { "inputs": inputs or {}, "query": query, "user": user, "response_mode": "streaming" if stream else "blocking", } if stream: async with httpx.AsyncClient(timeout=self.timeout) as client: async with client.stream("POST", url, json=body, headers=headers) as resp: async for chunk in resp.aiter_lines(): if chunk.startswith("data:"): yield json.loads(chunk[5:].strip()) else: async with httpx.AsyncClient(timeout=self.timeout) as client: resp = await client.post(url, json=body, headers=headers) return resp.json() ``` ### 3.4 Wingman 服务 ```python # backend/app/services/wingman.py import asyncio import hashlib import json from datetime import datetime, timedelta from typing import List, Optional from app.services.dify_client import DifyClient from app.services.ws_manager import ws_manager from app.config import settings class WingmanService: def __init__(self): self.dify = DifyClient() self.redis = get_redis() self.cache_ttl = 300 # 5 分钟 async def draft_reply( self, conv_id: str, agent_id: str, last_employee_msg: str, context: List[dict], ) -> List[str]: """生成 3 条草稿回复""" # 缓存 key(同输入同输出) ctx_hash = hashlib.md5(json.dumps(context, sort_keys=True).encode()).hexdigest()[:8] cache_key = f"wingman:draft:{conv_id}:{ctx_hash}" cached = self.redis.get(cache_key) if cached: return json.loads(cached) # 调 Dify 工作流 drafts = [] async for chunk in self.dify.chat_messages( workflow_id=settings.DIFY_DRAFT_WORKFLOW_ID, query=last_employee_msg, user=agent_id, inputs={ "context": context[-10:], # 最近 10 条 "tone": "professional_friendly", "n_drafts": 3, }, stream=False, # 草稿不需要流式 ): if chunk.get("event") == "workflow_finished": drafts = chunk["data"]["outputs"].get("drafts", []) break if not drafts: drafts = ["(AI 暂未生成草稿,请手动回复)"] # 缓存 self.redis.setex(cache_key, self.cache_ttl, json.dumps(drafts)) # 推 WS 给坐席 await ws_manager.send_to_agent(agent_id, { "type": "wingman_draft", "conv_id": conv_id, "drafts": drafts, }) return drafts async def summarize(self, conv_id: str, agent_id: str) -> str: """生成会话摘要(异步)""" # 取会话所有消息 messages = await self._get_conv_messages(conv_id) # 调 Dify 摘要工作流 summary = "" async for chunk in self.dify.chat_messages( workflow_id=settings.DIFY_SUMMARY_WORKFLOW_ID, query=f"会话 ID: {conv_id}", user=agent_id, inputs={ "messages": messages, "max_words": 200, "focus": ["problem", "solution", "key_info"], }, stream=False, ): if chunk.get("event") == "workflow_finished": summary = chunk["data"]["outputs"].get("summary", "") break # 存库 if summary: await self._save_summary(conv_id, agent_id, summary) # 推 WS await ws_manager.send_to_agent(agent_id, { "type": "wingman_summary", "conv_id": conv_id, "summary": summary, }) return summary async def recommend_knowledge( self, keyword: str, conv_id: str, agent_id: str, top_k: int = 5, ) -> List[dict]: """知识推荐(基于关键字 + 向量检索)""" # 向量检索(用 Dify 知识库 API) results = [] async with httpx.AsyncClient() as client: resp = await client.post( f"{settings.DIFY_BASE_URL}/datasets/{settings.DIFY_DATASET_ID}/retrieve", headers={"Authorization": f"Bearer {self.dify.api_key}"}, json={ "query": keyword, "top_k": top_k, "retrieval_model": { "search_method": "hybrid", # 向量 + 关键字 }, }, ) results = resp.json().get("records", []) # 记录命中 for r in results: await self._record_knowledge_hit(conv_id, agent_id, r["id"]) # 推 WS await ws_manager.send_to_agent(agent_id, { "type": "wingman_knowledge", "conv_id": conv_id, "knowledge": results, }) return results async def troubleshoot( self, category: str, description: str, agent_id: str, ) -> List[dict]: """排查步骤生成""" steps = [] async for chunk in self.dify.chat_messages( workflow_id=settings.DIFY_TROUBLESHOOT_WORKFLOW_ID, query=description, user=agent_id, inputs={ "category": category, "max_steps": 7, "format": "markdown", }, stream=False, ): if chunk.get("event") == "workflow_finished": steps = chunk["data"]["outputs"].get("steps", []) break return steps ``` ### 3.5 API 端点 ```python # backend/app/api/ai_wingman.py from fastapi import APIRouter, Depends, BackgroundTasks from app.services.wingman import wingman from app.dependencies import get_current_agent router = APIRouter(prefix="/api/v1/ai", tags=["AI Wingman"]) @router.post("/draft") async def gen_draft( body: DraftRequest, agent: Agent = Depends(get_current_agent), ): """生成草稿""" drafts = await wingman.draft_reply( conv_id=body.conv_id, agent_id=agent.id, last_employee_msg=body.last_message, context=body.context, ) return {"drafts": drafts} @router.post("/summary/{conv_id}") async def gen_summary( conv_id: str, background: BackgroundTasks, agent: Agent = Depends(get_current_agent), ): """生成摘要(异步)""" background.add_task(wingman.summarize, conv_id, agent.id) return {"status": "queued"} @router.post("/knowledge") async def recommend( body: KnowledgeRequest, agent: Agent = Depends(get_current_agent), ): """知识推荐""" results = await wingman.recommend_knowledge( keyword=body.keyword, conv_id=body.conv_id, agent_id=agent.id, ) return {"knowledge": results} @router.post("/troubleshoot") async def troubleshoot( body: TroubleshootRequest, agent: Agent = Depends(get_current_agent), ): """排查步骤""" steps = await wingman.troubleshoot( category=body.category, description=body.description, agent_id=agent.id, ) return {"steps": steps} @router.post("/draft/{draft_id}/accept") async def accept_draft( draft_id: int, index: int, # 0/1/2 agent: Agent = Depends(get_current_agent), ): """采纳草稿""" await wingman.accept_draft(draft_id, index, agent.id) return {"status": "accepted"} ``` --- ## 📌 4. 前端设计 ### 4.1 右侧栏布局 ``` ┌─────────────────────────────────────┐ │ 对话区 (主) │ Wingman 栏 │ │ ┌─────────────┐ │ ┌────────┐ │ │ │ 员工消息 │ │ │ 草稿 │ │ │ │ 坐席消息 │ │ │ 摘要 │ │ │ │ ... │ │ │ 知识 │ │ │ └─────────────┘ │ │ 步骤 │ │ │ [输入框 + 草稿采用] │ └────────┘ │ └─────────────────────────────────────┘ ``` ### 4.2 组件 #### 4.2.1 `WingmanPanel.vue` (主容器) ```vue ``` #### 4.2.2 `DraftList.vue` ```vue ``` #### 4.2.3 `useWingman.ts` composable ```ts import { ref } from 'vue' import { aiApi } from '@/api/ai' import { useAgentStore } from '@/stores/agent' export function useWingman() { const agentStore = useAgentStore() const genDraft = async ( convId: string, lastMessage: string, context: any[], ): Promise => { const resp = await aiApi.draft({ conv_id: convId, last_message: lastMessage, context, }) return resp.data.drafts } const acceptDraft = async (convId: string, index: number) => { await aiApi.acceptDraft(convId, index) } const markDraft = async (convId: string, index: number, type: string) => { await aiApi.markDraft(convId, index, type) } const genSummary = async (convId: string) => { return aiApi.summary(convId) } const recommendKnowledge = async (convId: string, keyword: string) => { return aiApi.knowledge({ conv_id: convId, keyword }) } const troubleshoot = async (category: string, description: string) => { return aiApi.troubleshoot({ category, description }) } return { genDraft, acceptDraft, markDraft, genSummary, recommendKnowledge, troubleshoot, } } ``` --- ## 📌 5. 性能与限流 ### 5.1 限流 | 端点 | 限制 | |---|---| | `/ai/draft` | 20 次/分钟/坐席(打字频率) | | `/ai/summary` | 5 次/分钟/坐席 | | `/ai/knowledge` | 30 次/分钟/坐席 | | `/ai/troubleshoot` | 10 次/分钟/坐席 | **实现**: `backend/app/middleware/rate_limit.py` (slowapi) ### 5.2 缓存 | 项 | 策略 | |---|---| | 草稿 | 同 conv + 同 context hash → 缓存 5 分钟 | | 知识 | 向量检索结果 → 缓存 10 分钟 | | 摘要 | 不缓存(每次新生成) | ### 5.3 超时与降级 - Dify 超时(> 10s)→ 返回"AI 暂不可用,请手动回复" - 配额耗尽(企业级 Dify 限额)→ 走备用 Dify 实例 或 降级到"无 AI" - 错误重试 1 次,二次失败 fallback --- ## 📌 6. 验收标准 ### 6.1 功能验收 - [ ] 坐席打字 → 右侧栏 1.5 秒内出现 3 条草稿 - [ ] 草稿采用率 ≥ 50%(统计) - [ ] 会话结束 → 5 秒内生成 200 字摘要 - [ ] 关键字命中 → 5 条 FAQ 推右侧栏 - [ ] 排查步骤 → 5-7 步 markdown 格式 ### 6.2 性能验收 - [ ] 草稿响应 P95 ≤ 1.5 秒 - [ ] 摘要响应 P95 ≤ 5 秒 - [ ] 知识推荐 P95 ≤ 2 秒 - [ ] 排查步骤 P95 ≤ 8 秒 ### 6.3 集成验收 - [ ] Dify 工作流 3 个跑通 - [ ] 标注数据可查(阶段 4 用) - [ ] WS 推送实时(≤ 1 秒延迟) --- ## 📌 7. 风险与缓解 | 风险 | 等级 | 缓解 | |---|---|---| | Dify API 限流 | 🟠 高 | 多实例 + 限流 + 缓存 | | 草稿质量差(不专业) | 🟡 中 | Prompt 工程 + 反馈迭代 | | 知识库召回率低 | 🟡 中 | 阶段 4 闭环优化 | | 坐席不信任 AI | 🟡 中 | 培训 + 反馈机制 | | 隐私泄露(敏感信息) | 🟠 高 | 输入脱敏 + 输出审查 | --- ## 📌 8. 实施路径 ### 8.1 阶段 A:基础设施(2 周) 1. Dify 客户端 + 限流中间件 2. 4 个 API 端点(草稿/摘要/知识/排查) 3. 数据模型 + Alembic 013 4. 前端右侧栏骨架 ### 8.2 阶段 B:Dify 工作流(2 周) 1. 草稿工作流(Prompt + 测试) 2. 摘要工作流 3. 排查工作流 4. 知识库导入现有 FAQ ### 8.3 阶段 C:联调(2 周) 1. 前后端联调 2. 性能调优 3. 错误降级 4. Beta 试用(内部 5 个坐席) ### 8.4 阶段 D:上线(1 周) 1. 全量上线 2. 监控指标 3. 收集反馈 4. 迭代优化 --- ## 📌 9. 监控指标 | 指标 | 来源 | 看板 | |---|---|---| | 草稿生成数 / 采纳数 | DB `wingman_drafts` | 阶段 4 看板 | | 摘要生成数 / 编辑数 | DB `wingman_summaries` | 阶段 4 看板 | | 知识命中数 / 反馈 | DB `wingman_knowledge_hits` | 阶段 4 看板 | | 草稿响应 P95 | 应用日志 | Prometheus | | Dify 调用失败率 | 应用日志 | Prometheus | | 坐席使用率 | DB 统计 | 阶段 4 看板 | --- ## 📌 10. 关联文档 - [[阶段2-3-任务]] §3: 阶段 3 任务拆解 - [[阶段4-5-规划]] §4.1.2: 知识库迭代 - [[外部系统集成]]: Dify 集成细节 - [[风险跟踪表]]: M-1 / M-7 / H-9 相关 --- *本设计是 2026-06-15 Claude 满载跑批产出,待评审*