Day 10:Function Calling 基础
学习目标
前两天你已经学会了调用模型 API 和获取结构化输出。今天要进入一个新的领域:让模型不仅能”说话”,还能”做事”。
Function Calling(函数调用)是大模型从”聊天机器人”进化为”智能助手”的关键能力。通过 Function Calling,模型可以根据用户的请求,决定调用哪个函数、传递什么参数,然后你执行函数并把结果返回给模型,模型基于结果继续推理。
这听起来可能有点抽象。用一个简单的例子来说:用户问”今天半导体板块涨了没有”,模型本身不知道今天的股价数据,但它可以决定调用一个名为 get_stock_price 的函数,参数是 sector=“半导体”。你的程序执行这个函数(去查真实的股价数据),把结果返回给模型,模型基于真实数据回答用户的问题。
完成今天的学习后,你应该能理解 Function Calling 和普通文本生成的本质区别,能设计工具函数的 Schema,能处理函数选择和执行的全流程,能应对工具调用失败的情况。
核心概念
一、Function Calling 概念
Function Calling 的本质是让大模型具备了”使用工具”的能力。
在普通的文本生成模式下,用户提一个问题,模型直接生成一段文字回答。模型的输出只有文字,它能做的只是”说”。
在 Function Calling 模式下,模型可以选择不直接回答,而是告诉你”我需要调用某个函数来获取信息”。模型的输出不是最终答案,而是一个函数调用指令——函数名是什么、参数是什么。你的程序收到这个指令后,实际执行函数,把结果返回给模型,模型再基于函数返回的结果生成最终答案。
这个模式有几个关键点值得深入理解:
模型不直接执行函数。很多人以为 Function Calling 是模型自己去调用函数,不是的。模型只是”决定”要调用什么函数、传什么参数。实际执行函数的是你的代码。模型扮演的是一个”决策者”的角色,你的代码扮演的是”执行者”的角色。这种分工是有意为之的——如果让模型直接执行函数,安全性和可控性都无法保障。
函数调用是模型的一种输出格式。从技术角度看,Function Calling 就是模型输出了一种特殊的结构化数据——不是自由文本,而是一个包含函数名和参数的 JSON 对象。所以 Function Calling 可以理解为一种特殊形式的结构化输出。
函数调用可以多轮进行。模型调用一个函数后,拿到结果,可能发现还需要调用另一个函数才能完整回答用户的问题。比如用户问”分析一下台积电的竞争力”,模型可能先调用 search_company 获取公司信息,再调用 analyze_industry 获取行业信息,最后综合两方面的信息生成回答。
模型可以同时调用多个函数。当用户的请求涉及多个独立的操作时,模型可以一次性输出多个函数调用指令,你的程序并行执行后把所有结果返回给模型。比如用户说”对比一下台积电和三星的市场份额”,模型可以同时调用两次 search_company。
Function Calling 的价值在于它打破了模型的”信息孤岛”。模型本身的知识是静态的(训练数据有截止日期),能力是有限的(只能生成文字)。通过函数调用,模型可以获取实时信息(查数据库、调 API)、执行操作(发邮件、写文件)、使用外部工具(计算器、搜索引擎)。这把模型从一个”只会说话的百科全书”变成了一个”能使用工具的助手”。
二、Tool Schema
Tool Schema(工具描述模式)是告诉模型”你有哪些工具可以用”的标准格式。
模型需要知道每个工具的名称、功能描述、参数列表,才能在合适的时机选择合适的工具。Tool Schema 就是这些信息的结构化描述。
一个 Tool Schema 包含以下几个核心部分:
工具名称(name)。一个简洁、自解释的标识符。比如 search_company、calculate_roi、generate_report。命名规范建议用小写字母加下划线(snake_case),动词开头(表示这个工具”做什么”)。名称要能一眼看出这个工具的功能——不要用 func1、tool_a 这种无意义的名称。
工具描述(description)。一段话说明这个工具的功能、适用场景、返回什么信息。描述要写得清楚,因为模型是根据描述来决定是否使用这个工具的。描述写得好,模型在正确的时机选择正确的工具;描述写得模糊,模型要么不用该用的工具,要么乱用不该用的工具。
参数列表(parameters)。定义这个工具需要什么输入参数。参数本身也是一个 JSON Schema,包含每个参数的名称、类型、描述、是否必填。
好的 Tool Schema 设计有几个原则:
描述要具体。不要写”搜索公司信息”,要写”根据公司名称或股票代码搜索公司的基本信息,包括行业、规模、主要业务、竞争对手。当用户提到具体的公司名称并需要了解公司背景时使用此工具”。描述越具体,模型的选择越准确。
参数要精简。每个工具的参数不要超过 5 个。参数太多模型容易搞混参数的对应关系。如果一个操作确实需要很多参数,考虑把它拆成多个步骤、多个工具。
参数描述要包含示例。比如公司名称参数的描述可以写”公司的全称或常用简称,例如’台积电’、‘TSMC’、‘台湾积体电路制造’“。示例能帮助模型理解参数的期望格式。
要有清晰的触发条件。在描述中明确说明”当用户问什么问题时应该使用这个工具”。比如 calculate_roi 的描述可以写”当用户询问某个 AI 方案的投资回报率、成本效益分析或ROI计算时使用此工具”。
Tool Schema 的设计直接影响模型选择工具的准确性。花时间把 Schema 设计好,比花时间调试模型的输出要高效得多。
三、参数定义
参数定义是 Tool Schema 中的一个子结构,但它的重要性足以单独讨论。
参数定义的本质是告诉模型:调用这个工具时,你要提供什么信息、以什么格式提供。
参数的类型可以是基本类型(string、number、boolean、integer)或复杂类型(array、object)。最常用的是 string 和 number。
参数的设计要考虑以下几点:
必填与选填。核心参数设为必填,辅助参数设为选填。比如搜索公司工具中,公司名称是必填参数,行业筛选条件是选填参数。
枚举约束。如果参数值只能从有限的选项中选择,用 enum 定义。比如分析维度参数只能是”市场”、“技术”、“竞争”、“全部”。
默认值。某些参数可以设置默认值,模型不提供时就用默认值。比如搜索数量参数默认为 10。
参数之间的关系。有些参数之间存在依赖——如果提供了参数 A,就必须提供参数 B。这种关系目前无法在标准 JSON Schema 中完美表达,但可以在工具描述中用自然语言说明。
参数验证。你的代码在执行函数时,应该对模型传入的参数做验证。模型可能传入不符合预期的值——参数类型不对(传了字符串但期望数字)、值超出范围(要求返回 10000 条结果)、必填参数缺失。验证失败时,返回错误信息给模型,让它修正后重试。
一个常见的错误是把参数定义得太笼统。比如一个 generate_report 工具只有一个参数叫 content(报告内容),类型是 string。这个定义基本没有约束力——模型不知道应该传什么内容进来。应该拆成多个具体参数:industry_name(行业名称)、analysis_type(分析类型)、include_recommendation(是否包含建议)。
四、函数选择
函数选择是指模型根据用户的请求,从可用的工具列表中选择合适的工具来使用。
这个过程完全由模型自主完成。你提供工具列表和用户请求,模型自己判断是否需要调用工具、调用哪个工具、传什么参数。
模型做函数选择的依据是什么?主要是两个信息来源:工具的描述(description)和用户的请求内容。模型会比较用户请求和每个工具描述的语义相似度,选择最相关的工具。
这带来了一些设计上的考量:
工具之间的区分度要高。如果你定义了两个工具——search_company_by_name 和 search_company_by_industry,模型的描述里都写着”搜索公司”,模型可能分不清该用哪个。应该在描述中明确区分:“search_company_by_name 用于当用户提到具体公司名称时搜索;search_company_by_industry 用于当用户想了解某个行业的所有公司时搜索”。
工具数量不要太多。同时提供超过 10 个工具时,模型的选择准确性会下降——它要在多个工具之间做比较和权衡,容易选错。实际使用中,5-8 个工具是比较理想的范围。如果确实需要更多工具,可以通过两阶段选择来缓解——先选大类,再选具体工具。
模型可能选择不使用工具。不是所有请求都需要函数调用。如果用户问”什么是半导体”,模型可以直接用自己的知识回答,不需要调用任何工具。模型会自主判断是否需要使用工具。
模型可能选择错误的工具。这是不可避免的。应对方式是在执行函数前做一个简单验证——检查模型选择的工具和传入的参数是否在合理范围内。如果明显不合理(比如用户问股价但模型选择了发邮件的工具),可以拒绝执行并让模型重新选择。
五、函数执行
函数执行是你的代码在收到模型的函数调用指令后,实际运行对应函数的过程。
这是 Function Calling 流程中模型唯一不参与的部分——模型决定调用什么,你的代码负责执行什么。
函数执行的设计要点:
函数注册表。维护一个函数名称到实际函数实现的映射。当模型决定调用 calculate_roi 时,你的代码通过函数注册表找到对应的 Python 函数并执行。函数注册表可以是一个简单的字典,键是函数名(字符串),值是函数对象。
参数传递。模型输出的参数是一个 JSON 对象,你需要把它转换成 Python 函数的参数。这个转换过程要处理类型映射——JSON 中的 string 可能对应 Python 的 str,number 可能对应 int 或 float,array 对应 list。
执行超时。函数执行不应该无限等待。为每个函数设定超时时间(比如 30 秒),超时后返回错误信息。特别是涉及网络请求的函数(搜索、API 调用),更要严格控制超时。
结果序列化。函数的返回值需要转换成字符串(JSON 格式)才能返回给模型。如果函数返回的是一个复杂对象,需要先序列化。序列化时要注意信息量——返回太多信息会浪费 Token,返回太少模型无法做有效推理。一般建议返回模型需要的核心信息,而不是把整个数据库记录都丢给模型。
错误处理。函数执行可能失败——网络不通、数据库查询出错、参数不合法。执行失败时,不要向模型隐瞒错误,而是返回错误信息(比如”查询失败:数据库连接超时”)。模型收到错误信息后可以决定重试、换一个工具、或者直接告诉用户出了什么问题。
执行日志。每次函数执行都应该记录日志——函数名、参数、返回值、执行时间、是否成功。这些日志对于调试和优化工具非常重要。
六、函数结果返回
函数执行完毕后,结果需要按照特定的格式返回给模型。
返回格式是由 API 规范定义的。你需要构造一个 role 为 “tool”(或 “function”)的消息,包含函数名和执行结果。这条消息追加到对话历史中,模型基于这条消息继续推理。
结果返回的设计有几个要点:
结果要精炼。函数返回的信息越精炼越好。模型处理大量文本的能力是有限的,信息太多反而会干扰模型的推理。比如数据库查询返回了 50 条记录,你应该做摘要后返回,而不是把 50 条记录的完整内容都塞给模型。
结果要有结构。函数返回的信息最好也是结构化的——用 JSON 格式返回,而不是一段自由文本。这样模型能更高效地提取关键信息。
结果要包含元数据。除了核心返回值,还应该包含一些元信息——查询时间、数据来源、结果数量、是否完整(是否有更多数据没有返回)。这些信息帮助模型判断结果的可靠性。
错误结果也要返回。如果函数执行失败了,不要直接丢弃,而是把错误信息返回给模型。模型可能能根据错误信息调整策略——比如搜索失败后换个关键词重试。
七、工具调用失败
工具调用失败是一个必须面对的现实。模型可能选错工具、传错参数、函数执行出错、网络中断。你的系统需要能处理所有这些失败场景。
失败的类型:
选择错误。模型选择了一个不合适的工具。比如用户问”帮我计算 ROI”但模型选择了搜索工具而不是计算工具。应对方式:在函数执行前验证选择是否合理,不合理则返回提示让模型重新选择。
参数错误。模型传入了不合法的参数——类型不对、值超出范围、必填参数缺失。应对方式:在函数执行前验证参数,不合法则返回参数错误提示。
执行失败。函数执行过程中出错——网络超时、数据库异常、API 返回错误。应对方式:返回错误信息,模型可以重试或换工具。
结果异常。函数成功执行但返回了意料之外的结果——搜索没有结果、计算结果不合理。应对方式:把异常结果返回给模型,让它决定下一步。
对于每种失败,处理原则是一样的:不要隐藏失败,把错误信息返回给模型,让模型决定下一步。模型具备处理错误信息的能力——它能理解”搜索没有结果”意味着什么,并调整策略。
但也有需要人工介入的情况:模型反复重试同一个失败的工具(陷入循环)、模型调用了一个不应该被调用的工具(安全风险)、函数返回了敏感信息(数据泄露风险)。对于这些情况,你的系统应该设定上限(最大工具调用次数)和安全检查(工具权限白名单)。
八、工具调用日志
工具调用日志是调试和优化 Function Calling 系统的关键数据。
每次工具调用应该记录以下信息:
调用上下文。用户的原始请求、模型选择此工具的理由(从对话上下文中推断)、调用时间戳。
工具信息。工具名称、传入的参数、参数的来源(是模型直接生成的还是从用户输入中提取的)。
执行信息。函数执行结果(或错误信息)、执行时间、是否成功。
后续影响。模型收到工具返回结果后做了什么——是生成了最终答案,还是又调用了另一个工具,还是决定重试。
这些日志的用途:
调试。当系统给出错误答案时,通过日志回溯工具调用的全过程——模型选对了工具吗?参数正确吗?返回结果有问题吗?问题出在哪个环节?
优化。分析日志中的模式——哪些工具经常被误选?哪些参数经常出错?哪些函数执行经常超时?针对性地优化工具描述、参数定义或函数实现。
评估。统计工具调用的成功率、平均执行时间、平均调用次数(解决一个问题需要调用几次工具)。这些指标反映了 Function Calling 系统的整体质量。
成本分析。工具调用会消耗额外的 Token——模型的函数调用决策、函数返回结果的解析、基于结果生成最终答案,每一步都需要 Token。通过日志统计 Token 消耗,评估成本。
概念关系图
+------------+ +----------------+ +------------------+
| 用户请求 |---->| 模型推理决策 |---->| 生成最终答案 |
| "分析台积电"| | (需不需要工具?) | | (基于工具结果) |
+------------+ +-------+--------+ +------------------+
|
不需要工具 --> 直接回答
需要工具 --> 选择工具 + 生成参数
|
v
+------------------------+
| Tool Schema 列表 |
| - search_company |
| - analyze_industry |
| - calculate_roi |
| - generate_report |
| - save_result |
+------------+-----------+
|
+------+------+------+------+
| | |
+-----v----+ +----v-----+ +-----v----+
| 工具执行 | | 工具执行 | | 工具执行 |
| search | | analyze | | calculate|
+-----+----+ +----+-----+ +-----+----+
| | |
+------+------+------+------+
|
v
+------------------------+
| 结果返回给模型 |
| (结构化的 JSON 结果) |
+------------------------+
|
需要更多工具? --Yes--> 再次选择工具
|
No
v
+------------------------+
| 日志记录 |
| - 工具选择记录 |
| - 参数记录 |
| - 执行结果 |
| - 耗时与 Token 统计 |
+------------------------+
实战分析
任务:定义五个工具函数
指南要求定义五个工具:搜索公司、分析行业、计算 ROI、生成报告、保存结果。我们来逐一分析设计思路。
search_company(搜索公司)
功能描述:根据公司名称或关键词搜索公司的基本信息,包括行业、规模、主要业务、竞争对手。适用场景:用户提到具体公司名称并需要了解公司背景时。
参数设计:query(搜索关键词,必填字符串)、industry_filter(行业筛选,选填枚举)、limit(返回数量,选填整数,默认 5)。
这个工具的核心设计决策是搜索的粒度。不要试图用一个工具同时支持”按名称搜索”和”按行业搜索”——让模型自己根据用户意图选择合适的查询方式。query 参数足够灵活,可以传公司名也可以传行业名。
analyze_industry(分析行业)
功能描述:对一个指定行业进行全面分析,输出行业概况、市场规模、增长趋势、竞争格局、关键挑战。适用场景:用户要求分析某个行业的整体情况时。
参数设计:industry_name(行业名称,必填字符串)、analysis_depth(分析深度,选填枚举:概览/标准/深入,默认”标准”)、focus_areas(重点关注领域,选填字符串数组)。
analysis_depth 参数是一个好设计——它允许用户控制分析的详细程度。概览模式生成简短摘要,深入模式生成完整报告。这给不同使用场景提供了灵活性。
calculate_roi(计算 ROI)
功能描述:根据给定的成本和预期收益数据,计算投资回报率。适用场景:用户询问某个 AI 方案是否值得投入、成本效益如何时。
参数设计:initial_cost(初始投入成本,必填数字)、monthly_saving(每月节省金额,必填数字)、implementation_months(实施周期月数,选填整数,默认 3)、time_horizon_months(计算周期月数,选填整数,默认 12)。
ROI 计算工具的特点是参数都是数字类型,模型传错参数的概率相对较低。但需要注意参数的单位一致性——成本和收益都用”元”。
generate_report(生成报告)
功能描述:根据给定的分析结果数据,生成一份结构化的 Markdown 格式报告。适用场景:用户要求把分析结果整理成正式报告时。
参数设计:report_type(报告类型,必填枚举:行业分析/岗位分析/方案建议)、data(报告数据,必填字符串)、format_style(格式风格,选填枚举:简要/标准/详细,默认”标准”)。
这个工具的设计有一个特别之处:它的输入 data 参数是一个字符串(JSON 序列化后的分析结果)。这意味着这个工具通常不会单独使用,而是接在其他工具的后面——先分析、再生成报告。
save_result(保存结果)
功能描述:将分析结果保存到本地文件或数据库,支持后续查询和引用。适用场景:用户要求保存分析结果,或系统需要记录输出时。
参数设计:content(要保存的内容,必填字符串)、filename(文件名,选填字符串)、tags(标签,选填字符串数组)。
这是一个有副作用的工具——它会修改外部状态(创建文件或写入数据库)。这类工具需要特别注意权限控制和确认机制,防止模型在用户不知情的情况下覆盖重要文件。
让模型根据任务选择工具
定义好五个工具后,需要测试模型在不同场景下是否能正确选择工具。
测试场景设计:每个工具至少设计两个测试问题,验证模型能正确选择。比如”帮我查一下中芯国际的背景”应该触发 search_company;“分析一下半导体行业的投资机会”应该触发 analyze_industry;“投入 50 万做 AI 质检系统,多久能回本”应该触发 calculate_roi。
还要设计复合场景——一个问题可能需要多个工具配合。比如”帮我分析光伏行业并生成报告”应该先触发 analyze_industry,再触发 generate_report。
当日产物说明
Function Calling 工具示例
五个完整的工具定义,每个包含:Tool Schema(JSON 格式的完整定义)、工具描述文档(说明这个工具的功能、适用场景、使用限制)、工具执行函数的接口设计(函数签名、参数类型、返回值格式)。
Tool Schema 设计文档
一份总结性的文档,说明 Tool Schema 的设计原则和最佳实践。包含:命名规范、描述写法指南、参数设计原则、枚举值设计建议、工具之间的协作模式。
工具调用日志样例
至少 10 次工具调用的完整日志记录。每次记录包含:用户输入、模型选择的工具和参数、工具执行结果、模型的最终回答、是否成功。日志应该包含成功和失败的样例,展示系统的完整行为。
常见误区与避坑
误区一:以为模型会自己执行函数
第一次接触 Function Calling 的人经常以为模型会自己去调用函数。不会的。模型输出的只是一个”函数调用指令”——函数名和参数。你的代码才是真正的执行者。如果只把模型的输出发给用户看,用户看到的只是一段 JSON,而不是函数执行的结果。
误区二:工具描述写得太简短
工具描述只写一行”搜索公司信息”。模型看到这么模糊的描述,不知道什么时候该用这个工具,可能在不该用的时候乱用,或者该用的时候不用。描述要详细、具体,包含触发条件和使用场景。
误区三:不考虑工具调用失败的情况
函数执行一定会失败。网络会断、数据库会挂、参数会错。如果不处理失败,模型拿到一个错误异常就不知道该怎么办了。正确的做法是把错误信息包装成可读的文本返回给模型,让模型根据错误信息调整策略。
误区四:给模型太多工具
一次性给模型 20 个工具。模型需要在 20 个工具中做选择,准确率会明显下降。实际使用中,5-8 个工具是比较理想的范围。如果场景复杂需要更多工具,可以通过分类分组的方式分阶段提供——先提供大类选择,再根据大类提供具体工具。
误区五:不记录工具调用日志
工具调用过程完全是个黑盒——不知道模型选了什么工具、传了什么参数、执行了什么、结果如何。出问题时只能靠猜。从第一天写 Function Calling 就应该加上日志,这是后续优化的基础。
延伸思考
Function Calling 是 AI Agent 的核心能力。今天学的内容看起来是在”定义工具”,但实际上你是在为未来的 Agent 系统打地基。
在 Week 4 中,你会基于 Function Calling 构建完整的 Agent 系统。Agent 之所以能”做事”,就是因为 Function Calling 赋予了它使用工具的能力。今天的五个工具(搜索、分析、计算、生成、保存),在 Agent 系统中会扩展到十几个甚至几十个。
Function Calling 还有一个深层意义:它改变了人和 AI 的交互模式。在 Chatbot 模式下,人问 AI 答,AI 只能输出文字。在 Function Calling 模式下,人提出目标,AI 决定需要哪些工具、按什么顺序使用,最终完成任务。这是一个从”被动回答”到”主动执行”的转变。
从工程角度看,Function Calling 的设计还有很大的优化空间。比如动态工具选择(根据对话上下文动态提供不同的工具列表)、工具链编排(预定义常用工具的组合模式)、工具调用缓存(相同参数的工具调用结果缓存,避免重复执行)。这些都是后续 Week 5 工程化阶段要考虑的问题。
自测问题
- Function Calling 中,模型的角色是什么?你的代码的角色是什么?为什么不让模型直接执行函数?
- Tool Schema 的 description 字段为什么重要?写得好和写得差会导致什么不同的结果?
- 模型是根据什么信息来决定调用哪个工具的?如何提高模型选择的准确性?
- 函数执行失败时,应该怎么处理?为什么不能隐藏错误信息?
- 设计一个工具的参数时,哪些参数应该设为必填?哪些应该设为选填?
- 如果模型反复调用同一个失败的工具(陷入循环),你应该怎么处理?
- 为什么同时提供太多工具会降低模型的选择准确率?理想范围是多少?
- 工具调用日志应该记录哪些信息?这些日志有什么用途?
- Function Calling 和结构化输出(Day 9 学的内容)有什么关系?
- “生成报告”工具通常需要接在其他工具后面使用,这种工具链式调用的设计要点是什么?
关键词
- Function Calling:大模型根据用户请求自主决定调用外部函数的能力
- Tool Schema:描述工具功能、参数和返回值的结构化定义
- 工具选择:模型根据请求内容和工具描述自主判断使用哪个工具
- 工具执行:你的代码实际运行工具函数并获取结果的过程
- 函数注册表:工具名称到实际函数实现的映射关系
- 工具调用失败:工具选择错误、参数错误或执行出错的场景
- 参数定义:工具输入参数的类型、约束和描述
- 结果返回:将工具执行结果按规范格式返回给模型
- 工具调用日志:记录工具调用全过程的日志,用于调试和优化
- 副作用工具:会修改外部状态的工具(如保存文件、发送邮件),需要权限控制