“挑战给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 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(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_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}, 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-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格式错误 | 检查工作目录和文件编码 | 使用绝对路径加载配置文件 |
| 创建通话返回404 | agent_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电话、接游戏角色电话都只是换一套角色配置的事。