拓冰建站拓冰建站
首页 / 资讯中心 / 正文

用Python搭建与Ollama本地模型对话的语音助手

相信不少开发者都有过这样的体验本地跑好了 Ollama、llama.cpp 或者 vLLM模型回答质量也不错但每次对话都要在终端里敲命令、复制粘贴总觉得少了点什么。尤其是当你想把本地模型接到智能音箱、车载系统或者会议纪要工具里的时候语音交互几乎是绕不开的一环。最近在 Hacker News 上看到 Riffn 这个项目定位很有意思它想做一个“与 AI agents 和本地模型之间的即时语音链接”。简单说就是让你用自然语言开口说话本地模型能听懂、能思考、能回答并且把回答用语音反馈给你。这个思路很适合拿来拆解成一套可落地的工程方案。本文将围绕“语音 本地模型 AI agents”这条主线先讲清楚语音交互链路的核心组成再带大家用 Python 从零搭建一个可以和 Ollama 本地模型对话的语音助手。代码会完整给出关键配置逐行解释最后还会覆盖常见报错、延迟优化和工程化建议。无论你是想给本地模型加一个语音入口还是准备做一个带工具调用的语音 Agent这篇文章都能给你一个可以照着改的底子。1. Riffn 是什么给本地模型装上“语音入口”1.1 一个很常见的痛点本地模型这几年发展很快像 Qwen、Llama、DeepSeek 等开源模型在普通消费级显卡甚至纯 CPU 机器上都能跑起来。但使用体验一直有个短板交互方式太“程序员化”。大多数时候我们和本地模型的对话流程是打开终端 - 输入命令 - 等待模型启动 - 输入问题 - 等待生成 - 阅读回答打字输入对于开发者来说很自然但对于普通用户、老人、孩子或者开车、做家务、生病卧床这类不方便打字的场景就完全不适用了。Riffn 这类工具解决的正是这个“最后一公里”问题把模型的文字输入输出替换为语音输入输出。1.2 Riffn 的定位一条即时语音通道从项目命名和描述来看Riffn 核心做的是“instant voice link”也就是即时语音连接。它不一定是某个大模型也不是某个语音识别算法而更像一个连接层麦克风 - 语音识别(STT) - Agent/本地模型 - 语音合成(TTS) - 扬声器它的价值在于把上面这条链路串起来并且让开发者可以方便地接入自己的 agents 和本地模型。由于 Riffn 目前还在快速迭代阶段不同版本的接口可能有差异本文不打算逐行讲解它的源码而是把重点放在**“如何自己实现一套同样的语音链路”**上。掌握了链路原理无论你后续是直接使用 Riffn还是参考它的思路做定制开发都会轻松很多。1.3 为什么强调“本地模型”这里有一个容易被忽略的隐私优势。云端语音助手如各类智能音箱通常会把录音上传到服务器处理录音内容、对话文本都会经过第三方。而Riffn 强调接入本地模型意味着整条语音链路的数据可以完全不出本机语音识别在本地完成 模型推理在本地完成 语音合成在本地完成对于医疗、法律、企业内部数据等敏感场景这种全本地化的处理方式非常关键。它规避了“录音被上传”“对话内容被用于训练”等隐私风险这也是很多开发者愿意自建语音助手而不是直接使用云服务的原因。1.4 适用场景从实际使用角度Riffn 这类工具适合以下场景个人知识库助手本地跑一个 RAG 服务用语音提问语音获取答案。智能家居控制把本地模型接到 Home Assistant 等平台通过说话控制设备。会议纪要录下会议讨论本地转写并生成摘要。语音编程助手不方便打字时口头描述需求让 Agent 生成代码。离线环境部署内网、无外网环境下仍然能使用语音 AI 能力。明白了 Riffn 的定位接下来重点拆解它背后的核心技术链路。2. 语音链路拆解从麦克风到本地模型再回到耳朵一条完整的语音交互链路通常由四个环节组成音频采集、语音识别STT、模型推理Agent/LLM、语音合成TTS。下面逐一说明。2.1 音频采集把声音变成机器能处理的信号麦克风采集到的是模拟声音信号经过声卡采样后变成数字信号。在 Python 中我们通常使用sounddevice、pyaudio等库来完成采集。一个需要注意的概念是采样率sample rate。常见值是 16000 Hz 和 44100 Hz。语音识别引擎一般要求 16000 Hz 或 8000 Hz而语音合成对采样率的要求通常由引擎自己决定。采集端如果采样率设置不对可能出现“识别不了”或“声音变调”的问题。2.2 STTSpeech-to-Text让机器听懂人话语音识别负责把音频转成文字。方案主要分两类在线识别如 Google Speech Recognition、百度语音、讯飞。优点识别准确率高、无需管理模型。缺点需要联网音频可能被上传。离线识别如 Vosk、Whisper本地部署、FunASR。优点数据不出本机、无网络依赖。缺点需要下载模型占用磁盘和内存。Riffn 强调“本地模型”所以在 STT 环节更贴合它思路的是Vosk或Whisper 本地版。本文实战部分默认使用speech_recognition库因为它封装了多种后端切换成本低。2.3 Agent/LLM大脑在这里文字送到模型后模型负责理解意图、组织回答。这里有两个概念需要区分普通 LLM 调用用户问一句模型答一句无状态、无工具。Agent模型不仅回答问题还能决定调用哪些工具比如搜索、执行代码、查询数据库并根据工具结果继续推理。Riffn 强调支持 AI agents意味着它不满足于“纯聊天”而是要能完成多步任务。这在工程上主要体现在上下文管理和工具调用两个方面后面第 5 章会详细展开。2.4 TTSText-to-Speech让机器开口说话模型生成文字后需要 TTS 引擎把它变成语音播放出来。常用方案有pyttsx3离线、轻量、跨平台基于系统 TTS。edge-tts微软 Edge 的在线 TTS音色自然但需要联网。bark、GPT-SoVITS本地神经 TTS效果更好但资源占用大。本文实战使用pyttsx3理由是最简单、最容易跑通。生产环境可以按需替换。2.5 全链路延迟四个环节中每个环节都会引入延迟。经验参考值如下环节延迟来源大致量级音频采集 语音识别录音时长 识别计算0.5s ~ 2s取决于识别引擎模型推理本地模型计算1s ~ 10s取决于模型大小语音合成TTS 计算0.3s ~ 1s想要让“即时语音”体验更好通常要做三件事流式识别边说边出字、流式生成模型边生成边输出、流式合成文字边生成边合成语音。本文第一阶段先做“录音结束再识别、全部生成后再朗读”的非流式版本理解更直观后面再讨论优化方向。3. 环境准备与项目结构在开始写代码之前先把环境准备好。本文以 Windows 为演示环境macOS/Linux 原理相同个别命令需要微调。3.1 确认本地模型环境本文实战部分使用Ollama作为本地模型服务。请先确认已安装 Ollama并成功启动服务默认端口为11434。已经拉取至少一个模型例如ollama pull qwen2.5:7b模型名称以你本机实际拉取的为准。没有 GPU 的机器可以考虑qwen2.5:3b或llama3.2:3b。启动后可以快速验证模型是否正常ollama run qwen2.5:7b 你好请简单介绍一下你自己如果终端能正常回答说明模型服务可用。3.2 Python 环境建议使用 Python 3.10 及以上版本。先创建虚拟环境python -m venv voice-agent-envWindows 激活voice-agent-env\Scripts\activatemacOS/Linux 激活source voice-agent-env/bin/activate3.3 安装依赖创建requirements.txt内容如下sounddevice0.4.6 numpy1.26.4 SpeechRecognition3.10.1 pyttsx32.90 requests2.31.0安装pip install -r requirements.txt注意SpeechRecognition库导入时写法是import speech_recognition as sr不要写错。pyttsx3在 macOS 上可能依赖espeak如果初始化报错先安装系统级espeak。3.4 项目结构本文实战项目的目录结构如下voice-link/ ├── requirements.txt ├── stt.py ├── llm.py ├── tts.py └── main.py每个文件的职责文件职责stt.py音频采集 语音识别llm.py调用 Ollama 本地模型tts.py语音合成播放main.py主流程串联整个语音链路4. 完整实战搭建一个可与 Ollama 本地模型通话的语音助手这一章是文章的核心。我们会从零实现一个“说话 - 听懂 - 思考 - 回答 - 读出来”的闭环。4.1 语音识别模块stt.py创建一个stt.py文件负责录音和识别。# 文件路径stt.py import speech_recognition as sr def listen_once(timeout5, phrase_time_limit10): 监听一次麦克风输入并返回识别出的文字。 :param timeout: 等待说话的超时时间秒 :param phrase_time_limit: 单次说话的最大时长秒 :return: 识别文本字符串失败时返回空字符串 recognizer sr.Recognizer() with sr.Microphone() as source: print([STT] 正在调整环境噪音请保持安静...) recognizer.adjust_for_ambient_noise(source, duration0.5) print([STT] 请开始说话...) try: audio recognizer.listen( source, timeouttimeout, phrase_time_limitphrase_time_limit ) except sr.WaitTimeoutError: print([STT] 未检测到语音超时) return try: # 注意recognize_google 需要联网。 # 想完全离线可改为 recognizer.recognize_vosk(audio) text recognizer.recognize_google(audio, languagezh-CN) print(f[STT] 识别结果{text}) return text except sr.UnknownValueError: print([STT] 无法识别你说的话) return except sr.RequestError as e: print(f[STT] 识别服务请求失败{e}) return 关键点说明adjust_for_ambient_noise会在正式录音前采样环境噪音降低误识别率。timeout是等待用户开口的时间phrase_time_limit限制单次说话长度防止一直不说导致程序卡住。recognize_google使用 Google 在线识别languagezh-CN表示中文普通话。想要离线方案可以把这一行替换为recognizer.recognize_vosk(audio)但需要事先下载 Vosk 中文模型。4.2 模型调用模块llm.py创建一个llm.py文件负责与 Ollama 通信。# 文件路径llm.py import requests class LocalLLM: def __init__(self, base_urlhttp://localhost:11434, modelqwen2.5:7b): self.base_url base_url self.model model self.messages [] def chat(self, user_text): 把用户输入加入上下文并请求本地模型。 :param user_text: 用户输入的文本 :return: 模型回复文本 self.messages.append({role: user, content: user_text}) payload { model: self.model, messages: self.messages, stream: False, } resp requests.post( f{self.base_url}/api/chat, jsonpayload, timeout120 ) resp.raise_for_status() data resp.json() assistant_text data[message][content] # 保存模型回复到上下文保证多轮对话记忆 self.messages.append({role: assistant, content: assistant_text}) return assistant_text def reset(self): 清空会话上下文 self.messages []关键点说明stream: False表示等模型生成完整回答后再返回逻辑简单适合第一阶段。把历史消息保存在self.messages里后续再调用chat时模型能记住前文内容这是多轮对话的基础。timeout120是为了防止模型推理时间过长导致请求超时。模型越大、机器越慢这个值应该适当调大。4.3 语音合成模块tts.py创建tts.py文件负责把文字读出来。# 文件路径tts.py import pyttsx3 class Speaker: def __init__(self, rate180): self.engine pyttsx3.init() # 调整语速数值越大语速越快 self.engine.setProperty(rate, rate) def speak(self, text): 朗读文本并阻塞直到朗读完成 print(f[TTS] 开始朗读{text}) self.engine.say(text) self.engine.runAndWait()关键点说明pyttsx3是离线本地 TTS不需要联网。rate控制语速中文场景 150~200 之间比较自然可以根据个人偏好调整。runAndWait会阻塞代码直到朗读完毕。这样做的意图是一条完整的“问-答-读”链路结束后再进入下一轮监听。4.4 主流程main.py创建main.py把所有模块串起来。# 文件路径main.py from stt import listen_once from llm import LocalLLM from tts import Speaker def main(): model_name qwen2.5:7b llm LocalLLM(modelmodel_name) speaker Speaker(rate180) print( * 50) print(本地语音助手已启动) print(f模型{model_name}) print(说“退出”或“再见”以结束程序) print( * 50) while True: user_text listen_once() if not user_text: print([Main] 未识别到有效输入进入下一轮监听) continue # 判断退出条件 if 退出 in user_text or 再见 in user_text: speaker.speak(好的再见) break # 调用本地模型 print([LLM] 正在请求本地模型...) try: reply llm.chat(user_text) except Exception as e: print(f[LLM] 请求模型出错{e}) speaker.speak(抱歉模型调用失败了请检查本地模型服务) continue # 语音朗读回答 speaker.speak(reply) print(f[LLM] 回答{reply}) print(程序已退出) if __name__ __main__: main()4.5 运行与验证确保 Ollama 服务已启动模型已拉取然后执行python main.py预期流程如下 本地语音助手已启动 模型qwen2.5:7b 说“退出”或“再见”以结束程序 [STT] 正在调整环境噪音请保持安静... [STT] 请开始说话... [STT] 识别结果你好介绍一下你自己 [LLM] 正在请求本地模型... [TTS] 开始朗读你好我是一个运行在本地的语言模型... [LLM] 回答你好我是一个运行在本地的语言模型...结果说明当你说出问题后程序会先把语音转成文字。文字进入 Ollama 模型模型生成回答。回答通过pyttsx3播放出来。到这里一个最基础的“语音 本地模型”闭环就走通了。整个过程没有把音频和文本上传到外部服务前提是你把recognize_google换成离线识别器数据完全可控。5. 从单轮问答到 Agent 任务上下文、工具调用与流式交互第 4 章的代码能完成最基本的语音问答但它还算不上真正意义上的“AI Agent”。要让 Riffn 支持 agents需要再深入三个方向。5.1 上下文记忆让 Agent 记住前文LocalLLM类中已经通过self.messages保存了对话历史这就是最基础的上下文记忆。但要注意一个问题本地模型上下文窗口有限。假设模型上下文窗口是 8192 token如果用户一直对话历史消息会越来越长最终超出限制。工程上常见的做法是设置最大历史轮数例如只保留最近 10 轮对话。对历史内容做摘要压缩。使用向量数据库保存历史按相关性检索。一个简单的滑动窗口实现思路如下MAX_HISTORY 10 def chat(self, user_text): self.messages.append({role: user, content: user_text}) # 只保留最近 MAX_HISTORY 条消息 if len(self.messages) MAX_HISTORY * 2: self.messages self.messages[-(MAX_HISTORY * 2):] # ... 后续请求逻辑不变5.2 工具调用让 Agent 能“动手干活”真正的 Agent 不只是聊天它会根据用户需求决定调用工具。比如用户说“帮我查一下明天的天气”Agent 可能会调用一个天气查询函数用户说“帮我把这段文字翻译成英文”Agent 可能会调用翻译工具。Ollama 目前也支持部分模型的工具调用tool calling能力。整体思路是给模型声明可用工具列表。模型判断需要调用工具时返回结构化 JSON。我们在代码中执行工具。把工具结果回传给模型模型据此生成最终回答。简化示意如下tools [ { type: function, function: { name: get_current_time, description: 获取当前时间, parameters: { type: object, properties: {} } } } ] payload { model: qwen2.5:7b, messages: [{role: user, content: 现在几点了}], tools: tools, stream: False }如果模型返回了tool_calls代码就执行对应工具再把结果追加到消息列表self.messages.append( {role: tool, content: str(tool_result)} )然后再次请求模型让它基于工具结果生成自然语言回答。这个机制是语音 Agent 能真正“干活”的关键。5.3 流式交互边听边识别、边生成边朗读Riffn 强调“instant”也就是即时性。要做到即时就要尽量缩短每一段等待时间。流式识别目前绝大多数 STT 引擎都支持分片识别用户边说边出词而不是等整句话说完才开始识别。speech_recognition库默认是非流式的如果想做流式建议直接用 Vosk 的流式 API或者接入 FunASR 的流式接口。流式生成Ollama 支持stream: True可以让模型逐 token 返回生成结果。这样播报端不用等全部生成完才开始。伪代码如下resp requests.post( f{self.base_url}/api/chat, json{... , stream: True}, streamTrue, ) for line in resp.iter_lines(): if not line: continue chunk json.loads(line) token chunk.get(message, {}).get(content, ) # 把 token 送给 TTS 逐段合成播放流式合成TTS 也可以分段合成。收到模型输出的一批 token 后先合成音频片段并播放同时继续接收后面的 token。这样可以大幅降低用户感知的第一字延迟。不过流式改造会让代码复杂很多建议先跑通第 4 章的闭环再逐步升级。6. 常见问题与排查思路在实际运行过程中最容易遇到的报错和现象基本集中在麦克风权限、依赖冲突、模型调用失败这几个方面。整理成下表方便按图索骥。问题现象常见原因解决思路Microphone初始化报错没有安装 PyAudio 或系统未授权麦克风Windows 检查“隐私设置 - 麦克风”macOS 检查“系统设置 - 隐私 - 麦克风”确认安装pyaudioAdjustForAmbientNoise卡住很久麦克风采集到了持续噪音或设备静音检查麦克风是否被静音换一个安静环境如果设备列表有多个麦克风指定设备编号识别结果全是乱码或错字采样率不匹配或语言参数错误确认languagezh-CN换用更安静的录音环境考虑使用 Vosk 中文模型识别结果经常为空字符串说话音量小、离麦克风远调大麦克风音量靠近麦克风说话把phrase_time_limit调大requests.exceptions.ConnectionErrorOllama 服务未启动或端口不对运行ollama serve访问http://localhost:11434验证确认端口一致模型返回超时模型太大、机器性能不足、上下文过长换更小的模型增大timeout清空历史上下文pyttsx3初始化报错系统缺少 TTS 引擎Windows 检查语音设置Linux 安装espeaksudo apt install espeak朗读时英文、中文混杂效果差系统 TTS 音色限制换edge-tts或本地神经 TTS让模型回答尽量使用统一语言长时间运行后内存上涨历史消息无限累积实现滑动窗口定期reset()会话语音识别延迟大在线识别网络 RTT 高或离线模型解码慢本地化部署识别模型升级 CPU/GPU使用更小的识别模型通用排查顺序建议先确认麦克风在系统层面工作正常用录音机录一段。再单独测试 STT只运行stt.py不接模型。再单独测试 LLM直接用 curl 请求 Ollama确认模型输出正常。最后把 TTS 接进来全链路联调。按照这个顺序可以把问题快速隔离到单一环节。7. 工程化建议与最佳实践7.1 离线优先数据不出本机如果你的使用场景对隐私敏感建议把 STT 也切换为离线方案。Vosk 是目前比较成熟的离线识别选择中文模型体积约 1.8GB 左右。使用起来也简单import vosk import json import sounddevice as sd import numpy as np model vosk.Model(model-zh) # 模型目录 rec vosk.KaldiRecognizer(model, 16000) with sd.RawInputStream(samplerate16000, blocksize8000, dtypeint16, channels1, callbackNone): while True: data stream.read(4000) if rec.AcceptWaveform(data): j json.loads(rec.Result()) print(j.get(text, ))7.2 延迟优化要分阶段进行市面上宣传的“即时语音”大多做了很多性能工程。如果你是从零搭建建议按以下阶段优化阶段一非流式闭环。先保证链路通、逻辑正确。阶段二流式 STT 流式 LLM。把等待时间从“整句说完”缩短到“第一个字出声”。阶段三TTS 打断机制。当用户中途插话时可以停止当前朗读并开始新一轮监听。这个在 Riffn 这类工具里几乎是标配。7.3 安全边界与权限控制本地语音助手涉及两个层面需要特别注意模型安全给模型设定 system prompt明确它的行为边界。比如你是部署在用户本地的语音助手。你只能执行用户明确授权的任务 涉及删除文件、执行系统命令、修改配置等操作时必须先向用户确认。系统安全不要让 Agent 获得过高的系统权限。如果实现了工具调用所有危险操作都要二次确认并且用独立低权限账号运行服务。语音指令本身存在误识别风险不能让“删除项目目录”这种指令因为识别错误就真的执行。7.4 日志与可观测性语音链路环节多任何一个环节出问题都可能导致“用户问完了没回应”。生产环境建议记录结构化日志import logging logging.basicConfig( levellogging.INFO, format%(asctime)s [%(levelname)s] %(name)s: %(message)s )每轮对话至少记录用户语音识别文本脱敏后模型回复文本各环节耗时STT 耗时、LLM 耗时、TTS 耗时异常堆栈有了这些数据才能定量分析“即时”到底够不够快。7.5 模型选择的经验不同任务对模型的要求差别很大简单问答/闲聊qwen2.5:3b或llama3.2:3b即可响应快。复杂推理/RAG建议 7B~14B 参数并配合更好的量化格式如 Q4_K_M。工具调用选择明确支持 tool calling 的模型版本并在 system prompt 中强调“如果需要获取实时信息请调用工具”。尽量在同一个项目里把模型名做成配置项方便切换对比效果。8. 总结与学习路线本文从 Riffn 这个项目切入把“即时语音链接 AI agents 和本地模型”拆解成一条清晰的工程链路音频采集 → 语音识别 STT → Agent/LLM 推理 → 语音合成 TTS并用 Python 完整实现了一个本地语音助手。你应该已经掌握了以下核心能力理解语音交互链路的四个主要环节。能在本地启动 Ollama并通过 HTTP API 调用模型。能用speech_recognition把语音转成文字。能用pyttsx3把模型回答朗读出来。知道了从单轮问答升级到 Agent 需要处理上下文、工具调用、流式交互。下一步可以继续深入学习把recognize_google替换成 Vosk 或 Whisper实现完全离线识别。研究 Ollama 的 tools 工具调用机制给语音助手加上“查时间”“控制设备”等实际能力。引入向量数据库把本地文档变成可检索的知识库做成真正的语音版个人助理。研究流式 TTS 与打断机制把“即时感”拉到接近商用语音助手的水平。技术上没有什么神秘的地方语音助手本质上就是几条成熟技术的组合。关键是先跑通闭环再逐步替换掉效果不满意的环节。如果你准备给本地模型加一个语音入口建议直接复制第 4 章的代码跑一遍亲身体验“话音落下、模型回答在耳畔响起”的完整链路然后按自己的场景迭代优化。如果你在实际搭建中遇到报错优先回看第 6 章的排查表把问题定位到单一环节后再动手基本都能快速解决。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门