语音购物助手:从需求到功能的多智能体实践
灿若繁星先生 Lv6

电商的交互方式长期被"搜索框 + 筛选器"统治:用户得会打字、会组织关键词、会在一堆参数里勾选。可真实世界里,我们想买东西时脱口而出的往往是「我想换个手机,预算 3000 以内,主要拍照用」——一句话里同时塞了品类、预算、使用场景。能不能让系统像导购一样听懂这句话,主动把缺的信息问清楚,再挑出合适的商品念给你听?

这是我最近在做的个人项目 语音购物助手(Voice Shopping) 想解决的核心问题。它基于多智能体(Multi-Agent)架构,用户全程语音交互,系统自动完成意图理解、需求澄清、商品推荐、情感应答,全链路流式处理:ASR → Agent → TTS,WebSocket 全双工通信。本文从需求功能两个角度,聊聊这个项目是怎么把"听懂人话的导购"这件事拆解落地的。

一、需求拆解:要做一个"会听话"的购物助手,难在哪

在动手写代码前,我先把"语音购物"这个模糊概念拆成了五条具体需求。每一条都对应一个真实的工程难点。

需求 1:交互方式必须是语音,且要"边说边听"

打字搜索对双手和眼睛都有占用,而语音的最大价值是解放双手——做饭、开车、走路时都能用。这带来一个硬约束:系统不能等用户说完一整段才开始处理,否则延迟高到没法用;同时用户随时可能打断、补充。所以底层必须是全双工的:麦克风录音和扬声器播放要能同时进行。

需求 2:延迟要低,体感要像在跟人说话

人对语音延迟的容忍度远低于文字。文字回复慢一两秒无感,语音停顿超过 1 秒就会觉得"卡"。这就要求从用户开口到听到回复的整条链路(语音识别 → 理解 → 检索 → 合成)必须流式串联,每一段产出一点就立刻喂给下一段,而不是等前一段完全结束。

需求 3:得"听懂",而不是机械匹配关键词

「预算 3000 以内」要理解成价格上限,「主要拍照用」要理解成用途槽位。更要命的是用户经常信息不全——只说"换个手机",没说预算、没说用途。一个合格的导购不会瞎推,而是会反问"平时主要拍照还是打游戏?“。系统必须能判断"信息够不够推荐”,不够就主动澄清。

需求 4:要个性化,记住用户的偏好

同一个用户反复来,系统不该每次都从零问起。身高体重、常买的品牌、价格区间,这些应该沉淀成画像,影响推荐结果。同时对话内的上下文(比如刚才说过预算 500)也要记住,不能用户补充一句就忘了前面。

需求 5:能服务多个商家,数据互不串

这不是单店工具,而是 SaaS 形态——每个商家入驻后看到的是自己的商品、自己的用户、自己的订单。A 商家的用户绝不能搜到 B 商家的商品。这就要求行级数据隔离成为系统能力,而不是靠业务代码自觉。

还有一条隐性的工程需求:成本可控。每次对话都让大模型从头推理很贵,高频问题应该尽量短路掉。

二、功能全景:每条需求如何对应到具体功能

先上一张整体架构图建立全局印象,再逐项拆解:

image

需求清楚了,功能就是需求的工程映射。下面这张表是总览,后面逐项展开。

需求 对应功能 关键技术
语音交互、全双工 WebSocket 双向音频流 + ASR/TTS Paraformer 实时 ASR + CosyVoice TTS
低延迟 三段流式管道 ASR/Agent/TTS 流式串联
听懂人话、主动澄清 4 个专业 Agent + 状态机编排 AgentScope Java,qwen-max/turbo
个性化 用户画像 + 三层记忆 静态/动态画像合并,Redis + PG
多商家隔离 行级 merchant_id 过滤 Sa-Token + MyBatis-Plus 租户插件
成本可控 FAQ 语义短路 + 多级缓存 pgvector 阈值命中,Redis 缓存

功能 1:全双工音频流——让"边说边听"成立

WebSocket 上同时跑两种帧:二进制帧传 PCM 音频(16bit / 16kHz / 单声道),文本帧传 JSON 控制消息(开始录音、停止录音、ASR 中间结果、推荐卡片)。

1
2
Mic ──PCM──▶ WebSocket ──▶ ASR ──text──▶ Agent ──text──▶ TTS ──PCM──▶ WebSocket ──▶ Speaker
(binary) (streaming) (streaming) (streaming) (binary)

上行录音和下行播放同时进行,用户说话的同时也能听到上一轮的回复尾巴——这正是全双工的体感来源。

功能 2:三段流式管道——压低端到端延迟

ASR、Agent、TTS 三段以管道方式串联,不等待前一段完全结束。ASR 产出中间文本时即可触发 Agent 推理,Agent 产出文本片段时即可触发 TTS 合成。这是把"语音回复延迟"压到可接受范围的核心手段。

功能 3:多智能体编排——把"听懂 + 反问 + 推荐 + 应答"拆给专职 Agent

这是整个系统最核心的设计。一个"全能大模型"看似能搞定一切,但在工程上,把意图理解、需求澄清、商品推荐、情感应答混在一个 Prompt 里,既难调试、又贵、效果还不稳定。我把它们拆成 4 个专职 Worker Agent,再用一个手写的 Orchestrator 状态机显式编排。

1
2
3
4
5
6
7
8
9
┌─────────────────────────────────────────────────┐
│ Orchestrator │
│ (手写 Service 级状态机) │
│ IDLE → INTENT_PARSED → CLARIFYING → │
│ READY_TO_RECOMMEND → GENERATING_SPEECH → IDLE │
└──────┬──────────┬──────────┬──────────┬──────────┘
│ │ │ │
IntentAgent ClarifyAgent RecAgent EmotionAgent
(意图理解) (需求澄清) (商品推荐) (情感应答)

四个 Agent 各司其职:

Agent 职责 输入 输出
IntentAgent 意图理解 用户话语 + 近期历史 意图、槽位、置信度
ClarifyAgent 需求澄清(规则优先,LLM 兜底) 当前槽位 反问 or 就绪
RecAgent 商品推荐 槽位 + 用户画像 推荐列表
EmotionAgent 情感应答 推荐结果 + 用户情绪 口语话术 + 展示卡片

为什么用手写状态机而不用现成的 Agent pipeline?因为购物对话的分支是确定性的业务规则(意图决定走哪条链路),不是模型自由发挥。显式编排让链路可读、可测、可降级。意图路由表如下:

意图 走的链路
商品推荐 ClarifyAgent → RecAgent → EmotionAgent
商品对比 RecAgent → EmotionAgent
需澄清 ClarifyAgent(循环直到信息齐备)
确认下单 直接走订单流程
闲聊 EmotionAgent 直接应答
超出范围 EmotionAgent 礼貌拒绝

功能 4:向量检索——让"语义"取代"关键词"

商品和 FAQ 都做语义搜索,底层是 PostgreSQL + pgvector。关键设计是在 SQL 层一次性完成向量检索 + 属性过滤,而不是先 ANN 再用代码过滤:

1
2
3
4
5
6
7
-- HNSW 向量检索 + JSONB 属性过滤,一次查询搞定
SELECT id, name, price, attributes
FROM product
WHERE attributes @> '{"category": "手机"}'::jsonb
AND price <= 3000
ORDER BY embedding <=> $query_vector -- 余弦距离,HNSW 索引加速
LIMIT 20;

vector(1024) 列存 text-embedding-v3 的语义向量,HNSW 索引保证检索速度,JSONB 列存灵活属性(品类、规格)用于硬过滤。这样"手机"能召回"智能手机"“5G 手机”,而不会被关键词字面匹配卡住。

功能 5:FAQ 语义短路——成本可控的关键一招

高频问题(“怎么退货”“支持货到付款吗”)如果每次都走大模型对话,又慢又贵。系统先做向量检索,相似度 ≥ 0.75 直接命中 faq_entry 表的预写答案,根本不进 LLM 对话链路;低于阈值才走 LLM 兜底。这一招把大量重复提问挡在了 token 消耗之外。

功能 6:用户画像 + 三层记忆——个性化与上下文连续

画像分静态(身高、体重等基本属性)和动态(浏览/购买行为沉淀出的品类、品牌偏好)两类,合并成不可变的 UserProfileSnapshot 供所有 Agent 读取,Redis 缓存 24 小时。用户每次浏览、购买都会触发行为回流事件,实时更新动态偏好并刷新缓存。

记忆则是三层架构,对应不同时间尺度:

  • 短期记忆(Redis List,30min TTL)——最近 N 轮对话,供 IntentAgent 读 3 轮上下文、EmotionAgent 读 2 轮情绪趋势;
  • 会话状态(PG + Redis 双写)——Orchestrator 状态机本身,读优先 Redis,写同时落 PG;
  • 长期记忆(PG user_profile)——跨会话的画像和历史行为,会话中提及的品类/品牌会回流写进动态画像。

功能 7:多商家行级隔离——SaaS 的安全底线

每张业务表都带 merchant_id,靠 MyBatis-Plus 租户插件自动注入 WHERE 条件,业务代码无感知:

1
2
// 自动注入: WHERE merchant_id = #{currentMerchantId}
// SQL 层面保证数据隔离,不靠业务代码自觉

鉴权用 Sa-Token:REST 走手机号登录,WebSocket 走 Sub-Protocol token,连进来的 Channel 解析出 SessionScope(即"哪个用户、在哪个商家"),后续所有检索都强制带这个 scope 过滤。

对订单这类用户/商家私有数据,还有一条更严的"主体强制过滤"原则——Repository 查询方法必须显式带 userId/merchantId 维度(如 findByIdAndUserId),让"忘传身份"在编译期就被拦下,而不是靠"先查出来再判 ownership"的事后补救。后者一旦遗漏就直接越权。

功能 8:订单闭环——从推荐到下单的最后一公里

推荐完只是开始,真正闭环是下单。系统设计了 preview → confirm → cancel 三态流转:推荐结果先解析出具体商品引用(OrderReferenceResolver),用 Redis 暂存预览态(PendingOrderStore),用户确认后才真正扣库存。库存扣减走 PostgreSQL 原子操作防超卖,下单成功后在事务内发布事件,画像和长期记忆通过 @TransactionalEventListener(AFTER_COMMIT) + @Async 异步回流——保证"先落库,再更新画像"的顺序,即使画像更新失败也不影响订单。

三、一个完整交互走查

光说架构不直观,拿开头那个例子走一遍。用户说「我想换个手机,预算 3000 以内」:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1. IntentAgent
输入: "我想换个手机,预算 3000 以内"
输出: { intent: 商品推荐, slots: { category: "手机", budget: 3000 }, confidence: 0.91 }

2. ClarifyAgent(发现用途槽位为空,主动反问)
输出: { action: ASK, questionToAsk: "平时主要拍照、打游戏,还是日常使用?" }

3. 用户回答: "主要拍照"

4. ClarifyAgent(槽位补齐)
输出: { action: READY }

5. RecAgent(结合用户画像检索推荐)
输出: { recommendations: [{ name: "Redmi Note 13 Pro+", price: 1499, ... }] }

6. EmotionAgent(口语化包装 + 生成展示卡片)
输出: { speechText: "好,给你挑了三款拍照很出色的……", displayBlocks: [...] }

把这段多轮交互画成时序图,箭头方向和数据流向一目了然:

image

第 6 步的 speechText 流式喂给 TTS 合成音频,displayBlocks 通过 WebSocket 文本帧推到前端渲染成推荐卡片。用户耳朵听到话术的同时,眼睛看到商品卡——听觉和视觉信息同步到达。

四、开发过程:从骨架到闭环的演进

这个项目按六个阶段推进,git log 完整记录了从骨架到闭环的演进。整体节奏遵循一条清晰的顺序:先打通数据,再叠加智能,最后抠性能与成本。每一步都对应一个可独立验证的能力增量。

阶段一:搭骨架、定契约

  • 初始化 Maven 多模块骨架,定下 web → business → ai → infrastructure → common 的单向依赖;
  • 同步写系统架构文档和架构图——先画图后写代码,避免后期返工;
  • 落地向量检索与数据契约规范,把表结构、DTO 提前定死。

这一阶段最关键的决策是「契约先行」:DTO、表结构、模块边界在写第一行业务代码前就锁定,后面所有 Agent 都按契约对接,省掉了大量联调摩擦。

阶段二:打通数据与语音两条链路

  • 商品向量检索 + FAQ 问答全链路(pgvector + DashScope embedding);
  • 用户画像服务、会话管理、短期记忆;
  • 语音通道 ASR/TTS 流式链路 + WebSocket 全双工通信;
  • Agent 工厂与 Prompt 加载器基础架构。

这一阶段把「数据能查到」和「语音能进出」两件最底层的事各自跑通,且互不依赖——为后面 Agent 编排留出了干净的接入点。

阶段三:四个 Agent 逐个落地

  • ClarifyAgent:规则优先 + LLM 兜底的混合澄清链路;
  • RecAgent:商品推荐管线;
  • SentimentAgent → EmotionAgent 改名;
  • EmotionAgent 情感应答;
  • 多视角点评团 + 推荐并行合流 + 事件总线。

这一阶段是「智能」密度最高的环节。改名那次提交值得记一笔:一个准确的名字能纠正对职责的误解——它不只是「分析情感」,而是「带着情感去应答」。

阶段四:编排层串起一切

  • OrchestratorService 编排层 + ComplianceChecker + WebSocket 接入;
  • 会话级短期记忆与跨会话长期记忆优化。

四个 Agent 各自能跑后,还需要一个「指挥」。Orchestrator 按意图路由把它们串成完整链路,至此「用户说一句话到听到回复」的主干道贯通。

阶段五:SaaS 化与商业闭环

  • 多商家数据隔离与权限模型(Sa-Token + 行级 merchant_id);
  • 订单查询主体强制过滤(编译期拦截越权);
  • 下单确认闭环:PG 原子扣库存防超卖 + AFTER_COMMIT 事件回流。

这一阶段把「能聊天」升级成「能做生意」。两个安全设计是重点:行级隔离让多商家互不串数据;主体强制过滤把越权风险在编译期拦下,而不是靠事后 if 判 ownership。

阶段六:性能与成本

  • 延迟优化与 H5 流式改造(压低端到端延迟);
  • LLM/ASR/TTS 成本控制 + logfmt 结构化埋点。

最后阶段回归工程现实:延迟决定能不能用,成本决定能不能上线。FAQ 短路、多级缓存、logfmt 日志都是这一阶段补上的「生产级」打磨。

六个阶段下来,每个阶段的提交都对应一个可独立验证的能力增量——这其实解释了「功能全景」里那些功能为何能稳定存在:它们不是一蹴而就设计出来的,而是一层一层垒起来的。

五、模块划分与技术栈

工程是 Maven 多模块,依赖严格单向:

1
2
3
4
5
6
voice-shopping/
├── voice-shopping-common # 公共工具、常量、DTO、异常
├── voice-shopping-infrastructure # Entity、Repository、向量检索、Redis
├── voice-shopping-ai # AgentScope 配置、Agent 实现
├── voice-shopping-business # Service 层(画像、会话、记忆、订单)
└── voice-shopping-web # Controller、WebSocket、Sa-Token

依赖方向:web → business → ai → infrastructure → common,禁止反向和循环。

技术栈一览:

层级 技术 说明
语言/框架 Java 21 + Spring Boot 4.0.5 虚拟线程 + REST/WebSocket
Agent AgentScope Java 1.0.11 ReActAgent + 手写 Orchestrator
大模型 qwen-max / qwen-turbo 主对话 + 推荐排序
向量化 text-embedding-v3 (DashScope) 1024 维语义向量
语音 Paraformer ASR + CosyVoice TTS 阿里云 NLS,流式
数据库 PostgreSQL + pgvector 关系数据 + HNSW 向量检索
缓存 Redis 7.x 对话状态、短期记忆、画像缓存
权限 Sa-Token 1.45.0 多商家认证 + 行级隔离

六、小结

回过头看,这个项目的核心思路其实就两条:

  • 把模糊的"智能"拆成确定性的工程。"听懂人话"不是靠一个大模型硬扛,而是拆成意图、澄清、推荐、应答四个专职 Agent,用状态机显式编排——可读、可测、可降级。
  • 让每个需求落到一个可验证的功能点上。全双工对应 WebSocket 双向音频流,低延迟对应三段流式管道,个性化对应画像 + 三层记忆,多商家对应行级隔离 + 主体强制过滤,成本可控对应 FAQ 短路 + 多级缓存。

语音购物只是个壳,真正有意思的是这套"多智能体 + 向量检索 + 流式管道"的组合范式——它同样适用于语音客服、语音问诊、语音政务等任何"自然语言驱动 + 检索增强 + 实时交互"的场景。后续打算再聊聊里面的几个深水区:全双工音频流的握手细节、ClarifyAgent 的规则优先策略、以及订单事务与异步事件回流的边界处理。