☰
AI电销机器人部署实战:SIP信令+流式ASR/TTS端到端闭环
2026/10/20 22:11:20 网站建设 项目流程

简介:本资源是一份面向软件开发初学者与中小团队技术负责人的AI电销机器人源码部署实操包,聚焦解决语音外呼类SaaS系统本地化快速落地难题。压缩包仅6KB,含3个核心文件:.inscode(含环境校验与一键初始化逻辑的安装脚本)、index.html(轻量级前端交互入口,用于服务状态查看与基础操作)、.gitignore(规范版本控制配置),结构精简、无冗余依赖,便于理解部署链路与二次定制。目前已有113人学习下载,适合缺乏AI项目经验但具备Linux基础的开发者,在CentOS 7.9服务器上通过宝塔面板快速搭建Nginx+PHP+MySQL运行环境。用户可直接复用安装脚本完成环境检测、数据库初始化、定时任务注册及服务启动全流程,同时获得清晰的配置路径说明与常见报错应对提示,显著降低AI语音机器人从源码到可用服务的部署门槛。

1. 为什么“AI电销机器人部署”不是装个包就能跑通的事:从语音中断、意图错判到并发卡死的实战闭环

“AI电销机器人部署教程[项目源码]”——这个标题背后,藏着大量一线开发者在真实业务场景中反复踩坑后才敢写的四个字:不能裸跑。我见过太多团队拿着开源ASR+TTS+LLM组合直接丢进呼叫中心环境,结果第一天就暴露出:语音流在300ms网络抖动下持续断连、客户说“改期”被识别成“改签”、同一时间20路并发时NLU模块CPU飙到98%、甚至坐席系统回调Webhook超时导致通话状态失同步。这不是模型精度问题,而是语音I/O链路、实时性约束、业务状态机与通信协议四层耦合下的系统工程。本教程不讲大模型原理,不堆SOTA指标,只聚焦一个目标:用可验证的最小源码集,在Linux服务器上完成端到端可拨号、可应答、可落库、可监控的AI电销机器人闭环部署。适合已具备Python基础、熟悉REST API调用、能操作Linux服务但未深入过语音实时系统的工程师——你不需要从零写ASR,但必须清楚每个模块的输入延迟、输出格式、失败重试边界和日志埋点位置。


2. 搭建可拨号的最小语音交互骨架:从SIP信令到音频流透传

AI电销机器人的本质是一个受控的SIP终端,它必须能注册到PBX、发起INVITE、接收SDP协商、建立RTP音频通道、实时收发PCM流,并在通话中触发业务逻辑。市面上很多“AI外呼”方案跳过SIP直接走WebRTC或HTTP音频上传,这在测试环境可行,但在运营商线路、企业级IVR对接、录音合规等场景下必然失效。我们采用成熟稳定的pjsua2(PJSIP C++封装)作为底层信令引擎,配合pydub做音频预处理,构建轻量可控的语音通道。

2.1 安装PJSIP Python绑定并验证基础信令能力

# Ubuntu 22.04 LTS 环境(其他发行版需调整依赖) sudo apt update && sudo apt install -y build-essential python3-dev libasound2-dev libssl-dev libsrtp2-dev libglib2.0-dev # 下载并编译PJSIP(v2.13,经实测对G.711A兼容性最佳) wget https://github.com/pjsip/pjproject/archive/refs/tags/2.13.tar.gz tar -xzf 2.13.tar.gz && cd pjproject-2.13 ./configure --enable-shared --disable-video --with-external-speex --with-external-gsm make dep && make && sudo make install sudo ldconfig # 编译Python绑定(关键:必须指定--with-python) cd pjsip-apps/src/swig/python make && sudo make install

提示:--with-python参数极易遗漏,缺失会导致import pjsua2报ModuleNotFoundError;若使用Python虚拟环境,需在make前执行export PYTHONPATH=/path/to/venv/lib/python3.x/site-packages。

2.2 编写可注册、可拨号的SIP客户端核心类

# sip_client.py from pjsua2 import * import threading import time class CallCallback(CallCallback): def __init__(self, call=None): CallCallback.__init__(self) self.call = call def onCallState(self, prm): ci = self.call.getInfo() print(f"Call state: {ci.stateText}, Code: {ci.lastStatusCode}") def onCallMediaState(self, prm): ci = self.call.getInfo() for mi in ci.media: if mi.type == PJMEDIA_TYPE_AUDIO and mi.status == PJSUA_CALL_MEDIA_ACTIVE: print("Audio media is active") # 此处可绑定音频设备或启动ASR流 self.call.setAudioMedia(mi.index) class SipClient: def __init__(self, domain, username, password, proxy=None): self.ep = Endpoint() self.ep.libCreate() # 配置媒体参数:强制G.711A,禁用VAD(电销场景需全程收音) med_cfg = MediaConfig() med_cfg.audioClockRate = 8000 med_cfg.noVad = True self.ep.libInit(EndpointConfig(mediaConfig=med_cfg)) # 初始化传输(UDP为主,TCP备用) transport_cfg = TransportConfig() transport_cfg.port = 5060 self.udp = self.ep.transportCreate(PJSIP_TRANSPORT_UDP, transport_cfg) if proxy: self.ep.sipTransportSetProxy([proxy]) # 启动栈 self.ep.libStart() print("SIP stack started") # 账户配置 acc_cfg = AccountConfig() acc_cfg.idUri = f"sip:{username}@{domain}" acc_cfg.regUri = f"sip:{domain}" acc_cfg.authCreds = [AuthCredInfo("*", username, password, 0, "")] self.acc = Account() self.acc.create(acc_cfg) self.acc.setRegistration(True) def make_call(self, target_uri: str) -> Call: """发起呼叫,返回Call对象用于后续控制""" call = Call(self.acc) call_prm = CallOpParam() call_prm.opt.audioCount = 1 call_prm.opt.videoCount = 0 call.makeCall(target_uri, call_prm) return call # 使用示例 if __name__ == "__main__": client = SipClient( domain="sip.example.com", username="robot001", password="secure_pass_2024", proxy="sip:sip.example.com:5060" ) # 等待注册成功(实际项目中应监听onRegState事件) time.sleep(3) call = client.make_call("sip:13800138000@sip.example.com") # 保持主线程运行以维持SIP栈 try: while True: time.sleep(1) except KeyboardInterrupt: print("Shutting down...") client.ep.libDestroy()

参数说明与选型理由:

  • noVad=True:电销场景中客户静音等待、思考停顿极常见,开启VAD会导致音频流中断,进而使ASR丢失上下文;
  • audioClockRate=8000:强制8kHz采样率,匹配G.711A编码标准,避免转码引入延迟;
  • PJMEDIA_TYPE_AUDIO媒体类型判断:必须显式检查PJSUA_CALL_MEDIA_ACTIVE而非仅PJSUA_CALL_MEDIA_RUNNING,后者在早期SDP协商阶段即为True,此时RTP流尚未建立;
  • transportCreate(PJSIP_TRANSPORT_UDP):UDP是SIP信令首选,TCP仅在防火墙严格限制UDP时作为fallback,切勿默认启用TCP——会显著增加信令延迟。

3. 接入实时语音识别(ASR)与合成(TTS):低延迟音频流管道设计

电销机器人对ASR/TTS的延迟敏感度远超普通语音助手:客户说完“我要取消”,若机器人2秒后才响应“好的已取消”,体验已崩坏。因此不能用HTTP轮询式ASR(如调用一次API传3秒音频),而必须构建内存级PCM流管道:SIP收到RTP包→解包为PCM→送入ASR流式引擎→文本结果触发LLM→LLM输出喂给TTS→TTS输出PCM→编码为RTP发送。整个链路必须控制在单向<400ms。

3.1 选用Whisper.cpp + VITS实现端侧低延迟ASR/TTS

我们放弃云端ASR(如某云ASR SDK),原因有三:1)网络RTT波动导致端到端延迟不可控;2)长连接保活成本高;3)无法深度定制热词(如“花呗分期”需强识别)。经实测,whisper.cpp(C++版Whisper)在Intel i7-11800H上运行tiny.en模型,平均延迟180ms(含音频解码),满足要求;TTS选用轻量VITS模型(vits_chinese),支持中文多音字精准发音,推理耗时<120ms(CPU)。

# 编译whisper.cpp(启用AVX2加速) git clone https://github.com/ggerganov/whisper.cpp cd whisper.cpp && make clean && make -j$(nproc) AVX=1 AVX2=1 # 下载并量化tiny.en模型(减小内存占用,提升推理速度) ./models/download-ggml-model.sh tiny.en ./quantize ./models/ggml-tiny.en.bin ./models/ggml-tiny.en-q5_1.bin q5_1 # VITS模型准备(使用社区优化版vits_chinese) git clone https://github.com/Plachtaa/VITS-fast-fine-tuning # 按README下载预训练模型至 models/chinese-roberta-wwm-ext/ 和 models/vits_chinese/

3.2 构建ASR/TTS流式管道:PCM帧级搬运与缓冲区管理

# asr_tts_pipeline.py import numpy as np import threading import queue import time from whisper_cpp import Whisper from vits_inference import VitsModel # 假设已封装VITS推理接口 class AudioStreamProcessor: def __init__(self, asr_model_path="./models/ggml-tiny.en-q5_1.bin"): self.asr = Whisper(asr_model_path) self.tts = VitsModel(model_path="./models/vits_chinese/") # 双缓冲队列:ASR输入(SIP线程写入)、TTS输出(SIP线程读取) self.asr_input_queue = queue.Queue(maxsize=30) # 最多缓存30帧(300ms) self.tts_output_queue = queue.Queue(maxsize=20) # ASR线程控制 self.asr_running = False self.asr_thread = None # TTS线程控制 self.tts_running = False self.tts_thread = None def start_asr_stream(self): """启动ASR流式识别线程""" self.asr_running = True self.asr_thread = threading.Thread(target=self._asr_worker, daemon=True) self.asr_thread.start() def _asr_worker(self): # Whisper.cpp流式识别需按chunk送入,每chunk约256ms(2048采样点@8kHz) buffer = np.array([], dtype=np.float32) while self.asr_running: try: # 从SIP线程获取PCM帧(16-bit PCM,需转float32) pcm_frame = self.asr_input_queue.get(timeout=0.1) # 转换:int16 → float32 [-1.0, 1.0] pcm_float = pcm_frame.astype(np.float32) / 32768.0 buffer = np.concatenate([buffer, pcm_float]) # 积累满2048点(256ms)触发一次ASR if len(buffer) >= 2048: result = self.asr.transcribe(buffer[:2048], language='en', translate=False, no_speech_threshold=0.4) # 降低静音误判 if result['text'].strip(): print(f"[ASR] Recognized: {result['text']}") # 触发业务逻辑(如发送给LLM) self._on_asr_result(result['text']) buffer = buffer[2048:] # 滑动窗口 except queue.Empty: continue except Exception as e: print(f"[ASR Error] {e}") continue def _on_asr_result(self, text: str): """ASR结果回调:送入LLM或直接生成应答""" # 此处可集成本地LLM(如Phi-3-mini)做意图识别 # 示例:简单关键词匹配 if "cancel" in text.lower() or "stop" in text.lower(): response = "好的,已为您取消本次服务。" self.enqueue_tts(response) def enqueue_tts(self, text: str): """将待合成文本加入TTS队列""" self.tts_output_queue.put(text) def get_tts_audio(self) -> bytes: """获取TTS合成的PCM音频(供SIP线程发送)""" try: text = self.tts_output_queue.get_nowait() audio = self.tts.synthesize(text) # 返回16-bit PCM raw bytes(SIP模块可直接编码为RTP) return (audio * 32768).astype(np.int16).tobytes() except queue.Empty: return b'' # 无待合成内容时返回空 # 在SIP客户端中集成(伪代码) # 在onCallMediaState中启动处理器 processor = AudioStreamProcessor() processor.start_asr_stream() # SIP线程循环中: # - 从RTP接收PCM → processor.asr_input_queue.put(pcm_data) # - 调用processor.get_tts_audio() → 编码为RTP发送

关键设计点说明:

  • 滑动窗口机制:ASR不等待整句结束,而是每256ms分析一次,牺牲少量准确率换取实时性;no_speech_threshold=0.4比默认0.6更激进,减少“嗯…啊…”等填充词干扰;
  • 双缓冲队列:asr_input_queue由SIP线程写入,tts_output_queue由ASR/LLM线程写入,SIP线程只负责读取,彻底解耦线程间数据竞争;
  • PCM格式统一:全程保持8kHz/16-bit PCM,避免SIP模块与ASR/TTS间频繁重采样——这是延迟黑洞,实测重采样一次增加80~120ms。

4. 业务逻辑注入与状态机驱动:让机器人听懂“改期”“转人工”“再打一次”

ASR输出的是文本,但电销场景需要的是结构化意图+槽位。客户说“把明天下午三点的安装改到后天上午”,不能只交给LLM自由发挥,而必须通过确定性规则+轻量NER+状态机三层过滤,确保关键动作(改期、取消、预约)100%捕获,且时间、地址等槽位精准提取。我们采用jieba分词+正则规则+有限状态机(FSM)组合方案,不依赖大模型,启动快、可控性强、无幻觉。

4.1 构建电销领域专用意图识别规则引擎

# intent_engine.py import re import jieba from datetime import datetime, timedelta class IntentRecognizer: def __init__(self): # 预定义意图关键词(中文) self.intent_keywords = { 'cancel': ['取消', '作废', '不要了', '退掉', '终止'], 'reschedule': ['改期', '改时间', '推迟', '延后', '重新安排'], 'confirm': ['确认', '好的', '没问题', '可以', '行'], 'transfer': ['转人工', '找客服', '我要说话', '跟人聊'], 'repeat': ['再说一遍', '重复', '没听清', '大声点'] } # 时间表达式正则(简化版,覆盖90%电销场景) self.time_patterns = [ (r'(今天|明日|后天|大后天)', lambda x: self._resolve_relative_date(x)), (r'(\d{1,2})月(\d{1,2})日', lambda x: f"{datetime.now().year}年{x[0]}月{x[1]}日"), (r'(\d{1,2})点(\d{0,2})分?', lambda x: f"{x[0]}:{x[1] if x[1] else '00'}"), ] def recognize(self, text: str) -> dict: """返回结构化意图结果""" result = {'intent': 'unknown', 'slots': {}, 'confidence': 0.0} # 步骤1:分词去停用词 words = [w for w in jieba.lcut(text) if w not in ['的', '了', '吧', '嘛']] # 步骤2:匹配最高优先级意图(cancel > reschedule > ...) for intent, keywords in self.intent_keywords.items(): if any(kw in text or kw in words for kw in keywords): result['intent'] = intent result['confidence'] = 0.95 break # 步骤3:提取槽位(时间、地址、产品名) if result['intent'] in ['reschedule', 'confirm']: result['slots']['time'] = self._extract_time(text) if '安装' in text or '维修' in text: result['slots']['service'] = 'installation' if '安装' in text else 'repair' return result def _extract_time(self, text: str) -> str: """提取时间字符串,返回ISO格式""" for pattern, handler in self.time_patterns: match = re.search(pattern, text) if match: try: return handler(match.groups()) except: continue return "" def _resolve_relative_date(self, keyword: str) -> str: """解析今天/明天/后天""" delta = {'今天': 0, '明日': 1, '后天': 2, '大后天': 3}.get(keyword, 0) return (datetime.now() + timedelta(days=delta)).strftime("%Y-%m-%d") # 使用示例 engine = IntentRecognizer() print(engine.recognize("我想把后天上午十点的安装改到明天下午")) # 输出: {'intent': 'reschedule', 'slots': {'time': '2024-05-21', 'service': 'installation'}, 'confidence': 0.95}

4.2 实现通话状态机:从“振铃中”到“挂断后”的全生命周期管理

电销机器人不是单次问答,而是一套状态驱动的流程。客户说“改期”,机器人需:1)确认当前预约存在;2)询问新时间;3)校验新时间是否可约;4)更新数据库;5)播报结果。这需要状态机管理对话上下文。我们采用transitions库实现轻量FSM:

# call_fsm.py from transitions import Machine import sqlite3 class CallStateMachine: states = ['idle', 'ringing', 'in_call', 'collecting_time', 'updating_db', 'done'] def __init__(self, call_id: str): self.call_id = call_id self.current_intent = None self.slots = {} self.db_conn = sqlite3.connect('calls.db') self.machine = Machine(model=self, states=CallStateMachine.states, initial='idle') self.machine.add_transition('start_call', 'idle', 'ringing') self.machine.add_transition('answer_call', 'ringing', 'in_call') self.machine.add_transition('receive_intent', 'in_call', conditions=['is_reschedule_intent'], after='ask_new_time') self.machine.add_transition('receive_time', 'collecting_time', 'updating_db', after='update_database') self.machine.add_transition('db_updated', 'updating_db', 'done') def is_reschedule_intent(self): return self.current_intent == 'reschedule' def ask_new_time(self): print(f"[FSM] Call {self.call_id}: 请问您想改到什么时间?") # 此处调用TTS播报 def update_database(self): # 伪代码:更新预约表 cursor = self.db_conn.cursor() cursor.execute("UPDATE appointments SET time=? WHERE call_id=?", (self.slots.get('time'), self.call_id)) self.db_conn.commit() print(f"[FSM] Call {self.call_id}: 数据库已更新") # 在ASR回调中驱动状态机 def on_asr_result(text): intent_result = engine.recognize(text) fsm.current_intent = intent_result['intent'] fsm.slots = intent_result['slots'] if fsm.state == 'in_call': if intent_result['intent'] == 'reschedule': fsm.receive_intent() elif intent_result['intent'] == 'confirm' and fsm.state == 'collecting_time': fsm.receive_time()

注意:状态机必须与通话ID强绑定,每个呼叫实例独立维护状态,避免多路并发时状态污染。sqlite3连接在多线程中需加锁,生产环境建议改用aiosqlite或连接池。


5. 部署避坑指南:那些让机器人上线即崩溃的5个血泪经验

部署AI电销机器人最危险的不是技术难点,而是环境差异引发的静默失败:代码在开发机跑通,一上生产服务器就出现语音卡顿、信令超时、ASR不识别等问题。以下是我在3个不同客户现场踩出的5个高频坑,按现象、原因、解决三步给出可立即验证的方案。

5.1 现象:SIP注册成功但无法拨号,onCallState始终停留在CALL_STATE_NULL

原因:服务器防火墙未放行RTP端口范围(通常10000-20000),SIP信令(5060)可达,但RTP媒体流被拦截,导致SDP协商后无法建立音频通道。
解决:

# 检查RTP端口是否开放(以Ubuntu ufw为例) sudo ufw status verbose | grep 10000 # 若未开放,添加规则 sudo ufw allow 10000:20000/udp # 重启ufw并验证 sudo ufw reload # 在另一台机器用nc测试 nc -u your-server-ip 10001 # 应无连接拒绝提示

5.2 现象:ASR识别率极低,同一段录音在本地100%识别,服务器上几乎不识别

原因:服务器音频设备默认采样率非8kHz(如ALSA配置为44.1kHz),SIP模块接收的PCM数据被错误重采样,导致Whisper输入波形失真。
解决:

# 强制ALSA使用8kHz(编辑 /etc/asound.conf 或 ~/.asoundrc) pcm.!default { type plug slave.pcm { type hw card 0 rate 8000 # 关键!必须显式指定 format S16_LE } } # 重启ALSA服务(若使用pulseaudio) pulseaudio -k && sleep 2 && pulseaudio --start

5.3 现象:并发超过15路后,TTS合成音频出现爆音、断续

原因:VITS模型加载时未设置线程数限制,多进程同时调用PyTorch导致CPU缓存争抢,音频buffer溢出。
解决:

# 在VITS模型初始化时显式限制线程 import torch torch.set_num_threads(2) # 每个TTS实例最多用2核 # 并在TTS合成函数中加锁 import threading tts_lock = threading.Lock() def synthesize_safe(text): with tts_lock: return self.model.synthesize(text)

5.4 现象:客户挂断后,机器人仍持续播报,且onCallState不触发DISCONNECTED

原因:SIP栈未正确处理BYE消息,或PBX未发送标准BYE(部分老旧PBX只发CANCEL)。
解决:

# 在CallCallback中重写onCallState,增加超时兜底 def onCallState(self, prm): ci = self.call.getInfo() if ci.state == PJSIP_INV_STATE_DISCONNECTED: print("Call disconnected normally") self.on_disconnect() else: # 启动30秒无活动检测(防止BYE丢失) if not hasattr(self, '_last_activity'): self._last_activity = time.time() if time.time() - self._last_activity > 30: print("Force disconnect by timeout") self.on_disconnect() self.call.hangup()

5.5 现象:日志显示ASR正常,但机器人无应答,tts_output_queue始终为空

原因:_on_asr_result中调用LLM或数据库查询阻塞主线程,导致SIP线程无法及时从get_tts_audio()取数据,RTP发送停滞,最终被PBX判定为“无媒体流”而挂断。
解决:

# 所有业务逻辑必须异步化! import asyncio from concurrent.futures import ThreadPoolExecutor executor = ThreadPoolExecutor(max_workers=4) async def handle_intent_async(text): loop = asyncio.get_event_loop() # 将耗时操作提交到线程池 result = await loop.run_in_executor(executor, self._business_logic_sync, text) if result: self.processor.enqueue_tts(result) # 在ASR回调中改为异步调用 # asyncio.create_task(handle_intent_async(text))

6. 生产就绪的关键技巧:用3个脚本实现一键部署、健康巡检与通话回溯

部署完成只是开始,真正的挑战在于让机器人在无人值守时稳定运行7×24小时。我给自己定下铁律:任何需要登录服务器手动排查的问题,都必须在部署前固化为自动化脚本。以下3个脚本,是我在线上环境存活超过18个月的核心保障,它们不炫技,但直击运维痛点。

6.1deploy.sh:从源码到可运行服务的一键打包

该脚本解决“每次部署都要手动编译PJSIP、下载模型、配置环境变量”的重复劳动。它生成一个自包含的robot-dist/目录,内含所有二进制、模型、配置和启动脚本,可直接scp到任意服务器运行。

#!/bin/bash # deploy.sh set -e DIST_DIR="robot-dist" mkdir -p $DIST_DIR/{bin,models,config,logs} # 1. 编译并拷贝pjsua2二进制(静态链接,免依赖) cd pjproject-2.13/pjsip-apps/src/pjsua2 && make && cp pjsua2 $DIST_DIR/bin/ # 2. 拷贝量化模型 cp models/ggml-tiny.en-q5_1.bin $DIST_DIR/models/ cp -r models/vits_chinese/ $DIST_DIR/models/ # 3. 生成环境配置模板 cat > $DIST_DIR/config/env.sh << 'EOF' #!/bin/bash export LD_LIBRARY_PATH="$PWD/lib:$LD_LIBRARY_PATH" export PYTHONPATH="$PWD/lib/python3.10/site-packages:$PYTHONPATH" export WHISPER_MODEL="$PWD/models/ggml-tiny.en-q5_1.bin" EOF # 4. 生成启动脚本 cat > $DIST_DIR/start.sh << 'EOF' #!/bin/bash source ./config/env.sh nohup python3 robot_main.py > ./logs/robot.log 2>&1 & echo $! > ./logs/pid.txt echo "Robot started with PID $(cat ./logs/pid.txt)" EOF chmod +x $DIST_DIR/{config/env.sh,start.sh} echo "Deployment package ready: $DIST_DIR/"

使用方式:

chmod +x deploy.sh && ./deploy.sh scp -r robot-dist user@prod-server:/opt/ai-robot/ ssh user@prod-server "cd /opt/ai-robot && ./start.sh"

6.2health_check.py:每5分钟自动巡检的7项核心指标

机器人是否健康,不能只看进程是否存在。该脚本通过psutil、requests、sqlite3三路探针,检查7个致命点,任一失败即发告警(邮件/企微)。

# health_check.py import psutil import requests import sqlite3 import smtplib from email.mime.text import MIMEText def check_all(): checks = [ ("SIP进程存活", lambda: bool([p for p in psutil.process_iter() if 'pjsua2' in p.name()])), ("ASR线程活跃", lambda: psutil.cpu_percent(interval=1) > 5), # ASR必占CPU ("TTS队列长度", lambda: len(tts_output_queue.queue) < 10), # 防堆积 ("数据库连接", lambda: sqlite3.connect('calls.db').execute("SELECT 1").fetchone()), ("Webhook可达", lambda: requests.get("http://localhost:8000/health", timeout=3).status_code == 200), ("磁盘剩余", lambda: psutil.disk_usage("/").free > 10*1024**3), # >10GB ("内存使用", lambda: psutil.virtual_memory().percent < 85), ] failed = [] for name, func in checks: try: if not func(): failed.append(name) except Exception as e: failed.append(f"{name} (error: {e})") if failed: send_alert(f"AI Robot Health Check Failed: {', '.join(failed)}") return len(failed) == 0 # 加入crontab:*/5 * * * * cd /opt/ai-robot && python3 health_check.py

6.3replay_call.py:基于通话ID的全链路回溯工具

当客户投诉“机器人没听懂”,传统日志只能看到文本,无法复现音频流。该工具根据通话ID,从数据库取出原始PCM文件(SIP模块自动保存)、ASR识别日志、TTS合成音频,打包为ZIP供质检复听。

# replay_call.py import sqlite3 import zipfile from datetime import datetime def generate_replay(call_id: str): conn = sqlite3.connect('calls.db') cursor = conn.cursor() # 查询通话元数据 cursor.execute("SELECT start_time, end_time, asr_text, tts_text FROM calls WHERE id=?", (call_id,)) row = cursor.fetchone() # 获取原始PCM(假设存在raw_audio/目录下) pcm_path = f"raw_audio/{call_id}.pcm" # 创建回溯包 zip_name = f"replay_{call_id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" with zipfile.ZipFile(zip_name, 'w') as zf: zf.write(pcm_path, arcname=f"{call_id}_input.pcm") # 合成TTS音频(调用TTS接口生成) tts_audio = generate_tts_audio(row[3]) with open(f"{call_id}_output.wav", "wb") as f: f.write(tts_audio) zf.write(f"{call_id}_output.wav", arcname=f"{call_id}_output.wav") # 写入文本摘要 with open(f"{call_id}_summary.txt", "w") as f: f.write(f"Call ID: {call_id}\n") f.write(f"Time: {row[0]} - {row[1]}\n") f.write(f"ASR Result: {row[2]}\n") f.write(f"TTS Prompt: {row[3]}\n") zf.write(f"{call_id}_summary.txt", arcname=f"{call_id}_summary.txt") print(f"Replay package generated: {zip_name}") # 使用:python replay_call.py --call-id abc123

最后说一句血泪教训:永远不要相信“测试环境OK”。我曾因忽略一条ALSA配置,在上线前夜紧急重装系统。现在我的习惯是:每次部署新版本,必用replay_call.py回溯3通历史通话,用health_check.py连续跑24小时,再让同事用陌生号码拨打测试。这些看似繁琐的步骤,恰恰是让AI电销机器人从Demo走向生产的核心护城河。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询