大模型如何长出“手”?手撸一个Function Calling的本地调用Demo
文章目录
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 代码了。
更多推荐



所有评论(0)