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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

python 复制代码
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 实现,它有两个工具:一个模拟搜索引擎,一个做数学计算。

python 复制代码
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,可能是网络超时"------后者让模型有机会做出合理的后续决策(比如等一下再重试)。

相关推荐
leeyi2 小时前
可观测性:Langfuse、Langsmith 集成
aigc·agent·ai编程
米小虾16 小时前
联合国发布首份全球AI评估报告:我们正站在AI治理的十字路口
aigc·ai编程
AlbertZein20 小时前
Agent任务实测:谁能稳定跑完,谁只是看起来很强?
aigc·openai·ai编程
Token炼金师20 小时前
去噪扩散:从随机噪声到高保真图像的数学之路
人工智能·aigc
AlbertZein21 小时前
别被模型宣传骗了,真实 Agent 任务一跑就知道
aigc·openai·ai编程
小碗细面1 天前
让 AI Agent 真正读懂你的资料:我开源了 source-skill-pipeline
aigc·ai编程·claude
刘棕霆1 天前
30—AI Skill 怎么写才可测:Skill 编写规范与设计方法论
aigc·ai编程·测试
leeyi1 天前
调试工具:Eino Dev 交互式调试
aigc·agent·ai编程
Darling噜啦啦1 天前
拆解 LLM 的内部黑盒:从 Token 到 Self-Attention 的逐层解码之旅
llm·aigc