写在前面

learn-claude-code 项目目前有两条教程线,一条是之前的 s12 章节的,另一条是最近更新的 s20 章节的,大家如果刚学习这个项目的话推荐直接看最新的 s20 章节的教程即可,由于博主之前学习过 s12 章节的内容,因此打算把 s20 章节中新增章节的内容给补充学习,内容重复的章节博主这边就跳过了。

下面是旧版到新版的对应关系:

Legacy 12-lesson track Current 20-lesson track Topic
old s01 new s01 Agent Loop
old s02 new s02 Tool Use
old s03 new s05 TodoWrite
old s04 new s06 Subagent
old s05 new s07 Skill Loading
old s06 new s08 Context Compact
old s07 new s12 Task System
old s08 new s13 Background Tasks
old s09 new s15 Agent Teams
old s10 new s16 Team Protocols
old s11 new s17 Autonomous Agents
old s12 new s18 Worktree Isolation
new only s03, s04, s09, s10, s11, s14, s19, s20 Permission, Hooks, Memory, System Prompt, Error Recovery, Cron, MCP, Comprehensive Agent

从上表中我们可以看出我们需要补充的内容包括 s03(已补充)s04(已补充)s09(已补充)、s10、s11、s14、s19 以及 s20 八个章节的内容。

新版学习路径如下:

主线:能动手 → 能做复杂任务 → 能记住和恢复 → 能长期运行 → 能协作 → 能扩展并合体

在这里插入图片描述

前言

在上篇文章 Learn-Claude-Code | 笔记 | Memory Management | s09_new Memory 中,我们介绍了开源项目 learn-claude-code 新版第九个章节 s09_new: Memory 的内容,这篇文章我们继续跟着教程文档来学习规划与拆解相关内容,记录下个人学习笔记,和大家一起分享交流😄

Note:本篇文章主要学习记录 新版教程 第二部分 Planning & Coordination 中 s10: System Prompt 章节的内容。

githubhttps://github.com/shareAI-lab/learn-claude-code

referencehttps://chatgpt.com/

1. s10: System Prompt

前面从 s01 到 s09,我们已经把一个 Coding Agent 的基础能力逐步搭起来了:s01 讲 Agent Loop,s02 讲 Tool Use,s03 加入 Permission,s04 加入 Hooks,s08 引入 Context Compact,s09 又进一步加入 Memory。到这里为止,Agent 已经不再是一个只能简单调用工具的小脚本,而是开始具备 “长期记忆、上下文压缩、工具执行、运行时扩展” 的完整雏形。

但是随着能力越来越多,一个新的工程问题开始出现:system prompt 不能再继续用一整段硬编码字符串来维护了

在早期章节中,system prompt 很简单,例如:

SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks."

这在 s01、s02 里完全够用,因为当时 Agent 只有最基础的身份说明和工具说明。但到了 s09 之后,Agent 已经有了 memory、compression、skill loading、permission、hooks 等能力。如果仍然把所有内容都写进一个巨大的 SYSTEM 字符串里,后续每增加一个能力,就要不断往里面追加说明。久而久之,这个 prompt 会变成一个难以维护的大杂烩。

s10 要解决的正是这个问题:system prompt 应该是运行时根据当前状态组装出来的,而不是提前写死的一整段固定文本

2. 问题

s10 的核心问题可以概括为一句话:

prompt 是组装出来的,不是写死的。

在真实 Agent 系统里,system prompt 本质上不是一段静态说明,而是当前运行环境的 “配置快照”。它应该反映当前 Agent 到底有哪些工具、当前工作目录在哪里、有没有可用记忆、有没有加载某些技能、是否处在某种特殊模式中。

如果继续使用硬编码 SYSTEM 字符串,会带来三个明显问题。

第一个问题是 可维护性差。当工具、记忆、技能、权限、团队协议越来越多时,所有规则都挤在一个字符串里。想修改工具描述时,可能不小心影响身份说明;想增加 memory 规则时,又可能和已有上下文规则冲突。prompt 变得越来越难拆、难查、难调试。

第二个问题是 不够动态。同一个 Agent 在不同项目、不同目录、不同工具配置、不同记忆状态下,system prompt 应该不同。比如当前没有 .memory/MEMORY.md 时,就没必要告诉模型 “这里有相关记忆”;当前没有启用某个工具时,也不应该在 prompt 里描述这个工具。硬编码 prompt 无法很好反映真实运行态。

第三个问题是 浪费 token。system prompt 每轮都会发送给模型,如果每次都把所有能力说明全部塞进去,即使当前请求根本用不到,也会造成上下文浪费。更重要的是,过多无关说明也会让模型注意力分散,降低当前任务的执行稳定性。

因此,s10 不再把 prompt 当成 “一段固定文字”,而是把它拆成多个可独立维护、可按需加载、可缓存复用的 section。

3. 解决方案

教程文档中的 system-prompt-overview.svg 正是这一节的核心架构图:

在这里插入图片描述

这张图可以看成 s09 Memory 之后的一次 “prompt 工程升级”。图中蓝色部分表示 s09 已经保留的能力:messages[] 经过压缩和 memory loading 后送入 LLM,LLM 再决定是否调用 TOOL_HANDLERS,工具结果继续追加回 messages[]。也就是说,原来的 Agent Loop、压缩管线、记忆加载、工具执行逻辑都还在。

s10 新增的是上方绿色部分:PROMPT_SECTIONSassemble_system_promptget_system_prompt

PROMPT_SECTIONS 相当于一个 section 货架,把 system prompt 拆成多个有名字的段落。比如 identity 负责说明 Agent 是谁,tools 负责说明有哪些工具,workspace 负责说明当前工作目录,memory 负责说明是否存在相关记忆。每个 section 都是独立的,某个 section 的变化不会影响其他 section。

assemble_system_prompt 负责根据当前 context 选择哪些 section 应该被拼接进去。这里的关键是 “按需”,比如 identitytoolsworkspace 是始终加载的,因为它们是 Agent 每轮都需要的基础信息;而 memory 只有在当前确实存在记忆内容时才加载。

get_system_prompt 则负责缓存。它会先把当前 contextjson.dumps(context, sort_keys=True) 序列化成一个确定性的 key。如果当前 key 和上一轮一致,说明运行状态没有变化,就直接复用上一次组装好的 system prompt;如果 key 变化,才重新调用 assemble_system_prompt 组装。

所以 s10 的整体设计可以概括为:

PROMPT_SECTIONS 拆 prompt,用 assemble_system_prompt(context) 按真实状态拼 prompt,用 get_system_prompt(context) 缓存 prompt。

这让 system prompt 从一段写死的字符串,变成了一个可以被运行时状态驱动的配置产物。

4. Runtime Prompt Assembly 流程图分析

Web 教程中的 Runtime Prompt Assembly 用 6 张图把 s10 的运行过程讲得非常清楚。它不是从代码角度出发,而是从 “系统如何一步步把运行态变成 prompt” 的角度出发。

1. Runtime State Arrives:运行态先到达

在这里插入图片描述

第一张图展示的是初始状态。左侧是 Runtime context,里面包含 workspace、tools、memory、skills 等运行时信息。中间是 Section shelf + cache,也就是 section 货架和缓存。右侧是最终的 System prompt,但此时还没有构建出来。

这张图想表达的是:system prompt 的来源不是一段静态文本,而是当前 runtime context。当前工作目录是什么、启用了哪些工具、memory 是否可用、skills 是否加载,这些都属于运行态信息。

换句话说,prompt 不是先验固定的,而是从当前 Agent 所处环境中 “长出来” 的。

2. Section Shelf Selects Owners:每个子系统拥有自己的 section

在这里插入图片描述

第二张图展示了 section shelf 的作用。每个 section 都有自己的 owner:identity 属于 core,tools 属于 tool registry,workspace 属于 runtime,memory + skills 属于 context loader。

这点非常重要。它说明 system prompt 不应该由一个地方集中硬写,而应该由不同子系统分别贡献自己的 prompt section。

比如工具系统最清楚当前有哪些工具,因此 tools section 应该由工具注册表负责。memory 系统最清楚当前有哪些可用记忆,因此 memory section 应该由 memory/context loader 负责。workspace 是运行时环境信息,因此 workspace section 应该由 runtime 负责。

这样做的好处是调试边界清晰。如果模型对工具理解错误,我们就去看 tools section;如果模型没有遵守记忆偏好,我们就看 memory section;如果工作目录不对,就看 workspace section。每个规则都有归属,不会全部混在一段 prompt 里。

3. Context Key Checks the Cache:用 context key 检查缓存

在这里插入图片描述

第三张图对应代码中的 get_system_prompt(context)

中间的 context key 使用类似:

json.dumps(context, sort_keys=True)

这样的方式,把当前 runtime state 转成一个稳定的 cache key。只要 runtime state 没变,这个 key 就不会变,system prompt 就可以复用。

图中此时显示的是 cache miss: assemble sections,说明当前上下文还没有对应的缓存结果,需要重新组装 prompt。

这一张图强调的是:缓存不是拍脑袋做的,而是基于确定性的上下文状态。相同状态得到相同 key,相同 key 复用相同 prompt。这样 prompt 既具备动态性,又具备可复现性。

4. Prompt Is Assembled:section 被拼成最终 prompt

在这里插入图片描述

第四张图展示了 prompt 组装完成后的状态。

中间的 section shelf 里,identitytoolsworkspacememory + skills 都已经被选中并标记为可用。右侧的 System prompt 则显示出最终拼接出来的内容:

[identity]
You are a helpful coding agent.

[tools]
Available tools: bash, read_file.

[workspace]
Current workspace: /repo.

[memory + skills]
Load memory index and code-review skill.

这说明最终送给 LLM 的 system prompt 是由多个 section 组合出来的,而不是某个地方提前写死的一整段字符串。

这种结构化组装有两个好处。第一,它让 prompt 更容易审计,我们可以清楚看到每一段来自哪里。第二,它让后续扩展更自然,新增一个能力只需要新增一个 section,不需要重写整个 system prompt。

5. Same Key Reuses the Prompt:上下文不变就复用 prompt

在这里插入图片描述

如果 runtime context 没有变化,那么 context key 也不会变化。此时就不需要重新 assemble,系统直接复用上一次的 system prompt。图中显示:

cache hit: reuse prompt

这对应代码中的:

if key == _last_context_key and _last_prompt:
    print("[cache hit] system prompt unchanged")
    return _last_prompt

这一步虽然在教学代码里只是一个简单的字符串缓存,但它引出了真实 Agent 系统里非常重要的思想:prompt 的稳定性会直接影响缓存命中率。

在 Claude Code 这样的真实系统中,prompt cache 不只是为了少拼一次字符串,而是会影响 API 层的缓存命中、延迟和 token 成本。因此 prompt section 的顺序、静态/动态边界、动态内容变化范围,都会成为工程设计的一部分。

6. LLM Sees the Built Prompt:模型看到的是运行时产物

在这里插入图片描述

最后一张图展示的是最终结果:LLM 接收到的不是一段旧式硬编码 prompt,而是由 runtime state 组装出来的 system prompt。

这张图的重点在右侧:sent to LLM。也就是说,前面的 runtime context、section shelf、cache key、assembled prompt,最终都服务于一件事:在调用模型时,把一个可追踪、可解释、可缓存的 system prompt 交给 LLM。

这也解释了为什么 s10 很重要。模型本身并不知道当前工具、目录、记忆和技能状态,必须由 harness 层把这些状态整理成 prompt 交给它。s10 做的就是把这件事工程化。

完整动画演示如下图所示:

在这里插入图片描述

5. 工作原理(代码分析)

OK,下面我们就来看看代码具体是如何实现的:

1. PROMPT_SECTIONS:把一整段 prompt 拆成可维护的 section

s10 首先定义了一个 PROMPT_SECTIONS 字典:

PROMPT_SECTIONS = {
    "identity": "You are a coding agent. Act, don't explain.",
    "tools": "Available tools: bash, read_file, write_file.",
    "workspace": f"Working directory: {WORKDIR}",
    "memory": "Relevant memories are injected below when available.",
}

这一步看起来只是把字符串拆开,但它背后的工程意义很重要。

在旧版本里,system prompt 是一个整体。只要你要改 prompt,就必须去改同一大段字符串。随着能力增加,这种写法会越来越脆弱,因为不同能力之间没有边界。工具说明、身份说明、记忆说明、工作目录说明都混在一起,后续调试时很难知道到底是哪一段影响了模型行为。

而在 s10 中,每个 section 都有明确的名字和职责:

  • identity 表示 Agent 的身份和行为风格,它告诉模型自己是一个 coding agent,并且应该执行任务而不是长篇解释。
  • tools 表示当前可用工具,它告诉模型可以使用 bashread_filewrite_file
  • workspace 表示当前工作目录,它给模型一个明确的操作边界。
  • memory 表示记忆相关提示,它只有在存在记忆内容时才真正参与组装。

这种拆法让 prompt 变得像代码模块一样可以维护。后面如果新增 skill、MCP、团队协议、错误恢复策略,都可以继续增加新的 section,而不是把所有规则继续塞进一个大字符串。

2. assemble_system_prompt:根据真实状态按需拼接

接下来是 assemble_system_prompt

def assemble_system_prompt(context: dict) -> str:
    """Select and join prompt sections based on current context."""
    sections = []

    # Always loaded — identity, tools, workspace
    sections.append(PROMPT_SECTIONS["identity"])
    sections.append(PROMPT_SECTIONS["tools"])
    sections.append(PROMPT_SECTIONS["workspace"])

    # Conditional — memory loaded when MEMORY.md exists and has content
    memories = context.get("memories", "")
    if memories:
        sections.append(f"Relevant memories:\n{memories}")

    return "\n\n".join(sections)

这个函数的作用是 “选 section,然后拼成最终 system prompt”。

这里最关键的点是:section 是否加载不是根据用户问题里的关键词判断,而是根据 context 中的真实运行状态判断

例如用户没有说 “memory” 这个词,但如果 .memory/MEMORY.md 存在且有内容,那么 memory section 就应该加载。反过来,即使用户提到了 “记忆”,如果当前系统里没有真实记忆内容,也不应该硬塞一个空的 memory section。

这和 s09 的设计是一脉相承的:Memory 是从 .memory/MEMORY.md 这样的真实文件系统状态中加载出来的,而不是靠模型自己猜测。s10 把这种思想推广到了 system prompt 组装上:prompt 不再由静态文本决定,而是由当前运行态决定。

在这个教学版本中,始终加载的 section 有三个:identitytoolsworkspace。这三个是 Agent 每轮必须知道的基础信息。条件加载的 section 是 memory,只有当 context["memories"] 非空时才加入。

最后用 "\n\n".join(sections) 拼接,保证不同 section 之间有清晰分隔,方便阅读和调试。

3. get_system_prompt:用 context key 做缓存

s10 的第二个关键函数是 get_system_prompt

_last_context_key = None
_last_prompt = None


def get_system_prompt(context: dict) -> str:
    global _last_context_key, _last_prompt
    key = json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)
    if key == _last_context_key and _last_prompt:
        print("  \033[90m[cache hit] system prompt unchanged\033[0m")
        return _last_prompt
    _last_context_key = key
    _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")
    return _last_prompt

这个函数解决的是 “重复组装” 的问题。

如果当前 context 没有变化,那么最终拼出来的 system prompt 也不会变化。此时没有必要每轮都重新拼接字符串。因此代码用 _last_context_key_last_prompt 保存上一次的上下文 key 和 prompt 结果。

这里有一个细节:代码中使用的是:

json.dumps(context, sort_keys=True, ensure_ascii=False, default=str)

而不是 Python 内置的 hash()

原因是 json.dumps(..., sort_keys=True) 可以把 dict 转换成稳定、确定性的字符串。只要 context 内容相同,生成的 key 就相同。而 Python 的 hash() 不适合这里:一方面 Python 的 hash 有进程随机化,另一方面 dict/list 这类嵌套结构本身也不能直接 hash。

当 key 命中时,代码打印:

[cache hit] system prompt unchanged

这说明当前 system prompt 没有重新组装,而是直接复用了缓存结果。

当 key 不命中时,代码重新调用 assemble_system_prompt(context),并打印当前加载了哪些 section:

[assembled] sections: identity, tools, workspace

如果 memory 存在,则会变成:

[assembled] sections: identity, tools, workspace, memory

这也让 prompt 组装过程变得可观测。我们不再只能猜测模型看到了什么,而是可以通过日志明确知道哪些 section 被送进了 system prompt。

需要注意的是,教学版里的 cache 只是 Python 进程内的字符串缓存,它只是避免重复拼接 prompt。它和 Claude Code 真正的 API prompt cache 不是一回事。Claude Code 源码里还有更复杂的静态/动态 section 边界、global cache scope、section cache 等机制。教学版先把最核心的思想讲清楚:运行时状态不变,prompt 就可以复用;运行时状态变化,prompt 就重新组装

4. update_context:用真实状态生成 prompt 输入

接下来是 update_context

def update_context(context: dict, messages: list) -> dict:
    """Derive context from real state: which tools exist, whether memory files exist."""
    memories = ""
    if MEMORY_INDEX.exists():
        content = MEMORY_INDEX.read_text().strip()
        if content:
            memories = content
    return {
        "enabled_tools": list(TOOL_HANDLERS.keys()),
        "workspace": str(WORKDIR),
        "memories": memories,
    }

这个函数负责把当前真实环境整理成 context

它做了三件事。

第一,它读取当前实际注册的工具:

"enabled_tools": list(TOOL_HANDLERS.keys())

也就是说,prompt 中描述的工具应该来自真实工具注册表,而不是手写猜测。

第二,它记录当前工作目录:

"workspace": str(WORKDIR)

这样 system prompt 可以知道当前操作边界。

第三,它检查 .memory/MEMORY.md 是否存在:

if MEMORY_INDEX.exists():
    content = MEMORY_INDEX.read_text().strip()

如果存在且有内容,就把它放入 context["memories"]。如果不存在,memory 就是空字符串,后续 assemble_system_prompt 就不会加载 memory section。

这一步体现了 s10 的核心工程原则:prompt 由真实状态驱动,而不是由自然语言关键词驱动

5. agent_loop:主循环不再使用硬编码 SYSTEM

最后看 agent_loop

def agent_loop(messages: list, context: dict):
    """Main loop — uses assembled system prompt instead of hardcoded SYSTEM."""
    system = get_system_prompt(context)
    while True:
        response = client.messages.create(
            model=MODEL, system=system, messages=messages,
            tools=TOOLS, max_tokens=8000)
        messages.append({"role": "assistant", "content": response.content})
        if response.stop_reason != "tool_use":
            return

        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            print(f"\033[36m> {block.name}\033[0m")
            handler = TOOL_HANDLERS.get(block.name)
            output = handler(**block.input) if handler else f"Unknown: {block.name}"
            print(str(output)[:200])
            results.append({"type": "tool_result",
                            "tool_use_id": block.id, "content": output})
        messages.append({"role": "user", "content": results})

        # Re-evaluate context and prompt after each tool round
        context = update_context(context, messages)
        system = get_system_prompt(context)

这里最重要的变化是:

system = get_system_prompt(context)

以前是直接把硬编码的 SYSTEM 传给模型。现在是每轮根据当前 context 获取 system prompt。

执行流程变成:

1. 根据当前 context 获取 system prompt;

2. 调用 LLM;

3. 如果模型请求工具,就执行工具;

4. 工具结果追加回 messages;

5. 工具执行后重新 update_context;

6. 根据新的 context 再次 get_system_prompt;

7. 继续下一轮。

这意味着如果某个工具调用改变了环境状态,例如创建了 .memory/MEMORY.md,那么下一轮 update_context 就会发现 memory 文件已经存在,get_system_prompt 就会重新组装 prompt,并把 memory section 加进去。

这也是 s10 和 s09 的连接点:s09 解决 “记忆如何存储、加载、提取、整理”,s10 解决 “这些运行时状态如何进入 system prompt”。

博主在给定下面的提示词情况下:

Read the file README.md

Note:观察始终加载的三个 section。

想通过调试看看整个过程发生了什么,我们来具体分析下:

Prompt 1:Loop 1

在这里插入图片描述

Prompt 1:第一次循环获取系统提示词

在这里插入图片描述

Prompt 1:第一次循环模型响应

在这里插入图片描述

Prompt 1:第一次循环工具执行结果

在这里插入图片描述

Prompt 1:第一次循环更新上下文

从第一轮调试可以看到,agent_loop() 一开始并没有继续使用过去那种固定写死的 SYSTEM 字符串,而是先调用:

system = get_system_prompt(context)

这里的 context 当前包含三个核心字段:enabled_toolsworkspacememories。由于此时还没有额外的 memory 内容,因此最终组装出来的 system prompt 只包含三个始终存在的 section:identitytoolsworkspace

也就是说,s10 的关键变化不是改变 Agent Loop 的基本形态,而是把原来散落在代码里的系统提示词,抽象成了一个可组装、可追踪、可缓存的运行时产物。此时终端中打印出了:

[assembled] sections: identity, tools, workspace

这说明当前 system prompt 是由这三个 section 动态拼接出来的,而不是一段固定文本。

随后模型收到用户请求 Read the file README.md,在第一轮响应中返回了两个 block:一个是 ThinkingBlock,表示模型内部判断用户希望读取 README.md;另一个是 ToolUseBlock,表示模型选择调用 read_file 工具读取文件。这一点和前面章节的 Agent Loop 逻辑保持一致:模型仍然负责 “决定要不要用工具”,工具执行权仍然在 harness 层。

工具执行完成后,read_file 返回 README.md 的文件内容,并被封装成 tool_result 追加回 messages。这一步之后,s10 额外做了一件重要的事:调用 update_context(context, messages),并重新执行:

system = get_system_prompt(context)

这表示每一轮工具执行结束后,系统都会重新评估当前运行时状态是否发生变化。如果 workspace、工具列表、memory index 等上下文没有变化,那么 system prompt 可以继续复用缓存;如果上下文发生变化,则需要重新组装。

因此,第一轮 Loop 的完整链路可以概括为:用户输入 → get_system_prompt(context) → 组装 identity/tools/workspace → LLM 选择 read_file → 工具执行并返回 README.md → tool_result 追加到 messages → update_context() → 再次检查 system prompt 是否需要更新。

这一轮调试说明,s10 的 system prompt 已经从 “静态配置” 变成了 “运行时状态的投影”。模型看到的不是硬编码字符串,而是由当前工具、工作目录、记忆状态等信息动态生成的执行说明。

Prompt 1:Loop 2

在这里插入图片描述

Prompt 1:第二次循环获取系统提示词

在这里插入图片描述

Prompt 1:第二次循环模型响应

第二轮 Loop 开始时,代码再次调用:

system = get_system_prompt(context)

从调试结果可以看到,这一轮生成的 system prompt 仍然只包含:identitytoolsworkspace,也就是说,在第一轮读取 README.md 之后,虽然 messages 中已经多了工具调用结果,但 context 本身并没有出现新的 memory section,也没有修改工具列表或 workspace 信息,因此 system prompt 的结构没有发生变化。

模型在第二轮响应中不再继续发起工具调用,而是根据上一轮 read_file 返回的内容,直接生成 README.md 的总结。此时 response.stop_reason != "tool_use",Agent Loop 结束。

OK,本轮对话结束后我们继续使用下面的提示词写入记忆索引:

Create a file called .memory/MEMORY.md with content "- [test](test.md) — test memory"

Prompt 2

调用工具写入文件这些我们就跳过分析了,我们直接来看整个过程完成中终端的输出:

在这里插入图片描述

Prompt 2:终端完整输出

Prompt 2 的目标是创建 .memory/MEMORY.md 文件:

Create a file called .memory/MEMORY.md with content "- [test](test.md) — test memory"

这一步本质上是在模拟 s09 Memory 中的记忆索引文件。前面 Prompt 1 中,context["memories"] 为空,所以 system prompt 只包含 identitytoolsworkspace 三个 section。而当 .memory/MEMORY.md 被写入后,系统的运行时状态发生了变化:memory index 从 “空” 变成了 “存在一条 test memory”。

从终端完整输出可以看到,在写入 .memory/MEMORY.md 之前,多次提示:

[cache hit] system prompt unchanged

这说明在创建文件的工具调用过程中,system prompt 仍然复用了旧缓存。但是当工具执行完成、update_context() 重新扫描上下文后,系统发现 memory index 已经变化,于是重新组装 system prompt,并打印:

[assembled] sections: identity, tools, workspace, memory

这就是 s10 最核心的现象:system prompt 的 section 不是固定存在的,而是根据运行时 context 按需拼接。只要 .memory/MEMORY.md 中出现了可用的记忆索引,memory section 就会被加入 system prompt。

因此,Prompt 2 的意义不是“写文件”本身,而是验证 system prompt 可以随着运行时状态变化而变化。新增 memory index 之后,系统提示词从:identity + tools + workspace 升级为 identity + tools + workspace + memory,这正好对应教程图中的主线:s09 负责让 memory 存在,s10 负责把 memory 作为一个 section 拼进 system prompt。

OK,本轮对话结束后我们继续使用下面的提示词观察下 memory section 是否出现:

Read the file code.py

Prompt 3

在这里插入图片描述

Prompt 3:第一次循环获取系统提示词

Prompt 3 再次请求读取文件:

Read the file code.py

这一次进入 agent_loop() 后,调试结果显示 get_system_prompt(context) 生成的 system prompt 中已经出现了 memory section:

Relevant memories:
- [test](test.md)test memory

这说明 Prompt 2 写入的 .memory/MEMORY.md 已经被 update_context() 捕获,并在下一轮用户请求中进入 system prompt。换句话说,memory 不只是一个普通文件,而是被运行时上下文识别后,成为模型输入规则的一部分。

这次调试最重要的结论是:s10 没有改变 Agent Loop 的执行骨架,而是把 system prompt 从硬编码文本升级成了运行时组装机制

在 Prompt 1 中,我们看到当 memory 为空时,系统只组装 identitytoolsworkspace 三个基础 section。模型依然按照原来的 loop 方式选择工具、执行工具、接收工具结果,并在第二轮生成最终回答。

在 Prompt 2 中,我们通过写入 .memory/MEMORY.md 改变了运行时上下文。此时 system prompt 的缓存失效,系统重新组装提示词,并新增了 memory section。这说明 system prompt 的内容不是固定的,而是由当前 context 决定的。

在 Prompt 3 中,我们进一步验证了 memory section 已经真正进入 system prompt。模型后续请求中看到的不只是用户当前输入,还包括 harness 根据 .memory/MEMORY.md 拼接出来的长期记忆索引。

所以,s10 的工程价值可以概括为一句话:让 system prompt 从 “写死的一段字符串”,变成 “由多个可维护 section 按运行时状态动态拼接出来的产品”

OK,以上就是 s10 System Prompt 工作原理的完整分析了。

那大家感兴趣的话可以试试下面这些 prompt 感受下 System Prompt 动态拼接后的一些变化:

1. Read the file README.md(观察始终加载的三个 section)

2. Create a file called .memory/MEMORY.md with content "- [test](test.md) — test memory"(写入记忆索引)

3. Read the file code.py(观察 memory section 是否出现)

6. 相对 s09 的变更

组件 之前 (s09) 之后 (s10)
prompt 硬编码 SYSTEM 字符串 PROMPT_SECTIONS + assemble_system_prompt
缓存 get_system_prompt(json.dumps 检测 + 缓存)
新函数 assemble_system_prompt, get_system_prompt, update_context
工具 bash, read_file, write_file (3) bash, read_file, write_file (3) — 不变
循环 用固定 SYSTEM 用 get_system_prompt(context)

7. 小结

s10 这一节表面上讲的是 system prompt,实际上讲的是一个更通用的 Agent Harness 设计原则:不要把运行时配置写死在 prompt 里,而要让 prompt 成为当前系统状态的组装结果

在早期 Agent 中,硬编码 system prompt 足够简单直接。但当 Agent 开始拥有工具、权限、Hooks、记忆、技能、压缩、任务系统之后,prompt 就不能再被当成一段普通字符串维护。它必须被拆成 section,由不同子系统分别贡献内容,再由 harness 根据当前真实状态按需拼接。

这样一来,prompt 的职责边界更清楚,调试路径更明确,扩展能力也更强。新增一个能力,不再是往大字符串里硬塞一句话,而是新增一个有 owner、有加载条件、有缓存边界的 section。

所以 s10 的关键收获可以总结为:System Prompt 不是静态说明书,而是 Agent 运行态的结构化投影。Harness 负责收集真实状态、选择 prompt section、组装 system prompt,并在状态不变时复用缓存。这一步让 Agent 从 “写死规则” 走向 “运行时配置”,也为后续 Error Recovery、MCP、Comprehensive Agent 等更复杂能力打下了基础。

OK,以上就是本期想要分享的全部内容了。

结语

本篇文章我们围绕 s10 System Prompt 这一节,完整梳理了 Agent 是如何将 system prompt 从 “写死的一段字符串”,升级为可动态组装、可缓存、可复用的运行时产物。

相比 s09 Memory,s10 的核心变化不在于新增功能,而在于将运行时状态与模型输入明确分离:不同子系统(identity、tools、workspace、memory 等)各自贡献 prompt section,Harness 根据当前 context 按需组装,并在 context 未变化时复用缓存。这一设计让 prompt 变得可维护、可扩展,同时避免了无关 token 消耗和模型注意力分散。

从工程视角来看,这一步是对 Agent Loop 的重要优化:主循环无需关注 prompt 内部细节,权限、工具、工作目录、记忆状态的变化都通过 context 驱动 prompt 自动更新。这样,Agent 不再依赖硬编码规则,而是通过运行时状态生成输入,使模型始终看到的是反映真实环境的系统快照。

更进一步,s10 的设计体现了一个通用原则:系统越复杂,运行时配置越不应该硬编码;应通过结构化、按需加载和缓存机制管理信息流。这不仅提升了 prompt 的可调试性,也为后续 Error Recovery、MCP 和 Comprehensive Agent 等高级能力的落地奠定了基础。

可以说,s10 完成了从 “静态规则” 到 “动态运行态投影” 的关键跃迁,为整个 Agent 系统提供了可持续扩展和长期运行的底层架构能力。

下篇文章我们将来学习新版教程 s11 Error Recovery 章节的内容,敬请期待🤗

参考

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐