无框架手写实现Function Calling:原理拆解+纯Python手写实现

零、文章基础说明

很多朋友在使用Function Calling时,都会依赖 LangChain、LlamaIndex 等框架,对function calling完全不清楚底层调用逻辑、对话流转机制、参数解析原理

  1. 本文零框架、零第三方AI工具库依赖,基于阿里云通义千问 OpenAI 兼容接口,从底层原理、对话流转、原生参数解析、手写代码实现四个维度,完整拆解 Function Calling 核心能力,理清楚大模型工具调用的本质

  2. 所有内容均为原生实现,不依赖任何封装框架,全程可追溯、可调试、可自定义改造

  3. 大模型使用阿里云百炼的qwen-plus:help.aliyun.com/zh/model-st...

  • 用DeepSeek或者别的任何模型都行,最核心的是理解执行原理

一、Function Calling 核心本质

Function Calling(工具调用)的本质并不是大模型自动执行代码,而是 在给大模型问题的时候,将可能用到的工具信息(函数)一起全部发给大模型。大模型根据用户问题,判断是否需要调用对应的工具(函数),如需要则大模型结构化输出工具名称和入参,由本地代码(例如你自己的Python项目)完成工具执行、然后将Python执行结果回传给大模型,大模型集合工具输出和问题返回最终答案 的流程。

整个流程分为三段核心逻辑:

  1. 第一轮对话:传入用户问题+工具描述,大模型判断需要调用工具,返回结构化的工具调用参数(不直接回答用户)
  2. 本地执行:解析大模型返回的工具参数,本地Python代码执行对应函数,获取真实结果
  3. 第二轮对话:将工具执行结果回传给大模型,大模型整合结果,生成最终自然语言回答

二、通义千问 Function Calling 对话全流程(原生参数详解)

这部分将通过普通的 http 请求完成function calling的演示,无论用postman/apifox/apipost都可以,此处使用的是 Reqable(即使使用CURL也完全OK)

  1. 本文基于阿里云通义千问 OpenAI 兼容接口实现(接口地址:https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions),适配标准 OpenAI 格式的 Function Calling 参数。

  2. 以「查询深圳天气」场景为例,完整拆解第一轮请求、第一轮响应、第二轮请求、最终响应的路数据流转。

2.1 核心请求参数详解(Function Calling 必备)

  1. 区别于普通对话,Function Calling 需要新增toolstool_choice 两个核心参数,结合基础对话参数,完整必填参数说明如下:
  • model :必选,模型名称,本文使用 qwen-plus
  • messages:必选,对话上下文数组,包含 system(角色设定)、user(用户提问)、assistant(模型回复)、tool(工具返回结果)四类消息
  • tools:必选,工具描述数组,告知大模型「有哪些工具、工具作用、需要什么参数」,是大模型判断调用逻辑的核心依据
  • tool_choice :可选,工具调用策略,auto 表示大模型自主判断是否调用工具、调用哪个工具

2.2 第一轮对话:用户提问,模型触发工具调用

第一轮核心目的:告诉大模型用户需求 + 可用工具,让大模型输出标准化工具调用指令。此时大模型不会直接回答问题,只会返回工具调用参数

  • 重点:这里的function信息实际上就是对应一个Python函数的描述

第一轮完整请求体

json 复制代码
{
  "model": "qwen-plus",
  "messages": [
    {
      "role": "system",
      "content": "你是一位天气预报专家,能够预测给定城市的天气情况"
    },
    {
      "role": "user",
      "content": "深圳今天的天气怎么样"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather_for_location",
        "description": "Get weather for a given city",
        "parameters": {
          "type": "object",
          "properties": {
            "city": {
              "type": "string",
              "description": "需要查询天气的城市名称"
            }
          },
          "required": ["city"]
        }
      }
    }
  ],
  "tool_choice": "auto"
}

第一轮响应结果(核心)

大模型识别到「天气查询需要外部工具」,返回 tool_calls 字段,代表需要进行工具调用(对应需要执行Python方法拿到结果)

  • 特别注意:tool_calls里面有对应的Id,也就是函数调用Id:call_734c63b9a3d44470937769
json 复制代码
{
  "model": "qwen-plus",
  "id": "chatcmpl-0ba4eb7c-101b-9da5-ac3b-aa09a0bbb089",
  "choices": [
    {
      "message": {
        "content": "",
        "tool_calls": [
          {
            "index": 0,
            "id": "call_734c63b9a3d44470937769",
            "type": "function",
            "function": {
              "name": "get_weather_for_location",
              "arguments": "{\"city\": \"深圳\"}"
            }
          }
        ],
        "role": "assistant"
      },
      "index": 0,
      "finish_reason": "tool_calls"
    }
  ],
  "created": 1779518579,
  "object": "chat.completion",
  "usage": {
    "total_tokens": 168,
    "completion_tokens": 21,
    "prompt_tokens": 147,
    "prompt_tokens_details": {
      "cached_tokens": 0
    }
  }
}

关键标识:finish_reason: "tool_calls",代表模型判定需要调用外部工具,终止文本生成

2.3 本地中间处理:解析参数、执行工具函数

拿到第一轮响应后,本地代码需要完成 3 件事:

  1. 解析tool_calls,提取工具名称 get_weather_for_location 和入参 city=深圳
  2. 匹配本地同名Python函数,传入参数执行,获取工具结果(模拟天气接口返回:多云转阴)
  3. 拼接 tool 类型消息,用于第二轮对话回传

2.4 第二轮对话:工具结果回传,模型生成最终答案

第二轮核心目的:将「用户问题+模型工具调用指令+本地工具执行结果」完整回传给大模型,让大模型整合信息,输出自然语言最终回答。

重点:messages 必须完整拼接上下文,不可截断,需要包含 system、user、assistant(工具调用指令)、tool(工具结果) 四类消息

第二轮完整请求体

json 复制代码
{
  "model": "qwen-plus",
  "messages": [
    {
      "role": "system",
      "content": "你是一位天气预报专家,能够预测给定城市的天气情况"
    },
    {
      "role": "user",
      "content": "深圳今天的天气怎么样"
    },
    {
      "content": "",
      "tool_calls": [
        {
          "index": 0,
          "id": "call_734c63b9a3d44470937769",
          "type": "function",
          "function": {
            "name": "get_weather_for_location",
            "arguments": "{\"city\": \"深圳\"}"
          }
        }
      ],
      "role": "assistant"
    },
    {
      "role": "tool",
      "content": "多云转阴",
      "tool_call_id": "call_734c63b9a3d44470937769"
    }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather_for_location",
        "description": "Get weather for a given city",
        "parameters": {
          "city": "string"
        }
      }
    }
  ],
  "tool_choice": "auto"
}

第二轮最终响应(用户可见答案)

json 复制代码
{
  "model": "qwen-plus",
  "id": "chatcmpl-9affd19c-da42-9516-a028-70ef17f90d17",
  "choices": [
    {
      "message": {
        "content": "深圳今天的天气是多云转阴。",
        "role": "assistant"
      },
      "index": 0,
      "finish_reason": "stop"
    }
  ],
  "created": 1779523575,
  "object": "chat.completion",
  "usage": {
    "total_tokens": 195,
    "completion_tokens": 9,
    "prompt_tokens": 186,
    "prompt_tokens_details": {
      "cached_tokens": 0
    }
  }
}

恭喜,function calling的核心原理就是这样的交互方式

三、原生Python函数参数解析原理(无框架)

  1. 上文的 tools 结构体是固定的大模型工具格式
  2. 了解了对应的交互原理后,我们只需要将http请求转成Python即可
  3. 生产环境中,我们需要自动解析本地Python函数,生成标准tools参数 ,核心依赖Python内置 inspect 库,无需任何第三方库

3.1 核心原理:inspect 库能力

inspect 是Python内置反射库,可实现对函数的逆向解析,获取三大核心信息,完美适配大模型工具参数格式:

  1. 函数名称:对应 tool function 的 name 字段
  2. 函数文档注释:对应 tool function 的 description 字段(工具功能描述)
  3. 函数参数签名:获取参数名、参数类型、是否必填,对应 tool function 的 parameters 字段

3.2 解析规则(适配通义千问工具格式)

我们自定义一套原生解析规则,将Python原生函数,标准化转为大模型可识别的 tools 结构体:

  • 函数名 → function.name
  • 函数首行docstring → function.description
  • 函数参数名/类型注解 → parameters.properties
  • 无默认值的参数 →required 必填列表
  • 统一参数类型映射:str→string、int→integer、float→number、bool→boolean

3.3 解析示例演示

本地原生Python工具函数:

python 复制代码
def get_weather_for_location(city: str) -> str:
    """Get weather for a given city
    Args:
        city: 需要查询天气的城市名称
    """
    # 模拟天气查询接口
    weather_map = {"深圳": "多云转阴", "北京": "晴", "上海": "小雨"}
    return weather_map.get(city, "未知天气")

通过inspect自动解析后,自动生成前文标准的tools结构体

四、纯Python无框架 Function Calling 完整实现

基于上述原理,我们手写完整可运行代码,包含:函数自动解析、两轮对话流转、工具参数解析、本地函数执行、结果回传全流程,零框架依赖。

4.1 整体流程流程图

整体闭环流程:

定义本地工具函数 → inspect自动解析生成tools参数 → 第一轮API请求(触发工具调用)→ 解析tool_calls参数 → 本地执行工具函数 → 拼接完整对话上下文 → 第二轮API请求 → 输出最终自然语言答案

结构化流程图:可以使用 www.jyshare.com/front-end/9... 在线的 Mermaid 渲染功能实现

less 复制代码
graph TD
    %% 样式定义:区分不同类型节点,适配博客可视化
    classDef init fill:#e6f7ff,stroke:#1890ff,stroke-width:1px;
    classDef parse fill:#f0f8ff,stroke:#40a9ff,stroke-width:1px;
    classDef api fill:#fff7e6,stroke:#faad14,stroke-width:1px;
    classDef local fill:#f6ffed,stroke:#52c41a,stroke-width:1px;
    classDef endnode fill:#f0f2f5,stroke:#8c8c8c,stroke-width:1px;

    %% 流程节点
    A[初始化全局配置]:::init --> B[定义本地Python工具函数]:::init
    B --> C[inspect反射自动解析生成标准Tools参数]:::parse
    C --> D[第一轮API请求传入用户问题+工具列表]:::api
    D --> E{模型判断是否需要调用工具?}

    %% 分支流程
    E -->|无需调用工具| F[模型直接生成自然语言答案]:::api
    E -->|需要调用工具| G[解析Tool_Calls参数提取工具名+入参]:::local
    
    G --> H[本地执行对应Python函数获取真实业务结果]:::local
    H --> I[拼接完整对话上下文挂载Tool执行结果]:::local
    I --> J[第二轮API请求回传全部对话数据]:::api
    J --> K[模型整合数据输出最终回答]:::api
    
    %% 流程收尾
    F --> L[流程结束]:::endnode
    K --> L

4.2 完整可运行代码

  1. 依赖安装:仅需安装官方 openai 基础SDK(仅用于请求封装,无框架逻辑), pip install openaiuv add openai,此处使用uv,还需要添加 dotenv 用于加载环境变量
shell 复制代码
uv add openai dotenv
  1. 代码实现
python 复制代码
import os
import json
import inspect
from typing import Callable, Dict, Any
from openai import OpenAI

# 加载环境变量(兼容本地.env配置)
from dotenv import load_dotenv

load_dotenv()

# 1. 初始化通义千问兼容客户端(无框架,原生请求)
client = OpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)


# 2. 定义本地工具函数(可自定义扩展)
def get_weather_for_location(city: str) -> str:
    """Get weather for a given city
    Args:
        city: 需要查询天气的城市名称
    """
    weather_map = {"深圳": "多云转阴", "北京": "晴", "上海": "小雨", "广州": "多云"}
    return weather_map.get(city, "暂无该城市天气数据")


# 3. 核心工具:inspect自动解析函数,生成大模型标准tools格式
def parse_function_to_tool(func: Callable) -> Dict[str, Any]:
    """
    原生解析Python函数,转为通义千问Function Calling标准tool结构
    无任何框架依赖,纯内置inspect实现
    """
    # 获取函数签名、文档、参数
    sig = inspect.signature(func)
    doc = inspect.getdoc(func) or ""
    params = sig.parameters

    # 基础工具结构
    tool = {
        "type": "function",
        "function": {
            "name": func.__name__,
            "description": doc.split("\n")[0],
            "parameters": {
                "type": "object",
                "properties": {},
                "required": []
            }
        }
    }

    # 类型映射:Python类型 -> 大模型参数类型
    type_map = {str: "string", int: "integer", float: "number", bool: "boolean"}

    # 遍历解析每个参数
    for param_name, param in params.items():
        # 获取参数类型,默认string
        param_type = type_map.get(param.annotation, "string")
        # 获取参数描述(从docstring简易解析)
        param_desc = ""
        if f"{param_name}:" in doc:
            param_desc = doc.split(f"{param_name}:")[-1].split("\n")[0].strip()

        # 写入参数属性
        tool["function"]["parameters"]["properties"][param_name] = {
            "type": param_type,
            "description": param_desc
        }

        # 无默认值的参数为必填
        if param.default is param.empty:
            tool["function"]["parameters"]["required"].append(param_name)

    return tool


# 4. 工具调用执行器:【修复兼容问题】解析模型返回的tool_calls对象
def execute_tool_call(tool_calls: list, func_map: Dict[str, Callable]) -> str:
    """执行模型触发的工具调用,返回工具执行结果
    兼容新版OpenAI SDK对象取值,杜绝下标报错
    """
    for call in tool_calls:
        # 修复:新版SDK返回实体对象,不支持字典下标取值,需用属性调用
        func_name = call.function.name
        func_args = json.loads(call.function.arguments)
        # 匹配本地函数并执行
        if func_name in func_map:
            return func_map[func_name](**func_args)
    return "工具调用失败,未匹配到本地函数"


# 5. 完整Function Calling闭环流程
def function_calling_chat(user_query: str):
    # 5.1 初始化对话上下文、工具列表、函数映射
    messages = [
        {"role": "system", "content": "你是一位天气预报专家,能够预测给定城市的天气情况"},
        {"role": "user", "content": user_query}
    ]
    # 注册本地工具函数
    local_functions = [get_weather_for_location]
    # 函数名-函数实体映射,用于快速调用
    func_map = {func.__name__: func for func in local_functions}
    # 自动解析生成标准tools参数
    tools = [parse_function_to_tool(func) for func in local_functions]

    # 5.2 第一轮对话:触发工具调用
    response = client.chat.completions.create(
        model="qwen-plus",
        messages=messages,
        tools=tools,
        tool_choice="auto",
        stream=False,
        extra_body={"enable_thinking": False}
    )

    msg = response.choices[0].message
    # 判断是否需要调用工具
    if not msg.tool_calls:
        return msg.content

    # 5.3 本地执行工具函数
    tool_result = execute_tool_call(msg.tool_calls, func_map)

    # 5.4 拼接第二轮对话上下文
    # 适配新版SDK:对象转字典,避免上下文格式报错
    messages.append(msg.model_dump())
    # 加入工具执行结果
    messages.append({
        "role": "tool",
        "tool_call_id": msg.tool_calls[0].id,
        "content": tool_result
    })

    # 5.5 第二轮对话:生成最终答案
    final_response = client.chat.completions.create(
        model="qwen-plus",
        messages=messages,
        tools=tools,
        tool_choice="auto",
        stream=False,
        extra_body={"enable_thinking": False}
    )

    return final_response.choices[0].message.content


# 6. 测试运行
if __name__ == "__main__":
    result = function_calling_chat("深圳今天的天气怎么样")
    print("最终回答:", result)

五、核心总结

  1. Function Calling 核心不是模型执行代码,而是模型决策调用、本地执行、结果回传的两轮对话闭环;

  2. 所有工具参数均可通过Python内置 inspect 反射自动解析,无需手动维护,彻底解耦;

  3. 无框架实现的核心价值:掌握底层对话流转、参数规范、调用逻辑,为后续学习如LangChain等实现原理才能触类旁通

相关推荐
Gradpaper41 小时前
论文之后,表达之前:PPT 是关键一步
人工智能
Data-Miner1 小时前
国产AI做表工具数以轻舟Agent全新更新:新增支持火山引擎API
人工智能·microsoft·火山引擎
song150265372981 小时前
光伏iv测试仪 光伏电池片组件IV测试设备 太阳光模拟器
大数据·人工智能
传说故事1 小时前
【论文阅读】VGGT-Ω
论文阅读·人工智能·3d·具身智能
码点滴1 小时前
Workload 自动化进化论:从手动运维到 AI 驱动的 Kubernetes 智能管控
运维·人工智能·kubernetes·自动化·workload
25Qi导航1 小时前
找刊网使用指南:从选刊到发表的功能说明
人工智能·深度学习·期刊·找刊网.com·找刊网
j_xxx404_1 小时前
Linux进程信号捕捉与操作系统运行本质深度解析
linux·运维·服务器·开发语言·c++·人工智能·ai
AI技术控1 小时前
KV Cache 缓存机制的原理和应用:从 Transformer 推理到大模型服务优化
人工智能·python·深度学习·缓存·自然语言处理·transformer
泛联新安1 小时前
重磅新品|泛联新安Omni Security构建AI时代软件安全生产力
人工智能·智能体·软件安全