Agent-Reach这个名字最早是我在整理多套智能体项目代码时冒出来的一个想法。团队里每个人都在做Agent但每个人对Agent究竟能不能把事情做成的判断标准都不一样。有人看它有没有调用工具有人看它有没有输出正确格式有人直接看最终结果。而实际跑起来你会发现大模型推理本身是概率性的工具调用又可能失败上下文窗口还有上限这种情况下Agent的成功定义根本立不住。我们后来把所有项目统一收敛到一套方法论上就是Agent-Reach一套围绕目标可达性来设计、评估和调试AI代理的框架思路。它解决的核心问题是不要让Agent在复杂的多步骤任务里迷路也不要在每一步执行时靠运气而是从规划阶段就持续回答两个问题——当前离最终目标还差几步这些步骤是否真的可达这套内容适合谁看不只适用于做大模型应用开发的工程师也包括正在选型Agent框架、做技术方案的架构师甚至刚接触智能体的同学。如果你已经被ReAct、Plan-and-Execute、GraphRAG这些概念绕得头晕不妨回到最朴素的标准一个Agent能不能稳定触达它要触达的结果。这篇文章我会完整拆解Agent-Reach的核心设计包括可达性模型怎么定义、规划器怎么工作、工具注册表怎么构建、失败恢复怎么做以及我踩过的几个坑。1. 内容整体设计与思路拆解1.1 从自动执行转向可达性判断大多数Agent框架给我的感觉是它们过度关注怎么让模型调用更多的工具而很少讨论怎么判断当前路径是否还有意义。ReAct模式让模型进行Reasoning然后Act但Reasoning一旦跑偏后面的Act会跟着错得越来越远。Plan-and-Execute模式虽然先规划再执行但规划执行完以后如果你想中途调整计划实现起来非常僵硬。Agent-Reach借鉴了传统路径规划里的可达性分析概念。简单说我们在每一步都维护一个状态快照明确Record三个东西当前节点、已消耗的步骤、剩余的可达工具。模型不直接回答下一步做什么而是先回答基于当前状态目标集合里哪些项是可达的再由规划器决定走哪条可达路径最划算。这就像你在一个陌生城市里导航。普通Agent的做法是根据直觉挑一条路走不通再回头。Agent-Reach的做法是先看地图确认哪些路口能走哪些是断头路每次转弯都重新检查去终点的可行性。两者的区别一个是走一步看一步的运气模式一个是基于空间感知的搜索模式。1.2 为什么选这个方案而不是堆Prompt在早期原型里我们也试过在System Prompt里写几百字严格要求模型务必检查工具是否可用、务必分析上下文长度、务必保守决策。效果很一般原因在于LLM的注意力是分布式的你写在前面的强调容易被后面的复杂指令稀释。与其依赖模型的自觉不如把约束写进流程里。Agent-Reach的方案本质上是把可达性判断从模型的自觉动作变成系统的强制动作。系统每一步都对状态做结构化校验校验结果直接决定下一步的搜索空间。模型依然负责关键的推理和生成但它不再需要一边推理一边自我监督这大大降低了认知负担。换句话说这个方案减少了模型在单次调用里要做的事情把监督自己这件事交给确定性代码。1.3 一套方法三类场景通用在我们团队内部Agent-Reach至少覆盖了三个场景而且都是生产环境在跑的。第一类是复杂信息收集任务比如让Agent从多个数据源收集线索并交叉验证这个场景极其考验路径规划能力。第二类是多步业务操作比如Agent在内部系统里创建工单、更新状态、发送通知这类任务最怕操作顺序颠倒。第三类是长文档处理与结构化输出比如让Agent分析一份几十页的合同并输出结构化摘要这种任务真正的难点是怎么确保结论里的每个字段都来自文档中的对应依据。这三个场景看起来差异很大但拆开以后核心需求完全一致每一步都要知道自己在哪、能去哪、离目标还有多远。所以Agent-Reach不是一个狭窄的专用框架而是一套可以用来重新审视你现有Agent架构的思维模型。2. 核心细节解析与实操要点2.1 可达性数学模型Agent-Reach里我把一个任务定义成一个五元组S所有可达状态的集合。AAgent可以执行的动作集合通常对应工具调用。T(s, a)状态转移函数表示在状态s下执行动作a后进入的新状态。G目标状态集合也就是任务成功完成的判定条件。R(s, a)奖励函数用于量化动作a带来的收益。Agent每一轮从当前状态出发做一次前向搜索计算到达G的可达性分数。这个分数不是简单的布尔值而是分层的Strong Reachable表示当前搜索树中存在一条路径其每一步转移都能被验证通过Weak Reachable表示存在一条路径但其中至少一步依赖模型推测需要额外验证Not Reachable表示在当前状态和剩余动作集合下不存在到达G的路径。模型的任务被压缩成一个局部动作选择器它只需要从当前可行动作里挑一个最有希望的动作而不是像传统Agent那样从规划执行验证三条线同时思考。系统再通过实时检查可达性分数来决定是继续执行、回溯还是宣告失败。这套数学模型的工程落地并不复杂本质上就是一个最小堆状态剪枝的搜索过程但它让整套Agent行为从概率驱动变成了搜索驱动。2.2 状态编码与上下文压缩想让可达性判断在长任务中始终可靠最大的敌人是上下文膨胀。Agent跑了几轮以后如果把每一轮的工具返回全文都塞给模型很快会塞满Context Window。Agent-Reach里每个状态节点都带一个压缩摘要器对工具返回数据进行两段式处理第一段是信息抽取用一次专门的LLM调用从原始返回里抽取出与当前目标相关的结构化字段。比如目标是找出报价跌幅超过5%的客户抽出来的就应该是客户名称、跌幅数值、时间戳。第二段是状态落库把抽取出的字段写进节点的元数据原始内容直接丢弃或者进归档存储。这么做有两个直接好处。其一后续每轮调用LLM的输入规模几乎是恒定的不会随着任务推进无限膨胀。其二状态节点的元数据成为可达性判断的输入后续搜索可以直接在结构化字段上做条件过滤而不需要反复把大段文本喂给模型。我实测下来同样一个50步左右的长任务用Agent-Reach后单轮上下文从16000 tokens左右降到了6000 tokens以内而且准确率不降反升。2.3 工具注册表的设计原则Agent-Reach里的工具不是简单的一个函数列表每个工具在注册时要声明三类元信息能力描述、前置条件、副作用声明。前端能力描述用自然语言写给模型看的。前置条件用结构化的Schema写给可达性判断用的。副作用声明则记录了该工具执行可能改变的外部状态比如发送邮件更新数据库这类带副作用的动作必须和读取数据在可达性图里分区管理。我举个例子你就明白了。一个查询客户合同工具能力描述可能只需要写输入合同编号返回合同关键条款但它的前置条件会声明需要在上下文节点中存在客户ID实体并且节点状态为verified输入参数必须满足合同编号正则。而可达性判断在决定是否选择这个工具之前会检查前置条件是不是都满足。这样的好处是模型永远不会拿到一个它根本没法成功调用的工具清单大量幻觉工具调用问题在根源上就被堵住了。2.4 失败恢复与多路径搜索Agent注定会失败这一点写进Agent-Reach的第一条设计原则里。我们的处理办法是不给Agent重试到天荒地老的机会而是设置两层恢复机制。第一层是局部回溯当某个动作返回错误或者可达性分数降到WeakAgent回退到这个动作的前一个节点重新选择另一条分支。这个逻辑有点像Git的rebase保留当前分支的上下文但换一条路径继续前进。第二层是全局重新规划如果局部回溯超过两次系统会放弃当前搜索树基于全局状态重新生成一次摘要规划相当于让你在山里迷路时重新拿出GPS定位而不是继续原地打转。在实际项目中我最常看到的失败不是工具报错而是Agent死循环——反复调用同一个工具、得到同一类结果然后继续调用。Agent-Reach的死循环检测很简单每个节点带一个指纹是把压缩摘要字段拼接后哈希的结果。如果下一个状态和过去三个状态里的某个重复直接判定循环并强制切换路径。3. 实操过程与核心环节实现3.1 从零搭一个Agent-Reach实例写到这里你可能觉得理论偏多我直接带你走一遍实操。我会用Python伪代码加上合理的开源组件搭建一个能处理从三家数据源收集信息并交叉验证的Agent-Reach实例。第一步是定义状态和工具。我这里使用Pydantic定义结构化状态节点用装饰器注册工具函数。from pydantic import BaseModel, Field from typing import Optional from datetime import datetime class NodeState(BaseModel): node_id: str goal: str collected_entities: dict Field(default_factorydict) current_source: Optional[str] None verified_result: Optional[str] None step_count: int 0 parent_id: Optional[str] None tool( namefetch_from_crm, description从CRM系统拉取指定客户的基础资料, preconditions[has_client_id, node_is_verified] ) def fetch_from_crm(client_id: str) - dict: # 实际实现里这里调用CRM HTTP接口 return {client_name: 示例客户, industry: 制造业, source: crm}注意这里的preconditions不是装饰器上的字符串实际工程里我会把它解析成一个函数在调用前由调度器统一执行校验。我习惯把每个工具的preconditions分两类一类是状态前置检查节点状态另一类是参数前置检查输入参数的模式或者取值约束。第二步是实现可达性规划器。核心逻辑是用一个优先队列维护搜索边界每次扩展时调用一个轻量级LLM评估当前状态下候选动作里哪些最有利于到达目标。import heapq class ReachPlanner: def __init__(self, tools, llm): self.tools tools self.llm llm def evaluate_reachability(self, state: NodeState, candidate_actions: list) - dict: # 构造可验证的前置条件检查返回 Strong/Weak/Not scores {} for action in candidate_actions: pre_ok self.tools[action].check_preconditions(state) if not pre_ok: scores[action] Not Reachable continue # 用LLM估算该动作对目标的影响权重权重高 - Weak很高 - Strong weight self.llm.score_action_relevance(action, state) scores[action] Strong Reachable if weight 0.8 else Weak Reachable return scores def search(self, start_state: NodeState, max_depth: int 20): heap [(0, start_state)] visited set() while heap: cost, state heapq.heappop(heap) if state.node_id in visited or state.step_count max_depth: continue visited.add(state.node_id) if self.is_goal(state): return state candidate_actions [t.name for t in self.tools if t.check_preconditions(state)] scores self.evaluate_reachability(state, candidate_actions) for action, score in scores.items(): if score Not Reachable: continue next_state self.execute_action(state, action) next_state.parent_id state.node_id heapq.heappush(heap, (cost 1, next_state)) return None这段代码是Agent-Reach的骨架也是我最想让你理解的环节。它不试图一次推理让模型达到目标而是把目标拆进搜索树的扩展过程中。模型在这里只做了一个很轻量的决策评估候选动作的相关性权重。权重怎么来的我实际用的是GPT-4o的一次调用Prompt里提供当前节点的压缩摘要和候选动作列表要求模型从目标相关性、前序依赖满足度、潜在副作用三个维度打分。一次调用成本只需要几个token但整个搜索过程的成功率提升非常显著。第三步是执行与验证。状态转移函数里我还会留一个动作后验证钩子工具执行回来后系统自动做一次轻量校验比对返回结果的Schema和关键字段。这里的校验不是LLM推理而是确定的代码逻辑。比如CRM工具返回的数据必须包含client_name字段如果缺了就标记该动作执行失败并返回错误码。3.2 中间层上下文压缩器的工程实现在上面的实例里最优美也最易被忽略的一个环节是execute_action里的摘要更新。真实场景里工具返回的结果可能是一个几百页的PDF解析后的长文本你不可能把它整个塞给模型。我的做法是维护一个AgentState的局部片段每轮执行完以后调用一次extract_summary。def extract_summary(raw_response: str, goal: str, llm) - str: prompt f 当前任务目标{goal} 原始返回片段截断前3000字 {raw_response[:3000]} 请提取与目标直接相关的结构化事实并入以下JSON格式返回 {{entities: [...], metrics: [...], source_trace: [...]}} 不要复述原文不要推测只提取你能确信的内容。 result llm.complete(prompt) return result注意这里的原始返回片段我会截断到3000字以内。为什么要截断而不是全给两个原因一是大多数工具返回的长文本里冗余信息占了绝大多数截断不会损失关键事实二是这本身就是一道隐性的上下文安全阀防止某一个工具返回意外超长文本直接把整个上下文窗口击穿。如果你担心截断会丢信息可以在截断前先把原始返回全文写入磁盘或对象存储摘要里保留source_trace字段指向原文位置后续需要溯源时再拉取。3.3 目标判定怎么写才不自欺欺人目标判定看似简单其实是最容易出问题的点。如果你的goal判定是让LLM自己看结果满不满意那Agent大概率每次都觉得自己完成了。Agent-Reach的做法是把目标拆成机器可验证的原子条件。再拿从三家数据源收集信息并交叉验证举例我不会写判定是否完成调查而是定义条件一collected_entities中source集合必须包含三家不同的数据源标识。条件二每个客户实体的关键字段比如企业名称、注册资本至少被两个来源覆盖。条件三verified_result字段保存了一份冲突列表列表里不能存在未解决的不一致项。只有同时满足这三个条件搜索过程才返回Goal Reached。每一个条件对Agent来说都是可验证的黑盒函数模型不需要对我做到了吗做模糊判断系统替它把这个问题变成了纯计算题。这个设计的妙处还在于它天然具备可测试性你可以为每个目标条件单独写单元测试而不是端到端地靠运气验证行为。4. 常见问题与排查技巧实录4.1 前置条件永远不满足Agent卡在起点我第一次把Agent-Reach接入真实业务时遇到最头疼的问题是候选动作的前提条件已经检查过了模型也选了这个动作真到了执行那一步工具的内部校验还是报错。后来调了半天才意识到问题出在前置条件的粒度不对。我在工具注册表里只声明了需要client_id却没有进一步声明client_id必须是CRM系统里真实存在且未删除的客户。模型拿出来的client_id虽然格式合法但语义上并没有通过校验。解决办法是在前置条件里增加一个语义校验层用轻量的向量检索或者SQL查询预先确认参数存在性。比如在fetch_from_crm工具的前置步骤里先执行一次SELECT COUNT(*) FROM clients WHERE id ? AND deleted_at IS NULL再做状态判断。虽然多了一次数据库查询但误报率下降了非常多。这类问题对应的排查技巧给每个Agent加日志时一定要把前置条件检查失败的原因单独打一条结构化日志而不是只记录工具执行失败。否则你一眼望过去全是失败但根本分不清是参数缺失、状态过期还是权限不足。4.2 压缩摘要器开始撒谎上下文压缩器是Agent-Reach里最依赖LLM的环节因为它每一次输出都是重新生成的所以很容易出现提取出的实体在前两轮出现了、第三轮忽然消失的问题。我最初用的是一次性独立调用每个摘要之间没有一致性约束结果Agent跑到第10轮之后早期收集的信息被慢慢洗掉后面的决策越来越依赖幻觉补充。我的修复方案是给摘要器加一个持久化事实库而不是让它从零生成。每个节点除了当前摘要还维护一个facts dict新抽取的字段和旧字段做一次融合再写回。融合规则也很简单如果字段名相同且来源不同取置信度高的保留并标记证据来源如果字段冲突把两个版本都记录到conflict_list里等交叉验证步骤再处理。4.3 模型只挑好走的路而不是正确的路这个坑挺有意思。Agent-Reach里模型要给候选动作打分它自然倾向于给更容易执行的、返回结果更漂亮的工具打高分比如查询公开资料库永远比调用内部审核接口受欢迎。因为这些动作前置条件更容易满足执行过程中模型的认知负担更小。结果就是Agent总是绕开真正的关键动作导致任务无法收敛。针对这个问题我在可达性分数里引入了一个关键动作权重base weight把工具注册表里的少数动作标记为milestone actions。milestone动作在计算权重时直接加一个0.2的强行偏移除非前置条件完全不可满足否则它们始终排在普通动作前面。当然这里要小心权力越大越容易被滥用所以milestone动作的判定条件必须非常苛刻我通常只在跨数据源验证、最终写入、发送通知这类任务终点动作上打标。4.4 死循环检测的误杀问题前面提到我用节点指纹的方式检测循环实际测试中确实有误杀。最常见的误杀场景是Agent在反复调用同一个查询工具每次查询条件都变化但因为摘要器把查询参数也抽取成实体并写进facts最后指纹哈希出来是一样的。调整方案是指纹计算排除查询参数本身只计算目标相关字段和已收集实体的哈希。换句话说Agent重复问同一类问题不应该被判定为死循环真正要防的是它重复收集同一批数据、得到一模一样的结果还继续收集。排查这类问题时我习惯直接看搜索树的可视化把每个节点的指纹和父节点关系画出来循环路径一眼就能看出来。4.5 常见问题速查表现象根本原因快速解法Agent在起点反复失败前置条件粒度不够参数语义未校验增加语义校验步骤预查询参数存在性连续执行多步后决策质量骤降上下文压缩导致信息丢失或冲突引入持久化事实库字段合并写回总是绕过关键步骤模型偏好低难度动作引入milestone actions权重偏移反复调用同一工具路径循环节点指纹重复指纹排除查询参数增加循环检测工具返回超长文本导致越权占用上下文缺少截断与归档策略截断返回片段原文落盘留痕明明结果错误却报告成功目标判定依赖模型主观判断将目标拆成机器可验证的原子条件5. 后续扩展与实际生产建议你如果想把Agent-Reach的思路接入现有项目不需要彻底推翻原来的Agent框架。我从自己改造两个存量系统的经验来看最少只需要做三件事第一把工具注册改造成带前置条件和副作用声明的结构化Schema第二在每次工具调用前插入一个可达性检查拦截器这个拦截器可以是几十行代码的中间件第三把Agent最终输出里的任务是否成功字段改成和你目标条件的布尔检查结果硬绑定。做完这三步你的Agent行为就会从依赖模型自觉往依赖系统约束迈一大步。另一个值得升级的方向是把Agent-Reach做成多Agent协作模式。目前我们探讨的是一个Agent单机执行但在实际业务里往往需要多个Agent分工。这时可达性就会从单个Agent内部的概念上升为Agent之间的协议Agent A完成前置节点后如何让Agent B确认这部分状态已经可达再决定是否开启后续动作。我在生产系统里是用一个共享状态库来实现的每个Agent把节点状态写入一张带版本号的状态表其他Agent读取时通过版本号判断状态的新鲜度。如果你已经有一个事件总线或者消息队列完全可以在消息里套这个节点结构实现成本也不高。还有一个我觉得特别实用的小技巧用Agent-Reach的状态快照做任务复盘。传统Agent项目出了问题你只能看一堆乱七八糟的日志很难复现完整路径。Agent-Reach里每个节点都有结构化的状态赋值给事件可以非常方便地钻进任何一个中间节点查看当时的facts、候选动作和可达性分数。我团队现在排查线上问题的平均耗时从之前的半天降到了二十分钟以内全靠这里。最后说一点我从几次踩坑里沉淀下来的习惯性建议Agent-Reach的魅力不在于它的算法多复杂而在于它逼你把任务结构想清楚。写工具注册表的过程实际上就是一次业务建模写原子目标条件的过程就是一次验收标准定义。这些动作看起来多花了时间但和后面调试Agent时省下的时间相比代价完全可以忽略。如果你正在做一个涉及复杂多步骤的Agent我真心建议先别急着堆Prompt把可达性模型这层结构先画出来再动手写代码你会回来感谢这个决策。