
做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. 构造目标系统需要的请求体 MapString, 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/GoAgent相关项目很多团队会默认选Python因为模型生态好。但我给Agent-Reach 选的是JavaSpring 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走进业务流程里。接下来很长一段时间我都会持续迭代这套东西有新的坑和经验再来分享。