春哥的Agent通关秘籍05:工具调用 Function Calling【知识与思路篇】

0 阅读7分钟

LLM 问世之后,很长一段时间,人们发现了很多用例可以证明:

AI不行啊,不是人工智能,而是人工智障。

比方说,最经典的一个问题就是:

7.11 和 7.9 哪个数字更大?

你也可以修改我们第一节课【春哥的Agent通关秘籍02:搭建环境及语言选择】里的 check_env.py代码,问AI:"今天是几月几日?"

deepseek 会自信地回答你:

早期的AI对于这样的用例毫无防备,闹了很多笑话。

但是,在Agent时代,这些根本不是问题,你可以稳定解决所有加减乘除,可以准确比较大小,可以知道今天是几月几日。

比如,无论你去deepseek官网,或者是豆包,提问"今天是几月几日?"

都能得到准确而正确的回答:

这是为什么?

因为官网和APP都利用了我们今天要学习的内容:Function Calling。

工具调用

一、Function Call的历史及现状

为了解决早期大语言模型无法做精细活儿,无法应对准确计算和实时信息获取的问题。

2023年6月13日 ,OPENAI 在发布 gpt-4-0613 和 gpt-3.5-turbo-0613 版本时,推出了一项轰动世界的设计和能力:

Function Calling

发布这个功能之前

开发者(包括早期的 LangChain 用户)想要让 AI 调用工具,必须使用 ReAct 模式。

做法:你在 System Prompt 里写一大段话:“你是一个可以使用工具的 AI。如果你想用计算器,请输出 Action: Calculator, Input: 1+1。”

痛点:

  • 不稳定:AI 经常忘了格式,或者多打了个标点符号。

  • 解析难:开发者必须写复杂的正则表达式(Regex)去抓取 AI 的回复。

  • Token 浪费:你需要把工具的描述写在 Prompt 里,占用大量上下文。

OpenAI的创新

OpenAI 受Meta的Toolformer启发,通过微调(Fine-tuning),让模型在底层“原生”支持了调用外部工具的能力。

你不再需要在 Prompt 里求 AI “请按格式输出”。你只需要把 JSON Schema 传给 API,模型就能极度稳定地输出结构化的 JSON 调用指令。

可以说:Function Calling 是 OpenAI 将“学术界的 Tool Learning”转化为“工业级 API 标准”的产物。

它标志着 AI Agent 开发从“手搓 Prompt 的黑客时代”进入了“标准化工程开发时代”。现在,Anthropic (Claude)、Google (Gemini)、DeepSeek 等主流模型厂商都跟随并支持了这一标准。

二、Function Calling 基本流程

我画了一张图,给大家解释当用户输入“北京今天天气如何”之后,整个的架构流转:

你会发现一个重要的定律:

LLM 对于 Agent 而言,其实是无状态的。

当Agent朝LLM发起会话时,需要每次都携带历史记录,以完成多轮会话。

每次都需要携带注册到agent中的function列表,每次都需要写代system prompt。

你可以把它想象成电影《初恋50次》里的女主角,或者是一条只有 7 秒记忆的金鱼。 无论你们之前聊得多么热火朝天,当你发起的 HTTP 请求结束那一刻,它就把你彻底忘了。

因此,这带来了三个很大的负担问题:

  1. 人设负担
    • 每次会话都要重新声明一次人设,不然LLM就会不知道该信息。
  2. 技能树负担
    • 每次会话都要重新声明一次所有Agent拥有的全技能Json Schema,这会占用巨大的Token。
  3. 记忆负担
    • 随着对话进行,messages 列表会越来越长。

这会导致以下几个问题:

  • 费钱:LLM 是按 Input Token 收费的。第 1 句的历史,在第 100 轮对话时被重复计费了 100 次。
  • 变慢:处理的文字越多,首字延迟(TTFT)越高。
  • 溢出:一旦超过模型的上下文上限(比如 32k 或 128k),模型就会报错或强制截断,导致“失忆”。
  • 变蠢:一旦token过长,LLM在巨大的token里关于会话里的关键信息的注意力会被稀释。

如何解决这个问题,是所有Agent开发必须面对的。这里不过多展开了,先提一下。

三、工具定义与注册

在让AI知道应该在何时调用工具的第一步,相当于你需要在每次会话里附带一个目录,告诉AI:

嗨,牢A,我这里有一个名叫get_current_time的工具,它是个方法,作用是“获取当前准确的时间”。

这样,当没有记忆的LLM面对“当前准确时间是什么”的时候,它才会知道要调用这个工具。

很巧,上节课我们才介绍了 JSON Schema,这节课的 Function Calling 注册工具,也需要用到JSON Schema。

但它不是完全标准的 JSON Schema,你可以先看看结构:

tools = [
    {
        # 1. 类型:目前仅支持 "function"
        "type": "function",
        # 2. 函数定义主体
        "function": {
            # 2.1 (必填) 函数名
            "name": "...",
            # 2.2 (强烈推荐) 给 AI 看的函数说明书
            "description": "...",
            # 2.3 (必填) 参数定义,遵循 JSON Schema 标准
            "parameters": {        
                "type": "object",
                "properties": { ... },
                "required": [...]
            },
            # (可选) DeepSeek/OpenAI 支持的严格模式,设为 True 强制输出符合 Schema
            "strict": True 
        }
    }
]

如果想找官方文档的话,国内开发者优先建议参考 deepseekFunction Calling 文档:api-docs.deepseek.com/zh-cn/guide…

其中提到:

用户需要设置 base_url="api.deepseek.com/beta" 来开启 Beta 功能

嗯,看到这里,我们可以默默地去修改一下我们 .env 文件的 BASE_URL 参数,以求获得更稳定的返回结构。

另外还可以参考:platform.openai.com/docs/guides…

让我们根据上面学到的,随手注册两个工具方法:

# 定义工具列表 
tools = [
    # 方法A:获取当前精准时间
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "获取当前精确时间",
            "parameters": {"type": "object", "properties": {}}
        }
    },
    # 方法B:计算两个数的乘积
    {
        "type": "function",
        "function": {
            "name": "calculate_multiply",
            "description": "计算两个数的乘积",
            "parameters": {
                "type": "object",
                "properties": {
                    "a": {"type": "number", "description": "第一个数字"},
                    "b": {"type": "number", "description": "第二个数字"}
                },
                "required": ["a", "b"]
            }
        }
    }
]

有了 Schema,如何添加到会话里去呢?

非常简单!

response = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    tools=tools, # 【关键】把工具箱递给它
)

四、识别AI的返回,调用具体的方法

刚刚我们在请求里附带了tools清单。

那么,怎么识别AI在什么时候需要调用我们的tools呢?

很简单:

# 获取 AI 的回复消息
ai_msg = response.choices[0].message
print(f"ai_msg.tool_calls:{ai_msg.tool_calls}")

在ai_msg这个LLM 返回值信息里,我们需要重点关注 ai_msg.tool_calls 这个字段。

如果它是None或者空数组,则表示AI没有想要调用的Function,否则的话,即代表:

刚才这一次会话,AI没有得到结论,而是希望调用一个或者多个Function。

所以,我们可以先写出如下的大致逻辑:

if ai_msg.tool_calls:
    print(f"🤖 AI 决定调用工具!")
    
    # 必须把 AI 的这句“我想调用工具”加入对话历史
    # 否则后面通过工具结果回复时,AI 会失忆
    messages.append(ai_msg)

    # === 你的代码介入:执行工具 ===
    for tool_call in ai_msg.tool_calls:
        print(f"🤖 AI 决定调用工具: {tool_call.function.name}")
        func_name = tool_call.function.name
        # 解析参数 (JSON 字符串 -> 字典)
        func_args = json.loads(tool_call.function.arguments)
else:
    # AI 觉得不需要工具,直接回答
    return ai_msg.content

拿到方法名与方法参数后,接下来要做的就是执行方法,得到结果,这一点连刚接触编程的新手都知道怎么写了。

拿到结果后,只需要再次执行:

messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": tool_result
})
# === 第二轮交互 ===
final_response = client.chat.completions.create(
    model="deepseek-chat",
    messages=messages,
    tools=tools
)

为什么这里的第二轮交互里还要带上 tools

因为Agent并不知道LLM是否会再发起一次Function Calling,如果不携带tools,LLM就非常可能在遇到问题时无奈地瞎编或者报错。

所以,很显然,我们需要递归,或者循环。

这就涉及到了一个新的,深刻的,重要的,难以规避的核心问题:

在Agent开发中,应该如何统一AI的思考与行动,并巧妙设计这个循环。

下一节课再见!

下一步预告

下节课,我们将探索 Agent 编程的一个核心问题:

思考范式。

敬请期待!