🐸 青蛙编程网
首页 / AI / 智能体 Agent / 8. 给 Agent 装上手

8. 给 Agent 装上手:工具调用

这是什么:工具(Tool) = Agent 能调用的外部能力,模型通过函数名、描述和参数 schema 认识它。

为什么用:Agent 的能力上限由工具决定——模型看不见你的函数体,只能读工具描述;描述写不清,它就会瞎调。

如何用:本章把工具说明书、参数 schema、返回值格式和危险操作的刹车这四件事一次讲透。

8.1 工具描述是写给模型看的说明书

模型看不到你的函数体,它只能读 description。描述含糊,模型就会瞎调:

描述写法 模型的理解
"查天气" 不知道传什么参数,也不知道返回什么
"查询指定城市的当前天气。城市名用中文,如 '北京'。返回形如「北京:晴,26℃」的文本。" 知道传什么、传什么格式、期待什么结果

写描述的三个要点:

  1. 说清边界:什么情况下该用、什么情况下不该用
  2. 说清参数:格式、取值范围、单位
  3. 说清返回:模型会照着你的描述复述结果
{
    "name": "query_db",
    "description": "查询订单表。status 取值限 'pending'/'paid'/'cancelled';"
                   "返回最多 20 条记录的 JSON 字符串,字段为 id、amount、created_at。",
    "parameters": {
        "type": "object",
        "properties": {
            "status": {"type": "string", "enum": ["pending", "paid", "cancelled"]},
            "limit": {"type": "integer", "description": "返回条数,默认 20,最大 100"}
        },
        "required": ["status"]
    }
}

enum 是个好用的东西——它把可选值直接告诉模型,模型基本不会传错。

8.2 参数设计:宁可多问一句

模型传错参数是常事。两种防御:

① 用 Schema 约束(类型、枚举、必填、默认值):

"parameters": {
    "type": "object",
    "properties": {
        "days": {"type": "integer", "description": "未来几天,1-30", "default": 3},
        "city": {"type": "string", "description": "城市名"}
    },
    "required": ["city"]
}

② 在函数里兜住脏数据,别让异常直接把 Agent 掀翻:

def query_db(status: str, limit: int = 20):
    limit = max(1, min(limit, 100))          # 夹紧范围
    status = status.strip().lower()
    if status not in ("pending", "paid", "cancelled"):
        return f"status 只能是 pending/paid/cancelled,收到的是 {status!r}"
    ...

返回一句人话让模型自己纠正,比抛异常好:

return "没找到该城市,请确认城市名后重试"     # 模型会追问或换参数

8.3 返回值:给模型看什么,不给什么

模型读得懂自然语言,读不懂冗长的原始数据。

做法 效果
直接返回 API 的完整 JSON 噪音大,token 烧得快,模型容易看错重点
返回裁剪过的关键字段 + 人类可读的格式 ✅ 模型抓重点快,回答自然
# ❌ 噪音
return json.dumps(data, ensure_ascii=False)          # 可能几千 token

# ✅ 裁剪 + 结构化 + 人类可读
lines = [f"订单 {o['id']}:{o['amount']} 元,状态 {o['status']}" for o in rows]
return f"共 {len(rows)} 条:\n" + "\n".join(lines)

还有一个常被忽略的点:结果里要带上「规模」信息。模型不知道返回了 20 条还是 20000 条,就没法判断要不要再查。

8.4 危险工具:给 Agent 装个刹车

能读文件、跑命令、发请求的工具,一旦被诱导就可能造成真实损失。三道防线:

① 分级:只读工具随便给,写/执行类工具要克制。

READ_ONLY = {"query_db", "read_file", "search_docs"}       # 无副作用
SIDE_EFFECT = {"send_email", "delete_file", "run_command"}  # 有副作用

② 人工确认:有副作用的操作,先问人。

if name in SIDE_EFFECT:
    ok = input(f"Agent 想执行 {name}({args}),确认?(y/N) ")
    if ok.strip().lower() != "y":
        result = "用户拒绝了这次操作。"
    else:
        result = run_tool(name, args)

③ 范围收紧:别把整台机器交出去。

def run_command(cmd: str) -> str:
    ALLOWED = {"ls", "cat", "git status", "git log", "pytest"}
    if not any(cmd.startswith(a) for a in ALLOWED):
        return f"命令不在白名单内,已拒绝:{cmd}"
    ...

这三条不是「以后再说」的事——Agent 拿到工具的那一刻,权限就已经给出去了。

8.5 工具太多也有问题

给 Agent 挂 30 个工具,它会开始「选择困难」,甚至调错。经验值:

  • 常用工具控制在 5~10 个
  • 工具多的时候按场景分组,让模型分批用
  • 把功能相近的工具合并成一个带参数的(比如「查天气」和「查空气质量」合成「查环境」)

8.6 常见翻车现场

报错 / 现象 原因 修法
模型老调错工具 两个工具描述太像 把「什么时候用我」写进 description
参数总是传错 Schema 太松 加 enum / type / 必填 / 默认值
工具一报错 Agent 就死 没兜住异常 try/except,把错误信息回传
结果太长烧 token 直接返回原始数据 裁剪 + 聚合
Agent 删了你的文件 没设人工确认 副作用操作加确认和白名单
模型忽略某个工具 描述没写清或和其他工具重叠 改描述,或合并工具

8.7 动手改

  1. 给你手上真实的工具写一版 description,故意写得很含糊,跑一次看模型怎么误解
  2. 给一个有多个可选值的参数加上 enum,对比加与不加的调用准确率
  3. 给你的循环加一条规则:同一个工具连续调两次就拒绝,看模型怎么反应

8.8 小结

  • 工具描述是写给模型看的说明书:边界、参数、返回三样都要写清
  • Schema 约束能挡住大部分参数错误,enum 最好用
  • 返回值裁剪 + 人类可读,并带上规模信息
  • 副作用工具要分级 + 人工确认 + 白名单
  • 工具控制在 5~10 个,多了模型会选错

下一章:记忆与上下文,让 Agent 记住东西、又不被上下文撑爆 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。