ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent-Reach:为大模型智能体补齐工具调用与执行能力的工程实践

Agent-Reach:为大模型智能体补齐工具调用与执行能力的工程实践 很多做AI应用的朋友都遇到过这个场景大模型聊天很顺一让它干实事就抓瞎。我搭建Agent-Reach的初衷很简单就是给智能体补上“触达外部世界”这一环——从查数据库、调API到操作内部系统让Agent真正能把活儿干完而不是只会给建议。这个项目我做了一个多月踩了不少坑也沉淀了一套可复用的方案这篇就完整分享一下设计思路、核心实现和排障经验。如果你正在做Agent类产品或者想在现有应用里加一层工具调用能力这篇文章会比较对路。我会尽量把关键设计讲透代码部分也直接给你能跑的示例拿过去改一改就能在你的项目里落地。1. 内容整体设计与思路拆解1.1 为什么要做Agent-Reach大模型的“最后一公里”问题先聊一个很实际的现象。大模型本身是一个“文本进、文本出”的系统它再聪明也看不到你数据库里的订单表更没法帮你把工单状态改成“已完成”。过去我们接Agent产品时最常用的办法是RAG——把知识库内容切成片段喂给模型让它在回答时引用。但RAG解决的是“信息获取”解决不了“动作执行”。用户跟Agent说“帮我取消今天的会议”模型就算理解了语义也没法真的去调日历接口。Agent-Reach就是把“理解”和“执行”之间的断层补起来。它的核心思路不复杂给Agent一组预先定义好的工具工具本质上就是带描述、带参数schema的函数模型根据用户意图选择合适的工具、填好参数然后由Agent-Reach这一层去安全地执行调用再把结果返回给模型继续生成最终回复。这个设计不是新技术OpenAI的Function Calling、各大厂商的Tool Use都是同一个路子。但我的体会是光有模型能力远远不够落地过程中真正难的是外围工程工具怎么注册、参数怎么校验、权限怎么控制、失败了怎么处理。Agent-Reach就是围绕这几个问题做的一套轻量基础设施。1.2 架构设计的几个关键取舍先说说我踩过的一个大坑。第一个版本我图省事让Agent直接拿着API Key去调外部服务代码是少了但出了两个问题一是Key的权限没法细分一个Agent能查数据也能删数据二是模型偶尔会编造参数直接把线上数据搞乱了。所以后面我调整了整个架构思路。Agent-Reach采用“中央网关”模式所有工具调用都过一层统一网关。这样做有三个好处。第一权限集中管控每个工具可以单独配置允许哪些调用方使用甚至可以细化到参数级别第二审计日志完整谁在什么时间调了什么工具、结果如何全部留痕第三可以统一做限流、缓存、重试这些横切逻辑不用每个工具重复实现。架构上分成三块Reach Registry工具注册中心负责工具的注册、发现、schema管理Agent通过它拿到当前可用的工具列表。Reach Gateway执行网关接收Agent的工具调用请求做身份校验、权限判断、参数校验然后分发给具体的工具实现。Reach Runner运行时实际执行工具代码的地方支持本地函数、HTTP接口、数据库查询等不同类型。这个结构看起来多了一层但实际投入产出比非常高。尤其当你的Agent要从两三个工具扩展到几十个工具时没有这层网关维护成本会指数级上升。1.3 为什么不用现成的Agent框架肯定有人会问市面上LangChain、AutoGen、各种Agent平台都带工具调用能力为什么还要自己折腾一个Agent-Reach这个我确实纠结过但最终决定自研原因有三。一是可观测性不够。现成框架封装得太好工具调用的中间过程是黑盒出了问题很难定位是模型理解错了、参数填错了还是工具本身报错了。二是发版成本高。业务工具变化很快每加一个工具都要改Agent的主流程耦合太重。Agent-Reach把工具作为独立组件注册新增工具不需要动主程序。三是定制空间有限。特别是权限模型、审批流、与公司内部系统的对接方式这些一样的需求每个公司都不一样通用框架很难完全覆盖。当然这不是说现成框架不能用。如果你只是做个Demo或者工具数量非常少直接用LangChain的Tool机制完全够。Agent-Reach更像是一个“工具多了以后的长远解法”你可以先小规模验证觉得有必要再迁移过来。2. 核心细节解析与实操要点2.1 工具注册表Agent的“接口说明书”工具注册表是整个Agent-Reach的灵魂。它解决一个根本问题模型怎么知道有哪些工具、每个工具怎么用、参数怎么填。我参考OpenAI Function Calling的格式给每个工具定义了一份描述文档内容包含工具名称、功能描述、参数schema、返回格式、调用权限、错误码约定。下面是一个实际例子{ name: query_order_status, description: 根据订单ID查询订单当前状态包括订单是否已支付、是否已发货、物流单号等信息, parameters: { type: object, properties: { order_id: { type: string, description: 订单编号格式如 ORD-20250101-001 }, include_logistics: { type: boolean, description: 是否同时返回物流信息默认false } }, required: [order_id] }, returns: { type: object, properties: { status: {type: string, enum: [pending, paid, shipped, completed, cancelled]}, logistics: {type: array} } }, permission: { roles: [user, support], rate_limit: 30 } }这里有个很多人容易忽略的细节参数描述一定要写得足够详细。模型是靠描述来理解参数的你要是只写“order_id订单ID”模型很可能把用户说的“查一下昨天那单”解读成错误的ID格式。我在实践中会把格式、举例、边界情况全都写清楚甚至会把参数之间的依赖关系写进去。这比在prompt里反复强调“要按格式传参”有效得多。工具描述写得好不好直接决定调用成功率。我做过一次对比实验同样的20个测试问题不写参数示例时调用成功率为61%加上示例后提升到87%。模型不是不会填参数是你不给它足够上下文它只能靠猜。2.2 工具发现的两种模式预置与动态Agent-Reach支持两种工具发现模式。第一种是“预置模式”启动时把全部工具schema一次性发给模型适合工具数量少比如10个以内的场景。优点是实现简单模型在上下文中能直接看到所有工具不用额外做检索。缺点是工具多了以后token消耗很大而且模型会超载选择困难。第二种是“动态发现模式”先根据用户问题做一步工具检索只把相关的工具schema发给模型。这适合工具数量多的场景。检索可以简单点比如用关键词匹配也可以用嵌入向量做语义检索。我目前用的是混合方式一个轻量级关键词索引加一个embedding召回效果比较稳定。两种模式还有一个折中方案——把高频工具常驻上下文低频工具走动态发现。比如“查询订单状态”这类工具使用频率超过80%直接常驻半年用一次的“导出年度报表”就放动态检索。这样上下文不会太长又能保证常用功能响应快。2.3 参数校验别让模型乱填参数模型填参数不像代码那么严谨偶尔会填错格式、漏必填项甚至虚构出根本不存在的ID。Agent-Reach在Gateway层做了三道校验。第一道是类型校验。严格按照JSON Schema校验参数类型和必填项字符串太长、数字超范围都会被拦下来。第二道是字典校验某些参数如果只允许特定取值比如订单状态字段只允许那五个枚举值就用枚举校验。第三道是实体校验这个比较关键比如订单ID模型可能编一个“ORD-XXXX”根本不存在这需要校验时去数据库里查一下ID是否真实存在。实体校验我觉得是Agent-Reach和简单Demo的最大区别。Demo里一般不做这层校验但你一旦放生产环境模型乱传ID引发的脏数据问题会让你痛不欲生。校验失败的请求不会直接报错返回给用户而是把“该订单不存在”这类错误信息回传给模型让它重新组织回复比如告诉用户“没有找到这个订单请确认订单编号是否正确”。2.4 安全边界与权限模型Agent-Reach的权限模型我最终设计成了三层用户层、工具层、参数层。用户层就是判断当前对话来自谁是普通用户、运营人员还是管理员工具层决定这批人能不能调用某个工具参数层最细比如普通用户可以查订单状态但只有管理员可以取消订单。这个三层模型覆盖了我目前能想到的绝大多数场景。具体实现时权限判断不写在工具代码里而是统一配置在注册表的permission字段中。这样工具开发者不需要关心权限逻辑只管实现业务权限调整也不用改代码。另外一个安全细节是执行超时和资源限制。某些工具如果跑太久会影响整体响应时间。我给Runner加了一个超时控制默认单个工具执行不得超过15秒超过就中止并把超时信息返回给Agent让它提示用户操作未完成。同时每个工具的并发数也做了限制防止某个工具被打爆拖垮整个系统。3. 实操过程与核心环节实现3.1 最小闭环从对话到工具调用我会用一个真实的例子来演示Agent-Reach的落地过程。假设我们要做一个“订单助手”Agent用户可以问“帮我查一下订单ORD-20250101-001的状态”。先是最小闭环分四步定义工具、注册工具、模型选择工具并填参数、执行工具并返回结果。定义工具我用一个装饰器写法这样最直观。核心工具函数就是一个普通的Python函数加上reach_tool装饰器函数名就是工具名docstring会自动变成工具描述参数用类型标注声明。注册时Agent-Reach会扫描这些函数自动生成JSON Schema。from reach import reach_tool, AgentRuntime from typing import Optional reach_tool def query_order_status(order_id: str, include_logistics: bool False): 根据订单ID查询订单当前状态。 Args: order_id: 订单编号格式如 ORD-20250101-001 include_logistics: 是否同时返回物流信息 # 这里实际会去查数据库示例里直接返回模拟数据 return { order_id: order_id, status: shipped, logistics: [ {time: 2025-01-03 10:00, event: 包裹已到达上海转运中心}, {time: 2025-01-04 08:30, event: 包裹派送中} ] }接着初始化Agent运行时加载所有带reach_tool装饰器的函数启动。整个调用链路由Agent-Reach托管业务代码只需要关注函数实现。runtime AgentRuntime(api_keyyour-llm-api-key) runtime.load_tools(order_tools) response runtime.chat(查一下订单ORD-20250101-001的状态) print(response)在后台实际发生的事情是模型收到用户问题和工具列表输出一个结构化的工具调用请求——tool_calls里包含工具名query_order_status和参数{order_id: ORD-20250101-001, include_logistics: true}Agent-Reach拦截到这个请求做完权限校验和参数校验执行函数把返回值塞回对话上下文再请求模型生成最终回复。最终用户看到的是一句自然的“您的订单ORD-20250101-001已发货目前正在派送中最新一条物流记录是今天早上8点30分包裹正在派送。”3.2 用网关包装一层HTTP工具接入业务场景里很多工具不是本地Python函数而是现成的HTTP服务。Agent-Reach的Runner要能直连这些服务。我封装了一个通用的HTTPTool类只要在注册时给一个URL模板和参数映射它就能通过HTTP完成调用。from reach import HTTPTool, ToolRegistry registry ToolRegistry() registry.register(HTTPTool( namecreate_ticket, description在客服系统创建一张新的工单, urlhttps://api.example.com/v1/tickets, methodPOST, params_schema{ type: object, properties: { title: {type: string, description: 工单标题}, priority: {type: string, enum: [low, medium, high]} }, required: [title] }, headers{Authorization: Bearer ${token}}, permission{roles: [admin, support]} ))这里有一个实际经验HTTP工具的响应经常不是模型友好的格式比如返回一大段包含很多冗余字段的JSON。我在Runner里加了一个response_processor将HTTP响应裁剪成精简结构再喂给模型不然模型会被冗余信息带偏回复质量明显下降。3.3 加缓存与重试稳定压倒一切工具调用里最容易引发用户不满的是“慢”和“莫名的失败”。我加了两个兜底机制。第一个是缓存。对于幂等只读类工具比如查订单状态、查天气、查库存结果缓存30秒到5分钟不等具体看业务容忍度。这一下帮我扛住了不少重复请求LLM接口的token费用也省了不少。cache_config { query_order_status: {ttl: 60, key_by: args.order_id}, query_stock_level: {ttl: 30, key_by: args.sku_id}, create_ticket: {enabled: False} # 写操作绝对不能缓存 }第二个是重试。临时网络错误、上游服务5xx这类问题简单设置重试2-3次加指数退避就能解决。但有一个红线写操作不能盲目重试。比如“创建工单”“转账”“删除文件”重试可能导致重复执行。我的处理方式是在工具的定义里标注is_idempotent只有幂等的工具才允许自动重试非幂等的失败直接进入人工处理队列。3.4 上下文管理别让工具结果撑爆prompt工具调用的结果特别是那些一次返回几百行的数据表如果全塞进上下文会带来两个问题token成本飙升以及模型注意力被无关细节稀释。我做的处理是结果摘要化。在工具返回之前先经过一个格式化层根据配置把结果截断、聚合。比如“查询库存列表”的工具可能返回500个SKU的库存数据传给模型的就压缩成“库存不足的SKU有12个其中最缺货的是...”具体明细让用户点击查看详情时再实时调一次。这样做之后一个明显的感觉是模型的回复更聚焦了不再被长列表干扰而且每次请求的token消耗掉了差不多40%。如果你的Agent工具返回结果都比较长我建议把这个机制加上。3.5 接入多工具场景让Agent自主决策说实话单工具调用只是热身Agent真正有实用价值的场景是多工具配合。比如用户说“帮我处理一下这个退款然后通知客户”这就得调“查询订单”“创建退款单”“发送通知”三个工具。Agent-Reach处理多工具的方式是让模型先规划再执行。它在每轮对话中允许模型返回多个tool_calls并且允许工具结果作为下一轮决策的输入。实际流程里模型先查订单拿到订单金额和状态再创建退款单拿到退款单号最后调通知接口。{ tool_calls: [ { id: call_1, type: function, function: { name: query_order_status, arguments: {\order_id\: \ORD-20250101-001\} } }, { id: call_2, type: function, function: { name: create_refund, arguments: {\order_id\: \ORD-20250101-001\, \amount\: 199.00, \reason\: \客户申请退款\} } } ] }注意Agent-Reach执行多个工具时默认是顺序执行不是并行。原因很简单第二个工具往往依赖第一个工具的结果。代码层面支持声明依赖关系如果工具B的参数要从工具A的结果中取值就标记depends_on。当然也有不依赖的情况比如同时查三个城市的天气可以并行Runner里通过线程池实现并发上限默认5防止同时打爆上游接口。3.6 测试Agent-Reach模拟与回归Agent工具调用和传统函数测试最大的不同是你还要测模型的“理解能力”——同样一句话模型是否能选中正确的工具、填对参数。我搭了一套模拟测试工具链。一方面准备测试用例集。我梳理了50条高频用户语句覆盖正常表达、模糊表达、带干扰信息的表达比如用户同时提到多个订单只让查其中一个每一条都标注预期应该调用哪个工具、传什么参数。另一方面准备模拟回归。每改一次工具描述我都跑一遍全量用例对比调用正确率和参数准确率。我用的工具叫reach_eval跑完之后出一份报告哪条用例选错了工具、哪条参数填错了、哪条没有调用工具直接瞎回答了一目了然。效果方面工具描述优化前调用准确率约72%经过两轮描述明与workshop打磨之后稳定在90%以上。建议你如果也在做这一类项目一定要建立这个测试集不然改一个描述很难知道到底是变好了还是变差了。4. 常见问题与排查技巧实录4.1 工具明明存在Agent却不调用这类问题处理得最多表象是用户问“查一下订单”模型却直接回答“很抱歉我无法查询订单信息”。排查思路是按下面的顺序来第一先确认工具schema真的传到模型了。打印出实际发给模型的messages内容看tools字段里有没有这个工具。很多时候是因为上下文长度限制某个工具被截断了。第二检查工具描述质量。描述太笼统比如“查询订单”模型会不确定它到底返回什么、参数怎么填可能就谨慎地不敢调用。把描述改成“根据订单ID查询订单当前状态包括是否已支付、是否已发货、物流单号适用于用户咨询订单进展、物流时效等场景”模型就清楚多了。第三检查prompt里的角色设定。如果系统提示词说“你是一个只能回答知识问题的助手”那模型会主动回避工具调用。要让系统提示词明确告诉模型“当用户需要查询实时数据或执行操作时调用对应工具”。我处理这个问题的一个实际经验是在system prompt里直接列一遍Agent-Reach的能力范围比如“你可以查询订单、发起退款、查询物流记录”并特别标注“不要编造数据所有动态信息一律以工具返回结果为准”。这招能压住相当一部分模型瞎编的冲动。4.2 工具调用成功但回复质量差还有一种很气人的情况工具返回明明是对的用户也很满意但模型生成的回复很生硬比如直接输出“工具调用结果订单状态为shipped”这种内部风格的话。原因通常是模型不熟悉工具结果的表达方式。解决办法是在工具返回的数据里加一个natural_language_fallback字段把推荐表达写进去。比如查询订单状态这个工具返回“订单状态是shipped”同时带一条“该订单已于昨日发货正在派送途中”模型会很自然地采用这条表达。本质上这是用最少的人工预置引导模型的输出风格。另一个因素是temperature设置。工具调用后生成最终回复的这轮我建议把temperature调低到0.2左右让模型更贴近工具结果的事实减少自由发挥的空间。用户问题理解阶段可以调高一点增加回答的灵活度但到了执行阶段事实准确比文采更重要。4.3 参数幻觉模型填了不存在的ID做Agent工具调用一定要面对“参数幻觉”。用户说“查一下我上一个订单”模型找不到具体订单ID就直接编造了一个“ORD-20250101-123”来查然后工具返回空模型再根据空结果一本正经地编一份不存在的数据。我采用的方案是三层防控。第一层把查询类工具的兵力集中在“先搜索后调用”模式如果工具参数需要一个ID而用户提供的是模糊描述模型先调用“搜索订单列表”工具拿到真实ID后再查详情。第二层在参数校验里加强实体校验如果ID在数据库里不存在直接给模型返回“订单ORD-20250101-123不存在请提示用户确认订单号”。第三层在开发环境开启“参数来源审计”标记工具参数中哪些来自用户原话、哪些来自上一个工具结果、哪些是模型生成的审计标记能帮你快速定位是哪个环节产生了幻觉。如果做的产品涉及金钱、权限、删除类操作我的建议是再加一道人工确认步骤。Agent-Reach里实现了require_confirmation标记打上这个标记的工具执行前会推送一条确认消息给用户用户确认后才真正执行。就算模型再能编也绕不过最后这道人肉关卡。4.4 超时与重试的常见坑超时是工具调用最常见的故障之一。这里分享两个我踩过的坑。第一个坑是LLM请求本身会超时模型响应慢的时候外部工具调用再多都是白搭。Agent-Reach的整个处理链路是串行的一次对话包含模型决策、工具执行、模型总结其中任何一环慢都会拉长整体时延。我在网关层加了分阶段超时监控哪里慢了就看哪里实测下来模型决策阶段占整体时延的60%以上所以优化优先级应该在prompt长度和模型选择上而不是只盯工具本身。第二个坑是重试策略写得太粗工具失败后马不停蹄重试3次结果3次全撞在同一个下游故障上白白多等了十几秒。后来我改成“重试前先做健康检查”比如下游服务返回503时先GET一下/health端点如果确实不可用就不重试直接告诉用户稍后再试。这个改动把失败请求的平均处理时间从12秒降到3秒以内。4.5 审计日志与问题回溯的工程实践Agent-Reach上线第一周业务方反馈“某天有上百条重复工单”排查时发现是Agent对同一用户的重复请求各创建了一张工单。如果没有审计日志这种问题能让你排查到怀疑人生。我现在把审计记录拆成两张表一张是tool_call_log记录每次工具调用的用户、工具名、参数、耗时、结果摘要另一张是conversation_log记录整个对话的完整轨迹包括模型每轮的思考输出、工具返回结果、最终回复。任何一次线上问题都能从这两张表里反查出完整链路。日志记录不需要大而全关键是几个字段必须有时间戳、调用方用户ID、工具名、入参、出参摘要、执行结果、异常信息、请求ID。请求ID尤其重要整个链路串起来全靠它。这里的一个细节是出参尽量用摘要而不是全量数据不然日志表会长得飞快而且日志里不要记录敏感的业务字段比如手机号、银行卡限制一下日志级别和脱敏规则。4.6 工具数量膨胀后的性能对策Agent-Reach跑了一段时间后工具从10个增加到40个我明显感觉到了性能变化预置模式下的工具列表占用了大量token模型选择工具的准确率也开始下降。如果你也会遇到这个问题可以用下面这套对策。工具检索层做分层。第一层用关键词匹配快速过滤比如“订单”“退货”“发票”这些词把工具分门别类第二层用embedding做语义召回过滤出最相关的5-8个工具第三层是规则支持的高频工具常驻。这套流程下来工具列表从40个缩到6个左右模型选择准确率回升到95%以上token消耗也降了很多。另外一个更彻底的办法是按场景拆Agent。比如把订单助手、售后助手、数据分析助手拆成三个独立的Agent每个Agent只挂自己场景内的10个工具。这样既减少了工具选择范围也让系统提示词更聚焦。Agent-Reach支持多Agent共享一个Gateway只是注册表里每个Agent只暴露自己可见的那几条工具。5. 一些值得沉淀的经验做Agent-Reach这一个月整体下来最大的体会是Agent工具调用这个方向真正难的不是调用本身而是外围的工程完整性。模型能力大家都有但工具注册、权限、校验、缓存、审计、测试这些东西决定了一个Agent从Demo到可用要走多远。如果你也想搭类似的东西我建议你不要一上来就追求大而全的架构先跑通最小的闭环定义三五个工具接一个大模型API让Agent能查、能算、能回。跑通以后再按照“每天都能稳定在线”的标准把外围工程补齐。我的顺序是先做权限和参数校验这两个直接关系到系统安全再做缓存放放大然后做日志审计和测试集最后优化工具检索和多Agent拆分。还有一点我始终在坚持每加一个工具都要写测试用例。工具描述改了、参数调整了测试集一跑立刻知道有没有坏。这套测试集对我来说是Agent-Reach稳定性的底气。最后说个小技巧工具返回结果给模型之前加一个“结果备注”字段工具执行状态、耗时、有没有走缓存都写在里面。模型回复时偶尔会把“查询耗时0.3秒”这种信息带出来用户反而会觉得系统很快很用心。这种小细节实测下来对体验提升挺明显的。Agent-Reach后续我准备把多Agent协作的调度做得更完善一些到时候再专门写一篇分享。
返回列表