1. 为什么服务端要统一 Key:三类模型各接一套的痛
上一篇把「为什么给女儿自建 AI 朋友」讲完了,这一篇直接进服务端。我最早那版文字机器人能聊天,但语音链路一直没做成,卡的地方不是模型本身,而是 ASR、TTS、LLM 三类能力各自要一套鉴权、一套地址、一套超时重试。你想想,一个三岁小孩对着手机说「播放西游记」,背后要跑:语音识别把声音转文字、大模型理解并生成回复、语音合成把文字变回声音。三个环节如果分别对接三家服务,Key 管理、额度监控、报错排查就是三份工作量,任何一环挂了都要单独查。
TaoToken 在这里的价值不是「多一个模型」,而是把 ASR、TTS、LLM 收敛到同一个 API 入口和同一把 Key。服务端只需要维护一份配置骨架,三类能力用同一套鉴权、同一套重试、同一套日志。对家庭本地化场景来说,这直接决定了你半夜被孩子叫醒时,是改一个配置文件还是翻三个后台。
这篇给的是可复制的settings.json与config.toml片段,以及一次端到端语音对话验证动作。目标很明确:确认本地服务端能稳定调用三类模型,而不是停留在「能跑一次」。
2. TaoToken 前置:Key 与通道准备
在动手改配置前,先把入口理清楚。TaoToken 的 API 地址是https://taotoken.net/api,注意这个地址不带任何查询参数,直接作为 base_url 使用。控制台里创建 Key 的入口在 API Keys 页面,建议按用途分 Key:一个给服务端主链路,一个给调试脚本,方便出问题时快速定位是配置错还是额度问题。
模型对话的在线调试入口可以先用起来,确认你要调的模型名拼写正确,再写进配置文件。长期跑编码类或 Agent 类任务的话,Coding Plan 的额度模型更适合常驻服务,不会因为单次调用波动影响体验。
注意:Key 只放在服务端环境变量或本地配置文件里,不要写进客户端 App,也不要提交到公开仓库。家庭局域网场景下,客户端只连你自己的服务端,不直连模型通道。
接入文档里有各语言 SDK 的 base_url 写法,Python 侧就是 OpenAI 兼容风格,把base_url指向https://taotoken.net/api即可。下面所有配置都围绕这个入口展开。
3. 可复制配置:settings.json 与 config.toml
服务端我用 FastAPI 做统一网关,配置分两层:settings.json管服务级参数(端口、日志、Key 引用),config.toml管三类模型的调用参数。这样拆的好处是,换模型只动 toml,不动服务代码。
先看settings.json:
{ "server": { "host": "0.0.0.0", "port": 8000, "log_level": "info", "request_timeout": 60 }, "taotoken": { "base_url": "https://taotoken.net/api", "api_key_env": "TAOTOKEN_API_KEY", "max_retries": 3, "retry_backoff": 1.5 }, "audio": { "sample_rate": 16000, "channels": 1, "format": "wav" } }关键点:api_key_env指向环境变量名,而不是把 Key 明文写进来。启动前export TAOTOKEN_API_KEY=你的Key,服务读取时用os.environ取。max_retries和retry_backoff是三类能力共用的重试策略,避免每个模块各写一套。
再看config.toml,三类模型分节:
[llm] model = "qwen3.5-2b" base_url = "https://taotoken.net/api" temperature = 0.7 max_tokens = 256 num_ctx = 8192 keep_alive = "30m" think = false [asr] model = "sensevoice-small" base_url = "https://taotoken.net/api" language = "zh" sample_rate = 16000 [tts] model = "matcha-tts-zh" base_url = "https://taotoken.net/api" voice = "baker" speed = 1.0这里有个我踩过的坑:num_ctx必须三类调用保持一致。之前主对话写 8192、摘要请求写 4096,同一个模型两种窗口交替,运行时每切换一次就重载,首 token 延迟恒定在 10 秒左右。统一成 8192 后,模型常驻,延迟降到 1 秒出头。think = false也是必须的,Qwen 系列默认带思考模式,会把输出预算吃光,正式回答为空。
Python 侧读取配置的骨架:
import json, os, tomllib from openai import OpenAI with open("settings.json", "r", encoding="utf-8") as f: settings = json.load(f) with open("config.toml", "rb") as f: config = tomllib.load(f) client = OpenAI( base_url=settings["taotoken"]["base_url"], api_key=os.environ[settings["taotoken"]["api_key_env"]], timeout=settings["server"]["request_timeout"], max_retries=settings["taotoken"]["max_retries"], )这样 LLM、ASR、TTS 三个模块共用同一个client实例,鉴权和重试逻辑只维护一份。
4. 验证请求:一次端到端语音对话
配置写完不能只看不跑。验证动作分三步:先单独确认 LLM 通,再确认 ASR 通,最后串起来跑一次完整语音对话。
第一步,LLM 冒烟测试:
resp = client.chat.completions.create( model=config["llm"]["model"], messages=[ {"role": "system", "content": "你是小朋友的 AI 朋友,回答简短、口语化。"}, {"role": "user", "content": "你好呀,你叫什么名字?"}, ], temperature=config["llm"]["temperature"], max_tokens=config["llm"]["max_tokens"], extra_body={"think": False, "num_ctx": config["llm"]["num_ctx"]}, ) print(resp.choices[0].message.content)跑通会看到一句简短回复,比如「我叫小星,很高兴认识你」。如果返回空字符串,先检查think是否传了 false。
第二步,ASR 验证。准备一段 16kHz 单声道 WAV,调用识别接口:
with open("test_16k.wav", "rb") as f: audio_bytes = f.read() asr_resp = client.audio.transcriptions.create( model=config["asr"]["model"], file=("test_16k.wav", audio_bytes), language=config["asr"]["language"], ) print(asr_resp.text)这里要提醒一个真实坑:Android 录音 API 在部分机型上会静默回退到 AMR-WB 编码,文件名却还是.wav。服务端按 WAV 解析就是乱码。正确做法是按文件头魔数嗅探,#!AMR-WB开头先用 FFmpeg 转 16kHz WAV,RIFF开头才直通。
第三步,端到端串联。把 ASR 输出喂给 LLM,再把 LLM 输出喂给 TTS:
user_text = asr_resp.text llm_resp = client.chat.completions.create( model=config["llm"]["model"], messages=[ {"role": "system", "content": "你是小朋友的 AI 朋友。"}, {"role": "user", "content": user_text}, ], max_tokens=config["llm"]["max_tokens"], extra_body={"think": False, "num_ctx": config["llm"]["num_ctx"]}, ) reply_text = llm_resp.choices[0].message.content tts_resp = client.audio.speech.create( model=config["tts"]["model"], voice=config["tts"]["voice"], input=reply_text, speed=config["tts"]["speed"], ) with open("reply.wav", "wb") as f: f.write(tts_resp.content)成功结果是:reply.wav能正常播放,内容是模型对你说的话的语音回复。实测下来,从音频输入到音频输出,整条链路在本地服务端跑通,三类模型都走同一个 base_url 和同一把 Key。
5. 本篇常见错排查
报错一:401 Unauthorized。九成是环境变量没生效。检查echo $TAOTOKEN_API_KEY是否有值,以及settings.json里的api_key_env名字是否和实际环境变量名一致。别把 Key 直接写进 json,那样换环境容易漏。
报错二:模型名不存在。ASR、TTS、LLM 的模型名拼写要和控制台里一致。建议先在模型对话入口手动发一条消息,确认模型可用,再写进config.toml。
报错三:TTS 合成报错或卡住。子句里混入()~这类标点会进词表外路径。合成前做一次清洗,过滤掉无发音字符。另外切分可能产生纯标点空子句,要按「无发音字符」过滤,否则长度计算会除零崩溃。
报错四:首 token 延迟异常高。先看运行时ollama ps里 CONTEXT 是否稳定。如果主对话和摘要请求的num_ctx不一致,模型会在两种窗口间反复重载,KV Cache 每轮清空。统一窗口、显式传keep_alive、开启 Flash Attention,三刀下去延迟能从 10 秒级降到 1 秒级。
报错五:中文乱码。Windows 下子进程输出按控制台代码页编码,GBK 和 UTF-8 都可能出现。解码要做双回退,先试 UTF-8,失败再试 GBK,否则中文全乱。
提示:排障时优先看服务端日志里的请求耗时和状态码,再对照上面五类。接入相关的细节可以查接入文档,模型可用性用模型对话入口快速验证。
6. 下一步:把这条链路交给客户端
服务端这套骨架跑通后,客户端要做的就简单了:录音、推流、播放。但弱机上的录音插件、双模式交互、动态播放队列和打断机制,还有华为机型那个「麦克风被系统静音」的坑,是下一篇的内容。
如果你打算长期跑编码类或 Agent 类任务,Coding Plan 的额度模型比按次调用更适合常驻服务。先把这篇的配置骨架落地,确认三类模型都能稳定调用,再往下走客户端,会顺很多。