去年我在做一个内部数据看板工具时遇到一个很现实的问题大模型聊得挺好但它没法替我去查数据库、拉接口、算指标。我只能在提示词里把数据贴进去让它做个“文字复读机”稍微复杂一点的动态查询就完全没辙。后来我把Function Call引入到Agent流程里情况才彻底逆转。大模型不再只是个“聊天窗口”而是变成了一个能自己决定“下一步该调用什么工具、传什么参数、拿什么结果继续推进”的调度中枢。这篇内容就是围绕LLM Agent和Function Call这两个核心概念做一次比较完整的拆解。我会用真实的项目经验来讲清楚Agent到底是怎么运转的Function Call在中间扮演什么角色一个能跑起来的Agent程序该怎么搭以及我在实际项目中踩过哪些坑、总结出哪些规则。无论你是刚接触大模型应用开发还是已经在做Agent相关项目这篇都有可落地的参考价值。1. Agent不是“大模型套壳”而是“工具中枢”很多人一提起Agent第一反应是“就是 Chatbot 加了点记忆”。但从工程实现角度看Agent的本质是一个能感知环境、做出决策、调用行动、观察结果的循环系统。这四件事缺一不可。1.1 一个能自转的循环感知—决策—行动—再感知我习惯把Agent的运行逻辑类比成“一个新员工入职第一天”接到任务后它先看一遍任务目标然后决定“我应该用什么工具来干这件事”工具执行完拿到结果再决定“下一步到底继续干、还是任务已经完成可以交付了”。这个过程不是一次性的而是一个循环感知把用户需求、历史对话、当前环境里的约束条件一起拼装成模型能读懂的上下文。决策模型基于感知到的信息输出下一步的意图。意图可以是“直接回答用户”也可以是“我需要调用某工具并且给出该工具的入参”。行动应用程序拦截模型的输出解析出工具名和参数在后端执行真实函数。观察把工具执行结果回传拼进消息上下文让模型“看到”结果再继续下一轮决策。我在项目里见过不少“看起来像Agent”的实现其实只是用if prompt.contains(天气)这种规则去触发第三方接口这是“伪工具调用”。真正的Agent应该是模型自己在语义层面判断“该调用工具了”而不是靠死板的关键词命中。1.2 Function Call在循环中的正确定位Function Call有些平台叫Tool Call或Tool Use并不是一个独立“功能”它是Agent循环当中“决策→行动”这一段的关键实现机制。模型在决策阶段除了生成普通文本还能生成一个结构化的调用指令——指令里包括工具名和按声明好的格式填好的参数。我见过最典型的误解是有人以为Function Call是让大模型“直接执行函数”。这个理解错得离谱。大模型本质上是一个概率模型它只会“生成文本”不会“执行代码”。Function Call做的事情是让模型以文本形式结构化地输出“我想调用哪个工具、参数是什么”真正去执行函数的是你的应用程序代码。打个比方领导不会亲自帮你把数据算完而是给你写一张“批示条”上面写着调什么档、按什么口径算具体的活儿还是由你来干。这个“批示条”就是Function Call的输出你的执行环境就是干活的员工。1.3 Agent只要链路走通效果立刻“活”起来当Agent接上Function Call后最直观的变化就是它能做动态决策了。比如你问它“在上海和北京之间选一个今天气温更低的城市”——如果只靠模型内置知识它大概率答错甚至胡编但接上天气查询工具后模型会先分别把两个城市的温度查出来再基于真实返回值做比较判断最终给出带数据依据的答案。这一步打通后Agent才真正从一个“文本生成器”变成了“能处理真实世界任务的执行器”。这也是为什么我觉得做Agent应用绕不开Function Call。它不是锦上添花而是基石。2. Function Call的触发原理模型到底在“理解”什么我们聊Function Call时得先弄清楚一个关键点模型凭什么知道“这个问题需要调用工具”它又是怎么生成出完全符合参数格式的JSON的这背后其实没有那么玄学就是训练和上下文的组合拳。2.1 工具描述就是模型的“操作手册”在调用支持Function Call的模型接口时我们需要在请求里附带一个工具列表列表里每个工具包含工具名称一句话描述说明这个工具解决什么问题参数的JSON Schema说明每个字段的类型、是否必填、含义主流的模型服务商都遵循大同小异的这套协议。我在实际使用中深刻体会到工具描述的质量直接决定模型能不能在正确的时机触发正确的工具。写得太笼统模型就会犹豫写得太琐碎模型又容易找错工具。这里有个很容易被忽视的细节JSON Schema里的每个字段描述模型都会当成“使用说明”来读。所以字段描述一定要写“人话”要把边界情况也写进去。比如一个查询天气的工具字段date的描述不能只写“日期”而要写“要查询的日期格式为YYYY-MM-DD支持未来1-7天”。描述越明确模型传参的准确率越高。2.2 Studio里看到的“调用”其实是越来越强的指令遵循能力不同代际的模型在Function Call能力上的差异很大。早期模型经常出现“工具名拼写错误”“参数是字符串却传成对象”“该调用工具时不调用”等问题。近两年的模型在指令遵循方面提升明显特别是加入了大量工具调用数据的对齐训练后模型会在语义层面判断“这个任务如果不调用外部工具就完成不了”然后优先走工具调用分支。我自己试验过一批模型得出的规律是模型在处理“需要实时数据”“需要计算”“需要查库”这三类任务时触发Function Call的准确率普遍较高而处理“总结文本”“翻译”“闲聊”这类任务时模型会更倾向于直接作答。这其实跟模型的训练目标是一致的——它不是靠“猜测”来调用工具而是靠“判断任务类型”来决策。2.3 输出侧才是真正考验你的部分理解Function Call的下一步关键是看清输出侧长什么样。正常情况下模型的输出里会附带一个结构化的tool_calls字段里面是完整的工具名和参数JSON。但这里要注意两件事第一模型可能会“一本正经地编造参数”。当用户的问题里缺少数值时模型有时会自己推断一个值填进去而不是空着不传。我在开发中遇到太多次用户只说了“查上海天气”模型直接把日期填成了当天日期恰好当天日期需要从系统取——如果没做校验就会用一个“猜测的日期”去查。所以参数校验环节绝对不能省。第二模型的输出并不保证永远合法JSON。即使模型服务商给出了结构化的tool_calls在实际传输过程中依然可能出现字段缺失、类型错乱、内容截断等情况。你的代码要按“外部输入”去解析不能按“内部保证”去解析。2.4 为什么说Function Call比“提示词里塞工具说明”更稳在Function Call能力普及之前团队通常用“提示词内嵌工具描述”来实现类似效果在Prompt里写“如果用户询问天气请输出JSON格式的调用结果字段为city和date”。这种做法能跑通Demo但在生产环境中问题很多——模型的输出格式不稳定不是多了多余的说明文字就是JSON里混进非JSON内容解析起来十分痛苦。Function Call通过“约束解码”的方式从源头做了限制模型虽然还是靠生成但在生成策略上会强约束输出结构让工具调用结果更规范、更容易被解析。从工程角度看这是一种“把格式问题前置解决”的思路比你把希望寄托在模型自觉上要靠谱得多。3. 从零实现一个能查天气的Agent完整代码拆解光讲概念没有说服力。我拿一个极简但完整的例子来展示Agent与Function Call是怎么协作的用户输入一个城市名Agent调用天气查询函数拿到真实天气数据后再回答用户问题。这是优秀入门案例麻雀虽小五脏俱全。3.1 第一步定义工具清单和参数Schema我项目里习惯把所有工具的定义集中放在一个字典里这样便于维护和复用。核心部分包括工具名、描述、参数字段和必填项tools [ { type: function, function: { name: get_weather, description: 查询指定城市当天的天气情况包括温度和天气现象。只能用于查询天气相关的问题。, parameters: { type: object, properties: { city: { type: string, description: 城市名称如北京、上海、广州必须是中文城市名 } }, required: [city] } } } ]这里有个细节我通常会在代码注释里标注参数描述里倾向加上“边界口径”比如“必须是中文城市名”。为什么会这样写因为模型可能在用户只输入英文名时依然调用工具而下游函数只支持中文名导致执行报错。提前在Schema层堵住这个坑能省去下游大量的异常处理。3.2 第二步写一个能被真实执行的后端函数工具描述是给模型看的后端函数是给自己系统执行的。它不一定要放在“模型SDK”里只要能在你的进程里正常被调用就行def get_weather(city: str) - str: # 实际项目中这里会请求真实的天气服务接口 # 这里用mock数据保证示例可运行 weather_map { 北京: (晴, 18), 上海: (小雨, 22), 广州: (多云, 27), } if city not in weather_map: return f未找到城市 {city} 的天气数据 cond, temp weather_map[city] return f{city} 当前天气{cond}气温{temp}摄氏度我刻意让这个函数返回一段自然语言文本而不是返回JSON。因为实际项目中下游函数可能是“查询数据库返回DataFrame”“调用第三方API返回JSON”“执行一段Shell命令返回输出”它们的结果形态各异。Function Call的回传机制并不会强制要求某一种格式但我会建议“先统一转成字符串”再回传因为模型最擅长的输入就是文本。JSON也行但文本更省token、更好读。3.3 第三步多轮对话循环里维护消息序列Agent的关键在于“多轮”两个字。每一轮工具调用都会产生新的消息这些消息必须追加进消息列表模型才能在下一次决策时“看到”工具执行的结果。我会用一个维护状态的主循环来完成这件事import json def run_agent(user_input: str, messages: list[dict]) - str: # 把用户的输入推进会话历史 messages.append({role: user, content: user_input}) # 开始Agent循环 for _ in range(5): # 限制最大轮数防止死循环 # step1: 把历史消息和工具定义发给模型 response call_llm(messagesmessages, toolstools) msg response[choices][0][message] messages.append(msg) # 模型完整的回复都要入历史 # step2: 判断模型是否要求调用工具 if msg.get(tool_calls): tool_call msg[tool_calls][0] fn_name tool_call[function][name] fn_args json.loads(tool_call[function][arguments]) # step3: 执行真实函数 if fn_name get_weather: result get_weather(**fn_args) # 将工具执行结果按协议格式追加进消息 messages.append({ role: tool, tool_call_id: tool_call[id], content: result }) # 继续循环让模型基于工具结果做下一步决策 continue # step4: 模型没要求调用工具说明它已经可以给出最终回答 return msg[content] return 已达最大调用轮数请简化问题后重试这个循环就是Agent的心脏。梳理一下核心逻辑模型只要输出tool_calls说明它“决定求助工具”系统执行完工具把结果以roletool回填消息里有了工具结果模型才能生成最终回答。整个过程说到底就是“让模型保持在信息完备的情况下做决策”。我在设计这个循环时加了轮数上限实际项目中通常设3到10轮具体看任务复杂度。为什么不设置更高主要防两类问题一类是模型在工具结果不理想时反复尝试同一个工具造成资源浪费另一类是消息崩溃式增长上下文越来越长费用越来越高。3.4 第四步call_llm的微妙细节上面例子里我用了call_llm(messagesmessages, toolstools)这种抽象写法这里补一点实战细节有些模型服务商的接口里工具参数名是tools有些是functions新版本统一走tools更稳妥。有些模型支持“强制工具调用”也就是tool_choiceauto或tool_choicerequired前者适合“能不调用就不调用”后者适合“这轮必须调用工具”的场景。对Agent来说auto是最自然的模式但也有开发者为了让模型优先调用检索工具而把tool_choice设为required这就需要根据应用场景取舍了。工具描述里的中文要精简但边界和歧义一定要说清楚。因为工具描述会占用输入token写得太多则浪费写得太少则模型容易误判。我在做完这个Demo后做了个小实验故意用tool_choice强制get_weather被调用结果模型在面对“你好你是谁”这种打招呼时也返回工具调用帧回答变成了“查询天气”。这验证了一个结论强制调用适合“必须走工具”的场景但在泛化聊天场景里必须放回auto否则会降低模型体验。4. 让Agent真正稳定的关键参数路由、校验与容错Demo能跑通只是第一步。我真正想分享的是项目中后期那些直接影响Agent稳定性的工程细节。这些点不做你的Agent就会“时好时坏”看起来不可控。4.1 参数表驱动的路由分发在实际项目里工具可能有几十个。用一行行if fn_name xxx去分发代码会越写越难看还容易漏掉工具。我建议用参数表驱动的方式来做路由分发def dispatch_tool(fn_name: str, fn_args: dict) - str: # 用一个字典维护“工具名-执行函数”的映射 registry { get_weather: get_weather, search_document: search_document, calc_expression: calc_expression, } handler registry.get(fn_name) if handler is None: return f未找到工具 {fn_name} try: # 执行前做一次显式schema校验非必需但建议 return handler(**fn_args) except TypeError as e: return f工具 {fn_name} 参数错误{e}这样的好处很明显新增工具时只需要在registry里加一行不用改分发逻辑而且所有工具统一入口后加日志、加监控、加错误处理都方便得多。4.2 参数校验宁可拒绝不要错跑模型生成的参数天然带有“概率性”也就是说它有极大概率正确、也有小概率离谱。我在实际项目里吃过亏有一次模型把“乌鲁木齐”传成了“乌市”下游API直接404。从那以后我在所有工具执行之前都加了一道校验层。校验分两层第一层是硬性类型校验用JSON Schema的规范来校验字段类型不对直接返回错误信息给模型重新生成。这一步能拦截大概八成的问题。第二层是业务语义校验比如日期不能早于今天、城市必须在服务商支持列表里。这一层往往要自己写规则。业务语义校验失败的返回值同样重要它会作为tool消息回传给模型。模型读取到错误信息后通常会调整参数再调一次。这个“试错-修正”的闭环天然就弥补了很多模型在参数层面的不足。4.3 工具调用什么时候该“拆”什么时候该“并”很多刚接触Agent的开发者容易掉进另一个坑把一个复杂能力强行封装成一个超大工具。比如做一个“综合分析工具”内部里调四五个系统参数多达十几项。结果模型经常传错参数错误不好排查调试成本极高。我的经验是“按原子操作拆工具”。宁可工具多一点也要保证每个工具的职责单一、参数少、返回值干净。比如数据平台场景我习惯拆成“查表结构”“查分区信息”“执行SQL”“取样例数据”四个独立工具而不是一个大而全的“数据分析工具”。这样模型的决策链路简单可靠每个工具自身的能力边界清晰。但也不能拆得太碎。如果某个工具绑定着不可省的分支逻辑比如“查询订单需要先校验用户身份再调接口”那就应该让“校验身份”成为该工具内部逻辑而不是让模型分两次调用。否则模型很难记住“先A后B”的时序约束执行顺序错乱的风险会明显上升。4.4 返回值的设计为模型“省点事”工具返回的内容最终会原样塞给模型所以它的设计直接影响模型下一轮的理解成本。我总结出三条实际经验返回值尽量转成简短的文本摘要。模型读长文本JSON时容易被细节带偏反而忽略关键结论。返回值里带结构标识符。比如返回状态成功数据xxx比裸返回一个数组清晰得多。错误信息里带上可修正提示。比如“城市名不在支持列表请参考支持城市列表北京、上海、广州”模型看到这个提示后大概率会修正参数而不是原地放弃。这三条经验看起来轻描淡写但在实际操作中对任务成功率的影响非常明显。尤其是错误信息带建议经常能把一次注定失败的调用“救回来”。5. 实测中常见的“翻车现场”与应对思路做Agent开发对“模型会以各种离奇方式出错”要有心理预期。下面挑几个我实际遇到的、也比较有代表性的问题来讲讲排查经历和解决方案。5.1 模型一直调用同一个工具停不下来我遇到过的最典型翻车案例一个数据分析Agent让它查“近7天GMV趋势”结果它在“获取订单明细”和“获取订单统计”两个工具之间来回切换了近10次疯狂重复调用。原因是两个工具功能边界有重叠模型无法确定到底该用哪个于是来回试探。解决办法是我对这两个工具的description做了更明确的边界区分订单明细工具明确写“仅返回每条订单字段不做聚合统计”统计工具明确写“聚合结果不返回明细行”。改完描述后模型基本只调用一次统计工具就进入总结阶段整体调用次数明显下降。还有一个配套措施设置“同一工具连续调用上限”。如果某个工具在连续两次调用中都返回类似结果就强制终止Agent循环把已有信息返回给用户。这种“保底逻辑”在对话体验上比无限循环好得多。5.2 模型突然不调用工具直接编答案工具调用链路整体稳定后偶尔也会遇到模型“偷懒”不调用工具的情况。比如用户问“查一下某个订单的物流状态”模型却根据训练知识直接回复“您的订单正在配送中”。这在用户体验上是灾难级的——因为模型给出的信息很可能是幻觉。我排查后发现问题出在消息历史上。在Agent循环里如果上一轮工具结果为空字符串或者错误信息写得含糊模型就会倾向于“反正系统也没给有用信息我自己编一个吧”。修复思路有两个方向工具返回值即使没有结果也要明确返回“查无数据”之类的状态不能返回空字符串。在系统提示词里加一句硬约束“只要用户请求涉及实时数据必须先调用工具获取结果禁止直接回答。”这句话虽然朴素但实测对降低幻觉率很有帮助。5.3 并发调用模型一次想跑好几个工具怎么办现在不少模型支持在一次响应里输出多个工具调用指令也就是并行Function Call。这个能力在“需要同时查多个城市天气”这类场景下非常有用但如果你的后端函数有依赖关系并行调用就会出问题。比如“先生成订单ID再用订单ID查询状态”如果模型试图一步到位同时调用两个工具订单ID根本不存在。我处理并行调用的策略很简单先看工具之间有没有数据依赖有依赖的工具必须依赖系统逻辑上一轮先执行结束而不是把“依赖关系”交给模型去理解。工具描述里也要写明“该工具依赖上一个工具生成的xxx值”。实现层面如果某个模型不支持并行调用我在解析时只取tool_calls数组里的第一项其余忽略。如果支持并行调用且工具相互独立那就把它们的返回结果都追加进消息里再让模型统一归纳。5.4 上下文被工具结果撑爆怎么办Agent每多调用一轮工具消息列表就会增加两条一条模型调用帧、一条工具结果帧。若工具结果有上千字三轮调用后上下文就非常臃肿一方面费用暴涨另一方面模型对早期信息的“注意力”会下降。我的通用做法是在每轮循环最后做一次“消息收敛”。把已有历史消息中最早的、且不需要保留细节的轮次压缩成一段摘要只保留最新一轮的工具结果和用户的原始意图。这个操作能显著控制token成本并保持模型决策清晰。不过“消息收敛”也不能做得太激进如果用户中途修改过需求早期信息可能还有用。我会在收敛时把“用户原始输入”和“最近一次工具结果”强制保留大幅压缩中间的模型长文本回复。6. 从“能用”到“好用”Agent落地阶段的进阶扩展做通了单工具Agent下一步自然是多工具协作、多种复杂任务。这部分我聊几个更贴近真实落地场景的进阶方向。6.1 用“子Agent”解决工具的职责恐惧工具多了以后单一Agent需要管理几十个工具的决策准确率会明显下降。我见过一个比较有效的实践是把“顶层Agent”当“路由”把一批“子Agent”当“执行者”。顶层Agent只负责判断“这个任务交给哪个子Agent”子Agent各自维护自己的工具集与上下文。比如一个电商客服Agent顶层Agent根据用户提问把会话分流给“售后处理子Agent”“物流查询子Agent”“商品推荐子Agent”。每个子Agent内部只感知自己范围内的工具。这就像公司里部门分工顶层Agent是前台接待子Agent是各业务部门。分工清晰后每个层级的决策负担都小工具调用的准确率随之提高。6.2 引入“观测与日志”的意识Agent是多轮决策系统一旦出错排查链路过长。我在项目里很早就要求团队成员在每个工具调用节点打印结构化日志包括模型触发了哪个工具、传参是什么、返回值是什么、耗时多久、该轮决策基于哪段历史。这一步看似增加了一点工作量但对线上Agent问题的定位效率提升是数量级的。我建议日志至少包含四类字段会话ID用来串联多轮消息工具名和入参原始值工具返回值的摘要模型当前轮的完整原始消息。有了这些你才能快速回答“模型为什么这么决策”而不是凭感觉调参。6.3 评估Agent效果不能只看“最终答案”很多团队在验收Agent项目时只看最后一轮模型的回答是否准确。这对Agent这种多轮动态系统来说远远不够。我的评估体系分三层工具选择准确率该调用工具时是否调用了正确的工具。参数完整率工具调用是否传全了必填参数参数值是否合法。最终答案正确率模型有没有基于工具结果给出准确的总结或行动。前两层是过程指标第三层是结果指标。如果结果指标一直不达标但过程指标很好问题多半出在prompt的总结指令上如果过程指标就低问题往往出在工具描述和参数Schema设计上。分层评估能帮你快速定位瓶颈避免“头痛医脚”。我之前在一个检索类Agent项目里就靠把评估从“最终答案”拆成“检索召回率”和“回答忠实度”两个指标定位到根因是工具返回内容太长模型在总结时遗漏了关键数据。后来对工具返回做了摘要裁剪一个长期困扰团队的“答非所问”问题才彻底解决。6.4 做Agent开发的最后一点体会从这个项目一路做下来我最大的体会是Agent项目最核心的工程能力不是“调模型”本身而是“把模型当成一个不可靠但聪明的协作者”来设计系统。你需要给它的决策提供清晰的工具说明书给它的错误留下足够的回退空间给你的系统加上充分的可观测性。一旦接受“模型会犯错、工具会失败、参数会缺失”这三个前提你在设计Agent时就能少走很多弯路校验该加就加兜底逻辑该做就做日志该全就全。你会发现一个看起来“不够聪明”但“稳定可靠”的Agent在实际业务中的价值远大于一个“偶尔惊艳”但“频繁翻车”的Demo。Function Call就是把大模型的能力“焊”进真实系统的那道焊点。焊点不牢整个Agent都会散架焊点牢了你接下来要做的就是在这个地基上不断盖更高的楼了。
阅读完成 · 觉得有帮助?