跳到主要内容

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

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


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

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

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

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

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

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

后端(BackendType)手脚落到哪典型工具底层是什么
shell一台机器的命令行 / 文件系统run_shellread_fileshell_agentsubprocess 或远程 HTTP 执行
gui整块屏幕、桌面应用gui_agentAnthropic Computer Use(截图 + 视觉定位)
mcp任意外部 MCP 服务器由服务器动态提供stdio / HTTP / WebSocket 协议
web互联网deep_research_agent联网搜索 + 深度调研 API

BackendType 枚举定义于 grounding/core/types.py:15(MCP/SHELL/WEB/GUI/SYSTEM/NOT_SET)。

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

# 示意,非源码:一次工具调用的最小样子
gc = GroundingClient() # 读配置,注册好四种后端
tools = await gc.get_tools_with_auto_search( # 工具太多就自动检索,只留相关的
task_description="把 data.csv 转成柱状图",
backend=BackendType.SHELL,
)
result = await gc.invoke_tool("run_shell", {"command": "python plot.py"})

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

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


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

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

2.1 部件与职责

部件干什么在哪个文件
GroundingClient面向上层的唯一门面;管 Provider、Session、工具缓存grounding/core/grounding_client.py:19
ProviderRegistryBackendType → Provider 的注册表grounding/core/provider.py:131
Provider一个后端的「部门经理」,管该后端的所有 Sessiongrounding/core/provider.py:18
BaseSession一次具体连接(一个 shell 进程 / 一个 MCP 服务器)grounding/core/session.py
BaseTool所有工具的抽象;执行后自动打分grounding/core/tool/base.py:41
SearchCoordinator / ToolRanker工具 RAG:从候选工具里检索、排序grounding/core/search_tools.py:553/:31
ToolQualityManager记录每次执行、算惩罚分、驱动进化grounding/core/quality/manager.py:30
SecurityPolicyManager / SandboxManager危险命令拦截、沙箱隔离grounding/core/security/policies.py:21/sandbox.py:35

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

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

上层(ToolLayer / Grounding Agent,见第 1 章)
│ task_description

┌───────────────────────────────────────────────┐
│ GroundingClient (门面) │
│ │
│ ① list_tools ── 从各 Provider 收集全部工具 │
│ │ │
│ ▼ │
│ ② get_tools_with_auto_search │
│ │ 工具数 > 阈值? │
│ ├── 否 ─► 全给 │
│ └── 是 ─► search_tools │
│ │ │
│ ▼ │
│ SearchCoordinator ──► ToolRanker │
│ (LLM 预筛 + BM25/向量/混合排序) │
│ ┌───────────◄──── 相关的一小撮工具 │
│ ▼ │
│ ③ invoke_tool ── 定位 backend/session │
│ │ │
│ ▼ │
│ Provider.call_tool ─► Session ─► 真实后端 │
│ │ (shell 命令 / 截图点击 / MCP RPC / 搜索) │
│ ▼ │
│ ④ BaseTool.arun 执行完 │
│ └─► ToolQualityManager.record_execution │
│ (成功率、耗时、惩罚分) │
└───────────────────────────────────────────────┘
│ 质量退化时

第 3 章:技能进化(process_tool_degradation)

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


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

3.1 统一后端:一套接口,四种手脚

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

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

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

Session 按需创建。 create_session(grounding_client.py:226)是统一入口:

# 示意,非源码:命名与复用策略
if server: # 只有 MCP 会传 server
name = name or f"{backend}-{server}" # 如 "mcp-filesystem"
else:
name = name or backend.value # shell/web/gui 固定一个 session
if name in self._sessions:
return name # 已存在直接复用,不重开
  • 非 MCP 后端(shell/web/gui)天然单 session,名字就是后端名;
  • MCP 每个 server 一个 session,名字是 mcp-<server>;
  • 真正的创建委托给 provider.create_session(sess_cfg)(:281),各后端自己实现。

四个后端各自怎么落地:

  • shell——RunShellTool(backends/shell/session.py:263)把命令原样交给 connector 执行; ShellAgentTool(:313)则内嵌一个小 agent,自己决定用 Python 还是 Bash、出错自动重试。 本地走 subprocess(backends/shell/transport/local_connector.py),远程走 HTTP。
  • gui——GUIAgentTool(backends/gui/tool.py:12)是视觉 GUI agent:给它一句自然语言, 它截屏 → 交给 AnthropicGUIClient(backends/gui/anthropic_client.py:46,用 Claude Sonnet 4.5 + 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:79)只暴露一个 deep_research_agent (:146),背后是联网深度调研 API。

system 后端(core/system/provider.py:9)是个元后端,提供 list_providerslist_backend_tools自省工具,让 agent 能查询自己有哪些能力,始终可用。

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

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

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

三层漏斗。 入口 get_tools_with_auto_search(grounding_client.py:632)先做一个便宜的判断:

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

工具数不超过 max_tools(默认 30)就全给,超了才进 search_tools(:549)→ SearchCoordinator._arun(search_tools.py:649)。

关键设计:MCP 工具才参与筛选,非 MCP 工具永远保留。 shell/gui/web 的工具是「基础设施」,数量少 且几乎总要用,所以 _arun 先把候选按后端一分为二,只对 MCP 工具做检索(search_tools.py:673-698)。

SearchCoordinator 走两条路(search_tools.py:707-801),看 MCP 工具多不多:

MCP 工具数

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

└── > max_tools

├── > llm_filter_threshold(默认 50) ──► 路径 1:LLM 预筛
│ _llm_filter_with_planning:先让 LLM 读"每个 server 有哪些工具",
│ 产出 {brief_plan, utility_tools, domain_servers},
│ 把工具分成"精确要的辅助工具"和"整个领域 server",再对领域工具排序

└── ≤ threshold ──────────────────────► 路径 2:计划增强检索
_generate_search_query:让 LLM 把任务扩成关键词,
拼到原 query 后面,再交给 ToolRanker 排序
  • 路径 1(大工具集):_llm_filter_with_planning(search_tools.py:864)按 server 分组、把每个 server 的工具名和示例能力喂给 LLM,让它输出一个 JSON 计划,粗筛掉整批不相关的 server。这是「先 规划、再检索」——用一次 LLM 调用把搜索空间砍小。
  • 路径 2(小工具集):_generate_search_query(search_tools.py:1001)让 LLM 把任务翻译成 能力关键词,拼接成增强 query,提升向量检索命中率。

ToolRanker:三种排序算法(search_tools.py:31)。最终排序落到 rank(:194)分发:

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

向量嵌入可远可近。 _ensure_model(:256)优先用环境变量里配置的远程 embedding API (_init_remote_embedding :265),否则退回本地 fastembed(_init_local_embedding :289)。

持久化缓存是省钱关键。 算一次嵌入不便宜,ToolRanker 把嵌入按 {backend: {server: {tool}}} 结构缓存,并可落盘:_load_persistent_cache(:120)启动时读 pickle,_save_persistent_cache (:161)有新嵌入时写回。缓存带 CACHE_VERSION(:36)做失效控制,文件名带模型名以支持多模型共存。 _semantic_search 里只对缺失嵌入的工具重新计算(:355),命中缓存的直接复用。

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

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

思路: invoke_tool(grounding_client.py:745)是「万能调用」,支持四种调用姿势,核心是先 解析出 (backend, session, server) 三元组,再委托给 Provider。

# 示意,非源码:invoke_tool 的解析逻辑骨架
if isinstance(tool, BaseTool) and tool.is_bound:
backend, session, server = tool.runtime_info(...) # 用绑定的运行时信息
elif isinstance(tool, str) and not (backend or session):
matching = [t for t in await self.list_tools() if t.name == tool]
if len(matching) > 1:
raise GroundingError("Multiple tools ... specify backend") # 歧义要报错
backend, session, server = matching[0].runtime_info(...)
# 确保 session 存在(SYSTEM 后端例外,它不用 session)
if backend != BackendType.SYSTEM and session not in self._sessions:
session = await self.ensure_session(backend, server)
result = await provider.call_tool(session, tool_name, params)

几个要点:

  • 运行时信息绑定。 list_tools 拉回工具时会调 bind_runtime_info(tool/base.py:163)把 backend/session/server/grounding_client 焊到工具实例上,之后 tool.invoke() 就能自调用, 无需再传后端。
  • 同名歧义显式报错(:856-865):两个后端都有 read_file 时,不猜,要求上层指定。
  • 自动开关 session(:876-899):调用前 ensure_session 保证连接在;keep_session=False 时用完即关(SYSTEM 后端跳过)。
  • 执行落到 Provider.call_tool(provider.py:100)→ session.call_tool → 真实后端。

执行完自动打分——这是本层的暗线。 每个工具的 arun(tool/base.py:140)在返回前调 _auto_record_execution(:235)→ _record_to_quality_manager(:286)→ 全局 ToolQualityManager.record_execution。也就是说,只要工具跑过,质量记录就自动累积,上层无感知。

3.4 工具质量与自进化:好用的往前排,坏掉的反噬技能

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

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

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

# 示意,非源码: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:481)把 语义分 × penalty 作为最终分重排——坏工具自动沉底。 这个 adjust_ranking 就是 §3.2 里 SearchCoordinator 末尾调的那步(search_tools.py:804-810)。

记录一次执行。 record_execution(manager.py:259)把成败、耗时、错误信息塞进 record,并 自增全局计数、自动落盘。

描述质量也能被 LLM 评。 evaluate_description(manager.py:294)让 LLM 从「清晰度 / 完整度」 两维给工具文档打 0-1 分。描述变了(靠 _compute_description_hash 检测)或成功率骤降才重评。

LLM 语义失败也进同一条管道。 规则打分只能看「有没有报错」,但 HTTP 200 却返回错数据这种 语义失败它看不出。record_llm_tool_issues(manager.py:185)接收分析 LLM 标记的问题,经 add_llm_issue(types.py:150)注入成一条 success=False 的执行记录——于是 LLM 的定性判断和 规则统计汇入同一个 recent_success_rate → penalty 管道,统一驱动排序。

自进化节律。 should_evolve(manager.py:847)按全局执行数每隔 evolve_interval(默认 5) 触发一次;evolve(manager.py:791)一个周期做四件事:检测工具变化(check_changes :509)、 重评需要重评的描述、算自适应质量权重、产出建议。

关键:工具退化如何反噬技能(回连第 3 章)。 这是本层和进化引擎的接缝,发生在 tool_layer.py:_maybe_evolve_quality(tool_layer.py:856):

# 示意,非源码:tool_layer.py 里的接缝
if quality_mgr.should_evolve():
report = await self._grounding_client.evolve_quality() # grounding_client.py:173
problematic = quality_mgr.get_problematic_tools() # 成功率过低的工具
if problematic:
self._skill_evolver.schedule_background( # 后台
self._skill_evolver.process_tool_degradation(problematic),
)

GroundingClient.evolve_quality(grounding_client.py:173)跑质量进化周期;若发现 get_problematic_tools(manager.py:609)有工具烂掉,就把它们交给 _skill_evolver.process_tool_degradation ——依赖坏工具的技能会被排队修复(第 3 章 的 FIX 进化)。质量记录还与 SkillStore 共用同一个 SQLite 库(.openspace/openspace.db,见 quality/store.py:66),让工具健康度和技能进化天然打通。

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

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

危险命令拦截。 SecurityPolicy(core/types.py:89)维护 blocked_commands 黑名单; check(:155)用 shlextoken 级匹配(防 rm -rf 加空格绕过), find_dangerous_tokens(:181)找出具体命中的词。SecurityPolicyManager.check_command_allowed (security/policies.py:76)在命中黑名单时弹交互确认(CLI 里 y/n),用户点头才放行。 这道闸门就装在执行路径上——shell 本地连接器执行前先调它(shell/transport/local_connector.py:371)。

沙箱隔离。 SandboxManager(security/sandbox.py:35)按后端管理沙箱;BaseSandbox (:7)定义 start/stop/execute_safe/get_connector 抽象。具体实现是 E2BSandbox (security/e2b_sandbox.py:40),用 E2B 云沙箱跑不可信代码。MCP 的 stdio server 也能选择跑进 沙箱(backends/mcp/config.py:85SandboxConnector)。

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

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

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

  • 懒初始化 + 动态导入。 Provider 在 import 阶段只实例化不连接(grounding_client.py:59),首次 用到才 initialize()——避免启动就阻塞事件循环,也让「装了但没用」的后端零开销。
  • MCP 才筛,基础工具全留。 把工具按后端二分,只对量大的 MCP 工具做 RAG(search_tools.py:673), 既省算力又保证 shell/gui/web 这些「刚需」永远在手边。
  • 先规划再检索。 路径 1 用一次 LLM 调用产出「计划 + server 分类」再检索(:864),把上千工具的 搜索空间先粗筛掉大半,比直接对全量做向量检索更准更省。
  • 惩罚而非奖励 + 新工具豁免。 质量排序只罚「确实烂」的工具(成功率 <40% 且调用 ≥3 次, types.py:108),避免冷启动误伤,克制且稳。
  • 规则分与 LLM 分同管道。 把 LLM 发现的语义失败注入成 success=False 记录(types.py:150), 让两套信号汇成一个 penalty,而不是各算各的,下游排序逻辑无需改动。
  • 质量库与技能库同一个 DB。 quality/store.py 和 SkillStore 共用 openspace.db,让「工具健康度 → 技能进化」的回连几乎零成本(manager.py:62)。

5. 边界与局限(诚实)

  • 单机进程内状态。 Session、工具缓存、质量记录都在 GroundingClient 实例的内存里(落盘的只有 嵌入缓存和质量 DB),多进程/分布式部署下这些状态不自动共享。
  • 黑名单式安全。 SecurityPolicyblocked_commands 黑名单 + token 匹配(types.py:155), 是「列已知坏的」,不是白名单沙箱;没被 E2B 隔离的本地 shell 仍能跑任意未列入黑名单的命令,拦截 最终还依赖人点确认。
  • 凭据外泄无专门检查。 代码里的安全检查集中在命令黑名单和域名白名单;从源码看没有独立的 「凭据/密钥外泄扫描」模块(inferred,基于对 security/ 目录的通读)。
  • 质量信号有滞后。 penalty 要 ≥3 次调用才生效、should_evolve 按执行数节流(manager.py:847), 所以一个刚坏的工具需要几次失败后才会被降权和触发技能修复。
  • 检索质量吃 embedding 可用性。 语义/混合检索依赖 fastembed 或远程 API;两者都不可用时 _hybrid_search 会退化为纯关键词甚至「返回前 N 个」(search_tools.py:414)。

6. 横向对比

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

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

主题文件路径关键符号
门面 / 全局入口grounding/core/grounding_client.pyGroundingClient_register_providers_from_configcreate_sessioninvoke_tool
智能取工具grounding/core/grounding_client.pyget_tools_with_auto_searchsearch_toolslist_tools
质量回连进化grounding/core/grounding_client.py / tool_layer.pyevolve_quality_maybe_evolve_qualityprocess_tool_degradation
后端抽象grounding/core/provider.pyProviderProviderRegistrycall_tool
工具基类 / 自动打分grounding/core/tool/base.pyBaseToolarunbind_runtime_info_auto_record_execution
工具 RAG 排序grounding/core/search_tools.pyToolRanker_keyword_search_semantic_search_hybrid_search
工具 RAG 协调grounding/core/search_tools.pySearchCoordinator_llm_filter_with_planning_generate_search_query
嵌入持久化缓存grounding/core/search_tools.py_load_persistent_cache_save_persistent_cacheCACHE_VERSION
质量管理grounding/core/quality/manager.pyToolQualityManagerrecord_executionevaluate_descriptionevolveshould_evolvecheck_changesget_problematic_tools
质量数据模型grounding/core/quality/types.pyToolQualityRecordpenaltyadd_llm_issue
质量落盘grounding/core/quality/store.pyQualityStore(共用 openspace.db)
安全策略grounding/core/types.py / security/policies.pySecurityPolicy.checkfind_dangerous_tokensSecurityPolicyManager.check_command_allowed
沙箱grounding/core/security/sandbox.py / e2b_sandbox.pySandboxManagerBaseSandboxE2BSandbox
shell 后端grounding/backends/shell/ShellProviderRunShellToolShellAgentTool
gui 后端grounding/backends/gui/GUIProviderGUIAgentToolAnthropicGUIClient
mcp 后端grounding/backends/mcp/MCPProvidercreate_connector_from_configconvert_mcp_tool_to_base_tool
web 后端grounding/backends/web/WebProviderDeepResearchTool
system 元后端grounding/core/system/SystemProviderListProvidersTool