资讯动态

AI Agent落地实战:Agent-Reach触达层设计全复盘

发布时间:2026/9/18 4:03:29 来源:尧图企业网站定制
我最早做AI Agent相关项目的时候踩过一个特别典型的坑模型选的是当时最强的Prompt也反复调了好几个版本Demo演示的时候各种丝滑结果一接到真实业务场景Agent就开始满嘴跑火车——让它查一下内部知识库里的流程文档它说根据我的知识这个流程大概是……然后一本正经地编了一段根本不存在的规定。问题出在哪儿不是模型不行也不是Prompt不行而是Agent的手和脚太短了。模型本身只有一堆训练参数它连接不到你的数据库、读不到你上传的PDF、也按不了网页上的按钮。你给它再强的推理能力它也只能在你塞给它的那点上下文里盲猜。这个问题的本质就是Agent的触达能力Reach不够它够不到实时数据、够不到业务系统、够不到互联网上那些还没进入训练集的长尾信息。我想了很多办法去解决这个问题最后沉淀下来一套思路名字就叫Agent-Reach。核心就一个字够。把Agent需要的一切外部能力拆成显式的、可插拔的触达组件让模型只负责规划和决策真正去够东西的动作全部交给一个我们完全可控的执行层。这篇文章是我自己做这个项目的一次完整复盘包括架构设计、最小实现、踩过的坑以及一些在文档里翻不到的实操经验。适合正在做Agent落地、或者准备把Agent从一个聊天机器人升级成能干活的机器人的工程师和产品同学。1. 为什么多数Agent项目看起来厉害一接业务就废先说一个我观察到的现象很多人做Agent第一个版本永远是给我写一首关于春天的诗、帮我整理一下这份文档的要点这类任务。这类任务模型本身就能完成跟Agent沾边但本质上只是一个带上下文的高级Chat接口。一旦进入真实业务比如根据最近一周的销售数据找出去年同类产品推广中最有效的三个渠道并生成一份下周投放建议问题就来了——数据在哪儿历史推广记录在哪儿模型根本不知道。它只能开始编。不是说模型想骗你而是它手里没有真实资料不编就只能沉默而沉默对Agent来说等于失败所以它宁可编。我把这类问题总结成三个看不见的手铐。1.1 上下文囚笼你的知识库塞不进对话窗口现在的模型上下文窗口看着很大128K、200K甚至更大但对比真实业务数据这点窗口其实小得可怜。我手头有个内部SOP文档加起来一千多页转成纯文本大概一百五十万token就算全部塞进上下文窗口先不说成本模型处理这么长的内容时注意力早就被稀释得差不多了后面讲什么它根本记不住。更关键的是你不可能每次对话都把全量知识库塞进去。真正的解法是让Agent知道我需要的时候就去找也就是具备检索触达能力。Agent-Reach落地时第一步就是给Agent装上一套检索的手用向量召回加关键词召回的方式把知识库变成一堆可检索的碎片Agent需要哪块就去捞哪块而不是指望它把整个图书馆背下来。1.2 手脚被绑模型只能输出文字干不了任何实事模型的输出本质上就是一个字符串。它没法自己去查数据库、没法调用支付接口、没法登录你的内部CRM。早期智能体只能做到我告诉你SQL你自己去执行这中间只要多一层人工拷贝Agent的自动化价值就废了一半。后来大模型厂商都支持了函数调用Function Calling或者工具调用Tool Use模型可以在回复里生成一个结构化的调用指令比如调用search_web工具参数是关键词XXX。但这里有个认知误区模型只是决定要调用工具真正执行工具的还是我们的代码。Agent-Reach在这一点上做了一个很关键的拆分——把模型决策和代码执行彻底解耦。模型永远不直接连接什么东西它只输出工具名和参数真正发起HTTP请求、打开浏览器、查询数据库的动作全部由Agent-Reach的触达层去完成。这样既发挥了大模型的规划能力又把不可控的部分锁在了一个可控的沙箱里。1.3 记忆没长脚每次对话都像第一次上班的新人做过客服类Agent的朋友应该很有感触用户上午问过一个工单进展下午再问一次Agent已经完全不记得了。所有信息都要靠外部系统重新捞一遍。这是记忆触达的缺失——Agent不是没有记忆而是它不知道去哪儿找记忆。印象很深的一次我们给一个售前机器人加了历史会话记忆库结果机器人确实会去查了但查的是我们自己塞进Prompt里的一段JSON摘要那段JSON一长上下文就爆了。后来改成用向量检索按需拉取历史对话片段效果立刻好了很多。这件事让我意识到记忆不只是一个存储问题更是一个触达问题要让Agent具备主动去够历史经验的能力而不是被动接受塞给它的东西。2. Agent-Reach到底在够什么四类触达目标与取舍想清楚够这个概念之后下一步是拆解一个真实业务里的Agent到底需要够哪些东西我做了几个项目之后把触达目标分成了四类每一类的底层技术差别很大、代价也完全不同事先分清楚能省掉大量返工。2.1 触达本地知识库从全量灌输到按需检索这是最常见的需求——把公司内部的文档、FAQ、过往方案变成Agent可以随时调用的知识。最朴素的做法是把文档全部灌进向量数据库然后做相似性检索。但单纯的向量检索有一个很典型的毛病对说法变了但意思一样的问题召回好对包含精确代号、型号、编号的问题则经常翻车。比如你问PRD-2024-0815号项目的验收标准是什么如果文档里写的是2024年8月15日启动的XX项目纯向量检索很可能因为字面差异太大而召回不到。所以我的经验是不要在向量检索一棵树上吊死至少要做向量召回 关键词召回的双路混合再合并排序。Agent-Reach在知识库触达这一块实际上封装的就是这类混合检索逻辑把底层用什么数据库、用什么Embedding模型、怎么合并分数这些事屏蔽掉给Agent暴露一个简单的接口问一句自然语言返回N个最相关的片段。2.2 触达外部服务工具调用的实战形态没那么简单函数调用现在大家都会写比如给模型一个get_weather的JSON Schema它就会返回{city: 北京}之类的参数。但真正到生产环境工具触达要面对的问题远不止让模型返回一段JSON。我遇到过的最大坑是工具返回值的长度和格式不可控。有的API返回一坨几万字的JSON直接塞进上下文模型看着看着就疯了开始胡言乱语。Agent-Reach的做法是给每个工具的输出都做一次压缩适配要么在工具侧先用代码提炼出关键字段要么在把结果喂给模型之前做一次摘要。原则很简单保证模型的上下文里只出现决策所需的信息而不是触达到的原始数据。这个原则我后面会反复强调几乎所有Agent翻车都跟违反它有关。2.3 触达真实界面当API不存在时浏览器就是你的接口有些老系统没有开放API甚至连数据库都不让直连你想让Agent帮用户查一下订单状态能怎么办唯一的通路是浏览器。Agent-Reach里我保留了这样一个模块基于Playwright的浏览器触达器让Agent可以打开页面、填表单、点按钮、读结果。但是这里有个容易搞混的点浏览器触达和普通爬虫是两回事。爬虫是写死路径去抓固定字段浏览器触达则要面对页面结构变了怎么办弹窗挡住了怎么办登录态过期了怎么办。我的方案是给浏览器触达器加了一层视觉级兜底——当DOM选择器定位失败时降级为截图并用视觉模型识别页面元素的位置再通过坐标点击。这个方案不算完美但确实把Agent在真实网页上的容错能力提高了一大截。2.4 触达记忆与协作让Agent拥有过去时和另一个人前面提到过记忆不是存储问题是触达问题。Agent-Reach里把记忆分成了两层短期工作记忆当前会话里已经聊过的内容通过摘要不断压缩防止爆上下文。长期项目记忆已经沉淀的事实、结论、用户偏好存在向量库里Agent在做决策前按需检索。这两层之外还有一类容易被忽略——跨Agent的共享经验。我们当时有两个Agent在跑同一个业务流程一个负责售前咨询一个负责售后跟进用户先咨询再报修售前Agent聊的内容售后Agent完全不知道用户就要重复一遍。后来我们把两个Agent接到同一个记忆库售后Agent在开场时会主动检索这个用户最近是否咨询过其他问题体验一下就顺了。所以记忆触达还要解决谁的经验可以被谁够得到的问题这本质上是一个权限和共享的设计。3. 最小可用的触达层长什么样一个会查资料写摘要的骨架理论讲完上点能用的东西。我搭Agent-Reach早期版本的时候没有一上来就上那种重型的Agent框架所有编排逻辑先用一个两百行的Python骨架跑通。这里把核心设计分享出来你可以直接照这个思路去搭你自己的触达层。3.1 整体结构决策与执行严格分离我的代码结构只有三层编排层Orchestrator接收用户问题交给大模型由大模型决定要调用哪个工具传入什么参数。触达层Reach Layer真正执行工具调用的地方。每一个工具都是一个独立函数有输入校验、超时控制、返回结果统一包装。合成层Synthesis把多个工具返回的结果交给大模型由大模型整合成最终回复。核心思想是编排层永远不知道工具是怎么实现的。它只看到一张工具清单工具叫什么、是干什么的、需要什么参数、参数长什么样。至于这个工具是去调GPT的接口还是去操作浏览器编排层一概不管。这样做的好处是便于测试工具可以单独单测便于审计每个工具调用都留下了入参和出参的日志更便于替换今天用某家的搜索API明天换另一家对Agent来说完全无感。3.2 触达层核心代码注册表加统一执行器下面这段代码是这个骨架里最关键的部分一个极简工具注册表加统一执行器。你不需要原样抄理解这个设计思路就够了。# agent_reach_reach_layer.py # Agent-Reach 触达层最小骨架 from typing import Any, Callable, Dict, Optional from dataclasses import dataclass, field import time import json import logging logger logging.getLogger(agent_reach) # 工具返回值的统一包装不管是API返回、数据库查询还是抓取的网页 # 一律包装成这个结构避免后续处理时格式混乱。 dataclass class ReachResult: tool_name: str status: str # success / failed data: str # 已经压缩提炼后的文本直接可喂给模型 raw_data: Any None # 原始返回主要用于日志审计 duration_ms: float 0.0 def _default_validator(params: Dict[str, Any]) - Dict[str, Any]: 默认参数校验实际项目中建议用 pydantic / jsonschema。 return params dataclass class ReachTool: name: str description: str parameters_schema: dict execute: Callable[[Dict[str, Any]], str] None timeout_seconds: float 10.0 validator: Callable[[Dict[str, Any]], Dict[str, Any]] _default_validator # 工具注册表所有可被Agent调用的触达能力都注册到这里 class ToolRegistry: def __init__(self): self._tools: Dict[str, ReachTool] {} def register(self, tool: ReachTool) - None: if tool.name in self._tools: raise ValueError(f工具重名: {tool.name}) self._tools[tool.name] tool def list_tools_for_llm(self) - list[dict]: 把工具清单转换为模型Function Calling需要的格式 specs [] for tool in self._tools.values(): specs.append({ type: function, function: { name: tool.name, description: tool.description, parameters: tool.parameters_schema, } }) return specs def has_tool(self, name: str) - bool: return name in self._tools def get_tool(self, name: str) - Optional[ReachTool]: return self._tools.get(name) # 统一执行器负责入参校验、超时保护、日志记录、返回值包装 def run_tool(registry: ToolRegistry, tool_name: str, params: Dict[str, Any]) - ReachResult: # 防御模型传了不存在的工具名 if not registry.has_tool(tool_name): return ReachResult(tool_nametool_name, statusfailed, dataf工具未找到: {tool_name}) tool registry.get_tool(tool_name) start time.monotonic() try: # 入参校验很关键模型偶尔会生成不符合Schema的参数 validated tool.validator(params) if tool.execute is None: return ReachResult(tool_nametool_name, statusfailed, data工具无实现) # 统一在这里做超时保护防止第三方API挂死 # 简化写法生产环境建议用 asyncio.wait_for 或线程池 raw_output tool.execute(validated) duration_ms (time.monotonic() - start) * 1000 # 关键一步把工具输出截断到合理长度防止污染模型上下文 # 这个阈值可以根据你的模型窗口调整我用的是2500字符 max_chars 2500 output_text str(raw_output) if len(output_text) max_chars: output_text output_text[:max_chars] \n...[截断] logger.warning(工具 [%s] 返回超过 %d 字符已截断, tool_name, max_chars) return ReachResult(tool_nametool_name, statussuccess, dataoutput_text, raw_dataraw_output, duration_msround(duration_ms, 2)) except Exception as exc: # 异常绝不落进模型上下文而是转成固定格式的错误信息 duration_ms (time.monotonic() - start) * 1000 logger.exception(工具 [%s] 执行异常: %s, tool_name, exc) return ReachResult(tool_nametool_name, statusfailed, dataf工具执行失败: {str(exc)[:200]}, duration_msround(duration_ms, 2))这套骨架用起来很直接写工具函数、注册到registry、将registry里的工具清单传给模型的Function Calling配置。模型返回要调用哪个工具、传什么参数之后直接交给run_tool执行再把ReachResult.data塞回对话历史让模型继续总结。就这一个闭环已经足够支撑非常多真实场景。3.3 为什么这样设计三个反直觉的决策第一工具返回值强制截断。很多人的直觉是工具返回越详细越好模型看得越多答得越准。但我实测下来恰恰相反一旦某次触达返回超过一定长度模型对后续指令的遵循能力会明显下降。信息密度比信息总量重要得多。我习惯在工具函数内部就做好提炼比如查订单在函数里直接执行SQL然后返回的不是原始表而是该订单状态为已发货物流单号SF123456预计明天到达。这样模型拿到的就是可以直接用的信息。第二异常不交给模型处理。刚开始做的时候有一次搜索API超时返回的是一大段堆栈错误模型看到之后一本正经地分析可能是网络不稳定、也可能是防火墙设置有问题非常危险因为它可能就此开始猜测而不是去重试或者换路。现在我把所有异常统一转成工具执行失败超时模型就只会做两件事换个工具再试或者告诉用户现在查不了。可控多了。第三工具清单不要一口气全开。注册表里放二十个工具有时候反而是灾难。模型面对的工具选择越多选错的概率就越大Prompt里工具描述还会占用大量token。我的经验是按场景动态开放工具售前机器人只开放订单查询、产品库检索、知识库搜索这几个工具售后机器人则开放工单创建、进度查询等。Agent能够到的范围先窄后宽跑稳了再逐步加。4. 实测三次翻车上下文阻塞、来源幻觉、并行错序的完整排查链路骨架搭好只是开始真正的教室是生产环境。我在这套系统上跑了大概两个月中间翻过三次比较大的车每次都是查日志查到大半夜才定位到根因。这几次教训我觉得比架构本身更值得拿出来说。4.1 第一次翻车一次搜索几乎吞掉全部上下文窗口现象是这样的Agent在回答一个关于竞品分析的问题时突然开始答非所问甚至把用户之前问过的问题又重复了一遍。起初怀疑是模型抽风后来看token日志才发现那次会话中某个搜索工具返回了大约十篇网页的全文摘要加起来接近六万个token直接把上下文窗口占满了。后面模型不是在思考而是在遗忘前面聊的内容、工具返回的内容全被挤出窗口了。排查链路先看每次触达返回的字符数再统计历史对话中模型开始失忆的时间点发现无一例外都发生在某次超长工具返回之后。根因确认后我在两个环节做了修复第一工具侧按域名和正文结构做抽取只返回标题、发布时间、正文前几百字第二在run_tool里加绝对上限超过阈值直接截断并打日志。这个修复上线后失忆问题基本绝迹。4.2 第二次翻车引用来源张冠李戴Agent开始幻觉来源第二次的问题更隐蔽。我们的Agent会在回答末尾附上引用来源比如[1]、[2]、[3]这是通过Prompt要求模型做的。某次用户问对比一下A产品和B产品在售后政策上的区别Agent答得头头是道结果我点开引用链接一看[1]链接指向的是A产品的页面但正文里那句关于[1]的描述实际上是B产品页面的内容。排查链路去翻日志发现那次回答之前触达层并行检索了六个网页返回给模型的时候只是简单地拼接成一个列表。模型在处理这些并列来源时把编号和内容搞混了。根本原因是多个来源并列返回时没有在结构上强制建立编号内容的强绑定。文本拼接的格式模型可以读对但压力大时就会错。修复方式把并列返回改成结构化块每块先出现[来源ID]然后是URL然后是内容同时Prompt里明确要求引用时只能基于[来源ID]对应的内容不得混用。说到底不是模型不够聪明而是我给它制造了一个容易混淆的输入结构。从那以后我给触达层的所有多返回值都定了规矩先标识来源再给内容来源和内容永远是一体的拆开就是事故。4.3 第三次翻车并行调用的返回顺序乱了输出驴唇不对马嘴触达层支持并发之后出过一个大乱子Agent同时调用了查天气和查航班两个工具两个结果几乎同时返回由于实现里把结果按返回顺序填进了消息历史结果模型读到的是天气国航CA1234延误航班晴28度。它自己都困惑了最后给用户输出了一段颠三倒四的回复。排查链路在日志里比对工具调用ID和结果ID发现我的代码压根没有给并发任务打标签。不同触达的返回只是简单地append顺序完全取决于哪个先回来。找到根因后我立即改为按调用ID分组再拼接同时要求编排层在模型执行前先拿到完整的工具清单和工具返回值按固定顺序塞回上下文而不是逐个append。这个坑让我体会到在工程上模型对输入顺序是非常敏感的任何不确定的顺序最后都可能变成输出里的混乱。4.4 三次翻车的共性问题都不在模型仔细复盘这三次事故我发现一个共性每次翻车根因都不在模型而在触达层的数据流设计。模型只是忠实地消费了我喂给它的东西我喂的是混乱、超长、错序的信息它就只能产出混乱的结果。这就是为什么我一直强调Agent质量的上限取决于触达层的整洁度——模型推理能力再强也顶不住脏乱差的数据流。所以后来每写一个工具我都会先问一遍这个工具返回的东西一个刚入职的实习生拿到手里能不能不迷糊如果实习生会迷糊模型也会迷糊。5. 从能跑到稳定跑缓存、退避、校验与安全栏Agent-Reach跑通功能之后我花了大把时间在生产化上。这个阶段的收益比调Prompt高得多。抽象地说就是把触达层从能被调用打磨成可以被信任。5.1 结果缓存同样的触达不要花第二次钱Agent在真实使用中有很多查询是高度重复的。比如退款政策是什么这种高频问题触达层每次都要去检索知识库、甚至调用外部搜索既慢又烧钱。我的方案是加一层结果缓存以工具名 参数哈希作为key把触达结果缓存一段时间。实测下来在我们的客服场景里缓存命中率能做到四成以上响应时间从四五秒降到一秒以内API费用也省了将近三分之一。缓存需要考虑失效策略。我当时用的是TTL 主动失效的组合知识库类触达缓存一小时订单类触达缓存五分钟涉及库存、价格的触达直接不缓存或者缓存三十秒。数据更新时由写入方主动调用失效接口把相关key打掉。这一套对于大多数业务场景够用了。5.2 限流、超时与指数退避第三方API不是你家服务器触达层一旦跑起来第三方API的限流就成了家常便饭。我见过最离谱的一次某个外部接口在高峰期连续返回429我的Agent在短时间内重试了十几次把对方彻底惹毛直接封了我们的IP。修复思路有两条腿。第一条是别让Agent把重试当饭吃触达层统一接管重试策略对429和5xx做指数退避第一次等1秒、第二次2秒、第三次4秒最多重试三次重试还不行就降级。第二条是限流前置在触达层做一个简单的令牌桶每秒最多调用N次该工具超出的请求直接排队等待而不是一窝蜂打到第三方。做完这两件事我们的工具调用故障率降了一个数量级。5.3 返回结果校验脏数据进不了上下文幻觉少一半工具返回的数据五花八门有些字段缺失、有些格式错乱、有些干脆是HTML标签串。把这些脏数据直接喂给模型模型会说根据以上信息然后开始一本正经地胡说。Agent-Reach在触达层加了一道结果校验闸门用JSON Schema做校验字段缺了补默认值类型错了做转换完全不合法的直接标记为失败让Agent换一条路。这一步要花点时间把每个工具的返回格式定义清楚尤其是那些对接已久的老接口但收益非常直接——模型吃到的数据干净输出自然干净。5.4 安全边界让Agent决定做什么但别让它独立执行危险动作最后一条也是最重要的一条触达层必须对危险动作有硬性的安全护栏。什么是危险动作删除记录、修改配置、发送对外消息、支付转账这些都属于不可逆或高影响操作。我的做法是给工具打标普通工具Agent可以自主调用危险工具走人工审批流程。Agent照样会生成调用指令但触达层拦截下来生成一条审批请求推到钉钉或企业微信人点同意才真正执行。同时触达层的完整调用日志做了审计谁在什么时候调了什么工具、传了什么参数、得到了什么结果全部可追溯。这个东西一定要在生产化初期就建好后面再补成本会翻好几倍。6. 不要神化Agent-Reach哪些场景根本不该上这套东西每篇文章都在告诉你怎么做我来泼一点冷水并不是所有项目都适合上Agent-Reach这套触达架构。如果符合下面几种情况我建议你先用普通程序解决别硬凹Agent。6.1 流程固定、规则明确时定时任务或脚本远好于Agent如果业务需求是每天凌晨把昨天订单汇总成表格这个流程是固定的、可预期的直接写脚本、上定时任务三百行代码就搞定稳定又省钱。如果用Agent来做等于让一个灵感型选手去干流水线工人的活模型每次决策还会有一点点随机性反而引入了不确定性。Agent-Reach的价值场景是路径不可预先穷举、需要动态决策的任务。如果一个任务你闭上眼睛都能写出执行步骤那它根本不需要Agent。6.2 输入输出完全结构化时传统接口效率碾压还有一种常见的误区以为带上了AI就有智能化。比如某个系统原来用Webhook接收JSON处理完再回传JSON中间逻辑非常明确。有人问能不能改成Agent我说改了之后唯一的变化就是延迟从200毫秒变成3秒、成本从几分钱变成几块钱准确率还未必有原来的高。Agent擅长的其实是非结构化输入、模糊意图、开放式输出。两边搞反了就是把好东西用错了地方。6.3 高一致性、强事务场景谨慎引入涉及资金、法务、医疗这些要求强一致性的场景Agent的概率性正确就有风险。不是说不能用而是要在Agent外面套上强规则壳Agent负责生成建议规则引擎负责校验建议是否合规双人复核甚至多人复核之后才能执行。我帮一个金融客户做过类似的落地最后的方案是Agent只负责做信息整理和风险提示具体交易动作全部由传统流程引擎控制Agent没有最终决策权。这个边界划清楚之后项目的推进顺畅了很多。6.4 我的取舍建议别让触达能力反过来绑架你的系统如果你看完这篇还是觉得Agent-Reach这套思路可以试我给一个路线图先挑选一个最痛、路径最杂、通常是人工翻阅十几个系统才能完成的业务场景做一次最小闭环。工具数量控制在五个以内先把触达层的规范立起来返回值结构、超时控制、异常协议、缓存策略。跑通之后再横向复制到其他场景。我在实践中反复体会到一个道理Agent项目的复杂度几乎全部来自触达层模型本身反而简单。触达层做得越规整Agent的表现就越稳定。最后分享一个细节我后来把所有工具注册表里的工具描述集中review了一遍把那些形容词太多、暗示性太强的描述全部改成简短客观的写法比如把这个工具很好用可以快速查询订单信息并返回详情改成查询订单信息按订单号返回订单状态、金额、物流。模型的工具选择准确率立刻就有肉眼可见的提升。模型不需要你夸工具它只需要知道工具能干什么、什么时候该用。这大概也是Agent-Reach这条路的缩影与其去调教模型不如去打磨它可以够到的世界。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价