从零理解Function Calling:大模型与外部世界交互的核心协议
1. 项目概述:从“笨办法”开始,理解Function Calling的本质
最近在AI圈里,Function Calling和AI Agent这两个词的热度居高不下。无论是想自己动手搭建一个能自动处理任务的智能体,还是想搞明白大模型除了聊天还能怎么用,Function Calling都是一个绕不开的核心概念。但很多教程一上来就讲架构、讲框架,对于新手来说,就像还没学会走路就被要求跑步,很容易一头雾水。
所以,今天我们不谈那些高大上的架构图,也不急着去配置复杂的开发环境。我们就用一个最“笨”的办法,亲手写几行代码,来把Function Calling到底是什么、怎么工作、以及它和AI Agent的关系,给彻底搞明白。这个方法虽然“笨”,但胜在直观。当你亲手实现一遍之后,再看那些开源框架和复杂项目,就会有一种“哦,原来如此”的通透感。你会发现,那些看似神秘的AI Agent,其最基础的通信机制,正是建立在Function Calling这块基石之上。
简单来说,Function Calling就是大语言模型(LLM)与外部世界“握手”的协议。模型本身是个“思想家”,它擅长理解和生成文本,但它不会查天气、不会发邮件、不能操作数据库。Function Calling就是给这位“思想家”配上了一双可以指挥“手”和“脚”的大脑皮层。模型通过一种结构化的方式告诉系统:“我想调用‘查询天气’这个功能,参数是‘北京’”,然后系统就去执行对应的代码,并把结果返回给模型,模型再组织成自然语言回答你。这个“告诉系统”的过程,就是Function Calling。而一个能够自主规划、调用多个功能来完成复杂目标的系统,就是AI Agent的雏形。
2. 核心需求解析:为什么我们需要Function Calling?
在深入代码之前,我们必须先弄清楚为什么要发明Function Calling。直接让大模型输出一段可执行的Python或JavaScript代码不就行了吗?理论上可以,但这在实践中存在巨大的缺陷和风险,这正是Function Calling要解决的核心问题。
2.1 解决大模型的“幻觉”与不可控性
大模型生成代码是开放式的,它可能生成任何语法正确但逻辑诡异、甚至存在安全风险的代码。比如,你问“帮我删除一些没用的文件”,模型可能直接生成
os.system(‘rm -rf /’)
这样的危险命令。Function Calling通过“定义功能清单”的方式,将模型的输出严格限制在预设的安全范围内。模型只能从清单里选择功能,并提供符合预定义结构的参数,这就好比给了模型一份安全的“工具菜单”,它只能点菜,不能自己进厨房乱搞。
2.2 实现结构化与可靠的数据交换
让模型生成自然语言描述的结果,再由程序去解析,是极其不可靠的。例如,模型回答“今天北京最高气温28度,最低气温15度”。程序要如何准确无误地从这句话里提取出“28”和“15”这两个数字?正则表达式会写得非常复杂且脆弱。Function Calling要求模型必须按照
{“temperature_high”: 28, “temperature_low”: 15}
这样的JSON格式输出,程序解析起来就变成了一个简单的字典键值对读取,百分之百可靠。这种结构化的输出,是AI与现有软件系统、API接口无缝集成的前提。
2.3 构建复杂AI Agent的基石
一个真正的AI Agent,比如能自动处理客服工单、能进行多步骤数据分析的智能体,其核心工作流就是“思考-决策-执行-再思考”。Function Calling标准化了“决策”到“执行”的接口。Agent的“大脑”(LLM)根据当前目标和状态,从技能库(一组定义好的Function)中选择一个或多个来调用。这个选择过程本身就是一种规划能力。没有Function Calling,Agent的规划和执行将是割裂的;有了它,Agent才能形成一个完整的感知-决策-执行闭环。
所以,学习Function Calling,绝不是仅仅学习一个API调用技巧。它是在学习如何为AI构建可扩展、安全、可靠的行为能力,是打开AI Agent开发大门的第一把钥匙。
3. 环境准备与工具选型:最小化起步
我们坚持“笨办法”哲学,意味着用最少的依赖、最直观的工具来开始。避免一上来就引入LangChain、AutoGen等重型框架,它们封装了太多细节,不利于理解本质。
核心工具:Python + OpenAI API(或兼容的本地模型)
- Python 3.8+ :AI领域的事实标准语言,库生态丰富。确保你的环境已安装。
-
OpenAI Python库
:
pip install openai。我们将使用其ChatCompletion接口,这是目前Function Calling事实上的标准接口定义,绝大多数其他模型和平台都兼容此格式。 -
一个API Key
:如果你使用OpenAI的模型,需要去平台申请。
为了完全本地化和零成本学习,我强烈建议使用Ollama搭配本地模型
。
- 安装Ollama:访问官网下载安装。
-
拉取一个适合Function Calling的轻量级模型,例如
ollama pull qwen2.5:7b-instruct。Qwen、Llama等较新的模型都具备良好的Function Calling能力。 - 这样,你的所有实验都在本地进行,无需担心费用和网络问题。
为什么不用更高级的框架?
像LangChain这样的框架,提供了
Tool
抽象和便捷的Agent执行器,但它们在你和底层机制之间增加了一层抽象。在初学阶段,这层抽象会掩盖掉“模型究竟输出了什么”、“请求体到底长什么样”这些关键细节。我们先用手动的方式把整个过程走通,未来再使用框架时,你就能清晰地知道它在帮你做什么,出了问题也能快速定位。
代码编辑器 :VS Code、PyCharm甚至Jupyter Notebook都可以。选择你顺手的。
注意:本文后续的代码示例将基于OpenAI API的格式,因为它是最通用的标准。如果你使用Ollama+本地模型,只需将请求的
base_url指向本地服务(如http://localhost:11434/v1),并将model参数改为你拉取的模型名称即可,Function Calling的请求和响应格式是完全一致的。
4. 从零开始:手动实现第一个Function Calling
让我们从一个最简单的场景开始:让AI帮我们查询某个城市的当前天气。当然,我们没有真正的天气API,但我们可以模拟一个。这个过程分为三个清晰步骤:定义函数、与大模型对话、解析并执行。
4.1 第一步:定义你的“功能菜单”
首先,我们要告诉大模型,它现在有哪些“超能力”可以用。这个菜单需要按照特定的格式来写。
# 这是我们要提供给模型的“功能清单”
tools = [
{
“type”: “function”, # 固定字段,表示这是一个函数定义
“function”: {
“name”: “get_current_weather”, # 函数的名字,要求清晰明确
“description”: “获取指定城市的当前天气情况”, # 关键!用自然语言描述这个函数是干什么的。模型主要靠这个描述来决定是否调用它。
“parameters”: { # 定义函数需要的参数,使用JSON Schema格式
“type”: “object”,
“properties”: {
“location”: {
“type”: “string”,
“description”: “城市名称,例如:北京, 上海”, # 对参数的描述同样重要
},
“unit”: {
“type”: “string”,
“enum”: [“celsius”, “fahrenheit”], # 枚举类型,限制参数只能是指定的值
“description”: “温度单位,摄氏度或华氏度”,
}
},
“required”: [“location”], # 指定哪些参数是必须的
},
},
}
]
关键解读 :
-
description字段是灵魂。模型不理解代码,它只理解自然语言。你必须用清晰、无歧义的语言描述这个函数的功能和每个参数的意义。比如,如果你把location描述成“地点”,模型可能填入“在公园里”,而“城市名称”则明确得多。 -
JSON Schema是一种描述数据结构的标准。在这里,它严格定义了模型输出参数的“形状”。这保证了我们收到的参数一定是可解析的JSON对象。
4.2 第二步:与大模型对话,触发Function Calling
现在,我们带着这份“菜单”去问大模型一个问题。
import openai
# 如果你用Ollama,client可以这样初始化:
# from openai import OpenAI
# client = OpenAI(base_url=‘http://localhost:11434/v1’, api_key=‘ollama’)
# 如果你用OpenAI官方API,请配置你的API Key
# openai.api_key = ‘your-api-key’
# 模拟使用OpenAI格式的请求
def chat_with_ai(user_message):
response = openai.ChatCompletion.create(
model=“gpt-3.5-turbo”, # 或你在Ollama中使用的模型名,如“qwen2.5:7b-instruct”
messages=[
{“role”: “user”, “content”: user_message}
],
tools=tools, # 关键!在这里传入我们定义好的功能清单
tool_choice=“auto”, # “auto”表示让模型自己决定是否调用函数。还可以强制指定“none”或不调用,或指定某个函数。
)
return response
# 用户提问
user_question = “北京今天天气怎么样?”
response = chat_with_ai(user_question)
print(“模型原始响应:”)
print(response)
执行与观察
:
运行这段代码,你会得到一个复杂的响应对象。不要被吓到,我们关心的是其中最关键的部分:
response.choices[0].message
。
如果模型认为需要调用函数来回答你的问题,这个
message
对象里就不会有常规的
content
文本,而是会包含一个
tool_calls
数组。这是Function Calling机制的核心标志!
一个典型的
tool_calls
内容如下:
{
“role”: “assistant”,
“content”: null,
“tool_calls”: [
{
“id”: “call_abc123”,
“type”: “function”,
“function”: {
“name”: “get_current_weather”,
“arguments”: “{\”location\“: \”北京\“, \”unit\“: \”celsius\“}”
}
}
]
}
看!模型没有直接生成“北京天气是...”,而是说:“我要调用
get_current_weather
这个函数,参数是
location=北京
和
unit=celsius
”。
arguments
是一个JSON格式的字符串,其结构完全符合我们之前定义的
parameters
Schema。
4.3 第三步:执行函数并返回结果给模型
模型已经做出了“决策”,现在轮到我们的程序来“执行”了。
# 首先,解析模型传来的参数
import json
message = response.choices[0].message
if message.tool_calls:
# 通常一次只调用一个函数,我们取第一个
tool_call = message.tool_calls[0]
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments) # 将字符串解析为字典
print(f“模型要求调用函数:{function_name}”)
print(f“函数参数:{function_args}”)
# 根据函数名,执行对应的真实函数
if function_name == “get_current_weather”:
# 这里是你的真实业务逻辑!可以调用真正的天气API。
# 我们这里用一个模拟函数代替。
def get_current_weather(location, unit):
# 模拟API调用返回
weather_info = {
“location”: location,
“temperature”: 28,
“unit”: unit,
“condition”: “晴朗”,
“humidity”: 65
}
return weather_info
# 执行模拟函数
result = get_current_weather(**function_args) # 用**将字典解包为关键字参数
print(f“执行结果:{result}”)
现在,我们得到了一个包含天气信息的字典
result
。但对话还没结束,我们需要把这个结果“喂回”给大模型,让它来组织最终的自然语言回答。
4.4 第四步:将结果返回,让模型生成最终回答
我们把模型的第一次回复(包含
tool_calls
的消息)和执行函数的结果,一起作为新的上下文,再次发送给模型。
# 构建新的消息列表,包含整个对话历史
messages = [
{“role”: “user”, “content”: user_question},
message, # 助理的第一次回复(包含tool_calls)
{
“role”: “tool”, # 注意!这是一个新的角色类型 “tool”
“content”: json.dumps(result), # 将执行结果转为JSON字符串
“tool_call_id”: tool_call.id # 必须对应上第一次调用时的ID
}
]
# 第二次请求,让模型基于函数执行结果生成最终回答
final_response = openai.ChatCompletion.create(
model=“gpt-3.5-turbo”,
messages=messages,
# 这次不需要再传递tools参数,除非你希望模型能继续调用新函数
)
final_answer = final_response.choices[0].message.content
print(f“\nAI的最终回答:{final_answer}”)
这次,模型收到了
role
为
tool
的消息,里面包含了它要求的天气数据。于是,它会生成类似这样的自然语言回答:“北京今天天气晴朗,气温28摄氏度,湿度65%。”
至此,一个完整的Function Calling流程就走通了。它清晰地分为两个回合:
- 第一回合 :用户提问 -> 模型分析后,决定调用函数,并返回结构化调用请求。
-
第二回合
:程序执行函数 -> 将结果以
tool角色返回 -> 模型消化结果,生成面向用户的最终回答。
5. 核心机制深度剖析:不仅仅是“调用函数”
通过上面的“笨办法”实操,我们已经看到了Function Calling的外在流程。现在,我们来深入它的内在机制,理解它为何如此设计,以及它如何赋能AI Agent。
5.1 结构化输出:从自由文本到精确指令
这是Function Calling最根本的价值。传统的提示词工程(Prompt Engineering)是在和模型的“自由意志”博弈,你永远无法百分百保证输出的格式。而Function Calling通过
tools
参数,为模型划定了一个“结构化输出沙箱”。
当模型看到
tools
定义时,它内部的任务就从“生成一段回答”转变为“根据用户问题,从工具列表中选择最合适的一个,并填充其参数”。这是一种
任务范式的转换
。模型的输出被严格约束在预定义的JSON Schema内,这使得后续的程序处理变得 deterministic(确定性的)。对于构建生产级应用,这种可靠性是生命线。
5.2 多函数调用与并行处理
我们的例子只调用了一个函数。但
tool_calls
是一个数组,这意味着模型可以
同时决定调用多个函数
。例如,用户问:“对比一下北京和上海今天的天气。”一个足够聪明的模型可能会在同一个回复中,生成两个
tool_calls
,一个查询北京天气,一个查询上海天气。
程序可以并行或串行执行这两个函数调用,然后将所有结果收集起来,在一次
tool
消息中或分多条
tool
消息返回给模型。模型再综合这些信息,生成对比性的回答。这种
并行任务规划与信息整合
的能力,正是复杂AI Agent的核心。
5.3 Tool Choice策略:控制模型的自主权
在请求中,
tool_choice
参数给了我们控制权:
-
“auto”(默认):模型自主决定是否调用、调用哪个工具。这是构建自主Agent的模式。 -
“none”:强制模型不调用任何工具,只生成文本回复。当你想确保模型进行纯文本对话时使用。 -
{“type”: “function”, “function”: {“name”: “xxx”}}:强制模型调用指定的某个工具。这在构建严格工作流时很有用,比如第一步必须调用“数据查询”,第二步必须调用“数据分析”。
通过灵活运用
tool_choice
,我们可以设计出从完全自主到严格流程控制的各类AI应用。
5.4 与AI Agent架构的关联
现在,让我们把视野拉高,看看Function Calling在AI Agent宏大架构中的位置。一个典型的Agent架构包含以下层次:
- LLM核心(大脑) :负责理解、规划、决策。
-
Harness/Agent Core(基础设施层)
:这是包裹在LLM之外的一层框架。它负责管理对话状态(
messages历史)、维护工具清单(tools)、处理Function Calling的请求/响应循环、调度工具执行。我们上面手写的代码,就是一个极简的Harness。 -
Tools/Skills(技能层)
:一个个具体的函数,如
get_weather,send_email,query_database。这就是我们定义的tools列表里的内容。 - Memory(记忆层) :存储对话历史、工具执行结果、知识片段等,为LLM的决策提供上下文。
- Planning & Execution(规划与执行循环) :Agent的核心工作流。LLM根据目标(“用户想对比天气”)和记忆,规划步骤(“先调A工具,再调B工具”),通过Harness调用Tools执行,将结果存入Memory,再进行下一步规划,直到任务完成。
Function Calling,正是连接LLM(大脑)、Harness(调度中心)和Tools(手脚)的标准化协议 。没有这个协议,Harness就无法理解LLM的意图,Tools也无法被准确调用。因此,深入理解Function Calling,是理解整个AI Agent运行机制的基础。
6. 实战进阶:构建一个多技能AI助手
理解了单次调用,我们来挑战一个更复杂的场景:一个能处理“查询天气”和“计算器”两种任务的AI助手。这会让我们的Harness逻辑变得更通用。
6.1 定义多工具清单
tools = [
{
“type”: “function”,
“function”: {
“name”: “get_current_weather”,
“description”: “获取指定城市的当前天气情况”,
“parameters”: {
“type”: “object”,
“properties”: {
“location”: {“type”: “string”, “description”: “城市名称”},
“unit”: {“type”: “string”, “enum”: [“celsius”, “fahrenheit”], “description”: “温度单位”}
},
“required”: [“location”]
}
}
},
{
“type”: “function”,
“function”: {
“name”: “calculator”,
“description”: “执行数学计算。支持加(+)、减(-)、乘(*)、除(/)、乘方(**)等运算。”,
“parameters”: {
“type”: “object”,
“properties”: {
“expression”: {“type”: “string”, “description”: “数学表达式,例如:’3 + 5 * 2‘ 或 ’(10 - 4) / 3‘”}
},
“required”: [“expression”]
}
}
}
]
6.2 实现通用的工具执行分发器
我们需要一个中央处理器,能根据模型返回的
function_name
,自动找到并执行对应的函数。
# 首先,实现具体的工具函数
def get_current_weather(location, unit=“celsius”):
# 模拟实现
return {“location”: location, “temperature”: 22, “unit”: unit, “condition”: “多云”}
def calculator(expression):
# 警告:在生产环境中,直接eval是极度危险的!这里仅用于演示。
# 真实场景应使用安全表达式解析库(如`ast.literal_eval`或自定义解析器)。
try:
result = eval(expression) # 仅作演示,切勿用于生产!
return {“expression”: expression, “result”: result}
except Exception as e:
return {“expression”: expression, “error”: str(e)}
# 建立工具名到函数对象的映射
TOOL_REGISTRY = {
“get_current_weather”: get_current_weather,
“calculator”: calculator,
}
# 通用的工具调用执行函数
def execute_tool_call(tool_call):
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
if function_name in TOOL_REGISTRY:
func = TOOL_REGISTRY[function_name]
# 安全起见,可以在这里检查参数
return func(**function_args)
else:
return {“error”: f“未知的工具函数:{function_name}”}
6.3 实现多轮对话循环
一个真正的助手需要支持多轮对话,并且能记住历史。同时,模型在一次回复中可能调用多个工具。
def run_conversation(user_input, conversation_history=[]):
# 1. 将用户输入加入历史
conversation_history.append({“role”: “user”, “content”: user_input})
# 2. 发送请求给模型,携带完整历史和工具定义
response = openai.ChatCompletion.create(
model=“gpt-3.5-turbo”,
messages=conversation_history,
tools=tools,
tool_choice=“auto”,
)
assistant_message = response.choices[0].message
# 3. 将助理的回复(可能包含tool_calls)加入历史
conversation_history.append(assistant_message.to_dict()) # 注意转为字典格式
all_tool_results = []
# 4. 检查并处理所有工具调用
if assistant_message.tool_calls:
for tool_call in assistant_message.tool_calls:
print(f“[系统] 正在执行工具:{tool_call.function.name}, 参数:{tool_call.function.arguments}”)
# 执行单个工具
tool_result = execute_tool_call(tool_call)
result_str = json.dumps(tool_result, ensure_ascii=False)
# 为每个工具结果创建一条“tool”消息,并加入历史
tool_message = {
“role”: “tool”,
“content”: result_str,
“tool_call_id”: tool_call.id
}
conversation_history.append(tool_message)
all_tool_results.append(tool_result)
# 5. 如果有工具被调用,需要再次请求模型,让它基于工具结果生成最终回复
print(“[系统] 工具执行完毕,正在生成最终回答...”)
second_response = openai.ChatCompletion.create(
model=“gpt-3.5-turbo”,
messages=conversation_history,
# 注意:这次请求通常不再需要传递tools,除非希望开启新一轮工具调用
)
final_message = second_response.choices[0].message
conversation_history.append(final_message.to_dict())
print(f“[AI助手] {final_message.content}”)
else:
# 6. 如果没有调用工具,直接输出助理的回复
print(f“[AI助手] {assistant_message.content}”)
# 返回更新后的对话历史,以便下一轮使用
return conversation_history
# 开始多轮对话
history = []
print(“欢迎使用多技能AI助手(天气/计算器)。输入‘退出’结束。”)
while True:
user_input = input(“\n你: ”)
if user_input.lower() in [“退出”, “exit”, “quit”]:
break
history = run_conversation(user_input, history)
这个进阶示例实现了一个微型的、但功能完整的AI Agent Harness。它具备了多工具管理、多轮对话状态维护、并行工具调用处理等核心能力。你可以通过向
TOOL_REGISTRY
和
tools
列表添加新函数,轻松地为这个助手扩展新的技能,例如发送邮件、查询数据库等。
7. 避坑指南与最佳实践
在亲手搭建和实验的过程中,我踩过不少坑,也总结出一些让Function Calling更稳定、更高效的经验。
7.1 工具描述的“艺术”
工具的
description
和参数的
description
是模型决策的唯一依据。写得好坏,天差地别。
-
要具体,不要抽象
:
- 差:“处理数据”。(模型不知道具体做什么)
- 好:“根据用户提供的城市名,从天气API查询当前的温度、湿度和天气状况。”
-
明确边界和限制
:
- 在描述中说明前提条件。例如,“此函数仅支持国内城市拼音或英文名查询。”
-
说明输出格式。“返回一个包含
temperature(数字)、condition(字符串)的JSON对象。”
- 使用同义词和场景提示 :如果用户可能用多种方式表达同一意图,在描述中涵盖。例如,“获取天气、查询气温、今天天气怎么样”。
7.2 处理模型的“错误”调用
模型有时会调用错误的工具,或提供不合规的参数。
-
参数验证是必须的
:在工具函数内部,第一步永远是验证参数。检查
location是否在支持的城市列表里,检查expression是否包含危险字符。 -
优雅降级
:当模型调用错误时,不要在
tool消息里返回一个程序错误堆栈。而是返回一个结构化的错误信息,比如{“error”: “暂不支持该城市查询”, “suggestion”: “请提供国内主要城市名”}。这样模型还能基于这个错误信息,生成对用户友好的回复。 -
使用
tool_choice进行引导 :在复杂工作流中,可以通过动态设置tool_choice来限制模型在当前步骤只能调用特定工具,减少出错概率。
7.3 性能与成本考量
-
工具列表不宜过长
:每次请求都将完整的
tools列表发送给模型,这会消耗Tokens(尤其是长描述)。如果工具很多(比如几十个),可以考虑根据对话上下文动态筛选相关的工具子集发送给模型。 - 本地模型是学习的最佳伙伴 :正如开头建议的,使用Ollama+本地模型进行学习和原型开发,零成本、响应快、无隐私顾虑。在确定流程后,再考虑切换到更强的云端模型进行生产部署。
- 缓存结果 :对于耗时或消耗资源的工具(如复杂的数据库查询),可以考虑对相同参数的调用结果进行短期缓存,避免重复执行。
7.4 安全第一
-
永远不要相信模型的输入
:将模型通过
arguments传来的参数视为“用户输入”,必须进行严格的清洗、验证和转义,防止SQL注入、命令注入等攻击。上面的calculator函数使用eval是极其危险的示范,绝对不能在真实项目中使用。 -
权限控制
:不同的工具可能对应不同的权限级别。在Harness层,需要根据用户身份或会话上下文,动态过滤
tools列表,只提供当前用户有权访问的工具。
通过这个从“笨办法”开始,逐步深入到架构理解的旅程,你应该已经对Function Calling有了扎实的、可操作的认识。它不是什么黑魔法,而是一种设计精巧的通信协议。掌握它,你就掌握了让大语言模型从“聊天机器人”迈向“智能体”的关键一步。接下来,你可以用这个模式去探索更复杂的Agent框架,那时你会更加得心应手,因为你已经理解了它们底层究竟在做什么。
更多推荐

所有评论(0)