跳转到内容

事件驱动工具执行

事件驱动工具执行

学习目标

本章聚焦 LibreChat 的事件驱动工具执行系统。你将了解:

  • defer_loading 机制如何让工具定义在 prompt 中可见但按需实例化
  • tool_search 动态发现协议的设计与双格式兼容解析
  • 提供商原生工具与平台工具的冲突解决策略
  • 代码执行工具的 {{tool<idx>turn<turn>}} 占位符替换

前置知识

下文假设你已理解上述概念,直接聚焦 LibreChat 的具体实现。


项目实践

defer_loading 机制

LibreChat 的每个工具定义(LCTool)上有一个 defer_loading 标记。当为 true 时:

  • 工具的定义(名称、描述、参数 Schema)会注入到 prompt 中,模型可以看到并选择调用
  • 工具的执行实例不会在 Agent 初始化时创建
  • 当模型实际调用该工具时,运行时才从注册表中查找并实例化

这在 MCP 工具场景中特别有用——MCP 服务器可能有 50+ 工具,但一个 turn 中用户可能只会用到其中几个。

tool_search 动态发现

当工具数量超过上下文预算时,LibreChat 注入了一个特殊的 tool_search 工具。模型通过自然语言查询可用工具:

用户: "帮我搜索一下今天的新闻"
模型: [调用 tool_search, 参数: {query: "news"}]
tool_search: 返回匹配的工具列表
模型: [调用 web_search 工具]

双格式解析:LibreChat 的 tool_search 输出格式经历了从文本到 JSON 的演进,解析器实现了向后兼容:

新格式 (JSON):
{ "found": 3, "tools": [{ "name": "web_search", ... }, ...] }
旧格式 (文本):
- web_search (score: 0.95)
- tavily_search (score: 0.87)

解析器优先尝试 JSON.parse,失败后回退到正则表达式 /- ([^\s(]+)\s*\(score:/gm

工具发现缓存优化

extractDiscoveredToolsFromHistory 函数从消息历史中提取之前 turn 的 tool_search 结果:

for message in messages:
if message.type == 'tool' and message.name == 'tool_search':
解析输出 → discoveredTools
for toolName in discoveredTools:
if toolRegistry.get(toolName).defer_loading == true:
toolRegistry.get(toolName).defer_loading = false // 下次不再重新搜索

这避免了每个 turn 都重新搜索工具——一旦某个工具被发现,它的 defer_loading 被覆盖为 false,后续 turn 直接使用。

提供商工具冲突解决

resolveProviderToolConflicts 函数处理 Anthropic 和 Google 原生搜索工具与 LibreChat web_search 工具的冲突:

如果 provider == Anthropic 且 web_search 已启用:
移除 Anthropic 原生 web_search // 使用 LibreChat 的多提供商搜索
如果 provider == Google/VertexAI 且 googleSearch/googleSearchRetrieval 存在:
移除 Google 原生搜索 // 使用 LibreChat web_search
// 注意: 仅 gemini-3 模型支持同时启用

设计理由:LibreChat 的 web_search 聚合了多个搜索提供商(Tavily、SearXNG、Firecrawl 等),提供了比单一提供商原生工具更丰富的能力。当平台工具可用时,优先使用平台工具。

代码执行工具的占位符替换

当代码执行环境可用时,LibreChat 启用 toolOutputReferences 功能:

如果 anyAgentHasCodeEnv(agents):
RunConfig.toolOutputReferences.enabled = true

这使得代码执行工具的描述中包含 {{tool<idx>turn<turn>}} 占位符,在执行前会被替换为实际的输出内容。SDK 默认截断限制为 ~400 KB per output、5 MB total,防止超出 shell ARG_MAX。


问题与规避

问题 1:工具搜索格式化变更导致解析失败

规避:双格式解析器(JSON 优先,正则回退)。新增格式时只需在 parseToolSearchJson 中添加新的解析逻辑。

问题 2:子 Agent 继承了不应继承的工具发现状态

规避isSubagent: true 标志完全跳过 defer_loading 覆盖和工具定义注入。子 Agent 的工具上下文从零开始。

问题 3:Google 同时启用原生工具和平台工具导致模型混淆

规避supportsGoogleToolCombination 函数严格限制仅 gemini-3 模型允许组合,其他模型直接抛出 GOOGLE_TOOL_CONFLICT 错误。


设计取舍

延迟加载 vs 预加载

LibreChat 选择了延迟加载作为默认策略,因为:

  • MCP 服务器工具数量可能很多(50+),全量实例化成本高
  • 不是每个 turn 都会调用所有工具
  • 工具可能需要 OAuth 认证,按需触发比预认证更合理

代价是首次调用有额外延迟——但从实际场景看,工具搜索 + 调用的总延迟通常小于预加载所有工具的启动延迟。


参考来源