0 前言

最近在折腾大模型的Function Calling(函数调用),也就是让大模型能联网、能查库、能执行代码。以前觉得这东西挺神秘,其实拆解开来看,核心逻辑并不复杂。

正好手头有一段刚跑通的代码,本来是记在Jupyter里的,现在整理记录一下。这段代码用的是本地部署的Qwen3(通义千问)模型,配合OpenWeather的API查天气。

下面就顺着代码逻辑,把这套“让大模型通过外部工具获取信息”的流程给捋一遍。

1 准备工作:API Key和基础设置

import numpy as np
import pandas as pd
import json
import io
import inspect
import requests
from openai import OpenAI
import os

# 这里填你自己的key,这个key也是我网上看到的,供大家学习使用
open_weather_key = "5c939a7cc59eb8696f4cd77bf75c5a9a" 

# 初始化客户端,我这里用的是本地的Ollama起的Qwen服务
client = OpenAI(api_key="None", base_url="http://localhost:11434/v1")

这段没啥好说的,就是导包。注意那个 base_url,我指向了本地的Ollama接口,说明这套逻辑不仅仅是OpenAI专用的,只要支持OpenAI格式接口的模型都能跑。

2 定义工具函数:Get Weather

这是模型要调用的“手”。它本质就是一个普通的Python函数。

def get_weather(loc):
    """
    查询即时天气函数
    :param loc: 必要参数,字符串类型,用于表示查询天气的具体城市名称,\
    注意,中国的城市需要用对应城市的英文名称代替,例如如果需要查询北京市天气,则loc参数需要输入'Beijing';
    :return:OpenWeather API查询即时天气的结果,具体URL请求地址为:https://api.openweathermap.org/data/2.5/weather\
    返回结果对象类型为解析之后的JSON格式对象,并用字符串形式进行表示,其中包含了全部重要的天气信息
    """
    # Step 1.构建请求
    url = "https://api.openweathermap.org/data/2.5/weather"

    # Step 2.设置查询参数
    params = {
        "q": loc,               
        "appid": open_weather_key,    
        "units": "metric",            
        "lang":"zh_cn"                
    }

    # Step 3.发送GET请求
    response = requests.get(url, params=params)
    
    # Step 4.解析响应
    data = response.json()
    return json.dumps(data)

get_weather('BeiJing')

这里有个关键点:看那个函数注释。这玩意儿不是写给我看的,是写给大模型看的。大模型读不懂代码逻辑,但它读得懂中文注释。所以注释里必须写清楚参数 loc 是干嘛的,格式是什么。如果注释写得烂,模型就很有可能传错参数。

3 偷懒神器:Auto Functions

Function Calling最烦人的地方在于,你得给模型传一个JSON Schema,告诉它这个函数长什么样。手写那个JSON格式极其痛苦且容易出错。

所以我搞了个 auto_functions,利用大模型自己来生成这个Schema。

def auto_functions(functions_list):
    """
    Chat模型的functions参数编写函数
    :param functions_list: 包含一个或者多个函数对象的列表;
    :return:满足Chat模型functions参数要求的functions对象
    """
    def functions_generate(functions_list):
        functions = []
        for function in functions_list:
            # 读取函数对象的函数说明
            function_description = inspect.getdoc(function)
            # 读取函数的函数名字符串
            function_name = function.__name__

            system_prompt = '以下是某的函数说明:%s' % function_description
            user_prompt = '根据这个函数的函数说明,请帮我创建一个JSON格式的字典,这个字典有如下5点要求:\
                           1.字典总共有三个键值对;\
                           2.第一个键值对的Key是字符串name,value是该函数的名字:%s,也是字符串;\
                           3.第二个键值对的Key是字符串description,value是该函数的函数的功能说明,也是字符串;\
                           4.第三个键值对的Key是字符串parameters,value是一个JSON Schema对象,用于说明该函数的参数输入规范。\
                           5.输出结果必须是一个JSON格式的字典,只输出这个字典即可,前后不需要任何前后修饰或说明的语句' % function_name

            response = client.chat.completions.create(
                            model="qwen3:4b-instruct",
                            messages=[
                                {"role": "system", "content": system_prompt},
                                {"role": "user", "content": user_prompt}
                            ]
                        )
            json_function_description=json.loads(response.choices[0].message.content.replace("```","").replace("json",""))
            json_str={"type": "function","function":json_function_description}
            functions.append(json_str)
        return functions
    
    # 最大可以尝试4次
    max_attempts = 4
    attempts = 0

    while attempts < max_attempts:
        try:
            functions = functions_generate(functions_list)
            break  # 如果代码成功执行,跳出循环
        except Exception as e:
            attempts += 1  # 增加尝试次数
            print("发生错误:", e)
            if attempts == max_attempts:
                print("已达到最大尝试次数,程序终止。")
                raise  # 重新引发最后一个异常
            else:
                print("正在重新运行...")
    return functions

# 生成满足Chat模型functions参数要求的functions对象
functions_list = [get_weather]
functions = auto_functions(functions_list)

这函数利用 inspect 库抓取我们上面写的 docstring,然后扔给大模型转成 JSON Schema 格式。

这属于是用魔法打败魔法。这样一来,以后不管我写什么新工具,只要注释写好,直接扔进这个列表里就能自动生成配置,不用手动去抠 JSON 括号了。

4 核心引擎:Run Conversation

这是整段代码的灵魂。所有的调度逻辑都在这儿。

def run_conversation(messages, functions_list=None, model="qwen3:4b-instruct"):
    """
    能够自动执行外部函数调用的对话模型
    :param messages: 必要参数,字典类型,输入到Chat模型的messages参数对象
    :param functions_list: 可选参数,默认为None,可以设置为包含全部外部函数的列表对象
    :param model: Chat模型,可选参数
    :return:Chat模型输出结果
    """
    # 如果没有外部函数库,则执行普通的对话任务
    if functions_list == None:
        response = client.chat.completions.create(
                        model=model,
                        messages=messages,
                        )
        response_message = response.choices[0].message
        final_response = response_message.content
        
    # 若存在外部函数库,则需要灵活选取外部函数并进行回答
    else:
        # 创建functions对象
        tools = auto_functions(functions_list)
        available_functions = {func.__name__: func for func in functions_list}

        # --- 第一次调用大模型 ---
        response = client.chat.completions.create(
                        model=model,
                        messages=messages,
                        tools=tools,
                        tool_choice="auto", )
        response_message = response.choices[0].message
        tool_calls = response_message.tool_calls

        if tool_calls:
            messages.append(response_message)  # 把模型的思考结果加进对话历史
            
            # 循环执行模型想要调用的所有函数
            for tool_call in tool_calls:
                function_name = tool_call.function.name
                function_to_call = available_functions[function_name]
                function_args = json.loads(tool_call.function.arguments)
                
                # --- 真正执行外部函数 ---
                function_response = function_to_call(**function_args)
                
                # 把函数运行结果,以 'tool' 的角色塞回对话历史
                messages.append(
                    {
                        "tool_call_id": tool_call.id,
                        "role": "tool",
                        "name": function_name,
                        "content": function_response,
                    }
                ) 
            
            # --- 第二次调用大模型 ---
            second_response = client.chat.completions.create(
                model=model,
                messages=messages,
            ) 
            final_response = second_response.choices[0].message.content
        else:
            final_response = response_message.content
                
    return final_response


messages = [{"role": "user", "content": '今天北京的天气如何?'}]

run_conversation(messages=messages, 
                 functions_list=functions_list, 
                )

这里有几个技术关键点,值得掰扯一下:

第一,为什么要调用两次大模型?

很多人以为 Function Calling 是模型直接去运行 Python 代码,其实不是。

第一次调用:是为了“决策”。你问“北京天气怎么样”,模型手里有工具说明书(Schema)。它通过计算发现自己答不上来,但 get_weather 这个工具能解决,于是它不是返回文本,而是返回一个“我想要调用 get_weather,参数是 Beijing”的请求对象(也就是 tool_calls)。

中间执行:代码在本地运行 Python 函数,拿到这串 JSON 格式的天气数据。

第二次调用:是为了“人话翻译”。这时候 messages 列表里已经有了用户的问句、模型想调函数的意图、以及函数返回的一大坨 JSON 数据。第二次调用就是把这一堆上下文扔给模型,说:“看,数据查回来了,你把它组织成人类能听懂的话告诉用户。”

第二,模型怎么判断要不要调函数?

看那个 tool_choice=“auto”。这个参数告诉模型:“你自己看着办”。 底层的原理是,现在的 Instruct 模型(特别是针对 Tool 使用微调过的)在训练时见过大量类似的数据结构。当它发现用户的 Prompt 和我们传入的 tools 里的描述语义匹配度很高时,它就会触发特定的输出模式(输出 Function Call 的 Token),而不是输出普通的文本。

第三,上下文(Context)的构建

注意看 messages.append 的顺序。

User: “北京天气?”

Assistant (Call 1): “我要调 get_weather(Beijing)” —— 这一步必须加进去,不然模型不知道前因后果。

Tool: “{‘temp’: 25, ‘city’: ‘Beijing’…}” —— 这一步的角色是 tool,专门用来放函数返回值。

这一整条链条完整了,模型才能在第二次调用时生成正确回答。

5 对话循环:Chat With Model

最后这个函数就是个简单的壳子,用来维持多轮对话的 Loop。

def chat_with_model(functions_list=None, prompt="你好呀", ...):
    messages = system_message
    messages.append({"role": "user", "content": prompt})
    
    while True:           
        answer = run_conversation(messages=messages, functions_list=functions_list, ...)
        print(f"模型回答: {answer}")

        user_input = input("您还有其他问题吗?(输入退出以结束对话): ")
        if user_input == "退出":
            break
        messages.append({"role": "user", "content": user_input})

chat_with_model(functions_list, prompt="你好")

这里唯一的要点就是别忘了在 while 循环里把 user_input 也不断 append 到 messages 里,否则聊两句它就失忆了。

总结

这一套下来,其实就是把“思考”和“行动”拆开了。

大模型(大脑):负责理解意图,生成参数。

Python代码(手脚):负责拿着参数真正去干活(发请求、查数据库)。

大模型(大脑):负责看着干活的结果,组织语言汇报。

这就是 Function Calling 的本质。只要掌握了 run_conversation 里的那个三步走逻辑(查意图 -> 跑代码 -> 润色输出),你基本可以用大模型去驱动任何本地的 Python 代码了。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐