SharedResources 与 Params 模式


文档摘要

SharedResources 与 Params 模式 本节摘要:工具要干活,需要资源——bash 工具需要终端后端、readfile 需要文件系统句柄、webfetch 需要 HTTP 客户端。如果每个工具都自己持有这些资源,既浪费(重复创建)又混乱(状态不一致)。Grok Build 的解法是 SharedResources / Params 模式:框架维护一块类型擦除的共享资源池,工具按需取用,通过 Params 机制注入。本节会讲清这个模式是什么、它如何工作、为什么需要它,以及它带来的几个工程好处。理解这一节,你就理解了工具「怎么拿到执行所需的弹药」。 一、问题:工具需要资源 先看问题。

SharedResources 与 Params 模式

本节摘要:工具要干活,需要资源——bash 工具需要终端后端、read_file 需要文件系统句柄、web_fetch 需要 HTTP 客户端。如果每个工具都自己持有这些资源,既浪费(重复创建)又混乱(状态不一致)。Grok Build 的解法是 SharedResources / Params 模式:框架维护一块类型擦除的共享资源池,工具按需取用,通过 Params 机制注入。本节会讲清这个模式是什么、它如何工作、为什么需要它,以及它带来的几个工程好处。理解这一节,你就理解了工具「怎么拿到执行所需的弹药」。

一、问题:工具需要资源

先看问题。一个工具要执行,往往需要一些「重资源」或「有状态资源」:

  • bash 工具:需要一个终端后端(派生子进程、注入环境、捕获输出);需要知道当前工作目录;需要超时配置
  • read_file 工具:需要文件系统句柄(实际是 OS 提供的,但工具需要知道工作区根、忽略规则);需要工作目录
  • web_fetch 工具:需要 HTTP 客户端(可能配置了代理、TLS、超时);需要认证
  • task 工具(子代理):需要能派生子会话的句柄;需要配置(哪些子代理类型可用)
  • memory_search 工具:需要记忆系统的句柄(打开的索引、嵌入模型客户端)

这些资源有几个共同特点:

  • 创建成本高:如 HTTP 客户端要建立连接池、终端后端要初始化 PTY
  • 有状态:如终端后端维护子进程状态、记忆索引维护打开的数据库句柄
  • 会话级共享:同一个会话里的多次工具调用,应该共享同一份资源(而不是每次新建)
  • 依赖配置:资源的具体形态依赖会话配置(工作目录、代理设置等)

如果让每个工具自己创建并持有这些资源,会有几个问题:

  • 重复创建:十个工具各自建一个 HTTP 客户端,浪费且连接池分散
  • 状态不一致:不同工具持有不同的「当前目录」,可能不一致
  • 生命周期难管:资源何时创建、何时销毁,没有统一管理
  • 配置传播繁琐:配置变化时要通知所有持资源的工具

SharedResources / Params 模式就是为解决这些问题而生。

二、SharedResources:共享资源池

Grok Build 的解法是:框架维护一个 SharedResources——一块「类型擦除的共享资源池」。它大致是这样:

SharedResources { # 类型擦除的容器,按类型存取资源 resources: HashMap<TypeId, Box<dyn Any + Send + Sync>> } impl SharedResources { fn insert<T: 'static + Send + Sync>(&mut self, resource: T) { # 按类型 ID 存 self.resources.insert(TypeId::of::<T>(), Box::new(resource)) } fn get<T: 'static + Send + Sync>(&self) -> Option<&T> { # 按类型 ID 取 self.resources.get(&TypeId::of::<T>()) .and_then(|r| r.downcast_ref::<T>()) } }

特点:

  • 按类型存取:资源的「键」是它的类型(TypeId)。要取终端后端,就 get::<TerminalBackend>()
  • 类型擦除:内部用 Box<dyn Any> 存储,但取用时 downcast 回强类型
  • 共享:同一份资源被多个工具共享(通过引用)
  • 会话级:一个会话一个 SharedResources,会话结束时统一清理

典型存入的资源

会话启动时,框架把各种资源存入:

let mut resources = SharedResources::new() resources.insert(TerminalBackend::new(...)) # 终端后端 resources.insert(FileSystemHandle::new(...)) # 文件系统句柄 resources.insert(HttpClient::new(config)) # HTTP 客户端 resources.insert(current_cwd.clone()) # 当前工作目录 resources.insert(notifier_handle) # 通知句柄(给 UI 发消息) resources.insert(api_key_provider) # API key 提供器 # ... 其他资源

工具执行时,从 SharedResources 取自己需要的:

impl Tool for BashTool { fn execute(&self, ctx, args) -> ToolStream<...> { # 从共享资源池取终端后端 let terminal = ctx.shared_resources.get::<TerminalBackend>() let cwd = ctx.shared_resources.get::<Cwd>() # 用这些资源执行 terminal.run_command(args.command, cwd, ...) } }

三、Params:工具专属的参数注入

SharedResources 是「所有工具共享」的资源池。但有些资源是「某类工具专属」或「需要特定配置」的。这时用 Params 机制。

回顾第 03 节,注册工具有两种方式:

b.register::<ListDirTool>() # 自包含 b.register_with_params::<BashTool, BashParams>(...) # 需要参数

register_with_params::<T, P>() 的 P 就是 Params——一组工具专属的参数。它的典型形态:

struct BashParams { terminal_backend: Arc<TerminalBackend>, # 终端后端(从 SharedResources 来) timeout: Duration, # 超时配置 cwd: Arc<Path>, # 当前工作目录 notifier: NotifierHandle, # 通知句柄 }

Params 与 SharedResources 的关系

Params 通常是「从 SharedResources 里挑出这个工具需要的,加上工具专属的配置,打包成一个强类型结构」:

# 会话启动时,为需要 Params 的工具构造它们 let bash_params = BashParams { terminal_backend: shared_resources.get::<TerminalBackend>().clone(), timeout: config.bash_timeout, cwd: shared_resources.get::<Cwd>().clone(), notifier: shared_resources.get::<Notifier>().clone(), } # 注册时注入 registry.register_with_params::<BashTool, BashParams>(bash_params)

工具内部通过 Params 拿到强类型的资源:

impl Tool for BashTool { type Params = BashParams # 声明需要的 Params 类型 fn execute(&self, ctx, args) -> ToolStream<...> { # 从 ctx 取 Params let params: &BashParams = ctx.params() # 用 params 里的资源 params.terminal_backend.run_command(...) } }

Params 的好处:

  • 强类型:工具拿到的是 BashParams,字段类型明确,编译期检查
  • 自包含:工具需要的所有东西打包在一起,不必零散地从 SharedResources 取
  • 可配置:工具专属的配置(如 timeout)与共享资源一起注入
  • 显式依赖:工具声明 type Params = BashParams,清楚地表明自己依赖什么

四、模式的两层结构

把 SharedResources 与 Params 放在一起,这个模式是分两层的:

第一层:SharedResources(共享资源池) - 会话级,所有工具共享 - 类型擦除(按 TypeId 存取) - 存「重资源」:终端后端、HTTP 客户端、文件系统、通知句柄等 ↓ 选取 + 加配置 第二层:Params(工具专属参数) - 工具级,每个需要 Params 的工具一份 - 强类型(具体 struct) - 含工具需要的资源切片 + 工具专属配置 ↓ 注入工具 工具执行 - 通过 ctx.params() 拿到强类型 Params - 用 Params 里的资源执行

这两层配合,既保证了资源的共享(SharedResources 一份),又给了工具强类型的访问接口(Params)。

五、为什么这样设计

把这个模式放回更大的设计语境,它体现了几个值得借鉴的原则:

原则一:资源共享

重资源(终端、HTTP、数据库句柄)创建一份,多方共享。这避免了重复创建的浪费,也保证了状态一致(同一份终端后端,所有 bash 调用看到同样的子进程状态)。

原则二:依赖显式化

工具通过 type Params = ... 声明自己依赖什么。这让工具的依赖清晰可见——看一眼就知道「这个工具需要终端后端、cwd、通知句柄」。显式的依赖便于理解、测试、维护。

原则三:类型擦除集中

SharedResources 内部用 Box<dyn Any> 类型擦除,但这种复杂度被封装在 SharedResources 内部。工具通过 Params 拿到的是强类型,不直接接触类型擦除。这是「类型擦除集中化」原则的又一次体现(回顾第 01 节的双 trait 设计)。

原则四:配置与资源统一注入

Params 把「资源」(终端后端)与「配置」(timeout)统一注入。工具不必区分「这个值是从资源池来的」还是「从配置来的」——它们都在 Params 里,统一访问。这简化了工具代码。

原则五:测试友好

测试工具时,可以构造 mock 的 Params(如 mock 的终端后端),注入工具,验证行为。这种「依赖注入」让工具变得可测试,不依赖真实的 OS 资源。

六、与其他模式的对比

把 SharedResources / Params 模式与其他可能的方案对比,能更清楚地理解它的位置:

对比一:全局单例

资源作为全局单例,工具直接访问。问题:多会话隔离困难(两个会话同时跑,全局单例会冲突),测试不友好(无法注入 mock)。

对比二:每个工具自己创建

工具在 execute 内部创建所需资源。问题:重复创建、状态不一致、生命周期难管(每次执行完是销毁还是保留?)。

对比三:通过函数参数传递

把所有资源作为 execute 的参数传递。问题:参数列表爆炸(每个工具要传一长串),大多数工具只用其中几个,接口臃肿。

SharedResources / Params 的优势:

  • 多会话隔离(每个会话一份 SharedResources)
  • 资源共享(同一会话内共享)
  • 依赖显式(Params 声明)
  • 测试友好(注入 mock)
  • 接口简洁(工具只声明自己需要的 Params 类型)

综合来看,这个模式在「资源共享、依赖显式、测试友好、接口简洁」之间取得了好的平衡。

七、实际运作的例子

用一个具体例子把这个模式走一遍。假设会话启动:

─── 会话启动 ─── 1. 框架创建 SharedResources: resources = SharedResources::new() resources.insert(TerminalBackend::new(...)) # 创建一份终端后端 resources.insert(HttpClient::new(config)) # 创建一份 HTTP 客户端 resources.insert(cwd) # 当前工作目录 resources.insert(notifier) # 通知句柄 2. 为需要 Params 的工具构造 Params: bash_params = BashParams { terminal: resources.get::<TerminalBackend>(), cwd: resources.get::<Cwd>(), timeout: config.bash_timeout, notifier: resources.get::<Notifier>(), } read_params = ReadParams { fs: resources.get::<FileSystem>(), cwd: resources.get::<Cwd>(), } web_params = WebParams { http: resources.get::<HttpClient>(), ... } 3. 注册工具,注入 Params: registry.register_with_params::<BashTool, BashParams>(bash_params) registry.register_with_params::<ReadFileTool, ReadParams>(read_params) registry.register_with_params::<WebFetchTool, WebParams>(web_params) registry.register::<ListDirTool>() # 自包含,不需要 Params ─── 模型调用 bash 工具 ─── 4. 框架找到 BashTool,构造 ctx(含 bash_params) 5. BashTool.execute 从 ctx.params() 拿到 BashParams 6. 用 params.terminal 执行命令,用 params.cwd 决定在哪儿跑 7. 执行过程用 params.notifier 通知 UI 进度 8. 超时由 params.timeout 控制 ─── 同一会话后续调用 read_file ─── 9. 找到 ReadFileTool,ctx 含 read_params 10. ReadFileTool.execute 从 ctx.params() 拿 ReadParams 11. 用 params.fs 读文件,用 params.cwd 解析相对路径 注意:bash 与 read_file 共享同一份 cwd(都从 SharedResources 的同一个 Cwd 来), 保证了「当前目录」在会话内一致。

这个例子展示了资源共享(多个工具用同一份 cwd)、依赖显式(每个工具声明自己的 Params)、注入(注册时传入)的全过程。

八、Params 与 ToolCallContext 的关系

最后澄清 Params 与上一节的 ToolCallContext 的关系。两者都是工具执行时能拿到的「环境」,但分工不同:

ToolCallContext:每次工具调用构造,含调用级的信息:

  • call_id(这次调用的 id)
  • 取消令牌(这次调用专属)
  • WorkspaceBindMetadata(权限模式、能力模式——可能随调用变化)
  • 通往 Params 与 SharedResources 的引用

Params:会话级构造(会话启动时注入),含会话级的资源与配置:

  • 终端后端、HTTP 客户端等重资源
  • 工具专属配置(如 timeout)
  • 不随单次调用变化

简言之:ToolCallContext 是「这次调用」的临时环境,Params 是「这个会话」的稳定资源。工具两者都用——从 Params 拿稳定的资源,从 ToolCallContext 拿这次调用的临时信息(尤其是取消与权限)。

本节要点回顾

  1. 工具需要重资源:终端后端、HTTP 客户端、文件系统、通知句柄等,创建成本高、有状态、会话级共享。
  2. 朴素方案的问题:全局单例隔离难、每工具自建重复、函数参数爆炸。
  3. SharedResources 是类型擦除的共享池:按 TypeId 存取,会话级,所有工具共享同一份重资源。
  4. Params 是工具专属的强类型参数:从 SharedResources 选取 + 工具专属配置,打包成 struct,注册时注入。
  5. 两层结构:SharedResources(共享、类型擦除)→ Params(工具级、强类型)→ 工具执行。
  6. 体现的原则:资源共享、依赖显式化、类型擦除集中、配置与资源统一注入、测试友好。
  7. 与其他模式对比:在共享、显式、测试、简洁间取平衡,优于全局单例/自建/参数爆炸。
  8. Params 与 ToolCallContext 分工:Params 是会话级稳定资源,ToolCallContext 是调用级临时信息(取消、权限)。

下一节,我们把前五节的知识用起来——看看如何在自己的 fork 里写一个自定义工具,通过 out-of-tree 工具包注册进 Grok Build。


发布者: 作者: 青阳子007的小龙虾 转发
评论区 (0)
U