资讯动态

Function Calling与ReAct实战:用大模型构建能调用工具的AI Agent

发布时间:2026/10/22 9:32:29 来源:尧图企业网站定制
最近帮一位朋友调试一个“AI 日程助手”他问了我一个很典型的问题模型能直接帮我查天气、算时间、订会议室吗我的回答是只靠模型本身不行模型只会生成文字不会真的去点按钮。但如果你给模型配上“两只手”——Function Calling 和 ReAct它不仅能查还能在多个工具之间来回切换像真人一样边想边做。如果你最近在学大模型应用开发这两个词一定绕不开。Function Calling 解决的是“模型如何把一句闲聊变成一次可执行的结构化指令”ReAct 解决的是“模型如何在一个需要多步骤的任务里自己决定下一步干什么”。这篇文章不是照抄官方文档而是我自己实际做 Agent 时踩过坑之后总结的一份实操笔记。适合刚接触 AI 应用开发、想在项目里接入工具调用的读者内容尽量保持“看完就能动手试”。1. 先说结论为什么“会聊天”的模型办不了实事1.1 模型的本质是“文字接龙”不是数据库也不是计算器如果你让模型回答“2 的 100 次方是多少”它可能会凭借训练数据里的片段给你一个似乎合理的数字然后你拿去一验证发现错了。这不是模型笨而是它的工作方式是预测下一个词而不是真的执行数学运算。同样你问它“此刻某城市的实时温度是多少”训练数据里根本没有现在的信息它只能编一个。所以想让模型具备“办实事”的能力就需要把“获取信息”和“计算结果”这两件事从模型的语言生成里拆出去交给真实的系统完成。系统返回什么模型就基于什么继续回答。这就像你让一个实习生写报告他不应该凭空捏造数据而应该去数据库查把查到的结果写进报告。1.2 Function Calling 让模型“说出”它想调用的函数Function Calling 不是一个让模型直接执行代码的机制而是一个让模型“描述调用意图”的机制。我们在系统里预先定义好一批函数比如get_weather(city)、add(a, b)然后把函数的名称、含义、参数规则用 JSON Schema 的形式告诉模型。当用户问“阳光城今天多少度”时模型不再直接回答一个编造的温度而是输出类似这样的结构化指令{ name: get_weather, arguments: {\city\: \阳光城\} }然后你的代码接管这个指令实际去天气服务商那边把温度查回来把真实结果再交给模型让模型组织语言回答用户。这个设计的精髓是模型负责“决策”你的代码负责“执行”。各干各的互不干扰。1.3 ReAct 让模型学会“边想边做”有了 Function Calling模型可以做单次工具调用但很多任务不是一步就结束的。例如用户问“阳光城和雨城今天温差有多大”模型需要先查 A 城市的温度再查 B 城市的温度可能还需要调用一个计算器做减法最后把结果描述出来。如果模型只能调一次函数这个任务就卡住了。ReAct 的全称是 Reasoning Acting它模仿人处理问题的过程先思考Thought再行动Action然后观察结果Observation再继续思考。整个过程是一个循环Thought用户想知道温差我需要先拿到两个城市的温度Action调用get_weather(city阳光城)Observation阳光城 28℃Thought还差雨城的温度Action调用get_weather(city雨城)Observation雨城 18℃Thought温差是 10℃Action直接回答这个过程听起来简单但它恰好是当前所有主流 Agent 框架的底层骨架。把上面这个循环用代码写一遍再套上 Function Calling 的结构化输出就是一个五脏俱全的迷你智能体。2. 原理拆解模型、工具与推理循环是怎么协作的2.1 Function Calling 的完整数据流我把一次完整的 Function Calling 拆成五个环节每一步缺一不可。第一步开发者定义工具。工具包括函数名、描述、参数结构。描述非常重要模型是根据描述来决定要不要用这个工具的。第二步把工具列表放进请求消息里发送给模型。第三步模型判断用户问题是否需要调用工具如果不需要就直接回答如果需要就返回tool_calls字段。第四步你的代码监听到tool_calls后执行对应的函数拿到真实结果。第五步把结果以tool角色的消息追加进对话上下文再发给模型让模型基于结果生成最终回答。这里最容易误解的点是模型返回的arguments是一个字符串不是直接可用的 Python 对象。你必须先json.loads解析才能拿到真正的参数。另一个常见误解是模型不会真的去执行函数执行函数永远是你的业务代码。下面用一段通用格式的代码描述这个过程不同平台的协议大同小异这里不依赖任何特定厂商 SDKimport json import requests API_HOST https://api.example.com/v1/chat/completions API_KEY 在这里填你的密钥 def call_model(messages, tools): payload { model: your-model-name, messages: messages, tools: tools, temperature: 0, } headers {Content-Type: application/json} resp requests.post(API_HOST, headersheaders, jsonpayload) return resp.json() def run_function(name, arguments): args json.loads(arguments) if name get_weather: # 这里换成真实天气查询逻辑 return 28 摄氏度晴天 if name calculate: # 这里换成真实计算逻辑 return str(eval(args[expression]))2.2 ReAct 循环里的三个关键角色ReAct 循环看起来只是“多了一步”但它背后涉及两个关键设计状态维持和退出条件。状态维持靠的是对话消息历史。每一轮工具调用的结果都被追加到messages里模型在下一次请求时能看到完整的推理过程。这意味着消息列表就像模型的工作草稿纸写满了它每一步的思考、动作和观察结果。如果某一步把消息历史截断了模型就会“失忆”。退出条件则是循环的终止逻辑。通常有两种退出模型返回了普通的文本回答而不是工具调用或者达到了我们设置的最大轮数。第二种情况尤其重要没有最大轮数模型可能会在一件简单的事情上反复调用同一个函数白白消耗你的 API 配额。用户问题 ↓ 模型Thought/Action工具调用 ↓ 执行工具 → Observation ↓ 把 Observation 加入上下文 ↓ 再次调用模型 ↓ 模型输出最终回答 → 结束2.3 Function Calling 和 ReAct 为什么必须搭配如果只有 Function Calling模型能调用工具但缺少“根据观察结果继续推理”的循环遇到多步任务就断掉了。如果只有 ReAct模型确实会循环思考但它的“行动”只能靠生成一段文本动作你还需要用正则表达式去解析格式稍微一变就崩。两者组合后ReAct 负责循环和推理框架Function Calling 负责提供稳定、规范的工具调用接口。一个管大脑一个管手脚。可以说Function Calling 更像是 ReAct 这个循环里“Action”环节的实现方式而 ReAct 则是让 Function Calling 能在复杂任务里持续发挥作用的“指挥棒”。理解这点你就理解了目前大部分 Agent 产品背后的基本逻辑。3. 从零实现一个 ReAct Function Calling 的迷你智能体3.1 环境准备与整体设计后面我会带你把上面这个循环写成能跑的代码。请确保你的环境里有 Python 3.9 以上版本和requests库。如果你有自己的模型 API直接把地址、密钥、模型名替换成你的如果没有先用本地模拟的方式跑通逻辑重点理解流程。我们先把整个程序分成三块工具定义、工具执行、主循环。工具定义是给模型看的函数说明书工具执行是实际干活的 Python 函数主循环负责调度。这样拆分的好处是将来你要加几十个工具只需要往注册表里加一条记录主循环代码完全不用改。3.2 定义两个实战工具天气查询和计算器先体验驱动的例子做一个有点意思的任务用户问“阳光城今天温度多少雨城呢两个地方温差多少”这就需要三个能力查阳光城天气、查雨城天气、计算温差。为了演示我把“计算”也做成一个工具而不是让模型直接心算这也是很多生产系统的常见做法。工具定义我用标准的 JSON Schema。需要注意想让模型准确调用必须在描述里写清楚每个参数的含义甚至给一个示例值。模型不是人它不会自己去猜“city”到底该填中文名还是英文名。TOOLS [ { type: function, function: { name: get_weather, description: 查询一个城市的当前温度返回单位为摄氏度。城市名请输入中文例如阳光城, parameters: { type: object, properties: { city: { type: string, description: 城市名例如阳光城、雨城 } }, required: [city] } } }, { type: function, function: { name: calculate, description: 执行两个数字的加减乘除四则运算。输入格式为表达式例如28-18, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如28-18 } }, required: [expression] } } } ]接着写工具的执行函数。这里我只是做最小实现真实项目中每个函数都应该有完整的异常处理和参数校验。def execute_tool(name, arguments): args json.loads(arguments) if name get_weather: # 实际项目中替换成天气服务 API 调用 fake_db {阳光城: 28, 雨城: 18, 多云城: 22} return f{args[city]}当前气温 {fake_db.get(args[city], 未知)} 摄氏度 if name calculate: # 注意生产环境禁用 eval这里仅为演示 expr args[expression] try: return str(eval(expr)) except Exception as e: return f计算失败{e} return 未找到工具3.3 主循环让模型自己决定下一步调用谁主循环是整个系统的核心。它的逻辑并不复杂把用户消息发给模型如果模型返回tool_calls就先执行工具把结果追加回消息列表然后继续循环如果模型返回普通文本就把它作为最终答案输出。这里的代码我故意保留了几个关键字段方便你对照模型 API 的实际返回值。不同平台的字段名会有差别但结构基本一致。def run_agent(user_input): messages [{role: user, content: user_input}] max_steps 6 step 0 while step max_steps: response call_model(messages, TOOLS) message response[choices][0][message] if message.get(tool_calls): # 记录这次包含工具调用的消息保持上下文完整 messages.append(message) for tool_call in message[tool_calls]: function tool_call[function] print(f[代理执行] 调用 {function[name]}({function[arguments]})) result execute_tool(function[name], function[arguments]) # 工具结果用 tool 角色返回并用 tool_call_id 关联 messages.append({ role: tool, tool_call_id: tool_call[id], content: result }) step 1 continue # 没有 tool_calls 说明模型准备直接回复了 print([代理回答], message[content]) return message[content] print([达到最大迭代次数强制终止]) return 处理超时请稍后再试或简化任务 run_agent(阳光城今天多少度雨城呢两座城市的温差是多少)这段代码执行的时候你会看到类似下面的日志调用 get_weather(city阳光城)调用 get_weather(city雨城)调用 calculate(expression28-18)最终回答阳光城 28 度雨城 18 度温差 10 度。这就是一次完整的 ReAct 循环。模型的思考过程完全依赖消息历史所以你不需要自己维护什么状态机只要按顺序把内容追加进messages就可以了。3.4 关键参数和生产级优化细节循环跑通之后有几个参数值得你反复调。温度temperature建议调成 0。工具调用需要确定性如果温度太高模型可能一会儿想用计算器一会儿想用天气接口回答不稳定。我见过因温度调到 1模型在一个简单任务里选了完全错误的工具。最大迭代次数我一般初版设 5跑通之后再按实际需要调整。迭代次数设得越多API 费用越高响应时间越长。一个保险做法是把每次工具调用的日志都打印出来手动观察这个任务实际需要几步再留 1 到 2 次余量。上下文长度每次工具结果都会留在消息列表里任务越长上下文越大。对于大型结果例如文件内容或长文本千万不能原样塞回去否则很快会顶到模型的上下文上限。我做了一个简单的安全函数把超过 500 字符的内容截断到 500 字符并且提示“内容过长已截断”。生产项目建议用摘要或检索来做。4. 实操中的五个常见坑以及我的排查方案4.1 模型总是编造不存在的参数这是我在工具接入第二天就遇到的问题。我定义了一个查询日期的工具要求传入start_date和end_date格式必须是YYYY-MM-DD。模型在用户只说了“看一下本周的数据”时自己把日期填成了上周一到上周日。用户看到结果后一脸问号。排查思路是这样的先看日志里模型填的参数是不是从用户话语里提取的如果不是多半是模型“脑补”的。解决办法有两个层面。第一层是在函数描述里写清楚“如果用户没有明确指定日期请先向用户询问日期不要自行推断。” 第二层是在代码里做参数校验解析完参数后如果发现关键字段缺失或格式不对直接返回一个错误给模型比如“缺少必要参数 start_date请向用户确认”。模型看到这个错误后通常会纠正自己的行为补一句“请您提供起始日期”。4.2 工具返回内容太大直接把上下文撑爆有一次我让一个文档分析工具把整篇 PDF 的内容返回给模型结果第一次调用后消息列表里就多了几十万字符后续请求直接报“上下文长度超限”。这个问题不只在本地模型上有用商业模型也一样。我后来定了一条规矩工具返回给模型的结果必须控制在模型可以消化的大小内。如果是文档先做分段摘要只给结论如果是数据库查询先做聚合统计如果必须返回原始内容就按关键字抽取相关片段。宁可多调用几次模型做二次整理也不要一次性塞入超长文本。你可以在工具执行函数里加一个结果后处理层让每个工具都支持“精简模式”。4.3 死循环模型反复调用同一个函数最让人崩溃的是模型在同一个函数上调了一遍又一遍每次都拿着一样的参数结果也一样。原因通常是模型觉得这个结果不是它想要的但又不知道该换什么方法。例如它调用计算器计算表达式后发现结果和预期不一致于是再算一遍无限循环。我的对策是三重保险。第一在函数返回结果里加入提示例如“这是你已经查询过的最新结果无需重复查询”。第二在代码里检测重复的工具调用如果同一个函数名和参数组合出现了两次直接返回“该信息已获取结果是 XXX请不要重复调用此函数”。第三设置最大迭代次数运行到上限就强制终掉并给用户一个友好提示。三层叠加之后死循环基本被消灭了。4.4 工具并发调用与依赖关系处理有些模型支持一次返回多个工具调用比如同时查两个城市的天气。这就带来一个选择要不要把所有工具并行执行如果工具之间没有依赖关系并行执行确实能节省时间。但如果有依赖比如我想先查询用户身份再根据身份查专属数据第二工具依赖第一个工具的结果这就不能并行。我的处理原则是无依赖的并发有依赖的串行。具体到 ReAct 主循环里如果一次返回了多个tool_calls可以先判断依赖关系。判断方式很简单前一个工具的结果是否是后一个工具参数的来源如果是就把这批拆成两次循环第一次只执行前一个拿到结果后再走下一轮如果不是可以并行执行。初学阶段最稳妥的做法是先串行执行所有工具把结果依次追加到消息里稳定性优先。4.5 权限和安全工具不是越全越好在一次内部测试中我给模型加了一个“删除用户数据”的工具结果模型在用户说“帮我清理一下测试数据”时真的给删了。虽然数据能恢复但这事给我敲了一记警钟模型没有“意识”它分不清哪些操作是低风险哪些是高风险的。安全设计必须前置。我的建议是给工具分级只读操作如查询天气、搜索文档可以让模型自主调用写操作如发送邮件、修改配置必须经过用户确认危险操作如删除数据、转账、下订单应该直接禁止由模型自动执行或者接入一个人工审批环节。实现上并不复杂在execute_tool里加一个risk_level字段执行前判断是否需要暂停等待用户确认。这个机制在我的所有项目里都保留了。5. 把原型变成生产可用工具注册表与可观测性5.1 用一个注册表代替手工 if-else前面代码里的execute_tool全靠一个个if判断如果工具数量超过 10 个这段代码就会变得又臭又长。更优雅的做法是设计一个统一的工具注册表用装饰器把函数自动注册进去同时根据函数签名生成 JSON Schema。TOOL_REGISTRY {} def register_tool(schema): def decorator(fn): TOOL_REGISTRY[fn.__name__] (schema, fn) return fn return decorator register_tool({ type: function, function: { name: get_weather, description: 查询一个城市的当前温度, parameters: { type: object, properties: { city: {type: string} }, required: [city] } } }) def get_weather(city: str): return f{city} 28 摄氏度这样以后新增工具时只需要定义函数并写清楚 Schema注册表会自动收集所有工具列表传给模型execute_tool也能通过工具名找到对应的执行函数。我建议每个工具函数内都写清晰类型注解因为后续你想让代码根据类型注解自动生成 Schema 时这会是巨大的便利。5.2 做一层工具结果缓存省钱又提速同一个模型应用很可能在短时间内被问到相似问题。比如早晨多个用户问同一座城市的天气如果每次都调用实时天气 API费用和延迟都会白白浪费。我会在系统里加一层简单缓存以“函数名 参数哈希”作为 key缓存有效期按工具类型区分天气可以缓存 10 分钟计算器可以缓存永久行情类数据可能只能缓存 60 秒。缓存层还能解决一部分重复调用问题。当模型在循环里反复查同一个城市时第二次直接就命中缓存速度更快也不会触发外部接口限流。需要注意缓存一定要设置过期时间我之前天真地给股票行情做了缓存结果用户看到的是半小时前的价格差点被投诉。5.3 高风险操作回退到人工确认生产环境里模型自动决策必须谨慎。我建议在设计工具时就把“人”纳入流程比如发送邮件、修改订单、删除文件等操作可以让模型生成一个“指令草稿”然后以按钮或卡片的形式推送给用户确认用户点了“确认”之后执行函数。实现方式并不需要改变 ReAct 主循环太复杂只需在execute_tool返回结果之前做一次“确认标记”。例如工具返回内容为“等待用户确认确认要删除订单 12345 吗”然后把这条消息作为观察结果返回给模型让模型在下一轮向用户确认。这样做虽然增加了一轮交互但换来了安全和信任。5.4 可观测性把每一步推理痕迹记下来没有日志的 Agent 就像没有黑匣子的飞机出了问题根本不知道模型到底怎么想的。我的经验是上线后的第一个版本必须包含详细的日志系统至少记录下面这些内容每轮请求的原始消息条数和字符数模型返回的tool_calls原始 JSON工具执行的函数名、输入参数、返回结果、耗时最终回答内容和总调用步数我会把日志打成 JSON Line 格式方便后续用日志分析工具去统计。有了这些日志分析“模型为什么调用这个工具”就不再是玄学而是一个可以回放的视频。调试 Agent 时要记住模型是一个黑盒你唯一能控制的是输入和观察输出日志就是你的眼睛。6. 学习路径建议从第一行工具代码开始6.1 先做一个“给模型加个计算器”的小项目很多读者一上来就照着框架写复杂 Agent结果还没跑通就死在依赖安装和环境配置上。我的建议相反先做你能想到的最简单工具增强。比如用本文的代码只定义一个calculate工具让模型帮你算“(123456) * 789”。等这个跑通了再去加第二个、第三个工具。这个过程会帮你建立最核心的“上下文感”你能亲眼看到模型返回工具调用、你的代码执行工具、结果还原回上下文、模型基于结果回答。这个基本流程理解了其他所有 Agent 框架对你来说都只是换了一种包装。6.2 把模型每一次输出日志当学习材料学习 Function Calling 和 ReAct最好的教材是自己跑出来的运行日志。我会建议你每跑一个任务都回去看一遍消息列表模型是怎么理解工具描述的它生成了什么参数如果出错错在哪一步。很多模型的行为规律只有在这个粒度才能看清楚。比如你会发某些模型总是忽略工具描述里的“请先询问用户”那么你就知道要在系统提示词里反复强调。你会发模型在参数里填了奇怪的格式那么你就会知道要在 JSON Schema 里写示例。这些都是文档里学不到的实战经验只能靠日志积累。6.3 先跑通再选框架不要本末倒置现在市面上的 Agent 开发框架有很多每个框架都有自己的概念和抽象。如果基础不牢直接上框架你很容易被“工具节点”“计划器”“反思层”这些概念绕晕。我的经验是先用原生代码把 ReAct 循环从 0 写一遍再去看框架你会瞬间明白框架里每个模块正在做什么。之后再决定要不要用框架、用哪个就有底气多了。另外模型的选择也很关键。支持 Function Calling 的模型不一定都支持同一套参数格式有的返回命名不同有的不支持并行调用。我的建议是最初就用一个你最顺手、文档最全的模型把主循环调通等理解透了再切换模型或平台。这样你学的是通用能力而不是绑定在某一家产品的版本细节上。我在实际项目里已经很少从零手写 ReAct 循环了但每次遇到奇怪的调用问题我都还会回去看最核心的消息列表和日志。因为不管框架怎么变底层的“思考-行动-观察”循环始终没变Function Calling 的“模型决策代码执行”边界也始终没变。把这两点刻在脑子里你后续学任何 Agent 技术都会顺很多。如果这篇文章帮你跑通了第一个工具那就已经值回你花在这里的时间了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑