ARTICLE DETAIL

资讯详情

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

AI语音陪伴应用开发:从录音到语音回复的完整闭环实现

AI语音陪伴应用开发:从录音到语音回复的完整闭环实现 之前在手机上做一个“能语音聊天、还能记住用户”的 AI 陪伴应用时踩了不少坑。网上能找到的多是单点教程有的只讲 ASR有的只讲大模型 Prompt很难找到一条从“麦克风录音”到“语音回复播放”并带上记忆能力的完整闭环。本文整理了一套可运行的最小实现前后端代码都能直接复用包含语音识别、大模型对话、语音合成和记忆功能四部分适合想动手做 AI 语音陪伴应用或聊天软件的开发者参考。1. 为什么要做“AI 语音陪伴 记忆功能”1.1 AI 陪伴应用到底在做什么AI 陪伴类应用的核心并不神秘用户说话AI 听懂AI 回复用户听完然后对话继续。它本质上是一个“语音交互 对话生成”的循环和常见的智能音箱、语音助手架构非常接近。差异点在于“陪伴感”。工具类语音助手追求的是准确、高效比如“帮我定个闹钟”陪伴类应用更强调人设、情绪和连续性。用户希望 AI 记得自己昨天说过什么知道自己的喜好最好还能用自己喜欢的人设语气说话。这也是“AI 伊蕾娜”这类角色扮演型陪伴应用能吸引人的原因——用户喜欢的不是一段单次回复而是“这个角色一直在陪着我”的感觉。1.2 为什么记忆功能是真正的难点把语音识别和大模型接口接好只是一个 Demo。一旦进入真实使用你会发现记忆功能才是最麻烦的部分短期记忆AI 要能记住当前这轮对话前面说了什么不能上一句和下一句毫无关联。长期记忆AI 要能跨天记住用户的名字、喜欢的音乐、最近在忙什么事这样才像“认识你”的朋友。记忆更新用户说“我不喜欢喝咖啡了”AI 要能更新旧信息不能让过时记忆一直干扰后续对话。如果只把每次对话独立发给大模型AI 永远是“第一次见面”的状态陪伴感会大打折扣。1.3 本文适合谁学完后能掌握什么适合有一定 Python 基础或者对 FastAPI、语音识别、大模型接口不陌生但没完整做过项目的开发者。本文不会只讲概念会带你实现一个可以放在手机浏览器里访问的语音聊天页面并用后端串联整条链路。学完后你可以掌握如何用 Vosk 做离线语音识别。如何设计短期记忆与长期记忆的存储结构。如何组装 Prompt让大模型扮演某个角色。如何让后端返回可播放的语音回复。如何用手机浏览器完成录音、上传、播放。2. 整体架构一次语音对话的完整旅程2.1 一条语音消息的处理流程先用一张图理解整条链路手机麦克风录音 - 上传音频文件 - 后端语音识别(ASR) 把语音转成文字 - 后端读取记忆并调用大模型生成回复文本 - 后端把回复文本合成语音(TTS) - 手机播放语音回复这里的每一步都可以替换成不同实现但整体框架基本固定。2.2 四个核心模块的职责模块英文缩写作用常用方案语音识别ASR把音频转成文字Vosk、Whisper、服务商语音识别 API对话引擎LLM理解上下文并生成回复OpenAI 兼容接口、国产大模型、本地模型语音合成TTS把回复文本转成音频edge-tts、Pyttsx3、服务商 TTS API记忆模块Memory保存和检索对话上下文SQLite、Redis、向量数据库2.3 记忆模块在架构中的位置记忆模块不是独立运行的服务而是贯穿在对话引擎之前和之后。用户每次说完话后端先保存这句话再从数据库里取出最近的历史记录和用户画像一起拼进 Prompt最后把大模型的回复也保存下来。这样一个循环结束后数据库里的对话记录就多了一轮AI 下一轮就能“想起来”刚才发生了什么。3. 技术选型与开发环境准备3.1 客户端与服务端怎么分工我这里采用“轻客户端 重后端”的方式手机浏览器只负责录音、上传、播放。所有识别、对话、记忆、语音合成逻辑都放在后端。这样好处很明显换设备不用重写业务逻辑而且后续可以把网页包成 Android WebView 或 App也能继续复用同一套后端。3.2 语音识别为什么推荐 Vosk 离线方案Vosk 是一个开源语音识别工具支持中文模型体积相对可控并且可以离线运行。对陪伴聊天场景来说离线识别有两个好处不需要把音频送到第三方识别服务隐私压力更小。没有额外网络延迟识别速度更快。它的使用方式也不复杂后端安装vosk库加载一个中文模型把音频喂给识别器就能拿到文字结果。需要特别注意的是Vosk 对音频格式有要求一般要转成 16kHz 采样率、单声道的 WAV 格式。3.3 对话引擎大模型接口怎么选对话引擎是整个应用“有没有灵魂”的关键。不同团队可用的模型服务不同所以本文不绑定某个具体服务商而是以 OpenAI 兼容接口为例。你只需要配置LLM_BASE_URL模型服务的接口地址。LLM_API_KEY访问密钥。LLM_MODEL模型名称。如果你本地有可用的模型服务只要它兼容这套接口格式配置好环境变量就能跑通。3.4 语音合成TTS 方案对比语音合成方案比较多edge-tts效果自然、接入简单但依赖网络。系统自带 TTS手机端可以直接调用系统语音合成但音色不可控。云端 TTS API质量高、可选音色多但需要额外对接和费用。本文先使用 edge-tts 作为示例并在代码里做好降级处理如果 TTS 失败就只返回文本手机端仍然能展示 AI 的回复。3.5 开发环境清单由于每个人的环境不一样我不写死某个具体版本下面给出相对稳定的组合后端语言Python 3.10 或更高版本。Web 框架FastAPI Uvicorn。语音识别Vosk。音频处理FFmpeg用于把手机上传的 webm、m4a 等格式转成 WAV。数据库SQLite不需要额外安装服务。前端HTML JavaScript利用浏览器 MediaRecorder API。4. 动手实现搭建带记忆的语音对话后端4.1 初始化项目结构建议新建如下目录结构ai-companion/ ├── backend/ │ ├── main.py │ ├── speech.py │ ├── memory.py │ ├── chat.py │ ├── tts.py │ ├── requirements.txt │ ├── models/ # 放 Vosk 模型 │ └── audio_cache/ # 临时音频文件 └── mobile/ └── index.html后端依赖写入requirements.txtfastapi uvicorn python-multipart vosk openai edge-tts安装命令pip install -r requirements.txt还需要确认本机已经安装 FFmpeg可以用下面命令验证ffmpeg -version如果没有安装需要先安装 FFmpeg否则手机上传的非 WAV 音频会无法转码。4.2 角色人设让 AI 变成“伊蕾娜”陪伴类应用的核心是角色感。“伊蕾娜”这个名字来源于动漫作品你可以在自己的应用里做一个类似设定的角色喜欢阅读和旅行、性格温和但偶尔会吐槽的少女魔女。角色人设本质是一段 Prompt需要在每次调用大模型时放在最前面。下面这段设计比较克制适合做基础模板SYSTEM_PROMPT 你正在扮演一位名叫伊蕾娜的少女魔女。你性格温和、沉稳喜欢旅行和阅读偶尔会有一点小吐槽。 你要像熟悉的朋友一样和用户聊天回复不要太长不要总讲大道理。 如果用户提到了个人信息你可以在后续对话中自然地提起让对方感觉到你记住了他。 实际项目中你可以在这段 Prompt 里继续补充语气、口头禅、喜好等细节。角色设定越具体AI 的回复风格越稳定。4.3 语音识别模块Vosk 离线转写新建speech.py核心代码如下import json import os import wave from vosk import Model, KaldiRecognizer MODEL_PATH os.getenv(VOSK_MODEL_PATH, models/vosk-model-small-cn) _model None def _get_model(): global _model if _model is None: _model Model(MODEL_PATH) return _model def transcribe(audio_path: str) - str: 把 16kHz 单声道 WAV 文件转成文字 model _get_model() rec KaldiRecognizer(model, 16000) rec.SetWords(True) with wave.open(audio_path, rb) as wf: if wf.getframerate() ! 16000: raise ValueError(音频采样率不是 16000请先转码成 16kHz WAV) while True: data wf.readframes(4000) if len(data) 0: break rec.AcceptWaveform(data) result json.loads(rec.FinalResult()) return result.get(text, )这段代码的关键点Model(MODEL_PATH)会加载模型目录。KaldiRecognizer(model, 16000)告诉识别器音频采样率是 16kHz。SetWords(True)让结果包含词级信息方便后续做日志分析。Vosk 模型需要单独下载然后解压到backend/models/目录下并把VOSK_MODEL_PATH指向模型文件夹。不同环境下载方式可能不同请参考 Vosk 官方说明。4.4 记忆模块SQLite 存储短期与长期记忆新建memory.py用 SQLite 保存两张表conversations保存每一轮的对话内容。user_profiles保存长期用户画像。import sqlite3 class MemoryManager: def __init__(self, db_path: str): self.db_path db_path self._init_tables() def _get_conn(self): conn sqlite3.connect(self.db_path) conn.row_factory sqlite3.Row return conn def _init_tables(self): with self._get_conn() as conn: conn.execute( CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT, role TEXT, content TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.execute( CREATE TABLE IF NOT EXISTS user_profiles ( user_id TEXT PRIMARY KEY, profile TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) def add_message(self, session_id: str, role: str, content: str): with self._get_conn() as conn: conn.execute( INSERT INTO conversations (session_id, role, content) VALUES (?, ?, ?), (session_id, role, content) ) def get_recent_messages(self, session_id: str, limit: int 10): with self._get_conn() as conn: rows conn.execute( SELECT role, content FROM conversations WHERE session_id ? ORDER BY id DESC LIMIT ? , (session_id, limit) ).fetchall() return [{role: r[role], content: r[content]} for r in reversed(rows)] def get_profile(self, user_id: str): with self._get_conn() as conn: row conn.execute( SELECT profile FROM user_profiles WHERE user_id ?, (user_id,) ).fetchone() return row[profile] if row else def update_profile(self, user_id: str, profile: str): with self._get_conn() as conn: conn.execute( INSERT INTO user_profiles (user_id, profile, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP) ON CONFLICT(user_id) DO UPDATE SET profile excluded.profile, updated_at CURRENT_TIMESTAMP , (user_id, profile) )短期记忆的处理方式比较直接每次对话前取出最近 10 条记录按时间顺序返回拼进大模型的 messages。这样 AI 能记住当前 session 内聊了些什么。长期记忆则用user_profiles表保存。示例里用简单规则从用户发言中提取信息比如匹配“我叫小明”“我是程序员”等句式。生产环境可以改成调用大模型做信息抽取效果更稳定。4.5 对话模块组装上下文并调用大模型新建chat.py负责组装 Prompt、调用大模型、更新记忆。import os import re from openai import OpenAI SYSTEM_PROMPT 你正在扮演一位名叫伊蕾娜的少女魔女。你性格温和、沉稳喜欢旅行和阅读偶尔会有一点小吐槽。 你要像熟悉的朋友一样和用户聊天回复不要太长不要总讲大道理。 如果用户提到了个人信息你可以在后续对话中自然地提起让对方感觉到你记住了他。 client OpenAI( api_keyos.getenv(LLM_API_KEY, your-api-key), base_urlos.getenv(LLM_BASE_URL, https://api.openai.com/v1) ) def build_messages(system_prompt, profile, recent_messages, user_textNone): messages [{role: system, content: system_prompt}] if profile: messages.append({role: system, content: f你了解到的用户信息{profile}}) messages.extend(recent_messages) if user_text: messages.append({role: user, content: user_text}) return messages def extract_profile(user_id, memory_manager, user_text): 简单规则提取长期记忆生产环境可换成 LLM 抽取 profile memory_manager.get_profile(user_id) changed False patterns [ (r我叫(.{1,6}), 用户名字), (r我是(.{1,6}), 用户身份), ] for pattern, label in patterns: match re.search(pattern, user_text) if match: value match.group(1).strip() part f{label}: {value} if part not in profile: profile f{profile}; {part}.strip(; ) changed True if changed: memory_manager.update_profile(user_id, profile) return profile async def chat_with_memory(memory_manager, user_id, session_id, user_text): profile extract_profile(user_id, memory_manager, user_text) memory_manager.add_message(session_id, user, user_text) recent_messages memory_manager.get_recent_messages(session_id, limit10) messages build_messages(SYSTEM_PROMPT, profile, recent_messages) resp client.chat.completions.create( modelos.getenv(LLM_MODEL, gpt-3.5-turbo), messagesmessages, temperature0.8 ) reply resp.choices[0].message.content memory_manager.add_message(session_id, assistant, reply) return reply, profile需要说明的是不同大模型服务的接口细节可能不同。上面的OpenAI客户端只是一个兼容层如果你的服务商接口格式一致直接替换LLM_BASE_URL和LLM_API_KEY即可如果接口格式不同需要按服务商文档调整。4.6 语音合成模块把回复转成音频新建tts.py使用 edge-tts 生成语音文件import os import edge_tts VOICE os.getenv(TTS_VOICE, zh-CN-XiaoyiNeural) async def synthesize(text: str, output_path: str) - bool: try: communicate edge_tts.Communicate(text, VOICE) await communicate.save(output_path) return True except Exception as e: print(f[TTS] 合成失败: {e}) return Falsezh-CN-XiaoyiNeural是一个常见的中文女声音色你可以根据 edge-tts 支持的音色列表调整。如果 TTS 生成失败后端会返回audio_url: null前端可以降级成纯文字聊天不影响整体体验。4.7 组合成完整的 FastAPI 服务最后把模块串起来。新建main.pyimport os import subprocess import uuid from pathlib import Path from fastapi import FastAPI, File, Form, UploadFile from fastapi.responses import FileResponse, JSONResponse from chat import chat_with_memory from memory import MemoryManager from speech import transcribe from tts import synthesize app FastAPI() AUDIO_CACHE Path(audio_cache) AUDIO_CACHE.mkdir(exist_okTrue) memory_manager MemoryManager(companion.db) def convert_to_wav(input_path: str, output_path: str): 把任意格式音频转成 16kHz 单声道 WAV subprocess.run( [ ffmpeg, -y, -i, input_path, -ar, 16000, -ac, 1, output_path ], checkTrue ) app.post(/chat) async def chat( session_id: str Form(...), audio: UploadFile File(...), user_id: str Form(default_user), ): raw_ext Path(audio.filename or voice.webm).suffix or .webm raw_path AUDIO_CACHE / f{uuid.uuid4()}{raw_ext} wav_path AUDIO_CACHE / f{uuid.uuid4()}.wav content await audio.read() raw_path.write_bytes(content) try: # 转码 convert_to_wav(str(raw_path), str(wav_path)) # 语音识别 text transcribe(str(wav_path)) if not text or not text.strip(): return JSONResponse({error: 没有识别到有效语音}, status_code400) # 对话 记忆 reply, _ await chat_with_memory( memory_managermemory_manager, user_iduser_id, session_idsession_id, user_texttext ) # 语音合成 audio_filename f{uuid.uuid4()}.mp3 audio_path AUDIO_CACHE / audio_filename ok await synthesize(reply, str(audio_path)) return JSONResponse( { transcript: text, reply: reply, audio_url: f/audio/{audio_filename} if ok else None, } ) finally: # 清理原始文件和临时 wav生产环境建议保留更完整日志 raw_path.unlink(missing_okTrue) wav_path.unlink(missing_okTrue) app.get(/audio/{filename}) async def get_audio(filename: str): audio_path AUDIO_CACHE / filename if not audio_path.exists(): return JSONResponse({error: 音频不存在}, status_code404) return FileResponse(audio_path, media_typeaudio/mpeg)启动服务uvicorn main:app --host 0.0.0.0 --port 8000启动后可以先在电脑上用接口测试工具调/chat接口确认语音识别、对话、记忆、TTS 全链路正常。5. 手机端接入录音、上传、播放5.1 用手机浏览器快速验证没有 Android 或 iOS 原生开发经验也没关系手机浏览器已经提供了录音能力。新建mobile/index.html!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleAI 语音陪伴/title /head body h2和 AI 伊蕾娜语音聊天/h2 p idstatus点击开始说话/p button idstartBtn开始录音/button button idstopBtn disabled结束录音/button p idtranscript/p p idreply/p audio idplayer controls styledisplay:none/audio script const server http://你的电脑IP:8000; const sessionId session- Date.now(); let mediaRecorder; let chunks []; document.getElementById(startBtn).onclick async () { const stream await navigator.mediaDevices.getUserMedia({ audio: true }); mediaRecorder new MediaRecorder(stream); chunks []; mediaRecorder.ondataavailable e chunks.push(e.data); mediaRecorder.onstop sendAudio; mediaRecorder.start(); document.getElementById(startBtn).disabled true; document.getElementById(stopBtn).disabled false; document.getElementById(status).textContent 录音中...; }; document.getElementById(stopBtn).onclick () { mediaRecorder.stop(); document.getElementById(stopBtn).disabled true; document.getElementById(status).textContent 正在上传识别...; }; async function sendAudio() { const blob new Blob(chunks, { type: audio/webm }); const form new FormData(); form.append(session_id, sessionId); form.append(audio, blob, voice.webm); const resp await fetch(server /chat, { method: POST, body: form }); const data await resp.json(); document.getElementById(transcript).textContent 你说 data.transcript; document.getElementById(reply).textContent 回复 data.reply; if (data.audio_url) { const player document.getElementById(player); player.src server data.audio_url; player.style.display block; player.play(); } document.getElementById(startBtn).disabled false; document.getElementById(status).textContent 可以继续说话; } /script /body /html这里有几个需要注意的地方手机浏览器录音必须在 HTTPS 安全上下文下运行。如果只是本地开发可以用http://localhost访问如果要用手机访问电脑服务建议通过局域网地址访问并注意部分浏览器对 HTTP 录音有限制。server地址要改成你电脑在局域网中的 IP比如http://192.168.1.100:8000。后端已经通过 FFmpeg 把 webm 转成了 WAV所以这里不需要关心浏览器录音编码格式。5.2 把网页封装成 App如果后续想发布成 App最简单的方案是用 Android WebView 或类似框架把index.html包起来。这样录音、上传、播放都属于 Web 侧已经验证过的逻辑原生壳只负责加载页面和权限申请。当然原生开发也能做大致逻辑是申请RECORD_AUDIO和INTERNET权限。使用MediaRecorder录音到本地文件。用 OkHttp 以multipart/form-data上传文件。用MediaPlayer播放返回的音频。但第一版用 Web 做原型验证会更高效建议先把产品体验跑通再考虑原生化。6. 常见问题与排查思路问题现象常见原因解决思路启动报错找不到模型Vosk 模型没有下载或路径错误检查VOSK_MODEL_PATH是否指向模型目录手机录音上传后识别为空音频格式不对后端没有成功转码确认服务器安装了 FFmpeg检查日志中转码是否报错识别结果乱码音频采样率不是 16k查看转码命令里的-ar 16000是否生效大模型接口一直超时LLM_BASE_URL配置错误或网络不通先用 curl 测试接口连通性再检查 API KeyAI 回复和记忆对不上短期记忆条数太多或太少调整get_recent_messages的 limit 参数TTS 生成失败edge-tts 网络不通或音色名错误先单独测试 TTS 函数检查音色名称是否支持手机无法录音页面不是 HTTPS 或未授权麦克风使用 HTTPS 访问检查浏览器权限设置如果接口返回 400优先去服务端日志看具体异常。语音链路涉及音频格式、网络、模型服务多个环节建议按“音频是否到达后端 - 是否识别成功 - 是否调用大模型 - 是否合成音频”的顺序排查。7. 工程化建议与上线注意事项7.1 延迟优化当前示例是串行处理每一步都会增加延迟。实际项目中可以这样优化语音识别和 TTS 放到线程池执行避免阻塞 FastAPI 事件循环。使用流式识别在用户说话的同时就开始转写而不是等录音结束。大模型回复过程中使用流式输出先把第一句话发回来提升“开口速度”。音频文件做好缓存相同回复不要反复合成。7.2 记忆管理策略记忆不是越多越好上下文太长会占用大模型输入窗口也会让响应变慢。建议短期记忆限制最近 10 到 20 轮太早的内容可以淘汰。长期记忆定期用大模型把对话历史压缩成用户画像而不是把所有历史都塞进 Prompt。敏感信息处理不要在数据库里明文保存身份证、地址等敏感信息即使测试阶段也要养成脱敏习惯。7.3 内容安全与合规做 AI 陪伴应用内容审核是不可跳过的一环。不要做“无限制”“无审核”的 AI 聊天应用这类应用既违反平台规范也有严重法律风险。上线前至少要完成输入侧过滤违法、暴力、涉政等敏感词。输出侧对大模型回复做二次审核避免生成违规内容。用户举报提供举报入口方便及时处置不良内容。大模型本身也有使用条款限制接入时务必阅读并遵守服务商规范。7.4 数据存储与隐私陪伴类应用会收集大量用户语音和对话历史隐私设计非常重要。建议明确告知用户录音会被上传和处理。提供导出和删除对话记录的入口。数据库加密存储传输走 HTTPS。不要长期保留原始音频识别成文字后及时删除音频文件。8. 总结做一个带记忆的 AI 语音陪伴应用核心不是某个炫酷模型而是把录音、识别、对话、记忆、语音播放这条链路完整跑通。本文从整体架构讲起带你在后台上实现了 Vosk 离线语音识别、SQLite 短期与长期记忆、大模型角色人设对话、edge-tts 语音合成并提供了一个手机浏览器可以直接用的录音页面。建议你把代码 clone 或手动敲一遍先在本机用电脑浏览器验证再通过局域网让手机访问。跑通最小闭环后可以继续优化角色 Prompt、增加更多记忆提取规则、接入流式语音体验或者把页面封装成 App。改角色人设只是换一段 Prompt 的事情你可以把“少女魔女”改成任何你喜欢的设定欢迎在评论区聊聊你的角色配置和踩坑经历。
返回列表