ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

给100个野人接电话:多角色AI语音系统工程实践

给100个野人接电话:多角色AI语音系统工程实践 “挑战给100个野人接电话的第一天”这个标题乍看像一个整活视频但拆开需求就很技术了要能自动接听、要能听懂对方说什么、要能用不同角色回话、还要同时管住100个“野人”的人设和音色。换句话说这是一个“多角色AI语音电话接线系统”。本文不碰真实号码资源只做本地软电话和浏览器通话测试重点演示怎么把ASR、大模型、TTS、批量任务调度串起来让100个不同人格的角色真的能“接电话”。这次我们用工程拆解的方式来做这件事。先把它当成一个语音机器人项目来看通话入口用WebRTC或SIP软电话模拟ASR负责把语音转成文字LLM根据角色人设生成回复TTS用不同音色把回复读出来。核心问题不是“接电话”这个动作而是“100个角色”的多路并发、资源隔离和角色一致性。下面会先给出核心能力速览再按环境准备、启动服务、功能验证、API批量调用的顺序走一遍最后给出常见问题排查清单。1. 核心能力速览能力项说明项目类型多角色AI语音电话接线系统本地可跑的Demo主要功能自动接听、语音识别、角色对话、多音色回复、通话记录角色数量可配置演示目标为100个“野人”角色通话入口浏览器WebRTC页面或SIP软电话不依赖真实电话号码音频处理双工或半双工音频流按通话任务调度识别方案本地ASR或云端ASR接口需按实际选型配置对话方案本地或云端LLM通过system prompt维护角色人设语音合成本地TTS模型或云端TTS接口每个角色绑定独立音色启动方式Python FastAPI启动浏览器打开通话页面接口能力提供角色注册、来电创建、绑定角色、查询记录等REST API批量任务支持任务队列可批量发起多路模拟来电显存占用取决于ASR和TTS模型规格需按本机实测适合场景语音客服Demo、虚拟角色互动、社交应用原型、测试环境验证这个方案最关键的设计是“角色配置与管线解耦”。100个野人本质上是100份JSON配置每份配置包含角色名称、人设提示词、音色ID、说话风格。通话任务进来时系统只要根据配置动态拼装system prompt并调用对应的TTS音色就能在不新增服务的情况下支撑大量角色。2. 适用场景与使用边界这类系统最有价值的场景是客服机器人、虚拟偶像互动、游戏NPC语音对话、无障碍电话助手一类的产品原型。技术上验证的是“多角色语音交互”能不能在低成本和可控资源下跑通。如果你有100个不同人设的语音角色需要统一管理这套结构也能直接复用。但必须明确几条使用边界。第一不要在未经对方知情同意的情况下发起自动外呼更不能用于骚扰电话、电话诈骗、催收、营销轰炸等场景。第二如果使用真人声音进行克隆或配音必须获得本人明确授权。第三真实运营商外呼、批量呼叫、号码资源使用涉及电信业务资质个人开发者在本地测试时不要触碰这类能力。第四通话内容如果涉及用户隐私必须有录音提示、授权声明和删除机制。本文所有演示只使用模拟呼入或开发者自拨的测试通话请把系统限制在实验环境里。3. 环境准备与前置条件3.1 硬件建议语音识别和语音合成如果是本地模型建议有一张6GB以上显存的NVIDIA显卡如果只用CPU推理对显存没有要求但并发路数会明显下降。更稳妥的做法是ASR和TTS分别做成独立服务再通过网络接口和主服务对接这样哪边资源紧张可以单独扩容。内存建议16GB以上。磁盘空间取决于模型数量一般ASR模型加TTS模型预留30GB以上比较宽裕。100个音色如果全部使用独立语音模型磁盘占用会很高更合理的方式是使用一个支持多音色的TTS模型每个角色只维护一份音色参数文件。3.2 软件依赖操作系统推荐Ubuntu 22.04或Windows 10/11Linux服务器跑服务更稳定。需要安装Python 3.10或3.11安装FFmpeg用于音频格式转换如果走WebRTC方案还要保证浏览器能访问本地麦克风。建议单独创建虚拟环境不要和系统Python混用。# 创建独立Python环境 python3.11 -m venv venv source venv/bin/activate # 升级pip并安装基础依赖版本以实际安装为准 pip install --upgrade pip pip install fastapi uvicorn pydantic pip install websockets pip install requests3.3 组件选型ASR可选本地Whisper类模型或FunASR类方案也可以对接云端语音识别接口。LLM可选本地部署的Qwen系列或通过API调用角色人设统一通过system prompt控制。TTS可选支持多音色的开源TTS如CosyVoice、GPT-SoVITS等每个角色绑定一个音色ID。通话接入本地测试用WebRTC浏览器页面里直接发起“来电”避免购买语音线路。由于不同组件的安装方式差异很大建议先用一个简化实现跑通主流程再替换为完整ASR和TTS。下面代码给出的就是一个可调整的最小骨架。4. 安装部署与启动方式4.1 最小服务骨架创建一个项目目录里面放app.py和agents.json。app.py用FastAPI实现三个核心能力查看可用角色、创建通话任务、查询通话状态。实际项目中ASR和TTS通过Python函数封装这里先用打印日志的方式模拟方便你替换成真实模型。# app.py import json import time import uuid from pathlib import Path from fastapi import FastAPI, HTTPException from pydantic import BaseModel app FastAPI(titleWild Agent Phone System) AGENT_FILE Path(agents.json) agents {} if AGENT_FILE.exists(): agents json.loads(AGENT_FILE.read_text(encodingutf-8)) class CallRequest(BaseModel): agent_id: str text: str 你好请问你是谁 class CallTask: def __init__(self, agent_id: str, message: str): self.call_id str(uuid.uuid4()) self.agent_id agent_id self.message message self.status pending self.created_at time.time() self.answer call_tasks {} def asr(message: str) - str: # 实际项目在这里调用ASR接口 return message def llm_reply(agent, user_text: str) - str: # 实际项目在这里调用LLM接口把agent[system_prompt]传给模型 return f我是{agent[name]}你说的是“{user_text}”我已经听到了。 def tts(agent: dict, reply: str) - str: # 实际项目在这里调用TTS接口返回音频URL或音频文件路径 return f/media/{agent[voice_id]}.wav app.get(/agents) def list_agents(): return {total: len(agents), agents: agents} app.post(/calls) def create_call(req: CallRequest): if req.agent_id not in agents: raise HTTPException(status_code404, detailagent not found) task CallTask(req.agent_id, req.text) agent agents[req.agent_id] user_text asr(req.text) reply llm_reply(agent, user_text) media_url tts(agent, reply) task.answer reply task.media_url media_url task.status completed call_tasks[task.call_id] task return { call_id: task.call_id, status: task.status, agent_id: req.agent_id, reply: reply, media_url: media_url, } app.get(/calls/{call_id}) def get_call(call_id: str): task call_tasks.get(call_id) if not task: raise HTTPException(status_code404, detailcall not found) return { call_id: task.call_id, agent_id: task.agent_id, status: task.status, answer: task.answer, created_at: task.created_at, }4.2 角色配置文件agents.json是核心100个野人就是100个JSON对象。每个人都需要一个稳定的voice_id这个ID对应TTS里保存的音色。人设提示词决定说话风格比如“豪爽型野人”“话少型野人”“爱讲冷笑话的野人”。{ wild_boss: { name: 野人大王, voice_id: voice_wild_boss_01, system_prompt: 你是一个住在森林里的野人大王说话豪爽喜欢用短句自称本大王。, temperature: 0.8 }, wild_doctor: { name: 野人医生, voice_id: voice_wild_doctor_01, system_prompt: 你是一个懂草药知识的野人医生说话温和总劝人注意身体。, temperature: 0.7 } }启动服务uvicorn app:app --host 127.0.0.1 --port 8000启动后打开 http://127.0.0.1:8000/docs 能看到Swagger文档/agents接口直接返回当前角色库。这里先验证的不是“通话质量”而是“角色配置是否生效、接口链路是否通”。接入真实ASR和TTS时只需要修改asr()、llm_reply()、tts()三个函数的内部实现接口结构不需要大改。5. 功能测试与效果验证5.1 先验证角色注册使用curl查看角色列表确认角色配置加载正常。curl http://127.0.0.1:8000/agents预期返回total字段和完整角色列表。如果返回404多半是启动目录不对agents.json没有被正确读取。5.2 验证单路通话链路创建一条来电任务绑定野人大王。curl -X POST http://127.0.0.1:8000/calls \ -H Content-Type: application/json \ -d {agent_id: wild_boss, text: 你好我要找森林里的老大}预期返回内容包含call_id、reply和media_url。这一步通过就说明“接听 - 识别 - 生成回复 - 返回音频”的流程能跑通。5.3 验证多角色切换再创建一条绑定野人医生的通话任务观察返回的agent_id是否正确、回复人设是否有差异。这里重点关注的是LLM是否真正读到了system_prompt。如果两个角色的回复风格完全一致说明提示词没有拼接成功需要检查LLM调用代码。5.4 验证批量模拟呼入编写一个Python脚本循环创建100条通话任务。注意这里是“模拟呼入”不是真实电话线路目的是观察系统在连续请求下是否稳定。import requests import random base_url http://127.0.0.1:8000 agent_ids [wild_boss, wild_doctor] for i in range(100): agent_id random.choice(agent_ids) text f第{i}通电话我想问一下角色状态 resp requests.post(f{base_url}/calls, json{agent_id: agent_id, text: text}, timeout30) data resp.json() if resp.status_code ! 200: print(f第{i}通失败: {data}) else: print(f第{i}通成功: {data[call_id]} - {data[agent_id]})如果100通全部返回200说明主服务接口和任务队列设计没问题。如果出现超时要看是ASR、LLM还是TTS拖慢了响应。这一步的核心观察指标是“错误率”和“响应时间是否递增”。5.5 判断成功标准一通电话是否成功可以参考几个指标角色ID是否正确、回复内容是否贴合人设、音频文件是否生成、通话记录里状态是否为completed。在此基础上再做质量判断比如“这个野人说话真的像野人”或者“音色是不是太像了”。如果回复内容很容易串角色优先检查system prompt和TTS音色绑定逻辑。6. 接口 API 与批量任务6.1 接口列表方法路径功能GET/agents获取所有角色配置POST/calls创建一条通话任务GET/calls/{call_id}查询通话结果POST/calls/batch批量创建通话任务批量接口是实际工程里必须有的能力。100个野人同时接电话任务不能全部塞在线程里而是要放进队列按顺序处理。真实部署时建议引入Celery或Redis队列轻量场景可以直接用FastAPI的BackgroundTasks。6.2 批量任务接口示例在app.py里增加批量接口每次最多接受20条批量请求避免一次打爆ASR和TTS服务。from fastapi import BackgroundTasks class BatchCallRequest(BaseModel): items: list[CallRequest] app.post(/calls/batch) def create_batch_calls(req: BatchCallRequest, background_tasks: BackgroundTasks): batch_id str(uuid.uuid4()) results [] for item in req.items[:20]: if item.agent_id not in agents: results.append({agent_id: item.agent_id, error: agent not found}) continue task CallTask(item.agent_id, item.text) call_tasks[task.call_id] task results.append({ batch_id: batch_id, call_id: task.call_id, agent_id: item.agent_id, status: task.status, }) background_tasks.add_task(run_batch, batch_id) return {batch_id: batch_id, count: len(results), results: results} def run_batch(batch_id: str): # 实际项目中从数据库或队列拉取任务逐个更新状态 time.sleep(5)批量接口有一个关键设计先快速返回call_id再异步处理业务逻辑。这样前端不会被长时间阻塞后端也能控制并发。真实系统要加失败重试比如ASR超时就单独重试一次TTS音频生成失败则重试并记录日志。6.3 Python调用示例import requests base_url http://127.0.0.1:8000 req { items: [ {agent_id: wild_boss, text: 喂喂喂有人吗}, {agent_id: wild_doctor, text: 我嗓子不舒服怎么办}, ] } resp requests.post(f{base_url}/calls/batch, jsonreq, timeout30) print(resp.json())返回后轮询/calls/{call_id}查询结果。注意轮询间隔不要小于1秒避免给服务造成无效压力。批量任务全部跑完后导出通话记录统计成功率、平均耗时、每人设的首响时间。7. 资源占用与性能观察7.1 显存观察方法在Linux环境使用nvidia-smi实时查看显存占用。重点观察ASR和TTS两个模型同时加载时显存是否足够。如果显存不足最简单的方法是让ASR和TTS分时加载或者把其中一个换成API接口。watch -n 1 nvidia-smi7.2 性能热点这类系统的性能瓶颈通常在TTS。LLM生成文本速度相对快但TTS合成一段3秒语音往往需要几百毫秒到几秒。100个野人同时在线时TTS模块会迅速成为瓶颈。推荐做法是给TTS加结果缓存相同文本不重复合成同时每个角色维护独立音色但共用同一个TTS模型实例而不是每个角色加载一个模型。7.3 降低资源占用的手段音频转写前先做静音裁剪和降噪。对话回复控制在50字以内降低TTS合成时间。批量任务限制并发数为2到4路。使用流式TTS边生成边播放减少首响等待。保证日志里记录每条任务的“开始时间”和“结束时间”方便定位慢请求。如果走WebRTC页面接听还要注意浏览器音频设备的占用情况。同一时间只能有一个页面使用麦克风否则会出现设备冲突。更好的方案是让SIP软电话或专用测试终端负责音频采集服务端只负责ASR和TTS。8. 常见问题与排查方法问题现象可能原因排查方式解决方案启动后 http://127.0.0.1:8000/docs 打不开服务没起来或端口被占用查看进程和日志检查端口lsof -i:8000换端口启动/agents 返回空列表agents.json路径不对或JSON格式错误检查工作目录和文件编码使用绝对路径加载配置文件创建通话返回404agent_id拼写错误或角色文件没更新先查 /agents 对比ID复制返回里的真实ID所有角色回复风格一样system_prompt没有传到LLM打印每次LLM请求参数修正提示词拼接逻辑TTS音频没有生成音色ID不存在或TTS服务未启动查看TTS服务日志和返回码核对voice_id单独测TTS接口批量请求大量超时并发太高下游ASR/TTS被压垮看CPU、显存、队列长度限制并发数引入消息队列浏览器拿不到麦克风未授权或页面不是HTTPS检查浏览器权限确认访问地址是localhost或HTTPS更换浏览器或使用软电话方案通话记录丢失任务处理到一半服务重启检查是否有持久化数据库使用SQLite或Redis保存任务状态音色和角色不匹配语音ID绑定错误打印每个agent的voice_id建立音色与角色映射表统一管理批量任务卡住是最常遇到的问题根因往往是ASR或TTS接口没有设置超时。给所有外部调用加上超时和重试比如超时设为15秒失败重试1次再失败就标记为failed而不是一直pending。9. 最佳实践与使用建议第一永远先跑通最小链路再加功能。先把“角色列表 - 创建通话 - 文本回复”跑通再接入ASR和TTS。不要一上来就追求100路并发那只会让你分不清是代码问题还是模型资源问题。第二把100个角色做成数据驱动。无论使用什么框架角色配置、音色ID、提示词、温度参数都放进配置文件或数据库。新增野人角色时只加数据不改业务代码。这个设计能让你在“第一天”就能轻松增加角色数量。第三通话流程要有日志和状态机。每个通话任务至少包含pending - processing - completed/failed三个状态。任务卡住时可以通过状态快速定位是ASR阶段、LLM阶段还是TTS阶段出了问题。第四涉及音频、录音、声音克隆时务必确认授权。演示环境不要使用未经授权的真人声音。如果做产品Demo在页面上写明“AI角色服务通话内容可能被记录用于调试”给用户明确知情权。第五接口服务不要裸奔。如果服务部署在服务器上至少要限制访问来源IP不要在公网开放8000端口。建议加一个简单的API Key验证所有请求都必须携带Token。10. 总结与下一步“给100个野人接电话”这个挑战最值得先试的是把两个角色、一条模拟通话流程完整跑通。等你看到野人大王真的用不同音色说出符合人设的回复再往100个角色扩展就有了底。最容易踩的坑有两个一个是角色提示词没真正传给LLM导致100个野人说一样的话另一个是TTS并发能力跟不上批量任务一上来就超时。下一步可以做几件事给系统加真实ASR和TTS组件把100个野人音色配置齐全用SIP软电话替代浏览器页面让通话入口更接近真实电话引入Redis队列管理批量任务让100路呼入有序调度最后生成一份完整的通话质量报告统计每路电话的接通率、回复耗时和角色人设一致率。这套系统跑稳之后不只可以接野人的电话接客服电话、接NPC电话、接游戏角色电话都只是换一套角色配置的事。
返回列表