上个月给客户做了个 AI 客服 Demo,演示的时候客户问:"我们公司昨天的销售额是多少?"
AI 非常自信地回答:"昨天贵公司销售额约为 128 万元,同比增长 15%。"
全场沉默。那个数字是我见过最离谱的幻觉,因为那个系统根本没接数据库。
这就是不用 Function Calling 的后果——模型会编,而且编得很有自信。
什么是 Function Calling
Function Calling(也叫 Tool Use)是让模型在回答时,能够主动调用你预先定义好的函数,而不是靠猜。
你告诉模型有哪些工具可以用,模型在需要的时候会说"帮我调用这个工具,参数是这些",然后你执行,把结果还给模型,模型再给出最终回答。
这个机制解决了大模型最核心的问题:知识截止日期 + 无法访问私有数据。
最简单的例子:查天气
先从最经典的天气查询开始,把整个流程跑通。
import openai
import json
client = openai.OpenAI(
base_url='https://api.ofox.ai/v1',
api_key='sk-xxx'
)
tools = [
{
'type': 'function',
'function': {
'name': 'get_weather',
'description': '获取指定城市的当前天气',
'parameters': {
'type': 'object',
'properties': {
'city': {
'type': 'string',
'description': '城市名称,如:北京、上海'
}
},
'required': ['city']
}
}
}
]
def get_weather(city: str) -> dict:
weather_data = {
'北京': {'temp': 22, 'condition': '晴', 'humidity': 45},
'上海': {'temp': 26, 'condition': '多云', 'humidity': 70},
}
return weather_data.get(city, {'temp': 20, 'condition': '未知', 'humidity': 50})
def chat_with_tools(user_message: str):
messages = [{'role': 'user', 'content': user_message}]
response = client.chat.completions.create(
model='claude-sonnet-4-6',
messages=messages,
tools=tools,
tool_choice='auto'
)
message = response.choices[0].message
if message.tool_calls:
messages.append(message)
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
if func_name == 'get_weather':
result = get_weather(**func_args)
messages.append({
'role': 'tool',
'tool_call_id': tool_call.id,
'content': json.dumps(result, ensure_ascii=False)
})
final_response = client.chat.completions.create(
model='claude-sonnet-4-6',
messages=messages,
tools=tools
)
return final_response.choices[0].message.content
return message.content
print(chat_with_tools('北京今天天气怎么样?'))
跑起来之后,模型会先判断这个问题需要查天气,然后告诉你调用 get_weather(city='北京'),你执行完把结果还给它,它再组织语言回答用户。整个流程清晰,不会有任何幻觉。
进阶:多工具 + 数据库查询
天气查询太简单了,来个实际场景:让 AI 帮你查订单数据。
import sqlite3
from datetime import datetime
def init_db():
conn = sqlite3.connect(':memory:')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer TEXT,
amount REAL,
status TEXT,
created_at TEXT
)
''')
test_data = [
(1, '张三', 1280.00, 'completed', '2026-04-21'),
(2, '李四', 560.50, 'pending', '2026-04-21'),
(3, '王五', 3200.00, 'completed', '2026-04-20'),
(4, '赵六', 890.00, 'cancelled', '2026-04-20'),
]
cursor.executemany('INSERT INTO orders VALUES (?,?,?,?,?)', test_data)
conn.commit()
return conn
db_conn = init_db()
tools = [
{
'type': 'function',
'function': {
'name': 'query_orders',
'description': '查询订单数据,支持按日期(YYYY-MM-DD)、状态(completed/pending/cancelled)、客户名筛选',
'parameters': {
'type': 'object',
'properties': {
'date': {'type': 'string', 'description': '日期,格式 YYYY-MM-DD'},
'status': {'type': 'string', 'description': '订单状态:completed/pending/cancelled'},
'customer': {'type': 'string', 'description': '客户姓名'}
}
}
}
},
{
'type': 'function',
'function': {
'name': 'get_sales_summary',
'description': '获取指定日期的销售汇总:总订单数、总金额、已完成金额',
'parameters': {
'type': 'object',
'properties': {
'date': {'type': 'string', 'description': '日期,格式 YYYY-MM-DD'}
},
'required': ['date']
}
}
}
]
def query_orders(date=None, status=None, customer=None):
cursor = db_conn.cursor()
query = 'SELECT * FROM orders WHERE 1=1'
params = []
if date:
query += ' AND created_at = ?'
params.append(date)
if status:
query += ' AND status = ?'
params.append(status)
if customer:
query += ' AND customer LIKE ?'
params.append(f'%{customer}%')
cursor.execute(query, params)
rows = cursor.fetchall()
if len(rows) > 20:
return {'data': [dict(zip(['id','customer','amount','status','date'], r)) for r in rows[:20]], 'total': len(rows), 'note': f'共 {len(rows)} 条,仅返回前 20 条'}
return {'data': [dict(zip(['id','customer','amount','status','date'], r)) for r in rows], 'total': len(rows)}
def get_sales_summary(date: str):
cursor = db_conn.cursor()
cursor.execute('''
SELECT COUNT(*), SUM(amount),
SUM(CASE WHEN status='completed' THEN amount ELSE 0 END)
FROM orders WHERE created_at = ?
''', (date,))
row = cursor.fetchone()
return {'date': date, 'total_orders': row[0], 'total_amount': row[1], 'completed_amount': row[2]}
TOOL_MAP = {'query_orders': query_orders, 'get_sales_summary': get_sales_summary}
def chat_with_db(user_message: str):
messages = [{'role': 'user', 'content': user_message}]
while True:
response = client.chat.completions.create(
model='claude-sonnet-4-6',
messages=messages,
tools=tools,
tool_choice='auto'
)
message = response.choices[0].message
if not message.tool_calls:
return message.content
messages.append(message)
for tool_call in message.tool_calls:
func_name = tool_call.function.name
func_args = json.loads(tool_call.function.arguments)
func = TOOL_MAP.get(func_name)
result = func(**func_args) if func else {'error': '工具不存在'}
messages.append({
'role': 'tool',
'tool_call_id': tool_call.id,
'content': json.dumps(result, ensure_ascii=False)
})
print(chat_with_db('昨天(2026-04-21)的销售情况怎么样?'))
print(chat_with_db('帮我查一下张三的所有订单'))
这段代码支持多轮工具调用——如果模型觉得需要先查汇总、再查明细,它会连续调用两个工具,你只需要在循环里处理就行。
几个容易踩的坑
工具描述要写清楚
模型靠 description 决定要不要调用这个工具,以及怎么传参。描述越清晰,调用越准确。
# 差的描述
'description': '查询数据'
# 好的描述
'description': '查询订单数据库,支持按日期(YYYY-MM-DD格式)、状态(completed/pending/cancelled)、客户姓名筛选,返回匹配的订单列表'
工具执行要做参数校验
模型传过来的参数不一定合法,特别是日期格式、枚举值这类。
def query_orders(date=None, status=None, customer=None):
if date:
try:
datetime.strptime(date, '%Y-%m-%d')
except ValueError:
return {'error': f'日期格式错误:{date},应为 YYYY-MM-DD'}
valid_statuses = {'completed', 'pending', 'cancelled'}
if status and status not in valid_statuses:
return {'error': f'无效状态:{status}'}
# ... 正常查询逻辑
工具结果要控制大小
如果查询返回几千条数据,直接塞进 messages 会撑爆 context window,而且费钱。上面的 query_orders 已经做了截断处理,超过 20 条只返回前 20 条并附上总数提示。
处理工具调用失败
网络抖动、数据库超时都可能发生,要给模型一个明确的错误信息。
try:
result = func(**func_args)
except Exception as e:
result = {'error': str(e), 'success': False}
工具管理:稍微封装一下
工具多了之后,散落在各处的 dict 很难维护。简单封装一个注册表:
from dataclasses import dataclass
from typing import Callable, Any
@dataclass
class Tool:
name: str
description: str
parameters: dict
func: Callable
def to_schema(self):
return {
'type': 'function',
'function': {
'name': self.name,
'description': self.description,
'parameters': self.parameters
}
}
class ToolRegistry:
def __init__(self):
self._tools: dict[str, Tool] = {}
def register(self, tool: Tool):
self._tools[tool.name] = tool
def get_schemas(self):
return [t.to_schema() for t in self._tools.values()]
def execute(self, name: str, args: dict) -> Any:
tool = self._tools.get(name)
if not tool:
return {'error': f'未知工具:{name}'}
try:
return tool.func(**args)
except Exception as e:
return {'error': str(e)}
registry = ToolRegistry()
registry.register(Tool(
name='query_orders',
description='查询订单数据,支持按日期、状态、客户名筛选',
parameters={...},
func=query_orders
))
新增工具只需要 register 一下,get_schemas() 直接传给 API,execute() 统一处理调用和异常。
小结
Function Calling 的核心就三步:定义工具 → 让模型决策 → 执行并返回结果。
掌握这个之后,AI 应用能做的事情就多了很多:查数据库、调外部 API、操作文件系统、发送通知……只要你能写成函数,模型就能用。
我的 API 调用统一走 ofox.ai,Claude 和 GPT 的 Function Calling 接口格式基本一致,切换模型不用改工具定义这部分代码,省了不少事。
下一步可以试试把多个工具组合起来,让模型自己规划调用顺序——那就是 Agent 的雏形了。