尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Agent-Reach:智能体能力触达层的架构设计与工程实践

Agent-Reach:智能体能力触达层的架构设计与工程实践 1. Agent-Reach 到底在解决什么问题1.1 智能体卡在最后一公里的普遍现象如果你最近在折腾 agent 开发大概率遇到过这种场景模型把任务拆得头头是道计划写得比你自己想得还周全但一到真正动手那一步就卡住了——它不知道当前这台机器上有哪些工具可用不知道怎么把一段自然语言需求映射到某个具体接口上不知道调用失败之后该重试还是该换路。整个 agent 项目看起来什么都有有 prompt、有编排、有记忆模块就是没有触达真实世界的能力。我把这类问题统称为触达断层。断层不在推理层也不在模型层而在意图和执行之间。Agent-Reach 这个项目的出发点就是补上这一段它不重新造一遍大模型也不重写一套编排引擎而是专门做能力触达这一层——让 agent 清楚知道自己能碰什么、怎么碰、碰到什么程度是安全的、碰完之后结果怎么回流到上下文里。所以你会看到它和常见的 agent 框架定位不太一样。别人做的是大脑Agent-Reach 做的是手和神经。你完全可以把它挂在你现在用的任何编排方案下面判断标准很简单你的 agent 能不能在不改 prompt 的情况下多接一个新工具并且立刻可用如果不能那这层就是缺的。1.2 为什么我做触达层而不是又一个全栈框架市面上 agent 框架已经足够多了从轻量编排到全家桶应有尽有。再做一个从模型到 UI 全包的东西除了给自己增加维护负担之外没有别的意义。真正稀缺的是边界清晰、可以被替换的一层。这里有个很现实的工程考量编排逻辑和业务强相关几乎每个团队都要改但工具接入、权限校验、执行沙箱、结果归一化这些事几乎所有团队写的代码都长得差不多。把它们抽出来做成独立层收益非常直接——编排层随便换触达层不用动新工具接入只改一处注册表不用满世界找调用点。还有一个原因是我在实际项目里吃过的亏工具调用逻辑散落在各个节点里的时候安全边界根本没法统一。A 节点直接读了环境变量B 节点放开了文件写入C 节点把用户输入的字符串拼进了命令行。等到要做权限收敛的时候你会发现要改的地方有几十处。把触达收敛成一层之后所有出站行为都经过同一个闸门审计和限流才有落点。注意把能力注册与执行收拢到单独一层最大的收益不是代码变少而是风险面收敛。所有对外动作有唯一入口这件事在后期做安全评审时价值极高。1.3 这套方案适合谁、不适合谁不是所有项目都值得引入这一层。我列了个对照表你可以对着自己的情况判断一下你的项目现状是否建议引入触达层原因只有一两个固定工具逻辑写死不太需要抽象成本高于收益工具数量超过 5 个且持续增加强烈建议注册表模式能显著降低新增成本多个 agent 共享同一批工具强烈建议避免各 agent 重复实现权限与重试需要审计所有对外调用必须引入唯一入口是审计的前提对延迟极度敏感毫秒级谨慎多一层路由会带来额外开销纯离线、无任何外部调用不需要没有触达需求如果你的答案是工具会越来越多或者我后面要上多 agent 协作那这层几乎是绕不开的。多 agent 协作的本质不是让几个模型互相聊天而是让它们共享同一套能力和同一套约束否则协作只会放大混乱。2. 架构设计与关键取舍2.1 四层结构接入、注册、执行、观测Agent-Reach 的整体结构我按职责切成了四块切法遵循一个原则变化频率不同的东西必须分开。接入层把外部世界的东西翻译成内部统一的能力描述。这里同时处理工具、浏览器、文件系统、外部服务等不同类型的触达对象。变化频率中等。注册层维护能力清单负责描述、检索、版本管理。变化频率低。执行层真正发起调用处理重试、超时、并发、沙箱隔离。变化频率低但一旦改动影响面最大。观测层记录每一次触达的输入、输出、耗时、失败原因供调试和审计使用。变化频率中等。为什么把注册和执行分开因为这两件事的失败模式完全不同。注册错了是agent 不知道该做什么执行错了是agent 知道但做不成。混在一起排查的时候你根本分不清是模型没选对工具还是工具本身炸了。分开之后日志里一眼就能看明白。2.2 工具描述与工具执行必须解耦这是我在设计里最坚持的一条。很多实现会把告诉模型有哪些工具和真正调用工具写在同一个函数里看起来省事实际上会带来三个问题。第一描述要进上下文执行不能进。工具描述是要塞进模型上下文的文本需要压缩、裁剪、按相关性筛选执行逻辑是本地代码越完整越好。这两者的优化方向完全相反绑在一起就没法各自优化。第二执行结果的格式差异巨大。有的工具返回 JSON有的返回纯文本有的返回二进制。执行层必须做归一化把结果统一成可读摘要 结构化数据两部分前者进上下文后者进程序逻辑。第三权限校验的位置不一样。描述阶段可以做粗粒度过滤这个用户能看到哪些工具执行阶段必须做细粒度校验这次调用带什么参数、目标资源属于谁。粗筛和细筛分两处漏检概率会低很多。我在实际项目里做过一个对比解耦之前新增一个工具的改动涉及 3 到 4 个文件解耦之后只需要在注册表里加一个声明加一个执行函数。这个差距在工具数量上到二十个之后会变得非常明显。2.3 上下文预算最容易被低估的成本项几乎所有人都低估了工具描述占用的上下文。我做过一次统计一个描述写得比较完整的工具声明包含名称、用途、参数说明、示例大概在 150 到 400 个 token 之间。听起来不多但二十个工具就是 3000 到 8000 token而且这部分内容是每一轮对话都要重新带上的。算一笔账假设你有 30 个工具平均每个 250 token单轮工具描述开销是 7500 token。如果一次任务平均 8 轮光工具描述就是 6 万 token 的输入。按主流模型的输入价格算一个任务光在这上面的花费就相当可观更别说这些内容还会稀释模型的注意力降低工具选择的准确率。所以 Agent-Reach 在注册层做了三件事摘要与详情分离。上下文里只放一句话摘要模型明确表示需要时再拉取完整参数说明。按场景分组。不同任务类型只加载对应分组的能力而不是全量广播。动态裁剪。连续几轮都没有被选中的能力自动降权退出当前上下文。实测下来这套组合能把工具描述的常驻开销压到原来的三分之一左右同时工具选择的准确率反而有提升——因为干扰项少了。3. 核心模块实现细节3.1 能力注册表让 agent 知道自己会什么注册表不是简单的字典它至少要包含这几个字段唯一标识、自然语言描述、参数结构、返回值类型、权限等级、超时配置、幂等性标记。参数结构我建议用标准的 JSON Schema 来描述好处是可以直接拿来做输入校验也可以自动生成给模型看的说明一份定义两处使用。幂等性标记这个字段特别容易被忽略但它在重试逻辑里是决定性的。只读操作可以放心重试写操作重试可能造成重复下单、重复发消息这类严重后果。注册表里标清楚了执行层才能自动决策。from dataclasses import dataclass, field from typing import Callable, Any dataclass class Capability: name: str # 唯一标识建议用 域.动作 的格式 summary: str # 一句话摘要进上下文用 parameters: dict # JSON Schema 描述 handler: Callable[..., Any] # 真正的执行函数 permission: str read # read / write / admin idempotent: bool True # 能否安全重试 timeout_s: float 10.0 def to_prompt_snippet(self) - str: return f- {self.name}: {self.summary}这段代码的重点不是写法而是summary 和 parameters 的职责分工。summary 是给模型看的要短、要具体、要说明什么时候用它parameters 是给校验器和调试面板看的要完整。很多团队把这两者写成一个长描述结果是模型被无关细节干扰同时参数校验还得另写一套。3.2 路由识别节点把意图映射到能力路由节点是整个链路里最考验设计的地方。我的做法是两级路由先用规则做粗筛再用模型做精排。粗筛阶段用关键词、能力分组标签、当前任务上下文做个快速过滤把候选集从几十个压到五到八个。这一步不需要模型参与纯规则就够速度快而且零成本。精排阶段把这几个候选的摘要交给模型让它选出最匹配的一个或几个并生成调用参数。为什么要加粗筛这一层因为直接让模型在几十个工具里选准确率会明显下降而且每次都要支付全量描述的 token 成本。粗筛把候选集缩小之后精排阶段可以给每个候选项更详细的说明准确率反而更高。还有一个细节路由节点必须允许无法匹配这个结果。我见过太多实现强行让模型选一个工具出来结果就是在没有合适工具的时候乱调用。正确做法是提供一个显式的无匹配出口走人工确认或者澄清提问的分支。参数生成完之后别急着执行先过一遍 Schema 校验。校验不通过就把错误信息回灌给模型让它修正最多重试两轮。这个生成—校验—修正的小循环能挡掉大部分因为参数格式问题导致的执行失败。3.3 记忆分层短期、工作、长期各管各的agent 记忆这个词被讨论得很多但落到实现上最常见的错误是只用一种存储打天下——把所有历史都塞进一个向量库需要的时候检索。这样做的后果是最近三轮的关键信息可能检索不到而半年前的一段无关对话倒是被翻出来了。我的分法是三层层级存储内容生命周期实现方式短期记忆当前任务的完整对话与工具结果单次任务内存列表直接进上下文工作记忆任务中提炼出的事实与约束任务跨轮次结构化键值按需注入长期记忆跨任务的偏好、经验、知识长期检索库按相关性召回分层的核心判据是注入时机。短期记忆是每轮都带的所以要控制长度工作记忆是条件注入的只在相关时才带长期记忆是检索触发的靠相关度决定带不带。三个层级各自有独立的裁剪策略不会互相挤占。工作记忆这一层我觉得最值得投入。它存的不是原始对话而是从对话里提炼出来的结构化事实比如用户所在时区是东八区这个任务不允许修改生产环境数据。这类信息用自然语言存太浪费用键值存既能精确注入又能做逻辑判断。3.4 执行沙箱与权限边界安全这块我不想讲得虚直接说几个具体做法。文件系统访问收拢到根目录白名单。所有文件操作必须经过一个路径解析函数解析出真实绝对路径之后判断它是否落在允许的根目录之内。这里必须用真实路径判断不能只做字符串前缀匹配否则各种路径拼接技巧能轻松绕过去。命令执行走参数数组不走字符串拼接。把用户输入直接拼进命令行是经典的风险来源。执行接口设计成接收一个列表第一个元素是程序名后面是参数由运行时负责转义。同时维护一个程序白名单白名单外的程序一律拒绝。网络请求做域名白名单加响应大小限制。域名白名单防止 agent 被诱导去访问任意地址响应大小限制防止一次调用把上下文塞爆。这两个限制看起来简单但能挡掉相当一部分意外情况。写操作默认需要确认。注册表里标记为 write 或 admin 的能力执行前先生成一份将要做什么的说明交给上层决定是否放行。在自动化流程里可以配置成自动放行但默认必须是人工确认。提示沙箱不是一次配好就完事的它需要跟着能力清单一起演进。每新增一个能力都要重新问一遍这个能力会不会突破现有边界4. 从零搭建的实操过程4.1 环境准备与目录结构先把骨架搭起来。目录结构我建议按职责分不要按业务分原因还是前面说的变化频率问题agent-reach/ ├── registry/ # 能力注册表与声明文件 │ ├── builtin/ # 内置能力 │ └── custom/ # 业务自定义能力 ├── router/ # 路由识别节点 ├── executor/ # 执行层调度、重试、沙箱 ├── memory/ # 三层记忆实现 ├── observe/ # 日志、追踪、指标 └── config/ └── policy.yaml # 权限策略与白名单依赖方面保持克制。核心只需要一个 HTTP 客户端、一个 Schema 校验库、一个结构化日志库再加一个你惯用的模型 SDK。不建议一上来就引入完整的工作流引擎那会让你在调试阶段难以判断问题出在哪一层。4.2 注册第一个能力从只读查询开始第一个能力我强烈建议选只读查询类的比如查询某个数据源的状态。原因是它的失败模式最简单不会造成副作用方便你把整条链路跑通。# registry/custom/query_status.py from registry import Capability def query_status(resource_id: str) - dict: # 实际项目里替换成真实的查询逻辑 resp http_client.get(f/api/resources/{resource_id}/status, timeout5) return {resource_id: resource_id, state: resp.json()[state]} capability Capability( nameresource.query_status, summary查询指定资源的当前运行状态返回状态码与简要说明, parameters{ type: object, properties: { resource_id: {type: string, description: 资源唯一标识} }, required: [resource_id], }, handlerquery_status, permissionread, idempotentTrue, timeout_s5.0, )写完声明之后把它放进 builtin 或 custom 目录注册表会自动扫描加载。这里有个实操细节摘要的措辞直接决定模型选不选它。我改过很多次摘要文案结论是要写什么时候用而不是写这个工具是什么。比如查询指定资源的当前运行状态比资源状态查询接口的命中率高不少因为前者更接近模型的思考方式。4.3 多 agent 协作编排单 agent 跑通之后接下来是协作。我的经验是多 agent 协作最怕的不是 agent 之间沟通不畅而是职责重叠。两个 agent 都能做同一件事结果要么互相推诿要么重复执行。做法是给每个 agent 划定能力子集。注册表支持按标签分组每个 agent 在初始化时声明自己需要哪几个分组。这样从机制上就避免了重叠——能力清单不重叠职责自然清晰。# config/agents.yaml agents: - name: researcher capability_groups: [read_only] max_steps: 8 - name: operator capability_groups: [read_only, write_confirmed] max_steps: 5 requires_approval_for: [write_confirmed]角色划分上我总结出一个还算好用的模式一个负责收集信息一个负责执行动作必要时加一个专门做校验。收集型的 agent 只给只读能力权限最小可以放开了跑执行型的 agent 能力多一些但写操作需要确认校验型的 agent 不给任何执行能力只负责检查前两者的产出是否自洽。4.4 观测与调试接入调试 agent 和调试普通程序最大的区别是普通程序的错误通常有明确堆栈agent 的错误往往是选错了路或者参数理解偏差不会有异常抛出。所以观测的重点要放在决策过程上而不只是结果。我记录这几类事件能力路由的候选集与最终选择、参数生成与校验结果、每次执行的开销与返回摘要、以及规划步骤的每一步输入输出。把这些按任务 ID 串起来出问题的时候能完整回放一次决策链路。注意日志里不要记录完整的工具返回内容。一方面体积会爆炸另一方面外部数据里可能混有不该落盘的信息。记录摘要加长度就够了需要细节的时候用任务 ID 去原始存储里捞。5. 常见问题与排查实录5.1 典型故障速查表下面这张表是我在实际运行中整理出来的覆盖了大部分高频问题现象大概率原因排查动作模型总选同一个工具摘要措辞过于宽泛区分度不够检查摘要是否都写了什么时候用参数校验反复失败Schema 太严格或说明不清放宽枚举范围补充参数示例执行超时但无错误日志超时未纳入统一异常处理检查执行层是否捕获了超时异常同一操作被重复执行幂等性标记缺失重试策略过激核对 write 类能力的 idempotent 字段上下文超限工具描述或历史未裁剪统计常驻描述占用启用分组加载agent 执行中途停止步数上限或错误中断未兜底检查 max_steps 与异常恢复分支多 agent 输出冲突能力分组重叠核验各 agent 的 capability_groups这张表的价值在于把现象直接映射到动作避免每次出问题都从头推。我在项目里把它贴在内部文档最上面新人上手的时候省了很多解释成本。5.2 我踩过的几个坑坑一把重试做成无脑循环。早期版本里执行失败就重试三次看起来挺合理。结果有一次写操作超时之后重试目标系统实际上已经处理成功于是产生了重复数据。后来加了幂等性标记只读操作重试三次写操作默认不重试、直接把失败原因回灌给模型让它决策。坑二工具描述写成了接口文档。最开始我追求描述完整把每个参数的类型、范围、默认值都写全。跑起来发现模型反而更容易选错因为信息过载之后它抓不住这个工具是干什么用的这个核心。后来改成摘要只讲用途和场景参数细节放到需要时再拉取选择准确率明显提升。坑三忽略了返回内容的体积。有个查询类工具返回了完整列表几千条记录直接进了上下文。一次调用就把后续所有轮次的预算挤没了。现在的做法是在执行层统一做截断和摘要超过阈值的返回值只保留前若干条加统计信息完整数据放到结构化存储里供程序访问。坑四权限校验放在了错误的位置。最初权限校验写在路由阶段结果发现模型偶尔会绕过路由直接构造调用。后来把校验下沉到执行层成为不可跳过的闸门路由阶段的过滤只作为性能优化存在。这个改动很小但安全性提升很实。5.3 上线前的自检清单上线之前我一般会过一遍这份清单每一项都对应过一次实际踩坑所有能力的摘要是否都写清了什么时候用它而不是它是什么。写操作与管理员操作是否都有幂等性标记且默认不自动重试。文件路径解析是否使用真实路径判断而非字符串前缀匹配。命令执行是否全部走参数数组且维护了程序白名单。网络请求是否有域名白名单与响应体积上限。工具描述的常驻上下文开销是否统计过是否启用了分组加载。每次执行是否记录了任务 ID、耗时、结果摘要能否完整回放。多 agent 的能力分组是否互不重叠写操作是否都走了确认流程。步数上限、超时、错误中断是否都有兜底分支不会让任务悬在半空。日志里是否排除了完整的原始返回内容。这份清单过完基本能挡住上线初期八成以上的问题。后面能力越加越多也不需要重新想一遍——每新增一个能力把清单第 3 到第 5 项对着它再走一遍就够了。我个人在实际操作中的体会是触达层这种东西前期多花两天把边界划清楚后面能省下几十次的返工。
返回列表