☰
Agent-Reach:解决Agent任务触达问题的可靠路径规划框架
2026/10/6 10:40:09 网站建设 项目流程

把 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( name="search_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_effect="read_only" ) # 注册第二个工具:查订单详情 graph.add_tool( name="get_order_detail", description="根据订单ID获取订单详情", input_schema={ "order_id": {"type": "string", "required": True} }, output_schema={ "amount": {"type": "number"}, "status": {"type": "string"} }, side_effect="read_only" ) # 声明工具之间的数据依赖关系 graph.connect( from_tool="search_orders", to_tool="get_order_detail", mapping={"order_ids": "order_id"} )

这里最关键的参数是side_effect。它声明工具是否会产生副作用,read_only表示只读操作。这个字段在后面的自动回退机制里会用到,我现在先标记上。

3.2 一个跨工具链任务的完整执行示例

工具图建好之后,完整跑一个任务只需要四步:

from agent_reach import ReachPlanner, ReachabilityValidator, ReachExecutor # 1. 解析用户目标 planner = ReachPlanner(graph=graph, llm=llm) goal = planner.parse("查一下张三在上个月的订单,并汇总所有订单金额") # 2. 规划路径 plan = planner.plan(goal) # 输出类似:[search_orders -> get_order_detail(get_order_detail 需要循环执行)] # 3. 验证路径 validator = ReachabilityValidator(graph=graph, sandbox=True) is_valid, report = validator.validate(plan) if not is_valid: print("路径不可达,原因:", report.error_hint) else: # 4. 执行 executor = ReachExecutor(graph=graph, llm=llm, max_replan=2) result = executor.run(plan) print(result["summary"])

注意第 3 步的sandbox=True,这会让验证器用模拟数据检查参数链路,不会真实调用张三的订单查询接口。只有验证通过之后,第 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平均token
A 单工具查询93.5%95.0%1.61.248023156
B 两跳依赖72.0%89.5%3.42.0118307840
C 三跳+动态参数48.5%82.0%5.82.72864015720
D 不可达任务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_replan=2,如果两次失败原因相同,就不再继续尝试,直接把用户转交给人工处理。同时把失败原因作为结构化对象传给验证器,如果是参数格式问题,验证器会直接修正 schema 中的枚举值,而不是让模型在黑暗里乱猜。

5.3 工具图粒度失控与上下文修剪的取舍

工具图的粒度并不是越细越好。我最初想把每个工具的参数拆成独立的原子能力,比如“获取客户ID”“获取客户姓名”“获取客户等级”各建一个节点,结果工具图变得非常稠密,路径搜索的速度慢了好几倍,还经常搜索出无意义的调用顺序。

后来我把粒度对齐到业务功能级别:一个工具对应一个可独立调用的接口函数,内部可以包含多个参数,这样工具图保持在几十个节点的稀疏水平,路径搜索几乎瞬间完成。同时,上下文修剪也要小心过度裁剪。我们遇到过一次问题:第一步查出的订单号在第二步用于查详情,第三步生成账单时又要引用订单号,但上下文修剪把订单号从早期数据里裁掉了,导致第三步无法引用。解决办法是在目标解析器中声明“需要保留的字段”,保留字段不参与裁剪。

5.4 一个小技巧:在Goal Parser里加入“反目标”

最后分享一个我认为 Agent-Reach 最有价值的设计细节——反目标。最初接一个财务周报 Agent 时,要求是“归档上个月的旧邮件”,结果模型在工具链中为了“清理空间”,竟然计划调用批量删除接口,差点把带附件的关键邮件全删掉。这是因为目标状态只描述了“要做什么”,没有描述“绝对不能做什么”。

后来我在目标解析器里增加了anti_goals字段,支持结构化声明,比如“不得发送邮件”、“不得删除任何记录”、“不得对金额做四舍五入”。可达性验证器在规划路径之后,会检查每条路径上的工具操作是否触碰反目标。一旦命中,立即判定为不可达并返回人工确认。这个机制救了我至少三次,也把很多本来要苦口婆心写提示词兜住的边界问题,变成了系统级的硬约束。

如果你也要做 Agent 可靠性方向的框架,我建议第一版就加上反目标字段,不要等到出事故了再补判断条件。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询