大概半年前,我被一个看似普通的需求折腾得够呛:公司内部要做一个"能查订单、能发通知、能跟别的系统对话"的助手。第一版我的思路很直接,把大模型塞进去就完事。模型本身表现相当不错,生成的话术一套一套的,挑不出毛病。结果一到真实环境就露馅了——它在我面前信誓旦旦地说"您的订单已发货,预计明天送达",实际上底层接口根本没调通,数据是从一个已经废弃的服务里拿到的。
那次事故让我彻底想明白一件事:做Agent的时候,生成能力只是最外面那层皮,真正决定Agent能不能用的,是它能不能"够得着"外部世界——工具接口、业务数据、其他Agent。也是从那时候起,我开始动手做Agent-Reach。Reach就是触达,这个项目解决的是Agent最容易被低估、但翻车率最高的那段路:从模型的意图到外部世界的确定性动作,到底由谁来保障。
这篇文章是我在Agent-Reach上的完整总结,包括这个项目为什么会出现、核心设计怎么拆、一套可以直接抄走的接入示例,以及我在真实环境里踩过的几个大坑。适合正在做Agent应用、或者打算把Agent接进内部系统的朋友参考——尤其是那些已经发现"模型很聪明但系统总是莫名其妙出错"的人。
1. Agent-Reach的诞生:先说清楚Agent最大的痛点在哪里
1.1 从一次"模型什么都会、落地全翻车"的现场说起
先还原一下那次事故的细节。我们的场景很简单:用户问"帮我查一下订单OW-20240618的状态",Agent按计划应该调用一个订单查询接口,拿到数据后格式化回复。
表面上链路是通的:Agent正确识别了意图,选对了工具,发出了HTTP请求。问题出在请求的目标上。当时那个订单服务经历过一次迁移,旧域名虽然还能解析,但返回的已经是另一个项目的占位数据——状态永远是"已发货",物流单号永远是一个假号码。Agent并不知道自己拿到的是垃圾数据,它只觉得自己完成了任务,于是很有底气地编了一段"合情合理"的回复。
最可怕的是,整个过程中没有任何一个环节报警。大模型不会因为"数据可疑"而拒绝回答,工具调用层只关心HTTP状态码是不是200,业务方看结果的时候又只盯着话术是否通顺。等我们发现真实数据早就对不上了,已经过去了两周多。
这次踩坑让我意识到,Agent系统里有一个长期被当成"理所当然"的部分,就是工具调用和外部系统触达。大家默认模型会选对工具、默认接口会正常返回、默认拿到数据就能直接用,但这些假设在真实环境里几乎全部会失效。Agent-Reach这个项目,就是冲着这些"默认会失效"的假设去的。
1.2 "触达"和"生成"是两码事:为什么Reach会单独成为一个问题
大模型的本质是一个生成器,它根据上下文预测下一段最合理的文字。生成这个动作本身是概率性的,同一个问题换一种问法、换一次采样,回答可能就变了。但"触达"完全不是概率性的——调一个下单接口,发出消息,更新一条数据库记录,这些动作的结果必须是确定性的、可验证的。
生成要的是"像",触达要的是"对"。
我见过太多团队把这两件事混在一起处理:Agent的Prompt里写了一大堆"如果调用失败请重试""注意接口返回格式",然后把工具调用的重试逻辑、超时控制、数据校验全部压给模型去"临场发挥"。这就像让一个文科生去写底层通信协议——不是说一定不行,是这种方式完全没法保证质量和可维护性。
触达涉及的是网络、鉴权、数据格式、一致性、多服务协作,这些全是传统工程问题。模型可以在触达失败后生成一段"抱歉"的话,但它不能让请求变成功。所以在Agent-Reach里,我把触达单独抽象出来,作为一层基础设施来做:模型只负责表达意图,触达层的连接器、路由、重试、熔断、观测,全部由工程手段保障。
1.3 我决定做的不是框架,而是一层连接基础设施
市面上其实不缺Agent编排框架,有轻量级的、有全家桶的,我也用过好几个。我的感受是:编排框架解决的是"让模型跑流程、调工具"的问题,但当工具多了、系统复杂了以后,真正缺的不是"怎么让模型调工具",而是"调工具这段路怎么才能不出事"。
所以Agent-Reach从第一天起就不是一个Agent框架,它是一层连接基础设施。核心只做四件事:
- 连接器管理:统一封装外部API、数据库、其他Agent的调用,屏蔽协议和鉴权差异。
- 路由决策:根据意图、标签、权限选择合适的连接器,避免模型在几十个工具里乱选。
- 执行保障:超时控制、重试、幂等、熔断、降级,让每一次调用都有据可依。
- 观测记录:每一次触达都留下结构化日志,出问题能定位,做报表有数据。
它不被任何特定编排框架绑架,既可以被LangChain这类流程调用,也可以被我自己的一个简单循环调用,甚至可以两个Agent之间直接互调。边界划得很清楚:Agent负责想和说,Agent-Reach负责够得着和靠得住。
2. Agent-Reach的核心设计:把触达拆成四个可度量、可运维的环节
2.1 连接器层:先解决"够得着"的问题
Agent要触达的每个外部能力,在Agent-Reach里都被封装成一个连接器。连接器是一个很薄的适配层,它要做的事情是把外部世界的复杂性隔离在统一接口后面。
我看过很多人写Agent工具函数,上来就直接在函数里写HTTP请求,然后把requests库的Response对象原样返回给模型。你猜模型拿到的是什么?一个status_code、一个可能被截断的响应体、一堆头信息。模型得自己猜"这个返回到底代表成功还是失败",猜错了就翻车。
Agent-Reach的连接器不一样,它强制要求返回值是一个结构化的ActionResult:
{ "status": "ok", # ok / failed "biz_code": 0, # 0表示业务成功,非0表示业务层失败 "data": {...}, # 经过清洗后的业务数据 "error": None, # 失败时的具体错误信息 "meta": { "source_ts": "2025-06-18T15:30:00Z", # 数据产生时间,防止模型拿旧数据当新数据 "latency_ms": 87, # 本次触达耗时 "idempotency_key": "a92f03..." # 幂等键 } }我把这个结构称为"触达协议"。它的核心思想是:连接器不仅要负责把请求发出去,还要负责判断这次触达到底算不算成功。HTTP 200只代表网络层通了,业务层成没成是另一回事。这事绝对不能交给模型去判断。
下面是我常用的一个连接器骨架:
@agent_reach.register("order.query") class OrderQueryConnector: def __init__(self, endpoint: str, timeout_ms: int = 3000): self.endpoint = endpoint self.timeout = timeout_ms async def execute(self, request: ActionRequest) -> ActionResult: # request里带有action_id、params、trace_id、agent_id async with aiohttp.ClientSession() as session: try: async with session.post( self.endpoint, json={"order_id": request.params["order_id"]}, timeout=aiohttp.ClientTimeout(total=self.timeout), ) as resp: raw = await resp.json() except asyncio.TimeoutError: return ActionResult.failed("timeout", "order.query timeout") # 业务层校验:很多接口哪怕失败也会返回200 if raw.get("code") != 0: return ActionResult.failed(f"biz_error_{raw.get('code')}", raw.get("message")) # 数据清洗:只把模型真正需要的字段返回出去 data = { "order_id": raw["data"]["order_id"], "status": raw["data"]["status"], "tracking_no": raw["data"].get("tracking_no", ""), } return ActionResult.ok(data)注意代码里的几个关键点:超时设置是针对触达的第一个保障;biz_code的校验是把"假成功"掐死在连接器这一层;data字段只保留必要字段,防止上下文被无关数据撑爆。这些细节单拎出来都不难,但组合在一起,就是"触达"和"裸调HTTP"的区别。
2.2 路由层:让正确的工具被正确的Agent用上
工具少的时候,把全部工具函数塞给模型随便选就行。可一旦工具数量超过十个、几十个,模型选错工具的概率会直线上升——不是它变笨了,而是描述相似的工具太多了。
Agent-Reach的路由层解决的就是这个问题。它把"模型选工具"这件事从自由发挥变成半自动决策。常用的路由策略有三类:
| 路由策略 | 适用场景 | 说明 |
|---|---|---|
| 标签路由 | 工具数量中等、分类明确 | 按order/read、order/write这类标签圈定工具子集 |
| 语义路由 | 工具数量大、命名混乱 | 用embedding计算意图与工具描述的相似度,取TopK |
| 权限路由 | 多租户、多Agent | 先按Agent的权限范围过滤可用工具集 |
我的经验是:标签路由是主力,语义路由做兜底,权限路由在任何场景下都不能省。配置上大概是这个感觉:
routes: - name: query_order match: labels: [order, read] semantic: "查订单、订单状态、物流信息、签收时间" connector: order.query timeout_ms: 3000 retry: max_attempts: 3 backoff: [200, 600, 1800] circuit_breaker: error_threshold: 0.4 cooldown_ms: 30000路由层在真正调用连接器之前,先把候选集合缩小。比如用户问"订单状态",模型只需要在order+read这两个标签下的三个工具里做选择,而不是在包含库存、售后、支付、物流HR等在内的三十个工具里大海捞针。这一步做完,模型选错工具的概率会下降一个量级。
还有一个很多人忽略的细节:路由结果本身也要记录。我在观测数据里专门加了一个routed_to字段,用来还原"当时这套配置把这个问题路由到了哪里"。排查"Agent为什么调用了一个奇怪的接口"之类的问题时,这个字段能帮你快速界定是路由配错了,还是模型在路由层之外乱调了。
2.3 执行与重试:把"一次调用"变成"一次有保障的触达"
连接器把外部系统包了起来,路由把工具选择范围缩到最小,接下来真正动手调用的时候,还有一个更麻烦的问题:哪怕是同一个接口,我上次调用成功了,这次调用可能就超时了。触达不能靠运气,必须有执行保障机制。
Agent-Reach的执行保障包含四个层次,我按优先级排一下:
超时控制。每个连接器都有独立的超时时间,初始值建议3000ms,写操作可以放宽到5000ms。没有超时的触达是事故温床,一个慢接口能把整个Agent卡到天荒地老。
重试。超时类错误、网络抖动类错误才值得重试;业务失败(比如余额不足)绝对不能盲目重试。重试策略我默认用指数退避,按[200, 600, 1800]毫秒的间隔试探三次,避免流量瞬间打爆下游。
幂等。重试必须配合幂等,不然重试就是制造重复订单的工具。我给每次触达生成一个idempotency_key,由trace_id、action_id和重试序号共同计算。下游支持幂等键时直接传给它,不支持的话就在连接器内部做去重记录。
熔断与降级。这层是给"下游已经半死不活"的场景准备的。某个连接器在30秒内错误率超过40%,开关就打开,后续触达直接走降级路径(比如返回缓存数据、返回明确的故障文案),不再去蹂躏下游。
这套机制跑起来之后,我再也没有遇到过"Agent因为一个超时问题反复重试同一个接口,把下游拖垮"的惨剧。模型也不需要靠Prompt去"理解"怎么重试了,它在触达层看到的结果永远只有两个形态:成功的结构化数据,或者失败的结构化错误。至于怎么重试成功的,那是Agent-Reach的事,不是模型该操心的事。
2.4 观测层:不盯着链路,就别谈可靠性
我见过太多的Agent项目,上线之后只有一个"最终回复是否成功"的指标,中间发生了什么完全是一团黑盒。模型为什么选了这个工具?那次调用到底超时了没有?重试了几次?拿到的数据是哪个时间点的?这些问题全都没有答案。
Agent-Reach的观测层把每一次触达都记录成一条结构化的trace,核心字段包括:
| 字段 | 含义 | 排查价值 |
|---|---|---|
| trace_id | 一次触达的唯一标识 | 把Agent思考过程、路由结果、连接器执行串起来 |
| agent_id | 发起触达的Agent | 多Agent场景下定位是哪个角色惹的祸 |
| intent | 模型识别出的意图 | 判断意图识别阶段是否就偏了 |
| routed_to | 路由到的连接器名称 | 判断是路由配错还是模型乱选 |
| status | ok/failed | 快速汇总成功率 |
| error_type | timeout/biz_error/network/empty | 嫌status不够细就靠它分类 |
| latency_ms | 连接器实际耗时 | 延迟优化最直接的依据 |
我自己的使用习惯是每次迭代都导一份这样的数据,按error_type分组看看这周翻车翻在哪。有一周数据出来特别有意思:错误类型里占比最高的不是timeout也不是network,而是empty——接口返回成功,但是查不到数据,返回了一个空列表。这说明我们的数据覆盖出了问题,跟模型一点关系都没有。如果没有观测层,这种问题大概率会被当成"模型答错了"来处理,那就真的跑偏了。
观测数据还有一个高阶用法:喂回给路由层。如果大量"查订单"的请求都被路由到了物流查询,说明两个连接的语义描述太接近,路由配置需要调整。观测不只是为了事后复盘,它本身可以驱动配置迭代。
3. 动手实录:用Agent-Reach接一个"查订单→发通知"的真实场景
3.1 场景拆解与最小配置
理论讲再多,不落地全是空谈。下面我把自己实际跑通过的一个场景完整拆一遍,你们拿去可以直接参考。
场景是这样:用户问"帮我看看我的订单到哪了,顺便发个短信通知我"。这个需求拆开看包含两个动作:查订单状态,发通知。对应到Agent-Reach里就是两个连接器:order.query和notify.sms。
某一次实际处理过程会比较典型:Agent先确定意图是"查询订单且需要通知",然后通过路由选到order.query连接器,拿到订单状态后,再调用notify.sms发送短信。这中间涉及的问题包括参数从哪来、下游接口用什么鉴权、失败的时候怎么办。这些我都写进配置。
3.2 自定义连接器的接入流程
我假定你们公司已经有一个内部订单服务,它真正的业务逻辑是post一个order_id进去、返回订单状态。下面这个连接器就是Agent-Reach里最标准的一个形态:
@agent_reach.register("notify.sms") class SmsNotifyConnector: def __init__(self, endpoint: str, api_key: str): self.endpoint = endpoint self.api_key = api_key async def execute(self, request: ActionRequest) -> ActionResult: payload = { "phone": request.params["phone"], "content": request.params["content"], } headers = {"X-API-Key": self.api_key} try: async with aiohttp.ClientSession(headers=headers) as session: async with session.post( self.endpoint, json=payload, timeout=aiohttp.ClientTimeout(total=5000), ) as resp: result = await resp.json() # 关键:很多短信服务在欠费、模板不对时也会返回200 if result.get("code") != 0: return ActionResult.failed( f"sms_biz_{result.get('code')}", result.get("message", "unknown sms error") ) # 只返回模型需要的最小信息 return ActionResult.ok({"msg_id": result["data"]["msg_id"]}) except asyncio.TimeoutError: return ActionResult.failed("timeout", "sms provider timeout") except aiohttp.ClientError as exc: return ActionResult.failed("network", str(exc))这段代码能跑,但我更想让你们注意几个设计上的细节。第一,短信发送是一个不可逆的写操作,重试策略务必保守——如果短信服务方不支持幂等键,宁可不要自动重试。我在这个连接器上直接把max_attempts配成了1,反正没发出用户会再问一次,总比发三条一模一样的短信强。第二,返回给模型的内容只保留msg_id,不把整个短信服务商的响应体丢给模型,不然Agent会从里面读出一些奇怪的东西来。
3.3 路由、超时和熔断的具体参数
连接器写完后,接下来就是路由和执行策略的配置。注意,这一层才是Agent-Reach的核心价值所在,每次让连接器"裸奔"的时候都会出事。
# config.yaml connectors: order.query: endpoint: http://order-svc.internal/query timeout_ms: 3000 retry: max_attempts: 3 backoff: [200, 600, 1800] circuit_breaker: error_threshold: 0.4 cooldown_ms: 30000 notify.sms: endpoint: http://sms-provider.internal/send timeout_ms: 5000 retry: max_attempts: 1 circuit_breaker: error_threshold: 0.2 cooldown_ms: 60000 routes: - name: query_order match: labels: [order, read] semantic: "订单状态,物流,什么时候到,发货了吗" connector: order.query - name: send_notification match: labels: [notify, write] semantic: "发短信,发通知,提醒我" connector: notify.sms参数看着简单,但每个都是踩坑换来的。order.query是查询接口,幂等天然安全,所以允许三次重试,但每次重试间隔要拉开——200毫秒、600毫秒、1800毫秒,加起来约2.6秒,加上首次调用的3秒超时,用户最多等大约5.6秒。对于"查订单"这种场景,这个延迟基本可接受。
notify.sms我单独做了更严的熔断设置,错误阈值降到0.2、冷却时间拉到60秒。原因是短信发送一旦连续失败,往往意味着服务商那边出大事了,这时候疯狂重试只会让用户收不到短信还白白烧钱。按我的经验,对写操作和花钱类操作,熔断阈值应该比对查操作更严格。
3.4 跑通后的观测演示
配置完、跑通一次完整流程后,我会看观测数据。一条典型的成功trace长这样:
trace_id: tr_20250618_9f2a agent_id: customer_service intent: query_order_and_notify routed_to: order.query -> notify.sms segment1: order.query status=ok latency_ms=87 segment2: notify.sms status=ok latency_ms=420 final_answer: "您的订单已由上海分拨中心发出,预计明天送达,已发送短信通知。"这条trace一眼能看出两件事:查订单很快,但发短信相对慢,占了总耗时的绝大部分。如果担心整个请求太慢,优化的方向就应该是短信这道环节,而不是去调大模型的Prompt。这就是观测层的意义:它把模糊的"Agent好慢"变成了具体的"notify.sms平均耗时420ms"。
我实际跑下来还有一些细节值得注意:短信连接器第一次调用耗时3000毫秒以上,一开始我以为都这样,后来发现是因为短信服务商的沙箱环境冷启动慢。预热的经验是,把短信服务商那边的连接预热一下,首次调用前先发一条测试短信,整体延迟就能降到百毫秒级别。
4. 实测中踩过的坑:触达失败往往不是模型的错
4.1 假成功:HTTP 200但业务失败
这个坑我在开头那次事故里就踩过,后来在Agent-Reach上又遇到一次,只是形态换了。
之前有一个库存查询接口,设计得很不规范:参数传错了、没有库存、接口内部异常,全部返回HTTP 200,区别只在响应体里的code字段。第一版我的连接器只看了HTTP状态码,等于把所有异常全放行了。结果模型拿到"code=500,库存充足"的数据之后,给用户回复了"有货"。这是一个非常伤害信任的错误。
解决方案就是我在连接器里加的那行biz_code校验。现在所有连接器都强制遵守一个约定:HTTP层面只代表网络层结果,业务是否成功必须看响应体里约定的业务码。没有业务码、含义不清楚的返回,一律按失败处理,宁可得不到数据,也不能把脏数据当成真的喂给模型。
4.2 Agent在同一工具上无限循环调用
有一次做客服Agent,用户问的是"我的优惠券为什么用不了"。Agent第一次调用coupon.query拿到了一个异常状态码,按说应该返回给用户"该券已过期"这类话。但那个模型的决策出了问题:它觉得是自己调用次数不够,于是反复调用同一个工具,一次、两次、三次……直到token几乎耗尽才停下来。
表面看是模型的错,其实根子还在触达层。Agent-Reach后来加了两个防护:一是每个连接器记录调用结果的指纹,同样的params和同样的结果在短时间内重复出现时,直接给模型附加一条警告:"该结果与您上次调用的结果完全一致,重复调用可能无法解决问题";二是给单个连接器设置最大调用次数,默认5次,超了就强制终止这条触达分支。这个坑让我意识到,触达层不仅要管"怎么成功",还要管"怎么停下来"。
4.3 上下文里塞满了JSON,模型反而不会用了
这是我自己设计翻车的一个坑。早期版本里,查询连接器会把接口返回的完整JSON直接当作data返回给模型,我心想"多给点信息模型能看得更准"。结果恰恰相反。
有个订单接口的返回体特别庞大,包含几十个字段、嵌套了三层的物流轨迹,还有一堆ARRAY类型的重复数据。模型在写总结的时候,被那些无关字段严重干扰,把"签收已超时"说成了"正在派送中",因为它的注意力被某个状态字段吸引了。
后来我在连接器里加了一个返回字段白名单机制:每个连接器声明它允许返回给模型的字段,白名单之外的一律裁掉。同时给长文本字段提供摘要选项。这之后模型的准确率肉眼可见地提升了。
4.4 多Agent互等:分布式触达里的死锁
Agent-Reach支撑过两个Agent协同的场景:一个客服Agent需要从客户数据Agent那边拿用户画像,客户数据Agent在拿不到某条数据时又会反过来请求客服Agent提供补充信息。听起来很绕,实际运行的时候出现了更尴尬的事:两个Agent通过共享连接器互相等待对方的返回,各自的超时设置又不一致,最后整条链路卡住,双方都触发了重试,把系统拖到崩溃边缘。
这个问题本质上是分布式系统里的死锁,发生在Agent世界里时特别隐蔽。后面我在Agent-Reach里引入了一个规则:多Agent协作时,整个触达链路上必须携带全局trace_id,并且每条子触达的超时时间必须小于链路剩余预算。两个Agent之间的互调请求都走同一个连接器,由连接器记录"谁在等谁",一旦发现循环等待,立刻给其中一个发送超时中断。做了这个之后,类似死锁场景再没出现过。
5. 触达质量的评测思路:不要只看任务完成率
5.1 触达成功率要拆成三个阶段看
跟传统服务不一样,Agent触达一个目标是个多阶段链路,每一环都可能出错,但它们出错的诱因完全不同。我在给Agent-Reach做评测的时候,把触达拆成了三个阶段,分别统计成功率:
| 阶段 | 含义 | 常见失败原因 | 归因对象 |
|---|---|---|---|
| 意图识别 | 用户输入能否被正确理解为可执行意图 | Prompt不合适、语义模糊 | 模型层 |
| 路由命中 | 正确触达是否被路由到正确连接器 | 路由规则冲突、相似描述、标签配错 | 配置层 |
| 执行成功 | 连接器调用下游是否顺利返回有效数据 | 超时、下游故障、数据缺失 | 基础设施层 |
只看总任务完成率很容易掩盖问题。比如总成功率是85%,你根本不知道是意图识别错得多,还是路由错得多,更不知道怎么优化。拆分后就通透多了:明明是路由环节的问题,你反而去调Prompt,等于白忙活。
我给自己定了一个及格线:意图识别>95%,路由命中>95%,执行成功>98%。执行成功率要求最高,因为这里是纯工程问题,理论上最可控。
5.2 延迟预算:多Agent协作下如何分层分配
触达一旦涉及多Agent协作,延迟问题就会被放大。单个Agent查一次订单只要300ms,但如果它还要问另一个Agent要数据、再写回记录,总耗时可能就变成"自身触达+对方触达+通信时间"的叠加。
我在项目里给多Agent协作设了一套延迟预算公式:
- 明确一条完整触达的总预算,比如5秒。
- 预算分配:意图识别阶段500ms、路由阶段300ms、执行阶段4200ms。
- 执行阶段内部再按依赖链拆分,每跳分配1200ms左右,剩下的500ms留作缓冲。
- 关键原则:每一跳的超时时间都要严格小于链路剩余预算,保证整条链路不会因为某一跳迟迟不返回而崩溃。
这套东西在单一Agent场景下价值有限,但到了多Agent协作它就特别重要。没有预算分层,系统就像没有红绿灯的路口:谁都想先走,结果谁都走不了。
5.3 可回滚性:触达之后的事往往比触达本身更关键
最后一个评测维度,也是我最想强调的:触达不只是"把请求发出去、拿回响应"这么简单,它往往伴随状态改变,比如发了短信、下了订单、更新了记录。这些后果无法撤回的时候,触达的可靠触达标准就不只是一句"成功了",而是"成功且没有副作用"。
Agent-Reach对流式写操作引入了一份事件日志,每次触达在真正执行前先记录一条"pending"事件,执行结束后把事件标记为"done"或"failed"。后续如果下游系统不一致,排查人员拿这份日志就能知道某个动作到底执行没有。没有这个设计的时候,我们遇到过一次短信发送状态不明的问题:服务商那边超时了,但短信其实发成功了,用户收到两条通知还以为是系统抽风。有了事件日志和幂等键,这类问题从"怎么查都查不清"变成了"查一下日志就能给出结论"。
写在最后的实际体会
Agent-Reach做到现在,我最大的体会是:大模型确实让Agent的"脑子"变聪明了,但"手脚"还是那套传统分布式系统的问题——超时、重试、鉴权、数据格式、链路追踪,一个都少不了。很多Agent项目翻车,不是模型不够强,而是触达这段路从来没人认真铺过。
如果你现在正在做Agent,我的建议很直接:把触达层单独拎出来设计,哪怕第一版只做连接器和日志这两件事,后面都会帮你躲掉无数个"它在胡说八道"的深夜。等工具数量多起来了,再把路由、重试、熔断逐层加进去。Agnet-Reach这个名字我很喜欢,因为它时刻提醒我:Agent的价值从来不在生成,而在触达。