WhatsApp 商业账号通常不缺消息入口,真正困难的是让文本、语音留言和实时语音通话共享同一套点单逻辑。基于 Amazon Bedrock AgentCore 与 Amazon Nova 2,可以把三个渠道统一接入一个业务代理,同时保持渠道层和订单层解耦,并通过共享记忆识别跨渠道出现的同一位顾客。
一条业务链路,三种交互节奏
三类输入看似都来自同一个 WhatsApp 号码,处理方式却明显不同:
- 文本消息是离散请求,可以直接进入代理推理流程。
- 语音留言需要先下载媒体并转写,再把文本及媒体元数据交给代理。
- 实时通话是持续的双向音频流,需要处理打断、低延迟响应和会话状态。
因此,渠道适配器不应直接创建订单。它只负责认证 webhook、解析事件、处理媒体或音频流,并生成统一的内部消息:
{
"customer_id": "wa:+8613800000000",
"conversation_id": "call-or-message-id",
"channel": "voice_note",
"input": {
"text": "我要两份牛肉汉堡,其中一份不要洋葱",
"media_id": "media-123"
}
}
后面的订单代理只接收这种标准事件。它不需要知道语音留言如何下载,也不应该依赖 WhatsApp webhook 的原始字段。这样更换通信供应商、升级转写服务或增加网页点单入口时,菜单查询、规格确认、价格计算和下单工具都不必重写。
共享记忆的主键必须稳定
跨渠道识别顾客的关键不是模型猜测,而是稳定的身份映射。可以把经过规范化和验证的 WhatsApp 电话号码映射为内部 customer_id,再让三种渠道都使用这个标识访问 AgentCore 管理的共享记忆。
记忆内容适合分成两层:
- 短期会话状态:当前购物车、尚未确认的规格、配送地址确认进度。
- 长期顾客偏好:常用地址、过敏原、语言偏好和历史上明确保存的口味偏好。
实时通话开始时,代理可以读取此前文本会话留下的购物车;通话中确认的修改,也可以在随后收到的文本消息里继续使用。但共享记忆不能替代订单数据库。商品价格、库存、支付状态和最终订单必须来自具备事务能力的业务系统,而不是模型记忆。
敏感信息也应设置边界。支付卡数据不应进入提示词或长期记忆;地址和饮食偏好需要明确保留期限,并提供删除机制。读取记忆时还要按租户和顾客隔离,不能仅凭模型生成的标识查询数据。
把模型限制在可审计的订单工具内
Amazon Nova 2 可以理解自然语言和多模态内容,但修改购物车、查询菜单和提交订单应该通过结构化工具完成。一组实用的工具边界可以包括:
search_menu(query, store_id):检索当前门店可售商品。update_cart(customer_id, items):以幂等方式更新购物车。quote_order(cart_id):计算税费、配送费和优惠。submit_order(cart_id, idempotency_key):在顾客明确确认后创建订单。handoff_to_staff(reason):遇到过敏原争议、支付异常或低置信度输入时转人工。
系统指令也要把确认规则写清楚。例如:
你是餐厅点单代理。只能使用菜单工具返回的商品、规格和价格。
在提交订单前,必须向顾客复述商品、数量、规格、配送方式和总价,并获得明确确认。
不得推测过敏原信息;工具没有可靠数据时必须转人工。
实时通话中允许简短回应,但顾客说话时应停止播报。
这种约束让模型负责理解和对话,把最终业务写入交给可测试、可审计的服务。
可以这样实践:先跑通统一事件入口
下面是一个可运行的 FastAPI 最小项目。它模拟三个 WhatsApp 渠道进入同一个订单处理函数,并用内存字典演示共享顾客状态。示例中的 AgentCore、WhatsApp 媒体下载和实时音频连接是待替换的边界,不代表具体 AWS SDK 接口。
先安装依赖:
python -m venv .venv
source .venv/bin/activate
pip install fastapi uvicorn pydantic
创建 app.py:
from typing import Literal
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
memory: dict[str, dict] = {}
class ChannelEvent(BaseModel):
customer_id: str
conversation_id: str
channel: Literal["text", "voice_note", "voice_call"]
text: str
def invoke_ordering_agent(event: ChannelEvent, state: dict) -> dict:
# 实际部署时,在这里调用 AgentCore 托管的代理,并传入 Nova 2、工具和共享记忆标识。
state["last_channel"] = event.channel
state["last_request"] = event.text
return {
"reply": f"已收到:{event.text}。提交订单前我会再次确认商品和总价。",
"requires_confirmation": True,
}
@app.post("/channel-events")
def receive_event(event: ChannelEvent) -> dict:
# 生产环境应使用持久化存储,并按租户和 customer_id 隔离。
state = memory.setdefault(event.customer_id, {})
result = invoke_ordering_agent(event, state)
return {"customer_id": event.customer_id, "channel": event.channel, **result}
启动服务并分别模拟文本和语音通话:
uvicorn app:app --reload --port 8000
curl -s http://127.0.0.1:8000/channel-events \
-H 'Content-Type: application/json' \
-d '{
"customer_id":"wa:+8613800000000",
"conversation_id":"msg-001",
"channel":"text",
"text":"给我加一份鸡肉卷"
}'
curl -s http://127.0.0.1:8000/channel-events \
-H 'Content-Type: application/json' \
-d '{
"customer_id":"wa:+8613800000000",
"conversation_id":"call-001",
"channel":"voice_call",
"text":"刚才那份鸡肉卷不要辣"
}'
两个请求使用相同的 customer_id,因此会命中同一份状态。接入生产系统时,可以把 invoke_ordering_agent 替换为 AgentCore 调用,把内存字典替换为受控的共享记忆或持久化会话存储;语音留言转写结果和实时通话识别片段仍然发送到同一内部接口。
实时音频不能简单地按普通 HTTP 消息处理。适配器通常还需要维护流式连接、语音活动检测、部分转写和中断信号,并且只把稳定的语义片段交给订单代理。否则每个临时转写结果都可能触发一次购物车修改。
上线前需要守住的边界
可以按以下清单推进部署:
- 为 webhook 验签,并对媒体下载地址和回调来源做校验。
- 使用稳定、规范化的顾客标识,同时实施租户隔离和最小权限控制。
- 为购物车更新和订单提交增加幂等键,避免 webhook 重试造成重复下单。
- 把菜单、库存、价格和订单状态保存在权威业务系统中。
- 记录工具调用、确认内容、模型版本和追踪 ID,但对电话号码、地址及音频转写做脱敏。
- 为低置信度转写、过敏原问题、缺货替换和支付失败准备人工接管。
- 分别监控文本响应时间、语音留言处理时间和实时通话端到端延迟。
- 用相同顾客跨三个渠道切换的测试用例验证记忆连续性,并测试顾客数据删除流程。
这套架构的核心价值不只是支持三种输入,而是让渠道变化停留在适配层,让订单规则集中在同一个代理和工具集合中。共享记忆负责延续对话,订单系统负责事实和事务,二者边界清楚后,多模态体验才不会以数据混乱和重复下单为代价。