Learn Claude Code · Session 10

SYSTEM PROMPT

运行时 Prompt 组装 + 确定性缓存 — 相比 s09 的新增内容解析
§0 整体功能 · Overview

解决的问题

s09 中 build_system() 把整个 system prompt 拼成一个字符串,内容不可分离、不可按需加载。 s10 将 prompt 拆成 可独立启用的片段字典,根据运行时真实状态(工具列表、记忆文件等)动态组装,并加入确定性缓存避免重复拼接。

在系统中的角色

每次 agent_loop 调用前,update_context() 读取真实状态 → get_system_prompt() 比较缓存键 → 命中则跳过 → 未命中则调 assemble_system_prompt() 生成新字符串并传入 client.messages.create()

这是 Claude Code 在生产中 prompt caching(API 层)的教学简化版本。

关键新增 vs s09

  • PROMPT_SECTIONS — 片段字典
  • assemble_system_prompt(context) — 按 context 选片段
  • get_system_prompt(context) — 缓存包装
  • update_context(context, messages) — 从真实状态推导 context
  • agent_loop 签名增加 context 参数,每轮工具调用后刷新

§1 PROMPT_SECTIONS — 片段字典

定义与结构

代码行语法注解
PROMPT_SECTIONS = { 模块级常量,类型为 dict[str, str];键是片段名,值是 prompt 文本
"identity": "You are a coding agent. Act, don't explain.", 固定身份片段 — 无条件加载;字符串字面量作为 dict 值
"tools": "Available tools: bash, read_file, write_file.", 固定工具列表片段;生产版本会从注册表动态生成此字符串
"workspace": f"Working directory: {WORKDIR}", f-string:模块加载时一次性求值,WORKDIR = Path.cwd() 已在此之前定义
"memory": "Relevant memories are injected below when available.", 条件片段 — 仅当 context["memories"] 非空时才追加;这里只是占位说明文字
} dict 字面量结束;Python 3.7+ 保证插入顺序
示例 — 运行时值 PROMPT_SECTIONS["workspace"] # → "Working directory: /home/user/myproject" PROMPT_SECTIONS["identity"] # → "You are a coding agent. Act, don't explain."

设计意图

为何用字典而非字符串常量?
字典允许按键有选择地组装:固定片段始终包含,条件片段(memory)仅在状态满足时追加。 生产中可将键与 Claude API 的 cache_control 边界对齐,最大化 prompt cache 命中率。

片段粒度决定缓存效果:粒度太细 → 拼接频繁;太粗 → 动态内容污染缓存块。 s10 的平衡点是:3 个静态块 + 1 个动态记忆块。


§2 assemble_system_prompt(context) — 选择并拼接片段

逐行注解

代码行语法注解
def assemble_system_prompt(context: dict) -> str: 函数签名;context: dict 类型注解(非强制),返回 str
sections = [] 空列表,按顺序收集要包含的片段文本;顺序决定最终 prompt 结构
sections.append(PROMPT_SECTIONS["identity"]) list.append() 就地修改;无条件追加身份片段(每次必有)
sections.append(PROMPT_SECTIONS["tools"]) 无条件追加工具片段;顺序保证 Claude 先看到身份再看到工具
sections.append(PROMPT_SECTIONS["workspace"]) 无条件追加工作目录片段
memories = context.get("memories", "") dict.get(key, default):若键不存在返回 "" 而非抛 KeyError
if memories: Python 布尔求值:空字符串为 False,非空为 True — 条件追加记忆片段
sections.append(f"Relevant memories:\n{memories}") f-string 内嵌 \n 换行符;把 MEMORY.md 内容插入 system prompt
return "\n\n".join(sections) str.join():用双换行把所有片段拼为一个字符串;双换行让 Claude 清晰分段

输入 → 输出示例

输入 context(有记忆) context = { "enabled_tools": ["bash", "read_file", "write_file"], "workspace": "/home/user/myproject", "memories": "- [user-pref-tabs](user-pref-tabs.md) — 偏好 tabs" }
输出 system prompt "You are a coding agent. Act, don't explain. Available tools: bash, read_file, write_file. Working directory: /home/user/myproject Relevant memories: - [user-pref-tabs](user-pref-tabs.md) — 偏好 tabs"
输入 context(无记忆) context = {"enabled_tools": [...], "workspace": "...", "memories": ""} # → 输出不含 "Relevant memories:" 块,只有 3 段

§3 get_system_prompt(context) — 确定性缓存包装

逐行注解

代码行语法注解
_last_context_key = None 模块级私有变量(约定 _ 前缀 = 内部使用);进程内单例缓存键,初始为 None
_last_prompt = None 缓存的 prompt 字符串;与 _last_context_key 配对使用
def get_system_prompt(context: dict) -> str: 公开函数,外部调用点;返回组装好的 prompt 字符串
global _last_context_key, _last_prompt global 声明:允许函数内对模块级变量赋值(读可以不声明,写必须声明)
key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str) json.dumps:将 dict 序列化为字符串作为缓存键;sort_keys=True 保证 key 顺序无关;ensure_ascii=False 允许中文字符;default=str 处理不可序列化类型
if key == _last_context_key and _last_prompt: 缓存命中条件:键相同 prompt 非空(防止 None/空串漏过)
print(" \033[90m[cache hit] system prompt unchanged\033[0m") ANSI 颜色码:\033[90m = 灰色,\033[0m = 重置;用于终端调试可见性
return _last_prompt 早返回(early return):缓存命中则直接返回,跳过下方所有组装逻辑
_last_context_key = key 缓存未命中:更新缓存键(赋值给模块级变量,需 global 声明)
_last_prompt = assemble_system_prompt(context) 调用组装函数,将结果存入模块级缓存变量
loaded = ["identity", "tools", "workspace"] 调试用列表:记录本次加载了哪些片段;不影响逻辑
if context.get("memories"): 再次检查记忆是否加载,仅用于调试输出
loaded.append("memory") 追加到调试列表
print(f" \033[32m[assembled] sections: {', '.join(loaded)}\033[0m") \033[32m = 绿色;', '.join(loaded) 将列表转为逗号分隔字符串
return _last_prompt 返回新组装的 prompt

为何不用 Python hash()?

hash() 在 Python 3.3+ 引入了进程级随机化(PYTHONHASHSEED),同一 dict 在不同进程中哈希值不同,且 dict/list 本身不可哈希。

json.dumps(..., sort_keys=True) 产生确定性字符串,跨进程一致,且嵌套结构均可序列化。

缓存命中 vs 未命中流程

第 1 轮(无记忆) context = {"memories": "", ...} key = '{"enabled_tools":[...],"memories":"","workspace":"..."}' # _last_context_key = None → 缓存未命中 # 输出: [assembled] sections: identity, tools, workspace
第 2 轮(context 未变) # key 相同 → 缓存命中 # 输出: [cache hit] system prompt unchanged
第 3 轮(记忆更新后) context = {"memories": "- [user-pref]...", ...} # key 不同 → 缓存未命中,重新组装 # 输出: [assembled] sections: identity, tools, workspace, memory

§4 update_context(context, messages) — 从真实状态推导 context

逐行注解

代码行语法注解
def update_context(context: dict, messages: list) -> dict: 返回 dict;接受旧 context 和消息列表,但当前实现不使用旧 context(保留参数为未来扩展)
memories = "" 本地变量初始化为空串;后续条件赋值,保证返回 dict 中 memories 键始终存在
if MEMORY_INDEX.exists(): Path.exists():检查 .memory/MEMORY.md 文件是否实际存在于磁盘(真实状态,非关键字匹配)
content = MEMORY_INDEX.read_text().strip() Path.read_text():读取文件全文为字符串;.strip() 去除首尾空白
if content: 二次检查:文件存在但为空时不注入记忆(避免传递空字符串给 Claude)
memories = content 仅当文件存在且非空时才赋值;体现"从真实状态推导"设计原则
return { 返回新 dict(不修改传入的 context);函数式风格,易于测试
"enabled_tools": list(TOOL_HANDLERS.keys()), dict.keys() 返回 view 对象,list() 转换为列表;反映当前注册的工具集合
"workspace": str(WORKDIR), str(Path):将 Path 对象转为字符串,确保 json.dumps 可序列化
"memories": memories, 空串 or MEMORY.md 内容;assemble_system_prompt 用此键决定是否追加记忆片段
} dict 字面量结束

真实状态 vs 关键字触发

s09 的 build_system() 通过 read_memory_index() 读取文件决定是否包含记忆片段。

s10 将这个逻辑抽出为 update_context(),把"状态感知"与"prompt 组装"分离:context 是状态的快照,prompt 组装函数是纯函数(同一 context → 同一输出)。

调用位置 # 主循环启动前 context = update_context({}, []) # 每轮工具调用后(工具执行可能创建新记忆文件) context = update_context(context, messages) system = get_system_prompt(context) # 缓存检查

§5 agent_loop(messages, context) — 签名变化与动态刷新

s09 vs s10 对比

代码行(s10 新增/变更)说明
def agent_loop(messages: list, context: dict): s09 只有 messages 参数;s10 增加 context,由主循环传入初始状态
system = get_system_prompt(context) 用缓存包装替代 s09 的 build_system();首次进入必组装
while True: 主循环;不变
response = client.messages.create( API 调用;去掉了 s09 的记忆注入逻辑(context 已包含记忆)
# Re-evaluate context after each tool round 注释说明:工具执行后状态可能变化(如创建了新记忆文件)
context = update_context(context, messages) 每轮工具后刷新 context;返回新 dict,不修改旧值
system = get_system_prompt(context) 缓存检查:context 未变 → 命中,跳过重组装;context 变了 → 重新组装

整体数据流

每轮完整流程 用户输入 → history.append({"role":"user","content":query}) → agent_loop(history, context) → system = get_system_prompt(context) # 缓存检查 → client.messages.create(system=system, …) # LLM 调用 → [工具执行] → context = update_context(context, msgs) # 状态刷新 → system = get_system_prompt(context) # 再次检查 → context = update_context(context, history) # 主循环刷新
s10 简化了什么?
去掉了 s09 的 memory extraction、consolidation、snip/micro compaction,专注于演示 system prompt 组装机制。 这是刻意的教学分层。