从语音到答案:实时语音客服机器人技术实践
面向技术博客发布的项目梳理稿。本文按业务逻辑与核心能力分节,说明「为什么这么做」与「关键怎么跑通」。
一、项目定位:语音客服为什么难
文本客服可以接受秒级延迟;语音场景则要求:
- 低首包延迟:用户说完后尽快听到回应
- 可打断(Barge-in):播报中途插话,旧答案必须立刻停掉
- 答案可信:政策、计费、作业等问题必须基于内部客服 Wiki,不能靠模型「编」
本项目 voice-robot 的定位是:客服场景的实时语音/文本对话助手。
一句话链路:
text
浏览器采集语音 → WebSocket 编排 → 腾讯 ASR → DeepAgent(强制 Wiki 工具)→ 火山 TTS 播报
它不是物理机器人控制,也不是端到端 Speech-to-Speech 单模型,而是一条可控、可观测、可审计的 ASR → LLM → TTS 级联流水线。
产品侧目标(摘自 PRD):
| 指标 | 目标 |
|---|---|
| 端到端首包 | P50 ≤ 900ms / P95 ≤ 1500ms |
| 打断生效 | P95 ≤ 300ms |
| 同轮重复提交 | < 0.1% |
二、总体架构:前端采集 + 后端编排
2.1 技术栈
| 层级 | 选型 |
|---|---|
| 前端 | React 18、TypeScript、Vite、Web Audio API、端侧 VAD |
| 后端 | FastAPI、Uvicorn、Pydantic Settings |
| Agent | LangGraph / DeepAgents、langchain-openai |
| ASR | 腾讯实时语音识别 |
| LLM | 火山方舟 Ark(OpenAI 兼容接口) |
| TTS | 火山引擎 TTS v3 双向 WebSocket |
| 知识库 | Markdown Wiki + jieba 关键词检索 |
| 可观测 | Prometheus、可选 LangSmith、审计流水(默认 SQLite) |
2.2 模块关系
text
┌─ frontend/ ─────────────────────────────────────────────┐
│ VoicePage · audioCapture(VAD) · TypewriterText · Ops │
└──────────────────────┬──────────────────────────────────┘
│ /ws/voice
┌──────────────────────▼──────────────────────────────────┐
│ FastAPI:voice_endpoint → Orchestrator │
│ ├─ TencentAsrClient (语音转写) │
│ ├─ DeepAgent + query_kefu_wiki(推理 + 知识检索) │
│ ├─ PunctuationBuffer → TTS (标点切句播报) │
│ └─ TurnManager / Audit (幂等轮次 + 审计) │
└─────────────────────────────────────────────────────────┘
业务上,Orchestrator 是唯一编排中枢:ASR 结果、轮次提交、LLM 流、TTS 切段、打断取消,都汇聚于此,避免「前端直接调 LLM / TTS」带来的状态分裂。
三、核心业务链路:一轮对话怎么走完
3.1 建连与开场白
- 前端连接
ws://host:8000/ws/voice - 发送
session_init - 后端流式下发
greeting_delta→greeting_complete(可选 TTS) - 开场白不经 LLM ;同会话首轮推理时,再把开场白注入为历史
AIMessage
设计意图:开场白固定、可控、零推理成本;同时保证多轮上下文语义连贯。
3.2 语音一轮(Happy Path)
text
用户开口
→ 前端 VAD speech_start → vad_event
→ 后端预建腾讯 ASR(规避 15s 空闲超时)
→ audio_chunk(PCM s16le / 16k / 单声道)
→ ASR → asr_partial / asr_final(字幕)
用户停说
→ VAD speech_end → 关闭 ASR
→ turn_commit_request(input_mode=voice)
→ TurnManager 幂等 commit → turn_committed(generation_id)
→ Orchestrator.run_turn:
链接增强(OCR / 网页正文)
→ DeepAgent 流式(可调 Wiki 工具)
→ llm_delta(打字机)
→ 标点切段 → TTS → tts_chunk
→ audio_complete
3.3 文本旁路
同一套 turn_commit_request,input_mode=text 时跳过 ASR,适合调试与无麦场景。
3.4 打断(Barge-in)
- 用户插话或点取消 → 前端停播 +
cancel(generation_id) TurnManager清空当前 generation- Orchestrator 轮询
is_generation_active,停止继续推 LLM/TTS - 仅把已产出的 partial 文本写回 thread,避免「未播完长文」污染多轮上下文
状态机可概括为:
text
listening → thinking → speaking
↘ interrupted → listening
同一 turn_id 只允许一次成功 commit,从协议层杜绝双触发 LLM。
四、核心功能点拆解
4.1 端侧采集与 VAD
getUserMedia开启回声消除 / 噪声抑制 / 自动增益- Float32 → 16k Int16 → base64 分片上行(约 100~200ms)
- Analyser 能量阈值产出
speech_start/speech_end
关键文件:frontend/src/audio/audioCapture.ts
4.2 WebSocket 协议与幂等轮次
上下行事件统一携带 session_id / turn_id / seq / trace;上行用 Pydantic 校验。
| 方向 | 关键事件 | 作用 |
|---|---|---|
| 上行 | session_init |
触发开场白 |
| 上行 | vad_event / audio_chunk |
语音起停与 PCM |
| 上行 | turn_commit_request |
提交本轮用户文本 |
| 上行 | cancel |
按 generation_id 打断 |
| 下行 | asr_partial / asr_final |
实时字幕 |
| 下行 | turn_committed / turn_rejected |
提交结果 |
| 下行 | llm_delta |
助手增量文本 |
| 下行 | tts_chunk / audio_complete |
播报与结束 |
权威协议:docs/protocol.md
幂等实现:backend/app/services/turn_manager.py((session_id, turn_id) 只 commit 一次)
4.3 实时 ASR 与预建连
腾讯实时 ASR 有「约 15 秒未发音频则超时」的约束。实践做法是:
- 收到
speech_start立刻建连 - 再边收
audio_chunk边append_audio speech_end后关闭连接并汇总 final
关键文件:backend/app/services/asr/tencent_ws_client.py
4.4 DeepAgent 流式推理
- 火山方舟 ChatOpenAI,
streaming=True - LangGraph
stream_mode=messages逐 token 推送 - 系统提示词约束:政策/计费/API/作业等问题必须先调
query_kefu_wiki;禁止口头提「知识库」;答复控制在约 100 token 内,利于 TTS
关键文件:backend/app/services/agents/deepagent_runner.py
4.5 客服 Wiki RAG(防幻觉核心)
知识库形态对齐「分类 / 概念 / FAQ」Markdown Wiki:
text
backend/kefu-know/wiki/
index.md
categories/ # 主题页
concepts/ # 概念页
faqs/ # 标准问答
检索主入口 retrieve_kefu_wiki 的策略:
- jieba 抽关键词(停用词 + 用户词典)
- 优先匹配
index.md路由到分类/概念;命中则不回退全库扫描 - 未命中再全量扫
categories/、concepts/ - 链接递归扩展(wikilink,深度约 2)补齐关联页与 FAQ
- 统一置信度排序(标题权重大于正文;分类/概念高于 FAQ)
- 可选 LLM 兜底:规则无结果时,让模型读 index 选页
python
from pathlib import Path
from app.services.wiki_query import retrieve_kefu_wiki
result = retrieve_kefu_wiki(
question="账号被限流怎么办",
wiki_root=Path("backend/kefu-know/wiki"),
)
print(result.keywords)
print(result.retrieval_notes) # 如 index_match / global_scan / index_llm_select
print(result.to_prompt_text()) # 供 Agent 合成答复的上下文
关键文件:backend/app/services/wiki_query.py、wiki_query_tool.py
4.6 用户消息链接增强
用户文本里若含 URL:
- 图片:视觉 OCR / RapidOCR 兜底
- 网页:抽取正文再拼入本轮上下文
关键文件:backend/app/services/user_content_enricher.py
4.7 标点切句 + 流式 TTS
LLM token 边出边进 PunctuationStreamBuffer:遇到句读或超长强制切句,再送火山 TTS v3 双向 WebSocket。这样播报可与打字机并行,缩短「听到第一句」的等待。
关键文件:text_punctuation_buffer.py、tts/volcano_ws_client.py
4.8 前端体验
- ASR 字幕实时刷新
llm_delta/greeting_delta驱动TypewriterText- 运维页对接 Admin API(摘要、审计、导出)
4.9 Mock → Live 工程化
VOICE_ROBOT_MOCK_STREAMING_ENABLED=true 时可本地跑通全链路,不调外网。联调时再填腾讯 ASR / 火山 TTS / Ark 凭证。/readyz 会在 live 缺密钥时返回 503,避免「假健康」。
五、业务逻辑亮点
5.1 为什么不用端到端语音模型
客服场景强依赖可审计的知识来源 与可打断的中间态。级联架构虽然多一跳,但:
- ASR / LLM / TTS 可独立替换与限流
- Wiki 工具调用可强制、可日志
- 打断粒度为
generation_id,不会误杀新一轮
5.2 双端点判停,但只允许服务端 commit
前端 VAD + 腾讯 ASR VAD 共同感知「说完了」,但 LLM 触发必须由服务端 commit_turn_once 完成。这是把「感知」和「决策」拆开,避免双端各跑一次推理。
5.3 意图不靠独立 NLU
没有单独意图分类器。意图由 DeepAgent + 系统提示词 + 强制 Wiki 工具完成:能答的题先检索,答不了的题也不编造事实。工具侧允许轻度补全指代以便检索,但禁止杜撰政策条款。
5.4 打断后的历史修补
取消时用「提交前历史 + HumanMessage + 已播 partial AIMessage」覆盖 checkpoint。否则用户打断后,未播完的长回答仍会留在多轮记忆里,下一轮会「答非所问」。
六、HTTP 运维面(旁路能力)
| 接口 | 说明 |
|---|---|
GET /healthz |
存活探活 |
GET /readyz |
凭证就绪检查 |
GET /metrics |
Prometheus |
GET /admin/ops/summary |
运维摘要(需 Admin Key) |
GET /admin/audit/... |
会话/轮次查询与 CSV 导出 |
主业务仍在 WebSocket;HTTP 负责健康检查与审计运营。
七、配置与运行方式(读者可复现)
环境:Python ≥ 3.11,Node ≥ 18。变量前缀多为 VOICE_ROBOT_。
bash
# 后端
cd backend
cp .env.example .env
pip install -e ".[dev]"
uvicorn app.main:app --host 127.0.0.1 --port 8000
# 前端
cd frontend
npm install
npm run dev
常见踩坑(适合博客「实践笔记」小节):
- ASR 15 秒超时 → 必须在
speech_start预建连 - 打字机一次蹦整段 → 查 LLM
streaming、stream_mode=messages与前端 Typewriter - 总是返回 mock 占位文案 → 检查 MOCK 开关与 Ark Key / 模型配置
八、总结:可复制的设计取舍
本项目把「实时语音客服」拆成四条可独立演进的能力:
- 协议层:幂等 turn + generation 级打断
- 感知层:端侧 VAD + ASR 预建连
- 认知层:DeepAgent + 强制 Wiki RAG,拒绝幻觉
- 表达层:标点切句 TTS + 前端打字机,缩短感知延迟
若只记三点:服务端统一编排、知识库强制检索、打断可恢复上下文。这三点决定了它能上线做客服,而不是 demo 聊天机器人。