Files
bigwo/test2/VOICE_REFACTOR_PLAN.md

497 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 语音通话模块重构方案 — KB直出 + 重排 + Redis上下文
> 版本1.0 | 日期2026-03-24
---
## 1. 现状分析
### 1.1 当前 KB 查询链路
```
ASR final
→ realtimeDialogRouting.resolveReply()
→ ToolExecutor.searchKnowledge()
→ rewriteKnowledgeQuery() // query改写
→ searchArkKnowledge() // ★ 核心瓶颈
┌──────────────────────────────────────────────┐
│ POST ark.cn-beijing.volces.com │
│ /api/v3/chat/completions │
│ │
│ model: Seed-2.0-lite (ep-xxx) │
│ metadata.knowledge_base: { dataset_ids, ... }│
│ max_tokens: 80 │
│ thinking: disabled │
│ │
│ → 1. 向量检索 top_k=3 chunks │
│ → 2. LLM 基于 chunks 生成回答文本80 tokens│ ← 要去掉
└──────────────────────────────────────────────┘
→ classifyKnowledgeAnswer() // 判断 hit/no-hit
→ external_rag (event 502) // 注入 S2S
→ S2S 基于 RAG 内容合成语音
```
### 1.2 痛点
| 痛点 | 说明 |
|------|------|
| **LLM 加工多余** | KB 检索后还经过 Seed-2.0-lite 生成 80 tokens 回答,增加 ~1-2s 延迟,且回答被模型"改写",不够原汁原味 |
| **无重排** | 向量检索 top_k=3 直接使用,靠 threshold=0.3 卡阈值,相关性排序不够精准 |
| **上下文断裂** | S2S 仅接收单次 RAG 内容,不知道前几轮聊了什么,多轮追问体验差 |
| **context 来源慢** | 上下文从 MySQL `getRecentMessages()` 获取,单次 50-200ms |
| **内存缓存不可靠** | KB 缓存用 Node.js `Map`进程重启即丢失PM2 多实例不共享 |
---
## 2. 重构目标
```
ASR final
→ resolveReply()
→ rewriteKnowledgeQuery()
→ ★ 方舟 KB 纯检索(返回原始 chunks不经 LLM 加工)
→ ★ 重排模型reranker对 chunks 按相关性排序
→ 取 top 3 reranked chunks
→ ★ 从 Redis 读取最近 5 轮对话
→ 构建 external_rag payload{ chunks + conversation_context }
→ event 502 注入 S2S
→ S2S 结合上下文 + KB 原始片段,自主生成回答并合成语音
```
### 核心改变
| 项目 | 现状 | 目标 |
|------|------|------|
| KB 检索 | chat/completions + knowledge_base metadata检索+生成一体) | 纯检索 API只返回原始 chunks |
| 回答生成 | Seed-2.0-lite 生成 80 tokens | 去掉,由 S2S 直接基于 RAG 内容生成 |
| 重排 | 无 | 方舟重排模型,对检索结果排序 |
| 上下文存储 | MySQL + Node.js 内存 Map | Redis最近 5 轮 = 10 条消息) |
| S2S 输入 | 单条 RAG 回答文本 | 3 条原始 KB 片段 + 5 轮对话上下文 |
---
## 3. 改动全景图
```
涉及文件 改动类型 改动量级 说明
─────────────────────────────────────────────────────────
新增文件
services/redisClient.js 新增 ~120行 Redis 连接管理 + 对话读写
services/kbRetriever.js 新增 ~200行 KB 纯检索 + 重排 + 结果组装
修改文件
toolExecutor.js 重构 大 searchArkKnowledge → 调用 kbRetriever
nativeVoiceGateway.js 修改 中 persist* 同步写 RedissendExternalRag 注入上下文
realtimeDialogRouting.js 修改 小 resolveReply 适配新返回格式
package.json 修改 小 添加 ioredis 依赖
.env / .env.example 修改 小 新增 Redis + 重排模型配置
不动的文件
realtimeDialogProtocol.js 不动 event 502 协议格式不变
knowledgeKeywords.js 不动 关键词路由不变
contextKeywordTracker.js 不动 关键词追踪不变
contentSafeGuard.js 不动 内容安全不变
fastAsrCorrector.js 不动 ASR纠错不变
```
---
## 4. 详细设计
### Phase 1Redis 基础设施
#### 4.1 新增 `services/redisClient.js`
**职责**Redis 连接管理 + 对话历史读写
```
功能清单:
├── createRedisClient() // 连接管理,断线重连,错误日志
├── pushMessage(sessionId, msg) // LPUSH + LTRIM 保留最近 10 条
├── getRecentHistory(sessionId) // LRANGE 获取最近 5 轮10 条)
├── clearSession(sessionId) // DEL 清理
└── setKbCache / getKbCache // KB 结果缓存(替代内存 Map
```
**数据结构设计**
```
Key: voice:history:{sessionId}
Type: List
Items: JSON string → { role, content, source, timestamp }
MaxLen: 10 (5轮 × 2条/轮)
TTL: 1800s (30分钟)
Key: voice:kb_cache:{cacheKey}
Type: String (JSON)
TTL: 300s (hit) / 120s (no-hit)
```
**降级策略**Redis 不可用时,自动降级到当前的内存 Map + MySQL 方案,不阻塞主链路。
#### 4.2 环境变量
```env
# Redis
REDIS_URL=redis://127.0.0.1:6379
REDIS_PASSWORD=
REDIS_DB=0
REDIS_KEY_PREFIX=bigwo:
# 重排模型
VOLC_ARK_RERANKER_ENDPOINT_ID=ep-xxxxxxxx
VOLC_ARK_RERANKER_TOP_N=3
# KB 纯检索(替代 chat/completions
VOLC_ARK_KB_RETRIEVAL_MODE=raw
# 可选值: raw纯检索本次启用| answer当前模式保留降级用
```
#### 4.3 依赖
```json
{
"ioredis": "^5.4.0"
}
```
`ioredis` 而非 `redis`自动重连、Sentinel/Cluster 原生支持、性能更好。
---
### Phase 2KB 检索改造
#### 4.4 新增 `services/kbRetriever.js`
**职责**KB 纯检索 + 重排 + 结果组装(替代 `searchArkKnowledge` 中的检索+生成一体逻辑)
```
功能清单:
├── retrieveChunks(query, datasetIds, topK) // 纯向量检索,返回原始 chunks
├── rerankChunks(query, chunks, topN) // 重排模型调用
├── buildRagPayload(chunks, history) // 组装 external_rag 载荷
└── searchAndRerank(query, opts) // 主入口:检索 → 重排 → 组装
```
**核心流程**
```
searchAndRerank(query, { datasetIds, sessionId, session })
├─ 1. retrieveChunks(query, datasetIds, topK=10)
│ POST /api/v3/chat/completions
│ model: 最轻量模型
│ metadata.knowledge_base: { dataset_ids, top_k: 10, threshold: 0.1 }
│ max_tokens: 1 ← 关键只要1个token目的是触发检索
│ stream: false
│ ※ 从 response 中提取 references/chunks 字段
│ ※ 如果方舟API不在response中返回原始chunks
│ → 降级方案:用 snippet 模式 prompt 让模型返回原文
│ → 或调用方舟知识库独立检索API如有
├─ 2. rerankChunks(query, chunks, topN=3)
│ POST /api/v3/chat/completions或 /api/v3/rerank
│ model: reranker-endpoint-id
│ 输入query + documents[]
│ 输出:按相关性排序的 chunks + scores
│ 超时3s
│ 降级:重排失败时直接使用原始检索排序
├─ 3. getRecentHistory(sessionId) ← 从 Redis
│ 返回最近 5 轮对话
└─ 4. buildRagPayload(top3Chunks, history)
组装 ragItems[]
[
{ title: "对话上下文", content: "用户: xxx\n助手: xxx\n..." },
{ title: "知识库片段1", content: chunk1.content },
{ title: "知识库片段2", content: chunk2.content },
{ title: "知识库片段3", content: chunk3.content },
]
```
#### 4.5 关于方舟 KB 纯检索的技术方案
方舟 `chat/completions` + `knowledge_base` 的 response 结构中,`choices[0].message` 包含 LLM 生成的回答,但**原始检索 chunks 可能在 `references` 字段中返回**(取决于方舟版本)。
需要验证的关键点:
```
方案 A优先方舟 response 中有 references 字段
→ response.data.references = [{ content, score, doc_name, ... }, ...]
→ 直接提取,不用 choices[0].message.content
方案 B备选方舟有独立的知识库检索 API
→ POST /api/v3/knowledge-base/retrieve 或类似端点
→ 纯向量检索 + BM25不经 LLM
方案 C保底利用 snippet 模式 prompt 提取原文
→ system prompt = "原样输出检索到的所有文档片段,不改写不总结"
→ max_tokens = 500
→ 解析输出为独立 chunks
```
**建议**:先用方案 A 验证 `references` 字段是否存在;若不存在,采用方案 B 或 C。
#### 4.6 重排模型选型
| 模型 | 延迟(估) | 精度 | 建议 |
|------|----------|------|------|
| bge-reranker-v2-m3 | ~200ms | 高 | 首选,多语言支持好 |
| bge-reranker-large | ~150ms | 较高 | 备选 |
| cohere-rerank-v3 | ~300ms | 最高 | 如果方舟支持 |
需要在方舟平台创建重排模型的推理接入点。
---
### Phase 3S2S 上下文注入
#### 4.7 修改 `nativeVoiceGateway.js`
**改动 1`persistUserSpeech` / `persistAssistantSpeech` 同步写 Redis**
```
persistUserSpeech(session, text)
├─ 原有逻辑不变MySQL + 字幕推送)
└─ 新增redisClient.pushMessage(sessionId, { role: 'user', content: text })
persistAssistantSpeech(session, text, opts)
├─ 原有逻辑不变
└─ 新增redisClient.pushMessage(sessionId, { role: 'assistant', content: text })
```
**改动 2`sendExternalRag` 接受新格式**
```
现状sendExternalRag(session, [{ title, content }])
→ 直接发 event 502
新增:注入上下文到 ragItems
→ ragItems 前置一条 { title: "对话上下文", content: "最近5轮..." }
→ 再跟 3 条 KB 片段
→ 发 event 502
```
#### 4.8 修改 `realtimeDialogRouting.js` — `resolveReply`
```
现状 resolveReply
→ ToolExecutor.execute('search_knowledge', { response_mode: 'answer', ... })
→ extractToolResultText → 得到 LLM 生成的回答文本
→ delivery: 'external_rag', ragItems: [{ content: LLM回答 }]
改为:
→ kbRetriever.searchAndRerank(query, { sessionId, session, datasetIds })
→ 返回 { chunks: [...], rerankedChunks: [...], history: [...], ragPayload: [...] }
→ delivery: 'external_rag', ragItems: ragPayload已包含上下文 + 3个原始片段
```
#### 4.9 修改 `toolExecutor.js`
```
现状 searchKnowledge
→ searchArkKnowledge() → LLM 加工 → classifyKnowledgeAnswer()
改为:
→ kbRetriever.searchAndRerank() → 纯检索 + 重排
→ 判断 hit/no-hit 改为基于 reranker score 阈值
→ 不再调用 classifyKnowledgeAnswer()(因为没有 LLM 生成的回答文本了)
```
---
### Phase 4清理与适配
#### 4.10 可移除/简化的代码
| 位置 | 内容 | 原因 |
|------|------|------|
| `toolExecutor.js` L925-946 | system prompt 构建baseAnswerPrompt, buildQuestionSlotInstruction等 | 不再需要 LLM 生成回答 |
| `toolExecutor.js` L953-966 | chat/completions body 构建max_tokens:80, thinking:disabled | 改为纯检索调用 |
| `toolExecutor.js` L981-996 | classifyKnowledgeAnswer() 调用 | 改为 reranker score 判断 |
| `toolExecutor.js` L47-74 | 内存 kbQueryCache Map | 迁移到 Redis |
| `realtimeDialogRouting.js` L275 | `normalizeTextForSpeech(replyText).replace(...)` 去除"根据知识库" | 不再有 LLM 加工文本 |
#### 4.11 保留不动的模块
| 模块 | 原因 |
|------|------|
| KB 保护窗口60s | 多轮追问保护仍有价值 |
| shouldForceKnowledgeRoute | 路由判断逻辑不受影响 |
| prequery 预查询 | 仍然有效,改为调用 kbRetriever |
| ASR 纠错 + 词表 | 与 KB 检索解耦 |
| HOT_ANSWERS | 保留作为 0ms 极速响应,但需标记来源 |
| 品牌保护兜底(传销问题) | 安全需求,保留 |
| 助手资料系统 | 与 KB 检索解耦 |
---
## 5. 数据流对比
### 5.1 现状LLM 加工模式)
```
用户: "小红产品怎么吃?"
searchArkKnowledge()
→ Ark API: model=Seed-2.0-lite, max_tokens=80
→ KB 检索到 3 chunks + LLM 生成回答:
"小红Activize的服用方法是兑100-150ml温水小口慢饮建议在大白之后15-30分钟。"
event 502: [{ title: "知识库结果", content: "小红Activize的服用方法..." }]
S2S 合成语音播报S2S 不知道前几轮聊了什么)
```
### 5.2 重构后(直出 + 重排 + 上下文模式)
```
用户: "小红产品怎么吃?"
kbRetriever.searchAndRerank()
→ 纯检索: top_k=10, threshold=0.1 → 得到 10 个原始 chunks
→ 重排: reranker 对 10 chunks 排序 → 取 top 3
→ Redis: 取最近 5 轮对话
event 502: [
{ title: "对话上下文",
content: "用户: 你们有什么产品?\n助手: 我们有基础三合一...\n用户: 小红是什么?\n助手: 小红是Activize Oxyplus..." },
{ title: "知识库片段1",
content: "Activize Oxyplus小红水溶性CoQ10+维生素B族+瓜拉那提取物..." },
{ title: "知识库片段2",
content: "基础三合一服用方法大白空腹→小红15-30分钟后→小白睡前..." },
{ title: "知识库片段3",
content: "小红用法兑100-150ml温水小口慢饮。水温不超过40度..." },
]
S2S 基于上下文 + 3个原始片段自主生成自然口语回答合成语音
```
---
## 6. 延迟预估
| 环节 | 现状 | 重构后 | 变化 |
|------|------|--------|------|
| query 改写 | ~5ms | ~5ms | 不变 |
| KB 检索 | ~2000ms检索+LLM生成 | ~800ms纯检索 | ↓60% |
| 重排 | 无 | ~200ms | 新增 |
| Redis 读上下文 | 无MySQL ~100ms | ~5ms | ↓95% |
| 组装 payload | ~1ms | ~2ms | 不变 |
| **总计** | **~2300ms** | **~1000ms** | **↓56%** |
> 注意S2S 接收到更丰富的 RAG 内容后,语音合成可能略增,但整体用户体验(首字节到达时间)应显著改善。
---
## 7. 风险与降级
| 风险 | 概率 | 影响 | 降级方案 |
|------|------|------|----------|
| 方舟 API 不返回原始 chunks无 references 字段) | 中 | 高 | 用 snippet prompt + max_tokens=500 提取原文 |
| 重排模型部署/调用失败 | 低 | 中 | 跳过重排,直接使用检索排序 |
| Redis 连接失败 | 低 | 中 | 降级到内存 Map + MySQL |
| S2S 处理多条 RAG 片段质量下降 | 中 | 高 | A/B 测试对比,保留旧模式开关 |
| Redis 内存溢出 | 低 | 低 | TTL=30min + maxmemory-policy=allkeys-lru |
### 关键降级开关
```env
# .env 中控制
VOLC_ARK_KB_RETRIEVAL_MODE=raw # raw | answer(旧模式)
ENABLE_RERANKER=true # true | false
ENABLE_REDIS_CONTEXT=true # true | false
```
每个新能力可独立关闭,回退到当前行为。
---
## 8. 实施计划
### 阶段一基础设施1天
| # | 任务 | 文件 | 预估 |
|---|------|------|------|
| 1.1 | 安装 ioredis 依赖 | package.json | 5min |
| 1.2 | 新建 redisClient.js实现连接管理 + pushMessage + getRecentHistory | services/redisClient.js | 2h |
| 1.3 | .env 增加 Redis 配置 | .env, .env.example | 10min |
| 1.4 | 服务器安装 Redis宝塔一键装 | 运维 | 30min |
| 1.5 | 验证 Redis 连接 + 读写 | 手动测试 | 30min |
### 阶段二Redis 对话存储接入0.5天)
| # | 任务 | 文件 | 预估 |
|---|------|------|------|
| 2.1 | persistUserSpeech 增加 Redis 写入 | nativeVoiceGateway.js | 30min |
| 2.2 | persistAssistantSpeech 增加 Redis 写入 | nativeVoiceGateway.js | 30min |
| 2.3 | resolveReply 中的 getRecentMessages 改为优先读 Redis | realtimeDialogRouting.js | 1h |
| 2.4 | KB 缓存迁移到 Redis替代内存 Map | toolExecutor.js, redisClient.js | 1h |
### 阶段三KB 纯检索 + 重排1.5天)
| # | 任务 | 文件 | 预估 |
|---|------|------|------|
| 3.1 | 方舟 API 验证:确认 references 字段是否可用 | 手动测试 | 2h |
| 3.2 | 新建 kbRetriever.jsretrieveChunks 实现 | services/kbRetriever.js | 3h |
| 3.3 | 方舟部署重排模型,获取 endpoint_id | 方舟控制台 | 1h |
| 3.4 | kbRetriever.jsrerankChunks 实现 | services/kbRetriever.js | 2h |
| 3.5 | kbRetriever.jsbuildRagPayload + searchAndRerank 主流程 | services/kbRetriever.js | 2h |
| 3.6 | toolExecutor.js 改造searchKnowledge 调用 kbRetriever | toolExecutor.js | 2h |
### 阶段四S2S 注入适配0.5天)
| # | 任务 | 文件 | 预估 |
|---|------|------|------|
| 4.1 | resolveReply 适配新的 ragItems 格式(多片段 + 上下文) | realtimeDialogRouting.js | 1h |
| 4.2 | sendExternalRag 验证多 items 发送 | nativeVoiceGateway.js | 1h |
| 4.3 | hit/no-hit 判断改为 reranker score 阈值 | kbRetriever.js | 1h |
### 阶段五:测试 + 部署1天
| # | 任务 | 说明 | 预估 |
|---|------|------|------|
| 5.1 | 单元测试kbRetriever、redisClient | tests/ | 2h |
| 5.2 | 集成测试:端到端语音对话验证 | 真机测试 | 2h |
| 5.3 | A/B 对比:旧模式 vs 新模式(延迟、回答质量) | 测试脚本 | 2h |
| 5.4 | 部署到生产 | deploy 脚本 | 1h |
**总工期预估4-5 天**
---
## 9. 验证标准
### 功能验证(必须全部通过)
| # | 场景 | 期望结果 |
|---|------|----------|
| V1 | 问"小红怎么吃" | S2S 基于 KB 原始片段回答,包含正确服用方法 |
| V2 | 连续追问"那大白呢""小白呢" | S2S 结合上下文理解"那"指的是什么,正确回答 |
| V3 | 问"你们公司正规吗" | 品牌保护兜底正常触发 |
| V4 | Redis 断开时问产品问题 | 自动降级到 MySQL不报错 |
| V5 | 重排模型超时 | 跳过重排,使用原始检索结果 |
| V6 | KB 检索无结果 | 走 honest_fallback 或 upstream_chat |
### 性能验证
| 指标 | 当前基线 | 目标 |
|------|----------|------|
| KB 查询延迟P50 | ~2300ms | <1200ms |
| KB 查询延迟P95 | ~4500ms | <2000ms |
| 上下文读取延迟 | ~100ms (MySQL) | <10ms (Redis) |
| 多轮追问准确率 | ~40%无上下文 | >80%5轮上下文 |
---
## 10. 后续迭代方向
| 方向 | 说明 | 优先级 |
|------|------|--------|
| **语义缓存** | 用 embedding 相似度判断是否命中缓存,而非精确 query 匹配 | P1 |
| **Redis Streams** | 对话历史用 Streams 代替 List支持消费者组、重放 | P2 |
| **Hybrid Search** | 向量检索 + BM25 关键词检索融合,提升召回率 | P1 |
| **动态 top_k** | 根据 query 复杂度自动调整检索数量 | P3 |
| **多轮 query 改写** | 基于 Redis 上下文做指代消解("那个"→"小红" | P1 |