慢任务快回答:VisionClaw异步结果交付的心跳、延迟降级与关键词验证设计
【免费下载链接】VisionClawReal-time AI assistant for Meta Ray-Ban smart glasses -- voice + vision + agentic actions via Gemini Live and OpenClaw项目地址: https://gitcode.com/gh_mirrors/vi/VisionClaw
VisionClaw 是一个专为 Meta Ray-Ban 智能眼镜打造的实时 AI 助手:戴上眼镜,用语音下达任务——查天气、发消息、网购比价,它都能看着你看到的世界直接回答。但真正的难点在于"慢任务":让 AI 帮你浏览购物网站、操作日历,往往要几十秒甚至几分钟。VisionClaw 用一套心跳保活 + 延迟降级 + 关键词验证的异步交付设计,做到"慢任务快回答"——语音永远不被卡住,结果落地也绝不打丢。
痛点:眼镜上的 AI,等不起
在手机 App 里,转圈等待尚可接受;但在眼镜上,用户看不见屏幕,唯一的感知渠道是声音。一次实测观察写在源码注释里:任务超过十几秒没动静,用户就会以为"AI 死机了"直接挂断——而挂断会连带杀掉会话、工具调用和还没到的答案。
所以 VisionClaw 的核心设计目标很明确:语音通道永远不能阻塞在慢任务上,且结果最终必须可靠送达。
整体思路:三条防线
整套机制集中在语音工作进程 agent/main.py 与任务网关 gateway/src/server.ts 中,可以概括为:
| 防线 | 触发时机 | 作用 |
|---|---|---|
| ⚡ 快速通道 | 结果 10 秒内返回 | 当作普通一问一答,直接播报 |
| 🔄 延迟降级 | 超过 10 秒 | 立刻放话"还在做",任务转后台 |
| 💓 心跳保活 | 每 30 秒 | 插一句简短进度,证明 AI 还活着 |
| 🔍 关键词验证 | 结果落地后 | 确认结果真的被"说出口",没说到就重播 |
| 🅿️ 结果停放 | 用户已挂断 | 存进网关,下次通话开场补报 |
第一道防线:10 秒延迟降级,先答后做
关键常量定义在 agent/main.py#L345-L366:
QUICK_ANSWER_S = 10 # 工具调用可占用"对话回合"的秒数上限代理核心逻辑 _run_delegated() 的流程是:发起后台任务后只等 10 秒。若结果按时回来,一切照旧;若没回来,立即向模型注入一条指令(而非一句固定台词):"任务需要更长时间,请在自己的话里简短告诉用户你还在处理,不要猜测答案。"
选 10 秒不是拍脑袋:简单任务在干净会话上实测 8~9 秒完成,10 秒恰好让它们"一次干净回答",而不必走"进度提示 + 后续补报"两段式。
网关侧是配套的双速回合(two-speed turns,见 gateway/README.md):请求等待上限(默认 30 秒)一到,立即返回一句确认,最终结果改走 WebSocket 事件推送。超时时返回的不是给人读的句子,而是带[task started]前缀的指令块(gateway/src/server.ts#L51-L55)——源码注释解释了原因:固定台词会被原样念出、像客服播报;让模型用自己的话"垫场",语气才融进当前对话。语音侧则通过GATEWAY_DEFERRAL_PREFIX认出这个前缀,永远不会把指令块当成答案转述给用户。
第二道防线:心跳,让"活着"被听见
任务转后台后,relay 循环 每隔HEARTBEAT_S = 30秒检查一次任务状态。没完成,就请模型补一句极短的进度提示——"几个词以内,融入对话,不要宣布、不要复述请求、不要说缺结果"。
两个细节值得注意:
- 上限 4 次(
MAX_HEARTBEATS = 4):心跳太多会变成唠叨,120 秒后闭嘴,静候结果; - 只在通道空闲时插话(
_session_busy()检测):模型正在说话或用户正在说话时绝不抢话,避免和打断(barge-in)处理互相竞争——这个竞争正是早期版本里"用户回合无人应答"的元凶。
注释里有一句设计哲学值得所有做实时语音的开发者收藏:"一个工作中的 agent,永远不能和一个死掉的 agent 无法区分。"
第三道防线:关键词验证,确认结果真的被"说"了
结果落地后,模型并不保证一定会把它播报出来(可能忙着处理新对话)。VisionClaw 的答案是关键词验证,实现在 agent/main.py#L275-L294:
- 提取特征词:从结果文本中抽取长度 ≥7 的英文单词,过滤掉
finished、result、summary等停用词; - 定达标线:需要命中的词数随结果长度缩放(
n // 10,至少 1 个)——短结果不要求超过它拥有的词数,否则一行话的结果永远"验证不过"; - 只数"交棒之后"的话:这是踩过坑的修正。早期版本把整段通话历史都算数,导致一句"还在等 Amazon 的结果……包装尺寸、价格、评分"因为和结果共享词汇而提前"验证成功",重播机制随即停手,结果实际从没被说;
- 轮询 + 重试 + 兜底:每 5 秒检查一次、最多看 45 秒;不达标就等空闲回合后再注入一次更强硬的指令("你还没告诉用户结果,现在就完整说");两轮都不行,进入下一道防线。
最后一里路:结果停放,挂断也不丢答案
如果用户已经挂断、会话都不存在了,_park_result() 会把"任务 + 结果"整体 POST 到网关的/pending-notifications端点;网关 queuePending() 将其持久化(每用户最多保留 20 条,超出丢最旧的),并记一条result_parked追踪事件。
下次用户拨通时,语音进程开场第一件事就是 _drain_pending() 取回积压结果,并注入专门措辞的指令(agent/main.py#L1270-L1329):"你不在的时候任务完成了,下面这段就是你该说的话,先别只顾打招呼。" 随后同样启动一套关键词验证轮询(90 秒,18 × 5s)——因为取回是破坏性的(取走即删),若模型这次仍没说到,宁可重新停放,留给下下次通话。
这条"停放—补报"链路也解释了为什么浏览类任务结束后,屏幕上的实时卡片会多保留 90 秒(BROWSE_KEEPALIVE_S):云端浏览器还停在最终页面,让结果在被说出的同时还能被"看见"。
关键参数速查
| 常量 | 值 | 位置 | 含义 |
|---|---|---|---|
QUICK_ANSWER_S | 10s | agent/main.py#L350 | 快答预算,超过则延迟降级 |
HEARTBEAT_S | 30s | agent/main.py#L354 | 心跳间隔 |
MAX_HEARTBEATS | 4 | agent/main.py#L355 | 单次任务心跳上限 |
DELIVER_WAIT_S | 8s | agent/main.py#L358 | 注入播报前等待空闲回合的上限 |
BROWSE_KEEPALIVE_S | 90s | agent/main.py#L362 | 浏览结果卡片保留时长 |
MAX_PENDING | 20 条 | gateway/src/notify.ts#L57 | 每用户停放结果上限 |
QUICK_ANSWER_TIMEOUT_MS | 30s | gateway/README.md | 网关侧快答预算 |
想深入阅读?
- 语音工作进程全部逻辑(心跳、验证、停放):agent/main.py
- 网关双速回合与确认指令:gateway/src/server.ts
- 事件推送与结果停放队列:gateway/src/notify.ts
- 网关整体说明与端点清单:gateway/README.md
- 应用端(iOS / Android):samples/CameraAccess/ 与 samples/CameraAccessAndroid/
这套"快答 + 降级 + 心跳 + 验证 + 停放"的组合拳,本质上是把异步编程里"最终一致"的思想搬进了口语对话:答案可以迟到,但不能沉默,更不能丢失。如果你也在做实时语音 Agent,这套心跳与验证的取舍思路非常值得借鉴。
【免费下载链接】VisionClawReal-time AI assistant for Meta Ray-Ban smart glasses -- voice + vision + agentic actions via Gemini Live and OpenClaw项目地址: https://gitcode.com/gh_mirrors/vi/VisionClaw
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考