ARTICLE DETAIL

资讯详情

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

Agent-Reach:构建LLM工具调用与外部触达层的工程实践

Agent-Reach:构建LLM工具调用与外部触达层的工程实践 做个Agent项目我最常被问的一句话是你的智能体到底能够到什么这不是句废话。LLM再聪明它的世界也只到训练数据截止那一刻为止想要查实时股价、改工单状态、发一封邮件它全都做不到。这就是为什么我把这个项目命名为Agent-Reach——Reach就是触达让智能体真正把手伸到外部世界去执行操作而不只是停留在对话框里陪你聊天。这篇内容我会从Agent-Reach要解决的核心问题讲起拆一拆触达层的设计原理、ReAct循环和工具注册表再把我落地过程中踩过的坑和排查思路完整捋一遍。做AI Agent应用、做RAG、做自动化流程的团队或者准备在项目里引入工具调用能力的朋友应该都能从中找到一些可直接用的经验。1. Agent-Reach到底解决什么问题从能聊天到能办事的那一段路1.1 对话式AI的边界大模型再聪明也不该靠猜来做事大模型的本质是预测下一个token它的知识来自训练语料而训练语料是有截止日期的。你把一个实时性问题丢给它比如现在北京到上海明天最早的航班几点它可以给你一个像模像样的答案但那个答案是猜出来的不是查出来的。在简单问答场景里猜错了顶多被笑话一句可如果这个智能体被接入了业务系统猜错一个订单状态、一个库存数字那造成的损失就不是抖个机灵能糊弄过去的了。Agent-Reach这个项目的出发点非常朴素让模型在需要外部信息的时候明确知道自己不知道然后通过工具去获取。模型负责推理和决策触达层负责执行和取数两边各干各擅长的活儿。听起来简单但真正落地的时候你会发现这里面的细节比想象中多得多——工具怎么描述、结果怎么回传、上下文怎么控制、失败怎么恢复每一环都能决定这个Agent是能用还是仅仅是Demo。1.2 手和眼的分工触达不只是调API我最初的设计文档里写的是Agent调用外部API后来被自己推翻了。因为调用API只描述了动作没有描述闭环。Agent真正需要的是三件事认知模型根据用户意图决定需要查什么、需要做什么这是大脑的部分触达工具层按模型的决策去实际调用系统API、执行脚本、查询数据库这是手感知外部系统返回的数据经过加工再变成模型能理解的Observation回传让模型看到结果变成了什么这是眼。Agent-Reach这个名字里的Reach我后来把它解释成了这三件事的总和。只做API封装不叫Reach真正的Reach是让外部世界对模型可见、可操作、可反馈。你在提示词里给模型塞一堆文档那不叫触达那叫灌输。触达是动态的——查询的时候才去取数操作的时候才去改状态这是本质区别。1.3 我瞄准的几个典型场景在设计Agent-Reach的早期我列了一个表格把应用场景按触达方式做了分类这决定了后面工具层的整体组织思路。场景类型典型例子触达方式关键难点实时数据查询天气、股价、航班状态只读API数据源多、格式杂需要统一Schema业务系统操作更新工单、创建订单、修改配置读写API权限控制、幂等性、操作审计私有知识检索查公司制度、查历史项目文档向量库全文检索召回质量、引用溯源自动化流程发邮件、建日程、跑定时脚本脚本/三方服务失败恢复、状态同步这个表格在我后来的开发里反复用到因为每个场景对Tool层的要求都不一样。只读查询容忍稍微慢一点的响应但写操作必须考虑重放和幂等知识检索要的是结果精而不在多所以返回截断逻辑和查询改写是重点流程自动化则对状态管理要求极高Agent执行到一半停了重跑会不会发两封一模一样的邮件这类问题必须在设计阶段就想清楚。2. 核心原理拆解ReAct循环、工具注册表与统一的触达协议2.1 ReAct循环先想一步再动一下Agent-Reach的推理引擎用的是ReAct模式也就是Reasoning和Acting交替进行。每一轮循环里模型先输出Thought描述它打算怎么处理再输出Action决定调用哪个工具工具执行完把结果作为Observation返回模型再基于Observation决定下一步。这个循环会一直持续到模型认为目标已经达成输出Final Answer。# agent_reach/core/loop.py def run_agent(query: str, tools: dict[str, Tool], max_steps: int 8): messages [{role: system, content: SYSTEM_PROMPT}] messages.append({role: user, content: query}) for step in range(max_steps): response llm.chat(messages) messages.append({role: assistant, content: response}) action parse_action(response) if not action: return final_answer(response) result execute_tool(tools, action.name, action.arguments) messages.append({ role: system, content: fObservation: {truncate_and_summarize(result)} }) return fallback_message(steps exceeded, please retry)为什么不用一次性规划完整个任务再统一执行我试过很快就明白了原因。外部系统的状态时刻在变一步的执行结果会直接影响下一步的决策。比如你要先查用户当前的会员等级再决定给他推哪种优惠券如果第一次查询结果没拿到就规划后续那后面的动作全是猜的。ReAct的价值就是让模型每一步都基于最新的事实做决策而不是基于推理时的假设。2.2 工具注册表把外部能力暴露给模型模型怎么知道有哪些工具可用答案是靠工具注册表。在Agent-Reach里每个工具都用一份结构化的描述注册进来包括工具名称、功能说明、参数Schema。大模型根据这些描述来决定调用哪个工具、传什么参数本质上工具描述就是写给模型看的API文档。{ type: function, function: { name: get_flight_status, description: 查询航班的实时动态。当用户询问航班是否准点、起飞到达时间时使用。, parameters: { type: object, properties: { flight_no: { type: string, description: 航班号例如CA1234 }, date: { type: string, description: 日期格式YYYY-MM-DD默认今天 } }, required: [flight_no] } } }这段代码里description部分是我踩过最多坑的地方。一开始我写的是查询航班状态结果模型在用户问CA1234几点落地这种并不带状态字眼的问题时就不太会选择这个工具。后来我把描述改成了当用户询问航班是否准点、起飞到达时间时使用命中率立刻就上来了。写工具描述的铁律是说清楚功能叫什么不重要说清楚什么情况下调用它才重要最好把用户最可能的口语化问法和场景都揉进去。2.3 统一触达协议为什么不能让Agent直接写死SDK早期开发时团队里有人提了一个方案直接把各个系统的SDK引入Agent代码要调什么就写什么。这个方案在Demo阶段跑得很欢但一接入第三个系统就开始乱了。每个SDK的鉴权方式不同、错误码不同、数据结构不同模型在切换不同系统时经常把A系统的参数格式用在B系统的调用上。Agent-Reach后来借鉴了MCP的标准协议思路但不是照搬。我们把所有外部能力统一封装成工具服务每个工具服务对外只暴露统一的方法、参数Schema、错误码规范内部再各自去适配真实系统的SDK或REST接口。模型只跟统一协议打交道统一鉴权Agent层不感知各系统的密钥触达层统一注入和执行权限校验统一错误码外部系统报错之后触达层翻译成模型能理解的错误信息统一返回格式无论底层是JSON还是XML统一转成结构化的数据字典。这样做的直接好处是新增一个外部系统只需要写一个适配器给模型注册新工具不需要改推理循环也不需要在提示词里给模型讲一堆各系统的认证细节。3. 落地过程中的几个关键设计从Demo到可用的距离3.1 工具粒度怎么定不是每个API都适合直接暴露给模型工具粒度的设计我前后走了两遍弯路。第一版我图省事把底层系统的API几乎1:1暴露出去一个查询航班接口配一个工具一个查询天气接口配一个工具结果模型在一堆功能类似的工具之间反复横跳选择错误率很高。第二版我又走了一个极端把多个操作揉成一个神级工具参数多达十几个模型根本填不对。最终定下来的原则是粒度按用户可感知的意图来切而非按后端接口来切。后端接口是获取航班基础信息接口获取航班延误详情接口但用户意图只有一个查航班状态所以Agent-Reach只注册一个get_flight_status内部自己去决定调哪个后端接口。工具参数尽量控制在5个以内超过5个就要怀疑是不是拆分粒度不够。从实测来看5个参数以内的工具模型一次填对的概率明显更高这个结论在多个模型上都是稳定的。3.2 上下文管理触达一次不代表全都能记住工具返回结果是要塞进上下文里给模型看的但塞多少是个门道。刚开始我把一次查询返回的5000条航班记录全都塞进messages结果模型看着一屏幕密密麻麻的数据直接开始编造一些根本不存在的结论。后来又出现过一次类似事故一个日志查询工具返回了几万token把上下文窗口冲得只剩下很少的余量模型连基本的推理能力都保不住了。我在Agent-Reach里加了一批结果预处理函数原则就三条先截断、再摘要、后结构化。# agent_reach/core/observation.py def compress_observation(raw_data: dict, max_chars: int 1200) - str: if len(raw_data) max_chars: return json.dumps(raw_data, ensure_asciiFalse, defaultstr) summary_data { total_count: len(raw_data) if isinstance(raw_data, list) else 1, head_preview: raw_data[: int(max_chars / 80)] if isinstance(raw_data, list) else raw_data, note: 结果过长已截断。如需要更详细信息请缩小查询范围后再试。 } return json.dumps(summary_data, ensure_asciiFalse, defaultstr)注意返回文本末尾那一句如需要更详细信息请缩小查询范围后再试这句话很重要。它不是写给用户看的是写给模型看的。当模型发现拿到的数据只是摘要时它会自己决定要不要发起一个更精确的查询而不是强行根据残缺数据给结论。这算是Agent-Reach里一个低成本但提升明显的设计。3.3 权限与审计Agent能触达的越多风险越大越往后做我越明白Agent比传统程序更难控制风险因为它的触发链路有一个非常不稳定的因素——大模型可能会产生预期之外的调用序列。所以Agent-Reach的权限设计从一开始就遵循最小权限原则所有工具默认不可用按实际场景逐项授权。高危敏感操作一律需要人工确认我加了一个前置拦截逻辑。当模型请求调用高风险工具时Agent先不执行而是把调用意图转成一条待确认消息发给用户用户点确认后再放行。另外一个必不可少的部分是审计日志每一轮工具调用都记录完整的入参、出参、调用者、时间戳和链路ID。说一个我们自己的真实教训。测试环境的数据库写入工具曾经不小心暴露给了Agent当时只是为了让模型能演示创建测试订单的功能。结果有一次模型在回答用户闲聊的过程中不知道哪根弦搭错了自己发起了一次写操作把一条脏数据写进了测试表。虽然测试环境没有真实业务影响但那次事故之后我意识到了一个问题权限不仅仅取决于谁调用了工具还取决于模型在什么场景下会触发什么调用。闲聊场景就该禁用写工具这是后来我在权限模型里专门加的策略规则。4. 踩过的坑与排查链路Agent-Reach为什么总在执行到一半卡住4.1 工具描述不清导致模型反复选错现象有一阵子get_flight_status工具被调用的成功率很高但参数正确率很低。模型经常把CA1234和2025-06-01传到一个只接受flight_nodate的工具里甚至有时候会把航班号传给一个完全不相干的天气工具。排查链路我先去翻了Agent-Reach的日志发现模型根本不是选错工具而是对date这个参数的理解有偏差。工具定义里写着日期格式YYYY-MM-DD默认今天模型在用户说明天的时候就自己把明天换算成了一个具体日期这本身没错但当用户说查一下后天CA1234的时候模型经常直接把这个具体日期传进去没有意识到用户提供的日期可能是用户所在时区的日期而查询系统用的是UTC。修复我在参数描述里加了一句话date参数请基于用户所在时区理解若用户未强调日期则使用今天并在描述末尾附上可枚举的日期示例。改动之后参数正确率提升明显。工具定义是给模型看的API文档文档里模糊一个字调用链路上就会放大成一次错误。4.2 返回结果太长把上下文挤爆现象模型连续正常执行了几个步骤之后开始输出一些与用户问题完全无关的内容。比如用户问帮我查一下最近的三笔订单模型查完以后突然开始讲订单系统是公司自研的基于微服务架构...。排查链路这显然不是模型变傻了而是上下文被撑爆了。我看了日志里的token用量统计发现有一个订单列表工具返回了完整的订单详情字段包含备注、物流轨迹、售后记录等等每单差不多3000 token三笔订单加上系统提示词直接把可用上下文长度推到了极限附近。模型在残余的一小段上下文里开始自由发挥就产生了那些离谱的输出。修复压缩工具返回限制列表接口只返回核心字段订单号、金额、状态、日期并且对接下来的查看详情动作做专门的子工具让模型按需取详情而不是一次全给。我还要提一个细节建议在循环里加一个可用上下文的实时水位检查剩余量低于阈值时主动提示模型上下文空间不足请直接给用户最终答复能有效阻止后半段胡言乱语。4.3 Agent在错误触达后不会自己纠正现象某个内部接口偶尔会超时返回500Agent在拿到错误结果后并没有换一种方式重试而是用一模一样的参数又发起了四次调用全部超时最终报错退出。排查链路我看了循环里的消息序列问题出在Observation里。工具层返回的错误信息是Internal Server Error对模型来说这就是一条无意义的失败信号它既不知道错在哪也不知道怎么办只能笨办法原样重试。大模型的笨不是不够聪明而是信息不足时它会选择最保守的路径也就是复制上一次的动作。修复我改写了触达层的错误处理逻辑凡是工具失败返回给模型的信息必须包含三层错误类型、可以采取的建议动作、重试上限。Observation: tool call failed error_type: remote_timeout suggestion: 该接口偶发超时建议等待2秒后重试若重试两次仍失败请直接告知用户系统繁忙 retry_count: 1/3加上这套结构之后模型的自我纠错能力瞬间正常了。它在第二次超时后会主动选择等待重试连续三次失败后会老老实实告诉用户系统暂时不可用。想让Agent稳定不是靠把提示词写得多巧而是要把错误信息翻译成模型听得懂的下一步建议这个经验我觉得值得每个做Agent的人抄走。4.4 多工具组合时的状态同步现象有个场景需要先查订单当前状态再执行更新订单备注。日志显示模型先调用查询拿到status是pending然后又发起更新备注请求但更新工具的入参里带的是旧状态值导致数据库校验失败。排查链路表面上看着像流程顺序错了实际上问题出在Agent-Reach的组合状态管理上。查询和更新虽然是两个独立工具调用但它们操作的是同一个业务实体。模型在执行第二个工具的时候携带的还是第一个工具查询时候的状态快照没有意识到这个实体在两次调用之间已经变了。修复我给工具层引入了请求级别的实体状态缓存同一轮会话里操作同一实体时第二次调用前会重新拉一次最新状态并把这个操作前状态和操作后状态都记录进日志。另外工具定义里也加了说明update_order_note在执行前会自动获取订单最新状态请勿依赖此前查询结果。这句话有效避免了模型传旧数据。多工具组合的场景里状态一致性是最容易被忽略的坑你永远不会在Demo里遇到但一上真实业务就会冒出来。5. 实测效果与调参心得让Agent-Reach稳定工作的小细节5.1 一组实测数据做完整套框架后我用三个典型场景跑了对比测试一组是裸模型只给系统提示词不给工具一组是Agent-Reach完整链路。数据是在同样的测试集上测的每个场景50个问题。场景裸模型准确率Agent-Reach准确率平均交互轮次平均耗时实时航班状态查询34%92%1.83.2s订单信息多轮查询21%87%3.46.8s知识库流程操作12%81%5.19.5s裸模型在实时数据类场景的低准确率并不意外因为它本质上是在猜。让我印象更深的是平均轮次这个指标Agent-Reach在订单多轮查询场景下平均要3.4轮才能完成这意味着有一半的问题需要两次以上的工具调用所以上下文水位控制、重试策略、错误结构化的好坏会直接决定这类场景的天花板。5.2 模型选型与参数调整Agent-Reach的推理核心对模型能力的要求和纯文本对话有所不同。我实测了几类模型结论是工具调用的稳定性跟模型参数量不是单纯线性关系跟训练数据里是否包含高质量的工具调用语料关系更大。几个我实际在用且有效的参数设定temperature工具调用场景我固定设成0.2。太高会让模型自己发挥出各种奇奇怪怪的参数值太低又会让模型在对话里显得机械死板0.2是一个测试了一圈之后折中最稳的值。max_tokens给每次模型生成留足够余量因为输出里可能包含Thought加Action两部分我设的是800留白多一点能避免模型生成到一半被截断完整性对工具调用至关重要。系统提示词只写场景约束和不做什么不写具体工具用法。工具用法全部放工具定义里杜绝提示词和工具定义两套说明互相矛盾的情况。5.3 日志与可观测性复现问题必须先能回放Agent-Reach的日志系统是我后来重新做过的部分。一个Agent的失败排查和传统程序很不一样——传统程序打印几行堆栈就能定位Agent的问题出在模型在那个时刻收到了什么信息、做出了什么决策没有完整的回放链路几乎无从下手。我的做法是每一轮循环都记录完整的结构化日志{ chain_id: abc123, step: 2, thought: 用户需要查询最新航班状态先用航班号查今天日期作为默认, action: get_flight_status, arguments: {flight_no: CA1234}, observation_preview: CA1234 准点到达 18:35, token_used: 1240, latency_ms: 380 }chain_id把一次完整会话的所有步骤串起来排查的时候直接按chain_id拉出整个决策链看模型在第几步收到了什么信息能直接复现大多数问题。这个习惯我强烈建议所有做Agent项目的人养成别等问题出现了再补日志到时候你已经补不出来了。5.4 后续扩展方向Agent-Reach目前完成度大约算70%剩下30%我在思考几个方向。一个是多Agent协作下的触达隔离不同Agent各管一摊工具中间通过消息总线传结果这样权限边界更清晰。另一个是智能重试与降级策略不只是超时重试而是根据历史成功率动态决定是否换用备用数据源。还有一块是把工具调用的效率和合规结合起来在异步场景下如何平衡Agent自主性和操作可追溯这块我们还在摸索。Agent-Reach做完后我最大的感受是真正难的不是让Agent调通一个API而是让它在一个充满不可靠、不一致、会变化的外部世界里稳定地完成一件完整的事。工具描述怎么写得清楚错误信息怎么给得有用上下文怎么分配合适每一件都是小事但叠加起来就是一个Agent从能跑到好用的全部距离。如果你也在折腾类似的触达层设计我建议别急着堆功能先把底层的工具注册、错误回传、日志回放这三件事打牢再去谈Agent多聪明会省下大量的返工时间。
返回列表