Files
gui-yue 1baf1aca5a Follow up PR #265: refine chapters, diagrams, and add S20 (#283)
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience

Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building
incrementally on the previous. Key fixes across chapters:

- s01-s04: agent loop, tool dispatch, permission pipeline, hooks
- s05-s08: todo write, subagent, skill loading, context compact
- s09-s11: memory system, system prompt assembly, error recovery
- s12-s14: task graph, background tasks, cron scheduler

All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS,
json.dumps cache, real-state context, can_start dep protection, etc.).

* feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools

Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform
chapters. Each chapter inherits all previous fixes and adds one mechanism:

- s15: agent teams (TeamCreate, teammate threads, shared task list)
- s16: team protocols (plan approval, shutdown handshake, consume_inbox)
- s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox)
- s18: worktree isolation (git worktree, bind_task, cwd switching, safety)
- s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache)

All appendix source code references verified against CC source. Config priority
corrected: claude.ai < plugin < user < project < local.

* fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash

- s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02)
- s06-s08: todo_write validates content/status required fields (inherited from s05)
- s09: extract_memories uses pre-compression snapshot instead of compacted messages
- s16: submit_plan docstring clarifies protocol-only (not code-level gate)
- s17-s19: match_response restores type mismatch validation (from s16)
- s17-s19: claim_task deps list handles missing dep files without crashing

* fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation

- s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task,
  non-interactive/SDK defaults to TodoWrite. Fix env var name to
  CLAUDE_CODE_ENABLE_TASKS (not TODO_V2).
- s14/s15: add _validate_cron_field with per-field range checks (minute 0-59,
  hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi.
  Replace old try/except validation that only caught exceptions.
- s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree,
  not just create_worktree.

* fix: align s16-s19 teaching tool consistency

* fix pr265 chapter diagrams

* Add comprehensive s20 harness chapter

* Fix chapter smoke test regressions

* Clarify README tutorial track transition

---------

Co-authored-by: Haoran <bill-billion@outlook.com>
2026-05-20 21:45:38 +08:00

10 KiB
Raw Permalink Blame History

s10: System Prompt — 运行时组装,不硬编码

中文 · English · 日本語

s01 → ... → s08 → s09 → s10s11 → s12 → ... → s20

"prompt 是组装出来的, 不是写死的" — 分段 + 按需拼接 + 缓存。

Harness 层: 提示 — 运行时组装, 不硬编码。


问题

从 s01 到 s09system prompt 都是一行硬编码:

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

s01 够用,只有 bash、read、write 三个工具。但到 s09Agent 已经有记忆、有压缩、有技能加载。prompt 该提的能力越来越多:

SYSTEM = (
    f"You are a coding agent at {WORKDIR}. "
    "Use tools to solve tasks. Act, don't explain. "
    "Before starting any multi-step task, use todo_write. "
    "Skills are available via list_skills and load_skill. "
    "Relevant memories are injected below when available. "
    # ... 加一个能力就多一段
)

三个问题:

  1. 换项目要重写整个 prompt,不知道哪些该改、哪些该留
  2. 修改一处可能影响全局,加一段工具描述可能跟前面的指令冲突
  3. 每次请求都带全部内容,即使当前对话用不到某些段落也浪费 token

System prompt 应该是运行时根据当前状态组装的配置:哪些工具启用、哪些上下文可见、哪些记忆相关、哪些内容必须保持稳定以命中 prompt cache。


解决方案

System Prompt Overview

s10 聚焦 prompt 组装机制。以 s08-s09 的能力为背景,但不重复实现压缩和记忆系统。核心变动:把硬编码的 SYSTEM 拆成独立段落section运行时根据真实状态按需拼接缓存结果避免重复组装。

四个 section两种加载策略

Section 加载策略 内容 判断依据
identity 始终 你是谁、怎么做事 始终存在
tools 始终 可用工具列表 enabled_tools
workspace 始终 工作目录 始终存在
memory 按需 相关记忆内容 .memory/MEMORY.md 是否存在

关键设计section 是否加载取决于真实状态(工具是否存在、文件是否存在),不是消息里的关键词。


工作原理

PROMPT_SECTIONS: 分段定义

把一大段字符串拆成字典,每个 key 是一个主题:

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.",
}

每个 section 独立维护。修改 tools 不影响 identity,新增 memory 不动 workspace

assemble_system_prompt: 按需拼接

不是所有 section 每次都需要。当前没有记忆文件,加载 memory section 只是浪费 token。根据 context 的真实状态决定加载哪些:

def assemble_system_prompt(context: dict) -> str:
    sections = []

    # 始终加载
    sections.append(PROMPT_SECTIONS["identity"])
    sections.append(PROMPT_SECTIONS["tools"])
    sections.append(PROMPT_SECTIONS["workspace"])

    # 按需加载 — 基于真实状态,不是关键词
    memories = context.get("memories", "")
    if memories:
        sections.append(f"Relevant memories:\n{memories}")

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

"始终加载"的是每轮都需要的:身份、工具、工作目录。"按需加载"的只在特定条件下才有用。

为什么不全加载token 有成本system prompt 每轮计费),信息越少 LLM 越专注(无关指令是噪音)。

get_system_prompt: 缓存避免重复拼接

上下文没变时(同一轮对话的多次 LLM 调用context 相同),重新拼接是浪费。用确定性序列化检测变化,命中缓存直接返回:

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:
        return _last_prompt
    _last_context_key = key
    _last_prompt = assemble_system_prompt(context)
    return _last_prompt

json.dumps 而不是 hash()Python 内置 hash() 有进程随机化,不适合做稳定 cache key而且遇到 list/dict 会报 unhashable type

注意:这里的缓存只是"避免重复拼接字符串",和 CC 的 API prompt cache 不是一回事。CC 的 prompt cache 通过 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 分隔静态和动态部分,静态部分命中 global cache不因动态内容变化而失效。

context: 真实状态,不是关键词猜测

context 反映当前运行态的真实状态:

def update_context(context: dict, messages: list) -> dict:
    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,
    }

enabled_tools 列出实际注册的工具。memories 检查 .memory/MEMORY.md 是否存在。section 加载基于这些真实状态,不在消息里搜关键词。

合起来跑

def agent_loop(messages: list, context: dict):
    system = get_system_prompt(context)
    while True:
        response = client.messages.create(
            model=MODEL, system=system, messages=messages,
            tools=TOOLS, max_tokens=8000)
        # ... 工具执行 ...
        context = update_context(context, messages)
        system = get_system_prompt(context)

每轮循环开头拿一次 system prompt。context 变了就重新组装,没变就返回缓存。


相对 s09 的变更

组件 之前 (s09) 之后 (s10)
prompt 硬编码 SYSTEM 字符串 PROMPT_SECTIONS + assemble_system_prompt
缓存 get_system_promptjson.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)

试一下

cd learn-claude-code
python s10_system_prompt/code.py

观察重点:

  1. 输出中能看到哪些 section 被加载了([assembled] sections: ... 标签)
  2. 连续对话时,缓存命中显示 [cache hit]
  3. 创建 .memory/MEMORY.md 文件后,下一轮 memory section 自动加载

试试这些 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 是否出现)

接下来

System prompt 可以运行时组装了,但 Agent 碰到错误还是会崩。网络抖动、API 限流、输出被截断、上下文超限,这些不是 bug是常态。

s11 Error Recovery → 四条恢复路径。升级 token、压缩上下文、指数退避、切换模型。

深入 CC 源码

以下基于 CC 源码 constants/prompts.ts914 行)、constants/systemPromptSections.ts68 行)、context.ts189 行)、utils/api.ts718 行)、utils/systemPrompt.ts123 行)、bootstrap/state.ts 的分析。

CC 的 system prompt 有多少 section

数量不固定,受 feature flag、output style、KAIROS/Proactive 模式、用户类型、token 预算等影响。大致分两类:

静态 section始终加载identity、system、doing_tasks、actions、using_tools、tone_style、output_efficiency 等。

动态 section按状态加载session_guidance、memory、ant_model_override、env_info_simple、language、output_style、mcp_instructions、scratchpad、frc、summarize_tool_results、numeric_length_anchors、token_budget、brief 等。

mcp_instructions 是唯一的易失性 section通过 DANGEROUS_uncachedSystemPromptSection() 创建),因为 MCP server 可以在轮次间连接和断开。

组装函数

getSystemPrompt(tools, model, additionalWorkingDirs?, mcpClients?): Promise<string[]>

返回 string[](每个元素是一个 sectionSYSTEM_PROMPT_DYNAMIC_BOUNDARY 分隔静态和动态部分。

cache scope

启用 global cache boundary 时,静态 section 合并成一个 global cache block动态 section 不使用 global cachecacheScope: null)。没有 boundary 或跳过 global cache 的路径才会走 org scope。

教学版的缓存只避免重复拼接字符串。CC 的三层缓存:

  1. lodash memoizegetSystemContextgetUserContext 在会话中缓存(context.ts
  2. section 注册缓存STATE.systemPromptSectionCache 缓存动态 section 结果,/clear/compact 时清除
  3. API 级缓存splitSysPromptPrefix()api.ts)把 prompt 按 boundary 分成不同 cache scope 的块

getUserContext vs getSystemContext

getSystemContext getUserContext
内容 gitStatus、cacheBreaker CLAUDE.md 内容、currentDate
注入方式 追加到 system prompt 数组 前置为 <system-reminder> 用户消息
何时跳过 自定义 system prompt 时 始终运行

模式如何改变 prompt

  • CLAUDE_CODE_SIMPLE:整个 prompt 只有 2 行
  • Proactive/KAIROS:用紧凑版 prompt 替换所有标准 section
  • Coordinator:用协调器专用 prompt 完全替换
  • Agent 模式Agent 定义的 prompt 替换或追加到默认 prompt

总大小

标准交互模式下 system prompt 核心约 20-30KB 文本。CLAUDE_CODE_SIMPLE 约 150 字符。用户上下文CLAUDE.md和系统上下文git status在此基础上累加。