大模型工具调用次数限制

大模型工具调用次数限制

本文介绍在 Agent / Tool Calling 场景中,如何设计与实现「工具调用次数限制」。


1. 背景:什么是工具调用

现代大模型(LLM)可通过 Function Calling / Tool Calling 调用外部能力,例如:

  • 搜索网页
  • 读写文件
  • 执行代码
  • 查询数据库
  • 调用业务 API

典型交互循环如下:

text 复制代码
用户提问
  → 模型决定是否调用工具
  → 执行工具,把结果回填给模型
  → 模型继续推理 / 再调工具 / 最终作答

若不加限制,模型可能:

  • 反复调用同一工具(死循环)
  • 一次任务打出上百次调用(成本失控)
  • 长时间占住会话(延迟与资源占用)

因此需要在工程层显式约束「最多能调几次工具」。


2. 为什么要限制

风险 说明
成本 每次工具结果都会进入后续上下文,token 费用快速上升
延迟 串行工具调用会显著拉长端到端耗时
稳定性 模型可能陷入「再试一次」式循环
安全 危险工具(删文件、发邮件、支付)需要硬闸
配额 下游 API 本身有 QPS / 日调用上限

限制的目标不是「禁止用工具」,而是:在可控预算内完成任务


3. 限制维度

实践中很少只设一个总开关,通常按多个维度组合:

3.1 按会话 / 任务

  • 单次用户请求最多 N 次工具调用
  • 适用于大多数 Agent 主循环

3.2 按轮次(Turn)

  • 模型每一轮回复里,最多允许 K 个并行 / 串行 tool call
  • 防止单轮「工具风暴」

3.3 按工具类型

  • search:最多 5 次
  • run_code:最多 3 次
  • send_email:最多 1 次(敏感操作)
  • 通用工具松、危险工具紧

3.4 按时间窗口

  • 每分钟 / 每小时 / 每天的调用配额
  • 适合多用户共享同一下游服务的场景

3.5 按成本预算

  • 不只数次数,还估算工具结果带来的 token 增量
  • 例如:累计工具相关 token 不超过预算 B

3.6 按深度 / 递归

  • 子 Agent、工具再调模型时,限制嵌套层数
  • 防止「Agent 调 Agent」无限展开

推荐起步配置:

text 复制代码
单任务总调用次数:8 ~ 20
单轮并行工具数:1 ~ 4
危险工具:1 ~ 2,且需确认
递归深度:1 ~ 3

具体数值应结合业务延迟、成本与成功率压测后调整。


4. 限制策略:软限制 vs 硬限制

4.1 硬限制(Hard Limit)

达到上限后直接拒绝后续工具调用,并强制模型进入收尾。

适用:

  • 成本红线
  • 安全红线
  • 必须保证系统不会失控

行为示例:

text 复制代码
已达工具调用上限(12/12)。
请基于已有信息直接给出最终答案,不要再请求调用工具。

4.2 软限制(Soft Limit)

接近上限时先「提醒」模型,仍允许少量调用。

例如:

  • used < 0.7 * max:正常
  • 0.7 * max ≤ used < max:注入提示「请尽快收敛」
  • used ≥ max:硬拒绝

软限制能减少突然截断导致的答案质量下降,但仍需硬上限兜底。

4.3 分级响应

阶段 动作
正常 正常执行
预警 在 system / developer 消息中提示剩余次数
拒绝新调用 返回结构化错误给模型
强制收尾 不再把 tool schema 发给模型,或 tool_choice=none

5. 核心实现思路

无论用哪种 SDK,本质都是在 Agent Loop 外包一层计数与裁决。

5.1 最小状态机

text 复制代码
remaining = MAX_TOOL_CALLS
while True:
    response = llm.chat(messages, tools=tools)
    if response 没有 tool_calls:
        return response.content
    if len(tool_calls) > remaining:
        拒绝超额部分 / 或整批拒绝
        强制模型总结
        break
    执行允许的 tool_calls
    remaining -= 实际执行次数
    把 tool 结果写回 messages

5.2 伪代码(Python 风格)

python 复制代码
from dataclasses import dataclass, field


@dataclass
class ToolBudget:
    max_total: int = 12
    max_per_turn: int = 3
    used_total: int = 0
    per_tool: dict[str, int] = field(default_factory=dict)
    max_per_tool: dict[str, int] = field(default_factory=dict)

    def allow(self, name: str, count: int = 1) -> bool:
        if self.used_total + count > self.max_total:
            return False
        if count > self.max_per_turn:
            return False
        limit = self.max_per_tool.get(name)
        if limit is not None and self.per_tool.get(name, 0) + count > limit:
            return False
        return True

    def consume(self, name: str, count: int = 1) -> None:
        self.used_total += count
        self.per_tool[name] = self.per_tool.get(name, 0) + count

    def remaining(self) -> int:
        return max(0, self.max_total - self.used_total)


def run_agent(user_input: str, llm, tools, budget: ToolBudget) -> str:
    messages = [{"role": "user", "content": user_input}]

    while True:
        # 接近上限时提示模型收敛
        if budget.remaining() <= 2:
            messages.append({
                "role": "system",
                "content": (
                    f"工具调用剩余 {budget.remaining()} 次,"
                    "请优先给出最终答案,避免非必要调用。"
                ),
            })

        # 用尽后不再暴露工具,逼模型直接回答
        active_tools = tools if budget.remaining() > 0 else None
        tool_choice = "auto" if active_tools else "none"

        resp = llm.chat(messages, tools=active_tools, tool_choice=tool_choice)
        calls = getattr(resp, "tool_calls", None) or []

        if not calls:
            return resp.content

        # 单轮截断
        calls = calls[: budget.max_per_turn]
        allowed = []
        denied = []

        for call in calls:
            if budget.allow(call.name):
                allowed.append(call)
            else:
                denied.append(call)

        messages.append(resp.as_assistant_message())

        for call in allowed:
            result = execute_tool(call)
            budget.consume(call.name)
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": result,
            })

        for call in denied:
            messages.append({
                "role": "tool",
                "tool_call_id": call.id,
                "content": (
                    "ERROR: tool call budget exceeded. "
                    "Do not retry. Answer with available information."
                ),
            })

        if not allowed and denied:
            # 已无法继续调工具,再要一轮纯文本总结
            final = llm.chat(messages, tools=None, tool_choice="none")
            return final.content

要点:

  1. 先裁决,再执行:超限工具不要真的跑
  2. 给模型可理解的错误:否则它可能反复重试
  3. 用尽预算后去掉 tools:比口头劝说更可靠
  4. 单轮与总量双重约束:防止「一轮打爆」

6. 与模型侧参数配合

除了业务层计数,还可利用 API 能力降低失控概率:

手段 作用
tool_choice="none" 禁止本轮调用工具
tool_choice="required" 强制调用(一般不用于限流)
parallel_tool_calls=false 降低单轮爆发
缩小 tools 列表 只暴露当前阶段需要的工具
更短 tool description 降低模型「顺手多调一次」的倾向

注意:模型侧参数是辅助,不能替代服务端硬计数。客户端或模型请求可以被构造,真正闸门必须在执行层。


7. 常见架构落点

7.1 Agent Loop 内计数(最常见)

while 循环里维护 budget,简单、直观,适合单进程单会话。

7.2 工具执行中间件

把限制做成统一拦截器:

text 复制代码
request → auth → rate limit → policy → execute → audit

优点:

  • 所有入口一致
  • 便于加审计日志、用户级配额
  • 可与 HTTP API 网关限流思路对齐

7.3 分布式配额

多实例部署时,本地内存计数不够,需要:

  • Redis INCR + TTL(按分钟 / 天)
  • 或令牌桶 / 漏桶算法

示例(概念):

text 复制代码
KEY = tool_quota:{user_id}:{yyyyMMdd}
VALUE = INCR
IF VALUE > DAILY_LIMIT THEN REJECT
EXPIRE KEY 2 days

7.4 工作流 / 图编排框架

在 LangGraph、AutoGen、自研状态机中,把「调用次数」做成图状态字段:

text 复制代码
state.tool_calls_used += 1
if state.tool_calls_used >= MAX:
    next_node = "finalize"

这样限制成为编排的一部分,而不是散落在各工具内部。


8. 失败后怎么告诉模型

错误信息质量会直接影响是否「空转重试」。

推荐

text 复制代码
Tool budget exceeded (12/12).
You must stop calling tools and produce the final answer now
using only information already in the conversation.

不推荐

text 复制代码
Error

或:

text 复制代码
请稍后重试

后者会诱导模型继续调工具。

可同时做两件事:

  1. tool 结果里返回明确 budget exceeded
  2. 下一轮请求设 tool_choice="none",并可选移除 tools 定义

9. 观测与调参

没有指标就无法选对 N。至少记录:

指标 用途
每次任务的工具调用次数分布 max_total 的 P95 / P99
因超限而截断的比例 判断限制是否过严
超限后答案可用率 评估硬截断伤害
各工具调用占比 给高频 / 高危工具单独设限
平均时延与费用 把次数限制映射到 SLA / 成本

调参经验:

  1. 先统计无限制时的自然分布
  2. 把上限设在 P95 ~ P99 附近
  3. 对失败任务做 case review:是真需要更多调用,还是模型在空转
  4. 空转多 → 降上限 + 强化收敛提示;有效长链路多 → 提上限或做分阶段预算

10. 设计检查清单

  • 有单任务总上限
  • 有单轮并行上限
  • 危险工具有独立更严上限
  • 超限时工具不会被真实执行
  • 超限错误对模型可读,且明确禁止重试
  • 用尽预算后可关闭 tools / 设 tool_choice=none
  • 多实例场景有共享配额(如 Redis)
  • 有审计日志:谁、何时、调了什么、是否被拒
  • 有监控:调用次数、拒识率、费用、时延
  • 递归 / 子 Agent 有深度限制

11. 最小可行方案(MVP)

若只想快速落地,按下面 5 步即可:

  1. 设定 MAX_TOOL_CALLS = 12
  2. 在 Agent 循环维护计数器
  3. count >= MAX 时停止执行新工具
  4. 向模型返回「预算耗尽,请直接作答」
  5. 最后一轮强制 tool_choice="none" 生成最终回答

这已经能挡住大多数失控循环。后续再按工具类型、用户等级、时间窗口逐步细化。


12. 小结

工具调用次数限制的本质是:在执行层实施可审计的预算控制

记住三句话:

  1. 模型可以「请求」调用,但系统决定「是否执行」
  2. 软提示提高体验,硬上限保证安全与成本
  3. 限制值应由观测数据校准,而不是拍脑袋

掌握「计数 → 裁决 → 拒绝 / 收尾 → 观测调参」这条链路后,就可以在任意 Agent 框架中复用同一套思路。

相关推荐
windliang1 小时前
Claude Code 源码分析(五):一次 Tool 调用怎样通过权限检查
前端·人工智能·面试
2601_961391461 小时前
杭州极序时代|GEO的对抗防御与幻觉免疫工程
人工智能·极序时代geo
Think_Higher1 小时前
CID行业趋势周报|7.27—8.02:大盘表现、重点赛道与投测建议
大数据·人工智能
MC皮蛋侠客1 小时前
SQLAlchemy 系列(八):AsyncIO、并发与 Web 生命周期——让每个并发任务持有自己的 Session
数据库·python
林澈在路上2 小时前
2026 主流 AI 写歌 APP 全面测评|零基础原创歌曲工具选购指南
大数据·人工智能·aigc·音视频·音频
lancyu2 小时前
零基础AI应用编程开发入门 | 吴恩达Prompt工程极简通关指南:新手从零学会工业级提示词开发(可直接复用代码)
人工智能·深度学习·机器学习
科研小刘带你玩学术2 小时前
AI Agent正在改变人工智能应用模式:从工具助手走向自主智能系统
人工智能·大语言模型·未来趋势·智能体·智能系统
阿文和她的Key2 小时前
一个失控的AI智能体在4.5天内完成了17,600次攻击:从这次事件看AI安全治理的四层裂痕
人工智能·安全
东方小月2 小时前
从零开发一个 Coding Agent(五):使用 TypeBox 校验工具参数
前端·人工智能·后端