60天企业 AI 实战训练营 · 第 7 周 · Agent 工程

Function Calling
与 Tool 设计

模型只负责「点菜」,后厨必须由你掌控 —— 把企业系统能力封装成受控工具

Day 32 讲师 · 包学斌 2026 · 09 含实操 · 请自带电脑
user 订单到哪了? LLM tool_calls 只有参数 你的执行层 鉴权·校验·调API 日志·幂等 企业系统:SAP · CRM · WMS · 数据库 模型从未直接接触它们 提议 → 执行 → 回填 → 再作答 每一跳都留痕,每一步都可拒

按 → 或 空格键 翻页 · F 全屏

今天这堂课解决什么问题

课程目标

01
说清"谁在做事"
模型只输出 tool_calls 参数建议,真正执行的是你的代码 —— 这条边界说不清,后面全是事故。
02
写出可被正确选择的工具
命名、描述、参数、返回、错误、权限六要素 —— 让模型"选得对、填得准"。
03
把工具关进笼子
六道执行闸 + 写操作审批 + 错误契约;用 Golden Set 度量选择与拦截,而不是演示一次。
它在 60 天课程中的位置
Day 31 · 四种 AI 架构的区别已完成
Day 32 · Function Calling 与 Tool 设计今天
Day 33 · 多 Tool 路由、串并行调用下一课
Day 34 · Memory、Session 与上下文管理本周
Day 35 · Agent Planning 与 Human-in-the-loop本周
昨天选架构,今天造零件:Tool 是 Agent 唯一能"动手"的地方 —— 也是唯一能造成真实损失的地方。
互动 · 举手投票 + 点击选择

模型回复"已为您把订单改到苏州",谁真正改了地址?

凭直觉点一个 —— 这决定了你的安全设计从哪开始

点一个选项,看看它意味着什么 →
01
Part 01

模型不会做事
它只会点菜

Function Calling 的真实链路:两次模型调用,一次代码执行

tool_calls 两次请求 执行在你
Part 01 · 机制

一次工具调用,实际发生了四步

点一次走一步 —— 注意颜色:橙色是模型侧,绿色是你的代码侧

① 定义
把工具 schema 随请求发出
→
② 提议
模型返回 tool_calls
→
③ 执行
你的代码鉴权后调用
→
④ 回填
结果作为 tool 消息再请求
模型侧:只产生"文本"
它看到的是工具的名字、描述和参数 schema;输出的是"我想调用谁、参数填什么"。它没有网络、没有数据库连接、没有执行权。
代码侧:唯一有副作用的地方
鉴权、参数校验、超时、重试、幂等、审计 —— 全部在这里做。模型给的是不可信输入,不是命令。
"模型负责提议,代码负责执行,权限负责兜底。"
Part 01 · 报文

请求里带「菜单」,响应里只有「点单」

第一次请求 · 你发给模型
{ "model": "deepseek-chat", "messages": [{"role":"user", "content":"订单 A102 到哪了?"}], "tools": [{ "type": "function", "function": { "name": "get_order_status", "description": "查订单状态与物流节点", "parameters": { "type": "object", "properties": { "order_id": {"type": "string"}}, "required": ["order_id"]} }}] }
第一次响应 · 模型只回这个
{ "tool_calls": [{ "id": "call_7f3a", "type": "function", "function": { "name": "get_order_status", "arguments": "{\"order_id\":\"A102\"}" } }], "finish_reason": "tool_calls" } // arguments 是字符串,不是对象 // 它可能缺字段、类型错、编造 ID
关键:这不是执行结果。你的代码执行完,还要把 {role:"tool", tool_call_id, content} 发回去,模型才能生成最终回答 —— 所以是两次请求。
Part 01 · 对照

没有 Function Calling 时,大家怎么"土法炼钢"

不是不能用,是每一层都在赌模型"守规矩"

✗ 正则 / 关键词硬匹配意图 "帮我看看这单"匹配不到任何规则;问法一多变维护地狱
✗ 让模型输出一段 JSON,自己 split 解析 多一句解释、少一个引号就解析失败;字段漂移无人约束
✗ 在 Prompt 里写"请调用 queryOrder 接口" 模型不会真的发请求,只会编一个"调用成功"的回复
✓ 声明式 schema + 结构化 tool_calls 工具边界由 schema 定义,参数可校验,执行可拦截、可审计
模型更少跑偏
工具名和参数被 schema 钉死,不再自由发挥。
工程可测试
选择、参数、执行三层都能单独断言。
责任可归属
每一次执行都有 tool_call_id 可追溯。
互动 · 快问快答

三句话,测你有没有真懂

1. 模型能直接查我们的数据库吗?
Function Calling 让模型"知道有这个工具",但连接、SQL、权限都在你的执行层。
不能
2. 收到 tool_calls 就代表这次调用成功了?
它只是"申请单"。可能被鉴权拒绝、参数校验拦下、下游超时 —— 都要作为结果回填给模型。
不代表
3. 模型填的参数一定是对的吗?
可能缺必填、类型错、ID 编造、金额越界。参数校验不是防用户,是防模型。
不一定
记住这句:tool_calls 是"申请",不是"结果"。
实操 ① · 8 分钟 + 讲评 3 分钟

写第一个工具契约

1
打开 demo/tool_schema_lab.py,先看 TOOL 里已经给好的三个字段:name / description / parameters。
2
补全 get_order_status 的参数:order_id 必填、detail 用枚举(summary / full),再加一句"什么时候不该用"。
3
运行脚本,它会用 6 条模拟问句检查你的契约:该走的走到、不该走的没走。
4
故意删掉 required 再跑一次 —— 看模型生成的参数缺字段时,校验器怎么拦。
08:00
验收三条
□ 能说出 name / description / parameters 各影响什么
□ 枚举写对,模型不会填出 "detailed"
□ 缺必填时,校验器能给出明确错误
没有 API Key 也能跑:脚本内置模拟模型,输出是确定性的,方便你对答案。
02
Part 02

好工具
窄而清楚

工具设计不是写接口文档,是设计模型的决策空间

六要素 窄接口 Description 即路由
Part 02 · 契约

Tool 的六个字段,每一个都在替模型做决定

① Name
动词_名词_限定词
get_order_status、query_purchase_orders。全公司统一命名;避免 getX / queryX 同义混用。
② Description
何时用 / 何时不用
写清适用场景、与相似工具的区别、返回什么;50–200 字,附 1–2 个典型问句。
③ Arguments
少而精,带约束
类型、枚举、范围、默认值;必填项越少越好,可选参数给默认值。
④ Return
结构化且最小
统一 {status, data, message, trace_id};大结果摘要或分页,别让模型读一万行。
⑤ Error
错误也是契约
error_code + 可读 message + retryable。InputError / ExternalError / InternalError / RateLimit。
⑥ Permission
授权在工具内
按角色/租户配置可见工具子集;每次调用先鉴权,不能只信模型。
缺任何一项,模型就会用"猜"来补齐 —— 而猜错的成本由你的业务承担。
Part 02 · 粒度

一个"万能工具",是灾难的起点

✗ 万能工具
{ "name": "query_anything", "description": "查询企业数据", "parameters": { "sql": {"type": "string"}, "table": {"type": "string"} } }
模型直接写 SQL → 注入、越权、全表扫描
描述含糊 → 所有问题都"看起来能用"
无法按动作授权 → 只能给或不给
出错无法区分 → 全是 500
✓ 三个窄工具
get_order_status(order_id, detail) — 只读订单状态
get_order_items(order_id) — 只读明细行
create_refund(order_id, amount, reason, idempotency_key) — 写操作,带审批参数
窄接口的三个好处:模型选得准;每个动作能单独授权;失败能精确归因。粒度以"一个业务动作"为单位,不是以"一张表"为单位。
工具不是越强越好,是边界越清楚越好。
Part 02 · 描述

Description 就是模型的路由算法

模型看不到你的代码,只能看这段文字决定"叫不叫这个工具"

写法公式
做什么 + 什么时候用 + 什么时候不用 + 返回什么 + 1–2 个典型问句。
description: 查询订单的当前状态与物流节点。 当用户问"到哪了/发货没/签收没"时使用; 当用户问"订单里有什么商品"时不要用, 应改用 get_order_items。 返回状态码、节点列表和预计到达时间。 // 50–200 字;写差异,不抄字段名
✗ "查询订单信息"
信息 = 状态?明细?金额?模型只能猜 —— 猜错就是选错工具。
含糊
✗ "调用 WMS 的 /api/v2/order/status 接口"
这是给工程师看的,不是给模型看的;模型不知道"什么时候该用"。
抄接口
✓ "查订单状态与物流节点;问商品明细时改用 get_order_items"
能力 + 边界 + 替代方案,一句说清。
可路由
实测经验:工具语义重叠时,模型会在两个工具间"犹豫"甚至随机选。描述里写清"何时不用",比写十行参数说明更有效。
Part 02 · 参数

参数 schema 的四个约束,缺一不可

"parameters": { "type": "object", "properties": { "order_id": {"type": "string", "pattern": "^[A-Z]\\d{3,8}$"}, "detail": {"type": "string", "enum": ["summary", "full"], "default": "summary"}, "amount": {"type": "number", "minimum": 0.01, "maximum": 50000} }, "required": ["order_id"], "additionalProperties": false }
① 类型:别用 string 装一切
数字、布尔、日期分开声明;模型对类型的遵守度远高于对文字描述的遵守度。
② 枚举:把自由发挥关掉
detail 只有 summary/full,模型就不会造出 "detailed"、"完整"。
③ 范围:金额/日期必须夹住
退款金额 minimum/maximum;日期用 format: date 并校验不晚于今天。
④ 必填:少而精
必填越多,模型编造的概率越高;能推断的用默认值,不能推断的才 required。
注意:schema 是"约束模型"的第一道,不是最后一道。服务端仍要再校验一次 —— 模型可以不遵守 schema,攻击者更可以绕过模型直接打你的接口。
实操 ② · 10 分钟 + 讲评 4 分钟

把"万能工具"拆成三个窄工具

1
打开 demo/tool_design_lab.py。里面的 query_anything(sql) 是反面教材。
2
把它拆成 get_order_status / get_order_items / create_refund 三个工具,每个都写 name、description、parameters。
3
用脚本里的 12 条问句跑路由:看每条问题是否落到正确工具,以及"退款"类问题是否被标记为需要审批。
4
至少改一处 description,让两个容易混的问题("订单里有什么"/"订单到哪了")分开。
10:00
验收三条
□ 12 条问句路由全对
□ 三个工具没有语义重叠
□ 写工具带 idempotency_key 与审批标记
讨论:为什么不做一个 query_order(order_id, what) 用 enum 区分?—— 因为授权粒度、审计语义和失败处理都不同。
03
Part 03

工具是
Agent 的安全出口

执行侧六道闸:模型给的每一个参数,都当作攻击载荷

鉴权 参数校验 幂等 审计
Part 03 · 执行

tool_calls 进来,先过六道闸

顺序有讲究:先拒绝不该做的,再谈怎么做

1
工具白名单
只允许注册表里的工具;模型编出来的名字直接拒绝。
2
身份与权限
以"当前用户"身份执行,不用系统超管账号;按工具+数据范围鉴权。
3
参数校验
类型、枚举、范围、业务规则(金额≤订单金额);不合法立即返回错误。
4
超时与限流
每个工具独立超时;并发与 QPS 限流,防止 Agent 把下游打挂。
5
幂等
写操作带 idempotency_key;重试不会重复退款、重复下单。
6
审计日志
谁、何时、用哪个工具、什么参数、结果如何、耗时多少 —— 全链路可回放。
心法:把模型输出当作"外部用户输入" —— 你会怎么防前端传参,就怎么防 tool_calls。
Part 03 · 写操作

写操作的三条铁律

读错了只是答错,写错了是真金白银

1
读写分离:GET 不改状态,写操作单独成工具
查询工具可重试、可缓存;写工具必须单独授权、单独审计。绝不用一个 execute(sql) 同时承担读写。
2
写工具必带"审批参数"与"影响预览"
create_refund 不只要 order_id/amount,还要 approved_by 或 approval_token;执行前返回"将退 9999 元到原账户"的预览,而不是直接执行。
3
幂等键:同一请求重放一百次,只生效一次
由"业务动作 + 订单 + 金额 + 会话"生成稳定 key;服务端落库去重。网络超时重试、用户连点两次都不会重复退款。
真实事故形态
Agent 调 create_refund 超时 → 自动重试 → 第二次成功 → 用户收到两笔退款。审计里两条记录都"成功",没人发现。
工程解法
idempotency_key + 唯一索引 + "先查后写"事务;重试只对超时/限流,参数错与权限拒绝永不重试。
Part 03 · 失败语义

错误不是异常,是模型要读的"下一步提示"

{ "status": "error", "error_code": "ORDER_NOT_FOUND", "message": "未找到订单 A999,请让用户确认订单号", "retryable": false, "trace_id": "tr_8c21" } // 模型看到这段,会追问而不是编造 // 如果只抛 500 / 空字符串,模型只能猜
InputError · 参数错/缺字段
retryable=false。告诉模型哪个字段错了,让它向用户澄清。
ExternalError · 下游系统故障
retryable=true(有限次)。回填"系统繁忙,稍后再试",不要伪装成功。
InternalError · 你的代码 bug
retryable=false。记录 trace_id,向用户给通用话术,别泄露堆栈。
RateLimit · 限流
retryable=true,带 retry_after;指数退避 + jitter,最多 3 次。
反例:所有错误统一 {"error": "failed"} —— 模型无法区分"订单不存在"和"网络抖了一下",只能要么乱编、要么一律道歉。
实操 ③ · 10 分钟 + 讲评 5 分钟

给工具加上闸门,然后攻击它

1
打开 demo/tool_guard_lab.py,四个工具已注册;先跑一遍正常路径,确认全绿。
2
补上 参数校验(金额 0.01–50000、order_id 格式)与 权限检查(只能查自己部门订单)。
3
打开 ATTACKS 列表,逐条运行:越权查单、负数退款、重复退款、超大金额、注入 SQL。
4
把每条攻击的拦截结果记下来 —— 这就是你的第一张"工具安全回归表"。
10:00
验收四条
□ 5 条攻击全部被拦截
□ 错误码能区分"越权/参数/重复"
□ 正常请求不受影响
□ 每条拦截都有审计记录
关键:拦截率必须是 100%。漏一条,就等于没做。
04
Part 04

用数据验收
不靠演示

工具也要 Golden Set:选择、参数、执行、拦截,四个维度分开量

Golden Set 四项指标 拦截率红线
Part 04 · 度量

工具的四个指标,少一个都说不清好坏

指标 1
选择准确率
该调 A 却调了 B,或该调却答"我不知道"。目标 ≥ 95%。
指标 2
参数准确率
字段齐、类型对、枚举合法、ID 真实存在。目标 ≥ 90%。
指标 3
执行成功率
排除模型因素后,工具本身调下游的成功率。目标 ≥ 99%。
指标 4
危险调用拦截率
越权、越界金额、重复写。红线:必须 100%,一票否决。
Golden Set 怎么组(20–50 条)
12 条正常查询 + 8 条边界(空结果、模糊订单号、多义问法)+ 8 条应拒绝(越权/缺审批/危险参数)+ 5 条对抗(注入、诱导、伪造 ID)。
最常见的假验收
只测 happy path,10 条全过就宣布上线。真实流量里 30% 是模糊问法、5% 是越权尝试 —— 没测过的那部分,就是事故的来源。
Part 04 · 复盘 · 互动讨论

三大血泪坑

1
给 Agent 一个"万能 SQL 工具"
看起来最灵活,实际把注入、越权、全表扫描、慢查询全部打开;出了事无法按动作追责。
2
工具返回全量数据,直接塞回上下文
1000 行订单明细 → 上下文爆炸、成本飙升,模型还读不完;应在工具内聚合/分页,只回必要字段。
3
"演示时调对了"就上线
没有选择/参数/拦截的回归集,换个问法就选错工具;模型升级一次,行为可能全变。
课堂讨论 · 2 分钟
业务方说:"给 Agent 一个能执行任意 SQL 的工具,什么问题都能查,多省事。"你答应吗?
答案:不答应,给三条替代方案
① 预定义指标/视图工具(窄接口);② 只读账号 + 强制 LIMIT + 超时;③ 行级权限在数据层兜底。要灵活性,用"参数化的查询模板",不是自由 SQL。
总结

今天你带走了什么

机制
模型只返回 tool_calls,执行永远在你的代码里
机制
一次工具调用 = 两次模型请求 + 一次代码执行
机制
arguments 是字符串,可能缺、错、编造
设计
六要素:Name / Desc / Args / Return / Error / Permission
设计
一个业务动作一个工具,拒绝万能接口
设计
Description 写"何时不用",比参数说明更重要
受控
六道闸:白名单/鉴权/校验/超时/幂等/审计
受控
写操作:读写分离 + 审批参数 + 幂等键
受控
错误契约让模型"追问"而不是"编造"
验收
四项指标:选择/参数/执行/拦截
验收
Golden Set 20–50 条,含越权与对抗
验收
拦截率不是 100%,就不上线
"模型负责提议,代码负责执行,权限负责兜底。"
课后作业 · 明日预告

把你们部门的"一个动作"封装成工具

作业 1 · 工具契约卡
选一个真实业务动作(查库存/查审批进度/开票),写出六要素完整 schema,并标注它读什么、写什么、需要什么权限。
作业 2 · 对抗 5 条
为你的工具写 5 条"必须被拒绝"的调用:越权、越界金额、伪造 ID、缺审批、重复提交,并说明拦截点在哪一层。
作业 3 · 阅读
FastGPT OpenAPI 文档(doc.fastgpt.io/zh-CN/openapi/intro);复习 JSON Schema 的 enum / required / additionalProperties。
明日预告 · Day 33
多 Tool 路由、串并行调用
一个工具能查订单,但"这单能不能退"要同时看订单、支付、库存、政策 —— 明天教 Agent 组合多个数据源,以及什么时候该并行、什么时候必须串行。
讲师 · 包学斌

谢谢 · Q&A —— 实操问题随时在群里 @ 包学斌

Day 32 · Function Calling 与 Tool 设计 · 包学斌
1 / 25
本站由 2AIGC Pages 托管 · 30 秒发布你的网站我也要建站