去年年底我接手了一个有点尴尬的内部项目:公司里跑着的几套大模型应用,个个都能把话说得头头是道,可真要让它们干点实事——比如查一下某个客户的订单状态、给工单系统开一张票、或者从内部数据库里拉一份报表——就全卡壳了。
模型再聪明,够不到业务系统,它就是台昂贵的问答机器。
这个项目后来在公司内部代号叫“Agent-Reach”,名字起得直白:让智能体真正“触达”外部系统。做完之后我最大的感受是——难点根本不在模型能力上,而在工程侧你怎么把“让Agent干活”这件事做得稳定、可控、可排查。
这篇文章把我的完整实现思路、架构拆解、以及上线后踩过的坑都梳理一遍,给正在做同类事情的朋友一个能直接参考的底稿。
1. Agent-Reach 要解决的问题:智能体与业务系统的最后一公里
1.1 大模型的“知识”和“行动”之间隔着一条鸿沟
我们最开始上线的那批Agent应用,本质上都是“套了层壳的GPT”——用户提问,模型回答,过程很丝滑,业务上也挑不出大毛病。但等这套东西要往生产环境深处走的时候,问题立刻暴露了:用户问的是“帮我查一下销售部上个月的报销总额”,模型如果没被提前喂过数据,就只能给一段“建议你登录财务系统查看”的废话。
你当然可以说“那就把数据喂给它”,可财务数据每小时都在变,报销单随时会新增和修改,靠RAG拉一堆文档进去,既慢又不准。
Agent-Reach 的思路完全换了个方向:不让模型记住数据,而是让模型知道“去哪里查数据”。模型负责理解用户意图、拆解任务、决定调用哪个工具,真正去系统里取数、改写、执行动作的,是一套独立的调度和执行层。
1.2 直接写代码调API,为什么不靠谱?
有人可能会问:既然要访问业务系统,我自己写个接口,按需调用不就行了?用得着专门搞一个Reach框架吗?
现实是:当你只有一两个固定场景时,确实用不着。但你一旦要面对的是几十个不同部门的系统、上百个接口、还有随时会变化的需求,直接硬编码的调用逻辑就成了一团乱麻。我们第一版就是这么干的,后来维护成本高到离谱:
- 每个新场景都要改代码、重新部署,业务方等不起那个周期;
- Agent 每次调用都是针对特定接口的“一次性胶水代码”,没法复用;
- 权限基本靠“后端接口鉴权”,但大模型这一层的调用入口是谁?用户有没有权限看这份数据?根本管不到;
- 出问题的时候,你分不清是模型理解错了、路由选错了、还是下游接口报错了。
Agent-Reach 把这一整块从“写死在代码里”变成了“可配置、可注册、可观测”的基础设施。
1.3 和RPA、传统ESB集成平台的本质差异
这里再多说一句,因为总有人把类似的项目和RPA或ESB混为一谈。
传统RPA强调的是“模拟人操作界面”,重点在UI自动化;ESB强调的是企业级消息路由和数据转换,重点在异构系统对接。而Agent-Reach这一类东西,核心在于**“以模型为主体,以工具为延伸”**——它的路由决策不是预设的固定流程,而是模型根据用户输入实时生成的。
说得再直白点:RPA是照着剧本演戏,ESB是邮政分拣中心,Agent-Reach是一个会自己看路牌的司机。
2. 整体设计:一条消息怎么穿越Agent-Reach
2.1 架构总览,先跑通主干再谈细节
Agent-Reach 的整体链路我用一句话概括:用户输入 → 意图理解 → 任务规划 → 工具选择 → 参数抽取 → 权限校验 → 执行调用 → 结果回传 → 模型生成回复。
这个链路看起来简单,但每一环都有不少坑。我们先看主干的组件划分:
| 模块 | 职责 | 关键考虑 |
|---|---|---|
| 接入层 | 接收对话消息、上下文管理 | 多轮对话时上下文不能丢 |
| 规划引擎 | 大模型分析意图,拆解为子任务序列 | 用 Function Calling / Tool Calling 输出结构化计划 |
| 路由模块 | 从工具注册中心匹配候选工具 | 按语义相似度和参数结构过滤 |
| 工具注册中心 | 登记所有可被Agent调用的外部能力 | 工具描述、入参schema、鉴权配置 |
| 权限沙箱 | 校验用户、角色、数据范围 | 不能只校验到“接口级”,要校验到“行级” |
| 执行引擎 | 真正发 HTTP/gRPC 请求,处理超时重试 | 幂等设计很关键 |
| 观测模块 | 记录调用链路、Token、耗时、错误 | 没有这一步,线上排障会疯 |
规划引擎是大脑,执行引擎是手脚,权限沙箱是门卫,观测模块是监控摄像头。缺了哪个,这套系统都跑不长远。
2.2 工具注册中心:把业务能力变成大模型能看懂的服务清单
让大模型调用外部工具,第一步是要让模型“知道”有什么工具可用。这里不能靠模型自己猜,我们需要把每个工具的能力描述清楚,形成一份结构化的清单。
工具注册中心里每条记录都包含这种字段:
- 工具名:全局唯一,例如
get_order_detail - 描述:一句话说明这个工具做什么,供模型理解语义
- 参数Schema:JSON Schema格式,声明入参结构、必填项、类型
- 鉴权方式:API Key、OAuth、内部签名等
- 访问范围:谁可以用、能看到哪些数据范围
- 超时和重试配置:不同的下游能力差别很大,得单独配
我们实践中的建议是:参数的Schema要尽量严,描述要尽量口语化。因为模型是靠“描述”来理解这个工具什么时候该用的,描述写得含含糊糊,路由的准确率就直线下降。比如你要注册一个“查订单详情”的工具,描述不要只写“查询订单”,而是写“根据订单ID查询订单的当前状态、金额、物流信息和收件人地址,常用于用户咨询订单进度或售后场景”。
2.3 意图路由:让模型在几百个工具里选中正确的那个
工具少的时候,路由很简单,告诉模型“有这三个函数,你挑一个”。可当工具数量到了几百个,你不可能把全部函数定义一次性塞给模型——Token会爆炸,模型也会看花眼。
Agent-Reach 的路由模块做了两级过滤:
- 粗筛:用 embedding 对用户输入做向量化,和每个工具的描述算相似度,召回 Top N 候选(通常20个以内)。
- 精排:把这20个候选工具的完整Schema塞给模型,让模型结合上下文选最终工具并抽取参数。
这个方案我们对比过:全量塞所有工具时,不仅费Token,而且模型偶尔会选错;用两级过滤之后,准确率从89%左右提升到了97%,成本还降了一半。粗筛策略可以很简单,就是一个向量库加余弦相似度,不必一开始就上重模型。
3. 核心链路的工程实现:从自然语言到可执行调用
3.1 用 Function Calling 输出结构化计划,而不是让模型自由发挥
Agent执行任务时,最怕的就是“自由发挥”。早期我们试过让模型输出一段JSON格式的“行动计划”,格式时好时坏,有的模型会夹带私货,有的会把参数名写错。后来彻底转向了各大大模型厂商都支持的 Function Calling / Tool Calling 机制。
这才是正路:让模型原生地输出“我要调用哪个函数、参数是什么”,而不是让它用自然语言描述“我准备去调哪个接口”。
以ChatGPT的接口风格为基准,我们给模型的工具调用定义类似这样(伪代码示意):
tools = [ { "type": "function", "function": { "name": "get_order_detail", "description": "根据订单ID查询订单状态、金额、物流信息,用于订单进度相关咨询", "parameters": { "type": "object", "properties": { "order_id": {"type": "string", "description": "订单ID"} }, "required": ["order_id"] } } } ]模型在生成回复时,如果判断需要查订单,会输出一个结构化调用请求,而不是在回复里写“我要调用get_order_detail”。这个细节非常重要——结构化输出意味着我们可以程序化地解析、校验、执行,而不是用正则去剥一段可能随时变形的文本。
3.2 参数抽取与校验:模型填的参数不能直接信
模型能把工具名选对,不等于参数能抽准。用户说“帮我查那个上周买的手机订单”,模型可能会把“那个上周买的手机”当成订单ID传进去。所以 Agent-Reach 在参数这层做了三道保险:
- Schema校验:用类似 Pydantic 的方式强校验参数类型和必填项,缺了就回去追问用户。
- 业务兜底查询:如果必填参数是“实体ID”,而模型只拿到了模糊描述,我们会调用一个专门的“实体解析工具”,把用户描述转换成内部ID。
- 多轮澄清策略:模型拿不准参数时,不硬猜,主动向用户要。宁可多问一句,也不要拿错参数跑一趟下游。
这第三条,一开始很多产品经理接受不了,觉得每件事都要用户确认很蠢。但实际跑下来,多问一句比瞎查一个结果再被用户纠正要高效得多。
3.3 执行引擎的可靠性设计:超时、重试与幂等
你让Agent调用一个内部接口,这个接口必然会出各种幺蛾子:慢、超时、报错、返回格式不对。执行引擎就是扛这一层脏活的地方。
我们的实践原则是:
- 每个工具调用都有独立超时,默认5秒,个别慢接口可以放宽到15秒,但绝不允许无限等;
- 重试只在幂等接口上做。查询类接口可以重试两三次,但像“创建工单”“发起退款”这类会改状态的接口,重试要做幂等控制,用上游生成的请求ID去重;
- 下游返回的非结构化错误信息,要翻译成模型能理解的语言,比如“数据库连接失败”转成“查询服务暂时不可用,请稍后再试”,这样模型回给用户的话才是人话。
这里顺便贴一个执行引擎的关键伪代码逻辑:
def execute_tool_call(call, user_context): tool = registry.get(call.function_name) # 权限校验:用户有没有权力调用这个工具、看这份数据 permission_check(user_context, tool, call.arguments) # 幂等键:同一轮对话里同一个调用只能执行一次 idempotency_key = f"{call.request_id}:{tool.name}:{hash_args(call.arguments)}" # 带超时的执行 for attempt in range(tool.max_retries + 1): try: resp = http_client.post( tool.endpoint, json=call.arguments, headers=auth_headers(tool), timeout=tool.timeout, headers={"X-Idempotency-Key": idempotency_key} ) return normalize_response(resp) except TimeoutError: if not tool.idempotent: raise continue raise ToolExecutionError("all retries exhausted")这段逻辑看起来不复杂,但它是Agent-Reach能稳定上线的地基。没有这套兜底,Agent再聪明也只是个看起来很聪明但一干活就把事情弄砸的实习生。
3.4 结果回传与状态机:多步骤任务怎么编排
工单查询这种单工具任务简单,真正麻烦的是多步骤任务,比如“对比A产品和B产品的价格、库存、销量,然后给出购买建议”。这种任务模型需要依次调用多个工具,甚至要根据上一次的结果决定下一次调用什么。
Agent-Reach 用一个简化的状态机来管理整个过程:
- 待执行 → 执行中 → 等待工具结果 → 继续规划 → 完成/失败
每一轮执行完工具调用,把结果拼回对话历史,再次让模型决定下一步动作。这看起来像在“循环”,其实是目前最成熟的多步Agent执行模式——每个中间结果都会被模型看见,模型可以据此调整后续计划。
实际过程中我们会限制最大执行步数(比如5~8步),防止模型在一个问题上绕圈圈。同时每步都会记录中间产出,方便事后复盘“模型为什么这么决策”。
4. 上线之后,那些真实踩过的坑
4.1 并发一上来,下游系统先扛不住了
Agent原来只是聊天,不懂事,顶多是多烧点API费。接入Agent-Reach之后,它真的会去调业务接口了——结果上线第三天,我们就把某下游系统的数据库连接池打满了。
因为Agent收到30个用户提问,每个问题可能要调两三次接口,瞬间并发就是几十上百。而下游老系统的接口设计并发能力只有个位数。
这个问题的解法分三层:
- Agent侧限流:每个用户会话的并发调用数限制,比如同一用户最多同时2个工具调用;
- 接口侧排队:高并发请求先放到Redis队列里排队,执行引擎以固定的速率消费;
- 缓存:对于订单状态、商品信息这类变化不极频繁的数据,加上60秒左右的缓存,瞬间削掉一大半重复请求。
后两条是重点。我建议做Agent-Reach对接前,先摸排一遍下游接口的并发上限,不然你这边模型调优做得再好,也会被一个几十毫秒的慢接口拖死。
4.2 排查问题的时候,发现没有日志链路
这是上线初期最真实的痛:用户说“刚才机器人告诉我说查单失败”,你怎么查?
从前只有对话日志,看不到工具调用细节。后来我们在观测模块里补全了这些记录:
- 模型规划结果(选了哪些工具、为什么)
- 路由模块的候选工具列表和最终命中结果
- 每个工具调用的入参、出参、耗时、状态码
- 执行引擎的重试记录和幂等命中记录
- 模型最终回复的组装过程
统一打点成一个类似request_id + span_id的链路结构,挂到原有的日志系统上。再出问题的时候,一条工单从头查到尾,几分钟就知道卡在哪了:是模型误解了意图?是路由没召回?还是下游接口真的挂了?
4.3 模型的“幻觉式调用”:没有这个工具,它也要硬调
这是最隐蔽也最难防的一个坑:模型偶尔会“编造”一个工具调用,明明工具注册中心里根本没有get_refund_status,它却输出了一个get_refund_status的函数调用。
第一次遇到时我整个人是蒙的——Function Calling 不是受控的吗?后来才明白,当前的模型在工具调用上依然有小概率产生幻觉,尤其是多个工具描述相似的时候。
我们实践中做了三层防御:
- 执行前校验:凡是工具名不在注册中心里的调用,一律拦截,并让模型重新生成;
- 参数重校验:参数里出现了明显不存在的ID格式,也会触发重新生成;
- 低置信度兜底:如果路由阶段的模型打分偏低,我们不直接执行,而是让模型先向用户确认一次。
别觉得这多余。生产环境里一个幻觉调用可能意味着向CRM系统写入一条错误工单,这种事故出一次就能让公司对整套Agent方案失去信心。
5. 从内部工具到全业务系统:Agent-Reach 的进阶玩法
5.1 接入CRM、工单、数据平台之后,Agent才真正“有用”
我们把 Agent-Reach 的第一批业务场景定为三类:
- 订单域:订单查询、物流跟踪、售后进度;
- 数据域:报表查询、指标解释,比如“上个月华东区销售额环比变化”;
- 工单域:创建工单、补充备注、流转状态。
这三类场景跑通后,业务方对Agent的评价从“好玩但没用”变成了“你们做出来那个东西确实能帮上忙”。差别就在于Agent能不能触达真实数据。
5.2 Agent-Reach + RAG 的组合:先查文档再调工具
有些问题属于“知识类”的,比如“退换货政策是什么”;有些问题属于“动作类”的,比如“帮我把这个订单申请退货”。我们后来把知识库能力也并了进来,先让模型判断用户意图是“查资料”还是“调工具”。
如果是查资料,走RAG链路,从知识库检索相关文档;如果是调工具,走Agent-Reach链路,访问业务系统。两者的结果最终一起组装进回复。
这个组合覆盖了客服场景里绝大多数提问。用户问政策,模型答内容;用户要办事,模型去执行。从实际效果看,一线客服的重复性工作量大概降了三成左右。
5.3 给团队落地Agent类项目的几点建议
最后,基于这大半年带队折腾Agent-Reach的经验,我总结几条供参考:
先选场景,再选技术。别一上来就想做一个无所不能的超级Agent,找两三个高频、低风险、效果可量化的场景跑通链路,比什么都强。
把“可观测性”提前到第三天做。我们就是吃了晚做的亏,前两周一出问题就得靠人肉翻日志,后面补上链路追踪后,排查效率完全不是一个级别。
给模型配一个“拒绝”选项。Agent必须具备说“我不知道”“我做不到”的能力。强制让模型什么都能干,一定会产出幻觉调用。把“无法确定时主动澄清”写进系统提示词,我们的假操作数量减少了一半以上。
别忽视权限设计。很多Agent项目悲剧的起点就是:模型调用了一个接口,但这个接口背后的数据权限没控制好,某个普通员工查到了管理层的数据。Agent-Reach 的权限校验一定要跑到业务数据层,而不是停留在“能不能调这个API”的层面。
把工具描述当产品文案来写。这一点容易被忽视,但影响巨大。同一个接口,描述写得模糊和写得精准,模型选对的概率相差悬殊。我们每周都会根据错误路由案例反查工具描述,然后修改措辞——这是一项持续优化的工作,不是上线就完事。
写在最后的几点体会
Agent-Reach 这个项目做下来,我的核心感受是:大模型本身早已不是瓶颈,真正体现工程水平的是“Agent如何可靠地触达系统”。模型负责聪明,工程负责靠谱,两条腿缺一不可。
如果你正在做类似的智能体落地项目,我建议你把精力重点压在三个方面:工具注册和描述的规范化、执行引擎的可靠性和幂等控制、全链路的可观测性。这三个基础打牢了,后面加再多工具、接再多系统都只是时间问题。
最后分享一个我们内部一直在用的小技巧:每次给系统接入一个新工具前,先人工模拟三轮对话——分别用一个正常提问、一个模糊提问、一个恶意提问去打这个工具,看模型路由、参数抽取、权限拦截这三个环节的表现。这三轮过了,再放上线,基本能过滤掉绝大多数低级事故。
希望这篇实战拆解能给你带来一些参考。Agent 落地这件事没有银弹,一步一个坑地踩过去,稳定性和实用性自然就出来了。