给100个野人接电话:多角色AI语音系统工程实践
2026/9/7 8:30:43 网站建设 项目流程

“挑战给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/11,Linux服务器跑服务更稳定。需要安装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 requests

3.3 组件选型

  • ASR:可选本地Whisper类模型或FunASR类方案,也可以对接云端语音识别接口。
  • LLM:可选本地部署的Qwen系列或通过API调用,角色人设统一通过system prompt控制。
  • TTS:可选支持多音色的开源TTS,如CosyVoice、GPT-SoVITS等,每个角色绑定一个音色ID。
  • 通话接入:本地测试用WebRTC,浏览器页面里直接发起“来电”,避免购买语音线路。

由于不同组件的安装方式差异很大,建议先用一个简化实现跑通主流程,再替换为完整ASR和TTS。下面代码给出的就是一个可调整的最小骨架。

4. 安装部署与启动方式

4.1 最小服务骨架

创建一个项目目录,里面放app.pyagents.jsonapp.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(title="Wild Agent Phone System") AGENT_FILE = Path("agents.json") agents = {} if AGENT_FILE.exists(): agents = json.loads(AGENT_FILE.read_text(encoding="utf-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_code=404, detail="agent 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_code=404, detail="call 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_idreplymedia_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}, timeout=30) 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", json=req, timeout=30) print(resp.json())

返回后轮询/calls/{call_id}查询结果。注意轮询间隔不要小于1秒,避免给服务造成无效压力。批量任务全部跑完后,导出通话记录,统计成功率、平均耗时、每人设的首响时间。

7. 资源占用与性能观察

7.1 显存观察方法

在Linux环境使用nvidia-smi实时查看显存占用。重点观察ASR和TTS两个模型同时加载时显存是否足够。如果显存不足,最简单的方法是让ASR和TTS分时加载,或者把其中一个换成API接口。

watch -n 1 nvidia-smi

7.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电话、接游戏角色电话都只是换一套角色配置的事。

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

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

立即咨询