跳转到内容

Agent 感知环境变化,怎样用好 LLM 缓存

直接看三次模型请求,弄清环境变化放在哪里、哪些消息保持不动,以及缓存究竟省了什么。

Agent 要跟上环境变化,可以让后台程序持续监听,在需要模型判断时,把新状态追加到下一次请求。系统提示词保存行为规则,设备状态放进查询结果;稳定指令和旧消息保持原样,LLM 就有机会复用已经处理过的输入,减少重复计算。

后台收到事件,不代表正在生成回答的模型已经知道。控制脚本必须在执行前重新检查条件,拦住依据旧状态提出的动作。缓存只帮助节省输入处理成本,既不保证每次命中,也不替代这项检查。

我给家庭助手提供 Skill,用固定脚本调用 Home Assistant。下面沿用这个做法,走一遍开空调的任务。设备状态是演练数据,接口规则来自官方文档。

用户提出一个带条件的要求:

窗户关着的话,把客厅空调设成制冷 26°C。只做这一次,条件不满足就算了。

难点在于,模型查询时窗户还关着,准备开空调时,有人把窗户打开了。

下面把消息写成人能直接读懂的样子,省略 API 参数格式。带 + 的绿色行是本次新增内容,其余行与前一次完全相同。实际发送时没有这些展示标记。

发给模型的内容:

工具:查询房间状态、设置空调
规则:先查状态;条件不满足就停
用户:窗户关着才开空调
制冷 26°C,只做这一次

这里,“规则”放在系统提示词里,“用户”是本次用户消息。工具说明只写怎样调用,不塞当前设备状态。

模型看完,要求调用“查询房间状态”。后台脚本执行查询,得到窗户关闭、空调未开的结果。这次工具查询由程序完成,不需要模型自己写脚本。

工具:查询房间状态、设置空调
规则:先查状态;条件不满足就停
用户:窗户关着才开空调
制冷 26°C,只做这一次
模型:查询客厅状态
工具:窗户关闭,空调未开
查询回执 read_41

模型读到结果,准备调用“设置空调”,并带上 read_41

查询回执就是服务端保存的一份查询记录。脚本用它核对“模型刚才看到的状态,现在还有效吗”,不能只相信模型自己说有效。

就在模型生成这次调用期间,窗户被打开了。

后台监听程序收到 Home Assistant 的状态变化,更新本地记录。设置空调的脚本执行前再检查,发现旧回执对应的状态已变化,于是拒绝控制,空调没有被打开。

第三次,告诉模型为什么没有执行

Section titled “第三次,告诉模型为什么没有执行”
工具:查询房间状态、设置空调
规则:先查状态;条件不满足就停
用户:窗户关着才开空调
制冷 26°C,只做这一次
模型:查询客厅状态
工具:窗户关闭,空调未开
查询回执 read_41
模型:设置空调,使用 read_41
工具:未执行,窗户已经打开

模型现在可以回复用户,窗户已经打开,本次没有开启空调。

请看两条工具结果。“窗户关闭”记录的是前一次查询,“窗户已经打开”记录的是后来发生的变化。旧结果不用改,新结果接在后面。 模型读得出事情的先后,早期消息也保持了原样。

这次任务到此结束。窗户后来关闭,只更新状态,不自动重试。如果用户再次要求开空调,就重新查询、检查、执行。控制接口返回后,还要等待或查询设备状态,确认结果再回复用户。

是后台监听程序先知道。

Home Assistant 可以通过 WebSocket 推送 state_changed 事件。监听程序收到开窗事件,就把本地记录从“关闭”改成“打开”。不需要为每次变化调用一次大模型。官方接口

这份状态有两个用处:

  • 查询工具从中取得当前任务需要的信息,交给模型。
  • 控制脚本据此检查模型提出的动作,拦住已经不适用的操作。

已经发出去的模型请求不会因为后台记录变化而自动更新。模型要等下一次请求,才能读到新情况。因此,执行前检查必须放在脚本里。

没有相关任务时,事件只更新记录。用户订阅了提醒,或者变化影响当前任务,程序才安排后续处理。若模型没有请求工具,也可以在下一次请求尾部追加一条明确标为“环境通知”的消息,再让它查询细节。通知不能冒充用户授权。

这里只能尽快感知变化,不能保证物理世界零延迟。设备上报、网络传输都需要时间,检查与实际控制之间也仍可能发生变化。

再看第三次请求。新增的是最后两条消息,前面仍然与第二次请求相同。命中缓存时,供应商复用相同前缀已经算过的结果,剩余输入和新回答照常处理。

“前缀”就是从输入开头连续相同的内容。它可以包含系统提示词,也可以包含用户消息和工具结果,取决于供应商缓存了哪里。前缀缓存的作用

这就决定了信息应该放在哪里:

信息怎么放
工具定义、助手规则、稳定角色资料固定内容和顺序,放在前面
用户新要求、设备查询结果、环境通知按发生顺序追加
旧对话、旧查询结果原样保留,不用最新状态覆盖

最容易破坏缓存的写法,是每轮都重写开头,比如把“现在几点”“窗户当前状态”塞进系统提示词。即使后面几千字没变,它们也不再属于原来的相同前缀。

时间确实需要更新,就作为新消息或新查询结果追加。旧记录里的观察时间保留原值。Claude Code 团队也公开介绍过用后续消息承载变化、保留稳定前部的做法。工程实践

工具列表顺序、模型和相关配置也会影响命中。固定系统提示词只是其中一项,最终要检查实际发出的整个请求。 如果 Agent 框架每轮自动改写历史摘要,只修改 Skill 仍然不够。

Claude 和 DeepSeek 怎样启用缓存,工具结果怎样传

前面的消息框是阅读示意。Claude 的真实请求由 toolssystemmessages 组成。

下面是缓存配置片段,三个变量分别代表固定工具定义、固定系统指令和连续追加的消息。还需要填写项目使用的模型等参数。

{
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 秒有效,这只是演示值。窗户传感器返回 unknownunavailable 时,不允许按关闭处理。

同一次业务操作要有固定的操作 ID,重复到达时查回已有记录。调用超时则先核对结果,不直接重试;用户取消后停止安排新动作,已经发出的动作另行确认。

Home Assistant 的状态可能来自物理反馈,也可能是集成的乐观更新。确认到了哪一层,就报告到哪一层。对“开窗必须立即停机”这类持续约束,应使用明确授权的自动化或设备安全机制,不能等模型下一轮决定。

对话太长时,在没有等待中工具调用的任务边界整理旧历史,保留未完成要求和最近交互。摘要替换会使部分缓存重新建立,需要把摘要调用的成本一起计算。不要为了命中率无限追加事件,也不要每轮都重写摘要。

服务重启后要恢复消息并重新同步设备,不能依赖供应商缓存保存业务进度。存储部分见服务重启后的 Agent 恢复

运行演练,并确认缓存是否真的省钱

配套脚本使用模拟设备和模型响应,需要 Node.js 22 或更新版本,不调用付费 API。

Terminal window
curl -fsSLo agent-environment-cache.mjs \
https://blog.mlxb.cc/examples/agent-environment-cache.mjs
node agent-environment-cache.mjs

可以看到旧操作被拒绝,用户再次请求后才执行。重复投递同一操作,模拟设备的写入次数仍为一次。脚本还检查连续请求是否保留相同系统指令和旧消息,但不会把这个检查冒充真实缓存命中。

真实接入时,记录每次请求返回的缓存用量:

API需要记录的字段
Claudecache_read_input_tokenscache_creation_input_tokensinput_tokens
DeepSeek Chatprompt_cache_hit_tokensprompt_cache_miss_tokens

Claude 的输入总量是表中三项之和,不能只看 input_tokens。费用按缓存读取、写入、普通输入和输出分别计算。DeepSeek 当前按命中输入、未命中输入和输出计费,不套用 Claude 的缓存写入费率。Claude 用量说明DeepSeek 价格口径

用同一批任务和事件,对比“每轮重写上下文”与“原样保留、追加消息”,模型与输出限制保持一致。先只改消息组织,再单独测减少模型调用的收益,避免把两笔节省混算。

看每个成功任务的总费用,包括失败、重试和摘要生成。缓存读取也收费,长时间没有请求时,不要仅为保温而持续调用模型。费用下降后还要检查旧状态误操作和重复执行有没有增加。