AI 工具调用机制全解析:从底层到应用

AI Agent工具调用机制深度解析:从function calling到自动编排

工具调用是Agent能力的核心。模型如何在对话过程中识别需要调用工具、选择正确的工具、构造正确的参数、处理调用的结果?本文从底层实现到上层策略,完整分析工具调用的技术原理。

一、工具调用的底层机制

1.1 Function Calling的结构

工具调用的底层实现是 function calling。模型输出一段结构化文本,描述要调用的工具名和参数。推理引擎解析这段文本,执行工具,将结果返回给模型。

一个典型的工具调用过程:

复制代码
// 1. 系统提示词中注册工具定义
{
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "search_files",
        "description": "在项目中搜索文件或文件内容",
        "parameters": {
          "type": "object",
          "properties": {
            "pattern": {"type": "string", "description": "搜索关键词"},
            "path": {"type": "string", "description": "搜索路径"}
          },
          "required": ["pattern"]
        }
      }
    }
  ]
}
​
// 2. 模型输出(结构化JSON)
{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "search_files",
        "arguments": "{\"pattern\": \"User\", \"path\": \"/src\"}"
      }
    }
  ]
}
​
// 3. 推理引擎解析并执行工具
// 4. 工具结果返回给模型
{
  "role": "tool",
  "tool_call_id": "call_abc123",
  "content": "src/UserService.java: 第42行"
}

1.2 tool_use API的底层实现

主流的tool calling API有OpenAI格式和Anthropic格式两种。

OpenAI格式:tool_calls是一个数组,每个元素包含id、type、function字段。工具调用在assistant消息中返回,工具结果在tool角色的消息中返回。一个assistant消息可以包含多个tool_calls(并行调用)。

Anthropic格式:tool_use是content block的一种类型。content数组中可以同时包含text block和tool_use block。工具结果以tool_result block的形式返回。

两种格式的底层逻辑相同:模型输出 -> 解析工具描述 -> 执行 -> 结果反馈。不同的是多工具调用的表示方式------OpenAI用数组,Anthropic用content blocks。

复制代码
# 两种格式的对比
# OpenAI:
messages = [
    {"role": "system", "content": "你是一个助手"},
    {"role": "user", "content": "搜索src目录下的User文件"},
    {"role": "assistant", "content": None, "tool_calls": [...]},
    {"role": "tool", "tool_call_id": "call_xxx", "content": "..."}
]
​
# Anthropic:
messages = [
    {"role": "user", "content": "搜索src目录下的User文件"},
    {"role": "assistant", "content": [
        {"type": "text", "text": "我来搜索..."},
        {"type": "tool_use", "name": "search", "input": {"pattern": "User"}}
    ]},
    {"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": "tu_xxx", "content": "..."}
    ]}
]

1.3 解析过程

模型输出的是token序列,不是结构化的JSON。推理引擎需要做两件事:检测模型是否在输出工具调用、解析工具调用参数。

检测是否在输出工具调用 :模型在遇到需要调用工具的场景时会输出一个特殊的token或token序列来标记开始tool call。OpenAI格式中这个标记是固定的JSON结构开头({"tool_calls":),Anthropic格式中是一个特殊的XML标记(<tool_use>)。

参数解析:模型输出的JSON需要解析为有效参数。JSON输出不稳定------模型偶尔会输出格式错误的JSON(缺少引号、多余的逗号、嵌套未闭合等)。推理引擎需要容错解析:

复制代码
# JSON容错解析的几种策略
import json
import re
​
def parse_tool_args(text):
    # 策略1: 直接解析
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        pass
​
    # 策略2: 修复常见错误后重试
    fixed = re.sub(r",\s*([\]}])", r"\1", text)  # 去掉末尾多余的逗号
    try:
        return json.loads(fixed)
    except json.JSONDecodeError:
        pass
​
    # 策略3: 提取有效的JSON片段
    match = re.search(r"\{.*\}", text, re.DOTALL)
    if match:
        try:
            return json.loads(match.group())
        except json.JSONDecodeError:
            pass
​
    # 策略4: 让模型重新生成(兜底)
    return None  # 需要模型重新输出

容错解析的命中率:直接解析约85-90%,修复末尾逗号后约93-95%,提取JSON片段后约96-98%。仍有约2-4%的情况需要让模型重新生成。

不同模型的JSON输出稳定性不同。DeepSeek V4的JSON稳定性最好(约98-99%直接解析成功),GLM-5.2稳定性其次(约95-97%)。稳定性差异来自训练数据的质量------训练时大量使用JSON格式输出的模型,生成JSON的能力更好。

二、工具选择策略

2.1 候选工具过多的问题

当注册的工具数量超过50个时,模型选择正确工具的概率显著下降。原因是工具描述在系统提示词中占用了大量token,模型需要在大量工具描述中寻找匹配的选项。

注册工具数 选择正确率(GPT-4级别) 系统提示词中工具描述占比
10个 ~95% ~2K token
30个 ~90% ~6K token
50个 ~82% ~10K token
100个 ~70% ~20K token
200个 ~55% ~40K token

正确率下降有两个原因:一是工具描述本身占据了模型的可分配注意力窗口,长提示词中模型对中间部分的关注度下降。二是相似工具之间的区分变得困难------当有多个搜索工具(search_files、search_database、search_web),模型可能选错。

2.2 工具过滤(Tool Filtering)

解决候选工具过多的问题,不是在注册端减少工具数量,而是在调用端动态过滤。

基于历史调用的过滤:维护一个工具调用频率表。当前会话中已经调用过的工具、该任务中常用的工具优先展示。一个运行30分钟的Agent任务,前5步使用的工具基本可以确定任务的类型,因此从第6步开始可以将候选工具列表从50个压缩到15个。

复制代码
def filter_tools_by_history(tool_usage_history, all_tools, max_tools=20):
    # 当前会话已使用的工具
    used = set(t["name"] for t in tool_usage_history)
​
    # 按使用频率排序
    usage_count = Counter(t["name"] for t in tool_usage_history)
​
    # 已使用的工具优先
    selected = [t for t in all_tools if t["name"] in used]
​
    # 未使用但高频的工具补充
    remaining = sorted(
        [t for t in all_tools if t["name"] not in used],
        key=lambda t: usage_count.get(t["name"], 0),
        reverse=True
    )
​
    selected.extend(remaining[:max_tools - len(selected)])
    return selected

基于语义的过滤:将用户输入和工具描述分别做embedding,计算语义相似度。选择相似度最高的top-N个工具。相似度不高的工具不参与选择,减少模型的决策负担。

语义过滤的关键是工具描述的embedding质量。描述越详细,embedding越准确。一个搜索文件的工具,描述写"搜索文件内容"和"使用关键词搜索项目目录下的文件,支持正则表达式,返回匹配的文件路径和行号",后者的embedding效果更好。

复制代码
def filter_tools_by_semantics(query, tools, embedding_model, top_k=20):
    query_embedding = embedding_model.embed(query)
    tool_embeddings = [embedding_model.embed(t["description"]) for t in tools]
    similarities = [cosine_sim(query_embedding, te) for te in tool_embeddings]
    ranked = sorted(zip(tools, similarities), key=lambda x: x[1], reverse=True)
    return [t for t, _ in ranked[:top_k]]

语义过滤可以将候选工具从200个压缩到20个,正确率从55%回升到85%左右。embedding模型的维度影响压缩质量:768维(如bge-base)和1536维(如bge-large)的效果差异约3-5个百分点。

2.3 工具路由

工具路由是多级过滤的扩展:先判断任务类型,再选择该类型下的工具集。

复制代码
# 工具路由的层级结构
task_classifier(user_input)
├── 代码类任务
│   ├── 代码工具集(search_files, read_file, write_file, patch, terminal)
│   └── Git工具集(git_commit, git_diff, git_push)
├── 数据类任务
│   ├── 数据库工具集(query_db, create_table, insert_data)
│   └── 文件工具集(read_csv, write_csv, read_json)
├── 网络类任务
│   ├── HTTP工具集(http_get, http_post, http_put)
│   └── 搜索工具集(web_search, web_fetch)
└── 通用工具
    ├── 对话工具集(clarify, confirm, explain)
    └── 日志工具集(log_info, log_error, log_warning)

路由的关键是一个轻量的分类模型(classifier),它的任务是判断用户输入属于哪个任务类型,而不是判断具体要调用哪个工具。分类任务比工具选择简单,准确率可以做到98%以上。分类模型可以用更小的模型(如qwen-turbo级别),不占用主模型的计算资源。

三、并行工具调用

3.1 串行vs并行

当多个工具调用没有数据依赖时,可以并行执行。

复制代码
# 串行(每个工具等待上一个完成)
result1 = search_files("User", "/src")       # 耗时0.3秒
result2 = search_files("Login", "/src")      # 耗时0.3秒
result3 = search_files("Auth", "/src")       # 耗时0.3秒
# 总耗时:0.9秒
​
# 并行
results = parallel_execute([
    ("search_files", {"pattern": "User", "path": "/src"}),
    ("search_files", {"pattern": "Login", "path": "/src"}),
    ("search_files", {"pattern": "Auth", "path": "/src"}),
])
# 总耗时:0.3秒

并行调用在IO密集型工具(API调用、搜索、数据库查询)上效果明显。在计算密集型工具(代码分析、编译)上效果有限------计算资源是瓶颈而不是IO等待。

3.2 依赖检测

并行调用的前提是工具之间没有数据依赖。依赖检测是一个静态分析问题:

复制代码
def detect_dependencies(tool_calls):
    """
    检测工具调用之间的数据依赖。
    如果工具B的参数引用了工具A的输出,则B依赖A。
    """
    dependencies = {}
    outputs = {}  # 工具名 -> 输出字段列表
​
    for i, call in enumerate(tool_calls):
        for j, prev in enumerate(tool_calls[:i]):
            dep_fields = []
            for param_name, param_val in call["arguments"].items():
                # 检查参数是否引用了前序工具的输出
                if isinstance(param_val, str):
                    ref = check_reference(param_val, prev["name"])
                    if ref:
                        dep_fields.append((param_name, ref))
            if dep_fields:
                dependencies.setdefault(i, [])
                dependencies[i].append(j)
​
    return dependencies

当前的大模型在输出多个工具调用时,不会显式声明数据依赖。推理引擎只能通过参数内容来推断。一个简单的启发式:如果工具B的参数中包含了与工具A输出相同的变量名,则B依赖A。这个启发式的准确率约90-95%,少量情况会误判。

3.3 冲突处理

当多个工具调用写同一个文件时,需要使用锁或其他并发控制机制。

文件级别的冲突:用事务性写入(先写临时文件,再原子重命名)。工具A写文件X,先写入X.tmp_A,工具B写文件X,先写入X.tmp_B。两个工具都完成后,合并X.tmp_A和X.tmp_B到X。如果合并失败(同时修改了同一行),进入冲突解决流程。

冲突解决流程:暂停后续工具调用,启动一个冲突解决Agent检查两个版本的diff,决定保留哪个版本或手动合并。

数据库级别的冲突:用乐观锁(版本号或时间戳)。写入时检查数据版本是否与读取时一致,不一致则回退重试。

四、工具调用失败处理

4.1 失败类型

失败类型 原因 发生频率 恢复策略
工具未找到 模型选择了不存在的工具 低(约1%) 重新选择
参数无效 模型构造了错误的参数 中(约5-8%) 修正参数重试
执行超时 工具执行超过时间限制 低(IO密集型工具中较高) 设置超时后重试
权限不足 工具需要但没有授权 低(约1-2%) 跳过或提示用户
结果异常 工具执行成功但结果不符合预期 中(约5%) 重新执行

4.2 参数修正

参数错误是最常见的工具调用失败。模型可能记错了参数名、参数类型、必填参数。

修复策略不需要重新让模型生成全部参数,只需要修正错误的参数:

复制代码
def fix_tool_args(name, args, error_msg):
    """
    根据工具定义和错误信息修正参数。
    不需要再次调用模型,用规则即可处理大部分情况。
    """
    tool_def = get_tool_definition(name)
​
    # 类型修正
    for param, value in args.items():
        expected_type = tool_def["parameters"]["properties"][param].get("type")
        if expected_type == "integer" and isinstance(value, str):
            try:
                args[param] = int(value)
            except ValueError:
                return None  # 无法修正
        elif expected_type == "array" and isinstance(value, str):
            args[param] = [value]  # 字符串转数组
​
    # 必填参数补全
    for required in tool_def["parameters"].get("required", []):
        if required not in args or args[required] is None:
            return None  # 需要模型补充
​
    return args

规则修正能处理约60-70%的参数错误。剩余的30-40%需要让模型重新生成参数,但可以让模型只重新生成工具调用的参数部分,而不是重新生成整个assistant消息。

4.3 工具链回退

当一个工具反复失败时,需要回退到替代工具或替代方法。

复制代码
# 工具回退链
tool_chains = {
    "search_files": [  # 主工具失败时的备选
        "grep_tool",   # 备选1:用系统命令替代
        "find_tool",   # 备选2:用其他搜索工具
        None           # 兜底:无法完成
    ],
    "web_search": [
        "curl_tool",   # 备选1:直接请求搜索引擎
        None
    ],
    "code_analyze": [
        "ast_parse_tool",
        "simple_analyze_tool",  # 备选:用简单的文本分析替代完整的AST分析
        None
    ]
}
​
def execute_with_fallback(name, args, max_retries=2):
    for attempt in range(max_retries + 1):
        result = execute_tool(name, args)
        if result["success"]:
            return result
        # 尝试用备选工具
        alternatives = tool_chains.get(name, [None])
        for alt in alternatives:
            if alt is None:
                return {"success": False, "error": "所有工具都失败"}
            result = execute_tool(alt, args)
            if result["success"]:
                return result
    return {"success": False, "error": f"重试{max_retries}次后仍失败"}

工具链回退的关键是备选工具的调用方式要和主工具兼容。grep_tool的参数(pattern, path)和search_files的参数类似,不需要模型重新生成参数。

五、工具描述的优化

5.1 描述质量的影响

工具描述的质量直接影响模型选择工具的正确率。

复制代码
# 差描述
{
    "name": "search_files",
    "description": "搜索文件"
}
​
# 中等描述
{
    "name": "search_files",
    "description": "在项目目录中搜索文件"
}
​
# 好描述
{
    "name": "search_files",
    "description": "使用关键词搜索项目目录下的文件内容,支持正则表达式,返回匹配的文件路径和行号。适合在代码库中找到包含特定关键词的位置。"
}

工具描述的优化原则:

  1. 包含使用场景示例:"适合在代码库中找到包含特定关键词的位置"------这不仅描述了功能,还暗示了应该在什么场景下使用这个工具。

  2. 说明参数的含义和约束:参数描述写"搜索路径"不如写"从哪个目录开始搜索,支持绝对路径和相对路径,默认是当前工作目录"。

  3. 区分相似工具:当有search_files和search_database两个工具时,描述中需要明确指出它们的差异。"search_files搜索文件系统,search_database搜索数据库记录"比简单的"搜索"要好得多。

好的工具描述可以使正确率提升5-10个百分点(对比差描述)。

5.2 参数约束的表达

参数约束需要在description字段中表达,而不是依赖schema约束(因为部分推理引擎不支持JSON Schema的高级约束)。

复制代码
{
    "name": "fetch_web_page",
    "parameters": {
        "type": "object",
        "properties": {
            "url": {
                "type": "string",
                "description": "要抓取的网页URL,必须以http://或https://开头,例如https://example.com"
            },
            "timeout": {
                "type": "integer",
                "description": "超时时间(秒),最小5秒,最大60秒,默认30秒"
            }
        },
        "required": ["url"]
    }
}

在description中包含约束信息比在JSON Schema中声明更稳定------不是所有推理引擎都支持JSON Schema的minimum/maximum/pattern约束。而description字段所有引擎都会传递给模型。

六、工具调用的安全

6.1 权限控制

工具调用的权限控制需要考虑:这个工具有没有权限执行?在当前任务下允不允许执行?

复制代码
def check_tool_permission(name, args, context):
    # 一级检查:是否在黑名单中
    if name in BLOCKLIST_TOOLS:
        return {"allowed": False, "reason": "该工具已被禁止使用"}

    # 二级检查:参数是否在安全范围内
    if name == "delete_file":
        path = args.get("path", "")
        if "/system/" in path or "/etc/" in path:
            return {"allowed": False, "reason": "不允许修改系统文件"}
        if not path.startswith(context.get("project_dir", "")):
            return {"allowed": False, "reason": "不允许修改项目目录外的文件"}

    # 三级检查:操作频率限制
    if is_rate_limited(name):
        return {"allowed": False, "reason": f"工具{name}被限流"}

    return {"allowed": True}

6.2 调用前确认

高风险工具可以在执行前要求确认。但Agent在自动执行任务时,不能每次都停下来问用户------这会影响自动化效率。

分级确认策略:

复制代码
CONFIRMATION_LEVELS = {
    "read_only": {
        "tools": ["search_files", "read_file", "web_search"],
        "action": "auto"  # 自动执行,不需要确认
    },
    "write_file": {
        "tools": ["write_file", "patch"],
        "action": "auto_if_pattern"  # 如果修改符合预定模式,自动执行
    },
    "destructive": {
        "tools": ["delete_file", "rm_rf", "database_drop"],
        "action": "ask"  # 必须由用户确认
    },
    "network_write": {
        "tools": ["git_push", "http_post", "deploy"],
        "action": "ask_once_per_task"  # 每个任务只需确认一次
    }
}

七、2026年的技术现状

项目 工具注册方式 并行调用 失败恢复 安全控制 工具数量上限
OpenAI API JSON Schema 支持 无(需开发者实现) API Key ~128
Anthropic API JSON Schema 支持 API Key ~64
vLLM JSON Schema 支持 部分 ~128
Claude Code 内置工具集 支持 自动重试 文件级 ~30
Hermes Agent 配置文件+插件 支持 自动重试+回退 分级确认 ~200
OpenHands 插件化 支持 自动重试 用户确认 ~100
LangChain 函数注册 支持 开发者实现 开发者实现 不限

大部分项目在工具注册和调度上已经成熟,但在失败恢复和安全控制上仍需要开发者自己实现。Hermes Agent和OpenHands在自动重试上做得较好。Claude Code的自动确认机制(当模式匹配时自动执行写操作)是安全控制中比较务实的做法------不过度打扰用户,又不完全放权。

八、总结

  • 工具调用的底层实现是function calling:模型输出结构化JSON -> 解析 ->执行 ->结果反馈

  • JSON容错解析:直接解析(85-90%),修复末尾逗号(93-95%),提取JSON片段(96-98%),剩余2-4%需模型重新生成

  • 候选工具超过50个时选择正确率开始显著下降(从95%降到82%)

  • 工具过滤:基于历史调用(已使用的工具优先)和基于语义(embedding相似度排序),可将200个工具压缩到20个

  • 工具路由:先分类任务类型再选择对应工具集,分类准确率98%+

  • 并行调用依赖检测:通过参数内容推断数据依赖,启发式检测准确率约90-95%

  • 文件冲突用事务性写入+合并,数据库冲突用乐观锁

  • 参数修正规则能处理60-70%的参数错误,剩余的需模型重试

  • 工具链回退:主工具失败后按优先级尝试备选工具

  • 工具描述质量影响正确率5-10个百分点,需包含使用场景和相似工具区分

  • 权限控制分三级:黑名单->参数安全检查->频率限制

  • 高风险工具的分级确认策略:只读自动、写文件(模式匹配时自动)、破坏性操作(必须确认)、网络写入(每任务确认一次)

相关推荐
OceanBase数据库官方博客3 小时前
OceanBase DataPilot AIP:Ontology 承载AI能力面的另一条路
人工智能·oceanbase
笨鸟先飞,勤能补拙4 小时前
AI 赋能网络安全:技术全景、成熟度评估与实战案例
人工智能·python·安全·web安全·网络安全·sqlite·github
一次旅行4 小时前
AI 前沿日报 | 2026年07月31日
人工智能
2601_963749104 小时前
标题:越华环保集团|面向美丽河湖项目的数字化污水治理云边协同采集架构设计
人工智能
沐籽李4 小时前
从溶剂可及表面积SASA理解抗体结构与工程改造
人工智能·药物设计·aidd·sasa
智慧物业老杨4 小时前
物业如何做好预算管理?落地架构逻辑
人工智能·架构
微学AI4 小时前
一根针指向所有方向:挂谷猜想对 LLM Agent 技能-记忆架构的启示
开发语言·人工智能·架构·挂谷猜想
城管不管4 小时前
ReAct、Plan-and-Execute、Reflection 三大智能 Agent 范式核心区别
java·人工智能·算法·spring·ai·动态规划
boppu4 小时前
布草特殊污渍去渍剂的种类及作用
大数据·人工智能
AIsoft_86885 小时前
会议录音转文字与AI纪要工具推荐:免费额度与核心功能对比指南
人工智能