Agent AtlasAI Agent 知识库
02.04提示与上下文 · Prompt & Context

工具设计与 Function Calling

工具是 Agent 的手脚。命名、参数、返回值、错误信息和权限,每一个细节都会改变成功率。

工具是 Agent 的手脚。模型再聪明,如果工具描述含糊、参数反直觉、错误信息无用,它也只能在错误的路径上反复尝试。

工具是给模型用的 API

给人用的 API 文档可以假设读者会读完整页、会查错误码、会理解业务背景。给模型用的工具没有这些假设:

  • 它可能只看名称和一行描述就决定调用;
  • 它不会「打开文档」;
  • 它对参数的含义只能从字段名和描述推断;
  • 它会把错误信息当成下一步的依据。

所以工具设计的目标不是「功能齐全」,而是在有限信息下做出正确选择并正确调用。

一个坏工具长什么样

python
{
  "name": "do_stuff",
  "description": "处理订单相关操作",
  "parameters": {
    "type": "object",
    "properties": {
      "data": { "type": "string" }
    }
  }
}

问题一目了然:

  • 不知道「处理」是查询、修改还是退款;
  • data 是 JSON、ID 还是自然语言?
  • 模型什么时候该用它?什么时候不该用?
  • 失败了会怎样?

模型只能猜。猜错之后,它往往会重试、换参数,浪费步数。

好工具的五个要素

图 1 · 工具契约的五个部分
名称动词+名词,见名知意
→
描述何时用 / 不用 / 副作用
→
参数少、扁平、可枚举
→
返回稳定结构 + 摘要
→
错误可操作、可判断是否重试

1. 名称:动词 + 名词

text
✓ search_orders        ✓ create_refund       ✓ get_invoice_pdf
✗ orderTool            ✗ handle             ✗ process2

名称是模型最先看到的信息。search_ 暗示只读,create_ 暗示写入,delete_ 暗示危险。不要用模糊的 manage_、handle_。

同一领域用统一前缀,便于模型区分:

text
orders_search / orders_get / orders_cancel
docs_search / docs_read / docs_list

2. 描述:写清楚「不用」比「用」更重要

一个可用的模板:

text
用途:查询订单状态与物流信息。
何时使用:用户提供了订单号,且问题涉及配送、签收、退款进度。
何时不要使用:用户没有订单号时,先询问;需要修改地址时用 update_shipping。
参数:order_id 为 8 位订单号(如 A1024B7C),不是用户 ID。
返回:status、carrier、tracking_no、last_event、eta。
副作用:只读,可安全重试。

尤其要写「何时不要使用」。很多误用不是因为模型不知道这个工具能做什么,而是因为它不知道还有更合适的工具。

3. 参数:越少越好,越具体越好

原则说明
参数数量 ≤ 5超过 5 个,模型漏填/填错的概率明显上升
扁平结构避免 {filter: {date: {from, to}}},改成 date_from / date_to
枚举优先status 用 enum,不要让模型自由发挥
ID 不让模型编先搜索拿到 ID,再传给写操作
单位写清楚amount_cents 比 amount 更不容易错
默认值少用默认值会掩盖模型没想清楚的情况

一个常见错误是让模型传 user_id。模型不知道当前用户是谁,也不该知道。用户身份应该由服务端从会话中注入,而不是让模型填写。

4. 返回值:为模型裁剪,而不是原样透传

工具返回的对象通常有两类消费者:模型和程序。给模型的版本应该:

  • 短:只保留决策需要的字段;
  • 稳定:字段名和结构不要随 API 版本漂移;
  • 带摘要:一句话说明结果,便于模型快速理解;
  • 带分页:结果多时给 next_cursor,而不是一次返回 500 条;
  • 带来源:文档类返回 source、page、updated_at。
json
{
  "summary": "找到 3 个订单,最近一笔已签收。",
  "items": [
    { "id": "A1024B7C", "status": "delivered", "total": 299.0, "currency": "CNY" }
  ],
  "next_cursor": null,
  "truncated": false
}

如果底层 API 返回 50KB,先在工具层裁剪。让模型读原始 JSON,既贵又容易迷失。

5. 错误:让模型知道下一步该做什么

错误不是异常,是观察。设计一个稳定的错误结构:

json
{
  "ok": false,
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "未找到订单 A1024B7C。请确认订单号,或让用户提供下单手机号后四位。",
    "retryable": false,
    "details": { "order_id": "A1024B7C" }
  }
}

错误分类建议:

类型例子模型应该做什么
参数错误日期格式不对、金额为负修正参数后重试
未找到订单不存在询问用户,不要反复猜
权限不足无权访问该账户停止,转人工
暂时性错误超时、503退避重试一次
限流429等待或降级
业务拒绝超出退款政策解释原因,不要换工具绕过

关键字段是 retryable。没有它,模型只能凭感觉重试,或者直接放弃。

不要把堆栈或 SQL 错误直接返回给模型

它们既占 token,又可能泄露内部结构。返回人可以理解的错误码和修正建议,详细堆栈写进服务端日志。

副作用:读与写必须分开

工具按风险分级:

级别例子策略
只读搜索、查询、读文件可自由调用,可并行,可缓存
可逆写创建草稿、加标签记录审计,失败可回滚
不可逆写退款、发邮件、删数据幂等 + 审批 + 限额
高危改权限、转账、执行任意代码沙箱 + 人工 + 多重校验

具体做法:

  • 读写工具分开命名,不要一个 manage_order 包办;
  • 写操作要求幂等键(idempotency_key),重试不会重复执行;
  • 提供 dry_run 参数,先返回「将会发生什么」;
  • 金额、数量、目标账户由服务端二次校验;
  • 不可逆动作返回 requires_approval,由工作流挂起等待人工。
python
def create_refund(order_id: str, amount: float, idempotency_key: str, dry_run: bool = False):
    order = db.get_order(order_id)
    if order is None:
        return err("ORDER_NOT_FOUND", "订单不存在,请先确认订单号", retryable=False)
    if amount > order.paid_amount:
        return err("AMOUNT_EXCEEDS_PAID", f"退款金额不能超过实付 {order.paid_amount}", retryable=False)
    if amount > 500:
        return {"ok": False, "requires_approval": True, "reason": "金额超过自动退款上限"}
    if dry_run:
        return {"ok": True, "preview": {"order_id": order_id, "amount": amount}}
    result = payments.refund(order_id, amount, key=idempotency_key)
    return {"ok": True, "refund_id": result.id, "eta_days": result.eta_days}

工具数量:少而清晰

工具越多,选择空间越大,误用概率越高。经验值:

  • 单一 Agent 5–15 个工具比较舒适;
  • 超过 20 个,考虑分组、动态加载或拆分子 Agent;
  • 功能重叠的工具(search_docs 和 find_documents)必须合并。

动态工具加载的思路:先让模型选择「领域」,再只暴露该领域的工具。这样每次上下文里的工具定义更少,选择更准。

并行调用:把独立性写进描述

模型不会自动知道两个调用可以并行。可以在工具描述或系统提示中说明:

text
查询多个独立订单、多个城市天气、多个文档时,可以在一次回复中并行调用;
有依赖的调用(例如先搜索再读取)必须串行。

宿主侧要正确处理:

  • 同时发起多个调用;
  • 分别捕获成功/失败;
  • 按 tool_call_id 合并结果;
  • 部分失败时返回每个调用的状态,而不是整体失败。

工具也需要测试

工具的质量不能靠「上线看看」。至少做三类测试:

  1. 契约测试:给定参数,返回结构是否符合 Schema;错误码是否稳定。
  2. 选择测试:给模型 20 个典型请求,看它是否选对工具、填对参数。统计混淆矩阵。
  3. 故障测试:超时、空结果、权限拒绝、部分失败时,Agent 是否能恢复或优雅停止。

一个很有效的做法是给每个工具准备 5 条「应该调用」和 5 条「不应该调用」的样本。工具描述每次修改都跑一遍,能提前发现歧义。

反模式

万能工具

一个 execute_sql 或 run_code 能完成所有事,但也意味着模型可以读任意表、执行任意代码。除非在强沙箱里,否则不要给这种能力。

参数让模型填身份

user_id、tenant_id、role 这类字段必须由服务端注入。让模型填,等于把越权入口交给提示注入。

返回整页 HTML

浏览器工具返回原始 HTML 会瞬间塞满上下文。先抽取正文、标题、关键链接,再返回结构化结果。

错误只说「失败」

「操作失败」无法帮助模型修正。至少给出错误类型、原因和可尝试的下一步。

检查清单

  • 工具名是动词 + 名词,读写可区分
  • 描述包含用途、适用/不适用场景、返回、副作用
  • 参数 ≤ 5 个、扁平、枚举优先、单位明确
  • 身份与租户信息由服务端注入
  • 返回经过裁剪,带摘要、来源、分页
  • 错误结构化,带 code / message / retryable
  • 写操作幂等、限额、可审批、可 dry-run
  • 工具数量受控,重叠工具已合并
  • 每个工具有选择测试与故障测试
  • 工具调用有超时、日志与权限校验

小结

  • 工具是给模型用的 API,可发现性和可恢复性比功能数量重要。
  • 名称、描述、参数、返回、错误,五个部分都要为「模型能否正确决策」服务。
  • 读写分离、幂等、审批和沙箱,是副作用管理的底线。
  • 工具需要像代码一样测试,尤其是「不该调用」的场景。

下一篇进入架构:把这些零件组装成一个能循环的系统。

读完了?进度会保存在本地浏览器。
内容会持续修订。发现错误或想补充,欢迎反馈。 遇到问题?查排查手册 →