1. 从最开始的问题说起:为什么我做了 Agent-Reach
先说背景。我和团队在过去一年多里一直在做 AI Agent 相关的落地项目,帮客户把大模型能力接进内部系统。做到第三四个项目的时候,我们被同一个问题反复折磨:Agent 本身的推理能力已经很成熟了,但"触达"能力异常薄弱。所谓触达,就是 Agent 要真正完成任务,需要调用多少外部系统、读取哪些数据源、推送消息到哪几个平台、在哪个环境里执行命令——这套链路如果不打通,推理再强也只是一个聪明的聊天机器人。
我们最初的做法和大多数人一样:在各个业务方代码里各写各的胶水逻辑,A 项目的 Agent 直接调 HTTP 接口,B 项目的 Agent 直接连数据库,C 项目的 Agent 写死了一个 shell 脚本路径。几个月后维护成本彻底失控。后来我们决定统一做一层"触达中间件",让所有 Agent 通过同一套协议去发现能力、申请执行、拿回结果。这个中间件,就是 Agent-Reach。
Agent-Reach 不是什么大模型本身,也不是像 LangGraph 那样的流程编排框架。它的定位很单纯:一个让 Agent 与真实系统之间建立可靠连接的调度与执行平台。它负责解决四件事:Agent 能发现哪些能力、任务该派给哪个执行器、执行过程如何被追踪和降级、以及执行失败后如何恢复一致性。
如果你正在做 Agent 相关的生产项目,或者你需要把多个 AI 工作流接到真实业务系统里,这篇文章的很多设计取舍和踩坑记录可以直接拿来参考。
2. Agent 的能力建模:先让每个工具学会"自我介绍"
2.1 能力描述文件:一个 Agent 的"简历"
Agent-Reach 的第一个核心设计,是把每个可被调用的能力抽象成一个标准单元。我们称之为Capability。它不是简单的"函数名+参数列表",而是一份包含语义信息的完整描述文件。一个典型的能力描述长这样:
name: crm.order.create version: "1.0.0" description: 在 CRM 系统中创建一笔订单,支持部分预付款,创建后回传订单号 owner_team: commerce tags: [crm, order, write] schemas: input: type: object required: [customer_id, total_amount] properties: customer_id: { type: string, description: 客户唯一标识 } total_amount: { type: number, minimum: 0 } items: { type: array, items: { type: object } } output: type: object properties: order_no: { type: string } timeout_ms: 5000 rate_limit: { max_per_min: 120 }为什么要把描述设计得这么重?因为 Agent 不是人,它没法看到一个函数名就猜到意图。能力描述里的description字段是给大模型看的,越准确,Agent 选错工具的概率越低。我们在实际测试中发现,把description从一句话扩成两三句话之后,工具选择准确率能提升大概 8% 到 12%。
2.2 注册中心与心跳:能力不是写死配置,而是动态发现
Agent-Reach 里所有能力都通过注册中心动态上报。每个执行器(我们叫 Reacher)启动时向注册中心发起注册请求,上报自己支持的能力列表,之后每隔 30 秒发一次心跳。注册中心如果在 90 秒内没收到某个 Reacher 的心跳,就把它的所有能力标记为不可用。
这个设计初期看起来有点过度。为什么不直接改配置文件?因为我们的执行环境非常分散:有 Python 微服务、有 Node.js 工具脚本、还有跑在独立容器里的浏览器自动化实例。如果靠配置文件维护,每次上线新版本都要同步一堆服务,漏一个就出线上事故。动态注册的好处是:新能力部署即生效,下线也即生效,不需要中心化发布。
注册信息里还包含了负载权重。比如有两个 Reacher 都注册了crm.order.create,权重分别是 100 和 50,那派发请求时会按 2:1 的比例分摊流量。这个机制在灰度发布时特别好用——新版本 Reacher 先以 10 权重上线,跑半小时没问题再把权重提到 100。
2.3 能力的版本兼容策略
能力是会演进的。crm.order.createv1.0 需要传customer_id,v1.1 可能允许传customer_phone来替代。我们给每个能力设计了独立的版本策略:
- 同一 Reacher 可以同时注册多个版本,比如 v1.0 和 v1.1 并存。
- 调度器默认优先派发新版本,但如果新版本连续失败超过阈值,会自动回退到旧版本。
- 旧版本下线需要经过一个 72 小时的 cooldown 周期,期间仍接受灰度流量。
这套版本策略帮我们扛住了一次很典型的线上事故。某次改动 CRM 订单创建逻辑,新版本在特定商品编码上有 bug,会导致库存扣减异常。由于回退机制存在,系统在 4 分钟内自动把流量全部切回旧版,业务几乎无感。
3. 任务描述与派发逻辑:Agent 怎么表达"我想做什么"
3.1 从自然语言意图到结构化任务:任务意图模型
Agent 调用 Agent-Reach 时,不是直接说"帮我调一下 createOrder 接口"。它发过来的是一份Task 描述,里面包含意图和目标信息。我们设计了一个专门的 JSON 结构:
{ "task_id": "8f3a2b1c", "intent": "crm.order.create", "priority": "HIGH", "payload": { "customer_id": "CUST-20240112-001", "total_amount": 3999.0, "items": [{ "sku": "SKU-10086", "quantity": 1 }] }, "constraints": { "max_wait_sec": 10, "required_region": "cn-shanghai" }, "trace_id": "trace-xyz" }这里有个容易被忽略的关键点:intent不是最终调用的能力名,而是一个语义意图。调度器拿到意图后,会结合当前的注册信息、负载情况、能力状态,解析出实际要执行的能力列表和执行顺序。比如intent是crm.order.create,但调度器发现当前该能力需要先调用一个customer.query拿最新客户信息,它就会自动在任务链里插入这个前置调用。
这就是 Agent-Reach 与普通 RPC 框架最大的区别:普通 RPC 是客户端指定方法名,Agent-Reach 是客户端表达目标,由平台解析出执行路径。
3.2 调度器的拆单逻辑:一次意图可能拆成一棵树
复杂的用户请求往往需要多个能力协作。比如 Agent 被要求"给华东区所有高价值客户发送个性化的优惠券提醒"。这可以拆成三步:查询目标客户名单、调用营销系统生成优惠券、再逐批发送通知。Agent-Reach 把这种多步骤拆成一张有向无环图(DAG),每个节点是一个能力调用,边代表依赖关系。
调度器的核心算法是一个简单的分层拓扑排序:
- 根据意图解析出所有候选任务节点。
- 根据能力之间的依赖关系(比如生成优惠券依赖客户名单)分层排列。
- 同一层且无数据依赖的节点可以并行执行。
- 层间用 Barrier 同步,下一层必须等上一层到达终态。
为了照顾初学者理解,我把拆单结果可视化成一个简单的表格:
| 层级 | 任务节点 | 依赖 | 执行方式 |
|---|---|---|---|
| 1 | crm.customer.query_segment | 无 | 并行(按区域分片) |
| 2 | marketing.coupon.create_batch | 依赖层级1产出 | 并行(按批次) |
| 3 | notification.send_multicast | 依赖层级2产出 | 并行(按渠道) |
拆单逻辑里有一个很实际的经验:不要试图在调度器里塞一个万能规划器。早期版本我们直接用 LLM 做动态规划,效果看起来很美,但延迟抖动大,而且 LLM 偶尔会规划出幻觉任务节点。后来我们改成"LLM 负责从自然语言提炼意图,规则引擎负责 DAG 拆解",稳定性显著提升。顺序是:意图识别可以交给大模型,任务编排必须交给确定性规则的代码。
3.3 优先级与队列:不能让一个突发任务轰垮整个集群
任务进来之后会先进入调度队列。Agent-Reach 的队列模型分三层:
- 优先级队列:HIGH 走独立通道,不跟普通任务争抢资源。
- 租户队列:每个接入方(比如不同业务线)有配额上限,防止某个业务线异常时拖垮其他人。
- 执行器队列:每个具体的 Reacher 有本地排队长度,调度器会优先选择队列最短的执行器。
任务到了以后先做准入检查:意图是否存在、payload 是否通过 schema 校验、配额是否充足。校验不通过直接返回结构化错误,不会浪费下游资源。这里强烈建议在任务入口做校验,而不是让每个能力自己校验。因为 Agent 可能带着明显残缺的参数发请求,你去打 CRM 系统才发现参数缺失,平白浪费一次外部调用,线上成本完全是白烧的。
4. 实际落地时最绕不开的问题:传输与凭证安全
4.1 传输协议抽象:不是只有 HTTP 一条路
Agent-Reach 设计了一个协议抽象层,不同执行环境可以接入不同的传输通道。目前最常用的是三种:
- HTTP/REST:适合微服务执行器,简单直接,改造成本低。
- 消息队列:适合高吞吐场景,任务先落 MQ,Reacher 异步消费,天然具备削峰填谷能力。
- 内部进程通信:适合同进程内的轻量执行器,比如嵌入式脚本,省去网络序列化开销。
这里有一个取舍需要讲清楚。HTTP 最直观,调试也方便,但它的问题是:调用方必须同步等待响应,一旦下游慢,整个 Agent 的响应时间就被拖住了。我们实测某些业务方下游 API 平均耗时 200ms,但 P99 能到 4 秒。这种情况下同步调用会导致体验非常差。
后来我们默认推荐异步消息队列模式:Agent 提交任务后立即拿到task_id,执行完成后通过回调或轮询拿到结果。用户感知上虽然多了一个查询动作,但整体超时率大幅下降。如果你也遇到"Agent 一调用外部系统就卡死"的问题,先别急着优化下游,试着改成异步模型,很多问题会自动消失。
4.2 凭证管理:代理执行系统最容易翻车的地方
说到这个,我是很有发言权的,因为吃过亏。早期版本我们把各系统的 API Token 放在配置中心里,Reacher 启动时直接拉取。后来排查安全问题发现:日志组件会把 Token 带出来,因为某些外部 SDK 在 DEBUG 级别会把整个请求头打出来。很吓人。
为了让凭证不再裸奔,Agent-Reach 引入了一个简单的"凭证保险箱"模块:
- 凭证不直接下发给 Reacher。Reacher 每次调用外部系统前,向平台申请一个短时凭证。
- 短时凭证有效期 5 分钟,仅绑定当前 task_id 的范围,比如只允许访问特定订单号。
- 所有凭证申请记录留痕,出现安全事故时可以完整回溯。
这个设计最直接的好处是:即使某个 Reacher 被攻破,攻击者拿到的不过是一个 5 分钟有效且限定范围的一次性凭证,没法横向移动。
4.3 沙箱与最小化执行环境
有些 Reacher 要跑不可信代码,比如处理用户上传的模板脚本。这类执行器必须放进沙箱。我们目前用两种方式:
- 容器级隔离:每个任务执行时临时拉起一个容器,完成后销毁,适合 Python/Node 脚本。
- 进程级隔离:用子进程加资源限制(CPU、内存、文件句柄),适合简单命令,启动快,密度高。
安全不管多强调都不过分。即便你只在内部环境使用 Agent-Reach,也建议至少做进程级隔离,因为 Agent 的输入来自用户的自然语言,你根本无法预判用户会让 Agent 传什么字符串给外部系统。
5. 我把 Agent-Reach 放到生产环境后踩到的坑
5.1 超时与重试:看似简单其实最容易被低估
第一个线上事故,是下游系统一个很普通的数据库慢查询把整个 Agent 链路拖垮了。设计超时策略时,我们只给每个能力调用设置了固定超时(比如 5 秒)。结果某天 CRM 系统做数据迁移,部分接口 P99 响应时间到了 30 秒。一个任务里嵌套了 6 个能力调用,每一层都在等下游超时,最后整个任务跑了 2 分钟才失败。用户感知就是"Agent 卡死"。
后来我们把超时策略改成三层:
- 连接超时:3 秒,建立连接超过这个时间就换执行器重试。
- 请求超时:根据能力描述里的
timeout_ms,但允许动态调整,如果连续失败会自动翻倍。 - 总超时:整个 DAG 的 deadline,到期直接终止,将所有未完成任务标记为失败。
重试策略也细化成三种模式:瞬时重试(仅限网络错误)、幂等重试(适用读操作)、补偿型重试(适用于跨系统写操作)。写操作的重试必须非常谨慎,一定要配合幂等键。我们给每个任务在 payload 里注入idempotency_key,下游系统用它做去重,否则某次"重试成功但响应丢失"的场景下,用户会收到两次扣款通知。
5.2 成功但没收到响应:这是分布式系统最细思恐极的时刻
生产跑了两周后,我们遇到了一个经典问题:CRM 系统里订单已经创建了,但 Agent 侧收到的是超时异常。原因是 CRM 收到请求后执行完了,在返回响应的瞬间网络抖动,响应报文丢了。
如果这时候 Agent 自动重试,就会建出两笔一模一样的订单。早期我们天真地认为"超时=失败,重试没毛病",很快被业务方投诉了。解决方案就是上文提到的幂等键机制——每个任务生成一个全局唯一的幂等键,下游创建类的接口必须校验幂等键,已处理过相同幂等键的请求直接返回上一次的结果。
这里必须强调:幂等键不只是网络层的补救措施,它是 Agent 系统的地基设施。你需要提前跟下游系统协调好,让他们支持幂等消费。如果对方不支持,宁可让任务失败进入人工处理队列,也不能盲目重试。
5.3 任务状态一致性:用 saga 模式兜底多系统写操作
Agent 执行过程中经常需要跨多个系统写数据。例如"用户发起退款,Agent 负责通知财务系统冻结款项、通知库存系统解锁库存、通知 CRM 更新订单状态"。三个系统可能来自于完全不同的团队,没有分布式事务。
我们一开始尝试了简单的"全部成功才算成功"策略:只要有一个失败,就整个任务标记为失败。但这样做的后果是:财务系统已经冻结了资金,库存已经解锁了,但任务报错,没有任何人把前面两步的操作撤销。
后来采用的是 saga 模式的实现。每个写类型的任务节点可以注册一个补偿节点(compensate)。比如finance.freeze_fund的补偿节点就是finance.unfreeze_fund。调度器发现任务链中某个节点失败后,会按照逆序执行已完成节点的补偿逻辑,把系统状态拉回执行前。
实际效果:一次 CRM 订单状态分别在业务侧和 Agent 侧各记录一份,我们通过 saga 让数据库记录最终能收敛到同一个终态,没有出现过对账差异持续超过 1 小时的情况。
6. Agent-Reach 的骨架代码:用最少的代码跑通核心链路
虽然 Agent-Reach 是一个完整项目,但核心逻辑其实并不复杂。为了让你能快速理解它的运行机制,我在这里给出一个最小骨架(伪代码风格),它能帮你快速验证核心概念。
# 调度器核心骨架(概念演示,非完整代码) class AgentReachScheduler: def __init__(self): self.registry = RegistryClient() self.executors = ExecutorPool() self.queues = PriorityQueuePool() async def submit(self, task: Task): intent = task.intent plan = self.planner.resolve(intent) dag = self.planner.build_dag(plan) return await self.execute_dag(dag, task.payload) async def execute_dag(self, dag, payload): results = {} for layer in topo_layers(dag): layer_tasks = [] for node in layer: cap = self.registry.find(node.capability) if not cap or not cap.is_healthy(): raise CapabilityUnavailable(node.capability) executor = self.executors.select( cap, region=payload.get("required_region") ) layer_tasks.append( executor.execute_with_timeout(cap, merge(payload, results)) ) layer_results = await asyncio.gather(*layer_tasks, return_exceptions=True) self.assert_all_succeeded(layer_results) results.update(layer_results) return results这个骨架体现出来的设计要点有三个:一是每次执行前都要动态查注册中心,避免把执行地址写死;二是按层并行执行,同一层无依赖节点同时跑;三是任何异常都会向上抛,由调用方决定补偿策略。
真正生产环境中代码量不是一个文件能写完的,但核心思路就这么多。如果你只是想自己快速尝试 Agent-Reach 的理念,完全可以用这个骨架半小时搭一个 Demo。
7. Agent-Reach 下一步想做的事:触达只是第一步,记忆才是关键
现在 Agent-Reach 已经在我们多个内部项目里稳定运行了半年左右,管理的 Reacher 总数约在 80 个上下,每天处理的任务量在几十万量级。观察下来,最让我满意的其实是它的"故障自愈"能力——所谓自愈集中在超时、重试、降级这些规则行为上,是我们把系统做扎实后的自然产物,它确实帮我们省了很多半夜被叫醒的时间。
接下来我最想做的一件事是给 Agent-Reach 加上"记忆"层。目前它只负责触达和执行的调度,但 Agent 每次执行完任务后,产生的上下文(比如客户偏好、订单历史、操作习惯)都浪费了,没有被沉淀。
我设想增加一个执行记忆仓库:每次任务执行完成后,将脱敏后的上下文摘要、结果摘要、用户反馈写回到一个向量存储中。下次 Agent 接到类似任务时,可以先检索历史记忆,避免重复向用户确认信息。这个方向做扎实之后,Agent-Reach 就从一个"执行管道"升级成"越用越聪明的执行管道"了。
如果你也在做 Agent 触达层的建设,我特别建议你重点关注能力描述文档的规范性,以及幂等和补偿机制的设计。这些基础工作做得好不好,决定了你的 Agent 系统能走多远——推理模型一年比一年强,但连接真实世界的那一层,终究要靠工程一点一点垒出来。