跳到主要内容

工具描述与发现

本节定位

很多人做 Agent 时,会先把工具函数接好,然后让模型自己选。
结果很快就会发现:

  • 工具很多时容易选错
  • 名字相近的工具容易混淆
  • 参数不知道该怎么填

问题往往不在模型“不会调用”,而在于:

工具本身没有被描述清楚。

所以工具层真正的第一步,不是执行,而是:

  • 描述
  • 注册
  • 发现

学习目标

  • 理解为什么工具元数据会直接影响调用质量
  • 学会设计更清楚的工具描述结构
  • 理解工具发现是如何把“用户需求”映射到“候选工具”的
  • 通过可运行示例看懂一个最小工具注册与发现系统

一、为什么工具不能只靠函数名存在?

1.1 对程序员来说够清楚,对模型不一定

比如下面两个函数名:

  • search_docs
  • search_policy

人类工程师也许很快能看出差别,
但模型并不知道:

  • 哪个更适合查退款规则
  • 哪个更适合查知识库文章
  • 两者参数是否一样

如果缺少描述,模型看到的只是两个看起来相近的名字。

1.2 工具描述本质上是在降低歧义

一个好工具描述,至少应该回答:

  1. 这个工具是干什么的
  2. 它适合什么场景
  3. 需要哪些参数
  4. 返回什么结构
  5. 权限和风险等级如何

这些信息越清楚,
模型就越容易做出稳定选择。

1.3 一个类比:商场导购比货架编号更重要

工具注册表很像商场导购手册。

  • 函数名像货架编号
  • 描述像导购说明

只有编号,没有说明,
用户和模型都很容易找错东西。


二、一个工具描述至少应包含什么?

2.1 名字要体现用途,而不是只体现实现细节

例如:

  • query_42 很差
  • search_refund_policy 更好

因为模型在选择工具时,更依赖语义而不是实现细节。

2.2 描述要写清“什么时候用”

不要只写:

  • 查询政策

更好的写法是:

  • 查询退款、发票、地址修改等售后政策类规则,不适合查询订单实时状态

这能直接降低误调用。

2.3 参数说明要回答“如何填”

例如:

  • 参数名是什么
  • 类型是什么
  • 举例值是什么
  • 是否必须传

2.4 返回结构最好也有约定

如果工具返回结构完全随意,
模型和调度器后面都很难稳定处理。

所以最好明确:

  • 成功时字段
  • 失败时字段
  • 错误码或错误类型

三、先跑一个真正像样的工具注册表示例

下面这段代码会做三件事:

  1. 注册工具元数据
  2. 根据 query 和 tags 做最小发现
  3. 返回候选工具列表

它比只打印一个工具数组更有教学意义,因为它已经体现出:

  • “工具描述”如何参与决策
from collections import Counter

TOOL_REGISTRY = [
{
"name": "search_refund_policy",
"description": "查询退款、发票、地址修改等售后政策规则",
"tags": ["policy", "refund", "invoice", "after_sales"],
"required_args": ["keyword"],
"returns": ["policy_text"],
"risk_level": "low",
},
{
"name": "get_order_status",
"description": "查询订单当前状态,例如未发货、已发货、已签收",
"tags": ["order", "status", "shipping", "after_sales"],
"required_args": ["order_id"],
"returns": ["order_status"],
"risk_level": "medium",
},
{
"name": "calculator",
"description": "做确定性数值计算,例如加减乘除和折扣金额计算",
"tags": ["math", "price", "discount", "calculation"],
"required_args": ["expression"],
"returns": ["result"],
"risk_level": "low",
},
]


def discover_tools(query, registry, top_k=2):
words = query.lower().replace("?", "").replace("?", "").split()
scored = []

for tool in registry:
text = " ".join([tool["name"], tool["description"], " ".join(tool["tags"])]).lower()
score = sum(word in text for word in words)
scored.append((tool["name"], score))

scored.sort(key=lambda item: item[1], reverse=True)
return scored[:top_k]


queries = [
"退款政策是什么",
"订单现在发货了吗",
"299 打 8 折再减 5 等于多少",
]

for query in queries:
print(query, "->", discover_tools(query, TOOL_REGISTRY))

3.1 这段代码到底在教什么?

它在教两件特别重要的事:

  1. 工具不是“裸函数”,而是带元数据的对象
  2. 工具发现本质上是在“需求”和“工具描述”之间做匹配

3.2 为什么 tags 很有用?

因为用户不一定会用和工具名完全一样的词。
例如:

  • 用户说“发货了吗”
  • 工具名里可能叫 get_order_status

如果没有 tags,发现阶段就容易漏掉候选工具。

3.3 为什么这里只返回候选,而不是直接执行?

因为“发现”只是第一步。
它解决的是:

  • 哪些工具值得进入候选集

后面通常还要继续做:

  • 参数补全
  • 工具选择
  • 执行和校验

四、真实系统里“发现”通常不止一种方式

4.1 关键词 / 标签匹配

这是最直观的一层,优点是:

  • 简单
  • 可解释

缺点是:

  • 语义泛化弱

4.2 向量检索式工具发现

当工具很多时,
常见做法会变成:

  • 把工具描述做成 embedding
  • 对用户意图做向量匹配

这样更适合:

  • 工具数量大
  • 工具描述比较长

4.3 显式路由规则

在一些高风险系统里,
甚至不会把工具发现完全交给模型,
而会先加规则:

  • 订单类请求先看订单工具
  • 删除类操作必须进人工确认

这说明工具发现不是纯召回问题,
也是策略问题。


五、返回结构为什么也属于“工具描述”的一部分?

5.1 因为发现不只是“找到工具”,还要知道能不能接上后续流程

例如:

  • search_refund_policy 返回 policy_text
  • get_order_status 返回 order_status

如果后续系统需要把它们整合到同一个答复里,
返回字段越清楚,后面越稳。

5.2 一个简单的统一返回约定

def normalize_tool_result(ok, data=None, error=None):
return {
"ok": ok,
"data": data or {},
"error": error,
}


print(normalize_tool_result(True, data={"policy_text": "7 天内可退款"}))
print(normalize_tool_result(False, error="missing_order_id"))

统一返回结构的好处是:

  • 调度器更容易处理
  • 日志更容易分析
  • Agent 更容易读 observation

六、工具描述最容易踩的坑

6.1 误区一:函数签名清楚就够了

对程序员可能够,
对模型通常不够。

6.2 误区二:工具描述越短越好

太短会导致歧义。
描述真正重要的是:

  • 精准
  • 可区分

而不是一味短。

6.3 误区三:发现只要能召回一个工具就行

如果候选集质量差,
后面的选择和执行都会跟着差。

所以工具发现是系统质量的重要前置层。


小结

这节最重要的,不是记住多少字段名,
而是建立一个清楚判断:

Agent 之所以能稳定选工具,不是因为模型“神奇地懂了所有函数”,而是因为工具被描述成了可发现、可区分、可校验的对象。

只要这条主线建立起来,
后面你再学:

  • 工具路由
  • 工具安全
  • 多工具协作

就会知道为什么“先把工具描述清楚”是第一步。


练习

  1. 给示例里的注册表再加一个 search_faq 工具,看看它和 search_refund_policy 会不会产生混淆。
  2. 为什么说 tags 往往比工具名更适合做第一层召回?
  3. 想一想:一个高风险工具的描述里,除了用途和参数,你还会额外写什么?
  4. 如果工具越来越多,你会优先加强“工具描述”还是“工具执行器”?为什么?