# 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
{{ draft }}
采用
重生成
👍
👎
重新生成
```
#### 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 满载跑批产出,待评审*