ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

AI工具链重构复盘:从调度器到平台化架构的演进实践

AI工具链重构复盘:从调度器到平台化架构的演进实践

这套项目复盘我拖了大半年才动笔。AIStarter和PanelAI这两个产品,一个负责把AI任务跑起来,一个负责把跑起来的任务管清楚,两个项目在同一家公司里并行演进,经历了从单体工具到平台化架构的完整重构过程。这次复盘不聊广告语,只聊我踩过的坑、删掉的重写、重构前后的决策逻辑,以及那些当时觉得正确、回头看其实可以做更好的判断。

写出来主要是想给正在做AI工具链产品重构的同行一个参考。尤其是那些手上同时管着“调度执行器”和“管理面板”两条产品线的同学,你们早期感受到的别扭,大概率不是错觉,而是架构分裂的前兆。

1. AIStarter最初的设计边界:能启动,不等于能用好

1.1 原始的形态和它解决的问题

AIStarter最早不是平台,更像一个高级脚手架。用户填写模型名称、上传提示词模板、选定输入文件,系统就会自动拉起推理任务,把结果打包返回。听起来很直接,但早期版本事实上解决了三件事:本地模型服务的接入、推理任务的批量触发、以及提示词模板的版本管理。

这三件事在2023年初是实打实的痛点。当时的团队都在各自写推理脚本,每个脚本里都硬编码着模型路径和API地址,换一个模型就要改代码。AIStarter把变量抽出来,用一套Web界面统一管理,本质上就是给团队内部用的低代码调度器。

这时候的架构非常简单:FastAPI提供REST接口,Celery处理异步任务,Redis做消息中间件,PostgreSQL存任务元数据和模板文件。单机部署,一套代码跑完所有逻辑。体量小的时候没有任何问题,即便同时跑上百个推理任务,Celery Worker拆开就行。

但问题在用户量涨起来之后慢慢显现。首先是任务类型开始分化:有人要跑Stable Diffusion批量生成图片,有人要调LLM做文本分类,还有人要跑OCR预处理。不同任务的资源占用差异极大,GPU饱和大户和轻量文本任务挤在同一个队列里,互相拖累。其次是任务依赖关系出现:用户想先做数据清洗,再喂给大模型做推理,然后自动把结果回填到数据库里。这种多步骤工作流,在Celery里表达起来极其痛苦。

1.2 “跑得起来”和“管得好”是两套能力

我在重构启动器的时候反复被一个认知纠正:AIStarter的核心竞争力被团队误判为“执行效率”,用户真正花的钱买的其实是“确定性”。所谓确定性包含三层:任务到底跑没跑成功、跑失败时是谁的责任、以及某一次结果是用哪个版本的模板和模型生成的。

结果当时的产品只实现了第一层,后两层完全没有。任务失败就红了,但为什么失败、失败之后能否重试、重试用的是不是同一套参数,全都靠用户自己登录服务器查日志。有一段时间AIStarter的留存率上不去,问题不在产品速度慢,而是出了问题之后用户只能干瞪眼。

这个阶段让我意识到:任何偏底层的AI工具,如果不能在“可观测性”上做到和“可执行性”同等级别的体验,那它本质上还是一个半成品。PanelAI最开始就是以补齐这块短板为目标立项的,结果做着做着,牵扯出来的管理问题远比预想的多。

2. PanelAI的诞生逻辑:控制面板不是附属品,是分布式系统的必需品

2.1 多模型、多密钥、多版本带来的管理灾难

当AIStarter支持的模型源从两三个变成二三十个,用户画像也从个人开发者扩展到小型团队,配置管理的复杂度开始指数级上升。每个人手里可能同时持有多个服务商的API Key,有的按量付费,有的按Token付费,有的内部私有化部署。密钥散落在各人的环境变量里,谁泄露了无法追溯,谁的额度用超了也不预警。

PanelAI在1.0版本做的就是密钥托管和用量聚合,把散落在各处的Key集中到一个面板里,通过统一网关转发请求,由网关记录每一次调用属于哪个项目、哪个人、哪条链路。到这一步它就不是传统意义上的“后台管理页面”,而是一个真正的基础设施组件。

这里有个典型的架构决策值得展开讲:当时我们讨论过两个方向,一是把管理能力内嵌进AIStarter直接做一个全功能模块,二是拆出一个独立服务专门负责控制面和数据面。最后选了后者,理由很朴素——AIStarter的执行链路将来大概率要容器化和边缘化,让调度组件背着管理数据库跑,既拖慢启动,又让两类团队的工作发生耦合。事实也证明这个决定是划算的:PanelAI独立演进后,后来做多租户隔离时基本不用改动AIStarter的核心调度代码。

2.2 双产品并行时的接口契约与版本协同

两条产品线并行之后,最痛苦的不是写代码,而是定契约。AIStarter负责把任务发给执行引擎,执行引擎回调PanelAI做状态同步,PanelAI再反向对AIStarter下发控制指令。整个链路涉及三套协议:任务定义协议、状态上报协议、控制指令协议。

我们早期采用的办法是共享一份OpenAPI定义文件,所有变更都必须在同一份Schema上做扩展。加字段可以,删字段必须提前两个版本做deprecation。允许实现进度不一样,但接口演化方向必须对齐。中间经历过一次改名风波:原定的task_type字段,AIStarter那边理解为“执行引擎类型”,PanelAI理解为“业务场景类型”,同一份字段出现两套枚举值。

这是典型的“分布式产品线的命名冲突”,修复成本只在当时看似不高,改字段涉及迁移旧数据、升级所有客户端,代价被严重低估。这次事故之后我们建立了语义化命名规范,前缀必须带命名空间:engine.kind、scene.type、meta.owner。丑,但不会错。

3. 架构演进的核心技术决策:从单体姿态切换成两张皮、三层结构

3.1 引入事件驱动替换任务队列

重构的导火索是线上出现过一次长任务饿死短任务的案例。Celery的默认队列是先进先出,一个超长的视频推理任务堵在队列头,后面十几个轻量的文本分类任务全部排队等待。虽然可以用优先级队列缓解,但当混合任务依赖关系和资源类型之后,任何排队策略都显得僵硬。

我们最终把调度核心从“队列”抽换成了“事件流”。所有任务状态变更不再通过轮询获取,而是模型服务、执行器、面板三者之间通过事件总线广播。任务进入执行器时发布task.started,执行器内部步骤完成时发布step.completed,全部成功发布task.succeeded,失败发布task.failed。事件流经过订阅层,写入PanelAI的时序数据库,同时也触发AIStarter的补偿动作。

选型上我们用了NATS,理由很直接:轻量、支持JetStream持久化、配置简单,运维成本比Kafka低一个量级,团队规模不到十个人,没必要上重型消息集群。如果你同样在做中间件选型,我的建议是先画清楚事件的消费方有几个。如果下游只有日志存数和状态更新,Kafka有点重量级;如果下游要做复杂事件溯源和流处理,NATS的生态支持又会让你后悔。

3.2 网关层到底需要拦截什么

PanelAI的重心逐渐落到API网关和数据平面上。网关不只是转发,它承担了三件必须做的事。

第一件是鉴权与密钥替换。用户的原始Key只存于加密库,调用时网关读取并注入真实Key,日志和任务详情里永远不出现明文。这一点在后来的安全审计里拉回了不少分数。第二件是限流与配额。每个租户有月度额度,网关侧做令牌桶拦截,超额请求直接返回429,不再进入执行链路。第三件是响应缓存。同一个提示词模板配上同参,在大版本模型迭代之前,缓存命中率能到15%,省下的成本对中小团队很可观。

三层网管的结构大概是这样的:

  • 第一层:接入网关,处理TLS、路由、Header标准化
  • 第二层:策略网关,处理鉴权、限流、密钥注入
  • 第三层:服务网关,处理具体任务的协议转换与模型适配

网关层是我整个重构过程中建议优先级放最高的组件。因为任何业务逻辑都可以以后再补,但所有请求都从同一个入口过一遍,后续做统计、审计、配额、缓存才有一个插入点。

3.3 模型抽象层与适配器的长期价值

运营后期我们发现,AIStarter的代码里,对具体模型服务的调用散落各处:有的代码块直接用OpenAI的client,有的用HTTP调用本地vLLM接口,有的调HuggingFace的InferenceEndpoint。每一处都在自己组装请求体、解析返回值、处理错误码。这导致每对接一个新模型,就要改五六个文件。

架构重构时我们加了一个专门的适配器层,所有上游模型服务都实现统一接口。核心抽象是三个方法:build_request、parse_response、translate_error。模型差异都被封装在这三层里。业务代码只面对一个统一的ModelClient,调用格式统一成client.run(task_spec)。

加上这层抽象之后,新增模型供应商的工时从三到四天压缩到半天到一天,而且很多模型可以走通用模板零开发接入。这个抽象带来的间接收益是成本控制变得可编程了:所有请求都汇总到统一出口,按服务商维度做成本归因就只是SQL聚合的事了。

4. 产品重构逻辑:从功能堆叠到业务闭环

4.1 用户动线从“先选模型”变为“先选场景”

技术上重构成功的标志是架构清晰不崩,产品上重构成功的标志是用户自作主张地走了新路径。AIStarter旧版的用户动线是典型的工具型思维:打开页面、新建任务、选择模型、填写参数、运行。这个路径默认用户已经知道该用什么模型,而真实用户往往只知道“我要做的是把客服工单自动分类”。

1.x重构版本把入口改成了场景模板:工单分类、文章摘要、图片批量生成、语音转文字。用户进来先选场景,再选模型。模型选择被降级为高级选项,默认值由系统根据效果、速度、价格三个维度自动推荐。

这套变化看似是交互微调,底层其实是产品责任的变化。旧版把“选模型”的责任推给用户,新版把“选对模型”的责任揽到自己身上。为了让推荐逻辑有数据支撑,PanelAI的用量统计反过来成了AIStarter推荐模块的输入源:同一场景下,哪个模型的历史成功率最高、平均延迟最短、Token消耗最低,算法会计算综合推荐分。

4.2 多租户隔离与角色权限的重构

产品演进到企业级服务之后,单用户的权限模型彻底不够用了。早期PanelAI只有两种角色:管理员和普通用户。企业客户一进来就有了需求:“我们公司三个部门都有自己的项目,项目之间不能互相看到数据;部门里的模型Key是共用的,但每个人每天能调用的额度不同。”

这一轮支撑多租户的数据模型改动很大,主要围绕四类对象做设计:

  • org(组织):最高层级,包含所有项目和成员
  • project(项目):资源隔离的基本单位,绑定模型配置
  • member(成员):绑定角色,支持自定义角色权限
  • usage(用量记录):每一笔调用记录归属某个项目与成员

权限判断不再放在应用层散点判断,而是统一收敛到网关的授权中间件。每个请求头里携带JWT,网关先解析租户上下文,再从策略库查询该角色允许调用的资源范围。这个方案虽然IO次数多,但逻辑单一,排查权限问题非常快。

4.3 从“执行工具”到“算力资产”的认知转变

产品重构过程中最抽象但也最关键的一步,是被迫重新定义AIStarter到底是什么。最初它被当作任务执行工具,使用价值在于省事;后来被看作流程自动化平台,使用价值在于串联业务;再后来PanellAI把用量数据补全后,两个产品联合起来更像一套算力资产管理台:每一次模型调用背后都有真金白银的成本,谁在用、用了多少、产生了什么价值,都应该有迹可循。

于是新版本中加入了两件事:成本预估与成本归因。创建任务时,系统根据输入长度、输出上限、模型单价预估本次调用成本;任务完成后,实际账单回传,标注在哪条工作流的哪一步消耗了多少钱。客户第一次看到这个功能时反响很大,他们反馈说“终于知道每月账单是怎么跑出来的了”。

这一步也让产品的付费模式从按席位收费平滑过渡到按调用量计费。席位费适合工具,调用量计费才适合基础设施。这也是我认为本产品重构中最关键的一次商业模式验证。

5. 重构过程踩过的坑:连续三次事故逼出来的工程纪律

5.1 迁移存量数据时低估了状态机的复杂度

AIStarter旧数据里有大量历史任务,其中有相当一部分处于pending、running、failed这类中间态。迁移时如果简单地把原始字段搬过去,新系统里的状态机根本解释不了这些旧状态。

比如旧版本里failed数据没有区分“用户取消失败”和“执行超时失败”,这两种状态在后来的重试和计费逻辑里处理完全不同。我们第一版迁移脚本直接平移数据,上线后统计报表里凭空出现了几千条无法归类状态的脏数据。从技术上说,这不是一个SQL能解决的,必须针对历史状态写专门的映射规则:区分死信、可重试、已补单。

这个教训让我明白,迁移的难度不是数据量大,而是历史数据的语义不能被新系统理解。建议任何团队做状态机阵型调整时,把“状态迁移映射表”作为评审必交材料。

5.2 双写双读方案没有预留好回滚开关

当时为了做到平滑过渡,核心表采用双写方案:新老系统并行写入,读取时优先读新库,失败时回退读老库。理论上这是稳妥的,但实际执行中忽略了“开关”本身也是要部署的东西。

有一个晚上上线时发现新库的排序结果和旧库有差异,业务上不能接受,我们决定回滚到旧库读。没想到回滚开关配置在配置中心里,而配置中心的客户端缓存了旧值十分钟才生效。这十分钟内,一部分请求读新库,一部分读旧库,报表对不上,运营部门直接投诉。

事后我们把所有发布开关从配置中心拉出来,独立成一个本地文件兜底,并且规定任何双写方案上线必须附回滚演练记录。说得难听点:没有回滚预案的双写就是给自己埋雷。

5.3 拆服务拆出来的分布式事务问题

把单体里的计费模块拆成独立服务后,计费逻辑需要同时更新用量表、余额表和账单明细表。单体时代这是本地事务,非常容易保证一致性;拆出来后变成了跨服务操作,立刻面临分布式事务难题。我们试过两阶段提交,太重;试过本地消息表,逻辑繁琐;最终用了Saga模式,通过事件补偿来维持最终一致性。

这期间发生过一次比较严重的扣费漏记:用量表的调用计数已经增加,但账单表生成的记录失败,因为没有完整的补偿逻辑,用户发现自己调用次数增加了但并没有生成账单,引发了对账差异。后来我们把账单生成与用量扣减改成同一条事件消息触发,并且都要求幂等键。每个调用请求带一个全局唯一的request_id,消费端去重,才算真正止血。

现在我倾向于直接给结论:跨服务数据一致性,优先用本地消息表+Saga组合,少碰分布式锁。不要试图让多个服务共享一个数据库,矛盾爆发时会让你怀疑人生。

6. 重构后沉淀下来的一套可复用的演进路径

6.1 从API网关起步的分阶段推进顺序

如果重新来一遍,我会把演进节奏压缩得更清晰。不是推翻重来,而是沿着一条明确的路径做增量重构:

  • 第一阶段(1-2周):统一请求入口,做好鉴权和审计
  • 第二阶段(1个月):把任务调度从队列换成事件流,补齐状态机
  • 第三阶段(1-2个月):抽取模型适配器,收敛所有模型调用
  • 第四阶段(连续推进):构建控制面板给调度层提供可观测性
  • 第五阶段(业务驱动):做多租户、配额、计费闭环

这个顺序是严格按照“先有入口、再做流动、后做抽象、再做可视化、最后做商业化”的逻辑排下来的。反过来做就会遇到这种情况:控制面板漂亮地展示着数据,但入口不统一导致数据不准,空有界面没有用。

6.2 架构演进中容易忽视的非技术问题

技术上想清楚之后,产品重构真正难啃的是非技术问题。我举三个真实发生过的情况:

第一个是团队沟通成本。后端拆了三套服务,前端还能像以前一样只连一个API吗?不行了。前端团队需要同时对接多个服务的API,错误码、日志追踪、联调环境都变了。没有在统一网关上做聚合,前端就要自己拼数据,效率奇低。

第二个是文档滞后。重构过程中API不断调整,文档更新永远赶不上代码变更。我们试过要求开发随手改文档,做不到。后来强制要求每个接口必须带OpenAPI标注,由CI检查是否与实现一致,不一致就构建失败。这个办法蠢但非常有效。

第三个是客户感知管理。灰度发布时,有一批老用户看到了新界面但不知道菜单去哪了,在线反馈刷爆了客服群。后来每次前端改版,提前两周就要公布菜单迁移对照表,并把旧入口保留一个完整版本周期。

6.3 适合直接抄走的复盘checklist

结尾分享一份我每次做产品架构复盘都会过的检查清单,没有一个花哨的框架,都是我拿事故换来的:

  • 入口层是否收敛?所有请求能否追溯到一条完整链路?
  • 状态机是否完整?失败是否有重试策略?重试是否幂等?
  • 抽象层是否隔离?换一个模型供应商需要改几个文件?
  • 数据一致性如何处理?每一条业务规则是否都有补偿路径?
  • 配置是否可动态变更?是否具备快速回滚手段?
  • 观测是否闭环?用户操作行为与系统实际状态是否可对齐?
  • 团队是否知道新架构的边界?出现问题时能否在5分钟内定位到所属服务?

把这个清单过一遍,比讨论一百遍“我们的架构是否优雅”实用得多。AIStarter和PanelAI从不优雅,但它们现在跑得稳、查得清、改得动,这对工具类产品来说就是最好的状态。

RELATED READING

延伸阅读

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