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 层)的教学简化版本。
PROMPT_SECTIONS — 片段字典assemble_system_prompt(context) — 按 context 选片段get_system_prompt(context) — 缓存包装update_context(context, messages) — 从真实状态推导 contextagent_loop 签名增加 context 参数,每轮工具调用后刷新| 代码行 | 语法注解 |
|---|---|
| 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+ 保证插入顺序 |
片段粒度决定缓存效果:粒度太细 → 拼接频繁;太粗 → 动态内容污染缓存块。 s10 的平衡点是:3 个静态块 + 1 个动态记忆块。
| 代码行 | 语法注解 |
|---|---|
| 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 清晰分段 |
| 代码行 | 语法注解 |
|---|---|
| _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 |
hash() 在 Python 3.3+ 引入了进程级随机化(PYTHONHASHSEED),同一 dict 在不同进程中哈希值不同,且 dict/list 本身不可哈希。
json.dumps(..., sort_keys=True) 产生确定性字符串,跨进程一致,且嵌套结构均可序列化。
| 代码行 | 语法注解 |
|---|---|
| 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 字面量结束 |
s09 的 build_system() 通过 read_memory_index() 读取文件决定是否包含记忆片段。
s10 将这个逻辑抽出为 update_context(),把"状态感知"与"prompt 组装"分离:context 是状态的快照,prompt 组装函数是纯函数(同一 context → 同一输出)。
| 代码行(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 变了 → 重新组装 |