这是《OpenCode v2 实战:从零搭建企业级 AI 编程助手》专题的第 23 篇。前一篇把模型目录、Variants、Aliases、本地模型和兼容网关串成了模型工程闭环,本篇继续进入运行时:当开发者暂停几分钟再回来时,怎样用 Warming 尝试保留 Provider 侧 Prompt Cache 或其他短期状态,又怎样避免把"优化"变成不可见的 Token 消耗与限流压力。
长会话的第一次请求已经支付了上下文处理成本,开发者却经常在读代码、改实现、等待测试时停顿几分钟。下一次提问若失去 Provider 侧缓存,延迟和输入成本可能重新升高。OpenCode v2 的 Warming 正是为这种间歇式工作设计:在最近活跃会话空闲后,周期性发送一次模型请求,尽量延续短期状态。
但它不是本地定时器心跳,更不是免费的缓存开关。每次预热都使用当前会话的模型、Agent、Instructions 与上下文,属于真实 Provider 请求,会消耗 Token、计费、占用速率限制,也可能失败。本文以 2026 年 10 月 30 日可见的 OpenCode v2 官方文档为基线,给出从语义理解、配置、容量估算到灰度、观测与回滚的完整方法。
一、先判断 Warming 解决的是什么问题
1. 它保温的是 Provider 侧短期状态
Warming 的目标是延续 Provider 侧 Prompt Cache 或其他短期状态,而不是给 OpenCode 本地数据库"保活"。一次普通模型请求完成后,Provider 可能在有限时间内保留可复用的提示前缀;预热请求尝试在这个窗口消失前再次访问同一会话上下文。
text
普通请求完成
→ 开发者阅读与修改代码
→ 会话进入空闲
→ Warming 发起真实模型请求
→ Provider 侧短期状态可能被延续
→ 下一次普通请求可能命中缓存
这里的"可能"很重要。缓存是否存在、缓存键怎样计算、保留多久、经过网关后是否仍可命中,都由 Provider 与兼容层共同决定,OpenCode 不能单方面保证收益。
2. 它不等于本地健康检查
健康检查关注服务能否连接;Warming 关注一个具体会话的 Provider 请求路径。用 /health、TCP 探测或模型目录请求替代预热,通常不会携带相同上下文,因此无法证明会话缓存被延续。
| 机制 | 主要目标 | 是否携带会话上下文 | 是否产生模型用量 |
|---|---|---|---|
| 服务健康检查 | 判断进程或端点可用 | 否 | 通常否 |
| 模型目录查询 | 判断模型可发现 | 否 | 通常否 |
| Warming | 延续会话相关短期状态 | 是 | 是 |
3. 先定义可验证的收益
上线目标不能写成"开启缓存优化",而要变成可观察指标:普通请求的缓存命中输入量、首 Token 延迟、总延迟、输入成本、429 比例和预热请求失败率。若 Provider 不返回缓存或用量明细,至少要做同类会话的对照实验。
yaml
warming_goal:
primary: next_normal_request_latency
secondary:
- cached_input_ratio
- input_cost_per_session
guardrails:
- warming_request_volume
- rate_limit_errors
- total_provider_cost
二、读懂默认行为再开启
1. 默认是关闭的
OpenCode v2 不会自动为所有会话发送预热请求。省略 warming 字段或显式写成 false,都表示关闭:
jsonc
{
"$schema": "https://opencode.ai/config.json",
"warming": false
}
这个默认值是合理的:平台无法替团队判断 Provider 缓存收益是否大于额外请求成本,也无法预先知道租户限流和合规边界。
2. 布尔值开启默认策略
最小配置只需把 warming 设为 true:
jsonc
{
"$schema": "https://opencode.ai/config.json",
"warming": true
}
默认策略会在最近一次非预热模型请求之后空闲 4 分钟时发送预热请求,并每隔 4 分钟继续一次,直到最近一次非预热请求后的 30 分钟窗口结束。
3. 默认窗口最多七次尝试
若没有新的普通模型活动,默认时间线是:
text
00:00 普通模型请求
00:04 第 1 次预热
00:08 第 2 次预热
00:12 第 3 次预热
00:16 第 4 次预热
00:20 第 5 次预热
00:24 第 6 次预热
00:28 第 7 次预热
00:30 活跃窗口结束
"最多"而不是"固定"七次,因为请求本身需要时间;下一段空闲间隔从上一次预热完成后才开始,实际尝试数可能更少。
三、把双计时器模型想清楚
1. 间隔与窗口不是同一个计时器
interval 控制下一次预热前需要空闲多久;duration 控制从最近一次非预热请求开始,整个预热资格能维持多久。两者分别回答"多久发一次"和"总共允许发多久"。
text
normal request
├─ interval timer: 到点后尝试一次 warming
└─ duration window: 到点后停止所有 warming
把二者混成一个 TTL,容易产生"预热请求为什么没有把 30 分钟重新续上"的误判。
2. 预热完成只重启空闲间隔
一次预热完成后,下一段 interval 才开始;但 duration 窗口不会因此延长。也就是说,预热不能靠自己永久续命。
text
00:00 普通请求,窗口截止时间锁定为 00:30
00:04 预热开始
00:05 预热完成,下一段 4 分钟间隔开始
00:09 下一次预热
00:30 无论此前成功多少次,窗口都结束
3. 普通活动会重置两个计时器
新的非预热模型请求会同时重置空闲间隔和活跃窗口:
text
00:00 普通请求
00:04 预热
00:20 新的普通请求,两个计时器重新开始
00:24 预热
00:50 新窗口结束
因此,用户正在持续交互时,预热不会与正常请求按旧时间线机械叠加。
四、对象配置怎样安全落地
1. 三个字段分别控制内容与时间
对象形式可以修改提示、间隔与窗口:
jsonc
{
"$schema": "https://opencode.ai/config.json",
"warming": {
"prompt": "Do not perform any work. Reply with exactly: OK",
"interval": "5 minutes",
"duration": "1 hour"
}
}
省略的字段保留默认值。若只需改间隔,不必复制默认 Prompt 和 Duration:
jsonc
{
"warming": {
"interval": "2 minutes"
}
}
2. 时长必须是有限正数
interval 与 duration 接受时长字符串,但解析结果必须是有限且大于零的值。无效配置不会偷偷退回某个猜测值;OpenCode 会跳过 Warming 并记录警告。
jsonc
{
"warming": {
"interval": "30 seconds",
"duration": "1 hour"
}
}
生产配置应在发布门禁中校验时长,避免把拼写错误留到运行时日志才发现。
3. 间隔不应大于等于窗口
若 interval 与 duration 一样长或更长,窗口会在首个预热到点前结束,因此一次预热也不会发送。
text
interval = 30 minutes
duration = 30 minutes
结果:00:30 窗口先结束,预热次数为 0
配置审查可以直接加入 interval < duration 这一条业务规则。
五、一次预热请求到底携带什么
1. 复用当前会话运行上下文
官方语义明确:预热请求使用会话当前的模型、Agent、Instructions 和对话上下文。它不是一个脱离会话的廉价模型 Ping。
text
[current model]
[current agent]
[current instructions]
[current conversation context]
[transient user message: keep-alive prompt]
→ one model response
这解释了为什么长会话的预热可能仍有明显输入用量,也解释了为什么不同会话的预热成本不能只按一个固定请求价估算。
2. Prompt 是瞬时用户消息
配置的 Warming Prompt 会作为临时用户消息追加,本次请求结束后不会写入持久会话历史。模型响应同样会被丢弃。
text
durable history before = durable history after
transient prompt persisted = false
model response persisted = false
因此,用户回到会话时不会看到一串 OK,也不会因为预热回复不断膨胀持久消息列表。
3. 不接受输入也不继续工具循环
预热请求不接收交互输入,不新增历史消息,不改变持久会话状态;OpenCode 也不会分发本地工具调用或继续工具循环。默认 Prompt 还会明确要求模型不要工作、不要使用工具并回复 OK。
yaml
warming_side_effect_contract:
accepts_user_input: false
writes_history: false
dispatches_local_tools: false
continues_tool_loop: false
keeps_model_response: false
六、Prompt 设计要以"无副作用"为第一目标
1. 默认 Prompt 通常更稳
自定义 Prompt 不是越复杂越好。预热的目标是触发一次最小模型请求,而不是让模型总结项目、检查代码或生成报告。复杂指令会增加输出 Token、延迟和不可预测行为。
text
推荐语义:不执行工作、不调用工具、只返回固定短词
不推荐语义:回顾上下文并给出下一步开发建议
若没有明确的 Provider 适配要求,保留默认 Prompt 往往比"优化提示词"更可控。
2. 不要在 Prompt 中加入秘密
Prompt 会发往当前模型 Provider。不要放入 Token、连接串、内部主机、真实用户标识或仅为预热构造的敏感数据。
jsonc
{
"warming": {
"prompt": "Do not perform any work. Reply with exactly: OK"
}
}
即使消息不持久化,也不意味着它没有经过网络、网关或 Provider 日志。
3. 固定短回复便于观测
固定回复可以帮助团队从 Provider 侧日志识别预热流量,并限制输出长度。但不要把响应内容当作业务正确性证据,因为 OpenCode 会丢弃它,预热成功只说明本次模型调用完成。
yaml
classification:
request_type: warming
expected_output: short_fixed_text
business_result: none
七、请求量上界怎样计算
1. 理论次数由间隔和窗口决定
忽略请求耗时时,若窗口末端不再发送请求,理论上界可近似写成:
text
N_max = ceil(duration / interval) - 1
默认值 30 / 4 得到 ceil(7.5) - 1 = 7。若窗口正好是间隔整数倍,例如 60 分钟与 5 分钟,则最多在 5、10......55 分钟发送 11 次,而不是在 60 分钟再发一次。
2. 请求耗时会降低实际次数
下一段间隔从上一次预热完成后开始,因此请求持续时间也占用窗口:
text
start(n+1) = finish(n) + interval
finish(n) = start(n) + provider_latency(n)
如果 Provider 忙、响应慢或重试策略增加耗时,实际次数通常低于纯时间除法的结果。
3. 集群容量要乘以活跃会话数
平台预算不能只看单个开发者。粗略峰值可以按最近活跃会话数计算:
text
warming_requests_per_hour
≈ eligible_sessions × attempts_per_session_window × windows_per_hour
还要考虑会话活跃时间分布、不同模型的限流池、普通请求峰值与团队时区。预热流量必须与业务流量共用一张容量表。
八、成本收益模型不能只看缓存命中
1. 每次预热都可能产生真实费用
官方文档明确提醒:Warming 请求会消耗 Token、产生费用、计入速率限制,并像普通模型请求一样失败。总成本至少包含预热输入、预热输出和对普通请求造成的限流机会成本。
text
warming_cost
= warming_input_tokens × input_price
+ warming_output_tokens × output_price
+ rate_limit_opportunity_cost
2. 收益来自后续普通请求
预热本身不产出业务结果。只有后续普通请求确实更便宜或更快时,才形成收益:
text
net_value
= saved_normal_request_cost
+ latency_value
- warming_cost
- operational_risk_cost
如果用户离开后不再回来,窗口内所有预热都只有成本没有收益。因此"返回概率"是评估中不可缺少的变量。
3. 用会话分群而不是全局平均
短会话、长上下文会话、昂贵模型会话、频繁交互会话的收益完全不同。建议至少按模型、上下文规模、空闲时长与用户返回概率分群。
| 分群 | 可能策略 | 原因 |
|---|---|---|
| 短上下文、低价模型 | 默认关闭 | 重算成本可能低于预热 |
| 长上下文、高返回率 | 小范围灰度 | 缓存收益更有机会覆盖成本 |
| 低限流租户 | 谨慎或关闭 | 预热可能挤占普通请求 |
| 无缓存指标的兼容网关 | 先验证再开启 | 无法证明真实收益 |
九、失败语义决定了怎样排障
1. 失败不会使会话失败
Warming 失败会被记录,但不会让原会话失败,也不会改变会话状态。下一次尝试仍等待正常的 interval,而原来的 duration 窗口继续倒计时。
text
00:04 预热失败,会话不变
00:08 到下一间隔后再尝试
00:30 原窗口结束,不因失败延长
2. 不要把失败重试做成紧循环
既然官方语义是在下一间隔再尝试,外部守护脚本不应看到一次失败就立即重复调用。紧循环会放大 429、网络抖动与费用问题。
yaml
retry_policy:
immediate_external_retry: false
next_attempt: configured_interval
extend_active_window: false
3. 按层区分故障
建议把日志分类为配置解析、计时资格、模型请求、认证、限流、网络和 Provider 错误。先确认"是否应该发",再判断"发了为什么失败"。
text
没有预热请求
→ warming 是否启用
→ 最近是否有非预热请求
→ interval 是否已到
→ duration 是否仍有效
→ 时长是否解析成功
有请求但失败
→ 当前模型与凭据
→ 网络 / 代理 / CA
→ Provider 限流与服务错误
十、共享后台服务会改变运行边界
1. 本地客户端默认连接共享服务
OpenCode v2 CLI 默认发现或启动当前用户的共享后台服务。这个服务拥有会话、配置、集成、权限和工具执行,本地 TUI、Mini 等客户端连接到同一个运行时。
text
TUI ─┐
Mini ├─→ shared background service
CLI ─┘ ├─ sessions
├─ config
├─ integrations
├─ permissions
└─ tool execution
因此排查 Warming 时,不能只盯着发起交互的终端窗口;要确认实际承载会话的服务实例和它加载的配置。
2. standalone 使用私有服务
--standalone 会使用一个私有服务,而 --server 连接指定的服务 URL:
bash
opencode --standalone
opencode --server http://localhost:4096
同一个仓库在共享服务与 standalone 中可能对应不同运行实例。灰度实验必须记录运行模式,否则指标可能把两套会话混在一起。
3. 禁用共享服务有功能边界
可以把 CLI 默认改为私有服务模式:
bash
opencode service set disabled true
opencode
显式 --server 仍可连接指定服务,opencode service start 仍可手工启动共享服务。要恢复自动共享服务:
bash
opencode service unset disabled
需要注意,Pairing 依赖共享服务;禁用时不可用。运行架构调整不能只验证 Warming,还要验证客户端连接方式。
十一、多客户端场景要按会话而不是窗口理解
1. 关闭一个界面不一定等于服务停止
在共享后台服务架构中,客户端窗口与会话承载服务不是同一个生命周期。仅关闭 TUI 不足以推断后台会话、计时器或配置实例已经终止。
text
client lifecycle ≠ shared service lifecycle
terminal closed ≠ session runtime necessarily gone
运维文档应明确由谁启动服务、谁负责停止、升级时怎样排空会话。
2. 普通活动重置的是对应会话
Warming 以最近活跃会话为对象。团队观测应使用会话 ID、模型和服务实例做关联,不要只按用户或进程聚合。
yaml
warming_event_dimensions:
- service_instance
- session_id_hash
- model_id
- agent_id
- interval_policy
会话标识应脱敏或哈希,日志中不要复制完整提示内容。
3. 配置变更要确认加载实例
修改配置文件后,必须确认当前会话所在服务实际读取了新值。若共享服务仍持有旧配置,重新打开一个终端也不一定改变运行时行为。
text
配置文件已修改
→ 目标服务实例已识别
→ 实际加载值已核对
→ 新普通请求建立计时基线
→ 观察首个 interval
十二、观测设计要把预热与普通请求分开
1. 最小事件模型
每次预热至少记录开始时间、完成时间、结果、模型、输入输出用量、Provider 状态码和所属策略版本。不要记录认证头或完整会话正文。
json
{
"event": "session_warming",
"policy_revision": "warming-canary-v1",
"model": "company/coding-default",
"result": "success",
"latency_ms": 820,
"input_tokens": 12000,
"output_tokens": 1
}
示例数值只是结构占位,不代表任何模型的固定消耗。
2. 关键指标分四组
yaml
metrics:
behavior:
- eligible_sessions
- warming_attempts
- attempts_per_session
quality:
- next_normal_request_latency
- cached_input_ratio
cost:
- warming_tokens
- total_cost_per_session
reliability:
- warming_failure_rate
- rate_limit_error_rate
只看缓存命中率会掩盖总费用上升;只看总费用又可能错过明显的交互延迟改善。
3. 关联下一次普通请求
预热收益发生在"下一次普通请求",因此需要建立窗口归因:一次普通请求之前最近是否有成功预热、间隔多久、是否跨模型或 Agent 变更。
text
warming success
→ elapsed until next normal request
→ cache metadata on normal request
→ latency and cost delta
若无法关联,平台只能知道"花了多少钱预热",不能证明预热带来什么。
十三、用对照实验决定参数
1. 建立关闭组与开启组
选择相似项目和模型,把会话随机分为关闭组、默认策略组和自定义策略组。至少覆盖开发者常见的 5、10、20、30 分钟空闲区间。
yaml
experiment:
control:
warming: false
default:
warming: true
candidate:
warming:
interval: "5 minutes"
duration: "20 minutes"
这里的候选值只是实验示例,应由实际返回分布和 Provider 缓存窗口决定。
2. 先限制模型和人群
不要第一天给所有模型、所有仓库、所有用户开启。优先选择缓存指标透明、限流余量充足、上下文较长且用户返回率高的模型和团队。
text
灰度顺序
1% 内部用户
→ 单一模型
→ 单一区域或租户
→ 观察一个完整工作周期
→ 扩大或回滚
3. 设置自动停止条件
yaml
stop_conditions:
warming_failure_rate: "> 5%"
provider_429_increase: "> 1 percentage point"
total_cost_per_session: "> budget"
cache_benefit: "not measurable"
阈值应按团队基线制定。核心原则是:一旦费用或可靠性越界,不等待人工复盘才关闭。
十四、常见误区与定位方法
1. "开启后就一定更快"
不成立。Provider 可能没有可复用缓存,兼容网关可能重写请求,缓存键可能随参数变化,用户也可能在窗口结束后才回来。应以下一次普通请求的指标证明收益。
text
warming success ≠ cache hit guaranteed
cache hit ≠ end-to-end latency always lower
2. "响应被丢弃,所以不计费"
不成立。响应是否持久化与 Provider 是否完成模型推理是两件事。Warming 是真实请求,会消耗 Token、计费并占用速率限制。
text
discarded locally = true
provider request = real
usage and billing = possible
3. "失败后窗口会顺延"
不成立。失败不改变会话,但活跃窗口继续从最近一次普通请求起倒计时;下一次尝试要等下一个间隔。
text
failure
→ session unchanged
→ duration not extended
→ wait next interval
十五、安全、合规与限流治理
1. 数据边界与普通请求一致
由于预热使用当前会话上下文,它遵循与普通模型请求相同的数据出境、Provider 审批和日志合规要求。不能因为 Prompt 是固定短词就把它当成"无数据请求"。
yaml
compliance_check:
provider_approved: true
data_region_approved: true
conversation_policy_applies: true
logging_redacted: true
2. 限流预算要预留普通请求
预热流量不应把开发者真正需要的请求挤出限流池。平台应为普通请求预留容量,必要时在高峰期关闭预热或延长间隔。
text
provider quota
├─ reserved for normal requests
├─ controlled warming budget
└─ emergency headroom
3. 配置权限要受控
过短间隔、过长窗口会快速放大请求量。企业环境中应把 Warming 策略纳入配置评审,限制可选范围并记录变更人、Revision 和回滚值。
yaml
warming_policy_change:
owner: ai-platform
revision: warming-2026-10-30
approved_ranges:
interval: "2 minutes .. 15 minutes"
duration: "10 minutes .. 1 hour"
rollback: false
示例范围只是治理结构,不是通用推荐值。
十六、生产上线清单与总结
1. 上线前检查
- 已确认 Provider 确实提供可观测的缓存或短期状态收益;
- 已按模型和会话分群,而不是全局无差别开启;
-
interval与duration能解析为有限正数; -
interval < duration,请求次数上界已计算; - Prompt 不包含秘密、内网地址或真实用户标识;
- 已验证预热不写历史、不分发本地工具、不继续工具循环;
- 已区分共享后台服务、standalone 与显式
--server; - 已确认实际服务实例加载了目标配置;
- 预热请求与普通请求在日志和指标中可区分;
- 已关联下一次普通请求的缓存、延迟与成本;
- Provider 429、认证、网络和服务错误可分类;
- 普通请求拥有独立的限流容量预留;
- 灰度组、对照组、停止条件和回滚值已建立;
- 日志不记录认证头、密钥和完整私有会话正文;
- 关闭策略已演练,设置
warming: false后不再产生新预热。
2. 推荐的最小灰度配置
jsonc
{
"$schema": "https://opencode.ai/config.json",
"warming": {
"prompt": "Do not perform any work. Reply with exactly: OK",
"interval": "5 minutes",
"duration": "20 minutes"
}
}
这不是所有团队的最佳值,而是一个容易计算、容易观测、容易回滚的实验起点。正式值应由 Provider 缓存窗口、用户返回分布、Token 成本和限流余量共同决定。
3. 总结
OpenCode v2 Warming 的完整语义可以压缩为一条运行链:
text
非预热模型请求建立基线
→ 会话空闲达到 interval
→ 使用当前模型、Agent、Instructions 与上下文
→ 追加瞬时 Prompt 并发起一次真实请求
→ 丢弃响应,不写历史、不执行本地工具循环
→ 成功或失败后等待下一 interval
→ duration 始终从最近一次普通请求计时
→ 新普通活动重置两个计时器
真正成熟的预热策略,不是把 warming 从 false 改成 true,而是证明三件事:Provider 侧收益真实存在,下一次普通请求确实更快或更便宜,额外请求没有破坏费用、限流、安全和服务稳定性。只有指标能回答这三件事,Warming 才是运行时工程,而不是看不见的后台消耗。
参考资料:
- OpenCode v2 Warming
- OpenCode v2 CLI
- OpenCode v2 Config
- OpenCode v2 Models
- OpenCode v2 Troubleshooting
- OpenCode v2 Build
本文依据 2026 年 10 月 30 日可见的 OpenCode v2 官方文档整理。Warming 的字段、默认值、后台服务行为、Provider 缓存策略和计费规则可能继续变化;生产环境应固定实际版本,并以当前 Schema、官方文档、Provider 用量明细和端到端观测结果为准。