有些项目做着做着就会发现,真正卡住进度的从来不是模型聪明不聪明,而是它到底能不能碰到外部世界。"Agent-Reach"这个项目就是干这个的——解决智能体"触达"的问题。这两年我落地过不少AI Agent项目,最深的体会是:让大模型输出一段漂亮话容易,让它真正把一件事办成、把数据查回来、把工单建好、把接口调通,难得多。缺的那一层,就是"手脚"。
我做的Agent-Reach,本质是一套智能体触达层方案,解决的问题非常具体:Agent如何安全、稳定、可控地调用企业内部和外部的能力。它把工具注册、权限收敛、调用编排、回传处理、审计追踪串成一条完整的链路,让Agent从"会聊天"变成"能干活"。这篇文章我会把整个项目的设计思路、架构分层、核心实现、遇到的坑和排查套路都摊开讲一遍,适合正在做Agent应用落地的朋友参考,尤其是那些已经被"模型乱调工具""上下文爆炸""权限难管"折腾过的团队,应该能在里面找到一些对得上号的东西。
1. 为什么需要Agent-Reach:智能体卡在了最后一步
1.1 大模型不缺脑子,缺的是手脚
做个简单的类比:把大模型想象成一个阅历丰富但四肢被绑住的专家。你问它问题,它能答得头头是道;但你让它去隔壁档案室调一份三年前的合同,它挪不了窝。没有外接能力,再聪明的模型也只是个"坐而论道"的咨询师。Agent-Reach要做的,就是给这位专家接上手脚,让他能真正走到档案室门口,撬开门锁,把合同翻出来,再带回来给你看。
很多人一开始会想:这还不简单?给Agent配几个API不就行了。真做了才明白,事情远没那么轻巧。配API只是第一步,后面还有一串连环问题等着你。比如模型到底该在什么时机调用哪个工具,工具的参数怎么从自然语言里准确提取出来,工具调用失败后怎么让模型自己修正,多个工具的执行顺序如何编排,权限边界在哪里。这一堆问题单个拎出来都能写一篇长文,合在一起就是Agent-Reach这个项目最核心的存在意义——它把"触达能力"从一个零散的工程技巧,提升成了体系化的基础设施层。
1.2 从对话到行动的鸿沟,是当前Agent落地最痛的断层
我自己见过太多"Demo惊艳、生产翻车"的Agent案例。做技术验证的时候,拿一个精心挑选的用例,模型发挥得很稳定,看起来已经具备"自主干活"的能力了。一旦放到真实业务里,情况急转直下:工具参数传错、接口超时没人管、模型在几个工具之间来回反复横跳、该调A工具的时侯去调了B工具。这些问题的根源,其实不在模型本身,而在"对话"和"行动"之间的那条鸿沟。
Dialogue只是信息的交换,Action是状态的改变。从前者跨越到后者,需要一套完整的工程机制来做支撑。Agent-Reach做的正是这件事——它把两者之间的每一次连接、每一次调用、每一次反馈都收纳到统一框架里,让模型的能力可以真正辐射出去,让Agent的"手"够得着那些业务系统。这个项目名字里的Reach,取的就是这个意思:触达、够得着、覆盖到。
1.3 项目定位:一套轻量但完整的Agent触达层方案
Agent-Reach的定位非常明确:它不是一个大而全的Agent框架,不是来替代LangChain或千亿参数模型的,它只做"触达层"。整个系统的定位可以概括成三句话:
统一收口。不管你的Agent底层用的是哪个模型、哪个框架,所有对外部工具的调用都从Agent-Reach这一层走,背后的API细节、鉴权逻辑、参数格式差异,全部在这一层消化掉。
安全可控。Agent能碰到什么、不能碰到什么,权限策略在这里集中管理,高危操作必须经过二次确认。
可观测可追踪。每一次触达都有记录,每一次失败都有迹可循。
这个定位让我在设计系统时省了很多心。边界清晰了,取舍就变得很容易——凡是跟"触达"无关的功能,比如Prompt调优、模型微调、向量检索的召回逻辑,统统不在Agent-Reach的考虑范围内。做好这一件事,比做一百件事但件件稀松要强得多。
2. 整体架构设计与核心思路
2.1 分层架构:模型层、触达层、工具层
Agent-Reach的架构图我在项目文档里画了一版又一版,最后稳定下来的核心是三个分层。最上层是模型层,负责意图理解和决策;中间是触达层,也就是Agent-Reach本体;最下面是工具层,包括企业内部系统API、第三方服务、数据库操作接口,甚至有些场景下是遗留系统的命令行脚本。
如果从一次用户请求的完整链路来看,整个系统的工作流程是这样的:用户输入进来,模型层先做意图分析,决定当前需要调用什么能力;决定之后,触达层接管,根据模型给出的工具选择,先去检查权限、解析参数、执行调用,然后把结果结构化地回传给模型;模型根据回传结果继续推理,要么再调下一个工具,要么形成最终答复。整个链路里,触达层充当的是一个接线员——它自己不产生决策,但保证每一次决策都能被准确无误地执行。
勤快点说,这个分层最大的好处是"职责单一"。模型不需要知道工具背后的API域名、鉴权逻辑、签名算法,工具也不需要关心模型的Token限制、思维链格式。两边通过触达层这个标准化接口对话,各自演进互不干扰。后来我们给这个方案起了个名字叫"能插拔的接线员架构"——想换模型,只动上层;想加工具,只动下层。这在项目迭代频率极高的Agent开发周期里,简直能救命的。
2.2 工具注册表的Schema设计:Agent触达世界的"能力清单"
工具注册表是整个Agent-Reach最基础的部分,可以理解成一本说明书目录,模型翻完这本目录才知道自己有哪些牌可以打。每一张牌,也就是每一个工具,都通过一份结构化的Schema来声明自己。这份Schema至少需要包含五个部分:工具名称、功能描述、参数定义、返回值说明、访问权限门槛。
我强烈建议参数定义直接沿JSON Schema规范来做,因为现在的模型对JSON Schema的解析能力已经相当成熟,你给它一段结构良好的schema,它返回的function call参数准确率会高很多。功能描述这一项容易被轻视,实际效果却最明显。同一个工具,描述写"查询订单"和"根据订单号查询指定订单的当前状态,订单号格式为纯数字",模型的调用准确率完全不是一个量级。描述要像写给外包人员看的那样具体,不能像产品需求文档那样抽象。
工具注册表还承担一个隐性功能:收敛能力的候选集。模型每次决策都要在工具列表里做选择,如果列表动辄几百个,就算是再强的模型也会犯晕。我限制单轮最多暴露50个工具,并按业务域做了分类标签,让模型在意图识别阶段就能缩小搜索范围。这个细节在后来的压测中证明收益非常大。
2.3 权限模型:让Agent在安全边界内行动
权限设计是Agent-Reach里最不能省的部分。我之前见过一个团队做Agent,为了让模型"充分发挥能力",把数据库的写入权限直接扔给它,结果一次测试中模型意图识别出现偏差,把一个预发布环境里的测试数据全部清空,生产环境差点遭殃。Agent是概率性的系统,它的每一步决策都带不确定性,你不给这头大象装上护栏,它哪天踩到你脚上,你都没地方说理。
所以Agent-Reach的权限模型有几条硬性原则:最小权限——默认拒绝所有访问,工具需要显式申请授权;分级确认——只读操作可以自动放行,写操作必须进入确认队列,涉及删除和资金的操作必须人工复核,缺一不可;粒度限制——权限不只是"能不能调用某个工具"这么粗,还要管到参数层面,比如一个查询工具,允许Agent查询的数据范围必须限定在用户所属的部门之内。
这些原则落到工程上是好几个机制的组合。工具注册时就带access_level属性,触达层在执行前检查权限 Token 里的角色信息,达不到门槛就拦下来;需要确认的操作,触达层返回一个pending状态,等确认信号回来了才继续执行。整个过程对模型来说是透明的——它只需要知道"这个操作被拒绝了"或者"这个操作在等待确认",不需要知道背后的规则矩阵长什么样。后面我会用具体的代码展示这个机制的实现细节。
2.4 为什么选择"触达层"而不是让Agent直接调API
这是一个关于架构取舍的问题。最初的版本里,我也试过让Agent直接调API——Prompt里把API文档塞进去,让模型自己构造HTTP请求。这么做原型阶段确实快,痛点却接踵而至。
第一个痛点是接口的鉴权逻辑没法写在Prompt里。我们很多内部系统的接口走的是动态Token,有效期只有五分钟,需要先调一个鉴权服务去换。你把这套逻辑让模型在对话里完成,且不说它能不能总结得清楚,Token过期重试这个机制,大概率会把模型绕晕。第二个痛点是API的参数格式千差万别——有的是Restful,有的是RPC风格,有的要传加密签名,把这一切语义塞给模型,模型根本消化不过来。第三个痛点是稳定性。直接调API,没有统一的超时控制、重试机制、错误分类,模型一旦拿到一个超时异常,很容易进入"自言自语"的混乱状态。
引入触达层之后,这些问题全部下沉成了基础设施能力。API的鉴权、签名、重试、超时、格式转换,都在触达层内部消化,模型拿到的永远是干净的结构化结果。这就像给每一把工具都装了一个标准化的USB接口,模型不再需要关心这工具里面是柴油机还是电动机,插上去就能用。真要说有什么代价,就是多了一层需要维护的系统,但相比它换来的稳定性,这笔账非常划算。
3. 核心实操:从零搭建Agent-Reach触达层
3.1 工具注册与能力声明的JSON Schema写法
这一节直接上干货。工具注册是Agent-Reach的起点,我在项目里维护了一套工具定义,存储在一个Python字典列表里,后端启动时由触达层统一加载进内存。以最常用的"查询订单状态"工具为例,完整定义是这样的:
{ "name": "query_order_status", "description": "根据订单号查询订单当前状态。订单号必须是纯数字字符串,若是其他格式请先询问用户。返回状态包括CREATED、PAID、SHIPPED、COMPLETED、CANCELLED。", "access_level": "read", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,纯数字,例如20250114001" } }, "required": ["order_id"] }, "returns": { "type": "object", "properties": { "order_id": {"type": "string"}, "status": {"type": "string", "enum": ["CREATED", "PAID", "SHIPPED", "COMPLETED", "CANCELLED"]}, "updated_at": {"type": "string"} } } }这五个字段里,我最想强调两个点。
第一,description的写法决定了模型是否会正确选到这个工具。别写"查询订单信息"这种过度概括的话,要写清楚"什么时候用这个工具"以及"什么时候不要用"。我见过一个让模型选错工具的经典案例:某个工具的描述里没有注明"仅支持已支付订单",结果模型拿一个未支付的订单号去调它,返回数据为空,模型就开始瞎编,说"订单不存在"。后来在描述里加上"仅用于查询已支付订单,未支付订单请先调用create_payment工具"之后,这个错误率直线下降。
第二,access_level是权限判断的元数据基础。read级别的操作一路放行,write级别的操作要经过确认队列。这个字段设计时看似平平无奇,却是整条权限链路的第一道闸门,后面所有权限判断都靠读它来决策。
工具注册表不是一次写完就完事的。我维护了一个tools.json文件放在仓库里,做了版本管理,每次新增或修改工具定义都走MR评审。因为工具定义和业务参数会直接影响模型决策,改坏了就是线上事故。这个谨慎的性能让我在后期省了不少背锅的机会。
3.2 Function Calling的请求构造与结果回传
工具定义好了,接下来是Agent和触达层之间的通信协议。我在Agent-Reach里的标准做法是采用Function Calling机制,但对请求和回传做了统一封装,方便后续无痛替换底层模型。
当用户发出"帮我查一下订单20250114001现在到哪了"这样的请求时,整体请求构造的逻辑是:模型的system prompt里拼接了工具注册表的摘要信息,用户消息进入对话后,模型会产生一个结构化的function_call请求。这个请求的格式我固定成这样:
{ "request_id": "req_8f3k2d9a", "tool_calls": [ { "tool_name": "query_order_status", "arguments": { "order_id": "20250114001" }, "call_id": "call_abc123" } ] }这里有一个地方处理不好会很麻烦:当一个请求里同时出现多个tool_calls时,是并行执行还是串行执行要考虑清楚。我采取的规则是——工具之间如果存在数据依赖就串行,如果相互独立就并行。比如"查询订单状态并查询最近物流轨迹"这两个动作没有依赖,可以一次性抛出两个tool_calls,触达层收到后用并发执行,响应速度会快很多。但如果两个工具存在依赖,比如先查订单号才能去查物流明细,就必须分多轮完成,模型会在第一轮拿到查询结果后,再发起第二个工具的调用。这个规则我把判断逻辑写在了触达层里:解析tool_calls时检查参数依赖关系,有依赖就顺序放入执行队列,没有就放入并发池。
结果回传的格式我统一封装,不管背后返回的是JSON还是XML还是文本,都转成统一的result结构再还给模型:
{ "call_id": "call_abc123", "status": "success", "data": { "order_id": "20250114001", "status": "SHIPPED", "updated_at": "2025-01-15 10:30:00" } }这个统一格式非常重要。模型不适应结构化JSON的概率极低,但非常不适应自由文本的混乱表达。查询失败的情况我会把错误码和可读信息一起返回:
{ "call_id": "call_abc123", "status": "error", "error": { "code": "ORDER_NOT_FOUND", "message": "订单号20250114001不存在,请检查订单号是否正确" } }把错误信息写得足够具体,模型就知道下一步该怎么办:要么跟用户澄清,要么换成另一个工具。这里就体现出了触达层的价值——错误处理逻辑都写在系统里,而不是靠模型临场发挥。
3.3 多工具编排:Agent的工作流怎么跑通
单工具调用是基本功,多工具编排才是Agent真正发挥价值的地方。拿一个典型的售后场景举例:用户说"我的订单有问题想退款"。Agent要完成的是一连串动作——先查订单状态,再判断是否符合退款条件,然后调起退款申请接口,最后通知用户进度。这串动作里每一步都有依赖关系,是典型的多步编排。
我在Agent-Reach里用一个简单的状态机来管理多工具调用链路。整条链路分成几个状态:tool_selected(工具已选择)、args_parsing(参数解析中)、executing(执行中)、awaiting_confirm(等待确认)、completed(完成)、failed(失败)。每个工具调用都在状态机里流转一次,模型通过观察当前状态来决定下一步动作。
实际落地中,多工具编排最容易出的问题是模型在中间步骤拿到了失败结果后不知道如何恢复。我的处理方式是在触达层加一个全局的step_retry_limit(单条链路最多重试次数,默认3次),超过3次就直接把链路状态标记为failed,并把"建议改为人工服务"的提示返回给模型。这个策略很笨但很有效——与其让模型在链路上反复横跳消耗Token,不如快速止损,把决策权交回人工。
另外链条执行过程中会出现一个隐性坑:长时间执行时,用户可能早就把这事忘了。我这里在处理长链路时会主动推送进度通知,等整条链路完成后把最终结果一并给到用户看。这让体验好很多,也避免了用户反复追问造成的模型上下文污染。
3.4 上下文管理与Token成本控制
Agent一旦跑起多工具链路,Token消耗是个根本绕不开的话题。长的工具定义、多轮对话历史、工具执行结果反复喂给模型,Token量很容易就爆炸。Agent-Reach这一块做了两个层面的优化。
第一层是工具定义裁剪。给模型看的工具列表切场景。用户当前意图明显是售后时,只把售后类工具的定义塞进上下文;用户问物流时,只把物流工具的定义塞进去。这项优化在意图分类准确的前提下,能省掉大约40%的输入Token。
第二层是结果压缩。工具返回的数据经常是完整对象,比如一个订单的全量字段有三十多个,但用户真正关心可能只有三五个。我写了一个字段过滤层,根据模型当前意图动态过滤返回结果。查询订单状态的时候,只把status、updated_at留下,其余字段统统不要。这个压缩策略进一步把输入Token砍了三分之一。
还有一层很多人忽略:历史消息裁剪。对话超过一定轮数之后,触达层会把之前的工具调用细节压缩成摘要,只保留"什么时间做了什么、结果是什么",而把完整过程中的代码块、异常栈全部丢弃。这就像会议纪要代替了录音录像,保留了关键信息却不占空间。事实证明这对模型的长期上下文处理很有帮助,也显著降低了长会话的失效风险。
3.5 超时、限流与重试:触达层的稳定性三板斧
触达层面对的工具大多是外部系统,外部就意味着不稳定。我在系统里为每个工具定义了三个稳定性参数:timeout_ms(超时时间)、max_retries(最大重试次数)、retry_delay_seconds(重试间隔)。这三个参数在不同工具上的配置策略不一样。
查询类工具超时时间给短一点,默认3秒,重试一次。写操作超时给长一些,比如5秒,因为写操作涉及的链路更多;但重试次数反而要少,避免重复提交业务操作造成双重扣款、重复建单之类的问题。这个原则我坚持了很久:读操作可以无脑重试,写操作宁可失败也不可重复。资金类操作的接口我都设置成不可重试,而且在调用前会生成一个幂等键(idempotency key),就算连接意外断开,服务端也能靠幂等键识别出这是同一个请求,不会重复执行扣款逻辑。
重试间隔考虑退避策略。第一次重试等1秒,第二次翻倍到2秒,第三次4秒,最多三次封顶。这个指数退避策略能有效避免多个Agent同时发起重试时产生"重试风暴",把下游系统直接打挂。如果你做的是高并发场景,建议还加一个全局限流器,我在Agent-Reach里用的是一个简单的令牌桶,默认每分钟最多120次工具调用。限流参数可以按Agent的优先级动态调整——高优Agent获得更多配额,普通任务排队等待。
4. 安全与权限:触达能力越大,责任越大
4.1 最小权限原则在Agent场景的落地
最小权限原则在人类组织里是个老概念,但落到Agent场景里,执行逻辑完全不同。人可以通过培训和自觉来理解"什么能看、什么不能碰",Agent没有自觉这种东西,它靠的是硬性的边界约束。
我落地的做法是把权限分成四级。第一级是只读类(read),Agent可以直接执行;第二级是写入类(write),Agent执行前需要用户确认;第三级是高风险类(high_risk),比如删除数据、修改核心配置、发起转账,除了用户确认外还要有一个"复核人"角色做二次审批;第四级是禁止类(forbidden),任何情况下Agent都不允许触达,这主要是给一些敏感系统保留的硬隔离区。这个四级模型在Agent-Reach里的映射就是ACL(访问控制列表)加一个简单的RBAC(基于角色的访问控制)。
实际操作中,一个关键点是要做到按用户维度收敛权限。用户A登录后触达层往上下文里注入的是一份经过授权的工具列表,哪些工具能用、哪些不能用,在源头就切好了。不该在候选集里的工具连让模型"看到"的机会都没有。这个做法的额外好处是进一步缩短了工具候选集长度,两边都受益。
权限校验发生的时机放在工具执行前,而不是工具注册时。为什么?因为同一个工具在A用户下是只读工具,在B用户下可能被标记为写入。权限永远是对"当前用户+当前上下文"的判断,而不是对"工具本身"的判断。我在系统里维护了一张权限映射表,每次调用到来时实时查一次,保证了权限策略变更秒级生效,不用重启服务。
4.2 高危操作的二次确认机制
高危操作的二次确认机制是我被现实毒打过之后才认真做的。之前踩过一个坑:一个Agent在整理销售数据时,误把"导出销售报表并删除缓存"理解成了"清理销售数据",直接在测试环境执行了删除操作。当时虽然没造成真实损失,但这种概率性失控让我意识到——对敏感操作,必须强制加入人类确认环节。
Agent-Reach的二次确认机制是这样的:当模型发出一个带high_risk标签的工具调用时,触达层不会直接执行,而是把操作详情推送给用户端,等用户点击"确认执行"按钮后,触达层才会真正把请求发往业务系统。这个确认界面展示的字段包括:操作类型、目标对象、影响范围、预计变更内容。确保用户明白自己点了什么。
有人说加了确认环节不就拖慢了Agent的速度吗?真实体验下来,这恰恰是体验最好的地方。用户看到关键动作前有确认,反而对系统的信任感会大幅提升。就像你请了个助理,助理做小事不用问你,但动钱动数据之前请示一下,你才敢真放权给它。这个心理机制放在人机协作里同样成立。
4.3 审计日志与全链路追踪
审计日志不仅仅是合规需求,更是调试Agent的救命稻草。我见过很多Agent项目的debug方式是反复看Prompt、反复试参数,但真正有效的手段其实是看日志。Agent-Reach里每次请求生成一个request_id,整条链路的所有事件都挂在这个id下面,包括:模型决策了哪个工具、传了哪些参数、权限是否通过、实际执行结果、用户是否点击了确认、耗时多久。
有了这套日志,排查问题就从"猜"变成了"看"。之前遇到过一个问题:用户反馈某个Agent经常莫名中断,百思不得其解。查了日志才发现是上游系统在特定时间段会主动断连,而触达层的重试策略对这种断连不生效。没有日志,这个问题可能要在生产环境里再游荡几个星期才暴露。
我还会在触达层记录一条关键日志:"模型选了工具A,但参数校验失败,错误是XXX"。这个信息的价值在于,它告诉我们模型在结构化理解上出了什么问题。如果频繁出现某个工具的参数解析失败,那基本可以确定是工具定义里的参数描述写得不够清楚,回去改Schema比换模型、调Prompt更管用。
5. 常见问题与排查实录
5.1 模型总是选错工具,怎么办
选错工具是Agent-Reach上线初期被吐槽最多的一个点。排查到最后,绝大多数情况都不是模型不行,而是工具定义"诱导性不足"。举一个真实案例:系统里有两个工具,一个叫query_customer_info,一个叫query_customer_orders,描述分别只有"查询客户信息"和"查询客户订单"。模型在做"这个客户最近买了什么东西"时,选了前者,返回结果跟问题牛头不对马嘴。
修法不在模型,而在Schema。我把两个工具的description重新写了一遍:
query_customer_info:"查询客户的基本档案信息,包括姓名、等级、注册时间、联系方式。注意:不包含订单记录,查询订单请使用query_customer_orders。"query_customer_orders:"查询客户的历史订单列表,包括订单号、商品、金额、下单时间。注意:只查订单,要查客户联系方式请使用query_customer_info。"
加上"注意"之后的排除性说明,选错率肉眼可见地降下来了。这里总结出一个原则:工具的description不仅要告诉模型它是什么,还要告诉模型它不是什么。排除法往往比直接描述更有效。
5.2 工具返回结果太复杂,Agent理解不了
有一次对接一个老系统,它的接口返回的是一棵嵌套了五层的JSON树。模型拿到这个结果后,经常在推理时把层次关系搞错,进而编造出一些似是而非的结论。
解这个问题不能靠换更聪明的模型,而要靠触达层做结果预处理。我在触达层加了一个Transformer,把复杂的嵌套JSON拍平成结构清晰的树,保留必要字段的同时,如果数据量过大则自动截断。另外,每次回传给模型的结果里,我额外加了一个字段叫"human_readable_summary",由代码直接生成一句概括性的话。比如"该客户共有3笔订单,总金额1820元,最近一笔下单时间是2025年1月12日"。模型拿这个摘要做推理,复杂结构的干扰就基本被消解了。
5.3 多工具串联时上下文爆炸
上下文爆炸是我在做长链路Agent时最痛的体会。节点一多,原先节省的Token很快被消耗殆尽,模型就开始"失忆"。我的处理方式前面提过,核心就一句话:回传结果永远用压缩后的摘要,而不是完整数据。比如一个工具返回了2000条记录,回传给模型的不是这2000条,而是"共2000条,其中A类1200条、B类800条,整体分布正常,如需明细请调用get_record_detail工具"。这就像人类做汇报,先讲结论再讲细节,细节需要按需索取。
另外,我会给历史消息设置一个窗口期,超过15轮的对话自动压缩。压缩器把过往的对话摘要成三行以内的短信息,然后作为一条system级别的历史摘要注入上下文。早期对压缩信息影响模型推理有顾虑,实测发现效果不错,模型理解摘要的能力比想象中强很多,而且因为上下文变干净了,决策稳定性反而更好了。
5.4 Agent陷入死循环
Agent跑多工具链路的时候,偶尔会出现一个诡异现象:它在两个工具之间来回横跳,反复调用同一个工具,同一类错误反复出现。我排查过几次,发现根因多半是"失败反馈没有形成有效的决策信号"。模型拿到的错误信息太含糊,比如"调用失败",它不知道是参数错了、权限不足、还是系统暂时不可用,于是只能重试,重试又失败,陷入死循环。
我的处理机制是双管齐下。一方面在错误返回里带上足够语义化的错误码和建议动作,比如"参数校验失败:order_id格式错误,请检查订单号是否包含字母"。模型拿到这个信号就能明确下一步。另一方面,在触达层加了一个loop_detector,连续三次工具调用如果还是同一个tool_name加同样的关键参数,直接中断并抛出一条提示"检测到重复调用,请换一个策略或者询问用户"。这个保险机制上线之后,死循环问题基本绝迹。
5.5 权限判断和工具选择的冲突
有一个前期设计时没预料到的坑:模型选了一个工具,但用户权限不够。这种情况下如果只是简单拒绝,模型经常会陷入"似乎被小看了"的状态,反过来尝试换个工具绕过限制。如果你不想让Agent变成"越权小能手",光靠拒绝是不够的。
我的方案是"软拒绝"。触达层在拦截一个越权调用时,不会光说"你无权访问这个工具",而是会给模型一个解释和替代建议:"当前用户无权限访问query_employee_salary,建议调用query_employee_basic_info,或引导用户申请权限。"这样模型就有了可执行的替代路径,而不是卡在原地道具。表面看这是给模型的引导语,实际上相当于给越权行为做了一层柔性化解,整个交互的满意度提升明显。
6. 一点经验谈:做好触达层,比换更强模型更优先
这几轮项目做下来的体会是:很多Agent项目的瓶颈并不在模型聪明与否,而在触达层的工程成熟度。如果你现在也遇到"模型很聪明、但Agent总办不成事"的问题,我的建议是别急着换大参数模型,也别急着堆新的Agent框架,先把触达层做扎实:工具定义写得够不够清楚、权限边界收得够不够稳、调用失败有没有兜底方案、链路运行有没有完整日志。这些问题解决了,体验提升立竿见影。
Agent-Reach这套方案还有不少可以扩展的方向。比如我在考虑把工具执行的历史数据拿来做离线分析,统计每个工具的成功率、平均耗时、失败原因分布,然后反过来优化工具定义和重试策略。还有多Agent协作时不同Agent之间的"触达"关系,也是一种Reach能力,这部分的编排逻辑还在设计中。慢慢折腾吧,趁着这个方向还有大量空地,把基础打牢,比抢着赶时髦更有价值。