【Agent开发】—— ToolCall 、 FunctionCall 底层原理与极简实现

提到 ToolCall(工具调用)或 FunctionCall(函数调用),很多人会觉得是 LLM 的"高级黑科技"------仿佛模型能直接操作外部程序、调用 API。但真相是:它没有任何魔法,就是"提示词引导 + 结构化生成 + 后端解析"的组合拳。

这篇文章会剥掉所有框架封装,从最底层讲清实现逻辑,最后用 50 行代码写一个极简版 ToolCall,让你彻底明白:原来 LLM 调用工具这么简单。


一、核心真相:ToolCall 本质是什么?

一句话概括:
ToolCall 是让 LLM 放弃"自然语言回答",转而输出「固定格式的工具调用指令」,再由后端解析指令并执行真实工具,最后把结果回传给 LLM 生成最终回答的闭环。

拆解成 3 个关键环节:

  • 告诉模型"有什么工具":通过提示词传递工具名称、功能、参数结构;
  • 让模型"输出规范指令":LLM 按约定格式(如 JSON)输出"调用哪个工具、传什么参数";
  • 后端"执行并回传结果":解析指令→调用工具→把结果塞回对话上下文,让 LLM 继续处理。

本质上,这就是「提示工程 + 结构化生成」的应用------模型没有任何"调用能力",只是个"格式正确的指令生成器"。


二、从 0 拆解实现流程(无框架版)

我们用"计算 3+5 并乘以 2"的场景,一步步拆解完整流程,不依赖 LangChain、LlamaIndex 等任何框架。

步骤 1:定义工具(真实可执行的函数)

首先,我们需要一个"真实能用的工具"------比如两个简单的数学函数:

python 复制代码
# 真实的工具函数(后端实现)
def add(a: int, b: int) -> int:
    """计算两个整数的和"""
    return a + b

def multiply(a: int, b: int) -> int:
    """计算两个整数的积"""
    return a * b

# 工具映射表(方便后续根据指令匹配函数)
tool_map = {
    "add": add,
    "multiply": multiply
}

这些工具就是普通的 Python 函数,和 LLM 没有任何直接关联------LLM 永远不会"直接调用"它们,只会输出调用指令。

步骤 2:用提示词"教模型用工具"

模型不知道我们有哪些工具,也不知道该怎么输出指令,所以必须通过提示词明确告诉它:

python 复制代码
system_prompt = """
你是一个数学助手,需要通过调用工具完成计算任务。
你可以使用以下工具:
1. 工具名:add
   功能:计算两个整数的和
   参数:a(整数)、b(整数)
2. 工具名:multiply
   功能:计算两个整数的积
   参数:a(整数)、b(整数)

### 输出规则:
如果需要调用工具,请严格按照以下 JSON 格式输出,不要加任何额外内容:
{
  "tool_calls": [
    {
      "name": "工具名",
      "parameters": {
        "参数名1": 值1,
        "参数名2": 值2
      }
    }
  ]
}

如果不需要调用工具(如直接能回答的问题),直接输出自然语言回答。
"""

这是 ToolCall 的核心:用提示词定义工具、约束输出格式。早期的 FunctionCall 本质就是这么实现的------没有任何特殊接口,全靠提示词引导。

步骤 3:让模型输出工具调用指令

用户输入问题后,我们把「系统提示词 + 用户问题」传给 LLM,模型会输出结构化的调用指令:

python 复制代码
import openai

# 用户问题
user_query = "3加5等于多少,再乘以2?"

# 构造对话上下文
messages = [
    {"role": "system", "content": system_prompt},
    {"role": "user", "content": user_query}
]

# 调用 LLM(这里用 OpenAI 示例,其他模型逻辑一致)
response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages,
    temperature=0  # 温度设为 0,保证输出格式稳定
)

# 提取模型输出的指令(JSON 字符串)
tool_calls_str = response.choices[0].message["content"]
print("模型输出的工具调用指令:")
print(tool_calls_str)

模型输出结果(严格遵循我们约定的格式):

json 复制代码
{
  "tool_calls": [
    {
      "name": "add",
      "parameters": {"a": 3, "b": 5}
    },
    {
      "name": "multiply",
      "parameters": {"a": 8, "b": 2}
    }
  ]
}

步骤 4:解析指令,执行工具

模型输出的是 JSON 字符串,我们需要解析它,找到对应的工具并执行:

python 复制代码
import json

# 解析 JSON 指令
tool_calls = json.loads(tool_calls_str)["tool_calls"]

# 执行每个工具调用
tool_results = []
for call in tool_calls:
    tool_name = call["name"]
    tool_args = call["parameters"]
    
    # 匹配工具函数
    tool_func = tool_map.get(tool_name)
    if not tool_func:
        tool_results.append(f"工具 {tool_name} 不存在")
        continue
    
    # 执行工具并记录结果
    result = tool_func(**tool_args)
    tool_results.append(f"调用 {tool_name}({tool_args}) → 结果:{result}")

# 打印工具执行结果
print("工具执行结果:")
print("\n".join(tool_results))

执行结果:

复制代码
调用 add({"a": 3, "b": 5}) → 结果:8
调用 multiply({"a": 8, "b": 2}) → 结果:16

步骤 5:把结果回传给 LLM,生成最终回答

工具执行完成后,我们需要把结果塞回对话上下文,让 LLM 整理成自然语言回答:

python 复制代码
# 构造工具结果的提示词
tool_result_content = "\n".join(tool_results)
messages.append({"role": "system", "content": f"工具执行结果:{tool_result_content}"})

# 再次调用 LLM,生成最终回答
final_response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages,
    temperature=0.7
)

# 输出最终回答
print("LLM 最终回答:")
print(final_response.choices[0].message["content"])

最终回答(自然语言整理):

首先调用 add 工具计算 3+5,结果为 8;再调用 multiply 工具计算 8×2,结果为 16。因此,3 加 5 再乘以 2 的最终结果是 16。


三、为什么后来有了"ToolCall"(而非 FunctionCall)?

早期 OpenAI 提出的 FunctionCall,本质是上面流程的"简化版"------只支持单个函数调用,格式是:

json 复制代码
"function_call": {
  "name": "...",
  "parameters": "..."
}

但实际场景中,我们需要:

  • 多工具并行调用:比如同时调用"搜索天气"和"查询日历";
  • 非函数工具:比如代码解释器、知识库检索(RAG)、浏览器等,这些不是单纯的函数;
  • 更灵活的格式:需要给每个调用加 ID,方便匹配结果(比如并行调用时区分哪个结果对应哪个调用)。

所以后来升级为 ToolCall,核心变化是:

  • 支持多个调用(tool_calls 数组);
  • 增加 idtype 字段(type 可指定是 function、code_interpreter 等);
  • 格式更通用,适配各种工具类型。

比如 OpenAI 新版 ToolCall 格式:

json 复制代码
{
  "tool_calls": [
    {
      "id": "call_123",
      "type": "function",
      "function": {
        "name": "add",
        "parameters": {"a": 3, "b": 5}
      }
    }
  ]
}

但本质没变------还是"提示词引导 + 结构化生成 + 解析执行"。


四、框架(LangChain)是怎么封装的?

上面的流程需要手动处理"解析指令、执行工具、回传结果",LangChain 等框架做的就是"自动化这些重复工作"。比如 LangChain 的 ToolCall 封装:

  • 工具标准化:用 @tool 装饰器自动生成工具描述和 JSON Schema,不用手动写提示词;
  • 指令解析自动化:自动识别模型输出的 tool_calls 字段,不用手动解析 JSON;
  • 执行流程自动化:通过 AgentExecutor 自动循环"调用工具→回传结果",直到完成任务;
  • 多工具适配:支持函数、RAG、代码解释器等各种工具,统一接口。

但底层逻辑和我们上面写的 50 行代码完全一致------框架没有创造新原理,只是减少了重复工作。


五、3 个关键真相(避坑必看)

  1. LLM 不会"直接调用"工具

    模型只输出字符串指令,真正执行工具的是你的后端代码。

  2. 格式稳定性靠"提示词 + 微调"

    早期 FunctionCall 纯靠提示词约束格式,后来模型通过微调优化了格式稳定性(比如不会输出多余字符)。

  3. ToolCall 是 FunctionCall 的超集

    FunctionCall 只能调用函数,ToolCall 可以调用任何工具(函数、搜索、RAG 等),格式更灵活。


六、极简版 ToolCall 完整代码(50 行)

把上面的流程整合,得到一个可直接运行的极简版 ToolCall:

python 复制代码
import json
import openai

# 1. 定义工具
def add(a: int, b: int) -> int:
    return a + b

def multiply(a: int, b: int) -> int:
    return a * b

tool_map = {"add": add, "multiply": multiply}

# 2. 提示词配置
system_prompt = """
你是数学助手,可调用 add/multiply 工具,格式如下:
{
  "tool_calls": [{"name": "工具名", "parameters": {"参数名": 值}}]
}
"""

# 3. 核心流程
def toolcall_demo(user_query):
    # 第一步:模型生成工具调用指令
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": user_query}
    ]
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo", messages=messages, temperature=0
    )
    tool_calls = json.loads(response.choices[0].message["content"])["tool_calls"]
    
    # 第二步:执行工具
    tool_results = []
    for call in tool_calls:
        func = tool_map[call["name"]]
        res = func(**call["parameters"])
        tool_results.append(f"{call['name']}{call['parameters']} → {res}")
    
    # 第三步:生成最终回答
    messages.append({"role": "system", "content": "\n".join(tool_results)})
    final_res = openai.ChatCompletion.create(
        model="gpt-3.5-turbo", messages=messages, temperature=0.7
    )
    return final_res.choices[0].message["content"]

# 测试
print(toolcall_demo("3加5再乘以2等于多少?"))

运行结果:

首先通过 add(a=3, b=5) 计算得到结果 8,再通过 multiply(a=8, b=2) 计算得到结果 16,因此 3 加 5 再乘以 2 的结果是 16。


总结

ToolCall/FunctionCall 不是什么高深技术,核心就是"让 LLM 按规则输出指令,后端按指令执行工具"。理解了这个逻辑,你就能轻松看透各种框架的封装,甚至自己实现一个定制化的工具调用系统。

如果需要扩展:

  • 想支持并行调用?只需把工具执行部分改成异步;
  • 想支持 RAG 工具?只需在 tool_map 中加入 RAG 检索函数;
  • 想支持错误处理?只需在执行工具时加入异常捕获。

本质不变,只是在"解析指令→执行工具"的环节做扩展而已。

相关推荐
mounter6252 小时前
【LSF/MM内核前沿】Linux 内存回收推倒重来?解析 MGLRU 与传统 LRU 的“统一之战”
linux·运维·服务器·网络·内核·内存回收
Exquisite.3 小时前
k8s的Pod管理
linux·运维·服务器
IMPYLH3 小时前
Linux 的 env 命令
linux·运维·服务器·数据库
桌面运维家3 小时前
Nginx服务器安全:高级访问控制与流量清洗实战
服务器·nginx·安全
奇妙之二进制3 小时前
后端常见分层模型
linux·服务器
拾贰_C3 小时前
【Ubuntu | Nvidia 】nvidia 驱动安装
linux·运维·ubuntu
zzzsde3 小时前
【Linux】EXT文件系统(2)
linux·运维·服务器
艾莉丝努力练剑3 小时前
【QT】QT快捷键整理
linux·运维·服务器·开发语言·图像处理·人工智能·qt
硅基导游3 小时前
bpf监控某个应用里各线程锁的申请得到及释放时间
服务器·互斥锁·性能监控