问题:工具一多就散

早期我习惯一个动作一个工具:

search_notes
read_note
create_note
update_note
delete_note
list_notes

领域一多,工具列表立刻膨胀。带来的问题很实际:

  1. 模型选错工具:名字相近,描述重叠
  2. 参数约定分散:每个工具一套字段,风格不统一
  3. 上下文更长:工具定义本身就吃 token
  4. 维护成本高:加一个动作就要加一个工具并改提示词

我后来改用一个更省事的做法:同领域合并,用 type 切换动作。

我现在的约定

工具名 {"type": "...", ...其它数据字段}

规则只有四条:

  1. 一个领域 = 一个工具(notes / orders / configs…)
  2. type 只能是英文(见下表)
  3. 数据参数挂在顶层(等于传统 **arys)
  4. help 是必须动作:先问用法,再填业务参数

type 取值

type含义典型参数
help帮助 / 用法说明无(或可选 topic)
search搜索query, 可选 limit
get按 id 查一条id
list列表可选 limit, offset
create新增领域字段,如 title, content
update修改id + 要改的字段
delete删除id

中文口语里我会说「增查改删搜索帮助」,但 落进 schema 的 type 一律英文,避免模型中英混填、也方便日志检索。

一个工具长什么样

from langchain.tools import tool

HELP = """notes tool
type must be one of: help | search | get | list | create | update | delete

help:
  {} 
search:
  {"type":"search","query":"...","limit":5}
get:
  {"type":"get","id":"n-01"}
list:
  {"type":"list","limit":10,"offset":0}
create:
  {"type":"create","title":"...","content":"...","tags":["agent"]}
update:
  {"type":"update","id":"n-01","content":"..."}
delete:
  {"type":"delete","id":"n-01"}

Rules:
- Call help first if unsure.
- Never invent note ids.
- delete is destructive; confirm with user when possible.
"""

@tool
def notes(type: str, query: str | None = None, id: str | None = None,
          title: str | None = None, content: str | None = None,
          tags: list[str] | None = None, limit: int | None = None,
          offset: int | None = None) -> str:
    """Single tool for personal notes: help/search/get/list/create/update/delete.

    Always start with type. If unsure, set type to help.
    """
    if type == "help":
        return HELP
    if type == "search":
        return do_search(query or "", limit or 5)
    if type == "get":
        return do_get(id or "")
    if type == "list":
        return do_list(limit or 10, offset or 0)
    if type == "create":
        return do_create(title or "", content or "", tags or [])
    if type == "update":
        return do_update(id or "", content, title, tags)
    if type == "delete":
        return do_delete(id or "")
    return f"ERROR: invalid type '{type}'. Call help for allowed values."

要点:

  • 工具名只有一个:notes
  • 模型看到的「能力面」很窄
  • 真正的能力在 type 分支里,由我们控制
  • 非法 type 直接返回可行动错误,而不是抛异常炸整轮

为什么 help 必须有

帮助不是装饰。它的作用是:

  1. 在工具列表变少后,仍能恢复细节
  2. 给出精确示例参数,降低瞎猜
  3. 写死危险规则(如 delete 要确认)
  4. 错误时可自纠:模型再调一次 help 就能改参

系统提示里我会写一句:

If unsure about a tool's parameters, call that tool with type=help before guessing.

和「一动作一工具」对比

维度多小工具type 合并工具
工具数量多少
选错工具更常见更少见
加新动作新工具 + 新描述加一个 type 分支
参数一致性易漂移易统一
风险分散集中在一处治理

合并的风险也存在:一个工具塞太满会变成「万能巨石」。
所以我仍坚持:只合并同一领域,不要把 notes + payments + email 揉成一个工具。

实用边界

适合合并:

  • 同一实体的 CRUD + search
  • 领域稳定、动作可枚举
  • 希望减少工具定义 token

不适合合并:

  • 动作语义差很远(只读检索 vs 不可逆外发)
  • 权限模型不同,最好拆开
  • 领域还在狂变,分支会失控

高危动作即使合并进大工具,也要在实现里做确认或降权。

调用示例

notes {"type":"help"}
notes {"type":"search","query":"ReAct loop"}
notes {"type":"get","id":"n-03"}
notes {"type":"create","title":"工具合并笔记","content":"用 type 收敛..."}
notes {"type":"update","id":"n-03","content":"补充 help 规则..."}

日志里只出现一个工具名,但 type 仍可统计、可告警。

结论

工具多的时候,我一般不先删能力,而是:

  1. 按领域合并成一个工具
  2. type 用英文枚举(help/search/get/list/create/update/delete)
  3. 数据参数放顶层
  4. help 必调,错误返回可行动提示

这样工具面更干净,模型更不容易选错,我们也好维护。
少工具不是目的,可控的能力面才是。