资讯动态

Agent-Reach实践:异步任务调度与反馈闭环的智能体框架设计

发布时间:2026/10/28 5:45:00 来源:尧图企业网站定制
这次不整那些花里胡哨的标题党直接把我从零搭建 Agent-Reach 这套系统过程中想清楚的东西写出来。它解决的不是什么高深的大模型推理问题而是我在做数据采集和自动化处理时最头疼的一件事如何让多个智能体既有足够的自主判断又能规规矩矩地把活干完。如果你正在被各类异步任务、脚本间调度、状态丢失搞得头疼这篇文章应该能给你一些不一样的参考。1. Agent-Reach 的立项背景从 脚本套脚本 到真正的调度框架我先说说为什么会有 Agent-Reach 这个东西。过去很长一段时间我处理批量任务的方式很简单写一个 Python 脚本然后循环调用中间夹几个 try-except失败了就重试三次。听起来挺常规的但用久了问题全冒出来了。你试过同时跑七八个不同来源的数据同步任务吗A 任务可能在凌晨一点触发B 任务可能要从消息队列里拉数据C 任务又依赖上一个任务产物。脚本多了之后互相之间的依赖关系、启动顺序、失败补偿机制完全靠人肉记忆。今天改一个任务的参数还好改到第三个任务时你已经不敢动了因为你根本不知道它会被谁调用、它的输出会被谁消费。Agent-Reach 这个项目就是想解决这个编排混乱问题。它的定位不是机器学习平台也不是 workflow 编辑器而是一个轻量的 Agent 调度与任务跑批框架。我把它定义为让每个 Agent 知道自己该做什么、什么时候做、做到什么程度算完成并且出了问题知道自己下一步怎么走。这里要先说明一下Agent-Reach 并不是某个开源仓库已有的可安装包它是我自己设计的一套系统思路。核心概念非常简单面向任务的 Agent 定义 可插拔的任务路由 内置一个反馈闭环。文章里我会用伪代码和实际可运行的 Python 片段来演示这套思路的落地方式方便你直接照搬设计。为什么推荐用 Agent-Reach 而不是直接上 Celery 这种成熟的分布式任务队列原因在于Celery 解决的是任务的可靠执行和分发它不关心任务逻辑如何上下文联动而我需要的是决策、执行、校验、修正这个循环这在 Agent 场景里非常典型。比如我在做多源数据聚合时每个源的行为特点都不同有的源时快时慢有的源喜欢返回 429有的源需要事先预热。如果用 Celery这些逻辑就要散落到 task 函数里的深层代码最后维护起来非常难受。把决策和任务主体拆开让 Agent 本身具备一定的临场反应能力这才是 Agent-Reach 的核心价值。2. 任务、路由与反馈闭环Agent-Reach 的三个核心原语在设计 Agent-Reach 时我参考了业界跑批框架的标准分层但针对 Agent 场景做了不少调整。整个框架最终抽象成三个原语Task任务、Router路由、Feedback Loop反馈闭环。下面我分别展开以及重点解释为什么这么设计。2.1 Task不只是封装一段函数而是带目标描述的执行体很多人设计任务系统时第一反应就是把任务抽象成一个函数加几个参数。Agent-Reach 里不太一样我要求每个 Task 必须显式声明它的目标和验收回调而不是把验收逻辑揉在 Agent 主体里。举个例子我的一个 Agent 要从某公开数据源同步商品信息。如果按传统写法代码大概是def fetch_products(store_id, paramsNone): ...但在 Agent-Reach 里我写的是agent_task(name商品信息同步) class FetchProductsTask: target: str 同步指定店铺的商品基本信息 def execute(self, context): store_id context.input[store_id] return api_client.get(f/stores/{store_id}/products) def validate(self, result, context): # 验收标准数据不能为空而且必须包含合法ID if not isinstance(result, list): return False, 返回结果不是数组 if any(item.get(id) is None for item in result): return False, 存在缺失ID的记录 return True, f成功获取 {len(result)} 条记录这段代码的关键并不在于多写了几个字而是把验收逻辑和取数逻辑绑到了一个 Task 里。好处是当任务出问题时Agent 可以直接读取 validate 返回的错误信息并据此决定是重试、跳过还是报警不需要人肉看日志。2.2 Router把任务流改成有状态的状态机任务之间如果只是顺序执行那其实用普通调度框架就行根本不需要 Agent。Router 的引入让 Agent-Reach 具备一定的判断能力。我在实现时把 Router 拆成了两部分静态路由表和动态路由规则。静态路由表很好理解就像路由表一样routes: - when: 数据源A同步完成 then: 进入数据清洗阶段 - when: 数据源A同步失败 then: 进入等待重试队列等待5分钟后重试动态路由规则就更进一步了。比如我按业务需求当一个任务执行时间超过设定阈值时就直接降级为小批量处理避免 Agent 长时间卡在某个慢源上拖垮整个链条。我还用嵌套字典给 Router 实现了一个简单的状态机扩展。基础版本大概长这样class WorkflowRouter: def __init__(self): self.states {} self.transitions {} def register(self, state, next_on_success, next_on_failure, timeout_sNone): self.transitions[state] { success: next_on_success, failure: next_on_failure, timeout: timeout_s } def next_state(self, current_state, result_status): mapping self.transitions.get(current_state) if not mapping: return None return mapping.get(result_status)状态机的引入让任务之间的跳转变得非常透明。以后排查问题只需要把路由日志打出来立刻就能看到任务A 10点05分开始 - 成功 - 跳到任务B - B失败 - 回退到重试队列整个链路一目了然。2.3 反馈闭环让 Agent 从错误中积累路感这是我认为 Agent-Reach 最值得借鉴的一点。以前我们做任务管理目标是任务能跑通而 Agent-Reach 追求的是让任务跑得越来越顺。反馈闭环的核心逻辑是把每次失败重试的结果喂回路由器和任务定义。我在系统里加了一个 lightweight 的经验存储模块。每个任务失败时经过人工或规则确认的失败原因会被记录下来比如某数据源超过 60 秒无响应后来发现需要先调用预热接口。某接口需要带 locale 参数否则返回的数据只包含默认语言内容。等到下次 Agent 再碰见相似上下文时它会优先加载这些历史经验自动携带上一步成功的参数组合。有点像一个跑运输的老司机第一次走山路容易暴死跑多了就知道哪个弯道要减速。这个路径带来的最直观收益就是不需要把所有的异常处理规则都写死到代码里。Agent 可以通过试错回顾的方式像一个真正的员工一样积累经验。当然这个功能不能无限制积累规则否则规则库会爆炸。我的做法是设置了一个规则有效期普通规则保存 30 天强规则比如连续三次失败确定需要这样处理可以保存半年以上到期后如果不再命中自动清理。3. 最小可运行代码先把调度骨架跑通再谈智能我相信不少人也踩过类似的坑一开始想把系统设计得尽善尽美结果写完 800 行发现跑不起来。Agent-Reach 的第一版我只用 Python 标准库和一个 Redis 任务队列实现了基础骨架并没有引入太多智能设计。先把骨架立起来后面的一切才有地方挂靠。3.1 任务队列的选型Redis 还是文件系统考虑过度设计的Agent 系统客户端在早期就被迫拴在某个重型消息队列上这其实不是个好主意。Agent-Reach 的早期版本我用了 Redis 的 list 结构作为任务队列理由如下足够轻量你甚至不需要单独搭服务直接复用已有的 Redis 实例对于中等规模的调度任务足够稳能够保证可靠性Redis 自带的 TTL 和后端阻塞式读取能很方便地支撑延迟重试场景。如果你还没有 Redis 环境也可以用文件系统模拟队列。比如用同一个目录下的文件作为任务对象文件名带上状态信息和优先级调度进程按文件名排序消费。这个方法在极其简单但对可靠性和性能要求不高的场景下完全够用而且更好调试。3.2 一个 100 行内的调度与执行循环下面我给出一个精简版本的 Agent-Reach 调度核心代码。它做了这样几个事情从 Redis 队列里读取任务Task检查任务是否在允许执行的窗口期执行任务并记录执行结果依据不同结果走不同分支处理。import asyncio import json import time import redis.asyncio as redis class AgentWorker: def __init__(self, task_registry, state_store): self.task_registry task_registry self.state_store state_store async def fetch_task(self, r): 从Redis列表左侧弹出一个任务 raw await r.lpop(agent_reach:queue) if raw is None: return None task_obj json.loads(raw) return task_obj async def run(self, r): task_obj await self.fetch_task(r) if task_obj is None: await asyncio.sleep(1) return task_name task_obj[task] context task_obj.get(context, {}) # 防止重复执行 if await self.state_store.has_executed(task_obj[trace_id]): print(f跳过重复执行: {task_obj[trace_id]}) return task_def self.task_registry.get(task_name) if task_def is None: print(f未注册的任务: {task_name}) return try: result await asyncio.wait_for( task_def.execute(context), timeoutcontext.get(timeout, 30) ) ok, msg await task_def.validate(result, context) if ok: await self.state_store.mark_success(task_obj[trace_id]) print(f任务 {task_name} 执行成功: {msg}) else: await self._handle_failure(r, task_obj, msg) except asyncio.TimeoutError: await self._handle_failure(r, task_obj, 任务执行超时) async def _handle_failure(self, r, task_obj, reason): retries task_obj.get(retries, 0) if retries 2: print(f任务 {task_obj[task]} 重试超过上限进入死信队列) return # 指数退避后重新入队 task_obj[retries] retries 1 await asyncio.sleep(2 ** retries) await r.rpush(agent_reach:queue, json.dumps(task_obj))这段代码的精髓不在于短而在于表达了一种非常朴素的执行模型拿任务、判断是否执行过、执行、校验、处理失败。尤其是state_store.has_executed这个检查在 Agent 场景中异常重要因为 Agent 可能因为网络波动重复投递同样的任务如果没有幂等保护轻则重复通知重则数据重复写入。3.3 为什么我会故意把路由逻辑放得很靠后你在上面代码里看到我把 Router 的调用做成了隐式的失败后直接回队重试成功则自然流向下一个 Worker 循环。有人可能会问说好的状态机路由呢我之所以这样设计是希望先保证执行骨架的准确性。如果一个任务连幂等、超时、重试都处理不好再优秀的路由也只能把错误放大。主流程越简单越容易在出问题时定位到谁坏了。实际经验也证明了这一点在骨架跑通之前那些精心设计的路由逻辑往往只能在纸上跳复杂舞蹈搬到真实系统里立刻被各种边界条件击穿。4. 解决真实业务里的协作问题任务路由、优先级与并发控制当 Agent-Reach 的基础骨架稳定下来之后我开始往里面加业务规则。这时候我遇到了三个绕不开的真实业务问题正好回应了 Agent-Reach 命名里Reach的含义以后你要做类似的系统也大概率会撞上。4.1 多个 Agent 之间怎么共享任务上下文在真实场景中业务上下文不会像教科书里那样干净。比如我要做一个全网竞品价格跟踪的任务步骤1: 从用户中心拿到所有需要追踪的商品ID列表步骤2: 根据商品ID去各大公开数据源查询价格多个数据源并行步骤3: 解析价格变化并写入结果库步骤4: 判断是否存在显著价格波动若有则生成告警。这四个步骤天然共享同一个商品ID价格基准数据上下文。在 Agent-Reach 中我为每个任务链创建了一个SharedContext对象它可以被所有 Agent 读写并最终持久化到 Redis 或数据库。class SharedContext(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.trace_id ftrace-{time.time()}-{uuid4().hex[:6]} def to_dict(self): return dict(self)这个设计虽然是老生常谈但在 Agent 场景里有个容易忽略的细节多个 Agent 对共享上下文的写入顺序会影响最终结果。比如步骤 3 的解析 Agent 可能在步骤 2 的价格 Agent 还未全部返回时就尝试读取价格列表导致拿到的是被截断的数据。解决方案也非常朴素给上下文加 key 级别的事件信号。只有prices_ready这个 key 被置为 True解析 Agent 才会开始循环等待。听起来像极了 Thread Barrier但工作得很好。你把上下文比作一个白板每个 Agent 在走廊里排队轮到自己才动手而不是一拥而上。4.2 优先级队列关键 Agent 不能和杂务 Agent 挤同一根水管真实系统里任务权重差异极大。我一度把高优告警任务和低优数据统计任务塞在同一个 Redis 队列结果数据量一上来高优任务被一堆低优任务堵在队尾告警晚了几分钟才发出来业务方自然不高兴。Agent-Reach 的解决办法是对队列做多个 bucketqueue:critical高优任务比如价格波动告警、用户主动操作触发的事件queue:normal常规业务任务比如同步商品详情、更新库存queue:scan低优任务比如每天凌晨的数据归档、统计报表。Worker 消费时按比例取样比如critical队列每取 3 个任务才取 1 个normal而scan则仅当critical和normal都为空时才消费。queues [ (agent_reach:queue:critical, 3), (agent_reach:queue:normal, 1), (agent_reach:queue:scan, 0), ] async def fetch_weighted(self, r): for queue_name, weight in queues: if weight 0: continue raw await r.lpop(queue_name) if raw: return raw raw await r.lpop(agent_reach:queue:scan) return raw这其实就是把控制面从任务何时入队下沉到任务批次如何出队。看起来只做了一小步调整却让 Agent 系统的响应速度质量提升了一个级别。4.3 并发控制与背压的视角有一个坑我会在项目里反复强调并发控制不能只靠任务队列的数量限制还必须配合执行端口的信号量。Agent-Reach 的执行端我用了 Redis-based semaphore保证同时运行的 Agent 总数不超过阈值。如果多个 Worker 进程同时拉任务靠本地信号量是管不住的一定要用分布式手段。我是这么实现的async def acquire_semaphore(self, r, key, permits10, timeout_s5): deadline time.time() timeout_s while time.time() deadline: # 用原子的减法模拟PV操作 count await r.decr(key) if count 0: return True await r.incr(key) await asyncio.sleep(0.1) return False当然这是简化的演示代码生产环境建议用 RedLock 或者干脆实现一个带心跳的令牌桶。用这个方案之后我再也不用担心某个 Agent 因为同时拉起 1000 个下游请求把目标服务打爆了。5. 我最喜欢的特性反馈闭环的实际应用案例这部分算是我个人在这个项目里最喜欢的部分也最能回答Agent-Reach 凭什么称自己为智能体而不是简单任务队列。5.1 设计一个轻量级的 经验存储经验存储底层其实就是类似 SQLite 或者一个 HashMap但必须提供三个操作record_experience(key, condition, tags, advice, ttl)query_experience(condition)— 返回命中规则集合update_experience_priority(rule_id)— 按命中次数调整权重这些操作不用引入向量数据库那么重的依赖。重点在于什么样的经验会让 Agent 变得更聪明只要信息粒度合适、没有过度匹配甚至最简单的一对一映射都能显著改善任务执行效果。比如我管理一个开源的公开学术数据抓取任务时发现某个数据源对同一 IP 的访问频率限制得又紧又爱搞静默限流——看起来响应 200实际内容被截断。我第一次遇到时靠人工观察日志才发现问题。后来把这个经验作为一条规则写入存储rule { if: response.ok True AND len(parsed_items) expected_min_count, then: 标记为潜在截断等待人工确认, priority: 5, ttl_days: 15, tags: [citation-data-source] }之后每当系统再次碰到这个特征时Agent 会主动降低该数据源的并发度并做一次二次请求来对比验证。这在没有 Agent 框架的传统任务系统中基本做不到因为触发动作需要通用规则判断而 Agent-Reach 的反馈闭环天然支持这类由结果反馈驱动行为的机制。5.2 什么场景最好不要用反馈闭环老实说虽然反馈闭环很强大但它不是万能的。我强烈建议你把闭环规则限制在低危操作。比如数据采集的降级策略、重试时机调整、请求参数调整这些误伤成本都很低。但如果 Agent 要做的是高风险决策比如自动扣费、自动关停服务器那就千万别让 Agent 根据失败经验直接调整行为。最好的做法是设置一个 human-in-the-loop 框架Agent 能做的仅是把事件分类并丢到一个审批队列里。在 Agent-Reach 的设计里我给这类 Agent 打了一个特殊标签needs_review一旦被标记Agent 的任何 then 指令都必须先经过审批网关才能执行。5.3 经验库可能会遇到的反模式我观察到很多人把 Agent 的 记忆 设计得很复杂比如用大模型接口做语义向量化然后又想搞知识图谱。我觉得对于 95% 的业务场景来说这是个不必要的复杂度。反模式主要有两种规则堆砌型 只加规则不做淘汰最后经验库变成一本千页攻略查询效率和准确性双下降。玄学归因型 把任务失败原因归结到一个正确率本就不高的模型输出上导致规则混乱且难以追溯。Agent-Reach 的经验库我刻意保持简单每个规则只保存触发条件、动作建议、置信分数、最后命中时间四个字段。所有规则的产生必须经过人工或者严格代码逻辑测试只有达到一定置信分数的才会自动执行。这个设计为系统带来了非常强的可解释性。6. 从单机到分布式Agent-Reach 的横向扩展方案基础版本跑通后我很快发现只用一个进程跑 Agent 是拿不出手的。当任务量上到日均数千甚至更多时你需要考虑多个 Worker 进程拉同一个队列、Agent 节点标识、调度器与执行器的分离。6.1 调度器和执行器分离的结构在我设计的 Agent-Reach 中调度器是不执行任务的它只负责根据路由表和反馈闭环决定把什么任务、按什么优先级、送到哪个队列。执行器则是纯粹的消费任务并执行的组件。两者之间通过数据库和 Redis 通信实现完全解耦。为此我给每个任务对象增加了一个assigned_worker_id就是为了确保任务被某个 Worker 认领后不会被其他 Worker 再拖走。这在分布式环境下尤其重要不然任务执行时如果 Worker A 挂了但任务已经被标记为执行中其他 Worker 永远不会接着处理任务就一直卡死。一个简单的解决方案是给任务增加 lease租约概念。Worker 在拉取任务成功后必须向 Redis 写入一个 TTL 租约比如 60 秒。若 Worker 在执行中途崩溃租约到期后任务可以被重新投递到队列。async def claim_task(self, r, task_obj, lease_seconds60): # 用 WATCH 事务防止并发签收同一个任务 key fagent_reach:lease:{task_obj[trace_id]} ok await r.set(key, self.worker_id, exlease_seconds, nxTrue) return bool(ok)这基本上是每个分布式队列都要处理的问题不太可能是独门绝技但是 Agent 场景容易被忽略因为 Agent 显得太智能了要是拿着失败会自动重试的借口去掩盖底层租约缺陷后续排查会特别痛苦。6.2 想清楚为什么需要多个 Worker 节点我见过太多人动不动就上一堆 K8s Pod 来跑 Agent结果真实瓶颈根本不在节点数量上而是任务之间逻辑耦合太紧密多节点只会把问题放大。Agent-Reach 的经验是先把单机多 Worker 跑稳定了再谈分布式。单机多 Worker 只需要multiprocessing造几个子进程拉同一个 Redis 队列根本不需要 Kubernetes 和服务发现。如果你的系统连这个模型都经常执行失败那不是节点不够而是逻辑有问题。在你确定需要横向扩展后请务必把工作重心放到任务无状态化。Agent 之间不要通过共享内存或者共用一个临时文件来交换数据必须统一走 Redis 队列或数据库。这也是我在项目里踩过最深的一个坑——起初为了图方便把一份中间结果文件写在同一台机器的 /tmp 目录结果后来加了 Worker第二个 Worker 根本拿不到第一个 Worker 生成的文件。6.3 我把分组路由也做了进去分布式环境下不同源的 Agent 往往要和特定机房或特定资源绑定。所以我在 Agent-Reach 里加了一个worker-group概念。每个 Worker 启动时可以声明自己属于哪个分组python worker.py --group cn-shanghai --group cn-shenzhen任务入队时也可以指定group。Router 在推送任务时如果任务包含 group 约束就只会投递到对应分组的队列。后续我还基于此实现了不同的资源配额管理比如把耗时的 LLM 推理 Agent 划到 GPU 分组把纯 IO 任务放在普通分组。这个方法很土但在多团队共用一套系统的时候尤其有效不会出现某个重任务把同组轻任务的节点资源全部抢跑的情况。7. 可观测性与排障经验没有日志Agent 就是黑盒我周围的不少朋友一开始做 Agent 系统时最喜欢的部分是实现各种 智能 逻辑最讨厌的则是写日志和出指标。但恰恰是到了排障阶段日志和追踪的功底才会真正分出水平。Agent-Reach 里我用了一套比较实用的观测体系。7.1 结构化日志带来的排查效率提升传统方式打印日志是print(task failed:, trace_id, task_name, error)但如果你有多个 Agent、多个 Worker、跨了好几个阶段用人眼去这个日志里找因果链会崩溃。所以我从一开始就把日志统一为结构化 JSON{ ts: 2025-01-20T18:30:21.421Z, event: task_completed, task: product_sync, trace_id: 3f7a8d9e1234, worker_id: worker-cn-1, duration_ms: 842, success: true }这样做的第 N 个好处是你可以直接用现成的日志检索工具把这些日志当成数据来查。排查今天哪个数据源比较慢时只需要一句查询语句而不是打开几十个窗口人肉瞎翻。经验之谈 日志里一定带上trace_id和task名宁可重复也不要省跨 Agent 的排障全部依赖它们串起来。7.2 时间戳对齐永远是第一受害者这就引出了另一个大坑。Agent-Reach 在分布式部署后我遇到过任务明明成功但监控图上显示时延暴涨的问题。排查很久发现是 Worker 服务器时间没同步好导致事件写入时间顺序错乱。反直觉的是加了一个 NTP 同步后整个系统都变正常了。所以务必记住任何 Agent 系统的监控和日志必须要求所有节点时间一致。这不是一句废话在实际运行中极其关键。我甚至在 Worker 的启动检查里加了时间偏差检查超过 1 秒就直接拒绝启动从源头杜绝脏数据进入监控库。7.3 状态监控 Dashboard 需要记录哪些维度围绕 Agent-Reach 我搭建的状态监控不是复杂 Prometheus 体系只用最少的指标就把问题观察清楚指标说明作用agent_reach_task_runtime_seconds任务耗时直方图快速定位慢任务agent_reach_task_retries_total重试次数计数发现频繁失败的任务agent_reach_dead_letter_total死信任务计数发现彻底不可恢复的任务agent_reach_queue_depth队列深度判断任务积压情况agent_reach_lease_expire_total租约到期次数判断 Worker 崩溃频率这些指标加上日志再配合简单 Grafana 看板就足以及时发现绝大多数系统异常。我见过不少团队想把这个观测体系一举搭建到天级但最后实际常用的也就两三个指标。务实一点把最危险的几个指标盯牢比一大堆花哨的可视化更管用。8. 项目阶段性复盘Agent-Reach 解决了什么还有哪些不足写到这里我想把这段时间积累的经验做一个总结不追求给所有人一个标准答案而是梳理一下这套系统到底给我带来了哪些改变以及它限制在哪里。8.1 真正好用的部分可解释的执行流在引入 Agent-Reach 之前我最怕的是半夜被用户喊醒数据不对。没有兼容的流程时我往往要登到服务器上带着模糊印象翻日志整个排查过程非常狼狈。有了这套系统以后从任务创建、入队、执行、校验、重试、到最终结果每一个环节都有迹可循。因为你打开 Dashboard直接看到的是某条链路里每一步的耗时和状态。这种可解释的执行流我认为比那些号称自动智能的黑盒系统有更高价值。8.2 离线分析能力的欠缺Agent-Reach 现在对任务历史数据的应用还停留在统计和报警层面。我其实一直想把它往更深的分析方向带比如把大量历史执行结果拿来做失败模式聚类判断哪些任务组合更容易故障。但这需要一个离线分析层以及一套专门的数据管道工作量不小还在慢慢推进。8.3 如果你也想做类似系统我建议从这三个问题开始你的任务定义到底有多清晰 如果你连什么算完成都说不清就不要期待 Agent 能做得更好。你的任务会不会因为外部依赖不同而反复改逻辑 如果是那你需要的是反馈闭环里的经验库而不是继续堆 if-else。你是否愿意在可观测性上提前投入 如果答案是否那无论系统内部多么优秀推广到生产环境都等于没有。最后说一句掏心窝的话Agent-Reach 这个项目让我最深的体会是Agent 系统的智能感并不来自某个单点模块而是来自整个任务定义—执行—校验—反馈链条的严密配合。先尊重工程的朴素逻辑再去谈那些炫酷的智能技巧这才是这条路上最该走的顺序。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑