ARTICLE DETAIL

资讯详情

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

构建智能体触达层:从工具注册到执行网关的Agent-Reach实践

构建智能体触达层:从工具注册到执行网关的Agent-Reach实践 相信不少做 AI 应用的朋友都有过这种经历明明是同一个模型别人家的能老老实实订会议室、查库存、发工单自己调教出来的却一聊就卡壳要么答非所问要么“知道了”半天却没下文。我前阵子就在这个问题上栽了不少跟头折腾来折腾去最后所有经验都沉淀到一个叫 Agent-Reach 的东西上。Agent-Reach 不是一个新模型也不是什么神秘框架。一句话概括它是给 AI Agent 装上“手脚”的触达层解决的是智能体“脑子想得到、身体够不着”的经典问题。大模型再厉害本质也只是一个文字接龙机器它知道要调接口、要查数据库、要发通知但如果没人帮它把“想法”翻译成真正能执行的动作那它就永远停留在“纸上谈兵”。Agent-Reach 做的事情就是在这层认知和行动之间搭一座桥——既让模型说人话也让系统听得懂人话并真正把事办了。这篇文章我准备把我搭建 Agent-Reach 过程中的设计思路、核心机制、关键代码和踩坑记录完整复盘一遍。不管你是刚接触 Agent 开发的后端工程师还是已经在业务系统里尝试接入智能体的产品技术负责人这套东西应该都能给你不少可复用的参考。1. 核心思路与设计拆解1.1 认知与执行之间的鸿沟要理解 Agent-Reach 的价值得先搞清楚一个前提大模型到底缺什么。大模型只能做“预测”不能做“执行”。模型会输出一串看起来像函数调用的文本但它本身并不会真的去请求 HTTP 接口也不会真的写入数据库。模型不了解系统细节。就算模型知道“该查一下这个用户的订单”它也不知道订单服务的地址是什么、鉴权 token 怎么放、返回的数据结构长什么样。模型无法感知执行结果是否正确。调接口超时了、参数被拒了、返回了 500 错误模型如果没有外部反馈它自己是意识不到的。如果把这些事情全部塞给模型去处理那模型就会陷入无休止的过度泛化。你得在提示词里把 API 文档、鉴权规则、错误码表全写进去上下文窗口直接爆掉而且模型表现极不稳定。Agent-Reach 的核心设计原则就是把“决策”和“执行”分离。模型负责决策Aent-Reach 负责执行这是整个框架的基石。1.2 用“触达层”而不是“插件列表”来解决问题市面上很多 Agent 框架都喜欢用“插件”这个概念你把一堆工具塞给模型让模型自己挑。听起来没错但实际跑起来你会发现问题很多。插件一般是静态注册的模型看到的工具描述常常是过时的。插件往往只解决了“调用”的问题没解决“链路”的问题。一个真实的业务动作往往要连续调用多个系统涉及多个依赖关系插件如果只是独立堆砌模型很容易在步骤衔接上错乱。插件没有统一的“反馈回路”模型调完也不知道结果到底成没成。Agent-Reach 换了个思路把问题抽象成一个个“能力端点”由一层独立的运行时统一托管。模型向 Agent-Reach 发出意图Agent-Reach 解析意图、匹配能力、执行调用、返回结果。这里的关键是所有对系统的访问能力都集中在触达层模型不直接面对乱七八糟的底层系统它只面对一层清晰、统一、可控的接口。1.3 Agent-Reach 的整体架构选型我最初是参考了不少主流开源框架的架构但最终决定做成轻量级的三段式结构意图接收层接收模型输出的结构化指令做基础校验。能力调度层维护一份“能力清单”把意图映射到具体的执行器。系统对接层封装具体的 API 调用、密钥管理、超时策略、错误归类。这个三段式的最大好处是每一层都能独立演进。比如系统这一侧换了供应商只改对接层不影响上层模型调用模型换了个版本大概率只影响意图接收层的解析方式不需要动执行逻辑。2. Agent-Reach 的核心机制解析与实操要点2.1 能力注册表的设计Agent-Reach 最核心的数据结构是“能力注册表”。你可以把它理解成一张服务目录每个条目描述一个智能体可以执行的动作。具体字段我建议至少包含name能力名称唯一标识尽量用动词名词结构比如“get_user_order”“create_work_order”。description给模型看的自然语言描述要写清楚这个能力做什么、在什么场景下触发、有没有前置条件。parameters参数列表包含参数名、类型、是否必填、取值范围、示例值这些信息是给模型提供“填写参考”的所以要写得越具体越好。permissions权限标记标清楚这个能力需要什么样的访问级别防止模型在错误场景下调用敏感操作。executor真正执行函数的引用或者一个指向对接层服务的路由键。这个注册表本身不需要多聪明但写法很有讲究。最常被忽略的是 description 字段的质量。我测试过很多次同样一个查库存的接口描述写成“查一下库存”和写成“当用户询问某商品是否有现货或可发货时间时查询该商品在当前仓库的实时库存数量并返回剩余量”模型选对工具的概率完全不一样。描述要覆盖触发条件和意图边界这是 Agent 精准度的第一道防线。2.2 统一的执行网关有了注册表还不够关键得有一个不让模型乱来的执行网关。我给 Agent-Reach 设计的执行网关做了三件事参数校验与缺省值补齐。模型传来的参数经常丢三落四比如查询条件带了个不存在的状态值。网关这里不能直接傻乎乎地透传到后端接口必须先做一次 Schema 校验把必填项缺了就直接拒绝把明显越界的值拦截下来。权限拦校。有些能力虽然模型能调用但某些上下文里不应该调用。比如在“闲聊”模式下模型说“顺便帮我删一下那个用户的订单”这种高风险调用就必须被网关拦住。我用的方式是给能力打上风险等级网关在调度时做规则匹配。调用轨迹记录。网关会对每一次执行生成一条 trace包含模型原始意图、解析结果、入参、出参、耗时、错误信息。这个 trace 极其重要后面排查问题全靠它。2.3 模型交互的安全边界这里要特别提一个我踩过坑的点模型输出的结构化指令绝对不能直接当成可信代码来执行。LLM 本身是一个概率模型它会一本正经地编造参数。我遇到过模型在调用报表接口时自己编了一个时间参数“2025年2月30日”传进去之后下游系统直接报错但模型还以为自己成功了。所以 Agent-Reach 在意图层必须加一道“边界检查”时间范围、枚举值、金额大小这种一眼就能看出不合理的参数直接拉回来让模型重新决策。3. 实操过程与核心环节实现这章我用一段实际跑通过的案例来拆解让 Agent 完成一次“库存查询 低库存预警通知”的完整任务。先说明下面所有代码都是最简化演示核心逻辑取自 Agent-Reach 真实实现但去掉了无关的中间件。3.1 环境准备依赖被我压到了最少建议你也这样起步Python 3.10我用的是 3.11FastAPI做网络回调用可换 Flaskopenai SDK 或任意兼容 OpenAI 协议的语言模型 SDK安装命令很简单顺手贴一下pip install fastapi uvicorn openai pydantic3.2 定义能力注册与执行器先看能力注册的基本数据结构我直接用 Pydantic 来定义JSON Schema 方便后续做参数校验。from pydantic import BaseModel, Field from typing import Dict, Any, Callable, Awaitable, List, Optional class ToolSchema(BaseModel): name: str Field(descriptiontool unique name) description: str Field(descriptiontool description for LLM) parameters: Dict[str, Any] Field(descriptionJSON schema of parameters) required: List[str] Field(descriptionrequired parameter names) call: Optional[Callable[..., Any]] Field(defaultNone, excludeTrue)注册的时候你只需要把工具的可调用对象传进去。我封装了一个简单的注册装饰器_registry: Dict[str, ToolSchema] {} def register_tool(schema: ToolSchema): def wrapper(func): _registry[schema.name] schema.copy(update{call: func}) return func return wrapper3.3 编写实际执行函数库存查询和预警通知这两个工具看起来简单但里面会暴露出很多执行层该注意的细节。先看库存查询。真实系统里查库存往往要跨多仓我这里简化了但保留了“库存量与阈值比较”这个关键动作。register_tool(ToolSchema( namequery_inventory, descriptionQuery the real-time inventory level of a product by SKU code. Use it when user asks about stock status., parameters{ sku: {type: string, description: the product SKU code, e.g. SKU-10086}, }, required[sku] )) def query_inventory(sku: str): # 实际项目中这里会调用库存服务HTTP API inventory_map {SKU-10086: {warehouse: SH-01, quantity: 12, threshold: 20}} inv inventory_map.get(sku) if not inv: return {found: False, message: fSKU {sku} not found} result { found: True, sku: sku, quantity: inv[quantity], threshold: inv[threshold], low_stock: inv[quantity] inv[threshold], } return result这里要把“低库存”的判断放在执行器里而不是模型那边非常关键。执行结果要把状态算得明明白白模型拿到的都是已完成判断的事实而不是原始数字让模型自己猜。这会降低模型误读的风险。再看预警通知。执行器里必须处理的是幂等、去重这些事这些也是从 Agent-Reach 踩坑中提炼出来的。register_tool(ToolSchema( namesend_low_stock_alert, descriptionSend an low-stock alert notification to the inventory manager via internal IM., parameters{ sku_list: {type: array, items: {type: string}, description: list of SKUs that need alert}, reason: {type: string, description: short reason or summary for the alert}, }, required[sku_list, reason] )) def send_low_stock_alert(sku_list: list[str], reason: str): alert_id hashlib.md5(f{sorted(sku_list)}|{reason}.encode()).hexdigest() # 实际项目里这里会调用IM机器人的Webhook并记录alert_id到Redis做幂等 return {status: sent, alert_id: alert_id, target: stock_manager_group}幂等这点一定不能偷懒。Agent 有时候会因为上游超时而重试同一个动作如果没有幂等机制意味着你可能会连发一打重复的告警给仓库负责人。我在实际项目里吃过这个亏后来所有写操作都要求执行器返回一个幂等键。3.4 模型意图解析与工具选择Agent-Reach 不搞花活直接用了 Function Calling 的方式。主要理由如下支持 Function Calling 的模型输出结构比裸文本稳定太多而且天生就是 JSON。结构化输出里带了明确的调用意图网关解析成本低也不用摸不着头脑地去猜模型想干嘛。下面是核心的调度函数它接收用户文本组装好 Tools 描述请求模型然后解析返回的 tool_calls。这里要注意一次模型返回可能包含多个 tool call我做过最多一次让模型一口气串了四个工具调用流水线效果还可以。def run_agent(user_input: str, max_rounds: int 5): messages [{role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}] openai_tools build_openai_tools() for _ in range(max_rounds): resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolsopenai_tools, tool_choiceauto, ) msg resp.choices[0].message if not msg.tool_calls: return msg.content messages.append(msg.model_dump()) for tc in msg.tool_calls: tool_name tc.function.name args json.loads(tc.function.arguments or {}) # 边界校验在这里执行 result dispatch_tool(tool_name, args) messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(result, ensure_asciiFalse), }) return max rounds exceeded注意上面把 result 用 json.dumps 序列化塞回助手消息里这一环是给模型“看到结果”用的。如果你执行器返回的是 Python 对象必须先序列化否则 OpenAI SDK 会直接报错。3.5 冷启动“请给我示例”的智能体系统提示词设计系统提示词这一层往往决定成败。Agent-Reach 的系统提示词我按照“角色 规则 限制 示例”四段式写。贴一段比较有代表性的供你参考你是企业内部运营助手。你的任务是理解用户的真实需求并严格通过工具完成操作。 规则 1. 只有需要真实数据或真实动作时才调用工具。 2. 所有参数必须来自用户表达或上下文推导禁止编造没有依据的参数。 3. 工具返回结果后必须对用户给出简洁的结果反馈不要重复展示中间信息。 4. 当工具返回错误时必须如实告知用户不得尝试用已有数据编造成功结果。 限制 - 你只能查询库存数据无权修改任何账户信息。 - 不清楚的问题直接向用户索要必要字段不要猜测。这套提示词的逻辑很简单把规则说死给模型划定边界别让它自由发挥。我对比过不加限制的版本同样一个“帮我看看这批货还够不够”的问题自由的模型能回答出“缺货 8 件建议发紧急订补”这种没有依据的结论限制之后的版本则会老老实实调接口查实数据再说。3.6 一次完整任务的跑通实录用“SKU-10086 现在还够吗如果低于 20 件帮我发个补货提醒”这句话做基准输入跑一次完整链路。第一步Agent-Reach 收到用户话术后组装 tools 并请求模型。模型返回如下意图[ { name: query_inventory, arguments: {sku: SKU-10086} } ]第二步执行网关验证参数没问题调用 query_inventory得到{ found: true, sku: SKU-10086, quantity: 12, threshold: 20, low_stock: true }第三步这个结果回填给模型模型看到 low_stock 为 true于是继续生成第二个动作[ { name: send_low_stock_alert, arguments: {sku_list: [SKU-10086], reason: 当前库存12低于阈值20} } ]第四步执行 send_low_stock_alert 返回成功最后模型生成给用户的总结消息“SKU-10086 当前库存只有 12 件已低于设定的 20 件阈值我已经把补货提醒发给库存负责人了。”到这里流程就结束了多轮循环的进出都非常干净。如果你想复现得更稳建议把上面这次调用的完整 trace 打开仔细看每轮消息的进出。Model 第一次返回如果没有 tool_call直接转到普通回答如果有多个 tool_call要按顺序执行再把每个结果分别按 tool_call_id 回流不要串。4. 常见问题与排查技巧实录这部分全部来自真实线上调试的踩坑记录我按问题频次排序。4.1 模型明显选错了工具怎么办大多数原因是工具描述没写清楚。比如你把查询工具的名称写成了“query_stock”但模型对“库存”这个词更敏感于是选了别的工具。解决方式是把常见说法都揉进 description比如“别名库存查询/余量查询/现货查询。当用户询问有没有货、还剩几件时使用。”这是治本的办法。还有一层思路是“少就是多”注册表里工具数量控制在 10 个左右。工具一多模型选择准确率会断崖式下跌。如果你业务工具超过 20 个建议根据场景分成多个 Agent每个 Agent 只挂当前场景相关的能力不要贪心全塞。4.2 模型传了非法参数执行器报了错但模型还嘴硬这种情况在海外模型的 age、date 参数上尤其常见。年份写错了、枚举值造了一个不存在的、SKU 大小写搞混了都是高频问题。我会在网关这一层做规则校验比如所有 date 类型参数强制用 datetime 解析解析失败直接返回明确错误“date format invalid, expected YYYY-MM-DD”。所有枚举值参数校验词典不在列表里直接拒绝。所有数值参数检查上下界。关键是错误返回给模型时要附带正确的参数格式示例。模型看到错误示例后下一轮基本能自我纠正。我实测下来这类情况有 80% 以上能在第二轮纠回正确输入。4.3 工具调用链路长中途失败了怎么办Agent 执行往往是多步链路比如先查订单、再查物流、再发拦截指令。哪一步失败了不能简单把所有状态都丢掉重来。Agent-Reach 的做法是在执行网关里为每一步生成“事件”一旦某一步失败立即向模型返回失败的上下文并让模型补一个函数调用或者直接终止链路返回给用户。这里有一个非常重要的实践一旦某个写操作已经执行成功即使后链路失败也不要让模型“假装没发生过”。状态必须由执行层维护不能让模型自己拿主意。这也是 Agent-Reach 不为了“看起来聪明”而牺牲一致性的原因。4.4 上下文膨胀工具调用几轮之后模型“失忆”模型上下文里除了用户消息还要塞很多工具描述。如果工具集合大再加上前面多轮调用的结果都完整回流很容易把上下文挤爆模型反而忽略掉早轮的消息。我目前比较成熟的做法是只保留最近两轮的工具调用结果更早的结果做摘要存到一个 field 里喂给模型。工具描述不每次全量带按意图粗分类只带相关的一个子集。检查 token 消耗一旦接近模型上限触发“压缩对话”流程让模型汇总当前关键状态后再继续。4.5 错误返回不统一模型理解困难不同执行器返回的错误风格五花八门有的返回字符串有的返回字典有的直接抛出异常。模型面对这种混乱很容易迷糊。Agent-Reach 对执行器返回值做了强制统一错误就长下面这样{error: {type: PERMISSION_DENIED, message: insufficient scope for deleting order, friendly: 订单删除权限不足请联系管理员}}统一的错误结构让模型只需要识别 error.type 和 error.friendly 就能判断要做什么。而且结构里必须带 friendly 字段这是直接给用户读的消息模型在最终回复时应该原样引用或润色。这样既省了模型重新组织语言的成本又让错误对用户友好。5. 我的一些额外体会Agent-Reach 这套东西做到现在我最大的感受是智能体应用能不能跑起来从来不取决于模型有多聪明而在于你愿不愿意在工程细节上较真。模型选错工具背后是描述不到位参数乱传背后是缺校验链路崩了背后是没有状态恢复机制。这些事看起来一个比一个朴素但把它们串成一个闭环之后Agent 才真正从“聊天玩具”变成了“能办事的系统”。如果你正准备在自己的业务里接一个智能体我建议别一上来就追最新的大模型和花哨框架。先用 Agent-Reach 这种思路把工具注册表建好把执行网关写好把错误规范定好用一条最简单的链路跑通用户请求。跑通之后再去扩能力、加模型、调提示词。你会发现后面的扩展其实就是一个又一个“注册一个工具 写一段描述 加一个校验规则”的重复劳动毫无玄学可言。最后再分享一个细节给工具起名时别用太抽象的词。我早期起过一个叫 “general_inquiry_handler” 的工具结果模型动不动就把所有问题都导向它真正该走的专用工具反而被冷落。后来把名字改成 “query_user_balance” 这种直抒胸臆的结构准确率立刻上来了。工具名就是给模型看的“第一印象”这块千万别懒。
返回列表