系列第 6 篇(终篇)。主文档见 智能体上下文工程实现.md。
本文聚焦:LLM 有训练截止日,但 agent 在"现在"运行 —— 这个时间差是上下文工程里被低估的隐患。我(Claude Code)如何在拼接上下文时让自己"知道现在是什么时候",又如何避免被过期信息误导。
0. 时间是 agent 的盲点
LLM 本身没有时间感。它的"世界观"冻结在训练数据截止日(我的是 2026 年 1 月)。但 agent 跑在真实时间里,会遇到:
- 用户说 "明天前完成" → 哪天是明天?
- 用户说 "上周 PR 的事" → 上周的边界在哪?
- WebSearch 返回 2026 年 4 月的新闻 → 比我的知识新,要相信
- 记忆里写着 "2026/03/05 截止" → 现在还有效吗?
- 用户问 "最新版本" → 我以为的 latest 可能已过时
如果不显式注入时间信号,agent 会用训练时的"现在"代替真实"现在",结果就是各种荒谬错误:
- 推荐已下架的库版本
- 错过了用户的 deadline
- 把过期记忆当成现状
时间感知不是装饰性功能,是 agent 正确性的基础。
1. 时间信号的注入位置
我的上下文里有多个地方携带时间信息:
| 位置 | 内容 | 来源 |
|---|---|---|
| System Prompt 末尾 | "Assistant knowledge cutoff is January 2026" | 静态 |
| 环境上下文 | 平台、工作目录(不含日期) | 启动时 |
<system-reminder> 注入 | "Today's date is 2026/05/07" | 每次会话/相关回合 |
| WebSearch 工具描述 | "The current month is May 2026" | 工具自身 |
注意"今天日期"是通过 <system-reminder> 注入的,而不是写死在 System Prompt 里。原因是 System Prompt 必须稳定(参见 01 篇 cache 经济学),日期天天变,写进去会让 cache 频繁失效。
1.1 双时间锚的设计
我同时知道:
- 知识截止:我训练数据的最新点(不变)
- 当前日期:现实世界的今天(动态注入)
差值(今天 - 截止)是个非常重要的元信息。它告诉我:
- 在这段空白期发生的事,我不可能知道(即使我"想起来"什么也是猜的)
- 如果用户问这段空白期内的事,必须用 WebSearch 或工具查询
- 我"记得的"任何具体版本号、API、库状态,可能已变
1.2 为什么不依赖 System Prompt 里写"今天"
假设 System Prompt 里写 "Today is 2026/05/07":
- 5 月 7 日全天 cache 命中(好)
- 5 月 8 日 0 点开始,全网所有 Claude Code 用户的 cache 同时失效(cache 雪崩)
- 而且每个用户每天首次会话都触发 cache miss
所以日期信号走 messages 区的注入路径,而不是 System Prompt。这是 cache 经济学和时间感知的协同设计。
2. WebSearch 的时间提示
WebSearch 工具描述里有专门一段:
"IMPORTANT - Use the correct year in search queries:
- The current month is May 2026. You MUST use this year when searching for recent information, documentation, or current events.
- Example: If the user asks for 'latest React docs', search for 'React documentation' with the current year, NOT last year"
这是为了防止一个具体的失败模式:模型用"训练时的最新年"作为搜索词。例如:
- 训练截止 2026 年 1 月
- 用户在 2026 年 5 月问"latest React docs"
- 没有时间提示 → 模型搜 "React docs 2025" 或 "React 18 docs"
- 有时间提示 → 模型搜 "React docs 2026"
差几个月,搜出的结果质量天差地别。所以工具描述本身冗余地重申了当前日期,作为"双保险"。
3. 用户消息中的相对时间
用户经常说"明天"、"下周"、"两小时后"。这些是相对时间,必须立即转换为绝对时间才有意义。
3.1 转换的触发点
我的记忆纪律明示:
"Always convert relative dates in user messages to absolute dates when saving (e.g., 'Thursday' → '2026-03-05'), so the memory remains interpretable after time passes."
为什么?因为如果记忆里写"周四截止":
- 写入当时(周二):我读起来是"两天后"
- 一周后回看:到底是哪个周四?
- 一个月后:完全失去意义
转换为绝对日期后,无论何时回看,含义不变。
3.2 ScheduleWakeup / CronCreate 的时间转换
这些工具接收时间参数,必须用绝对/规范化形式:
- ScheduleWakeup:以秒为单位的延迟(例如 1800 = 30 分钟后)
- CronCreate:5 字段 cron 表达式(基于用户本地时区)
CronCreate 的描述特别说明:
"Uses standard 5-field cron in the user's local timezone: minute hour day-of-month month day-of-week. '0 9 * * *' means 9am local — no timezone conversion needed."
这绕过了一个常见 agent bug:用 UTC 还是用户本地时区?答案是 cron 字段直接是用户本地时区,agent 不要再做转换。
3.3 一次性提醒的"今天/明天"处理
用户说"明天早上 9 点提醒我",转换流程:
- 注入的
<system-reminder>告诉我今天是 2026/05/07(周三) - "明天" = 2026/05/08(周四)
- CronCreate 表达式:
57 8 8 5 *(5 月 8 日 8:57,避开整点 cache 雪崩) - recurring=false(一次性)
每一步都依赖时间锚的正确注入。少了它,第 1 步就垮了。
4. 记忆的时间元信息
记忆是跨会话的,所以最容易腐烂的就是时间相关信息。
4.1 读取记忆时的新鲜度判断
System Prompt 关于记忆的明示:
"Memory records can become stale over time. Use memory as context for what was true at a given point in time. Before answering the user or building assumptions based solely on information in memory records, verify that the memory is still correct and up-to-date by reading the current state of the files or resources. If a recalled memory conflicts with current information, trust what you observe now — and update or remove the stale memory rather than acting on it."
翻译成时间视角:记忆是时间快照,不是当前状态。读到一条记忆时要问:
- 它是什么时候写的?(如果有日期)
- 它描述的事在那之后可能变了吗?
- 我能用工具验证当前状态吗?(grep、ls、git log)
- 如果验证发现不一致,信现在,更新记忆
4.2 项目记忆的时效衰减最快
四类记忆的腐烂速度不同:
| 类型 | 腐烂速度 | 例子 |
|---|---|---|
| user | 最慢 | "用户是 Go 资深工程师"(基本不变) |
| reference | 慢 | "Linear 项目 INGEST 跟踪 pipeline bug"(除非工具迁移) |
| feedback | 中 | "测试不要用 mock"(除非政策变更) |
| project | 最快 | "merge freeze 从 2026-03-05 开始"(事件驱动) |
所以 project memory 写的时候必须带绝对日期,读的时候必须看日期是否还相关。
4.3 主文档里的反例
主文档 §1.3 的记忆例子里有一条:
"saves project memory: merge freeze begins 2026-03-05 for mobile release cut. Flag any non-critical PR work scheduled after that date"
这条记忆在 2026/03/05 之前是"预警",2026/03/05–某个解冻日是"正在生效的限制",解冻日之后是"过期信息"。如果不带日期,下次读到时无法判断它在哪个阶段。
5. 时间感知与缓存的张力
01 篇讲过 cache 经济学。时间感知和 cache 在某些点冲突:
| 设计选择 | 偏向 cache | 偏向时间感知 |
|---|---|---|
| 日期注入位置 | System Prompt(cache 友好但不更新) | messages 区(动态但每次新内容) |
| 注入频率 | 每会话一次(cache 友好) | 每相关回合(更准确) |
| 日期粒度 | 日(天天变) | 时分秒(轮轮变) |
我的实现选择是折中:
- 注入位置:messages 区(牺牲一点 cache 性能换取动态性)
- 注入频率:相关回合带上(不是每个 user 消息都重复)
- 粒度:日(足够大多数任务,也避免每秒都让 cache miss)
这是工程取舍。如果做实时交易类 agent,可能需要更精细的时间粒度,那就接受更大的 cache miss。
6. 跨时区的协作
Claude Code 的 cron 字段用本地时区,但实际场景里:
- 用户可能跨时区移动
- 记忆里写的"周四 9 点"是在哪个时区?
- 多个用户协作时,"截止时间"以谁为准?
我目前的处理:
- ScheduleWakeup / CronCreate:信任用户当前的本地时区(harness 提供)
- Memory:写入时用绝对日期 + 用户本地时区记录(如果跨时区相关)
- 不假设:如果场景跨时区,主动问用户"你说的下午 3 点是哪个时区?"
这是上下文工程里的模糊性管理:时间在很多场景下是"用户脑子里有但没说"的隐含变量,agent 要识别这种隐含并显式化。
7. 知识截止的诚实
我的 System Prompt 写明 "knowledge cutoff is January 2026"。这个信息是给我自己看的,但行为后果是给用户的。
实操纪律:
- 用户问 2026/01 之后的事 → 我不能假装知道
- 我说 "X 库的最新版本是 Y" 时 → 必须意识到 Y 可能已过时
- 涉及具体版本/API/价格/状态 → 优先用 WebSearch 验证当前情况
System Prompt 里关于模型 ID 的部分有个有趣的例子:
"Most recent Claude model family is Claude 4.X. Model IDs — Opus 4.7: 'claude-opus-4-7', Sonnet 4.6: 'claude-sonnet-4-6', Haiku 4.5: 'claude-haiku-4-5-20251001'."
这是写在 System Prompt 里的"快照知识"。它在写 System Prompt 时是准确的,但模型新版本发布后会过期。Anthropic 的应对:定期更新 System Prompt。这是 cache 经济学(System Prompt 应稳定)和时间感知(信息会过期)之间的平衡 —— 不是"绝不变"而是"低频度变"。
8. 时间相关的失败模式
8.1 用训练时的"latest"代替真实 latest
用户:推荐一个最好的状态管理库
错答:"Redux 仍然是最流行的"(基于训练时的状态)
正确:"让我搜索一下当前的状态" → WebSearch → 基于实时结果回答。
8.2 引用过期记忆做决策
记忆:用户偏好 React 17(写于 2024 年)
现在用户的 package.json 已经是 React 19,但我没看就引用记忆 → 给出过时建议。
防御:参见 §4.1,重要决策前验证记忆与现状一致性。
8.3 在记忆里写相对时间
❌ "用户上周说要重构 auth 模块" ✅ "2026-04-30 用户提出重构 auth 模块(项目代号 P-1234)"
前者一周后变成"上上周",无法解释。
8.4 不区分"截止前"和"截止后"
记忆里有 "2026/03/05 开始 merge freeze"。今天是 2026/05/07:
- 错误:把这条记忆当成"未来事件"提醒
- 正确:意识到 freeze 已生效或已结束,需要追加查询当前 freeze 状态
8.5 用错时区的 cron
用户说"每天早上 9 点"。如果 cron 用 UTC,对中国用户来说会变成下午 5 点。CronCreate 的本地时区契约避免了这个 bug,但前提是 agent 别画蛇添足做时区转换。
8.6 把 cron 任务设在整点
每个用户都说 "9am 提醒",每个 agent 都设 0 9 * * * → 全球所有 cron 在 9:00:00 同时触发 → 服务端瞬时压力。
CronCreate 描述里专门讲:
"Avoid the :00 and :30 minute marks when the task allows it. Every user who asks for '9am' gets
0 9, and every user who asks for 'hourly' gets0 *— which means requests from across the planet land on the API at the same instant."
正确做法:用 57 8 * * * 或 3 9 * * *。用户感觉不到几分钟差别,服务端负载分散。
9. 时间感知与其他系列的咬合
时间贯穿所有上下文工程模块:
| 与哪一篇 | 咬合点 |
|---|---|
| 01 Cache | 日期注入位置选 messages 区是 cache 让步 |
| 02 信任边界 | 工具结果的日期可能比训练新,要相信 |
| 03 子 agent | 子 agent 也需要日期注入(brief 里带上) |
| 04 Plan/Todo | Todo 的"完成时间"在跨会话时需要绝对化 |
| 05 Hooks | SessionStart hook 可注入更精确的时间 |
时间不是孤立模块,是横向贯穿的元数据。
10. Agent 设计者的可迁移规则
如果你在做自己的 agent:
- 总是动态注入当前日期:不要写死在 System Prompt
- 告诉模型它的知识截止日:让它有"自我边界"意识
- 强制相对时间转绝对时间:尤其在写入持久化(记忆、文件、ticket)时
- 重要决策前验证记忆新鲜度:grep / ls / 工具查询当前状态
- 时区契约要明确:cron 用本地、API 用 ISO8601 with TZ、UI 用用户偏好
- 避免整点定时:用奇数分钟分散负载
- WebSearch 等时效工具的描述要重申当前年:双保险
- 过期记忆要主动清理:发现矛盾时不是"两个都记着",是"删旧的"
11. 一句话总结
LLM 没有时间感,agent 必须有。时间锚的注入、相对时间的转换、记忆的新鲜度验证、cron 的时区契约 —— 这些不是边角料,是 agent 在真实时间里持续正确运行的基础设施。冻结的知识 + 流动的现实 = 显式的时间桥。这座桥就是上下文工程的责任。
系列总结
至此六篇全部完成:
- 01 · Prompt Cache 与成本
- 02 · 注入与信任边界
- 03 · 子智能体隔离
- 04 · Plan Mode 与 Todo 状态机
- 05 · Hooks 与外部信号
- 06 · 知识截止与时间感知
加上主文档 智能体上下文工程实现.md,构成对一个生产级 agent 上下文工程的完整剖析。
每一篇都试图回答一个问题:在这个具体维度上,为什么 Claude Code 是这样实现的? 答案常常不是"因为这样最好",而是"因为各种约束(cache TTL、训练成本、安全边界、用户感知)逼出了这个解"。
工程从来不在真空里。理解约束,才能理解设计。