跳到主要内容

数据截至 (上游 commit 38277815ed44)

工具接地层:统一后端、工具预选与质量监控

30 秒导读: 技能层(第 2 章)告诉 agent「该怎么做」,但真正 去敲命令、点鼠标、查数据库、翻网页的「手脚」在这一层。本章讲 GroundingClient 如何把五种 完全不同的后端(shell / gui / mcp / web / meta)统一成一套工具接口,如何用「工具预选」从 成百上千个工具里选出跟当前任务相关的一小撮,如何执行一次调用,以及执行完怎么给工具打分—— 而工具质量退化时,又会经 QUALITY_SIGNAL 反向触发第 3 章的技能进化。

上游重构提示: 旧名"工具 RAG / auto search"已改叫预选(preselection): get_tools_with_auto_searchget_tools_with_auto_preselection(grounding_client.py:720), SearchCoordinatorToolPreselector(search_tools.py:567);「只筛 MCP 工具」改为按 tool.is_deferred 契约分离即时/延迟工具(注释 DEC-005,search_tools.py:698-699); 旧 system 元后端改名 meta(grounding/core/meta/)。机制骨架不变,本章按新版源码重锚。


1. 这是什么(零基础也能懂)

一句话定义: 接地层(grounding layer)是 agent 的「手脚」——把模型嘴上说的「我要读这个文件 / 点这个按钮 / 搜这个关键词」精确落到某个真实后端上并执行。

先建立一个直觉。一个 agent 大脑(LLM)只会输出文字,它本身既不能动文件、也不能点鼠标。要让它 真的干活,得给它一批工具(bash、read、gui_agent……),再有人负责:

  • 把这些工具收集起来、统一成一种格式;
  • 任务来了,从这一大堆工具里挑出相关的给模型看(工具太多会撑爆上下文);
  • 模型选了某个工具后,执行它,拿回结果;
  • 顺手记下这个工具好不好用(成没成功、快不快)。

这四件事,就是接地层干的活。

它为什么难。 难点从来不是「调用模型」,而是「把模型说的那句话,精确、可靠地落到一个真实目标 上」。目标不同,分出几类完全不同的「手脚」:

后端(BackendType)手脚落到哪典型工具底层是什么
shell一台机器的命令行 / 文件系统basheditweb_search/web_fetch(生产力工具)subprocess 或远程 HTTP 执行
gui整块屏幕、桌面应用gui_agentAnthropic Computer Use(截图 + 视觉定位)
mcp任意外部 MCP 服务器由服务器动态提供stdio / HTTP / WebSocket 协议
web互联网WebSearchWebFetch 两个扁平工具内置搜索/抓取;深度调研由 deep-researcher 子代理组合这两个工具完成
meta自省list_providers查询自己有哪些能力

BackendType 枚举定义于 grounding/core/types.py:35

用起来什么样。 上层不需要关心这些差异,只跟一个门面对象 GroundingClient 打交道:

# 示意,非源码:一次工具预选的最小样子
gc = GroundingClient() # 读配置,注册好各类后端
tools = await gc.get_tools_with_auto_preselection( # 工具太多就自动预选,只留相关的
task_description="把 data.csv 转成柱状图",
backend=BackendType.MCP,
)
result = await gc.invoke_tool("run_query", {"q": "..."})

一句话直觉:GroundingClient 想成一家公司的总机 + 前台——你不用知道每个部门(后端) 坐在哪、说什么方言,总机负责接通;工具太多时前台还会先帮你筛一遍「你这事该找哪几个人」。

本节不出现底层细节。记住一件事:接地层把「异构的手脚」统一成「一套工具接口」,并在预选、执行、 打分三处做文章。


2. 顶层全景(它大概怎么转)

本节讲清楚一个工具调用「从预选、到执行、到被打分」的完整路径——这是全章的主线。

2.1 部件与职责

部件干什么在哪个文件
GroundingClient面向上层的唯一门面;管 Provider、Session、工具缓存grounding/core/grounding_client.py:21
ProviderRegistryBackendType → Provider 的注册表grounding/core/provider.py:131
Provider一个后端的「部门经理」,管该后端的所有 Sessiongrounding/core/provider.py:18
BaseSession一次具体连接(一个 shell 进程 / 一个 MCP 服务器)grounding/core/session.py
BaseTool / LocalTool / RemoteTool工具抽象;执行后自动打分grounding/core/tool/base.py:106
ToolPreselector / ToolRanker工具预选:从候选工具里检索、排序grounding/core/search_tools.py:567 / :44
ToolSearchTool模型可见的 tool_search 工具(回合中按需发现延迟工具)grounding/core/tool_discovery.py:18
ToolQualityManager记录每次执行、算惩罚分grounding/core/quality/manager.py:27
SecurityPolicyManager / SandboxManager危险命令拦截、沙箱隔离grounding/core/security/policies.py:21sandbox.py:35

2.2 主线走一遍(高层,不进代码)

怎么读下面这张图:从上到下是一次工具调用的生命周期,左边是数据流经的部件,命中即往下走。

上层(回合循环 tool_turn_controller,见第 1 章)
│ task_description

┌───────────────────────────────────────────────┐
│ GroundingClient (门面) │
│ │
│ ① list_tools ── 从各 Provider 收集全部工具 │
│ │ │
│ ▼ │
│ ② get_tools_with_auto_preselection │
│ │ 工具数 > max_tools(默认 30)? │
│ ├── 否 ─► 全给 │
│ └── 是 ─► preselect_tools │
│ │ │
│ ▼ │
│ ToolPreselector ──► ToolRanker │
│ (按 is_deferred 分流;LLM 预筛 + │
│ BM25/向量/混合排序) │
│ ┌───────────◄──── 相关的一小撮工具 │
│ ▼ │
│ ③ invoke_tool ── 解析 backend/session │
│ │ │
│ ▼ │
│ Provider.call_tool ─► Session ─► 真实后端 │
│ │ (shell 命令 / 截图点击 / MCP RPC / 搜索) │
│ ▼ │
│ ④ BaseTool._arun 执行完 │
│ └─► ToolQualityManager.record_execution │
│ (成功率、耗时、惩罚分) │
└───────────────────────────────────────────────┘
│ 质量退化时(证据 → QUALITY_SIGNAL 作业)

第 3 章:技能进化(EvolutionEngine.process_job)

四步对应四节核心机制:统一后端(§3.1)、工具预选(§3.2)、一次调用(§3.3)、 质量与进化(§3.4),外加贯穿始终的安全沙箱(§3.5)。


3. 核心原理(逐个机制,由浅入深)

3.1 统一后端:一套接口,五类手脚

要解决的小问题: shell 是敲命令、gui 是点屏幕、mcp 是 RPC、web 是搜索、meta 是自省—— 五套完全不同的协议,怎么让上层用同一种方式调用?

思路: 经典的三层抽象。Provider(管一个后端)→ Session(一次连接)→ Tool(一个动作)。 GroundingClient 只跟 ProviderSession 打交道,不碰底层协议。

注册从配置来。 启动时 _register_providers_from_config(grounding_client.py:80)读 enabled_backends,按 provider_cls 字符串动态 import 出 Provider 类并实例化。注意此处 只实例化、不 initialize()——避免在 import 阶段阻塞事件循环,Provider 首次被用到时才懒初始化。 meta 后端是例外,因为它需要 GroundingClient 自身引用,单独在 _register_meta_provider (grounding_client.py:123)里注册。

Session 按需创建。 create_session(grounding_client.py:228)是统一入口:非 MCP 后端天然 单 session(名字就是后端名);MCP 每个 server 一个 session(名字 mcp-<server>);已存在直接 复用,真正的创建委托给 provider.create_session(...)

各后端各自怎么落地:

  • shell——ShellSession(backends/shell/session.py:148)注册 BashTool (backends/shell/session.py:226,工具名 bash);文件编辑 editbackends/shell/file_tools.py:1387;另有一组生产力工具(web_search / web_fetch / create_file / read 等,backends/shell/productivity_tools.py)。本地走 subprocess (backends/shell/transport/local_connector.py),远程走 HTTP。
  • gui——GUIAgentTool(backends/gui/tool.py:29)是视觉 GUI agent:给它一句自然语言, 它截屏 → 交给 AnthropicGUIClient(backends/gui/anthropic_client.py:43,Claude + computer-use API)→ 输出动作 → 连接器执行,循环若干步。
  • mcp——MCPProvider(backends/mcp/provider.py:23)每个 server 一个 session;工具由服务器 动态返回,经 convert_mcp_tool_to_base_tool(backends/mcp/tool_converter.py:149)包成 RemoteTool。传输层支持三种协议(见 §3.5 表)。
  • web——WebSession(backends/web/session.py:73)只注册 WebSearchWebFetch 两个 扁平内置工具;模块 docstring 明说旧的 Perplexity deep_research_agent 路径已移除, 深度调研改由 deep-researcher 子代理组合这两个工具完成(backends/web/session.py:1-9)。
  • meta——MetaProvider(grounding/core/meta/provider.py:14)提供 list_providerslist_backend_tools自省工具(ListProvidersTool,meta/tool.py:28),让 agent 能查询 自己有哪些能力,始终可用。(旧名 system 后端已改名 meta。)

3.2 工具预选:从几百个工具里挑出相关的几个

要解决的小问题: 接上一堆 MCP 服务器后,工具可能几百上千个。全塞进 prompt 会撑爆上下文、也让 模型选择困难。得先检索——像 RAG 检索文档那样检索工具。

注意:这跟第 2 章技能发现是两回事。技能发现找的是「SKILL.md 该怎么做」,这里检索的是「有哪些可执行的工具」。两者互补,不重复。

阈值判断。 入口 get_tools_with_auto_preselection(grounding_client.py:720)先做一个便宜判断 (docstring 写明逻辑,grounding_client.py:727-730):

# 示意,非源码:要不要预选?
tools_count = len(all_tools)
need_preselection = tools_count > max_tools and task_description is not None
if need_preselection:
return await self.preselect_tools(...) # 触发预选
else:
return all_tools # 工具不多,全给

工具数不超过 max_tools(默认取 config 的 tool_search.max_tools,默认 30)就全给, 超了才进 preselect_tools(grounding_client.py:637)→ ToolPreselector._arun

关键设计:按 is_deferred 契约分流,而不是写死 MCP。 _arun 把候选按 tool.is_deferred(tool/base.py:223 的属性)一分为二——延迟加载的工具(典型是 MCP 大目录) 才参与筛选,即时工具永远保留。注释点明这是刻意决策:

# Split by the single runtime contract: tool.is_deferred. # DEC-005: defer decisions are tool-level, not hard-coded to MCP. (search_tools.py:698-699)

ToolPreselector 走两条路,看延迟工具多不多:

延迟工具数

├── ≤ max_tools ─────────────► 直接全返回

└── > max_tools

├── > llm_filter_threshold(默认 50)──► 路径 1:LLM 预筛
│ _llm_filter_with_planning(search_tools.py:969):
│ 先让 LLM 读"每个 server 有哪些工具",产出计划,
│ 把工具分成"精确要的辅助工具"和"整个领域 server"

└── ≤ threshold ───────────────────► 路径 2:查询增强
_generate_search_query(search_tools.py:1119):
让 LLM 把任务扩成关键词,拼到原 query 后面再排序

ToolRanker:三种排序算法(search_tools.py:44,分发入口 rank,search_tools.py:202):

模式方法原理何时好用
keyword_keyword_search(:228)BM25(rank_bm25),缺库时退化成词重叠率任务里有明确关键词
semantic_semantic_search(:358)句向量余弦相似度语义相近但用词不同
hybrid_hybrid_search(:418)先 BM25 取 top,再在其上做语义排序兼顾精确与语义

向量嵌入可远可近、缓存可落盘。 ToolRanker 把嵌入缓存,并可持久化: _load_persistent_cache(:133)/ _save_persistent_cache(:174),带 CACHE_VERSION(:49) 失效控制,文件名带模型名支持多模型共存。

回合中还有"延迟发现"。 预选决定开局给哪些工具;回合中途模型可用 tool_search (grounding/core/tool_discovery.py:18ToolSearchTool)按需发现被延迟的 schema—— 这正是 ToolPreselector docstring 强调的分工("system-side preselection, not the model-facing tool_search discovery tool",search_tools.py:570-573)。

3.3 一次工具调用:从名字到结果

要解决的小问题: 上层可能拿到的是一个 BaseTool 实例,也可能只是一个工具名字符串,还可能 一个名字对应多个后端的同名工具。怎么把这些都统一执行?

思路: invoke_tool(grounding_client.py:1096)是「万能调用」,核心是先 解析出 (backend, session, server) 三元组(_resolve_tool_invocation,:833), 再委托给 Provider.call_tool(provider.py:100)→ session.call_tool → 真实后端。

几个要点:

  • 运行时信息绑定。 list_tools 拉回工具时会调 bind_runtime_info(tool/base.py:452)把 backend/session/server/grounding_client 焊到工具实例上,之后 tool.invoke() 就能自调用, 无需再传后端。
  • 调用前保证 session 在(_ensure_invocation_session,:934)。
  • 执行完自动打分——这是本层的暗线。 每个工具的执行在返回前调 _auto_record_execution(tool/base.py:548)→ 全局 ToolQualityManager.record_execution。 也就是说,只要工具跑过,质量记录就自动累积,上层无感知。

3.4 工具质量:惩罚分排序 + 反哺进化

要解决的小问题: 工具会退化——某个 MCP 服务器挂了、某个命令老失败。系统得自己发现并 ①在预选时给坏工具降权,②把「工具坏了」这件事反馈给依赖它的技能。

数据模型。 每个工具一条 ToolQualityRecord(quality/types.py:20),key 是 backend:server:tool_name(manager.py:72 get_tool_key),记录总调用数、成功数、耗时, 以及一个滚动窗口 recent_executions

惩罚分,不是奖励分。 核心是 penalty 属性(quality/types.py:84),设计克制:

# 示意,非源码:penalty 的核心逻辑
if self.total_calls < 3: # 新工具,不罚,给公平机会
return 1.0
success_rate = self.recent_success_rate
if success_rate >= PENALTY_THRESHOLD: # 默认 0.4,够好就不罚
return 1.0
penalty = 0.3 + (success_rate / 0.4) * 0.7 # 线性映射
if self.consecutive_failures >= 3: # 连续失败额外重罚
penalty -= min(0.3, (consec - 2) * 0.1)
return max(0.2, min(1.0, penalty)) # 夹到 [0.2, 1.0]

只有最近成功率低于 40% 才罚,新工具(<3 次)豁免,连续失败追加惩罚。预选时 adjust_ranking(manager.py:244)把排序分按惩罚系数调整——坏工具自动沉底。

记录一次执行。 record_execution(manager.py:192)把成败、耗时、错误信息塞进 record并落盘 (QualityStore,quality/store.py:59,与 SkillStore 共用同一个 SQLite 库 .openspace/openspace.db,见 quality/store.py:63-71)。

工具退化如何反噬技能(回连第 3 章)。 旧版在 tool_layer._maybe_evolve_quality 里把 问题工具交给 process_tool_degradation;新版改为证据 → 作业:maybe_evolve_quality (runtime/app.py:2319)按全局执行计数节流,_create_quality_signal_trigger_jobs (runtime/app.py:2330)把工具失败/语义问题(evidence/tool_adapter.py 里的 tool_failure_affects_skilltool_semantic_issue)变成 QUALITY_SIGNAL TriggerJob, 交由同一条进化流水线处理(映射表 triggers/policies.py:192-200)。依赖坏工具的技能会被 排队修复——这条回连的完整机制见第 3 章

3.5 安全与沙箱:执行前的闸门

要解决的小问题: agent 会执行任意命令、访问任意域名。得在真正落地前拦一道。

危险命令拦截。 SecurityPolicy(core/types.py:109)维护 blocked_commands 黑名单; find_dangerous_tokens(types.py:200)做 token 级匹配找出命中的词; SecurityPolicyManager.check_command_allowed(security/policies.py:76)在命中黑名单时 弹交互确认,用户点头才放行。这道闸门装在执行路径上——shell 本地连接器执行代码前先调它 (backends/shell/transport/local_connector.py:319)。

更细的权限系统(新增)。 grounding/core/permissions/ 现在有一整套规则引擎: engine.py 的权限决策、bash_permissions.py 的 bash 命令规则(含前缀/路径校验)、 filesystem.py 的文件系统边界、loader.py 的规则加载。回合循环里的每次工具调用都过 ToolUseContext 的权限上下文(第 1 章),Skill 工具还有技能粒度的 allow/ask/deny 规则 (第 2 章 protocol.py:913-942)。

沙箱隔离。 SandboxManager(security/sandbox.py:35)按后端管理沙箱;BaseSandbox (:7)定义 start/stop/execute_safe/get_connector 抽象。具体实现是 E2BSandbox (security/e2b_sandbox.py:40),用 E2B 云沙箱跑不可信代码。

MCP 传输三选一(backends/mcp/config.py:create_connector_from_config :29)——按 server 配置 自动挑连接器:

配置里有连接器场景
command(+ 非 sandbox)StdioConnector本地进程,stdin/stdout 通信
command + sandbox=TrueSandboxConnector(E2B)不可信 server 隔离运行
url / ws_urlHttpConnector / WebSocketConnector远程 HTTP / SSE / 长连接 server

4. 巧妙之处(可借鉴的技术)

  • 懒初始化 + 动态导入。 Provider 在 import 阶段只实例化不连接(grounding_client.py:80),首次 用到才 initialize()——避免启动就阻塞事件循环,也让「装了但没用」的后端零开销。
  • deferred 是工具级契约,不是后端写死。is_deferred 分流(search_tools.py:698), 任何后端的工具都能延迟;即时工具全留、延迟工具才筛,既省算力又保证刚需在手边。
  • 开局预选 + 回合内 tool_search 两级。 开局用 ToolPreselector 决定活跃集,回合中模型可用 tool_search 按需捞延迟 schema(search_tools.py:570-573 的分工注释)——大工具集不再 「要么全给要么漏给」。
  • 先规划再检索。 路径 1 用一次 LLM 调用产出「计划 + server 分类」再检索(:969), 把上千工具的搜索空间先粗筛掉大半,比直接对全量做向量检索更准更省。
  • 惩罚而非奖励 + 新工具豁免。 质量排序只罚「确实烂」的工具(成功率 <40% 且调用 ≥3 次, types.py:84),避免冷启动误伤,克制且稳。
  • 质量库与技能库同一个 DB。 quality/store.py 和 SkillStore 共用 openspace.db, 让「工具健康度 → 技能进化」的回连几乎零成本(quality/store.py:63-71)。

5. 边界与局限(诚实)

  • 单机进程内状态。 Session、工具缓存、质量记录都在 GroundingClient 实例的内存里(落盘的只有 嵌入缓存和质量 DB),多进程/分布式部署下这些状态不自动共享。
  • 黑名单式安全 + 规则权限并存,仍非白名单沙箱。 SecurityPolicyblocked_commands 黑名单 + token 匹配;新增的 permissions/ 规则引擎更细,但没进 E2B 的本地 shell 仍受限于 规则覆盖面,最终还依赖人点确认。
  • 凭据外泄无专门检查。 代码里的安全检查集中在命令黑名单、路径校验和权限规则;从源码看 没有独立的「凭据/密钥外泄扫描」模块(inferred,基于对 security/permissions/ 目录的通读)。
  • 质量信号有滞后。 penalty 要 ≥3 次调用才生效、maybe_evolve_quality 按执行数节流 (runtime/app.py:2319),所以一个刚坏的工具需要几次失败后才会被降权和触发技能修复。
  • 检索质量吃 embedding 可用性。 语义/混合检索依赖远程 embedding API 或本地模型; 都不可用时 _hybrid_search 会退化为纯关键词(search_tools.py:418)。

6. 横向对比

  • 与本项目其它章。 本章是「手脚」;第 1 章是驱动手脚的回合循环; 第 2 章发现的是「怎么做」的技能,和本章「有哪些工具」的预选互补; 第 3 章是本章质量信号(QUALITY_SIGNAL)反噬后的技能进化终点; 第 5 章讲把这套能力作为 MCP 服务对外暴露。
  • 取舍。 相比只接一种后端的 agent 框架,OpenSpace 用 Provider/Session/Tool 三层抽象换来 「五类手脚统一 + 可插拔 MCP」,代价是启动/配置更重;工具预选则是它区别于「工具全塞 prompt」 做法的关键工程投入。

7. 代码地图(导航索引)

主题文件路径关键符号
门面 / 全局入口grounding/core/grounding_client.pyGroundingClient_register_providers_from_configcreate_sessioninvoke_tool
智能取工具grounding/core/grounding_client.pyget_tools_with_auto_preselectionpreselect_toolslist_tools
质量管理接线grounding/core/grounding_client.py / runtime/app.py_init_quality_managermaybe_evolve_quality_create_quality_signal_trigger_jobs
后端抽象grounding/core/provider.pyProviderProviderRegistrycall_tool
工具基类 / 自动打分grounding/core/tool/base.pyBaseToolbind_runtime_info_auto_record_executionis_deferred
本地/远程工具grounding/core/tool/local_tool.pyremote_tool.pyLocalToolRemoteTool
工具预选排序grounding/core/search_tools.pyToolRanker_keyword_search_semantic_search_hybrid_search
预选协调器grounding/core/search_tools.pyToolPreselector_llm_filter_with_planning_generate_search_query
deferred 分流(DEC-005)grounding/core/search_tools.py:698(tool.is_deferred 分流注释)
回合内工具发现grounding/core/tool_discovery.pyToolSearchTool
嵌入持久化缓存grounding/core/search_tools.py_load_persistent_cache_save_persistent_cacheCACHE_VERSION
质量管理grounding/core/quality/manager.pyToolQualityManagerrecord_executionadjust_rankingget_problematic_tools
质量数据模型grounding/core/quality/types.pyToolQualityRecordpenalty
质量落盘grounding/core/quality/store.pyQualityStore(共用 openspace.db)
安全策略grounding/core/types.py / security/policies.pySecurityPolicyfind_dangerous_tokensSecurityPolicyManager.check_command_allowed
权限规则引擎grounding/core/permissions/(engine / bash_permissions / filesystem / loader)
沙箱grounding/core/security/sandbox.py / e2b_sandbox.pySandboxManagerBaseSandboxE2BSandbox
shell 后端grounding/backends/shell/ShellProviderShellSessionBashTool、edit 文件工具
gui 后端grounding/backends/gui/GUIAgentToolAnthropicGUIClient
mcp 后端grounding/backends/mcp/MCPProvidercreate_connector_from_configconvert_mcp_tool_to_base_tool
web 后端grounding/backends/web/WebSession(WebSearch / WebFetch 扁平工具)
meta 元后端grounding/core/meta/MetaProviderListProvidersTool