连锁点餐小程序语音下单能力建设:意图识别与菜品智能匹配二开实践
一、业务背景:语音下单不是锦上添花,而是交互效率问题
在连锁餐饮小程序中,点餐链路的效率直接影响用户体验、门店接待能力和复购意愿。LikeShop 连锁点餐系统原生已经具备完整的图文点单、购物车、规格选择、订单提交和支付能力,能够覆盖常规堂食和外卖场景。但随着门店数量增加和用户频次提升,传统图文点单的交互瓶颈逐渐显现。
1.1 传统点单模式的三类效率问题
- 操作链路过长
用户需要依次进入分类页、查找菜品、选择规格、确认数量、加入购物车,再进入结算页完成支付。完整链路通常需要多次点击,高峰期容易造成用户排队和门店接单压力。 - 熟客复购成本高
对于经常到店的熟客而言,很多菜品组合是固定的。但每次复购都需要重复相同操作,系统没有把“熟悉的点餐习惯”转化为更短的交互路径。 - 场景覆盖有限
在双手占用、视线受限、临时赶路或老年用户等场景下,图文点单的门槛明显高于语音交互。这类用户不一定不愿意下单,而是当前交互方式不适合场景。
1.2 语音下单的真正价值
语音下单的核心价值不是“智能化包装”,而是通过更自然的交互方式缩短点餐路径。具体体现在:
- 效率提升:熟客可以通过一句话快速完成复购;
- 场景扩展:覆盖图文交互不便的场景;
- 体验连续:在不替代原有点单流程的前提下,增加新的交互入口。
1.3 为什么只接 ASR 不够
很多团队认为语音下单就是“语音识别 + 关键词匹配”,但实际落地后往往准确率不足,无法进入商用。原因在于:语音识别只能解决“声音转文字”的问题,而点餐场景真正复杂的是业务语义理解。
用户表达通常具有以下特征:
- 使用别名:“宫保”代替“宫保鸡丁”;
- 使用简称:“可乐”代替“可口可乐中杯”;
- 口语描述:“那个招牌牛肉饭”;
- 信息分散:“来一份微辣的宫保鸡丁,再加一杯冰可乐”;
- 信息缺失:只说菜品名,没说规格或数量。
因此,可用的语音下单必须形成完整闭环:
语音输入 → ASR转写 → 意图识别 → 实体抽取 → 菜品匹配 → 信息补全 → 确认下单
其中,ASR 只是第一步,后续的语义理解和业务匹配才是准确率的关键。
二、问题分析:语音点餐的难点不在语音,而在业务语义
2.1 口语表达与标准菜品名之间存在语义鸿沟
连锁餐饮系统中的菜品名是标准化的,例如:
- “宫保鸡丁套餐”
- “招牌牛肉饭”
- “可口可乐中杯”
但用户实际表达往往是口语化的:
- “宫保鸡丁”
- “宫保套餐”
- “那个鸡肉饭”
- “上次那个”
如果只做关键词匹配,会出现两种情况:
- 匹配不到;
- 误匹配到相似菜品。
因此,必须建立标准菜品名与口语表达之间的语义映射。
2.2 信息碎片化导致结构化解析困难
一句自然语言中通常同时包含多个业务维度:
- 菜品:宫保鸡丁、可乐;
- 规格:中份、微辣、冰;
- 数量:一份、两杯;
- 备注:少辣、不要葱。
这些信息不是按固定顺序出现的,需要从非结构化文本中抽取出来,并映射到系统对应的规格字段。
2.3 准确率和容错率之间存在平衡难题
语音识别本身存在错误率,而点餐又涉及金额和商品选择,因此不能完全追求自由输入。
如果系统容错率过低,用户稍微说不清楚就失败;
如果容错率过高,又容易出现错单。
工程上必须设计为:
- 高置信结果自动进入确认;
- 中置信结果给出候选;
- 低置信结果引导用户补充或切换图文点单。
三、整体方案设计
3.1 设计原则
基于 LikeShop 连锁点餐系统做二开,必须坚持低侵入和可运营。
- 低侵入原则
新增语音交互层和语义处理层,不改造 LikeShop 原有菜品、购物车、订单和支付链路。 - 可运营原则
菜品别名、热词、规格映射和澄清话术应支持后台配置,不依赖发版即可持续优化。 - 可兜底原则
语音识别失败、匹配失败或用户中途退出时,必须能够无缝回到图文点单。 - 确认优先原则
语音结果必须经过用户确认后才进入下单流程,不能直接自动提交订单。
3.2 总体架构
建议采用六层架构:
小程序交互层:语音按钮、实时转写展示、确认页、候选选择页
↓
语音转写层:ASR识别、热词增强、文本清洗
↓
语义理解层:意图识别、实体抽取、规格解析
↓
菜品匹配层:别名词典、语义向量匹配、置信度计算
↓
对话控制层:缺失信息补全、多轮澄清、状态管理
↓
业务接入层:购物车组装、订单确认、调用原生下单接口
↓
原生底座层:LikeShop菜品、规格、购物车、订单、支付服务
3.3 与原生系统的边界
- 复用:菜品库、规格体系、价格库存、购物车、订单、支付全部复用 LikeShop 原生能力;
- 新增:新增语音语义模块、菜品匹配模块、对话控制模块和运营配置后台;
- 接入:通过原生接口组装购物车和确认订单,不直接修改核心业务表;
- 兼容:原有图文点单流程完整保留,语音作为并行入口存在。
四、核心模块设计
4.1 语音转写层
语音转写是后续理解的基础,但不是最终准确率的决定因素。
处理流程
- 接收用户语音输入;
- 调用 ASR 服务获取原始文本;
- 做基础文本清洗;
- 将清洗后的文本送入语义理解模块。
文本清洗规则
- 去除语气词:“嗯”“那个”“啊”;
- 去除礼貌词:“麻烦”“请”“帮我”;
- 归一化数量:“一份”“两个”“三瓶”统一为标准数量表达;
- 归一化规格:“微辣”“少辣”映射为同一标准规格。
热词增强
将门店核心菜品名、套餐名、规格名加入 ASR 热词库,提高专有名词识别率。
4.2 意图识别层
意图识别的目标是先判断用户想做什么,再决定后续动作。
核心意图体系
| 意图 | 用户表达示例 | 处理动作 |
|---|---|---|
| 下单 | “来一份宫保鸡丁套餐” | 解析菜品并进入确认 |
| 加购 | “再加一杯可乐” | 追加到当前购物车 |
| 查询 | “有没有牛肉饭” | 返回候选菜品 |
| 修改 | “换成大份” | 修改已有规格 |
| 结算 | “就这些,下单” | 进入结算页 |
实现策略
采用“规则模板优先,语义模型补充”的混合方案。
- 高频表达使用规则模板,速度快、稳定性高;
- 模糊表达使用语义模型补充,提升覆盖范围;
- 两者结果融合后取高置信意图。
4.3 实体抽取层
实体抽取负责从文本中识别业务字段。
实体类型
| 实体类型 | 说明 | 示例 |
|---|---|---|
| 菜品实体 | 用户提到的菜品或套餐 | 宫保鸡丁、可乐 |
| 数量实体 | 菜品数量 | 一份、两杯 |
| 规格实体 | 份量、温度、辣度等 | 中份、冰、微辣 |
| 备注实体 | 用户特殊要求 | 不要葱、少辣 |
输出结构
实体抽取后应输出结构化结果:
{
"dishes": [
{
"name": "宫保鸡丁",
"num": 1,
"spec": { "份量": "中份", "辣度": "微辣" },
"remark": "不要花生"
},
{
"name": "可乐",
"num": 1,
"spec": { "规格": "中杯", "温度": "冰" }
}
]
}
4.4 菜品匹配层
菜品匹配是语音下单准确率的核心模块。建议采用双层匹配机制。
第一层:别名词典匹配
为每个标准菜品配置多个别名,例如:
- 标准菜品:“宫保鸡丁套餐”
- 别名:“宫保鸡丁”“宫保套餐”“鸡丁饭”
别名词典匹配的优点是:
- 准确率高;
- 响应快;
- 运营可控。
第二层:向量语义匹配
当别名词典没有命中时,使用向量相似度检索。
流程如下:
- 将标准菜品名和描述向量化;
- 将用户提到的菜品名向量化;
- 计算相似度;
- 返回 Top3 候选。
置信度分级
| 置信度 | 处理策略 |
|---|---|
| 高 | 自动匹配,进入确认 |
| 中 | 返回候选列表,用户选择 |
| 低 | 提示未匹配,请换种说法 |
4.5 对话控制层
当信息不完整时,需要通过多轮对话补全。
需要补全的场景
- 只说了菜品名,没有规格;
- 规格存在多个必选项,但用户未说明;
- 数量缺失;
- 匹配到多个候选菜品。
补全策略
- 每次只问一个关键问题;
- 使用自然口语化追问;
- 用户回答后回填字段;
- 信息完整后进入确认页。
五、数据模型设计
建议全部通过新增表扩展,避免影响 LikeShop 核心表结构。
5.1 菜品别名词典表
CREATE TABLE ls_dish_alias_dict (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
dish_id BIGINT NOT NULL,
alias_name VARCHAR(64) NOT NULL,
shop_id BIGINT DEFAULT 0,
sort INT DEFAULT 0,
status TINYINT DEFAULT 1,
create_time DATETIME NOT NULL,
update_time DATETIME NOT NULL,
INDEX idx_dish_id(dish_id),
INDEX idx_alias_name(alias_name)
);
5.2 语音会话表
CREATE TABLE ls_voice_order_session (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(64) NOT NULL UNIQUE,
user_id BIGINT NOT NULL,
shop_id BIGINT NOT NULL,
status TINYINT NOT NULL,
cart_data TEXT,
last_voice_text VARCHAR(500),
create_time DATETIME NOT NULL,
finish_time DATETIME,
INDEX idx_user_id(user_id),
INDEX idx_session_id(session_id)
);
5.3 识别日志表
CREATE TABLE ls_voice_recognition_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
session_id VARCHAR(64),
voice_text VARCHAR(500),
intent_type VARCHAR(32),
match_confidence DECIMAL(3,2),
is_success TINYINT,
shop_id BIGINT,
user_id BIGINT,
create_time DATETIME NOT NULL,
INDEX idx_create_time(create_time)
);
六、关键工程实现
6.1 菜品匹配服务
@Service
public class DishMatchService {
@Autowired
private DishAliasMapper aliasMapper;
@Autowired
private VectorService vectorService;
@Autowired
private DishService dishService;
public DishMatchResult match(String dishName, Long shopId) {
DishAliasDict alias = aliasMapper.match(dishName, shopId);
if (alias != null) {
Dish dish = dishService.getById(alias.getDishId());
return DishMatchResult.high(dish, 1.0);
}
List<VectorMatchItem> candidates = vectorService.search(dishName, shopId, 3);
if (candidates.isEmpty()) {
return DishMatchResult.fail();
}
VectorMatchItem top = candidates.get(0);
if (top.getScore().compareTo(new BigDecimal("0.9")) >= 0) {
Dish dish = dishService.getById(top.getDishId());
return DishMatchResult.high(dish, top.getScore());
}
return DishMatchResult.candidate(candidates);
}
}
6.2 语音下单编排服务
@Service
public class VoiceOrderService {
@Autowired
private IntentService intentService;
@Autowired
private EntityService entityService;
@Autowired
private DishMatchService dishMatchService;
@Autowired
private CartService cartService;
public VoiceOrderResult process(String text, Long userId, Long shopId, String sessionId) {
IntentResult intent = intentService.recognize(text);
if (!IntentType.ORDER.equals(intent.getType())) {
return handleIntent(intent, text);
}
List<OrderDishEntity> entities = entityService.extract(text);
List<CartItemVO> cartItems = new ArrayList<>();
List<ClarifyItem> clarifies = new ArrayList<>();
for (OrderDishEntity entity : entities) {
DishMatchResult matchResult = dishMatchService.match(entity.getName(), shopId);
if (matchResult.hasClarify()) {
clarifies.add(matchResult.getClarifyItem());
continue;
}
CartItemVO item = buildCartItem(matchResult.getDish(), entity);
cartItems.add(item);
}
if (!clarifies.isEmpty()) {
return VoiceOrderResult.clarify(clarifies);
}
cartService.batchAdd(userId, shopId, cartItems);
return VoiceOrderResult.confirm(cartItems);
}
}
6.3 小程序交互逻辑
前端建议采用以下流程:
- 用户点击语音按钮;
- 实时展示转写文本;
- 返回匹配结果清单;
- 用户可修改规格、数量、备注;
- 确认后加入购物车;
- 进入原生结算页。
七、准确率保障体系
语音下单不是一次上线就结束,必须建立持续优化体系。
7.1 别名库运营
- 定期整理 badcase;
- 将用户高频口语补充为别名;
- 按门店维护区域化别名。
7.2 热词运营
- 新品上线同步加入热词;
- 活动菜品提升热词权重;
- 根据识别效果动态调整。
7.3 分级兜底
| 场景 | 处理方式 |
|---|---|
| 转写失败 | 提示重试 |
| 匹配失败 | 引导换种说法 |
| 候选较多 | 用户点选 |
| 用户不确定 | 切回图文点单 |
7.4 指标监控
重点监控:
- ASR 转写准确率;
- 意图识别准确率;
- 菜品匹配准确率;
- 规格映射准确率;
- 语音下单最终成交率。
八、方案价值与落地边界
8.1 业务价值
- 缩短熟客下单路径;
- 扩展点餐交互场景;
- 提升品牌数字化体验;
- 为后续语音复购、推荐加购打下基础。
8.2 落地边界
- 语音下单适合作为补充入口,不替代图文点单;
- 必须坚持确认制,不直接自动下单;
- 优先覆盖高频菜品和熟客场景,再逐步扩展长尾。
结语
连锁餐饮小程序的语音下单,本质上不是“加一个语音入口”,而是对点餐业务语义的一次整理。
对于 LikeShop 连锁点餐系统而言,最合理的方式不是重构原生交易链路,而是在菜品、规格、购物车和订单能力之上,新增语音转写、意图识别、实体抽取、菜品匹配和对话控制模块。通过这种低侵入的二开方式,可以把用户的自然语言转化为结构化的业务动作,并在确认机制的保障下形成真正可用的语音下单能力。
从长期看,语音下单不仅能提升点单效率,也为后续的语音复购、智能推荐和个性化点餐打下基础。