ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Orkas底层重构实践:AI Agent框架的状态管理与并发调度优化

Orkas底层重构实践:AI Agent框架的状态管理与并发调度优化 先说个背景Orkas 是我一直在维护的一个 AI Agent 开发框架最早它只是给内部项目用的一个“半成品脚手架”后来慢慢被团队推着往前走成了好几个业务线的公共底座。这次动它不是心血来潮也不是为了“重构而重构”而是它的底层设计已经明显撑不住当前的业务形态了。所以我决定把最核心的那几层重写一遍包括执行引擎、会话状态管理、工具调用链路和并发调度策略。这篇文章就把这次重构的前因后果、方案取舍、实操细节和踩坑实录完整记录下来。1. 为什么会走到“底层重构”这一步1.1 从业务增长聊起Orkas 当初是怎么诞生的Orkas 最早立项是在 2023 年年底当时我们团队在做一批偏自动化的业务机器人需要把大模型能力和业务系统打通。大家都清楚那会儿市面上的 Agent 框架还比较初级要么过于学术化要么绑定特定厂商的模型服务拿到真实业务里用总是差口气。于是我们自己撸了一个轻量执行内核核心就三件事定义 Agent 的任务循环、封装模型调用、提供工具注册机制。初版设计非常简单甚至可以说是“朴素”一个全局的任务队列一组常驻 Worker再加上一个用字典存状态的 Memory 模块。每个 Agent 本质上就是一个带有 system prompt 和工具表配置的对象跑到完成任务或者到达 max_iterations 为止。当时这套东西跑得挺顺的因为业务量小并发请求基本两位数模型调用频率也不高偶尔挂一个任务重试一下就能恢复。也正是因为它够简单团队接手成本很低后来几个新项目都是直接把 Orkas 拿过去改一改就上线。最多的时候内部同时有六个业务线在跑基于 Orkas 的 Agent 服务每天处理的任务量从几万涨到几十万。也就是从那个阶段开始我陆续收到来自不同业务方的“奇怪”反馈有的任务莫名卡死有的 Agent 上下文串了有的工具调用返回结果张冠李戴。这些问题的根源几乎都指向同一个方向——初版架构虽然能跑但它根本没有为一个“多业务、多租户、高并发”的规模化场景做设计。1.2 当“能用”变成“能扛”哪些信号逼我必须动地基真正让我下定决心重写底层是连续出现的几个生产事故叠加在一起。第一个信号是会话状态错乱。Orkas 早期的 Memory 是全局单例的每个会话通过 session_id 去读写上下文。理论上只要 key 设计得够严谨就不会串但现实是有些业务方在 tool 回调里直接往 Memory 里塞了自定义字段又没有做 session_id 隔离结果两个不同用户的 Agent 在极端时序下读到了彼此的历史消息。这个问题定位了整整一个下午最后翻日志才发现是全局字典的 key 冲突。能从日志里查出来算运气好再往后数据量一大这种问题几乎不可能根因定位。第二个信号是并发能力见顶。我们的压测环境里单个 Orkas 实例在 50 路并发时还能勉强维持一旦超过 100 路模型调用的超时率就开始飙升任务队列积压CPU 被无谓的轮询占满。我仔细看过火焰图相当一部分开销花在了“等锁”和“空转扫描”上说明调度层的设计完全不适合这种高吞吐场景。第三个信号是最致命的工具调用的可靠性。Agent 业务里工具调用是灵魂但我们的工具执行器是同步阻塞的一次 HTTP 调用如果对端响应慢会把整个 Worker 线程占住后续所有任务都在排队等它。更麻烦的是工具返回的数据没有统一的 schema 校验模型经常把字段类型解析错导致下游逻辑报错。这三个信号加在一起我基本可以确定Orkas 需要的不是“打补丁”而是从执行模型、状态存储、工具链路到调度策略的系统性重写。换句话说地基的承重结构已经出了隐患局部加固已经没有意义了。2. 重构前的全局评估与方案选型2.1 先盘点家底现有架构到底烂在哪动手之前我把 Orkas 的代码完整读了一遍画了一张现状图。整个系统可以分成四层接入层提供 HTTP 接口接收外部请求创建会话、触发 Agent 运行。调度层维护全局任务队列Worker 从队列里取任务执行。执行层负责 Agent 的循环推理调用 LLM、解析输出、触发工具。支撑层包含 Memory、工具注册表、配置中心、日志与追踪。看着分层挺清晰但实际耦合非常严重。最典型的问题是执行层和调度层之间没有明确的接口边界Worker 里直接 new 了一个 AgentRuntime这意味着调度器想干预执行过程比如超时取消、优先级抢占根本做不到。再比如 Memory 虽然是独立模块但它的读写散布在各个业务代码里想替换存储后端就得把所有调用点翻一遍。这种“伪分层”架构带来的直接后果是任何一层想动都得连带改其他层回归测试成本极高。所以重构的第一步不是写代码而是先把边界划清楚哪些属于内核哪些属于扩展哪些必须由框架保证哪些应该开放给业务方自定义。2.2 重写 vs 改造我为什么最终选择重构团队里有人提过既然能跑不如在上层加一些补偿机制比如重试、熔断、降级这样改动面小上线风险也可控。这个方案我认真考虑过但最后还是否决了原因有三点。第一补偿机制解决不了“根因类问题”。以会话状态错乱为例如果根因是存储层的隔离设计缺陷那么加再多的重试和熔断也只能降低故障概率不能消除故障。一旦数据量再上几个量级问题必然以更恶性的方式复现。第二旧架构的复杂度已经接近“修改不动”的临界点。有些函数动辄几百行内部同时处理模型调用、工具执行和异常归一化任何一处微小改动都可能引发连锁反应。在这种代码上做增量改造开发效率很低而且每次上线都要全量回归成本远超预期。第三新需求已经明确指向“多租户隔离、动态扩缩容、可观测性增强”这三个方向而这些在旧架构里几乎没有落点。与其花三个月把旧架构补成一个四不像不如花同样的时间把核心层重写干净留下一个清晰、可扩展的底子。最终我的选择是保留对外 API 语义不变重写内部实现。业务方不需要改调用方式但底层的执行、存储、调度全部换成新方案。2.3 新架构的三大设计原则重写不是推倒重来而是带着约束去做。我给新架构定了三条铁律。原则一内核与扩展彻底分离。内核只负责 Agent 执行的最小闭环接收任务、调用模型、执行工具、产出结果。其余一切能力包括记忆管理、权限校验、流量控制、观测上报都通过扩展点接入。这样两边的演进可以独立进行框架升级不必等业务适配。原则二一切状态皆可追溯。每个会话的每一步执行都产生不可变的事件记录从任务接收、模型请求发出、工具调用开始、工具返回结果到最终答案生成全部落盘。这样不仅能解决状态串扰的问题还能让定位问题变得像看回放一样简单。原则三并发模型显式化。我们不再依赖隐式的线程池和全局锁而是引入基于协程的调度模型每个会话有独立的执行上下文任务之间的资源争用通过调度器统一管理。高并发的场景下系统可以明确知道每个 Agent 正在跑什么、等待什么、占用了多少资源。这三个原则看起来平淡无奇但真正贯穿到每个模块的设计里工作量并不小。接下来的几个小节我会把重写过程中最关键的部分逐一拆开讲。3. 地基重写的具体实施过程3.1 核心网关从单体路由到按需调度的拆分旧版的接入层是一个简单的 HTTP Handler收到请求就创建一个 Agent 实例开始跑没有任何前置检查。新版本我把这个环节重构成“网关 路由 调度器”三段式结构。网关层负责做统一鉴权、流量染色和协议适配这里不涉及任何业务逻辑入口流量先在这里被标准化成一个内部调用请求。路由层根据请求的 app_id、agent_type 和 session_id 决定把任务送到哪个执行单元这一步是实现多租户隔离的基础。调度器则负责真正的资源分配它维护一个优先级队列每个任务都带着权重和超时时间Worker 不是“抢任务”而是由调度器“派任务”。我举一个具体的例子来说明这层拆分的意义。旧架构里如果两个业务方同时提交大批量任务它们共享同一个队列一个业务方的慢任务会拖慢所有业务方。新架构里每个业务方在调度器里拥有独立的队列和配额慢任务只影响它自己的队列其他业务方完全无感。这在实现上其实不复杂就是在任务入队时打上 namespace 标签出队时按 namespace 进行配额控制。3.2 会话与记忆层状态管理的一次彻底换代这一层是重写中改动最大的部分也是收获最明显的部分。旧版全局字典式的 Memory 被我彻底废弃取而代之的是一个基于事件溯源的会话存储模型。所谓事件溯源简单说就是不存“当前状态是什么”而是存“状态是如何一步步变成这样的”。每个会话都维护一个有序事件流例如user_message、assistant_start、tool_call、tool_result、assistant_end。要恢复会话的当前上下文时只需从事件流的起点回放或者按需生成一个快照然后把增量事件叠加到快照上。这个设计带来的直接好处有两个。第一状态串扰问题从根上消失。每个会话的事件流是独立的 Append-Only 记录物理隔离不存在一个共享容器里多个 session 互相覆盖的可能。第二排查问题变得极其直观。以前出问题只能靠打日志猜现在直接可以把这个会话的事件流拉出来看哪一步的 tool 返回了异常哪一步模型的输出和预期不符一目了然。实际存储上我没有引入特别重的中间件而是用了一个支持追加写入和范围查询的 KV 存储以 session_id seq 作为复合键。每个事件写入时还会附带一个服务端时间戳方便后续做时序分析。这里有一个具体的参数选择事件快照我设置成每 20 条增量自动生成一次快照内容的序列化格式用 MessagePack相比 JSON 能省差不多 40% 的存储空间回放时的反序列化速度也更快。3.3 工具调用与 MCP 集成层的重构工具调用链路是整个 Agent 系统里最容易出问题的地方也是这次重构里我花心思最多的一块。旧版的工具执行器是同步调用一个工具被拉起后整个 Worker 就被占住了哪怕对端响应需要 30 秒其他任务也只能等着。新架构我把工具执行器改造成异步调用模型配合信号量机制控制并发上限。具体实现上每个工具在注册时都要声明自己的执行类型sync、async或者stream。sync类型适合内部函数计算比如算个哈希、查一下本地配置执行时间通常在毫秒级。async类型适合外部 API 调用执行器会把它包装成一个 Future交给独立的事件循环去轮询完成状态。stream类型则专门为流式输出设计比如模型生成过程中逐步返回内容。不仅如此新版本里我把工具调用的结果做了一个强制 schema 校验。每个工具必须声明自己的输出结构比如字段名、类型、是否可空模型在决定调用工具之前系统会先把工具的描述和参数说明注入到上下文中工具返回后执行器还会对结果做一次结构校验不合格就直接丢弃并触发一次重新规划。MCP 集成层的重构同样重要。老版本里 MCP 服务的接入方式是写死的新增一个 MCP Server 就要改代码。新版本我把它抽象成“连接器”接口任何 MCP Server 只需要提供一份标准化的协议描述文件就能动态接入。连接器的生命周期也做了统一管理空闲超过 5 分钟的连接自动释放下次使用时重新建立避免长期占着连接资源。3.4 并发控制与资源隔离并发这块我引入了一个基于令牌桶的全局流量控制模块。每个 Agent 会话在执行前都必须向调度器申请令牌令牌的数量取决于当前实例的 CPU 核数、可用内存以及模型服务的配额。这里我给出一个具体的计算参考。假设一台实例配置是 8 核 16G模型服务的并发上限是 40那么我们设置的基础令牌池就是 40。每个 Agent 任务在执行时如果它需要调用外部工具还会额外申请一个“工具调用令牌”这个令牌池单独设置上限为 20。这样设计的原因是工具调用会占用额外的网络 I/O 和等待时间如果不单独控制模型并发和工具并发叠加起来很容易把实例的资源打满。资源隔离方面我按 namespace 做了四级隔离连接数隔离、令牌配额隔离、队列深度隔离和日志存储隔离。每一级都有独立的监控指标任何一个业务方出现异常流量都能在监控面板上第一时间定位到具体 namespace而不是整个系统一起遭殃。4. 重构期间的踩坑与排查实录4.1 迁移数据时冒出来的隐性问题任何重构都绕不开数据迁移Orkas 也不例外。我们当时面临的情况是旧系统的会话状态全部存在 Redis 的 Hash 结构里需要迁移到新的事件流存储。一开始我写了个简单的扫描任务把每个 session 的 Hash 读出来拼装成初始事件写入新库。结果跑到一半发现有些会话的上下文在迁移过程中丢失了最后几轮消息。查了半天原因发现问题出在“读快照和写事件不是原子的”。旧系统在持续写入我的迁移脚本读到的 Hash 可能是一个中间状态读完成之后又有新的写入但那条新写入没有被包含进迁移事件流里。这直接导致迁移后的会话上下文不完整尤其是在活跃会话上丢消息的概率更高。解决方案分两步走第一步迁移脚本只处理超过 24 小时没有更新的“冷会话”这些会话不会被写入干扰迁移结果可靠。第二步对于活跃会话采用双写策略新写入同时写到旧 Hash 和新事件流持续一段时间后再整体切换读流量。这个方案看起来笨但在保证数据不丢的前提下是最稳的。4.2 并发压测里反复出现的幽灵 Bug新架构上线前我做了一轮比较狠的压测一次性拉起 300 个并发会话每个会话 10 轮模型调用、每轮带 2 个工具调用模拟线上真实流量。压测一跑立刻暴露了一个很有意思的问题——某些任务的响应时间偶尔会突然飙到几十秒然后又恢复正常。刚开始我怀疑是模型服务的问题但去查模型服务端的监控发现它的响应时间非常平稳P99 只有 800 毫秒。问题显然出在我们自己的链路里。我开启了全链路追踪逐个事件排查最后发现耗时的根源在网络库的连接池设置上。我们的工具调用执行器使用的是一个共享 HTTP 连接池默认最大连接数是 50。压测场景里工具调用频率极高连接池被瞬间打满新的请求只能排队等待空闲连接这就造成了“响应时间尖刺”。问题是这个尖刺看起来像是偶发的因为只有连接池耗尽的那一刻才会出现。修复办法很简单把连接池的上限从 50 调到 200并增加一个“等待超时快速失败”的策略连接排队超过 3 秒直接返回错误让 Agent 走重规划路径而不是傻等。修复之后同样的压测场景下 P99 降到了 900 毫秒以内。4.3 兼容性灰度方案的设计底层重构最怕的就是“一次性切换出了事全完”。我们的做法是设计了一个比较保守的灰度策略。第一阶段新老两套架构并行运行线上流量按 1% 的比例切到新架构持续观察两天重点看错误率、延迟和会话状态完整性。第二阶段把切换比例提升到 10%同时对新架构的会话事件流做抽样检查人工比对部分任务的执行结果是否和老架构一致。第三阶段双跑验证没问题后把流量切到 50%并且让新老架构共享同一份会话事件流这样即使新架构执行异常还能用老架构兜底。这个灰度方案总共跑了大概两周时间。过程中发现过一个问题老架构里某些工具调用的参数封装方式和新的 schema 校验规则不匹配导致 10% 灰度阶段偶尔出现工具调用失败的告警。解决方式是在工具执行器里增加一套“兼容模式”当 schema 校验失败时尝试用宽松模式解析一次解析成功就记一条告警日志提示业务方尽快修正工具定义。5. 验证与运营结果反馈5.1 性能指标变化重写完成并全量上线一个月后我拉了一组指标做对比数据变化还是比较明显的。相同压测规模300 并发P95 响应时间从 8.2 秒降到 2.1 秒P99 从 15 秒降到 3.4 秒。单实例支持的最大并发会话数从约 80 提升到 350峰值时 CPU 使用率反而下降了约 25%。这要归功于协程调度替代了线程池阻塞等待减少了无谓的上下文切换。工具调用成功率从 96.2% 提升到 99.8%失败的场景主要集中在对端服务真正不可用的情况不再有因连接池耗尽或超时设置不合理导致的内部错误。线上会话数据错乱、串记忆的工单数量降为零之前平均每周至少能收到两起相关投诉。5.2 稳定性与服务治理改善除了性能数字稳定性方面的改善更让我觉得这次重构值了。最直观的一个变化是故障定位的时间从“小时级”缩到了“分钟级”。以前排查一个线上 Agent 行为异常需要在日志里翻各种上下文还不一定能拼出完整链路。现在直接打开会话事件流从任务创建到每一步工具调用、模型返回、状态变更全部可视化基本上一眼就能看出问题出在哪个环节。另外新的资源隔离机制让多业务方共享同一套 Orkas 集群变得非常省心。以前是“谁都怕被别人拖垮”现在是每个业务方都看得到自己的配额和实时水位出了问题也影响不到别人。已经有业务方主动要求接入新的可观测面板定期检查自己 Agent 的工具调用效率和令牌消耗情况。6. 一点后记重构这一路走下来我的一个很深的体会是Agent 框架的底层设计拼的不是某一个炫酷的技术点而是“在复杂业务场景下还能不能保持简单和可控”。事件溯源、协程调度、连接器抽象这些概念单独拎出来都不新鲜但真正把它们组织成一个自洽的系统并且经受住真实流量的考验还是有很多细节需要打磨。如果你也在维护一个 Agent 框架或者准备从零搭一个我建议你从一开始就把状态隔离和可观测性放在最高优先级。这两个东西在业务量小的时候显得“没有必要”但一旦规模上来它们就是你的救命稻草。还有一个实用的小建议重构时尽量保持对外接口不变哪怕内部改得面目全非也先让业务方无感迁移。技术上的重构已经够累了别再让业务方陪着一起折腾。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进