运行时和 UI 之间,背压是一等公民

📅 2026/7/19 2:52:50
运行时和 UI 之间,背压是一等公民
流式事件如果原样透传给渲染进程,一个多工具并发的 run 能把 IPC 打爆。主进程做自适应批处理:按 runId 把帧攒进 pendingStreamBatches,遇到终止事件、超过字节上限、或者攒够一小段时间就 flush。前台大约 33ms 一 flush(≈30fps,肉眼觉得实时),后台压到 150ms,还会合并可聚合的事件(比如连续的 text delta)。前台丝滑,后台不吵——一个正在后台默默跑的会话,不该抢着刷新你正看着的这个。Provider 无关,是靠能力协商,不是靠 if (provider …)renderer 不假设 worker 一定支持某个 provider,它问:canSidecarHandle(‘agent.run’)、canSidecarHandle(‘provider.anthropic’),底层是一个 capabilities/check 的 RPC。不支持就优雅降级、给个明确的错(${provider.type} requires the .NET Native Worker for execution.),而不是抛异常炸给用户。worker 侧则是个干净的模块注册表——WorkerModuleCatalog 列了 20 个模块,WorkerHostBuilder 遍历着把它们注册进 WorkerDispatcher,新增一个能力就是加一个 module,Program.cs 的 Main 干净到只有几行。顺带一个我很喜欢的细节:子 agent(Task 工具)默认路由到配置里的 fast model(subAgentProvider,取自 getFastProviderConfig()),让被委派出去的活儿用更便宜更快的模型,没配就退回主模型。这种省钱是刻在运行时里的,不是让用户在 prompt 里自己念叨。关于重试,补一句整套系统有两条独立的重试链,别搞混:原生 loop 自己对 provider 请求的重试,通过 request_retry { attempt, maxAttempts, delayMs, statusCode, reason } 事件流回 UI,用户能看到正在第几次重试;renderer 侧那些仍走 JS 的 provider,HTTP 重试在 api-proxy.ts:最多 10 次,只在 429 / ≥500 时重试,指数退避 25% 抖动,认 Retry-After 头,还顺手解析配额头推给 UI。没有经典的 circuit breaker——韧性是靠进程监管 分层重试 事件级降级这几层叠出来的,不是靠一个熔断器对象。三、那它跟一个 Agent Framework 到底差在哪?这是我最想掰扯清楚的一段,因为现在Agent 框架这词被用烂了。框架是你 import 进自己进程的一个库;运行时是你运维的一个服务。 这句是核心,剩下都是它的推论:Agent Framework(LangChain / AutoGPT / 各种 SDK) OpenCoWork 的 Runtime存在形式 一个库,活在你的进程里,跟你同生共死 一个独立 OS 进程,你的 UI 挂了它照跑优化目标 表达力、可组合性——“怎么把一个 agent 拼出来” 可运维性、隔离性、生命周期——“怎么让一堆 agent 跨越睡眠/崩溃/升级稳定跑几个月”loop 归属 in-process,进程一刷新就没 独立进程,能扛过 UI 导航、关窗、渲染进程 reload工具执行 在你的进程里直接调 默认在 worker,按能力反向路由到主进程 / 渲染进程provider 抽象 类型层面的接口抽象 传输 / 能力层面的协商 优雅降级崩溃语义 它崩 你崩 它崩,supervisor 收尸、重启、给在途 run 报丧产品性关注点 大多留给你自己接 审批、权限策略、前后台一致、多渠道、团队、子 agent、上下文压缩,全是运行时原语再具体一点,framework 通常给你的是一个会调工具的循环;而作为一个真实产品,我们需要的是:工具执行前的人工审批握手、按会话快照下发的权限策略、cron 和交互同源的执行路径、上下文压缩作为运行时内建的一等能力(而不是你在 userland 里塞个 callback)、跨窗口的会话同步……这些东西你在框架里都得自己缝,而且缝得对不对,要等线上崩了才知道。一句话:框架帮你把 agent 想出来,运行时帮你把 agent 养活。四、如果你也在造 Agent 产品,我觉得可以抄的几点不吹了,给点能带走的:把进程边界画在运行时上。 让 loop 住在一个能比 UI 活得久、能被单独监管的地方。这一条几乎决定了你的可靠性天花板。把 agent loop 当长跑服务运维。 心跳、退避、崩溃日志、睡眠唤醒、崩溃时给在途 run 合成终止事件——你现在偷的每一个懒,都是第二个月线上要还的债。这部分代码不酷,但它才是护城河。用反向请求解耦能力,别把工具在每一层各抄一遍。 让运行时够到宿主的能力,而不是把宿主能力塞进运行时。审批、UI 工具、OS 操作,都能走同一条反向通道。会话别绑在窗口上。 把 run 锚在一个进程里、用 runId 寻址,UI 只是可拆卸的视图。切走 换水槽,不是停。一套事件词汇,喂所有入口。 交互、定时、插件、团队,别给每个界面 fork 一份 loop。一份协议,N 个消费者。运行时到 UI 之间的背压,别当事后优化。 前台 30fps、后台压到 150ms,源头节流 渲染端缓冲,一开始就该设计进去。能力协商 provider 分支。 让调用方问而不是猜,不支持就优雅降级。