做AI Agent落地这大半年,我最大的感触是:模型能力早就不是瓶颈了,真正卡住项目进度的,往往是Agent怎么“够到”它需要的那些数据和服务。你让Agent写得一手好代码、做得出漂亮的规划,结果它想查个库存、发个消息、拉个报表,全都要靠人肉搬运,那这Agent就是个“脑子很好但手脚被绑住”的聪明人,落不了地。
所以当我开始搭一个叫Agent-Reach的内部项目时,核心目标就一句话:把Agent的“触达能力”做成一套可靠、可复用、可观测的基础设施。这篇文章我想把这套东西的思路、架构、关键代码和踩过的坑完整捋一遍,给正在做同类项目的团队一个参考。
1. 为什么“触达”会成为Agent落地的头号难题
先说说我为什么专门为“触达”单独立个项目,而不是直接在业务代码里东拼西凑。三个字:太乱了。
1.1 表面上是API调用,实际上是一堆“脏活”
你去看市面上的Agent框架,Demo阶段都特别光鲜:Agent自己规划步骤、自己调工具、自己总结结果。但一旦接真实业务,问题就全冒出来了:
- 内部系统有十几个,接口风格五花八门,有REST、有GraphQL、有gRPC,甚至还有只能连内网的老旧系统;
- 不少关键操作不是“调个接口”就完事,而是要过审批、走工单、做风控,这些流程Agent根本不知道;
- 权限模型复杂,同一个接口,不同角色能看的字段不同,Agent用谁的凭证去调?调错了算谁的?
- 第三方平台限流策略、幂等性要求、私有化部署环境,每个都有各自的脾气。
这些不是靠“让模型更聪明”能解决的,它需要一套工程化的东西去承接。Agent-Reach 说白了就是这一层:连接Agent和数据/服务之间的“路由器”。
1.2 一个比喻:触达层就是Agent的“手和脚”
如果把大模型比作大脑,那触达层就是手和脚。大脑负责思考“我要做什么”,手脚负责“我怎么够到那个东西”。大脑再聪明,手脚不利索,事情也办不成。
我之前在项目里见过一种典型情况:Agent要查“昨天华东区的销售额”,模型知道该去订单系统查,但它不知道该用哪个地址、该带什么token、返回的数据结构长什么样。于是开发者给Agent写了个自定义函数,查完发现字段名对不上,又开始写解析逻辑,最后还要处理超时和重试。光一个查询接口,就写了两三百行胶水代码。这种代码散布在项目各个角落,最终一定失控。
所以Agent-Reach 想解决的事情非常聚焦:把“Agent想做什么”翻译成“系统能执行的调用”,并且把整个过程中所有可复用的东西沉淀下来。
1.3 它适合什么样的团队来用
如果你满足下面任一条件,这套思路就值得参考:
- 你们正在做企业内部Agent,需要接多个业务系统,不想每个Agent各搞一套连接逻辑;
- 你们准备把Agent能力开放给多个业务方,但希望统一管控权限、审计和限流;
- 你们的Agent经常要处理长流程任务(比如“帮我处理退款”“帮我对接一下客户”),中间涉及多个系统协作;
- 你们对稳定性有要求,不希望Agent随便一个失败就把整个流程搞崩。
如果是上面这些场景,Agent-Reach 正是冲着这些痛点来的。
2. 整体设计:不追求“万能Agent”,先把连接这件事做稳
设定这个项目的时候,我给自己立了几个原则,也可以叫“反共识”的取舍,先说清楚,免得后面看着代码觉得奇怪。
2.1 先明确边界:触达层不做决策,只做翻译和执行
Agent-Reach 的核心定位是执行层,不是决策层。
- 决策(比如“该不该发起退款”“给用户的赔偿金额定多少”)由上层Agent或人工完成;
- Agent-Reach 负责把“已决策的动作”(比如“对订单ORD-10086发起退款,金额200元,原因:商品破损”)以标准化的方式,安全稳定地送达目标系统。
一开始我也想过让触达层参与决策,比如根据用户情绪自动调整赔偿金额,后来果断放弃了。原因有二:一是责任边界会变得很模糊,出了问题到底算Agent的还是算触达层的;二是在真实业务里,“能不能发退款”背后有一堆业务规则,这些规则应该由业务系统或专门的决策服务来管,不该让基础设施层去猜。
这个边界定下来之后,很多设计就顺了:Agent-Reach 收到的每条指令都是明确的、原子的。它要做的是:协议适配、参数校验、权限校验、流量控制、可靠投递、结果回传。
2.2 架构分层:网关、执行器、连接器,各管一段
我把整个系统分成三块:
网关层(Gateway):接收Agent发来的Action Request,做统一鉴权、限流、参数校验,然后根据请求类型路由到对应的执行器。
执行器层(Executor):负责编排一次特定类型的操作。比如“发起退款”是一个执行器,“创建工单”是另一个执行器。执行器内部定义了参数规范、校验规则、需要调哪些连接器、按什么顺序调。
连接器层(Connector):连接器是最底层的适配器,每一个面向一个具体系统。它知道目标系统的接口地址、认证方式、数据结构、限流要求。连接器只干一件事:把标准化的调用格式翻译成目标系统能理解的格式,并且把响应翻译回来。
这个分层最大的好处是:变化被隔离了。业务系统换了接口,只改对应连接器;新增一类操作,只加一个执行器;安全策略变了,只在网关层调整。Agent上层的代码基本不用动。
2.3 一次完整动作的链路图(文字版)
Agent-Reach 收到Agent消息之后,大致走这么一条链路:
- 网关层接收Action Request,解析出action类型、目标对象、参数、请求方身份;
- 做鉴权:这个身份有没有执行这个action的权限;
- 做参数校验:要求必须有订单号、金额必须为正数、原因不能为空;
- 做限流检查:这个action在当前时间段是否超过频控阈值;
- 将校验通过的请求包装成内部统一格式,投递给执行器;
- 执行器根据类型调对应连接器,连接器完成协议适配后调用目标系统;
- 目标系统返回结果,连接器做归一化,执行器判断结果状态,返回给网关;
- 网关把最终结果(成功/失败/需要人工介入)回传给Agent。
这整条链路里,每一步都可以被记录、被追踪、被审计。这也是我强调“触达”要做成基础设施的原因——它不是一个函数,而是一条完整的人造血管。
3. 能力拆解:我最终落地了哪些核心模块
这一节讲讲Agent-Reach 实际实现了哪些东西,也是我认为做类似项目最值得参考的部分。
3.1 Action Schema:让Agent和系统之间有一份“标准契约”
整个项目最关键的一个设计,就是引入了标准动作契约。每个可执行的动作(Action)都有自己的一套Schema,定义在JSON里:
{ "action": "refund.create", "version": "1.0", "input": { "order_id": {"type": "string", "required": true, "desc": "订单号"}, "amount": {"type": "number", "required": true, "min": 0.01}, "reason": {"type": "string", "required": true, "max_length": 200}, "source": {"type": "string", "enum": ["agent", "manual"], "default": "agent"} }, "output": { "refund_id": {"type": "string"}, "status": {"type": "enum", "values": ["PENDING", "SUCCESS", "FAILED"]} }, "rate_limit": {"calls_per_minute": 10}, "permission": "order:refund:create" }这套Schema不是给人看的,是给双方用的:
- 对Agent侧,它可以把这个Schema塞进Prompt里,让模型知道该提供哪些参数、格式是什么——这能显著降低模型瞎编参数的概率;
- 对Agent-Reach 侧,它可以直接用JSON Schema做运行时校验,不合法就直接拒绝,不用走到业务系统才发现参数错了。
我在实际项目里观察到一个规律:参数校验前置到网关层之后,Agent的错误率肉眼可见地下降了。原因也简单——模型在被校验挡回来几次之后,会慢慢学会照着Schema给参数。这比在Prompt里反复强调“请按照格式要求”有效得多。
3.2 连接器设计:每个外部系统都会被“规训”成统一格式
连接器对外暴露的接口我只定义了四个核心方法:
validate(context):轻量校验,比如参数是否齐全;execute(context):执行真实调用;recover(context, error):如果调用失败,尝试恢复;normalize(rawResponse):把原始响应转成统一结构。
以对接一个典型的内部订单系统为例,连接器大概长这样(Java伪代码,略去细节):
public class OrderSystemConnector implements Connector { public Response execute(ActionContext ctx) { // 1. 从上下文取参数 RefundRequest req = ctx.getInput(RefundRequest.class); // 2. 构造目标系统需要的请求体 Map<String, Object> body = new HashMap<>(); body.put("orderNo", req.getOrderId()); body.put("refundAmount", req.getAmount()); body.put("reasonCode", mapReason(req.getReason())); // 3. 走统一HTTP客户端,自动处理鉴权、超时、重试 HttpResult result = httpClient.post( "/api/v1/refunds", body, HttpOptions.builder() .auth(this.authStrategy) .timeout(5, TimeUnit.SECONDS) .retry(2, RetryPolicy.EXPONENTIAL_BACKOFF) .build() ); // 4. 归一化响应 return normalize(result); } }这段代码看起来简单,但真正实操的时候,mapReason这种翻译函数往往才是麻烦。不同系统对“退款原因”这种枚举值定义完全不同,我们这边叫“商品破损”,那边叫“DAMAGED_GOODS”,还夹着编码规则。这类差异都必须收在连接器层,不能让上层感知。
经验:连接器不要追求“一个类打完天下”,每个系统一个实现,宁可代码重复一点,也不要搞过度抽象的通用适配器。因为不同系统之间的差异,大到会让“通用”变成一个笑话。
3.3 可靠投递:Agent指令不能被“随便丢掉”
Agent发起的每一个动作,我都会生成一个唯一的ActionId,整个链路都带着它走。这个ID是追踪、审计、排查问题的核心线索。
投递可靠性的策略分三档:
- 同步直连:大多数查询类、低风险操作走这个,简单、快;
- 可靠异步投递:像“批量发送通知”“生成报表”这类不要求即时返回结果的操作,会先写消息队列,再由worker消费执行;
- 人工复核队列:涉及资金、权限变更、对外承诺的高风险动作,不直接执行,而是进入一个“人工审批队列”,由人来确认之后再真正调业务系统。
第三档是我在真实业务里觉得最值的一个设计。AI Agent在实际运行中,哪怕你Prompt写得再好、校验做得再严,总会有模型“自由发挥”的时候。让高风险操作强制经过人工复核,既保住了效率,也守住了底线。
这部分的实现也不复杂,核心就是给AsyncExecutor加一个requires_approval标志:
if (actionMeta.isRequiresApproval()) { approvalQueue.submit(actionContext); } else { executor.execute(actionContext); }3.4 可观测性:每一个动作全链路留痕
Agent触达东西多了以后,一定会出现一个问题:某一天用户来反馈“Agent说帮我处理了,但系统里根本没有”,你怎么查?
没有全链路追踪,光是定位这个“Agent以为做了”和“实际做了”之间的缺口,就能耗掉半天。所以在Agent-Reach 里,我强制要求每一步都打日志和指标:
- 网关层记录入参、校验结果、路由结果;
- 执行器记录执行耗时、调用了哪些连接器、每次调用的状态码;
- 连接器记录目标系统地址、请求体摘要、响应体摘要、异常堆栈;
- 所有日志都带上
ActionId、AgentId、TraceId。
指标方面我主要盯四个:成功率、P95耗时、限流拒绝数、人工复核队列积压数。前两个直接反映系统健康度,后两个能帮你预判Agent是不是要开始“憋活”了(比如队列积压严重,意味着Agent的请求在排队,用户体验会变差)。
这里有个心得:日志不要只记“成功/失败”,要把Agent最初发的原始指令也原样存下来。我见过太多排查困难的情况,都是因为中间某层只记了自己处理后的结果,等要找源头指令时发现早已丢失。
4. 关键设计决策:哪些地方我反复纠结过
这个项目有几个决策点,闭着眼睛也能做,但做了会后悔。我单独拿出来说说当时是怎么想的。
4.1 为什么选Java而不是Python/Go
Agent相关项目很多团队会默认选Python,因为模型生态好。但我给Agent-Reach 选的是Java(Spring Boot 3 + WebFlux),理由有三:
第一,这个项目要接的内部系统大部分是Java技术栈的,连接器要复用企业内部SDK的话,Java天然最顺;
第二,Agent-Reach 本质是个高并发的IO密集型服务,WebFlux的响应式模型在这种场景下资源占用更低,扛得住突发流量;
第三,Java在事务、消息队列、监控(比如Prometheus、SkyWalking)这些基础设施适配上,生态太成熟了,我们不需要再造轮子。
不是说Python不能做,如果你们的Agent本身是纯Python技术栈、内部系统也都是Python的,那用Python完全没问题。但跨技术栈硬接,会多出很多无谓的适配成本。
4.2 同步和异步的取舍
设计执行器的时候,我一度想把所有操作都做成异步的,好处是Agent不会被长时间阻塞。但实际跑下来发现这是错的。
查询类操作(查订单状态、查库存)必须同步返回,因为Agent的下一步决策依赖这个结果;创建类操作(创建工单、发起审批)可以异步,因为用户并不需要立刻看到结果;而确认类操作(确认收货、确认报价)通常需要同步,你要给用户一个即时反馈,告诉他“成了”。
同步和异步的比例,大概会落在“40%同步、60%异步”左右。这没有标准答案,跟业务强相关。我的判断标准就一条:这一步的结果,会不会影响Agent接下来15秒内的决策和回复?会,就同步;不会,就异步。
4.3 为什么给Agent的回复里强制留“证明”
这个算我的个人偏执。我要求Agent-Reach 返回给上层的结果里,必须携带“证据信息”:调用了哪个系统、拿到了什么单号、操作之后的系统状态是什么。
比如一个退款动作,返回结果是这样:
{ "status": "SUCCESS", "refund_id": "RF20241214001", "evidence": { "system": "order-center", "request_id": "req_9f2k3j", "target_status": "REFUNDED", "action_id": "act_7a1b2c" } }这样做的原因是:AI Agent在自然语言回复用户时,需要一段“硬事实”来垫底。有个refund_id和target_status在,它就能非常笃定地告诉用户“退款已成功,单号是xxx”。没有这些证据,模型容易在最终回复里含糊其辞,甚至自己脑补细节。这是减少AI幻觉的一个很务实的工程手段。
5. 真实落地中被反复摩擦的五个细节
这一节全是从项目上线后被现实毒打出来的经验,每一条都对应过真实的故障或返工。有参考价值,建议细看。
5.1 幂等性:没有幂等,Agent一回滚你就完了
这个坑我踩得很实。Agent在超时之后通常会重试,如果重试时没有幂等保护,一笔退款就被提交了两次。真实系统里,这类问题轻则重复扣款,重则污染数据、需要人工对账修复。
解决思路不复杂:所有写操作必须带上幂等键。我统一用ActionId作为幂等键传给下游系统。下游系统只需要在表里对这个键建唯一索引,重复请求直接驳回或返回已存在的结果。
还有一点:幂等不能只靠“调一次接口看看返回什么”来近似判断。因为如果你调第一次超时了,其实不确定它成没成功,这时候重试跟服务器确认的语义是完全不一样的。除非接口本身就支持“按幂等键查询执行结果”,否则不要假装它有幂等。
给执行器加幂等校验的伪代码逻辑大概是这样:
if (idempotencyStore.exists(actionId)) { return idempotencyStore.get(actionId); // 重复请求,直接返回之前的结果 } // 首次请求,执行并记录结果 Response response = doExecute(actionContext); idempotencyStore.put(actionId, response);超时、重试、表单重复提交,一网打尽。
5.2 超时管理:盲目设长超时,等于把故障放大
一开始我把外部调用的超时时间统一设为30秒,结果遇到一次下游系统雪崩,Agent-Reach 的线程全部挂在等响应上,整个服务跟着被拖垮。后来学乖了,改成“分级超时”:
| 调用类型 | 超时时间 | 重试策略 |
|---|---|---|
| 缓存/本地调用 | 500ms | 不重试 |
| 一般查询接口 | 3s | 重试1次 |
| 写操作 | 5s | 不重试,走幂等 |
| 人工审核后触发 | 10s | 不重试,告警 |
写操作我基本不重试,重试风险大于收益,不如把结果交给上层的业务补偿逻辑去处理。
5.3 限流不能只挡请求,还要“分级丢弃”
Agent并发上来以后,限流策略也得跟着迭代。最开始我是全局限流,发现一个Agent把配额全部吃光,其他Agent全部被拒绝。后来改成“按Agent ID分桶 + 全局水位线两层限流”,这样单个Agent发疯不会殃及别人,但也防了所有Agent加起来把下游打挂。
另外限流的拒绝方式也很关键:直接返回“429 Too Many Requests”,Agent会很困惑,然后开始重试,结果越限越死。更好的做法是返回“你已经被限流,预计xx秒后可重试”,让Agent有个明确预期。模型能理解这个提示,它会老实等待而不是疯狂重试。
5.4 凭证管理:别把密钥塞进Agent的认知范围里
有过一个安全小事故后,我把所有外部系统的访问凭证全部收口到Agent-Reach,上层Agent完全感知不到密钥。
具体来说:
- 连接器内部用自己的服务账号对接目标系统,Agent侧不传任何密钥;
- 每个连接器的认证方式可以独立配置(AK/SK、OAuth、证书),互不影响;
- 密钥存储在专门的密钥管理系统里,启动时拉取,不在代码和配置文件中明文出现。
这样设计还有一个附带好处:外部系统换密钥、换认证方式的时候,上层Agent完全无感。只改Agent-Reach的连接器配置,普通的大模型调用逻辑一行不动。
5.5 Agent“拒绝执行”时,要给它一个更好的回退选项
跑真实业务时,我发现Agent经常遇到“想做但做不到”的情况——权限不足、参数缺失、下游系统故障。如果Agent返回的是冷冰冰一句“我无法完成”,用户体验非常糟糕。
所以在Agent-Reach 里,每种失败类型我都设计了配套的“回退可选项”。比如参数校验失败,返回给Agent的信息里会直接列出缺失字段的格式示例;权限不足时,会返回“你需要有xx权限,可以找管理员开通”;下游系统故障时,返回“系统xx暂不可用,已为你自动转人工处理,请稍后在工单系统中查看进度”。
这些文案都是和上层Agent的Prompt一起调过的,目的就是让Agent的失败回复也尽量有用,而不是甩锅。
6. 我给同类项目的一份基础调试清单
如果你打算自己从零搭一个类似Agent-Reach 的触达层,下面这份清单可以帮你少走很多弯路。都是日常开发和压测时必须过一遍的项目。
6.1 功能自测:先把“能通”跑通
- 对每个连接器,准备一份mock数据,验证协议适配没有问题;
- 对每个执行器,准备好全部参数、缺参数、错类型、超范围四组测试用例;
- 对高风险动作,走一趟完整的人工复核流程,确认审批通过后才会执行、驳回后不会执行;
- 对所有写操作,连续重放同一ActionId,确认只有第一笔生效。
6.2 故障演练:把下游当“会死的服务”来测
- 模拟下游5xx故障,确认超时、重试、告警链路按预期工作;
- 模拟下游慢响应(故意延迟10秒),确认Agent-Reach的线程池不会被拖死;
- 模拟凭证过期,确认连接器能及时发现并抛出对应错误码,而不是给Agent一个“系统错误”这种模糊信息;
- 模拟消息队列堆积,确认异步任务消费有背压控制,不会一次性拉太多任务把下游打垮。
6.3 与Agent联调:别让Agent“自由发挥”
- 确认Agent生成的动作确实符合Schema定义,不符合要有默认纠偏逻辑;
- 确认Agent在收到错误码时,能根据Error Prompt给出合理的用户话术;
- 确认Agent不会擅自循环重试高风险操作(这块我在Prompt层面加了硬限制,并且Agent-Reach 的限流也兜了底);
- 确认Agent在长时间无响应时会主动通知用户“还在处理中”,而不是干等。
7. 下一步我要做的三件事
Agent-Reach 第一版已经跑通,但我很清楚它离“好用”还差得远。接下来几个方向是我已经在规划中的,也顺便说一下。
7.1 让连接器支持“动态发现”
目前的连接器都是预先注册好的。下个版本我想让外部系统通过注册接口动态提交自己的OpenAPI文档,Agent-Reach 自动生成对应的连接器模板。这样接入新系统的工作量会从“开发半天”降到“配置半小时”。
思考路径是:OpenAPI规范已经足够表达接口、参数、鉴权方式,只要做一个“OpenAPI到连接器模板”的转换器,剩下的交给工程化能力。但前提是要做好安全策略,不能允许一个未审核的连接器随便调用线上系统。
7.2 把失败恢复做成“可编排的自治流”
现在的故障恢复基本靠手工配置。我想升级成:当某个动作失败时,Agent-Reach 可以根据预设的策略自动选择“重试当前连接器”“切换备用连接器”或者“转人工”。这个相当于把运维手册里“失败后的处理步骤”给数字化。
这个方向做好之后,Agent触达层就不再是单纯的执行者,它会拥有一定的自治能力。但这里同样要注意边界,自治只限于技术层面(重试、切换备份),不能涉及业务层面的判断。
7.3 给Agent的上下文里注入“实时能力状态”
Agent在规划任务时,其实很需要知道“现在哪些系统可用、哪些接口在限流、哪些操作需要审批”。我打算把Agent-Reach 的能力列表和实时状态暴露成一个可被Agent定期读取的接口。这样Agent在第一步规划的时候,就能绕开不可用的工具,而不是等调用了才发现失败。
这一步做好的话,Agent的规划成功率和执行成功率都会有明显提升,因为它是从源头减少无效尝试。
说实话,Agent-Reach 这个项目走到现在,最让我确信的一件事情是:Agent项目的瓶颈从来不只是模型,而是它周围那套让它“够得着”的工程系统。把触达层做扎实,Agent才能真正从Demo走进业务流程里。接下来很长一段时间,我都会持续迭代这套东西,有新的坑和经验再来分享。