在这里插入图片描述

本文从原理到可运行代码完整覆盖 Function Calling,包含工程级封装模板、安全设计要点与参数调优建议,代码兼容 OpenAI 规范接口(DeepSeek、Qwen 等均适用)。


一、为什么 Function Calling 是智能体的核心?

大模型的能力演进可以分为几个阶段:从识别图像和语音的感知 AI,到能够聊天和生成内容的生成式 AI,再到能够思考并调用工具完成任务的代理式 AI(Agentic AI),最终走向能控制物理设备的物理 AI。

感知 AI

生成式 AI

代理式 AI

物理 AI

图像/语音识别

聊天/写作/生图

思考并调用工具

控制物理设备

Function Calling 是生成式 AI 迈向代理式 AI 的核心桥梁。

通俗来说:如果 LLM 是智能体的"大脑",Function Calling 就是它的"运动神经"——让 AI 跳出聊天框,通过调用外部工具、API、数据库,真正实现"思考并行动"。

举个具体例子:以前问 AI"今天北京天气怎么样",它只能根据训练数据猜测;有了 Function Calling,AI 会调用天气 API 获取实时数据,再整理成自然语言回答,这是"从对话到行动"的本质区别。


二、Function Calling 的工作原理

核心机制:LLM 做决策,程序做执行

很多开发者初接触 Function Calling 时会误以为模型能直接执行代码——实际上,LLM 本身不执行任何代码,它只负责决策:判断是否需要调用工具、调用哪个工具、传入什么参数。具体的执行逻辑全部由开发者编写的程序完成。

整个调用流程分为7步:

1. 定义工具(描述 + JSON Schema)

2. 输入需求

3. 决策:返回结构化 tool_calls 指令

4. 执行工具逻辑

5. 返回执行结果

6. 将结果反馈给 LLM

7. 整理为自然语言

开发者

大模型 LLM

用户

开发者程序

外部工具 / API / 数据库

职责边界与安全设计

明确 LLM 与开发者程序的职责边界,是避免安全漏洞、减少故障的关键:

维度 LLM 职责(决策层) 开发者程序职责(执行层)
工具定义 理解工具的功能描述与参数 Schema 编写具体的工具实现代码
意图识别 判断是否需要调用工具、选择哪个工具 接收 tool_calls 指令并触发执行
安全边界 严格按 Prompt 约束提取参数 隐藏内部路径、SQL 等敏感信息,防止泄露
闭环处理 根据执行结果判断是否继续调用 捕获工具异常,返回结构化错误信息

触发机制:原生支持 vs Prompt 模板

Function Calling 有两种触发方式:

  • 模型原生支持:主流模型(DeepSeek、Qwen、GPT-4o 等)内置了 Function Calling 能力,传入工具定义后模型自动判断是否调用,这是生产环境的主流选择。
  • 基于 Prompt 模板(ReAct):通过设计"思考→行动→观察"的 Prompt 结构,引导不原生支持 Function Calling 的模型执行类似行为,适合旧版或轻量模型。

工程避坑:当工具数量过多(如 10 个以上),把所有工具定义一次性传给模型会带来两个问题:一是 Token 消耗大幅上升,二是模型容易在过多选项中产生混淆,出现"调用错误工具"或"参数遗漏"。

推荐方案:意图预分类——先用轻量模型(或简单分类器)判断用户意图的大类(查询类、计算类、操作类等),再按意图只加载对应类别的工具子集,降低模型推理负担,提升调用准确率。


三、Agent 循环(Agent Loop):智能体的运行生命周期

Chatbot 和 Agent 的核心区别在于:Agent 有完整的"思考 → 执行 → 反馈"循环,能主动完成多步骤任务;Chatbot 只能被动响应单次输入。

在 LangGraph、AutoGen 等主流 Agent 框架中,一个完整的推理周期分为5步:

超过最大迭代次数

1. 发起请求
Prompt + 对话历史 + 工具定义

2. 模型决策
返回 tool_calls 指令

3. 本地执行
隔离上下文运行工具

4. 结果反馈
将 observation 存入对话历史

5. 任务完成?

返回最终响应

强制终止,返回错误信息

各步骤的工程要点:

发起请求:将用户 Prompt、完整的对话历史和当前意图对应的工具定义一起发送给 LLM,确保模型拥有足够的上下文。

模型决策:LLM 返回结构化的 tool_calls 指令(而非自然语言),包含工具名称和参数。如需防止多线程场景下的资源冲突,应在会话实例级别而非全局级别设计锁机制(全局锁会导致所有用户请求串行化)。

本地执行:在独立上下文(如独立线程)中运行工具,防止一个请求的执行错误影响其他请求。

结果反馈:将工具执行结果(observation)以 role: "tool" 的格式存入对话历史,再次发送给 LLM,让模型知道上一步操作的结果。

最大迭代次数(Max Iterations):这是防止死循环的关键机制,建议设置为 5~10 次。超过阈值后系统应强制终止并返回明确的错误信息,这是生产稳定性的基本保障。


四、工具设计原则:如何定义一个"好"的工具

极简工具集原则

"工具越多越强大"是一个常见误区——工具过多反而会让模型选择困难,导致调用错误率上升。

推荐从最小工具集出发,只在真正需要时扩展。一个经典的通用工具集只需4个工具就能覆盖大多数文件操作和系统任务:

  • read:读取文件或配置信息
  • write:写入数据或生成报告
  • edit:增量修改文件内容
  • bash:执行系统命令或脚本

核心思路:用最少、最通用的工具,让 LLM 的推理能力主导任务规划,而不是让模型陷入工具选择困难。实际业务中,应根据具体场景评估并扩展,而非盲目堆砌工具数量。

MCP 规范:标准化的错误处理

在 MCP(Model Context Protocol)规范中,工具返回的结果必须遵循统一格式,否则模型可能将错误信息误判为正常结果,导致错误推理。核心规范:

  • 必须包含 isError 字段:明确标识返回结果是成功(false)还是失败(true),不能让模型猜测
  • 禁止返回原始堆栈跟踪(Raw Traceback):堆栈信息会暴露文件路径、代码行数、环境变量等敏感信息,属于严重安全漏洞

一个"模型友好型错误"应该包含三个要素:

  1. 发生了什么:简洁说明错误类型(如"计算失败")
  2. 为什么发生:给出具体原因(如"表达式包含未定义的变量")
  3. 什么是正确格式:给出可执行的修正指引(如"请使用数字和 +-*/ 组成表达式,示例:100 * (2 + 3)")

对比示例:

❌ 错误示例(暴露敏感信息):
Traceback (most recent call last):
  File "/root/agent.py", line 25, in safe_calculator
    result = eval(expression)
NameError: name 'x' is not defined

✅ 正确示例(模型友好型):
计算失败。原因:表达式包含未定义的变量。
有效示例:'100 * (2 + 3)'、'1024 / 8 - 10'
请仅使用数字和常见运算符(+、-、*、/)。

五、实战:完整工具调用循环(Python)

以下是一个生产可用的 Function Calling 实现,包含安全计算器工具、Pydantic 参数校验、规范的错误处理和对话历史管理。兼容所有 OpenAI 规范接口(DeepSeek、Qwen、GPT-4o 等)。

环境要求:pip install openai pydantic(Pydantic v2)

import json
import os
import ast
from openai import OpenAI
from pydantic import BaseModel, ValidationError, ConfigDict

# client 在模块级别初始化,复用 HTTP 连接池
client = OpenAI(
    api_key=os.environ["LLM_API_KEY"],
    base_url=os.environ.get("LLM_BASE_URL", "https://api.deepseek.com"),
)


# ── 工具定义 ──────────────────────────────────────────────

class CalculatorParams(BaseModel):
    """安全计算器的参数 Schema(Pydantic v2 写法)"""
    model_config = ConfigDict(extra="forbid")  # 禁止传入多余参数
    expression: str


def safe_calculator(expression: str) -> dict:
    """
    安全计算器:使用 ast 模块白名单校验,替代裸 eval,防止代码注入。
    返回符合 MCP 规范的结构:{"content": str, "isError": bool}
    """
    ALLOWED_NODES = (
        ast.Expression,   # 顶层节点,必须包含
        ast.Constant,     # 数字字面量
        ast.BinOp,        # 二元运算(如 a + b)
        ast.UnaryOp,      # 一元运算(如 -a)
        ast.Add, ast.Sub, ast.Mult, ast.Div,
        ast.Mod, ast.Pow, ast.FloorDiv,
    )
    try:
        tree = ast.parse(expression, mode="eval")
        for node in ast.walk(tree):
            if not isinstance(node, ALLOWED_NODES):
                raise ValueError(f"不支持的表达式类型:{type(node).__name__}")

        # 禁用所有内置函数,防止沙箱逃逸
        result = eval(
            compile(tree, filename="<calculator>", mode="eval"),
            {"__builtins__": {}},
            {},
        )
        return {"content": str(result), "isError": False}

    except ValueError as e:
        return {
            "content": (
                f"计算失败。原因:{e}。"
                "有效示例:'1024 * 8 + 512'、'200 / 5 - 10'。"
                "请仅使用数字和运算符(+、-、*、/、**、//、%)。"
            ),
            "isError": True,
        }
    except Exception:
        # 通用异常:不暴露内部细节
        return {
            "content": (
                "计算失败。原因:表达式格式错误。"
                "有效示例:'1024 * 8 + 512'。"
            ),
            "isError": True,
        }


# ── 工具注册表 ─────────────────────────────────────────────

TOOL_REGISTRY = {
    "safe_calculator": safe_calculator,
}

TOOLS_DEFINITION = [
    {
        "type": "function",
        "function": {
            "name": "safe_calculator",
            "description": (
                "执行简单数学运算,支持 +、-、*、/、**(幂)、//(整除)、%(取余)。"
                "仅接受数字和上述运算符,不支持变量、函数或复杂表达式。"
            ),
            "parameters": CalculatorParams.model_json_schema(),  # Pydantic v2 正确方法
        },
    }
]


# ── Agent 循环 ─────────────────────────────────────────────

def run_agent_loop(user_input: str, max_iterations: int = 8) -> str:
    """
    完整的 Function Calling 循环。
    - 使用 dict 维护对话历史(避免序列化问题)
    - 工具执行异常不会中断循环,而是作为错误信息反馈给模型
    - 超过最大迭代次数后强制终止
    """
    messages = [
        {
            "role": "system",
            "content": (
                "你是一个严谨的数学助手。"
                "仅在需要计算时调用 safe_calculator 工具,非计算问题直接回复。"
                "调用工具时严格遵循参数规范,不传入多余字段。"
                "收到工具结果后,整理为清晰的自然语言回复。"
            ),
        },
        {"role": "user", "content": user_input},
    ]

    for iteration in range(max_iterations):
        response = client.chat.completions.create(
            model="deepseek-chat",
            messages=messages,
            tools=TOOLS_DEFINITION,
            temperature=0.1,  # 工具调用场景使用低温度,提升指令遵循确定性
        )

        msg = response.choices[0].message

        # 将模型回复转为 dict 存入历史(避免直接 append 对象导致序列化失败)
        assistant_message = {"role": "assistant", "content": msg.content}
        if msg.tool_calls:
            assistant_message["tool_calls"] = [
                {
                    "id": tc.id,
                    "type": "function",
                    "function": {
                        "name": tc.function.name,
                        "arguments": tc.function.arguments,
                    },
                }
                for tc in msg.tool_calls
            ]
        messages.append(assistant_message)

        # 模型决定不调用工具,直接返回结果
        if not msg.tool_calls:
            return msg.content

        # 逐个处理工具调用
        for tool_call in msg.tool_calls:
            tool_name = tool_call.function.name
            print(f"[iter {iteration + 1}] 调用工具: {tool_name} | 参数: {tool_call.function.arguments}")

            # 参数校验(防止模型返回非法参数)
            try:
                raw_args = json.loads(tool_call.function.arguments)
                CalculatorParams(**raw_args)  # Pydantic 校验
            except (json.JSONDecodeError, ValidationError) as e:
                tool_result = {
                    "content": (
                        f"参数校验失败。原因:{e}。"
                        "有效格式示例:{\"expression\": \"1024 * 8 + 512\"}"
                    ),
                    "isError": True,
                }
                messages.append({
                    "role": "tool",
                    "tool_call_id": tool_call.id,
                    "content": tool_result["content"],
                })
                continue

            # 执行工具(未知工具安全处理)
            executor = TOOL_REGISTRY.get(tool_name)
            if executor is None:
                tool_result = {
                    "content": f"工具 '{tool_name}' 不存在,请检查工具名称。",
                    "isError": True,
                }
            else:
                tool_result = executor(**raw_args)

            # 将执行结果反馈给模型
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": tool_result["content"],
            })

    # 超过最大迭代次数,强制终止
    return (
        f"已达到最大迭代次数({max_iterations}),任务强制终止。"
        "请简化任务描述,或检查输入是否符合工具能力范围。"
    )


# ── 使用示例 ───────────────────────────────────────────────

if __name__ == "__main__":
    query = "帮我算一下 1024 乘以 8 再加上 512 的结果,另外算一下 200 除以 5 减去 10 是多少"
    result = run_agent_loop(query)
    print("\n最终响应:", result)

代码设计要点说明

ast 白名单校验:必须在 ALLOWED_NODES 中包含 ast.Expression(顶层节点),否则所有表达式都会被错误拒绝。同时显式禁用 __builtins__,防止通过表达式调用内置函数实现沙箱逃逸。

对话历史用 dict 而非对象response.choices[0].message 是 SDK 返回的 ChatCompletionMessage 对象,不能直接追加到 messages 列表后重新传给 API——序列化时会报错。应手动转为 dict,或使用 SDK 提供的 .model_dump() 方法。

Pydantic v2 兼容写法Config 内部类是 Pydantic v1 语法,在 v2 中应改为 model_config = ConfigDict(...).schema() 方法已废弃,应改为 .model_json_schema()

工具注册表设计:通过 TOOL_REGISTRY 字典统一管理工具映射,避免在循环中写大量 if tool_name == "xxx" 的硬编码分支,便于扩展和维护。


六、进阶优化

幻觉抑制(工具错误调用的防控)

Function Calling 中最常见的问题是模型"幻觉调用"——在不需要调用工具时调用,或传入格式错误的参数。三个经过验证的抑制方法:

Prompt 强约束 + Few-shot 示例:在 System Prompt 中明确约束调用条件,并给出 2~3 个正反示例引导模型"先判断是否需要调用,再决定调用哪个":

仅在需要数学计算时调用 safe_calculator,其他情况直接回复。
示例1:用户问"1024乘以8是多少" → 调用 safe_calculator
示例2:用户问"介绍一下Python" → 直接回复,不调用工具

降低 Temperature:Function Calling 场景推荐将 Temperature 设置为 0.0~0.2,提升指令遵循的确定性,减少模型"随机发挥"导致的错误调用。注意:不同模型的默认 Temperature 不同(DeepSeek 和 OpenAI 默认均为 1.0),应根据实际使用的模型文档确认默认值。

Pydantic 强类型校验:对模型返回的工具参数做 Schema 校验,发现格式不符时立即返回结构化错误信息,引导模型在下一轮修正参数,而非让错误参数进入执行层。

模型选型建议

不同模型在 Function Calling 能力上差异显著,但模型迭代速度极快,具体排名随版本更新而变化。以下是选型的通用框架,具体选择应以最新的基准测试(如 Berkeley Function-Calling Leaderboard)为准:

场景 选型考量
复杂多步骤工具调用 优先选用参数量较大、推理能力强的模型;定期评估最新版本
中文意图识别 选择在中文语料上训练充分的模型(如 Qwen 系列、DeepSeek)
高频低延迟场景 选择有专门推理优化版本的模型(如各厂商的 turbo/lite 版本)
成本敏感场景 先用大模型跑通逻辑,再蒸馏或微调小模型替代

七、总结

Function Calling 是大模型从"聊天"走向"行动"的核心机制,也是构建 Agent 系统的基础能力。几个关键工程原则:

职责分离:LLM 只做决策(选工具、选参数),程序负责执行。永远不要让 LLM 直接操作敏感资源。

防御性设计:对模型输出的参数做强类型校验;工具执行时屏蔽底层异常细节;设置最大迭代次数防死循环。

极简工具集:从最少的工具开始,只在真正需要时扩展,避免模型选择困难。

错误反馈规范化:错误信息要"模型友好"——说清发生了什么、为什么、怎么修正,禁止返回原始堆栈跟踪。

可观测性:记录每次工具调用的输入、输出和执行时间,是排查 Agent 行为异常的基础。

随着多模态模型(视觉 + 语言 + 行动)的持续发展,Function Calling 的边界将从 API 调用延伸到物理设备控制,向真正的物理 AI 迈进。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐