跳到正文
Agent 工程实践Tool Calling 与 Function Calling
返回课程/Agent 工程实践
第 6 / 15 章 约 10 分钟

Tool Calling 与 Function Calling

为工具调用设置结构、预算、超时和错误边界。

这个模块在 Agent 里的位置

Tool 是模型接触外部世界的唯一受控入口。它决定:

  • 模型能读什么数据。
  • 一次能读多少。
  • 能不能写。
  • 失败时如何自救。
  • 结果如何进入上下文。
  • 副作用如何被审计。

在工程上,Tool 不是“包一层函数”就结束了。它是 Agent 的能力边界、成本边界、数据边界和副作用边界。

先记住几个概念

概念含义为什么重要
Tool schema输入参数的结构和范围防止模型传非法参数
Output contract返回字段、大小、分页、引用防止上下文被原始数据淹没
Tool budget单次调用、单 run、单租户的预算控制成本和资源
Typed error稳定错误码和 retryable 标记让模型停止盲目重试
Idempotency重复执行是否产生同一结果防止外部副作用重复发生
Permission是否允许读、写、执行收口安全边界
Observability记录耗时、大小、状态、错误支撑排障和成本治理

从一个真实问题开始:一次 Tool 返回把任务拖垮

一个时间线分析 Agent 需要回答:

找出过去 24 小时风险最高的 10 个实体。

错误设计:

{
    "name": "query_all_events",
    "input": {
        "tenantId": 7
    }
}

这个工具看起来简单,实际会引发一连串问题:

  1. 查询没有时间范围,数据库可能扫数亿行。
  2. 返回没有 topK,可能带出几十万行。
  3. 没有字段白名单,原始 JSON 全量进入上下文。
  4. 没有分页,模型无法判断是否还有数据。
  5. 没有调用上限,模型可能反复查询。
  6. 没有稳定错误码,失败后模型只能盲试。

结果通常是:

工具返回 10 万行
  -> 上下文膨胀
  -> 模型注意力被稀释
  -> 下一轮成本上升
  -> 触发 compaction
  -> 引用丢失
  -> 模型重复查询

这不是“模型不够聪明”,而是 tool contract 没有把工程边界表达出来。

正确的 Tool 应该长什么样

同一个需求,应该拆成受限查询:

{
    "name": "query_top_risk_entities",
    "input": {
        "tenantId": 7,
        "startTs": 1790000000000,
        "endTs": 1790086400000,
        "groupBy": "entity",
        "topK": 10,
        "metric": "risk_score"
    }
}

返回:

{
    "ok": true,
    "resolvedRange": {
        "startTs": 1790000000000,
        "endTs": 1790086400000
    },
    "entities": [
        {
            "entityId": "entity-9182",
            "riskScore": 87.4,
            "eventCount": 12345,
            "evidenceWindowIds": ["win-1001", "win-1002"]
        }
    ],
    "totalEntities": 487320,
    "truncated": true,
    "nextCursor": "entity-cursor-9183"
}

这和错误设计的核心区别是:

  • 查询有明确时间窗。
  • topK 由工具 clamp,不是模型想多大就多大。
  • 返回字段是白名单。
  • truncated 明确告诉模型还有更多数据。
  • nextCursor 让模型可以继续查,但每次仍受预算限制。
  • evidenceWindowIds 把证据引用化,而不是把原始事件全塞进上下文。

Tool 的预算应该管什么

很多团队第一反应是“给 tool 估算 token”。这不够,也容易误导。

原因:

  1. token 计数只有模型 provider 是权威。
  2. 工具返回进入上下文后,还会受序列化和历史影响。
  3. 只扣 token 不控制数据形态,真实成本未必下降。

更可靠的预算结构是:

维度控制点示例
调用次数per-run / per-user / per-tenant单 run 最多 20 次
输出大小bytes / rows / fields / images单次最多 2KB
执行时长timeout单次最多 10 秒
副作用read / write / approval写操作必须人审
成本model usage + tool cost按 run 聚合

一个 run 级预算可以这样表达:

{
    "runId": "run-9f27",
    "toolBudget": {
        "maxToolCalls": 20,
        "maxOutputBytes": 131072,
        "maxDurationMs": 120000,
        "maxExternalActions": 3
    }
}

当预算耗尽时,不应该让模型继续尝试,而应该返回明确的错误:

{
    "ok": false,
    "errorCode": "TOOL_BUDGET_EXCEEDED",
    "retryable": false,
    "message": "本任务工具调用次数已达上限,请基于已有结果作答。"
}

工具错误要有类型

不能只返回“查询失败”。模型无法判断是该重试、换参数,还是直接停止。

更合理的错误格式:

{
    "ok": false,
    "errorCode": "TIME_RANGE_TOO_LARGE",
    "retryable": false,
    "message": "请缩小时间范围"
}

常用错误码:

errorCode含义retryable
TIME_RANGE_TOO_LARGE时间范围过大false
TOO_MANY_ENTITIES请求实体过多false
TIMEOUT执行超时true
RATE_LIMITED被限流true
PERMISSION_DENIED无权限false
TOOL_BUDGET_EXCEEDED预算耗尽false

有了 retryable,模型才能知道:

  • true:可以等待后重试。
  • false:应该换策略或基于已有结果作答。

输出设计:给模型结论和引用,不给原始世界

一个工具返回可以分成两类:

输出类型模型可见用途
summary是少量结论
refs是稳定 ID 引用
rawPayload否排障或后续精查
imageId按需需要视觉判断时再读取

这带来的直接收益是:

  1. 上下文不会膨胀。
  2. 引用可以被 validator 校验。
  3. 原始数据保留在数据查询层。
  4. 模型可以按需继续精查。

例如:

{
    "summary": {
        "topRiskEntityCount": 10,
        "timeRange": "24h"
    },
    "refs": [
        {
            "entityId": "entity-9182",
            "evidenceWindowIds": ["win-1001", "win-1002"]
        }
    ],
    "rawPayload": {
        "objectKey": "s3://tool-results/run-9f27/query-1.json",
        "bytes": 1048576
    }
}

rawPayload 不进入模型上下文,只进入观测或审计。

数据库和查询下推

Tool 层不仅要限制返回,还要把查询下推到数据库。

错误做法:

select *
from raw_event
where tenant_id = 7
order by occurred_at desc;

正确做法:

select entity_id,
       sum(event_count) as event_count,
       sum(risk_score) as risk_score
from risk_event_rollup_hourly
where tenant_id = $1
  and event_hour >= $2
  and event_hour < $3
group by entity_id
order by risk_score desc
limit 10;

配套索引:

create index risk_rollup_query_idx
  on risk_event_rollup_hourly (tenant_id, event_hour, entity_id);

这条索引的含义是:

  1. 先按租户隔离。
  2. 再按时间过滤。
  3. 再按实体聚合。
  4. 最后由数据库完成排序和 limit。

不要把 10 万行拉到应用层再排序,那会把成本从数据库转移到 Agent 上下文。

副作用工具必须拆成 proposal 和 execution

只读工具可以直接执行。

写工具应该分成两步:

Agent 生成 proposal
  -> 权限 / 人审
  -> Action Executor 执行
  -> terminal result
  -> audit

也就是说,工具本身不应该因为模型“想执行”就直接执行。

工具类型是否直接执行说明
只读查询可以无外部副作用
聚合查询可以只读
写文件不可以必须进入 proposal / approval
发通知不可以必须进入 proposal / approval
执行代码不可以必须进入沙箱和权限链路

和传统后端的关系

Tool Calling 的很多问题,本质还是后端服务治理问题:

Agent 里的概念传统后端里的对应物
Tool schemaAPI contract / RPC 参数校验
Tool timeout下游超时
Retryable error可重试错误码
Rate limitQPS 限流
Budget流量配额 / 资源配额
Idempotency key写接口幂等键
Typed errorHTTP status + 业务错误码

所以做过后端的人不需要重新学一遍“重试、降级、限流、幂等”,只需要把这些经验搬到 tool 边界上。

常见工程错误

错误实际后果正确做法
返回原始 row上下文膨胀、注意力被稀释字段白名单 + topK
没有分页查询失控truncated + nextCursor
错误无类型模型盲目重试typed error + retryable
只在 prompt 里限制次数模型偶尔绕过runtime budget
工具自己估算 token成本归因失真记录真实 usage
写工具直接执行不可审计、不可回滚proposal + approval
没有幂等 key重复执行外部动作action key
没有权限分级越权读写permission policy

上线前检查

  1. 每个 tool 有 input schema。
  2. 每个 tool 有 output contract。
  3. 每个 tool 有 timeout。
  4. 每个 tool 有稳定 errorCode。
  5. run 级预算显式。
  6. 输出大小有上限。
  7. 大结果有分页。
  8. 只读和写操作分级。
  9. 写操作走 proposal / approval / executor。
  10. 观测记录耗时、大小、状态和错误。