让患者通过自然对话预约移动采血服务,看起来只是“语音识别加大模型”。真正进入生产环境后,系统却必须同时处理双向音频流、工具调用、身份验证、预约冲突和等待体验。Natera 基于 Amazon Bedrock AgentCore 构建的自动化语音智能体,将这些问题拆成双 WebSocket 桥接、事件驱动的延迟掩蔽和渐进式信任认证,并在其测试场景中实现了 100% 的工具调用准确率以及低于 7 秒的延迟。
这里值得关注的不只是模型选型,而是模型外围的工程控制面:什么事件可以立即反馈,什么操作必须经过验证,以及怎样让每一次工具调用都可检查、可拒绝、可追踪。
双 WebSocket:把实时音频与智能体执行解耦
语音预约涉及两个节奏不同的数据通道。一侧是电话或语音平台,它持续发送音频帧,也要求系统及时返回合成语音;另一侧是智能体运行时,它需要接收上下文、推理意图、调用预约工具并产生响应。
双 WebSocket 桥接可以把这两个边界隔开:
- 面向语音平台的连接负责音频接入、打断检测和播放控制。
- 面向 AgentCore 的连接负责智能体事件、工具调用结果和生成内容。
- 桥接层维护会话 ID、事件顺序、背压和取消信号。
- 结构化业务数据走工具接口,而不是混入音频传输逻辑。
关键点是不要把桥接器写成无状态的字节转发器。一次真实通话至少需要记录当前轮次、是否正在播放、患者是否打断、身份验证级别以及是否存在未完成的工具调用。否则,一句“等等,改到周五”可能在旧的预约请求已经提交之后才被处理。
下面是一个可以改造的最小 Python 桥接器。示例假设语音侧与上游智能体侧都使用 JSON WebSocket 消息;实际接入时,需要替换消息格式、鉴权头和 AgentCore 端点。
python -m venv .venv
source .venv/bin/activate
pip install websockets
export AGENT_WS_URL='wss://your-agent-runtime.example/ws'
python bridge.py
# bridge.py
import asyncio
import json
import os
import uuid
import websockets
AGENT_WS_URL = os.environ["AGENT_WS_URL"]
async def relay_voice_to_agent(voice_ws, agent_ws, session_id):
async for raw in voice_ws:
event = json.loads(raw)
if event.get("type") == "audio":
# 立即确认已收到音频,不必等待完整的模型响应。
await voice_ws.send(json.dumps({
"type": "activity",
"state": "processing",
"session_id": session_id,
}))
event["session_id"] = session_id
await agent_ws.send(json.dumps(event))
async def relay_agent_to_voice(agent_ws, voice_ws):
async for raw in agent_ws:
event = json.loads(raw)
# 工具参数不应直接播放给患者,也不应写入普通音频日志。
if event.get("type") == "tool_call":
continue
await voice_ws.send(json.dumps(event))
async def handle_call(voice_ws):
session_id = str(uuid.uuid4())
async with websockets.connect(
AGENT_WS_URL,
additional_headers={"x-session-id": session_id},
max_queue=32,
) as agent_ws:
tasks = [
asyncio.create_task(
relay_voice_to_agent(voice_ws, agent_ws, session_id)
),
asyncio.create_task(relay_agent_to_voice(agent_ws, voice_ws)),
]
done, pending = await asyncio.wait(
tasks, return_when=asyncio.FIRST_COMPLETED
)
for task in pending:
task.cancel()
await asyncio.gather(*pending, return_exceptions=True)
for task in done:
task.result()
async def main():
async with websockets.serve(handle_call, "0.0.0.0", 8765):
print("Voice bridge listening on ws://localhost:8765")
await asyncio.Future()
if __name__ == "__main__":
asyncio.run(main())
生产实现还要补上每轮超时、有限队列、断线重连策略和幂等键。不能无限缓存音频帧:下游变慢时,应明确丢弃过期的中间反馈或结束会话,而不是让患者听到几十秒前的内容。
延迟掩蔽不是伪造结果,而是尽早发出真实事件
低于 7 秒的延迟仍可能让电话另一端感到停顿。事件驱动的延迟掩蔽通过拆分响应阶段,避免让患者面对一段完全静默的等待:
- 语音活动事件表明系统已经听到用户说话。
- 意图初步确定后,可以播放“我正在查询可用时间”之类的过程反馈。
- 查询工具返回后,再播报真实的日期和时间段。
- 创建预约完成后,才给出明确的成功确认。
这里有一条重要边界:过程反馈可以提前,业务结论不能提前。“正在查询”是可验证的系统状态;“已经预约成功”必须等写操作返回并通过校验。若工具超时,智能体应该说明无法确认,而不是根据语言模型的推测补出预约编号。
延迟也应分段测量,而不是只记录整通电话的平均值。至少需要采集:语音结束到转写完成、转写完成到首个智能体事件、工具调用耗时、首个音频响应时间,以及预约确认总耗时。这样才能判断瓶颈来自网络、模型、合成语音还是排班系统。
渐进式信任:先帮用户查询,再逐步开放写操作
语音渠道不能默认“能说出姓名的人就是本人”。但如果在对话开始时要求患者一次性回答所有验证问题,又会增加放弃率。渐进式信任的做法是让权限随着验证状态提升:
- 匿名状态可以解释服务流程和收集大致地区。
- 基础识别后可以查询不含敏感信息的可用时间段。
- 完成一次性验证码等强验证后,才允许创建、修改或取消预约。
- 高风险操作可以增加二次确认,例如复述日期、地址和患者姓名后再提交。
可以这样实践一个独立于模型的工具授权层。即使模型错误地请求了 book_appointment,后端仍会拒绝未验证会话。
from dataclasses import dataclass
from enum import IntEnum
class Trust(IntEnum):
ANONYMOUS = 0
IDENTIFIED = 1
VERIFIED = 2
REQUIRED_TRUST = {
"search_slots": Trust.IDENTIFIED,
"book_appointment": Trust.VERIFIED,
"cancel_appointment": Trust.VERIFIED,
}
@dataclass
class Session:
session_id: str
trust: Trust = Trust.ANONYMOUS
def authorize_tool(session: Session, tool_name: str) -> None:
required = REQUIRED_TRUST.get(tool_name)
if required is None:
raise ValueError(f"Unknown tool: {tool_name}")
if session.trust < required:
raise PermissionError(
f"{tool_name} requires {required.name}; "
f"session has {session.trust.name}"
)
def invoke_tool(session: Session, tool_name: str, arguments: dict):
authorize_tool(session, tool_name)
return {
"ok": True,
"tool": tool_name,
"arguments": arguments,
"idempotency_key": f"{session.session_id}:{tool_name}:1",
}
if __name__ == "__main__":
call = Session("call-42", Trust.IDENTIFIED)
print(invoke_tool(call, "search_slots", {"postal_code": "98101"}))
try:
invoke_tool(call, "book_appointment", {"slot_id": "slot-9"})
except PermissionError as exc:
print({"ok": False, "error": str(exc)})
call.trust = Trust.VERIFIED
print(invoke_tool(call, "book_appointment", {"slot_id": "slot-9"}))
这类控制必须位于服务端,而不是只写进系统提示词。提示词可以要求模型先验证身份,但它不是权限系统。工具网关还应验证参数模式、患者与会话的绑定关系、时段是否仍然有效,以及幂等键是否已被使用。
100% 工具调用准确率背后的工程含义
来源摘要提到的 100% 工具调用准确率,应理解为 Natera 在其定义的场景和评估集上取得的结果,而不是所有口音、噪声环境和异常输入下的普遍保证。要接近这样的稳定性,团队需要把“调用正确”拆成可测试的条件:
- 在正确意图下选择正确工具。
- 参数满足 JSON Schema,并完成日期、时区和地址规范化。
- 缺少必填信息时继续询问,不猜测值。
- 身份等级不足时不执行写操作。
- 工具失败时不宣称成功。
- 用户打断或修改要求时,取消旧轮次的待执行操作。
测试数据也不能只包含顺畅的预约对话。还要覆盖同音姓名、模糊日期、跨时区电话号码、重复验证码、已被占用的时段、上游超时,以及患者在确认前临时改口等情况。工具调用日志应保存决策结果和脱敏后的结构化参数,但不应记录原始验证码或不必要的医疗信息。
上线前的检查清单
采用这套架构时,可以按以下顺序控制风险:
- 先开放只读的时段查询,再开放创建、修改和取消操作。
- 将身份状态、工具授权和幂等控制放在确定性的服务端代码中。
- 为双 WebSocket 通道设置队列上限、取消传播和超时预算。
- 分别监控首事件延迟、首音频延迟、工具延迟和端到端确认延迟。
- 用真实噪声、打断和表达歧义构建回归测试,而不只测试标准脚本。
- 对敏感字段执行最小化采集、日志脱敏、传输加密和保留期限控制。
- 让失败路径明确回退到人工服务,尤其是身份验证失败或预约状态不确定时。
Natera 的案例说明,可靠的语音预约智能体并不是把电话音频直接交给大模型。双 WebSocket 管住实时数据流,事件驱动反馈改善等待体验,渐进式信任限制工具权限;三者组合起来,模型才有机会在医疗预约这类高约束流程中稳定工作。