把前两课串起来:我们跟随一次「客户端想调用某个上游工具」的完整数据流, 看 3 个元工具是如何接力把请求从客户端送到上游、再把结果带回来的。
客户端连上 downstream(stdio 或 HTTP),调 list_tools, 看到的永远是 search_tools / get_tool_details / call_tool 这 3 个元工具。
metatools 在不可变的 GatewaySnapshot 上跑检索策略(BM25 或 Vector), 返回一组按相关性排好序的候选 ToolSummary(限定名 + 描述)。
从 catalog 取出该工具的完整 ToolDef(含 input schema), 让模型知道要传哪些参数、各是什么类型。
metatools 经 catalog 查出该限定名对应的 (server, tool) ——绝不靠拆 __ 去猜——再路由到 upstream 对应 handle 转发,带每调用超时。
检索(步骤 2/3/4 的读路径)始终发生在一份不可变快照上,所以读路径无锁: 网关用 ArcSwap<GatewaySnapshot> 持有当前快照。 当上游发来 tools/list_changed 时,后台 rebuild_snapshot 会 build-then-swap(先在旁边重建好新快照,再原子换上),整个过程不阻塞正在进行的检索。
// 读路径:在不可变快照上检索,无需加锁 pub async fn search_tools(snap: &GatewaySnapshot, query: &str, top_k: usize) -> Vec<ToolSummary> { snap.strategy.search(query, top_k).await ... } // 路由:经 catalog 查 (server, tool),绝不拆 "__" pub async fn call_tool(snap, registry, name, arguments) -> Result<_, MetaError> { let def = snap.catalog.get(name)?; // (server, tool) let handle = registry.get(&def.server)?; // 找到对应上游 handle.call_tool(&def.name, arguments).await // 转发(带超时) }
快照重建逻辑见 crates/gateway/src/lib.rs · GatewayState::rebuild_snapshot。
记住这条三步心智模型即可: