数据截至 (上游 commit 38277815ed44)
工具接地层:统一后端、工具预选与质量监控
30 秒导读: 技能层(第 2 章)告诉 agent「该怎么做」,但真正 去敲命令、点鼠标、查数据库、翻网页的「手脚」在这一层。本章讲
GroundingClient如何把五种 完全不同的后端(shell / gui / mcp / web / meta)统一成一套工具接口,如何用「工具预选」从 成百上千个工具里选出跟当前任务相关的一小撮,如何执行一次调用,以及执行完怎么给工具打分—— 而工具质量退化时,又会经 QUALITY_SIGNAL 反向触发第 3 章的技能进化。
上游重构提示: 旧名"工具 RAG / auto search"已改叫预选(preselection):
get_tools_with_auto_search→get_tools_with_auto_preselection(grounding_client.py:720),SearchCoordinator→ToolPreselector(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 | 一台机器的命令行 / 文件系统 | bash、edit、web_search/web_fetch(生产力工具) | subprocess 或远程 HTTP 执行 |
gui | 整块屏幕、桌面应用 | gui_agent | Anthropic Computer Use(截图 + 视觉定位) |
mcp | 任意外部 MCP 服务器 | 由服务器动态提供 | stdio / HTTP / WebSocket 协议 |
web | 互联网 | WebSearch、WebFetch 两个扁平工具 | 内置搜索/抓取;深度调研由 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 |
ProviderRegistry | BackendType → Provider 的注册表 | grounding/core/provider.py:131 |
Provider | 一个后端的「部门经理」,管该后端的所有 Session | grounding/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:21、sandbox.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 只跟 Provider 和 Session 打交道,不碰底层协议。
注册从配置来。 启动时 _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);文件编辑edit在backends/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-useAPI)→ 输出动作 → 连接器执行,循环若干步。 - 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)只注册WebSearch、WebFetch两个 扁平内置工具;模块 docstring 明说旧的 Perplexitydeep_research_agent路径已移除, 深度调研改由deep-researcher子代理组合这两个工具完成(backends/web/session.py:1-9)。 - meta——
MetaProvider(grounding/core/meta/provider.py:14)提供list_providers、list_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:18 的 ToolSearchTool)按需发现被延迟的 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_skill、tool_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=True | SandboxConnector(E2B) | 不可信 server 隔离运行 |
url / ws_url | HttpConnector / 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),多进程/分布式部署下这些状态不自动共享。 - 黑名单式安全 + 规则权限并存,仍非白名单沙箱。
SecurityPolicy靠blocked_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.py | GroundingClient、_register_providers_from_config、create_session、invoke_tool |
| 智能取工具 | grounding/core/grounding_client.py | get_tools_with_auto_preselection、preselect_tools、list_tools |
| 质量管理接线 | grounding/core/grounding_client.py / runtime/app.py | _init_quality_manager、maybe_evolve_quality、_create_quality_signal_trigger_jobs |
| 后端抽象 | grounding/core/provider.py | Provider、ProviderRegistry、call_tool |
| 工具基类 / 自动打分 | grounding/core/tool/base.py | BaseTool、bind_runtime_info、_auto_record_execution、is_deferred |
| 本地/远程工具 | grounding/core/tool/local_tool.py、remote_tool.py | LocalTool、RemoteTool |
| 工具预选排序 | grounding/core/search_tools.py | ToolRanker、_keyword_search、_semantic_search、_hybrid_search |
| 预选协调器 | grounding/core/search_tools.py | ToolPreselector、_llm_filter_with_planning、_generate_search_query |
| deferred 分流(DEC-005) | grounding/core/search_tools.py:698 | (tool.is_deferred 分流注释) |
| 回合内工具发现 | grounding/core/tool_discovery.py | ToolSearchTool |
| 嵌入持久化缓存 | grounding/core/search_tools.py | _load_persistent_cache、_save_persistent_cache、CACHE_VERSION |
| 质量管理 | grounding/core/quality/manager.py | ToolQualityManager、record_execution、adjust_ranking、get_problematic_tools |
| 质量数据模型 | grounding/core/quality/types.py | ToolQualityRecord、penalty |
| 质量落盘 | grounding/core/quality/store.py | QualityStore(共用 openspace.db) |
| 安全策略 | grounding/core/types.py / security/policies.py | SecurityPolicy、find_dangerous_tokens、SecurityPolicyManager.check_command_allowed |
| 权限规则引擎 | grounding/core/permissions/ | (engine / bash_permissions / filesystem / loader) |
| 沙箱 | grounding/core/security/sandbox.py / e2b_sandbox.py | SandboxManager、BaseSandbox、E2BSandbox |
| shell 后端 | grounding/backends/shell/ | ShellProvider、ShellSession、BashTool、edit 文件工具 |
| gui 后端 | grounding/backends/gui/ | GUIAgentTool、AnthropicGUIClient |
| mcp 后端 | grounding/backends/mcp/ | MCPProvider、create_connector_from_config、convert_mcp_tool_to_base_tool |
| web 后端 | grounding/backends/web/ | WebSession(WebSearch / WebFetch 扁平工具) |
| meta 元后端 | grounding/core/meta/ | MetaProvider、ListProvidersTool |