Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← 人工智能 / Artificial Intelligence

智能体工程 / Agent Engineering

1. Agent 工程体系全景 / Agent Engineering System Overview

2. Function Calling - 让 LLM 具备行动能力 / Function Calling for Giving LLMs the Ability to Act

3. Agent 框架演进 - 从裸 SDK 到 LangGraph / The Evolution of Agent Frameworks from Raw SDKs to LangGraph

4. RAG 基础 - 让 Agent 拥有"知识" / Retrieval-Augmented Generation Fundamentals for Agent Knowledge

5. 记忆管理 - Agent 的大脑 / Memory Management as the Brain of an Agent

6. Agent 工作流 - 从单步到复杂的执行编排 / Agent Workflows from Single Steps to Complex Orchestration

7. 多 Agent 系统 - 多个 Agent 协作 / Multi-Agent Systems and Agent Collaboration

8. RAG 进阶 - 企业级知识库实战 / Advanced RAG for Enterprise Knowledge Bases

9. 真实 Agent 应用场景 / Real-World AI Agent Applications

10. Structured Output - 让 LLM 输出可控的结构化数据 / Structured Output for Controllable, Machine-Readable LLM Responses

11. Tools Design Best Practices - AI Agent 工具设计最佳实践 / Tools Design Best Practices for AI Agents

12. Agent 架构模式 - 从单 Agent 到多 Agent 的工程范式 / Agent Architecture Patterns

13. Agent Modes — 编程 Agent 的交互模式设计 / Designing Interaction Modes for Coding Agents

14. Agent Workflow 编排:从循环到持久化执行的演进

15. Context Engineering - 从 Prompt 设计到上下文编排 / Context Engineering: From Prompt Design to Context Orchestration

16. Agent 缓存工程:从 KV Cache、Prompt Cache 到语义缓存 / Agent Caching Engineering

17. Harness Engineering, Skills, and Loop Engineering — 从信任模型到验证系统 / From Trusting Models to Verifying Systems

18. MCP 协议 - AI 工具的"USB 接口" / Model Context Protocol for AI Tool Integration

19. Agent 评估与测试 — 如何衡量一个"不可预测"的系统 / Agent Evaluation and Testing — How to Measure an "Unpredictable" System

20. 安全沙箱 - Agent 的安全边界 / Secure Sandboxes as Agent Safety Boundaries

21. 权限与门卫 - Agent 的安全控制中枢 / Permissions and Policy Gates for Agent Control

22. API Key 管理与安全 - Agent 的密钥生命周期的管理 / API Key Lifecycle Management and Security for Agents

23. 提示词注入防护 - Agent 的防御前沿 / Prompt Injection Defense for AI Agents

24. 可观测性与调试 - Agent 运行的透明度保障 / Observability and Debugging for Transparent Agent Operations

25. 模型路由 - 让正确的模型做正确的事 / Model Routing for Matching Models to Tasks

26. OpenClaw 设计深度分析 - 为什么它让人觉得"活"了 / OpenClaw Design Analysis and the Illusion of Liveliness

27. Claude Code 泄露源码深度分析 - 512,000 行代码揭示的生产级 Agent 架构 / Claude Code Source Analysis and Production Agent Architecture

28. LobeChat 设计深度分析 - 全栈 Agent Chat 应用工程实践 / LobeChat Design Analysis and Full-Stack Agent Chat Engineering

29. 编程 Agent 全面对比:从 Claude Code 到 Pi 的设计哲学 / Coding Agents Comparison: Design Philosophies from Claude Code to Pi

30. 领域 Agent 的确定性工具编译与延迟执行——从自然语言规格到单次 CAE 提交

31. Agent 工程学习指南 / An AI Agent Engineering Learning Guide

本页目录

Function Calling - 让 LLM 具备行动能力 / Function Calling for Giving LLMs the Ability to Act ​

📅 创建时间:2026-05-08 🏷️ 标签:#FunctionCalling #ToolUse #Agent基础 📚 前置知识:[[00-agent-overview]]


📋 本章目标 ​

  • 理解 Function Calling 的本质和作用
  • 掌握 Tool 的定义与注册方法
  • 理解 Tool Call 的完整工作流程
  • 能够使用 OpenAI SDK 实现简单的 Function Calling
  • 理解从"文字接龙"到"工具调用"的思维转变
  • 理解 API 网关的"解析层"作用,以及 LLM、网关、Agent 三者之间的关系

第0部分:先搞清数据到底是怎么流动的 ​

在理解 Function Calling 的设计意图之前,有一个更底层的问题必须先回答:数据在网络上到底长什么样? 很多人卡在这里,因为他们只见过 SDK 封装后的 client.chat.completions.create(),不知道这行代码背后 HTTP 请求体和响应体里到底是什么。

0.1 一次普通 LLM 调用的 HTTP 请求和响应 ​

你写的代码:

python
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "1+1 等于几?"}]
)
1
2
3
4

这行代码向 https://api.openai.com/v1/chat/completions 发送了一个 HTTP POST 请求。去掉 SDK 的封装,请求体(Request Body)是一个 JSON:

POST /v1/chat/completions

{
  "model": "gpt-4o",
  "messages": [
    {
      "role": "user",
      "content": "1+1 等于几?"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11

LLM 返回的响应体也是一个 JSON:

{
  "id": "chatcmpl-xxx",
  "object": "chat.completion",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "1+1 等于 2。"
      },
      "finish_reason": "stop"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

到此为止,输入和输出都是纯文本(嵌在 JSON 结构里)。LLM 看到的是 messages 数组中的文本内容,返回的也是文本内容放在 content 字段中。

0.2 带工具调用的 HTTP 请求和响应 ​

现在加一个工具。你写的代码:

python
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",
    messages=[{"role": "user", "content": "北京今天多少度?"}],
    tools=tools
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

实际发出的 HTTP 请求体——tools 就是请求 JSON 里的一个额外字段:

POST /v1/chat/completions

{
  "model": "gpt-4o",
  "messages": [
    {
      "role": "user",
      "content": "北京今天多少度?"
    }
  ],
  "tools": [                    ← 就多了这个字段
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "获取指定城市的实时天气",
        "parameters": {
          "type": "object",
          "properties": {
            "city": {"type": "string", "description": "城市名称"}
          },
          "required": ["city"]
        }
      }
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

LLM 返回的响应体:

{
  "id": "chatcmpl-xxx",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,           ← 没有文本内容!
        "tool_calls": [            ← 替代 content 的是这个
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "get_weather",
              "arguments": "{\"city\": \"北京\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"  ← 不是 "stop",是 "tool_calls"
    }
  ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

关键点:响应体始终是 JSON。区别在于 finish_reason:

  • "stop" → LLM 认为对话结束了,看 message.content 取文本
  • "tool_calls" → LLM 想调工具,看 message.tool_calls 取调用信息

0.3 那 LLM 到底"看到"了什么? ​

这个问题很关键。LLM 本身不直接处理 JSON——它只处理 token 序列。API 服务端在把请求发给模型之前,会把整个 JSON 请求体序列化为模型能理解的 token 格式。大致过程是:

你的 JSON 请求
      ↓
API 服务端序列化为 token 序列(概念示意):

  [system] 你是一个助手
  [user] 北京今天多少度?
  [tools] 你可以使用以下函数:
    1. get_weather(city: string) — 获取指定城市的实时天气
  [assistant]  ← 等待模型生成
1
2
3
4
5
6
7
8
9

tools 定义被 API 服务端转换成了一段自然语言风格的描述(在训练时模型就是这样学的),拼接在对话上下文中。模型看到这段描述后,知道"如果需要查天气,我可以输出一个特殊标记来请求调用 get_weather"。

所以你说的没错——传给 LLM 的本质上就是一段文字。tools 的 JSON 定义被 API 网关序列化成文字描述混入了 prompt。但这个过程对你透明——你只需要在请求 JSON 里加 tools 字段就行。

0.4 模型输出的也是文字——怎么从文字里知道要调工具? ​

模型输出的也是 token 序列(本质上是文字)。关键问题是:API 怎么知道模型"想调工具"而不是"想回答文本"?

答案是:模型在训练阶段被教会了一种特殊输出格式。当它判断需要调工具时,它输出的 token 序列包含特殊的分隔标记:

模型原始输出(token 层面,概念示意):

  <|tool_calls_begin|>
  get_weather
  {"city": "北京"}
  <|tool_calls_end|>
1
2
3
4
5
6

API 服务端的网关层实时监听模型输出的 token 流。当它检测到 <|tool_calls_begin|> 这个特殊标记时,就切到"工具调用解析模式"——把后续的 token 按结构化格式解析,填到响应 JSON 的 tool_calls 字段里,同时把 finish_reason 设为 "tool_calls"。如果模型没有输出这些特殊标记,网关就把所有 token 正常放进 content 字段,finish_reason 设为 "stop"。

所以:模型输入和输出在底层始终是文字(token 序列)。JSON 是 API 服务端在模型之外包的一层外壳——输入端把 tools JSON 转成文字拼进 prompt,输出端把特殊 token 模式解析成 tool_calls JSON。这就是为什么用本地模型(Ollama 等)跑 tool-use 时经常需要自己写解析层——因为你拿到的直接是模型的原始 token 输出,没有网关帮你做这一步转换。

0.5 所以"Agent 循环"到底是什么? ​

现在你知道了:

  • 每次 API 调用 = 一个 HTTP 请求(JSON in, JSON out)
  • 如果 finish_reason == "tool_calls" → LLM 想调工具,你需要执行工具并把结果发回去
  • 如果 finish_reason == "stop" → LLM 给出最终回答,结束

Agent 循环就是:

while True:
    response = POST /v1/chat/completions  (把 messages + tools 发过去)
    
    if response.finish_reason == "stop":
        return response.message.content   ← 结束了
    
    if response.finish_reason == "tool_calls":
        执行工具(response.tool_calls)
        把执行结果追加到 messages 里
        继续循环  ← 下一轮 POST 时 messages 里多了工具结果
1
2
3
4
5
6
7
8
9
10

每次 POST 都是独立的 HTTP 请求。LLM 不"记住"之前的对话——你的程序通过把完整的 messages 数组每次都发回去来维持上下文。这就是为什么 messages 数组越来越长。

带着这个数据流模型,下面进入具体实现。


第1部分:为什么需要 Function Calling? ​

1.1 LLM 的本质困境 ​

回顾 LLM 的工作方式:

┌─────────────────────────────────────────────────────────────┐
│                    传统 LLM 调用                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  输入:文字(如"北京今天的天气怎么样?")                     │
│         ↓                                                   │
│      LLM(语言模型)                                        │
│         ↓                                                   │
│  输出:文字(如"北京今天晴,温度 25 度...")                 │
│                                                             │
│  关键点:LLM 只做一件事——根据输入生成输出文字                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

问题来了:

┌─────────────────────────────────────────────────────────────┐
│                    LLM 无法做到的事情                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ❌ 查询实时信息  → 模型知识有截止日期,不知道今天发生了什么    │
│  ❌ 执行操作      → 只能"说",不能"做"                       │
│  ❌ 访问私有数据  → 没有权限访问你的数据库、文件、API         │
│  ❌ 进行精确计算  → 1+1=2 可能对,但 12345×67890 大概率错   │
│  ❌ 调用外部服务  → 无法连接第三方 API                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11

Function Calling 的解决思路:

┌─────────────────────────────────────────────────────────────┐
│                    Function Calling 解决思路                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  当 LLM "想" 做上述事情时:                                 │
│                                                             │
│  1. LLM 不是自己硬猜答案                                     │
│  2. 而是"告诉程序":我需要调用某个函数                      │
│  3. 程序执行函数,获取真实结果                               │
│  4. 把结果返回给 LLM                                        │
│  5. LLM 整合结果,给出最终回答                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13

1.2 什么是 Function Calling? ​

Function Calling = LLM 生成"调用指令",程序执行"真实操作"

┌─────────────────────────────────────────────────────────────┐
│                    Function Calling 示意图                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  用户:"北京今天多少度?"                                    │
│         ↓                                                   │
│      LLM "看到"这个问题                                      │
│         ↓                                                   │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  LLM 判断:我需要查询天气                             │   │
│  │  生成调用指令:                                      │   │
│  │  {                                                   │   │
│  │    "name": "get_weather",                           │   │
│  │    "arguments": {"city": "北京"}                    │   │
│  │  }                                                   │   │
│  └─────────────────────────────────────────────────────┘   │
│         ↓                                                   │
│  程序执行 get_weather("北京") → 返回 25°C                   │
│         ↓                                                   │
│  LLM 整合结果 → "北京今天气温 25°C,晴朗。"                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

第2部分:Function Calling 的工作流程 ​

2.1 完整工作流程 ​

Step 1:定义工具(Define Tools)

程序员告诉 LLM:"你有这些工具可以用"

python
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的实时天气",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如'北京'"
                    }
                },
                "required": ["city"]
            }
        }
    }
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

Step 2:发送请求(Send Request)

python
response = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "北京今天多少度?"}],
    tools=tools,  # 把工具列表传给 LLM
    tool_choice="auto"  # 让 LLM 决定是否调用工具
)
1
2
3
4
5
6

Step 3:解析响应(Parse Response)

python
tool_call = response.choices[0].message.tool_calls[0]
function_name = tool_call.function.name      # "get_weather"
function_args = tool_call.function.arguments  # {"city": "北京"}
1
2
3

Step 4:执行函数(Execute Function)

python
# 根据函数名执行对应的真实逻辑
if function_name == "get_weather":
    result = fetch_weather_from_api(function_args["city"])
    # result = {"temp": 25, "condition": "晴"}
1
2
3
4

Step 5:返回结果(Return Result)

python
messages.append({
    "role": "tool",
    "tool_call_id": tool_call.id,
    "content": json.dumps(result)  # 把函数结果转成 JSON 字符串
})

# 再次调用 LLM,整合结果,生成最终回答
final_response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages
)
1
2
3
4
5
6
7
8
9
10
11

2.1.1 JSON 里的 name 怎么关联到真实的 Python 函数? ​

这里有一个容易被忽略的细节:JSON 里 "name": "get_weather" 只是一个字符串——它不会自动调用你的 Python 函数。你需要自己维护一个名字到函数的映射表:

python
# 工具的实际实现就是普通 Python 函数
def get_weather(city: str) -> dict:
    resp = requests.get(f"https://api.weather.com?city={city}")
    return resp.json()

def send_email(to: str, subject: str, body: str) -> dict:
    # SMTP 逻辑
    return {"status": "sent"}

# 手动映射:把 JSON 里的 "name" 字符串和真实函数关联
AVAILABLE_FUNCTIONS = {
    "get_weather": get_weather,    # key 必须和 tools JSON 里的 name 一致
    "send_email": send_email,
}

# 当 LLM 返回 tool_calls: [{"function": {"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}}]
# Agent 做的事:
func = AVAILABLE_FUNCTIONS["get_weather"]        # 通过字符串名查表找到函数
args = json.loads(tool_call.function.arguments)   # 把 LLM 给的 JSON 字符串解析为 dict
result = func(**args)                             # 调用函数,传入 LLM 给的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

所以 LLM 从始至终没有"调用"任何东西。 它只是输出了一段文本(这段文本恰好是 {"name": "get_weather", "arguments": "{\"city\": \"北京\"}"}),你的程序读到这段文本后,用 name 的值去 AVAILABLE_FUNCTIONS 字典里找到对应的真实函数,然后调用它。函数名和参数不是程序员写死在代码里的,而是 LLM 在运行时决定的——这就是 Function Calling 的全部实质。

2.2 Tool Call 的五种状态 ​

┌─────────────────────────────────────────────────────────────┐
│                    Tool Call 状态机                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  状态1:不需要工具                                           │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 用户:"1+1 等于几?"                                  │   │
│  │ LLM:直接回答"2"(不需要查天气)                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  状态2:调用单个工具                                         │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 用户:"北京今天多少度?"                               │   │
│  │ LLM:调用 get_weather("北京")                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  状态3:调用多个工具(并行)                                 │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 用户:"北京、上海、广州今天分别多少度?"               │   │
│  │ LLM:同时调用 get_weather 三次(并行)                │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  状态4:多次调用(链式)                                     │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 用户:"帮我查天气然后发邮件"                           │   │
│  │ LLM:先查天气 → 把结果放入上下文 → 决定发邮件          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  状态5:拒绝调用                                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 用户:"帮我删掉公司所有数据"                           │   │
│  │ LLM:拒绝调用(危险操作)                              │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

第3部分:Function Calling 实战 ​

3.1 完整示例:天气 + 邮件 Agent ​

python
from openai import OpenAI
import json

client = OpenAI()

# Step 1: 定义工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "城市名称"}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "send_email",
            "description": "发送邮件",
            "parameters": {
                "type": "object",
                "properties": {
                    "to": {"type": "string", "description": "收件人邮箱"},
                    "subject": {"type": "string", "description": "邮件主题"},
                    "body": {"type": "string", "description": "邮件正文"}
                },
                "required": ["to", "subject", "body"]
            }
        }
    }
]

# Step 2: 初始化消息
messages = [
    {
        "role": "system",
        "content": "你是一个智能助手。你有天气查询和邮件发送的能力。"
    },
    {
        "role": "user",
        "content": "北京今天的天气怎么样?顺便帮我发封邮件给 zhang@example.com,主题是'今日天气',内容告诉他北京今天25度,晴天。"
    }
]

# Step 3: 第一次调用
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

assistant_message = response.choices[0].message
messages.append(assistant_message)

# Step 4: 处理工具调用(可能有多个)
while assistant_message.tool_calls:
    for tool_call in assistant_message.tool_calls:
        function_name = tool_call.function.name
        function_args = json.loads(tool_call.function.arguments)

        # 模拟执行函数
        if function_name == "get_weather":
            result = {"temp": 25, "condition": "晴天", "humidity": 40}
        elif function_name == "send_email":
            result = {"status": "sent", "message_id": "12345"}

        # 把结果返回给 LLM
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result)
        })

    # 再次调用 LLM
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools
    )
    assistant_message = response.choices[0].message
    messages.append(assistant_message)

# 最终输出
print(assistant_message.content)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

3.2 工具定义的要点 ​

┌─────────────────────────────────────────────────────────────┐
│                    好的 Tool 定义的关键                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 名称(name)要清晰                                       │
│     ❌ get_w   →  ✅ get_weather                            │
│     ❌ query   →  ✅ search_database                         │
│                                                             │
│  2. 描述(description)是给 LLM 看的,决定调用时机           │
│     ❌ "查询函数"                                            │
│     ✅ "用于查询实时天气信息,输入城市名,返回温度和天气状况"  │
│                                                             │
│  3. 参数(parameters)要完整且类型明确                      │
│     • required:必填参数                                     │
│     • description:每个参数的用途(帮助 LLM 正确填参)       │
│                                                             │
│  4. 不要暴露过于底层或复杂的参数                             │
│     LLM 不是程序员,尽量用业务语言描述                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

第4部分:强制调用 vs 自动选择 ​

4.1 tool_choice 的三种模式 ​

python
# 模式1:auto(默认)—— LLM 自己决定是否调用
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

# 模式2:none —— 强制不调用工具,LLM 直接回答
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice="none"  # 强制 LLM 直接回答
)

# 模式3:强制调用指定工具 —— 适合必须用工具的场景
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
    tool_choice={"type": "function", "function": {"name": "get_weather"}}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

4.2 何时用哪种模式? ​

┌─────────────────────────────────────────────────────────────┐
│                    tool_choice 选择指南                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  auto(自动):                                             │
│  • 通用场景,LLM 自主决定                                   │
│  • 节省不必要的 API 调用                                    │
│  • 推荐作为默认选择                                         │
│                                                             │
│  none(强制不调用):                                       │
│  • 简单问答不需要工具时                                      │
│  • 调试时隔离工具影响                                       │
│                                                             │
│  强制指定工具:                                             │
│  • 工作流固定、必须先查再处理的场景                          │
│  • 例如:Agent 必须先搜索,才能总结                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第5部分:Function Calling 的局限性与注意事项 ​

5.1 常见问题 ​

问题1:LLM 乱填参数
┌─────────────────────────────────────────────────────────────┐
│  用户问:"给我查一下天气"                                    │
│  LLM 可能调用 get_weather(city=null) 或 city="地球"        │
│                                                             │
│  解决:                                                     │
│  • 增强 description,明确要求必填城市参数                    │
│  • 使用 enum 限制可选值                                      │
│  • 在代码中校验参数,不合法则返回错误让 LLM 重试             │
└─────────────────────────────────────────────────────────────┘

问题2:LLM 该用工具时不用
┌─────────────────────────────────────────────────────────────┐
│  用户:"帮我查今天北京最高气温,然后发个朋友圈"                │
│  LLM 可能直接回答了最高气温,忘记调用发朋友圈的工具           │
│                                                             │
│  解决:                                                     │
│  • 在 System Prompt 中明确:"用户请求涉及工具时,必须调用"   │
│  • 分解任务,每个工具单独一次调用                            │
└─────────────────────────────────────────────────────────────┘

问题3:工具调用死循环
┌─────────────────────────────────────────────────────────────┐
│  LLM 不断调用工具 → 返回结果 → 又调用 → 永远不停             │
│                                                             │
│  解决:                                                     │
│  • 设置最大调用次数(如 max_calls=10)                       │
│  • 在 System Prompt 中明确:"调用工具后,根据结果直接回答"   │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

5.2 安全注意事项 ​

┌─────────────────────────────────────────────────────────────┐
│                    Tool 安全 Checklist                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 永远不要在 Tool 定义中包含敏感凭证                       │
│     ❌ {"api_key": "sk-xxx"}                                │
│     ✅ 在服务器端存储,工具只接收业务参数                     │
│                                                             │
│  2. 所有工具调用都要在服务端验证和执行                       │
│     • LLM 生成的调用指令只是"建议"                           │
│     • 服务端必须校验参数合法性                                 │
│     • 敏感操作需要二次确认                                   │
│                                                             │
│  3. 限制工具的能力边界                                      │
│     • 只暴露必要的操作                                       │
│     • 避免暴露删除、清空等高危操作                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第6部分:深入理解——API 网关与三层架构 ​

6.1 一个容易误解的关键问题 ​

前面讲到 LLM 返回的响应里有 content 和 tool_calls 两个字段:

API 响应
├── message
│   ├── content:     "这是说给用户听的文本"
│   └── tool_calls:  [{name: "get_weather", ...}]
1
2
3
4

这个结构化的响应是 LLM 的原始输出吗?答案是否定的。

LLM 在最底层做的事从来没变过:输入 token 序列 → 预测下一个 token → 输出 → 循环。它输出的永远只有一个东西:token 流,不是结构化 JSON。

6.2 谁做了"解析"这件事? ​

答案是 API 服务端的网关层。它在 LLM 和你的代码之间,做了一步关键转换:

LLM 原始输出的 token 流(概念示意):

  "...用户需要天气<|tool_start|>get_weather<|arg_start|>"
  "{"city":"北京"}<|tool_end|>好的,查询结果是..."

                    ↓
          API 网关层拦截并解析
                    ↓

  ┌───────────────────────────────────────┐
  │  识别到 <|tool_start|>...<|tool_end|> │
  │  把它们从文本中剥离                    │
  │  解析成结构化 JSON 放进 tool_calls     │
  │  其余文本留在 content                  │
  └───────────────────────────────────────┘

                    ↓
          返回给用户的最终结构:

          {
            "content": null,
            "tool_calls": [{
              "name": "get_weather",
              "arguments": "{\"city\": \"北京\"}"
            }]
          }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

所以 LLM 本身不"知道"什么叫 content 字段、什么叫 tool_calls 字段。这些概念是 API 网关层发明出来给你的。LLM 在训练时学到的只是:当需要调用工具时,输出一种特定的 token 模式(比如特殊的标记符号),就像它学会输出句号表示句子结束一样。

这也解释了为什么你无法通过 prompt 注入来伪造一个 tool_call——即使你在对话内容里让 LLM "复述一段 tool_calls 格式的文本",那段文本只会留在 content 字段里。网关层只认 LLM 输出的特殊 token 标记,不认 content 里的字符串。

6.3 模型训练:为什么有的模型会、有的不会? ​

一个只做过"文本补全"训练的原始基座模型(如早期的 GPT 基座、Llama 基座),你给它一个 tools 定义的 JSON 菜单,它完全不知道这是"可以调用的工具"。它只会把菜单当成普通文本,然后接着往下编瞎话。

而 GPT-4、Claude 这些支持 Function Calling 的模型,在训练阶段被专门输入了大量这样的示例:

训练示例(简化):

  输入: 用户问"北京多少度?" + tools 定义
  期望输出: <|tool_start|>get_weather{"city":"北京"}<|tool_end|>

  输入: 用户问"1+1等于几?" + tools 定义
  期望输出: 1+1 等于 2。(不需要调工具,正常回答即可)

  ... 成千上万条
1
2
3
4
5
6
7
8
9

经过微调后,模型学会了模式识别:当看到 tools 定义且用户问题超出了自身知识范围时,不要编造答案,而是输出特殊标记格式的"调用请求"。这不是模型"理解了函数"或"变聪明了",纯粹是训练出来的行为模式。

所以 Function Calling 在模型侧有一个硬性前提:模型必须被专门训练过这种输出模式。 不是所有模型都会,没训练过的模型你给它 tools 定义它也不会有反应。

6.4 完整的三层架构 ​

搞清楚网关层的存在后,Function Calling 的完整架构就可以清晰拆成三层:

┌───────────┐      ┌────────────────┐      ┌────────────────┐
│           │      │                │      │                │
│   LLM     │ ───→ │   网关/解析层   │ ───→ │  Agent 程序     │
│  (纯模型)  │      │                │      │                │
│           │      │  解析 token 流   │      │  读 tool_calls  │
│ 输出 token │      │  分离 content   │      │  执行真实函数    │
│   流      │      │  与 tool_calls  │      │  把结果返回 LLM  │
│           │      │                │      │                │
└───────────┘      └────────────────┘      └────────────────┘
1
2
3
4
5
6
7
8
9
层级由谁提供干什么
LLM模型文件根据输入预测并输出 token 流
网关/解析层API 厂商 或 客户端软件把 token 流解析为 content + tool_calls
Agent 程序你自己写的代码读 tool_calls → 匹配函数 → 执行 → 反馈结果给 LLM

网关层可以叫它"Agent 的前处理层"——它不执行工具,但负责把 LLM 的原始输出翻译成 Agent 程序能消费的格式。

6.5 不同部署场景下的对应关系 ​

理解了三层架构之后,再看不同的实际场景,每层的位置就清楚了:

场景 A:使用 OpenAI / 硅基流动等云端 API

你的电脑                    云端服务器
┌──────────────┐    ┌─────────────────────────┐
│ Agent 程序    │ ←──│ API 网关 + LLM           │
│ (只有执行层)  │    │ (厂商一体提供)            │
└──────────────┘    └─────────────────────────┘
1
2
3
4
5

你拿到的 response 已经是处理好的,content 和 tool_calls 干干净净。网关层对你不可见。

场景 B:用 Ollama 跑本地模型 + 自己写代码调用

你的电脑
┌──────────────────────────────────────┐
│  LLM (Ollama/本地)                    │
│     ↓ 原始 token 流                   │
│  你的解析代码  ← 你得自己写这层!      │
│     ↓                                │
│  Agent 执行层                         │
└──────────────────────────────────────┘
1
2
3
4
5
6
7
8

原生 Ollama 或 llama.cpp 输出的就是 token 流(或纯文本),没有 tool_calls 字段。你需要自己写解析逻辑:用正则或 json.loads() 从 content 里把工具调用的 JSON 扒出来。开源 Agent 框架(LangChain、CrewAI 对接 Ollama 时)内部都带了一层这样的解析器。

场景 C:本地模型 + CherryStudio / Chatbox 等对话客户端

你的电脑
┌──────────────────────────────────────┐
│  LLM (Ollama/本地)                    │
│     ↓ 原始 token 流                   │
│  CherryStudio 内置解析层  ← 客户端自带 │
│     ↓                                │
│  你看到干净的 UI 和结构化响应           │
└──────────────────────────────────────┘
1
2
3
4
5
6
7
8

同一个本地模型文件,挂在 CherryStudio 下面时你看到的是干净的聊天界面和工具调用,因为 CherryStudio 充当了网关层。但它并不是有魔法——底层 LLM 还是那个 LLM,如果模型本身没有经过 tool-use 训练,客户端解析得再漂亮也解析不出 tool_calls。

关键认知:同一个模型文件,它的能力完全没变。变的是你获取它输出时,中间有没有那一层解析器,以及那个解析器有多聪明。

6.6 工具函数到底是什么样的? ​

理解了三层架构,工具函数本身的形态就很简单了。它不是 exe、不是独立进程、不需要继承基类,就是项目中一个普通的函数:

python
# 这就是一个"工具"——普通函数,接受参数,返回结果
def get_weather(city: str) -> dict:
    """查询天气。真实项目里这里调 API"""
    import requests
    resp = requests.get(f"https://api.weather.com?city={city}")
    return resp.json()

def send_email(to: str, subject: str, body: str) -> dict:
    """发送邮件。真实项目里这里调 SMTP"""
    import smtplib
    # ... SMTP 逻辑 ...
    return {"status": "sent"}

# Agent 做的事:按 LLM 给的名字找到函数,调用它
TOOL_MAP = {
    "get_weather": get_weather,
    "send_email": send_email,
}

# 当 LLM 返回 tool_calls: [{name: "get_weather", args: {city: "北京"}}]
# Agent 执行的就是这一行:
result = TOOL_MAP["get_weather"](city="北京")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

它和普通函数的唯一区别是:函数名和参数不是程序员写死在代码里的,而是 LLM 在运行时决定的。 这就是 Function Calling 的全部实质。


核心总结 ​

总结1:Function Calling 的本质 ​

Function Calling = 让 LLM 多了"行动能力"

• LLM 不再只是"说话"
• 它可以"决定"调用什么工具
• 程序负责"执行"工具
• 结果返回 LLM,最终整合回答
1
2
3
4
5
6

总结2:工作流程 ​

定义工具 → 发送请求(含工具列表) → LLM 判断是否调用
→ 程序执行 → 结果返回 → LLM 整合 → 最终回答
1
2

总结3:三层架构 ​

LLM(输出 token 流)
  → 网关/解析层(把 token 流转成 content + tool_calls)
    → Agent 程序(读 tool_calls,执行真实函数,返回结果)
1
2
3
  • 厂商 API(OpenAI/硅基流动):网关层在云端,对开发者透明
  • 本地模型 + 自己写代码:网关层需要自己实现
  • 本地模型 + 客户端(CherryStudio):客户端充当网关层
  • 工具函数就是普通 Python 函数,函数名和参数由 LLM 运行时决定

总结4:tool_choice 三模式 ​

模式适用场景
auto默认选项,LLM 自主决定
none强制不用工具,直接回答
强制指定工作流固定,必须用某工具

章节测试 ​

测试1:概念理解 ​

Function Calling 的核心作用是什么?

测试2:工作流程 ​

请描述 Function Calling 的完整工作流程(5个步骤)。

测试3:参数设置 ​

在定义 Tool 时,required 字段的作用是什么?

测试4:tool_choice ​

什么场景下应该使用 tool_choice="none"?

测试5:安全问题 ​

以下哪个是 Function Calling 中的安全风险? A. LLM 生成的参数包含非法值 B. Tool 定义放在消息数组中传给 LLM C. 使用 tool_choice="auto" D. 同时调用多个工具

测试6:架构理解 ​

API 返回的 content 和 tool_calls 字段是谁生成的? A. LLM 直接输出的结构化 JSON B. API 网关层解析 token 流后分离出来的 C. Agent 程序自己从 content 里提取的 D. 用户在 tools 定义中指定的

测试7:部署场景 ​

用 Ollama 跑本地模型 + 自己写 Python 代码调用,与用 OpenAI API 相比,最大的区别是什么? A. 本地模型运行速度更慢 B. 本地模型不支持工具调用 C. 需要自己实现网关/解析层,从原始输出中提取工具调用信息 D. 本地模型无法联网


参考答案 ​

测试1答案 ​

答案:让 LLM 从"只能说话"变成"能够行动",通过调用外部工具获取实时信息、执行操作、访问私有数据。


测试2答案 ​

答案:

  1. 定义工具(Describe Tools):告诉 LLM 有哪些可用工具
  2. 发送请求(Send Request):把工具列表和用户问题一起发给 LLM
  3. 解析响应(Parse Response):从 LLM 响应中提取函数名和参数
  4. 执行函数(Execute Function):在程序中执行对应的真实逻辑
  5. 返回结果(Return Result):把执行结果发回 LLM,整合生成最终回答

测试3答案 ​

答案:required 字段声明哪些参数是必填的。如果 LLM 没有提供 required 参数,程序应该报错并让 LLM 重试。


测试4答案 ​

答案:当你想强制 LLM 直接回答而不使用任何工具时。例如:调试时隔离工具影响,或确定问题不需要工具(如常识问答)。


测试5答案 ​

答案:A(LLM 生成的参数包含非法值)

解析:

A. ✅ 风险:LLM 可能生成 null、错误格式、或超出范围的参数值
   必须服务端校验

B. ❌ 不是风险:Tool 定义放在 messages 中传给 LLM 是标准做法
   敏感凭证不应该放在 Tool 定义中

C. ❌ 不是风险:tool_choice="auto" 是安全的默认选项

D. ❌ 不是风险:并行调用多个工具是 Function Calling 的正常能力
1
2
3
4
5
6
7
8
9

测试6答案 ​

答案:B(API 网关层解析 token 流后分离出来的)

解析:

LLM 的原始输出只是 token 流,不是结构化 JSON。
API 网关层识别到特殊的 token 模式后,
将其解析分离为 content 和 tool_calls 两个字段。
这就是为什么你用 Ollama 跑本地模型时需要自己写解析层,
而用 OpenAI API 时拿到的已经是干干净净的结构化数据。
1
2
3
4
5

测试7答案 ​

答案:C(需要自己实现网关/解析层,从原始输出中提取工具调用信息)

解析:

本地模型和云端模型的能力相同(前提是模型本身支持 tool-use),
但 OpenAI API 替你做了 token 流解析这一步。
使用本地模型时,Ollama 等工具输出的是原始文本,
你需要自己解析 tool_calls,或者使用 CherryStudio 等
内置了解析层的客户端软件来充当网关层。
1
2
3
4
5

相关笔记 ​

  • [[00-agent-overview]] - Agent 整体学习路线
  • [[02-framework-evolution]] - 框架如何封装 Function Calling
  • [[05-agent-workflow]] - Tool Calling 在工作流中的应用

下一步学习 ​

  • [ ] 阅读 02 - Agent 框架演进

学习状态:🟡 开始学习

最后更新于:

Pager
上一篇1. Agent 工程体系全景 / Agent Engineering System Overview
下一篇3. Agent 框架演进 - 从裸 SDK 到 LangGraph / The Evolution of Agent Frameworks from Raw SDKs to LangGraph

持续记录,持续成长

Copyright © Tidenflow