返回学习路线/阶段 03 · 给 Agent 一双手
LESSON 07 / 18

把 Python 函数变成工具

设计工具名称、参数、返回值与执行边界。

35 分钟 · 含动手练习进阶Tool Calling函数

学完这一课,你将能够

  • 实现一个带参数校验的工具
  • 通过白名单分发工具调用
  • 把执行错误转成结构化结果

学习目标

  • 将一个普通函数包装成具有稳定契约的工具。
  • 用工具白名单和参数校验控制执行范围。
  • 把工具错误返回给决策层,而不是让整个任务崩溃。

前置要求:Python 函数、JSON 校验和模型适配层。 本节只用本地数据,不调用模型或外部服务。

工具调用并不是模型执行了代码

模型通常生成一个工具调用提议,包括工具名、参数以及调用标识。 应用收到提议后,检查是否允许,再调用实际函数。 执行结果通过对应调用标识交还给模型,模型才能依据结果继续回答。

这条边界让程序保留控制权。 即使模型提出一个不存在的工具,或把数量写成负数,执行层也必须能拒绝。 不能把模型输出直接交给 eval、任意 shell 或任意网络地址。

好工具的输入和输出都具体

lookup_stock(product_id) 比 do_everything(query) 更容易理解和测试。 名称表达动作,参数表达必要输入,描述说明适用范围与限制。 返回值保持稳定结构,让成功、空结果和失败可以明确区分。

{
  "name": "lookup_stock",
  "description": "按商品 ID 查询当前库存,只读,不下单。",
  "inputSchema": {
    "type": "object",
    "properties": { "product_id": { "type": "string", "minLength": 1 } },
    "required": ["product_id"],
    "additionalProperties": false
  }
}

这里用 inputSchema 表达通用概念。 真实模型服务的工具定义字段可能不同,应由适配层转换。

写一个可运行的分发器

保存为 tools.py,运行 python tools.py。

CATALOG = {"python-book": 3, "agent-book": 0}

def lookup_stock(args):
    if not isinstance(args, dict) or set(args) != {"product_id"}:
        return {"ok": False, "error": "invalid_fields"}
    product_id = args["product_id"]
    if not isinstance(product_id, str) or not product_id:
        return {"ok": False, "error": "invalid_product_id"}
    if product_id not in CATALOG:
        return {"ok": False, "error": "product_not_found"}
    return {"ok": True, "product_id": product_id, "stock": CATALOG[product_id]}

TOOLS = {"lookup_stock": lookup_stock}

def dispatch(call):
    if not isinstance(call, dict):
        return {"ok": False, "error": "invalid_call"}
    name = call.get("name")
    if not isinstance(name, str) or name not in TOOLS:
        return {"ok": False, "error": "unknown_tool"}
    return TOOLS[name](call.get("arguments"))

calls = [
    {"name": "lookup_stock", "arguments": {"product_id": "python-book"}},
    {"name": "lookup_stock", "arguments": {"product_id": "missing"}},
    {"name": "delete_database", "arguments": {}},
    {"name": "lookup_stock", "arguments": {"product_id": ["python-book"]}},
]
for call in calls:
    print(dispatch(call))

预期只有第一条返回库存,其余返回不同的结构化错误。 第四条特别检查了类型,避免把列表直接用作字典键导致异常。

参数合法之后,还要判断权限

Schema 检查数据形状,业务系统检查是否有权执行。 例如 customer_id 是合法字符串,不代表当前用户能读取该客户信息。 账户作用域应由可信会话注入,而不是让模型任意提供。

只读查询和写入操作也需要不同策略。 发邮件、提交订单或修改文件前,应明确操作对象、影响范围和批准条件。 第五模块会实现一个绑定具体参数的确认流程。

工具返回什么最有用

优先返回完成任务需要的字段,避免把整个数据库对象塞回上下文。 涉及事实时附带来源或更新时间,方便后续回答标注依据。 错误最好附稳定错误码与简短说明;是否可重试应由明确策略决定。

外部工具返回的文本可能包含误导指令,仍应被视为数据。 工具有权限,不意味着工具返回的内容有权改写任务规则。

练习与验收

增加一个只读工具 list_products,返回商品 ID 列表。 它不接收业务参数,传入多余字段时应拒绝。

  • 两个工具都只能通过白名单调用。
  • 未知工具不会触发动态代码执行。
  • 参数错误、商品不存在和零库存有不同结果。
  • 每条工具结果都可以序列化为 JSON。

常见误区

工具越多不一定越好。重叠的名称和模糊描述会让选择更加困难。 先用两三个职责明确的工具完成小任务,再增加能力。 重试未知工具名也通常没有意义,需要修正决策或工具清单。

延伸阅读

下一课将把分发器放进一个会读取观察结果的循环。

让这一课,真正成为你的收获

完成练习后标记完成,也可以随时回来复习。

笔记与进度保存在当前浏览器,无需登录

AgentStudy · Learn by building.以理解为起点,以作品为答案