364 lines
7.5 KiB
Markdown
364 lines
7.5 KiB
Markdown
|
|
# 技术方案 - AI回复来源标识
|
|||
|
|
|
|||
|
|
> **版本**: v1.0
|
|||
|
|
> **日期**: 2026-07-20
|
|||
|
|
> **状态**: [待评审]
|
|||
|
|
> **作者**: 许清楚
|
|||
|
|
> **REQ编号**: REQ-AI-004
|
|||
|
|
> **关联PRD**: `01-产品文档/03-AI服务/PRD-REQ-AI-004-AI回复来源标识-v1.0.md`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、方案概述
|
|||
|
|
|
|||
|
|
本技术方案实现 AI 回复来源标识功能,在 AI 回复消息上显示来源图标(图谱/Dify/本地路由等),提升来源透明度和可追溯性。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二、数据库设计
|
|||
|
|
|
|||
|
|
### 2.1 消息表新增字段
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
-- 消息表新增字段
|
|||
|
|
ALTER TABLE messages ADD COLUMN reply_source VARCHAR(50) DEFAULT NULL;
|
|||
|
|
-- 存储格式:JSON数组,如 '["graph"]' 或 '["routing","assets"]'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 字段说明
|
|||
|
|
|
|||
|
|
| 字段 | 类型 | 说明 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| reply_source | VARCHAR(50) | 来源标识,JSON数组格式 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、后端实现
|
|||
|
|
|
|||
|
|
### 3.1 来源标识配置
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 来源标识配置
|
|||
|
|
REPLY_SOURCE_MARKERS = {
|
|||
|
|
"byod": "🔒", # BYOD拦截
|
|||
|
|
"rule": "⚡️", # 本地快判(打招呼/呼叫人工)
|
|||
|
|
"vision": "🖼️", # 图片增强
|
|||
|
|
"graph": "🕸️", # Neo4j图谱
|
|||
|
|
"dify": "🤖", # Dify主推理
|
|||
|
|
"routing": "📋", # 业务路由
|
|||
|
|
"assets": "💻", # 资产推荐
|
|||
|
|
"fallback": "⚠️", # 降级回复
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 来源优先级
|
|||
|
|
REPLY_SOURCE_PRIORITY = [
|
|||
|
|
"rule", "byod", "vision", "graph", "dify", "routing", "assets", "fallback"
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.2 来源标识追加函数
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from collections import OrderedDict
|
|||
|
|
from typing import List
|
|||
|
|
|
|||
|
|
def append_source_marker(content: str, sources: List[str]) -> str:
|
|||
|
|
"""在回复内容末尾追加来源标识"""
|
|||
|
|
# 获取对应图标
|
|||
|
|
markers = [REPLY_SOURCE_MARKERS.get(s, "") for s in sources if s in REPLY_SOURCE_MARKERS]
|
|||
|
|
|
|||
|
|
# 按优先级排序并去重
|
|||
|
|
seen = set()
|
|||
|
|
unique_markers = []
|
|||
|
|
for marker in markers:
|
|||
|
|
if marker and marker not in seen:
|
|||
|
|
seen.add(marker)
|
|||
|
|
unique_markers.append(marker)
|
|||
|
|
|
|||
|
|
if unique_markers:
|
|||
|
|
return content + " " + "".join(unique_markers)
|
|||
|
|
return content
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.3 各路由返回格式
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 各路由返回时携带来源标识
|
|||
|
|
def handle_local_rule(...) -> dict:
|
|||
|
|
return {
|
|||
|
|
"content": "回复内容",
|
|||
|
|
"reply_source": ["rule"] # 来源列表
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def handle_byod(...) -> dict:
|
|||
|
|
return {
|
|||
|
|
"content": "回复内容",
|
|||
|
|
"reply_source": ["byod"]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def handle_vision(...) -> dict:
|
|||
|
|
return {
|
|||
|
|
"content": "回复内容",
|
|||
|
|
"reply_source": ["vision"]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def handle_graph(...) -> dict:
|
|||
|
|
return {
|
|||
|
|
"content": "回复内容",
|
|||
|
|
"reply_source": ["graph"]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def handle_dify(...) -> dict:
|
|||
|
|
return {
|
|||
|
|
"content": "回复内容",
|
|||
|
|
"reply_source": ["dify"]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def handle_routing(...) -> dict:
|
|||
|
|
return {
|
|||
|
|
"content": "回复内容",
|
|||
|
|
"reply_source": ["routing"]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def handle_assets(...) -> dict:
|
|||
|
|
return {
|
|||
|
|
"content": "回复内容",
|
|||
|
|
"reply_source": ["assets"]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def handle_fallback(...) -> dict:
|
|||
|
|
return {
|
|||
|
|
"content": "回复内容",
|
|||
|
|
"reply_source": ["fallback"]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3.4 Persist层统一处理
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def persist_message(conversation_id: str, content: str, reply_source: List[str], ...):
|
|||
|
|
"""持久化消息,统一处理来源标识"""
|
|||
|
|
|
|||
|
|
# 追加来源标识到显示内容
|
|||
|
|
display_content = append_source_marker(content, reply_source)
|
|||
|
|
|
|||
|
|
# 保存到数据库(原始content + reply_source字段)
|
|||
|
|
message = Message(
|
|||
|
|
conversation_id=conversation_id,
|
|||
|
|
content=content, # 原始内容
|
|||
|
|
reply_source=json.dumps(reply_source), # 来源列表
|
|||
|
|
created_at=datetime.now()
|
|||
|
|
)
|
|||
|
|
db.save(message)
|
|||
|
|
|
|||
|
|
# WebSocket推送(带标识的内容)
|
|||
|
|
ws_push(conversation_id, {
|
|||
|
|
"type": "ai_reply",
|
|||
|
|
"content": display_content, # 带标识
|
|||
|
|
"reply_source": reply_source
|
|||
|
|
})
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、配置文件
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
# config/ai_reply.yaml
|
|||
|
|
reply_source:
|
|||
|
|
markers:
|
|||
|
|
byod: "🔒"
|
|||
|
|
rule: "⚡️"
|
|||
|
|
vision: "🖼️"
|
|||
|
|
graph: "🕸️"
|
|||
|
|
dify: "🤖"
|
|||
|
|
routing: "📋"
|
|||
|
|
assets: "💻"
|
|||
|
|
fallback: "⚠️"
|
|||
|
|
priority:
|
|||
|
|
- rule
|
|||
|
|
- byod
|
|||
|
|
- vision
|
|||
|
|
- graph
|
|||
|
|
- dify
|
|||
|
|
- routing
|
|||
|
|
- assets
|
|||
|
|
- fallback
|
|||
|
|
max_display: 3 # 最多显示3个标识
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、前端实现
|
|||
|
|
|
|||
|
|
### 5.1 常量定义
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 来源标识映射
|
|||
|
|
const SOURCE_MARKERS = {
|
|||
|
|
byod: "🔒",
|
|||
|
|
rule: "⚡️",
|
|||
|
|
vision: "🖼️",
|
|||
|
|
graph: "🕸️",
|
|||
|
|
dify: "🤖",
|
|||
|
|
routing: "📋",
|
|||
|
|
assets: "💻",
|
|||
|
|
fallback: "⚠️"
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 来源标签(坐席端详细展示)
|
|||
|
|
const SOURCE_LABELS = {
|
|||
|
|
byod: "BYOD拦截",
|
|||
|
|
rule: "本地快判",
|
|||
|
|
vision: "图片增强",
|
|||
|
|
graph: "Neo4j图谱",
|
|||
|
|
dify: "Dify推理",
|
|||
|
|
routing: "业务路由",
|
|||
|
|
assets: "资产推荐",
|
|||
|
|
fallback: "降级回复"
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.2 H5员工端 - 消息气泡组件
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// src/components/MessageBubble.vue
|
|||
|
|
<template>
|
|||
|
|
<div class="ai-bubble">
|
|||
|
|
<div class="content">{{ content }}</div>
|
|||
|
|
<div v-if="markers" class="markers">{{ markers }}</div>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
export default {
|
|||
|
|
props: {
|
|||
|
|
content: String,
|
|||
|
|
replySource: Array
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
markers() {
|
|||
|
|
if (!this.replySource || !this.replySource.length) return '';
|
|||
|
|
return this.replySource
|
|||
|
|
.map(s => SOURCE_MARKERS[s])
|
|||
|
|
.join('');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.markers {
|
|||
|
|
margin-top: 4px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 5.3 坐席工作台 - 来源详情展示
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// src/components/SourceDetail.vue
|
|||
|
|
<template>
|
|||
|
|
<div class="source-detail">
|
|||
|
|
<span class="label">来源:</span>
|
|||
|
|
<span class="value">{{ sourceLabels }}</span>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
export default {
|
|||
|
|
props: {
|
|||
|
|
sources: Array
|
|||
|
|
},
|
|||
|
|
computed: {
|
|||
|
|
sourceLabels() {
|
|||
|
|
if (!this.sources || !this.sources.length) return '';
|
|||
|
|
return this.sources
|
|||
|
|
.map(s => SOURCE_LABELS[s])
|
|||
|
|
.join(' + ');
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六、接口设计
|
|||
|
|
|
|||
|
|
### 6.1 消息查询接口(扩展)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
// GET /api/messages/{conversation_id}
|
|||
|
|
{
|
|||
|
|
"messages": [
|
|||
|
|
{
|
|||
|
|
"id": "msg_001",
|
|||
|
|
"content": "重置密码有以下方式...",
|
|||
|
|
"reply_source": ["graph"],
|
|||
|
|
"sender_type": "ai",
|
|||
|
|
"created_at": "2026-07-20T10:00:00Z"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 6.2 WebSocket推送(扩展)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"type": "ai_reply",
|
|||
|
|
"content": "重置密码有以下方式... 🕸️",
|
|||
|
|
"reply_source": ["graph"],
|
|||
|
|
"message_id": "msg_001"
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七、测试用例
|
|||
|
|
|
|||
|
|
| 用例 | 输入 | 预期输出 |
|
|||
|
|
|------|------|----------|
|
|||
|
|
| TC1 | reply_source=["graph"] | 显示 "🕸️" |
|
|||
|
|
| TC2 | reply_source=["routing","assets"] | 显示 "📋💻" |
|
|||
|
|
| TC3 | reply_source=[] | 不显示标识 |
|
|||
|
|
| TC4 | reply_source长度>3 | 按优先级取前3个 |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 八、部署与配置
|
|||
|
|
|
|||
|
|
### 8.1 配置热更新
|
|||
|
|
|
|||
|
|
标识配置支持热更新,无需重启服务:
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 配置热加载
|
|||
|
|
def reload_config():
|
|||
|
|
global REPLY_SOURCE_MARKERS, REPLY_SOURCE_PRIORITY
|
|||
|
|
config = load_yaml("config/ai_reply.yaml")
|
|||
|
|
REPLY_SOURCE_MARKERS = config['reply_source']['markers']
|
|||
|
|
REPLY_SOURCE_PRIORITY = config['reply_source']['priority']
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 8.2 数据库迁移
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 执行迁移
|
|||
|
|
alembic upgrade head
|
|||
|
|
|
|||
|
|
# 或手动执行
|
|||
|
|
ALTER TABLE messages ADD COLUMN reply_source VARCHAR(50) DEFAULT NULL;
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 九、关联文档
|
|||
|
|
|
|||
|
|
| 文档 | 说明 |
|
|||
|
|
|------|------|
|
|||
|
|
| `PRD-REQ-AI-004-AI回复来源标识-v1.0.md` | 产品需求文档 |
|
|||
|
|
| `原型-REQ-AI-004-AI回复来源标识-v1.0.html` | 交互原型 |
|