本地运行大语言模型:实现函数调用与工具自主使用
在之前的文章中,我们已经介绍了如何在本地运行 Ollama 以及如何通过提供外部数据库的方式微调模型的答案。本篇文章将继续探索如何使用'函数调用(Function Calling)'功能以扩展模型能力,使其在'智能'的道路上越走越远。
Function Calling 介绍
根据 OpenAI 官方文档,Function Calling 是使得大型语言模型具备可以连接到外部工具的能力。简而言之,开发者事先给模型提供了若干工具(函数),在模型理解用户的问题后,自行判断是否需要调用工具以获得更多上下文信息,帮助模型更好地决策。
举个例子:在前文中我们是利用 Document Loaders 将事先准备好的文本作为上下文提供给模型,而使用 Function Calling 以后,我们只要提供一个'搜索函数'作为工具,模型即可自己通过搜索引擎进行搜索然后得出答案。
得益于最新的模型训练,现在的模型既能够检测何时应调用函数(取决于输入),还能够以比以前的模型更贴近函数签名的方式响应 JSON。
用途
Function Calling 有什么用?通常有以下三个主要应用场景:
- 创建通过调用外部 API 回答问题的助手:例如查询天气、股票价格或系统状态。
- 将自然语言转换为 API 调用:允许用户用口语描述需求,后端自动执行对应的程序逻辑。
- 从文本中提取结构化数据:从非结构化文本中抽取特定字段并格式化为 JSON。
下面我们就一步一步来理解一下 Function Calling 的实现原理。
定义 Function
首先定义 4 个 function,这 4 个 function 有不同的使用场景,用于模拟不同的外部服务。
def get_gas_prices(city: str) -> float:
"""Get gas prices for specified cities."""
print(f'Get gas prices for {city}.')
def github(project: str) -> str:
'''Get the information such as author from github with the project name.'''
print(f'Access the github for {project}.')
def get_weather(city: str) -> str:
"""Get the current weather given a city."""
print(f'Getting weather for {city}.')
def get_directions(start: str, destination: str) -> float:
"""Get directions from Google Directions API.
start: start address as a string including zipcode (if any)
destination: end address as a string including zipcode (if any)"""
print(f'Get directions for {start} {destination}.')
定义 Prompts
为了让模型知道这些函数的存在及其参数结构,我们需要构建一个包含函数元数据的 Prompt。
functions_prompt = f"""
You have access to the following tools:
{function_to_json(get_weather)}
{function_to_json(get_gas_prices)}
{function_to_json(get_directions)}
{function_to_json(github)}
You must follow these instructions:
Always select one or more of the above tools based on the user query
If a tool is found, you must respond in the JSON format matching the following schema:
{{
"tools": {{
"tool": "<name of the selected tool>",
"tool_input": <parameters for the selected tool, matching the tool's JSON schema
}}
}}
If there are multiple tools required, make sure a list of tools are returned in a JSON array.
If there is no tool that match the user request, you must respond empty JSON {{}}.
User Query:
"""
这是一个复杂的提示,让我们一步一步来看:
- 告知工具列表:告诉模型我提供了 4 个工具,让它自己去查询这 4 个工具的元数据。
function_to_json 函数返回的内容如下:
{
"name": "get_weather",
"description": "Get the current weather given a city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "str"
}
}
},
"returns": "str"
}
这一步就是让模型根据这几个函数的元数据来理解函数,尤其是 description 写的应该尽量详细,以便模型准确匹配意图。
-
指定行为规则:提示模型作出自己的判断,根据上面提供的工具选择一个或多个进行调用,并指定了模型返回数据格式——JSON 以及这个 JSON 的 Schema。
-
等待用户问题:最后留出位置等待用户的实际 Query。
完整代码实现
整段代码展示了如何结合 Python 脚本、Ollama API 以及自定义函数来实现这一流程。
import inspect
import json
import requests
from typing import get_type_hints
def generate_full_completion(model: str, prompt: str) -> dict[str, str]:
params = {"model": model, "prompt": prompt, "stream": False}
response = requests.post(
"http://localhost:11434/api/generate",
headers={"Content-Type": "application/json"},
data=json.dumps(params),
timeout=60,
)
return json.loads(response.text)
def get_gas_prices(city: str) -> float:
"""Get gas prices for specified cities."""
print(f'Get gas prices for {city}.')
def github(project: str) -> str:
'''Get the information such as author from github with the project name.'''
print(f'Access the github for {project}.')
def get_weather(city: str) -> str:
"""Get the current weather given a city."""
print(f'Getting weather for .')
() -> :
()
():
name = (t)
name name:
name
:
t.__name__
():
signature = inspect.signature(func)
type_hints = get_type_hints(func)
function_info = {
: func.__name__,
: func.__doc__,
: {: , : {}},
: type_hints.get(, ).__name__,
}
name, _ signature.parameters.items():
param_type = get_type_name(type_hints.get(name, ()))
function_info[][][name] = {: param_type}
json.dumps(function_info, indent=)
():
func_name = tool_data[]
func_input = tool_data[]
func = ().get(func_name)
func (func):
func(**func_input)
:
()
():
functions_prompt =
GPT_MODEL =
prompts = [
,
,
,
,
]
prompt prompts:
()
question = functions_prompt + prompt
response = generate_full_completion(GPT_MODEL, question)
:
data = json.loads(response.get(, response))
tool_data data[]:
execute_fuc(tool_data)
Exception:
()
()
__name__ == :
main()
测试与分析
整段代码就是用来测试模型对问题的理解能力以及是否能正确判断调用哪个工具的。
我们来看下设定的 4 个问题:
What's the weather like in Beijing?
What is the distance from Shanghai to Hangzhou and how much do I need to fill up the gas in advance to drive from Shanghai to Hangzhou?
Who's the author of the 'snake-game' on github?
What is the exchange rate between US dollar and Japanese yen?
按照我们的设想,如果模型理解了我提供的工具的功能以及读懂了用户的问题,应该按照以下规则选择工具:
- 问题 1 应该对应的是
get_weather('Beijing')
- 问题 2 应该对应的是
get_directions('Shanghai', 'Hangzhou') 和 get_gas_prices('Shanghai')
- 问题 3 应该对应的是
github('snake-game')
- 问题 4 应该没有对应的函数,模型不选择任何工具
下一步,我们也写好了代码进行测试,我们在工具函数里进行参数打印,以查看是否达到我们的预期。
测试结果
❓What
Getting weather for Beijing.
Total duration: 3.481918084 seconds
❓What is the distance from Shanghai to Hangzhou and how much do I need to fill up the gas in advance to drive from Shanghai to Hangzhou?
Get directions for Shanghai, China Hangzhou, China.
Get gas prices for Shanghai.
Total duration: 5.467253959 seconds
❓Who
Access the github for snake-game.
Total duration: 2.510993791 seconds
❓What is the exchange rate between US dollar and Japanese yen?
{}
No tools found.
Total duration: 0.20056292 seconds
深入理解与注意事项
1. 函数元数据的重要性
在 function_to_json 中,我们使用了 inspect 模块来获取函数的签名和文档字符串。这对于本地模型尤为重要,因为本地模型通常不如云端大模型那样经过广泛的指令微调。详细的 description 能显著提高模型识别意图的准确率。建议为每个工具编写清晰、具体的中文或英文描述,明确输入参数的含义。
2. JSON Schema 约束
Prompt 中定义的 JSON Schema 必须严格遵循模型期望的格式。在上述示例中,我们要求模型返回嵌套的 JSON 对象。如果模型返回的格式不符合预期(例如缺少引号或括号),解析时会抛出异常。在实际生产中,建议增加健壮的错误处理机制,例如尝试多次重试或使用正则表达式提取 JSON 块。
3. 本地模型的局限性
虽然 Mistral 等模型表现良好,但不同量化版本(如 q8_0, q4_k_m)在函数调用能力上可能存在差异。较小的模型可能无法严格遵守复杂的 JSON 结构。如果遇到模型频繁返回错误格式的情况,可以尝试简化 Prompt 中的 Schema 描述,或者更换参数量更大的模型。
4. 扩展性设计
目前的 execute_fuc 函数通过 globals() 动态查找函数。在生产环境中,建议使用注册表模式(Registry Pattern)来管理工具,避免全局变量污染,同时便于权限控制和日志记录。此外,可以将 print 语句替换为真实的 HTTP 请求,例如调用真实的天气 API 或 GitHub API,从而实现完整的自动化工作流。
总结
本篇文章详细介绍了使用 Function Calling 使得 Ollama 模型具备调用外部工具的能力。通过对定义函数、构建 Prompt、编写执行逻辑以及测试结果的完整分析,展示了本地大模型实现自主工具调用的可行路径。
希望本系列文章能使大家对本地运行大语言模型有一些深入的了解。后续我们将继续探讨更多本地 LLM 的实用场景,包括知识库检索增强生成(RAG)以及多模态交互等内容。