从 0 构建 AI Workload Platform(四):从单机工作流内核到可靠控制面 从 0 构建 AI Workload Platform四从单机工作流内核到可靠控制面摘要我正在从零开发一个新项目AI Workload Platform。它面向 Agent能够围绕目标调用模型、工具或外部服务的软件组件与确定性程序任务负责管理多步骤任务的依赖、状态、重试、取消和恢复。模块 1 已经证明工作流内核可以在单个进程中运行但“内核能运行”不等于“其他程序能稳定使用”。模块 2 为它增加 HTTP 控制面通过网络接收请求、保存状态并协调运行的服务部分、不可变工作流版本、PostgreSQL 持久化、幂等请求、身份权限、Go SDKSoftware Development Kit软件开发工具包和启动恢复协调。本文从零解释这些技术为什么需要一起出现重点讨论 HTTP、REST、OpenAPI、数据库事务、幂等、revision、迁移、Advisory Lock、Bearer Token、Docker Compose以及 PostgreSQL 与 MySQL 的取舍。最后结合真实实现、自动化验证和人工故障实验说明“数据库提交成功但后台执行尚未开始”这个崩溃窗口如何恢复、数据库断开时为什么要安全退出以及当前方案仍然不能解决什么。目录本章要解决的问题前置知识一句话理解控制面从本地命令到网络服务Workflow、版本和 RunPostgreSQL 如何保存运行事实事务、幂等与 revision控制面的配套能力迁移、认证、SDK 与 Docker一次 Run 的完整数据流技术选型与替代方案常见错误和故障窗口实际验证与发现的问题进一步思考总结与限制官方参考资料1. 本章要解决的问题模块 1 的本地命令可以读取一份 JSON 工作流定义执行任务再把完整状态保存到文件。它适合验证调度规则却有几个明显限制只有登录到运行机器的人才能使用命令每次运行都直接读取文件没有统一的定义版本多个客户端同时写文件时很难保证一致性很难按状态、时间或任务分页查询进程退出后没有服务负责自动扫描和恢复未完成的 Run不能给“只读查询”和“创建、取消”设置不同权限。一种直觉做法是给本地命令外面套一层 HTTP。问题在于网络会引入超时、重试和重复请求多客户端会引入并发写入服务重启会引入数据库状态与内存调度之间的窗口。如果只增加路由而不处理这些问题系统反而更容易产生重复 Run、状态覆盖和静默丢失。模块 2 要解决的核心问题是怎样把一套单机工作流内核变成可被其他程序稳定调用、状态可查询、进程崩溃后可恢复的单实例服务。2. 前置知识阅读本文只需要先理解五个基础概念客户端Client主动发起请求的程序例如命令行工具或另一个后端服务服务端Server监听网络请求、执行业务规则并返回响应的程序数据库Database把结构化数据持久保存并提供查询和并发控制的软件进程Process程序一次正在运行的实例退出后内存数据会消失JSONJavaScript Object Notation一种用文本表达对象和数组的数据格式。Executor执行器是接收任务尝试、解释 Action 并返回结果的组件。Action 是工作流定义中交给执行器解释的动作标识。本文中的控制面仍然只启动一个服务实例任务也仍由 Mock Executor模拟执行器处理当前模拟执行器不会把 Action 当作本机命令运行也不会调用真实模型。后文还会反复使用以下项目术语Workflow工作流描述任务、依赖、超时和重试规则的定义Task任务Workflow 中一个可调度的工作单元TaskKey任务键任务在一份 Workflow 中稳定且唯一的字符串标识Workflow Version工作流版本一份创建后不再原地修改的 Workflow 定义Run运行实例某个 Workflow Version 的一次实际运行每个 Run 都有唯一的RunID运行标识TaskRun任务运行状态一个 Task 在某次 Run 中的状态记录Attempt执行尝试TaskRun 的一次具体执行失败后重试会产生新的 AttemptEngine工作流引擎根据依赖和状态规则推进 Run并把每次状态变化交给存储层提交的核心组件Worker工作进程从平台领取任务并调用执行器完成工作的进程。模块 2 尚未实现独立 Worker任务仍在控制面进程中执行。3. 一句话理解控制面控制面Control Plane是接收用户操作、校验规则并管理系统状态的服务入口。在本项目中它负责创建工作流版本、启动和取消 Run、查询任务与事件并协调工作流内核和 PostgreSQL。它不负责真正完成清洗文档、调用模型等业务动作这些动作属于后续执行器或 Worker。因此“控制面”描述的是职责边界不是某种特定框架它决定什么可以创建、哪一份状态是事实、谁有权限操作以及运行由谁协调执行器只接收已经确定的任务尝试并返回结果。4. 从本地命令到网络服务4.1 HTTP、REST 和 OpenAPI 分别是什么APIApplication Programming Interface应用程序编程接口是程序之间约定好的调用入口和数据格式。控制面 API 让命令行工具、SDK 或其他服务通过网络创建和查询 Workflow、Run 等资源。HTTPHypertext Transfer Protocol超文本传输协议规定客户端怎样发送请求、服务端怎样返回状态码、Header 和 Body。Body 是请求或响应承载的数据Header 是认证、内容类型等元数据。RESTRepresentational State Transfer表述性状态转移是一种组织网络接口的架构风格。它把 Workflow、Run 等对象视为资源通过 HTTP 方法表达操作POST /api/v1/workflows 创建 Workflow v1 POST /api/v1/workflows/{id}/versions 创建不可变新版本 POST /api/v1/workflows/{id}/versions/1/runs 启动指定版本的 Run GET /api/v1/runs/{run-id} 查询 Run POST /api/v1/runs/{run-id}/cancel 请求取消这里的/api/v1是接口版本前缀。它让将来出现不兼容修改时可以保留旧契约而不是让所有客户端在同一天被迫升级。OpenAPIOpenAPI Specification开放 API 规范是用机器可读文档描述 HTTP 路径、参数、认证和响应的标准。Schema 在这里是对 JSON 对象字段、类型、必填项和取值范围的结构定义。模块 2 使用 OpenAPI 3.1并用结构化测试检查 15 个实际操作、成功响应 Schema、公共错误响应和本地引用。它不是服务实现也不会自动保证代码正确它的作用是形成可检查的接口契约并为文档或未来生成多语言客户端提供输入。服务中的HTTP HandlerHTTP 请求处理器接收一个具体请求完成路由、认证和参数解析再调用应用服务。OpenAPI 描述 Handler 对外应遵守的契约行为测试验证 Handler 实际返回的内容两者需要同时存在。4.2 为什么当前使用 HTTP/RESTHTTP 调试成本低浏览器、curl 和各语言标准库都能调用适合先建立公开控制面。当前没有选择gRPC它基于 Protocol Buffers 提供强类型和高效二进制通信。Protocol Buffers 是一种描述消息结构并把数据序列化为二进制格式的机制。gRPC 更适合内部服务间调用但浏览器和人工调试需要额外工具GraphQL它允许客户端组合查询字段适合复杂前端数据需求但本模块的资源和操作边界较固定引入查询语言会增加不必要复杂度。如果后续 Worker 需要高频双向流式通信可以单独评估 gRPC这不要求把当前外部 REST API 推翻。5. Workflow、版本和 Run5.1 为什么定义必须版本化Workflow 是逻辑工作流例如document-pipelineWorkflow Version 是它的一份不可变定义。假设 v1 的第二步是“清洗文档”v2 把它改成“清洗并脱敏”。已经用 v1 启动的 Run 必须继续指向 v1否则查询历史时会出现“状态记录说执行了旧步骤定义页面却显示新步骤”的矛盾。因此模块 2 采用Workflow: document-pipeline v1: 创建后不可修改 v2: 创建后不可修改 Run A - 永久引用 v1 Run B - 永久引用 v2“不可变”不是说数据库不能删除任何数据而是说同一个版本号对应的定义内容不能被原地改写。需要修改时创建下一个版本。5.2 为什么创建时和运行时都要编译创建版本时控制面调用模块 1 的编译器检查 TaskKey、依赖、环、超时和重试参数并保存经过默认值归一化的定义。归一化是把省略值转换成明确值例如把未填写的最大尝试次数补为1。运行时需要取得这份确定版本对应的编译结果因为调度器需要 TaskKey 到数组下标的映射、依赖表和下游表。进程内缓存可以复用不可变编译结果缓存丢失时服务从数据库读取定义并重新编译。创建时校验解决“非法定义不能入库”运行时编译解决“内存调度需要高效结构”。二者目的不同不是无意义地重复工作。5.3 为什么 Run 异步启动HTTP 启动请求只等待初始 Run 持久化成功然后返回202 Accepted和 RunID工作流在后台执行。202 Accepted表示服务已经接受请求但工作尚未完成。如果 HTTP 一直等到整个工作流结束长任务会占用连接客户端超时后也无法判断服务到底有没有创建 Run。6. PostgreSQL 如何保存运行事实6.1 从完整 JSON 文件到关系表模块 1 每个 Run 保存一个完整 JSON 快照。模块 2 改用多张关系表workflows workflow_versions workflow_runs task_runs attempts state_events idempotency_records关系表Relational Table使用行和列保存数据并通过主键、外键和约束表达关系。例如attempts必须引用一个已存在的task_runs这能让数据库拒绝孤立的 Attempt。完整 JSON 写入直观但每次任务变化都要重写整个 Run分页查询也需要先加载全部内容。关系表允许只更新受影响的 Run、TaskRun、Attempt 和事件并直接按索引查询。数据库仍保存完整的不可变 Workflow Definition JSON因为定义需要整体读取和重新编译运行状态则拆为关系行。这是“整体定义 增量运行事实”的组合而不是在文件和数据库各保存一份同样的 Run。6.2 索引是什么数据库索引Database Index是数据库根据一个或多个字段额外维护的有序查找结构。查询条件与索引顺序匹配时数据库可以从对应位置读取目标行不必扫描整张表。模块 2 为恢复状态、Workflow 版本、等待重试任务和事件时间等实际查询建立索引。索引会占用磁盘并让写入多一步维护成本因此不是越多越好。当前索引围绕已经存在的查询建立没有查询证据的字段不提前添加。7. 事务、幂等与 revision7.1 什么是事务事务Transaction是一组要么全部成功、要么全部回滚的数据库操作。回滚表示撤销本次事务已经产生但尚未提交的修改。创建 Run 不是只插入一行还要写全部初始 TaskRun、状态事件和幂等记录。如果写到一半数据库报错而前半部分已经永久生效就会留下无法执行的残缺 Run。正确边界是BEGIN 插入 workflow_runs 插入全部 task_runs 插入初始 state_events 插入 idempotency_records COMMIT任一步失败就ROLLBACK。只有COMMIT成功应用层才把 Run 放入后台执行队列。事务不能让外部模型调用和数据库写入自动变成一个原子操作。执行器成功、状态提交前进程崩溃时外部动作仍可能在恢复后重复执行这就是至少执行一次语义仍然需要幂等副作用的原因。7.2 什么是幂等幂等Idempotency表示同一个操作重复执行一次或多次最终业务效果与执行一次相同。网络客户端可能没有收到响应于是重试创建 Run。服务端要求写请求携带Idempotency-KeyIdempotency-Key: demo-run-20260820-001服务端在同一事务中保存 Key、请求哈希和资源 ID相同 Key、相同请求返回第一次创建的资源相同 Key、不同请求返回冲突不同 Key视为新的业务操作。请求哈希是根据规范化请求计算的固定长度摘要。它用来比较两次请求是否表达同一件事不用来保存密码也不能替代身份认证。7.3 revision 和乐观并发控制revision是 Run 每次成功提交状态后递增的版本号。两个执行路径可能同时从 revision 5 读取状态各自计算新状态。数据库更新使用SQLStructured Query Language结构化查询语言是关系数据库用于查询和修改数据的语言。下面这条 SQL 只有在数据库中的 revision 仍为 5 时才会更新 RunUPDATEworkflow_runsSETrevision6,status$new_statusWHERErun_id$run_idANDrevision5;第一个事务更新一行并成功第二个事务发现 revision 已不是 5更新零行并报告冲突。这样旧状态不能静默覆盖新状态。这种方法叫乐观并发控制Optimistic Concurrency Control先假设冲突不是常态提交时再检查版本。它没有让冲突消失而是把“悄悄写错”变成“明确失败并重新读取事实”。7.4 为什么列表使用键集游标列表不能一次返回全部数据因此接口会限制每页数量并在还有下一页时返回next_cursor。键集分页Keyset Pagination是根据上一页最后一条记录的稳定排序键继续查询。例如 Run 按(created_at, run_id)排序下一页只读取排序键更大的记录。这与**偏移分页Offset Pagination**不同。OFFSET 1000表示跳过当前结果中的前 1000 行页间发生插入或删除时行的位置会移动可能造成重复或遗漏。键集分页不依赖行的位置数据库也可以从索引中的已知位置继续扫描。模块 2 的游标是 URL 安全的 Base64 编码版本化 JSON。URLUniform Resource Locator统一资源定位符是网络资源地址“URL 安全”表示编码结果避开在地址中有特殊含义的字符。Base64是把二进制数据转换为文本字符的编码方式不是加密客户端仍可能解码其中内容。不透明游标表示客户端不应依赖其内部格式或修改内容只需原样回传。服务端仍把游标当作不可信输入限制解码大小并验证资源类型、WorkflowID、RunID 和过滤条件。Run 列表支持workflow_id与status组合过滤游标会绑定这组条件更换条件后继续使用旧游标会返回 400。键集分页避免的是页间行位移问题不会把多次 HTTP 请求变成同一个数据库快照。新数据如果排序在已经翻过的位置当前翻页过程不会回头返回它。8. 控制面的配套能力迁移、认证、SDK 与 Docker前面的事务、幂等和 revision 定义了控制面的核心一致性规则。本节介绍四类配套能力迁移让数据库结构可升级认证限制谁能调用接口SDK 和 CLI 提供稳定调用入口Docker Compose 使开发环境中的 PostgreSQL 版本和配置可重复。它们很重要但不会替代第 7 节的事务与并发控制规则。8.1 数据库迁移数据库迁移Database Migration是随代码版本管理、按顺序修改数据库结构的 SQL 文件。模块 2 使用显式migrate up命令创建表和约束服务启动时只检查迁移版本不擅自改表。显式迁移让部署者知道何时发生结构变化也避免多个服务实例在启动时同时修改生产数据库。代价是部署多一步操作换电脑或创建新数据库时不能忘记执行迁移。8.2 Advisory LockAdvisory Lock咨询锁是 PostgreSQL 提供的应用级协调锁。数据库不会自动把它绑定到某张业务表应用用一个固定整数表示“控制面执行协调者”身份。模块 2 在专用数据库连接上持有该锁。同一数据库的第二个服务实例拿不到锁就启动失败连接断开时锁自动释放。它只证明“当前只有一个协调者”不是高可用方案也不是未来多 Worker 的任务租约。单实例进程故障时仍有短暂停机。运行监督器Coordinator是控制面进程中统一管理新 Run、恢复 Run 和锁健康的组件。它会周期性检查专用连接和锁所有权并观察每次 Engine 执行返回的系统错误。一旦无法证明自己仍持有锁就采用fail-stop故障即停止先把ready设为 false中断本机执行停止 HTTP 服务再让进程以错误退出。ContextGo 上下文是 Go 在调用链中传递取消、截止时间和请求范围信号的标准机制。上层 Context 取消造成的基础设施中断不会写成业务canceled只有取消 API、显式Engine.Cancel或执行器主动取消才会进入取消状态。原进程不在连接恢复后自动重新加锁。原因是断连期间可能已有新进程取得锁旧进程继续运行会产生两个协调者。更强的方案需要fencing栅栏机制为每次所有权分配单调递增的令牌存储和下游系统拒绝旧令牌的迟到写入。模块 2 还没有这种分布式所有权协议因此选择退出并由新进程恢复更可靠。8.3 Bearer TokenBearer Token持有者令牌是放在 HTTPAuthorizationHeader 中的凭据Authorization: Bearer token谁持有有效 Token谁就拥有对应权限因此 Token 必须像密码一样保护。模块 2 配置一个 viewer Token 和一个 operator Tokenviewer 只能查询operator 可以创建和取消。这是本地单实例阶段的最小认证不支持用户账号、Token 轮换和细粒度资源授权。服务日志只记录请求 ID、路径、状态和错误码不记录 Authorization Header、Token 或请求体。8.4 SDK 与 CLIGo SDK 封装 HTTP、Bearer Token、JSON、错误和分页。CLICommand-Line Interface命令行界面复用 SDK不再自己拼接 HTTP 请求。SDK 不默认重试写请求因为库无法替调用方决定一次失败是否应该重试。调用方确实重试时必须复用原来的幂等 Key。SDK 的公开返回类型定义在公开包中不能泄露 Go 的internal类型否则仓库外的程序无法正常声明这些类型这会让“公开 SDK”只在项目内部好用。8.5 Docker ComposeDocker是构建、分发和运行容器化应用的平台。容器Container是受到一定隔离的进程及其运行环境它通常比虚拟机轻量但仍然共享所在 Linux 系统的内核不能被当作绝对安全边界。在 macOS 上Docker Desktop 同时提供 Docker CLI、Docker Engine、Linux 虚拟机和 Docker Compose。macOS 本身不是 Linux而容器依赖 Linux 内核所以 Docker Desktop 会在后台运行一个轻量 Linux 虚拟机。Apple Silicon Mac 上docker version显示客户端为darwin/arm64、服务端为linux/arm64是正常的客户端与服务端架构信息不是项目代码的架构冲突。理解本项目的 Compose 配置先分清四个对象对象含义当前项目中的实例镜像Image只读的运行模板包含程序和基础文件postgres:16.10-bookworm容器Container根据镜像创建的运行实例可以启动、停止或重新创建Compose 服务postgres对应的 PostgreSQL 容器数据卷Volume由 Docker 管理、生命周期独立于普通容器的数据目录workload-postgres-data保存数据库文件端口映射Port Mapping把宿主机端口转发到容器端口本机127.0.0.1:5432转发到容器的5432镜像本身只读一个镜像可以创建多个彼此独立的容器。容器被删除不会自动删除镜像数据卷也有独立生命周期所以删除并重建 PostgreSQL 容器后只要保留原数据卷数据库文件仍可继续使用。安装 Docker、查看 Dashboard 和清理数据卷的操作步骤见项目本地运行、演示与换电脑手册本文只保留技术边界。Docker Compose是用一份 YAML 配置文件声明相关容器、端口、环境变量、健康检查和数据卷的工具。YAML 是一种用缩进表达层级结构的文本数据格式。当前仓库的compose.yaml声明了一个 PostgreSQL 服务postgres:16.10-bookworm 镜像 - postgres 容器 - 监听容器内 5432 端口 - 映射到本机 127.0.0.1:5432 - 数据写入 workload-postgres-data 数据卷端口绑定使用127.0.0.1表示默认只允许当前电脑访问不把开发数据库直接暴露给局域网。健康检查会在容器启动后调用 PostgreSQL 的pg_isreadyDashboard 或docker compose ps显示healthy才表示数据库已经可以接受连接。模块 2 的 Compose 只启动 PostgreSQL 16Go 控制面本机进程 PostgreSQLDocker 容器 工作流任务安全的 Mock Executor它解决的是“换电脑后如何得到一致的开发数据库”不表示工作流任务已经容器化更不表示可以安全执行任意代码。真实任务容器和资源限制属于后续模块。8.5.1 为什么当前选择 Compose也可以把 PostgreSQL 直接安装到 macOS或者用 Kubernetes 启动。Kubernetes 是用于部署、调度和管理容器化应用的集群编排系统。当前选择 Compose是因为它把数据库版本、端口、初始账号、健康检查和数据卷写进仓库换电脑后可以用一条命令得到接近一致的开发依赖又不会过早引入 Kubernetes 的集群概念和排查层级。Compose 的代价是必须安装 Docker Desktop并理解容器和数据卷的生命周期。它也不负责数据库迁移、应用 Token 或 Go 服务启动这些仍由项目自己的配置和命令管理。Docker 账号不是本地运行的前置条件当前使用公开 PostgreSQL 镜像未登录也能启动。登录只能在匿名拉取受限时改善配额不能解决网络连接重置、DNS 或超时问题。9. 一次 Run 的完整数据流下面是从客户端请求到任务完成的关键顺序CLI / SDK - HTTP POST Bearer Token Idempotency-Key - 控制面认证、解析严格 JSON - 加载指定不可变 Workflow Version - 取得或重建编译缓存 - 创建 pending Run 候选状态 - PostgreSQL 事务写 Run、TaskRun、事件和幂等记录 - COMMIT 成功 - Coordinator 接收 RunID 并监督后台执行 - HTTP 返回 202 和 RunID - 后台 Engine 执行 - 每次状态变化带 expected revision 增量提交 - 客户端分页查询 Run、Task、Attempt 和事件这里最重要的顺序是“先提交后入队”。如果反过来执行器可能已经产生外部副作用数据库里却没有这个 Run。10. 技术选型与替代方案10.1 为什么选择 PostgreSQL而不是 MySQLPostgreSQL 和 MySQL 8 都支持事务、索引、JSON 和行锁都能实现本模块并不是 MySQL 性能或能力不够。当前选择 PostgreSQL 的原因是jsonb、部分索引和复杂状态查询组合较自然jsonb是 PostgreSQL 支持查询和建立索引的二进制 JSON 存储类型Advisory Lock 可以直接用于单实例协调实验后续可用RETURNING、SKIP LOCKED等能力验证并发领取RETURNING可以直接返回写入后的行SKIP LOCKED可以在并发领取时跳过已被其他事务锁定的行项目只维护一套 SQL避免第一阶段同时承担两种数据库方言和测试矩阵。没有优先选 MySQL 的原因是当前没有既有 MySQL 运维环境、兼容客户或团队标准要求。若真实使用方以 MySQL 为统一基础设施或者 PostgreSQL 成为部署阻力就应基于 Store 边界评估 MySQL 适配而不是坚持技术偏好。10.2 为什么不继续只用 FileStoreFileStore是模块 1 中把每个 Run 的完整 JSON 快照保存为独立文件的存储实现。它依赖少、便于理解和恢复演示但不适合多个客户端并发写、关系查询和分页。它仍保留为本地模式不承担网络控制面的主存储。10.3 为什么不先加 Redis 或消息队列Redis是常用于缓存和内存数据结构的独立服务消息队列Message Queue用于在生产者与消费者之间暂存并传递消息。它们能解决缓存、通知或大规模任务分发问题但不能替代本模块需要的关系事务和历史查询。当前先用 PostgreSQL 提交事实再由进程内后台执行当多 Worker 实验出现明确的领取吞吐或通知瓶颈时再评估队列。11. 常见错误和故障窗口11.1 数据库提交成功后台执行前崩溃这是模块 2 最重要的故障窗口事务 COMMIT 成功 - 进程崩溃 - 尚未调用后台 ExecuteRun 已经是数据库事实不能因为内存队列丢失而永远停留。服务下次启动时先取得 Advisory Lock再扫描并加载pending、running、waiting_retry等非终态 Run把它们交给受监督的后台恢复然后进入 ready。这里等待的是“恢复所有权已经交接”不是等待所有 Run 执行到终态否则一个长任务或很久以后才能重试的任务会阻塞整个 HTTP 服务启动。11.2 保存原始定义与编译定义不一致编译器会补默认重试次数。如果数据库保存用户原始 JSON而内存缓存保存补过默认值的定义同一个版本就出现两种内容创建 Run 时的定义一致性检查会失败。正确做法是保存编译结果返回的规范化定义并基于同一份内容计算哈希。这不是修改用户业务含义而是让省略的默认值变成持久化事实。11.3 把所有应用错误都映射成 500JSON 语法正确不代表业务输入有效。重复 TaskKey、缺失依赖、非法分页游标以及 URL WorkflowID 与定义 ID 不一致都应该返回 400数据库连接失败等服务端问题才返回 500。应用层需要稳定的错误类别HTTP 层根据类别映射状态码不能靠匹配错误字符串。11.4 测试共用数据库Repository仓储层是封装数据库读写、向应用层提供持久化接口的组件。E2EEnd-to-End端到端测试会从 HTTP 等外部入口开始经过应用服务和数据库验证完整链路。如果 Repository 测试会删除公共表而 E2E 测试同时使用同一个数据库Go 跨包并行测试可能互相破坏。即使连续运行几次没失败也只是时序碰巧没有重叠。模块 2 的集成测试为每个测试创建独立临时数据库。仅使用不同 schema数据库中的命名空间仍不能隔离数据库级 Advisory Lock因此这里选择独立数据库并在测试结束后清理。11.5 数据库断开后在原进程继续运行数据库连接断开会自动释放 Advisory Lock。即使网络随后恢复旧进程也不能假设自己仍是唯一协调者因为这段时间可能有新进程取得锁。模块 2 因此不在原进程重连并继续执行而是让运行监督器报告首个致命错误统一中断 Engine关闭 HTTP再由新进程从 PostgreSQL 已提交状态恢复。这个顺序还要求区分“执行中断”和“业务取消”。进程退出只是停止本机工作不代表用户要求取消 Run。旧进程不能在数据库故障时把 Run 写成canceled新进程恢复时会把遗留的runningAttempt 记为interrupted再根据剩余尝试次数继续或失败。12. 实际验证与发现的问题模块 2 使用真实 PostgreSQL 16、HTTP Handler、工作流 Engine 和 Mock Executor 完成了以下验证验证主题已验证行为事务中途写入失败时 Run 和幂等记录一起回滚幂等相同 Key 和请求返回同一资源不同请求返回冲突并发相同幂等 Key 的并发创建最终只有一个 Run版本创建 v2 后旧 Run 仍引用 v1查询Run 摘要、Task、Attempt 和事件可以分页或按单任务读取取消运行中任务收到取消Run、TaskRun 和 Attempt 收敛到 canceled单实例第二个服务实例因为 Advisory Lock 被占用而拒绝启动崩溃窗口已提交但未入队的 pending Run 能被启动恢复扫描并执行成功运行监督Execute、Resume 或锁检查的首个系统错误会令服务不可就绪并中断其他执行中断语义父 Context 中断不写 canceled显式取消仍持久化 canceled运行期停库PostgreSQL 中断后旧控制面安全退出新进程恢复同一 RunAttempt 1 为 interrupted、Attempt 2 为 succeeded迁移保护数据库存在当前程序未知的迁移版本时拒绝启动和继续迁移稳定分页Workflow、Version、Run 和 Task 使用键集游标Run 游标绑定过滤条件API 契约OpenAPI 3.1 结构化测试检查全部实际操作、Schema、鉴权、错误和引用权限viewer 不能执行 operator 写操作日志request_id请求标识、状态和错误码可关联凭据不进入请求日志性能1 个任务、每轮 100 次操作的本机 HTTP/PostgreSQL 基准运行 5 轮验证还暴露并修正了多类容易被普通成功路径遗漏的问题规范化定义与数据库内容不一致、公开 SDK 泄露项目内部类型、启动恢复等待 Run 终态、后台执行错误无人观察、数据库断开被误写成业务取消以及偏移游标在数据变化时不稳定。首轮性能基线中Run 串行查询五轮平均约0.281 ms/op10 逻辑 CPU 并行查询的总耗时折算约0.081 ms/op创建一个包含 1 个任务的 pending Run 平均约1.234 ms/op。测试使用本机回环 HTTP、没有启用 TLSTransport Layer Security传输层安全协议加密、编译缓存命中并关闭后台执行并行结果是吞吐口径不是单请求尾延迟。P95 和 P99 分别表示 95% 和 99% 的请求耗时不超过对应值用于观察少数慢请求。自动化验证之外项目还完成了一次真实进程故障实验先用 90 秒 Mock 延迟让任务保持running再停止 PostgreSQL。旧控制面在 Advisory Lock 健康检查收到连接终止错误后以非零状态退出数据库恢复并启动新控制面后ready检查通过同一个 Run 最终为succeeded第一次 Attempt 为interrupted第二次 Attempt 为succeeded。这验证了第 11.5 节描述的安全退出和恢复路径也验证了基础设施中断没有被记录成用户取消。这些自动化结果和一次人工故障实验能证明当前单实例控制面的主要代码路径并提供一个小规模性能回归起点但不能证明生产吞吐、高可用或多 Worker 安全性。当前没有 P95、P99 等尾延迟分位数、系统饱和点、跨机器数据库、后台执行竞争数据也没有重复停库和长时间稳定性实验因此本文不作生产性能承诺。13. 进一步思考13.1 为什么下一步先进入 Agent Runtime模块 2 已经让结构化 WorkflowDefinition 可以通过稳定接口提交、版本化保存和恢复但用户仍然必须手写每个任务的名称、动作标识、输入参数和依赖。项目定位还要求同时支持 Agent 与确定性程序任务因此“模型怎样参与工作流定义又不绕过权限、校验和人工确认”成为此时最直接的产品缺口。模块 3 先解决这个问题还有一个工程原因Agent 会扩展任务输入、模型协议和工具权限这些内容最终都要经过模块 2 的 API 与 PostgreSQL 保存。先稳定输入和授权边界再把执行拆到多个 Worker可以避免同时修改“任务表达方式”和“任务归谁执行”两组核心协议。新增 Agent Runtime管理模型、工具、权限和预算的 Agent 运行时的代价是模型输出不确定、调用有费用和网络失败因此第一版必须保留 Mock Model按预设返回模型响应的模拟实现、预算、超时、工具白名单和人工确认而不能把模型输出直接当作运行命令。这不表示多 Worker 不重要。模块 2 当时已经暴露单进程执行限制但项目选择先用模块 3 验证核心产品入口再在模块 4 单独处理执行所有权、租约和 Worker 失联使两类风险能够分别测试。13.2 后续仍需验证的问题完成模块 2 后有三个问题需要在后续实验中继续回答单实例恢复会扫描全部非终态 Run数量变大后应该怎样分页、限速并避免启动风暴多 Worker 出现后数据库事实、任务租约和消息通知怎样组合才能让旧 Worker 的迟到结果不能覆盖新租约Agent 调用模型和工具会产生不可逆副作用哪些操作可以用幂等 Key哪些必须采用结果去重或补偿流程这些问题不能靠增加几个字段提前“设计完成”需要在多 Worker、Agent Runtime 和故障注入模块中分别验证。后续证据需要按问题分开看系列第五篇已经对 Agent 的只读工具权限、草稿校验、内容哈希和人工确认给出了第一阶段答案但真实任务已经产生外部副作用时的幂等、去重和补偿仍未验证。系列第六篇已经用租约、心跳、过期回收、迟到结果拒绝和多 Worker 性能基线回答了前两个问题的第一阶段大规模恢复启动风暴、跨机器容量和生产稳定性仍未验证。14. 总结与限制从单机内核到控制面不是简单增加 HTTP 路由而是同时建立网络契约、不可变版本、事务、幂等、并发控制、认证和恢复顺序。本模块最重要的技术结论是数据库提交才是 Run 已被系统接受的事实内存队列只是推进执行的手段。先持久化、后执行再用启动扫描修复两者之间的崩溃窗口才能避免任务静默丢失。模块 2 完成时仍是单控制面实例和 Mock Executor当时没有多 Worker、真实 Agent、动态租约、完整指标、生产容量验证或高可用。已有性能数据只是本机回环环境下的回归基线不能直接推导生产容量。Bearer Token 也只适合本地和早期演示。后续模块 3 已经在这套结构化提交流程上增加 Agent Runtime、模型与工具边界、自然语言工作流草稿和用户确认流程模块 4 又把任务执行拆到独立 Worker并用租约和心跳处理执行节点失联模块 5 进一步加入日志、低基数指标、同步请求 Trace、告警、故障注入和多 Run 性能对照。后续进展不改变本文关于单实例控制面、当时故障实验和性能基线的证据边界。控制面高可用、生产容量和真实执行副作用仍然需要后续实验。15. 官方参考资料HTTP Semantics, RFC 9110The OAuth 2.0 Authorization Framework: Bearer Token Usage, RFC 6750OpenAPI SpecificationPostgreSQL: Transaction IsolationPostgreSQL: Explicit Locking and Advisory LocksPostgreSQL: JSON TypesMySQL 8.4 Reference Manual: InnoDB Transaction ModelDocker Docs: How Compose WorksDocker Docs: Docker ArchitectureDocker Docs: VolumesDocker Docs: Publishing and Exposing PortsGo Documentation: log/slog项目源码本文对应模块 2。完整源码、数据库迁移、验证报告和后续模块见 AI Workload Platform GitHub 仓库。