十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agent-Reach:智能体触达层架构设计与工程落地

Agent-Reach:智能体触达层架构设计与工程落地 Agent-Reach 这个词第一次出现在我视野里的时候我以为是某个爬虫库的分支点进去才发现方向完全不一样。它要解决的是智能体Agent世界里最容易被忽略、却最先卡住工程化落地的一段路你的团队里可能已经躺着七八个能跑通 demo 的 Agent它们分散在不同机器、不同语言栈、不同框架上有的用 Python 写的有的被塞在别人服务的某个角落还有的只是一个带 Flask 壳的脚本。这时候调用方想知道谁能处理这批发票识别任务得到的回答往往是去群里问一下。Agent-Reach 就是把这段从能跑到能被找到、被调用、被观测的空白补上的一层触达框架。这篇文章我打算按我自己实际落地这套东西的顺序来讲先拆清楚它到底解决哪几类具体断层再讲架构上为什么要把触达层做薄而不是做厚然后深入到能力描述、健康租约、路由匹配、流式返回、幂等去重这些真正吃时间的细节最后给一份可以照着跑的最小实现和一份踩坑速查表。不管你是刚开始接触智能体工程还是已经在维护一堆 Agent 服务的后端同学应该都能从里面挑到能直接用的东西。1. Agent-Reach 到底解决什么问题从能跑到能被找到1.1 三个真实的触达断层第一个断层是寻址断层。当你只有一个 Agent 的时候它的地址就是一行写死的 URL没什么好说的。但当 Agent 数量超过五个问题立刻变了调用方不知道该请求谁。有人用自然语言描述任务帮我审一下这份合同的付款条款有人用结构化参数描述task_typecontract_review, domainpayment。如果没有任何中间层做语义和能力标签的映射那就只能靠人肉路由写死在代码里改一次要发一次版。第二个断层是生命周期断层。Agent 进程是会挂的GPU 显存是会爆的模型服务是会限流的。如果没有一个统一的健康检查机制调用方只能靠超时报错来感知下游已经死了而这个感知延迟可能是几十秒甚至几分钟。更麻烦的是灰度场景新版本 Agent 上线只承接 5% 流量这种能力如果没有触达层统一承载每个业务线都要自己写一套权重逻辑。第三个断层是观测断层。这是最容易被低估的。一次触达调用跨了三四个服务其中两个是流式返回的最后统计出来的 P99 延迟到底是哪一段拖的调用方看到的是一次失败而下层日志里可能既有超时也有重试成功的记录两边对不上账。没有统一的 trace 和指标埋点排查一次线上问题要靠猜。Agent-Reach 的设计目标说白了就是把这三件事收到一个薄层里统一处理注册与发现解决寻址健康租约与权重路由解决生命周期统一埋点解决观测。1.2 为什么不做成又一个 Agent 编排框架这是我被问得最多的问题。市面上编排框架已经不少了为什么还要单独搞一层触达因为编排和触达是两个正交的关注点。编排关心的是任务怎么拆、步骤怎么串、记忆怎么存、失败怎么回滚触达关心的是这一步交给谁、它现在活着吗、超时怎么算、失败了要不要重试。前者是业务逻辑后者是基础设施。把它们揉在一个框架里最直接的后果就是业务团队想换编排方案的时候发现触达逻辑也在里面换不动反过来基础设施团队想升级路由策略又怕动到业务代码。我在实际项目里吃过这个亏。最早我们把路由逻辑写在一个自研编排器里后来发现有三分之一的 Agent 根本不在这个编排器管辖范围内它们是被别的系统直接调用的。这时候你只有两条路要么让编排器变成所有 Agent 的强制入口组织上很难推动要么把这部分逻辑抽出来做成独立的一层。Agent-Reach 走的是第二条路。具体到设计原则我总结成三条后面所有细节都是围绕这三条展开的薄协议优先能用 HTTP 讲清楚的事情不要引入新的协议栈。引入一个新协议意味着调用方要装新的 SDK、CI 里要多配一个依赖、新人要重新学一遍。描述与实现分离Agent 的能力描述是一份独立的数据不是藏在代码里的 if-else。描述可以在注册中心被检索、被过滤、被版本化。失败是一等公民超时、熔断、降级、重试这些不是异常处理是主流程的一部分。设计之初就要给出每种失败的默认行为。注意如果你的 Agent 总数长期稳定在三个以内、且调用方只有一个那真的不需要引入触达层。过度设计带来的复杂度会远超它省下的那点胶水代码。2. 架构设计与关键选型把触达层做薄2.1 四层拆解注册中心、网关、适配器、会话状态Agent-Reach 的骨架我拆成四块每块的职责边界尽量不重叠。注册中心Registry负责存 Agent 的能力描述和健康状态。它本质上是一个带语义检索能力的小型键值库写入方是 Agent 自己或它的 sidecar读取方是网关。这里有个关键决策注册中心不存业务数据只存元数据。我见过有人把 Agent 的输入输出样例也塞进去结果元数据表涨到几百万行检索性能直接崩掉。**网关Gateway**是所有触达请求的入口它做三件事鉴权、路由匹配、把请求转发给选中的 Agent。注意它不做业务逻辑也不做参数校验校验交给 Agent 自己因为只有 Agent 才知道自己的参数约束。**适配器Adapter**是协议转换层。有的 Agent 用 HTTP 暴露有的只支持 gRPC有的干脆是个消息队列消费者。适配器把这些差异吃掉对上统一暴露一种接口。适配器是分层里最脏的一层也是最该被隔离的一层——因为它变动的频率最高。**会话状态Session Store**单独拎出来讲是因为流式场景绕不开它。当一个 Agent 返回的是 SSE 流网关需要维护哪个客户端对应哪条下游流的映射以及断线重连后的续传位置。这块用 Redis 就够了键设计成session:{trace_id}TTL 设成比最长任务时间略长即可。2.2 协议选型HTTP SSE 打底WebSocket 只留给双向流选型这块我踩过两次坑说下结论和理由。对外接口用纯 HTTP SSEServer-Sent Events。理由是SSE 本质上是长连接的 HTTP 响应所有主流语言的标准库都能处理不需要引入额外的协议库它对中间代理友好重连语义由浏览器或客户端库自动处理单向推送的场景Agent 输出 token、进度、日志它完全够用。实测下来SSE 在跨机房调用时的心跳维持成本比 WebSocket 低不少。WebSocket 我只在一种情况下用Agent 需要中途接收客户端的补充输入。比如一个需要人工确认的审批流Agent 跑到一半要问这单要不要放行这时候单向推送就不够了。但这种场景在实际业务里占比很低为了它把所有链路换成 WebSocket 不划算我的做法是在适配器层单独开一条通道走独立端口和主链路隔离。gRPC 在内部 Agent 之间通信时是可以用的延迟和序列化效率确实更好但不要把它暴露给外部调用方。原因是生态割裂业务方可能用 Java、Go、Node 各种语言让他们都去生成 stub、管理 proto 版本协调成本远大于收益。协议适用位置优势主要代价HTTP JSON对外统一入口生态最广调试方便流式支持要额外约定HTTP SSE流式输出场景标准库支持好兼容代理单向需要心跳保活WebSocket双向交互场景真正的全双工连接管理复杂网关成本高gRPC内部 Agent 间调用序列化高效强类型对外生态割裂2.3 三个关键取舍为什么自研网关而不是套现成组件第一个取舍用现成的 API 网关还是自己写路由层。我一开始想直接套 Nginx 或 Kong后来放弃了。原因是 Agent 的路由规则不是传统的路径匹配它需要按能力标签做加权选择还要读注册中心的实时健康状态。现成网关的插件机制能做但每次规则变化都要 reload而 Agent 上下线的频率可能就是分钟级的。结论是用现成网关做接入和 TLS 终止路由决策自己写在一个轻量服务里。这样各司其职Nginx 那层几乎不用动。第二个取舍用消息队列还是同步调用。长任务超过 30 秒我建议走异步网关返回一个task_id结果通过回调或轮询获取。短任务走同步。这里的边界值不要拍脑袋定我一般按 P95 延迟的 3 倍来划如果 P95 是 8 秒那 24 秒以上的一律异步化。第三个取舍注册中心用 etcd/Consul 还是自己写。我的建议是别自己写用现成的键值存储加 TTL 能力即可。但要注意一点这些组件的健康检查是基于连接活跃度的而 Agent 的健康往往是基于业务状态的比如模型服务限流了进程还活着但不能接活。所以要做两层健康进程级用组件自带的租约业务级由 Agent 主动上报一个ready字段。3. 核心实现从注册到一次完整调用3.1 能力描述文件字段怎么定才不返工能力描述我习惯叫 Agent Card是整套系统的地基字段一旦上线就很难改所以值得多花半天时间想清楚。下面是我最终定下来的字段集合以及每个字段为什么这么定。{ agent_id: invoice-extractor-v2, display_name: 发票信息提取, version: 2.3.1, owner: finance-platform, endpoint: http://10.20.3.11:8080/invoke, protocol: http-sse, capabilities: [ {tag: invoice.extract, weight: 1.0}, {tag: ocr.table, weight: 0.6} ], input_schema_ref: schema://invoice-extract/input2, constraints: { max_payload_bytes: 5242880, supported_langs: [zh, en], timeout_hint_ms: 45000 }, runtime: { max_concurrency: 16, avg_latency_ms: 3200, cost_per_call: 0.012 }, lease_ttl_s: 30, ready: true }几个字段的取舍值得单独说。capabilities用标签加权重而不是自由文本。原因很实际自由文本检索在几十个 Agent 的时候靠关键词还能用上百个之后误召回率会飙升。标签是有层级的invoice.extract检索时可以前缀匹配也能做精确路由。权重用来表达这个 Agent 的主业是发票提取OCR 表格只是顺带能接。input_schema_ref用引用而不是内联。schema 可能有几百行内联到 Card 里会让注册中心的数据量膨胀十几倍而且 schema 更新频率比 Card 高得多。用 URI 引用的方式schema 单独存一份带版本号两边可以独立演进。runtime里的avg_latency_ms和cost_per_call不由 Agent 自己填而是由网关定期回写。自己填的数据基本不可信人都有报喜不报忧的倾向。网关按最近 5 分钟的滑动窗口计算写到 Card 的另一个分区里和 Agent 自报的字段物理隔离。lease_ttl_s定成 30 秒是有讲究的。太短会导致网络抖动时频繁摘除健康节点太长会让故障节点存活过久。30 秒是我在三次故障演练里试出来的平衡点Agent 崩溃后最长 30 秒被摘除而正常网络抖动通常 3 秒内恢复不会触发摘除。3.2 心跳与租约TTL 该设多久怎么续心跳机制看起来简单但细节全是坑。核心问题有三个用什么频率续约、续约失败几次算故障、恢复后怎么重新入池。频率上我选TTL 的三分之一即 10 秒续一次。这样即使连续两次续约失败20 秒租约还没到期还有一次补救机会。如果设成 TTL 的二分之一容错空间就只有一次实际跑下来误摘率明显偏高。故障判定上我用了连续失败计数 抖动退避的组合。第一次续约失败不标记异常只记录第二次失败标记为suspect从路由池里降权但仍然可被选中权重打三折第三次失败才真正摘除。为什么要留一个suspect中间态因为真实世界里大量故障是瞬时的比如一次 GC 停顿、一次 DNS 解析超时。直接摘除会让下游流量瞬间打到剩余节点上引发雪崩。恢复入池更要小心。Agent 重启后如果立刻以全权重接流量很容易出现刚起来就被打满然后再次挂掉的循环。我的做法是渐进式放量恢复后的前 60 秒权重固定为 0.260 到 120 秒升到 0.5之后才回到正常权重。这个策略在同一周内把一次灰度事故的影响面压到了原来的十分之一。# 心跳续约的核心逻辑伪代码去掉业务细节 class LeaseManager: def __init__(self, ttl_seconds30): self.ttl ttl_seconds self.interval ttl_seconds // 3 self.fail_count 0 self.state healthy # healthy / suspect / removed def renew_loop(self): while True: ok self._try_renew() if ok: if self.fail_count 2: self._enter_warmup() # 渐进放量 self.fail_count 0 self.state healthy else: self.fail_count 1 if self.fail_count 2: self.state suspect elif self.fail_count 3: self.state removed # 退避失败越多下次续约稍晚一点避免打爆注册中心 time.sleep(self.interval * (1 0.1 * min(self.fail_count, 5)))提示心跳续约一定要用独立的线程或协程不要挂在请求处理路径上。我见过把续约写在每次业务请求里的实现结果空闲的 Agent 因为长时间没请求租约过期被摘除了非常典型。3.3 路由匹配能力标签怎么算分路由是触达层的核心算法。输入是一个调用请求带能力需求、优先级、可能的偏好约束输出是选中的 Agent 实例。我用的策略是三因子加权打分不是简单的随机或轮询。三个因子分别是能力匹配度、实时健康分、成本因子。能力匹配度来自标签的精确匹配1.0和前缀匹配0.6健康分来自最近 5 分钟的成功率和延迟分位数成功率 99% 以上给 1.095% 到 99% 给 0.7低于 95% 给 0.3成本因子是1 / (1 cost_per_call * 100)让便宜且效果相当的实例有微弱优势。最终得分是三者相乘再乘以实例权重灰度权重然后做带权随机而不是取最高分。为什么不用取最高分因为那样会导致所有流量都打到同一个实例上其他实例永远拿不到流量也就永远得不到健康数据形成正反馈死循环。带权随机的做法是把得分归一化后当作概率分布来抽样。这样高分配置拿到的流量多但低分的也有机会被选中保持数据新鲜度。import random def pick_agent(candidates, required_tag): scored [] for c in candidates: if not c[ready]: continue cap max((m[weight] for m in c[capabilities] if m[tag] required_tag or required_tag.startswith(m[tag])), default0) if cap 0: continue health c[health_score] # 0.3 / 0.7 / 1.0 cost 1 / (1 c[runtime][cost_per_call] * 100) score cap * health * cost * c[traffic_weight] scored.append((c, score)) if not scored: raise NoAvailableAgent(required_tag) total sum(s for _, s in scored) r random.random() * total acc 0 for c, s in scored: acc s if r acc: return c return scored[-1][0]这里有个容易忽略的细节标签匹配要支持传递性。比如请求要invoice.extract而某个 Agent 只声明了invoice和document.ocr它其实是有能力接的。硬匹配会漏掉它。我的做法是在注册时维护一张标签继承表invoice.extract继承自invoice继承自document匹配时沿着继承链往上看。这张表不用很大手工维护几十条就够了比搞一套自动语义推断靠谱得多。3.4 流式返回与超时控制SSE 的三个实现要点流式这块看起来简单写起来翻车率很高。我把关键点列一下。第一首字节超时和整体超时要分开设。一个 Agent 可能在 200 毫秒内就返回了第一段内容然后推理跑了 40 秒。如果你只设一个整体超时短任务和长任务就没办法用同一套配置。我的配置是首字节超时 3 秒超过就认为下游有问题可以换实例重试整体超时按任务的timeout_hint_ms来但要加一个 20% 的缓冲。第二每条 SSE 消息都要带id字段。客户端断线重连时会带上Last-Event-ID请求头网关据此从会话状态里找到续传位置继续往下推。没有id的话重连就只能从头开始用户会看到内容重复出现体验很糟。第三心跳注释帧必须加。长时间没有数据输出的流比如 Agent 在做长推理中间的网络设备可能会主动断开空闲连接。每 15 秒推一条:ping注释帧成本几乎为零但能避免大量莫名其妙的断流。这个坑我在跨区域调用时踩过一次排查了两天才定位到。def stream_proxy(upstream_resp, session_id): seq 0 last_beat time.time() for chunk in upstream_resp.iter_content(chunk_sizeNone): seq 1 store.save(session_id, seq, chunk) # 存一份供断线续传 yield fid: {seq}\ndata: {chunk}\n\n last_beat time.time() # 没有数据时也要发心跳见下方 while 逻辑 yield data: [DONE]\n\n3.5 幂等与重试request_id 怎么设计才有效重试是必须的但重试带来的重复执行是灾难——尤其是 Agent 会调用外部工具、产生实际副作用的时候。解决办法是幂等键 去重表但幂等键的设计有几个细节。键不能只用request_id因为同一个request_id在不同租户下应该被视作不同请求。我的键是hash(tenant_id request_id capability_tag)。加上能力标签是因为同一个请求 ID 可能被复用到不同的能力上客户端复用了 ID 生成器的场景。去重表存三样东西键、状态进行中/已完成、结果。「进行中」这个状态必须显式存否则并发重试的时候两个请求会同时执行。做法是用 Redis 的SET NX抢锁抢到的执行没抢到的等待结果。等待要有超时超时后返回处理中请稍后查询。重试策略上我只对幂等且可安全重放的失败重试具体分三类连接失败、首字节超时这两类可以安全重试业务返回 5xx 且带retryable: true标记的可以重试已经产生部分流式输出的绝对不重试除非目标是另一个实例且明确支持续传。这个规则一定要写进网关代码不能靠调用方自觉。重试次数默认两次退避用指数加抖动基数是 200 毫秒第一次 200±50ms第二次 400±100ms。抖动很重要没有抖动的话多个并发请求会在同一时刻齐刷刷重试把下游再来一次打爆。4. 落地实操本地起一个最小可用版本4.1 环境准备与依赖说明最小版本我建议用这些组件都是成熟稳定的Redis 用来存注册信息和去重表FastAPI 用来写网关异步支持好SSE 支持直接uvicorn 作为服务容器。Python 版本建议 3.11 以上主要是为了asyncio.TaskGroup这个语法糖写并发清理逻辑会干净很多。pip install fastapi0.110.* uvicorn[standard] redis5.* httpx0.27.* pydantic2.* docker run -d --name agent-reach-redis -p 6379:6379 redis:7-alpine选 Redis 而不是自己写内存字典是因为注册信息必须能跨进程共享。单机内存存的话网关一开多副本就全乱了而且进程重启后所有 Agent 的注册信息全部丢失要等一个心跳周期才能恢复。这个代价在灰度发布时特别明显。4.2 注册一个 Agent完整可运行代码下面这段是我实际用的注册客户端去掉了一些监控埋点核心逻辑都在。import asyncio, json, time, httpx, redis.asyncio as aioredis REGISTRY_KEY areach:registry class AgentRegistrar: def __init__(self, card: dict, redis_urlredis://localhost:6379/0): self.card card self.r aioredis.from_url(redis_url) self.ttl card.get(lease_ttl_s, 30) self.fail 0 async def register(self): await self._write() asyncio.create_task(self._renew_loop()) async def _write(self): payload json.dumps(self.card, ensure_asciiFalse) # 用 hash 结构存方便后续单独更新 runtime 字段 await self.r.hset(REGISTRY_KEY, self.card[agent_id], payload) await self.r.expire(REGISTRY_KEY, 0) # 不设整体过期 async def _renew_loop(self): while True: await asyncio.sleep(self.ttl // 3) try: # 单独维护一个租约键TTL 就是存活证明 await self.r.setex( fareach:lease:{self.card[agent_id]}, self.ttl, str(time.time()), ) self.fail 0 except Exception: self.fail 1 # 连续失败只记录进程级存活由注册中心的租约键判断这里有个设计选择值得说存活状态和卡片信息分开存。areach:registry存卡片长期有效areach:lease:{agent_id}存租约TTL 30 秒。这样做的好处是Agent 进程崩溃后卡片信息还在运维能查到这个 Agent 是什么时候消失的、它的配置是什么便于事后排查。如果揉在一个键里进程一挂信息全没了事后什么都查不到。4.3 发起一次触达调用网关侧的调用接口我设计成两个同步的/invoke和流式的/stream。下面给出流式版本的简化实现。from fastapi import FastAPI, Request from fastapi.responses import StreamingResponse import httpx, json app FastAPI() app.post(/stream) async def stream(req: Request, body: dict): tag body[capability] agent await pick_agent_from_registry(tag) async def gen(): async with httpx.AsyncClient(timeouthttpx.Timeout(connect3.0, read60.0)) as c: async with c.stream(POST, agent[endpoint], jsonbody[payload]) as resp: seq 0 async for line in resp.aiter_lines(): if not line: continue seq 1 yield fid: {seq}\ndata: {line}\n\n yield data: [DONE]\n\n return StreamingResponse(gen(), media_typetext/event-stream, headers{Cache-Control: no-cache, X-Accel-Buffering: no})调用侧用 curl 验证最直接注意-N参数关掉缓冲否则你看不到流式效果会误以为下游没返回。curl -N -X POST http://localhost:8000/stream \ -H Content-Type: application/json \ -d {capability:invoice.extract,payload:{file_url:s3://demo/1.pdf}}X-Accel-Buffering: no这个响应头必须加。它告诉前置的 Nginx 不要缓冲响应否则 Nginx 会把整个流攒完再一次性吐给客户端流式就变成了等 40 秒然后瞬间出结果完全失去意义。4.4 压测与三个必须盯的指标压测不要一上来就堆并发先做阶梯加压从 10 并发开始每 30 秒加 10直到错误率超过 1%。这样才能找到真实的拐点而不是只有一个扛不住的结论。要盯的指标有三个缺一不可指标含义健康阈值参考首字节延迟 P95从请求发出到收到第一段内容小于 3 秒完整流时长 P99整个流式响应的耗时不超过 timeout_hint 的 1.2 倍路由失败率找不到可用 Agent 的比例小于 0.1%第一个指标反映下游健康度第二个反映任务复杂度第三个反映注册中心的一致性。我最开始只盯了第二个结果有一次注册中心的 Redis 主从切换大量请求路由失败但因为失败很快返回完整流时长的指标反而变好看了完全没预警。这个教训很值钱。注意压测时一定要用真实长度的流式响应。用假数据返回一个很短的流会把所有缓冲相关的瓶颈都掩盖掉测出来的数字没有任何参考价值。5. 踩坑记录与排查速查表5.1 常见问题速查下面这张表是我在三个项目里积累下来的基本都是文档里不会写、但现场一定会遇到的。现象大概率原因处理方式流式调用变成一次性返回中间层缓冲缺少X-Accel-Buffering: no检查所有代理层的缓冲配置Agent 频繁被摘除又恢复心跳续约线程被业务阻塞续约放独立线程检查 GIL 争用同一请求被执行两次幂等键设计不当或去重表未加锁用SET NX抢锁键加租户前缀新实例上线后立刻过载恢复后直接给全权重加渐进放量逻辑整体超时正常但偶发大批失败重试无抖动形成同步洪峰加重试抖动错开时间窗口标签匹配漏召缺少标签继承关系表维护继承表匹配时沿链上溯5.2 三个反直觉的经验第一注册中心的数据量比你想象的重要。我一开始觉得几十个 Agent 的元数据能有多大随便存。后来发现网关每次路由都要全量拉取一遍做打分QPS 上去之后光是反序列化就吃掉不少 CPU。解决方式是网关本地做一层带版本号的缓存注册中心的键加一个revision计数器网关只对比版本号变了才重新拉。这个优化把路由环节的 CPU 占用降了大概四成。第二健康的定义要按业务来不是按进程。进程活着但模型服务限流了这种情况应该算是部分健康。我后来加了一个degraded状态处于这个状态的实例仍然可以被选中但权重降到 0.1只承接极少量流量用于探活。这样既不影响整体成功率又能在它恢复的第一时间感知到。第三日志里一定要带 trace_id而且要贯穿到 Agent 内部。这件事听起来是常识但实践中最容易断在适配器层。我的做法是在网关生成 trace_id 后通过 HTTP 头X-Trace-Id透传并在适配器里强制要求下游把这个头再透传一层。少透一层排查时就要在两个系统之间手工对账。5.3 安全边界鉴权、限流、审计各管什么这三件事经常被混在一起做但职责其实很清楚。鉴权解决你是谁、你能调什么。我用的方案是租户级 API Key 加能力白名单。Key 存在网关侧每个 Key 绑定一组允许调用的能力标签。这里要注意白名单要支持通配比如invoice.*允许调用所有发票相关能力否则每次新增能力都要改配置。限流解决你能调多快。分两级租户级总量限流防止单个租户打爆整体Agent 级并发限流防止下游被压垮。算法上我用了令牌桶加并发计数的组合令牌桶控速率并发计数控同时占用。只做令牌桶在流式场景下会失效因为一次流式请求占用连接的时间可能长达几十秒速率限制完全挡不住。审计解决你调了什么。每条触达记录要落库至少包含 trace_id、租户、能力标签、目标 Agent、开始结束时间、状态。存储上建议做冷热分离最近 7 天放 Redis 或热库用于实时查询更早的落对象存储。审计日志的写入必须异步绝对不能同步写在请求路径上否则一旦存储抖动整个调用链路都会被阻塞。提示审计日志里的 payload 建议只记录摘要和哈希值不要存全量原文。一是存储成本二是好多业务数据本身就不适合落盘。这套东西我从最早的两三个 Agent 一路用到几十个中间重构过两次路由逻辑改过三次心跳参数。最大的体会是触达层的价值不在于功能多而在于它的行为可预测。调用方知道超时会怎样、重试会怎样、失败会怎样才敢放心地把核心业务链路挂上来。至于后续还能往哪扩我目前在看的方向是给路由加一层基于历史调用质量的反馈调节让那些长期稳定的实例慢慢拿到更多流量不过这属于锦上添花现有的三因子打分已经够用了。
返回列表