多智能体触达编排框架Agent-Reach:从上下文压缩到权限治理

发布时间:2026/10/27 16:22:58
多智能体触达编排框架Agent-Reach:从上下文压缩到权限治理

去年年底我负责的那套系统里,同时跑着三个智能体:客服、订单查询、库存分析。单独看任何一条链路都很顺畅,问题出在它们互相“够不着”对方数据这件事上。用户问了一句“我上周买的羽绒服什么时候发货”,客服智能体完全答不上来,因为它根本看不到订单状态。于是我开始研究多智能体之间的触达问题,想着能不能做一个框架,把“谁能找谁、怎么找、带什么上下文、有没有权限”这四件事一次管起来,这就是 Agent-Reach 的由来。

Agent-Reach 本质上是一个面向多智能体协作场景的触达编排框架,重点解决智能体之间的跨系统调用、上下文瘦身、权限控制和链路追踪问题。它把每个智能体能提供的能力抽象成注册项,把智能体之间的调用关系收敛成一张触达图,再通过路由决策器把一次请求送到正确的目标。如果你正在做多智能体产线,或者已经开始被“智能体调智能体”的上下文爆炸、权限失控、循环调用折磨,这篇文章的完整设计和踩坑记录应该对你有用。

1. 为什么我要做 Agent-Reach:一个真实的多智能体触达困境

1.1 背景:多智能体系统里最容易被忽略的“触达”问题

很多人搭建多智能体系统时,天然觉得“让一个智能体去调用另一个智能体”就是发一个 HTTP 请求这么简单。客服回答不了订单状态,那就把 openapi 地址配给它;库存分析需要调用 CRM 数据,那就再加一个工具函数。最初确实跑得通,但跑通和跑好是两回事。

我在实际项目中遇到了三个迭代阶段。第一阶段是单智能体挂工具,每个智能体只访问自己的知识库和数据库,问题不大。第二阶段是智能体之间开始互调,客服要查订单就得请求订单智能体,这就出现了“服务端”和“客户端”的边界问题。第三阶段是多个智能体互相组成网状协作,某个智能体既可能是调用方,又可能是被调用方,这时才真正意识到:每个智能体对外暴露的接口,不能只是简单的方法签名,它必须是一份完整的能力契约,包含入参、出参、权限、限流、超时、失败降级。否则系统大了以后,改一个智能体的内部逻辑就可能压垮另外五个调用方。

Agent-Reach 的定位不是重新发明一套智能体框架,而是在现有智能体之上加一层“触达治理层”。它不关心你的智能体是 LangChain 写的还是自研的,也不要求你重写业务逻辑,它只接管两件事:一次请求该往哪里去,以及这次请求能带多少上下文、能拿到多大权限。

1.2 我遇到的三类崩溃现场

先具体说说我在客户现场遇到的几个崩溃场面,这些是 Agent-Reach 设计需求的直接来源。

第一类是上下文爆炸。客服智能体为了确认订单状态,把自己和用户之间的整段对话历史全部打包,塞进给订单智能体的提示词里。一次两次没问题,但在高峰期,一段长达 50 轮、超过 8000 token 的上下文被原样转发,订单智能体不仅要解析订单号,还要忍受大量无关的寒暄和重复信息,响应速度从 1 秒跌到 5 秒,tokens 消耗也直接冲上云端账单。更糟的是,客服智能体自己的上下文窗口也被这条巨长的响应占满,后续对话质量明显下降。

第二类是循环调用。客服智能体发现自己缺库存数据,就去请求库存智能体;库存智能体需要知道物流信息,又请求了物流智能体;物流智能体看到这个单子涉及客户偏好,回过头来又请求客服智能体。整个过程没有任何人设置调用深度上限,四个智能体你拉我我拉你,最终把队列跑满,整条链路卡死。这个场景在真实业务里非常容易发生,因为每个智能体只知道自己缺什么,不知道谁正在等自己。

第三类是权限失控。我们的数据分析智能体被配置成可以访问 CRM 系统,本意是让它读聚合报表,结果某次触发了一个查询客户明细的调用,整个 10 万行客户列表被导出并拼进回复上下文里。那一刻我突然意识到,智能体之间如果只有“能调”和“不能调”的二元关系,而没有字段级、行级、操作级限制,它就是一个带着钥匙的定时炸弹。

2. Agent-Reach 的架构核心:触达图、能力注册表与路由决策器

2.1 触达图:先画清楚谁能找谁

做 Agent-Reach 的第一步,不是写代码,而是把团队里所有智能体之间真实的调用关系画出来。我在白板上画完才发现,很多人以为自己的系统是“中心辐射式”,实际上已经长成了全连通图。全连通图看起来灵活,但在智能体场景里就是灾难,每个智能体都要对其他 N-1 个智能体的能力做适配,调用深度不可控,安全审查也无从下手。

Agent-Reach 把触达关系抽象成一张有向图,节点是智能体,边代表“允许一次触达”。图由两份配置合并生成:一份是静态拓扑声明,在你确认某个智能体确实需要访问另一个智能体时手动添加;另一份是运行期策略,由管理员通过后台接口动态追加。我强烈建议第一版强制使用静态拓扑,动态策略等观察一段时间后再开放,否则你很难判断哪条边是真实业务场景需要的,哪条边只是某个智能体“心血来潮”想调用。

触达图上的每条边都有属性,不只是“可以访问”这么简单。属性包括允许的操作集合、单次调用的最大上下文大小、超时时间、每秒最大并发数,以及是否需要人工审批。举个例子,客服智能体访问订单智能体,允许的操作是“查询订单状态”和“查询物流轨迹”,但禁止“修改订单备注”;单次调用上下文最大 2000 token,超时 3 秒,并发上限 50,这些都是可以配置的。这张图最终被编译成路由表,供运行时查询。

2.2 能力注册表:用 JSON Schema 描述“你能帮我干什么”

触达图只是定义了“谁可以找谁”,真正决定调用质量的是能力描述。Agent-Reach 要求每个智能体对外暴露的能力都以 JSON Schema 形式注册,包括能力名称、输入参数、输出结构、错误码和权限标签。这样设计的好处是,调用方智能体不需要知道目标智能体的内部实现,只需要按 Schema 拼参数。

我们内部第一个注册的能力是“查询订单状态”,Schema 长这样:

{ "capability": "order.query_status", "version": "1.2.0", "description": "按 order_no 查询订单当前状态与预计发货时间", "input": { "type": "object", "additionalProperties": false, "required": ["order_no"], "properties": { "order_no": { "type": "string", "pattern": "^ORD\\d{8,12}$" }, "need_logistics": { "type": "boolean", "default": false } } }, "output": { "type": "object", "properties": { "status": { "type": "string", "enum": ["pending", "shipped", "completed", "cancelled"] }, "eta": { "type": "string", "format": "date" } } }, "permission_tags": ["order:read"], "timeout_ms": 3000, "rate_limit": { "rps": 50, "burst": 100 } }

这份 Schema 有三点是我交替迭代后确认下来的。第一,输入参数必须加正则约束和必填声明,否则上层智能体会传进来一些无法解析的脏参数,比如把订单号写成“ORD123”的英文缩写;第二,输出要明确枚举值,下游智能体可以直接做判断,不需要再猜;第三,permission_tags 不是装饰品,它会在路由阶段与请求方身份信息做匹配,不匹配直接返回 403,走不到业务代码。

2.3 路由决策器:把意图映射到具体能力端点

光有注册表还不够,Agent-Reach 还需要一个路由决策器,负责在收到一次触达请求时回答“这次请求需要哪些能力、按什么顺序调用”。路由决策器内部跑两层匹配。第一层是精确匹配,如果请求里已经显式声明了要调用的能力名,比如客服智能体明确说“我需要 order.query_status”,那么直接查表路由,速度最快。

第二层是语义匹配,适合上层智能体只给出自然语言意图的情况。比如客服智能体说“用户想了解物流,请帮我找能查物流轨迹的能力”,路由决策器会用 embedding 把这句话映射到向量空间,然后与能力注册表里所有能力的 description 做相似度排序,选 Top-N 返回给调用方确认。这里有个经验:语义匹配可以辅助,但不能代替人工确认。我会在返回结果时带上候选能力的 confidence 分数,低于某个阈值就混合一个“需要人工确认”的标记,避免路由错误把请求打到完全无关的智能体上。

路由决策器还要检查触达图和策略配置。一次请求即使语义匹配成功,如果触达图上没有对应边,或者权限标签不匹配,路由依然会拒绝。判断顺序上,先查权限再查语义、最后查限流,这是为了尽早拦截非法请求,节省计算资源。

2.4 触达策略:谁可以触达谁,什么条件下可以

触达策略是 Agent-Reach 里最细碎也最容易被低估的部分。我用一份 YAML 文件来描述,核心是 allow、deny、rate_limit、timeout 四类规则:

reach_policy: version: 2025-03-01 bindings: - from: customer_service_agent to: order_agent allow: - order.query_status - order.query_logistics deny: - order.modify_remark rate_limit: rps: 50 timeout_ms: 3000 - from: data_analysis_agent to: crm_agent allow: - crm.query_aggregated_report deny: - crm.export_raw_rows field_mask: - exclude: [customer_phone, customer_email] timeout_ms: 5000

deny 规则的优先级永远高于 allow,这一点必须写进代码里而不是依赖人脑记忆。我见过团队把 deny 写在后面,结果配置解析按顺序覆盖,本该被拒的导出操作反而被放行。field_mask 是字段级脱敏,它在下游返回数据前做一次序列化裁剪,比我之前在业务代码里到处写过滤逻辑可靠得多,因为你能在一个位置统一管理敏感字段。

需要提醒的是,策略文件里的每条规则建议加上版本号和生效时间,Agent-Reach 会把这些规则编译到内存中的一份只读快照,并通过原子替换方式更新。这样配置变更不会产生“一半请求用新策略、一半用旧策略”的中间状态。

3. 关键实现细节:三个核心模块的代码设计逻辑

3.1 上下文压缩器:只传对方需要知道的那部分

上下文压缩是 Agent-Reach 降低 token 消耗的核心模块。它的任务不是简单截断文本,而是在保留任务关键信息的前提下,把一段冗长的对话历史压缩成精炼的上下文卡片。

我的实现分三个阶段。第一阶段是实体抽取,用一套规则和标注模型从上下文里抽取出 order_no、user_id、sku_id、时间范围等结构化字段。第二阶段是语义摘要,调用 LLM 把整段对话压缩成 3-5 句会话概要。第三阶段是把实体和摘要合并成结构化的上下文字段传给下游智能体。核心代码如下:

def compress_context(history: list[dict], required_fields: list[str]) -> dict: raw_text = "\n".join( f"{turn['role']}: {turn['content']}" for turn in history ) entities = extract_entities(raw_text, required_fields) digest = summarize(raw_text, target_sentences=4) return { "entities": entities, "digest": digest, "original_length": len(raw_text), "compressed_length": len(digest), }

这里最需要控制的是实体抽取的结果质量。如果 required_fields 里有 user_id,而抽取器没有从对话里找到,它应该返回一个显式缺失标记,而不是留空数组。否则下游智能体会拿着一个空 user_id 去查询,最终只能返回“查无此人”,排查起来还得回溯到底是谁丢的字段。

我把 required_fields 定义在能力注册表的 input schema 里,调用方在发起触达请求时必须声明携带哪些字段。经过压缩后,同一段 8000 token 的对话上下文可以降到 200 到 400 tokon,同时对下游最有价值的结构化实体全部保留。这个模块上线后,我在另一篇文章里单独写过 token 成本对比,结论是压缩上下文比换更便宜的大模型参数每百万 token 的价格还要划算得多。

3.2 全链路追踪:一次跨智能体触达的完整日志

多智能体调试最大的难点在于,一个问题可能要跨三四个服务才能定位。Agent-Reach 从第一次请求进入网关时生成一个 reach_trace_id,并在所有内部调用里通过请求头透传。日志聚合后,你可以根据同一个 trace_id 拉出整条触达链路上每个环节的耗时、入参、出参、命中的策略规则。

具体实现上,我在 FastAPI 的中间件里统一处理 trace_id 的生成和透传:

@app.middleware("http") async def trace_middleware(request: Request, call_next): trace_id = request.headers.get("X-Reach-Trace-Id") if not trace_id: trace_id = f"trace-{uuid4().hex[:16]}" request.state.trace_id = trace_id process_time = time.perf_counter() response = await call_next(request) response.headers["X-Reach-Trace-Id"] = trace_id return response

日志字段必须包含四个维度:调用方智能体 ID、目标智能体 ID、能力名称、策略版本。缺任何一个维度,排查跨模块问题时就得靠猜。我遇到过的最难排查问题就是一个触达请求在系统里来回跳了 6 个节点,最后死在了第 6 个节点上,但因为日志只记录了智能体名称没有记录 trace_id,我花了一个下午才把链路拼出来。补上 trace_id 后,五分钟定位到问题根源是第 3 个节点的一个超时误判。

3.3 超时、降级与重试:触达失败的兜底策略

跨智能体调用比单体应用内函数调用脆弱得多,对方智能体可能因为上下文过长、依赖数据库慢查询、或者模型服务响应超时而迟迟不给结果。Agent-Reach 给每次触达设置了三层兜底。

第一层是超时控制,超时时间从能力注册表里读取,默认 3 秒。第二层是自动降级,当目标智能体指数级退避重试三次仍然失败时,调用方智能体会收到一个明确的“不可用”响应,而不是长时间阻塞。第三层是人工接管,高优先级的纠错类操作会在自动降级失败后生成一条待人工处理的消息,推给值班人员。

超时时间取值需要结合业务链路考虑。订单查询这种读操作,3 秒足够;跨智能体协作生成一份报告,可能需要 30 秒。不要把超时设得过大,否则一个下游智能体卡住,整条链路都会堆积线程。我建议所有触达链路都加一个总预算时间,比如单次用户请求在智能体编排层最多花 8 秒,子调用按比例分配,超了就快速返回当前已拿到的最优结果,而不是无限等待完整结果。

4. 实测数据:接入 Agent-Reach 前后的效果对比

4.1 上下文 token 消耗对比

我给一套真实线上系统做了七天的前后对比测试,场景就是“用户咨询订单状态”。第一组是基线组,客服智能体不经过 Agent-Reach,直接拿完整对话历史调用订单智能体;第二组是实验组,走 Agent-Reach 的上下文压缩器。为了控制变量,两条链路都使用同一个订单智能体、同一组测试对话集,共收集 2000 次触达调用。

指标(单次触达)基线组Agent-Reach 组
平均输入 token 数6842821
中位数输入 token 数7335764
p95 输入 token 数98601354
平均输出 token 数342338
平均响应延迟4.1 秒1.2 秒
成功率(包含超时重试后)93.8%99.1%

token 消耗降幅超过 87%,原因在于压缩器没有机械截断,而是先把实体挑出来,再用摘要概括无关对话,原始对话里用户聊的大量情绪化表达和不相关的商品偏好全部被过滤掉了。响应延迟的下降部分来自小上下文对模型推理速度的正面影响,部分来自超时和重试次数减少。

4.2 数据质量与准确性:压缩并没有让业务变差

token 降下来了,一个必须回答的问题是:压缩后的上下文还能不能支撑下游智能体做准确判断?我检查了其中 127 次有必要实体字段的调用,压缩器对 order_no、user_id、sku_id 三类核心字段的抽取准确率在 98.4%,剩余 1.6% 的缺失全部被显式缺失标记捕获,没有出现“静默丢字段”的情况。

下游订单智能体在拿到压缩上下文后,判断正确的比例是 98.2%,与基线组的 98.5% 基本持平。这说明上下文里的冗余信息对这类明确的任务判断帮助有限,真正起作用的是那几个关键实体。当然,如果你的场景是一个需要综合大量背景知识的决策型智能体,压缩策略就要更保守,我建议这类智能体用摘要加全文检索兜底,不能一刀切压缩。

4.3 权限事故归零的效果

接入前一个月,我们内部统计到四次智能体越权调用事件,最严重的一次是数据分析智能体尝试导出 CRM 原始数据行。接入后一个月,Agent-Reach 在路由层拦截了 12 次违反策略的调用,全部出自同一组调试脚本,没有一次真正到达业务层。这里有一个容易被忽视的收益:策略层拦截不仅保护数据,还制造了可审计的日志,方便安全团队了解哪些智能体正在尝试做它不该做的事,可以反向优化提示词,从根源上减少越权意图的产生。

5. 踩坑实录:我在 Agent-Reach 上摔过的四个跟头

5.1 第一个坑:循环触达,A 找 B,B 找 A,谁也不撒手

最早期版本我只做了路由和压缩,没有加调用深度控制。上线第三天,客服智能体在处理一个售后问题时发现缺少退货单数据,于是触达了售后退货智能体;退货智能体又想校验这笔退款是否已审批,又触达审批智能体;审批智能体想确认用户身份,又反向触达了客服智能体。整个循环转了 9 圈后才因为上下文超限被强制打断,期间产生了巨额 token 费用和大量误报日志。

修复方案是双管齐下。第一,每次触达请求携带一个 reach_depth 字段,入口算 0,每经过一个智能体加 1,超过最大深度 6 直接拒绝。第二,链路里始终保留 origin_trace_id,一旦发现同一个 trace_id 再次访问同一个智能体,立即判定为循环并返回明确错误。这个“同一个智能体重复访问”的检测逻辑简单又粗暴,但比单纯限制深度更能抓住循环的本质。

5.2 第二个坑:上下文压缩把关键实体弄丢了

上线前后端联调时,客服智能体传了一个 user_id,但订单智能体收到的上下文里没有该字段。查了日志才发现,压缩器在处理一段包含“新订单”和“历史订单”混合对话时,实体抽取器错误地把 user_id 归到了“非订单相关实体”而过滤掉了。这个问题比想象中严重,因为下游查询直接返回空数据,客服智能体会把“查不到”错误地理解为“没有订单”。

修复办法是给实体抽取器加一个 required_fields 白名单,凡是出现在能力注册表的 input.required 里的字段,抽不到就必须标记缺失并在日志里打 warning,不能直接丢弃。这一步也倒逼我在设计能力表时更谨慎地声明必需字段,避免下游智能体拿不到关键信息。

5.3 第三个坑:策略配置热加载时的竞态问题

为了让触达策略支持动态调整而不重启服务,我一开始用“定时读配置文件 + 内存变量替换”的方式。结果某次更新策略版本时,一个请求读取了新策略,另一个并发请求读到了旧策略,差了一个字段的权限判断,导致一个本应被拦截的导出操作在瞬间通过了校验。

解决办法是引入版本化原子切换:Agent-Reach 启动时把当前所有策略编译成一份不可变快照,更新时先构建新快照,构建完成后通过一个原子指针切换。所有请求只读到完整的新快照或完整的旧快照,不会读到拼凑状态。顺带也给策略文件加了格式校验,非法字段会在构建快照时报错,而不是默默吞掉。

5.4 第四个坑:一上来就做全连通图

最初设计时图省事,想用一份配置允许所有智能体互相访问,减少后续加边的麻烦。结果 6 个智能体就是 30 条边,安全审查寸步难行,大家根本说不清这条边到底是谁的业务需要。后来我收敛成了“子图内互通、子图间中继”的结构:同属于客服域的三个智能体在一个子图里可以互相触达,不同域之间统一由一个中继智能体代理,所有跨域请求在中继层做权限和审计。配置边数从 30 降到 11,反而清晰很多。

6. 这个项目适合谁,以及我打算继续扩展什么

6.1 场景适应性:不是所有系统都需要 Agent-Reach

做 Agent-Reach 的过程中我反复确认了一点:它不是银弹。如果你只有两个智能体,互相调用一次就能完成业务,那直接写一个函数调用就行了,引入治理层的成本大于收益。只有当智能体数量超过三个、且存在网状调用关系时,触达治理才值得做。另一个判断标准是失败影响面,如果某个智能体挂掉会导致三个下游智能体连带崩溃,那它就是需要治理的核心节点。

技术栈上,Agent-Reach 核心只依赖 Python 和 FastAPI,对已有系统侵入性比较低。你的智能体不管是用什么框架写的,只要提供一个 HTTP 接口,注册一份能力 Schema,就能接进来。这一点对已有系统改造很关键,我接入那三个存量智能体时没改它们内部逻辑,只给每个智能体配了一个适配层。

6.2 下一步计划:动态学习触达路径和可视化运维面板

当前版本还有一个短板:触达路径完全依赖人工配置,当业务复杂到一定程度时,配置维护成本会越来越高。下一步我想把触达图变成可学习的,让 Agent-Reach 自动记录那些高频、合法、无循环的触达路径,推荐给管理员一键固化。同时规划一个可视化运维面板,展示触达链路的实时拓扑、延迟分布、失败点和策略拦截记录,让运维人员不用翻日志就能看清整个协作网络的状态。

另外我准备把上下文压缩器扩展成按方向的场景模板。不同下游智能体关注点不同,订单智能体需要看订单号和时间线,客服智能体需要看用户情绪和沟通历史。给它们各配一套压缩模板,而不是用一套通用摘要硬套,应该还能进一步降低无效 token 的传输。

最后分享两点个人体会

第一,多智能体协作系统里,最大的成本往往不是模型的推理成本,而是智能体之间无效通信带来的隐性消耗。你费劲调好的一个智能体,可能因为另一个智能体给它的上下文太脏、太乱、太大,表现直接塌方。把触达层治理好,比单独优化某个智能体的提示词收益高得多。

第二,权限和审计一定要在上层治理框架里解决,别指望每个下游智能体自己自觉做过滤。人都会偷懒,让业务开发者在几十个接口里手动加权限校验,迟早会有漏网之鱼。把策略集中在 Agent-Reach 一层,用一份 YAML 管理整个系统里所有智能体的触达边界,安全审计也只需要看这一份配置。

我的建议是,不要一上来就把全公司的智能体都接进来。先挑两三个关系最复杂、最容易出问题的智能体做试点,跑两周看效果,再逐步扩大范围。触达治理这种事儿,做得越早越轻松,等网状调用已经长成一团乱麻再收拾,改造成本会高到一个让人崩溃的量级。