
上周三凌晨两点我在工单系统里翻到一条很扎眼的记录一个跑了三个月的智能助手在演示环境里能一口气查订单、改地址、发通知一上生产就频繁装死——要么工具调用超时没人管要么同一笔退款被重试触发了两次要么模型编了个根本不存在的工具名日志里却什么都查不到。Agent-Reach 这套东西就是被这类工单逼出来的。名字里的 Reach 是够得着的意思。一个只会聊天的大模型没有价值一个能真正够得着外部系统、并且够得稳、够得准、够得可追溯的 Agent 才有价值。Agent-Reach 干的事情很具体它站在大模型和真实世界之间把工具注册、参数校验、超时控制、重试幂等、权限边界、结果裁剪、链路追踪这一整套脏活累活收拢到一层里让上层业务代码只关心我要做什么不用关心这次调用会不会超时失败了会不会重复扣款。适合读这篇的人分三类正在做 Agent 落地、被稳定性折磨的工程同学想给自己工具链加一层保护壳的后端同学以及还在纠结要不要自己写调度层的技术负责人。下面我把这套东西的设计取舍、核心实现、踩过的坑一条条摊开讲。1. Agent-Reach 到底解决什么问题1.1 一个被最后一公里卡住的真实场景大部分 Agent 项目死在一个很尴尬的位置推理链路做得漂漂亮亮工具定义写了十几页提示词评测集上准确率 85%然后一上真实流量就崩。崩的原因往往不是模型不行而是模型和外部系统中间那最后一公里没有任何工程化保护。举个我亲手处理过的例子。用户问帮我查下上个月那笔 899 的订单到哪了模型返回了一个order.query调用参数是{order_id: 899}。业务接口收到之后直接 400异常往上抛Agent 主循环没接住整个会话挂掉。用户看到的是服务异常请稍后重试。整个过程里没有任何一处代码告诉模型你参数传错了订单号应该是 12 到 20 位数字。这就是典型的够得着了但够不准。我把这类问题归成四类够不着工具根本没注册、或者注册了但模型不知道什么时候该用、够不准参数格式、必填项、枚举值全错、够不稳超时、限流、下游抖动一抖就全崩、够不明出了问题查不到是哪一步、哪个参数、第几次尝试挂的。Agent-Reach 的四层结构——注册层、调度层、适配层、观测层——就是分别冲着这四个问题去的。注意不要把这一层写成万能胶水。它的价值在于把不确定性收口而不是把所有业务逻辑都塞进去。业务规则该在业务服务里Agent-Reach 只负责把调用这件事做可靠。1.2 四项可达性指标我给 Agent-Reach 定的验收标准定指标这件事很关键因为Agent 好不好用这种描述没法验收。我给团队定的是四项硬指标每一项都能从日志里算出来。指标定义我的目标线低于目标线先查什么工具调用成功率非模型原因导致的失败占比99.5% 以上下游健康度、超时预算是否太紧参数一次通过率首次调用就通过 schema 校验的比例92% 以上工具描述是否写清楚了取值格式重试放大倍数实际请求数 / 逻辑调用数低于 1.15退避策略、幂等键是否生效可定位率能凭一条 trace 还原现场的比例100%是否有调用没带 trace_id第三项最容易被忽略。重试是双刃剑它能救回瞬时抖动也会在下游已经过载时雪上加霜。1.15 这个数字是我从几次故障复盘里反推出来的经验值如果重试放大倍数长期高于 1.15说明失败不是偶发而是下游真的有问题这时候重试只会让情况更糟该做的是熔断和降级。第四项更狠我要求 100%。任何一次工具调用无论成功失败都必须能凭 trace_id 查到哪个 Agent 会话、哪一轮、模型原始参数、校验后的参数、发出去的请求体、收到的响应摘要、耗时、第几次尝试。做不到这一点后面所有排查都是猜。1.3 为什么不做成又一个 MCP 客户端现在协议层的方案已经很成熟很多人第一反应是直接用现成的协议客户端不就行了。我的答案是可以但不冲突。协议解决的是怎么连上Agent-Reach 解决的是连上之后怎么用得住。这两件事的边界很清楚协议层负责握手、能力协商、消息编解码Agent-Reach 负责在这之上加一层策略。比如同一个工具协议层只说有个send_email能力但 Agent-Reach 会补上这个工具不可幂等禁止自动重试这个工具属于高风险动作必须人工确认这个工具单次返回最多 800 token超了要分页。这些策略是业务层面的不属于任何协议的职责范围。所以我的做法是把适配层做成可插拔的HTTP 走一套适配器协议型工具走一套适配器命令行和浏览器各一套。上层调度逻辑完全不知道底下是什么只认统一的CallResult。这样将来换协议、换供应商调度层一行不用改。这个决定在后来吃过一次红利——我们换掉了一个不稳定的第三方工具服务只写了 40 行适配器代码其余零改动。2. 架构拆解与关键取舍2.1 工具注册层把工具说明书当接口契约来写工具注册层最大的误区是把它当成给模型看的提示词。不是。它是接口契约是运行时校验的依据也是权限判定的输入。一份工具描述至少承担四个职责告诉模型什么时候用、告诉校验器参数长什么样、告诉调度器怎么重试、告诉审计系统这属于什么敏感级别。我给每个工具定义四个区块。第一个是语义区块包含description和when_not_to_use。后者是我强制要求的实践下来收益极大模型选错工具八成不是因为不知道什么时候用 A而是不知道什么时候不该用 A。第二个是input_schema严格 JSON SchemaadditionalProperties一律设为false防止模型自由发挥塞进一堆没用的字段。第三个是reach区块描述适配器类型、端点、超时、重试、幂等性、所需权限。第四个是result区块描述返回结果的裁剪策略和 token 上限。有一处取舍值得说additionalProperties: false会让一些模型多传了个字段的调用直接失败。我一开始担心这会拉低成功率实测下来反而提高了——模型在被明确拒绝之后第二次调用基本都能改对而且日志干净了非常多。如果你担心误伤可以设成宽松模式多余字段记录 warning 但不拒绝。2.2 执行调度层所有脏活累活的集中地调度层是整套东西的心脏它要按固定顺序做六件事参数校验、类型纠正、权限判定、额度检查、带预算的执行、结果后处理。顺序不能乱。比如权限判定必须在真正发请求之前否则等于没做额度检查必须在参数校验之后因为额度可能跟参数有关查询 1 条和查询 1 万条是两回事。这里有个我踩过的坑早期我把类型纠正放到了参数校验后面结果模型传{limit: 50}的时候校验直接失败。后来改成先按白名单做一次温和强转再校验成功率立刻上了一个台阶。强转白名单要写得克制——只允许字符串与数字、字符串与布尔之间的安全转换数组和对象一律不转防止把[1,2]这种字符串悄悄变成数组导致意外。另一个关键设计是错误分层。我把错误分成三类框架内部可重试错误超时、429、5xx、连接重置框架内部不可重试错误400、401、403、404、schema 校验失败以及未知错误。第一类由调度层自己消化不打扰模型第二类必须转成结构化结果回传给模型让它自己改参数或换工具第三类保守处理直接终止这一轮并上报。注意最忌讳的做法是把下游异常原样抛给模型。模型看到一段 Java 堆栈大概率会开始胡编。你应该给它一句人话比如订单号格式不对需要 12 到 20 位纯数字请重新获取。2.3 接入适配层HTTP、协议型、命令行、浏览器的统一姿势适配层的唯一目标是输出统一的CallResult。我把适配器分成四类各自的重点不一样。HTTP 适配器最常见重点是超时和连接复用。连接池别用默认值Agent 场景的并发是突发的默认池子大了浪费、小了排队。我的经验值是最大连接数 平均 QPS 的 2 到 3 倍单主机连接数上限设成 32 就够读超时必须显式设置绝不能用无限等待。协议型工具适配器比如各类工具协议服务的重点是会话生命周期。这类连接通常有状态断了要重连重连期间不能把请求丢掉得放进短暂的重试队列。我用的策略是重连窗口 3 秒窗口内请求排队超过窗口直接失败避免无休止排队把线程池占满。命令行适配器最危险重点全是安全命令白名单、参数禁用 shell 拼接、工作目录锁定、执行超时、输出截断、非 root 用户运行。我要求所有命令行工具的参数都必须经过一次危险字符检查包含管道符、反引号、分号、重定向符号的参数一律拒绝绝不允许把模型给的字符串直接拼进 shell。浏览器适配器最贵也最慢重点是生命周期和快照体积。一个页面快照动辄几万 token必须做元素裁剪——只保留可交互元素和可见文本其余全部丢掉编号成短引用。我实测过一个列表页裁剪前 4 万多 token裁剪后 900 出头正确率没有下降。2.4 观测与审计层没有 trace 的 Agent 等于黑箱观测层的设计原则是一次会话一条链路一次调用一个节点。trace_id 在会话创建时生成贯穿模型推理、工具调用、下游请求三层。我用的是轻量方案进程内上下文变量存 trace_id所有出站请求自动带上X-Trace-Id头下游服务如果愿意透传就透传不愿意也不强求。审计日志要记什么我列了个最小集时间戳、trace_id、会话 id、轮次、工具名、工具版本、原始参数脱敏后、校验后参数、目标端点去掉 query 里的敏感值、尝试次数、每次尝试的耗时与结果码、最终状态、返回体积。注意工具版本这个字段——工具描述一改模型的行为就变没有版本号你根本没法做 A/B 归因。成本指标也放在这一层。每次调用记录输入输出字符数按当月单价折算金额按会话、按工具、按租户三个维度汇总。这个数据后来帮我抓到过一个很隐蔽的问题某个工具因为描述写得含糊模型每次都先调它再调另一个白白多花 30% 的推理成本。改完描述之后成本立刻降下来了。3. 从零搭一个最小可用版本3.1 目录结构与依赖我不建议一上来就上微服务、上消息队列、上分布式追踪。最小可用版本用单进程加一个键值存储就能跑起来先把协议定清楚再谈横向扩展。agent_reach/ registry/ loader.py # 加载工具清单 schema.py # JSON Schema 校验与强转 gateway/ executor.py # 超时预算 退避重试 idempotency.py # 幂等键与去重 breaker.py # 熔断与半开探测 adapters/ http_adapter.py mcp_adapter.py shell_adapter.py observe/ trace.py audit.py tools/ order_query.yaml user_lookup.yaml依赖尽量少。Python 侧我只需要一个 HTTP 客户端、一个 JSON Schema 实现、一个支持 TTL 的键值存储客户端。三个库封顶其余全部用标准库。依赖越少你的调度层越容易审计——这一层是要背稳定性责任的不适合堆花哨的东西。3.2 工具清单文件长什么样清单文件是契约的落地点我按下面的结构写name: order.query version: 3 description: 按订单号查询订单的状态、金额和物流信息。 when_not_to_use: 用户只提供手机号、昵称或模糊描述时不要使用先用 user.lookup 换取订单号。 input_schema: type: object properties: order_id: type: string pattern: ^[0-9]{12,20}$ with_logistics: type: boolean default: false required: [order_id] additionalProperties: false reach: adapter: http method: GET endpoint: https://internal.example.com/orders/{order_id} timeout_ms: 6000 retry: max_attempts: 3 backoff_base_ms: 600 backoff_factor: 2 jitter: 0.2 cap_ms: 4000 idempotent: true scope: [order:read] result: max_tokens: 1200 strategy: trim_arrays array_keep: 5when_not_to_use和description是给模型看的其余是给运行时看的。idempotent这个字段是重中之重如果是false调度层会直接禁用自动重试只允许一次尝试失败就把决策权交回给模型或人。3.3 参数校验与类型强转先温和强转再严格校验这个顺序是被现实逼出来的。import schema # 任意 JSON Schema 实现 SAFE_COERCE {string, integer, number, boolean} def coerce_args(raw: dict, spec: dict) - dict: props spec.get(input_schema, {}).get(properties, {}) out {} for key, value in (raw or {}).items(): rule props.get(key) if rule is None: continue # additionalPropertiesFalse多余字段直接丢 target rule.get(type) if target not in SAFE_COERCE: out[key] value continue if target integer and isinstance(value, str) and value.strip().lstrip(-).isdigit(): out[key] int(value) elif target number and isinstance(value, str): try: out[key] float(value) except ValueError: out[key] value elif target boolean and isinstance(value, str): low value.strip().lower() if low in (true, false): out[key] (low true) else: out[key] value elif target string and isinstance(value, (int, float, bool)): out[key] str(value) else: out[key] value return out强转完之后缺省值补齐再跑一遍校验。校验失败不要抛异常返回一个结构化错误对象def validate(tool, args): try: schema.validate(args, tool[input_schema]) return {ok: True} except schema.ValidationError as exc: return { ok: False, code: INVALID_ARGUMENT, field: exc.path, message: exc.message, hint: 参数不符合约束请按 field 提示修正后重试。, }hint这个字段是给模型看的别写内部黑话。写参数不符合约束比写SchemaValidationError at $.order_id有用得多。3.4 执行器超时预算、退避重试、幂等执行器是整个实现里最需要小心的一段。核心思路是给整次逻辑调用一个总预算然后在多次尝试之间分配。import time, random RETRYABLE_STATUS {408, 425, 429, 500, 502, 503, 504} def backoff_delay(base_ms, factor, attempt, jitter, cap_ms): raw min(base_ms * (factor ** (attempt - 1)), cap_ms) return raw * (1 random.uniform(-jitter, jitter)) def plan_budget(total_ms, max_attempts, overhead_ms, backoff_ms): usable total_ms - overhead_ms - backoff_ms if usable 0: raise ValueError(总预算太小放不下一次尝试) # 首次快速失败最后一次给足时间 weights [1.0 / (2 ** i) if i max_attempts - 1 else 2.0 for i in range(max_attempts)] scale usable / sum(weights) return [int(w * scale) for w in weights]这里有两个反直觉的点。第一第一次尝试的超时应该设得偏短而不是偏长。原因很简单第一次失败大多不是慢而是参数问题或瞬时抖动让它早点失败才能把时间留给后面。第二最后一次尝试要给足时间因为如果连最后一次都因为超时失败整次逻辑调用就彻底失败了多等一会儿的收益大于省那几秒。幂等键的构造有个陷阱绝对不能把 trace_id 或时间戳拌进去否则每次调用生成的键都不一样去重等于没做。import hashlib, json def idempotency_key(tool_name, args, tenant_id): payload json.dumps( {tool: tool_name, args: args, tenant: tenant_id}, sort_keysTrue, separators(,, :), ensure_asciiFalse, ) return hashlib.sha256(payload.encode(utf-8)).hexdigest()[:32]键的粒度也要想清楚。tenant_id必须进去否则 A 租户和 B 租户用同样参数调同一个工具会被误判成重复。但user_id通常不该进去——同一个业务动作可能由不同入口触发按租户加业务参数去重更符合直觉。3.5 长任务从同步等待改成提交-轮询凡是耗时超过 10 秒的工具一律不要同步等。我把它拆成两步提交任务拿到job_id然后用job.poll轮询。这样做有三个好处模型不会因为一次超长等待把上下文撑爆用户能看到进度下游服务不会因为长连接堆积被打挂。def submit_job(tool_name, args, tenant_id, store, queue): key idempotency_key(tool_name, args, tenant_id) existing store.get(fjob:{key}) if existing: return existing # 已有在跑的任务直接复用 job_id queue.enqueue(tool_name, args) store.set(fjob:{key}, job_id, ttl86400) return job_id轮询侧要设最长等待时间和轮询间隔递增。我的经验值是间隔从 1 秒起步每次乘 1.5上限 5 秒总等待上限 90 秒。到点还没结果就返回任务仍在处理中稍后可再查询别把用户的会话卡死。3.6 结果裁剪别让一次接口调用吃掉上下文结果裁剪不是截断字符串这么粗暴。我按三步走先按结构裁剪再按体积裁剪最后做外置引用。def estimate_tokens(text: str) - int: # 保守估算中日韩字符按 1 token其他按 4 字符 1 token cjk sum(1 for ch in text if \u4e00 ch \u9fff) other len(text) - cjk return cjk other // 4 1 def trim_result(payload, max_tokens1200, array_keep5): if isinstance(payload, dict): payload {k: trim_result(v, max_tokens, array_keep) for k, v in payload.items()} elif isinstance(payload, list): head [trim_result(x, max_tokens, array_keep) for x in payload[:array_keep]] if len(payload) array_keep: head.append({_truncated: len(payload) - array_keep}) payload head text json.dumps(payload, ensure_asciiFalse) if estimate_tokens(text) max_tokens: return payload return {_overflow: True, preview: text[: max_tokens * 2], note: 结果过大已截断可用更精确的筛选条件重试。}数组裁剪是最见效的一招。一个返回 200 条记录的接口裁到 5 条加一个还有 195 条的提示模型照样能正确回答有多少条这类问题而且几乎不会因为信息过载乱编。4. 可靠性参数怎么算而不是怎么猜4.1 超时预算的分配算法拿一个真实例子算一遍假设某个工具的整体预算是 30 秒最多尝试 3 次退避基础 600 毫秒、倍数 2、抖动 20%、上限 4000 毫秒结果序列化预留 1500 毫秒。第一步算退避总耗时0.6 1.2 1.8秒抖动上浮 20% 得 2.16 秒取整 2.2 秒。第二步算可用时间30 - 2.2 - 1.5 26.3秒。第三步按权重分配权重是[0.5, 0.25, 2.0]前两次快速失败最后一次给足归一化之后大约是[5.1, 2.6, 18.6]秒。等等这个分配显然不合理——第二次尝试 2.6 秒比第一次还短没有意义。我实际用的权重是[1.0, 1.5, 3.0]归一化后得到[4.8, 7.2, 14.3]秒。这个分配符合直觉第一次快确认是不是参数或路由问题第二次稍长看下游是否缓过来了第三次最长作为最后的兜底。这套算法的关键输入是退避总耗时你必须把它算进预算里否则会出现三次尝试的时间加起来刚好等于总预算退避却挤不进去的尴尬情况。我见过不少实现就是因为漏算退避导致最后一次尝试被强制截断。4.2 重试次数与退避序列重试次数不是越多越好。我给的经验值是幂等且轻量的读操作最多 3 次非幂等写操作最多 1 次也就是不重试跨系统强一致的写操作 0 次并强制人工确认。退避序列要加抖动不加抖动的退避会制造惊群——所有失败请求在同一时刻一起重试把刚缓过来的下游再打一次。抖动幅度我常用 20%实测足够打散又不会让延迟波动太夸张。还有一条容易被忽略的规则重试必须带上这次是不是最后一次的信息。执行器在最后一次尝试时应该走一条更保守的路径比如直接返回缓存结果、或者返回部分结果加提示而不是又一次硬碰硬。4.3 熔断阈值与半开探测熔断器我用滑动窗口实现窗口 60 秒。打开条件是失败率大于 50% 且样本数不少于 20。样本数这个门槛很重要样本太少的时候偶发失败会被误判成整体故障。半开探测我设的是放行 3 个请求全部成功则关闭熔断有一个失败立刻重新打开并延长冷却时间。冷却时间从 30 秒起步每次重开翻倍上限 5 分钟。这个指数冷却救过我一次某个下游服务其实已经挂了半小时但我们的熔断器每 30 秒就放三个探测请求过去结果把下游的恢复时间拖得更长。改成指数冷却之后下游恢复得快了很多。熔断打开时调度层要给模型一个明确的信号而不是简单报错。我回传的是{ok: false, code: DEPENDENCY_DEGRADED, hint: 该工具暂时不可用请改用其他方式回答或告知用户稍后再试。}模型拿到这句话通常能给出一个体面的降级回答而不是傻等。4.4 并发与成本控制并发控制我分两层。租户层用令牌桶限流防止单个租户吃掉全部配额工具层用信号量对下游脆弱的工具单独限并发。信号量的值不是拍脑袋定的而是压测出来的从并发 1 开始逐步加压找到 P95 延迟开始陡增的拐点取拐点值的 70% 作为上限。成本控制放在调用前。每个工具可以配单次成本上限和单会话成本上限超了就拒绝并回传提示。这看起来很粗暴但它拦住的是真实存在的失控场景模型陷入循环反复调用同一个高成本工具。5. 常见问题与排查速查表5.1 模型侧的三类典型问题第一类是编造工具名。模型输出一个不存在的工具调度层查注册表查不到直接报错。处理方式是结构化返回 可选工具列表把可用工具名以紧凑格式附在错误里模型的自我修正率相当高。第二类是参数格式想当然。比如把时间传成上个月把金额传成899 元。解决办法不是加更多校验而是改工具描述——在description里明确写时间参数必须为 ISO 8601 格式在when_not_to_use里写用户给出模糊时间描述时先用 time.resolve 转换。描述改完参数一次通过率能从 70% 出头提到 90% 以上。第三类是工具循环。模型反复调同一个工具、同样参数。我在调度层加了重复检测同一个会话内同工具同参数经幂等键归一化连续出现 3 次直接拒绝并把缓存结果返回同时在系统提示里插入一句你已经调用过这个工具请基于已有结果回答。5.2 工具侧的四类故障下游 400 但参数看起来没错八成是编码问题。中文参数没做 URL 编码、或者Content-Type里的字符集写错了都会导致下游解析失败。我现在的做法是强制 UTF-8 并在请求构造阶段就做编码不依赖下游宽容。下游偶发 502但重试就好。这种情况别急着加大重试次数先看是不是连接池配置问题。连接复用不当会导致每次请求都新建连接遇到网关的连接数限制就偶发失败。把连接池的keepalive打开、max_idle设成合理值这类 502 通常会消失。工具返回 200 但内容是错误信息。这是最常见也最阴的坑。有些下游不管成功失败都返回 200错误信息塞在 body 里。解决办法是在适配器里加响应语义检查按配置的字段路径判断成功与否比如检查body.code 0。工具版本升级导致模型行为变化。工具描述一改模型的调用模式就变。所以清单文件必须带版本号每次改动记录变更日志上线前跑一遍固定评测集回归。5.3 平台侧的三类问题trace 断链。最常见原因是异步任务里没有透传上下文。Python 的上下文变量不会自动跨线程或跨协程池传播必须在提交任务时显式捕获并还原。这个坑我踩过两次第二次之后我把它写进了代码审查清单。日志过大。工具返回的原始响应全量落到日志里一天就能把磁盘写满。做法是日志里只记摘要和哈希需要完整响应时按 trace_id 去对象存储拉。配额被一个租户吃光。这类问题的根因往往是身份识别没做对所有请求都被归到同一个默认租户。检查清单会话创建时是否正确解析租户、出站请求是否带上租户头、限流键里是否包含租户 id。5.4 一套可以直接照着查的速查表现象最可能的原因第一步查什么模型调用不存在的工具工具列表注入不完整系统提示里的工具清单条数参数校验大量失败工具描述没写清格式description是否含具体格式示例同一笔写操作被执行两次幂等键含了随机字段幂等键生成函数是否含时间戳下游偶发 502 但重试即好连接池配置不当连接池 idle 与 keepalive 配置工具返回 200 但模型说失败响应语义未解析适配器是否检查业务状态码整体成功率骤降但下游正常熔断阈值被误触发熔断窗口的样本数门槛一次工具调用吃掉半屏上下文结果未裁剪max_tokens与数组裁剪配置排查时找不到完整请求上下文未跨异步传播任务提交时是否捕获 trace 上下文这张表我打印出来贴在工位旁边。90% 的线上问题第一列的现象能在两分钟内定位到第二列剩下就是改配置或者改描述。6. 权限边界与安全底线6.1 双层权限取交集权限判定我用两层交集工具声明的scope比如order:read和当前用户被授予的 scope。两者交集为空则拒绝且拒绝要发生在发请求之前。很多实现把权限检查放在下游服务里这在 Agent 场景下是不安全的——模型可能通过一串看似无害的调用拼凑出越权效果必须在调度层就拦住。还有一条硬规则工具能访问的数据范围不能超过调用者自己的权限范围。比如工具支持传user_id参数就必须强制覆盖成当前登录用户绝不允许模型自己指定别人的 id。这个校验放在参数强转之后、发请求之前。6.2 高风险动作的人机确认我把动作分成三档。只读类自动执行有副作用但可逆的比如发送内部通知、创建草稿自动执行并记录审计有副作用且不可逆或涉及外部影响的比如发起付款、发送对外邮件、修改生产配置一律进入确认队列需要人来点头。确认环节的设计要点是把决策信息压缩到一屏内。给确认人看到的是谁触发的、要做什么、关键参数是什么、为什么模型认为该做这件事、预期影响范围。信息给全了确认才有意义给一堆原始 JSON人只会无脑点同意。6.3 审计日志该留什么、该抹什么留什么前面说过这里说抹什么。所有可能包含个人信息的字段必须在落盘前脱敏手机号保留中间四位之外的掩码、邮箱只留域名、身份证号全掩码、地址只留城市。脱敏要在写入前做而不是查询时做——查询时脱敏意味着原始数据已经落盘了风险已经产生。另外一条经验**审计日志的保留期要和业务风险匹配。**只读操作保留 30 天足够涉及资金或对外影响的操作至少保留 180 天。保留期这个数字最好在项目启动时就定下来并写进文档中途改会牵动存储成本和合规口径两件事。7. 一些踩坑之后的个人体会这套东西我从第一版做到现在最大的感受是Agent 落地难难在工程而不是在模型。模型能力这半年涨得很快但够不稳、够不明这两件事模型再强也解决不了只能靠调度层老老实实做。如果只让我留三条经验第一把工具描述当接口文档写尤其是什么时候不该用这一句收益远超预期第二非幂等工具一律不许自动重试这一条能挡掉绝大多数资损事故第三trace 必须做到 100% 覆盖因为排查能力决定了你能不能在半夜两点睡着。后续我打算补的东西有两块一块是把失败样本自动回流成评测集让工具描述的迭代有数据支撑另一块是给每个工具做健康画像把成功率、P95 延迟、参数一次通过率画成趋势图这样在指标下滑的早期就能介入而不是等工单进来才发现。这两块做完Agent-Reach 就从能用往好用走了一步。