
记不清是从第几个项目开始我发现自己反复被困在同一类问题上Agent跑通了demo也调通了单轮工具调用但一旦让它完成一个跨多个系统的真实任务就开始四处碰壁。要么是工具多了之后模型不知道该调哪个要么是跑着跑着上下文就乱成一团更常见的是Agent一头扎进某个死循环里出不来。就在这种反复折腾的过程里我陆续搭了一套叫 Agent-Reach 的东西——说白了就是解决一个非常朴素的问题怎么让AI Agent真正“够得着”它需要的资源、工具和上下文然后把一个复杂任务从头到尾执行完。这篇文章就是把它的设计思路、关键模块和落地过程中踩过的坑原原本本整理出来。它不是某个开源框架的上层封装也没有用什么花哨的新算法。Agent-Reach 的核心是一个轻量级的Agent运行时它负责管理工具注册、任务分解、上下文维护、执行调度和结果校验这一整条链路。如果你正在做Agent类应用特别是那种需要对接多个外部系统、执行多步骤任务的场景这篇文章里的设计取舍、参数调优和排查经验应该能让你少走不少弯路。1. Agent-Reach 的整体设计思路先把“够得着”这件事拆清楚1.1 三个核心问题决定了我为什么这么设计在动手写第一行代码之前我花了大概两周时间梳理过去项目里Agent失败的共性原因。最后收敛成了三个问题。第一Agent 不知道有什么可用。我在早期项目里习惯把十几个工具函数直接塞进System Prompt里让模型挑。效果嘛工具少于五个的时候还凑合超过十个之后模型开始频繁选错工具甚至自己编造不存在的工具名。这本质上不是模型笨而是我们在设计上没有给Agent提供清晰的“能力地图”。第二Agent 够不着上下文。真实任务往往是多轮、长时长的。用户最开始说的一句话到中间某一步突然要作为决策依据但上下文已经被后续十几轮的中间结果冲散了。做Agent不是做聊天机器人不能只维护对话历史还要维护任务状态、中间产物、外部数据引用这些东西放哪里、怎么存、什么时候压缩是运行时层面要想清楚的事。第三Agent 缺乏闭环反馈。很多Agent跑完一步就默认这一步成功了哪怕工具返回的是错误信息或者空结果它也会顺着错误往下走。缺少对执行结果的校验、重试和回退机制是项目从demo走向生产环境时最大的坎。Agent-Reach 的整个架构就是围绕这三个问题展开的。一句话概括Agent-Reach 是一个把“能力发现、上下文维护、执行闭环”三者统一管理的Agent运行时。1.2 架构分层调度层、工具层、执行层我把整个运行时拆成了三个相对独立的层这样每一层都能单独替换、单独测试。最上面的是调度层负责理解用户意图把一个复杂目标拆解成多个子任务然后决定子任务的执行顺序。我一开始用的是简单顺序执行后面才加入了DAG依赖关系。调度层不直接接触具体工具它只和“任务描述”打交道输出的是一份结构化的执行计划。中间的是工具层负责把Agent能用得上的所有能力统一注册进来。不管是调用内部API、查数据库、发HTTP请求还是执行一段本地脚本都以标准化的方式暴露给模型。工具层最关键的是注册机制和参数描述后面我会专门讲。最下面是执行层它负责真正跑每一个子任务调用模型、发起工具请求、拿回结果、做校验、判成功还是失败以及失败之后怎么办。执行层不关心这个任务“是什么”只关心“怎么稳定地跑完”。这样的分层带来的直接好处是你可以单独升级调度策略而不影响工具层也可以随时往工具层里加一个新工具而不用碰调度逻辑。对我这种喜欢反复改的人来说隔离性好就意味着敢于动手改。1.3 为什么不自上而下用一套重框架我的取舍肯定有人会问市面上LangChain、LlamaIndex这些框架不是都现成的吗为什么还要自研一套轻量运行时我不否认框架的价值早期Agent-Reach的原型也跑在LangChain上。但用了两个项目之后我意识到我的需求跟相对通用的框架产生了偏差我需要精确控制工具调用过程中的超时、重试和状态维护逻辑而这些逻辑在框架里往往被封装得太黑盒出了问题很难排查。还有一个现实问题是框架升级频繁接口说变就变而这些变跟我的业务场景没关系。Agent-Reach 不追求大而全它只做一件事让Agent在可控的工具集合里稳定地完成任务。如果你需要的是一个高度定制化的企业级Agent底座那重框架反而是好选择。如果你跟我一样想要一种“每个环节我都能看懂、都能改”的轻量方案Agent-Reach这种思路会更顺手。这本质上不是技术高下问题而是确定性和可控性的取舍。2. 工具注册与发现让 Agent 知道手里有什么牌2.1 用装饰器把“能力清单”变成模型能读懂的格式工具层是整个Agent-Reach的地基。让模型正确选择工具的前提是——工具的描述必须清晰、参数必须准确、返回必须稳定。这里不搞机器学习靠的是严格的约定。我实现了一个基于装饰器的注册机制。在Python里往Agent-Reach里注册一个新工具基本就长这样from agent_reach import register_tool, ToolResult register_tool( namequery_order_status, description根据订单ID查询订单当前状态和物流进度。当用户询问订单到哪里了、是否发货时使用。, parameters{ type: object, properties: { order_id: { type: string, description: 订单ID一般是字母O开头后面跟12位数字 } }, required: [order_id] } ) def query_order_status(order_id: str) - ToolResult: # 实际查询逻辑... data {order_id: order_id, status: shipped, tracking: SF1234567890} return ToolResult.ok(datadata)这里有几个设计细节值得展开。第一description必须写清楚“什么时候用”而不只是“这是个什么东西”。我为这个吃过亏早期描述写的是“查询订单状态”模型在用户问“我的东西发了吗”的时候选择的概率就是不如描述为“当用户询问订单是否发货、物流进度时使用”来得高。说白了你得替模型做一次意图匹配的预判。第二参数描述必须精确到格式。比如订单ID是“字母O开头”这个信息看着不起眼但对模型来说至关重要。你不说它就可能让用户提供数字开头的订单号然后工具层因为格式校验失败而报错整个流程就断了。第三所有工具统一返回ToolResult。这个结构强制要求工具函数不能毛糙地返回一个裸的dict或者字符串必须显式标注成功还是失败。这样做的好处后面讲执行层的时候你们会看到——它让Agent“已经完成任务了但误以为失败”的概率大大降低也让重试判断变得极其简单。2.2 工具发现怎么把上百个工具描述塞给模型还不撑爆上下文工具多了以后一个很现实的问题就是如果每个工具的描述平均200个token100个工具就是2万token光工具列表就把模型上下文吃得差不多了。所以工具发现不能全量塞必须做筛选。Agent-Reach 的思路是两层漏斗。第一步根据任务的目标描述先在工具层的内存索引里做一次关键词召回。这个检索我用的是简单的BM25没上向量数据库——原因是在我的场景里工具名和描述里的核心词基本能召回正确候选用向量检索反而会找回一堆语义相似但根本用不上的工具增加干扰。第二步把召回来的前10-15个工具的完整描述注入到当次调用的tools参数里。这个数量窗口我是反复试出来的低于5个经常漏召超过20个模型的选择准确率明显下降10-15个是最稳的区间。如果第一步召回的结果太少说明工具描述本身写得不到位需要回头改描述而不是放宽阈值。2.3 工具层的动态加载与热更新因为Agent-Reach接的工具越来越多我后来给它加了一个动态加载的功能——工具不一定要全量写在代码里也可以做成配置文件声明的插件。每个插件是一个Python模块加一个YAML声明文件name: order_toolkit version: 1.2.0 tools: - query_order_status - cancel_order - modify_address运行时会扫描插件目录加载所有声明的模块然后把模块里带注册装饰器的函数自动注册进工具池。现在我接新系统的时候基本就是写一个插件、写配置、丢进插件目录然后跑一个scan_plugins()就能热加载完成。不需要重启服务。这套机制让工具层的扩展成本降到了一个很舒服的程度。3. 任务调度与执行把大目标处理成靠谱的步骤清单3.1 从意图理解到执行计划塞给模型的分解PromptAgent-Reach 的调度层接到的用户输入通常是一段比较含糊的自然语言目标比如“帮我把这批对账单跟系统里的订单核对一下导出不一致的记录”。这种任务指望Agent一次性完成是不现实的抽象层级太高了。所以调度层的第一步是把目标拆成一个可执行的子任务序列。我的做法是用一个专用的分解模型调用来做这件事。这个调用跟后面执行步骤的模型调用分开温度设得比较低0.1-0.2因为它承担的是规划任务不需要发散。分解Prompt的核心约束是每一步必须对应一个具体的工具或一个可验证的中间结果禁止生成没有操作对象的纯描述性步骤。我实际用的分解Prompt模板大概长这样你是任务规划器。请将用户的目标拆解为3-8个可执行的子任务。 要求 1. 每个子任务必须对应一个工具调用或一个明确的信息整理动作。 2. 标记每个子任务之间的依赖关系格式为步骤N依赖于步骤M。 3. 输出必须是JSON数组。 用户目标{user_goal} 可用工具列表{available_tools}分解得到的JSON会再转换成内部的任务对象然后交给调度器决定执行顺序。这里有个比较反直觉的经验第一次分解出来的步骤往往还是太粗。比如用户说“核对对账单”模型第一步很可能会写“获取对账单数据”这个粒度其实还是没进到能调工具的级别因为“获取”本身需要知道数据源是什么、从哪个接口拿、参数是什么。所以我在分解Prompt里加了一条如果步骤名称无法让人不看上下文就知道要调用哪个具体工具那就说明粒度不够需要继续拆。3.2 顺序执行和依赖执行DAG 调度的实现思路任务之间不是所有时候都有依赖。有些子任务可以并行跑有些必须等前一步完成。Agent-Reach 用一张轻量的DAG来表示任务关系节点是子任务边是依赖关系。拓扑排序之后就得到了执行顺序。实现上我没有引入像Airflow那么重的调度框架就自己写了一个几十行的调度器。核心逻辑是def schedule(self, task_id: str): ready [t for t in self.tasks if not t.dependencies] executing set() completed set() while ready or executing: while ready: t ready.pop(0) executor.submit(self._run_task, t, completed, executing) executing.add(t.id) # 等待任一完成更新依赖状态这段代码示意了“不断把没有未完成依赖的任务推入执行”的过程。实际生产版本比这复杂一些因为还有超时控制和失败传播。但核心思想就是依赖满足才能跑跑完才解锁依赖它的下游任务。如果一个上游任务失败了下游所有依赖它的任务自动标记为“跳过”避免做无用功。并行执行的时候还有一个并发上限问题。我一开始贪多所有就绪任务全部丢进线程池结果有几个任务同时打同一个外部系统触发对方限流。后来加了一个信号量同类型工具的并发数限制在2-3个其他类型可以共享一个更大的池子。这个限制不是资源问题而是为了保护外部依赖的稳定性。3.3 模型调用的容错与降级调度层真正跑起来之后最影响稳定性的其实是模型调用本身。大模型接口偶尔会超时、偶发拒绝服务、甚至返回畸形JSON。这些都是常态不是你代码的问题所以必须做好充分容错。Agent-Reach 的做法是给模型调用包了一层重试逻辑。第一层是超时重试单次调用超时时间设置成30秒超时后自动重试一次第二层是格式重试如果返回的文本解析JSON失败会把报错信息回传让模型基于报错重新生成一次第三层是整体降级如果模型连续三次都失败就该子任务标记为失败并进入人工处理队列而不是无限重试。这个容错机制看着很基础但我在实际项目里真没少吃亏。早期阶段我曾让一个Agent在半夜因为模型接口抖动一个任务重试了20多次最后不仅消耗了大量token还把下游的数据库写进了脏数据。现在的策略是宁可标记失败也不要做无谓的重试。凡是需要人介入的早点交给人。3.4 执行结果校验不能默认成功也不能默认失败执行层里一个容易被忽略的模块是结果校验器。模型有时候会“高兴得太早”工具明明返回了一个空列表它在下游步骤里却当成“有数据”来推理反过来也有工具返回了有效结果但因为字段嵌套太深模型没读到误以为失败然后干了一次无意义的重试。所以Agent-Reach 在校验这一档上我给工具返回的ToolResult定义了几个必备字段success布尔值、data标准dict或者list、summary给模型看的文本摘要、raw原始数据可选。这样一来模型的下一步阅读只需要关注summary而校验器检查的是success和data的结构性。我还在校验环节加了一个“断言钩子”允许每个工具自带一个可选的validate函数对data做自定义校验。比如查询订单接口可以断言返回里必须有order_id字段否则就算success为true也判定为异常结果。这种双重校验让我在后面做复杂任务时心里踏实很多至少Agent拿着错数据继续往下跑的概率变低了。4. 上下文与记忆管理长任务不跑偏的关键4.1 全局上下文的分区设计不能一个buf装到底Agent运行过程中会产生各种类型的信息如果一股脑全拼到一个上下文窗口里很快就会混乱。Agent-Reach 把上下文做了分区管理核心分区有四个。第一个是任务区存用户的原始目标、拆解后的执行计划、整体进度状态。这些是“定盘星”只在任务启动和阶段结束的时候更新不会被中间过程冲掉。第二个是工作区存当前正在执行的这个子任务的输入、中间产物、工具返回结果。子任务结束后工作区会被清空只把摘要归档到记忆区。第三个是记忆区存跨子任务需要保留的历史关键信息比如“用户已经确认了A方案”“上一个结果里订单总金额是xx元”。记忆区我会做容量控制超过一定量就得压缩。第四个是对话区存Agent和用户之间的多轮交互记录主要用于回复风格的一致性和上下文引用。这四个分区在物理上不是严格隔离的但在逻辑上清晰分开。这就意味着我在往上下文里塞内容的时候可以有意识地控制每个区的配额。谁膨胀了就先压缩谁而不是一上来就把所有历史对话都喂给模型。4.2 上下文压缩什么时候该压缩怎么压缩上下文压缩是个躲不开的话题。运行时间长了任务多了记忆区是会爆的。Agent-Reach 的压缩策略分两层。第一层是结构化修剪——这是代价最低的。比如四个月前那批订单的明细列表对当前任务可能只剩下“该批订单已核对完成总差异金额x元”这一个结论有价值那明细列表就可以从原始数据区移出去只留摘要。这种修剪可以写规则不消耗模型调用。凡是能靠规则做的就不必浪费token。第二层是模型摘要——当结构化修剪解决不了或者需要保留的信息没法用简单规则提取时就用模型做一次生成式摘要。调用模型把一大段记忆压缩成一段250字以内的要点记录然后替换掉原文。注意这里的温度也要设低一点防止摘要过程中引入幻觉信息。压缩这一步做完之后我会在记忆区打一个compressed_at时间戳方便追踪哪部分记忆是二次生成的必要时可以回溯原始日志。4.3 状态持久化任务挂了也能从断点恢复Agent跑长任务的时候经常会遇到进程被杀、网络断开这些不可控情况。一旦状态全在内存里重启就等于从零开始。Agent-Reach 把任务状态定时持久化到Redis字段包括当前执行到的子任务编号、已完成节点的输出摘要、记忆区的数据、以及下一步候选动作。每完成一个子任务就写一次快照。恢复的时候调度器从快照里加载状态已经完成的子任务直接标记为done未完成但有依赖的就正常调度。这套断点续跑机制让我在服务器故障演练中省了大力气。早期没有这套机制的时候一个跑了30多分钟的对账任务因为一台机器重启全部作废那个画面我不想再见第二次。这里有个权衡频繁持久化会增加写IO和序列化开销对普通任务没必要每步都写。我现在的做法是快照间隔默认是3个子任务也可以按任务配置。凡是任务耗时超过10分钟的我都会建议把间隔调短到1宁可多写几次也不丢进度。5. 部署参数与调优实录实测下来的关键数据5.1 模型选型规划模型和执行模型分开Agent-Reach 设计了双模型架构这在成本和质量上都有优势。规划模型用的是一个体积小、响应快、便宜的模型比如部分中等规模的轻量模型扛得住高频次的意图识别和任务拆解真正执行时的内容生成和复杂推理则用更强的主模型。这个组合让整体成本大概降了40%左右而且响应速度更快。因为很多流程里规划才是高频调用执行模型只在最后生成结果或者需要工具时才调用算力用在了刀刃上。要注意的是两个模型的职责边界不能混淆规划模型千万别让它直接调工具否则容易出现它根本不理解自己的规划结果胡调一气的状况。5.2 温度、超时、重试这三组参数的经验值参数这个东西脱离场景谈取值都是耍流氓。但作为实验记录我把自己项目里用得比较稳定的参数整理出来你们参考的时候还是要结合自身场景调整。规划模型的温度我设0.1执行模型温度设0.7。超时方面单次模型调用30秒单次工具调用30秒到60秒取决于工具类型查数据库的可以给到60秒。任务级总超时按子任务数量动态计算再乘一个1.5的安全系数。重试次数统一3次指数退避第一次等待1秒、第二次2秒、第三次4秒。重启轮次太多徒增消耗3次之后基本可以判负启人工流程。还有一个容易被忽视的参数——max_tool_calls_per_task。一个子任务最多允许调多少次工具。这个限制是防止Agent进入“工具调用死循环”的保险丝。我一般设6次左右超过就强制结束该子任务并标记失败。这个参数我认为它比任何Prompt技巧都能有效防Agent跑偏强烈建议所有做Agent的同学都加上。5.3 并发与资源估算一个任务吃多少资源先算清楚Agent-Type任务是很吃资源的尤其是同时跑多个Agent实例的时候。我给一个参考计算方法单个任务平均需要5次模型调用每次模型调用消耗的token大约是1500-2500加上工具调用的网络IO和计算一个任务大概是2-4万元的token消耗量级。资源估算的时候主要算模型QPS能扛多少并发以及外部系统的限流阈值。我服务端给模型API的并发数上限是按任务并发数来算的一个任务内部的子任务并行度是2所以3个任务同时跑基本就是6个并发模型调用。如果模型API的QPS限制是20这6个并发完全不是问题。真正该担心的是外部系统像有些公共接口QPS限制就5一个任务里的两个并行子任务同时打过去就触发限流了。所以并发设计不只要看自己的算力更要看下游的容忍度。这个视角在自研Agent的时候容易忽略但在接入真实业务系统的时候几乎是第一优先级。6. Agent-Reach 的升级方向以下几个扩展值得做核心链路稳定之后我开始琢磨一些更长期的扩展方向。第一个是自动工具组合。现在的工具层是静态注册的下一步想做一个能力让Agent根据任务动态生成组合工具的脚本比如自动串联A接口和B接口做数据转换。这个做好了能进一步减少手工编排的负担。第二个是多Agent协作。目前Agent-Reach 是一个单Agent内部做任务分解身份单一。如果要接更复杂的协作场景需要支持多种角色Agent互相通信。我已经在代码层把消息传递接口预留出来了下一步计划是引入一个简单的“黑板”机制多个Agent往共享空间里写中间结果和需要协作的信号。第三个是可观测性强化。Agent执行过程的追踪对排障太重要了。我现在用的方案是把每次模型调用和工具调用的完整日志带上trace_id写入磁盘再配一个简单的Web UI展示执行轨迹。这一步虽然工程量大一些但上来之后排障效率是立竿见影的。Agent-Reach 这个项目到现在已经演进到第三版了。每跑一个真实业务场景我都会把暴露出来的问题回填到设计里。它不是什么了不起的框架但对我来说最珍贵的是它把我对Agent的理解从“用模型写Prompt”推进到了“设计一个能让Agent稳定工作的运行时系统”。如果你也卡在Agent项目从demo到生产的这段路上希望这套思路和参数能成为你的垫脚石。试过之后欢迎拿你的数据和问题来跟我对线踩坑的路上有人陪着走会轻松很多。