大语言模型函数调用实战:从原理到Agent构建,解决LLM输出结构化难题

发布时间:2026/8/11 4:15:05
大语言模型函数调用实战:从原理到Agent构建,解决LLM输出结构化难题 最近在模型圈和开发者社区里一个现象越来越明显很多人在尝试将大语言模型LLM应用到实际业务中时总会卡在同一个环节——如何让模型“稳定输出”我们想要的格式你或许也遇到过精心设计的提示词Prompt模型有时能完美返回结构化的 JSON有时却突然“放飞自我”给你一段散文诗或者干脆把字段名都改了。这种不确定性让 LLM 在需要与下游系统如数据库、API稳定对接的生产环境中显得像个“不靠谱的队友”。这背后的核心痛点是LLM 输出的非结构化与程序世界要求的结构化之间的根本矛盾。我们需要的不是一次性的“对话艺术”而是可编程、可预测、可集成的“工程组件”。今天要深入探讨的正是解决这一矛盾的利器函数调用Function Calling。很多人把它简单理解为“让 AI 调 API”这大大低估了它的价值。本文将揭示函数调用本质上是为 LLM 注入“确定性”的桥梁是构建可靠 AI 应用Agent的基石。我们将从原理拆解到代码实战手把手带你掌握如何利用主流模型如 OpenAI GPT、DeepSeek的函数调用能力构建一个能稳定输出、自主决策的智能体。读完本文你将彻底理解函数调用的运作机制并能独立完成以下任务将一个模糊的用户自然语言请求转化为对特定工具函数的精确调用。确保 LLM 的输出100%符合你预定义的数据结构如 JSON Schema。构建一个能根据上下文自动选择并执行多个工具的简易 Agent 系统。规避函数调用开发中的常见陷阱如幻觉、参数错误、循环调用等。1. 函数调用为什么它是 LLM 工程化的关键一跃在深入代码之前我们必须先建立正确的认知函数调用解决的到底是什么问题想象一个订餐场景。用户说“帮我订一份披萨要大号的多加芝士送到公司。” 传统的提示词工程可能会让模型回复“好的已为您记录披萨大号加芝士送至公司。” 这看起来不错但它是“文本”。你的订单系统无法直接处理这段文本需要一个复杂的解析器NLP模块来抽取“菜品”、“尺寸”、“配料”、“地址”等字段这个过程容易出错且维护成本高。函数调用的思路则完全不同。我们不再要求模型“生成回复文本”而是要求它“思考并决定调用哪个函数以及传入什么参数”。我们会预先定义一个函数比如create_order(item: str, size: str, extras: list, address: str)并告诉模型这个函数的用途和参数格式。当用户说出需求时模型的工作变成了“识别用户意图 → 匹配到create_order函数 → 从用户话语中提取出对应的参数值”。关键转变在于模型的输出从自由文本变成了对预定义结构的填充。这个结构函数签名是开发者完全掌控的。因此下游程序可以毫无歧义地解析这个结构化的调用请求然后真正去执行函数调用订单API。模型的“不确定性”被限制在了“从自然语言到结构化参数的映射”这一步而这一步的准确性通过清晰的函数描述和少量示例已经可以达到很高的水平。所以函数调用的核心价值是输出标准化强制模型返回格式化的 JSON 数据便于程序处理。意图路由将复杂的用户请求自动分发到对应的业务处理模块。能力扩展让 LLM 能够操作“外部世界”如查询数据库、发送邮件、控制设备而不仅仅是聊天。构建 Agent 基础多个函数构成了一个工具的“工具箱”模型学会在合适的时候选择合适工具这正是智能体Agent的雏形。不理解这一点你可能只会 copy-paste 一段函数调用的示例代码理解了这一点你才能设计出健壮、可扩展的 AI 应用架构。2. 核心概念与工作原理一次完整的调用是如何发生的要驾驭函数调用必须理解其背后的交互协议。它通常不是一次 API 请求就完成的而是一个“对话”过程。我们以 OpenAI 的 Chat Completions API 为例拆解这个流程。2.1 关键角色定义用户User提出自然语言请求的人例如“北京明天天气怎么样”开发者Developer定义函数、编写程序逻辑的你。大语言模型LLM如 gpt-3.5-turbo 或 gpt-4。它的角色是“理解者”和“决策者”。外部工具Tools/Functions由开发者实现的、具有具体功能的代码块例如get_weather(location: str, date: str)。2.2 交互流程详解一次完整的函数调用通常包含两轮或以上的 API 交互第一轮模型决定“要调用什么”开发者准备你将用户消息和一组函数描述function definitions一起发送给 LLM API。函数描述通常包括函数名、描述、参数列表及其 JSON Schema。模型推理LLM 分析用户消息判断是否需要调用函数、调用哪一个、参数应该是什么。模型响应LLM 不会直接执行函数而是返回一个特殊的响应。这个响应里包含一个tool_calls字段其中指明了它“想要”调用的函数名称和它推断出的参数一个 JSON 对象。第二轮执行并反馈开发者执行你的程序接收到模型的“调用请求”后在本地或远程真正执行这个函数并得到执行结果。反馈结果你将函数的执行结果一个字符串或结构化数据作为新的消息再次发送给 LLM API告诉它“这是你刚才想调用的那个函数的返回结果。”模型总结LLM 结合最初的用户问题和你反馈的函数结果生成最终面向用户的自然语言回答。这个过程可以概括为Plan (模型计划调用) → Act (程序执行函数) → Observe (反馈结果) → Answer (模型总结回答)。这就是 ReAct (Reasoning and Acting) 框架的简化体现。2.3 函数描述Function Definition的构成这是连接模型意图和实际代码的“契约”。一个典型的函数描述如下所示{ type: function, function: { name: get_current_weather, description: 获取指定城市的当前天气信息, parameters: { type: object, properties: { location: { type: string, description: 城市名称例如北京San Francisco }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位, default: celsius } }, required: [location] } } }name: 函数唯一标识模型返回的调用请求会使用这个名字。description:至关重要模型根据描述判断何时调用此函数。描述应清晰说明函数的用途和适用场景。parameters: 遵循 JSON Schema 标准定义了参数的类型、描述、是否必需、枚举值、默认值等。描述越详细模型填充越准确。3. 环境准备选择你的工具链在开始编码前你需要准备好开发环境。本文将使用 Python 作为示例语言因为它拥有最丰富的 LLM 生态库。3.1 基础环境Python 版本: 推荐 Python 3.8 及以上。包管理工具: 使用pip或poetry。3.2 核心依赖库我们将使用openai这个官方库来调用 OpenAI 兼容的 API包括 OpenAI 自身和许多开源模型部署的服务。同时为了更清晰地展示流程我们也会用到json和pydantic用于数据验证和生成 JSON Schema。通过 pip 安装pip install openai pydantic3.3 API 密钥配置你需要一个支持函数调用的模型服务 API 密钥。可以是OpenAI API Key: 访问 platform.openai.com 获取。其他兼容 OpenAI 格式的 API 端点: 如 Azure OpenAI, 或部署了 DeepSeek、Qwen 等支持 function calling 的模型服务。在代码中通常通过环境变量来管理密钥# 在终端中设置临时 export OPENAI_API_KEYyour-api-key-here# 在 Python 代码中读取 import os from openai import OpenAI client OpenAI( api_keyos.environ.get(OPENAI_API_KEY), # 从环境变量读取 base_urlhttps://api.openai.com/v1 # 如果是其他服务需修改此处 )4. 从零开始你的第一个函数调用程序让我们用一个经典的“查询天气”例子走通整个流程。我们将模拟一个天气函数而不是真正调用天气 API以聚焦于函数调用机制本身。4.1 步骤一定义工具函数首先我们实现一个本地的“天气查询”函数。# 文件weather_tools.py def get_current_weather(location: str, unit: str celsius) - str: 模拟获取当前天气的函数。 在实际应用中这里会调用如 OpenWeatherMap 的 API。 Args: location: 城市名如 北京。 unit: 温度单位celsius 或 fahrenheit。 Returns: 描述天气的字符串。 # 这里只是模拟数据 weather_data { 北京: {temperature: 22, condition: 晴朗, unit: unit}, 上海: {temperature: 25, condition: 多云, unit: unit}, 广州: {temperature: 28, condition: 阵雨, unit: unit}, } if location in weather_data: data weather_data[location] return f{location}的天气是{data[condition]}气温 {data[temperature]} 度{data[unit]}。 else: return f未找到{city}的天气信息。4.2 步骤二创建函数描述根据上面的函数我们创建对应的 JSON Schema 描述。使用pydantic可以让我们用 Python 类来定义 Schema并自动转换更易于维护。# 文件weather_tools.py (续) from pydantic import BaseModel, Field from typing import Literal class WeatherQuery(BaseModel): 查询天气所需的参数模型 location: str Field(description城市名称例如北京San Francisco) unit: Literal[celsius, fahrenheit] Field(defaultcelsius, description温度单位) # 根据 Pydantic 模型生成 OpenAI 函数描述格式 def get_weather_function_description(): return { type: function, function: { name: get_current_weather, description: 获取指定城市的当前天气信息, parameters: WeatherQuery.model_json_schema(), # 自动生成 JSON Schema } }4.3 步骤三发起对话并处理函数调用请求现在编写主程序逻辑与模型进行交互。# 文件main.py import os import json from openai import OpenAI from weather_tools import get_current_weather, get_weather_function_description # 初始化客户端 client OpenAI(api_keyos.environ.get(OPENAI_API_KEY)) def run_conversation(user_query: str): 执行一次完整的、可能包含函数调用的对话。 # 1. 准备消息历史和工具描述 messages [{role: user, content: user_query}] tools [get_weather_function_description()] # 将工具描述传入 # 2. 第一轮调用让模型决定是否调用函数 print( 第一轮模型分析用户请求 ) response client.chat.completions.create( modelgpt-3.5-turbo, # 或 gpt-4 messagesmessages, toolstools, tool_choiceauto, # 让模型自动决定是否调用工具 ) response_message response.choices[0].message print(f模型原始回复: {response_message}) # 3. 检查模型是否想要调用工具 tool_calls response_message.tool_calls if tool_calls: # 4. 将模型的“调用决定”添加到消息历史中这对于保持对话上下文很重要 messages.append(response_message) print(f\n 模型决定调用工具 ) for tool_call in tool_calls: function_name tool_call.function.name function_args json.loads(tool_call.function.arguments) print(f函数名: {function_name}) print(f参数: {function_args}) # 5. 执行本地函数 if function_name get_current_weather: location function_args.get(location) unit function_args.get(unit, celsius) function_response get_current_weather(location, unit) else: function_response f错误未知函数 {function_name} print(f函数执行结果: {function_response}) # 6. 将函数执行结果作为新的消息反馈给模型 messages.append({ role: tool, tool_call_id: tool_call.id, # 必须关联到具体的 tool_call content: function_response, }) # 7. 第二轮调用让模型基于函数结果生成最终回答 print(\n 第二轮模型生成最终回答 ) second_response client.chat.completions.create( modelgpt-3.5-turbo, messagesmessages, # 此时消息历史包含了用户问题、模型调用请求、函数结果 ) final_message second_response.choices[0].message.content print(f最终回答: {final_message}) return final_message else: # 模型没有调用工具直接返回其回答 print(模型未调用工具直接回答。) print(f回答: {response_message.content}) return response_message.content if __name__ __main__: # 测试不同的用户查询 queries [ 北京今天热吗, 帮我查一下上海的天气用华氏度。, 你好请介绍一下你自己。 ] for query in queries: print(f\n{*50}) print(f用户查询: 「{query}」) print(*50) run_conversation(query)5. 运行结果与效果验证运行上面的main.py脚本请确保已设置OPENAI_API_KEY你会看到类似下面的输出 用户查询: 「北京今天热吗」 第一轮模型分析用户请求 模型原始回复: ChatCompletionMessage(contentNone, roleassistant, function_callNone, tool_calls[ChatCompletionMessageToolCall(idcall_abc123, functionFunction(arguments{location:北京,unit:celsius}, nameget_current_weather), typefunction)]) 模型决定调用工具 函数名: get_current_weather 参数: {location: 北京, unit: celsius} 函数执行结果: 北京的天气是晴朗气温 22 度celsius。 第二轮模型生成最终回答 最终回答: 北京今天天气晴朗气温22摄氏度。这个温度对人体来说比较舒适不算热。 用户查询: 「帮我查一下上海的天气用华氏度。」 第一轮模型分析用户请求 模型原始回复: ChatCompletionMessage(contentNone, roleassistant, function_callNone, tool_calls[ChatCompletionMessageToolCall(idcall_def456, functionFunction(arguments{location:上海,unit:fahrenheit}, nameget_current_weather), typefunction)]) 模型决定调用工具 函数名: get_current_weather 参数: {location: 上海, unit: fahrenheit} 函数执行结果: 上海的天气是多云气温 25 度fahrenheit。 第二轮模型生成最终回答 最终回答: 上海目前的天气是多云气温是25华氏度约合-4摄氏度。天气比较冷请注意保暖。 用户查询: 「你好请介绍一下你自己。」 第一轮模型分析用户请求 模型原始回复: ChatCompletionMessage(content你好我是OpenAI创造的AI助手致力于为用户提供信息和帮助。请问有什么可以为您效劳的吗, roleassistant, function_callNone, tool_callsNone) 模型未调用工具直接回答。 回答: 你好我是OpenAI创造的AI助手致力于为用户提供信息和帮助。请问有什么可以为您效劳的吗如何验证成功意图识别正确对于天气查询模型正确识别并调用了get_current_weather函数对于闲聊模型没有调用函数直接回复。参数提取准确模型从“北京今天热吗”中准确提取了location: “北京”并使用了默认单位celsius。从“用华氏度”中准确提取了unit: “fahrenheit”。流程完整闭环程序完整经历了“用户提问 → 模型计划调用 → 执行函数 → 反馈结果 → 模型生成最终回答”的全流程。输出稳定结构化模型返回的tool_calls是严格的 JSON 结构你的程序可以稳定地解析function.name和function.arguments没有出现自由文本的干扰。6. 进阶实战构建一个多工具智能体Agent单一函数只是开始。真正的威力在于让模型在一个“工具箱”里自主选择。我们来构建一个简单的个人助理 Agent它可以使用“查天气”、“查时间”、“计算器”三个工具。6.1 定义多个工具函数和描述# 文件multi_tools_agent.py import json from datetime import datetime from pydantic import BaseModel, Field from typing import List, Literal import math # ---------- 1. 工具函数实现 ---------- def get_current_time(timezone: str Asia/Shanghai) - str: 获取指定时区的当前时间。时区参数暂未实现仅作演示。 now datetime.now() return now.strftime(f%Y-%m-%d %H:%M:%S (假设时区: {timezone})) def calculator(expression: str) - str: 计算一个数学表达式的结果。支持 , -, *, /, **, sqrt 等。 # 警告在生产环境中直接 eval 是极度危险的这里仅作演示。 # 应使用 ast.literal_eval 或专用数学库如 sympy进行安全计算。 try: # 替换一些常用函数 safe_dict {sqrt: math.sqrt, pi: math.pi, e: math.e} safe_dict.update({k: v for k, v in math.__dict__.items() if not k.startswith(_)}) # 极其简化的安全措施切勿用于生产 result eval(expression, {__builtins__: {}}, safe_dict) return f{expression} {result} except Exception as e: return f计算错误: {e} # 复用之前的 get_current_weather 函数 def get_current_weather(location: str, unit: str celsius) - str: weather_data {北京: {temperature: 22, condition: 晴朗}, 上海: {temperature: 25, condition: 多云}} if location in weather_data: data weather_data[location] return f{location}: {data[condition]}, {data[temperature]}°{unit}. return f未找到{location}的天气。 # ---------- 2. 使用 Pydantic 定义参数模型 ---------- class TimeQuery(BaseModel): timezone: str Field(defaultAsia/Shanghai, description时区名称如 Asia/Shanghai, America/New_York) class CalcQuery(BaseModel): expression: str Field(description数学表达式例如3 5 * 2, sqrt(16), pi * 2) class WeatherQuery(BaseModel): location: str Field(description城市名称) unit: Literal[celsius, fahrenheit] Field(defaultcelsius, description温度单位) # ---------- 3. 构建工具描述列表 ---------- def get_tool_descriptions(): 生成所有可用工具的 OpenAI 格式描述 tools [] # 时间工具 tools.append({ type: function, function: { name: get_current_time, description: 获取当前的日期和时间, parameters: TimeQuery.model_json_schema() } }) # 计算器工具 tools.append({ type: function, function: { name: calculator, description: 计算一个数学表达式的结果, parameters: CalcQuery.model_json_schema() } }) # 天气工具 tools.append({ type: function, function: { name: get_current_weather, description: 获取指定城市的当前天气信息, parameters: WeatherQuery.model_json_schema() } }) return tools # ---------- 4. 工具执行路由 ---------- def execute_tool(function_name: str, function_args: dict) - str: 根据函数名和参数路由并执行对应的工具函数 if function_name get_current_time: return get_current_time(**function_args) elif function_name calculator: return calculator(**function_args) elif function_name get_current_weather: return get_current_weather(**function_args) else: return fError: Unknown function {function_name} called.6.2 实现 Agent 主循环一个简单的 Agent 需要能够处理多轮对话和可能的连续工具调用。# 文件multi_tools_agent.py (续) from openai import OpenAI import os client OpenAI(api_keyos.environ.get(OPENAI_API_KEY)) def run_agent_conversation(initial_query: str, max_turns: int 5): 运行一个支持多轮对话和多工具调用的简易 Agent。 Args: initial_query: 用户的初始问题。 max_turns: 最大对话轮次防止无限循环。 messages [{role: user, content: initial_query}] tools get_tool_descriptions() for turn in range(max_turns): print(f\n--- Turn {turn 1} ---) # 1. 调用模型 response client.chat.completions.create( modelgpt-3.5-turbo, messagesmessages, toolstools, tool_choiceauto, ) response_message response.choices[0].message print(fAI 回复: {response_message.content or [Tool Call]}) # 2. 检查是否结束模型生成了最终文本回复 if response_message.content is not None and response_message.tool_calls is None: print(\n对话结束。) return response_message.content # 3. 处理工具调用可能同时调用多个 if response_message.tool_calls: messages.append(response_message) # 记录模型的调用意图 for tool_call in response_message.tool_calls: func_name tool_call.function.name func_args json.loads(tool_call.function.arguments) print(f - 执行工具: {func_name}({func_args})) # 执行工具 tool_response execute_tool(func_name, func_args) print(f - 工具结果: {tool_response}) # 将每个工具的结果作为单独的消息追加 messages.append({ role: tool, tool_call_id: tool_call.id, content: tool_response, }) else: # 既没有内容也没有工具调用结束 break print(\n达到最大轮次限制对话结束。) return messages[-1].get(content, 对话已结束。) if __name__ __main__: # 测试复杂查询 complex_queries [ 先告诉我现在几点了然后计算一下 15 的平方根是多少。, 北京和上海现在的天气怎么样分别用摄氏度和华氏度告诉我。, 今天天气如何哦对了再帮我算一下 (12 8) * 3 等于多少。 ] for query in complex_queries: print(f\n{#*60}) print(f用户: {query}) print(#*60) final_answer run_agent_conversation(query) print(f\n最终答案摘要: {final_answer[:100]}...) # 打印前100字符运行这个 Agent你会看到模型能够在一个查询中理解多个意图并可能顺序或并行地调用多个工具最后整合所有结果给出一个连贯的回答。这已经是一个初级智能体的雏形。7. 常见问题、陷阱与排查思路在实际开发中你会遇到各种问题。下表总结了常见坑点及解决方法问题现象可能原因排查方式解决方案模型不调用函数1. 函数描述 (description) 不清晰或与用户问题不匹配。2. 模型认为无需工具即可回答。3. API 参数tool_choice设置为none。1. 检查函数描述是否准确概括了功能。2. 使用更明确的用户查询测试。3. 检查 API 调用参数。1. 优化函数描述使其场景更具体。2. 将tool_choice设为auto或特定函数名 ({type: function, function: {name: xxx}}) 强制调用。3. 在系统消息 (System Prompt) 中指示模型优先使用工具。函数参数提取错误1. 参数 JSON Schema 描述模糊。2. 用户表达存在歧义。3. 必需参数缺失。1. 查看模型返回的argumentsJSON对比预期。2. 在parameters的description字段中添加更详细的说明和示例。1. 细化每个参数的description使用enum限制可选值设置合理的default。2. 在对话历史中提供少量示例 (few-shot)。3. 在代码中增加参数验证和错误处理逻辑。模型产生“幻觉”参数模型可能生成 Schema 中未定义的参数。检查返回的arguments是否包含未在properties中定义的字段。1. 在 JSON Schema 中设置additionalProperties: false。2. 在后端处理时只解析预期的字段忽略多余的。无限循环或重复调用Agent 逻辑中工具执行结果可能再次触发对同一或另一工具的调用陷入循环。观察对话历史 (messages)看是否在重复相似的模式。1. 设置最大对话轮次 (max_turns)。2. 在系统消息中明确限制工具使用规则。3. 实现简单的状态管理记录已执行的操作。处理并行工具调用模型可能一次性返回多个tool_calls。检查response_message.tool_calls的长度它是一个列表。代码必须能遍历tool_calls列表依次或并行执行所有被调用的函数并将每个结果都正确关联tool_call_id后追加到消息历史。函数执行失败本地函数抛出异常如网络错误、参数无效。在execute_tool函数中添加try...except块。捕获异常并将错误信息作为content返回给模型让模型决定如何回复用户如道歉或请求澄清。8. 最佳实践与工程化建议将函数调用用于生产环境需要超越示例代码考虑工程化因素。安全第一永远不要相信模型的输入对模型返回的参数进行严格的验证、类型转换和清理防止注入攻击。沙盒化工具执行像calculator这样的函数绝对禁止使用eval。应使用安全的表达式解析库如ast.literal_eval或sympy或在独立环境中执行。权限控制为不同的工具设定权限等级并在调用前验证当前用户/会话是否有权执行。设计清晰的函数描述名称直观使用verb_noun格式如search_products,create_order。描述具体描述应清晰说明“在什么情况下使用这个函数”。例如“当用户想查询商品库存并且提供了商品ID或名称时调用此函数。”参数文档化每个参数的description字段要写明格式和示例例如date: str的描述可以是“日期格式为 YYYY-MM-DD”。管理对话状态维护完整的消息历史包括所有的用户消息、助手消息、工具调用和工具响应。这是模型理解上下文的基础。处理长上下文当对话轮次很多时注意 Token 消耗。可以考虑摘要之前的对话或选择性遗忘。优化性能与成本批量处理如果用户请求可能触发多个独立工具调用可以考虑让模型一次性规划所有调用然后并行执行减少 API 往返次数。缓存结果对耗时或消耗资源的工具如复杂计算、外部 API 调用根据参数建立缓存。选择合适的模型对于简单的工具调用gpt-3.5-turbo通常足够且更经济。对于复杂逻辑和规划gpt-4可能更可靠。测试与监控编写测试用例覆盖典型用户查询、边界情况和错误输入确保 Agent 行为符合预期。记录日志详细记录模型的请求、响应、工具调用和结果便于调试和优化。设置超时和回退为工具调用和模型 API 调用设置超时并设计优雅的降级策略例如工具失败时让模型告知用户“服务暂时不可用”。函数调用不是 LLM 应用的终点而是一个强大的起点。它解决了“让模型输出结构化数据”和“连接外部能力”这两个核心问题。通过掌握它你已经能够构建出可以理解用户意图、自主选择工具、并返回可靠结果的初级智能体。接下来你可以探索更复杂的 Agent 框架如 LangChain、LlamaIndex它们提供了记忆、规划、工具编排等高级功能但底层原理万变不离其宗。建议从完善手头的多工具 Agent 开始尝试为它添加一两个真实的外部 API如发送邮件、查询数据库在实践中深化理解。当你能够稳定地让模型驱动起一个包含多个步骤的业务流程时你就真正跨入了 AI 应用开发的大门。