Runtime 拥有什么,不拥有什么
Runtime 接收任务、创建 Run、注入模型/Tool/Policy/State 依赖,并把执行过程转换为事件和最终结果。它拥有控制面的通用语义:身份、预算、取消、步骤推进、状态提交、观测和恢复入口;业务领域仍拥有退款资格、订单状态、不变量和最终验收。
把业务规则藏在框架 Hook 中,会让规则无法复用、测试或审计;把会话对象放在进程内 Map 中,会在扩容、发布和崩溃时丢失。框架提供生命周期,但系统必须显式决定哪些状态持久、哪些动作允许重放、哪些失败只能人工处置。
从请求进入到 Run 终止
接入与准入
Transport Adapter 先认证身份并解析租户,再做输入 Schema、大小、速率、并发和风险门禁。幂等键绑定“同一业务意图”,不能由服务每次随机生成。拒绝应发生在昂贵模型调用前,并返回稳定原因码,而不是让排队和超时替代容量控制。
执行上下文
Run Context 至少携带 Run/Trace/Tenant/User ID、绝对 Deadline、组合后的 AbortSignal、预算余额、策略版本和依赖引用。它是服务端可信对象,不应把用户可修改的消息历史当权限或业务状态。Node.js 当前提供 AbortSignal.timeout() 与 AbortSignal.any(),但只有下游 API 真正消费同一 Signal,取消才算贯穿调用链。
终止处理
模型输出先经过结构、领域和策略验证,再以带版本的条件更新写入终态;只有提交成功才能发送最终完成事件。随后持久化 Usage、结果引用和失败原因,关闭流并释放连接、临时文件和并发令牌。取消、失败、不完整和成功必须是互斥终态。
按责任而非框架类名分层
Transport Adapter
负责 HTTP/SSE/WebSocket 与内部命令/事件的转换、认证、断连和协议错误映射。Transport 断开只说明客户端不再消费,不能擅自把服务端 Run 标记失败;是否继续、取消或转后台执行由任务策略决定。
Runner
Runner 驱动 Loop、扣减步骤/Token/时间/费用预算,选择恢复点并维护唯一终态。它不直接实现业务 Tool,也不以“模型无 Tool Call”作为普遍完成条件;完成必须委托给任务验收器。
Tool Executor
在调用前执行 Schema、权限、审批和幂等裁决;调用中传播 Deadline/取消并限制并发;调用后验证结果、过滤敏感字段并归一化错误。模型只能提出 Tool Call,Executor 才拥有副作用入口。
State、Policy、Telemetry
State Store 保存可恢复事实;Policy 决定动作是否允许及是否需审批;Telemetry 记录 Span、事件、指标和诊断关联。三者应可替换但语义不能相互吞并:Trace 不是状态库,Prompt 不是 Policy,数据库行也不自动成为审计事件。
以下是省略模型 Adapter、持久化实现和错误映射的 TypeScript 骨架,用于展示责任连接,不应直接当完整生产服务:
type RunCommand = { runId: string; tenantId: string; input: unknown; deadlineAt: number }
async function executeRun(command: RunCommand, deps: RuntimeDeps, requestSignal: AbortSignal) {
await deps.admission.assertAllowed(command)
const deadlineSignal = AbortSignal.timeout(Math.max(0, command.deadlineAt - Date.now()))
const signal = AbortSignal.any([requestSignal, deadlineSignal, deps.shutdownSignal])
const lease = await deps.capacity.acquire(command.tenantId, { signal })
try {
const state = await deps.state.createOrLoad(command)
return await deps.runner.run(state, { ...deps, signal })
} finally {
lease.release()
}
}
并发控制必须贯穿调用链
需要同时限制入口并发、每租户 Run、每 Run Tool 扇出、Provider 并发和存储连接。只限制 HTTP 请求数无效:一个请求可能并行产生十几个 Tool Call;只限制 Tool 数也会让等待中的流耗尽 Socket 与内存。容量令牌应在明确边界获取和释放,并把排队时间计入 Deadline。
用户取消、服务 Deadline、发布关闭和策略撤销是不同原因,应分别记录。取消是协作协议而非强制回滚:已经提交的副作用不能被 abort() 撤销;晚到结果不得继续推进状态,但要保留用于对账。对不可取消调用,应标记“取消待确认”而不是谎报已停止。
配置、密钥与依赖生命周期
进程级复用无状态 SDK Client、连接池和编译后的 Schema;Run 级创建身份、预算和 Trace;Tool 调用级获取最小范围短期凭据。启动时校验必需配置和依赖版本,避免首个真实请求才发现模型名、数据库或策略包无效。
日志默认记录 ID、版本、大小、耗时和结果类别,不记录完整 Prompt、Tool 结果或 Token。OpenTelemetry 的 GenAI 语义约定也明确警告消息内容可能包含敏感数据;采集正文必须显式 Opt-in、脱敏、限长并遵循租户保留策略。
启动、健康检查与优雅关闭
Liveness 与 Readiness
Liveness 只回答进程是否需要重启;Readiness 回答是否还能接新流量。Provider 暂时失败不一定应杀进程,但队列不可写、状态库不可用或策略无法加载通常应摘除流量。依赖健康不能简单串行探测所有远端,否则健康接口本身会变成雪崩放大器。
关闭顺序是:Readiness 置否、停止接新 Run、给在途任务设置 Drain Deadline、可恢复任务保存检查点、不可恢复任务受控取消、刷新 Telemetry,最后关闭连接。滚动发布必须让旧 Run 绑定旧版本或经过显式迁移;不能由新进程拿旧状态后“尽量继续”。
生产骨架的最小证据
| 场景 | 必须观察到 | 失败信号 |
|---|---|---|
| 正常 Run | 入口、模型、Tool、状态提交共享 Run/Trace 关联 | 只有最终文本,无步骤证据 |
| 取消与 Deadline | Signal 到达下游,晚到结果被拒绝推进 | 客户端断开后仍无限执行 |
| 限流与高并发 | 有界队列、租户隔离、资源回落 | 排队无界、重试放大 |
| 优雅关闭 | 停止接流量,在途完成或形成可恢复检查点 | 发布后 Run 消失或重复副作用 |
故障推演:浏览器断开 SSE,Runtime 为什么不能立刻把 Run 标记 cancelled?
架构判断:什么时候进程内状态仍然可以接受?
资料与事实边界
以下以官方文档、标准、论文和原始工程资料为主。厂商 API、价格、限额与模型能力会变化,落地时仍需按目标版本做能力探测和回归。
- OpenAI Agents SDK:Running agents — Runner 生命周期、Loop、流式结果、状态与错误恢复的当前 SDK 事实
- Node.js:AbortController / AbortSignal — 取消组合、超时和事件语义;实际支持取决于项目 Node 版本
- OpenTelemetry:Semantic conventions — Trace/Metric/Log 命名与 GenAI 内容敏感性边界
- Kubernetes:Liveness, Readiness and Startup Probes — 健康检查三种语义及流量/重启边界