过去半年我一直在折腾一套内部代号为Agent-Reach的多智能体调度基础设施。起因很简单:当项目里的 AI Agent 从 2 个变成 20 个的时候,你会发现真正难的已经不是让单个 Agent 学会调用工具,而是你根本不知道此刻该把任务交给谁。这就像你手里攥着一堆各自能干的员工,但没有组织架构图、没有分工表、也没人告诉你谁正在休假、谁已经离职了。Agent-Reach 就是为这个问题写的答案——一套把 Agent 的"能力边界"和"运行状态"变成可查询、可路由、可兜底的服务治理方案。
这篇文章我会先把 Agent-Reach 的设计思路拆开讲透,再给出从零实现一个最小可用系统的完整代码和参数计算过程,最后把我实际跑业务时踩过的一些坑原原本本摆出来。不管你是正在做 Agent 应用开发的工程师,还是准备搭一套 AI 中台的技术负责人,这篇文章应该都能提供一些可以直接抄作业的参考。
1. 多 Agent 时代,为什么还需要一张"路由表"
1.1 单个 Agent 很强,一群 Agent 很乱
单个 Agent 的能力边界是比较清晰的:你给它一个任务、一组工具、一份 system prompt,它就能在限定范围内把事情办完。但一旦进入多 Agent 场景,问题就变味了。举个我在实际项目里遇到的例子:一个企业知识库问答系统里,我同时部署了负责制度文档检索的 Agent A、负责数据报表查询的 Agent B、负责工单系统操作的 Agent C,还有一个号称"全能"的通用 Agent D。
刚开始的做法最简单粗暴——所有请求一股脑发给 Agent D,让它自己判断要不要调用其他 Agent。结果可想而知:Agent D 经常高估自己的能力,明明需要查报表,它却直接编一个数字出来;偶尔它也会把任务分发下去,但下游 Agent 返回的中间结果在它手里走一圈后,信息损耗严重,用户看到最终答案时经常一脸问号。
后来我换了一种思路,让每个 Agent 独立对外提供服务,由上层代码自己写 if-else 做分发。这个方案在 Agent 数量少的时候还能跑,但很快就暴露出问题:每加一个新 Agent,业务代码就要改一次;Agent 的调用地址、超时时间、可用状态散落在各个服务的配置文件里,根本没法统一治理。更麻烦的是,一旦某个 Agent 因为模型服务限流而暂时不可用,调用方并不知道,只能等超时后才报错。
1.2 Agent-Reach 的三层"可达性"
我给这套系统取名 Agent-Reach,其实是把"Reach"这个词拆成了三层含义,这也是整个设计的核心骨架:
- 路由可达:一个任务进来后,系统能根据任务意图和 Agent 的能力声明,把任务准确送到正确的 Agent。
- 能力可达:每个 Agent 通过结构化描述,明确告诉系统"我能处理什么、不能处理什么、需要什么参数、返回什么格式"。能力可达是路由可达的前提。
- 运行可达:Agent 是否在线、心跳是否正常、平均响应时间是否在容忍范围内。运行可达决定了路由表里某个条目是否会被临时摘除。
这三层对应到架构上,就是注册中心、能力描述协议和健康检查探活机制。Agent-Reach 本质上是一张活的路由表——它不只是静态的映射关系,而是会根据每个 Agent 的实时健康状态动态调整路由优先级。任务进来时,系统优先把流量分给健康的、能力匹配度最高的 Agent;当某个 Agent 连续失败时,系统自动把它降级,并把任务转发给备选 Agent。
1.3 什么时候你其实不需要 Agent-Reach
不是所有多 Agent 项目都需要这么一套系统。如果你的业务只是把三五个 Agent 写死在代码里串行调用,或者你用的是某个商业化平台自带的可视化编排工具,那硬套一套自研调度中间件反而会增加维护成本。我在设计 Agent-Reach 之前也犹豫过一段时间,后来想明白了——只有当"Agent 数量"和"调用方数量"同时多到一定规模时,这件事才值得做。
具体来说,我判断的标准是三条:第一,Agent 数量超过 5 个,且各自负责完全不同的领域;第二,至少有两个以上业务方需要调用 Agent 能力,且它们不应该感知到 Agent 的具体部署位置;第三,Agent 的可用性开始直接影响线上核心链路,一旦某个 Agent 挂掉,需要有降级和兜底机制。如果你已经踩中了这三条中的两条,那 Agent-Reach 这类"Agent 服务治理"思路大概率能帮到你。
2. Agent-Reach 的总体设计与能力边界
2.1 核心模块拆解
Agent-Reach 在逻辑上由五个模块组成,但它们的实现复杂度差别很大。我在实际编码时,是按"先跑通路由,再补可观测,最后做治理"的顺序推进的。
| 模块 | 职责 | 核心难点 |
|---|---|---|
| 注册中心 | Agent 上线时登记能力描述、接入地址、鉴权信息 | 能力描述的标准化 |
| 路由引擎 | 匹配任务意图与 Agent 能力,生成路由决策 | 语义匹配的准确度 |
| 健康检查器 | 周期性探测 Agent 存活与响应状态 | 参数调优,避免误判 |
| 执行网关 | 转发请求、聚合响应、处理超时重试 | 上下文状态保持 |
| 可观测后台 | 记录路由决策、耗时、失败原因 | 链路数据的关联分析 |
这里我要特别说明一点:Agent-Reach 不是一个重编排引擎。它不负责定义"先调 A 再调 B"的复杂工作流,也不做任务拆分和结果合并。它只解决"这个任务应该给谁"和"这个 Agent 现在能不能接"这两个问题。至于 Agent 内部的推理过程、工具调用顺序,我完全交给 Agent 自己控制。这个取舍让 Agent-Reach 的定位非常清晰,也避免了和专用编排产品做重复的事。
2.2 Agent Manifest:给每个 Agent 写一份"岗位说明书"
Agent-Reach 的路由引擎不靠关键词匹配,也不靠写死的正则规则,它依赖一份结构化的 Agent 能力描述文件——我把它叫做 Agent Manifest。每一份 Manifest 都遵循统一的 JSON Schema,里面主要包含四类信息:
{ "agent_id": "hr-policy-agent", "name": "政策制度问答 Agent", "version": "2.3.1", "endpoint": "http://192.168.1.20:9001", "auth_token_ref": "token/hrsvc", "description": "负责回答员工关于考勤、休假、报销、绩效考核等公司内部制度类问题。仅可回答中国地区分支机构的制度,海外制度不在范围内。", "capabilities": { "input": { "type": "text", "max_length": 8000, "required_fields": ["query"] }, "output": { "type": "text", "content_format": "markdown", "max_tokens": 2000 } }, "tags": ["制度", "考勤", "报销", "HR", "员工手册"], "fallback_agents": ["general-chat-agent"], "timeout_ms": 40000, "health_check": { "path": "/healthz", "interval_seconds": 10 } }这份 Manifest 里最关键的不是tags,而是description和fallback_agents。我踩过的坑是:一开始我把 tags 设计得太简单,结果路由引擎经常把"年假怎么休"这类问题错误地判给了报销 Agent。后来我重写了描述,采用"职责边界 + 明确排除项"的写法,匹配准确率才有明显提升。关于 fallback,它解决的是"这个 Agent 确实匹配任务,但它不可用"的情况——路由引擎会把任务降级给fallback_agents里声明的替身,这是整套系统的兜底机制。
2.3 从规则路由到语义路由
Agent-Reach 的路由引擎我分了两代来实现。第一代是最朴素的基于标签的硬匹配:任务文本先做关键词提取,再和 Agent Manifest 里的 tags 做交集,谁命中的标签多就把任务给谁。这个方案的问题显而易见——用户的说法千奇百怪,"我要请两天假"这句话能命中"考勤"标签,但"家里有点事得回去一趟"就完全失配了。规则路由适合 Agent 数量少、业务边界非常清晰的场景,但它撑不住长尾表达。
第二代我改成了语义路由:每个 Agent 的 capability 描述和 description 先离线走一遍 embedding 模型,生成向量存入内存;任务进来时对用户 query 做同样的向量化,然后计算 query 向量与每个 Agent 描述向量之间的余弦相似度,得分最高的 Agent 胜出。这套方案上线后,路由准确率直接从 67% 提到了 91%。但注意,语义路由也有自己的新问题:相似度过低时该怎么处理?阈值定多少合适?这些问题我放到第 4 章的踩坑部分详细讲。
2.4 为什么选"瘦网关"而不是"重编排"
在设计阶段,团队里讨论过要不要把 Agent-Reach 做成一个完整的工作流引擎——支持 DAG 编排、条件分支、人工审批节点。我最后力排众议选了瘦网关方案,核心判断依据是:工作流引擎和路由网关处理的是两个不同维度的问题。
工作流引擎解决的是"一个复杂任务拆成哪几步、步骤之间的依赖关系是什么",它天然带有业务语义,应该有研发人员在界面上精心设计;而 Agent-Reach 解决的是"一个已经明确的单步任务交给谁执行",它更像网络里的路由器——不关心数据包怎么被生成,只负责把它送到正确的下一跳。把这两件事混在一起,会让系统变得极其复杂,而且一旦业务编排逻辑变了,你又要动基础设施的代码。瘦网关让 Agent-Reach 保持简单可靠,同时为上层的编排工具留出空间。
3. 实操:从零实现一个 Agent-Reach 最小系统
3.1 第一版注册与发现:别一上来就上 Redis
很多人听到"注册中心"就会想到用 ZooKeeper、etcd 或者 Redis 来做服务发现。我在第一版实现时采用了最笨的方式——一个本地 JSON 文件加定时扫描,这个决定帮我省了很多事。因为 Agent-Reach 在初期根本没有那么强的动态扩缩容需求,Agent 的上下线频率很低,用文件存储完全够用,还免去了中间件运维的成本。
# register_agent.py -- Agent-Reach 注册接口(基于 FastAPI) import json import pathlib from datetime import datetime from fastapi import FastAPI, HTTPException from pydantic import BaseModel app = FastAPI() REGISTRY_FILE = pathlib.Path("./agent_registry.json") class AgentManifest(BaseModel): agent_id: str name: str endpoint: str description: str capabilities: dict tags: list[str] = [] fallback_agents: list[str] = [] timeout_ms: int = 30000 health_check: dict = {} auth_token_ref: str = "" @app.post("/register") async def register_agent(manifest: AgentManifest): registry = json.loads(REGISTRY_FILE.read_text()) if REGISTRY_FILE.exists() else {"agents": {}} registry["agents"][manifest.agent_id] = { **manifest.model_dump(), "registered_at": datetime.now().isoformat(), "last_heartbeat": datetime.now().isoformat() } REGISTRY_FILE.write_text(json.dumps(registry, ensure_ascii=False, indent=2)) return {"status": "ok", "agent_id": manifest.agent_id}这个注册接口的逻辑很简单:收到 Agent 的 Manifest 后就写入本地 JSON 文件,同时打上注册时间戳和心跳时间戳。但在实际运行时,有几个细节必须处理好。第一,registered_at和last_heartbeat不能只存日期,必须存完整 ISO 格式,我调试时就吃过"任务刚分发过去就判定 Agent 失联"的亏。第二,重复注册时不要报错,直接覆盖旧条目即可——Agent 重启后重新注册是很正常的操作。第三,注册接口一定要做鉴权,至少校验一个内部 token,我见过裸奔的注册中心被内网扫描器灌了各种垃圾 Manifest 的案例。
发现端的逻辑更简单:路由引擎启动时读一次 JSON 文件,然后每隔 30 秒重新加载一次。为什么是 30 秒而不是实时推送?因为 Agent 数量少的时候,30 秒的延迟完全无感,而实时推送需要引入消息队列或者 WebSocket,复杂度会直线上升。等到 Agent 规模超过 50 个、上下线变得频繁之后,再接配置中心不迟。
3.2 路由引擎的核心逻辑:规则 + 向量双重匹配
路由引擎是 Agent-Reach 的大脑,我的实现不是抛弃规则路由,而是让规则和语义各管一段。整个路由流程分两步走:第一步用一批硬规则做过滤,比如"用户明确提到要查报表,那回答制度问题的 Agent 直接排除";第二步在剩余候选集里用语义相似度做排序。这种"规则前置缩小候选集,语义排序决定最终结果"的方式,比单纯用语义匹配要稳得多。
# router.py -- Agent-Reach 路由引擎核心逻辑 import json import numpy as np from sentence_transformers import SentenceTransformer class AgentRouter: def __init__(self, registry_path: str, model_name: str = "BAAI/bge-small-zh-v1.5"): self.registry = self._load_registry(registry_path) self.model = SentenceTransformer(model_name) self.agent_vectors = self._precompute_vectors() def _load_registry(self, path: str) -> dict: with open(path, "r", encoding="utf-8") as f: return json.load(f)["agents"] def _precompute_vectors(self) -> dict: # 用"描述+标签"拼接作为向量化文本,比只用 description 效果更好 vectors = {} for agent_id, info in self.registry.items(): text = info["description"] + " " + " ".join(info["tags"]) vectors[agent_id] = self.model.encode(text, normalize_embeddings=True) return vectors def route(self, query: str, top_k: int = 3): q_vec = self.model.encode(query, normalize_embeddings=True) scores = {} for agent_id, vec in self.agent_vectors.items(): scores[agent_id] = float(np.dot(q_vec, vec)) # 按相似度降序排序,返回前 top_k 个候选 ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_k] return ranked这段代码里有一个值得展开的设计:_precompute_vectors用的是"描述 + 标签"的拼接文本。单纯用 description,向量容易过度聚焦在句子的表面措辞上;单纯用 tags,又会丢失大量语义信息。两个拼在一起,相当于既拿到了关键词的强信号,又保留了完整句子的上下文语义。Embedding 模型我选的是bge-small-zh-v1.5,它在中文场景下的效果不错,而且模型体积小,CPU 上就能跑,不需要为路由功能专门烧一张 GPU 卡。
关于向量维度和模型选择,我的建议是先用小模型起步,等实际业务里出现明显的路由误判,再换更大的模型对比。这里有一个反直觉的经验:路由准确率从来都不是 embeddings 模型的单点问题,Manifest 里的描述写得稀烂,换再大的模型也没用。所以,先把 Agent 描述写清楚,比换模型性价比高得多。
3.3 硬性过滤规则:避免"看起来像"的误判
语义路由解决了长尾表达问题,但也带来了新的麻烦——语义相似度只能告诉你"像不像",不能告诉你"是不是"。我在实际运行中发现,用户的 query 里经常会出现一些关键词,它们和某个 Agent 的领域文本高度相似,但实际意图完全不同。
举一个让我印象深刻的例子:用户问"绩效考核结果能不能作为年终奖发放的依据",这条 query 和 "hr-policy-agent"(政策制度 Agent)的描述向量相似度高达 0.86,按语义路由应该分给它。但这条问题实际上需要同时读取绩效系统和薪酬制度两边的数据,仅靠一个文本问答 Agent 根本答不了。类似这种边界模糊的问题,只靠向量匹配是不够的。
所以我加了三层硬性过滤规则,在向量排序之前先跑一遍:
# hard_rules.py -- 路由前硬过滤 HARD_RULES = [ # (正则表达式, 排除的agent_id列表, 原因) (r"查|查询|报表|统计|数据", ["hr-policy-agent", "general-chat-agent"], "包含数据中心词"), (r"工单|报障|运维|线上故障", ["report-chat-agent"], "包含工单中心词"), ] def apply_hard_filters(query: str, candidates: list[str]) -> list[str]: for pattern, exclude_ids, reason in HARD_RULES: if re.search(pattern, query, re.IGNORECASE): candidates = [aid for aid in candidates if aid not in exclude_ids] return candidates这些硬规则不需要写得特别多,也不需要覆盖所有场景,它们只负责拦下那些"高相似度但方向完全错误"的 case。我带团队的时候经常说:语义路由负责找到最像的,硬规则负责排除最不像的,两者配合才能达到比较稳定的路由质量。具体维护多少条规则,取决于你们业务里出现误判的频次,我自己的经验是每个月根据线上误判日志沉淀两三条就足够了,不需要一开始追求完备。
3.4 健康检查与熔断降级:三个关键参数的计算方式
Agent-Reach 的"运行可达"能力依赖健康检查模块。这个模块本身不复杂——定时向 Agent 的/healthz接口发 HTTP 请求,根据返回状态码和响应时间判断 Agent 是否健康。但如果参数没调好,整套系统会比没有健康检查还糟。我在这里总结出三个关键参数的计算方式:
| 参数名 | 我采用的配置 | 推导逻辑 |
|---|---|---|
| 心跳间隔 | 10 秒 | 等于"平均单次任务耗时(5秒)"的 2 倍,保证不会把处理中的任务误判为失联 |
| 超时时间 | 3 秒 | 内部网络探活一般 1-3 秒即可,超过 3 秒基本说明 Agent 卡死或网络拥堵 |
| 熔断阈值 | 连续 3 次失败 | 基于"至少覆盖一个完整探测周期(30秒)"的原则,避免单次抖动触发熔断 |
这里有一个初学者最容易踩的坑:他们喜欢把心跳间隔设得很小(比如 1 秒),以为这样能更快发现问题。但实际上,Agent 正在处理一个长任务时,它的主线程可能正忙于等待模型返回,根本没空响应健康检查请求。如果心跳间隔太短,这个 Agent 就会频繁被判定为不健康,反而被摘出路由表。我的建议是:心跳间隔至少是 Agent 平均任务耗时的 2 倍,这样才能把"忙"和"死"区分开。
熔断降级的执行逻辑我用 Python 伪码表示如下:
# circuit_breaker.py -- 熔断状态机 class CircuitBreaker: def __init__(self, failure_threshold: int = 3): self.failure_threshold = failure_threshold self.failures = {} self.state = {} # agent_id -> "closed" | "open" | "half_open" def record_success(self, agent_id: str): self.failures[agent_id] = 0 self.state[agent_id] = "closed" def record_failure(self, agent_id: str): self.failures[agent_id] = self.failures.get(agent_id, 0) + 1 if self.failures[agent_id] >= self.failure_threshold: self.state[agent_id] = "open" def is_available(self, agent_id: str) -> bool: return self.state.get(agent_id, "closed") != "open"熔断器打开之后,我不会立刻把 Agent 永久剔除,而是让它进入半开状态,每隔一分钟放一个探测请求过去,看看 Agent 有没有恢复。这个"半开探测"的频率我选的是 60 秒,为什么是 60 秒而不是 10 秒?因为如果 Agent 是发生了内存泄漏或模型服务挂了,10 秒级别的探测只会持续加剧后端压力;60 秒既能让恢复后的 Agent 及时回到路由表,又不会对病中的 Agent 造成二次伤害。
3.5 健康数据如何反哺路由权重
健康检查模块产出的数据,如果只在熔断时用一下就太浪费了。Agent-Reach 里我把健康数据做成了一个"路由权重调节器"——路由引擎的最终得分不只是向量相似度,还要乘上一个健康系数。具体公式是:
最终得分 = 语义相似度 × 健康系数 健康系数 = 1.0(健康) / 0.7(平均响应时间超过正常值2倍) / 0.0(熔断中)这个设计的价值在于:当两个 Agent 的能力描述高度相似时(比如两个都内置了通用知识问答能力),系统会自动把流量偏向更健康、响应更快的那一个。这有点像 CDN 调度里的"负载最小优先"策略,只不过我们的权重信息来自健康检查数据而不是节点负载上报。实现上只需要在读注册表时额外读一个健康状态映射表,路由排序时乘上去就行,改动成本很小,但收益非常直观。
4. 实战中的拦路虎与排查套路
4.1 路由对但 Agent 答不对:问题出在上下文传递
Agent-Reach 上线后遇到的第一个大坑,不是路由错配,而是路由对了、Agent 也接了,但返回结果质量惨不忍睹。排查了半天发现,问题出在执行网关把任务转发给 Agent 时,只转发了用户最原始的那条 query,完全没有带上这个用户的历史对话上下文。比如用户先问"我们公司的年假制度是什么",Agent 正常回答了;用户接着问"那休年假期间工资怎么算",这个"那"字指向的目标如果不带上上文,新接手的 Agent 根本不知道用户在问什么。
解决方式很简单,但也藏着坑:网关里要维持按用户ID + 会话ID维度的上下文缓冲池。第一版我图省事,直接把最近的 6 轮对话文本拼在一起传给 Agent。结果上下文里混入了之前路由给其他 Agent 的中间结果,反而带来了严重干扰。后来我改成按 Agent 维度隔离上下文——每个用户在每个 Agent 侧都单独维护一份短期记忆,任务路由到哪个 Agent,就只带那个 Agent 的记忆过去。这个改动让答案质量立刻回升。
4.2 语义路由的相似度阈值应该设多少
语义路由上线后,我一直在调一个问题:当用户 query 与所有 Agent 描述向量的最高相似度都低于 0.65 时,该怎么办?如果调低阈值到 0.5,很多不相关的问题会被硬塞给某个 Agent,答出来就是一堆幻觉;如果调高到 0.75,又会有大量边缘问题被拒之门外。
我最终的方案是双阈值加兜底策略:主阈值 0.65,次阈值 0.45,低于 0.45 直接转人工兜底。具体逻辑是:相似度大于 0.65 的,交给最高分 Agent 处理;在 0.45 到 0.65 之间的,不直接拒绝,而是把任务分给最高分和次高分两个 Agent,让它们各自给出答案,再通过一个简单的投票或质量评分选出更好的结果——但这种方式会增加延迟,只在拿不准时启用。
这个双阈值方案我跑了两个星期,统计下来,进入"双 Agent 投票"模式的任务占总量约 12%,但这 12% 的任务贡献了超过 30% 的客服工单质量投诉和表扬。所以我现在的建议是:不要追求 100% 的自动路由,保留一部分不确定性任务给双路验证,反而是提升整体质量的关键。
4.3 微服务拆分后的健康检查竞争
当我把 Agent 服务的健康检查路径从/healthz改成/actuator/health时,出现了一个有意思的问题:一个本身已经很健康的 Agent 突然开始频繁被熔断,而它的上一级数据库其实已经因为连接池耗尽而濒临崩溃了。
这就是健康检查设计里最经典的"表面健康但实际不健康"问题。Agent 的主线程是健康的,但它依赖的数据库连接池已经满了,真正执行任务时根本拿不到连接,只是健康检查接口返回了一个 200,让人误以为一切正常。解决方式是在健康检查接口里加一层轻量的依赖自检:
# healthz.py -- 健康检查的依赖自检 async def healthz(): status = {"status": "ok"} try: # 检查数据库连接池是否有可用连接 async with db.pool.acquire(timeout=2): pass except Exception as e: status["status"] = "degraded" status["reason"] = f"db_pool_unavailable: {e}" if status["status"] != "ok": return JSONResponse(status, status_code=503) return JSONResponse(status, status_code=200)这个改动让我想起一个比喻:健康检查就像给员工测体温,体温正常不代表他没有内伤。所以设计健康检查时,一定要把 Agent 的关键依赖项纳进去。依赖项不用全查,只查最关键的三四个即可,检查太重反而会把健康检查接口本身拖垮。
4.4 实测对比:Agent-Reach 带来的量化收益
光说设计层面没意思,我直接贴一份我们内部压测和灰度期间记录的对比数据。这个数据来自一个真实业务模块:企业内部知识问答和工单分类系统,原来用固定的 if-else 分发,后来切到 Agent-Reach 路由。
| 指标 | if-else 硬编码 | Agent-Reach 语义路由 |
|---|---|---|
| 任务分发准确率 | 67% | 91% |
| 平均响应时间(含 Agent 推理) | 18.2s | 16.9s |
| 因 Agent 故障导致的任务失败率 | 6.1% | 1.7% |
| 新增 Agent 的平均接入时长 | 2 人天 | 0.5 人天 |
| 上线以来 Agent 异常自动摘除次数 | 0(全靠人工救火) | 23 次 |
最让我在意的是最后一行的"自动摘除次数"。之前用硬编码分发,某个 Agent 挂了,流量还是会照样往里打,直到运维发现才切流量;而现在 Agent-Reach 的健康检查模块会在 30 秒内自动摘掉故障节点,并把任务转发给 fallback Agent。这 23 次自动摘除背后,是 20 多次本来可能演变为线上事故的隐患被无声无息地消化掉了。
4.5 常见问题与排查速查表
我把这段时间遇到的典型问题整理成一张速查表,方便大家遇到类似情况时快速排查:
| 现象 | 可能原因 | 排查命令/思路 |
|---|---|---|
| 任务全部路由到兜底 Agent | 主 Agent 熔断未恢复 | 查看熔断器状态,检查 Agent 依赖项健康 |
| 路由结果飘忽不定,同一问题不同结果 | Embedding 模型为随机初始化 | 固定模型和向量化文本,开启缓存 |
| Agent 频繁被摘除但实际可用 | 心跳间隔太短 | 调长心跳间隔至任务均耗时 2 倍以上 |
| 相似度普遍偏低 | Agent Manifest 描述写得过于抽象 | 按"职责+明确排除项"重写 description |
| 新增 Agent 无流量 | Manifest 未通过 Schema 校验 | 查看注册中心日志,校验必填字段 |
| 网关超时,但 Agent 日志显示已返回 | 网关等待时间比 Agent timeout_ms 短 | 统一网关超时与 Manifest 中 timeout_ms 配置 |
表格里最后一行特别值得展开:网关超时和 Agent 的 timeout_ms 不一致,是我在联调时经常看到的问题。Agent 声明自己最慢需要 40 秒才能返回,但网关注入的 HTTP 客户端超时时间是 30 秒。于是 Agent 那边还在认真工作,网关这边已经等着急了,直接判超时重试,最终同一件事被两个 Agent 各做了一遍。我的建议是:执行网关注入的全局超时时间,必须以 Agent Manifest 里的timeout_ms为准,或者用一个更大的值包裹它。
5. 设计取舍手记与后续扩展蓝图
5.1 现在回看,哪些设计决策是最关键的
如果现在有人让我重新设计一遍 Agent-Reach,我会说几个最值得坚守的决策。第一,一定要把"能力描述"结构化,这是后面所有路由、鉴权、治理功能的地基。与其花时间调 embedding 模型,不如先把 Agent Manifest 的编写规范定清楚,我亲眼见过两个团队因为 Manifest 写得五花八门,导致路由引擎怎么调都白费力气。第二,瘦网关定位不能丢,Agent-Reach 不做工作流编排,一旦贪心地把任务拆分、结果合并这些功能加进来,这系统就会变得像一个没头没尾的怪物,谁也维护不动。第三,健康检查参数要留出调整余量,不要一上来就设一个看起来很"灵敏"的阈值,先观察几天真实流量,根据 Agent 任务耗时的 P95 值去设置心跳间隔和超时时间。
5.2 从 Agent-Reach 到 Agent 全生命周期治理
Agent-Reach 目前只覆盖了"注册、发现、路由、探活"四个环节,但它在跑起来之后,很快暴露出了上下游的刚性需求。比如 Agent 的灰度发布——我需要对新版本的 Agent 先导入小流量,验证稳定后再全量切流,而这件事依赖路由引擎的权重配置支持。再比如 Agent 的效能分析——每个 Agent 到底有多少次路由命中、成功率和 token 消耗如何,这些数据目前散落在网关日志里,没有一个统一视图。下一步我准备在 Agent-Reach 的外围加一个轻量可视化面板,把健康状态、路由命中率、平均耗时画成趋势图,再把 Agent 的版本信息纳入 Manifest,让每次升级都有据可查。
5.3 最后建议:从小场景起步,别一上来就追求完美
把 Agent-Reach 从一个想法变成线上基础设施,前后只花了两周,最关键的原因是我没有一上来就铺大摊子。建议想尝试同类方案的读者,先拿两三个 Agent、一个最简单的注册接口、一个凑合的本地 JSON 文件起步,把路由和健康检查的闭环跑通之后,再去考虑分布式存储、可观测性平台、消息通知这些锦上添花的东西。多 Agent 系统的工程化和单体应用很不一样——它的复杂性不在于代码,而在于运行时各组件之间此起彼伏的相互作用,这些只有真实流量能告诉你答案。这个项目让我最深的一个体会是:再多的设计文档都不如让一次真实的错误路由发生在你面前,那才是整改的最佳起点。