ARTICLE DETAIL

资讯详情

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

Agent-Reach:让大模型拥有触达业务系统的“手脚”

Agent-Reach:让大模型拥有触达业务系统的“手脚” Agent-Reach这名字是我在公司搭智能体服务平台时随手起的代号意思是“Agent的手能伸多远”。当时我们面临一个特别真实的问题大模型聊天很厉害可一旦让它去查个库存、调个工单、发个通知它就只会说“我暂时无法获取实时数据”。用户不会管你是不是纯LLM他们要的是“帮我把事情办了”。这个项目就是为了解决这事把AI Agent从一个只会说话的对话机器人变成真正能触达业务系统、操作真实数据的执行者。这篇内容适合正在做Agent工程化、想把LLM接进真实业务系统的开发者也适合刚接触Function Calling但不知道怎么设计一套稳定接入方案的同学。我会把Agent-Reach从需求拆解、架构设计、代码实现到上线后要盯的坑完整讲一遍所有方案都是我实际跑过的不是PPT层面的东西。1. 为什么需要一个叫Reach的东西1.1 Agent真正缺的不是大脑是手脚很多人第一次接触大模型应用时都会误以为“只要把模型接上API它就能操作一切”。实际上模型的能力边界非常清晰推理、理解、生成是它的强项但访问外部系统这件事它天生做不到。它没有手去点按钮没有权限去查数据库更没法在拿到数据后替你去执行一笔写入操作。我把这个问题总结为“触达半径过短”——Agent能感知到的只有你喂给它的上下文能输出的只有文本。一旦业务场景要求它跨出这个半径比如去看看某个SKU在华南仓还剩多少货它就原地卡住。Agent-Reach这个名字里的“Reach”指的就是触达半径。项目的核心不是教模型更聪明而是给模型装一套可靠的“手脚”——包括工具定义、调用协议、权限控制、结果回传和异常处理。换句话说大脑还是那个大脑但手脚是我们在外面给它接的。这个思路跟人类协作很像你不需要让实习生天生什么都会你需要的是给他清晰的授权范围、标准操作流程和报错机制。Agent-Reach做的就是这件事。1.2 Reach要解决的其实是三层触达在实际设计时我把“触达”拆成了三个层次每一层对应的技术方案都不一样触达层次典型场景核心问题Agent-Reach的做法工具触达调用天气API、计算器、图像处理函数模型怎么知道该用哪个工具、参数怎么填结构化工具描述 Function Calling协议数据触达查数据库、读文件、搜索内部知识库数据格式千奇百怪模型容易误读统一返回结构 字段级schema说明协作触达发起工单、发邮件、通知其他系统写操作有风险权限不能失控工具白名单 敏感操作二次确认 审计日志工具触达是最基础的一层解决的是“能不能调用”的问题数据触达解决的是“拿回来的东西模型能不能看懂”的问题协作触达解决的是“让Agent去做会影响真实世界的操作时怎么保证不出事”的问题。这三层必须一起设计只做第一层你会得到一个“能调用但经常调错”的Agent只做前三层不做权限你会得到一个“胆大包天”的Agent。我在项目里见过太多次工具调用链走到一半、模型自作主张调了一个它没权限的接口的情况所以权限和审计从一开始就内置进去而不是事后补。1.3 这套方案适合谁不适合谁Agent-Reach这套设计最适合的场景是“业务流程相对固定、工具数量可控、对结果稳定性有要求的内部系统接入”比如电商订单查询、仓储库存核对、客户工单流转、企业知识库问答。它不太适合做开放式创作助手那种场景——如果回答本身就是最终产物不需要去操作外部系统那这套东西完全是多余的。同样的如果你的工具数量一开始就超过几十个甚至上百个模型在工具选择环节的准确率会明显下降那就需要考虑分层路由或者语义检索先筛一遍工具这属于Agent-Reach的进阶扩展我后面会提一嘴。我个人的判断标准很简单用户要的到底是“一段话”还是一个“结果”。只要结果是“某个系统里发生了一件具体的事”就值得用Agent-Reach的思路来搭。2. 先想清楚架构再动手写代码2.1 两套触达方案的取舍市面上接Agent触达能力的主流做法有两套我在早期都试过。第一套叫“函数直调”就是把工具函数直接import进Agent的主进程模型在回答里说“我要调用query_inventory()”代码这边解析然后本地执行。好处是简单坏处是每加一个工具就要改主流程而且工具一多模型自己也不知道到底有哪些函数可用调用准确率掉得很快。第二套叫“工具注册中心 Function Calling协议”这也是我最终在Agent-Reach里采用的方案。每个工具都用一份标准化的manifest描述包括名称、功能、参数、返回结构、权限范围Agent每次收到用户请求时会根据这些描述动态决定调用哪个工具、填什么参数。主流程完全不用改加新工具就是新增一个描述文件和一个实现函数。对比项函数直调工具注册中心接入新工具成本要改主流程代码只加描述文件和实现模型对工具的可感知性低经常不知道有这函数高每轮请求都注入描述权限控制细粒度难做可以在manifest里声明可观测性散落各处统一集中在路由层适合规模5个以下工具10~50个工具当然工具注册中心也有代价每轮对话都要把工具描述拼进系统提示词token消耗会增加。以我实际经验一个设计良好的工具描述控制在100~200 token内10个工具大约占1.5K~2K token这个成本换来的调用准确率提升是值得的。2.2 目录结构与核心模块Agent-Reach的项目结构我当时是按“路由、适配、执行、治理”四个层面组织的现在的目录长这样agent_reach/ ├── main.py # 入口初始化Agent-Reach实例 ├── core/ │ ├── orchestrator.py # 核心编排请求理解-工具选择-执行-回填 │ ├── router.py # 工具路由根据用户请求选出候选工具 │ ├── executor.py # 工具执行器统一调用与超时控制 │ └── memory.py # 会话上下文管理处理多轮状态 ├── tools/ │ ├── registry.py # 工具注册中心负责加载manifest │ ├── manifests/ # 所有工具的JSON描述文件 │ │ ├── weather.json │ │ └── inventory.json │ └── impls/ # 工具实现代码 │ ├── weather_tool.py │ └── inventory_tool.py ├── security/ │ ├── policy.py # 权限策略、白名单、敏感操作标记 │ └── audit.py # 审计日志落盘 └── utils/ ├── llm_client.py # LLM调用封装 └── logger.py # 结构化日志这个结构的核心好处是“注册与实现分离”Manifest描述是给模型看的实现函数是给代码执行的两者用同一个工具名做关联。模型永远只接触manifest不会接触底层实现天然形成了一层隔离——即使模型在参数上乱来executor层也能在进入实际函数前做参数校验拦截。2.3 为什么必须把边界写死在架构里把边界写死在架构里的意思是不要指望模型自己有分寸而是从机制上让它没法越界。我先说个真实案例。早期版本里有个发邮件的工具第一天接入时只设置了“收件人必须匹配内部邮箱后缀”的校验结果测试时模型拿到用户一句话“给所有管理员发一封系统巡检通知”它居然真的遍历了通讯录名单逐个发邮件。从技术上说模型只是忠实地执行了任务它并不理解“群发”在真实世界的成本和影响。所以Agent-Reach里每个工具的manifest必须声明三样东西scope允许的操作范围、sensitive是否敏感操作、constraints强约束条件。拿发邮件来说max_recipients上限是10sensitive是true凡是sensitive的工具默认要过一道人工确认或白名单校验。下面是一个库存查询工具的manifest示例{ name: query_inventory, description: 根据SKU和仓库代码查询当前库存数量。当用户询问现货、库存、余量时使用。, parameters: { type: object, properties: { sku: {type: string, description: 商品SKU编码必填}, warehouse: {type: string, description: 仓库代码例如 east_1、south_2} }, required: [sku] }, scope: read_only, sensitive: false, constraints: { timeout_ms: 3000, max_return_bytes: 2048 } }scope拿来做执行层的强制拦截sensitive拿来做权限升级提醒constraints里的超时和返回限制则是防止工具本身出问题拖垮整个Agent。这些字段不是给模型看的装饰性描述而是executor真正会去执行的硬规则。把边界写死之后模型可以在规则内自由发挥但规则本身不归模型管。3. 从零搭一个能跑起来的实例3.1 环境准备只用三个依赖动手之前先准备环境。我的建议是Python 3.10以上版本理由是类型注解和内置语法支持更舒服。依赖方面Agent-Reach实际只需要三样东西LLM的官方SDK、dotenv管理密钥、requests用于调外部接口。mkdir agent-reach-demo cd agent-reach-demo python -m venv venv source venv/bin/activate pip install openai python-dotenv requests这里多说一句即使是企业内部项目我也建议把API key放在.env文件里别硬编码在代码里。仓库里永远只提交.env.example真正带密钥的.env加进.gitignore。这不是矫情是我见过不止一次有人把key提交到内部Git仓库然后被扫描工具抓出来的事故。在.env里配上你用的模型API keyOPENAI_API_KEYsk-xxx3.2 写两个真实工具天气与库存为了演示触达效果我准备了两个典型工具一个查实时天气外部HTTP接口一个查仓库库存本地数据。气象工具模拟的是“调第三方服务”库存工具模拟的是“查内部数据”正好覆盖Agent-Reach里最常见的两类触达对象。先看天气工具的实现# tools/impls/weather_tool.py import requests def get_weather(city: str) - dict: 根据城市名查询实时天气Demo使用公开测试接口 url fhttps://api.example.com/weather?city{city} resp requests.get(url, timeout5) resp.raise_for_status() data resp.json() return { city: data[city], temperature_c: data[temperature_c], condition: data[condition] }再看库存工具的实现# tools/impls/inventory_tool.py # 模拟仓库库存数据 INVENTORY { (A101, east_1): {quantity: 120, sku: A101}, (A101, south_2): {quantity: 35, sku: A101}, (B202, east_1): {quantity: 0, sku: B202}, } def query_inventory(sku: str, warehouse: str east_1) - dict: 查询指定SKU在某仓库的库存数量 key (sku.upper(), warehouse) record INVENTORY.get(key) if record is None: return {sku: sku, warehouse: warehouse, quantity: 0, status: no_record} return { sku: record[sku], warehouse: warehouse, quantity: record[quantity], status: ok }注意这两个函数都返回统一的dict结构字段名是固定的温度查询返回temperature_c、condition库存查询返回quantity、status。这样做是有原因的——统一返回结构能大幅降低模型对结果的误读概率。如果今天一个工具返回quantity明天另一个返回qty模型在长上下文里很容易混淆字段含义。对应的manifest放在tools/manifests/目录里{ name: get_weather, description: 根据城市名查询实时天气。当用户询问某地温度、天气、是否下雨时使用。, parameters: { type: object, properties: { city: {type: string, description: 城市中文名例如上海、广州} }, required: [city] }, scope: read_only, sensitive: false }库存工具的manifest类似这里不重复贴了。我想强调一点description的写法直接影响模型的工具选择准确率。写“当用户询问某地温度、天气、是否下雨时使用”就比写“查天气”强得多——前者明确告诉模型触发条件后者太含糊。这个点我踩过坑后面章节专门说。3.3 主流程把工具交到Agent手里现在把工具挂到Agent-Reach的编排器上。核心流程只有五步接收用户请求、带工具描述请求LLM、模型决定调用哪个工具、执行工具、把结果回填给模型生成最终回答。# core/orchestrator.py import json from tools.registry import get_manifests, execute_tool from utils.llm_client import llm_completion def agent_reach_loop(user_message: str, history: list) - str: tools get_manifests() # 从注册中心读取所有工具manifest messages history [{role: user, content: user_message}] response llm_completion( messagesmessages, toolstools, # 关键把工具描述传给模型 tool_choiceauto ) # 模型可能直接回答也可能要求调用工具 if response.tool_calls: for call in response.tool_calls: result execute_tool(call.function.name, json.loads(call.function.arguments)) messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result) }) # 把工具结果交还给模型生成面向用户的最终回答 final_response llm_completion(messagesmessages, toolstools) return final_response.content return response.content这段代码去掉了很多异常处理但已经能说明Agent-Reach的运作机制。注意一个细节每次tool_calls执行完之后我又调用了一次llm_completion而不是直接拿工具结果拼句话返回。为什么因为模型需要看到“用户提问工具结果”两个信息才能组织一个用户真正看得懂的回答。比如工具返回quantity:35模型会结合用户问题生成“A101在南仓还剩35件低于100件已准备通知仓管”。3.4 一次完整跑通的输出长这样我用一个具体例子展示Agent-Reach实际跑起来的效果。用户输入是广州仓还剩多少件A101如果低于100件帮我通知仓管补货。Agent-Reach的日志输出大致长这样[User] 广州仓还剩多少件A101如果低于100件帮我通知仓管补货。 [Router] 识别到工具候选: query_inventory [Agent] 选择工具: query_inventory, 参数: {sku: A101, warehouse: south_2} [Executor] 执行 query_inventory, 耗时 42ms, 返回: {sku: A101, warehouse: south_2, quantity: 35, status: ok} [Agent] 工具结果: 库存35件少于100启动补货通知流程... [Agent] 选择工具: send_notification, 参数: {recipient: warehouse_manager, message: A101在南仓库存仅剩35件请及时补货} [Executor] 执行 send_notification, 耗时 120ms, 返回: {status: sent} [Agent] 最终回答: 广州仓的A101目前还剩35件已经低于100件我已通知仓管安排补货。这组日志是我实际项目里Agent-Reach输出的简化版。值得注意的点是Agent在一次任务里连续调用了两个工具——先查库存再根据查到的数值决定是否发通知。这说明它不只是机械地“调一次工具”而是能根据中间结果做二次决策。这也是Function Calling方案比简单的“把工具提示词拼进Prompt”强的地方——模型真正拥有一个“推理-行动-观察”的循环而不是一次性吐完答案。4. 跑起来之后真正要盯的五件事4.1 可观测性每次工具调用都要留痕我见过太多Agent项目Demo阶段跑得飞起一上生产就抓瞎——因为没人知道Agent到底调了哪个工具、填了什么参数、成功了没有。Agent-Reach从第一天就要求所有工具调用写结构化审计日志每条日志一行JSON方便喂给日志平台做聚合分析。{event: tool_call, ts: 2025-06-11T15:34:2208:00, agent_id: demo-01, tool: query_inventory, args: {sku: A101, warehouse: south_2}, latency_ms: 42, success: true, result_summary: quantity35} {event: tool_call, ts: 2025-06-11T15:34:2308:00, agent_id: demo-01, tool: send_notification, args: {recipient: warehouse_manager}, latency_ms: 120, success: true, result_summary: statussent}日志里我刻意不记录完整返回体只记录result_summary原因是完整返回可能包含业务敏感数据落日志等于扩大数据暴露面。需要排查问题时再根据trace_id去查对应时间段的工具返回详情——这个设计是安全团队给我提的意见我觉得很对。有了这些日志你才能回答一个最基本的问题“Agent刚才到底干了什么”。没有日志排查就像闭着眼睛找针。4.2 大返回会撑爆上下文必须提前处理工具返回结果是要回填给模型的但模型上下文窗口有限而且塞太多不相关信息会稀释注意力。一个常见的翻车场景是查询工具有分页返回一次返回几百条记录模型直接“看不过来”然后开始胡编乱造。Agent-Reach的解法有三层Manifest里声明max_return_bytes执行器在返回前截断设置默认limit参数让工具只返回前N条对于长度不可控的返回内容先做摘要再回填。举个例子搜索知识库的工具可以返回“前5条结果的标题相关性得分”而不是把五篇全文都塞回去。如果模型真的需要看全文它可以在后续轮次调用“知识库详情查询”工具按ID获取。这种“先给摘要、按需取详情”的设计既省token又降低了模型处理复杂信息的错误率。4.3 权限失控往往发生在“你以为没人会这么用”前面提到过群发邮件的例子这里再补充一个更隐蔽的。库存查询工具最初没有做仓库代码校验测试时模型对“有哪些仓库”这个问题直接遍历了manifest里所有参数枚举值把每个仓库都查了一遍。从功能上没错但这种批量探测行为一旦被恶意利用就会变成数据爬取通道。我的经验是所有工具接入前都要过一遍“最坏情况”推演——如果用户故意构造极端输入这个工具会被用来干什么单条查询功能在“读取全部数据”的场景下就会成为漏洞。所以Agent-Reach给所有查询类工具统一加了rate_limit单用户每分钟最多调用20次、参数枚举校验仓库代码必须命中配置表、返回字段白名单不匹配的字段直接丢弃。权限控制不是一劳永逸的每加一个工具都要重新做一遍边界检查。4.4 从只读任务开始灰度把Agent-Reach接到真实业务系统时我的顺序非常保守先接只读工具查库存、查进度、查价格运行1~2周观察工具调用准确率、返回格式解析成功率、用户真实问题的覆盖度。确认稳定后再接低风险写操作创建草稿、提交预约单然后再接需要审批的高风险操作退款、批量通知、删除数据。每上一层都要配上对应级别的审计和确认机制。比如“创建草稿”只需要在回答里明确告知用户“已创建”“批量通知”就必须弹确认窗口“退款”则要触发人工审批流程。这么做的原因很简单一旦Agent的动作直接影响真实业务数据出错的代价是几何级上升的。灰度不是胆小而是给自己留足观察和修正的空间。4.5 多步任务的中断与补偿Agent执行多步工具调用时最怕中途失败——比如第一步查库存成功第二步发通知失败。这时候如果Agent直接说“已通知”就是事故。Agent-Reach里我加了两个机制一个是步骤级重试对网络抖动类错误自动重试最多2次另一个是结果确认工具成功返回后必须在最终回复中明确引用不能凭推测。更重要的是状态记录。每一步工具调用的结果都要存进会话上下文的memory里这样即使某一步失败Agent也能基于真实状态给用户一个准确回答“我已经查到了库存剩35件但通知发送失败了需要我重试吗”而不是含糊地说“操作完成”。5. 常见问题与排查实录5.1 工具返回格式不稳定Agent就开始胡说这是Agent-Reach上线后我遇到最多的一类问题。外部接口偶尔会变字段名比如天气接口今天返回temperature_c明天变成了temp。Agent拿到不认识的字段不会说“我不认识”而是会尝试猜一猜就错。解法是三层第一所有工具返回都必须经过一个normalizer统一字段名后再进入Agent上下文第二在manifest里显式声明schema版本号接口有变就升级版本第三对于可能为空的字段默认给一个安全值比如缺温度就填null并让工具描述里说明“温度可能为null”。这套组合拳之后误读率下降了差不多80%。5.2 Agent死活不调用工具宁可硬答模型能调用工具却不调用宁可编一个答案——这种情况在早期经常出现。原因通常是两种一个是工具描述触发了“使用门槛”太高模型觉得没必要、风险大另一个是系统提示词里没有明确告诉模型“你有工具可用”模型默认走纯文本回答。我的解决方法是把这句话写进系统提示词“你是一个可以操作真实系统的助手。当用户的问题涉及库存、天气、通知等功能时你必须调用对应工具获取真实数据禁止凭空猜测。”同时给模型提供一两个few-shot示例教它什么时候该用工具、什么时候不该用。加完few-shot后工具调用率从60%左右涨到了95%以上。5.3 工具返回太大把上下文塞爆如果工具是一次性返回一个巨大的JSON数组Agent-Reach会把整个数组回填给模型。轻则浪费token重则模型在后续轮次里完全丢失重点。排查时我会先看日志里工具返回的字节数如果经常超过2KB就说明工具设计有问题——正确的做法是给查询类工具都加上limit参数只返回需要的字段和数量。再配合max_return_bytes的硬约束这个问题基本能被堵住。5.4 日志记录要能支撑回溯很多人以为日志就是随便打两行print真到排查时就傻眼了。我建议日志至少包含trace_id一个完整用户请求的所有步骤共享、agent_id哪个Agent实例、tool_name、args参数、latency_ms耗时、success是否成功、error_msg失败原因、result_summary结果摘要。有了trace_id你在查“用户说收到了错误补货通知”时能用一条ID把整个调用链捞出来一眼看到是哪一步判断出了问题。没有这条ID日志就是一堆无法串联的信息碎片。5.5 排查优先级速查表现象优先排查项常见原因Agent调错工具工具描述是否清晰description写得太泛触发条件不明工具调用成功率低参数校验规则必填参数漏定义、枚举值过严返回结果模型看不懂返回结构是否统一字段名不一致、缺少示例值每轮回答都慢工具超时配置外部接口慢未设超时熔断用户收到虚假成功提示结果回填逻辑工具失败后模型未感知仍按猜测回答这张表是我让团队排查问题时先对照的标准流程。大多数看起来玄乎的Agent故障最后都会落到工具描述、参数规则、返回结构、超时控制这四个基础环节上。 结尾 Agent-Reach从最初一个解决“ChatGPT不会干活”的原型慢慢长成了一套我每天在用的接入方案。我个人最大的体会是AI Agent的工程化难点根本不在模型选型而在“模型-工具-边界”这三者的关系设计。模型负责判断和表达工具负责执行和返回边界负责安全和控制。三者各司其职Agent才能从玩具变成生产力工具。最后分享一个实用的小技巧每次新增或修改工具描述之前先在离线环境跑一遍固定的回归用例集比如10个典型用户问题看工具选择准确率和回答正确率的变化。如果准确率下降回退到上一个版本排查。我靠着这个习惯避免了至少三次“上线即事故”的尴尬。这个项目后续还可以往工具自动编排、多Agent协作、语义工具检索几个方向扩展每一步都有很多东西可挖但先把触达这件事做扎实是永远不会错的第一步。
返回列表