转载--Hermes Agent 03 | 工具系统全解析:从注册到执行的完整链路

大脑决定做什么,双手决定做得怎么样------工具系统就是 Agent 的双手。


先感受一下问题的规模

打开 Hermes Agent 的工具列表,数一数,你会看到 50+ 个内置工具 。下面是在 toolsets.py 中定义的 _HERMES_CORE_TOOLS 列表(CLI 和各消息网关共享的核心工具集):

类别 工具(真实注册名) 用途
终端 terminal , process 执行 shell 命令、管理后台进程
文件 read_file , write_file, patch, search_files 文件读写与搜索(grep/find 二合一)
Web web_search , web_extract 网页搜索与内容抽取
浏览器 browser_navigate , browser_snapshot, browser_click, browser_type, browser_scroll, browser_back, browser_press, browser_get_images, browser_vision, browser_console 浏览器自动化
视觉 vision_analyze , image_generate 图片理解与生成
语音 text_to_speech 语音合成
记忆/规划 memory , session_search, todo 记忆读写、会话搜索、待办管理
子 Agent delegate_task 委派子任务
代码 execute_code 代码执行沙箱
技能 skills_list , skill_view, skill_manage 技能浏览与管理
其他 clarify , cronjob, send_message, ha_* 澄清提问、定时任务、跨平台消息、Home Assistant
MCP 动态注入 外部 MCP 服务器提供的工具

50 个工具意味着什么?意味着你每次发消息给模型时,tools 数组的 JSON schema 可能有几千个 token。还记得上一讲说的------不同 Provider 对工具数量有各自的硬限制?

更麻烦的是:不是每个工具在每个场景下都可用。你在 Docker 容器里跑,浏览器工具可能没有浏览器后端;你没配 HASS_TOKEN,Home Assistant 工具全废;你的 gateway 没启动,send_message 就没法工作。

还要再补一句:这张表只覆盖了 registry 里的常驻工具面。 真正运行时的 tool surface 还会叠加三类动态来源:MCP 工具、memory provider 工具、context engine 工具。后两类不是在 toolsets.py 里静态列出来的,而是在 run_agent.py 初始化 session 时按配置注入。

如何在保持 50+ 工具丰富度的同时,精准控制每一轮推理中"模型能看到什么"------这是 Hermes Agent 工具系统要解决的核心问题。


Registry 模式:Schema + Handler + check_fn 三件套

我们从最基础的概念开始:一个工具在 Hermes Agent 里到底长什么样?

真实的工具注册:命令式调用,不是装饰器

Hermes Agent 的工具注册是命令式调用 ------每个工具文件在模块末尾直接调用 registry.register(...)。以 tools/file_tools.py 为例:

复制代码
# tools/file_tools.py 末尾(约第 796-799 行)

def _handle_read_file(args, **kw): ...
def _handle_write_file(args, **kw): ...
def _handle_patch(args, **kw): ...
def _handle_search_files(args, **kw): ...

def _check_file_reqs():
    """Lazy wrapper to avoid circular import with tools/__init__.py."""
    from tools import check_file_requirements
    return check_file_requirements()

registry.register(
    name="read_file",
    toolset="file",
    schema=READ_FILE_SCHEMA,
    handler=_handle_read_file,
    check_fn=_check_file_reqs,
    emoji="📖",
    max_result_size_chars=float('inf'),
)
registry.register(
    name="write_file",
    toolset="file",
    schema=WRITE_FILE_SCHEMA,
    handler=_handle_write_file,
    check_fn=_check_file_reqs,
    emoji="✍️",
    max_result_size_chars=100_000,
)
# ... patch / search_files 同理

ToolEntry 的真实字段

每一次 registry.register() 都会创建一个 ToolEntrytools/registry.py:76):

复制代码
class ToolEntry:
    """Metadata for a single registered tool."""
    __slots__ = (
        "name", "toolset", "schema", "handler", "check_fn",
        "requires_env", "is_async", "description", "emoji",
        "max_result_size_chars",
    )

核心三件套:

1. Schema(参数模式)。 就是那个 JSON Schema 对象(namedescriptionparameters)。它会被 registry.get_definitions() 转成 OpenAI 兼容的 {"type": "function", "function": {...}} 数组,发给模型。

2. Handler(执行函数)。 接收模型传来的参数,做真正的事,返回一个 JSON 字符串 (成功时 json.dumps(...),失败时 json.dumps({"error": "..."}))。注意:Handler 通常是同步函数ToolEntry 上的 is_async 字段标记异步 handler,dispatch() 会在需要时通过 _run_async() 桥接。

3. check_fn(可用性检查)。 这个是 Hermes Agent 工具系统最关键的设计之一。check_fn 是一个无参函数 ,返回 True(工具可用)或 False(工具不可用)。每次构建工具 schema 时 ,Registry 会对所有请求的工具跑一遍 check_fn,只有通过检查的才会出现在发给模型的 tools 数组里。

check_fn 的真实调用流程

get_definitions()tools/registry.py:258)的核心逻辑:

复制代码
def get_definitions(self, tool_names: Set[str], quiet: bool = False) -> List[dict]:
    """Return OpenAI-format tool schemas for the requested tool names.

    Only tools whose check_fn() returns True (or have no check_fn)
    are included.
    """

    result = [ ]

    check_results: Dict[Callable, bool] = {}
    entries_by_name = {entry.name: entry for entry in self._snapshot_entries()}
    for name in sorted(tool_names):
        entry = entries_by_name.get(name)
        if not entry:
            continue
        if entry.check_fn:
            if entry.check_fn not in check_results:
                try:
                    check_results[entry.check_fn] = bool(entry.check_fn())
                except Exception:
                    check_results[entry.check_fn] = False
            if not check_results[entry.check_fn]:
                continue
        schema_with_name = {**entry.schema, "name": entry.name}
        result.append({"type": "function", "function": schema_with_name})
    return result

几个细节:

  • check_fn 是无参的。 它通过模块闭包或全局状态做判断(例如 _check_file_reqs() 内部调用 tools.check_file_requirements())。

  • 结果会缓存一次。 如果多个工具共享同一个 check_fn(比如 read_file / write_file / patch / search_files 共用 _check_file_reqs),这一轮只跑一次。

  • 异常 → False。 check_fn 抛异常会被当作"不可用",不会让整个 schema 构建挂掉。

为什么 check_fn 这么重要?因为它解决了一个核心矛盾:工具的"注册"是静态的(启动时一次性完成),但工具的"可用性"是动态的(运行时随时变化)。

**check_fn** 让 Hermes Agent 不需要用 if-else 到处散落"当前能不能用这个工具"的判断------所有可用性逻辑都集中在注册点。 这是一个非常干净的设计,你自己做工具系统的时候可以直接抄。


Toolset 分组:一个扁平的元数据字典

50+ 个工具怎么组织?Hermes Agent 的实际做法是:文件扁平,分组由元数据决定

真实的目录结构

更准确地说,Hermes Agent 的工具系统是以扁平主干为主,少量后端实现放在子目录。也就是说,语义分组主要靠注册元数据,不靠目录层级表达:

复制代码
tools/
├── registry.py              # Registry 核心
├── file_tools.py            # read_file, write_file, patch, search_files
├── terminal_tool.py         # terminal, process
├── web_tools.py             # web_search, web_extract
├── browser_tool.py          # browser_* 系列
├── vision_tools.py          # vision_analyze
├── image_generation_tool.py # image_generate
├── memory_tool.py           # memory
├── session_search_tool.py   # session_search
├── delegate_tool.py         # delegate_task
├── skills_tool.py           # skills_list, skill_view
├── skill_manager_tool.py    # skill_manage
├── todo_tool.py             # todo
├── clarify_tool.py          # clarify
├── code_execution_tool.py   # execute_code
├── cronjob_tools.py         # cronjob
├── send_message_tool.py     # send_message
├── homeassistant_tool.py    # ha_*
├── tts_tool.py              # text_to_speech
├── mcp_tool.py              # MCP 动态注入
├── tool_result_storage.py   # 大结果三层防线
├── budget_config.py         # 预算/阈值配置
├── ...(50+ 个 .py 文件)
├── browser_providers/       # 浏览器后端实现
├── environments/            # 执行环境(local/docker/ssh/modal/...)
└── neutts_samples/          # TTS 样本

Toolset 是字符串标签,不是基类

工具的分组不是通过继承基类实现的 。每个工具在 register() 时通过 toolset= 参数指定一个字符串标签(比如 "file""web""browser"),仅作为元数据用于 UI 分类和按名字筛选。

真正的 toolset 组合配置在 toolsets.py 里,是一个普通的 dict

复制代码
# toolsets.py
TOOLSETS = {
    "web": {
        "description": "Web research and content extraction tools",
        "tools": ["web_search", "web_extract"],

        "includes": [ ],

    },
    "search": {
        "description": "Web search only (no content extraction/scraping)",
        "tools": ["web_search"],

        "includes": [ ],

    },
    # ...
}

这里的"toolset"是一个命名工具集合 ------CLI 启动时可以通过 -s web,search 来选择要启用哪些组。includes 支持组合嵌套(一个 toolset 可以包含另一个)。

对 Registry 工具来说,可用性主要只有一层:工具级 check_fn

当某个 toolset 下所有工具的 check_fn 都失败时,这个 toolset 在 UI 层会被标为不可用(registry.is_toolset_available()),但这是查询结果 ,不是加载门控。对于静态内置 registry 工具来说,模块 import 时就已经完成注册;真正决定"模型看不看得到"的主要就是 check_fn。MCP 和插件注入工具则是后面那两节讲的动态分支。

MCP 工具:真正的动态注册

内置工具是启动时静态注册的。但 MCP 工具不一样------它们来自外部 MCP 服务器,数量和 Schema 只有运行时连上服务器后才知道。

tools/mcp_tool.py 中的 discover_mcp_tools() 会在 model_tools.py 启动阶段被调用(model_tools.py:137),遍历已连接的 MCP 服务器,为每个远端工具动态调用 registry.register()。当 MCP 服务器发送 notifications/tools/list_changed 时,会走 registry.deregister() 做 "nuke-and-repave" 式刷新。

registry.py 对这种动态场景做了专门的设计------内部用 threading.RLock 保证 MCP 刷新和其他线程读取 schema 时不会打架(见 _snapshot_state())。

这套设计让 Hermes Agent 的工具数量不再有上限------你接入多少个 MCP 服务器,就有多少工具可用。

还有两条旁路:Agent 级工具与插件工具

如果你只看 tools/registry.py,很容易误以为"所有工具都走 registry.dispatch()"。源码并不是这样。

model_tools.py 里有一个 _AGENT_LOOP_TOOLS = {"todo", "memory", "session_search", "delegate_task"}。到了 run_agent.py,这些工具会在 _invoke_tool() 里被优先分流,不直接走通用 registry 分发。除此之外,memory provider 和 context engine 注入的工具也会在 agent loop 里直接命中各自的 handler。

这意味着 Hermes 的工具执行链路不是"只有一条总线",而是一条主路 + 几条旁路

  • 大多数内置工具和 MCP 工具走 registry

  • todo / memory / session_search / delegate_task 在 agent loop 内优先处理

  • memory provider / context engine 工具按当前 session 的插件配置直接分发


以大多数 Registry 工具为例:一个工具调用的完整旅程

理论够了,我们走一遍完整的链路。这里我们故意选 read_file 这种典型的 registry 工具来举例,因为它最能代表主路径。假设用户说"帮我看一下 /tmp/app.py 的内容",从这句话到文件内容返回给用户,中间经过了什么?

第一站:构建 tools 数组

Agent 主循环在调用模型前,先决定这一轮要把哪些工具暴露给模型(model_tools.py 中的 get_tool_definitions()),然后交给 registry:

复制代码
# tools/registry.py(简化示意)
schemas = registry.get_definitions(tool_names=set(enabled_tool_names))
# 每个 schema 都是 {"type": "function", "function": {...}} 格式

get_definitions() 内部会跑 check_fn 过滤------上一节已经讲过。这里的关键是:同一个 check_fn 在一轮内只跑一次,即使多个工具共享它。

第二站:模型决策

模型看到 tools 数组里有 read_file,决定调用它。返回的 response 里会带一个 tool_calls 字段:

复制代码
{
    "role": "assistant",
    "content": null,
    "tool_calls": [
        {
            "id": "call_abc123",
            "type": "function",
            "function": {
                "name": "read_file",
                "arguments": "{\"path\": \"/tmp/app.py\"}"
            }
        }
    ]
}

第三站:参数类型强转

这一步是很多 Agent 框架忽略的。模型返回的 arguments 是一个 JSON 字符串 ------它是模型"生成"出来的,常见的毛病是类型错乱 :该是整数的写成字符串,该是布尔的写成 "true"

Hermes Agent 的 model_tools.py:334 中有 coerce_tool_args() 专门处理:

复制代码
def coerce_tool_args(tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
    """Coerce tool call arguments to match their JSON Schema types.

    LLMs frequently return numbers as strings ("42" instead of 42)
    and booleans as strings ("true" instead of true).  This compares
    each argument value against the tool's registered JSON Schema and
    attempts safe coercion when the value is a string but the schema
    expects a different type.  Original values are preserved when
    coercion fails.
    """
    if not args or not isinstance(args, dict):
        return args
    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 args.items():
        if not isinstance(value, str):
            continue
        prop_schema = properties.get(key)
        if not prop_schema:
            continue
        expected = prop_schema.get("type")
        if not expected:
            continue
        coerced = _coerce_value(value, expected)
        if coerced is not value:
            args[key] = coerced
    return args

典型场景:

  • integer 强转 :模型说 "offset": "10"(字符串),Schema 要求 integer。转成 10

  • boolean 强转 :模型说 "replace_all": "true",Schema 要求 boolean。转成 True

  • Union 类型 :Schema 允许 ["integer", "string"] 时,按顺序尝试每种。

  • 转不了就保留原值------让 handler 自己报错,而不是让修正层乱改。

注意: coerce_tool_args() 只做类型强转,不会剥离多余字段、也不会自动填默认值 ------这些由 JSON Schema 自身机制或 handler 内部处理。

为什么做类型强转而不是严格校验? 因为 Agent 是一个多轮系统。报错意味着这一轮浪费了,要多走一轮让模型修正参数。模型的输出永远不可能 100% 符合 Schema,你要么接受这个现实做修正,要么接受永远有一个比例的轮次浪费在重试上。

第四站:进入分流点,再执行 Handler

参数修正完毕,接下来会先经过一个分流点 。对于 read_file 这类大多数 registry 工具,最后会进入 registry.dispatch();但如果当前工具是 todomemorysession_searchdelegate_task,或者是 memory provider / context engine 注入的工具,就会在 run_agent.py 里提前命中专门分支。

以主路径为例,registry.dispatch() 会根据 is_async 字段决定同步执行还是通过 _run_async() 桥接:

复制代码
# tools/registry.py:292
def dispatch(self, name: str, args: dict, **kwargs) -> str:
    entry = self.get_entry(name)
    if not entry:
        return json.dumps({"error": f"Unknown tool: {name}"})
    try:
        if entry.is_async:
            from model_tools import _run_async
            return _run_async(entry.handler(args, **kwargs))
        return entry.handler(args, **kwargs)
    except Exception as e:
        logger.exception("Tool %s dispatch error: %s", name, e)
        return json.dumps({"error": f"Tool execution failed: {type(e).__name__}: {e}"})

Handler 返回的是 JSON 字符串 。所有异常都会被 catch 并转成统一的 {"error": ...} JSON 格式,保证返回值类型一致。

第五站:结果三层防线------大结果的文件化

这一步又是一个容易被忽视的细节。如果读出来的内容有 50 万字符,直接塞进 messages 发给模型,有两个问题:

  1. 上下文爆炸:一次调用就吃掉大量上下文窗口。

  2. 模型注意力稀释:太长的工具结果会让模型"忘记"它本来要做什么。

Hermes Agent 对超大结果做的不是"一刀切的文件化",而是 三层防线tools/tool_result_storage.py 模块文档开头就明确写出):

复制代码
Defense against context-window overflow operates at three levels:

1. Per-tool output cap (inside each tool): Tools like search_files
   pre-truncate their own output before returning.

2. Per-result persistence (maybe_persist_tool_result): After a tool
   returns, if its output exceeds the tool's registered threshold
   (registry.get_max_result_size), the full output is written INTO
   THE SANDBOX temp dir (e.g. /tmp/hermes-results/{tool_use_id}.txt)
   via env.execute(). The in-context content is replaced with a
   preview + file path reference.

3. Per-turn aggregate budget (enforce_turn_budget): After all tool
   results in a single assistant turn are collected, if the total
   exceeds MAX_TURN_BUDGET_CHARS (200K), the largest non-persisted
   results are spilled to disk until the aggregate is under budget.
第一层:工具内部的输出上限

每个工具自己负责截断输出。比如 search_files 内部就有 limit 参数,本来就不会一次返回太多行。这是工具作者可控的第一道防线。

第二层:maybe_persist_tool_result() 按工具阈值持久化

每个工具在 registry.register() 时可以指定 max_result_size_chars。上面 file_tools.py 的例子里:

  • read_filefloat('inf')(不限制------因为用户就是在主动读文件,截断反而误事)

  • write_file / patch / search_files100_000 字符

当 handler 返回的结果超过阈值时,maybe_persist_tool_result()tools/tool_result_storage.py:116)会:

  1. 通过 env.execute() 把完整内容写入 sandbox 内部 的临时目录(/tmp/hermes-results/{tool_use_id}.txt,在 Termux 上会换成 $TMPDIR/hermes-results/...)。

  2. 返回给模型的是一个 <persisted-output> 标签块------包含预览前若干字符 + 文件大小 + 完整文件路径:

    This tool result was too large (523,891 characters, 511.6 KB). Full output saved to: /tmp/hermes-results/call_abc123.txt Use the read_file tool with offset and limit to access specific sections of this output.

    Preview (first 4000 chars):
    ... (实际前 4000 字符) ...
    ...

注意是走 env.execute() 写文件------这样无论后端是 local、Docker、SSH、Modal 还是 Daytona,模型后续用 read_file 都能访问到。

第三层:enforce_turn_budget() 整轮预算

即使单个结果都不超阈值,多个"中等大小"的结果叠加也可能超出。enforce_turn_budget() 在一轮所有工具结果收集完后检查总字符数,超过 MAX_TURN_BUDGET_CHARS = 200000 时,把最大的几个未持久化结果 spill 到磁盘,直到总量回到预算内。

这三层层层兜底的设计,是比"硬截断"或"简单文件化"都更健壮的方案。 它把"一次性大量阅读"变成了"按需分页阅读"------跟人类工程师看代码的方式一样。

第六站:回到模型

处理后的 tool result 被追加到 messages 数组里,连同之前的对话历史一起,再次发给模型。模型看到内容后,生成回复。如果它觉得还需要更多信息,会再发起新的 tool_calls------循环继续。

把源码里的"主路 + 旁路"一起摊开,完整图景更接近下面这样:

复制代码
用户消息
   │
   ▼
┌──────────────────────────────┐
│ 1. registry.get_definitions()│ ← check_fn 过滤可用工具
│    构建 tools 数组             │
└────────────┬─────────────────┘
             ▼
┌──────────────────────────────┐
│ 2. 模型推理                    │ ← messages + tools → response
│    决定调用哪个工具             │
└────────────┬─────────────────┘
             │ tool_calls
             ▼
┌──────────────────────────────┐
│ 3. coerce_tool_args()        │ ← 类型强转
│    参数修正                    │
└────────────┬─────────────────┘
             ▼
┌──────────────────────────────┐
│ 4. _invoke_tool() 分流点      │ ← agent-level / plugin / registry
│    主路进入 registry.dispatch() │
└────────────┬─────────────────┘
             │ JSON 字符串
             ▼
┌──────────────────────────────┐
│ 5. 三层防线                   │ ← 工具内截断 / 单结果持久化 / 整轮预算
│    结果处理                    │
└────────────┬─────────────────┘
             │ tool message
             ▼
┌──────────────────────────────┐
│ 6. 追加到 messages           │ → 回到第 1 步(下一轮)
│    回到模型                    │
└──────────────────────────────┘

50+ 内置工具分类详解

完整链路拆完了,我们回来看一下 50+ 内置工具的全貌。不是逐个列举------那样没意义------而是按设计意图分类,帮你理解"为什么是这些工具"。这里说的仍然是主干工具面,动态插件工具我们放到后面的扩展章节再展开。

第一类:基础操作

复制代码
read_file / write_file / patch / search_files / terminal / process

这是 Agent 最基础的"手脚"。你无法想象一个 Agent 不能读文件、不能跑命令。这些工具的 check_fn 主要检查"文件系统/执行环境是否可用"------在标准 Linux/macOS 上几乎总是 True

值得细说的是 patch 工具 。它不是简单的"写文件"------它接受一个 diff/边界字符串格式的补丁,只修改文件的指定部分。为什么不直接用 write_file?因为模型生成完整文件内容需要输出大量 token,而生成一个 diff 只需要输出变更的那几行。 patch 是 Token 效率的设计------用更少的输出 token 完成相同的修改。

还有 process 工具 ------管理后台执行的长时间命令(启动、查看、停止),这样 Agent 不需要等 npm install 跑完 30 秒才能继续干别的事。

search_files 是 grep 和 find 的合体:参数里 target="content" 搜内容,target="files" 搜文件名。

第二类:信息获取(条件加载)

复制代码
web_search / web_extract / browser_navigate / browser_snapshot / browser_* / vision_analyze / image_generate

这一类的共同特征是:依赖外部服务或环境web_search/web_extract 需要搜索/抽取后端的 Key(支持 Exa、Firecrawl、Parallel、Tavily 等多种 provider);浏览器系列需要浏览器后端(本地 Playwright 或远端 Browserbase);vision_analyze 需要支持 vision 的模型通道。

它们的 check_fn 都在做严格的前置检查。如果条件不满足,模型根本不知道这些工具的存在------这比"注册了但调用失败再报错"好得多,因为后者浪费了一整轮推理。

第三类:自我管理(Agent 内省)

复制代码
memory / session_search / skills_list / skill_view / skill_manage / delegate_task / todo

这一类工具让 Agent 能够管理自身的状态。这是 Hermes Agent 区别于大多数 Agent 框架的核心------它不只是帮你做事的工具,它还能:

  • memory:主动决定什么信息值得记住、什么记忆应该更新。

  • session_search:搜索过去的会话历史,找到"我们之前是怎么处理这个问题的"。

  • skill_manage / skills_list / skill_view:浏览、查看、管理 Hermes Agent 的 skill 系统。

  • delegate_task:把子任务委派给一个独立的 Agent 实例执行。

  • todo:维护一个轻量的待办清单,跨轮次保持规划状态。

第四类:其他工具与 MCP 扩展

源码中还注册了一批常被忽略但实用的工具:clarify(主动向用户澄清问题)、cronjob(管理定时任务)、send_message(跨平台消息网关)、ha_*(Home Assistant 智能家居控制)、execute_code(代码执行沙箱)、mixture_of_agents(多 Agent 混合推理)、rl_*(RL 训练相关工具)。

MCP 工具没有固定列表------完全取决于你连接了哪些 MCP 服务器。连一个 GitHub MCP 服务器,你就多出对应的工具;连一个数据库 MCP 服务器,你就多出数据库查询工具。

MCP 工具的存在让 Hermes Agent 的能力边界不再由框架决定,而由你的配置决定。 第 17 讲我们会深入 MCP 集成的实现细节。


一个工程上的细节:每工具独立的结果阈值

再回头看 registry.register() 中的 max_result_size_chars 参数------这是一个很容易被忽视但非常务实的设计。

不同工具的"合理输出大小"差异巨大:

工具 max_result_size_chars 为什么
read_file float('inf') 用户主动读文件,截断反而误事(靠第三层预算兜底)
write_file 100_000 通常只需要返回 "succeeded" 之类的确认
patch 100_000 同上
search_files 100_000 搜索结果本来就有 limit,100K 够用了

进一步,tools/budget_config.py 还支持更细的配置:

复制代码
Priority: pinned -> tool_overrides -> registry per-tool -> default.

也就是说,用户可以在 config.yaml 里 pin 住某个工具的阈值,覆盖注册时的默认值。

把"阈值"从硬编码变成多层可配置------这是生产系统对"不同场景有不同合理值"现实的回应。 你不应该相信单一常量能解决所有问题。


动手实战:理解工具注册的骨架

我们不写一个完整的自定义工具(那是后面章节的内容),但我们可以先看懂骨架。

一个最简工具的完整注册

假设你要做一个 count_lines 工具------统计文件行数并返回:

复制代码
# tools/count_lines_tool.py

import json
from tools.registry import registry

COUNT_LINES_SCHEMA = {
    "description": (
        "Count the number of lines in a file. Use this when you need "
        "a quick size estimate without reading the full content."
    ),
    "parameters": {
        "type": "object",
        "properties": {
            "path": {
                "type": "string",
                "description": "Absolute path to the file",
            },
        },
        "required": ["path"],
    },
}


def _handle_count_lines(args, **kwargs):
    path = args.get("path")
    if not path:
        return json.dumps({"error": "path is required"})
    try:
        with open(path, "r", encoding="utf-8", errors="replace") as f:
            content = f.read()
        line_count = content.count("\n") + (
            1 if content and not content.endswith("\n") else 0
        )
        return json.dumps({"ok": True, "path": path, "lines": line_count})
    except FileNotFoundError:
        return json.dumps({"error": f"File not found: {path}"})
    except PermissionError:
        return json.dumps({"error": f"Permission denied: {path}"})


def _check_count_lines_reqs():
    # 与 file_tools 一致:依赖文件系统能力
    from tools import check_file_requirements
    return check_file_requirements()


registry.register(
    name="count_lines",
    toolset="file",              # 归到现有的 file toolset
    schema=COUNT_LINES_SCHEMA,
    handler=_handle_count_lines,
    check_fn=_check_count_lines_reqs,
    emoji="🔢",
    max_result_size_chars=10_000,  # 返回值很短,阈值设小点
)

注意几个约定:

  1. Handler 返回 JSON 字符串 ,用 json.dumps(...) 序列化。成功可以用任意结构;失败统一用 {"error": "..."}。Agent 主循环会识别 error 字段并进入错误恢复流程。

  2. Handler 通常是同步的。 除非你确实在做异步 IO------那时候把 is_async=True 加到 register() 调用里,registry 会在 dispatch 时自动通过 _run_async() 桥接。

  3. **description** 要写给模型看。 不是给人看的 docstring------模型根据 description 决定要不要调这个工具。写得好,模型调得准;写得差,模型要么不调,要么调错场景。工具的 description 是一种 prompt engineering。

一个反直觉的建议

如果你要自己写工具,description 里不要描述实现细节,要描述"什么场景下该用这个工具"。

不好的:

复制代码
"description": "Reads a file using the file_manager.read() method with offset and limit parameters"

好的:

复制代码
"description": "Read the contents of a file at the given path. Use this when you need to examine source code, configuration files, or any text file. For large files, use offset and limit to read specific sections."

前者是给程序员看的,后者是给模型看的。模型不关心你用什么方法读文件,它关心的是"我现在需要看一个文件的内容,该调哪个工具?"

相关推荐
X54先生(人文科技)1 小时前
《元创力》纪实录·桥段刻舟遗碑:当“唯一解”的文明抵达终点
人工智能·开源·开源协议·零知识证明
bryant_meng1 小时前
【SAMv1】 The “Segment Anything” Revolution in Computer Vision
人工智能·深度学习·计算机视觉·大模型·sam·分割一切
百度Geek说1 小时前
用数据说话:贴吧 AI CR(小码哥)落地 10 周,bug密度下降 66.87%
人工智能
码农小白AI1 小时前
电子原始记录进入“可审计时代”:AI 报告审核如何给出标准答案,IACheck重塑实验室数智化底层逻辑
人工智能
老鱼说AI1 小时前
统计学习方法第五章:从浅入深解析决策树
人工智能·深度学习·算法·决策树·机器学习·学习方法
zhangfeng11331 小时前
llamafactory 0.6.3 没有 llamafactory-cli
人工智能·机器学习
KaMeidebaby1 小时前
卡梅德生物技术快报|蛋白修饰调控 NETosis 分子机制及实验研究进展
前端·数据库·人工智能·算法·百度
十铭忘1 小时前
个人Agent实践方案
人工智能
Luminbox紫创测控1 小时前
太阳模拟器自动化测试系统:稳态、脉冲、闪光光源的控制与数据采集
人工智能·测试工具·测试标准