
把 Agent-Reach 跑通的那一刻我最大的感受是终于有办法在 Agent 动手调用工具之前告诉它“这条路走不通”而不是等它把用户的工单系统翻个底朝天之后再丢给我一句“好像有点问题”。做过真实 Agent 项目的人都知道多轮工具调用里最贵的从来不是 API 费用而是模型在错误路径上反复试探浪费掉的轮次和上下文。今天我不讲那些“Agent 会越来越聪明”的概念直接把我这套解决任务触达问题的框架设计、接入过程和实测数据摊开聊。1. 为什么Agent会“够不着”目标任务可达性问题的本质先下一个我能接受的定义Agent 任务可达性指的是在给定的初始状态、工具集合和目标状态之间是否存在一条可执行的工具调用路径让 Agent 在有限步之内完成任务。大多数团队做 Agent 时只关注“模型能不能理解意图”但真实业务里最常翻车的恰恰是后半段——意图理解了工具链却走不通。1.1 三种典型的触达失败现场我测试过不少 Agent 项目暴露出来的失败方式几乎可以归纳成三种。第一种是目标漂移。用户让 Agent“查一下华东区所有客户上季度的退款订单汇总金额写进周报并抄送主管”。Agent 一开始执行得很好查了订单列表但写周报时觉得数据不够丰富主动去搜索行业新闻搜着搜着就把“退款订单金额”这个核心目标丢了。最后生成的周报里全是行业分析没有金额汇总。这不是模型能力差而是缺少一个机制让它始终知道“我要抵达的终点是什么”。第二种是路径断裂。任务依赖多个工具而且工具之间有先后关系比如先查订单号、再查订单详情、最后生成对账单。如果模型没意识到第二步需要第一步的输出作为参数或者干脆跳过某一步去调用一个不存在的函数路径就断了。我见过一个很典型的例子模型直接把用户提供的“公司名称”塞进“查询订单详情”工具的order_id参数因为这两个概念在语义上容易混淆实际运行时必然报错。第三种是上下文膨胀导致的“注意力够不着”。工具返回结果通常是大段 JSON多轮调用后所有历史输出全部堆在上下文里。模型不是不想用早期数据而是它在几百行 JSON 里看不到那个关键字段。有次我数了一下一个只有三步的工具链第一次查询返回了 8000 多字的商品列表到第三步时模型已经完全不记得第一步筛选出的商品编号。1.2 把任务执行看成一次“路径可达”问题这三种失败有一个共同点系统缺少对“目标是否能够达到”的显式判断。我在做 Agent-Reach 之前反思了很久最后决定把一次任务执行抽象成一个路径问题。假设当前系统状态是S0用户期望的目标状态是G工具集是T。Agent 要做的事情就是找到一条路径P {t1, t2, ..., tn}依次调用之后让系统从S0迁移到G。如果找不到这样的路径那么任务在当前工具集下是不可达的正确做法是直接告诉用户“做不到”并说明缺少哪种工具或信息而不是让模型硬着头皮猜。这个思路很像你用导航去陌生地址。普通 Agent 的模式是先开上车往大概方向走走到路口发现封路了再重新规划Agent-Reach 的模式是出发前先把路网、限行、营业时间都算一遍如果确实到不了导航会直接告诉你“该地址无法到达”而不是带你绕圈。这也是 Agent-Reach 名字的由来——我不关心模型是不是足够“聪明”我只关心它能不能触达任务目标以及怎么验证“不能触达”这件事。2. Agent-Reach的架构设计在模型之外补一条“可验证的路径”确定问题边界之后我开始设计 Agent-Reach 的整体结构。设计原则只有一条路径规划用确定性算法语言理解用大模型两边职责分开。这样既避免模型在多步依赖上记忆力不足的问题又保留了模型对自然语言的泛化能力。2.1 核心组件目标解析器、工具图、可达性验证器、执行器Agent-Reach 包含四个核心组件每个组件只做一件事。目标解析器Goal Parser负责把用户的自然语言指令转成结构化目标状态。它不是简单地把文本丢给 LLM 做意图分类而是要求输出一个类似“状态字段表”的结构。比如“找出张三上个月所有包含‘发票’关键字的邮件并汇总金额”解析结果大致是{ goal: email_amount_summary, target_object: email, filters: { sender: 张三, time_range: 2025-06-01~2025-06-30, keyword: 发票 }, output: { type: sum, field: amount, format: yuan }, anti_goals: [不得发送邮件, 不得删除邮件] }anti_goals这个词后面会单独讲它是 Agent-Reach 比普通 Agent 多出来的关键设计。工具图Tool Graph是整个框架的地图。它是一个有向图每个节点是一个工具每条边表示“前一个工具的输出可以作为后一个工具的输入”。我在注册工具时要求开发者同时声明输入输出 schema以及输出字段到后续工具参数的映射关系。搜索邮件工具会输出email_ids字段这个字段可以通过映射关系连接到邮件详情工具的email_id参数工具图上就自然形成一条边。可达性验证器Reachability Validator负责两件事第一在规划出来的路径上做 schema 兼容性检查包括参数类型、必填项、取值范围第二在沙箱模式下用模拟数据做一次“干跑”不实际调用外部 API只验证连续调用过程中每一步的输入能否从前一步的输出中取到。这一步能拦截掉大量“参数名看着对、实际对不上”的问题。执行器Executor是真正在真实环境里执行路径的模块。它记录了每一步的执行轨迹、中间状态、token 消耗更重要的是它支持局部重规划。当某一步执行失败时它不会像普通 Agent 那样从头开始新会话而是回到失败节点的前一个可达状态从那里重新规划后续路径。2.2 为什么把“规划”放在模型外面很多 Agent 框架把工具选择、参数填写、路径规划全部交给模型看起来灵活实际不稳定。原因很简单大模型擅长的是语义联想不是精确的状态转移。如果让模型规划一个“查询订单 → 订单详情 → 金额汇总”的三步链它通常能答对但当业务工具有三十个、状态依赖关系上百条时模型就会开始编造不存在的调用关系。Agent-Reach 的思路是把路径规划从“模型自由发挥”变成“图搜索”。模型只承担两件它擅长的事把用户意图翻译成目标状态以及从工具输出文本中提取语义字段。路径是否存在、路径是否可行、哪些工具之间有依赖全部由确定性的工具图和验证器决定。这样做的直接好处是模型不再有机会“凭空想象”一条没有依赖关系的路径因为它只能从已验证的路径集合中选择执行。我在早期原型里试过让模型直接调用图搜索接口效果反而不稳定。后来改成“模型不接触图结构只填目标和参数”成功率大幅上升。这说明 Agent-Reach 的核心价值不是增加更多智能而是把不可控的部分用工程手段锁死。3. 从零接入Agent-Reach核心接口与一次完整的任务跑通Agent-Reach 的接入方式不复杂但有几个接口设计上的细节值得说一下。我先给一个最小可运行的流程再解释为什么这么设计。3.1 安装与工具图声明目前项目以 Python 包形式提供源码安装方式如下git clone https://github.com/yourname/agent-reach.git cd agent-reach pip install -r requirements.txt安装完成后第一件事是声明工具图。以“查客户订单并汇总金额”为例from agent_reach import ToolGraph graph ToolGraph() # 注册第一个工具按客户名查订单列表 graph.add_tool( namesearch_orders, description按客户名称查询订单列表, input_schema{ customer_name: {type: string, required: True}, time_range: {type: string, required: False} }, output_schema{ order_ids: {type: array, items: {type: string}}, total_count: {type: integer} }, side_effectread_only ) # 注册第二个工具查订单详情 graph.add_tool( nameget_order_detail, description根据订单ID获取订单详情, input_schema{ order_id: {type: string, required: True} }, output_schema{ amount: {type: number}, status: {type: string} }, side_effectread_only ) # 声明工具之间的数据依赖关系 graph.connect( from_toolsearch_orders, to_toolget_order_detail, mapping{order_ids: order_id} )这里最关键的参数是side_effect。它声明工具是否会产生副作用read_only表示只读操作。这个字段在后面的自动回退机制里会用到我现在先标记上。3.2 一个跨工具链任务的完整执行示例工具图建好之后完整跑一个任务只需要四步from agent_reach import ReachPlanner, ReachabilityValidator, ReachExecutor # 1. 解析用户目标 planner ReachPlanner(graphgraph, llmllm) goal planner.parse(查一下张三在上个月的订单并汇总所有订单金额) # 2. 规划路径 plan planner.plan(goal) # 输出类似[search_orders - get_order_detail(get_order_detail 需要循环执行)] # 3. 验证路径 validator ReachabilityValidator(graphgraph, sandboxTrue) is_valid, report validator.validate(plan) if not is_valid: print(路径不可达原因, report.error_hint) else: # 4. 执行 executor ReachExecutor(graphgraph, llmllm, max_replan2) result executor.run(plan) print(result[summary])注意第 3 步的sandboxTrue这会让验证器用模拟数据检查参数链路不会真实调用张三的订单查询接口。只有验证通过之后第 4 步才会进入真实执行。这一步能提前拦下大量“工具调用格式错误”的问题。3.3 为什么这样设计接口我见过不少 Agent 框架把规划、验证、执行全部揉进一个agent.run(prompt)看起来很优雅但出了问题特别难排查。Agent-Reach 故意把流程拆成四个阶段每个阶段的内容可以单独调试、单独缓存。plan和validate分离核心原因是验证结果可以复用。同一个目标在工具集没有变化的情况下规划出来的路径往往是相同的。我们实际使用时把验证报告缓存了 24 小时重复任务的路径验证时间几乎降为零。另外executor.run接收的是规划好的plan而不是原始文本。这意味着开发者在测试阶段可以直接构造一个“错误路径”传入执行器用来验证回退机制是否真的有效。这种可测试性在 Agent 项目里非常稀缺因为没有哪个团队愿意为了测试回退逻辑真的去调用三次发送邮件接口。4. 实测对比三组任务下触达率与资源消耗的变化框架光有架构设计不够我还要看它在真实任务上的表现。我自己搭了一批测试集任务类型覆盖了简单查询和复杂依赖链和传统 ReAct Function Calling 的基线对比。4.1 测试集与对比基线测试集分四组每组 200 条任务A 组单工具查询任务比如“查一下某商品当前库存”。B 组两跳依赖任务比如“查用户信息 → 查该用户最近订单”。C 组三跳以上且包含动态参数的任务比如“查客户列表 → 逐个查订单 → 汇总金额并按客户分组”其中第二步依赖第一步的输出循环执行。D 组刻意设计的不可达任务比如要求 Agent 把订单金额发送到根本不存在的钉钉群但工具集里没有钉钉接口。基线方案就是用 Function Calling 让模型自己选择工具并允许它在失败后重新尝试。Agent-Reach 这边则使用完全相同的模型和工具列表只是加了工具图、验证器和局部重规划。4.2 实验结果与触达率变化结果如下表所示任务组基线成功率Agent-Reach成功率基线平均轮次Agent-Reach平均轮次基线平均tokenAgent-Reach平均tokenA 单工具查询93.5%95.0%1.61.248023156B 两跳依赖72.0%89.5%3.42.0118307840C 三跳动态参数48.5%82.0%5.82.72864015720D 不可达任务8.0%正确拒绝91.0%正确拒绝6.21.4163504106单工具查询提升不明显这符合预期因为任务本身简单模型不太容易犯错误。但从 B 组开始差距拉开D 组是变化最夸张的基线方案里绝大多数不可达任务会被模型硬生生“完成”——它会编造一个钉钉群发送成功的假象而 Agent-Reach 能直接告诉用户这个任务缺少工具并准确指出缺口在哪里。4.3 为什么资源消耗下降了很多人关心 token 消耗这里有一个关键机制上下文修剪。普通 Agent 会把历史步骤的所有输入输出都塞给模型Agent-Reach 在执行时只把“当前路径上正在处理的数据”传给模型。比如在 C 组任务里当模型正在汇总张三的订单金额时它不需要看到李四那几百行 JSON 原始订单列表只需要看到“李四订单金额5200 元”这种提炼结果。路径视图让上下文始终保持清爽减少了大模型被无关数据干扰的可能。不过也要说明Agent-Reach 并不是免费的。在沙箱验证阶段会消耗少量模型调用来模拟参数路由但测试中每任务平均耗时只增加了约 30 毫秒相比触达率提升和 token 减少这个开销可以接受。5. 踩坑记录副作用、回退风暴和工具图的粒度问题做 Agent 可靠性框架光有理想设计是不够的必须把真实运行中的脏活累活处理好。我在这里记录几个踩过的坑都是常规文档里不会写的东西。5.1 副作用工具会咬人最开始我做局部重规划时很天真某一步失败就回到上一步重新规划再执行一遍。结果遇到一个发送邮件工具的任务Agent 第一步拉取收件人列表第二步调用发送接口第三步入库。结果入库工具报错局部重规划从第二步重新执行于是同一封邮件被发了两遍。从那以后工具注册表里必须有side_effect字段。read_only工具可以随意回退幂等工具比如“按订单号标记已退款”可以回退但要保证重复执行结果一致非幂等工具比如“发送邮件”“创建工单”一旦执行过就不能自动重放。Agent-Reach 默认对非幂等工具取消自动回退改为暂停并等用户确认或者用我们生成的幂等键去查询“这次操作是否已经执行过”。5.2 局部重规划的“回退风暴”与保护措施另一个大坑是回退风暴某一步连续失败五六次模型每次换一种方式重新尝试上下文不断膨胀却始终卡在同一个地方。我发现这类失败有一个规律——失败原因往往完全相同。有一次调天气查询接口模型传的城市名被业务系统识别成两个字的省份简称连续三次都返回unknown city模型还坚持换不同的说法重试浪费了上千 token。解决办法是限制重规划次数并且对失败原因做去重。我在执行器里设置max_replan2如果两次失败原因相同就不再继续尝试直接把用户转交给人工处理。同时把失败原因作为结构化对象传给验证器如果是参数格式问题验证器会直接修正 schema 中的枚举值而不是让模型在黑暗里乱猜。5.3 工具图粒度失控与上下文修剪的取舍工具图的粒度并不是越细越好。我最初想把每个工具的参数拆成独立的原子能力比如“获取客户ID”“获取客户姓名”“获取客户等级”各建一个节点结果工具图变得非常稠密路径搜索的速度慢了好几倍还经常搜索出无意义的调用顺序。后来我把粒度对齐到业务功能级别一个工具对应一个可独立调用的接口函数内部可以包含多个参数这样工具图保持在几十个节点的稀疏水平路径搜索几乎瞬间完成。同时上下文修剪也要小心过度裁剪。我们遇到过一次问题第一步查出的订单号在第二步用于查详情第三步生成账单时又要引用订单号但上下文修剪把订单号从早期数据里裁掉了导致第三步无法引用。解决办法是在目标解析器中声明“需要保留的字段”保留字段不参与裁剪。5.4 一个小技巧在Goal Parser里加入“反目标”最后分享一个我认为 Agent-Reach 最有价值的设计细节——反目标。最初接一个财务周报 Agent 时要求是“归档上个月的旧邮件”结果模型在工具链中为了“清理空间”竟然计划调用批量删除接口差点把带附件的关键邮件全删掉。这是因为目标状态只描述了“要做什么”没有描述“绝对不能做什么”。后来我在目标解析器里增加了anti_goals字段支持结构化声明比如“不得发送邮件”、“不得删除任何记录”、“不得对金额做四舍五入”。可达性验证器在规划路径之后会检查每条路径上的工具操作是否触碰反目标。一旦命中立即判定为不可达并返回人工确认。这个机制救了我至少三次也把很多本来要苦口婆心写提示词兜住的边界问题变成了系统级的硬约束。如果你也要做 Agent 可靠性方向的框架我建议第一版就加上反目标字段不要等到出事故了再补判断条件。