资讯动态

Agent-Reach:给LLM Agent补上触达中间件

发布时间:2026/10/26 16:03:54 来源:尧图企业网站定制
写 Agent 这件事真正让我头疼的从来不是对话流而是触达。最近半年我一直在折腾一个内部项目名字就叫 Agent-Reach含义很直白让大模型驱动的智能体真正够得着它该够得着的东西——数据库、工单系统、监控接口、知识库甚至是另一个 Agent。做 Agent 工程化的人都明白写一个会聊天的 Agent 不难难的是让它调工具的时候不出乱子。工具注册混乱、鉴权缺失、一个慢接口把整个对话拖死这些都算轻的。Agent-Reach 就是我把这些坑一个个趟平之后沉淀下来的一层触达中间件。如果你正在做多工具调用或者准备把 Agent 接进内部业务系统这篇文章里的思路、代码和踩坑记录应该能帮你省下几个月的试错时间。1. Agent-Reach 原点Agent 的会说与会做之间隔着一层 Reach1.1 从会说到会做工具调用为什么是分水岭到今天为止大模型的能力边界已经不再取决于它能说得多漂亮而是取决于它能做得多靠谱。一个只能输出文字的 Agent 说白了就是个高级聊天框一旦接上工具调用Function Calling它才真正开始拥有行动力可以查订单、发工单、改配置、调报表。工具调用的原理本身并不神秘。模型在生成过程中会额外输出一个结构化的调用意图比如调用 search_knowledge_base参数 query报销流程然后由我们这些工程人员去解析这个意图、执行对应函数、把结果塞回上下文让模型继续推理。这套链路听起来简单但一旦工具数量从三五个涨到三五十个问题就接踵而至。我在项目早期就吃过这个亏。最开始只是给 Agent 挂了四五个 Python 函数一切都很美好。后来为了让它能查监控、查日志、查审批、查排班工具列表膨胀到了将近三十个真正的问题开始浮出水面模型开始混淆同名函数、编造不存在的参数、调用了超出权限范围的接口而且没人知道它每一次调用到底干了什么。那段时间我每天都在它怎么又乱调和它为什么该调的没调之间反复横跳。1.2 裸调 API 的三个死法每个都是真实踩过的死法一工具注册混乱模型幻觉调用不存在的工具。早期我把工具定义散落在各个业务代码里有的用 JSON Schema 写有的直接写在系统提示词里。模型一旦没在上下文中看到完整的工具清单就会自己脑补一个函数名出来然后代码侧抛出一个冷冰冰的FunctionNotFoundError整个 Agent 循环直接中断。死法二权限裸奔一个 API Key 走天下。最早我为了省事给 Agent 配了一个超级管理员的 Key什么都调得通。结果有一次测试我故意让它执行把生产环境的日志级别改成 DEBUG它真的去改了。那次之后我意识到Agent 触达得越多权限失控的风险就越大一个万能 Key 等于把整个系统的后门交给了模型的随机性。死法三没有超时和熔断一个慢接口拖死整轮对话。Agent 的推理是串行的每调用一个工具都要等它返回才能继续生成。我曾经接的一个报表接口平均响应时间 40 秒Agent 调了三次每次都在那干等最后直接把用户的会话超时干爆了。用户反馈这个 AI 半天憋不出一句话体验极差。1.3 Agent-Reach 的定位补上触达这一层经历了一轮又一轮的翻车之后我把所有问题归结到一个点上Agent 缺少一个统一的触达层。这个层要在模型和真实世界之间做翻译、过滤、鉴权、限流、审计。Agent-Reach 就是干这个的只不过我把它的边界划得很清楚第一它不做业务逻辑所有工具的具体实现仍然下沉在业务系统里 第二它做的是统一出入口所有工具调用都必须经过它来路由 第三它负责回答四个问题这个 Agent 是谁、它想调用什么、它有没有权限调用、这次调用是否应该被记录。我在内部给自己定了一个可量化的目标叫触达成功率成功完成的工具调用数除以模型发起的总工具调用数。 Agent-Reach 上线之前我的项目触达成功率大概只有 68%每次失败都要靠人工在日志里捞原因。上线稳定之后这个数字基本稳定在 95% 以上剩下的 5% 大多数是模型参数解析错误而不是底层触达问题。2. Agent-Reach 的三件套Gateway、Registry、Policy Engine2.1 Reach Gateway所有工具调用的统一出入口整个 Agent-Reach 最核心的组件是一个叫 Reach Gateway 的服务。它的作用和电信网络里的信令网关很像Agent 侧不用关心工具部署在哪里、用什么协议暴露只要往 Gateway 丢一个标准格式的请求就行。我选型时用了 Python 3.10 FastAPI Pydantic。原因不复杂团队本来就有 Python 技术栈Pydantic 对 JSON Schema 的天然支持能省掉大量胶水代码。Gateway 对外暴露一个统一的 HTTP 接口POST /v1/reach请求体长这样{ req_id: b7f3c2a1-xxx, agent_id: ops-assistant-v2, user_id: zhang.li, tool_name: search_knowledge_base, arguments: { query: 报销流程, top_k: 5 } }响应体统一为{ req_id: b7f3c2a1-xxx, status: success, output: { hits: [], total: 0 }, duration_ms: 1234, permission: granted }这套协议让我在做后续的插件扩展时轻松很多因为所有工具调用都在一个地方留下了痕迹不存在某个工具藏在某个服务里偷偷被调用的情况。2.2 Tool Registry让函数变成模型能够看见的工具Gateway 自己不知道有哪些工具可调它需要问 Tool Registry 要一份工具目录。Registry 的职责是管理所有的工具定义并把它们以模型能理解的方式暴露出来。每个工具在注册时都要提供一份完整的 Schema包括工具名称、功能描述、参数定义、超时时间、所需权限范围、限流大小。我最看重的是描述这一项的质量因为 LLM 是通过描述来决定何时调用工具的。举个实际的教训。早期我把一个工具描述写成处理数据结果模型在用户问帮我把明天的排期表整理一下的时候莫名其妙调用了这个处理数据工具因为它觉得整理就是处理。后来我把描述改成从排班系统获取指定日期范围内所有人员的值班安排返回列表格式不执行任何写入操作模型就基本不会再误用了。工具描述写得越像给一个刚入职的实习生看的需求说明模型的调用准确率就越高。Registry 对外提供的核心接口很朴素返回所有工具定义的 JSON Schema 列表。这个列表会被拼进系统提示词或者通过 function calling 的 tools 参数传给模型。这样模型在生成回复时会基于当前对话上下文从可见的工具列表里挑选最匹配的一个来调用。2.3 Policy Engine触达权限的第一道闸门Policy Engine 是整个 Agent-Reach 里最安全向的组件也是我认为最不能偷懒的部分。它的核心思想是工具不直接对 Agent 开放权限而是通过 Scope权限域来间接管理。我给每种工具声明一个 scope比如kb.read、incident.write、config.change然后给每个 Agent 分配一组 scope 集合。当 Agent 发起工具调用时Policy Engine 执行一次匹配该工具声明的 scope 是否在 Agent 的 scope 集合里。在的话放行不在的话直接拒绝。这个设计和 Linux 的权限模型有点类似普通用户不能随便改系统配置除非通过 sudo 临时提权。对应到 Agent 场景一个只读的运维助手默认只拥有monitor.read、log.read这类 scope它就算真的产生了我要去改配置的意图也会被 Policy Engine 无情拦下。我建议所有做 Agent 接入的人尽早把权限模型装上哪怕工具只有五个。因为 Agent 的调用行为具备级联放大效应一次调用失败它可能会尝试换一个参数再调被拒绝一次它可能换一个语义更模糊的工具名再试。没有权限闸门这种试错就是在搞破坏的边缘疯狂试探。3. 从零搭建一个最小可用的 Reach 层能跑多快3.1 技术选型与目录结构如果用一句话概括我当时的选型思路就是哪个东西能让我最快跑起来、并且之后不至于推翻重来就选哪个。最终我敲定了下面的组合并保持至今技术栈FastAPI 作为基础框架、Pydantic 做数据校验、Redis 做工具注册表的缓存、SQLite 做审计日志存储。日志量上来后可以考虑换 PostgreSQL。此处无内容此处无内容此处无内容此处无内容此处无内容此处无内容此处无内容整个目录结构非常简单agent-reach/ ├── gateway.py # Reach Gateway 主服务 ├── registry.py # 工具注册与发现 ├── policy.py # 权限判断引擎 ├── tools/ # 具体的工具实现 │ ├── __init__.py │ ├── knowledge_base.py │ └── monitor.py ├── schemas.py # 数据模型定义 └── audit.py # 审计日志写入第一版从动手写代码到跑通第一个工具调用用了一个下午。不是因为代码量少是因为边界切得足够清楚Gateway 只做路由Registry 只做注册Policy 只做判断。3.2 核心数据结构ToolSchema 与 ReachRequest工具注册的数据结构是根它直接决定了后续做权限、做限流、做超时控制的成本。我用 Pydantic 定义了一套简化的 ToolSchemafrom typing import Any, Callable, Dict, Optional from pydantic import BaseModel, Field class ToolSchema(BaseModel): name: str Field(description工具唯一名称使用点分命名如 kb.search) description: str Field(description给模型看的工具功能描述要具体到行为边界) parameters: Dict[str, Any] Field(descriptionJSON Schema 格式的参数定义) timeout_ms: int Field(default15000, description单次调用的最大等待时间) scope: str Field(description该工具所需的权限域如 kb.read) rate_limit: int Field(default60, description每分钟最大调用次数)ReachRequest 则保持精简只携带调用方身份、目标工具、参数和请求追踪 ID。一个额外的原则任何涉及写入的工具必须在请求体里带上require_confirmation: bool供上层决定是否需要人工确认。3.3 工具注册与动态发现的核心实现工具注册我用了一个装饰器实现简洁而且侵入性小# registry.py _TOOL_SCHEMAS: Dict[str, ToolSchema] {} _TOOL_FUNCTIONS: Dict[str, Callable] {} def register_tool(schema: ToolSchema): def decorator(func: Callable): _TOOL_SCHEMAS[schema.name] schema _TOOL_FUNCTIONS[schema.name] func return func return decorator def list_tool_schemas() - list[ToolSchema]: return list(_TOOL_SCHEMAS.values()) def get_tool_function(name: str) - Callable: return _TOOL_FUNCTIONS[name]在实际的工具文件中使用方式是这样的# tools/knowledge_base.py from registry import register_tool from schemas import ToolSchema from typing import Any, Dict register_tool( ToolSchema( namekb.search, description在内部知识库中搜索与关键词相关的文档片段返回标题、摘要和原文链接, parameters{ type: object, properties: { query: {type: string, description: 搜索关键词可以包含多个短语}, top_k: {type: integer, description: 返回条数, default: 5} }, required: [query] }, timeout_ms20000, scopekb.read, rate_limit120 ) ) def search_knowledge_base(query: str, top_k: int 5, **kwargs) - Dict[str, Any]: # 这里实际调用企业内部的知识库检索服务 return {hits: [], total: 0}模型侧的发现完全依赖list_tool_schemas()的输出。我在 Gateway 启动时把全量工具 Schema 加载一次并缓存到 Redis模型请求进入时直接读取不需要每次查库。这样做的额外好处是新上线一个工具只需要注册进 Registry 并刷新缓存Agent 下一次会话就能看见它完全不需要改 Agent 的代码。3.4 护栏参数超时、重试、熔断的工程参数这里分享一组我压测之后定下来的护栏参数可以直接抄作业护栏项参数值设置依据单工具超时默认 15s写操作放宽到 30s超过 30s 的工具调用多数是业务本身的问题不应该拖垮对话重试策略最多重试 1 次且仅限只读工具重试写操作会造成重复执行幂等没做好的情况下风险极高熔断条件连续失败 5 次进入半开状态暂停该工具 30 秒给下游服务恢复留出时间避免 Agent 疯狂重试一个坏掉的接口单 Agent 并发最多 3 个并发工具调用防止 Agent 面对多个子任务时把下游系统打爆尤其是数据库类工具有一个参数我花了两周才调明白工具调用并发数。一开始我允许 Agent 并行调用 8 个工具结果下游某系统的连接池被打满间接导致了一个线上报表服务超时。后来我把并发限制调整到 3丢掉的只是几秒钟的并行时间换来的是下游系统的稳定。这个取舍我认为非常划算。4. 权限与审计给 Agent 的触达边界提前画好红线4.1 最小权限原则在 Agent 场景下的变体传统软件里的最小权限原则是给每个程序只开它需要的那几个权限但到了 Agent 场景这件事有了新的麻烦Agent 自己也不知道它会走哪条调用路径。同一个对话里用户上一句问查一下这个月工单量下一句可能是把这个工单指派给我——那就是写操作。所以在 Agent-Reach 里我把最小权限拆成两层来落地。第一层是 Agent 的静态权限池决定了它能触碰的范围上限第二层是工具自身的动态校验某些涉及跨用户数据访问的调用还需要额外校验操作者与数据属主的关系。比如一个工单查询工具除了 scope 判断之外我会在函数内部再加一道数据级校验用户传进来的assignee_id如果和当前user_id对应的身份不一致直接返回无权限查看该工单。这样就算 Agent 被诱导着传入了别人的 ID也会被挡在数据层之外。4.2 审计日志该记录哪些字段很多团队做 Agent 审计的时候只记录它调了什么这远远不够。我现在的审计日志至少包含下列字段字段示例为什么非记不可req_idb7f3c2a1-xxx贯穿全链路排查问题的唯一线索agent_id / user_idops-assistant-v2 / zhang.li定位谁通过哪个 Agent 发起的tool_name 与参数摘要kb.search / query报销流程敏感参数做脱敏后再落库比如密码、令牌只记录前 4 位调用结果与耗时success / 1234ms排查性能问题和上下文膨胀的核心依据模型消耗 token 数1评估 Agent 调用成本防止失控循环权限判断结果granted / denied分析恶意或误用调用请求的来源有的团队会质疑参数摘要脱敏以后没法复现问题了。我的做法是保留一份高权限的可查询原始日志但审计系统默认只读脱敏版本两边通过 req_id 关联。既保证了排查能力又不会让敏感参数在普通日志系统里裸奔。4.3 Human-in-the-loop高危操作必须有人确认有些工具调用不能让 Agent 自己拍板尤其是写操作。我在 Gateway 里加了一个确认队列的机制当 Policy Engine 判定某个请求属于高影响操作比如修改配置删除数据发送消息给外部用户Gateway 不会直接调用工具而是返回一个特殊状态给 Agent等待用户确认。Agent 收到这个状态后会向用户展示一个确认卡片等用户点击确认后真正触发工具调用。整个过程在协议层强制而不是靠提示词约束。这一点很重要如果你只是告诉模型你执行删除前要先问用户它大概率会在某些模糊场景下忘了这条规则直接把删库行为当成普通操作执行掉。协议层强制模型就不可能出现违反规则的随机性。5. 跑通之后的真实复盘三个落地场景和一堆坑5.1 场景一企业内部知识库检索自动化第一个真实落地的场景是把 Agent-Reach 接到企业的内部知识库。Agent 面对的工具有三个kb.search、kb.get_article、wiki.recent_changes。全部是只读操作权限模型最简单。实测下来的提升很明显以前人工翻知识库找一条流程说明平均要 6 分钟Agent 端到端返回带引用的答案只要 40 秒左右。而且通过审计日志我们能看到用户最终点击了哪些答案里的链接反推出来 Agent 的检索质量。最深的感触是工具返回的格式要稳定否则 Agent 的后续推理会被带偏。5.2 场景二运维工单自动诊断第二个场景接入的是运维工单自动诊断。Agent 可以查询监控指标、检索最近日志、读取告警规则然后根据这些信息输出可能的根因和建议动作。在权限设计上这组工具占据了monitor.read、log.read两个 scope另外一个触发自动化修复的工具则单独要求ops.action权限并且强制走人工确认流程。上线初期踩的最大一个坑是日志检索的超时。日志系统查询的响应时间波动极大平时 300 毫秒高峰期秒级返回偶尔直接 30 秒超时。最终我把它单独设置了 20 秒超时 只读重试 1 次并且把结果截断到前 100 条避免模型被几万条日志塞满上下文失去判断力。5.3 场景三对外信息聚合与日常报告生成第三个场景是让 Agent 每天定时聚合多个只读数据源生成一份简报。整个过程涉及大约 10 个工具调用Agent 需要自己编排顺序先拉基础数据再拉对比数据最后生成总结。这里我学到的最有价值的一件事是不要试图让 Agent 一个多轮循环内完成全部工作而是把它拆成多个子任务每个子任务有独立的工具调用上下文。我最终实现了一个简单的编排规则Agent 每完成一次阶段性调用后会生成一个结构化的中间结果下一个阶段的工具调用只用引用这个结果而不是把全部原始数据带进上下文。这套阶段性摘要的思路让长任务稳定了不少。5.4 踩坑实录上下文膨胀、幂等性与工具幻觉上下文膨胀是最普遍的坑。工具返回结果动辄几千字Agent 一轮对话只能消化有限的信息。我的解决办法是两层截断工具端先做业务截断比如只返回 top 10 条网关再做格式截断把超过 2000 字的内容自动压成摘要并附上需要查看完整内容请调用 get_article 并传入以下 ID。这样模型既拿到了足够推理的信息又不会淹没在原始文本里。幂等性是写工具绕不开的问题。有一次我们测试创建告警规则这个工具Agent 在一次对话里因为网络重试调用了两次同一个接口直接生成了两条相同的告警规则。教训就是所有会写入的工具请求参数里必须携带幂等键业务侧依据幂等键做去重。我在 ToolSchema 里增加了可选字段idempotent_key并在 Gateway 层强制写入操作的请求生成该键。工具幻觉则是需要持续压制的无论 Registry 怎么完善LLM 还是偶尔会生成一个不存在的工具名。我的兜底方案是 Gateway 在工具不存在的响应里返回相近工具列表比如你想调用的 kb.search_doc 不存在你是不是想用 kb.search让模型有机会自我修正而不是直接断掉整轮对话。6. 下一步路线和给新手的几句实在话6.1 和 MCP 标准的关系不冲突反而互补聊 Agent 工程避不开 MCP 这个标准协议。我的观点很明确Agent-Reach 不等于 MCP它们是两个维度的东西。MCP 解决的是工具提供商和 Agent 之间怎么对话的协议标准问题而 Agent-Reach 解决的是这些工具调用怎么被管理、怎么被授权、怎么被审计的工程治理问题。最典型的落地组合是底层接各种 MCP Server 作为工具实现上层由 Agent-Reach 负责统一注册、鉴权和审计。也就是说MCP 负责把工具翻译成统一格式Agent-Reach 负责决定谁可以用、怎么用、用了要被记录。这个组合在后续做外部工具生态接入时会很有优势因为第三方工具只需提供 MCP 描述Agent-Reach 不用关心工具内部实现。6.2 给准备做 Agent 工程化的朋友几句实在话第一别急着上复杂框架先用几十行代码把自己的 Gateway 跑通把调用—鉴权—审计的最小闭环建立起来再考虑扩展。第二工具描述值得花时间和业务方一起打磨描述质量直接决定模型的调用准确率这个投入的性价比极高。第三权限模型从第一天就设计好后面再补的代价比你想的大得多尤其是涉及写操作的 Agent。第四必须把审计日志当成核心功能来做而不是事后补的辅助功能否则出问题的时候你连问题源头都定位不到。第五不要让 Agent 一次调用返回超大数据永远给结果加上限和摘要这是保护模型推理质量最有效的手段。Agent-Reach 这套东西我还在继续迭代眼下最想补的是多 Agent 之间的触达路由当一个 Agent 发现自己处理不了某个请求时它能把上下文和意图安全地转交给另一个更专业的 Agent而这中间的权限传递和审计也仍然要落在同一个 Reach 层里。这个方向让我觉得Agent 真正成熟的样子可能就是像今天微服务治理那样每个节点都能被安全、可控、透明地触达。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑