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( name="get_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. 一个完整的落地案例:客服退款查询 Agent
4.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 [执行] 状态判断: status=REJECTED,触发分支 -> 追加调用 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 工程。模型总有更聪明的版本,但触达层的基础设计,决定了你换多少个模型都不会推翻重来。