别再让 AI 瞎猜了:Function Calling 实战,10 分钟让模型真正会用工具

上个月给客户做了个 AI 客服 Demo,演示的时候客户问:"我们公司昨天的销售额是多少?"

AI 非常自信地回答:"昨天贵公司销售额约为 128 万元,同比增长 15%。"

全场沉默。那个数字是我见过最离谱的幻觉,因为那个系统根本没接数据库。

这就是不用 Function Calling 的后果------模型会编,而且编得很有自信。

什么是 Function Calling

Function Calling(也叫 Tool Use)是让模型在回答时,能够主动调用你预先定义好的函数,而不是靠猜。

你告诉模型有哪些工具可以用,模型在需要的时候会说"帮我调用这个工具,参数是这些",然后你执行,把结果还给模型,模型再给出最终回答。

这个机制解决了大模型最核心的问题:知识截止日期 + 无法访问私有数据

最简单的例子:查天气

先从最经典的天气查询开始,把整个流程跑通。

python 复制代码
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 帮你查订单数据。

python 复制代码
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 决定要不要调用这个工具,以及怎么传参。描述越清晰,调用越准确。

python 复制代码
# 差的描述
'description': '查询数据'

# 好的描述
'description': '查询订单数据库,支持按日期(YYYY-MM-DD格式)、状态(completed/pending/cancelled)、客户姓名筛选,返回匹配的订单列表'

工具执行要做参数校验

模型传过来的参数不一定合法,特别是日期格式、枚举值这类。

python 复制代码
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 条并附上总数提示。

处理工具调用失败

网络抖动、数据库超时都可能发生,要给模型一个明确的错误信息。

python 复制代码
try:
    result = func(**func_args)
except Exception as e:
    result = {'error': str(e), 'success': False}

工具管理:稍微封装一下

工具多了之后,散落在各处的 dict 很难维护。简单封装一个注册表:

python 复制代码
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 的雏形了。

相关推荐
摆烂工程师13 小时前
GPT-Image-2 真有点夯:中文不乱码了!GPT-Image-2的入口在哪?教你如何确认自己是否被灰度推送了 GPT-Image-2
gpt·chatgpt·openai
量子位20 小时前
跨维智能DexWorldModel斩获榜首,世界模型真正的考场在机器人执行里
openai
量子位20 小时前
这次,库克真的要卸任苹果CEO了!
openai
爱吃的小肥羊1 天前
我整理了 14 种 GPT-Image-2 的神仙玩法,大家看看效果怎么样!
aigc·openai
程序大视界1 天前
OpenAI放大招,GPT-6发布!
gpt·chatgpt·openai
舒一笑2 天前
大模型根本不是“学会了”,它只是会“看例子”:一文讲透 In-context Learning(ICL)
langchain·llm·openai
冬奇Lab2 天前
你的 Skill 真的好用吗?来自OpenAI的 Eval 系统化验证 Agent 技能方法论
人工智能·openai
Karl_wei2 天前
企业级 Vibe Coding 实操
openai·ai编程·cursor
冰凌时空2 天前
Swift vs Objective-C:语言设计哲学的全面对比
ios·openai