AI Agent实战:从Function Calling原理到Python工具调用实现
1. 从“聊天”到“做事”为什么我们需要AI Agent如果你最近关注AI领域可能会发现一个明显的趋势大家不再满足于让大模型仅仅当一个“聊天高手”或“文案生成器”。我们开始希望它能真正“做事”——比如让它帮你查一下明天的天气然后根据天气自动调整你的日程安排或者让它分析一份财报PDF提取关键数据并生成可视化图表。这种能感知环境、进行规划、并调用工具执行任务来完成目标的AI系统就是我们常说的AI Agent。简单来说AI Agent让大模型从一个“思想家”变成了一个“实干家”。它的核心能力在于工具调用。大模型本身就像一个知识渊博但“手无寸铁”的顾问它知道很多但无法直接操作外部世界。而工具调用就像是给这位顾问配上了一双灵巧的手和各种专业设备比如计算器、搜索引擎、代码执行器、API接口让它能真正将想法落地。那么如何让大模型学会“使用工具”呢这正是Function Calling函数调用要解决的问题。它本质上是一套标准化的“沟通协议”。我们预先定义好一系列工具每个工具就是一个函数有明确的名称、描述和参数格式然后告诉大模型“嘿这些工具你可以用。当用户提出需求时如果你判断需要用到某个工具就按照我规定的格式告诉我你要调用哪个工具以及传入什么参数。” 随后我们的程序接收到这个“调用请求”就去真正执行这个函数比如调用天气API拿到结果后再把结果返回给大模型由它整合信息最终回复给用户。这个过程听起来简单但却是构建实用AI应用的基石。无论是做一个能自动处理邮件的智能助手还是一个能联网搜索、分析数据的分析Agent都离不开Function Calling。它直接决定了你的Agent是否“智能”、是否“有用”。网上很多关于AI Agent的讨论无论是学习路线、开发框架选择还是部署、测试的难题其核心都绕不开如何高效、稳定地实现工具调用。2. Function Calling 深度解析大模型的“工具使用说明书”在深入代码之前我们必须先彻底理解Function Calling的运作机制。这不仅仅是“调用一个API”那么简单它涉及到大模型如何理解任务、如何匹配工具、如何生成结构化请求这一整套认知推理过程。2.1 核心机制从自然语言到结构化请求想象一下你作为项目经理向一位技术专家大模型布置任务。你不会直接说“执行函数A参数是x1, y2”。你可能会说“请比较一下我们产品上周和这周的日活跃用户数看看增长趋势如何。”Function Calling机制让大模型能够完成这样的翻译工作理解意图大模型首先解析你的自然语言指令理解你的核心诉求是“比较数据”和“分析趋势”。工具匹配它会在你预先提供的“工具清单”里寻找能完成此任务的功能。假设我们提供了一个名为get_weekly_active_users的函数描述是“获取指定产品在指定时间范围内的日活跃用户数列表”。参数提取与结构化大模型会从你的指令中提取关键信息作为参数。比如从“上周和这周”推断出需要两个时间范围参数从“我们产品”推断出产品ID如果上下文中有。然后它严格按照函数定义的参数格式JSON Schema生成一个结构化的调用请求例如{ name: get_weekly_active_users, arguments: { product_id: prod_123, start_date: 2024-05-20, end_date: 2024-05-26 } }紧接着它可能还会生成另一个调用请求用于获取再上一周的数据。这个过程的精妙之处在于大模型并非机械地关键词匹配而是基于对函数描述和用户指令的语义理解来进行逻辑推理。它知道“比较增长”需要至少两组数据因此会发起多次调用。2.2 与LangChain工具调用的区别在AI应用开发中LangChain是一个无法绕开的流行框架。它同样提供了强大的工具调用能力那么它和原生Function Calling有什么区别呢核心区别在于抽象层级和设计哲学原生Function Calling是大模型提供商如OpenAI、Anthropic、国内各大模型厂商在模型层面直接支持的一种协议。它更底层、更标准化。你直接与大模型的API对话告诉它有哪些函数可用模型返回结构化的调用请求然后由你的程序去执行。它的速度主要受网络延迟和大模型本身生成响应的速度影响。LangChain的工具调用是一个高级框架。它在原生Function Calling之上或之旁构建了一层抽象。LangChain帮你封装了工具的定义、调用链的编排、历史会话的管理等复杂逻辑。当你使用LangChain的bind_tools和invoke时它底层可能也是在调用大模型的Function Calling能力但为你处理了很多样板代码。LangChain工具调用的速度除了受上述网络和模型影响外还受到其自身框架复杂度和中间件数量的影响。如果你构建了一个很长的处理链Chain每个环节都可能引入开销。如何选择追求极简、可控和高性能如果你的应用场景相对直接工具数量不多且你希望完全掌控调用流程和错误处理那么直接使用大模型原生的Function Calling API是更佳选择。代码更清晰依赖更少。需要快速构建复杂流程如果你的应用涉及多步骤推理、需要组合多种工具、或者要方便地切换不同的大模型提供商那么LangChain提供的抽象能极大提升开发效率。它用一定的性能开销换来了开发速度和灵活性。对于入门实战而言我强烈建议从原生Function Calling开始。这能让你最直观地理解整个交互流程的每一个环节打下坚实的基础未来再根据需求考虑是否引入LangChain这类框架。2.3 工具描述的艺术写出清晰的“说明书”大模型能否正确调用工具很大程度上取决于你如何描述这个工具。一个模糊的描述会导致模型“误解”工具的用途。糟糕的描述示例tools [ { type: function, function: { name: query_data, description: 一个查询数据的函数, # 过于模糊 parameters: {...} } } ]用户说“今天上海热吗” 模型可能无法判断该用这个函数因为描述没有说明是查询什么数据。优秀的描述示例tools [ { 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: 温度单位默认为摄氏度celsius。, default: celsius } }, required: [location] } } } ]这个描述明确了函数的用途、返回的信息、参数的格式和规则。模型看到后就能准确地将“上海热吗”映射到需要调用get_current_weather并传入location: “上海”。注意参数描述同样重要。对于location我们强调要“明确的行政区划名称”这能减少模型传递“东方明珠附近”这类模糊地址的概率提高调用成功率。3. 实战构建一个能查天气、算数学、搜网页的Python AI Agent理论说得再多不如动手一试。我们将使用OpenAI的GPT模型兼容此协议的其他模型如DeepSeek、通义千问等同样适用和Python构建一个具备三种基础能力的AI Agent。3.1 环境准备与初始化首先确保你的Python环境在3.8以上。我们将使用openai这个官方库。如果你打算未来尝试本地模型ollama也是一个不错的选择它提供了类似OpenAI的API接口可以让你在本地运行Llama、Qwen等模型。# 安装必需库 pip install openai接下来初始化你的OpenAI客户端。你需要一个API密钥可以从OpenAI平台获取。切记不要将密钥硬编码在代码中或上传到GitHub使用环境变量是标准做法。import os import json from openai import OpenAI from dotenv import load_dotenv # 可选用于加载.env文件 # 加载环境变量如果你将API_KEY放在.env文件中 load_dotenv() # 初始化客户端 client OpenAI( api_keyos.getenv(OPENAI_API_KEY) # 从环境变量读取 ) # 定义一个简单的对话历史记录用于实现多轮对话 conversation_history []3.2 定义我们的“工具包”我们将为Agent装备三个工具获取天气模拟调用一个天气API。执行计算利用Python的eval生产环境请慎用或替换为安全计算库进行数学计算。搜索网络模拟调用搜索引擎API。# 工具定义列表 tools [ { type: function, function: { name: get_weather, description: 获取指定城市的当前天气信息。, parameters: { type: object, properties: { city: { type: string, description: 城市的中文或英文名称如北京、Tokyo。 } }, required: [city] } } }, { type: function, function: { name: calculate, description: 执行一个数学表达式计算支持加减乘除、乘方和括号。, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如(12 5) * 3 / 2 } }, required: [expression] } } }, { type: function, function: { name: search_web, description: 在互联网上搜索给定的查询词条返回相关的摘要信息。, parameters: { type: object, properties: { query: { type: string, description: 需要搜索的关键词或问题。 } }, required: [query] } } } ] # 实现具体的工具函数 def execute_tool(function_name, arguments): 根据函数名和参数执行对应的工具 if function_name get_weather: city arguments.get(city) # 这里模拟返回真实情况应调用如和风天气、OpenWeatherMap等API return f{city}的天气模拟数据晴温度25°C湿度60%。 elif function_name calculate: expr arguments.get(expression) try: # 警告生产环境使用eval有安全风险应替换为ast.literal_eval或专用数学库 result eval(expr) return f计算结果{expr} {result} except Exception as e: return f计算错误{e} elif function_name search_web: query arguments.get(query) # 模拟搜索返回 return f关于{query}的模拟搜索结果这是一个非常热门的话题涉及人工智能的多个子领域。 else: return f未知工具{function_name}3.3 核心对话循环发起请求、处理响应、执行工具这是Agent的大脑中枢。我们将创建一个循环不断接收用户输入发送给大模型检查是否需要调用工具执行工具并将结果返回给模型进行总结。def run_agent_conversation(user_input): 运行一轮Agent对话 global conversation_history # 1. 将用户输入加入历史 conversation_history.append({role: user, content: user_input}) # 2. 发送请求给大模型并告知它可用的工具 response client.chat.completions.create( modelgpt-3.5-turbo, # 或 gpt-4, gpt-4-turbo messagesconversation_history, toolstools, tool_choiceauto, # 让模型自动决定是否及调用哪个工具 ) # 3. 获取模型的回复消息 message response.choices[0].message # 将模型的回复可能包含工具调用加入历史 conversation_history.append(message) # 4. 检查回复中是否包含工具调用请求 tool_calls message.tool_calls if tool_calls: print(f[Agent] 我需要使用一些工具来帮你...) # 可能有多个工具调用并行 for tool_call in tool_calls: function_name tool_call.function.name function_args json.loads(tool_call.function.arguments) print(f - 调用工具 {function_name} 参数{function_args}) # 5. 执行工具 tool_result execute_tool(function_name, function_args) print(f - 工具返回{tool_result[:100]}...) # 打印部分结果 # 6. 将工具执行结果作为一条新消息追加到历史中告诉模型 conversation_history.append({ role: tool, tool_call_id: tool_call.id, content: tool_result, }) # 7. 再次调用模型让它基于工具结果生成最终回答 second_response client.chat.completions.create( modelgpt-3.5-turbo, messagesconversation_history, ) final_message second_response.choices[0].message conversation_history.append(final_message) assistant_reply final_message.content else: # 没有工具调用直接使用模型的回复 assistant_reply message.content return assistant_reply # 运行一个简单的例子 if __name__ __main__: print(你好我是你的AI助手我可以查天气、做计算、搜网页。) while True: try: user_input input(\n你) if user_input.lower() in [退出, exit, quit]: print(再见) break reply run_agent_conversation(user_input) print(f\n助手{reply}) except KeyboardInterrupt: print(\n程序被中断。) break运行这段代码你就可以和你的第一个AI Agent对话了。试试以下问题“北京和上海的天气怎么样” 它会顺序或并行调用两次get_weather“计算一下(15的平方加上28)除以3等于多少” 调用calculate“搜索一下什么是AI Agent” 调用search_web你会看到控制台打印出模型决定调用工具的过程、执行的参数以及最终整合后的回答。4. 避坑指南与进阶优化从“跑通”到“好用”让Agent跑起来只是第一步。在实际开发中你会遇到各种边界情况和性能问题。下面是我在项目中踩过的一些坑和总结的优化经验。4.1 常见问题与排查思路问题1模型不调用工具而是直接“瞎编”答案。可能原因工具描述不够清晰或者用户问题过于简单模型觉得用自己的知识就能回答。排查与解决检查工具描述确保description字段准确描述了工具的功能和适用场景。可以加上“请使用本工具获取实时/准确信息”等引导词。调整tool_choice参数如果你明确希望模型必须调用某个工具可以将tool_choice设置为{type: function, function: {name: your_tool_name}}。auto模式是让模型自主选择。优化系统提示System Prompt在对话历史开头加入一条role: “system”的消息明确指示模型“你是一个助手可以调用工具来获取实时信息或执行计算。当用户的问题涉及天气、计算或需要最新信息时请务必使用相应的工具。”问题2模型提取的参数格式错误或内容不对。可能原因参数的定义JSON Schema不够严格或者模型对用户指令的理解有偏差。排查与解决强化参数约束充分利用JSON Schema的能力。对于城市名可以尝试提供enum枚举列表如果城市范围固定。对于日期严格定义format: “date”。使用pattern定义正则表达式来约束字符串格式。提供更详细的参数描述在参数的description字段中给出明确的示例和格式要求。例如“日期格式必须为YYYY-MM-DD例如2024-05-27”。后置参数清洗与校验在execute_tool函数中不要完全信任模型传来的参数。加入校验逻辑比如检查城市名是否在支持列表中日期是否合理。如果校验失败可以将错误信息作为工具执行结果返回给模型让它重新尝试或向用户澄清。问题3多轮对话中工具调用历史混乱。可能原因conversation_history无限制增长导致模型混淆上下文或者tool_call_id匹配错误。排查与解决管理对话历史长度大模型有上下文窗口限制。需要设计一个策略来裁剪或总结过长的历史。例如只保留最近N轮对话或者将遥远的对话总结成一段摘要。确保ID匹配当模型发起一个工具调用时会生成一个唯一的tool_call_id。你返回结果时必须使用完全相同的id这样模型才能将结果与对应的请求正确关联。清晰的角色标识严格遵循OpenAI的消息角色格式user,assistant,system,tool。tool角色的消息必须包含正确的tool_call_id。4.2 性能与稳定性优化策略1. 并行工具调用上面的示例代码是顺序执行工具调用的。如果两个工具调用之间没有依赖关系比如同时查询北京和上海的天气模型可能会在第一次回复中一次性返回多个tool_calls。我们应该利用asyncio等机制并行执行这些独立调用以缩短整体响应时间。2. 流式输出Streaming对于需要长时间运行的工具如复杂的网络搜索或数据处理可以考虑使用流式输出。先让模型返回一个“正在处理”的文本然后在后台执行工具工具执行完毕后再以流式或单独消息的形式推送最终结果。这能极大提升用户体验。3. 结构化输出与验证Function Calling 本身要求模型输出结构化JSON。我们可以利用这一点不仅用于工具调用还可以强制模型以特定格式如包含“思考链”、“最终答案”、“置信度”的JSON来回答所有问题。这能让后续的程序处理更可靠。Pydantic库与OpenAI的结合可以很好地实现这一点。4. 后备与降级策略网络可能超时API可能限流工具可能失败。一个健壮的Agent必须有后备方案。工具调用失败捕获异常返回一个友好的错误信息给模型让模型决定是重试、换一种方式回答还是坦诚地告诉用户工具暂时不可用。模型API失败考虑设置重试机制使用指数退避或者准备一个轻量级的备用模型如本地部署的较小模型来接管简单的对话。4.3 安全考量重中之重计算安全我们的示例中使用了eval()这是极其危险的因为它会执行任何传入的Python代码。用户输入“__import__(‘os’).system(‘rm -rf /’)”就会导致灾难。在生产环境中绝对禁止使用eval。替代方案包括使用ast.literal_eval()只能评估字面量表达式安全很多。使用专门的数学表达式解析库如numexpr。自己编写安全的解析器只允许白名单内的操作符和函数。权限控制你的Agent能调用哪些工具应该与用户的权限绑定。一个普通用户不应该能调用“删除数据库”或“发送全员邮件”这样的工具。需要在execute_tool函数内部或之前加入权限校验逻辑。输入输出过滤对模型生成的内容和用户输入的内容进行必要的过滤和审查防止注入攻击或生成不当内容。5. 超越基础构建复杂Agent系统的思路当你掌握了单个Agent的工具调用后就可以向更复杂的系统迈进。这通常被称为“多智能体系统”或“智能体工作流”。1. 规划与执行分离一个高级的Agent不会立刻调用工具。它会先进行“思考”或“规划”。例如当收到“帮我策划一次北京三日游”的请求时一个具备规划能力的Agent可能会规划步骤1. 搜索北京热门景点。2. 查询景点间的距离和交通。3. 查询未来三天的天气。4. 根据以上信息排定日程。5. 估算预算。顺序执行然后按照这个计划一步步调用相应的工具并将上一步的结果作为下一步的输入。你可以通过设计系统提示词“你是一个旅游规划专家请先制定步骤再执行”或使用专门的“规划器”模型/模块来实现这一点。2. 工具的动态注册与管理在更复杂的系统中工具可能不是静态的。新的插件可以被安装工具的功能可能根据上下文变化。你需要设计一个“工具注册中心”Agent在运行时可以查询当前可用的工具列表。这类似于harness的概念——一套包裹在AI Agent核心推理逻辑之外的基础设施层它不代替Agent做决策但为Agent提供稳定的工具发现、调用和生命周期管理服务。3. 与本地大模型集成如果你关注数据隐私或成本可能会考虑使用本地部署的大模型如通过Ollama运行的 Llama 3、Qwen 或 DeepSeek Coder。好消息是许多优秀的本地模型已经开始支持 OpenAI 兼容的 API 和 Function Calling 协议。使用Ollama部署好Ollama并拉取模型后你只需要将代码中OpenAI客户端的base_url指向你的本地服务如http://localhost:11434/v1并将api_key设为任意非空字符串即可。大部分支持Function Calling的模型如llama3.1的某些版本、qwen2.5:7b-instruct都能以类似的方式工作。注意事项本地模型的工具调用准确性和指令遵循能力可能弱于顶尖的云端模型需要进行更多的测试和提示词优化。参数定义要尽可能清晰、简单。4. 技能Skill与工作流编排一个强大的Agent往往拥有众多“技能”Skill每个技能可能由多个工具调用和逻辑判断组成。例如“数据可视化”技能可能需要先调用“查询数据库”工具再调用“生成图表”工具。这就需要更高层级的编排框架如LangChain、Semantic Kernel或AutoGen它们可以帮助你定义复杂的工作流、管理Agent间的对话和状态。从Function Calling入门到构建一个能可靠完成复杂任务的AI Agent这条路充满了挑战但也极具成就感。核心在于理解“让大模型学会使用工具”这一范式转变并扎实地处理好工具定义、调用、错误处理和安全性每一个环节。我个人的体会是开始时尽量保持简单先让核心链路跑通再逐步叠加复杂性和健壮性。每次成功让模型准确调用工具并解决问题都像是教会了它一项新技能这种体验是单纯文本对话无法比拟的。