
1. 为什么我会做 Agent-Reach模型有智商但 Agent 没有手脚先从我最近半年最深的体感说起。市面上各种 Agent 框架层出不穷大模型的能力也确实在突飞猛进但真正把 Agent 落到生产环境里跑起来的人大概率都遇到过同一个尴尬模型在对话里上知天文下知地理可真让它去查个订单、调个接口、写个工单它就原地踏步。问题不在模型的脑力而在 Agent 的触达力——能不能稳定、安全、可追溯地触达外部工具和数据。我做的这个 Agent-Reach就是把触达这件事从业务代码里抽出来做成一个独立的服务层。它的定位很明确作为大模型和外部工具之间的中间层负责工具注册、意图路由、参数映射、权限校验和调用审计。你可以把它理解成给 Agent 配了一个总机——模型说人话总机负责接通正确的分机并记录每一次通话。这个方案不是某个商业产品的推广而是我在实际项目里沉淀下来的一套自建框架适合正在做 Agent 应用落地、内部 Copilot、或者想把 LLM 接入业务系统的开发者参考。1.1 先看看没有这层时Agent 是怎么被架空的多数人第一次做 Agent 工具调用走的都是硬编码一条路。模型输出一个意图代码里 if-else 去匹配然后手动调 API。这个小项目能跑通但一旦工具数量超过三五个问题就像雨后春笋一样冒出来。我归纳了四个最典型的问题。第一工具注册散乱。有的工具描述写在系统提示词里有的写在代码常量里有的干脆只有开发者的脑子记得。模型对工具的理解完全取决于提示词里那几行字写得不准确它就胡猜。第二调用路径脆弱。每个工具都是独立函数参数怎么传、错误怎么处理、超时多久全靠每个开发者自由发挥。武的工具A超时5秒工具B超时30秒模型等得怀疑人生。第三权限完全失控。一旦 Agent 能调用工具就等同于把系统操作权交给了模型。如果没有一层统一的校验任何越权操作都可能发生。第四链路不可追踪。我见过太多团队排查 Agent 问题时毫无头绪只能一边看模型日志一边猜到底是意图错了、参数错了、还是下游接口挂了全靠翻聊天记录。这四个问题单拎出来任何一个都不致命但合在一起Agent 就变得既不可靠又不可维护。1.2 Agent-Reach 要解决的四个核心问题Agent-Reach 在设计之初就是冲着这四个痛点去的。我把它们压缩成四个能力目标触达标准化、触达受控化、触达可观测化、触达可恢复化。触达标准化意味着所有工具用同一套 Schema 描述统一的注册、发现、调用流程。Agent 不需要知道工具背后的实现细节只需要知道怎么描述需求。触达受控化指每一次工具调用都要经过权限校验而且这个校验不是粗粒度的能或不能而是结合用户、会话、操作内容做精细化判断。触达可观测化是让每一次调用从意图解析到参数映射再到结果返回都有结构化日志可查。触达可恢复化是在下游失败时系统能自动重试、快速失败或者优雅降级而不是让 Agent 卡死在一个超时等待里。这四个目标听起来宏大但它们并不依赖什么黑科技。核心就是一个设计良好的中间层 一套严格约定的协议。1.3 为什么我不直接用 Function Calling 或 MCP 就够了我知道这里一定会有人问现在各家平台不是都有 Function Calling 吗不是还有 MCP 这种标准协议吗自己再造轮子图什么我的看法是Function Calling 只解决模型知道该调哪个函数这一步它不解决工具管理的完整生命周期。你依然需要自己写参数校验、错误处理、权限控制。而 MCP 解决的是工具调用协议的统一让 Agent 能通过标准接口发现和调用远程工具。但 MCP 不包含业务路由策略不包含你企业内部的人员权限体系也不包含调用审计和治理策略。Agent-Reach 的做法是站在两者之上把 Function Calling 当作意图识别引擎来用把 MCP 当作一种工具接入方式然后自己补上路由、治理、观测这三层。也就是说它不是替代品而是承接层。如果你已经有 MCP Server 在跑Agent-Reach 完全可以直接把 MCP 工具当作后端工具注册进来只是前面加了一道自己的管控。2. Agent-Reach 的整体设计把触达拆成四层服务在设计 Agent-Reach 的时候我给自己定了一个原则每一层只干一件事层与层之间通过清晰的接口通信不许跨层调用。整体架构我按职责拆成了四层——接入层、路由层、执行层和记录层。每层内部可以替换实现但对外接口保持稳定。2.1 四层架构的职责边界接入层负责接收来自 LLM 或用户的请求解析出这次触达的意图上下文。简单说它就是 Agent 与业务系统之间的适配器。路由层负责把意图映射到具体工具做参数对齐这是最核心的一层也是我花了最多时间调优的地方。执行层负责真正发起调用统一管理超时、重试、幂等和并发。记录层则把所有调用过程沉淀为结构化的审计日志和指标数据。每一层的边界用接口约定死。比如路由层只负责输出一个标准化的调用意图对象至于这个意图是不是合法、参数是不是合理由执行层去校验。这样做的好处是每一层都能独立测试、独立部署。我后来在生产环境里把接入层从 OpenAI 兼容协议换成自研协议时完全没动其他层这省了大力气。2.2 工具描述一份 Tool Card 说清所有事要让路由层可靠工作前提是每个工具都有完整、机器可读的描述。我参考了 MCP 和 OpenAI Function Calling 的 Schema 规范但做了一些更工程化的扩展最终定下一份叫 Tool Card 的结构。每一张 Tool Card 描述一个工具的全部信息。字段包括工具的唯一 ID、显示名称、功能描述、输入参数 Schema、输出结果 Schema、调用方式HTTP/MCP/本地函数、权限需求标签、SLA 指标和幂等键规则。其中最重要的是我给每个工具增加了正向触发条件和负向不触发条件两段自然语言描述。这个细节是后来解决模型选错工具的杀招后面我会展开说。{ tool_id: tool.weather.current, name: 查询当前天气, description: 根据城市名返回当前天气实况包括温度、湿度、风力等, positive_triggers: [查天气, 今天多少度, 下雨吗, 气温, Weather now], negative_triggers: [历史天气, 预报, 过去三天, 明天, 周末天气], input_schema: { type: object, required: [city], properties: { city: {type: string, description: 城市中文名如 北京}, unit: {enum: [celsius, fahrenheit], default: celsius} } }, output_schema: { type: object, properties: { temp: {type: number}, humidity: {type: number}, wind: {type: string} } }, call: {protocol: http, url: https://api.internal.example.com/v1/weather}, permission_tags: [public.read], sla: {timeout_ms: 3000, max_retries: 1} }这段 JSON 看起来简单但每一块都有讲究。描述字段决定了模型能否正确匹配工具正负触发条件是用来兜底模型输出格式异常时的规则引擎匹配权限标签则对应后面的权限模型。一份好的 Tool Card基本决定了工具被正确调用的概率上限。2.3 意图路由从用户说了什么到Agent 该做什么路由层是整个 Agent-Reach 的引擎。我在实现里采用了LLM 为主、规则为辅、关键词兜底的三级路由策略。主路由先走 LLM。把当前请求和全部 Tool Card 的描述、触发器组装成一个路由请求要求模型输出结构化的 ToolCall 结果。这一步和 Function Calling 类似但我在提示词里加了一个关键约束如果没有任何工具能匹配当前请求模型必须返回 no_op 标记而不是硬凑一个结果。次路由是规则引擎。当 LLM 返回的置信度低或者模型接口超时的时候我通过正向触发词做一次倒排匹配快速定位候选工具。第三级才是关键词精确匹配这是纯工程手段应对极端情况下的确定性需求。三级路由的切换逻辑是自适应的LLM 可用且响应快时永远优先 LLM一旦 LLM 连续两次返回 no_op 但规则引擎匹配到高命中工具系统会自动降级到规则引擎并把前置信息写入上下文后续请求直接走规则通道。这个设计让系统的整体响应时间从平均 1.2 秒降到了 400 毫秒左右。值得一提的是路由层的输出不是简单的工具ID参数二元组而是一个完整的调用意图对象包含工具ID、目标参数、安全上下文、调用链路ID和路由决策原因。这个对象的格式固定执行层和记录层都依赖它。3. 从零搭建 Agent-Reach完整实操与核心代码说完了设计下面进入动手环节。我以一个最简单的场景为例让 Agent 能查询实时天气、创建待办事项、读取本地时间。三工具足够说明问题又不会让代码淹没在琐碎里。整套代码我用了 Python 3.11 FastAPI选型原因很简单FastAPI 的异步支持好pydantic 做参数校验天然顺手部署也不折腾。3.1 环境初始化与工程结构建议直接用 uv 管理依赖比 pip 干净得多。我这里初始化了一个最小工程目录结构按 Agent-Reach 的四层来分agent-reach/ ├── pyproject.toml ├── reach/ │ ├── __init__.py │ ├── model.py # ToolCard 数据模型 │ ├── registry.py # 工具注册表 │ ├── router.py # 意图路由层 │ ├── executor.py # 执行层 │ ├── recorder.py # 审计记录层 │ └── server.py # FastAPI 接入层核心依赖只有三个fastapi、uvicorn、openai。如果你接的是 MCP 工具再加一个 mcp 的 SDK 就行。之所以刻意保持轻依赖是因为 Agent-Reach 本身不做模型推理它只是把模型和工具粘起来依赖越少越容易嵌入不同项目。3.2 第一步实现 ToolCard 注册表模型定义我直接用 pydantic它就是天然的 Schema 描述器。这里有个细节input_schema 和 output_schema 我刻意不解析成结构化类而是保留为原始 dict只做必要校验。原因是我要暂时把外部工具当作不透明黑盒只有路由层需要理解参数执行层只负责透传。过度建模反而会让注册流程变得复杂。from dataclasses import dataclass, field from typing import Callable, Any, Dict, Optional import time class ToolCard(BaseModel): tool_id: str name: str description: str positive_triggers: List[str] [] negative_triggers: List[str] [] input_schema: Dict[str, Any] output_schema: Dict[str, Any] {} permission_tags: List[str] [] call: Dict[str, Any] {} sla: Dict[str, Any] {timeout_ms: 3000, max_retries: 1} class ToolRegistry: def __init__(self): self._tools: Dict[str, ToolCallable] {} self._cards: Dict[str, ToolCard] {} def register(self, card: ToolCard, handler: Callable): self._cards[card.tool_id] card self._tools[card.tool_id] handler return card.tool_id registry ToolRegistry()注册表的核心就是两个字典一张存描述信息一张存实际调用函数。为什么分开因为路由的时候只读卡片不需要加载函数执行的时候才取函数。这在工具数量多的时候能显著减少内存占用和路由阶段的上下文体积。3.3 第二步实现路由逻辑路由层接收用户请求输出调用意图。这一层我用 OpenAI 的 Function Calling 接口但改成了自定义格式。关键点在构造路由上下文把注册表里所有 Tool Card 压缩成模型输入同时给一个强约束——不要臆造参数不清楚就返回 no_op。def route_request(user_input: str, user_context: dict) - Intent: tool_schemas [] for card in registry.list_cards(): tool_schemas.append({ type: function, function: { name: card.tool_id, description: f{card.description} || 正向: {card.positive_triggers} || 负向: {card.negative_triggers}, parameters: card.input_schema } }) response client.chat.completions.create( modelgpt-4o-mini, messages[ {role: system, content: 你是一个工具路由助手。只能从工具列表中选择一个工具。 如果用户请求不匹配任何工具输出 no_op。 参数必须从用户输入中提取不允许臆造。}, {role: user, content: user_input} ], toolstool_schemas, tool_choiceauto ) # 解析 ToolCall ...我在提示词里塞入正负触发词这个做法初看有点冗余但实测下来效果很好。模型在面对两个功能相似的工具时往往靠描述区分不开而明确的什么时候不选我能显著降低错选率。我做过一组对比实验只有描述的准确率是 78%加上正负触发词后提升到 93%。3.4 第三步执行层设计和并发防护执行层是真正发起外部调用的地方也是生产事故的多发区。核心做三件事超时控制、幂等去重、错误分类。超时控制我不用 requests 默认值而是在 httpx 里显式设置 timeout。每个工具的 SLA 从 Tool Card 里读取动态适配。幂等键是必选项——如果 Agent 重试时重复创建待办事项用户会疯掉的。所以每次路由产生的意图对象都带一个 call_id 作为幂等键下游接口支持就去重不支持的至少我们能在执行层拦截同一 call_id 的并发执行。错误分类则把下游异常分为可重试、不可重试、需降级三类。超时和 5xx 是可重试的按照 SLA 重试一次4xx 是不可重试的直接返回错误给模型熔断状态下的服务则直接走降级调用一个备用逻辑。async def execute(intent: Intent) - ExecutionResult: card registry.get_card(intent.tool_id) timeout_ms card.sla.get(timeout_ms, 3000) retries card.sla.get(max_retries, 1) async with httpx.AsyncClient(timeouttimeout_ms / 1000) as client: payload { call_id: intent.call_id, params: intent.params, user_context: intent.user_context } for attempt in range(retries 1): try: resp await client.post(card.call[url], jsonpayload) if resp.status_code 300: return ExecutionResult.success(resp.json()) if resp.status_code 500: continue return ExecutionResult.failed(resp.text) except httpx.TimeoutException: if attempt retries: return ExecutionResult.timeout(f超时 {timeout_ms}ms)这里还有一个值得讲的小细节payload 里除了业务参数我还固定塞入了 user_context 和 call_id。因为很多内部系统的接口是有权限校验的执行层透传用户上下文可以避免下游再反查一次登录态算是一个隐性优化。3.5 完整链路联调所有层写完以后一条从用户请求到工具返回的完整链路长成这样。假设用户说帮我看看北京现在多少度。第一步接入层接收请求把用户ID、会话ID、请求文本打包成路由输入。第二步路由层调用 LLM 匹配到 tool.weather.current提取参数北京生成 Intent 对象同时关联一个 call_id。第三步执行层检查权限标签 public.read 通过发起 HTTP 调用得到天气数据。第四步记录层写入一条审计记录谁、何时、调了什么工具、参数是什么、结果是什么、耗时多久。我特意在记录层花的功夫不少因为这直接关系到你之后排查问题的速度。每一条审计日志都包含路由决策原因比如LLM 匹配置信度 0.92或者规则引擎兜底关键词 天气。当模型行为不对时你看一眼决策原因就知道是模型选错了还是规则写错了不用再盲猜。4. 生产环境踩坑实录我遇到的问题和排查方法系统上线之后测试环境没出现的问题全都来了。我挑四个最有代表性的记录下来每一个都让人头大但每一个也都推动了 Agent-Reach 的迭代。4.1 工具描述写得太简单模型总是选错工具上线第一周用户反馈最多的是我说查昨天天气它却给我调了实时天气接口。排查审计日志发现路由层正确识别了意图是查历史但错误选择了 tool.weather.current。原因很简单我当时 Tool Card 的描述只写了返回天气正负触发器里只列了正向词没列负向词。模型面对昨天天气这种包含天气关键词但实际意图不同的请求时被实时天气工具的描述带跑了。解法就是我前面提到的 negative_triggers。我给每个工具都补了一段什么场景别选我同时在路由提示词里新增一句严格检查负向条件。改完之后错选率从 22% 降到了 7%。现在我的经验是每写一个 Tool Card至少花 30% 的时间在负向条件的打磨上这比正向描述更重要。4.2 下游服务超时Agent 卡死在等待里第二个事故更严重某个下游供应商 API 高峰期响应要 20 秒而我的执行层超时设了 10 秒。看起来是正常的超时保护但问题在于超时后我直接把错误抛给了模型模型又不擅长处理错误于是一遍遍重试把下游服务压得更死。排查后发现核心矛盾是我把超时设成了固定值而实际上不同请求的复杂度差异巨大。后来我改成动态超时路由层在生成 Intent 时根据参数复杂度估算期望耗时执行层用估算值的 1.5 倍作为超时上限。同时增加了熔断机制——当某个下游连续失败超过 5 次直接标记为熔断状态不再调用Agent 收到降级结果后告知用户稍后再试。这条经验帮我理解了Agent 的工具调用不能只做成功路径的保障失败路径的设计同样重要。甚至可以说失败路径设计得好不好决定了生产系统能不能撑住高峰期。4.3 权限模型一开始设计得太细结果没人愿意用我把权限模型做得极其精细每个工具、每个操作、每个用户组都单独控制。结果上线两周权限配置人员累得半死业务方抱怨审批流程太慢最终很多团队干脆绕过了系统直接在代码里把权限校验注释掉。这个教训比较深刻。后来我把权限模型简化成两级角色模板 例外清单。每个岗位对应一组预置的工具集合默认全使用特殊情况才加例外例外必须走审批。简化之后新员工入职默认就能用基础工具权限配置工作量下降了 80%合规率反而提高了。核心体会是安全设计要平衡业务效率。权限过严导致业务绕行的代价远大于偶尔的越权风险排查成本。4.4 常见问题速查表我把生产里高频问题整理成一张速查表方便你直接对照排查。症状根因排查思路标准解法模型频繁选错工具Tool Card 描述不完整查看审计日志的 route_reason 字段补充 negative_triggers优化描述调用下游超时严重固定超时兜不住慢请求检查执行层的 timeout 配置和下游 P99 耗时改为动态超时 熔断机制Agent 重复创建数据重试没有幂等控制检查两次调用的 call_id 是否一致执行层统一生成幂等键权限审批积压权限粒度太细统计审批耗时和绕过率改角色模板 例外清单路由 LLM 接口抖动依赖单一模型提供方监控路由层 P95 耗时与错误率增加规则引擎兜底通道5. 我的一些后续打算和体会Agent-Reach 从设计到现在迭代了四个月最深的体会是Agent 落地的瓶颈往往不在模型智商而在工程韧性。模型可以快速迭代但工具调用的路由稳定性、权限管控和可观测性是任何智能都得老老实实补齐的地基。后续我想在这个方向上做三件扩展。第一件是把工具触达扩展到数据触达让 Agent 能通过 Agent-Reach 安全地查询多维数据分析接口而不是只能调用事务型工具。第二件是加入主动触达能力——现在的 Agent-Reach 更像一个应答式总机用户问才答下一步我想让 Agent 具备定时触发和事件监听能力主动向用户推送有价值的触达结果。第三件是把审计日志做产品化直接生成可交付的安全合规报表让企业安全团队不用再追着开发要日志。最后再分享一个小技巧。如果你第一次自己搭这套东西别急着追求大而全的权限模型和复杂路由策略。先拿两三个高频工具把链路跑通记录一周的真实调用日志再根据日志反推该怎么设计路由描述、该设多少超时。日志会替你把坑标出来你只需要跟着它填。这套方法我从第一版 Agent-Reach 用到现在每次迭代都靠它兜底。