ARTICLE DETAIL

资讯详情

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

AI Agent统一触达层实践:从工具调用到多Agent协作

AI Agent统一触达层实践:从工具调用到多Agent协作 做AI Agent开发最难受的从来不是模型不会回答问题而是它什么都想清楚了偏偏手脚够不到外面。这几个月我在团队里维护了一个项目代号就叫Agent-Reach专治这种“触达不到”的毛病。简单说Agent-Reach 是一个面向智能体的统一触达层用来管理 Agent 和外部工具、业务接口、其他 Agent 之间的所有连接。它让 Agent 通过一套固定协议去调用库存查询、日程写入、消息推送这类能力同时把权限、超时、重试和审计都收口到一处。如果你也在做 Agent 应用、正在搭多 Agent 协作、或者被外部工具调用搞得一团乱这篇文章应该能给你省几天时间。我不会写那种“架构图挂墙上”的东西下面全是我自己跑过的流程和踩过的坑包括设计思路、核心代码、参数配置以及问题排查尽量让你照着就能复现。1. Agent-Reach 设计定位为什么 Agent 只差一层“触达”1.1 模型很聪明但 Agent 经常“够不到东西”今年我自己做了好几个 Agent 项目最大的感受是模型本身已经很强了思维链、工具选择、任务拆解都做得像模像样但真到“干活”这一步就卡壳。卡壳的点集中在三件事上。第一模型需要的数据不在它脑子里而在公司内部的 API 里。第二模型需要调用的动作有副作用比如发邮件、改订单、写数据库这些操作不能随随便便让它直接碰。第三多个 Agent 之间需要互相找对方办事但每个 Agent 的接口都不一样你写死了一个又一个适配器最后代码比业务逻辑还多。我最早的做法很朴素就是直接在 Agent 的代码里写requests.get(...)、httpx.post(...)再加几个 if-else 判断工具名。跑通一个 demo 没问题可一旦工具数量过 20问题就全来了权限没法收敛、超时没人管、日志里看不出调了谁、模型偶尔还传错参数。项目代号 Agent-Reach 就是在那时候定下来的目标很直接把“Agent 触达工具、触达 API、触达其他 Agent”这件事收敛成一层标准化的通道。1.2 我们想要的触达层统一、可控、可观测市面上的 Function Calling 看似解决了“模型调函数”但它解决得比较单薄。OpenAI 的 function calling 让模型输出一个 JSON里面写着函数名和参数然后你自己去派发。本地跑的 Agent 框架也都有 tool 注册机制但多数是进程内的函数映射。它们共同的短板是缺少运维维度。生产环境里你需要的不只是“能调用”而是“谁能调、调谁的、多久必须返回、失败了怎么办、调用链路能不能查”。我设计 Agent-Reach 的时候脑子里一直浮现一个词网关。API 网关管住了服务和调用方的所有流量Agent-Reach 就是想当 Agent 世界里的网关。它具备三个核心特征统一。不管是工具、HTTP API、消息队列还是另一个 Agent在 Agent-Reach 里都被描述成“资源”有一个统一的 reach name。模型侧不用关心后端是 Python 函数还是 Java 服务。可控。默认拒绝所有触达请求按角色、命名空间、调用方身份逐步放行。不是“白名单里的工具可用”而是“没被授权的资源一律不可见不可调”。可观测。每次触达都有全局 traceId从模型发出请求开始到工具执行结束中间的所有超时、重试、权限校验、返回结果都落到日志里。拿一个最直观的例子说明差距。之前我在一个 Agent 里直接写函数调用工具多了以后出问题第一反应是“打印一行 print 看看”。引入 Agent-Reach 后同一个问题只需要在日志平台搜 traceId就能看到请求是从哪个 Agent 发出的、在哪一步被拒绝、参数长什么样、返回是不是超时。这种体验一旦习惯就再也回不去了。1.3 核心架构注册中心、路由、权限、会话Agent-Reach 在实现上拆成了四个模块逻辑非常朴素第一块是注册中心。所有工具、服务、Agent 启动时都会往这里注册自己的 reach name 和描述信息。注册中心保存的是一份元数据表里面包括触达名、入参 schema、出参 schema、超时建议值、权限标签。它不做实际业务调用只负责“我知道谁在哪里”。第二块是触达路由。当 Agent A 请求 reach namewarehouse.query_stock时路由负责找到真正处理这个触达的 handler然后把请求转发过去。如果后端是 HTTP 服务会做服务发现和负载均衡如果后端是本地函数就直接走进程内调用。第三块是权限沙箱。触达请求在真正执行前会经过 ACL 校验。校验维度包括调用方身份、角色、资源所属域、操作类型。权限沙箱返回 allow 或 denydeny 的请求不会进业务代码直接返回权限错误给 Agent。这样即使模型“突发奇想”去调一个危险工具也会被挡在门外。第四块是会话上下文。多 Agent 场景下A 替用户去问 BA 需要把用户身份、原始任务上下文透传过去否则 B 不知道自己在为谁办事。会话模块负责维护session_id、trace_id、user_hint这些透传字段让整个链路不会断。这四块加起来Agent-Reach 本质上是一个很薄的“触达中间层”。它不参与模型推理不替代 Agent 框架只负责一件事当一个 Agent 想“伸手”到某个外部资源时给它一条安全、快速、有迹可循的路。2. 三个关键设计协议、权限和路由2.1 统一触达协议让每个工具都变成同一个形状Agent-Reach 能统一所有工具靠的是一套触达协议。协议不复杂就是把一次触达抽象成四个字段reach_name、payload、metadata、expects_response。reach_name资源的全局唯一名字可以带命名空间。例如warehouse.query_stock其中warehouse是域query_stock是操作。payload传给 handler 的参数必须是 JSON 对象。metadata携带调用方身份、traceId、sessionId、优先级等附加信息。expects_response布尔值表示这次触达是“请求-响应”还是“只管发不管回”。为了让模型能准确使用协议每个 reach name 在注册时都要提供一份 JSON Schema 作为入参约束。 Schema 的价值在于给模型“指路”。模型不需要读源码只需要看 schema就知道该传哪些字段、哪个字段必填、值是什么格式。我举个例子。注册一个库存查询触达from agent_reach import handler handler( namewarehouse.query_stock, description按SKU查询某仓库实时库存返回可售库存与锁定数量, input_schema{ type: object, properties: { sku: { type: string, description: 商品SKU编号例如SKU-10086 }, warehouse: { type: string, description: 仓库编码例如WH-01不传默认主仓 } }, required: [sku] } ) def query_stock(sku: str, warehouse: str WH-01) - dict: # 真实项目里这里会去向上游仓储服务 return { sku: sku, warehouse: warehouse, available: 86, locked: 2, timestamp: 2025-01-12T10:30:00Z }这段代码的重点不在函数本身而在于input_schema。它回答了一个很实际的问题模型怎么知道query_stock需要什么参数答案就是看 schema。没有 schema 时模型靠“猜”猜错的概率不低有 schema 后调用成功率高很多尤其是必填字段和枚举值能直接引导模型。接入 Agent-Reach 后SDK 会自动把 handler 包装成可被路由调度的资源同时把 schema 存进注册中心。设计协议时我踩过一个坑一开始把所有字段都设为必填结果模型在调用时经常因为缺一个可选字段就犹豫半天甚至拒绝调用。后来改成“必填字段只放真正必要的其他都给默认值”调用成功率立刻上来了。2.2 触达权限模型默认拒绝按需放行触达权限是 Agent-Reach 里我最看重的一块。因为 Agent 可以自主决策一旦权限失控后果比人工操作失控要快得多。模型可能因为 prompt 注入、错误上下文或者单纯幻觉去调用一个高危工具。权限模型的设计原则是“默认拒绝”。没有显式授权的触达请求一律不允许执行。这和传统的“默认允许、加黑名单”思路完全反着来。很多踩过坑的同事一开始觉得默认拒绝太麻烦要配很多规则但真跑一段就会发现默认拒绝虽然前期费点事后面却非常省心。权限配置我建议用 YAML 管理放在专门的配置文件里reach: acl: default_policy: deny rules: - name: ops_read_only role: [ops_agent] allow: [warehouse.*, order.query_*] deny: [order.create_order] - name: user_proxy_limited role: [user_proxy] allow: [warehouse.query_stock, calendar.*, mail.*] deny: []上面配置里ops_agent角色可以调用所有仓库相关触达和订单查询类触达但不能创建订单user_proxy角色只能碰库存查询、日历和邮件。规则不复杂但已经能挡住大部分越权操作。实际执行时ACL 会按以下顺序判断请求携带的角色标签是否命中规则命中的规则里是否包含触达名如果包含则继续检查 deny 列表如果不包含任何规则直接按default_policy处理。这套逻辑和 nginx 的访问控制有点类似但更面向 Agent 场景。关于权限我想提醒一点不要设计“审批后永久放行”。我见过有个团队给 Agent 配了临时权限审批第一次审批过了就缓存下来之后都不再审。结果模型在一次会话中被诱导反复调用某个工具权限缓存成了帮凶。对 Agent 这种自主主体权限尽量绑定 session 和任务上下文而不是绑全局身份。2.3 多 Agent 可达性路由怎么找到能办事的 Agent单 Agent 场景下触达路由就是从 A 到工具的转发。多 Agent 场景就多了一层Agent 之间怎么互相触达。比如用户想约明早十点的会你手头有一个日历 Agent、一个邮件 Agent还有一个会议室查询 Agent。用户代理 Agent 需要知道“约会议室”该找谁。Agent-Reach 在路由表里把每个 Agent 也当成一个可达资源。Agent 启动时注册自己的描述路由表记录类似这样的信息AgentReach 前缀负责事项cal_agentcalendar.*日程查询、写入、取消mail_agentmail.*邮件草稿、发送、查询room_agentroom.book_room会议室预订路由配置可以是routing: routes: - pattern: calendar.* target: cal_agent timeout_ms: 3000 - pattern: mail.* target: mail_agent timeout_ms: 5000 - pattern: room.book_room target: room_agent timeout_ms: 5000当用户代理发起calendar.create_event触达时路由直接把请求转发给cal_agent。对调用方来说它不需要知道cal_agent跑在哪台机器上、是 Docker 还是 Serverless它只需要知道“触达calendar.create_event就能完成建日程这件事”。多 Agent 路由里最容易忽略的是循环触达。A 调 BB 发现需要 C 的数据C 又回头调 A。这种循环在日志里看起来就是请求量突然暴涨。我处理这个问题的方法是给每条触达加两个参数max_depth和timeout_ms。max_depth限制触达链最多跳几层超过就丢弃timeout_ms限制单次触达的最大等待时间。这两参数后来成了排查多 Agent 问题的关键。路由解析时需要有一点近似匹配能力而不是只做字符串完全相等。因为模型可能生成calendar.creat_event这种带拼写错误的触达名。我在 Agent-Reach 里加了编辑距离阈值如果找不到完全匹配的 reach name会尝试找相似度在 0.85 以上的候选然后把候选列表返回给模型要求重选。这个机制实测可以把工具调用失败率降低一个量级。3. 从零接入 Agent-Reach 的实操记录3.1 安装与最小配置我这边用的是 Python 3.11Agent-Reach 以 SDK 的形式提供。安装没什么特别pip install agent-reach安装完先准备一个最小配置文件agent_reach.yamlserver: port: 8710 registry: storage: memory acl: default_policy: deny rules: - name: demo_allow role: [dev] allow: [warehouse.*] deny: []启动入口写在一个文件里from agent_reach import ReachServer server ReachServer(config_fileagent_reach.yaml) server.start()跑起来之后Agent-Reach 会监听8710端口后面所有工具注册和触达请求都走这个端口。本地调试时我习惯用uvicorn这类工具跑 HTTP 健康检查确认服务活着再继续往下接。3.2 注册第一个业务工具还是用库存查询做例子。除了 handler 装饰器你还需要把 handler 注册到服务里。完整代码如下from agent_reach import ReachServer, handler from agent_reach.schema import json_schema handler( namewarehouse.query_stock, description按SKU查询某仓库实时库存返回可售库存与锁定数量, input_schemajson_schema({ sku: {type: string}, warehouse: {type: string, default: WH-01} }) ) def query_stock(sku: str, warehouse: str WH-01) - dict: return { sku: sku, warehouse: warehouse, available: 86, locked: 2 } server ReachServer(config_fileagent_reach.yaml) server.register(query_stock) server.start()启动后你可以用命令行先手动测一次触达确认链路通不通curl -X POST http://127.0.0.1:8710/reach \ -H Content-Type: application/json \ -d { reach_name: warehouse.query_stock, payload: {sku: SKU-10086, warehouse: WH-01}, metadata: {role: dev, trace_id: test-001} }成功时返回{ code: 0, data: {sku: SKU-10086, warehouse: WH-01, available: 86, locked: 2} }我实际跑的时候第一次返回code: 403排查发现是因为配置文件里角色写的是dev而请求 metadata 里的 role 写的是testACL 不匹配。把请求里的 role 改成dev后就好了。所以当你看到权限类报错第一反应应该是看角色名而不是怀疑代码逻辑。3.3 把 Agent-Reach 接到 LLM 上注册好工具之后下一步是让模型知道有哪些工具能调。我用的方法是把 Agent-Reach 里注册的工具 schema 导出来转成 OpenAI function calling 的格式再喂给模型。from agent_reach import ReachClient client ReachClient(endpointhttp://127.0.0.1:8710) available_tools client.list_available_tools(roledev) tools [tool.to_openai_schema() for tool in available_tools]to_openai_schema会自动把 Agent-Reach 的 input schema 改成 OpenAI 需要的parameters格式。这一步很关键不要手写工具一多手写肯定出错。然后正常请求模型from openai import OpenAI llm OpenAI() resp llm.chat.completions.create( modelgpt-4o, messages[ {role: user, content: 查一下 SKU-10086 在主仓还有多少库存} ], toolstools, tool_choiceauto )模型会返回一个 tool call结构里包含function.name和function.arguments。接下来要做的是把这个 tool call 转成 Agent-Reach 的触达请求import json tool_call resp.choices[0].message.tool_calls[0] reach_name tool_call.function.name payload json.loads(tool_call.function.arguments) result client.invoke( reach_namereach_name, payloadpayload, metadata{role: dev, trace_id: llm-001} ) print(result.data)这里有一个细节arguments可能包含多余的空格、缺省字段或者模型自己脑补的字段。Agent-Reach 在做触达前会先按注册时的 schema 做一次校验多余字段不会直接透传给 handler而是被剥离缺的字段如果 schema 里有默认值就补上。这个小机制帮我挡掉了很多模型“自由发挥”的脏数据。3.4 配置多 Agent 协作路由单 Agent 通了以后我很快加了一个日历 Agent 和一个邮件 Agent。每个 Agent 侧都跑一个 Agent-Reach 客户端向同一个注册中心注册自己的触达能力。Agent A用户代理的配置agent: name: user_proxy role: user_proxy routes: - pattern: calendar.* target: cal_agent - pattern: mail.* target: mail_agentAgent B日历代理注册handler( namecalendar.create_event, description创建日程事件返回事件ID, input_schema{ title: {type: string}, start_time: {type: string, format: date-time}, end_time: {type: string, format: date-time}, attendees: {type: array, items: {type: string}} } ) def create_event(**kwargs): # 调用日历服务 return {event_id: evt_20250112_001}然后注册到中心server.register(create_event)我测试时让用户代理说“帮我约一个明天早上10点到11点的会邀请 A、B 两个同事并且新建一个日程”。模型先判断需要调用日历 Agent于是触达calendar.create_event路由看到calendar.*前缀直接转发给日历 Agent。日历 Agent 处理完后返回 event_id用户代理再把这个结果汇总给用户。这整个过程对用户代理来说和调一个普通工具没有差别。它不需要知道日历 Agent 的内部实现也不需要处理两个服务之间的鉴权路由和会话上下文帮它把底层细节全封装掉了。3.5 关键参数调优超时、重试与限流接入跑通后我花了大量时间调参数。调参不是拍脑袋每选一个值都要有依据。先说超时。工具可以分为快工具和慢工具。快工具比如查询库存、校验用户通常 200ms 内返回慢工具比如发送邮件、调用大模型生成报告可能要 5 秒以上。如果统一设一个 3 秒超时慢工具必挂统一设 10 秒模型侧等待太久下游会认为链路卡住。我的做法是分开配reach: handlers: warehouse.query_stock: timeout_ms: 1500 mail.send_mail: timeout_ms: 5000 calendar.create_event: timeout_ms: 3000再说重试。触达失败有很多种原因但不适合全部重试。只有网络抖动、上游瞬时 5xx、数据库连接超时这类错误适合重试如果是参数校验失败、权限拒绝、业务规则冲突重试多少次都没用。我在 Agent-Reach 里把异常分成RetryableError和NonRetryableError只有前者才会走重试逻辑。retry: max_attempts: 2 backoff_base_ms: 200 backoff_factor: 2重试次数不要设太高否则本来是一个慢查询最后因为重试把整个链路拖垮。实测下来最大重试 2 次比较合适第 1 次 200ms 后重试第 2 次 400ms 后重试再失败就交给模型判断让模型换个方式处理。最后是限流。多 Agent 同时触达同一个工具经常会把下游服务打爆。限流我建议在 Agent-Reach 这层做而不是每个 Agent 各自做。因为 Agent 数量一多各自为政很难控制总量。最简单的限流是令牌桶rate_limit: warehouse.query_stock: capacity: 50 refill_per_second: 20capacity是桶容量refill_per_second是每秒补充的令牌数。超过这个速度的请求直接返回“限流”错误Agent 收到后会主动放慢节奏。4. 高频问题与排查技巧实录4.1 “找不到工具”多数是命名空间在捣鬼我自己最常遇到的是 Agent 说“工具不存在”但明明代码里已经注册了。一开始以为注册中心坏了后来发现是命名空间不一致。比如注册的是warehouse.query_stock模型调用时却生成warehouse.queryStock大小写不一致导致路由匹配失败。解决办法是两件事。第一注册时统一用小写字母和下划线并在 handler 描述里明确写出“reach name”的完整拼写降低模型猜测成本。第二在路由层加归一化逻辑。Agent-Reach 默认会把触达名转成小写再把连续多个下划线合并成一个这样可以减少一些低级错误。如果你查日志发现确实是命名问题不要只改代码还要考虑是不是模型已经缓存了旧 schema。我们遇到过一次更新 handler schema 后客户端本地缓存里还是旧版模型一直按旧字段传参。后来强制给描述加了一个版本号字段每次升级 schema 时版本号递增才彻底解决。4.2 超时导致 Agent 开始胡编数据第二个高频问题是“工具明明很慢Agent 就开始瞎编”。有一次我设了 1000ms 超时上游库存服务要 1.5 秒才能返回。模型等不到真实结果就把一个想象中的结果当答案告诉用户。这是 Agent 系统里最危险的行为因为“有回复”不等于“回复正确”。排查时通过 traceId 发现触达请求在 Agent-Reach 侧已经超时终止但 LLM 接收到的是超时错误。模型看到错误后没有选择“告诉用户稍后重试”而是“猜一个数字说出来”。这说明需要在 Agent 的 system prompt 里加一条硬规则当工具返回超时、限流、非重试错误时必须明确告诉用户“暂时无法获取实时数据”不得自行编造结果。同时把工具的默认超时调成 3000ms 以上并且区分快慢工具。不要用一个全局超时值解决所有问题否则快工具等太久慢工具永远不够。4.3 权限拦截Denied 日志刷屏别慌权限配置齐全后你会看到大量DENIED日志。刚开始会慌后来发现大部分是“预期内的拒绝”。模型在没有充分把握时会尝试多个工具有些尝试本来就属于越权拒绝是正确行为。真正需要警惕的是两种异常 deny一是请求角色和配置文件对不上二是规则匹配顺序出了问题。我的排查办法是把 ACL 规则的匹配过程输出到 debug 日志里每次 deny 都记录命中了哪条规则、因为哪个字段被拒。这样不用猜直接看日志。有一次某工具需要ops_agent角色才能调但用户代理 Agent 用的角色是user_proxy按规则应该拒绝。模型反复试了三次都失败最后还是用户代理判断“无法完成这个任务”才停下来。说明 Agent 比我们想象中更容易放弃。这时要检查的其实是角色设计是不是该给用户代理补充一个ops_agent的临时角色而不是去改 ACL 把权限放开。4.4 多 Agent 互相提问消息像滚雪球多 Agent 场景我踩过最大的坑是循环调用。A 让 B 查数据B 觉得 C 更合适C 又回头来找 A 确认。如果系统里没有深度控制消息会在几秒内爆炸。日志里看到触达请求的数量成倍增长时九成是循环触达。我在 Agent-Reach 里给每条触达链加了一个上下文计数器每次触达时max_depth减 1。减到 0 时直接返回“触达不到链路过深”。同时记录一个trace_id所有嵌套触达都复用同一个 traceId方便追踪完整链路。另外还要注意“同一条消息被重复处理”的问题。Agent 在循环中可能收到多条内容相同的触达如果下游没有幂等处理数据库会被重复写入。建议在 Agent-Reach 的会话模块里加入基于session_id reach_name payload_hash的去重。同一个会话里完全相同的一次触达在 5 分钟内最多执行一次这是我后来加上的保命设计。4.5 排查速查表先看日志再查路由我把常见问题整理成了一张速查表团队里的同学直接用这张表排查效率高不少。症状可能原因第一顺位检查点Agent 提示“找不到工具”reach name 拼写不一致查注册中心列表核对完整触达名触达超时超时设置过短或上游慢看 trace 里触达耗时区分快慢工具返回错误但 Agent 还接着编缺少“不得编造”的系统规则检查 system prompt增加兜底提示Denied 日志多角色不匹配、规则顺序错误开启 ACL debug 日志看匹配详情多 Agent 消息暴涨循环触达、缺少深度控制查 trace 里嵌套层数设置 max_depth数据重复写入缺少幂等去重启用 session 级别 payload 去重这些方案看着不复杂但每一个都是踩坑踩出来的。如果你在接入 Agent-Reach 时也碰到类似现象建议先按表里的“第一顺位检查点”查一般很快能定位。5. 踩坑后的实操心得5.1 先打通最小链路再加权限我最后悔的一件事就是最开始把权限配置写得太满结果 demo 阶段被各种 deny 卡住花了两天时间排查才发现只是一个角色名写错了。如果重来一遍我会先关掉 ACL只保留default_policy: allow跑通一个完整触达链路确认路由和 schema 没问题之后再把默认策略改成 deny逐步加规则。对新手来说最小的可用闭环比一上来就考虑生产级安全更重要。5.2 所有触达请求都要带 traceId这句话以前觉得是老生常谈直到自己排查了上百次问题才意识到它是第一生产力。Agent-Reach 的每次触达都强制要求带 traceId我们会把 traceId 和 sessionId、用户 ID 绑在一起。排查时只要把 traceId 往日志中心一贴从模型调用到工具执行的所有过程全部摊开问题定位速度提升了不止一倍。任何新的接入方如果不传 traceId我会直接拒绝。5.3 后续扩展方向Agent-Reach 目前只是解决了“触达”这一层后面我还想继续做的方向有两个第一个是触达流控的自动化根据下游服务的实时负载动态调整并发和限流参数而不是靠手动配置。第二个是让触达协议支持非 JSON 的二进制数据比如文件、图片、音视频因为很多业务工具的返回其实是文件对象目前还得靠外部存储中转。如果你也在做 Agent 或者准备上多 Agent 架构我真的建议花点时间把“触达层”单独抽出来。不要让每个 Agent 都自己拿着 HTTP 客户端满天飞那样修起来太痛了。有个工具像 Agent-Reach 一样把路由、权限和观测收口你会轻松很多。
返回列表