拓冰建站拓冰建站
首页 / 资讯中心 / 正文

LLM函数调用实战:从原理到实现AI Agent外部工具集成

1. 项目概述从“对话”到“执行”的桥梁如果你最近在折腾大语言模型LLM的应用开发比如想做一个能帮你查天气、订日历或者操作数据库的智能助手那你大概率会碰到一个核心难题模型本身很擅长理解和生成文本但它无法直接“动手”做事情。它知道“查询北京明天天气”这句话的意思但它没法自己去调用一个天气API获取数据。这就是“Function Calling”函数调用机制要解决的核心问题。它不是某个特定模型的功能而是一种设计模式或协议让大语言模型能够与外部工具、API或代码进行安全、结构化的交互。简单说就是教会AI“动口也动手”——当它识别出用户的意图需要某个外部能力时它不是用自然语言回答“我可以帮你查天气”而是输出一个结构化的调用请求由我们的程序来实际执行这个请求并将结果返回给它最后由它整合成对用户的自然语言回复。这个机制是构建真正实用AI Agent智能体的基石。2. 核心需求与设计思路拆解2.1 为什么需要Function Calling在Function Calling出现之前我们想让LLM使用外部工具通常采用一些“土办法”。比如在提示词Prompt里详细描述API的用法期望模型能直接生成可执行的代码或URL。这种方法极不稳定输出格式不可控容易出错并且存在严重的安全风险如模型可能生成危险的系统命令。Function Calling机制通过引入“定义”和“约束”完美解决了这些问题。它的核心设计思路是“声明与调度”声明Definition开发者预先、明确地告诉模型它现在拥有哪些“能力”即函数。每个函数都需要清晰定义其名称、描述、参数包括参数名称、类型、描述以及是否必需。这相当于给模型一本“工具使用说明书”。理解与决策Understanding Decision用户输入一段话。模型结合对话上下文和这本“说明书”判断是否需要调用某个函数来满足用户需求。如果需要模型并不执行而是生成一个符合预定格式的、结构化的调用请求。调度与执行Dispatch Execution我们的应用程序接收到这个结构化的调用请求。因为格式是预定义的通常是JSON我们可以安全地解析它验证参数然后在受控的安全环境内调用真实的函数、API或代码。回复生成Response Generation将执行得到的结果数据再次交给模型。模型结合最初的用户问题和这个结果生成最终的自然语言回复给用户。这个流程将LLM的“思考规划”能力与外部系统的“安全执行”能力解耦既发挥了LLM的理解与推理优势又规避了让其直接操作系统的风险。2.2 主流方案与选型考量目前Function Calling主要有两种实现范式选择哪种取决于你的技术栈和需求。方案一OpenAI格式原生集成型这是目前最流行、生态最完善的标准。OpenAI在其Chat Completions API中直接内置了tools参数早期为functions。你只需要在API请求中以JSON数组的形式传入函数定义列表模型如gpt-3.5-turbo, gpt-4就会在回复中返回一个tool_calls字段其中包含了它想要调用的函数名和参数。优势开箱即用API原生支持无需额外适配。格式标准定义和响应的JSON结构非常清晰、稳定。生态强大LangChain、LlamaIndex等主流AI框架都深度集成并优先支持此格式。劣势平台绑定主要适用于OpenAI系列模型。虽然其他一些模型如DeepSeek、GLM也开始兼容此格式但并非全部。灵活性受限必须遵循OpenAI定义的参数结构。方案二ReActReasoning Acting范式这是一种更通用、更强调推理过程的提示工程方法。它不依赖API的特殊字段而是通过精心设计的提示词引导模型以特定的文本格式如Thought: ... Action: ... Action Input: ...来输出它的“思考”和“行动”指令。你的程序通过解析模型返回的文本来识别出需要执行的函数。优势模型无关理论上适用于任何具有足够推理能力的文本生成模型。过程透明可以要求模型输出推理链Chain-of-Thought便于调试和理解模型的决策过程。高度灵活可以自定义任何输出格式。劣势实现复杂需要自己设计提示词、编写文本解析逻辑稳定性和可靠性需要更多调试。容易出错模型输出的文本格式可能不稳定需要更鲁棒的解析器。选型建议 对于大多数应用尤其是快速原型开发和生产部署强烈建议从OpenAI格式开始。它的稳定性和便捷性远超ReAct范式。只有在必须使用不支持OpenAI格式的本地模型或者需要极度定制化推理流程时才考虑ReAct。3. 核心细节解析与实操要点3.1 函数定义的艺术如何写好“说明书”函数定义的质量直接决定了模型调用工具的准确率。这不仅仅是写参数更是一种与模型的“沟通”。1. 名称与描述要精准函数名使用蛇形命名法snake_case如get_current_weather清晰表达动作和对象。函数描述这是最重要的部分要用自然语言清晰说明在什么情况下应该调用此函数。例如“get_current_weather”的描述应该是“获取指定城市的当前天气情况。当用户询问天气、气温、是否需要带伞等问题时调用。” 避免写“这是一个获取天气的函数”这种无效描述。2. 参数设计的关键properties定义每个参数。每个参数本身也是一个对象需要包含type类型如string,integer、description描述和可选的enum枚举值。参数描述同样至关重要。对于location参数描述应该是“城市名称例如‘北京’、‘San Francisco’。请确保是完整的城市名。” 这能极大提高模型抽取用户语句中关键信息的准确性。required明确列出哪些参数是调用时必须的。一个完整的函数定义示例OpenAI格式{ type: function, function: { name: get_current_weather, description: 获取指定城市的当前天气状况包括温度、体感温度、天气现象和湿度。, parameters: { type: object, properties: { location: { type: string, description: 城市和国家的名称例如 中国北京 或 美国旧金山。 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位。默认为 celsius摄氏度。 } }, required: [location] } } }注意description字段是模型决定是否调用、如何填充参数的主要依据。花时间打磨描述比调整其他参数更有效。可以把自己想象成在教一个聪明的实习生如何使用这个API你会怎么描述3.2 调用流程的完整闭环一个健壮的Function Calling流程不是一次API请求而是一个包含多个步骤的循环或状态管理。以下是基于OpenAI格式的典型闭环流程用户输入接收用户查询如“北京今天热吗需要穿外套吗”首次模型调用携带工具定义将用户消息和历史对话连同定义好的函数列表tools一起发送给Chat Completions API。关键点必须将tool_choice参数设置为auto让模型自行决定是否调用。解析模型响应检查API返回的choices[0].message。如果包含tool_calls字段则说明模型决定调用函数。执行本地函数解析tool_calls中的name和arguments一个JSON字符串。在你的代码中找到对应的真实函数传入参数并执行。例如调用本地的get_current_weather(“北京”)函数该函数可能去请求第三方天气API。二次模型调用携带执行结果将函数执行的结果作为一个新的消息附加到对话历史中。这个消息的角色role必须是tool并且需要包含tool_call_id来自第一步的响应用于关联和content函数执行的结果通常是JSON字符串或文本。生成最终回复模型收到工具执行结果后会结合上下文生成面向用户的、整合了信息的自然语言回复如“北京今天气温25摄氏度天气晴朗湿度40%。体感比较舒适早晚温差大建议带一件薄外套备用。”循环将最终回复返回给用户并存储此次完整的交互记录到对话历史中以支持多轮对话。这个闭环确保了信息流的完整用户意图 - 模型规划 - 安全执行 - 结果整合 - 人性化回复。4. 实操过程与核心环节实现下面我们以Python为例使用OpenAI API兼容OpenAI格式的国产模型如DeepSeek、GLM等同样适用实现一个完整的天气查询助手。4.1 环境准备与依赖安装首先确保你的Python环境建议3.8以上并安装必要的库。我们将使用openai这个官方库它也兼容其他提供OpenAI兼容接口的服务。pip install openai你需要准备一个API密钥。如果你使用OpenAI请在官网获取如果使用国内兼容模型则使用对应服务商提供的密钥和基础URL。import openai import json from typing import Optional # 配置客户端 - 以OpenAI为例 client openai.OpenAI(api_keyyour-api-key-here) # 如果使用国内兼容服务例如DeepSeek # client openai.OpenAI(api_keyyour-deepseek-key, base_urlhttps://api.deepseek.com/v1)4.2 定义工具函数与模型调用函数我们定义两个部分真实执行工作的本地函数和负责与LLM通信的对话处理函数。第一步定义真实的工具函数这些函数是你代码库里实际存在的、能执行具体任务的函数。# 模拟一个获取天气的函数。真实场景中这里会调用如和风天气、OpenWeatherMap等API。 def get_current_weather(location: str, unit: str celsius) - str: 模拟获取天气信息。 在实际应用中这里应替换为真实的API调用。 # 模拟数据 weather_data { location: location, temperature: 25 if unit celsius else 77, unit: unit, feels_like: 27 if unit celsius else 80, condition: 晴朗, humidity: 40 } # 将结果转换为JSON字符串便于传递给模型 return json.dumps(weather_data, ensure_asciiFalse) # 可以定义更多工具函数例如查询日历、搜索网络等。 def search_web(query: str) - str: 模拟网络搜索 return json.dumps({results: [f关于{query}的模拟结果1, f关于{query}的模拟结果2]}, ensure_asciiFalse)第二步定义工具列表给模型的“说明书”这是按照OpenAI格式定义的工具列表。# 定义可供模型调用的工具列表 tools [ { type: function, function: { name: get_current_weather, description: 获取指定城市的当前天气状况包括温度、体感温度、天气现象和湿度。当用户询问天气、气温、是否需要带伞、穿衣建议等相关问题时调用。, parameters: { type: object, properties: { location: { type: string, description: 城市和国家的名称例如 中国北京 或 美国旧金山。必须是一个明确的地理位置。 }, unit: { type: string, enum: [celsius, fahrenheit], description: 温度单位。默认为celsius摄氏度。如果用户提到华氏度或F则使用fahrenheit。 } }, required: [location] } } }, { type: function, function: { name: search_web, description: 在互联网上搜索相关信息。当用户询问最新新闻、查找资料、获取未知领域信息时调用。, parameters: { type: object, properties: { query: { type: string, description: 需要搜索的关键词或问题。 } }, required: [query] } } } ]第三步核心对话处理函数这个函数管理整个对话流程处理模型可能发起的函数调用。def run_conversation(user_input: str, conversation_history: list None) - tuple: 运行一次对话轮次处理可能的函数调用。 返回(助理的文本回复, 更新后的对话历史) if conversation_history is None: conversation_history [] # 1. 将用户输入加入历史 conversation_history.append({role: user, content: user_input}) # 2. 首次调用模型传入工具定义 try: response client.chat.completions.create( modelgpt-3.5-turbo, # 或 gpt-4, deepseek-chat messagesconversation_history, toolstools, tool_choiceauto, # 关键让模型自行决定是否调用工具 ) except Exception as e: return f调用模型API时出错{e}, conversation_history response_message response.choices[0].message # 将模型的响应不含工具调用结果先加入历史 conversation_history.append({ role: response_message.role, content: response_message.content or , # 初次响应可能无content tool_calls: response_message.tool_calls # 保存工具调用信息 }) # 3. 检查模型是否想要调用工具 tool_messages [] # 用于存储工具执行结果的消息 if response_message.tool_calls: print(f模型决定调用 {len(response_message.tool_calls)} 个工具。) for tool_call in response_message.tool_calls: function_name tool_call.function.name function_args json.loads(tool_call.function.arguments) print(f正在调用函数: {function_name}) print(f参数: {function_args}) # 4. 根据函数名调度执行对应的本地函数 available_functions { get_current_weather: get_current_weather, search_web: search_web, } function_to_call available_functions.get(function_name) if function_to_call: try: # 执行函数 function_response function_to_call(**function_args) except Exception as e: function_response json.dumps({error: f执行函数{function_name}时出错{str(e)}}) else: function_response json.dumps({error: f函数{function_name}未找到或不可用}) # 5. 将工具执行结果构造成特定格式的消息 tool_messages.append({ role: tool, tool_call_id: tool_call.id, # 必须与请求中的id对应 content: function_response, }) # 将所有的工具执行结果一次性加入对话历史 conversation_history.extend(tool_messages) # 6. 第二次调用模型让它基于工具执行结果生成最终回复 second_response client.chat.completions.create( modelgpt-3.5-turbo, messagesconversation_history, # 此时历史已包含工具执行结果 # 注意第二次调用通常不再需要传递tools参数除非希望模型继续调用新工具 ) final_message second_response.choices[0].message assistant_reply final_message.content # 将助理的最终回复加入历史 conversation_history.append({ role: assistant, content: assistant_reply }) else: # 模型没有调用工具直接使用第一次的回复 assistant_reply response_message.content # 确保将助理回复加入历史如果第一次响应有content if assistant_reply: conversation_history.append({ role: assistant, content: assistant_reply }) return assistant_reply, conversation_history4.3 运行与测试现在我们可以运行一个完整的对话示例。# 初始化对话历史 history [] # 第一轮对话用户询问天气 user_query_1 上海现在的天气怎么样 reply, history run_conversation(user_query_1, history) print(f用户: {user_query_1}) print(f助理: {reply}\n) # 第二轮对话基于上下文的追问模型应能记住location是上海 user_query_2 那需要穿短袖吗 reply, history run_conversation(user_query_2, history) print(f用户: {user_query_2}) print(f助理: {reply}\n) # 第三轮对话切换话题触发另一个工具 user_query_3 帮我搜一下最近关于人工智能的新闻。 reply, history run_conversation(user_query_3, history) print(f用户: {user_query_3}) print(f助理: {reply})预期输出模拟数据用户: 上海现在的天气怎么样 模型决定调用 1 个工具。 正在调用函数: get_current_weather 参数: {location: 上海, unit: celsius} 助理: 上海目前天气晴朗气温25摄氏度体感温度27摄氏度湿度40%。天气状况良好。 用户: 那需要穿短袖吗 助理: 当前气温25摄氏度体感温度27摄氏度天气晴朗。这个温度穿短袖是完全可以的体感会比较舒适。如果您在户外长时间活动短袖搭配一件防晒衣会是不错的选择。 用户: 帮我搜一下最近关于人工智能的新闻。 模型决定调用 1 个工具。 正在调用函数: search_web 参数: {query: 最近 人工智能 新闻} 助理: 根据搜索找到了一些关于人工智能的最新动态。其中包括“关于‘最近 人工智能 新闻’的模拟结果1”和“关于‘最近 人工智能 新闻’的模拟结果2”。目前AI领域在大型语言模型的应用、多模态技术融合以及AI伦理法规方面讨论较多建议您可以关注这些方面的具体报道。这个示例清晰地展示了整个工作流模型理解意图、触发函数调用、程序安全执行、模型整合信息并生成友好回复。5. 常见问题与排查技巧实录在实际开发中你会遇到各种问题。以下是我踩过坑后总结的常见问题与解决方案。5.1 模型不调用函数怎么办这是最常见的问题。请按以下清单排查检查函数描述这是首要原因。模型的“决策”几乎完全基于description。描述必须清晰说明“在什么用户意图下”调用此函数。使用“当用户询问...时调用”这样的句式。模糊的描述如“获取天气数据”会导致调用失败。检查tool_choice参数你是否将其设置为auto如果设为none模型将永远不会调用工具。如果设为{type: function, function: {name: xxx}}则会强制调用指定函数。检查用户输入是否明确如果用户说“今天天气如何”模型可能因为缺少location参数而犹豫。在函数定义中可以将location参数描述为“城市名如果用户未指定请询问用户具体城市”。更好的做法是在你的应用前端引导用户输入更明确的信息。尝试更强大的模型gpt-3.5-turbo的函数调用能力已经不错但gpt-4或gpt-4-turbo在复杂意图理解和多工具调度上显著更优。如果逻辑复杂升级模型是立竿见影的方法。在系统提示词System Prompt中强调在对话开始时发送一条role: system的消息内容如“你是一个有帮助的助手并且可以使用工具来获取实时信息。当用户问题涉及天气、搜索等时请务必使用提供的工具。”这能强化模型使用工具的倾向。5.2 模型调用了错误的函数或参数解析错误参数描述冲突如果两个函数的描述相似模型可能混淆。确保每个函数的描述具有区分度。例如“获取当前天气”和“获取天气预报”是相似的可以将后者描述为“获取指定城市未来几天的天气预报包括每天的最高最低温度、降水概率等。”参数enum枚举值不完整如果参数unit只定义了[celsius]当用户问“旧金山多少华氏度”时模型可能无法正确填充fahrenheit导致调用你的函数时传参错误。确保枚举值覆盖常见情况。JSON解析失败模型返回的arguments是一个JSON字符串。务必用json.loads()解析并做好异常处理。有时模型可能会返回格式略有瑕疵的JSON如尾随逗号可以使用json5库来解析它更宽松。5.3 多轮对话中上下文管理混乱完整保存消息务必保存完整的消息序列包括user,assistant,tool三种角色的消息。特别是tool消息必须包含正确的tool_call_id否则模型无法将结果与之前的请求关联。上下文长度限制长时间的对话可能超出模型的上下文窗口。需要实现“摘要”或“滑动窗口”机制将过于久远的历史对话进行压缩或丢弃只保留最近的关键交互和工具调用结果。避免重复调用有时模型在得到工具结果后下一轮用户提问可能再次触发相同工具的调用。可以在系统提示词中说明“你已经掌握了[某个信息]无需再次查询”或者在历史消息中更清晰地呈现已有信息。5.4 性能与成本优化减少不必要的工具定义每次API调用都携带完整的工具列表会增加Token消耗。如果一次对话中可能用到的工具很多可以考虑动态管理。例如根据对话主题只向模型提供当前最相关的几个工具定义。并行执行工具调用如果模型一次返回多个tool_calls例如同时查询天气和日历且这些工具调用之间没有依赖关系一定要并行执行它们而不是串行。这能显著降低整体响应延迟。可以使用asyncio.gather或线程池来实现。设置超时和重试对真实API的函数调用如网络请求必须设置超时。对于非关键任务可以考虑失败后重试或者提供降级方案如返回缓存数据或友好错误提示。5.5 安全与可靠性永远不要相信模型的输入模型填充的参数arguments必须经过验证和清洗后再传入真实函数。特别是涉及数据库查询、文件操作或系统命令时要防范注入攻击。例如对location参数进行合法性检查确保它只是一个预期的城市名。权限控制不是所有已定义的函数都应该对所有用户开放。需要在应用层面实现权限校验在调度执行本地函数前判断当前用户是否有权调用此函数。设置调用频率限制防止用户通过你的应用恶意、高频地调用收费的第三方API如天气、搜索API导致成本激增。6. 高级模式与最佳实践当你熟悉基础流程后可以探索更高级的应用模式来提升体验和性能。6.1 并行工具调用与流式响应从gpt-3.5-turbo-1106和gpt-4-1106-preview版本开始OpenAI模型支持在单次响应中并行调用多个函数。这非常有用。例如用户问“对比一下北京和上海的天气”模型可以同时发起两个get_current_weather调用参数分别是location: “北京”和location: “上海”。你的程序应该并行执行这两个请求然后将结果一起返回给模型进行总结对比。流式响应Streaming可以与函数调用结合。你可以在首次请求中设置streamTrue。当模型决定调用工具时流式响应会先返回一个包含tool_calls的delta片段。此时你可以暂停流式传输去执行函数待函数执行完毕后再开启一个新的流式请求将工具结果作为历史消息传入让模型以流式方式生成最终回答。这能实现“思考 - 执行 - 回答”全流程的流式体验减少用户等待的焦虑感。6.2 让模型主动询问缺失信息一个健壮的助手应该能处理信息不全的请求。通过精心设计参数描述和系统提示词可以引导模型在参数不足时不直接调用函数而是主动向用户提问。技巧在参数的description中明确写出。例如对于location参数描述可以写为“城市名称。如果用户未明确指定你必须向用户提问以确认具体城市。” 同时在系统提示词中强调“如果调用工具所需的参数不完整请先向用户提问以获取必要信息不要猜测。”这样当用户说“天气怎么样”时模型会回复“请问您想查询哪个城市的天气呢”而不是尝试调用一个缺少location参数的函数这会导致API调用错误。6.3 与Agent框架结合LangChain对于复杂的多步骤任务手动管理对话状态和工具调度会变得非常繁琐。此时使用像LangChain这样的框架是更明智的选择。LangChain的Agent抽象封装了工具调用、决策循环、状态管理的所有复杂性。from langchain.agents import initialize_agent, AgentType from langchain_openai import ChatOpenAI from langchain.tools import Tool # 1. 将本地函数包装成LangChain Tool weather_tool Tool( nameGetCurrentWeather, funcget_current_weather, # 你的本地函数 description获取指定城市的当前天气。输入应为一个城市名。 ) # 2. 初始化LLM llm ChatOpenAI(modelgpt-3.5-turbo, temperature0) # 3. 创建并运行Agent agent initialize_agent( tools[weather_tool], llmllm, agentAgentType.OPENAI_FUNCTIONS, # 使用OpenAI函数调用代理 verboseTrue # 打印详细思考过程便于调试 ) result agent.run(北京和上海的天气哪个更热) print(result)LangChain Agent会自动处理工具选择、参数提取、执行、结果整合的整个循环甚至能处理需要连续调用多个工具的复杂任务如“查一下天气如果下雨就帮我订一辆明早8点去公司的车”。它大大降低了开发门槛。Function Calling机制彻底改变了我们与LLM的交互方式将其从一个纯粹的语言生成器升级为一个可以协调外部能力的“大脑”。掌握它你就拿到了构建下一代AI应用的关键钥匙。从清晰定义工具开始实现一个健壮的调用闭环再逐步处理多轮对话、并行调用等复杂场景最终结合LangChain这类框架迈向成熟的Agent开发。这个过程充满挑战但每当看到你的AI助手能准确调用工具并给出完美回答时那种成就感是无与伦比的。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门