在开发智能差旅助手时,我遇到过一个容易被忽略的问题:系统提示词越来越长,但每轮对话都要重复发送和计算。
差旅助手中的每个子 Agent 都有自己的角色定义、业务规则、工具使用规范和输出格式。随着功能增加,单个系统提示词可能达到几千 Token。用户每多说一句话,这些稳定内容就会被重复处理一次。
后来在阅读 Manus 的上下文工程实践时,我重点关注了 KV Cache。结合项目实际情况,我对系统提示词中的动态内容做了拆分,让稳定的提示词前缀可以在多轮对话中持续命中缓存。
这篇文章记录这个问题是怎么出现的,以及我在 差旅助手中采用的处理方式。
KV Cache 为什么依赖稳定前缀
大模型生成文本时采用自回归方式。生成后面的 Token 时,模型需要参考前面已经出现的内容。
Transformer 会为历史 Token 计算 Key 和 Value,也就是常说的 KV。后续生成过程可以复用已经计算过的 KV,避免每次都从头计算。
因此,当两次请求的开头内容完全相同时,模型可以复用这部分计算结果。这类缓存也被称为前缀缓存。
前缀缓存有一个约束:
缓存内容必须从开头开始连续一致。前缀中间一旦发生变化,后面的缓存也会失效。
例如,两次请求只有当前时间不同:
Plain
你是差旅助手。当前时间:14:32:07。请遵守以下业务规则......
你是差旅助手。当前时间:14:32:08。请遵守以下业务规则......
从语义上看,两段文字几乎没有区别。但从 Token 序列看,前缀已经在时间部分发生变化。时间后面的业务规则也就无法继续复用之前的缓存。
这也是 KV Cache 优化中最容易忽略的一点:动态内容放在长前缀中,会影响它后面的所有内容。
对话应用中,系统提示词是最值得优化的部分
对话类 Agent 的消息通常包含几部分:
Plain
系统提示词
当前时间
历史消息
用户问题
工具调用结果
其中系统提示词通常最长,也最稳定。
以我开发的 智能差旅助手为例,行程规划、差旅管理、酒店预订和通用信息查询等子 Agent,都包含较长的系统提示词。这些提示词描述了:
- Agent 的职责和边界;
- 差旅业务规则;
- 工具调用顺序;
- 多轮对话处理方式;
- 结果输出格式;
- 异常和确认流程。
这些内容在同一个 Agent 的多轮会话中很少变化。如果每轮都能命中缓存,节省的是:
Plain
系统提示词长度 × 对话轮数 × 用户数量
百炼提供了隐式缓存能力,平台会自动识别请求之间相同的前缀并复用。项目中的相关配置和注释按缓存命中部分约 20% 的价格进行估算。
因此,系统提示词成为上下文工程的主要优化对象。
问题从哪里开始:动态时间写进了系统提示词
差旅助手需要知道当前时间。
用户说"明天去杭州"或者"下周三返回"时,模型需要根据当前日期计算具体日期。时间信息不能直接删除。
最自然的做法,是在加载系统提示词时,把当前日期、星期和时间都填入模板:
Plain
当前日期:2026-07-27
当前星期:星期一
当前时间:14:32:07
这种方式使用起来简单,但秒级时间会不断变化。
差旅助手早期的 Prompt 加载方式就支持直接注入当前时间。对于不需要缓存优化的场景,这种方式没有问题;但对于需要频繁多轮对话的 Agent,它会破坏系统提示词的稳定前缀。
时间位于提示词前部时,影响范围如下:
Plain
当前时间发生变化
↓
系统提示词前缀发生变化
↓
时间后面的业务规则无法复用缓存
↓
每轮重新计算大量稳定内容
模型仍然需要时间,但时间不应该放在长系统提示词的前面。
我的处理方式:把稳定内容和动态内容分开
我最后采用的方案是:
- 系统提示词只保留当天内稳定的内容;
- 当前日期和星期继续写入系统提示词;
- 秒级变化的当前时间单独作为一条消息注入;
- 动态时间紧跟静态系统提示词之后。
消息结构变成:
Plain
[静态系统提示词]
[动态时间消息]
[历史对话和用户消息]
[工具调用消息]
这样,第一条系统消息在一天内保持不变。动态时间虽然每秒变化,但它位于缓存前缀之后,不会影响前面的系统提示词。
系统提示词中会明确告诉模型:
Plain
当前时间见消息列表中的动态时间注入。
模型仍然可以读取当前时间,只是时间不再参与静态提示词的构建。
日期和星期为什么仍然放在系统提示词中
日期和星期也属于动态信息,但它们一天只变化一次。
如果把它们也放到独立消息中,当然可以获得更长的缓存稳定时间。不过在差旅场景中,日期是系统提示词中比较重要的上下文,直接放在角色规则附近更容易让模型理解。
因此我采用了一个折中方案:
Plain
当前日期:一天变化一次
当前星期:一天变化一次
当前时间:每秒变化,独立注入
这样同一个自然日内,系统提示词保持稳定;跨过零点后,日期发生变化,重新建立缓存。
这个取舍适合差旅助手。相比每秒破坏缓存,每天重新建立一次缓存的代价更容易接受。
动态时间应该在什么时候注入
动态时间需要在每轮模型推理前加入消息列表。
在 差旅助手中,我把这部分处理放在 Agent Hook 中。Hook 会在模型推理开始前检查当前消息:
- 消息列表不能为空;
- 第一条消息必须是系统消息;
- 动态时间插入到第一条系统消息之后;
- 后面的历史消息和工具消息保持原有顺序。
这样每轮消息的开头都保持一致:
Plain
第一条:静态系统提示词
第二条:当前时间
后续:对话和工具消息
Hook 的执行优先级也需要靠前。项目中动态时间注入发生在上下文压缩等处理之前,这样后续处理拿到的是完整消息列表,动态时间的位置也不会被其他逻辑改变。
这里有一个边界条件:如果第一条消息不是系统消息,Hook 会跳过注入。这个判断用于保护异常消息结构,避免把时间插入到错误位置。
哪些 Agent 使用了这套方式
我在 差旅助手中逐步将需要高频多轮交互的 Agent 切换到静态系统提示词模式,包括:
- 行程规划 Agent;
- 差旅管理 Agent;
- 预订 Agent;
- 信息查询 Agent。
这些 Agent 的系统提示词较长,且会在一次会话中多次调用。缓存优化的收益比较明显。
项目中仍然存在一些旧的 Prompt 加载方式,用于兼容不需要缓存优化的场景。这里没有强行统一,因为不同 Agent 的调用频率、提示词长度和动态信息需求并不相同。
上下文工程需要结合调用场景处理,不是把所有消息都塞进同一种模板。
这套方案解决了什么问题
降低重复计算和输入成本
系统提示词不再因为秒级时间变化而频繁失去缓存。
长提示词在多轮对话中可以持续复用,尤其适合调用次数较多的行程规划和差旅管理场景。
降低首 Token 延迟
命中静态前缀后,模型不需要重新计算完整的系统提示词,首 Token 延迟也会相应降低。
保留时间理解能力
当前时间仍然会发送给模型。
用户提出下面这些问题时,模型仍然可以正常处理:
- 明天从上海去杭州;
- 后天返回;
- 下周五提交申请;
- 三天后入住酒店;
- 本周最后一个工作日。
变化的只是时间在消息中的位置。
减少提示词维护时的隐性成本
系统提示词中不再混入频繁变化的内容,排查缓存命中率时也更容易定位问题。
如果缓存命中率突然下降,可以优先检查:
- 系统提示词是否新增动态字段;
- 工具定义是否每轮变化;
- 消息顺序是否发生改变;
- 是否有请求级内容被拼接到了系统提示词前部。
还需要注意哪些问题
随机内容也会破坏前缀
除了时间,以下内容也不适合放在静态系统提示词中:
- 请求 ID;
- 随机字符串;
- 当前用户动态信息;
- 每轮变化的工具列表;
- 动态生成的调试信息;
- 不稳定顺序的 JSON 内容。
只要它们位于长提示词前部,就可能影响后续缓存。
消息顺序需要保持稳定
即使文本内容相同,消息顺序发生变化,也可能导致 Token 前缀不同。
因此,消息构建逻辑需要保持稳定:
Plain
静态系统提示词
→ 动态时间
→ 历史消息
→ 当前问题
→ 工具调用结果
缓存数据需要结合指标判断
缓存优化不能只看设计是否合理,还要观察实际调用数据。
我会重点关注:
- 缓存命中的 Token 数;
- 总输入 Token;
- 首 Token 延迟;
- 单轮调用成本;
- 不同 Agent 的缓存命中情况。
在 差旅助手的调用观测中,隐式缓存命中的 Token 总量基本保持在输入 Token 的 50% 左右。这个数据会随着 Prompt 长度、对话轮次和平台缓存策略变化,不能脱离测试条件单独解读。
总结
在 Agent 系统中,系统提示词往往是最稳定、也最容易重复计算的一部分。
时间、请求 ID、用户临时状态等动态信息如果直接写进长系统提示词,会破坏 KV Cache 的前缀匹配。
我在 差旅助手中的处理方式是:
Plain
稳定规则放在系统提示词中
动态时间放在独立消息中
消息位置固定
同一天内保持静态前缀不变
这套方案没有减少模型需要知道的信息,只调整了信息的组织方式。
上下文工程的一个实用判断是:
让稳定内容尽可能早出现,让动态内容尽可能晚出现。