ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Function Calling之后:Agent工具系统设计与落地的六大关键工程实践

Function Calling之后:Agent工具系统设计与落地的六大关键工程实践 1. 先搞清楚Function Calling 到底解决了什么问题1.1 “模型会调函数了”只是万里长征第一步Function Calling 刚火起来的时候很多人的反应是“模型终于能动手干活了”。确实它解决了一个很关键的问题让模型在生成自然语言回复的同时输出一段结构化的调用指令由外部系统去真正执行函数、查数据库、调用API、操作文件再把结果送回给模型。这一步直接打破了“对话模型只能聊天”的边界。但如果你真的动手做过 Agent 项目很快就会发现Function Calling 本身只是一个很窄的协议层能力它解决的是“模型如何把一个调用意图表达成结构化参数”这件事。也就是说模型给你的只是一张“写了函数名和参数的便签”至于这个函数是不是存在、参数对不对、执行超时怎么办、返回结果能不能被后续步骤理解、多个工具调用之间怎么协同——这些问题 Function Calling 一个都不管。我见过不少团队Demo 里跑通了 Function Calling觉得“Agent 已经做完了”结果一上真实场景就翻车。模型把参数生成了一个不存在的用户 ID工具把几 MB 的 JSON 全部塞回上下文一次调用失败后 Agent 卡在同一个步骤里反复重试最后把 token 烧光。这些问题不是调参能解决的而是整个工具系统在设计层面就没有做好。1.2 “能调用”和“会使用”之间隔着整个系统工程从产品形态上看一个成熟的 Agent 工具系统至少要覆盖下面这几层工具注册与发现层你的系统里有哪些工具每个工具的能力边界、参数规范、使用场景是什么模型怎么知道该用哪个、不用哪个。调用解析与参数校验层模型输出的“tool_call”是否合法参数格式是否正确是否有缺失、幻觉编造、越权请求这一步要挡住大量低级错误。执行与调度层工具怎么被真实执行是本地函数、远程 API还是代码解释器执行超时、重试、并发、幂等怎么处理结果处理与上下文管理层工具返回的结果怎么被截断、摘要、结构化怎么放回对话历史不会把窗口撑爆又能让模型拿到足够信息。策略与规划层Agent 什么时候该调用工具什么时候该直接回答多个工具按什么顺序调用工具结果和用户目标冲突时听谁的。可观测性与评估层每次调用是否正常工具是否被正确选择返回结果是否有效整个链路的成本和质量怎么度量。这六层堆起来才勉强算一个“能用”的工具系统。Function Calling 只是其中最底下、最基础的那个协议通道。后面每一层都有大量需要在真实场景里反复打磨的细节。2. 工具系统的第一个坎工具定义绝不只是“写个函数签名”2.1 模型看工具描述的方式和你完全不一样很多开发者写工具的时候习惯性按照“给人看”的方式写描述比如“这个函数用于查询用户信息”。这其实远远不够。模型不会像人一样“理解”你的业务它只是在大规模语义空间中做模式匹配。你给它的工具描述等于在教它“什么时候应该选中这一把锤子”。一个高质量的 tool schema至少要有几个层次名字要语义清晰避免过于通用。比如get_user就很模糊get_user_by_id、search_users_by_keyword会可预期得多。模型选错工具、选混工具很多时候是名字太接近导致的。描述description不要写“查询用户”要写清楚这个工具的使用场景、输入限制、典型示例、不适合用它的场景。我用过的实践中好的描述通常是一段 50 到 200 字的话包含一两句“什么情况用它”、一句“什么情况别用它”、偶尔带一个参数示例。参数 schema这是模型最容易出错的地方。要用严格的 JSON Schema 描述每个字段的类型、枚举、范围、必填性。能加enum就加enum能写pattern就写pattern能用minimum/maximum就写清楚。模型生成参数的可靠性和你的 schema 约束强度高度正相关。输出说明如果工具本身有副作用比如删除、修改、发送请在描述里明确写出来。比如“本工具会向用户发送短信通知请确认用户明确授权后再调用”这能在策略层帮你减少很多误调用。我做一个工具描述时通常会反复推敲措辞把它当成“写给一个聪明但完全不了解我们业务的实习生看的操作手册”。而且写完之后拿真实模型跑几个 case看它选不选得对。选错了就改描述直到稳定。2.2 参数校验别迷信模型幻觉参数比你想象的多模型在 Function Calling 中有个非常常见的问题编造参数。明明用户没有提供用户 ID模型会信心满满地生成一个user_id123456。这既不怪模型也不能怪你它本质上是概率生成只要输出分布里存在类似格式它就会“猜”。所以在执行层我强烈建议在工具调用真正发生之前加一道参数校验闸门。这个过程类似 API 网关的入参校验但多了一层针对模型幻觉的“常识校验”def validate_tool_call(tool_name: str, arguments: dict) - ValidationResult: schema TOOL_REGISTRY.get(tool_name).schema # 1. 用 jsonschema 做基础格式校验 errors validate_schema(arguments, schema) if errors: return ValidationResult.fail(格式不合法, errors) # 2. 对关键字段做业务合理性校验 if tool_name send_email and to in arguments: if not is_valid_email(arguments[to]): return ValidationResult.fail(收件人邮箱格式不正确, arguments[to]) # 3. 检查是否存在“模型编造”的常见痕迹 # 比如空会话里出现非空的 session_id、用户从未提供的 ID 等 if tool_name get_user_orders: uid arguments.get(user_id) if uid not in CURRENT_USER_IDS: return ValidationResult.fail(user_id 不在当前上下文疑似幻觉, uid) return ValidationResult.ok()这一步能挡掉非常多“看起来能跑、跑起来是错的”垃圾调用。尤其是涉及敏感操作或真金白银的工具比如发送消息、创建订单、删除资源、扣费没有这一层模型随时给你来个“惊喜”。就算不拦截至少也要把可疑调用打个标记进入人工确认流程而不是闷头执行。提示模型不是数据库它无法“记得”每一个真实用户 ID、每一份文件路径。凡是需要在真实运行时才能确定的参数要么由上游系统注入要么必须做动态校验不能让它自己猜。3. 工具系统的第二个坎执行层比你想的复杂得多3.1 工具返回值和上下文窗口的“内存战争”一个很现实的问题工具一执行返回结果往往又长又杂。比如查询一个订单列表数据库里可能有一百个字段调一个搜索接口响应里可能带着各种埋点字段和 Unused 参数。你把完整结果塞回对话历史等着几轮之后把 128K 上下文窗口烧穿吧。我在实践中做两件事。第一在工具函数内部做返回结果裁剪只保留和当前目标最相关的字段。查询订单就返回订单号、状态、金额、时间不返回内部备注、日志、无关标记。第二在调度层对工具输出做摘要或截断如果结果确实很长就把前面部分保留或者让模型写一句摘要再放回上下文。具体来说我会在每个工具返回时统一包一层def safe_tool_result(result, max_chars3000): text serialize_result(result) if len(text) max_chars: return {status: success, data: result} else: summary summarize_with_llm(text, max_chars // 2) return { status: success, data: summary, truncated: True, original_length: len(text), }注意截断操作也必须考虑“模型接下来要拿这个结果干什么”。如果它需要具体数字去算下一步那你就得把关键数字单独提炼出来而不是丢一段摘要了事。最痛苦的是返回结果被截断后模型就拿不到关键信息于是它又去调用一次工具白白浪费 token 和时间。3.2 错误处理与重试Agent 死循环的“万恶之源”我见过最多的生产事故是 Agent 在同一个工具上反复报错、反复重试、反复烧钱。原因很简单工具执行抛了异常但 Agent 框架把这个异常原样丢回给模型模型看不太懂就再试一次甚至稍微改动一下参数再试一次结果还是一样。破解这个问题的关键是把工具的错误转成模型能理解、能决策的结构化信息并且配上明确的“重试策略”超时错误、临时网络错误可以重试但要加退避最多两次。参数校验失败绝不能原样重试要让模型基于错误信息重新生成参数。权限不足、资源不存在这类业务错误直接终止让模型向用户说明原因并给替代方案。明确告诉模型这个步骤失败后可以走方案 B、C不要死磕方案 A。一个通用的“错误包装”格式我一般是这样设计的class ToolExecutionError(Exception): def __init__(self, code, message, retryableFalse, suggestion): self.code code # 如: VALIDATION_ERROR / TIMEOUT / PERMISSION_DENIED self.message message self.retryable retryable self.suggestion suggestion def to_llm_message(self): return { role: tool, content: ( f[工具执行失败]\n f错误码: {self.code}\n f原因: {self.message}\n f可重试: {self.retryable}\n f建议: {self.suggestion} ), }把“可重试”和“建议”明确写出来模型自然就不会傻傻重试了。实测下来这个改动能把无效重试率降一半以上。3.3 并发、沙箱和权限绕不开的安全底线工具系统的执行环境绝不是“一个函数调用”那么简单。当你的 Agent 变成多步骤任务执行器时它可能会同时调多个工具甚至并行调用。这时候要提前想清楚几个问题并发控制哪些工具不能并发执行比如创建订单和扣库存如果两单并发会不会超卖如果工具本身不具备幂等性那就得在调度层加锁或串行化。执行沙箱工具代码是不是可信的如果 Agent 能执行最终用户提供的代码或者调用动态生成的任务绝对要把这一步放在隔离环境里跑不能在宿主机上裸奔。权限最小化给 Agent 的工具权限应该比人工操作权限更小而不是更大。查询工具最好只有只读权限写操作必须显式授权删除操作需要二次确认。不要图省事直接给它一把万能钥匙。在协议层写清楚“这个工具会产生什么副作用”是权限系统落地的第一步。我自己体验最深的是只要在注册表里给每个工具加一个permission_level字段比如readonly/user_confirm/admin上生产之后能挡住一大半误操作。4. 从“会调工具”到“会用好工具”策略层才是 Agent 的分水岭4.1 工具选择不是所有工具都应该进 prompt很多开发者的第一版实现是把所有工具一股脑塞给模型让模型自己挑。但工具一多问题马上来了token 开销爆炸每个工具的 JSON Schema 都要算进 prompt100 个工具光定义就有几万 token。选择准确率下降工具之间的描述互相干扰模型很容易把相似工具搞混选一个“看起来像但不正确”的。上下文稀释模型看到一堆无关工具反而更不容易聚焦当前任务。成熟的方案是给工具做分层路由。先用一个轻量分类器可以用模型也可以用一个 Embedding 规则判断当前用户意图属于哪个域比如“订单域”“商品域”“用户域”“售后域”然后只把这个域下的 5~10 个工具带进 prompt。这样既省 token又显著提高选型准确率。也可以在这个路由阶段顺便做一次意图澄清如果用户问题本身模棱两可先问清楚再路由到对应工具集而不是盲猜。4.2 规划层什么时候该调用工具什么时候不该调用工具系统最容易出的一个毛病是“手里有锤子看什么都是钉子”。模型很倾向于有事没事都调用工具哪怕用户只是随便问一句“你好”“谢谢”“这个功能怎么用”它也可能去调一个查询工具白白增加延迟和消耗。一个合理的策略是先判断“是否需要外部信息”再判断“是否需要改变外部状态”。如果用户问题属于常识问答、闲聊、情绪表达不需要查任何实时数据直接回答。如果用户问题涉及“实时库存”“当前订单状态”“待办列表”才需要调用查询工具。如果用户明确要求“把 A 改成 B”“给我发一封邮件”“帮我取消订阅”才需要调用写操作工具且写操作前最好给用户一句确认。在 prompt 层写清楚这个判断逻辑比靠模型自由发挥稳定得多。也可以加一个NONE的默认动作把“不调用任何工具”作为合法的决策输出避免模型被迫选一个工具来执行。4.3 状态和记忆工具结果怎么被记住而不是马上忘掉另一个很隐蔽的难点是“工具结果用完即走”。很多基础框架里工具调用的返回结果只在当前轮次的对话历史里存在下一次模型做决策时可能已经看不到前面工具返回的数据了。这会导致 Agent 在长任务里反复查询同一个订单、反复搜索同一个关键词造成大量重复劳动。你需要把工具结果做持久化记忆并设计好“哪些信息值得记、记多久”短期记忆当前任务流程内工具结果保留在上下文中供后续步骤直接引用。中期记忆当前会话内把已经查询过的关键实体订单号、用户 ID、搜索结果存到一个 memory store 里下次模型需要时可以直接取不必重新调用工具。长期记忆跨会话比如用户偏好、历史订单摘要可以进向量库或结构化存储供后续会话初始化时加载。在做记忆时我强烈建议区分“事实”和“推断”。工具返回的订单金额是事实模型推测的“用户可能想退款”是推断。记忆里如果混了这两类信息很容易导致错误决策。给它们打上不同的标签至少逻辑上是干净的。5. 工具系统怎么持续迭代评估、可观测性、兜底5.1 可观测性给每次工具调用都打上点Agent 工具系统最让人头疼的一点是“黑盒”。模型为什么选了这个工具参数是怎么生成的工具执行花了多长时间返回结果有没有被截断这些信息如果没有被记录下来出问题的时候你只能靠猜。我落地可观测性的经验是把每个工具调用变成一个独立 trace 单元{ trace_id: abc-123, tool_name: get_user_orders, input_args: {user_id: u-5678}, is_valid: true, execution_ms: 320, result_truncated: false, token_cost: 156, error_code: null, retry_count: 0, model_choice_score: 0.92 }把这些日志汇到统一的监控平台按工具名聚合出调用次数、平均耗时、成功率、token 消耗、幻觉参数拦截率、重试率。这些指标能告诉你哪些工具描述需要修哪些工具本身有性能问题哪些工具经常被模型误选。没有这套数据你优化 Agent 只能靠“感觉”。5.2 Agent eval不能只靠跑一两个 Demo工具系统的迭代节奏非常快改一个描述、加一个工具、调整一下路由规则都可能引发行为变化。如果你没有一套自动化评估很难知道某个改动到底是变好了还是变坏了。我建议至少建立三类 eval 数据集单元用例针对单个工具的选择和参数生成给模型一段用户输入期望它选get_user_orders并生成正确的 user_id判分标准是“工具名是否命中 参数是否正确”。流程用例多步骤任务比如“帮我查一下上周的订单然后把金额总和告诉我”期望模型先查订单列表再对返回结果做汇总。判分标准是步骤序列是否合理、最终答案是否正确。边界用例故意给模糊输入、危险输入、上下文不完整输入看看模型会不会乱调工具、会不会编造参数、会不会触发敏感操作。跑 eval 的方式可以是离线批量跑也可以把真实线上日志录下来做“回放”看看新 prompt/新工具下模型行为有没有变差。不建议只看 end-to-end 的“成功/失败”因为 Agent 任务太长了中途失败和最后失败是完全不同性质的问题最好把每个步骤的 key action 都纳入评分范围。5.3 降级策略模型不听话时系统要有自己的兜底就算你把 prompt 写得再清楚把 schema 定义再严格模型还是会有抽风的时候。所以工具系统必须有一套独立的“系统兜底”不完全依赖模型的判断力。几个直接能落地的兜底策略最大重试次数任何工具调用重试超过 N 次就强制停止把控制权交给用户或人工客服别让 Agent 自动循环。关键操作二次确认涉及资金、隐私、删除、发送等敏感动作无论模型多自信一律先给用户展示确认卡片用户确认后才真正执行。置信度回退如果路由层对“用户想去哪个域”的置信度很低不要硬选工具直接反问用户或者给一组选项让用户选。超时熔断单个工具执行超过一定时间就主动取消避免把整个 Agent 流程卡死在慢接口上。这个“兜底”思维是工具系统从“能用”走向“可靠”的关键一步。模型负责聪明系统负责稳定两者配合才是合格的 Agent。6. 一些实用建议和常见坑6.1 上生产前花半小时对照这份清单自查我把自己踩过的坑整理成一份 checklist每次上新 agent 前都会过一遍每个工具描述里是否说清楚了“什么时候用它、什么时候别用它、有什么副作用”工具参数的 JSON Schema 是否足够严格有没有 enum、range、format 约束所有工具返回是否做了结果裁剪和长度控制会不会出现单次返回超过 5K token 的情况工具报错时模型拿到的信息是不是结构化、带可重试标记和建议的写操作类工具是否有二次确认或权限校验读操作是不是只读有没有给工具调用加上统一 trace出错时能不能定位到具体哪一步、哪次调用eval 集是否覆盖了单工具选型、多步骤流程、边界输入最近一次代码改动有没有重新跑过全部 eval这些问题如果有一两个没想清楚大概率上线后会在真实流量里暴露出来。6.2 框架怎么选别被“全家桶”绑架也别什么都自己造工具系统这块市面已经有相当多开源的 Agent 框架LangChain、LlamaIndex、Semantic Kernel、Spring AI、各类国产框架等。我的建议是用框架的基础编排能力但把工具注册、执行、错误处理、可观测性这四块变成自己的代码不要每一层都交给框架。框架擅长的是把“调模型、调工具、塞历史”这个循环串起来让你不用从零写框架不擅长的恰恰是各种真实世界的边缘场景——奇怪的参数、脏数据、超时、权限、多租户隔离、费用控制。这些细节框架很难给你开箱即用的答案必须结合业务自己定制。也因此“选框架”不应该只看它 Star 多不多、名字响不响而要看它是否允许你在工具执行链路的关键节点插入自定义逻辑。如果框架把工具调用流程写死了不支持拦截、校验、改写工具结果那它再火也不适合你。最后再分享一个经验不管用什么框架先写一个最简版一个模型 两个工具 一个校验器把整个调用链路跑通再逐步加复杂度。不要一开始就追求工具数、agent 数、多智能体协作那些复杂度会瞬间淹没你。工具系统这件事地基打稳了上面的楼才好盖。
返回列表