Function Calling:LLM 是怎么学会调工具的

Function Calling:LLM 是怎么学会调工具的

目录

Tool Use 的 Agent 侧实现通常比较容易理解:LLM 输出工具名称和参数,Agent 根据名称从工具注册表中找到对应函数,然后执行调用(参考笔者主页的《Tool Use:Agent是如何调用工具的》)。但这里还有一个关键问题经常被忽略:LLM 为什么能够生成符合要求的工具调用结构?本质上,LLM 仍然只是一个文本生成模型,它并不会真正调用工具,只是在根据上下文预测下一个 token。类似 {"name":"bash","command":"ls -la"} 这样的结构化请求,并不是模型内部存在一个"调用工具"的动作,而是在提示词约束、训练数据以及模型接口规范的共同作用下,让模型按照约定格式生成一段文本,随后由 Agent 框架解析这段文本,并映射到实际的工具函数执行。因此,LLM 负责产生调用意图和参数,Agent 框架负责完成工具发现、参数校验和实际执行,二者共同组成了完整的 Tool Use 流程。

Function Calling是什么

Function Calling 是让 LLM 输出结构化工具调用请求的机制。模型在推理时拿到工具定义,判断用户意图是否需要调用某个工具,如果需要,就输出一段符合约定格式的 JSON;如果不需要,就正常回答。

可以把 Function Calling 理解成"填工单"的过程。用户提了一个需求,模型判断这个需求需要某个部门配合,于是填了一张工单(工具名 + 参数),交给外部系统去执行。模型本身不执行任何操作,它只负责判断要不要填工单、填哪个、参数怎么写。

和上一篇 Tool Use 的关系是这样的:

Function Calling 是上游,Tool Use 是下游。一个管"怎么生成工单",一个管"怎么执行工单"。

工具定义怎么喂给模型

模型不是天生就知道有哪些工具可用。你需要把工具的定义告诉它,方式很直接:拼进 prompt。

来看一个实际的工具定义。假设我们有一个查询天气的工具,用 Anthropic 的格式定义:

python 复制代码
tools = [
    {
        "name": "get_weather",
        "description": "查询指定城市的当前天气",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "城市名称,如 '北京'、'上海'"
                }
            },
            "required": ["city"]
        }
    }
]

OpenAI 的格式稍有不同,字段名用的是 parameters 而不是 input_schema

python 复制代码
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "查询指定城市的当前天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称"
                    }
                },
                "required": ["city"]
            }
        }
    }
]
维度 Anthropic OpenAI
外层字段 input_schema parameters
工具定义位置 直接在对象内 嵌套在 function
响应格式 content 数组中的 tool_use message.tool_calls 数组
结果回传 tool_result content block role: "tool" 消息

格式不同,但本质一样:把工具的名字、用途、参数格式以结构化的方式传给模型。从模型能力的角度看,工具定义最终会以模型能够理解的上下文形式参与推理,本质上是向模型提供了一组关于工具名称、用途和参数约束的信息。不同厂商在接口层可能会采用不同的编码方式(比如通过特殊 token 或 hidden instruction 注入),但目的都是让模型获得工具描述。

把工具定义序列化后,模型实际看到的内容大概是这样的(以 Anthropic 格式为例):

复制代码
You have access to the following tools:

<tool_name>get_weather</tool_name>
<tool_description>查询指定城市的当前天气</tool_description>
<tool_input_schema>
{
    "type": "object",
    "properties": {
        "city": {"type": "string", "description": "城市名称"}
    },
    "required": ["city"]
}
</tool_input_schema>

对模型来说,这就是 prompt 的一部分。它通过理解这段文本来理解"我有一个叫 get_weather 的工具,接收一个 city 参数"。没有编译器,没有类型检查,纯粹是文本理解。

这也解释了为什么 工具的 description 字段那么重要 。模型决定用哪个工具,靠的不是代码逻辑,是语义匹配。你写 "查询天气" 和写 "获取指定地理位置的气象数据,包括温度、湿度和风速",模型的判断行为会不同。description 写得越准确,模型选对工具的概率越高。

模型为什么能生成工具调用

LLM 的基础能力是续写文本,给它一段话,它预测下一个 token。那它是怎么学会在合适的时机输出一段合法 JSON 的?

通常可以理解为两个阶段:先通过 SFT 等监督训练让模型掌握工具调用的格式和基本模式,再通过偏好优化、强化学习或大量工具使用数据训练,让模型进一步学习什么时候应该调用工具、调用哪个工具以及如何填写参数。

第一步:SFT 学格式

SFT(Supervised Fine-Tuning,监督微调)的核心是给模型看大量的"标准答案"。训练数据是 (query, expected_tool_call) 的配对:

复制代码
输入:
  用户: "北京今天天气怎么样?"
  可用工具: [{"name": "get_weather", ...}]

输出:
  {"name": "get_weather", "arguments": {"city": "北京"}}

输入:
  用户: "帮我读一下 config.yaml"
  可用工具: [{"name": "read_file", ...}, {"name": "bash", ...}]

输出:
  {"name": "read_file", "arguments": {"file_path": "config.yaml"}}

输入:
  用户: "你好"
  可用工具: [{"name": "get_weather", ...}, {"name": "read_file", ...}]

输出:
  你好!有什么可以帮你的吗?

第三个例子很关键,模型不仅要学会"怎么输出工具调用",还要学会"什么时候不该调工具"。用户说"你好",没有任何工具调用的意图,模型应该正常回复。

通过大量这样的样本训练,模型学会了一个模式:当用户的问题匹配某个工具的用途时,输出符合该工具 input_schema 的 JSON;否则,正常生成文本。

上面的例子用简化 JSON 来展示工具调用意图。实际格式会根据模型厂商和 API 协议不同有所差异,比如 OpenAI 的工具调用会被包装在 tool_calls 数组中,Anthropic 则以 tool_use content block 的形式返回。但无论外层格式怎么包装,模型内部做的事情是一样的:生成工具名和参数。

来看一个 SFT 训练循环的伪代码:

复制代码
for each (query, tools, expected_output) in training_data:
    # 拼接 prompt:system + 工具定义 + 用户消息
    prompt = build_prompt(system_msg, tools, query)

    # 模型生成输出
    predicted = model.generate(prompt)

    # 计算 loss:模型输出和标准答案的差异
    loss = cross_entropy(predicted, expected_output)

    # 反向传播,更新模型参数
    loss.backward()
    optimizer.step()

训练完成后,模型就具备了"看到工具定义 → 输出结构化调用"的基础能力。但 SFT 有一个问题:它教的是"照着样本抄",模型学到的是格式,不是判断力。

第二步:偏好优化与工具使用训练

SFT 让模型学会了格式,但在边界情况下经常犯错:

  • 用户只是随口聊天,模型却调了工具(不该调的时候调了)
  • 用户明显需要工具帮助,模型却在那瞎编答案(该调的时候没调)
  • 输出的 JSON 格式对了,但参数值是编的(hallucination)

这个阶段的训练手段不只是 RLHF,还包括 DPO(Direct Preference Optimization)、IPO 等偏好优化方法,以及大规模合成工具调用数据的训练。不同厂商的方案各有侧重,但目标一致:让模型学会更精准的调用策略。

以偏好优化为例,训练的核心思路是给模型展示"好"和"差"的对比:

复制代码
用户提问: "帮我查一下什么是量子计算"
    │
    ├── 候选 A:正常回答,不调工具          →  偏好(正例)
    └── 候选 B:调用了搜索工具              →  不偏好(负例)

用户提问: "帮我查一下北京今天多少度"
    │
    ├── 候选 A:调用了 get_weather 工具      →  偏好(正例)
    └── 候选 B:瞎编了一个温度               →  不偏好(负例)

模型通过大量这样的对比样本,逐渐学会了一个更精细的判断:不是所有包含"查"、"读"、"获取"的问题都需要调工具。"帮我查一下什么是量子计算"和"帮我查一下北京今天多少度",表面结构很像,但前者是知识问答,后者才需要调天气工具。

两步训练的分工:

训练阶段 学什么 解决什么问题
SFT 输出格式、工具名匹配 模型知道工具调用长什么样
偏好优化/强化学习 调用时机、参数准确性 模型知道什么时候该调、什么时候不该调

SFT 是"学会填工单",后续的偏好优化和工具使用训练是"学会判断什么时候需要填工单"。两者缺一不可。

模型怎么决定调不调工具

从 token 生成的角度看,模型做的其实是连续的条件概率计算。

假设用户说"北京今天天气怎么样",模型逐 token 生成输出。当生成到第一个 token 的位置时,它面前有两条路:

复制代码
路径 1: {" → 继续输出工具调用 JSON
路径 2: 北  → 继续输出自然语言回答("北京今天...")

模型根据 prompt 中的工具定义、用户意图、以及训练时学到的模式,计算下一 token 的概率分布。在采样过程中,如果工具调用相关的 token 序列具有更高概率,最终就可能生成工具调用结构。

之后每一步都是同样的过程。当模型输出 {"name": "get_weather" 的时候,剩下的参数选择也是一系列 token 级别的概率计算。

stop_reason 是 API 层返回的状态标识,用于告诉客户端本次生成是否因为工具调用而结束。当模型判断不需要调工具时,它会正常生成文本直到结束,stop_reasonend_turn;当它判断需要调工具时,输出工具调用块后停止,stop_reasontool_use。不同厂商的实现方式不同,但这个字段帮助 Agent 代码快速判断下一步该做什么:拿到 tool_use 就去执行工具,拿到 end_turn 就把结果返回给用户。

两层定义的关系

Tool Use 那篇我们提过,工具定义了两遍:JSON Schema 给 LLM 看,注册表给代码看。现在从模型侧来看这两层的关系。

JSON Schema 定义了工具的名字、参数类型、必填字段,模型通过理解这些信息来生成调用请求。当它决定调用某个工具时,按照理解到的格式输出 JSON。

真正的执行在 Agent 侧。Agent 代码拿到模型输出的 JSON,解析出工具名,去注册表里查找对应的执行函数。两层定义通过"工具名"这个字符串桥接:

复制代码
工具定义(JSON Schema)
    │
    │  拼接进 prompt
    ▼
LLM 理解工具的 name、description、parameters
    │
    │  输出 {"name": "bash", "command": "ls"}
    ▼
Agent 代码解析输出
    │
    │  用 "bash" 去注册表查找
    ▼
注册表 → execute_bash 函数 → 执行命令

模型不需要知道注册表的存在,注册表也不需要知道工具定义长什么样。两者通过工具名解耦。这也是为什么不同厂商的 JSON Schema 格式可以不同(Anthropic 用 input_schema,OpenAI 用 parameters):格式不同不影响模型理解工具,也不影响 Agent 侧的执行。

一个容易忽略的细节:工具的 description 比 schema 本身更影响模型行为 。模型选工具靠的是语义匹配,不是代码解析。同样一个 read_file 工具,description 写"读文件"和写"读取指定路径的文件内容,返回 UTF-8 编码的文本",模型在遇到"帮我看看这个文件里写了什么"时的匹配概率是不同的。

和原生 JSON 输出的区别

你可能会想:让模型输出 JSON 不是早就能做了吗?直接在 prompt 里说"请以 JSON 格式回复",模型也能输出 JSON。Function Calling 和这有什么区别?

区别在于语义。

维度 Function Calling 原生 JSON 输出
目的 告诉外部系统"我要调用某个工具" 输出结构化数据
输出语义 工具名 + 参数,有明确的调用意图 任意 JSON,没有调用意图
是否触发副作用 是,Agent 会根据输出执行工具 否,纯数据,不执行操作
训练方式 SFT + 偏好优化/强化学习 prompt engineering 即可
可靠性 高,格式基本稳定 低,模型可能输出非法 JSON

用一个具体例子说明。用户问:"北京今天多少度?"

Function Calling 输出:

json 复制代码
{"name": "get_weather", "arguments": {"city": "北京"}}

这是一个工具调用请求。Agent 收到后会执行 get_weather(city="北京"),拿到真实数据返回给用户。

原生 JSON 输出(prompt 要求以 JSON 回复):

json 复制代码
{"city": "北京", "temperature": "25°C", "condition": "晴"}

这是一个普通的数据对象。但这个温度是模型编的,不是真实数据。模型没有调用任何工具,它只是按照 JSON 格式输出了它"认为"的答案。

Function Calling 的价值不在于"输出 JSON",而在于模型通过训练学会了两个判断:什么时候该调工具 ,以及调哪个工具、参数怎么填。原生 JSON 输出没有这个判断能力,它只是格式化了模型的文本生成。

小结

Function Calling 的本质并不是让 LLM 获得执行工具的能力,而是让模型具备在特定上下文下生成结构化调用意图的能力。LLM 负责理解用户需求、选择合适的工具并生成参数,Agent Runtime 负责工具校验、实际执行以及结果回传,二者结合形成完整的工具调用闭环。

在实际项目中接入 Function Calling 时,工具定义的设计非常重要。其中,工具的名称、description 以及参数 schema 都会影响模型的调用效果。清晰准确的工具描述能够帮助模型更好地理解工具用途,减少错误选择和参数填写问题。同时,还需要结合实际运行情况观察模型的调用稳定性,例如是否存在误调用、漏调用或参数错误等问题,再通过优化提示词、调整工具设计、增加训练数据等方式持续改进。

相关推荐
zhayujie1 小时前
如何设计一个 Agent 友好的 CLI 工具
ai·大模型·agent·cli·harness
zavoryn2 小时前
Word 图片如何进入 RAG:Apache POI、MinIO 与 URL 映射
word·agent·rag
枫叶丹42 小时前
OpenClaw Agent 搭建全流程:从本机自托管到可用、可控、可审计的个人 AI 助手
人工智能·agent
怕浪猫2 小时前
第16章 未来展望:通往 AGI 之路
openai·agent·ai编程
七牛开发者2 小时前
从 Fable 案例看 Coding Agent 的未知项管理
ai·agent·claude·loop·codex
老谷子.AI原始技术3 小时前
《Claude Code 工程化实战》第 27 爆款 Agent 的设计密码与工程范式
agent·claude code
Z-D-K12 小时前
我认为的AGI
人工智能·ai·人机交互·agent·agi
树獭非懒15 小时前
七、智能体的Reflection机制:让AI学会三省吾身
人工智能·llm·agent
copyer_xyf17 小时前
RAG 入库实战:一份原始资料,如何变成可检索的知识
agent