7.7 KiB
RFC:可选时间上下文插件
Status: implemented
English | 中文
问题
如果部署方既没有将当前时间硬编码到提示词中,也没有向模型提供查询时间的工具,agent(智能体)请求就无法获得实时准确的时钟信息。静态文本会立即失真,而对于日期、截止时间或会话闲置时长等常规推理,调用工具会带来不必要的开销。模型还缺少另一项配套信息:已经过去的时长。模型虽然能收到当前用户提示词,却无法区分紧接着发送的消息与上一条会话消息几小时后才发送的消息。
提示词组装流程和会话日志已经提供所需输入。区段提供方会在每个步骤中针对活跃 agent 运行一次;模型可见的会话事件带有持久的追加时间戳;请求头折叠结果会记录系统提示词实际渲染的确切内容。设计需要决定时间信息应归属何处、应以多高频率变化,同时避免累积陈旧读数或创建后台任务。
决策
@deepseek-ai/dsh-time-context 是位于 packages/context/time-context/ 的可选函数式插件。它新增 context/ 产品分组,用于容纳既不定义工具、也不定义服务边界的有界请求上下文增强。dsh-agent-core 和仓库提供的任何示例都不会加载该 package;只有当时间上下文值得占用 token 和披露信息时,部署方才显式挂载它。
该插件注册一个名为 context:time、顺序值为 10 的全局 ctx.systemPrompt.section() 贡献,位置在部署方角色设定之后、工具指导之前。对于处于活跃轮次中的 agent,其提供方返回两行内容:一行是带数字 UTC 偏移和 IANA 时区、形似 ISO 的时间戳;另一行是从该轮次开始前最后一条模型可见消息起算的紧凑整秒时长。未绑定 agent 或 agent 处于空闲状态时,提示词组装结果中的该区段为空。
上一条消息基线
在轮次首次组装时,提供方从该轮次的 turn/start 向前扫描,采用最近的 user/message、assistant/message、tool/result、context/message 或 steering/message 时间戳。当前轮次中新追加的用户提示词会被刻意排除:若从该事件开始计时,首次请求会报告接近零的时长,从而丢失此功能要表达的轮次间隔。同一轮次内的后续刷新始终保留这条基线,因此长时间运行的轮次会报告从上一条会话消息起不断增加的时长。首个轮次报告 unavailable (no earlier message in this session)。
基线采用会话事件的追加时间,而不是日志中不存在的客户端接收时间。这样,恢复和 fork 行为都能从持久日志中确定性重现,模型可见值也无需新增事件即可重建。如果系统挂钟向后调整,插件会将显示时长钳制为零,而不会产生负数间隔。
刷新策略
refreshIntervalMs 默认值为 60,000,并且必须是非负安全整数。每个轮次的首次请求都强制刷新,不受上一轮次时间戳影响。在包含多个步骤的轮次内,后续组装会复用缓存区块,直至其存在时间达到该间隔;设为 0 时每个步骤都刷新。该策略仅由请求驱动:agent 正在等待模型调用、运行工具或处于空闲状态时,没有请求会消费新值,因此计时器不会创建任何任务。
timeZone 默认为 UTC,插件加载时会校验它是否为 IANA 标识符。格式化器会输出形似 ISO 的本地时间戳,其中包含已解析的时区及其当前数字偏移,使夏令时变化保持显式可见,而不是让不带时区的时钟在无提示的情况下发生偏移。
日志与 token 形态
时间区块属于动态系统提示词状态。agent loop(智能体循环)现有的 request/header 快照和 request/header-delta 折叠结果会在发送前记录每次渲染变化,从而满足可重建请求契约。每个请求只携带一个当前区块;先前的读数不会保留在派生的会话历史中。该设计遵循提示词变量 RFC的所有权规则:可选插件拥有时间信息,并通过常规提示词注册表贡献该信息,不为循环添加特殊分支。
测试
该 package 的测试套件使用伪造的系统时间,覆盖 UTC 与偏移格式化、首轮次回退文本、所有符合条件的上一条消息类型、完整时长单位、挂钟回拨钳制、逐轮次刷新、间隔内复用、间隔到期、0 对应的逐步骤行为、相互独立的逐 agent 缓存、无效配置、HMR(热模块替换)资源释放以及 Loader 命名空间路径。一个使用真实 agent loop 的测试会固定实际发送的系统提示词及其 request/header-delta 刷新记录。默认快照不发生变化,因为所有仓库提供的组合都刻意不包含该插件;若在默认快照 fixture 中挂载它,将违反显式选择加入的决策。
考虑过的替代方案
- 每个轮次或每次刷新都追加一条
context/message——不予采纳:每个读数都会留在派生历史中,因此陈旧时钟值和 token 成本会随会话长度累积。表层替换操作无法同时删除旧节点并将新读数移动到尾部;替换会保留旧节点的位置,而通过尾部节点替换又会隐藏中间的会话内容。 - 使用
agent/session-prefix——不予采纳:前缀在每个循环实例中只组装一次,并且按设计在会话期间保持稳定,因此无法表示每个轮次或步骤都会变化的时钟。 - 在
agent/request中修改请求——不予采纳:该边界只负责塑造调用配置,触发时间晚于消息边界;如果在此处插入模型可见内容,会同时绕过提示词压力核算和请求头日志契约。 - 注册独立的
{{current_time}}和{{elapsed}}提示词变量——不予采纳:两个独立提供方可能在不同时间点采样,并且需要共享缓存才能保证刷新语义的原子性。单个区段提供方将二者作为一个值计算和记录;部署方也不需要在角色设定中重复时间模板。 - 按照配置的间隔通过后台计时器注入——不予采纳:没有正在组装的模型请求时,新值没有消费方。由计时器驱动
agent.inject()会创建持久的一次性轮次,并且只为通知时间流逝就唤醒或修改空闲会话。 - 在
dsh-agent-core中挂载插件——不予采纳:时区、信息披露、token 预算和期望新鲜度都属于部署策略。显式选择加入能保持默认 harness 上下文稳定。 - 将 package 放入
core/——不予采纳:core 负责产品 API 主干。上下文增强是没有服务键的可选叶节点,因此专用分组能直接表达其组合角色。
后果
- 选择加入的部署无需消耗工具调用,即可让模型获得无歧义的分区时钟和轮次间隔时长。每个请求的系统提示词 token 成本固定,不会随会话增长。
- 刷新会改变请求头,因此会新增一条
request/header-delta事件。refreshIntervalMs用时钟新鲜度换取这些持久增量记录的数量;将其设为零会在每个整秒渲染结果发生变化的步骤中刻意记录新值。 - 系统不会仅为刷新时间而创建请求。工具运行时间超过该间隔时,先前读数会保持不变,直至下一步骤开始组装,此时提供方会追赶到当前时间。
- 时长反映持久追加边界处的 harness 处理时间,不包含消息进入日志之前的客户端网络延迟。若要保留客户端来源时间戳,需要单独的持久输入契约,不属于本插件范围。