1. 项目概述与核心需求解析
1.1 Agent-Reach 到底是什么
年初我开始搞这个项目的时候,心里想的是“怎么能让一堆分散在各个系统里的智能体真正跑起来”。市面上的 Agent 框架、Agent 编排引擎不缺,但真正让 Agent 与 Agent 之间稳定通信、路由、协同的中间层,反而是大多数项目里最容易被低估的一块。Agent-Reach 这个名字起得很直白——它解决的就是“触达”问题:消息能到达、指令能到达、状态能到达。简单说,这是在多个 Agent 之上做了一层透明的通信与路由总线,让不同团队开发的、不同语言实现的、不同协议暴露的 Agent 服务,可以通过统一的方式互相找到对方、发送请求、交换事件。
如果你熟悉微服务里的服务网格或者消息中间件,那 Agent-Reach 的定位可以类比成“Agent 网格”。但它和传统服务网格的关键区别在于,Agent 不是无状态的 HTTP 服务,它有会话上下文、有长期记忆、有权限边界、有主动推送的行事风格。所以 Agent-Reach 的消息模型必须更丰富,路由规则必须更灵活,安全控制必须更细粒度。这也是我踩了无数坑之后,反复调整设计方案的根本原因——拿现有的微服务工具硬套 Agent 场景,短期能用,长期必炸。
这个项目适合谁看呢?两类人。一类是已经在跑 Agent 应用、但觉得各 Agent 之间像是“孤岛”,没法互相调用或者状态不同步的开发者;另一类是正在做 Agent 平台底座、需要为上层应用提供统一接入能力的架构师。博文后面所有内容都基于一个原则:把我实际验证过的东西拿出来讲,不写教科书里没有的悬浮方案。
1.2 触发我要做这个项目的三个场景
真正让我决定动手的,并不是某个 PPT 上的宏大叙事,而是三个极其具体的问题。第一个场景,公司内部同时存在三个 Agent:一个负责工单分类、一个负责知识库检索、一个负责自动生成周报。业务上希望工单分类 Agent 识别出“这个工单需要查历史方案”,自动把上下文转给知识库 Agent,拿到结果后再回流给周报 Agent。表面上看,相互调 HTTP API 就行了,但真实落地很痛苦:三个 Agent 的开发语言不同,数据格式各写各的,有的返回 Markdown,有的返回 JSON,Message 里根本没有统一的 request_id 概念,出了问题根本没法串链路排查。
第二个场景,Agent 的会话是有寿命的。用户跟工单分类 Agent 聊五分钟,这个过程中 Agent 可能需要动态调用别的 Agent,但用户的身份凭证在多个服务之间怎么安全传递?总不能每个服务都拿着用户名、密码各自去登录一遍。需要一种“身份印章”机制,让上游 Agent 在调用下游时,带着一个可验证的调用链上下文,下游知道是谁在调用、原上下文是什么、权限边界在哪。
第三个场景更加头疼,Agent 之间长任务协作。比如让数据分析 Agent 拉取数据,活干到一半,它需要问一下风险控制 Agent 某个策略是否允许这样算。这种长时间挂起的请求,如果用普通 HTTP 同步调用,连接早就断了。Agent-Reach 必须支持一种轻量的任务句柄机制,让请求可以先被接受、拿到 task_id,之后主动查询结果或者靠回调通知,而不是一条路走到黑同步阻塞。三个问题放到一起,结论就是一个专用通信层,而不是简单的服务网关。
2. 整体架构设计与核心技术选型
2.1 为什么不能用现成的 RPC 框架硬扛
先说结论:我一开始直接用 gRPC 加 Protobuf 做了第一版,后来又加了 Kafka 做事件广播。技术上完全可行,但逼近生产环境时发现,Agent 场景的通信模式弹性远比普通服务大,RPC 框架的“请求-响应”心智模型太窄了。
普通微服务基本两种模式:同步调用的 API,或者异步的消息队列。Agent 之间的交互远不止这两类。除了常见的同步问询、异步发布订阅之外,还有一类很特殊的模式叫“委派-回报”。A Agent 把一件事完整托付给 B Agent,B 在执行过程中可能需要找 C、D 拿辅助信息,最后 B 把完整结果回报给 A。这一来一回里,A 并不关心 B 是通过什么步骤完成的,但 A 需要能随时撤销委派、查询进度、甚至在 B 挂掉之后重新分派。这种模式在 RPC 框架里实现很别扭,但在 Agent-Reach 里它就是一等公民。
还有一层原因更现实:Agent 之间的消息格式不是固定的。不同的 Agent 可能是不同时期、不同团队甚至不同供应商开发的,你没法要求大家都把自己的核心数据结构暴露在 IDL 里,更不可能共用一个大而全的 Proto 仓库。所以 Agent-Reach 在设计之初就放弃了强类型定义,转而在消息外壳上做统一,把载荷部分保持为二进制透明传。外壳负责路由、鉴权、追踪、超时、优先级,内壳交给业务自己解析。这个思想其实很像 HTTP 的 Header 和 Body——协议管 Header,业务管 Body,只不过咱们的 Header 更懂 Agent 一点。
2.2 三层模型:传输层、消息层、语义层
Agent-Reach 的架构我分成了三个明确层次。最底下是传输层,负责真正把字节流从一个进程搬到另一个进程。默认支持两种,一种是基于 HTTP/2 的长连接传输,适合有固定网络环境的内部服务;另一种是 WebSocket 通道,适合浏览器端接入或者任意网络穿透场景。传输层只解决“能不能到”的问题。
中间是消息层,这是 Agent-Reach 最核心的部分。消息层定义了统一的外壳格式,包含消息 ID、消息类型、来源 Agent 标识、目标 Agent 标识、路由策略、优先级、超时控制、调用链上下文、身份令牌引用。所有 Agent 通信都只认这个外壳,业务数据塞在 payload 字段里。这样不管上下游用什么语言,只要实现了一个简单的 SDK 编解码,就能接入网络。
最上面是语义层。这一层开始理解 Agent 的特殊概念,比如会话 session、任务 task、上下文 context、记忆 memory、能力 capability。语义层会做事件映射,把底层的消息流转变成 Agent 能理解的领域事件,比如“任务已委派”“上下文已更新”“能力未注册”。语义层也是插件机制的主要挂载点,后面要说的中间件全部工作在语义层。
之所以刻意做三层解耦,是因为每个层次都允许独立演进。传输层以后要支持 QUIC 或者 MQTT,可以不动另外两层;消息层以后要支持新的路由算法,同样不需要重写上层逻辑。
2.3 通信模式的分类:命令通道与事件通道
Agent-Reach 里把通信分成两大通道:命令通道(Command Channel)和事件通道(Event Channel)。
命令通道负责一切明确的、需要执行的请求。它的特征是必须有接收方、必须等待处理结果或明确失败。命令通道内部支持同步等待(适用于低延迟、可预期的调用)、异步等待(先拿到 task_id 再轮询或回调)、委派追踪(A 发出后,这个命令被 B 进一步转委给 C,链路可追踪)。命令通道是“点对点”的。
事件通道则负责广播、订阅、通知。比如某个 Agent 完成了训练、某个 Agent 检测到数据异常、某个 Agent 的状态发生变更,这些不是发给特定对象的,而是发给所有感兴趣对象的。事件通道内部带主题(topic)机制,同时支持通配符订阅(比如agent.analytics.*)。事件通道是“一对多”的。
两个通道共享同一个传输管道,但路由策略和可靠性要求完全不同。命令通道必须做到不丢消息,该重试重试;事件通道则允许采用“尽力投递”策略,允许消费者自己拉取补偿。如果混在一起管理,你会发现可靠性配置变得无比细碎,所以从设计上分开,后续的监控报警也能分门别类。
3. Agent 注册发现与消息路由实现
3.1 Agent 身份模型:能力标签比服务名更重要
在传统微服务里,服务发现靠的是服务名和实例地址。但在 Agent 场景里有一个严重的语义错位:一个 Agent 实例,可能同时具备多项能力;同一个能力,又可能被多个 Agent 实例实现。如果你只靠“服务名”路由,打个比方就相当于你跑到餐厅只说“我要找厨师”,不说“我要做宫保鸡丁”还是“我要做牛排”——厨师能接,但匹配粒度太粗了。
Agent-Reach 的身份模型里,一个 Agent 实例注册时,必须声明自己的能力集(capabilities)。每个能力有一个全局唯一的名字,比如text.summarize、data.query.sql、risk.control.check。能力名字建议用领域.动作.细分的三段式命名,域名用倒置也行,但一定要可读、可归类。注册信息里还要附带一个 JSON Schema,描述这个能力接受什么参数、返回什么结构。能力是一个 Agent 对外提供价值的唯一线索,找到合适能力的 Agent 比找到某个名字的 Agent 更有意义。
注册之后,Agent 还会声明两类元数据:一是运行时特征,比如支持的并发量、平均处理时长、是否支持流式输出、是否支持取消任务;二是接入信息,即命令通道和事件通道的地址、协议、鉴权方式。运行时特征对路由打分非常重要,同样是两个能查数据的 Agent,一个平均响应 100 毫秒但并发只有 5,另一个响应 500 毫秒但并发 50,路由策略会根据自己的负载情况做出不同选择。
3.2 路由策略:从简单匹配到加权打分
有了注册信息,路由就是一个典型的决策问题。Agent-Reach 里实现了四级路由,从前往后依次生效。
第一级是能力硬匹配。调用方声明要什么能力,系统把注册表里具备该能力的 Agent 全部选出来。一个能力都不具备的直接淘汰。
第二级是策略过滤。根据调用的上下文环境过滤掉不合规的 Agent。比如数据敏感级别:这个请求涉及用户隐私数据,那就只允许通过 PII 审计的 Agent 参与;再比如网络区域:请求发起方在上海机房,优先过滤同区域的 Agent 以降低时延。策略过滤是强制性的,目的是把候选集缩到一个安全合法的范围内。
第三级是偏好匹配。调用方可以在请求里附加偏好条件,比如“优先选择支持流式输出的”“优先选择上次交互过的”“优先选择延迟低于 300 毫秒的”。这些偏好不是硬性约束,但会给对应的 Agent 加分。
最后一级是动态加权打分。系统根据 Agent 的实时健康指标(当前负载、最近失败率、响应延迟分位数、队列深度),给每一个候选 Agent 算出动态分。加权公式是静态权重和动态权重的乘积。静态权重是运维人员配置的(比如 A 是主用,B 是灾备,静态权重 A 更高),动态权重是每个周期自动更新的。最后选分最高的实例。
这里有个关键细节:路由结果一定要带上“路由理由”。Agent-Reach 的每个路由决策都会生成一条路由记录,内容是“为什么选了 B 而不是 A”,具体到哪个策略加分、哪个指标导致减分。没有这个能力,生产环境出了诡异现象连查都没法查。我印象很深的一次,某个 Agent 持续被路由到一台负载已经很高的实例上,看路由记录才发现,偏好匹配里有人配了prefer.same.host,这个偏好权重设得太高了,直接把动态分压过去了。
3.3 注册中心的存储与会话
注册中心看起来就是个服务端数据库,但实现时要注意几个点。Agent 实例的注册信息不是一锤子买卖,它需要定期续约,否则过期自动摘除。续约周期我默认设 10 秒一次,过期时间设 30 秒。这个节奏我们压测过,心跳太勤对上千个 Agent 会造成不小的压力,太慢又会导致故障转移不灵敏,10秒/30秒是平衡点。
注册信息我存放在内存里,同时定期落盘做快照。内存结构包括三个索引:能力索引(capability -> agent id 集合)、实例索引(agent id -> 注册详情)、租户索引(namespace -> agent id 集合)。增删改查都是 O(1) 或 O(log n) 级别。Agent 数量一旦超过几千,全量广播注册变更就不现实了,所以我加了一个版本号机制:每个 Agent 的注册信息带有版本号,实例之间通过同步版本号来判断是否要拉取增量。
3.4 一个路由请求的完整生命周期
既然聊到实现,用一个完整例子串一遍。假设外部业务系统向 Agent-Reach 发了一个命令请求,要求调用能力data.query.sql。
第一步,Agent-Reach 的接入网关解析外壳消息,验签、鉴权。通过之后,提取目标能力名和后置策略标签。
第二步,路由引擎从能力索引里捞候选 Agent。假设捞出来三个:Agent-A、Agent-B、Agent-C。
第三步,策略过滤。这次请求带了 PII 标记,Agent-B 没通过 PII 审计,被直接划掉。剩 A 和 C。
第四步,偏好匹配。请求里说“优先延迟低”,Agent-A 的静态延迟基线 100ms,Agent-C 是 450ms,A 加上偏好分。
第五步,动态打分。查询监控数据:A 最近错误率 2%,C 错误率 0.1%,但 A 的 QPS 只用了 30%,C 已经跑到 80%。动态分算下来 A 依然领先,于是路由选择 A。
第六步,把命令转发给 A 的命令通道,并把路由记录持久化。同时设置全局超时、重试策略、优先级标记。A 处理完后,结果消息沿着同一链条返回。
整个过程对调用方是透明的,调用方只看得见“我发了请求,Agent-Reach 帮我找到合适的人处理了,结果回来了”。这也是我觉得 Agent-Reach 最有魅力的地方:路由本身变成了一种可以演进、可以迭代的基础设施能力。
4. 状态同步、会话上下文与任务生命周期管理
4.1 Agent 状态同步为什么不能靠数据库轮询
Agent 除了消息通信之外,还有一个绕不开的主题:状态。每个 Agent 都有状态——它当前在忙什么、它记住的上下文的版本、它已完成任务的结果集、它的健康状态。最笨的办法是把所有状态写到数据库里,其他 Agent 需要时去查。真的这么干了,你会发现两个问题:第一,查询方拿到的永远是上一秒的快照,如果 Agent 刚更新完状态还没提交,别人拿到的是旧值,业务上可能做出错误判断;第二,高频状态变化会让数据库压力大得离谱,明明一个 Agent 每几秒就发一次心跳信息,全部写库太浪费。
Agent-Reach 里的状态同步采用事件流机制。每个 Agent 在自己的生命周期里,把状态变更作为事件发布到自己的状态流(State Stream)上。比如agent.online、agent.busy、agent.context_updated、agent.task.completed。其他关心这些状态的 Agent,以订阅者的身份订阅对应流。事件流天然具备顺序性,并且可以回溯,新加入的订阅者可以从某个历史位置开始重放。
4.2 会话上下文传递:从单次请求到多轮协同
做 Agent 的人都知道,会话上下文是灵魂。A 叫 B 帮忙处理一个请求时,如果 B 拿不到 A 之前和用户的聊天记录,那 B 只能从零开始理解,效果会很差。但如果把整个聊天记录全部塞给 B,大概率超过模型上下文窗口,还带来大量无关信息。
Agent-Reach 的做法是引入上下文引用与按需拉取。命令消息里不直接携带完整上下文,而是带一个context_ref,指向一个上下文存储服务。B 收到消息后,可以根据自己的需要拉取部分上下文内容——比如只拉取最近三轮对话、只拉取用户明确声明过的偏好、只拉取数据查询的中间结果。这样既实现了信息传递,又控制了传递量。
上下文存储服务在 Agent-Reach 内部是一个独立的模块,它有两个操作:写入上下文片段、按条件查询上下文片段。每个上下文片段都带标签,比如dialog.turn.3、user.preference.timezone、analysis.intermediate.sql。查询时支持按标签范围和关键词组合过滤。这正是为了照顾不同 Agent 的上下文需求差异。
还要处理上下文覆盖问题。同一个任务链条上,如果 A Agent 修改了上下文内容,B Agent 后收到消息时怎么知道自己该用旧版本还是新版本?我采用了乐观版本控制:每条上下文都带版本号,命令消息里会带上自己期望的版本范围,B 拉取时如果发现版本冲突,会提示上游“上下文已变更”,由上游决策是放弃还是重试。这样避免了分布式系统里最难搞的强一致问题,把取舍权交还给业务。
4.3 任务生命周期:从 wait 到 cancel 的七种状态
Agent-Reach 的任务管理一开始我简化成了三步:创建、执行、完成。真实跑起来立刻发现不够用,后来扩展成了七种状态。
- PENDING:任务已创建,排队等待被调度。
- DISPATCHED:任务已分发给某个 Agent 实例。
- RUNNING:Agent 开始执行,并周期性上报进度。
- WAITING_INPUT:执行过程中需要额外信息,暂时挂起,类似上面说的“数据分析 Agent 去问风控策略”。
- COMPLETED:执行成功,结果已归档。
- FAILED:执行失败,带错误码和失败上下文。
- CANCELED:被调用方或管理者主动取消。
这七种状态里的核心是WAITING_INPUT。这个状态在普通任务系统里很少见,但在 Agent 协作里几乎是常态。本质上是因为 Agent 执行任务时并不是一条直线,它经常需要“想一下”“问一下”“等一下”。没有显式的 WAITING_INPUT 状态,上层只能通过超时来判断是不是卡住了,非常笨拙。有了显式状态,调用方可以优雅地等待,Agent 也可以随时恢复。
每个任务在 Agent-Reach 里都有独立的 Task 对象,包含任务 ID、发起方、接收方、当前状态、状态流转历史、关联的命令消息 ID、关联的上下文版本。任务 ID 全局唯一,后续所有查询、进度汇报、取消操作都基于它。任务完成后,结果保存在结果存储中,保留一定期限供调用方拉取。
4.4 超时、重试与幂等控制
分布式环境里的消息通信,永远绕不开“消息到底送达没有”的问题。Agent-Reach 采用的可靠性策略是三层配合。
第一层是传输确认。命令消息发送到接收方时,接收方必须先回一个 ack 消息,表示“我收到了”。这个 ack 在传输层做,不依赖业务。如果收不到 ack,发送方就认为可能丢了,进入重试流程。
第二层是业务确认。接收方 Agent 真正处理完消息后,要发出一个task.completed事件。如果发送方一直没等到这个事件,会按照预设规则重新发送原命令。这里必须做幂等。Agent-Reach 的每个命令消息自带msg_id,接收方 SDK 会自动做去重——同一个msg_id只处理一次。因此命令的执行结果要么是一次真实执行,要么是一次“已处理过”的回放,从上层看效果一致。
第三层是外部补偿。有一些流程确实不适合简单重试,比如一个任务已经走到 WAITING_INPUT,此时重发命令没有意义,因为 Agent 正在等人输入。这种情况就启用任务查询机制,发送方用 task_id 轮询任务当前状态,直到它走出 WAITING_INPUT。三种层配合下来,基本不会出现“僵尸消息”或者“重复执行”这种老大难。
5. Agent-Reach 的安全模型与接入实践
5.1 双向身份认证:不只是 API Key
Agent-Reach 的安全设计,第一步是搞清楚“谁在说话”。和 Web 应用不同,Agent 之间往往是系统在调用系统,没有用户交互会话。最常见的做法是发一个 API Key,但我觉得远远不够。API Key 一旦泄露,攻击者就能以你的 Agent 身份做任何事,而且不好吊销。
Agent-Reach 里默认采用 mTLS 双向证书认证,每个接入的 Agent 都有独立的证书,由系统内部 CA 统一签发和吊销。连接建立时,双方都要验证对方证书链,也就是说只有持有合法证书的 Agent 才能连到 Agent-Reach,同时 Agent-Reach 也要证明自己不是伪装的中间人。证书的有效期我设的是 90 天,到期自动轮换并且推送新证书到 Agent 实例。
但 mTLS 解决传输级身份还不够,应用级身份必须再做一层。每个 Agent 实例在注册时获得一个agent_id和一个初始化令牌,后续所有业务消息都会携带这个 agent 的签名。所以即便是从合法通道进来的消息,如果签名验证失败,一样会被拒绝。
5.2 调用链上下文与权限边界传递
Agent-Reach 里有一个贯穿所有模块的东西,我叫它CallChainContext,调用链上下文。它是一段结构化数据,记录了当前这条调用链条的完整信息:最初的发起者是谁、经过了哪些 Agent、当前的深度是多少、携带的安全策略标签是什么。
权限判定完全依赖这个上下文。比如 A Agent 调用 B Agent 时,B 需要判断 A 是否有权限请求某项能力。Agent-Reach 不做全局权限表,而是把权限判断下沉到每个 Agent 的能力接入处。Agent 在注册能力时,声明一个权限策略,比如“仅允许来自 internal.production 命名空间的 Agent 调用”。当请求到达时,Agent-Reach 会把调用链上下文里的命名空间、凭据信息传给能力校验器,校验器给出允许/拒绝的结论。
传输层还做了一个非常实用的功能:上下文防伪。调用链上下文里边有每一跳的签名,任何中间人试图篡改来源 Agent 的标识,后续校验就会失败。这一点在跨团队协作时尤其重要,防止某个团队为了绕过权限,伪造别人的上下文来调高权限接口。
5.3 接入 Agent-Reach 的五个步骤
说了那么多设计,给一组实操步骤。假设我现在有一个 Python 写的 Agent,要接进 Agent-Reach。
第一步,安装 Agent-Reach SDK。我这里有一个agent_reach_sdk的 Python 包,封装了注册、消息发送、事件订阅的全部逻辑,底层走 HTTP/2 或 WebSocket。
第二步,实例化 SDK 并配置证书。代码里指定证书路径、Agent-Reach 网关地址、运行环境(生产/测试)。SDK 启动时会自动建立 mTLS 连接。
第三步,定义能力并注册。代码示例如下:
from agent_reach_sdk import AgentRuntime, Capability cap = Capability( name="data.query.sql", description="执行只读 SQL 查询并返回结果", input_schema={ "type": "object", "properties": { "sql": {"type": "string"}, "limit": {"type": "integer", "default": 100} } }, output_schema={ "type": "object", "properties": { "columns": {"type": "array"}, "rows": {"type": "array"} } }, execution_mode="sync", avg_duration_ms=300 ) runtime = AgentRuntime(agent_id="analyst-agent-01") runtime.register_capability(cap)第四步,订阅事件并启动服务。比如订阅task.command.*事件,收到事件后调自己的业务函数。
async def on_command(event): if event.capability_name == "data.query.sql": payload = event.payload result = execute_sql(payload["sql"], payload.get("limit", 100)) await event.reply(result) runtime.on("task.command.*", on_command) await runtime.start()第五步,启动之后在 Agent-Reach 控制台确认注册成功、健康检查正常,然后用一个测试命令把链路跑通。
上面这组代码我故意写得很简洁,实际上每一步背后都有一堆细节,比如心跳怎样发送、回调里抛异常怎么处理、证书轮换时怎么做优雅下线和重连。这些细节 SDK 已经帮你处理掉了,但理解原理会让你在配置的时候更安心。
5.4 事件订阅的两种模式
Agent-Reach 里事件订阅分为两种模式:队列模式和广播模式。队列模式适合多个 Agent 实例处理同一个任务,比如多个数据分析 Agent 同时订阅data.query.sql的请求事件,Agent-Reach 会把事件轮流分配给不同的实例,形成负载均衡。广播模式则适合所有实例都要收到的情况,比如model.updated这种模型版本变更通知,每个缓存了旧模型的 Agent 都应该收到并更新。
选错模式是很经典的坑。一开始我把所有事件都配成广播,结果同一条任务命令被两个副本同时执行,数据写了两遍。后来又把一个通知事件配成了队列,导致只有一台实例更新了缓存,其他实例还在用旧数据。所以设计事件 Topic 时,必须想清楚这个事件的语义是“谁来做”还是“谁都要知道”,前者走队列,后者走广播。
6. 四个典型异常场景的排查实录
6.1 症状一:消息已发送但 Agent 一直没处理
这是最常遇到的问题。排查路径我固定成下面几步。先在 Agent-Reach 控制台看消息状态。如果消息卡在PENDING,说明路由引擎还没完成匹配,去看是不是候选 Agent 一个都没选出来——大概率是能力名拼错了,或者目标 Agent 已经下线。如果消息已经变为DISPATCHED但没有后续,问题出在接收端。去目标 Agent 的日志里查有没有收到 ack 的回执,没收到说明网络分区或者证书过期导致连接断开。
遇到这个问题我们要学会看一个指标,Agent 的“命令 ack 率”。健康状态下应该接近 100%,一旦掉下来,网络中间设备或者证书轮换出问题的概率很大。证书轮换是我踩过的坑,轮换后 Agent 还在用旧证书,连接被网关拒绝,消息全部堆积。后来我加了证书到期前自动重连的钩子,问题才消失。
6.2 症状二:任务执行到一半变成僵尸任务
任务变成僵尸指的是状态停留在RUNNING或者WAITING_INPUT,再也没有进度上报。这种情况先确认 Agent 是否真的在处理,还是进程已经挂了。如果进程还活着但没有进度上报,很可能是业务代码里有个长耗时操作没走 Agent-Reach 的进度上报接口。Agent-Reach 里进度上报是显式的,业务代码里每隔一定阶段就要上报一次,比如处理完每一批数据。
如果确认进程已经死了,Agent-Reach 会因为心跳超时把对应任务标记为失败,此时发起方会收到失败事件。有一个特殊情况是 WAITING_INPUT 状态的任务,进程死了又重启,它不知道自己之前有任务挂着,此时需要依赖任务存储的持久化能力,重启后主动拉取自己的未完成任务列表,恢复上下文。恢复逻辑我实现了“断点续跑”,先把状态流重放到崩溃点,再继续执行。
6.3 症状三:高并发下消息乱序导致上下文覆盖
当同一个会话里有多个并发命令时,目标 Agent 可能同时收到多个消息。如果这些消息里都带了上下文更新命令,后到的消息可能让上下文回复到旧版本,覆盖掉新内容。我们称之为上下文竞态。
处理方案是在上下文存储层加了一个序列号检查:每个上下文更新请求必须带上基础的版本号,如果存储里当前版本高于传入版本,则拒绝这次更新。这个策略叫乐观锁。Agent 收到拒绝后,会重新拉取最新上下文,再做业务判断。代价是多一轮交互,但好处是绝对不会因为并发更新把上下文搞乱。如果业务对延迟敏感,可以把拒绝的比例调低,但必须保证至少对关键上下文做了乐观锁。
6.4 症状四:跨命名空间调用被拒绝
生产环境里多个团队共用一套 Agent-Reach,命名空间隔离做得好的话,跨命名空间调用必须是显式配置的。有一段时间,数据分析团队想调用风控团队的risk.control.check能力,后台日志一直报 403。查下来发现,风控 Agent 注册时权限策略写的是allow: namespace = risk-control,而数据团队调用时传的 namespace 是>