Agent 感知环境变化,怎样用好 LLM 缓存
直接看三次模型请求,弄清环境变化放在哪里、哪些消息保持不动,以及缓存究竟省了什么。
Agent 要跟上环境变化,可以让后台程序持续监听,在需要模型判断时,把新状态追加到下一次请求。系统提示词保存行为规则,设备状态放进查询结果;稳定指令和旧消息保持原样,LLM 就有机会复用已经处理过的输入,减少重复计算。
后台收到事件,不代表正在生成回答的模型已经知道。控制脚本必须在执行前重新检查条件,拦住依据旧状态提出的动作。缓存只帮助节省输入处理成本,既不保证每次命中,也不替代这项检查。
我给家庭助手提供 Skill,用固定脚本调用 Home Assistant。下面沿用这个做法,走一遍开空调的任务。设备状态是演练数据,接口规则来自官方文档。
用户提出一个带条件的要求:
窗户关着的话,把客厅空调设成制冷 26°C。只做这一次,条件不满足就算了。
难点在于,模型查询时窗户还关着,准备开空调时,有人把窗户打开了。
直接看三次模型请求
Section titled “直接看三次模型请求”下面把消息写成人能直接读懂的样子,省略 API 参数格式。带 + 的绿色行是本次新增内容,其余行与前一次完全相同。实际发送时没有这些展示标记。
第一次,用户提出要求
Section titled “第一次,用户提出要求”发给模型的内容:
工具:查询房间状态、设置空调规则:先查状态;条件不满足就停用户:窗户关着才开空调 制冷 26°C,只做这一次这里,“规则”放在系统提示词里,“用户”是本次用户消息。工具说明只写怎样调用,不塞当前设备状态。
模型看完,要求调用“查询房间状态”。后台脚本执行查询,得到窗户关闭、空调未开的结果。这次工具查询由程序完成,不需要模型自己写脚本。
第二次,把查询结果交回模型
Section titled “第二次,把查询结果交回模型”工具:查询房间状态、设置空调规则:先查状态;条件不满足就停用户:窗户关着才开空调 制冷 26°C,只做这一次模型:查询客厅状态工具:窗户关闭,空调未开 查询回执 read_41模型读到结果,准备调用“设置空调”,并带上 read_41。
查询回执就是服务端保存的一份查询记录。脚本用它核对“模型刚才看到的状态,现在还有效吗”,不能只相信模型自己说有效。
就在模型生成这次调用期间,窗户被打开了。
后台监听程序收到 Home Assistant 的状态变化,更新本地记录。设置空调的脚本执行前再检查,发现旧回执对应的状态已变化,于是拒绝控制,空调没有被打开。
第三次,告诉模型为什么没有执行
Section titled “第三次,告诉模型为什么没有执行”工具:查询房间状态、设置空调规则:先查状态;条件不满足就停用户:窗户关着才开空调 制冷 26°C,只做这一次模型:查询客厅状态工具:窗户关闭,空调未开 查询回执 read_41模型:设置空调,使用 read_41工具:未执行,窗户已经打开模型现在可以回复用户,窗户已经打开,本次没有开启空调。
请看两条工具结果。“窗户关闭”记录的是前一次查询,“窗户已经打开”记录的是后来发生的变化。旧结果不用改,新结果接在后面。 模型读得出事情的先后,早期消息也保持了原样。
这次任务到此结束。窗户后来关闭,只更新状态,不自动重试。如果用户再次要求开空调,就重新查询、检查、执行。控制接口返回后,还要等待或查询设备状态,确认结果再回复用户。
窗户变了,谁先知道
Section titled “窗户变了,谁先知道”是后台监听程序先知道。
Home Assistant 可以通过 WebSocket 推送 state_changed 事件。监听程序收到开窗事件,就把本地记录从“关闭”改成“打开”。不需要为每次变化调用一次大模型。官方接口
这份状态有两个用处:
- 查询工具从中取得当前任务需要的信息,交给模型。
- 控制脚本据此检查模型提出的动作,拦住已经不适用的操作。
已经发出去的模型请求不会因为后台记录变化而自动更新。模型要等下一次请求,才能读到新情况。因此,执行前检查必须放在脚本里。
没有相关任务时,事件只更新记录。用户订阅了提醒,或者变化影响当前任务,程序才安排后续处理。若模型没有请求工具,也可以在下一次请求尾部追加一条明确标为“环境通知”的消息,再让它查询细节。通知不能冒充用户授权。
这里只能尽快感知变化,不能保证物理世界零延迟。设备上报、网络传输都需要时间,检查与实际控制之间也仍可能发生变化。
哪些内容能继续用缓存
Section titled “哪些内容能继续用缓存”再看第三次请求。新增的是最后两条消息,前面仍然与第二次请求相同。命中缓存时,供应商复用相同前缀已经算过的结果,剩余输入和新回答照常处理。
“前缀”就是从输入开头连续相同的内容。它可以包含系统提示词,也可以包含用户消息和工具结果,取决于供应商缓存了哪里。前缀缓存的作用
这就决定了信息应该放在哪里:
| 信息 | 怎么放 |
|---|---|
| 工具定义、助手规则、稳定角色资料 | 固定内容和顺序,放在前面 |
| 用户新要求、设备查询结果、环境通知 | 按发生顺序追加 |
| 旧对话、旧查询结果 | 原样保留,不用最新状态覆盖 |
最容易破坏缓存的写法,是每轮都重写开头,比如把“现在几点”“窗户当前状态”塞进系统提示词。即使后面几千字没变,它们也不再属于原来的相同前缀。
时间确实需要更新,就作为新消息或新查询结果追加。旧记录里的观察时间保留原值。Claude Code 团队也公开介绍过用后续消息承载变化、保留稳定前部的做法。工程实践
工具列表顺序、模型和相关配置也会影响命中。固定系统提示词只是其中一项,最终要检查实际发出的整个请求。 如果 Agent 框架每轮自动改写历史摘要,只修改 Skill 仍然不够。
接入时再看这些细节
Section titled “接入时再看这些细节”Claude 和 DeepSeek 怎样启用缓存,工具结果怎样传
前面的消息框是阅读示意。Claude 的真实请求由 tools、system 和 messages 组成。
下面是缓存配置片段,三个变量分别代表固定工具定义、固定系统指令和连续追加的消息。还需要填写项目使用的模型等参数。
{ tools: fixedTools, system: [{ type: 'text', text: fixedSystem, cache_control: { type: 'ephemeral' } }], cache_control: { type: 'ephemeral' }, messages}系统文本末尾的 cache_control 标记固定前部;顶层标记用于缓存增长的对话。组合使用仍受断点数量、最小长度、有效期和接入平台限制,不能保证这个短例子命中。Claude 缓存说明
Claude 的工具调用保存在 assistant 消息的 tool_use 中,工具结果放进紧接着的 user 消息里的 tool_result,两者用调用 ID 配对。这里的 user 是 API 格式,不代表工具内容来自真人用户。
不要把环境通知插在工具调用和结果之间。外部数据留在工具结果中,不要为了缓存搬进系统指令;没有工具调用时,也不能伪造一个 tool_result。工具消息规则
DeepSeek 的 Context Caching 默认开启,命中要匹配已经保存的完整缓存前缀。即使两次请求开头相同,也不保证第二次就命中。应用保留历史、继续追加,实际是否命中看返回的用量字段,不套用 Claude 的配置参数。DeepSeek 缓存说明
多角色场景可以把公共工具和规则放在角色资料前面。角色真的更新就使用新版本,安全规则和权限收回不能为了缓存延后。不同用户的数据仍要分别鉴权,缓存不是权限系统。
监听、控制和长对话,怎样避免用到过期信息
监听程序首次连接时要取得当前状态,再持续接收更新。可以复用 Home Assistant 官方客户端的 subscribeEntities;自己实现时,要处理初始快照读取期间到达的事件,避免旧事件覆盖较新的快照。实体订阅实现
断线后把状态标成未同步,旧查询回执失效。重连后重新同步,再查询。即使断线前后窗户都关闭,中间也可能发生过开窗又关窗,不能沿用旧回执。
控制脚本应检查会话权限、参数、回执有效期和相关设备版本。配套演练将回执设为 30 秒有效,这只是演示值。窗户传感器返回 unknown 或 unavailable 时,不允许按关闭处理。
同一次业务操作要有固定的操作 ID,重复到达时查回已有记录。调用超时则先核对结果,不直接重试;用户取消后停止安排新动作,已经发出的动作另行确认。
Home Assistant 的状态可能来自物理反馈,也可能是集成的乐观更新。确认到了哪一层,就报告到哪一层。对“开窗必须立即停机”这类持续约束,应使用明确授权的自动化或设备安全机制,不能等模型下一轮决定。
对话太长时,在没有等待中工具调用的任务边界整理旧历史,保留未完成要求和最近交互。摘要替换会使部分缓存重新建立,需要把摘要调用的成本一起计算。不要为了命中率无限追加事件,也不要每轮都重写摘要。
服务重启后要恢复消息并重新同步设备,不能依赖供应商缓存保存业务进度。存储部分见服务重启后的 Agent 恢复。
运行演练,并确认缓存是否真的省钱
配套脚本使用模拟设备和模型响应,需要 Node.js 22 或更新版本,不调用付费 API。
curl -fsSLo agent-environment-cache.mjs \ https://blog.mlxb.cc/examples/agent-environment-cache.mjsnode agent-environment-cache.mjs可以看到旧操作被拒绝,用户再次请求后才执行。重复投递同一操作,模拟设备的写入次数仍为一次。脚本还检查连续请求是否保留相同系统指令和旧消息,但不会把这个检查冒充真实缓存命中。
真实接入时,记录每次请求返回的缓存用量:
| API | 需要记录的字段 |
|---|---|
| Claude | cache_read_input_tokens、cache_creation_input_tokens、input_tokens |
| DeepSeek Chat | prompt_cache_hit_tokens、prompt_cache_miss_tokens |
Claude 的输入总量是表中三项之和,不能只看 input_tokens。费用按缓存读取、写入、普通输入和输出分别计算。DeepSeek 当前按命中输入、未命中输入和输出计费,不套用 Claude 的缓存写入费率。Claude 用量说明、DeepSeek 价格口径
用同一批任务和事件,对比“每轮重写上下文”与“原样保留、追加消息”,模型与输出限制保持一致。先只改消息组织,再单独测减少模型调用的收益,避免把两笔节省混算。
看每个成功任务的总费用,包括失败、重试和摘要生成。缓存读取也收费,长时间没有请求时,不要仅为保温而持续调用模型。费用下降后还要检查旧状态误操作和重复执行有没有增加。