Hermes Agent 源码专题【左扬精讲】— Model Tools 编排层

Hermes Agent 源码专题【左扬精讲】— Model Tools 编排层

这是 Hermes Agent 源码专题【左扬精讲】系列的 第 6 篇 / 共 40 篇。本篇是 Layer 2 的工具编排层。

本篇不是工具实现的讲解,而是工具系统的 "编排层" 分析:当你调用 hermes 时,工具是如何被发现、过滤、分发给模型的?当你让模型执行一个函数时,调用链是怎么走的?

Layer 框架的视角是:先看分层(Layer),再看路由(工具从注册到调用的完整链路),最后看边界(安全拦截点的设计意图)。

Layer 2 ─ 工具编排层(model_tools.py)
  ════════════════════════════════════════════════════════════
    
      Layer 3 ─ 工具实现层(自注册)
        tools/registry.py       ←  全局注册表
        tools/*.py              ←  工具实现(import 时自调用 registry.register())
      
      Layer 2 ─ 工具编排层(model_tools.py)
        model_tools.py          ←  get_tool_definitions() 发给模型的 schema
                                    ←  handle_function_call() 分发函数调用
        toolsets.py             ←  _HERMES_CORE_TOOLS / TOOLSETS 字典
      
      Layer 2 ─ 工具编排层(model_tools.py)
        model_tools.py          ←  coerce_tool_args() 参数类型修正
                                    ←  run_async() 异步桥接
                                    ←  _sanitize_tool_error() 错误脱敏
      
      Layer 2 ─ 工具编排层(model_tools.py)
        run_agent.py            ←  AIAgent 类 ─ 调用 model_tools
  

Layer 2 工具编排 model_tools 工具集 Tool Search

本篇学习重点

必须掌握

  • 理解 model_tools.py 的两个核心函数:get_tool_definitions()handle_function_call()
  • 理解工具集的过滤逻辑:enabled / disabled toolsets 如何组合
  • 理解参数类型修正(coerce_tool_args)为什么必要

需要了解

  • 工具搜索渐进式加载(Tool Search)的触发条件
  • 异步工具调用的 run_async 桥接机制
  • 错误信息脱敏防止提示注入

目录

一、Layer 2.1 ─ get_tool_definitions:从注册表到模型 schema

Layer 视角 ─ 这一层解决什么?

model_tools.py 是工具系统的编排层,位于注册表(tools/registry.py)和智能体核心(run_agent.py)之间。它负责两件事:

  • 暴露:把注册表中的工具按工具集过滤后,以 OpenAI 兼容的 schema 格式发给模型
  • 分发:接收模型的函数调用请求,路由到实际工具函数并返回结果

第 5 篇讲了工具如何注册,本篇讲注册后的工具如何被发现和调用。

1.1 定位:核心函数在哪

model_tools.py 的两个核心函数:

源码视角 ─ get_tool_definitions 和 handle_function_call 的位置

model_tools.py

# 第 272 行:发给模型的工具 schema
  def get_tool_definitions(
      enabled_toolsets: Optional[List[str]] = None,
      disabled_toolsets: Optional[List[str]] = None,
      quiet_mode: bool = False,
      skip_tool_search_assembly: bool = False,
  ) -> List[Dict[str, Any]]:
      ...
  
  # 第 876 行:分发函数调用
  def handle_function_call(
      function_name: str,
      function_args: Dict[str, Any],
      task_id: Optional[str] = None,
      ...
  ) -> str:
      ...
  

两个函数分别是工具系统的"入口"和"出口":get_tool_definitions() 把工具暴露给模型,handle_function_call() 把模型的调用路由到实际工具。

1.2 工具 schema 的生成链路

行为:

get_tool_definitions() 的调用链:

get_tool_definitions() 调用链
  ════════════════════════════════════════════════════════
  
  调用方(AIAgent / Gateway)
      │
      ▼
  get_tool_definitions(enabled_toolsets, disabled_toolsets, quiet_mode)
      │
      ├─── 缓存命中? ──→ 直接返回(quiet_mode=True 时,约 7ms 节省)
      │
      ▼
  _compute_tool_definitions()
      │
      ├─── 1. enabled_toolsets 非空? ──→ resolve_toolset() 获取工具名列表
      │         空? ──→ 从所有工具集聚合
      │
      ├─── 2. disabled_toolsets ──→ 集合差运算排除
      │
      ├─── 3. registry.get_definitions() ──→ 按工具名取 schema + check_fn 过滤
      │
      ├─── 4. sanitize_tool_schemas() ──→ schema 规范化
      │
      └─── 5. Tool Search 渐进加载(可选)──→ 大量 MCP/插件工具时触发
              │
              └─── assemble_tool_defs() ──→ 超出阈值则替换为 bridge 工具
  

1.3 缓存机制:为什么需要缓存

源码视角 ─ get_tool_definitions 的缓存键

model_tools.py 第 304-319 行:

if quiet_mode:
      cache_key = (
          frozenset(enabled_toolsets) if enabled_toolsets is not None else None,
          frozenset(disabled_toolsets) if disabled_toolsets else None,
          registry._generation,
          cfg_fp,  # 配置文件修改时间
          bool(os.environ.get("HERMES_KANBAN_TASK")),
          bool(skip_tool_search_assembly),
      )
      cached = _tool_defs_cache.get(cache_key)
      if cached is not None:
          return list(cached)
  

缓存键包含:工具集配置 + registry 代际 + 配置文件修改时间 + Kanban 任务标记。Gateway 运行时每次 turn 都用 quiet_mode,缓存命中可节省约 7ms 的 registry 遍历 + schema 过滤 + check_fn 探测。

What-if ─ 不用缓存会怎样?

① 每次 turn 都要遍历 registry、取 schema、调用 check_fn,在高频交互场景下累积显著延迟。

② 缓存通过 registry._generation 自动失效 —— 当有工具注册/注销时,registry._generation 递增,所有缓存条目失效,保证新工具立即可见。

本节小结

  • get_tool_definitions() 是工具系统的"入口"—— 把注册表中的工具按工具集过滤后发给模型
  • 缓存键包含工具集配置 + registry 代际 + 配置文件修改时间,配置变更自动失效
  • Tool Search 在工具数超过阈值时触发,把大量 MCP/插件工具替换为 bridge 工具

二、Layer 2.2 ─ handle_function_call:函数调用的分发链路

Layer 视角 ─ 这一层解决什么?

当模型返回 tool_calls 时,Hermes 需要:

  • 把模型返回的 JSON 参数转换为 Python 类型
  • 依次通过安全拦截点(插件 hook、ACP 审批)
  • 调用实际工具函数并返回 JSON 结果
  • 对错误信息脱敏,防止提示注入

handle_function_call() 是这个链路的唯一入口。

2.1 分发链路概览

行为:

handle_function_call() 的分发链路:

handle_function_call() 调用链
  ════════════════════════════════════════════════════════
  
  模型返回 tool_calls
      │
      ▼
  coerce_tool_args() ──→ 修正 "42"→42 等类型问题
      │
      ├─── Tool Search bridge? ──→ dispatch_tool_search / tool_describe
      │                              tool_call 展开为真实工具后递归
      │
      ▼
  apply_tool_request_middleware() ──→ 请求中间件
      │
      ├─── pre_tool_call hook ──→ 插件可阻止(block)
      │
      ├─── ACP edit approval ──→ 文件修改前审批
      │
      └─── reset_consecutive_read_counter() ──→ 非读工具则重置
      │
      ▼
  registry.dispatch() ──→ 调用实际工具函数
      │
      ├─── 同步工具 ──→ 直接调用
      │
      └─── 异步工具 ──→ run_async() 桥接
      │
      ▼
  _sanitize_tool_error() ──→ 错误信息脱敏
      │
      ▼
  _emit_post_tool_call_hook() ──→ 插件可观测性
      │
      ▼
  JSON 字符串返回给模型
  

2.2 三层安全拦截

源码视角 ─ handle_function_call 的拦截点

model_tools.py 第 1017-1078 行:

try:
      if function_name in _AGENT_LOOP_TOOLS:
          return json.dumps({"error": f"{function_name} must be handled by the agent loop"})
  
      # 第 1 层:插件 pre_tool_call hook(可阻止)
      if not skip_pre_tool_call_hook:
          block_message = get_pre_tool_call_block_message(function_name, function_args, ...)
          if block_message is not None:
              return json.dumps({"error": block_message}, ensure_ascii=False)
  
      # 第 2 层:ACP/Zed 文件修改审批
      edit_block_message = maybe_require_edit_approval(function_name, function_args)
      if edit_block_message is not None:
          return edit_block_message
  

分发前有三层拦截:Agent 循环专用工具(todomemory 等)、插件 block hook、ACP 审批。这确保了安全边界在工具执行前被检查。

2.3 Tool Search bridge 展开

源码视角 ─ tool_call 如何展开为真实工具

model_tools.py 第 955-995 行:

if function_name == _ts_mod.TOOL_CALL_NAME:
      underlying_name, underlying_args, err = _ts_mod.resolve_underlying_call(function_args or {})
      if err or not underlying_name:
          return json.dumps({"error": err or "tool_call could not be resolved"})
      
      # 安全检查:工具必须在会话的 deferrable 集合中
      _scoped_deferrable = _ts_mod.scoped_deferrable_names(current_defs)
      if underlying_name not in _scoped_deferrable:
          return json.dumps({
              "error": f"'{underlying_name}' is not available in this session."
          })
      
      # 递归调用真实工具,所有 hook 都会触发
      return handle_function_call(
          function_name=underlying_name,
          function_args=underlying_args,
          ...
      )
  

tool_call bridge 会展开为真实工具后递归调用,这样所有 hook(pre/post、guardrails)都针对真实工具名触发,bridge 本身对插件透明。

LessonBridge 对插件透明 ─ Tool Search 的三个 bridge 工具(tool_searchtool_describetool_call)在插件看来是透明的。当模型通过 bridge 调用真实工具时,pre_tool_call hook 收到的是真实工具名,而不是 tool_call。这是设计上的对称性:bridge 是模型的"代理",对安全边界不可见。

本节小结

  • handle_function_call() 是工具系统的"出口"—— 把模型的调用路由到实际工具
  • 三层安全拦截:Agent 循环专用工具、插件 block hook、ACP 审批
  • Tool Search bridge 展开为真实工具后递归调用,bridge 对插件透明

三、Layer 2.3 ─ 辅助机制:参数修正、异步桥接、错误脱敏

Layer 视角 ─ 这一层解决什么?

除了核心的分发逻辑,model_tools.py 还有三个重要的辅助机制:

  • coerce_tool_args:修正常见的 LLM 参数类型错误
  • run_async:在 sync/async 上下文之间桥接
  • _sanitize_tool_error:防止工具错误信息中的提示注入

3.1 coerce_tool_args:参数类型修正

源码视角 ─ coerce_tool_args 的实现

model_tools.py 第 619-700 行:

def coerce_tool_args(tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
      schema = registry.get_schema(tool_name)
      if not schema:
          return args
  
      properties = (schema.get("parameters") or {}).get("properties")
      if not properties:
          return args
  
      for key, value in list(args.items()):
          prop_schema = properties.get(key)
          if not prop_schema:
              continue
          
          # Wrap bare non-list values when schema declares array.
          # Also coerce strings to expected types (integer, number, boolean).
          if isinstance(value, str):
              coerced = _coerce_value(value, prop_schema.get("type"), prop_schema)
              if coerced is not value:
                  args[key] = coerced
              continue

          # Handle array type: wrap non-list scalars in a list.
          expected = prop_schema.get("type")
          if expected == "array" and value is not None and not isinstance(value, (list, tuple)):
              args[key] = [value]
      return args
  

核心逻辑:遍历每个参数,如果值是字符串且 schema 期望其他类型,尝试修正。支持 integer、number、boolean、array、object 类型。

为什么需要这个修正?LLM 返回的参数经常类型不正确:

  • 数字返回为字符串:"42" 而不是 42
  • 布尔值返回为字符串:"true" 而不是 true
  • 数组返回为字符串:"[1, 2, 3]" 而不是 [1, 2, 3]

工具函数的类型注解是严格的 Python 类型,必须修正后才能正确执行。

3.2 run_async:异步桥接

源码视角 ─ run_async 的完整实现

model_tools.py 第 84-173 行:

def _run_async(coro):
    """Run an async coroutine from a sync context."""
    try:
        loop = asyncio.get_running_loop()
    except RuntimeError:
        loop = None

    if loop and loop.is_running():
        # Inside an async context (gateway, RL env) — run in a fresh thread
        import concurrent.futures
        worker_loop: Optional[asyncio.AbstractEventLoop] = None
        loop_ready = threading.Event()

        def _run_in_worker():
            nonlocal worker_loop
            worker_loop = asyncio.new_event_loop()
            loop_ready.set()
            try:
                asyncio.set_event_loop(worker_loop)
                return worker_loop.run_until_complete(coro)
            finally:
                pending = asyncio.all_tasks(worker_loop)
                for t in pending:
                    t.cancel()
                if pending:
                    worker_loop.run_until_complete(
                        asyncio.gather(*pending, return_exceptions=True)
                    )
                worker_loop.close()

        pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
        future = pool.submit(_run_in_worker)
        try:
            return future.result(timeout=300)
        except concurrent.futures.TimeoutError:
            if loop_ready.wait(timeout=1.0) and worker_loop is not None:
                for t in asyncio.all_tasks(worker_loop):
                    worker_loop.call_soon_threadsafe(t.cancel)
            raise
        finally:
            pool.shutdown(wait=False)

    if threading.current_thread() is not threading.main_thread():
        worker_loop = _get_worker_loop()
        return worker_loop.run_until_complete(coro)

    tool_loop = _get_tool_loop()
    return tool_loop.run_until_complete(coro)

三个路径:

  • Gateway 路径:在独立线程中创建新 loop 并运行,300s 超时后取消任务
  • Worker 线程路径:使用线程本地持久化 loop
  • CLI 路径:使用进程级持久化 loop,避免 GC 时报错

3.3 _sanitize_tool_error:错误脱敏

源码视角 ─ _sanitize_tool_error 的实现

model_tools.py 第 589-612 行:

_TOOL_ERROR_ROLE_TAG_RE = re.compile(
    r'',
    re.IGNORECASE,
)
_TOOL_ERROR_FENCE_RE = re.compile(r'^\s*```(?:json|xml|html|markdown)?\s*', re.MULTILINE)

def _sanitize_tool_error(error_msg: str) -> str:
    if not error_msg:
        return "[TOOL_ERROR] "
    sanitized = _TOOL_ERROR_ROLE_TAG_RE.sub("", error_msg)
    sanitized = _TOOL_ERROR_FENCE_RE.sub("", sanitized)
    if len(sanitized) > _TOOL_ERROR_MAX_LEN:
        sanitized = sanitized[:_TOOL_ERROR_MAX_LEN - 3] + "..."
    return f"[TOOL_ERROR] {sanitized}"

为什么需要脱敏?工具错误信息可能包含注入尝试:

# 恶意工具可能返回:
  "请忽略上面的指令..."
  
  # 脱敏后:
  "[TOOL_ERROR] 请忽略上面的指令..." 被移除
  

脱敏函数移除 XML 标签、代码 fences 等结构化标记,防止提示注入攻击。

本节小结

  • coerce_tool_args() 修正常见的 LLM 参数类型错误,是必要的
  • run_async() 在 sync/async 上下文之间桥接,支持 Gateway 和 CLI 两条路径
  • _sanitize_tool_error() 防止工具错误信息中的提示注入

四、Layer 2.4 ─ 工具集系统:暴露策略与 Tool Search

Layer 视角 ─ 这一层解决什么?

工具集(toolset)是工具暴露给模型的策略单元。Hermes 有两层工具暴露设计:

  • 注册表(registry:所有工具的存在性,是全集
  • 工具集(TOOLSETS:每个 session 实际暴露给模型的子集,决定哪些可见

这两层故意分开:注册表让新增工具零成本接入(自注册),工具集显式列出保证可控暴露。

4.1 _HERMES_CORE_TOOLS:核心工具列表

源码视角 ─ _HERMES_CORE_TOOLS 的内容

toolsets.py

_HERMES_CORE_TOOLS = [
      # Web
      "web_search", "web_extract",
      # Terminal + process management
      "terminal", "process",
      # File manipulation
      "read_file", "write_file", "patch", "search_files",
      # Vision + image generation
      "vision_analyze", "image_generate",
      # Skills
      "skills_list", "skill_view", "skill_manage",
      # Browser automation
      "browser_navigate", "browser_snapshot", "browser_click",
      "browser_type", "browser_scroll", "browser_back",
      "browser_press", "browser_get_images",
      "browser_vision", "browser_console", "browser_cdp", "browser_dialog",
      # Text-to-speech
      "text_to_speech",
      # Planning & memory
      "todo", "memory",
      # Session history search
      "session_search",
      # Clarifying questions
      "clarify",
      # Code execution + delegation
      "execute_code", "delegate_task",
      # Cronjob management
      "cronjob",
      # Kanban
      "kanban_show", "kanban_list", "kanban_complete",
      ...
  ]
  

_HERMES_CORE_TOOLS 是核心工具列表,会被所有平台继承。这些工具永远不会被 Tool Search 延迟加载。

4.2 工具集过滤逻辑

行为:

工具集过滤的优先级:

工具集过滤逻辑
  ════════════════════════════════════════════════════════
  
  1. enabled_toolsets 非空?
     ├─── 是 ──→ 只包含这些工具集的工具
     └─── 空 ──→ 包含所有工具集的工具
  
  2. 应用 disabled_toolsets(集合差运算)
     └─── 排除被禁用的工具集的工具
  
  3. 特殊处理:HERMES_KANBAN_TASK 环境变量
     └─── 自动追加 kanban 工具集(dispatcher-spawned worker 必须有 kanban 工具)
  

注意:disabled_toolsets 在 enabled_toolsets 之后应用,所以禁用列表可以覆盖启用列表。例如:如果 enabled 是 ["web", "file"],disabled 是 ["file"],最终只有 web 工具。

4.3 Tool Search 渐进加载

当 MCP 服务器和插件工具数量很多时,全部发给模型会消耗大量上下文窗口。Tool Search 在工具总数超过阈值(默认 10% 上下文长度)时触发:

  • 把大量 MCP/插件工具替换为三个 bridge 工具:tool_searchtool_describetool_call
  • 模型需要某个工具时,通过 bridge 按需加载
  • 核心工具(_HERMES_CORE_TOOLS)永远不会被延迟

What-if ─ 不用 Tool Search 会怎样?

① 当 MCP 服务器和插件数量很多时,工具 schema 的总 token 数可能超过模型的上下文窗口,导致无法正常调用。

② 实时案例:Gateway 懒加载 MCP 工具时,如果 MCP 服务器响应慢,会阻塞 Gateway 启动 120 秒。修复方案是把 MCP 工具 discovery 移出模块级,改为按需加载。

本节小结

  • 工具集是工具暴露给模型的策略单元,工具必须被 TOOLSETS 显式列出才暴露
  • _HERMES_CORE_TOOLS 是核心工具列表,会被所有平台继承,永远不会被 Tool Search 延迟
  • Tool Search 渐进加载防止大量 MCP 工具撑爆上下文窗口

五、FAQ 20 问

FAQ 分组说明

本节围绕 model_tools.py 的编排层设计,每条都是"读整个系列前最该知道的事"。

FAQ 覆盖的核心问题:

  • 工具集和工具的关系
  • 缓存失效机制
  • 安全拦截的设计意图
  • Tool Search 的原理
  • 参数修正的必要性

Q1. 工具集(toolset)和工具(tool)是什么关系?

工具集是工具的分组,决定哪些工具对模型可见。例如 web 工具集包含 web_searchweb_extract。一个工具可以属于多个工具集。工具集可以通过 includes 字段引用其他工具集,实现组合。

Q2. 为什么有些工具属于 _HERMES_CORE_TOOLS?

_HERMES_CORE_TOOLS 是核心工具列表,会被所有平台继承。这些工具包括 terminal、read_file、patch 等基础能力,永远不会被 Tool Search 延迟加载。

Q3. 模型调用了一个不存在的工具名会怎样?

handle_function_call() 会返回错误 JSON。registry 会尝试获取 handler,如果工具名不存在,返回 {"error": "tool not found"}。这个错误会经过 _sanitize_tool_error() 脱敏后返回给模型。

Q4. quiet_mode 参数是干什么的?

quiet_mode=True 时不打印工具集启用的提示信息,并且启用结果缓存。Gateway 运行时每次 turn 都用 quiet_mode,CLI 调试时用 quiet_mode=False 看到详细的工具加载信息。

Q5. disabled_toolsets 和 enabled_toolsets 的优先级?

先按 enabled_toolsets 过滤,再应用 disabled_toolsets 排除。这确保了禁用列表可以覆盖启用列表。例如 enabled=["web", "file"],disabled=["file"],最终只有 web 工具。

Q6. Tool Search 是什么原理?

当工具 schema 超过阈值时,用 bridge 工具替代实际工具。三个 bridge 工具(tool_search、tool_describe、tool_call)替代大量 MCP/插件工具。模型通过 bridge 按需发现和调用工具。

Q7. check_fn 是什么时候被调用的?

在 registry.get_definitions() 中调用。当 check_fn 返回 False 时,该工具不会被包含在返回的 schema 列表中,从而对模型不可见。这实现了凭据门控(如 API key 缺失时隐藏相关工具)。

Q8. 为什么需要 _emit_post_tool_call_hook?

插件可以通过 post_tool_call hook 记录工具调用日志、发送遥测数据。这个 hook 在工具执行完成后、错误脱敏之前发射,确保插件看到真实的执行结果。

Q9. canary 工具集是什么?

在 toolsets.py 中定义,包含可能不稳定的新功能工具。用于向用户暴露测试中的功能,同时与稳定工具集分开管理。

Q10. execute_code 工具为什么特殊处理?

execute_code 需要知道当前会话可用的工具列表。它使用 enabled_tools 参数传入,这个参数来自 _last_resolved_tool_names(最近一次 get_tool_definitions() 的结果)。

Q11. 缓存是如何失效的?

通过 registry._generation 自动失效。当有工具注册/注销时,registry._generation 递增,所有缓存条目失效,保证新工具立即可见。配置文件修改时间也包含在缓存键中。

Q12. pre_tool_call hook 可以做什么?

插件可以通过 pre_tool_call hook 阻止工具执行。hook 返回 block_message 时,handle_function_call() 直接返回错误,工具不会被执行。这用于实现访问控制、审批流程等。

Q13. 为什么需要两个异步桥接路径?

Gateway 有 running loop,CLI 没有,需要不同的处理策略。Gateway 内部已经有 asyncio 在运行,不能再创建新 loop;CLI 场景没有 running loop,可以用持久化的 loop 避免客户端在 GC 时报错。

Q14. Tool Search 的阈值是多少?

默认是模型上下文窗口的 10%。可以通过配置修改。当工具 schema 总 token 数超过这个阈值时,触发渐进加载。

Q15. _AGENT_LOOP_TOOLS 包括哪些工具?

todo、memory、session_search、delegate_task。这些工具需要 agent 级别的状态(TodoStore、MemoryStore 等),必须由 run_agent.py 处理,不能通过 model_tools.py 分发。

Q16. 为什么错误信息要脱敏?

防止工具错误信息中的提示注入攻击。恶意工具可能返回包含 XML 标签或代码 fences 的错误信息,脱敏后移除这些结构化标记。

Q17. MCP 工具 discovery 为什么移出模块级?

Gateway 懒加载 MCP 工具时,如果 MCP 服务器响应慢,会阻塞启动 120 秒。修复方案是把 MCP discovery 移出模块级,改为按需加载。

Q18. 如何让一个工具在某些平台不可见?

在 check_fn 中实现平台检测逻辑,返回 False 时工具对模型不可见。例如 read_terminal 工具的 check_fn 检测 HERMES_DESKTOP 环境变量,只在桌面应用中可用。

Q19. kanban 工具为什么只在特定条件下暴露?

kanban 工具通过 check_fn 实现条件暴露。只有当 HERMES_KANBAN_TASK 环境变量设置(dispatcher-spawned worker)或显式启用 kanban 工具集时,kanban 工具才对模型可见。

Q20. _last_resolved_tool_names 是什么?

最近一次 get_tool_definitions() 返回的工具名列表。用于 execute_code 工具生成沙箱代码时的工具提示,以及 Tool Search bridge 的安全检查。

FAQ 全篇总纲

model_tools.py 是工具系统的编排层:

  • 入口get_tool_definitions() 把工具暴露给模型(按工具集过滤 + 缓存)
  • 出口handle_function_call() 把调用分发给实际工具(三层安全拦截)
  • 辅助:参数修正、异步桥接、错误脱敏
  • 策略:工具集控制暴露,Tool Search 防止上下文溢出

六、下一篇

学习路径

本篇介绍了 Model Tools 编排层的完整链路。下一篇将继续深入 Layer 2 的其他组件。

Model Tools 工具编排 工具集 Tool Search FAQ

posted @ 2026-07-17 19:20  左扬  阅读(33)  评论(0)    收藏  举报