摘要
Function Calling 是大语言模型从"对话工具"进化为"行动代理"的关键能力。本文以 AI Agent 工程化落地为视角,系统拆解 Function Calling 的全链路实践:从 JSON Schema 参数定义规范、工具描述的艺术,到多工具并行/串行调用的区别,再到错误处理全链路设计。文中包含一个完整的多工具 Agent(搜索+计算+数据库查询)实战案例,覆盖参数校验、执行异常、超时降级等真实工程问题。适合正在构建 AI Agent 的后端工程师和架构师阅读,基于 OpenAI Function Calling 规范(2024+ 版本),兼容主流开源模型工具调用协议。
📌 版本声明:本文基于 OpenAI Function Calling 规范(gpt-4o / gpt-4o-mini,2024+ 版本)编写,同时参考 Anthropic Claude Tool Use、开源模型 Tool Calling 协议(如 Qwen、Llama 等)。示例代码使用 Python 3.11+。如使用其他模型或版本,部分参数命名和返回格式可能存在差异,请参考对应模型的官方文档。
文章目录
-
- 摘要
- [一、Function Calling 的本质:从"模型说话"到"模型做事"](#一、Function Calling 的本质:从"模型说话"到"模型做事")
-
- [1.1 没有 Function Calling 的世界](#1.1 没有 Function Calling 的世界)
- [1.2 Function Calling 带来了什么](#1.2 Function Calling 带来了什么)
- [1.3 Function Calling 的核心价值](#1.3 Function Calling 的核心价值)
- [1.4 Function Calling 的工作机制详解](#1.4 Function Calling 的工作机制详解)
- [1.5 从概念到工程:理解 Function Calling 的抽象层次](#1.5 从概念到工程:理解 Function Calling 的抽象层次)
- [二、参数定义规范:JSON Schema 设计与最佳实践](#二、参数定义规范:JSON Schema 设计与最佳实践)
-
- [2.1 为什么参数定义如此重要](#2.1 为什么参数定义如此重要)
- [2.2 JSON Schema 基础结构](#2.2 JSON Schema 基础结构)
- [2.3 参数类型选择指南](#2.3 参数类型选择指南)
- [2.4 高级 Schema 设计技巧](#2.4 高级 Schema 设计技巧)
- [2.5 参数定义的常见反模式](#2.5 参数定义的常见反模式)
- [2.6 参数 Schema 设计的工程原则](#2.6 参数 Schema 设计的工程原则)
- [三、工具描述的艺术:description 字段如何决定 Agent 决策质量](#三、工具描述的艺术:description 字段如何决定 Agent 决策质量)
-
- [3.1 description 是模型唯一的"选型依据"](#3.1 description 是模型唯一的"选型依据")
- [3.2 好的 description 的三个层次](#3.2 好的 description 的三个层次)
- [3.3 多工具场景下的 description 设计](#3.3 多工具场景下的 description 设计)
- [3.4 description 的反模式与修正](#3.4 description 的反模式与修正)
- [3.5 description 的测试与迭代](#3.5 description 的测试与迭代)
- 四、多工具调用:并行调用与串行调用的区别
-
- [4.1 从单工具到多工具](#4.1 从单工具到多工具)
- [4.2 并行调用](#4.2 并行调用)
- [4.3 串行调用](#4.3 串行调用)
- [4.4 并行 vs 串行的对比](#4.4 并行 vs 串行的对比)
- [4.5 并行调用的代码实现](#4.5 并行调用的代码实现)
- [4.6 串行调用的代码实现](#4.6 串行调用的代码实现)
- 五、错误处理全链路:参数校验→执行异常→超时→降级
-
- [5.1 为什么错误处理是 Function Calling 的命门](#5.1 为什么错误处理是 Function Calling 的命门)
- [5.2 错误处理的四个层次](#5.2 错误处理的四个层次)
- [5.3 第一层:参数校验](#5.3 第一层:参数校验)
- [5.4 第二层:执行异常处理](#5.4 第二层:执行异常处理)
- [5.5 第三层:超时控制](#5.5 第三层:超时控制)
- [5.6 第四层:降级策略](#5.6 第四层:降级策略)
- [5.7 错误信息回传给模型的最佳实践](#5.7 错误信息回传给模型的最佳实践)
- [5.8 错误监控与可观测性](#5.8 错误监控与可观测性)
- [六、实战:构建一个多工具 Agent(搜索+计算+数据库查询)](#六、实战:构建一个多工具 Agent(搜索+计算+数据库查询))
-
- [6.1 场景定义](#6.1 场景定义)
- [6.2 完整实现](#6.2 完整实现)
- [6.3 工具执行层](#6.3 工具执行层)
- [6.4 Agent 主循环](#6.4 Agent 主循环)
- [6.5 运行结果分析](#6.5 运行结果分析)
- 七、适用边界与风险提示
-
- [7.1 Function Calling 的适用场景](#7.1 Function Calling 的适用场景)
- [7.2 风险提示与安全考量](#7.2 风险提示与安全考量)
- [7.3 模型能力差异](#7.3 模型能力差异)
- [7.4 成本考量](#7.4 成本考量)
- [7.5 性能优化建议](#7.5 性能优化建议)
- 八、总结
- 参考资料
一、Function Calling 的本质:从"模型说话"到"模型做事"
1.1 没有 Function Calling 的世界
在 Function Calling 出现之前,大语言模型(LLM)的本质是一个文本生成器------你给它一段文本,它返回一段文本。模型无法查询数据库、无法调用 API、无法执行计算,它只能"说",不能"做"。
这意味着,如果你问模型"今天北京天气怎么样",它只能基于训练数据中的知识"猜"一个答案,而无法获取实时数据。这种局限性可以用一个简单的图来理解:
#mermaid-svg-GFRGqFi4wx7J1p19{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GFRGqFi4wx7J1p19 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GFRGqFi4wx7J1p19 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GFRGqFi4wx7J1p19 .error-icon{fill:#552222;}#mermaid-svg-GFRGqFi4wx7J1p19 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GFRGqFi4wx7J1p19 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GFRGqFi4wx7J1p19 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GFRGqFi4wx7J1p19 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GFRGqFi4wx7J1p19 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GFRGqFi4wx7J1p19 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GFRGqFi4wx7J1p19 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GFRGqFi4wx7J1p19 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GFRGqFi4wx7J1p19 .marker.cross{stroke:#333333;}#mermaid-svg-GFRGqFi4wx7J1p19 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GFRGqFi4wx7J1p19 p{margin:0;}#mermaid-svg-GFRGqFi4wx7J1p19 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GFRGqFi4wx7J1p19 .cluster-label text{fill:#333;}#mermaid-svg-GFRGqFi4wx7J1p19 .cluster-label span{color:#333;}#mermaid-svg-GFRGqFi4wx7J1p19 .cluster-label span p{background-color:transparent;}#mermaid-svg-GFRGqFi4wx7J1p19 .label text,#mermaid-svg-GFRGqFi4wx7J1p19 span{fill:#333;color:#333;}#mermaid-svg-GFRGqFi4wx7J1p19 .node rect,#mermaid-svg-GFRGqFi4wx7J1p19 .node circle,#mermaid-svg-GFRGqFi4wx7J1p19 .node ellipse,#mermaid-svg-GFRGqFi4wx7J1p19 .node polygon,#mermaid-svg-GFRGqFi4wx7J1p19 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GFRGqFi4wx7J1p19 .rough-node .label text,#mermaid-svg-GFRGqFi4wx7J1p19 .node .label text,#mermaid-svg-GFRGqFi4wx7J1p19 .image-shape .label,#mermaid-svg-GFRGqFi4wx7J1p19 .icon-shape .label{text-anchor:middle;}#mermaid-svg-GFRGqFi4wx7J1p19 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GFRGqFi4wx7J1p19 .rough-node .label,#mermaid-svg-GFRGqFi4wx7J1p19 .node .label,#mermaid-svg-GFRGqFi4wx7J1p19 .image-shape .label,#mermaid-svg-GFRGqFi4wx7J1p19 .icon-shape .label{text-align:center;}#mermaid-svg-GFRGqFi4wx7J1p19 .node.clickable{cursor:pointer;}#mermaid-svg-GFRGqFi4wx7J1p19 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GFRGqFi4wx7J1p19 .arrowheadPath{fill:#333333;}#mermaid-svg-GFRGqFi4wx7J1p19 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GFRGqFi4wx7J1p19 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GFRGqFi4wx7J1p19 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GFRGqFi4wx7J1p19 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GFRGqFi4wx7J1p19 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GFRGqFi4wx7J1p19 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GFRGqFi4wx7J1p19 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GFRGqFi4wx7J1p19 .cluster text{fill:#333;}#mermaid-svg-GFRGqFi4wx7J1p19 .cluster span{color:#333;}#mermaid-svg-GFRGqFi4wx7J1p19 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GFRGqFi4wx7J1p19 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GFRGqFi4wx7J1p19 rect.text{fill:none;stroke-width:0;}#mermaid-svg-GFRGqFi4wx7J1p19 .icon-shape,#mermaid-svg-GFRGqFi4wx7J1p19 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GFRGqFi4wx7J1p19 .icon-shape p,#mermaid-svg-GFRGqFi4wx7J1p19 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GFRGqFi4wx7J1p19 .icon-shape .label rect,#mermaid-svg-GFRGqFi4wx7J1p19 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GFRGqFi4wx7J1p19 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GFRGqFi4wx7J1p19 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GFRGqFi4wx7J1p19 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户提问
LLM 处理
文本回复
❌ 无法访问外部数据
❌ 无法执行操作
❌ 无法获取实时信息
传统 LLM 的输出空间被限制在"训练数据中见过的文本"范围内。它不知道今天的天气、不知道最新的股价、无法帮你预订机票、无法查询你的订单状态。
1.2 Function Calling 带来了什么
Function Calling 的核心思想很简单:让模型学会"调用工具"。模型不再只是生成文本,而是能够识别出"这个问题我需要借助外部工具来回答",然后生成一个结构化的函数调用请求,由外部系统执行后返回结果,模型再基于结果给出最终回答。
这个过程可以用下面的流程图来描述:
#mermaid-svg-JjMI490X78UX9Ag8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JjMI490X78UX9Ag8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JjMI490X78UX9Ag8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JjMI490X78UX9Ag8 .error-icon{fill:#552222;}#mermaid-svg-JjMI490X78UX9Ag8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-JjMI490X78UX9Ag8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JjMI490X78UX9Ag8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JjMI490X78UX9Ag8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JjMI490X78UX9Ag8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JjMI490X78UX9Ag8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JjMI490X78UX9Ag8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JjMI490X78UX9Ag8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-JjMI490X78UX9Ag8 .marker.cross{stroke:#333333;}#mermaid-svg-JjMI490X78UX9Ag8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-JjMI490X78UX9Ag8 p{margin:0;}#mermaid-svg-JjMI490X78UX9Ag8 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-JjMI490X78UX9Ag8 .cluster-label text{fill:#333;}#mermaid-svg-JjMI490X78UX9Ag8 .cluster-label span{color:#333;}#mermaid-svg-JjMI490X78UX9Ag8 .cluster-label span p{background-color:transparent;}#mermaid-svg-JjMI490X78UX9Ag8 .label text,#mermaid-svg-JjMI490X78UX9Ag8 span{fill:#333;color:#333;}#mermaid-svg-JjMI490X78UX9Ag8 .node rect,#mermaid-svg-JjMI490X78UX9Ag8 .node circle,#mermaid-svg-JjMI490X78UX9Ag8 .node ellipse,#mermaid-svg-JjMI490X78UX9Ag8 .node polygon,#mermaid-svg-JjMI490X78UX9Ag8 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-JjMI490X78UX9Ag8 .rough-node .label text,#mermaid-svg-JjMI490X78UX9Ag8 .node .label text,#mermaid-svg-JjMI490X78UX9Ag8 .image-shape .label,#mermaid-svg-JjMI490X78UX9Ag8 .icon-shape .label{text-anchor:middle;}#mermaid-svg-JjMI490X78UX9Ag8 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-JjMI490X78UX9Ag8 .rough-node .label,#mermaid-svg-JjMI490X78UX9Ag8 .node .label,#mermaid-svg-JjMI490X78UX9Ag8 .image-shape .label,#mermaid-svg-JjMI490X78UX9Ag8 .icon-shape .label{text-align:center;}#mermaid-svg-JjMI490X78UX9Ag8 .node.clickable{cursor:pointer;}#mermaid-svg-JjMI490X78UX9Ag8 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-JjMI490X78UX9Ag8 .arrowheadPath{fill:#333333;}#mermaid-svg-JjMI490X78UX9Ag8 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-JjMI490X78UX9Ag8 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-JjMI490X78UX9Ag8 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JjMI490X78UX9Ag8 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-JjMI490X78UX9Ag8 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JjMI490X78UX9Ag8 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-JjMI490X78UX9Ag8 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-JjMI490X78UX9Ag8 .cluster text{fill:#333;}#mermaid-svg-JjMI490X78UX9Ag8 .cluster span{color:#333;}#mermaid-svg-JjMI490X78UX9Ag8 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-JjMI490X78UX9Ag8 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-JjMI490X78UX9Ag8 rect.text{fill:none;stroke-width:0;}#mermaid-svg-JjMI490X78UX9Ag8 .icon-shape,#mermaid-svg-JjMI490X78UX9Ag8 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-JjMI490X78UX9Ag8 .icon-shape p,#mermaid-svg-JjMI490X78UX9Ag8 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-JjMI490X78UX9Ag8 .icon-shape .label rect,#mermaid-svg-JjMI490X78UX9Ag8 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-JjMI490X78UX9Ag8 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-JjMI490X78UX9Ag8 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-JjMI490X78UX9Ag8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 工具执行层
LLM处理
用户侧
JSON 调用请求
JSON 返回结果
用户输入: 北京今天天气如何?
理解意图
选择工具: get_weather
生成参数: city=北京, date=today
接收工具返回结果
基于结果生成自然语言回复
get_weather 函数
调用天气 API
返回 JSON 结果
回复: 北京今天晴,气温25°C...
关键点在于:模型并不直接执行函数,它只是生成一个函数调用的描述(通常是 JSON 格式),然后由外部的运行时(Runtime)来执行实际的函数调用。这种设计确保了安全性------模型始终没有直接的代码执行权限。
1.3 Function Calling 的核心价值
Function Calling 之所以重要,不仅在于它让模型能够"做事",更在于它建立了一套标准化的模型-工具交互协议。在 Function Calling 出现之前,开发者要让 LLM 调用外部工具,通常需要依赖 Prompt Engineering------在系统提示中描述可用工具,然后解析模型的文本输出来提取工具调用意图。这种方式脆弱、不可靠、且难以标准化。Function Calling 将这个过程从"提示词技巧"提升为"API 级协议",带来了质的飞跃。
| 能力维度 | 无 Function Calling | 有 Function Calling |
|---|---|---|
| 实时信息 | ❌ 依赖训练数据,知识有截止日期 | ✅ 可调用搜索 API、天气 API 等获取实时数据 |
| 精确计算 | ❌ 模型做数学题容易出错 | ✅ 可调用计算器函数,保证计算精度 |
| 数据查询 | ❌ 无法访问私有数据库 | ✅ 可调用数据库查询函数 |
| 操作执行 | ❌ 只能生成文本建议 | ✅ 可调用 API 执行操作(发邮件、建任务等) |
| 系统集成 | ❌ 与外部系统隔离 | ✅ 可与任意 API 系统集成 |
| 可靠性 | ❌ Prompt 解析输出,容易出错 | ✅ 结构化 JSON 输出,程序可靠解析 |
| 标准化 | ❌ 每个项目自己设计调用格式 | ✅ 统一的 JSON Schema 协议 |

图:Function Calling 前后能力对比,左侧为传统LLM能力边界,右侧为扩展后的能力范围
1.4 Function Calling 的工作机制详解
一次完整的 Function Calling 流程包含以下步骤:
- 工具注册 :在调用 LLM 时,将可用的函数定义(包括函数名、描述、参数 Schema)传入
tools参数 - 意图识别:LLM 接收用户输入后,分析用户意图,判断是否需要调用工具
- 函数选择:如果需要调用工具,LLM 从已注册的函数中选择最合适的一个(或多个)
- 参数生成:LLM 根据函数的参数 Schema,从用户输入中提取信息,生成符合 Schema 的参数 JSON
- 调用执行:外部运行时接收到 LLM 的函数调用请求,执行实际的函数
- 结果返回:将函数执行结果以 JSON 格式返回给 LLM
- 最终回复:LLM 基于函数返回结果,生成自然语言回复给用户
这个过程是一个两轮对话:第一轮用户提问 → 模型返回函数调用,第二轮把函数结果喂给模型 → 模型返回最终回复。理解这一点对于后续的错误处理设计至关重要。
1.5 从概念到工程:理解 Function Calling 的抽象层次
在实际工程中,Function Calling 涉及三个抽象层次,理解它们的边界有助于你设计更好的 Agent 架构:
#mermaid-svg-beYzLJBLHnLsKygN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-beYzLJBLHnLsKygN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-beYzLJBLHnLsKygN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-beYzLJBLHnLsKygN .error-icon{fill:#552222;}#mermaid-svg-beYzLJBLHnLsKygN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-beYzLJBLHnLsKygN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-beYzLJBLHnLsKygN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-beYzLJBLHnLsKygN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-beYzLJBLHnLsKygN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-beYzLJBLHnLsKygN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-beYzLJBLHnLsKygN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-beYzLJBLHnLsKygN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-beYzLJBLHnLsKygN .marker.cross{stroke:#333333;}#mermaid-svg-beYzLJBLHnLsKygN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-beYzLJBLHnLsKygN p{margin:0;}#mermaid-svg-beYzLJBLHnLsKygN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-beYzLJBLHnLsKygN .cluster-label text{fill:#333;}#mermaid-svg-beYzLJBLHnLsKygN .cluster-label span{color:#333;}#mermaid-svg-beYzLJBLHnLsKygN .cluster-label span p{background-color:transparent;}#mermaid-svg-beYzLJBLHnLsKygN .label text,#mermaid-svg-beYzLJBLHnLsKygN span{fill:#333;color:#333;}#mermaid-svg-beYzLJBLHnLsKygN .node rect,#mermaid-svg-beYzLJBLHnLsKygN .node circle,#mermaid-svg-beYzLJBLHnLsKygN .node ellipse,#mermaid-svg-beYzLJBLHnLsKygN .node polygon,#mermaid-svg-beYzLJBLHnLsKygN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-beYzLJBLHnLsKygN .rough-node .label text,#mermaid-svg-beYzLJBLHnLsKygN .node .label text,#mermaid-svg-beYzLJBLHnLsKygN .image-shape .label,#mermaid-svg-beYzLJBLHnLsKygN .icon-shape .label{text-anchor:middle;}#mermaid-svg-beYzLJBLHnLsKygN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-beYzLJBLHnLsKygN .rough-node .label,#mermaid-svg-beYzLJBLHnLsKygN .node .label,#mermaid-svg-beYzLJBLHnLsKygN .image-shape .label,#mermaid-svg-beYzLJBLHnLsKygN .icon-shape .label{text-align:center;}#mermaid-svg-beYzLJBLHnLsKygN .node.clickable{cursor:pointer;}#mermaid-svg-beYzLJBLHnLsKygN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-beYzLJBLHnLsKygN .arrowheadPath{fill:#333333;}#mermaid-svg-beYzLJBLHnLsKygN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-beYzLJBLHnLsKygN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-beYzLJBLHnLsKygN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-beYzLJBLHnLsKygN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-beYzLJBLHnLsKygN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-beYzLJBLHnLsKygN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-beYzLJBLHnLsKygN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-beYzLJBLHnLsKygN .cluster text{fill:#333;}#mermaid-svg-beYzLJBLHnLsKygN .cluster span{color:#333;}#mermaid-svg-beYzLJBLHnLsKygN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-beYzLJBLHnLsKygN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-beYzLJBLHnLsKygN rect.text{fill:none;stroke-width:0;}#mermaid-svg-beYzLJBLHnLsKygN .icon-shape,#mermaid-svg-beYzLJBLHnLsKygN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-beYzLJBLHnLsKygN .icon-shape p,#mermaid-svg-beYzLJBLHnLsKygN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-beYzLJBLHnLsKygN .icon-shape .label rect,#mermaid-svg-beYzLJBLHnLsKygN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-beYzLJBLHnLsKygN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-beYzLJBLHnLsKygN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-beYzLJBLHnLsKygN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 执行层
协议层
模型层
生成 tool_calls
传递调用请求
返回执行结果
喂回 tool result
LLM 推理引擎
意图识别
工具选择
参数生成
JSON Schema 契约
tool definitions
tool_calls 响应格式
tool result 消息格式
Runtime 运行时
参数校验
函数执行
错误处理
结果返回
- 模型层:LLM 负责"思考"------理解用户意图、选择合适工具、生成正确参数。这一层的质量取决于模型能力和工具描述的清晰度。
- 协议层:JSON Schema 定义了模型与执行层之间的契约。这一层是 Function Calling 的"语言",确保双方的期望一致。
- 执行层:Runtime 负责"行动"------校验参数、执行函数、处理异常、返回结果。这一层的质量取决于工程实现的健壮性。
工程师最常见的错误是混淆这三层。例如,试图通过优化 Prompt 来解决参数校验问题(应该在执行层解决),或试图通过代码逻辑来弥补工具描述的模糊(应该在协议层解决)。每层各司其职,问题才能被精准定位和解决。
二、参数定义规范:JSON Schema 设计与最佳实践
2.1 为什么参数定义如此重要
Function Calling 中,参数定义(JSON Schema)是模型与外部系统之间的契约。模型依赖这份契约来理解"这个函数需要什么参数、参数是什么格式",而外部系统依赖这份契约来校验模型生成的参数是否合法。
一个设计糟糕的参数 Schema 会导致:
- 模型生成的参数格式不正确,函数执行失败
- 模型无法从用户输入中准确提取参数值
- 模型在多个相似函数之间做出错误选择
- 参数校验逻辑复杂,维护成本高
2.2 JSON Schema 基础结构
OpenAI Function Calling 使用 JSON Schema 来定义函数参数。一个完整的函数定义包含以下字段:
python
# 一个完整的函数定义示例
{
"type": "function",
"function": {
"name": "search_products",
"description": "在商品数据库中搜索商品。支持按名称、类别、价格范围筛选。返回匹配的商品列表,包含名称、价格、库存信息。",
"parameters": {
"type": "object",
"properties": {
"keyword": {
"type": "string",
"description": "搜索关键词,用于匹配商品名称。例如:'iPhone' 或 '蓝牙耳机'。"
},
"category": {
"type": "string",
"enum": ["electronics", "clothing", "food", "books", "home"],
"description": "商品类别。可选值:electronics(电子产品)、clothing(服装)、food(食品)、books(图书)、home(家居)。"
},
"min_price": {
"type": "number",
"description": "价格范围下限(单位:元)。例如:100 表示只搜索价格≥100元的商品。"
},
"max_price": {
"type": "number",
"description": "价格范围上限(单位:元)。例如:5000 表示只搜索价格≤5000元的商品。"
},
"in_stock_only": {
"type": "boolean",
"description": "是否只搜索有库存的商品。true 表示只返回有货商品,false 表示也返回缺货商品。默认为 false。"
}
},
"required": ["keyword"],
"additionalProperties": False
}
}
}
这段代码定义了一个 search_products 函数的完整 Schema。让我逐字段解释其设计要点:
name:函数名使用snake_case命名,语义清晰,避免与内置函数冲突description:不仅说明"做什么",还说明了"返回什么",这对模型决策至关重要properties中每个参数都有description,包含示例值和使用说明enum限定category的取值范围,防止模型生成无效类别required只包含keyword,其他参数是可选的------这降低了模型生成参数的难度additionalProperties: false防止模型生成多余参数
2.3 参数类型选择指南
选择正确的参数类型是 Schema 设计的基础。以下是各类型的最佳实践:
| 参数类型 | 适用场景 | 设计建议 | 常见错误 |
|---|---|---|---|
string |
文本类参数 | 在 description 中给出格式示例 |
不给 enum 限制自由文本,导致模型随意生成 |
number / integer |
数值类参数 | 在 description 中注明单位和取值范围 |
混用 number 和 integer,如价格应为 number 而非 integer |
boolean |
开关类参数 | 默认值在 description 中说明 | 不说明默认值,模型不确定是否需要传该参数 |
string + enum |
枚举类参数 | enum 值附带中文说明 | 只给英文 enum 值,模型不理解含义 |
array |
列表类参数 | 指定 items 的类型和约束 |
不限制数组长度,模型可能生成超长数组 |
object |
复杂嵌套参数 | 嵌套层数不超过 2 层 | 深层嵌套导致模型生成参数困难 |
2.4 高级 Schema 设计技巧
python
# 高级 Schema 设计:使用 anyOf、约束、默认值描述
{
"type": "function",
"function": {
"name": "schedule_meeting",
"description": "安排一个会议。需要指定参会人员、时间和会议主题。支持重复会议设置。",
"parameters": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "会议主题。不超过50个字符。例如:'Q3 产品评审会'。"
},
"participants": {
"type": "array",
"items": {
"type": "string",
"description": "参会人员邮箱地址,例如:'zhangsan@example.com'"
},
"description": "参会人员邮箱列表。至少1人,最多20人。",
"minItems": 1,
"maxItems": 20
},
"start_time": {
"type": "string",
"description": "会议开始时间,ISO 8601 格式。例如:'2024-12-25T14:00:00+08:00' 表示北京时间2024年12月25日下午2点。"
},
"duration_minutes": {
"type": "integer",
"description": "会议时长(分钟)。建议值:30、60、90、120。默认60分钟。",
"minimum": 15,
"maximum": 480
},
"recurrence": {
"type": "object",
"properties": {
"frequency": {
"type": "string",
"enum": ["daily", "weekly", "monthly"],
"description": "重复频率:daily(每天)、weekly(每周)、monthly(每月)。"
},
"count": {
"type": "integer",
"description": "重复次数。例如:5 表示重复5次。",
"minimum": 1,
"maximum": 52
}
},
"required": ["frequency"],
"description": "重复会议设置。不传则表示一次性会议。"
}
},
"required": ["title", "participants", "start_time"],
"additionalProperties": False
}
}
}
这个 schedule_meeting 函数的 Schema 设计体现了几个高级技巧:
minItems/maxItems约束参会人数,防止模型生成过多或过少的参与者minimum/maximum限制时长范围,15分钟到8小时覆盖了大多数会议场景- 嵌套对象
recurrence只嵌套了一层,保证了模型生成的可靠性 description中给出格式示例(如 ISO 8601 格式),引导模型生成正确格式required只包含核心字段,可选参数通过 description 中的"默认"说明来引导
2.5 参数定义的常见反模式
| 反模式 | 问题描述 | 改进方案 |
|---|---|---|
| ❌ 参数过多 | 一个函数定义了 10+ 个参数,模型难以准确生成 | 拆分为多个函数,或使用 anyOf 分组 |
| ❌ description 为空 | "description": "" 或过于简略如 "description": "查询" |
至少 30 字,包含用途、格式示例、取值范围 |
| ❌ 深层嵌套 | 参数对象嵌套 3 层以上 | 扁平化或拆分为多个函数 |
| ❌ 无 enum 限制 | type: string 不加 enum,模型随意生成 |
枚举值明确的必须加 enum |
| ❌ required 过多 | 所有参数都设为 required | 只保留真正必须的参数,降低模型生成难度 |
| ❌ 类型模糊 | 用 string 表示日期/时间/URL |
在 description 中明确格式要求,如 ISO 8601 |
| ❌ 缺少约束 | 数值参数不设 minimum/maximum | 加上范围限制,防止模型生成极端值 |
2.6 参数 Schema 设计的工程原则
在实际项目中,参数 Schema 设计应该遵循以下工程原则:
原则一:最小必要参数
只定义模型必须生成的参数,能从上下文推断的参数应该由 Runtime 填充。例如,用户身份信息应该从认证 Token 中提取,而不是让模型生成。这既减少了模型出错的概率,也提高了安全性。
原则二:宽容度优先
在不影响功能的前提下,尽可能降低参数的"严格度"。例如,日期参数允许 "2024-12-25" 和 "2024/12/25" 两种格式,由 Runtime 做格式归一化。模型生成的参数可能不完全符合预期格式,宽容的校验策略能提高系统的鲁棒性。
原则三:示例驱动
在 description 中给出具体的参数示例值,比抽象的格式描述更有效。模型从示例中学习的效率远高于从规则描述中学习。一个好的示例胜过十行格式说明。
原则四:可测试性
每个函数的 Schema 都应该可以独立测试。设计一个"黄金参数集"------一组已知正确的参数和一组已知错误的参数,用于回归测试。当 Schema 修改时,用黄金参数集验证不会引入回归问题。

图:JSON Schema 参数定义的层次结构,展示 properties、required、enum 等关键字段关系
三、工具描述的艺术:description 字段如何决定 Agent 决策质量
3.1 description 是模型唯一的"选型依据"
在 Function Calling 体系中,模型选择哪个工具(或是否选择工具)主要依赖两个信息源:用户输入和工具的 description 字段。参数的 Schema 只在模型决定调用该工具后才发挥作用,而 description 是模型决策阶段唯一的判断依据。
这意味着:如果两个工具的 description 区分度不够,模型就会混淆它们;如果 description 太模糊,模型就会在不该调用工具时调用工具。
3.2 好的 description 的三个层次
一个高质量的 description 应该包含三个层次的信息:
#mermaid-svg-MuhnjiEfgwYZAiaD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MuhnjiEfgwYZAiaD .error-icon{fill:#552222;}#mermaid-svg-MuhnjiEfgwYZAiaD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MuhnjiEfgwYZAiaD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MuhnjiEfgwYZAiaD .marker.cross{stroke:#333333;}#mermaid-svg-MuhnjiEfgwYZAiaD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MuhnjiEfgwYZAiaD p{margin:0;}#mermaid-svg-MuhnjiEfgwYZAiaD .edge{stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .section--1 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section--1 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section--1 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section--1 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section--1 text{fill:#ffffff;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth--1{stroke-width:17;}#mermaid-svg-MuhnjiEfgwYZAiaD .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-0 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-0 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-0 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-0 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-0 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-0{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-0{stroke-width:14;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-1 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-1 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-1 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-1 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-1 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-1{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-1{stroke-width:11;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-2 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-2 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-2 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-2 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-2 text{fill:#ffffff;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-2{stroke-width:8;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-3 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-3 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-3 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-3 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-3 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-3{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-3{stroke-width:5;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-4 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-4 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-4 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-4 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-4 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-4{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-4{stroke-width:2;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-5 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-5 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-5 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-5 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-5 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-5{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-5{stroke-width:-1;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-6 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-6 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-6 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-6 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-6 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-6{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-6{stroke-width:-4;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-7 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-7 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-7 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-7 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-7 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-7{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-7{stroke-width:-7;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-8 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-8 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-8 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-8 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-8 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-8{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-8{stroke-width:-10;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-9 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-9 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-9 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-9 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-9 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-9{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-9{stroke-width:-13;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-10 rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-10 path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-10 circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-10 polygon,#mermaid-svg-MuhnjiEfgwYZAiaD .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-10 text{fill:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .node-icon-10{font-size:40px;color:black;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .edge-depth-10{stroke-width:-16;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled circle,#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:lightgray;}#mermaid-svg-MuhnjiEfgwYZAiaD .disabled text{fill:#efefef;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-root rect,#mermaid-svg-MuhnjiEfgwYZAiaD .section-root path,#mermaid-svg-MuhnjiEfgwYZAiaD .section-root circle,#mermaid-svg-MuhnjiEfgwYZAiaD .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-MuhnjiEfgwYZAiaD .section-root text{fill:#ffffff;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-root span{color:#ffffff;}#mermaid-svg-MuhnjiEfgwYZAiaD .section-2 span{color:#ffffff;}#mermaid-svg-MuhnjiEfgwYZAiaD .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-MuhnjiEfgwYZAiaD .edge{fill:none;}#mermaid-svg-MuhnjiEfgwYZAiaD .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-MuhnjiEfgwYZAiaD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} description 设计
第一层: 功能定义
这个函数做什么
输入什么
返回什么
第二层: 使用边界
什么时候该用
什么时候不该用
与其他函数的区别
第三层: 参数提示
关键参数的格式
特殊约束
示例值
这三层信息从内到外,逐步为模型提供决策支持。下面通过对比来感受 description 质量的差异:
python
# ❌ 糟糕的 description
{
"name": "get_data",
"description": "获取数据",
"parameters": { ... }
}
# ❌ 稍好但不够的 description
{
"name": "get_weather",
"description": "获取天气信息",
"parameters": { ... }
}
# ✅ 高质量的 description
{
"name": "get_weather",
"description": "查询指定城市在指定日期的天气信息。返回温度、天气状况、湿度、风力等数据。当用户询问天气、气温、是否下雨、穿衣建议等问题时使用此函数。不适用于查询历史天气数据(请使用 get_historical_weather)。",
"parameters": { ... }
}
三个版本的区别在于:第一个版本没有说明获取什么数据、需要什么参数;第二个版本说明了功能但没有区分边界;第三个版本清晰定义了功能、使用场景、返回内容,并明确指出了与另一个函数的区分边界。
3.3 多工具场景下的 description 设计
当 Agent 有多个工具可用时,description 的设计需要特别注意消歧------确保模型能在多个相似工具之间做出正确选择。来看一个实际案例:
python
# 多工具场景:一个数据分析 Agent 的工具集
TOOLS = [
{
"name": "query_sql_database",
"description": "执行 SQL 查询语句,从关系型数据库中获取结构化数据。适用于精确的数据查询、聚合统计、多表关联等场景。返回 JSON 格式的查询结果。当用户需要查询具体业务数据(如订单、用户、产品)或进行数据统计时使用。",
"parameters": { ... }
},
{
"name": "search_documents",
"description": "在文档知识库中进行语义搜索,基于向量相似度匹配相关文档。适用于查找文档内容、FAQ、知识库条目等非结构化文本。返回最相关的文档片段及相似度分数。当用户查找操作指南、政策文档、技术资料等文本内容时使用。",
"parameters": { ... }
},
{
"name": "search_web",
"description": "在互联网上搜索实时信息。适用于获取新闻、实时数据、最新动态等训练数据中可能不包含的信息。返回搜索结果摘要和链接。当用户询问最新发生的事件、实时数据或模型知识截止日期之后的信息时使用。",
"parameters": { ... }
},
{
"name": "calculate",
"description": "执行数学计算表达式并返回精确结果。支持四则运算、指数、对数、三角函数等。适用于需要精确数值计算的场景,避免 LLM 自身计算误差。当用户要求精确计算、数据统计运算、单位转换时使用。",
"parameters": { ... }
}
]
这四个工具的 description 设计有几个关键点:
- 每个 description 都说明了"适用场景":模型能根据用户意图判断该用哪个工具
- 明确了数据来源差异:SQL 数据库是结构化业务数据,文档搜索是非结构化文本,Web 搜索是互联网实时信息
- 说明了返回格式:帮助模型理解工具返回结果的类型和结构
- 区分边界清晰 :
query_sql_database和search_documents不会被混淆,因为 description 明确区分了"结构化数据"和"非结构化文本"

图:Agent 多工具选择决策流程,展示从用户输入到工具选择的完整推理链路
3.4 description 的反模式与修正
| 反模式 | 示例 | 问题 | 修正 |
|---|---|---|---|
| 功能模糊 | "查询数据" | 无法判断查什么数据 | "查询订单数据,返回订单号、金额、状态" |
| 缺少边界 | "搜索信息" | 不知道何时该用 | "搜索互联网获取实时新闻和最新动态" |
| 无区分度 | "查询用户" 和 "搜索用户" | 模型无法区分 | "通过用户ID精确查询用户信息" vs "按关键词模糊搜索用户列表" |
| 过度详细 | 200字+的 description | 模型注意力分散,关键信息被淹没 | 控制在 50-100 字,突出功能和边界 |
| 无返回说明 | "查询天气" | 模型不知道返回什么 | "返回温度、湿度、天气状况、风力等数据" |
| 中英文混用 | "查询用户的 order 信息" | 术语不统一 | 统一使用中文或英文描述 |
3.5 description 的测试与迭代
description 的质量不是一次设计就能完美的,需要通过实际测试来验证。一个实用的测试方法是对比测试法:
- 准备一组真实的用户问题(至少 20 个)
- 让 Agent 在当前 description 下处理这些问题
- 记录模型选择的工具是否正确
- 如果工具选择错误,分析原因是 description 不够清晰还是区分度不够
- 修正 description 后重新测试
这个迭代过程通常需要 3-5 轮才能达到稳定的工具选择准确率。在实际项目中,建议将测试用例自动化,形成一个回归测试集,每次修改工具定义后自动运行。
四、多工具调用:并行调用与串行调用的区别
4.1 从单工具到多工具
实际工程中的 Agent 往往不会只有一个工具。一个数据分析 Agent 可能同时拥有数据库查询、文档搜索、计算器、图表生成等多个工具。当用户提出一个复杂问题时,可能需要调用多个工具才能完成回答。
这就引出了多工具调用的两种模式:并行调用 和串行调用。
4.2 并行调用
并行调用是指模型在一次推理中同时生成多个函数调用请求,这些调用之间没有依赖关系,可以同时执行。
汇率API 天气API Runtime LLM 用户 汇率API 天气API Runtime LLM 用户 #mermaid-svg-pWa9v4Ep3wzoeKeo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-pWa9v4Ep3wzoeKeo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-pWa9v4Ep3wzoeKeo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-pWa9v4Ep3wzoeKeo .error-icon{fill:#552222;}#mermaid-svg-pWa9v4Ep3wzoeKeo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-pWa9v4Ep3wzoeKeo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-pWa9v4Ep3wzoeKeo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-pWa9v4Ep3wzoeKeo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-pWa9v4Ep3wzoeKeo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-pWa9v4Ep3wzoeKeo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-pWa9v4Ep3wzoeKeo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-pWa9v4Ep3wzoeKeo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-pWa9v4Ep3wzoeKeo .marker.cross{stroke:#333333;}#mermaid-svg-pWa9v4Ep3wzoeKeo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-pWa9v4Ep3wzoeKeo p{margin:0;}#mermaid-svg-pWa9v4Ep3wzoeKeo .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-pWa9v4Ep3wzoeKeo text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-pWa9v4Ep3wzoeKeo .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-pWa9v4Ep3wzoeKeo .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-pWa9v4Ep3wzoeKeo .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-pWa9v4Ep3wzoeKeo .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-pWa9v4Ep3wzoeKeo #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-pWa9v4Ep3wzoeKeo .sequenceNumber{fill:white;}#mermaid-svg-pWa9v4Ep3wzoeKeo #sequencenumber{fill:#333;}#mermaid-svg-pWa9v4Ep3wzoeKeo #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-pWa9v4Ep3wzoeKeo .messageText{fill:#333;stroke:none;}#mermaid-svg-pWa9v4Ep3wzoeKeo .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-pWa9v4Ep3wzoeKeo .labelText,#mermaid-svg-pWa9v4Ep3wzoeKeo .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-pWa9v4Ep3wzoeKeo .loopText,#mermaid-svg-pWa9v4Ep3wzoeKeo .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-pWa9v4Ep3wzoeKeo .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-pWa9v4Ep3wzoeKeo .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-pWa9v4Ep3wzoeKeo .noteText,#mermaid-svg-pWa9v4Ep3wzoeKeo .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-pWa9v4Ep3wzoeKeo .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-pWa9v4Ep3wzoeKeo .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-pWa9v4Ep3wzoeKeo .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-pWa9v4Ep3wzoeKeo .actorPopupMenu{position:absolute;}#mermaid-svg-pWa9v4Ep3wzoeKeo .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-pWa9v4Ep3wzoeKeo .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-pWa9v4Ep3wzoeKeo .actor-man circle,#mermaid-svg-pWa9v4Ep3wzoeKeo line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-pWa9v4Ep3wzoeKeo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} par 并行执行 北京天气如何?美元汇率多少? 意图分析:需要两个独立信息 并行生成两个调用 get_weather(city=北京) get_exchange_rate(from=USD, to=CNY) 调用天气API {temp: 25, weather: 晴} 调用汇率API {rate: 7.24} 返回两个结果 北京今天晴,25°C。美元汇率7.24元。
并行调用的优势在于减少总延迟------两个 API 调用同时执行,总耗时约等于较慢的那个调用的时间,而非两者之和。
4.3 串行调用
串行调用是指模型需要先获取一个工具的结果,才能确定下一步该调用什么工具。这种模式通常出现在有依赖关系的场景中。
数据库 搜索API Runtime LLM 用户 数据库 搜索API Runtime LLM 用户 #mermaid-svg-XQutcyokp9LbxHtx{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-XQutcyokp9LbxHtx .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-XQutcyokp9LbxHtx .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-XQutcyokp9LbxHtx .error-icon{fill:#552222;}#mermaid-svg-XQutcyokp9LbxHtx .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-XQutcyokp9LbxHtx .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-XQutcyokp9LbxHtx .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-XQutcyokp9LbxHtx .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-XQutcyokp9LbxHtx .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-XQutcyokp9LbxHtx .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-XQutcyokp9LbxHtx .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-XQutcyokp9LbxHtx .marker{fill:#333333;stroke:#333333;}#mermaid-svg-XQutcyokp9LbxHtx .marker.cross{stroke:#333333;}#mermaid-svg-XQutcyokp9LbxHtx svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-XQutcyokp9LbxHtx p{margin:0;}#mermaid-svg-XQutcyokp9LbxHtx .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XQutcyokp9LbxHtx text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-XQutcyokp9LbxHtx .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-XQutcyokp9LbxHtx .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-XQutcyokp9LbxHtx .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-XQutcyokp9LbxHtx .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-XQutcyokp9LbxHtx #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-XQutcyokp9LbxHtx .sequenceNumber{fill:white;}#mermaid-svg-XQutcyokp9LbxHtx #sequencenumber{fill:#333;}#mermaid-svg-XQutcyokp9LbxHtx #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-XQutcyokp9LbxHtx .messageText{fill:#333;stroke:none;}#mermaid-svg-XQutcyokp9LbxHtx .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XQutcyokp9LbxHtx .labelText,#mermaid-svg-XQutcyokp9LbxHtx .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-XQutcyokp9LbxHtx .loopText,#mermaid-svg-XQutcyokp9LbxHtx .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-XQutcyokp9LbxHtx .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-XQutcyokp9LbxHtx .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-XQutcyokp9LbxHtx .noteText,#mermaid-svg-XQutcyokp9LbxHtx .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-XQutcyokp9LbxHtx .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XQutcyokp9LbxHtx .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XQutcyokp9LbxHtx .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-XQutcyokp9LbxHtx .actorPopupMenu{position:absolute;}#mermaid-svg-XQutcyokp9LbxHtx .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-XQutcyokp9LbxHtx .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-XQutcyokp9LbxHtx .actor-man circle,#mermaid-svg-XQutcyokp9LbxHtx line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-XQutcyokp9LbxHtx :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 查找张三最近的订单总金额 意图分析:先找用户ID,再查订单 第一步:search_user(name=张三) 搜索用户 {user_id: "U00123"} 返回用户信息 获取到user_id,准备查订单 第二步:query_orders(user_id=U00123) 查询订单 {orders: ..., total: 3850.00} 返回订单数据 张三最近订单总金额为3850.00元
串行调用的关键特征是第二步的参数依赖第一步的结果------必须先知道用户ID才能查询订单。模型在第一步调用时无法预知用户ID是什么,所以必须等待第一步完成后再决定第二步的参数。
4.4 并行 vs 串行的对比
| 维度 | 并行调用 | 串行调用 |
|---|---|---|
| 延迟 | 低(取最大值) | 高(取累加值) |
| 适用场景 | 多个独立信息需求 | 有依赖关系的数据获取 |
| API 调用次数 | 1 次推理 + N 次工具执行 | N 次推理 + N 次工具执行 |
| Token 消耗 | 较少(1 次推理) | 较多(N 次推理) |
| 模型要求 | 模型需支持 parallel_tool_calls | 所有支持 Function Calling 的模型均可用 |
| 错误影响 | 一个工具失败不影响其他 | 一个工具失败可能中断整条链路 |
| 典型场景 | "北京天气和上海天气" | "查用户再查订单" |
4.5 并行调用的代码实现
python
import openai
import json
client = openai.OpenAI(api_key="your-api-key")
# 定义两个工具
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "查询指定城市的天气信息。返回温度、天气状况、湿度等数据。",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "城市名称,例如:北京、上海。"
}
},
"required": ["city"],
"additionalProperties": False
}
}
},
{
"type": "function",
"function": {
"name": "get_exchange_rate",
"description": "查询两种货币之间的汇率。返回当前实时汇率。",
"parameters": {
"type": "object",
"properties": {
"from_currency": {
"type": "string",
"description": "源货币代码,例如:USD、EUR。"
},
"to_currency": {
"type": "string",
"description": "目标货币代码,例如:CNY、JPY。"
}
},
"required": ["from_currency", "to_currency"],
"additionalProperties": False
}
}
}
]
# 用户提问(包含两个独立的信息需求)
response = client.chat.completions.create(
model="gpt-4o",
messages=[
{"role": "user", "content": "北京天气怎么样?美元兑人民币汇率多少?"}
],
tools=tools,
parallel_tool_calls=True # 显式启用并行调用
)
# 模型会返回两个 tool_calls
print(f"工具调用数量: {len(response.choices[0].message.tool_calls)}")
for tc in response.choices[0].message.tool_calls:
print(f"函数名: {tc.function.name}, 参数: {tc.function.arguments}")
# 输出示例:
# 工具调用数量: 2
# 函数名: get_weather, 参数: {"city": "北京"}
# 函数名: get_exchange_rate, 参数: {"from_currency": "USD", "to_currency": "CNY"}
这段代码展示了并行调用的完整流程。parallel_tool_calls=True 是 OpenAI API 中启用并行调用的参数(默认为 True)。模型在一次推理中识别出两个独立的信息需求------天气和汇率------并生成了两个 tool_calls。Runtime 可以同时执行这两个调用,不需要等待其中一个完成再执行另一个。
4.6 串行调用的代码实现
python
import openai
import json
client = openai.OpenAI(api_key="your-api-key")
# 工具定义(省略完整 Schema,仅展示结构)
tools = [
{
"type": "function",
"function": {
"name": "search_user",
"description": "按姓名搜索用户,返回用户ID和基本信息。",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "用户姓名,如'张三'"}
},
"required": ["name"],
"additionalProperties": False
}
}
},
{
"type": "function",
"function": {
"name": "query_orders",
"description": "查询指定用户的订单列表,返回订单详情和总金额。",
"parameters": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "用户ID,如'U00123'"}
},
"required": ["user_id"],
"additionalProperties": False
}
}
}
]
# 模拟工具执行函数
def execute_function(name, arguments):
if name == "search_user":
# 模拟数据库查询
return {"user_id": "U00123", "name": "张三", "email": "zhangsan@example.com"}
elif name == "query_orders":
return {"orders": [
{"order_id": "ORD-001", "amount": 1200.00, "date": "2024-12-01"},
{"order_id": "ORD-002", "amount": 850.00, "date": "2024-12-05"},
{"order_id": "ORD-003", "amount": 1800.00, "date": "2024-12-10"}
], "total": 3850.00}
# 串行调用循环
messages = [{"role": "user", "content": "查找张三最近的订单总金额"}]
round_count = 0
while round_count < 5: # 最大轮次限制
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools
)
msg = response.choices[0].message
messages.append(msg)
if not msg.tool_calls:
# 模型没有调用工具,说明已经得出最终回复
print(f"最终回复: {msg.content}")
break
# 执行每个工具调用,将结果加入对话
for tc in msg.tool_calls:
args = json.loads(tc.function.arguments)
result = execute_function(tc.function.name, args)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False)
})
print(f"第{round_count+1}轮: {tc.function.name}({args}) → {result}")
round_count += 1
串行调用的核心是循环执行:模型推理 → 生成工具调用 → 执行工具 → 将结果送回模型 → 模型基于结果继续推理。这个循环会一直持续,直到模型不再需要调用工具(生成了最终回复)或达到最大轮次限制。
注意代码中的 round_count < 5 限制------这是一个重要的安全措施,防止模型陷入无限循环。在实际生产中,建议设置为 3-10 轮,具体取决于你的 Agent 任务复杂度。
五、错误处理全链路:参数校验→执行异常→超时→降级
5.1 为什么错误处理是 Function Calling 的命门
在 Function Calling 体系中,模型生成的参数可能格式不对,工具执行可能抛出异常,API 可能超时,网络可能中断。如果你的 Agent 没有完善的错误处理机制,任何一个环节的失败都可能导致整个系统崩溃或给用户返回无意义的错误信息。
错误处理不是一个"锦上添花"的功能,而是 Function Calling 从 Demo 走向生产环境的必经之路。
5.2 错误处理的四个层次
#mermaid-svg-qeMBl4P0rr4VkEYg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-qeMBl4P0rr4VkEYg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qeMBl4P0rr4VkEYg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qeMBl4P0rr4VkEYg .error-icon{fill:#552222;}#mermaid-svg-qeMBl4P0rr4VkEYg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qeMBl4P0rr4VkEYg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qeMBl4P0rr4VkEYg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qeMBl4P0rr4VkEYg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qeMBl4P0rr4VkEYg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qeMBl4P0rr4VkEYg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qeMBl4P0rr4VkEYg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qeMBl4P0rr4VkEYg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qeMBl4P0rr4VkEYg .marker.cross{stroke:#333333;}#mermaid-svg-qeMBl4P0rr4VkEYg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qeMBl4P0rr4VkEYg p{margin:0;}#mermaid-svg-qeMBl4P0rr4VkEYg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-qeMBl4P0rr4VkEYg .cluster-label text{fill:#333;}#mermaid-svg-qeMBl4P0rr4VkEYg .cluster-label span{color:#333;}#mermaid-svg-qeMBl4P0rr4VkEYg .cluster-label span p{background-color:transparent;}#mermaid-svg-qeMBl4P0rr4VkEYg .label text,#mermaid-svg-qeMBl4P0rr4VkEYg span{fill:#333;color:#333;}#mermaid-svg-qeMBl4P0rr4VkEYg .node rect,#mermaid-svg-qeMBl4P0rr4VkEYg .node circle,#mermaid-svg-qeMBl4P0rr4VkEYg .node ellipse,#mermaid-svg-qeMBl4P0rr4VkEYg .node polygon,#mermaid-svg-qeMBl4P0rr4VkEYg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-qeMBl4P0rr4VkEYg .rough-node .label text,#mermaid-svg-qeMBl4P0rr4VkEYg .node .label text,#mermaid-svg-qeMBl4P0rr4VkEYg .image-shape .label,#mermaid-svg-qeMBl4P0rr4VkEYg .icon-shape .label{text-anchor:middle;}#mermaid-svg-qeMBl4P0rr4VkEYg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-qeMBl4P0rr4VkEYg .rough-node .label,#mermaid-svg-qeMBl4P0rr4VkEYg .node .label,#mermaid-svg-qeMBl4P0rr4VkEYg .image-shape .label,#mermaid-svg-qeMBl4P0rr4VkEYg .icon-shape .label{text-align:center;}#mermaid-svg-qeMBl4P0rr4VkEYg .node.clickable{cursor:pointer;}#mermaid-svg-qeMBl4P0rr4VkEYg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-qeMBl4P0rr4VkEYg .arrowheadPath{fill:#333333;}#mermaid-svg-qeMBl4P0rr4VkEYg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-qeMBl4P0rr4VkEYg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-qeMBl4P0rr4VkEYg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qeMBl4P0rr4VkEYg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-qeMBl4P0rr4VkEYg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qeMBl4P0rr4VkEYg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-qeMBl4P0rr4VkEYg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-qeMBl4P0rr4VkEYg .cluster text{fill:#333;}#mermaid-svg-qeMBl4P0rr4VkEYg .cluster span{color:#333;}#mermaid-svg-qeMBl4P0rr4VkEYg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-qeMBl4P0rr4VkEYg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-qeMBl4P0rr4VkEYg rect.text{fill:none;stroke-width:0;}#mermaid-svg-qeMBl4P0rr4VkEYg .icon-shape,#mermaid-svg-qeMBl4P0rr4VkEYg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-qeMBl4P0rr4VkEYg .icon-shape p,#mermaid-svg-qeMBl4P0rr4VkEYg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-qeMBl4P0rr4VkEYg .icon-shape .label rect,#mermaid-svg-qeMBl4P0rr4VkEYg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-qeMBl4P0rr4VkEYg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-qeMBl4P0rr4VkEYg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-qeMBl4P0rr4VkEYg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 校验失败
校验通过
执行异常
执行成功
超时
未超时
可降级
不可降级
正常
模型生成工具调用
第一层: 参数校验
返回错误信息给模型
第二层: 执行异常
捕获异常,返回错误描述
第三层: 超时检查
中断执行,返回超时错误
返回执行结果
第四层: 降级处理
使用备选方案或默认值
返回友好的错误提示
送回模型继续推理
结束
5.3 第一层:参数校验
模型生成的参数并不总是正确的。虽然 JSON Schema 提供了类型约束,但模型仍然可能生成不合规的参数值。参数校验是错误处理的第一道防线。
python
import json
from typing import Any, Optional
from pydantic import BaseModel, Field, ValidationError
# 使用 Pydantic 定义参数模型,实现运行时校验
class WeatherParams(BaseModel):
city: str = Field(..., min_length=1, max_length=50, description="城市名称")
date: Optional[str] = Field(None, pattern=r"^\d{4}-\d{2}-\d{2}$", description="日期,YYYY-MM-DD格式")
def validate_and_execute(tool_name: str, raw_arguments: str) -> dict:
"""
参数校验 + 执行函数。
返回 {"success": True, "data": ...} 或 {"success": False, "error": "..."}
"""
try:
# 第一步:解析 JSON
args = json.loads(raw_arguments)
except json.JSONDecodeError as e:
return {
"success": False,
"error": f"参数JSON解析失败: {str(e)}",
"error_type": "json_parse_error"
}
try:
# 第二步:基于工具名选择对应的参数模型进行校验
if tool_name == "get_weather":
params = WeatherParams(**args)
# 校验通过,执行函数
return execute_weather_query(params.city, params.date)
elif tool_name == "search_user":
params = UserSearchParams(**args)
return execute_user_search(params.name)
else:
return {
"success": False,
"error": f"未知工具: {tool_name}",
"error_type": "unknown_tool"
}
except ValidationError as e:
# 参数校验失败,返回详细的错误信息
return {
"success": False,
"error": f"参数校验失败: {e.errors()}",
"error_type": "validation_error",
# 关键:将错误信息返回给模型,让模型知道哪里出了问题
"suggestion": "请检查参数格式,确保 city 是非空字符串,date 是 YYYY-MM-DD 格式。"
}
except Exception as e:
# 其他未预期的异常
return {
"success": False,
"error": f"执行异常: {str(e)}",
"error_type": "execution_error"
}
这段代码实现了一个通用的参数校验+执行框架。核心设计点包括:
- 双层校验:先校验 JSON 格式是否正确,再用 Pydantic 模型校验参数值
- 结构化错误返回 :每种错误都有
error_type分类,便于后续处理和分析 suggestion字段:向模型提供修正建议,让模型在下一轮推理中可以自我修正- 统一的返回格式 :
{"success": True/False, ...},无论成功还是失败都返回统一结构
5.4 第二层:执行异常处理
即使参数校验通过了,工具执行过程中仍然可能抛出异常------数据库连接失败、API 返回错误、文件不存在等。执行异常处理需要做到捕获所有异常并转化为模型可理解的结构化错误信息。
python
import logging
import traceback
from functools import wraps
logger = logging.getLogger(__name__)
def tool_executor(func):
"""
工具执行装饰器:统一捕获异常,记录日志,返回结构化结果。
所有工具函数都应该用这个装饰器包装。
"""
@wraps(func)
def wrapper(*args, **kwargs):
try:
result = func(*args, **kwargs)
return {"success": True, "data": result}
except ConnectionError as e:
logger.error(f"工具 {func.__name__} 数据库连接失败: {e}")
return {
"success": False,
"error": f"数据库连接失败,请稍后重试",
"error_type": "connection_error",
"retryable": True # 标记为可重试
}
except TimeoutError as e:
logger.error(f"工具 {func.__name__} 执行超时: {e}")
return {
"success": False,
"error": f"查询超时,请缩小查询范围后重试",
"error_type": "timeout_error",
"retryable": True
}
except ValueError as e:
logger.warning(f"工具 {func.__name__} 参数值错误: {e}")
return {
"success": False,
"error": f"参数值无效: {str(e)}",
"error_type": "value_error",
"retryable": False # 参数错误重试也没用
}
except Exception as e:
logger.error(f"工具 {func.__name__} 未预期异常: {e}\n{traceback.format_exc()}")
return {
"success": False,
"error": f"内部错误,请联系管理员",
"error_type": "unexpected_error",
"retryable": False,
"internal_error": str(e) # 内部错误详情,不返回给用户
}
return wrapper
# 使用示例
@tool_executor
def query_database(sql: str, params: dict) -> list:
"""执行SQL查询"""
db = get_db_connection() # 可能抛出 ConnectionError
cursor = db.cursor()
cursor.execute(sql, params)
return cursor.fetchall()
这个 tool_executor 装饰器的设计要点:
- 按异常类型分类处理:连接错误、超时错误、参数错误、未预期异常分别有不同的处理策略
retryable字段:标记异常是否值得重试。连接超时可以重试,参数错误重试无意义- 双层错误信息:对外返回用户/模型可理解的信息,对内记录完整的堆栈日志
- 日志记录:所有异常都记录日志,便于事后排查
5.5 第三层:超时控制
工具执行超时是生产环境中最常见的问题之一。一个 API 调用正常 200ms 返回,但在网络抖动时可能 30 秒不返回。如果没有超时控制,Agent 会一直等待,用户体验极差。
python
import signal
import time
from contextlib import contextmanager
from typing import Generator
class ToolTimeoutError(Exception):
"""工具执行超时异常"""
pass
@contextmanager
def timeout_context(seconds: int):
"""
超时上下文管理器。
使用 signal 实现(仅适用于 Unix 系统)。
Windows 系统建议使用 threading.Timer 或 asyncio.wait_for。
"""
def signal_handler(signum, frame):
raise ToolTimeoutError(f"工具执行超时,超过 {seconds} 秒")
old_handler = signal.signal(signal.SIGALRM, signal_handler)
signal.alarm(seconds)
try:
yield
finally:
signal.alarm(0)
signal.signal(signal.SIGALRM, old_handler)
def execute_tool_with_timeout(tool_func, args: dict, timeout_seconds: int = 10):
"""
带超时控制的工具执行器。
Args:
tool_func: 工具函数
args: 参数字典
timeout_seconds: 超时秒数,默认10秒
Returns:
执行结果或错误信息
"""
start_time = time.time()
try:
with timeout_context(timeout_seconds):
result = tool_func(**args)
elapsed = time.time() - start_time
return {
"success": True,
"data": result,
"elapsed_ms": int(elapsed * 1000)
}
except ToolTimeoutError as e:
elapsed = time.time() - start_time
logger.warning(f"工具 {tool_func.__name__} 超时: {timeout_seconds}s, 实际: {elapsed:.1f}s")
return {
"success": False,
"error": f"工具执行超时({timeout_seconds}秒),请简化请求或稍后重试",
"error_type": "timeout",
"elapsed_ms": int(elapsed * 1000),
"retryable": True
}
# 工具超时配置表(不同工具设置不同的超时时间)
TOOL_TIMEOUTS = {
"get_weather": 5, # 天气查询,5秒足够
"query_database": 15, # 数据库查询,给宽松一点
"search_web": 10, # 网页搜索
"download_file": 60, # 文件下载,可能需要更长时间
"calculate": 3, # 计算器,3秒够了
}
超时控制代码的设计要点:
- 可配置的超时时间 :不同工具有不同的超时阈值,存储在
TOOL_TIMEOUTS字典中 - 记录实际耗时 :
elapsed_ms字段记录了实际执行时间,可用于性能监控 - 友好的错误提示:超时错误信息引导用户简化请求或稍后重试
- 跨平台考虑:代码注释中说明了 Windows 系统的替代方案
5.6 第四层:降级策略
当工具执行失败时,系统不应该直接报错给用户,而应该尝试降级策略------用备选方案或默认值来提供尽可能好的回答。
python
def execute_with_fallback(tool_name: str, args: dict, messages: list, client) -> dict:
"""
带降级策略的工具执行器。
降级策略:
1. 首次执行:正常调用工具
2. 首次失败 + 可重试:重试一次(带延迟)
3. 重试失败 / 不可重试:尝试备选工具
4. 备选工具也失败:返回错误,让模型用自身知识回答
"""
timeout = TOOL_TIMEOUTS.get(tool_name, 10)
# 第一步:首次执行
result = execute_tool_with_timeout(
TOOL_REGISTRY[tool_name], args, timeout
)
if result["success"]:
return result
logger.info(f"工具 {tool_name} 首次执行失败: {result.get('error_type')}")
# 第二步:如果可重试,等待1秒后重试一次
if result.get("retryable", False):
time.sleep(1)
logger.info(f"重试工具 {tool_name}...")
result = execute_tool_with_timeout(
TOOL_REGISTRY[tool_name], args, timeout
)
if result["success"]:
return result
# 第三步:尝试备选工具
fallback_tool = FALLBACK_MAP.get(tool_name)
if fallback_tool:
logger.info(f"尝试备选工具 {fallback_tool}...")
fallback_result = execute_tool_with_timeout(
TOOL_REGISTRY[fallback_tool], args,
TOOL_TIMEOUTS.get(fallback_tool, 10)
)
if fallback_result["success"]:
fallback_result["used_fallback"] = True
fallback_result["original_tool"] = tool_name
return fallback_result
# 第四步:所有尝试都失败,返回结构化错误
# 关键:将错误信息以模型可理解的方式返回,让模型用自身知识尝试回答
return {
"success": False,
"error": f"工具 {tool_name} 执行失败: {result.get('error', '未知错误')}",
"error_type": result.get("error_type", "unknown"),
"fallback_used": fallback_tool is not None,
"instruction": "工具调用失败,请基于你的知识尝试回答用户问题,并说明可能无法提供实时数据。"
}
# 工具备选映射表
FALLBACK_MAP = {
"get_weather": "get_weather_cached", # 实时天气失败 → 用缓存数据
"search_web": "search_local_docs", # 网页搜索失败 → 搜本地文档
"query_database": "search_cached_data", # 数据库查询失败 → 用缓存数据
}
降级策略的核心设计思路是逐级降级 :先重试,再换备选工具,最后让模型用自身知识兜底。instruction 字段是关键------它告诉模型"工具失败了,请用你的知识尝试回答",这样用户至少能得到一个有价值的回答,而不是一个冷冰冰的报错信息。
5.7 错误信息回传给模型的最佳实践
错误处理中最容易被忽略的一环是:错误信息如何回传给模型。当工具执行失败时,返回给模型的内容会直接影响模型下一步的决策。一个好的错误信息应该包含三个要素:
- 发生了什么错误:简洁明了的错误描述
- 为什么发生:可能的原因说明
- 下一步怎么办:建议模型的行动方向
python
# 错误信息回传的最佳实践
def format_error_for_model(tool_name: str, error_result: dict) -> str:
"""将错误结果格式化为模型可理解的消息"""
error_type = error_result.get("error_type", "unknown")
error_msg = error_result.get("error", "未知错误")
# 根据错误类型给出不同的建议
suggestions = {
"timeout": "工具执行超时,建议简化查询条件或减少数据范围后重试。",
"connection_error": "无法连接到数据源,建议基于已有知识回答用户,并说明可能无法提供最新数据。",
"validation_error": f"参数格式错误:{error_msg}。请检查参数格式后重试。",
"unknown_tool": f"工具 {tool_name} 不存在。请从可用工具列表中选择。",
"unexpected_error": "内部错误,建议直接基于已有知识回答用户。"
}
suggestion = suggestions.get(error_type, "请尝试其他方式回答用户问题。")
return json.dumps({
"status": "error",
"error_type": error_type,
"message": error_msg,
"suggestion": suggestion
}, ensure_ascii=False)
# 使用示例
# messages.append({
# "role": "tool",
# "tool_call_id": tc.id,
# "content": format_error_for_model(tc.function.name, error_result)
# })
这段代码将错误信息转化为结构化的 JSON,包含错误类型、错误消息和建议。suggestion 字段是核心------它直接告诉模型下一步应该怎么做。例如,超时错误建议"简化查询条件",连接错误建议"基于已有知识回答"。这种设计让模型在工具失败后能够做出合理的下一步决策,而不是陷入困惑。
5.8 错误监控与可观测性
在生产环境中,Function Calling 的错误处理不仅是"出了错怎么办",还应该包括"出了什么错、多久出一次、为什么出"。可观测性是生产级 Agent 系统的必备能力。
建议监控以下指标:
| 指标 | 说明 | 告警阈值建议 |
|---|---|---|
| 工具调用成功率 | 成功调用次数 / 总调用次数 | < 90% 时告警 |
| 平均执行耗时 | 每个工具的平均执行时间 | 超过 timeout 的 50% 时告警 |
| 错误类型分布 | 各 error_type 的占比 | 突然出现新的 error_type 时告警 |
| 重试次数 | 每个工具的平均重试次数 | > 0.5 时告警 |
| 降级触发率 | 触发降级策略的调用占比 | > 10% 时告警 |
| 模型推理轮次 | 平均每轮对话的推理轮次 | > 5 时告警(可能存在循环) |
这些指标可以帮助你发现隐藏的问题------例如,如果某个工具的成功率持续下降,可能是上游 API 不稳定;如果模型推理轮次突然增多,可能是 description 修改导致模型选型不准确。
六、实战:构建一个多工具 Agent(搜索+计算+数据库查询)
6.1 场景定义
现在让我们把前面的知识整合起来,构建一个完整的多工具 Agent。这个 Agent 具备三种能力:
- 网络搜索:获取实时信息
- 数学计算:执行精确计算
- 数据库查询:查询业务数据
用户可以提出综合性的问题,Agent 会自主选择合适的工具来完成任务。
6.2 完整实现
python
"""
多工具 Agent 完整实现:搜索 + 计算 + 数据库查询
依赖: pip install openai pydantic
"""
import openai
import json
import time
import logging
import sqlite3
from typing import Any, Optional
from pydantic import BaseModel, Field, ValidationError
from functools import wraps
from contextlib import contextmanager
# 配置
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
client = openai.OpenAI(api_key="your-api-key")
# ============================================================
# 1. 工具定义(JSON Schema)
# ============================================================
TOOLS = [
{
"type": "function",
"function": {
"name": "search_web",
"description": "在互联网上搜索信息,获取实时新闻、公开数据、技术文档等。返回搜索结果摘要和来源链接。当用户询问最新事件、实时数据或模型知识截止日期之后的信息时使用此工具。",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "搜索关键词,不超过100个字符。例如:'2024年诺贝尔物理学奖获得者'"
},
"num_results": {
"type": "integer",
"description": "返回结果数量,默认5条,最多10条。",
"minimum": 1,
"maximum": 10
}
},
"required": ["query"],
"additionalProperties": False
}
}
},
{
"type": "function",
"function": {
"name": "calculate",
"description": "执行数学计算并返回精确结果。支持四则运算、指数、对数、三角函数等。当用户需要精确数值计算、数据统计运算、单位转换时使用此工具,避免LLM自身计算误差。",
"parameters": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式字符串。例如:'2**10'、'sin(3.14159/6)'、'sum([1,2,3,4,5])'"
}
},
"required": ["expression"],
"additionalProperties": False
}
}
},
{
"type": "function",
"function": {
"name": "query_database",
"description": "查询业务数据库,获取订单、用户、产品等结构化业务数据。返回JSON格式的查询结果。当用户需要查询具体的业务数据、统计数据或数据关联分析时使用此工具。",
"parameters": {
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "SQL查询语句。仅支持SELECT查询,禁止DML/DDL操作。例如:'SELECT * FROM orders WHERE status = \"pending\" LIMIT 10'"
}
},
"required": ["sql"],
"additionalProperties": False
}
}
}
]
上面的代码定义了三个工具的完整 JSON Schema。注意三个工具的 description 字段设计:
search_web强调"实时信息"和"知识截止日期之后的信息",让模型知道何时该用搜索calculate强调"精确计算"和"避免LLM自身计算误差",让模型知道计算类问题要用工具而非自己算query_database强调"业务数据"和"结构化",与搜索工具的非结构化文本形成区分
6.3 工具执行层
python
# ============================================================
# 2. 工具执行层(含错误处理)
# ============================================================
# 初始化测试数据库
def init_test_db():
conn = sqlite3.connect(":memory:", check_same_thread=False)
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer TEXT NOT NULL,
amount REAL NOT NULL,
status TEXT NOT NULL,
created_at TEXT NOT NULL
)
""")
cursor.execute("""
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL,
city TEXT NOT NULL
)
""")
# 插入测试数据
cursor.executemany(
"INSERT INTO orders (customer, amount, status, created_at) VALUES (?,?,?,?)",
[("张三", 1200.00, "completed", "2024-12-01"),
("张三", 850.00, "completed", "2024-12-05"),
("张三", 1800.00, "pending", "2024-12-10"),
("李四", 2300.00, "completed", "2024-12-03"),
("李四", 560.00, "cancelled", "2024-12-08"),
("王五", 3200.00, "completed", "2024-12-07")]
)
cursor.executemany(
"INSERT INTO customers (name, email, city) VALUES (?,?,?)",
[("张三", "zhangsan@example.com", "北京"),
("李四", "lisi@example.com", "上海"),
("王五", "wangwu@example.com", "深圳")]
)
conn.commit()
return conn
DB_CONN = init_test_db()
def execute_search_web(query: str, num_results: int = 5) -> dict:
"""执行网络搜索(模拟实现)"""
# 实际项目中替换为真实搜索API(如 Google Custom Search、Bing Search API)
mock_results = [
{"title": f"搜索结果: {query}", "snippet": f"这是关于'{query}'的搜索结果摘要...", "url": "https://example.com/1"},
{"title": f"相关资讯: {query}", "snippet": f"与'{query}'相关的最新资讯...", "url": "https://example.com/2"},
]
return {"results": mock_results[:num_results], "total": len(mock_results)}
def execute_calculate(expression: str) -> dict:
"""执行数学计算"""
# 安全限制:只允许数学运算,禁止导入和危险函数
allowed_names = {
"abs": abs, "round": round, "min": min, "max": max, "sum": sum,
"pow": pow, "len": len, "range": range,
}
# 导入数学函数
import math
for name in dir(math):
if not name.startswith("_"):
allowed_names[name] = getattr(math, name)
# 限制内置函数
allowed_names["__builtins__"] = {}
try:
result = eval(expression, allowed_names, {})
return {"expression": expression, "result": result, "type": type(result).__name__}
except Exception as e:
raise ValueError(f"表达式计算失败: {e}")
def execute_query_database(sql: str) -> dict:
"""执行SQL查询"""
# 安全校验:只允许 SELECT
sql_stripped = sql.strip().upper()
if not sql_stripped.startswith("SELECT"):
raise ValueError("仅允许 SELECT 查询,禁止 DML/DDL 操作")
if any(keyword in sql_stripped for keyword in ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "CREATE"]):
raise ValueError("检测到危险SQL关键词,操作被拒绝")
cursor = DB_CONN.cursor()
cursor.execute(sql)
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
return {
"columns": columns,
"rows": [list(row) for row in rows],
"row_count": len(rows)
}
# 工具注册表
TOOL_REGISTRY = {
"search_web": execute_search_web,
"calculate": execute_calculate,
"query_database": execute_query_database,
}
这段代码实现了三个工具的执行函数。关键设计点:
- 数据库使用内存 SQLite:方便演示,实际项目中替换为真实数据库连接
- SQL 安全校验:只允许 SELECT 查询,禁止所有 DML/DDL 操作,防止 SQL 注入风险
- 计算器安全限制 :
eval函数被严格限制------清空__builtins__,只允许数学函数 - 统一的工具注册表 :
TOOL_REGISTRY字典映射工具名到执行函数,便于动态扩展
6.4 Agent 主循环
python
# ============================================================
# 3. Agent 主循环
# ============================================================
MAX_ROUNDS = 10 # 最大推理轮次
TOOL_TIMEOUTS = {
"search_web": 10,
"calculate": 3,
"query_database": 15,
}
def run_agent(user_message: str) -> str:
"""
Agent 主函数:接收用户消息,返回最终回复。
"""
messages = [
{"role": "system", "content": "你是一个数据分析助手,可以帮助用户搜索信息、执行计算和查询业务数据库。请根据用户问题选择合适的工具。"},
{"role": "user", "content": user_message}
]
for round_num in range(1, MAX_ROUNDS + 1):
logger.info(f"=== 第 {round_num} 轮推理 ===")
# 调用模型
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=TOOLS,
tool_choice="auto" # 让模型自主决定是否调用工具
)
msg = response.choices[0].message
messages.append(msg)
# 如果模型没有调用工具,说明已经生成最终回复
if not msg.tool_calls:
logger.info(f"模型返回最终回复(第{round_num}轮)")
return msg.content
# 处理每个工具调用
for tc in msg.tool_calls:
tool_name = tc.function.name
logger.info(f"调用工具: {tool_name}, 参数: {tc.function.arguments}")
# 解析参数
try:
args = json.loads(tc.function.arguments)
except json.JSONDecodeError:
args = {}
# 执行工具(带错误处理)
result = safe_execute_tool(tool_name, args)
logger.info(f"工具 {tool_name} 执行结果: success={result['success']}")
# 将结果送回模型
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result, ensure_ascii=False, default=str)
})
return "抱歉,处理您的请求时超出了最大推理轮次限制。"
def safe_execute_tool(tool_name: str, args: dict) -> dict:
"""安全的工具执行器,包含完整的错误处理链路"""
if tool_name not in TOOL_REGISTRY:
return {"success": False, "error": f"未知工具: {tool_name}", "error_type": "unknown_tool"}
func = TOOL_REGISTRY[tool_name]
timeout = TOOL_TIMEOUTS.get(tool_name, 10)
start_time = time.time()
try:
# 带超时执行
result = func(**args)
elapsed = time.time() - start_time
return {"success": True, "data": result, "elapsed_ms": int(elapsed * 1000)}
except TimeoutError:
return {
"success": False,
"error": f"工具执行超时({timeout}秒)",
"error_type": "timeout",
"retryable": True
}
except (ValueError, TypeError, KeyError) as e:
return {
"success": False,
"error": f"参数或执行错误: {str(e)}",
"error_type": "execution_error",
"retryable": False
}
except Exception as e:
logger.error(f"工具 {tool_name} 未预期异常: {e}", exc_info=True)
return {
"success": False,
"error": f"内部错误: {str(e)}",
"error_type": "unexpected_error",
"retryable": False
}
# ============================================================
# 4. 运行示例
# ============================================================
if __name__ == "__main__":
# 测试用例1:单工具调用(计算)
print("=" * 60)
print("测试1: 计算问题")
print("=" * 60)
result = run_agent("计算 2 的 10 次方加上 3.14159 的正弦值")
print(result)
# 测试用例2:串行工具调用(数据库查询)
print("\n" + "=" * 60)
print("测试2: 数据库查询")
print("=" * 60)
result = run_agent("查询张三的已完成订单总金额")
print(result)
# 测试用例3:并行工具调用(搜索 + 计算)
print("\n" + "=" * 60)
print("测试3: 综合问题")
print("=" * 60)
result = run_agent("搜索2024年诺贝尔物理学奖信息,同时计算1024除以3的结果")
print(result)
这段代码实现了 Agent 的主循环,是整个系统的核心。执行流程为:
run_agent函数是入口,接收用户消息,返回最终回复- 每轮循环中,模型决定是否调用工具。如果调用了工具,将工具结果送回模型继续推理;如果没有调用工具,说明模型已经生成了最终回复
safe_execute_tool是统一的工具执行器,包含超时控制和异常捕获MAX_ROUNDS = 10防止无限循环,这在生产环境中是必须的
6.5 运行结果分析
python
# 预期输出(简化版)
# 测试1: 计算问题
# === 第 1 轮推理 ===
# 调用工具: calculate, 参数: {"expression": "2**10 + sin(3.14159)"}
# 工具 calculate 执行结果: success=True
# === 第 2 轮推理 ===
# 模型返回最终回复(第2轮)
# 2的10次方是1024,sin(3.14159) ≈ 2.6536e-06,
# 两者相加的结果约为 1024.00000265
# 测试2: 数据库查询
# === 第 1 轮推理 ===
# 调用工具: query_database, 参数: {"sql": "SELECT SUM(amount) as total FROM orders WHERE customer='张三' AND status='completed'"}
# 工具 query_database 执行结果: success=True
# === 第 2 轮推理 ===
# 模型返回最终回复(第2轮)
# 张三的已完成订单总金额为 2050.00 元(共2笔已完成订单)。
# 测试3: 综合问题(并行调用)
# === 第 1 轮推理 ===
# 调用工具: search_web, 参数: {"query": "2024年诺贝尔物理学奖"}
# 调用工具: calculate, 参数: {"expression": "1024/3"}
# 工具 search_web 执行结果: success=True
# 工具 calculate 执行结果: success=True
# === 第 2 轮推理 ===
# 模型返回最终回复(第2轮)
# 2024年诺贝尔物理学奖... 1024/3 ≈ 341.33
通过测试结果可以看到:
- 测试1:单工具调用,2轮完成(1轮工具调用 + 1轮最终回复)
- 测试2:数据库查询,2轮完成。模型生成了正确的 SQL 查询语句
- 测试3:并行调用,2轮完成。模型在一次推理中生成了两个工具调用,并行执行后送回模型生成最终回复

图:多工具Agent整体架构,展示从用户输入到工具执行到最终回复的完整数据流
七、适用边界与风险提示
7.1 Function Calling 的适用场景
Function Calling 并非万能的,它有明确的适用场景:
| 场景 | 适用度 | 说明 |
|---|---|---|
| 实时信息获取 | ✅ 强烈推荐 | 天气、新闻、股价等实时数据,模型训练数据无法覆盖 |
| 精确计算 | ✅ 强烈推荐 | 数学计算、统计运算,工具比模型自身计算可靠得多 |
| 数据库查询 | ✅ 强烈推荐 | 业务数据查询,模型无法直接访问你的数据库 |
| API 集成 | ✅ 推荐 | 发送邮件、创建任务、操作第三方系统 |
| 复杂决策推理 | ⚠️ 谨慎使用 | 模型可以选择工具,但复杂的多步决策推理效果取决于模型能力 |
| 纯文本生成 | ❌ 不适用 | 写文章、翻译、摘要等纯文本任务不需要 Function Calling |
| 情感分析 | ❌ 不适用 | 模型自身就能完成,不需要外部工具 |
7.2 风险提示与安全考量
⚠️ 安全风险:SQL 注入
在数据库查询工具中,如果直接将模型生成的 SQL 语句执行,存在 SQL 注入风险。虽然模型通常不会恶意注入,但它可能生成不安全的 SQL。建议措施:
- 强制使用参数化查询(Parameterized Query)
- 限制 SQL 中可操作的表和字段
- 使用只读数据库用户,禁止 DML/DDL 操作
- 对模型生成的 SQL 进行安全审查
⚠️ 安全风险:任意代码执行
计算器工具如果使用 eval(),存在代码注入风险。建议措施:
- 限制
eval的命名空间(如本文示例中清空__builtins__) - 白名单方式只允许数学函数
- 考虑使用
ast.literal_eval或专用的数学表达式解析库
⚠️ 可靠性风险:模型幻觉
模型可能"幻觉"出不存在的工具或参数。建议措施:
- 设置
tool_choice="auto"让模型自主决定,而非强制调用 - 对模型生成的参数进行严格校验
- 实现完善的错误处理和降级策略
⚠️ 成本风险:无限循环
串行调用模式下,模型可能陷入循环------不断调用工具但无法得出结论。建议措施:
- 设置
MAX_ROUNDS限制最大推理轮次 - 监控每轮推理的 Token 消耗
- 在系统提示中明确告知模型"如果没有需要调用的工具,请直接回答"
⚠️ 延迟风险:超时累积
串行调用模式下,多轮工具调用的延迟会累积。建议措施:
- 为每个工具设置合理的超时时间
- 使用并行调用减少总延迟
- 在 UI 层显示进度提示,告知用户正在处理
7.3 模型能力差异
不同模型在 Function Calling 上的表现差异很大:
| 模型 | 并行调用 | 工具选择准确度 | 参数生成准确度 | 推荐使用场景 |
|---|---|---|---|---|
| GPT-4o | ✅ 优秀 | 优秀 | 优秀 | 生产环境首选 |
| GPT-4o-mini | ✅ 良好 | 良好 | 良好 | 成本敏感场景 |
| Claude 3.5 Sonnet | ✅ 优秀 | 优秀 | 优秀 | Anthropic 生态 |
| Qwen 2.5+ | ✅ 良好 | 良好 | 良好 | 开源/私有化部署 |
| Llama 3.1+ | ⚠️ 有限 | 一般 | 一般 | 开源/私有化部署 |
💡 提示:模型能力在不断迭代,建议在选型时使用你的实际工具集进行测试,不要完全依赖基准测试结果。
7.4 成本考量
Function Calling 会增加 Token 消耗和 API 调用次数,这是不可忽视的成本因素。以下是成本优化的几个方向:
Token 消耗分析:工具定义本身会占用 Token。每个工具的 Schema(包括 name、description、parameters)大约占用 100-300 Token。如果注册了 20 个工具,仅工具定义就占用 2000-6000 Token,这还不算每次对话中模型生成的 tool_calls 和返回的 tool results。
成本优化策略:
- 按需注册工具:不要把所有工具都注册进去。根据对话上下文动态选择相关工具,例如用户讨论天气时只注册天气相关工具
- 精简 Schema:description 控制在 50-100 字,避免冗长描述。参数 description 只包含关键信息
- 使用小模型做路由:先用成本更低的小模型(如 gpt-4o-mini)判断用户意图,再根据意图选择工具子集传给大模型
- 缓存工具结果:相同参数的工具调用结果可以缓存(如天气查询),避免重复调用
7.5 性能优化建议
除了成本,性能也是 Function Calling 生产化的重要考量:
- 并行化:对无依赖关系的工具调用,尽量使用并行调用模式
- 流式输出:使用 Streaming API,在工具执行期间先给用户返回"正在查询..."的提示,避免长时间空白
- 预热连接:数据库连接、HTTP 客户端等资源在 Agent 启动时预热,避免首次调用时的连接延迟
- 异步执行:对于耗时较长的工具(如文件下载、批量数据处理),考虑使用异步执行模式,先返回任务 ID,后续轮询结果
八、总结
本文从 Function Calling 的本质出发,系统性地拆解了从参数定义到错误处理的完整工程实践。核心要点回顾:
1. Function Calling 的本质是让模型从"文本生成器"进化为"工具使用者"------模型不直接执行函数,而是生成结构化的函数调用请求,由外部运行时执行。这种设计既保证了安全性,又扩展了模型的能力边界。
2. 参数定义规范是模型与外部系统的契约。好的 JSON Schema 应该:为每个参数提供清晰的 description、合理使用 enum 限制取值范围、限制 required 参数数量、避免深层嵌套。参数 Schema 的质量直接影响模型生成参数的准确率。
3. 工具描述的艺术决定了模型的工具选择准确度。高质量的 description 应包含三个层次:功能定义(做什么+返回什么)、使用边界(何时用+何时不用)、参数提示(格式+示例)。多工具场景下,description 之间要有足够的区分度。
4. 并行调用与串行调用各有适用场景。并行调用适用于多个独立信息需求,延迟低但需要模型支持;串行调用适用于有依赖关系的数据获取,通用性更好但延迟较高。理解两者的区别是设计高效 Agent 的基础。
5. 错误处理全链路是 Function Calling 从 Demo 走向生产的关键。四个层次------参数校验、执行异常、超时控制、降级策略------缺一不可。每个工具执行都应该是"安全的"------不崩溃、不泄露内部信息、能给出有意义的错误描述。
6. 实战案例展示了一个完整的多工具 Agent 实现,涵盖搜索、计算和数据库查询三种工具。通过工具注册表、安全执行器和 Agent 主循环的分层设计,实现了可扩展、可维护的 Agent 架构。
Function Calling 是 AI Agent 工程化落地的基石技术。掌握它,你就掌握了让 LLM 从"聊天机器人"变成"智能助手"的核心能力。本系列的下一篇《MCP 协议详解》将探讨如何通过标准化协议让 Agent 接入更多工具生态。