看见即解锁,走过成游记:我如何在 GPASS 上把旅行问答做成一个会推进、会记忆的 AI 眼镜 Agent
作品:镜游记|参赛方向:AI 眼镜陪伴旅游出行
本文记录镜游记在 GPASS 上的完整实现。工作流截图和 Web 回归用于说明技术链路,Rokid 第一视角操作见作品 Demo。
如果只是问“二七广场有什么故事”,普通旅行助手已经能给出不错的答案。但把它戴到眼镜上以后,我总觉得还差一点:它知道很多,却不知道我现在走到了哪儿,也不会因为我看见了什么而真正推进旅程。
镜游记就是从这个不太满意的地方开始的。
我希望它不只是一个景点百科,而是一个能陪人把一段城市探索走完的搭子:先规划路线,到达后拍照确认,再完成一个很短的观察任务;途中可以找吃饭、休息的地方;最后把真正解锁过的地点和照片留下来。整个过程可以概括成:
规划路线 → 选择必去点 → 拍照确认 → 观察任务 → 下一站/顺路服务 → 游历记忆
眼镜和手机最大的差别,不只是屏幕更小。它一直处在人的第一视角里,所以“看见”应该成为一次业务事件,而不是一张普通的聊天附件。
在镜游记里,同一张照片可能带来三种不同结果:
- 画面模糊,或者没有唯一地点线索:不解锁,请用户换角度重拍;
- 画面能够唯一匹配当前路线点:写入确认记录,然后开启这个地点的观察任务;
- 旅程结束:只有这些确认记录会进入游历总结,路线里还没去的地方不会被写成“已经到访”。
这件事听上去很自然,真正实现时却牵出了路线状态、视觉证据、任务顺序、外部记忆和跨设备配对。后面的绝大多数设计,都是为了让这几个环节不要互相抢话,也不要靠模型临场猜。
先看一遍镜游记是怎么用的
以“郑州二七广场 90 分钟人文路线”为例,我先对它说:
帮我规划一个郑州二七广场周围 90 分钟的城市游。
系统发现玩法还没说,只追问这次更想看人文故事、建筑,还是街巷烟火。回答“人文故事”之后,工作流才第一次进入 search_candidates。CandidateFinder 在这一轮搜索动作里调用一次地图工具,把真实候选交给用户;如果用户说“再试一次”,才会开启一轮新的搜索动作。

这里没有直接让模型黑盒生成一条固定路线。它会先问:
请选择一个最想去的必去点,我会围绕它生成完整路线。
如果不止想去一个,也可以直接说“1 和 3 都想去”。选中的地点都会成为必经点,后端再根据时长和步行片区补齐整条路线。

路线生成后,第一步不是回答任务,而是先到达地点、停稳、拍照。没有清晰证据时,系统会明确拒绝解锁:

换成能看清牌匾或建筑特征的画面,且它与当前路线点唯一匹配后,才会形成 confirmed checkin,并给出接下来的观察任务。

完成任务、查询顺路服务、收藏以后,可以让它生成游历地图。总结只读取已经确认的地点、完成的任务和明确收藏的服务。

这五张图都是 Web 上传附件跑出来的回归证据,用来验证工作流和后端契约,不代表用户物理到场。真正的现场第一视角会在 Rokid 真机 Demo 中单独录制,并与屏幕素材明确区分。
这套工作流是怎么搭起来的
最开始很容易把所有能力都塞进一个大 Prompt:理解用户、找地点、判断照片、决定路线阶段,最后再组织一段回复。实际多跑几轮就会发现,一旦模型换了、历史变长了,或者插件返回稍有不同,整条链都会漂。
后来我把职责拆成四层:眼镜和 Web 负责输入,GPASS 负责分流与模型理解,Vercel 状态服务负责阶段和事实,Redis、Blob 与实时工具负责数据。

GPASS v5 工作流由 39 个节点、44 条连线组成,包含 11 个业务意图和默认“其他”。非视觉模型节点统一使用 GLM-5.2,视觉节点使用 Qwen3.7-plus。

上面是真实工作流画布。节点比较多,直接看截图不容易理清关系,所以我又依据同一份 runtime snapshot 画了一张核心拓扑:

现在的分工比较克制:
- 模型理解本轮自然语言和画面;
- 地图、本地生活等插件提供实时外部信息;
- 后端决定路线阶段、候选范围、解锁、幂等和持久化;
- RouteSpeech、CandidateSpeech、QuestSpeech、MapSpeech、FinishSpeech 只读取后端返回的
data.speech,不再让另一个模型改写一次业务事实。
Web 调试没有真实眼镜相机,所以视觉回归采用“先上传附件,再发送一句话”的方式,走的仍然是同一条 VL 和 /api/checkins 链路。真机端则由眼镜拍照插件取得第一视角图片 URL。
工具也不是每轮全开。进入 search_candidates 时,高德周边搜索每个搜索动作只执行一次;本地生活、天气和辅助搜索只放在“附近服务”分支,其他意图的通用工具预算为 0。模型拿到插件结果后仍要接受数量、距离、状态和持久化约束,不能补造插件没有返回的地点或商户。
最难调的,是“第二个”和“1 和 3 都想去”
多轮路线里最麻烦的输入,往往不是长句,而是“人文故事”“第二个”“就这个”这种短回答。
同一句“第二个”,在候选选择阶段代表路线点;有活动观察任务时,它又可能是任务答案;如果后端已经没有路线和任务状态,它就不该凭几轮之前的聊天记录被强行解释。
为了解决这个问题,每轮开始前,后端都会给意图节点一个 interaction_context_1:
focus=route:正在补路线槽位、选候选或确认换线;focus=quest:有一个等待回答的现场任务;focus=none:当前没有可承接的业务状态。
聊天历史仍然可以帮助理解指代,但不能单独恢复已经不存在的阶段。这样“第二个”到底指什么,取决于后端真实焦点,而不是模型看着旧对话猜。
中文语序又带来了另一个问题。比如:
郑州规划一次,二七广场 90 分钟的行程。
如果要求地点证据必须是一整段连续字符串,模型很难在不补字的情况下得到“郑州二七广场”。RouteSlotExtractor 因此把证据拆成 evidenceParts:
{
"destination": {
"value": "郑州二七广场",
"evidenceParts": ["郑州", "二七广场"]
},
"durationMinutes": {
"value": 90,
"evidenceParts": ["90 分钟"]
}
}
这些片段必须逐字来自本轮原话,并保持出现顺序。用户可以自然地插入、倒装,后端也仍然能检查模型有没有从历史里补造地点。
候选点多选同样没有完全交给模型。后端会逐项过滤地图 POI,空地址等坏候选不会再否决整批;去重后最多保留 8 个,至少有 2 个有效候选就可以继续。用户可以说“第一个和第三个”“文庙和城隍庙都想去”,单轮最多选 4 个必经点。
路线容量会随时长变化:60 分钟以下最多 2 点,60–119 分钟最多 3 点,120 分钟及以上最多 4 点;相邻点按约 1500 米的步行聚类约束。候选不是路线成品,更像是用户和路线算法之间的一层控制面。
拍照以后,状态还得继续往前走
视觉模型输出的不只是给用户听的一句话,还会附带一行机器状态:
STATE|spot=<地点>|confidence=<low/medium/high>|unlock_candidate=<true/false>|capture_source=<direct_scene/screen_display/unknown>|evidence=<可见线索>|ocr=<文字>|next_action=<动作>
后端收到它以后,会再检查置信度、证据、当前活动路线以及能否唯一匹配路线点。Qwen3.7-plus 可以说“看起来像这里”,但没有权力自己宣布 confirmed。
capture_source 曾经也是一个容易把用户卡死的地方。视觉模型偶尔漏字段并不罕见,如果缺失就直接 422,用户刚拍的照片等于白拍。现在缺失或空值会默认成 unknown,只有非法枚举或互相冲突的值才拒绝。
不过它只是模型对“画面像直接场景、屏幕内容还是未知”的判断,并不是可信采集通道。固定回归的真实输入是 web_upload,VL 仍有可能判断为 direct_scene。所以 Web 上传、屏幕素材和 Rokid 现场究竟属于哪一种,要由测试记录和人工标注单独留证。
任务顺序也在一次真实测试里暴露过问题。早期话术会在地点尚未解锁时就让用户寻找年代或人物名,用户一回答,/api/quests 却找不到活动任务,平台直接把 404 暴露出来。
现在改成了很明确的顺序:先拍照确认,解锁后才生成观察任务。活动旅程已经存在、但当前还没有等待回答的任务时,POST /api/quests 返回 HTTP 200,同时给出 accepted=false、reason=active_quest_not_found 和“请先停稳拍照确认”的引导。
这轮重构还顺手解决了几处不太显眼、但很容易复发的故障:
- CandidateFinder 漏进一个坏 POI 时,后端过滤单项,不再让整批陷入确定性重试;
- RouteTurnEnvelope 先剥离
<think>,再取最后一个可解析的平衡 JSON,避免未来换模型又读到第一个推理块; - 同一
requestId或幂等键的重复请求不会重复建旅程、重复解锁或重复完成任务; - Redis 原子提交会同时检查当前活动状态;
- 最终游历只读取 confirmed checkins,不因为某个点出现在规划里就把它写成已经走过。
一开始我尝试过几种文本模型。最后把非视觉节点统一回 GLM-5.2,是因为它在中文短回答、结构输出和路线承接上更稳定;VL 则使用 Qwen3.7-plus 处理 OCR、地点线索、置信度和画面类型。模型以后仍然可以换,但每次更换都要重新跑静态工作流 gate 和固定回归,不能只看一两轮聊天感觉不错。
旅程不能只活在聊天窗口里
镜游记的外部记忆服务使用 Node.js 24、TypeScript 5.9、Vercel Functions、Upstash Redis 和 Vercel Blob,保存路线草稿、活动旅程、确认解锁、任务答案、收藏和照片。开启新对话后说“我以前去过哪里”,读取的是后端历史,不是让模型凭聊天记录回忆。
照片和完整路线更适合在手机上看。手机打开 /connect 后会生成一个 5 分钟一次性二维码;眼镜拍摄并调用 /api/pair,手机获得 HttpOnly、Secure、SameSite=Lax 会话 Cookie,然后进入对应用户的游历页面。

这张是生产域名上的静态郑州样例页,用来检查布局、图片和控制台错误,不是配对后的动态用户页面。

服务端使用 sharp + jsQR 解码。标准二维码必须匹配本站 /connect?pair=... 的 origin 和 path,同时兼容 32 位原始 token 与 6 位备用码;过期、重复 claim 或错误用户都会被拒绝,二维码本身也不携带设备唯一标识。动态用户页由这条 Rokid 配对链路打开,上面的静态样例只负责展示页面布局。
我怎么确认它不是“刚好跑通”
这类工作流最容易出现的错觉,是刚刚在调试窗口里聊对了一次,就觉得已经稳定。现在每次修改 Prompt、模型、变量引用、插件、HTTP、Code 节点或连线后,都会从清理旧状态开始,把同一条 Web 主链完整跑一遍:
取消规划 → 地点/时长 → 补玩法 → 多选必经点 → 无任务保护 → 模糊图拒绝 → 清晰图解锁 → 完成任务 → 下一站 → 真实服务 → 收藏 → 游历地图 → 结束 → 新对话历史 → 生产页面
这套固定回归的结果如下:
| 检查 | 结果 |
|---|---|
| TypeScript build | 通过 |
| Vitest | 15 个测试文件,211 条测试全部通过 |
npm audit --audit-level=high | 0 vulnerabilities |
| GPASS v5 Web 回归 | 18 项 Web 可执行步骤全部通过 |
| Rokid 专属设备链路 | 动态配对、播报与退出在真机 Demo 中呈现 |
| 生产健康页 | HTTP 200,存储为 Upstash |
Web 回归同时保留了完整录像,可以按实际发送顺序复核路线、视觉、任务、服务和记忆链路。Rokid 真机 Demo 则负责呈现第一视角拍照、动态配对、单消息播报和官方退出,两类证据各自对应不同的运行环境。
如果第一次体验镜游记,可以像平常说话一样走完下面这段:
- “帮我规划一个郑州二七广场周围 90 分钟的城市游。”
- “人文故事。”
- “1 和 3 都想去。”
- 到第一站停稳后说:“我到了,看一下这里。”
- 根据眼前真实内容回答观察任务。
- “下一站去哪?”或者“附近有什么适合快速吃的?”
- 对真实返回结果说:“收藏第一个。”
- “生成游历地图。”
- “结束本次游玩。”
- 在新对话里问:“我以前去过哪里?”
- 手机打开
https://jingyouji.cepio.cc/connect,再对眼镜说:“连接手机。” - 使用结束后说:“退出智能体。”
镜游记的后端服务部署在 Vercel,GPASS v5 负责多轮意图编排,Rokid 负责第一视角拍照、播报、配对和退出。Web 回归用来固定工作流契约,真机 Demo 展示真实佩戴环境下的完整体验。
镜游记把能力集中在第一视角理解、任务推进和可追溯的游历记忆上,不依赖连续录像、AR 箭头或后台持续定位。confirmed checkin 表示本轮可见内容通过当前路线匹配并完成确认解锁;观察任务则依据活动状态和用户提交的现场细节完成记录。每一种状态都保留清楚的事实边界,不把技术验证包装成系统尚未做出的判断。
这也是我做完这一轮后最在意的地方:镜游记不需要假装什么都能做,但它答应推进的每一步,都应该能从状态、图片和回归记录里找得到。
看见即解锁,走过成游记。
更多推荐

所有评论(0)