Files
bigwo/test2/VOICE_REFACTOR_PLAN.md

19 KiB
Raw Permalink Blame History

语音通话模块重构方案 — 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 环境变量

# 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 依赖

{
  "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

改动 1persistUserSpeech / persistAssistantSpeech 同步写 Redis

persistUserSpeech(session, text)
  ├─ 原有逻辑不变MySQL + 字幕推送)
  └─ 新增redisClient.pushMessage(sessionId, { role: 'user', content: text })

persistAssistantSpeech(session, text, opts)
  ├─ 原有逻辑不变
  └─ 新增redisClient.pushMessage(sessionId, { role: 'assistant', content: text })

改动 2sendExternalRag 接受新格式

现状sendExternalRag(session, [{ title, content }])
  → 直接发 event 502

新增:注入上下文到 ragItems
  → ragItems 前置一条 { title: "对话上下文", content: "最近5轮..." }
  → 再跟 3 条 KB 片段
  → 发 event 502

4.8 修改 realtimeDialogRouting.jsresolveReply

现状 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 中控制
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