新闻详情

新闻详情

首页 / 资讯中心 / 详情

Agent底层重构实战:状态机、分层记忆与工具网关如何扛住500并发

发布时间:2026/10/2 15:38:54来源:尧图网络
Agent底层重构实战:状态机、分层记忆与工具网关如何扛住500并发
1. 为什么非要重写Orkas 旧底层是怎么被撑爆的2025年初我们把 Orkas——支撑着公司二十多个 Agent 应用的编排运行平台——的底层整个推倒重写了一遍。很多人听到底层重构第一反应是你们疯了吧但当时真的没得选旧架构下 agent 并发撑不过 30 个P99 延迟在压测里冲到 45 秒任务一多进程就 OOM。更致命的是一旦某个 Agent 任务执行到一半 worker 崩掉整个会话状态直接丢光用户只能重新开始。这篇文章把我从决定重构到灰度上线全过程里想清楚的事、踩过的坑、和最后沉淀下来的方案原原本本讲一遍给正在做 agent 框架、agent 平台或者打算重构 agent 底层的朋友一个参考。1.1 旧版 Orkas 的架构长什么样旧版 Orkas 本质上就是一个带壳的 while 循环每个 agent 任务由一个 worker 线程独占循环体就是调 LLM → 拿结果 → 执行工具 → 把新消息拼进上下文 → 再调 LLM。所谓 harness执行壳和 agent 核心逻辑是焊死在一起的每个 agent 的循环代码里都各自维护着自己的一套工具调用、上下文拼接、错误处理逻辑。你有多少 Agent 应用就相当于复制了多少份布局差不多的执行壳代码只是细节各改各的。这种架构在 demo 场景下完全够用跑三五个演示 agent 非常顺滑但一旦往真实生产环境上顶问题就全暴露了。首先是并发模型太笨重一个线程一个会话线程栈加上下文内存8 核 32G 的机器跑满 30 个并发 agent 就开始频繁 GC再往上顶就直接 OOM。其次是没有可恢复性agent 的状态散落在内存的 messages 数组里进程一崩所有正在跑的任务全部丢失没有 checkpoint没有重放日志。最难受的是上下文管理所有历史消息每一轮都全量塞给 LLM跑 50 轮对话的 agent光历史 token 就能吃掉上万成本和延迟双双失控。1.2 重写范围的边界哪些保留、哪些推翻重构最容易犯的错就是把整个系统当成屎山一锅端。我们在动手之前花了两周列了一份清单把 Orkas 拆成三层对外稳定层、内部实现层、基础设施层。对外稳定层包括两样东西一个是面向业务方的 Agent 任务提交 API另一个是面向开发者的 skill 扩展接口。这两块的语义我们决定完全不破坏让上层二十多个应用在重构期间无感迁移。内部实现层则是这次重写的重点执行循环、状态管理、记忆系统、工具调用链路这四个模块旧代码加在一起大约 3 万行重写后压到了 1.2 万行但功能覆盖反而更完整。基础设施层像是 Redis 里的数据结构和任务队列的 topic 划分该换的换、该迁的迁这一层允许有短暂的不兼容期。这个边界划分的意义在于重构是高风险动作必须把影响面锁死在一个可控范围内。如果连对外 API 都跟着改那上层所有 Agent 应用都要一起返工问题排查时根本分不清是底层 bug 还是上层适配 bug。我们把内部实现完全重写但对外接口语义保持一致相当于给这次大手术装了一根体外循环。2. 新地基的三大支柱编排、记忆、工具调用重写后的 Orkas 不再是一个while 循环 全局上下文的脚本而是一个事件驱动 显式状态机 三层记忆 工具网关的运行平台。这一节讲这三根柱子各自是怎么设计的以及为什么这么设计。2.1 生命周期显式化把 Agent 变成一台状态机第一个大改动是把 agent 任务的运行过程从隐式流程改成显式状态机。旧代码里一个 agent 当前到底在干嘛是靠看它卡在哪一行代码来判断的新架构里每个 agent 任务都有一个明确的 state 字段状态迁移只能在定义好的转移表里发生。我们把生命周期定义成七种状态PENDING排队中、INITIALIZING初始化、READY就绪、RUNNING推理中、TOOL_WAIT等待工具返回、COMPLETED完成、FAILED失败。另外还有两个辅助状态 PAUSED人工介入暂停和 CANCELLED取消主要用于运维场景。为什么说这是地基级改动因为显式状态机直接解了两个旧架构的死结。第一是可恢复性每个状态都是一个可持久化的快照点worker 崩了之后新的 worker 可以从最近的快照继续跑而不是从头再来。第二是并发安全状态迁移必须通过 Redis 分布式锁加乐观版本号来做天然杜绝了两个 worker 同时操作一个任务的竞态。实现上我们用一张转移表做合法性校验非法迁移直接抛错而不是像以前那样靠各种 if-else 硬撑。# 简化版转移表key (当前状态, 触发事件) - 目标状态 TRANSITIONS { (State.PENDING, dequeue): State.INITIALIZING, (State.INITIALIZING, ready): State.READY, (State.READY, start): State.RUNNING, (State.RUNNING, llm_done): State.TOOL_WAIT, (State.TOOL_WAIT, tool_done): State.RUNNING, (State.RUNNING, complete): State.COMPLETED, (State.RUNNING, error): State.FAILED, (State.RUNNING, pause): State.PAUSED, } def transit(task_id: str, event: str, version: int, redis) - State: state redis.hget(task_id, state) new_state TRANSITIONS.get((state, event)) if not new_state: raise InvalidTransition(ftask {task_id}: {state} - {event}) # 乐观锁version 不匹配则说明状态已被其他 worker 修改 done redis.hset(task_id, state, new_state, version, version 1) if not done: raise StateConflict(task_id) return new_state状态机带来的另一个隐性收益是观测性。以前我们想知道线上这 1000 个 agent 任务都在干什么只能去翻日志现在直接按状态分桶统计就行一眼就能看到多少任务在等 LLM、多少在等工具、多少卡死在某一步。后面做告警也简单了某个任务在 TOOL_WAIT 状态停留超过阈值直接报警不用再去 trace 代码。2.2 记忆系统拆掉一条上下文打天下的旧模型旧架构的记忆问题可以用四个字概括全量硬塞。每一轮对话都把整个 messages 数组原封不动丢给 LLM既不关心 token 预算也不区分哪些信息还有用。重写时我们把记忆拆成三层每一层都有自己的容量上限和淘汰策略。短期记忆ephemeral当前会话最近几轮交互的原始消息默认预算 4k token采用滑动窗口超出后最老的消息被移出。工作记忆working summary对已经滑出窗口的早期内容做结构化摘要默认预算 2k token摘要里除了发生了什么事还强制提取用户明确提出的约束条件。长期记忆long-term memory跨会话的持久信息写入向量库需要时通过 top-k 检索取回默认取 10 条单条上限 200 token。这个设计的核心思路是不要再让所有历史无差别地占上下文而是让 Agent 在每一轮开始时只装载当前决策真正需要的信息。短期记忆负责最近的对话连贯性工作记忆负责压缩后的中长程信息长期记忆只按需召回。token 预算的分配本质上是在给记忆定价每一层能占多少上下文是经过压测调出来的不是拍脑袋定的。这里有一个很关键的工程细节摘要不是后台定时任务做的而是在短期记忆使用量达到 80% 预算时触发一次压缩动作。压缩动作由一条独立的、更轻量的模型调用完成它读取当前短期记忆里的原始消息输出结构化摘要同时单独抽取出约束条件字段。约束条件会被固定注入进系统提示词避免出现摘要之后把用户早期提出的硬性要求比如全部走新接口别用老的给压丢的情况。这个坑我们后面会细讲属于重构期间踩得最深的一个。2.3 工具调用从直接执行到网关管控第三根柱子是工具调用链路的彻底改造。旧架构里 agent 执行工具的方式就是进程内直接调函数超时、重试、限流这些逻辑散落在各个 agent 的代码里有的写了、有的没写、写的还不一样。新架构里所有工具调用统一走一个组件我们内部叫它 Tool Gateway工具网关。工具网关负责四件事。第一是注册与校验所有工具上线前都要在网关里注册 schema参数由网关统一校验非法参数在进业务代码之前就被拦截。第二是超时治理每个工具可以声明自己的超时时间默认 30 秒超过直接终止不再让一个慢工具拖死整条任务。第三是熔断与重试连续 5 次失败进入熔断状态60 秒内不再发起新调用只有声明幂等的工具才允许自动重试非幂等的比如扣款、发券一律不重试。第四是结果归一化工具返回的内容统一做 token 截断、超长截断和结构化包装避免一个工具吐回 10 万 token 的 JSON 把上下文直接打爆。{ tools: { order_query: { timeout_ms: 5000, idempotent: true, retry: {max_attempts: 2, backoff: exponential}, circuit_breaker: {failure_threshold: 5, open_seconds: 60}, max_result_tokens: 1500 }, payment_refund: { timeout_ms: 8000, idempotent: false, retry: {max_attempts: 0}, max_result_tokens: 800 } } }工具网关还有一个隐藏作用它是天然的安全边界。所有工具调用都在网关这一层做权限校验——这个 agent 的 skill 授权范围是什么、能不能调这个工具、最多能传什么参数都在网关执行。之前 agent 权限散落在各业务系统里审计的时候根本说不清一个 agent 到底碰过哪些资源现在网关统一留痕所有工具调用都有 trace 可查。关于harness 和 agent 的区别我现在的理解也更清楚了agent 只管推理和决策它决定该调用什么harness底层的运行壳负责怎么安全地调、调出问题怎么办。这个分离正是重构最大的收益之一。3. Agent 怎么扛并发从线程独占到异步调度说完了地基的三根柱子这一节专门聊聊并发。ai agent 怎么扛并发几乎是每个做 agent 平台的人都会被问的问题我在这次重构之前的理解也是错的以为并发上不去是机器配置不够其实根本不是。3.1 真正的瓶颈不在 CPU而在于等待Agent 任务的执行模式是高度 IO 密集的绝大多数时间都花在等 LLM 返回和等工具响应上真正在算的 CPU 时间很少。旧架构的并发模型是一个 agent 任务独占一个 worker 线程线程大部分时间在阻塞等待网络 IO这是对计算资源最浪费的用法。一个 8 核 32G 的节点跑 30 个并发 agent 就开始吃紧不是 CPU 打满了而是每个线程的栈空间默认 8MB、以及每份全量上下文的内存几十 KB 到几 MB 不等把内存和 GC 拖垮了。3.2 异步化改造asyncio 不是银弹但方向是对的新版 Orkas 的运行时改为 Python asyncio 驱动所有耗时的 IO 操作LLM 调用、工具请求、Redis 读写全部异步化。单机可以承载的 agent 任务数量直接从几十跳到几百因为一个事件循环里可以同时挂几千个等待中的协程内存开销比线程小一到两个数量级。但异步化只是第一步真正的难点是并发控制。LLM API 是有速率限制的不同供应商、甚至不同模型、不同租户的配额都不一样。我们在运行时里做了一个多级信号量的设计全局有一把信号量限制节点级别同时发起的 LLM 请求数默认 50每个租户还有一把独立信号量防止一个重度用户把整个节点的配额吃光。信号量的数值不是写死的而是根据线上对 LLM API 的错误率动态调整出现 429 就自动降低并发稳定后逐步回升。class AgentRuntime: def __init__(self, llm_max_concurrency: int 50): self.llm_semaphore asyncio.Semaphore(llm_max_concurrency) self.tenant_semaphores: dict[str, asyncio.Semaphore] {} async def run_loop(self, session): while True: async with self.llm_semaphore: # 全局限流 async with self._tenant_semaphore(session.tenant_id): # 租户限流 response await llm_client.chat(session.build_messages()) action await self._execute_tool(response.action_call) await session.apply(action) if session.is_finished(): return session.result()这套异步调度模型还有一个配套设计任务租约lease。一个任务被某个 worker 取走后不是永远归它所有而是带着一个租约时间worker 必须定期续租并上报心跳。如果 worker 进程崩溃导致心跳超时任务会自动回到待调度队列由另一个 worker 接管接管点是最新的状态快照而不是任务开头。这从根本上解决了旧架构进程一崩、任务全丢的问题。租约的续租与状态的乐观版本号绑定避免两个 worker 同时操作一个任务这个机制后面在坑里会专门讲。3.3 压测数据重构前后到底差了多少重构接近尾声时我们在同样的环境8 核 32G 单节点分别对旧版和新版跑了一轮压测对比数据如下。指标旧版 Orkas新版 Orkas变化单节点最大并发 agent 数30500提升约 16 倍300 并发下 P99 延迟无法压到该并发已 OOM8.5s可稳定支撑任务级失败率12%1.8%下降 85%崩溃后任务恢复不支持直接丢失30 秒内自动接管从不可用到可用单任务平均 token 消耗基准下降约 62%记忆分层加摘要压缩延迟这一项我想多说一句300 并发下 P99 是 8.5 秒其中大头是 LLM 推理本身的时间Orkas 自己的调度开销不到 500ms。也就是说异步化之后我们又回到了LLM 是瓶颈的常态而这也正是重构成功的标志——平台本身不再是瓶颈你只需要针对模型 API 做配额管理就够了。4. 重构路上的坑和排查实录重写底层这种工程方案再漂亮落地时一定会有意外。这一节挑四个我们真实踩过的坑都是那种事后看很简单、当时排查到崩溃的问题。4.1 状态机上线后老任务被重复执行了灰度第一批流量进去之后监控里出现了诡异的现象一批老任务的执行次数翻倍了。排查下来发现原因很丢人旧版本的 Redis 任务数据里根本没有 state 字段新代码读取时按默认值处理结果把一批已完成的任务默认成了 PENDING又被调度器捞起来重新执行了一遍。这属于典型的数据迁移问题——你以为自己只是改了代码实际上你同时改了线上数据格式而旧数据不会帮你自动补齐新字段。修复方案是加一个显式的数据迁移脚本所有存量任务在访问前先做一次 schema 校验缺字段的按已结束标记而不是按默认值放行。这一刀切干净之后重复执行就消失了。教训是重构时不要把新代码的默认值当成旧数据的真实含义所有历史数据必须显式迁移哪怕你只是加了一个枚举字段。4.2 记忆压缩搞丢了用户的硬性约束上线记忆分层后遇到一个更隐蔽的问题某个 agent 在会话早期被用户明确告知只能用接口 v2不要碰 v1这个约束被写进了短期记忆。当短期记忆滚动到工作记忆时摘要模型把它当成普通信息压缩进了两行摘要里没有单独结构化保留。过了十几轮之后agent 又一次面临选接口的决策这时候上下文里的约束信息已经模糊它直接选了 v1导致一次线上事故。这个坑直接催生了前面说的约束字段设计摘要生成时强制抽取用户约束单独存成一个结构化字段每一轮都固定注入系统提示词。至此我们才敢说记忆压缩真正可用。我后来跟团队复盘时反复讲Agent 的记忆压缩最该保的不是发生了什么而是用户不允许做什么。什么不能做的信息一旦丢失损失的往往是不可逆的。4.3 租约过期导致的双写和重复执行租约机制上线后我们一度以为并发问题都解决了直到一个支付类工具在线上出现了重复退款。追踪后发现是这么个链路worker A 处理任务租约到期前因为 GC 停顿没能及时续约任务被调度器重新分配给 worker Bworker A 恢复后并不知道自己已经失权继续执行到工具调用于是同一笔退款被提交了两次。这个问题的根源是租约机制只有所有权没有版本控制。我们的修复是引入 fencing token每次租约续期或重新分配都生成一个单调递增的 tokenworker 在每次写状态、每次发起工具调用前都必须携带当前 token由 Redis 校验 token 是否仍然有效。旧 worker 拿着过期 token 发起请求直接被网关拒绝。这个机制是分布式系统里的经典解法真正落地时才发现它跟幂等性是配套的——fencing token 防的是旧进程继续干活幂等键防的是同一个请求被提交两遍两层都需要。4.4 灰度的节奏影子运行、比例放量、功能开关重构再怎么自信也不可能不上灰度直接全量。我们的灰度分四步走节奏很大程度决定了这次重构没有出大事故。第一步是影子运行新版运行时在后台并行执行真实流量结果只用于对比不回放给用户。这一步主要比动作轨迹——同样一个任务进来新旧两版做出的工具调用序列是否一致。第二步是内部用户放量把公司内部工具用的 agent 流量切 5% 到新架构跑一周观察。第三步是按功能开关灰度记忆分层、工具网关这种大改动分别用独立开关控制先开工具网关、再开记忆分层每开一个观察三天。第四步才是全量。每步都配了一键回滚因为状态机是显式可持久化的回滚在技术上就是把 agent 运行时的镜像切回旧版本灰度中的任务全部从最近快照继续。正是因为回滚成本低我们在灰度期间才敢放手压测、才敢让问题暴露而不是靠人工死守。我强烈建议任何打算做 agent 底层重构的团队先把灰度通道和回滚脚本写好再动第一行代码。5. 重构完成后的体感和几条实在建议Orkas 这次重构从立项到全量上线用了 11 周核心代码量从 3 万行压到 1.2 万行线上并发能力提升了一个量级。但这些数字都不如几件小事让我觉得这刀动得值研发同学新增一个 agent skill 的时间从平均 3 天缩短到 1 天线上排查问题再也不用满世界翻日志直接在状态面板里看任务卡在哪一步最明显的是告警变干净了OOM 和任务丢失这两个长期占榜首的告警彻底消失。5.1 长期沉淀下来的几个原则事后看这次重构能成靠的不是某个惊艳的设计而是一组朴素的工程原则。第一Agent 底层一定要把决策和执行分开agent 只负责想清楚调什么工具、用什么记忆harness 负责工具安全调用、状态持久化和并发治理两边用明确的接口隔开。第二状态必须是显式的、可持久化的、可恢复的任何隐式状态都是事故的种子。第三记忆系统的第一步永远是定 token 预算不是选向量库预算不清再牛的检索方案都是空转。第四工具调用链路一定要有一个统一的网关层超时、熔断、幂等、审计全部收敛在一个地方否则每个 agent 都在重复造一个偷工减料的轮子。5.2 如果你也要重写 Agent 底层最后给正在做类似事情的朋友几个实在建议。第一千万不要顺手改对外 API重构的改动面越小排查问题的半径就越小。第二做一个基于历史流量的重放测试平台把真实会话采集下来、在新旧两版回放、对比动作轨迹这是成本最低的回归手段。第三并发控制先做观测埋点更不能省每个 agent 任务至少要能回答三个问题现在在哪一步、卡了多久、token 花了多少。第四灰度回滚脚本写好了再开工这句话怎么强调都不过分。重构完成之后Orkas 这台老房子算是真正把地基换成了钢筋混凝土。我个人最大的体感是我们终于能回答这个 Agent 现在为什么这么干、下一步要干什么、出了问题怎么恢复这类以前根本答不出的问题。Agent 的底层也许还有更先进的范式在等着被发明但把状态机、事件驱动、记忆分层、工具网关这几块地基打牢无论未来上层怎么演进都不会白费。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

【小白版】OpenCode桌面版+vscode+多模型供应商:把 Base URL 改到 TaoToken 的完整配置 2026/10/2 16:29:58

【小白版】OpenCode桌面版+vscode+多模型供应商:把 Base URL 改到 TaoToken 的完整配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Remote In Tech 公司档案解析:以 Conversio 为例读懂远程友好科技公司的结构化数据模型 2026/10/2 16:29:52

Remote In Tech 公司档案解析:以 Conversio 为例读懂远程友好科技公司的结构化数据模型

数据集 【免费下载链接】remote-jobs Source for remoteintech.company — a community-maintained directory of remote-friendly tech companies 项目地址: https://gitcode.com/GitHub_Trending/re/remote-jobs 点击查看 免费下载 Remote In Tech(RE…

阅读更多 →
冒险岛027源包搭建教程:服务端部署、数据库配置与辅助开发实战 2026/10/2 16:29:45

冒险岛027源包搭建教程:服务端部署、数据库配置与辅助开发实战

简介:这份RAR压缩包提供的是冒险岛V027版本源码,面向游戏开发者、怀旧服搭建者及希望研究2D横版网游机制的爱好者。源码具备无错可编译特点,可直接用于调试、修复和二次开发;整体结构清晰、逻辑严谨,便于快速定位角色、…

阅读更多 →
极速搭建!OpenClaw 一键部署,快速搭建自动化平台:TaoToken 统一 Key 接入实战 2026/10/2 16:29:45

极速搭建!OpenClaw 一键部署,快速搭建自动化平台:TaoToken 统一 Key 接入实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
JetBrains MCP Server 教程:把 Codex auth.json 与 Claude 配置改到 TaoToken 调用 IDE 索引 2026/10/2 16:29:45

JetBrains MCP Server 教程:把 Codex auth.json 与 Claude 配置改到 TaoToken 调用 IDE 索引

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
炸裂!又一 VSCode 神器面世:TaoToken 统一 Key 让 AI 自动编程不再断连 2026/10/2 16:29:38

炸裂!又一 VSCode 神器面世:TaoToken 统一 Key 让 AI 自动编程不再断连

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉