AI Agent 入门实战:用 Function Calling 让大模型学会调用工具

36 阅读7分钟

一、引子:大模型缺的那条腿

你有没有遇到过这种情况——你问 ChatGPT "今天北京的天气怎么样",它说"我无法实时获取天气信息"?

这不是模型不够聪明,而是它天生缺了一条腿:无法和外部世界交互。大模型的知识截止于训练数据的那一刻,它不知道你的数据库里有什么,也没法直接调用外部 API。

2024 年 OpenAI 推出了 Function Calling(函数调用)能力,让模型不仅能"说话",还能"动手"——调用 API、查数据库、发邮件、搜网页。这就是 Agent 架构最核心的基石。

今天我们就从零手写一个能自动调用工具的 Agent,你不需要任何框架,只需要 Python 和一点耐心。

二、Agent 核心逻辑:三步循环

别管任何花哨的名词,Agent 的本质就是一个简单的循环:

步骤做什么谁来干
① 思考理解用户问题,决定是否需要调用工具LLM
② 调用输出结构化参数(JSON),程序执行对应函数你的代码
③ 反馈把工具执行结果送回 LLM,让它生成最终回答LLM + 代码

这个三步循环跑一次叫"单轮工具调用",跑多次叫"多轮 Agent"。

很多人把 Agent 和 RAG 搞混,它们的区别其实很清晰:

能力维度RAGAgent(Function Calling)
检索知识库✅ 向量搜索✅ 可调用搜索 API
操作数据库✅ 执行 SQL 或 CRUD
调用外部 API✅ GET/POST 任意接口
获取实时数据✅ 天气/股票/新闻
自主决策✅ 判断何时调、调哪个工具
多步推理✅ 链式思考与中间结果复用

一句话总结:RAG 让 AI"知道更多",Agent 让 AI"能做更多"。

三、Function Calling 到底怎么工作的?

OpenAI 的 Chat Completions API 提供了一个 tools 参数,你可以把"函数定义"以 JSON Schema 的格式传给模型:

import json
from openai import OpenAI

client = OpenAI()

# 定义工具(JSON Schema 格式)
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如 北京、上海"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

# 第一次调用:模型决定是否要调工具
response = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
    tools=tools
)

msg = response.choices[0].message
if msg.tool_calls:
    print(f"模型选择了工具:{msg.tool_calls[0].function.name}")
    print(f"参数:{msg.tool_calls[0].function.arguments}")
    # 输出:模型选择了工具:get_weather
    # 输出:参数:{"city": "北京"}

模型返回的 tool_calls 里包含了工具名称参数,你只需要解析它、执行对应的函数、把结果送回模型——Agent 循环就完成了。

关键点在于:模型只负责"决定调哪个工具、传什么参数",不负责"执行"。执行工具拿到结果后,再送一次请求让模型生成人类可读的回答。

四、实战:80 行代码写一个能搜索和计算的 Agent

下面是一个完整的 Agent 实现,它有两个工具:一个模拟搜索引擎,一个做数学计算。

import json
from openai import OpenAI

client = OpenAI()

# --- 工具函数 ---
def search_web(query: str) -> str:
    """模拟搜索引擎,实际项目可替换为 Bing/SerpAPI 等"""
    results = {
        "北京天气": "北京今天晴,气温 25-32°C,湿度 40%",
        "北京人口": "北京市常住人口约 2188 万(2024年)",
    }
    return results.get(query, f"未找到「{query}」的相关信息")

def calculator(expr: str) -> str:
    """安全计算器,只允许数字和四则运算"""
    try:
        allowed = set("0123456789+-*/.()")
        if not all(c in allowed for c in expr):
            return "表达式包含非法字符"
        return str(eval(expr))
    except Exception as e:
        return f"计算错误:{str(e)}"

# --- 工具注册表(映射 name → 函数) ---
FUNCTIONS = {
    "search_web": search_web,
    "calculator": calculator,
}

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "搜索实时信息,适合查天气、新闻、百科",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词"}
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculator",
            "description": "执行数学计算,支持加减乘除",
            "parameters": {
                "type": "object",
                "properties": {
                    "expr": {"type": "string", "description": "数学表达式"}
                },
                "required": ["expr"]
            }
        }
    }
]

# --- Agent 主循环 ---
def agent_loop(user_input: str, max_turns: int = 5) -> str:
    messages = [{"role": "user", "content": user_input}]

    for turn in range(max_turns):
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=messages,
            tools=TOOLS
        )
        msg = response.choices[0].message

        # 模型不再需要工具 → 返回最终回答
        if not msg.tool_calls:
            return msg.content

        # 把模型的工具调用请求加入对话历史
        messages.append(msg)

        # 逐个执行工具,把结果送回
        for tc in msg.tool_calls:
            func_name = tc.function.name
            args = json.loads(tc.function.arguments)
            result = FUNCTIONS[func_name](**args)
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": result
            })

    return "已超过最大推理轮数"

# --- 测几个例子 ---
print(agent_loop("北京人口是多少?"))
# 输出:北京市常住人口约 2188 万

print(agent_loop("计算 (35 + 28) * 2 的结果"))
# 输出:126

print(agent_loop("查一下北京天气,然后帮我算如果温度降低 5 度是多少"))
# Agent 先调 search_web 查天气 → 拿到 25-32°C → 再调 calculator 算 32-5 → 最终回答

核心逻辑不到 60 行,但已经具备了一个 Agent 的全部要素:

  • 工具定义:JSON Schema 描述函数签名
  • 模型判断:LLM 自主决定是否调用、调哪个
  • 函数执行:代码安全执行并返回结果
  • 结果反馈:工具结果送回模型,生成自然语言回答
  • 多轮循环:上一轮结果可以做下一轮输入

五、从手写到框架:什么时候该用什么?

手写 Function Calling 灵活性高、零依赖,适合原型验证。如果要上生产,框架能帮你省很多事:

使用场景推荐方案优势
快速原型验证OpenAI SDK 直接手写零依赖,完全可控
单 Agent 复杂工具链LangChain Agent丰富的工具生态和内置 memory
多 Agent 协作编排LangGraph / CrewAI内置状态机 + 图编排
企业级 .NET/Java 集成Semantic Kernel(微软)强类型,企业级链路追踪
低代码 AI 应用Dify / Coze可视化编排,内置 RAG + Agent

六、避坑指南

这几个坑我亲身体验过,帮你省时间:

  1. 工具描述要写得像说明书。模型理解工具的唯一渠道就是 description 字段。写"搜索信息"不如写"搜索最新的实时信息,适合查天气、新闻、百科类问题"。

  2. 参数名要有语义queryq 好,city_namecity 更明确——模型能通过参数名推断含义。

  3. tool_call_id 必须一一对应。多工具并行调用时,每个工具结果必须匹配正确的 ID,否则 API 报错。

  4. 注意 Token 消耗tools 定义本身会占用 Token。工具多了(10+ 个),光工具定义就可能吃掉 2000+ Token。

  5. 工具失败不要抛异常,把错误信息返回给模型。这样模型可以自主决定换参数重试,而不是让你的程序 crash。

  6. 限制 max_turns。防止 Agent 陷入死循环——5 轮通常是合理的上限。

七、总结

Agent 架构听起来高大上,拆开来看核心就是三步循环:LLM 思考 → 调用工具 → 反馈结果。Function Calling 让它成为可能,框架让它变得可维护。

下一期预告:当 Agent 遇到 RAG——如何让 AI 助手既能查知识库,又能操作数据库,真正实现"能问能查能做"。

关注我,不错过后续内容。欢迎在评论区讨论你的 Agent 使用场景或踩过的坑。

扩展阅读:Function Calling 与 MCP 的关系

如果你读过我之前的 MCP 系列文章,可能会问:Function Calling 和 MCP 有什么区别?

简单说,Function Calling 是一种机制,MCP 是一种协议。Function Calling 定义了"模型怎么告诉程序我要调什么工具"的接口格式,而 MCP 定义了"工具服务器和 AI 客户端之间怎么通信"的完整协议。两者不是替代关系,而是互补关系——你完全可以在 MCP Server 内部用 Function Calling 来实现工具调度。

工具函数的设计原则

写工具函数时,有几个设计原则值得记住:

原则一:函数的输入输出都应该是字符串。 不管是搜索 API 返回的 JSON、数据库查询结果、还是计算器算出的数字——最终给模型看到的都应该是一个人类可读的字符串。模型理解自然语言远比理解结构化数据容易。

原则二:工具粒度要适中。 一个工具函数只做一件事。把"搜索+计算+发邮件"塞进一个函数比拆成三个函数难维护得多,而且模型更可能用错参数。

原则三:错误信息也要有信息量。 "查询失败"不如"查询失败:API 返回 500,可能是网络超时"——后者让模型有机会做出合理的后续决策(比如等一下再重试)。