山东大学软件学院-项目实训-个人开发日志(七):多Agent流式问答、语音闭环与多模态输入能力完善
引言
大家好,我是山东大学软件学院2023级本科生张钧虹,“字节摇篮队”负责人。
在前一阶段开发中,我主要围绕BabyMind的成长管理能力推进实现,包括生长曲线、儿保检查、成长阶段组织以及本地通知等功能。那一阶段的重点,是让系统逐渐具备“围绕宝宝成长过程持续记录、持续提醒、持续反馈”的能力。
而这一阶段,我重新把注意力转回到了BabyMind的交互核心上:既然系统已经有了越来越多的业务数据和模块能力,那么它能不能以一种更自然、更流畅、更贴近真实使用场景的方式被用户调用?
对我来说,这一阶段重点要解决的问题主要有三个:
- 多Agent问答虽然已经可用,但响应方式仍然偏“整段返回”,交互体感不够自然;
- 语音功能虽然已有基础接口,但还没有真正形成从提问到播报的完整闭环;
- 现实育儿场景里,很多问题并不只是文字描述,图片同样是非常重要的输入信息。
因此,这一阶段我主要推进了以下几项工作:
- 为多Agent问答接入SSE流式输出,降低首字等待感;
- 为问答链路接入多轮上下文、停止生成和重复输出修复;
- 进一步优化专家Agent的执行轮次、风险等级判定与并行工具调用;
- 接入Qwen3-VL多模态图像识别能力;
- 完成后端ASR/TTS模型修正与Android端TTS播报闭环;
- 让BabyMind开始逐步从“能问答”走向“更像一个真正可持续使用的智能育儿助手”。
一、为什么这一阶段要优先优化交互体验,而不是继续堆功能点
在前面几个阶段中,BabyMind已经逐步具备了不少核心业务能力:
- 宝宝档案;
- 提醒与时间轴;
- 健康记录与疫苗计划;
- 营养推荐与食谱;
- 统一问答入口与RAG知识库。
从“有没有功能”的角度来看,系统已经初步成形。但当我继续联调和实际体验时,我越来越明显地感受到一个问题:功能存在,并不等于使用体验已经足够自然。
例如,在多Agent问答场景中,即便后端已经可以完成路由和回答生成,但如果用户每次都必须等待整段回答完全生成后才能看到结果,那么交互体感依然会比较生硬。对于家长来说,这会让系统更像是一个“请求-响应工具”,而不是一个自然交流的智能助手。
同样,语音功能如果只能做到“语音转文字”或者“文本转语音”中的某一半,也仍然不能真正适配抱娃、腾不开手的使用场景。
此外,很多育儿场景下的问题并不只是文字,例如:
- 宝宝皮肤起疹子,家长更想直接拍照给系统看;
- 某个辅食配料表能不能吃,家长更自然的方式是拍包装图片;
- 某个动作是否属于当前月龄应有发育表现,也很可能依赖图像判断。
也就是说,BabyMind如果想从“能用”进一步走向“更像一个真实产品”,就不能只继续堆模块,而必须开始打磨交互链路本身。
二、多Agent问答接入SSE流式输出:从“整段返回”转向“边生成边展示”
这一阶段我优先推进的第一件事,就是把多Agent问答改造成流式输出模式。此前统一问答虽然已经能够完成问题路由和回答生成,但在交互方式上仍然是典型的“请求发出去,等待完整结果,再一次性返回”。这种方式的问题非常明显:
- 首字等待时间长;
- 用户不知道系统现在处理到哪一步;
- 在复杂问题下,等待感会更强。
因此,我在后端新增了流式问答服务,对应核心代码位于backend/services/qa_stream_service.py。这个模块的目标,是把现有的LangGraph多Agent执行过程转成SSE事件流,让前端能够逐步接收状态和回答片段。
在实现中,我把一次完整问答过程拆成了若干种稳定的事件类型,例如:
- status:当前进度提示;
- token:逐步返回回答片段;
- done:最终结构化结果;
- error:异常信息。
SSE帧构造逻辑如下:
def sse(payload: dict[str, Any]) -> str:
return f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
而完整的流式问答主入口如下:
async def stream_multi_agent_answer(
*,
user_message: str,
baby_id: int | None,
user_id: int | None,
conversation_history: list[QAConversationTurn] | None = None,
image_base64: str | None = None,
image_media_type: str = "image/jpeg",
http_request: Request | None = None,
) -> AsyncIterator[str]:
这里我特别关注的一点,是不仅要流式返回结果,还要把多Agent执行中的阶段状态也尽可能传给前端,比如:
- 正在调度专家;
- 正在检索知识库;
- 正在生成回答。
这样用户在等待过程中,不再是对系统状态完全无感,而是能明显看到“系统正在做什么”。这一阶段完成后,BabyMind的问答链路开始从“整段返回”逐渐转向“边生成边展示”,交互方式更加接近真实对话。
三、前端SSE接入:让Android端真正接住流式回答
后端支持流式输出之后,前端如果仍然只按普通HTTP响应去处理,那这部分能力就无法真正落地。因此这一阶段我也同步在Android端补上了SSE客户端。对应代码位于frontend/app/src/main/java/com/babymind/network/SseClient.kt。
在这一层里,我基于OkHttp的EventSource封装了一套轻量SSE客户端,用于连接后端/qa/ask/stream接口。入口如下:
fun streamAsk(token: String, request: QAAskRequest): Flow<SseEvent> = callbackFlow {
同时,我把后端的几种事件类型在前端也定义成了对应的数据结构:
sealed class SseEvent {
data class Status(val message: String) : SseEvent()
data class Token(val content: String) : SseEvent()
data class Done(val fullResponse: QAAnswer) : SseEvent()
data class Error(val message: String) : SseEvent()
}
这样做的意义在于,前端不再只是等待一个最终JSON对象,而是能在UI层面逐步处理:
- 状态提示;
- 实时追加文本;
- 完整结果回收;
- 错误回退。
四、多轮上下文、停止生成与重复输出修复:把流式问答从“能跑”变成“更稳定”
仅仅支持流式输出还不够。实际开发过程中,我很快又遇到几个问题:
- 流式问答在某些情况下没有真正带上前文,导致“看起来像多轮,其实还是无状态”;
- 用户一旦发现问题问错了,或者等待时间过长,没有办法中途停止生成;
- 流式链路中还出现过“同一条回答重复输出两次”的问题。
因此,这一阶段我继续围绕问答稳定性做了几个重要修正。
1、多轮上下文接入
为了让系统真正记住近几轮对话内容,我在前端ViewModel中补充了上下文构建和会话维护逻辑,对应代码位于frontend/app/src/main/java/com/babymind/ui/viewmodel/QAViewModel.kt。其中,会把最近若干轮对话构造成标准上下文发送给后端:
private fun buildConversationHistory(messages: List<QAChatMessage>): List<QAConversationTurn>
后端流式服务也会把历史消息转换成图执行需要的格式:
def _history_to_messages(conversation_history: list[QAConversationTurn] | None) -> list[dict[str, str]]:
这样,用户现在不必每次都重新描述上下文,而可以基于上一轮回答继续追问。
2、中途停止生成
为了适配真实使用过程中的“随时打断”需求,我在流式链路中加入了中止处理逻辑。在后端,如果HTTP请求已经断开,就会尽快停止继续向客户端发送内容。前端也对应支持取消底层EventSource连接,从而实现“停止生成”。
3、重复输出修复
在流式化过程中,我还遇到过一个典型问题:由于顶层节点和嵌套Agent节点都可能输出文本,导致某些情况下同一条回答会被重复推送两次。因此,我在流式服务中明确过滤掉不该进入实时输出的节点,并只保留真正代表专家回答的token流。
五、专家Agent执行策略优化:限制轮次、并行工具调用与风险边界收紧
随着多Agent链路越来越复杂,我也越来越明显地感受到:如果不对Agent的执行过程进行约束,它虽然“理论上更智能”,但在真实工程环境中可能会带来更多不可控问题。
因此,这一阶段我继续围绕Agent执行策略做了几个方向的优化。
1、限制专家Agent最大轮次
对应核心代码位于backend/agents/multi_agent_graph.py,我对单次专家执行设置了轮次上限,避免反复调用导致响应时间过长或者死循环:
MAX_AGENT_TURNS = 3
在图执行逻辑中,也会对轮次数进行判断,超过阈值后强制结束:
if agent_turns >= MAX_AGENT_TURNS: return {"next": "FINISH"}
2、启用并行工具调用
随着一个问题里可能同时涉及知识检索、上下文读取、业务数据查询,如果所有步骤完全串行,问答延迟会明显增加。因此,这一阶段我也针对三个专家Agent补充了并行工具调用能力,从而尽可能减少等待时间。
3、风险等级边界收紧
在育儿问答场景中,尤其是健康场景里,我最不希望出现的就是风险等级误判。因此这一阶段我还专门收紧了高风险关键词判断逻辑,避免营养、喂养等普通问题被错误打成高风险。
这一部分调整主要位于backend/services/qa_stream_service.py和backend/services/agent_router_service.py
六、多模态输入能力接入:让图片也能成为多Agent问答的一部分
这一阶段另一个我很重视的增强,就是多模态图片输入,在育儿场景里,很多问题如果只依赖文字,其实天然存在信息损失。例如:
- 宝宝皮疹到底是什么样子;
- 宝宝是否真的出现某种动作能力;
- 辅食包装配料表里到底写了什么。
这些都很适合通过图像辅助判断。因此,我在这一阶段为三个专家Agent接入了Qwen3-VL多模态能力,对应工具位于backend/utils/vision_utils.py。这里我首先实现了把文本和图片统一打包为多模态消息的逻辑:
def build_multimodal_message(text: str, image_base64: str, media_type: str = "image/jpeg") -> HumanMessage:
同时,我并不是简单地“让所有Agent都能看图”,而是给不同专家补充了面向各自领域的图片处理提示。例如:
- 健康Agent更关注皮疹、伤口、可视症状;
- 时间轴Agent更关注动作和发育表现;
- 营养Agent更关注食物、配料表和分量。
七、语音服务修正:把ASR/TTS真正接到正确模型上
在交互体验优化中,语音这一条链路也是我重点补的能力。前面虽然已经有语音相关接口,但实际推进过程中,我发现语音服务接入的模型细节仍需要进一步修正,否则很容易出现:
- 接口能调通,但模型不对;
- 返回格式和前端预期不一致;
- 实际TTS播放链路不稳定。
因此,这一阶段我重点修正了后端语音服务,对应代码位于backend/services/voice_service.py以及配置项backend/core/config.py,ASR和TTS模型分别使用了SiliconFlow上的:
- FunAudioLLM/SenseVoiceSmall
- FunAudioLLM/CosyVoice2-0.5B
对应测试也被补充到了tests/test_voice_service.py,例如,测试中会明确校验ASR请求是否使用正确模型名:
self.assertEqual(capture["data"]["model"], "FunAudioLLM/SenseVoiceSmall")
也会校验TTS请求是否使用完整voice前缀:
self.assertEqual(body["voice"], "FunAudioLLM/CosyVoice2-0.5B:claire")
八、Android端语音播报闭环:从“语音输入”走向“语音问答全流程”
在后端ASR/TTS服务稳定之后,我进一步推进了Android端的语音闭环接入。这一部分涉及的前端代码主要位于:
- frontend/app/src/main/java/com/babymind/data/VoiceRepository.kt
- frontend/app/src/main/java/com/babymind/ui/viewmodel/VoiceViewModel.kt
- frontend/app/src/main/java/com/babymind/ui/screens/VoiceButton.kt
- frontend/app/src/main/java/com/babymind/ui/screens/VoiceSettingsScreen.kt
在这一阶段中,我重点做了几件事:
- 让前端能够上传录音文件并调用后端/voice/ask;
- 让后端在完成ASR、问答后再返回TTS音频;
- 让前端接收音频Base64并进行播放;
- 增加音色和语速设置、试听能力。
例如,语音问答请求入口如下:
suspend fun voiceAsk(token: String, audioFile: File, babyId: Int?, voice: String, speed: Float): Result<VoiceAskResponse>
而语音按钮本身则负责录音、处理状态切换和结果回调:
fun VoiceButton( viewModel: VoiceViewModel, onVoiceResult: (transcribed: String, answer: String, audioBase64: String?) -> Unit, )
这一阶段之后,BabyMind的语音功能终于不再只是“能录音”或“能转文字”,而开始形成这样一条闭环:用户录音 → 后端ASR → 多Agent问答 → TTS合成 → 前端播放
这对育儿场景来说非常重要,因为很多时候家长抱着孩子、做着别的事,并不适合频繁打字。语音闭环越完整,系统的可用性就越强。
九、这一阶段的主要收获与后续计划
这一阶段开发完成后,我最大的感受是:BabyMind的“智能交互能力”终于开始不只是体现在功能列表上,而是逐渐体现在真实交互体验中。
第一,问答体验更自然了。从统一入口到流式输出,再到多轮上下文和停止生成,系统不再只是一次性返回一大段文字,而开始逐渐具备更接近真实对话的交互方式。
第二,交互输入形式更丰富了。现在不仅有文字输入,还有语音输入和图片输入。对育儿这种高度场景化的问题来说,这是非常重要的增强。
第三,系统在工程上更稳了。这一阶段我做的很多工作,其实不是单纯“新增功能”,而是对现有链路做修正和约束,例如:
- 风险边界收紧;
- 重复输出修复;
- 轮次限制;
- 并行调用优化;
- 语音模型接入修正。
接下来,我会继续沿着这条路径推进:
- 继续优化多Agent问答的稳定性和响应速度;
- 完善图片输入和前端展示链路;
- 继续强化语音问答在真实设备上的联调表现;
- 让问答、成长、营养、提醒等更多模块之间形成更自然的联动。
结语
这一阶段,我主要围绕多Agent流式问答、语音闭环和多模态图片输入等方向推进了BabyMind的交互能力优化。
相比前几个阶段更多关注“模块是否存在”,这一阶段我更关注的是:系统能不能以更自然的方式和用户交流,能不能更快地反馈,能不能接住更真实的输入形式,能不能在工程层面保持稳定。
欢迎各位老师、同学批评指正!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)