news 2026/9/7 5:36:13

Java技术栈实现陪伴型AI语音对话:从ASR到记忆分层

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java技术栈实现陪伴型AI语音对话:从ASR到记忆分层

想把一个带角色人设的 AI 语音聊天软件从想法变成能装进手机的应用,第一关不是模型能力,而是链路管理。用户对着手机说一句话,要经过语音识别转成文字、大模型按角色人设生成回复、语音合成播报出来,同时还要有一层记忆功能,让应用在下一轮对话里记住用户是谁、聊过什么。这样拆开看,开发一个陪伴型聊天软件难的不是某一个环节,而是把语音、模型、记忆几条线串起来后还能保持稳定。下面以“和魔法使伊蕾娜语音对话”这类陪伴型应用为例,拆解链路设计、Java 技术栈落地、人设控制方法和记忆功能的分层实现。内容组织上,先讲链路和选型,再给环境与代码,然后落到验证和排错,最后补生产建议,适合刚接触大模型应用开发、想了解 AI 语音对话项目如何搭起来的开发者阅读。

1. 陪伴型 AI 语音聊天应用的链路:不是“接个大模型”那么简单

1.1 从语音输入到语音输出,要过四段处理

很多人第一次做这类应用时,第一反应是“调用一个大模型 API,让它聊天不就行了”。但实际产品里,用户输入的是语音,不是文字;AI 返回的也是语音,不是屏幕上的文本。于是中间至少多了语音识别和语音合成两段。再加上“陪伴型”这个定位,用户会期待 AI 记得自己刚才说过什么、昨天聊过什么,因此还需要一段完整的记忆读写逻辑。

完整的处理链路可以分成四段:

  1. ASR(Automatic Speech Recognition,自动语音识别):把手机采集到的语音转为文本。这一段做不好,后面的模型再强也白搭,因为输入已经是错的。
  2. 对话生成:把识别出的文本、角色人设、记忆上下文一起交给大模型,让它生成符合角色性格的回复文本。
  3. TTS(Text To Speech,语音合成):把模型生成的文本转为语音,返回给手机播放。
  4. 记忆读写:在对话前后记录关键信息、更新用户画像、加载相关历史,让对话在多次会话之间保持连续。

四段之间的依赖关系很直接:ASR 的输出是对话生成的输入,对话生成的输出是 TTS 的输入,而记忆模块同时影响对话生成的输入和后续存储。任何一段延迟都会直接叠加到用户感受到的“响应速度”上。通常来说,一段语音对话的完整往返预算是 2 到 4 秒:ASR 占用几百毫秒,大模型生成占用 1 到 3 秒,TTS 占用几百毫秒到 1 秒。超出这个范围,用户会明显觉得“卡”。

1.2 记忆不是全文存档,而是三种层次配合

“记忆功能”这个词很容易被理解成“把聊天记录全部保存下来”。实际上,只保存聊天记录是不够的,因为大模型的上下文窗口有长度限制,把几天甚至几个月的记录全部塞进去,既浪费 token,也会让模型抓不住重点。更合理的做法是把记忆分成三层。

第一层是短期记忆,指当前会话内最近若干轮对话。它决定 AI 能不能接住“我刚才不是说过了吗”这种话。实现上是一个滑动窗口,只保留最近 N 轮。

第二层是长期记忆,指跨越多次会话仍然需要保留的事实性信息,比如用户养了一只猫、喜欢喝美式咖啡、上次聊到下周要出差。这层信息不是全部聊天记录,而是从聊天里抽取出来的关键条目,通常存到向量数据库里,在对话前按相似度召回最相关的一两条。

第三层是用户画像,指相对稳定的用户属性,比如用户希望 AI 怎么称呼自己、偏好什么风格。它单独存储,每次对话都固定加载,不依赖相似度检索。

用一句话概括:短期记忆解决“接得上话”,长期记忆解决“记得住事”,用户画像解决“知道你是谁”。三层配合起来,才是完整的记忆功能。

1.3 手机端和服务器端的分工

这类应用的移动端不能只做一个“录音加播放”的空壳,也不能把所有计算都塞进手机。比较常见的做法是分层分工。

手机端负责音频采集、音频播放、界面交互,以及可选的本地预处理。如果使用 Vosk 离线识别,也可以选择在手机侧完成 ASR,只把文本发给服务器,这样语音数据不出设备,隐私性更好,但识别效果和模型更新受限于端侧模型。

服务器端负责大模型调用、记忆存储、TTS 合成、用户状态管理。选择把对话生成放在服务器而不是手机上的原因很直接:大模型需要显卡或 API 能力,手机上跑小模型可以,但角色扮演质量和知识广度通常不如云端模型。另外,长期记忆需要数据库支撑,放在服务器端更容易做统一的用户维度和数据备份。

还有一种折中方案:ASR 在端侧离线做,LLM 和 TTS 在云端做。这样既减少了语音数据上传的隐私风险,又保留了云端模型的质量优势。后面的实现会以这种“端侧识别加云端生成”的混合结构为例。

2. 技术选型:Vosk 离线识别、Spring AI 接入大模型、TTS 服务化

2.1 语音识别三种方案的取舍

先说 ASR 的选型。市面上可用的方案大致有三类:商用云端语音识别、开源离线识别、端侧推理框架。

方案典型代表优点缺点适用场景
商用云端 ASR阿里云、百度、讯飞识别准确率高、支持方言、接口稳定按次数付费、依赖网络、音频需上传对准确率要求高、网络稳定的正式产品
开源离线 ASRVosk、Whisper 本地部署免费、可离线、数据不出服务器准确率低于商用方案、中文模型需要挑选调参、CPU 资源占用学习验证、隐私敏感场景、低成本原型
端侧推理 ASRwhisper.cpp、端侧 onnx 模型真正离线、延迟低手机性能和内存受限、模型迭代麻烦、识别效果受设备影响纯离线 App、隐私要求极高的场景

实际选型时要注意一个容易被忽略的问题:商用 ASR 的计费和对网络的要求。如果是学习项目或内部原型,Vosk 是更合适的起点,因为模型文件可以本地下载,代码完全可控,不依赖外部配额。

2.2 Vosk 为什么适合作为离线识别起点

Vosk 是一个开源语音识别工具包,支持中文、英文等二十多种语言,Java 端有官方库com.alphacephei:vosk,输入需要 16kHz 采样率、单声道、16 位 PCM 格式的音频。它提供两种使用方式:一种是流式识别,边输入音频边出结果,适合实时对话;另一种是文件识别,一次性给完整音频,适合录音后处理。

对陪伴型语音对话来说,Vosk 的定位不是替代云端 ASR,而是让开发者先用一个免费、可离线的方案把整条链路跑通。等验证了产品形态,再根据准确率要求决定是否切换商用 ASR。这样不会在项目最初阶段就被 API 配额或账单卡住。

使用 Vosk 需要单独下载中文模型,常用模型是vosk-model-small-cn-0.22(小模型,体积小、速度较快)和vosk-model-cn-0.22(大模型,准确率更高、资源占用更大)。小模型适合验证流程,大模型适合追求识别效果。模型文件需要解压到项目目录或指定路径,并把路径配置到应用里。

2.3 Spring AI 在大模型接入层解决什么问题

如果直接使用大模型厂商的 SDK,项目里会出现一坨与具体厂商绑定的调用代码。换模型、加记忆、改提示词时,都容易牵连业务代码。Spring AI 是 Spring 生态用来统一大模型接入的框架,它做的事情和 Spring Data 类似:定义一套通用接口,把不同大模型厂商的差异封装在适配层后面。

对本文场景,Spring AI 主要带来三个价值。第一是ChatClient统一调用入口,业务代码不直接依赖具体厂商 SDK;第二是消息抽象,SystemMessageUserMessageAssistantMessage可以把人设提示词、长期记忆、对话历史都组织成结构化的消息序列,而不是拼一个大字符串;第三是VectorStore接口,可以用同一套 API 对接内存向量库、PostgreSQL、Redis 等不同存储实现。

需要特别注意,Spring AI 的版本更新比较快,API 在不同版本之间有调整。写代码前要确认当前使用的 Spring Boot 版本对应哪个 Spring AI 版本,以官方文档为准。下面示例用的是较新的ChatClientAPI,如果版本不同,按项目实际 API 调整即可。

2.4 语音合成方案选择

TTS 部分是整条链路里最容易出现“音色不对”的环节。选择方案时,重点看三件事:音色是否适合角色、合成延迟是否可控、以及是否可以商用。

如果使用云端 TTS,阿里云、微软 Azure、OpenAI TTS 等方案音质较好,支持多种音色,但按字符或按次计费,且合成请求同样依赖网络。如果追求完全免费和本地化,可以考虑开源 TTS,例如 CosyVoice、GPT-SoVITS 等,但它们通常需要一定的 GPU 资源,普通服务器 CPU 上跑实时合成会比较吃力。

在最小闭环阶段,建议直接使用一个可用的云端 TTS API,把 TTS 封装成独立的TtsService接口。这样后续替换本地 TTS 时,只需要改实现类,不需要动 controller 和业务逻辑。TTS 的输出建议直接返回音频文件 URL 或音频字节流,由手机端播放。

3. 环境准备、依赖配置与项目结构

3.1 学习环境最小清单

先把环境列清楚,避免后面代码跑起来才发现缺少依赖。

组件版本建议用途
JDK17 或 21运行 Spring Boot 3
Maven3.8+依赖管理
Spring Boot3.2 及以上Web 服务基础
Vosk 模型vosk-model-small-cn-0.22离线中文识别
数据库MySQL 8 或 PostgreSQL记忆与历史记录
Redis可选短期记忆缓存、会话状态
大模型 APIOpenAI 兼容接口即可对话生成
TTS API按所选服务申请语音合成

如果只是先跑通代码,MySQL 和 Redis 可以在本地用 Docker 起,或者先用 Spring AI 自带的 SimpleVectorStore 和内存存储验证,等确认链路没问题再引入数据库。

3.2 Maven 依赖与关键配置

pom.xml中加入以下依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.alphacephei</groupId> <artifactId>vosk</artifactId> <version>0.3.47</version> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> <version>1.0.0-M6</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

Vosk 的 Java 版本在 0.3.x 系列,模型路径和采样率通过配置管理。Spring AI 相关依赖需要声明它的仓库地址,或者使用已经带 BOM 的 Spring Boot 依赖管理。实际项目中以官方最新版本为准,不要直接照搬这里写死的版本号。

注意:Spring AI 版本更新较快,不同版本的 API 可能有差异。落地前先确认与 Spring Boot 和 JDK 的版本对应关系,以官方文档为准。

application.yml配置示例:

server: port: 8080 spring: application: name: elaina-voice-chat ai: openai: base-url: ${LLM_BASE_URL:https://api.example.com/v1} api-key: ${LLM_API_KEY:} chat: options: model: ${LLM_MODEL:qwen-plus} temperature: 0.8 max-tokens: 300 app: vosk: model-path: ./models/vosk-model-small-cn-0.22 sample-rate: 16000 tts: endpoint: ${TTS_ENDPOINT:} token: ${TTS_TOKEN:}

这里用环境变量占位符管理密钥,避免把 API Key 写死在配置文件里。temperature在角色陪伴场景一般设置在 0.7 到 0.9,太低会像机器人在背台词,太高会容易跑出人设。max-tokens限制单次回复长度,语音对话场景通常 300 以内就够了,太长 TTS 播放也累赘。

3.3 项目目录结构

一个清晰的包结构能减少后续混乱:

src/main/java/com/example/elaina/ ├── ElainaVoiceChatApplication.java ├── controller/ │ ├── ChatController.java │ └── VoiceController.java ├── service/ │ ├── AsrService.java │ ├── VoskAsrService.java │ ├── ChatService.java │ ├── TtsService.java │ └── MemoryService.java ├── prompt/ │ └── ElainaCharacterPrompt.java ├── entity/ │ ├── MemoryItem.java │ └── UserProfile.java └── repository/ ├── MemoryItemRepository.java └── UserProfileRepository.java

AsrService接口和 Vosk 实现分离,后续换成云端 ASR 只改实现类。controller 只负责接收请求和返回结果,业务逻辑放到 service。

4. 实现语音对话闭环

4.1 移动端音频采集与格式转换

手机录音得到的最常见格式是 AAC、WebM/Opus 或 M4A,而 Vosk 需要的是 16kHz、单声道、16 位 PCM 裸数据。这一步不处理,后面识别结果一定是空的或乱码。

有两种处理方式。一种是在手机端完成转码,例如 Web 端用 WebRTC 采集 16kHz 单声道音频直接上传;另一种是服务器端转码,收到原始音频后用 FFmpeg 转换:

ffmpeg -i input.webm -ar 16000 -ac 1 -f s16le output.pcm

参数含义:

  • -ar 16000:重新采样为 16kHz
  • -ac 1:转成单声道
  • -f s16le:输出 16 位有符号小端序 PCM

在服务器端转码的好处是手机端逻辑简单,缺点是增加一道处理延迟和 CPU 开销。如果是实时对话,推荐手机端直接输出 16kHz 单声道 PCM,省掉转码环节。

4.2 基于 Vosk 的 Java 离线语音识别

写一个VoskAsrService,启动时加载模型,请求到来时把 PCM 字节数组交给 Recognizer:

@Component public class VoskAsrService implements AsrService { private final ObjectMapper objectMapper = new ObjectMapper(); private Model model; @Value("${app.vosk.model-path}") private String modelPath; @Value("${app.vosk.sample-rate}") private int sampleRate; @PostConstruct public void init() { // 模型加载较耗时,放在启动阶段,不要每次请求都 new Model model = new Model(modelPath); } @Override public String recognize(byte[] pcmData) { try (Recognizer recognizer = new Recognizer(model, sampleRate)) { boolean complete = recognizer.acceptWaveForm(pcmData, pcmData.length); String json = complete ? recognizer.getResult() : recognizer.getFinalResult(); return extractText(json); } catch (Exception e) { throw new RuntimeException("ASR 识别失败", e); } } private String extractText(String json) throws JsonProcessingException { JsonNode node = objectMapper.readTree(json); return node.path("text").asText(""); } }

这里有两个关键点。第一,Model对象加载很耗时,必须放在@PostConstruct里一次性初始化,不能放进每次识别的代码中。第二,Recognizer是非线程安全的,高并发时需要对它做对象池管理,否则多个请求同时使用会出问题。学习阶段可以先这样写,生产环境要改成池化。

acceptWaveForm返回true表示模型认为一句话已经结束,可以从getResult()里取完整结果;返回false表示还在识别中,最后要用getFinalResult()强制取最终结果。对一次性上传整段音频的场景,直接判断返回值即可。

注意:Recognizer 不是线程安全的,生产环境高并发场景必须使用对象池,不能每个请求都 new 一个,更不能让多个线程共用一个实例。

4.3 通过 Spring AI 生成角色回复

有了识别文本,下一步是把人设、记忆和文本一起交给大模型。ChatService中组织消息序列:

@Service public class ChatService { private final ChatClient chatClient; private final MemoryService memoryService; public ChatService(ChatClient.Builder builder, MemoryService memoryService) { this.chatClient = builder.build(); this.memoryService = memoryService; } public String reply(String userId, String userText) { List<Message> messages = new ArrayList<>(); // 第一层:角色人设 messages.add(new SystemMessage(ElainaCharacterPrompt.TEMPLATE)); // 第二层:长期记忆和用户画像 String memoryContext = memoryService.buildMemoryContext(userId); if (StringUtils.hasText(memoryContext)) { messages.add(new SystemMessage("以下是这个用户的长期记忆:" + memoryContext)); } // 第三层:当前会话的短期历史 List<ChatRecord> history = memoryService.loadRecentHistory(userId, 10); for (ChatRecord record : history) { messages.add(new UserMessage(record.getUserText())); messages.add(new AssistantMessage(record.getAssistantText())); } messages.add(new UserMessage(userText)); return chatClient.prompt() .messages(messages) .call() .content(); } }

这里ChatRecord是保存一轮对话的简单对象,实际项目里可以用 Java record 或实体类。消息组织顺序很重要。SystemMessage 放在最前面,表示这是系统级约束;用户历史按时间正序排列在最新用户消息之前;最新输入放到最后。如果顺序颠倒,模型可能分不清哪些是历史、哪些是当前输入。

短期历史只加载最近 10 轮,这是为了避免上下文过长。10 轮在语音对话场景大约对应几千 token,对多数模型都比较安全。具体轮数要根据模型上下文长度和单轮平均 token 数调整。

4.4 语音合成与音频返回

TTS 部分封装成接口:

public interface TtsService { byte[] synthesize(String text); }

云端实现类内部调用 TTS HTTP 接口,把返回的音频字节流直接交给 controller:

@RestController @RequestMapping("/api") public class VoiceController { private final AsrService asrService; private final ChatService chatService; private final TtsService ttsService; @PostMapping("/voice-chat") public VoiceChatResponse voiceChat(@RequestBody byte[] pcmAudio) { String userText = asrService.recognize(pcmAudio); String replyText = chatService.reply("u1001", userText); byte[] replyAudio = ttsService.synthesize(replyText); return new VoiceChatResponse(userText, replyText, replyAudio); } }

这个接口把语音输入、对话生成、语音输出串成了最小闭环。这里把userId写死是为了演示,实际项目应从登录态或请求参数中获取。userText回传给客户端还有一个好处:用户能看到 AI 是否正确理解了自己的话,便于调试 ASR 环节。

需要注意,synthesize如果是同步阻塞调用,TTS 接口耗时会被算进请求总时长。生产环境应该改成先返回回复文本和音频 URL,再异步合成;或者用 SSE 分流式返回。学习阶段用同步方式没问题。

5. 角色人设控制:让 AI 真正“像伊蕾娜”

5.1 人设的本质是对回复空间的约束

“让 AI 像伊蕾娜”不是给模型讲一个故事,而是通过系统提示词约束它的回复空间。没有约束时,大模型会默认变成一个客服式的助手,回复大多是“好的”“没问题”“请问还有什么需要帮助的吗”。这种回复放在陪伴型应用里立刻出戏。

人设提示词要完成四件事:定义角色身份、规定说话风格、划定知识边界、设定互动规则。前两件事决定“像不像”,后两件事决定“稳不稳”。很多角色扮演项目只写前两件,结果模型会自己编造用户的信息,甚至在用户问不确定的问题时直接答错。后两件才是生产环境里真正要花时间调的。

5.2 角色提示词的组成与示例

下面是一份面向伊蕾娜角色的示例提示词,用于说明写法,实际项目需要根据角色设定和产品定位持续迭代:

你正在扮演《魔女之旅》中的魔女伊蕾娜。 【身份】 你是一位独自旅行的年轻魔女,去过很多国家,见过形形色色的人和事。 【说话风格】 自信、从容,带着一点旅行者特有的漫不经心。 偶尔会小小得意一下,但不傲慢。 对熟悉的人会放松一些,对陌生人保持礼貌的距离。 【知识边界】 只聊旅行见闻、魔女生活、日常小事、伙伴和写信这些话题。 遇到不确定的事,用“我没有亲眼见过,就不乱说了”来回应,不要编造确定答案。 不要假装认识用户生活中的具体人物或地点,除非用户刚才提到。 【互动规则】 回复长度控制在三到五句话,口语化,像真正在对话。 不重复用户的问题,不总结用户的话。 如果用户提到姓名、喜好、重要日期,自然地记住并以后用得时候带过。

提示词里的每一项都在干一件事:减少模型的猜测空间。“不要假装认识用户生活中的具体人物或地点”这条看起来简单,实际上能显著减少记忆功能带来的幻觉问题。

5.3 人设与记忆配合时的边界

人设和记忆不是两个独立模块。长期记忆如果没有过滤,可能把用户闲聊中随口说的话当成事实塞进提示词,模型就可能说出“我记得你下周三去医院”,而用户并不想让 AI 知道这件事。这里要注意三点:

第一,写入长期记忆前先按“事实性、重要性、私密性”初步判断,不要什么都存。第二,召回记忆后,只把与当前问题相关的条目放进消息,相关度低的不要加。第三,角色提示词里要约定“自然使用记忆”而不是“复述记忆”。用户不会喜欢 AI 每句话都带一句“还记得吗”,这种回复会立刻出戏。

6. 记忆功能实现:短期、长期、用户画像

6.1 短期记忆:会话内滑动窗口

短期记忆的实现在 4.3 已经出现过:从数据库加载最近 N 轮对话,按顺序放进消息。这层要注意两个问题。

一个是窗口大小。窗口太大会浪费 token,太小会接不上话。语音对话场景,建议先取最近 10 轮,观察实际 token 消耗再调整。另一个是存储位置。MySQL 或 PostgreSQL 存历史记录没问题,但高频会话中每次都查最近 10 轮,数据库压力会上升。学习阶段直接在库里查,生产环境可以引入 Redis,把最近对话缓存起来,数据库只做持久化备份。

6.2 长期记忆:向量存储与相似度召回

长期记忆的思路是把聊天中抽取出的关键事实向量化,等下一次对话时用当前问题做相似度检索,取最相关的几段记忆加入上下文。Spring AI 的VectorStore接口可以屏蔽不同向量库的差异。

示例代码:

@Service public class MemoryService { private final VectorStore vectorStore; private final MemoryItemRepository memoryItemRepository; public String buildMemoryContext(String userId, String query) { SearchRequest request = SearchRequest.query(query) .withTopK(3); List<Document> docs = vectorStore.similaritySearch(request); return docs.stream() .filter(doc -> userId.equals(doc.getMetadata().get("userId"))) .map(Document::getText) .collect(Collectors.joining("\n")); } }

这里的关键参数是topK。取 3 条左右比较常见,取太多会冲淡当前消息的重要性,取太少又可能漏掉关键记忆。向量检索出的结果还要按用户维度过滤,不要让用户 A 的记忆进入用户 B 的上下文。最简单的做法是把userId写进 Document 的 metadata,检索后逐条校验。

新记忆的来源需要单独处理。比较实用的做法是:每轮对话结束后,用大模型对当前对话做一次“是否值得记录”的抽取,把结果异步写入向量库。这样不会阻塞主流程,也能过滤掉大部分闲聊内容。简单实现可以用预设规则判断,比如包含“我喜欢”“我下周”“我叫”等句式时记录。

6.3 用户画像:稳定属性的独立存储

用户画像和长期记忆的区别在于更新频率和使用方式。用户画像适合单独表存储,每次对话直接加载,不需要检索。画像内容建议保持精简:称呼方式、避免的话题、对话语气偏好、固定日程等。如果每次对话都把几十条画像塞进提示词,人设会被淹没,模型反而抓不住重点。

6.4 数据表设计与写入时机

三张核心表的 DDL 如下:

CREATE TABLE t_user_profile ( user_id VARCHAR(64) PRIMARY KEY, call_name VARCHAR(64), preferences TEXT, avoid_topics TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE t_dialog_history ( id BIGINT PRIMARY KEY AUTO_INCREMENT, session_id VARCHAR(64) NOT NULL, user_id VARCHAR(64) NOT NULL, role VARCHAR(16) NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, created_at) ); CREATE TABLE t_memory_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id VARCHAR(64) NOT NULL, content TEXT NOT NULL, importance TINYINT DEFAULT 5, is_active TINYINT DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) );

写入时机按“不强求实时”的原则设计。对话历史可以在每次请求结束时写入,但长期记忆的抽取和写入应该异步执行。推荐用 Spring 的@Async或消息队列,让记忆写入失败也不影响主回复链路。t_memory_item里的is_active字段用于软删除,用户明确说“忘掉这件事”时,应支持把对应记忆标记为失效,而不是物理删除后无法追溯。

7. 运行验证、常见问题与排查路径

7.1 分阶段验证链路

整个链路一次调通的可能性很低,建议按“ASR、对话生成、TTS、记忆”四个阶段分别验证。

先验证 ASR。准备一段 16kHz 单声道 PCM 音频,请求识别接口:

curl -X POST http://localhost:8080/api/asr \ -H "Content-Type: application/octet-stream" \ --data-binary @voice.pcm

预期返回 JSON 中包含识别出的文本。如果 text 为空,先检查音频格式和模型路径。

再验证对话生成:

curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"userId":"u1001","text":"你好,伊蕾娜"}'

预期返回三到五句符合角色风格的回复。

验证记忆时,先发送一条包含用户信息的文本,例如“我喜欢喝美式咖啡”,然后查看数据库:

SELECT user_id, content FROM t_memory_item WHERE user_id = 'u1001';

再开一个新会话问“我喜欢喝什么”,看模型能否回答出来。这一步能同时验证长期记忆的写入和召回是否正常。

注意:不要只验证程序能启动,还要验证输入、输出、异常分支和日志是否符合预期。

7.2 常见问题与排查表

问题现象常见原因检查方式处理建议
识别结果为空音频不是 16kHz 单声道 PCM;模型路径错误用 ffprobe 查看音频参数;检查启动日志模型加载是否成功统一转码为 s16le 16kHz 单声道;确认模型解压路径
回复不像角色人设提示词太弱;temperature 太高;用户消息压过 SystemMessage打印最终发给模型的 messages 内容加强人设约束;temperature 调到 0.7 到 0.8;确认 SystemMessage 在最前
记忆没生效写入和召回时机不对;检索结果未按 userId 过滤查 t_memory_item 是否有记录;打印 buildMemoryContext 返回值明确异步写入逻辑;检索后按 metadata 过滤 userId
上下文过长报错短期历史窗口设置过大查看模型返回的错误信息和 token 统计减少窗口轮数;对历史消息做摘要
整段请求耗时长ASR、LLM、TTS 同步串行分别打印三段耗时TTS 改异步;LLM 开启流式;ASR 换小模型

一个很常见的坑是:修改了配置或提示词后,结果没有任何变化。这种情况优先检查是不是改错了环境,再检查应用是否真的重启加载了新配置,最后看日志里有没有加载到旧模型缓存。

7.3 推荐排查顺序

遇到问题先按这个顺序定位,不要直接去改代码:

  1. 输入是否正确:音频格式、文本内容、userId 是否传对。
  2. 路径和命名:Vosk 模型路径、配置项名称、环境变量是否生效。
  3. 依赖版本:Spring AI 版本与 Spring Boot 是否匹配,Vosk 模型与库版本是否兼容。
  4. 配置参数:采样率、temperature、topK、max-tokens 是否符合预期。
  5. 日志关键字:model loadedASR 识别失败LLM responsememory saved
  6. 数据库状态:历史记录是否写入,记忆条目是否存在,userId 是否一致。
  7. 最后再考虑框架限制:Spring AI 版本 API 差异、Vosk 对特定方言支持不足。

按这个顺序排查,大部分问题都能在不动业务代码的前提下找到方向。

8. 从学习环境到生产环境:六项落地保障与扩展方向

8.1 学习环境与生产环境的差异

学习环境的目标是“链路能通”,生产环境的目标是“用户能用”,两者差异集中在表格里:

维度学习环境生产环境
配置写死在 application.yml环境变量、配置中心、密钥管理
记忆存储SimpleVectorStore、本地表PostgreSQL/PGVector、Redis 缓存、备份
ASR 并发单个 RecognizerRecognizer 对象池、云端 ASR 兜底
TTS同步返回音频异步合成、音频 CDN 分发
日志控制台打印结构化日志、监控告警
内容安全不处理输出内容安全审核、敏感话题兜底

8.2 发布前检查清单

下面这份清单可以在每次发布前过一遍:

  1. 密钥是否已从代码和仓库中清除,是否改成环境变量。
  2. 大模型 API 密钥是否配置了限额和告警。
  3. 语音数据是否有留存策略,是否需要用户同意后保存。
  4. 短期记忆窗口大小是否在目标模型上下文限制内。
  5. 长期记忆召回是否正确按 userId 隔离。
  6. ASR 和 TTS 服务是否有超时和失败兜底。
  7. 是否有请求频率限制,异常流量是否会被识别。
  8. 角色提示词版本是否记录,能否在线上快速回滚。
  9. 是否配置了结构化日志,关键链路是否可追踪。
  10. 是否具备模拟高并发压测的数据和脚本。

提示:这份清单不是一次性的,角色提示词和记忆策略只要改一次,就要重新过一遍相关项。

8.3 后续扩展方向

链路跑通后,可以按优先级推进这些方向。

第一优先级是延迟优化。把 LLM 调用改成流式输出,边生成边合成语音,用户听到第一个字的等待时间能从秒级降到亚秒级。第二优先级是语音质量,接入支持音色定制的 TTS 服务,解决“声音不像角色”的用户反馈。第三优先级是记忆质量,把简单规则抽取改成基于大模型的记忆提炼,并增加记忆合并和遗忘机制。

再往后,可以把记忆、人设、工具调用整合成一个完整的 AI Agent,让角色不仅能聊,还能在用户允许的前提下完成查天气、记日程、定闹钟这类操作。也可以扩展情绪识别、角色动画、多人会话、多角色切换等产品能力。

从工程角度看,这类项目最有价值的地方不是某个单一技术,而是把 ASR、大模型、TTS、记忆四段链路串成一个稳定系统的能力。先把最小闭环跑通,再用真实对话数据持续调优人设和记忆策略,比一开始就追求复杂架构更稳妥。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 5:35:21

大模型与Agent智能体开发实战:从工具链选型到工程落地

做了几年的AI应用开发&#xff0c;我越来越觉得“大模型”和“Agent智能体”这两个词快被讲得没边了。上周还有朋友问我&#xff0c;市面上几十个Agent开发框架&#xff0c;今天LangChain、明天LangGraph、后天LangChain4j&#xff0c;到底该从哪个下手。我给他的回答其实很简单…

作者头像 李华
网站建设 2026/9/7 5:34:45

Win32窗体可拖动工具栏实现:从WM_NCHITTEST到菜单联动的完整指南

简介&#xff1a;演示VC窗体中工具栏菜单拖拽功能的完整实例&#xff0c;属于界面窗体类源码&#xff0c;特别适合想了解MFC自绘控件和动态窗口布局的VC初学者。本例展示如何将带图标菜单和按钮的工具条从主窗口直接拖下&#xff0c;放置到屏幕任意位置&#xff0c;形成类似悬浮…

作者头像 李华
网站建设 2026/9/7 5:34:03

Wayland与PipeWire:Linux桌面底层协议换代与迁移实践

很多 Linux 用户聊到 Wayland 时&#xff0c;会陷入两个极端&#xff1a;一边是“切过去五分钟就劝退”——录屏黑屏、旧应用模糊、Qt 插件找不到、远程工具失效&#xff1b;另一边是“早该换代了”——从 Ubuntu 到 Fedora&#xff0c;从 GNOME 到 KDE&#xff0c;Wayland 会话…

作者头像 李华
网站建设 2026/9/7 5:34:00

Blackboard批量下载工具:Python自动化备份课程资料

简介&#xff1a;BlackboardDownloader是一款基于Java开发的自动化下载工具&#xff0c;面向使用Blackboard在线学习平台的师生与教务人员&#xff0c;用于批量获取课程中的所有文档。用户只需输入用户名和密码&#xff0c;程序便会按照平台原有目录结构&#xff0c;将教学大纲…

作者头像 李华
网站建设 2026/9/7 5:33:21

将Grok4.3接入QQ机器人:超长上下文与定制人设实战

这次我们来看一个很多人都在问的玩法&#xff1a;把 Grok4.3 这类大模型接入 QQ 机器人&#xff0c;做群聊自动回复、私聊陪聊、角色扮演和内容摘要。标题里有两个重点值得先划出来&#xff0c;一个是“超长上下文”&#xff0c;另一个是“丰富调教内容”。前者的价值在于机器人…

作者头像 李华