Atlas如何「嗅探」Agent使用的模型?atlas-acp的model_sniff实战拆解
2026/9/18 22:22:46 网站建设 项目流程

Atlas如何「嗅探」Agent使用的模型?atlas-acp的model_sniff实战拆解

【免费下载链接】atlasSource control for agents. Use multiple coding agents, track their changes and query them in one place项目地址: https://gitcode.com/GitHub_Trending/atlas115/atlas

Atlas 是一款「面向 AI 编程 Agent 的源代码管理工具」:你可以在同一处同时使用多个 Coding Agent(如 Claude Code、Codex、OpenCode 等),统一追踪它们对代码库的改动并随时查询。它的 ACP 客户端核心位于 crates/atlas-acp/,其中model_sniff模块解决了一个很隐蔽的问题——如何准确「嗅探」出每个 Agent 当前使用的模型列表,让 UI 上的模型选择器永远有货可显示。

问题背景:为什么模型列表会「消失」

Atlas 通过ACP(Agent Client Protocol)与各个 Agent 子进程通信:按行分隔的 JSON-RPC 消息走 stdin/stdout。模型信息最初藏在session/new(新建会话)和session/load(恢复会话)的响应里,形如:

{ "currentModelId": "opencode/grok-code", "availableModels": [ {"modelId": "...", "name": "Grok Code"} ] }

但 ACP v1 规范发生了漂移:NewSessionResponse结构体悄悄删除了顶层models字段,模型选择被挪进了configOptions。新版 Agent(如 Kilo)没问题——crates/atlas-acp/src/schema.rs 里的model_blob_from_config_options会把configOptions归一化成统一的模型列表格式。

可问题在于:OpenCode 1.3.x、Cursor 等已发布的 Agent 仍在使用旧方言,坚持返回顶层models对象。类型化的反序列化器会把这个字段直接丢弃——结果就是模型选择器渲染不出来,用户看到的是"这个 Agent 不能切换模型"。

💡 详见 crates/atlas-acp/src/model_sniff.rs 文件头部的注释,完整记录了这段设计动机。

核心思路:不 Fork 协议,在「线路」上偷听

Atlas 没有去分叉 ACP schema,而是采用了更轻量的方案——在原始 JSON-RPC 线路上挂钩子

  1. 挂调试钩子:ACP 的AcpAgent::with_debug提供逐行回调,能看到写入 stdin 和读自 stdout 的每一条原始消息。在 crates/atlas-acp/src/driver.rs 中,Atlas 把每个方向的行分流给ModelSniffer的两个观察方法。
  2. 记住请求 IDobserve_outgoing监听发出的session/new/session/load请求,记录其 JSON-RPCid(数字和字符串统一转成字符串作键)。
  3. 匹配响应抓取observe_incoming收到响应时按 id 匹配,从result.models里把整个模型 blob 摘出来,按 session id 暂存。
  4. 取用即销毁:注册表在组装NewSessionInfo时调用take(session_id)取走 blob——读取即消费,避免重复填充。

整个实现只有 crates/atlas-acp/src/model_sniff.rs 一个文件,核心结构体是ModelSniffer

字段作用
pending在途的session/new(None)/session/load(会话 ID)请求表
captured已抓到的旧方言modelsblob,按会话 ID 索引,等待注册表取走

工程细节:四个容易踩的坑都被填上了

model_sniff的代码值得细读,因为它把协议嗅探里所有边缘情况都处理干净了:

  • 内存有界pending超过 32 条、captured超过 64 条时直接清空。一个卡死不回复的 Agent 不会让映射表无限泄漏。
  • session/load响应不带会话 IDsession/new的响应会返回新生成的 id,但session/load只回显模型列表。所以嗅探器在发请求时就把params.sessionId存下来,响应没带 id 时兜底使用(见 model_sniff.rs 的注释)。
  • 无关流量免疫:无 id 的通知(如session/prompt)、未跟踪的 id、错误响应、甚至 Agent 启动时打在 stdout 上的 banner 文本(如opencode v1.3.15)都会被安全忽略,不会 panic。
  • 读取即消费take()remove实现,同一 blob 不会被折叠进NewSessionInfo两次。

测试代码同样精彩——model_sniff.rs 里用 OpenCode 1.3.x 的真实响应报文做了逐字复刻,4 个测试分别覆盖:常规抓取、session/load兜底 ID、无关流量与错误响应。想跑一遍完整链路,可以直接用示例工程 crates/atlas-acp/examples/smoke.rs,它同样演示了with_debug钩子打印每条进出消息的用法。

嗅探结果如何送达前端

抓到 blob 只是上半场,后半段是「接力」:

  1. 注册表兜底:crates/atlas-acp/src/registry.rs 的new_session里,若类型化响应没给出模型(info.models.is_none()),就从model_sniffer取嗅探到的 blob 补上——旧方言 Agent 的模型选择器因此得以正常渲染。
  2. 前端缓存加速:模型列表本质上是静态的(只有 Agent 升级才会变),但首次获取需要等待进程启动 + 握手(约 3–4 秒)。src/features/chat/lib/acp-models-cache.ts 把每次确认的列表持久化到 localStorage,切换 Agent 时立即乐观填充模型选择器。
  3. 后台预热:src/features/chat/lib/warm-acp-models.ts 会在后台为其他用过的 Agent 开一个"一次性会话"刷新缓存(带 7 天 TTL,避免每次启动都白开会话),让这个"嗅探 + 缓存"的闭环对用户完全无感。

小结:一段不到 130 行的协议考古

model_sniff是"在协议规范与现实之间打补丁"的优秀范例:

  • 🎯不动 schema:不 fork 协议,只在原始线路上旁路监听,侵入性最小;
  • 🛡️防御式编程:有界缓存、ID 归一化、ID 兜底、脏数据免疫,全部就位;
  • 🔗端到端闭环:从 Rust 侧嗅探 → 注册表兜底 → 前端缓存/预热,UI 体验始终"秒出模型列表"。

对想维护多 Agent 工具链的开发者来说,这套"wire tap"(线路窃听)手法通用性很强:凡是类型化反序列化会丢字段、而你又不想受制于某个库版本的场景,都可以照此办理。核心源码就在 crates/atlas-acp/src/model_sniff.rs,配合 crates/atlas-acp/src/driver.rs 的钩子接线一起读,半天就能吃透。

【免费下载链接】atlasSource control for agents. Use multiple coding agents, track their changes and query them in one place项目地址: https://gitcode.com/GitHub_Trending/atlas115/atlas

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询