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

255 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# s10: System Prompt — 运行时组装,不硬编码
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → ... → s08 → s09 → `s10` → [s11](../s11_error_recovery/) → s12 → ... → s20
> *"prompt 是组装出来的, 不是写死的"* — 分段 + 按需拼接 + 缓存。
>
> **Harness 层**: 提示 — 运行时组装, 不硬编码。
---
## 问题
从 s01 到 s09system prompt 都是一行硬编码:
```python
SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks."
```
s01 够用,只有 bash、read、write 三个工具。但到 s09Agent 已经有记忆、有压缩、有技能加载。prompt 该提的能力越来越多:
```python
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](images/system-prompt-overview.svg)
s10 聚焦 prompt 组装机制。以 s08-s09 的能力为背景,但不重复实现压缩和记忆系统。核心变动:把硬编码的 `SYSTEM` 拆成独立段落section运行时根据真实状态按需拼接缓存结果避免重复组装。
四个 section两种加载策略
| Section | 加载策略 | 内容 | 判断依据 |
|---------|---------|------|---------|
| identity | 始终 | 你是谁、怎么做事 | 始终存在 |
| tools | 始终 | 可用工具列表 | `enabled_tools` |
| workspace | 始终 | 工作目录 | 始终存在 |
| memory | 按需 | 相关记忆内容 | `.memory/MEMORY.md` 是否存在 |
关键设计section 是否加载取决于真实状态(工具是否存在、文件是否存在),不是消息里的关键词。
---
## 工作原理
### PROMPT_SECTIONS: 分段定义
把一大段字符串拆成字典,每个 key 是一个主题:
```python
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 的真实状态决定加载哪些:
```python
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 相同),重新拼接是浪费。用确定性序列化检测变化,命中缓存直接返回:
```python
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 反映当前运行态的真实状态:
```python
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 加载基于这些真实状态,不在消息里搜关键词。
### 合起来跑
```python
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) |
---
## 试一下
```sh
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、压缩上下文、指数退避、切换模型。
<details>
<summary>深入 CC 源码</summary>
> 以下基于 CC 源码 `constants/prompts.ts`914 行)、`constants/systemPromptSections.ts`68 行)、`context.ts`189 行)、`utils/api.ts`718 行)、`utils/systemPrompt.ts`123 行)、`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 可以在轮次间连接和断开。
### 组装函数
```typescript
getSystemPrompt(tools, model, additionalWorkingDirs?, mcpClients?): Promise<string[]>
```
返回 `string[]`(每个元素是一个 section`SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 分隔静态和动态部分。
### cache scope
启用 global cache boundary 时,静态 section 合并成一个 global cache block动态 section 不使用 global cache`cacheScope: null`)。没有 boundary 或跳过 global cache 的路径才会走 org scope。
教学版的缓存只避免重复拼接字符串。CC 的三层缓存:
1. **lodash memoize**`getSystemContext``getUserContext` 在会话中缓存(`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在此基础上累加。
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->