← 返回文章

从复现 NanoJev 到 AGX 上的实时随机迷宫

记录如何在 Jetson AGX Orin 上复现 NanoJev 的本地推理能力,并把一个离线迷宫演示改造成模型驱动、实时展示、可导出 JSON 的随机迷宫实验。

#NanoJev#Jetson AGX Orin#PyTorch#Edge AI#迷宫探索#WebSocket/SSE

从复现 NanoJev 到 AGX 上的实时随机迷宫

最近花了一段时间研究 TianyuCodings/NanoJev。一开始只是想回答一个很实际的问题:这个项目能不能不训练、只在自己的 Jetson AGX Orin 上完成推理复现?后来在看懂它的迷宫和 Snake 演示之后,我又把它扩展成了一个更适合观察的实验:现场随机生成一张保证有通路的迷宫,让 NanoJev 在没有完整地图的情况下探索,浏览器实时显示每一步,并且最后导出完整 JSON 记录。

这件事的价值不在于做出了一个更漂亮的迷宫游戏,而在于把“模型判断”“外部记忆”“环境反馈”和“可视化”拆成了几个可以单独验证的部件。这样才能看清楚:究竟是模型在做什么,控制器在做什么,哪些能力来自训练,哪些能力来自程序设计。

1. 先把目标从“训练”改成“复现”

如果目标是训练一个新的 NanoJev,问题会变成数据集、训练时间、显存、优化器、检查点和评估协议的一整套工程。对于 AGX Orin 这样的边缘设备来说,这条路线并不适合用来做第一次验证。

我这次选择的是更小、更明确的目标:

  • 使用项目已有的 checkpoint;
  • 在 AGX 上加载模型并完成本地 forward;
  • 不调用外部 API,不用教师模型兜底;
  • 复现项目中的判断协议和迷宫控制逻辑;
  • 把每次决策、碰撞和位置变化记录下来。

这两个目标经常被混在一起,但它们的难度完全不同。训练关心的是模型能不能学会,复现关心的是已有模型能不能在新的硬件和新的入口上稳定运行。先把后者做通,才有必要继续讨论训练或微调。

2. NanoJev 在迷宫里到底负责什么

从表面看,迷宫演示像是让模型“看地图、找终点”。但实际拆开后,模型做的事情更窄:它接收当前位置附近的局部窗口,并对四个方向分别回答“这条边是否安全、是否可以走”。

它并不知道完整迷宫,也不直接输出一条从起点到终点的全局路径。全局行为由外围控制器完成:

局部窗口 ──► NanoJev ──► 四个方向的安全概率
                         │
                         ▼
              EdgeExplorer 选择下一条边
                         │
                         ▼
                 迷宫环境返回反馈
                         │
                         └── 更新访问记忆和边状态

这个区分非常重要。模型本身更像一个局部选择器,而不是传统意义上的 A*、Dijkstra 或端到端路径规划器。它能不能最终走到目标,取决于三部分共同作用:模型对局部边的判断、控制器如何处理访问过的节点,以及环境是否提供清晰一致的反馈。

这也解释了最初看到的“来回走”:如果没有足够的访问记忆,局部判断会重复面对相同的问题,模型可能反复选择看起来概率相近的方向。单纯把模型换成更大的模型,不一定能解决这个结构性问题。

3. 在 AGX 上搭建最小推理环境

这次部署采用了独立的 Python venv,把 NanoJev 的依赖和系统 Python、其他项目隔离开。推理服务只负责三件事:加载 checkpoint、接收判断请求、返回本地模型结果。

部署时有几个原则:

  1. 训练依赖和推理依赖分开看,不为了复现把整套训练环境搬到 AGX 上。
  2. 先验证 checkpoint 能否加载,再验证单条请求,最后才接入迷宫循环。
  3. 明确禁止外部教师或远程 API 作为失败时的隐藏兜底。
  4. 对模型调用加锁,避免实时控制线程和普通测试请求同时占用模型。

服务启动后,先用一个最小判断请求检查返回协议,再运行固定种子的 8×8 迷宫。固定 seed 的意义是让每次排错面对同一张地图,避免把地图变化误判成模型变化。

这一步也再次说明了边缘部署里的一个现实:PyTorch 版本并不是孤立变量。checkpoint、CUDA、JetPack、GPU 架构、精度模式和 Python 包版本必须一起匹配。复现阶段最重要的是找到一组能稳定加载和推理的组合,而不是盲目追逐最新版本。

4. 把离线演示改成现场随机生成

原来的演示更接近“已经准备好一组环境,再播放结果”。为了观察模型在新环境中的行为,我增加了一个实时 session:创建迷宫时才生成地图,后端持有 session 状态,浏览器通过事件流接收每一步结果。

一张地图需要同时满足两个条件:随机性和可达性。最简单的做法是先从一个起点生成连通区域,再从可达区域中选择终点;更适合迷宫显示的做法是使用生成树,再按比例增加少量额外通路。

我最终保留了几种拓扑:

  • Tree:分支清楚,但没有环;
  • Long corridors:直路较多,适合观察长距离移动;
  • Loops:存在回路,能测试重复访问;
  • Random obstacles:更接近障碍物环境;
  • High branching:专门给实时实验增加支路,避免地图大部分都是单一长走廊。

这里的 High branching 是实时迷宫的新拓扑,不去改变原有离线评估数据的含义。这样既能增加实验中的分支,也不会让旧的 benchmark 在不知情的情况下换了地图分布。

5. 模型只看局部,浏览器才看完整地图

实时页面里最容易造成误解的一点是:浏览器画出了完整地图,所以看起来像是模型也知道完整地图。实际上两者使用的是不同的数据路径。

后端环境状态 ───────────────► 浏览器 Canvas
       │                           │
       │                           └── 墙、通路、访问轨迹、Agent、Goal
       │
       └── 当前点附近的 3×3/5×5 窗口 ──► NanoJev

浏览器需要完整地图,才能把动画画出来;模型只接收当前位置附近的 ASCII 局部窗口。导出的 JSON 也会把初始地图和运行轨迹保存下来,但这不代表模型在运行时读取了整张地图。

每次到达一个尚未判断过的位置,控制器才调用一次模型。模型返回四个方向的概率后,EdgeExplorer 会结合已有记录选择边:

  • 已确认的开放边可以继续使用;
  • 已确认的阻塞边不再重复撞击;
  • 新边优先进入候选集合;
  • 回到旧节点时,复用已有判断,而不是重新问模型;
  • 当当前分支耗尽时,利用访问图回到仍有未探索边的节点。

因此,访问记忆不是模型训练结果的一部分,而是控制器为模型补上的外部状态。它解决的是“同一个局部问题不要无限重复”的问题。

6. 实时网页的实现方式

页面没有把后端计算完的结果一次性下载下来再播放,而是使用了一个有状态的 live session:

  1. 浏览器提交地图尺寸、seed、拓扑和步进延迟;
  2. 后端生成可达迷宫并创建 session;
  3. 浏览器打开该 session 的 SSE 事件流;
  4. 每次模型判断和环境反馈完成后,后端发布一个 JSON snapshot;
  5. Canvas 根据 snapshot 重绘地图,右侧面板更新概率、碰撞、访问格数和模型调用次数;
  6. 运行结束后,页面提供 JSON 导出链接。

这种设计保留了一个很有用的观察边界:后端是真正运行控制器,前端只是展示已经发生的决策。即使动画速度调整为很慢,也不会改变模型的输入或路径,只改变事件到达浏览器的节奏。

为了让 50×50 地图仍然可读,后来又调整了视觉层级:墙体使用更亮的蓝灰色,未探索通路压暗,已访问区域用青绿色标出,Agent 和 Goal 使用高亮颜色。迷宫的图例也直接写出每种颜色对应的状态,避免把“墙”和“还没有走过的通路”混在一起。

7. 实际遇到的三个问题

7.1 地图被顶到页面下方

第一版页面给地图容器设置了较大的 min-height。在右侧控制面板变高时,整个 grid 被拉伸,Canvas 的正方形地图就被挤到了可视区域下方,截图看起来像是顶部空了一大截。

修复方式是把舞台高度从“至少这么高”改成明确的固定高度,并在小屏幕断点下再恢复自适应。布局问题不需要改模型,属于可视化容器和绘图区域没有分离好。

7.2 地图分支太少

传统的随机生成树容易形成长走廊。它能保证连通,却不一定适合作为观察局部决策的实验环境。于是我在生成树基础上增加一定比例的额外开放边,并把这个策略单独命名为 High branching。

这样做的目的不是让地图更难,而是让模型更频繁地遇到“多个方向都看起来可行”的局部选择,从而可以观察访问记忆、碰撞反馈和回溯机制是否真的在工作。

7.3 页面提示 SERVER ERROR

这次错误最容易被误判成模型崩溃。检查 AGX 日志后发现,模型服务本身是健康的,真正的原因是:页面每次加载都会自动 POST 创建一个新的 session,而早期实现最多保留 16 个 session;刷新页面不会自动删除旧 session,于是第 17 次创建收到 400。

最后做了三层修复:

  • 前端把当前 session id 放入 localStorage,刷新时优先恢复原 session;
  • 用户点击“New maze”时,先删除旧 session,再创建新地图;
  • 后端在达到上限前回收已经没有 SSE 订阅的断开 session,并停止其后台线程。

修复后重启服务清理了当时已经堆积的内存 session,并用创建、删除、健康检查三个请求验证了生命周期。这个问题和 NanoJev 的决策能力无关,却是把实验做成可长期使用的应用时必须处理的工程细节。

8. 这个实验说明了什么

这套系统并没有证明 NanoJev 已经具备通用的规划能力。它更准确地说明了几件事:

  1. 一个小模型可以作为局部安全判断器,接入一个有状态的控制器。
  2. 访问记忆和模型参数是两种不同的能力来源,不能把控制器效果全部归因于模型。
  3. “比随机好一些”和“能稳定完成任务”之间,还隔着环境设计、回溯、终止条件和异常处理。
  4. 只看局部窗口的模型,如果外部控制器维护了访问图,也可以在未知地图上形成可观察的探索行为。
  5. 一个好的复现实验应当把输入、输出、模型调用次数、碰撞和最终状态都记录下来,而不是只展示一段看起来成功的动画。

换句话说,NanoJev 更像一个可以嵌入外部状态机的“判断器”。它有点像把神经网络放进一个小型决策系统:模型负责对当前局部情况给出偏好,程序负责记住历史、执行动作、解释反馈并决定什么时候结束。

9. 下一步可以做什么

有了这个实时 session,后续应用不必局限在迷宫:

  • 把局部安全判断接入 AGX 上的机器人避障实验;
  • 将“订单是否已经退款”这类状态判断做成可配置的 JSON schema;
  • 比较不同模型、不同窗口大小和不同记忆策略对探索效率的影响;
  • 对一批随机地图批量运行,统计成功率、碰撞率和平均模型调用次数;
  • 将控制器从四方向网格扩展到工具选择、流程节点选择或有限状态任务。

其中最值得继续做的是批量评估。单次动画很容易受到 seed 影响,只有在多个地图、多个起点和多个拓扑上统计,才能回答“模型到底比随机好多少”“访问记忆带来了多少收益”这类更可靠的问题。

结语

从源码阅读到 AGX 部署,再到实时随机迷宫,这次工作的核心不是把一个 demo 搬到另一台机器,而是把 demo 背后的结构拆了出来:

NanoJev:局部判断
EdgeExplorer:访问记忆与边选择
MazeEnvironment:动作执行与反馈
Live session:状态、事件流与导出
Canvas:把结果变成可观察的动画

当这几层边界清楚之后,模型到底看到了什么、走错时是谁负责、成功是怎么产生的,就都可以被检查和复现。对边缘设备上的小模型来说,这种可解释、可记录、可替换的结构,往往比单纯追求一个更大的模型更有实践价值。

Conversation

留言

加载中

正在读取留言…

Leave a note

留下你的想法

留言提交后会先进入审核,审核通过才会公开显示。