OpenCode v2 运行时预热实战:Warming、Prompt Cache、后台服务与成本治理

这是《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 才是运行时工程,而不是看不见的后台消耗。

参考资料:

本文依据 2026 年 10 月 30 日可见的 OpenCode v2 官方文档整理。Warming 的字段、默认值、后台服务行为、Provider 缓存策略和计费规则可能继续变化;生产环境应固定实际版本,并以当前 Schema、官方文档、Provider 用量明细和端到端观测结果为准。

相关推荐
AI_Lab012 小时前
服务器管理工具要在服务器上装 Agent,安全吗?
服务器·安全·github
晓天衡宇•评测社区2 小时前
当需求具体到图片尺寸,Nano Banana 2.1 交出了什么答案?
人工智能·计算机视觉
2601_961146202 小时前
实验室等离子表面处理设备安全整改记录:通风、UV、接地三项
笔记·安全·uv
fruge2 小时前
没有独显也想做AI配音?我用MOSS-TTS-Nano在CPU电脑上跑通了本地语音生成
人工智能·电脑
天树星智源2 小时前
静态物理满分、交互 11.54%:3D 场景生成系统的幻觉
论文阅读·人工智能·3d·机器人
OpenAnolis小助手2 小时前
多租户公有云共享 CXL 内存池场景下的内存卸载研究
人工智能·操作系统·龙蜥社区·risc-v·agentic os
zhongerzixunshi3 小时前
落实等保要求,筑牢信息系统安全防线
安全
xsd202411183 小时前
OCEC 24×40 开闭眼脑电分类:从 α 波节律到实时 BCI 落地方案
人工智能·1024程序员节
Ivanqhz3 小时前
MoE + LLM 应用
数据库·人工智能·pytorch·python·深度学习·算法·1024程序员节