ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Agent-Reach实践:构建大模型触达企业系统的三层架构

Agent-Reach实践:构建大模型触达企业系统的三层架构 Agent-Reach 这个项目名我是从一次内部工具对接的讨论里带出来的。当时我们团队接了一个 OA 系统的自动化改造老板的要求特别简单让业务人员在聊天窗口里直接查订单、催流程、拉报表。听起来就是给大模型接几个 API 嘛但真做起来才发现模型能聊天和能干活完全是两码事。Agent-Reach 这个名字的立意很简单——它解决的不是模型能不能推理的问题而是模型能不能触达真实系统并安全地把事办成的问题。如果你也在做 AI Agent 相关的应用尤其是要把大模型接进企业内部系统这篇文章里的设计思路、实现细节和踩坑记录应该能帮你少走很多弯路。1. 先聊清楚一个事Agent-Reach 到底卡在哪个环节1.1 一个让我意识到问题所在的真实场景事情的开端特别朴素。我们一开始给 OA 系统接大模型方式是常见的 RAG 问答模型只负责读文档、回答报销流程是什么这类问题。上线一周效果还行然后业务方就提了新需求能不能在对话框里直接说帮我查一下上周提交的退款单到哪一步了然后系统直接给结果这一下需求性质就变了。查退款单不是一个问答动作它涉及三个环节第一模型得理解用户意图把查退款单映射到某个具体接口第二系统得拿到用户的身份和权限确认这个人有权利查这些数据第三调用接口拿到结果之后还要把结构化的数据组织成自然语言回复给用户。整个链路里模型只是大脑真正干活的是触达动作。我们当时最缺的就是一套能统一管理这些触达动作的基础设施。1.2 对话能力与任务执行能力之间的差距很多刚接触 Agent 开发的人会有一个误区只要选一个聪明的模型它自己就会调用工具。实际上模型的本领边界非常清晰——它擅长的是文本生成和推理但调用工具这件事模型本身并不天然具备。你得先告诉它有哪些工具、每个工具接收什么参数、什么时候该调用哪个工具然后它才能在你的引导下做出选择。这些告诉的过程就是 Agent-Reach 这类触达层框架要解决的事。再往深一层说对话型应用和任务型 Agent 的核心差异在于有状态和无状态。纯问答是无状态的用户问一句模型答一句上下文丢了也无所谓。但任务型 Agent 是有状态的用户说帮我查退款单系统先查询列表用户接着问那第一单为什么被驳回这个时候 Agent 得记得第一单指的是列表里的哪一项。状态管理、工具调度的上下文衔接、结果回填这些都不是模型本身能搞定的而是需要一层专门的中枢来编排。Agent-Reach 最开始的项目定位就是这个中枢。所以我很建议所有准备做 Agent 应用的朋友先别急着选模型、调 prompt。先把触达层想清楚你的 Agent 要接触哪些系统这些系统之间的调用关系是怎样的谁来负责权限校验如果是靠写死代码把几十个接口糊在一起短期能跑长期一定崩。下面我把 Agent-Reach 的三层设计拆开讲这是我落地之后觉得最值得参考的部分。2. Agent-Reach 的主体设计触达、发现、执行三层分离2.1 触达层把不同模型的 function calling 差异抹平第一次做 Agent 的时候我天真地以为只要选一个厂家的大模型后面就只用对着它开发即可。实际上业务场景里经常要替换模型或者同时用多个模型做不同任务。不同模型的 function calling 格式差异很大有的要求严格的 JSON Schema有的支持自然语言描述工具用途有的在并行调用多个工具时行为完全不同。如果把这些差异散落在业务代码里每换一次模型就要改一遍全链路代价很高。触达层的职责就是把这种差异收敛掉。我们做了一套统一的工具描述格式把每个工具定义成 id、名称、描述、参数 Schema、返回结构五个字段。模型侧只需要关心这五个字段底层再针对不同模型厂家的 API 做适配转换。打个比方这就像电脑的 USB 接口不同设备有自己的协议和电压需求但 USB 接口把所有差异都隐藏了插上就能用。触达层就是这个 USB 接口让业务系统不用关心模型是哪家的。2.2 发现层工具注册表的设计逻辑发现层解决的是模型怎么知道有哪些工具可以用的问题。刚开始我就踩过一个坑把所有工具的描述全部塞到系统提示词里结果上下文被撑爆模型的选择准确率反而下降。后来我把工具信息抽出来做成了注册表按需注入效果立刻不一样。这个注册表的核心字段如下工具 ID 全局唯一用于调用记录追踪。名称要短便于模型理解例如 get_refund_order 就比 queryRefundOrderDetailByIdV2 好理解得多。描述要包含触发条件、适用场景和典型用户指令的语义表达这直接影响模型选工具的准确率。参数 Schema 用标准 JSON Schema 定义模型按此生成参数。返回结构要说明返回的字段和格式方便模型组织语言。权限元数据是可选的默认空闲。注册表真正起作用的机制是语义检索。用户输入帮我查退款单时系统先把这句话向量化从注册表里召回最相关的 5 到 8 个工具再把这几个工具的 Schema 注入到模型请求中。这比把所有工具都塞给模型有两个好处省 token同时减少干扰项。实际落地后工具选择的准确率从 82% 提升到了 95% 左右。2.3 执行层会话编排与任务分发执行层是整个 Agent-Reach 里最容易被低估的部分。模型选好了工具、生成好了参数接下来要做的事包括调用外部接口、处理超时和重试、把结果回传给模型、维护会话状态。这些事看起来琐碎但每一个细节都可能让整个 Agent 崩掉。我设计的执行层核心是一个会话状态机状态流转如下会话启动后进入意图解析阶段Agent 判断用户想干什么如果无法判断则主动澄清。随后进入工具选择阶段执行层根据注册表召回候选工具。然后是参数填充阶段模型根据用户输入和工具 Schema 生成参数如果是语料缺失的场景则需要追问补全参数。接着进入执行阶段调用工具触达外部系统。最后是结果组织阶段模型把结构化数据转成用户能看懂的自然语言。这个状态机的关键设计在于每个阶段都允许用户干预。比如参数填充时用户说算了不查了状态机应该能优雅退出而不是继续强行调用工具。多轮对话里用户可能中途修改条件例如先让 Agent 查退款单又说还是查昨天的吧状态机要把修改后的参数重新绑定到同一个工具调用上而不是新开一个任务。这种编排逻辑写起来不复杂但要想清楚边界避免状态错乱。3. 把 Agent-Reach 跑起来的关键实现细节3.1 工具注册表从普通函数到可发现资产的改造如果你已有现成的内部 API把它接入 Agent-Reach 并不是写一个 Python 函数那么简单。API 的参数可能来自请求头、查询参数、请求体返回结果可能是嵌套 JSON还可能涉及分页。要让模型能顺利调用每个 API 都要被重新包装成模型友好的形式。我建议的包装方式是写一个适配器层agent_reach_tool( nameget_refund_order, description根据退款单号查询退款进度。当用户询问退款、退单状态时使用。, params_schema{ type: object, properties: { refund_id: {type: string, description: 退款单号如 RF20250101}, include_detail: {type: boolean, description: 是否返回明细默认 false} }, required: [refund_id] }, return_schema{ type: object, properties: { status: {type: string}, current_node: {type: string}, history: {type: array} } } ) def get_refund_order(refund_id: str, include_detail: bool False): # 这里调用真实的内部 API resp http_client.get(f/api/refund/{refund_id}) return normalize(resp.json())装饰器做的事是把函数注册进工具表同时把参数 Schema 转成模型的 prompt 片段。这里最值得注意的细节是描述字段的写法对模型选工具的影响非常大。我试过查询退款单状态和根据退款单号查询退款进度适用于用户咨询退款到哪一步、是否通过、被驳回原因等场景可附带明细开关这两种写法后者的工具选择准确率明显更高。模型是靠语义匹配选择工具的描述越接近用户的真实表达方式选得越准。3.2 会话状态管理记忆、压缩、过期多轮对话的 Agent 很容易出现上下文失控的问题。一开始我把所有历史消息都塞给模型几个来回之后 token 就上去了而且模型会被旧信息干扰。后来我把 Agent-Reach 的会话管理分成了三个层级。工作记忆层保存最近 1 到 2 轮完整的对话原文直接注入模型保证衔接准确。摘要记忆层对更早的历史做摘要用一个专门的轻量模型递归压缩把用户要求查退款单 → 查询结果正常→用户追问第一单驳回原因这样的过程提炼为一句话。关键事件层则记录所有工具调用记录包括输入、输出、耗时、状态这些信息不直接给模型而是用于审计和问题排查。记忆过期策略也要讲究。用户的会话如果 10 分钟无操作我建议直接把工作记忆清空只保留摘要记忆避免用户回来时上下文错乱。长期无操作的会话直接归档用 session_id 恢复时会提示您可以重新描述需求。这套策略执行下来上下文 token 占用减少了约 40%同时用户的追问体验并没有明显下降。3.3 并发、超时与熔断触达外部系统不能裸奔工具调用是高风险的。外部接口可能很慢可能直接报错也可能返回的数据格式和预期不一致。如果 Agent 不加保护地反复调用失败接口不仅浪费资源还会让用户对系统的信任度下降。我在执行层里加了三道防线。第一道是超时控制。每个工具调用必须显式声明超时时间默认 5 秒。有些报表生成类接口可能需要 30 秒那就单独调高但要标记为长任务走异步流程避免阻塞整个会话。第二道是重试策略。网络抖动导致的瞬时失败可以重试但重试上限是 2 次间隔采用指数退避1 秒、2 秒。更重要的是要区分可重试错误和不可重试错误。超时、503、连接断开属于可重试权限不足、参数非法、404 这类错误重试多少次都没用直接终止并把错误信息返回给模型让模型向用户解释。第三道是熔断器。如果某个工具在 30 秒内失败率超过 50%直接熔断 2 分钟。熔断期间该工具不再被调度模型会收到该功能暂时不可用的提示。这个设计救了我很多次因为下游系统的故障往往不是单次请求的问题而是持续性的不熔断就会产生雪球效应。3.4 权限与审计最小权限触达不是选项而是底线Agent 触达真实系统权限问题躲不开。一个能让模型自由调用内部接口的框架本身就是极大的安全风险。在 Agent-Reach 里我把权限控制放到了执行层的最前面而不是让模型去判断。每个工具有一个最小角色权限声明调用前执行层会取出当前用户身份验证是否具备权限。权限不足就直接拒绝不给模型任何尝试的机会。这里有个容易踩坑的地方不要在系统提示词里让模型判断权限模型经常会给出模棱两可的结果而权限是二元的必须由确定性的代码来判断。同时每次工具调用都要写审计日志记录谁在什么时间通过哪个会话调用了什么工具传入什么参数。一旦出现数据泄露或越权审计日志就是追查的第一手资料。4. 一个完整的落地案例客服退款查询 Agent4.1 从需求到触达链路的拆解理论讲完拿我们实际做的客服退款查询 Agent 当例子串一遍完整链路。需求来自客服部门用户来电咨询退款时客服希望直接在聊天辅助工具里输入用户提供的退款单号系统自动展示退款状态、当前处理节点、历史流转记录。这个需求可以直接套进 Agent-Reach 的框架里。工具层面需要三个后端接口查询退款单基本信息、查询退款审批流转记录、查询驳回原因详情。会话流程设计成客服输入退款单号 → Agent 解析意图并冻结单号 → 调用基本信息接口 → 如果状态是已驳回再调用驳回原因接口 → 汇总成一段结构化的客服话术。这个案例里有一个关键决策是让模型自由决定调用哪几个接口还是用预设流程我采用了折中方案。当退款单状态是正常退款中时只调用基本信息和流转记录两个接口当状态是已驳回时自动追加调用驳回原因接口。这个状态驱动分支不是让模型现场推理出来的而是由执行层的规则引擎判断的。原因很简单——退款查询这种操作路径固化、出错代价高的场景流程确定性比灵活性更重要。4.2 一条真实触达链路的完整日志放一段脱敏后的链路日志你可以直观感受 Agent-Reach 在背后做了什么[会话 8f3a] 用户输入: 客户说 RF20250301 退款还没到账帮他查一下 [编排] 意图解析 - 查退款进度 [发现] 召回工具: get_refund_order, get_refund_flow, get_refund_reject_reason [调度] 注入工具 Schema 到模型 [模型] 选择工具: get_refund_order, 参数: {refund_id: RF20250301} [执行] 权限校验: 客服组通过 [触达] GET /api/refund/RF20250301 200 OK耗时 312ms [执行] 状态判断: statusREJECTED触发分支 - 追加调用 get_refund_reject_reason [触达] GET /api/refund_reject/RF20250301 200 OK耗时 156ms [模型] 组织话术: 该笔退款已于昨天被驳回原因是发票信息不一致请联系用户重新上传发票。 [会话] 完整回复已返回用户整个过程大概 1.5 秒。如果不用 Agent-Reach这段逻辑要么写成硬编码的 if-else要么让模型自由调用前者没法覆盖新场景后者在权限控制上漏洞百出。这个案例说明Agent-Reach 这类触达层最有价值的地方不是智能而是把智能和真实系统之间的缝隙填平。5. 我在 Agent-Reach 上踩过的坑和最终解法5.1 function calling 参数枚举的坑第一次让模型调用工具时我遇到一个让人非常头疼的问题模型的参数幻觉。工具定义里写着 status 只有 PENDING、PROCESSING、REJECTED、SUCCESS 四种取值模型却在调用时传了一个 in progress。原因很简单模型是根据语义生成参数的它认为 in progress 表达的就是处理中但它不知道后端接口只认枚举值。解决思路是在两层做防护。第一层参数 Schema 里把所有枚举值明确定义并在描述里强调只允许传以下值第二层执行层在调用工具前用 JSON Schema 做严格校验校验不通过就直接返回参数不合法给模型让模型自我修正重新生成。这里要特别提醒不要指望模型一次生成完全合法一定要做校验和重试机制否则任何一个字段的小偏差都会让整个链路中断。5.2 工具返回结果过大撑爆上下文另一个高频问题是外部系统返回的数据太大了。比如退款单的完整流转记录有 80 条全塞给模型既浪费 token又干扰模型组织话术。刚开始我们没做处理一个查询任务能吃掉 8000 多 token成本高不说回复质量还差。最后用了两招解决。第一招是返回结构裁剪工具适配器默认只返回最近 5 条流转记录完整数据放在 details 字段里标注仅在用户明确要求时展示。第二招是结果摘要化对于一些本身就很大的报表数据工具先做一次摘要再返回给模型例如该订单共 40 条操作记录分为 3 个阶段当前处于第二阶段。这两招把平均 token 消耗压到了原来的三分之一而且用户感知几乎没有下降。5.3 外部接口不稳定导致 Agent 反复重试有一次灰度测试下游审批系统出现间歇性超时结果我们的 Agent 像疯了一样对同一个接口连续重试了 8 次每次重试间隔仅 1 秒直接把下游系统的负载又抬高了一截。问题出在我最初的重试逻辑太简单只要超时就重试完全不考虑失败模式。后来我下了三个硬性规定重试上限固定为 2 次一旦超过就立刻把问题抛给用户说明系统繁忙请稍后再试只对幂等接口开重试查询类接口一般幂等但提交审批这种操作类接口绝不自动重试否则会出现重复提交的严重事故任何工具连续失败 3 次就自动熔断熔断期间不再调用该工具并在回复里诚实告知当前功能暂不可用。这几条看着简单但能拦住大多数外部系统故障引起的连环问题。5.4 多个 Agent 同时触达同一个资源时的竞争问题Agent-Reach 后期支持了多个业务 Agent 共用一个工具注册表新的问题又出现了两个 Agent 几乎同时调用同一业务接口修改同一份订单数据造成数据冲突。比如销售 Agent 更新了客户联系方式客服 Agent 紧接着又用旧信息覆盖了一遍。解决方式是在执行层加了资源锁。同一资源 ID 在同一时间只允许一个写操作后续的写操作会排队等待同时对写类工具强制要求带上操作幂等键同一个幂等键的重复请求直接返回之前的结果。这个设计对用户体验的影响微乎其微但对数据安全的意义极大。6. 从 Agent-Reach 继续往前走的一点想法6.1 把触达过程变成可观测的数据资产做到后期我发现 Agent-Reach 更大的价值不在调通而在可观测。每个工具调用都有完整的链路日志从意图识别、工具选择、参数填充、权限校验到最终执行结果。这些日志不仅用于排查问题还可以反哺模型效果。举个例子我们通过日志发现用户提问退款到哪里了时模型经常选错工具选成了查询退款审批流程。原因是两个工具的描述语义边界有重叠。后来我们根据日志反馈把描述改成了更明确的分工表述get_refund_order 负责查状态和到账信息get_refund_flow 负责查审批流转节点。改完之后选对率明显上升。这个优化过程完全依赖可观测数据没有它就只能靠猜。6.2 从单点触达到网状协作的扩展方向最后聊一下扩展。目前的 Agent-Reach 是一个 Agent 触达多个工具的单点模式再往下走就是多 Agent 协作一个主 Agent 拆解任务子 Agent 各自触达不同系统最后汇总结果。这种模式对执行层的要求又高了一个档次子 Agent 之间的通信、任务结果的中转、优先级调度都会成为新的瓶颈。我的建议是无论怎么扩展尽量不要把 Agent 之间的协作做成完全自由的网状结构而是借鉴工作流引擎的思路定义好节点和边。完全自由的多 Agent 协作看起来很美但排错会让人崩溃。Agent-Reach 目前保持的设计原则是流程确定性优先智能灵活性补充。这也是我在多次踩坑之后沉淀下来的核心体会。最后分享一个实际经验做 Agent 项目第一版能用就行但触达层的工具注册、权限校验、审计日志这些骨架一定要在一开始就搭好。这些东西不依赖具体模型也不依赖具体业务后面的所有迭代都是在这个骨架上长肉。如果你现在正准备做 Agent 应用我真心建议先把触达层想明白再谈模型调优和 Prompt 工程。模型总有更聪明的版本但触达层的基础设计决定了你换多少个模型都不会推翻重来。
返回列表