Learn Claude Code

學習路徑對比

比較兩個章節之間新增了什麼能力、為什麼在這裡引入,以及學習時該先盯住哪條主線。

學習躍遷

先決定你要比較哪一步升級

這頁優先幫助你理解能力邊界的變化,而不是先把你拖進原始碼細節裡。

One-Click Compare

Start with these safe comparison moves instead of selecting two chapters every time

These presets cover the most useful adjacent upgrades and stage boundaries. They work both for a first pass and for resetting when chapter boundaries start to blur.

學習躍遷

Agent 迴圈工具使用

這是緊鄰的一步升級,最適合按教程順序學習系統是如何自然長出來的。

從 A 帶走

沒有迴圈,就沒有 agent。

B 新引入

加一個工具, 只加一個 handler

推進關係

這是緊鄰的一步升級,最適合按教程順序學習系統是如何自然長出來的。

After B

Add a new tool without changing the main loop.

Agent 迴圈

沒有迴圈,就沒有 agent。

130 LOC1 tools核心閉環

工具使用

加一個工具, 只加一個 handler

169 LOC4 tools核心閉環
相隔章節

1

B 中新增工具

3

共有工具

1

新增實現面

5

Jump Diagnosis

This is the safest upgrade step

A and B are adjacent, so this is the cleanest way to see the exact new branch, state container, and reason for introducing it now.

Safer Reading Move

Read the execution flow first, then the architecture view, and only then decide whether you need the source diff.

Bridge docs most worth reading before this jump

Jump Reading Support

Before jumping from Agent 迴圈 to 工具使用, read these bridge docs

A good comparison page should not only show what was added. It should also point you to the best bridge docs for understanding the jump.

Mainline Flow Comparison

Compare how one request evolves between the two chapters: where the new branch appears, what writes back into the loop, and what remains a side lane.

Agent 迴圈

How to Read

Read the mainline first, then inspect the side branches

Read top to bottom for time order. The center usually carries the mainline, while the sides hold branches, isolated lanes, or recovery paths. The key question is not how many nodes exist, but where this chapter introduces a new split and write-back.

Focus First

Focus first on how `messages`, `tool_use`, and `tool_result` close the loop.

Easy to Confuse

Do not confuse model reasoning with system action. The loop is what turns thought into work.

Build Goal

Be able to write a minimal but real agent loop by hand.

Node Legend

Entry

Where the current turn enters the system.

Process

A stable internal processing step.

Decision

Where the system chooses a branch.

Subprocess / Lane

Often used for external execution, sidecars, or isolated lanes.

Write-back / End

Where the turn ends or writes back into the loop.

Branch / Side Lane

Permission branches, autonomy scans, background slots, and worktree lanes often expand here.

Mainline

The path the system keeps returning to during the turn.

Branch / Side Lane

Permission branches, autonomy scans, background slots, and worktree lanes often expand here.

yesnoUser InputLLM Calltool_use?Execute BashAppend ResultOutput

Dashed borders usually indicate a subprocess or external lane; arrow labels explain why a branch was taken.

工具使用

How to Read

Read the mainline first, then inspect the side branches

Read top to bottom for time order. The center usually carries the mainline, while the sides hold branches, isolated lanes, or recovery paths. The key question is not how many nodes exist, but where this chapter introduces a new split and write-back.

Focus First

Focus on the relationship between `ToolSpec`, the dispatch map, and `tool_result`.

Easy to Confuse

A tool schema is not the handler itself. One describes the tool to the model; the other executes it.

Build Goal

Add a new tool without changing the main loop.

Node Legend

Entry

Where the current turn enters the system.

Process

A stable internal processing step.

Decision

Where the system chooses a branch.

Subprocess / Lane

Often used for external execution, sidecars, or isolated lanes.

Write-back / End

Where the turn ends or writes back into the loop.

Branch / Side Lane

Permission branches, autonomy scans, background slots, and worktree lanes often expand here.

Mainline

The path the system keeps returning to during the turn.

Branch / Side Lane

Permission branches, autonomy scans, background slots, and worktree lanes often expand here.

yesnoUser InputLLM Calltool_use?Tool Dispatchbash / read / write / editAppend ResultOutput

Dashed borders usually indicate a subprocess or external lane; arrow labels explain why a branch was taken.

架構檢視

先看模組邊界和協作關係,再決定要不要往下鑽實現細節。

Agent 迴圈

What This Chapter Actually Adds

LoopState + tool_result feedback

The first chapter establishes the smallest closed loop: user input enters messages[], the model decides whether to call a tool, and the result flows back into the same loop.

Mainline

The path that actually pushes the system forward.

Agent Loop

NEW

Each turn calls the model, handles the output, then decides whether to continue.

State Records

The structures the system must remember and write back.

messages[]

NEW

User, assistant, and tool result history accumulates here.

tool_result write-back

NEW

The agent becomes real when tool results return into the next reasoning step.

Key Records

These are the state containers worth holding onto when you rebuild the system yourself.

LoopStateNEW

The smallest runnable session state.

Assistant ContentNEW

The model output for the current turn.

Primary Handoff Path

1

User message enters messages[]

2

Model emits tool_use or text

3

Tool result writes back into the next turn

工具使用

What This Chapter Actually Adds

Tool specs + dispatch map

This chapter upgrades one tool call into a stable multi-tool routing layer while keeping the main loop unchanged.

Mainline

The path that actually pushes the system forward.

Stable Main Loop

The main loop still only owns model calls and write-back.

Control Plane

Decides how execution is controlled, gated, and redirected.

ToolSpec Catalog

NEW

Describes tool capabilities to the model.

Dispatch Map

NEW

Routes a tool call to the correct handler by name.

State Records

The structures the system must remember and write back.

tool_input

NEW

Structured tool arguments emitted by the model.

Key Records

These are the state containers worth holding onto when you rebuild the system yourself.

ToolSpecNEW

Schema plus description.

Dispatch EntryNEW

Mapping from tool name to function.

Primary Handoff Path

1

The model selects a tool

2

The dispatch map resolves the handler

3

The handler returns a tool_result

工具對比

僅在 Agent 迴圈

共有

bash

僅在 工具使用

read_filewrite_fileedit_file

原始碼差異(選看)

如果你在意實現展開,可以再看原始碼 diff;如果你只關心機制,前面的學習卡片已經足夠。 程式碼量差異: +39

s01 (s01_agent_loop.py) -> s02 (s02_tool_use.py)
11#!/usr/bin/env python3
2-# Harness: the loop -- keep feeding real tool results back into the model.
2+# Harness: tool dispatch -- expanding what the model can reach.
33"""
4-s01_agent_loop.py - The Agent Loop
4+s02_tool_use.py - Tool dispatch + message normalization
55
6-This file teaches the smallest useful coding-agent pattern:
6+The agent loop from s01 didn't change. We added tools to the dispatch map,
7+and a normalize_messages() function that cleans up the message list before
8+each API call.
79
8- user message
9- -> model reply
10- -> if tool_use: execute tools
11- -> write tool_result back to messages
12- -> continue
13-
14-It intentionally keeps the loop small, but still makes the loop state explicit
15-so later chapters can grow from the same structure.
10+Key insight: "The loop didn't change at all. I just added tools."
1611"""
1712
1813import os
1914import subprocess
20-from dataclasses import dataclass
15+from pathlib import Path
2116
22-try:
23- import readline
24- # #143 UTF-8 backspace fix for macOS libedit
25- readline.parse_and_bind('set bind-tty-special-chars off')
26- readline.parse_and_bind('set input-meta on')
27- readline.parse_and_bind('set output-meta on')
28- readline.parse_and_bind('set convert-meta off')
29- readline.parse_and_bind('set enable-meta-keybindings on')
30-except ImportError:
31- pass
32-
3317from anthropic import Anthropic
3418from dotenv import load_dotenv
3519
3620load_dotenv(override=True)
3721
3822if os.getenv("ANTHROPIC_BASE_URL"):
3923 os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
4024
25+WORKDIR = Path.cwd()
4126client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
4227MODEL = os.environ["MODEL_ID"]
4328
44-SYSTEM = (
45- f"You are a coding agent at {os.getcwd()}. "
46- "Use bash to inspect and change the workspace. Act first, then report clearly."
47-)
29+SYSTEM = f"You are a coding agent at {WORKDIR}. Use tools to solve tasks. Act, don't explain."
4830
49-TOOLS = [{
50- "name": "bash",
51- "description": "Run a shell command in the current workspace.",
52- "input_schema": {
53- "type": "object",
54- "properties": {"command": {"type": "string"}},
55- "required": ["command"],
56- },
57-}]
5831
32+def safe_path(p: str) -> Path:
33+ path = (WORKDIR / p).resolve()
34+ if not path.is_relative_to(WORKDIR):
35+ raise ValueError(f"Path escapes workspace: {p}")
36+ return path
5937
60-@dataclass
61-class LoopState:
62- # The minimal loop state: history, loop count, and why we continue.
63- messages: list
64- turn_count: int = 1
65- transition_reason: str | None = None
6638
67-
6839def run_bash(command: str) -> str:
6940 dangerous = ["rm -rf /", "sudo", "shutdown", "reboot", "> /dev/"]
70- if any(item in command for item in dangerous):
41+ if any(d in command for d in dangerous):
7142 return "Error: Dangerous command blocked"
7243 try:
73- result = subprocess.run(
74- command,
75- shell=True,
76- cwd=os.getcwd(),
77- capture_output=True,
78- text=True,
79- timeout=120,
80- )
44+ r = subprocess.run(command, shell=True, cwd=WORKDIR,
45+ capture_output=True, text=True, timeout=120)
46+ out = (r.stdout + r.stderr).strip()
47+ return out[:50000] if out else "(no output)"
8148 except subprocess.TimeoutExpired:
8249 return "Error: Timeout (120s)"
83- except (FileNotFoundError, OSError) as e:
50+
51+
52+def run_read(path: str, limit: int = None) -> str:
53+ try:
54+ text = safe_path(path).read_text()
55+ lines = text.splitlines()
56+ if limit and limit < len(lines):
57+ lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
58+ return "\n".join(lines)[:50000]
59+ except Exception as e:
8460 return f"Error: {e}"
8561
86- output = (result.stdout + result.stderr).strip()
87- return output[:50000] if output else "(no output)"
8862
63+def run_write(path: str, content: str) -> str:
64+ try:
65+ fp = safe_path(path)
66+ fp.parent.mkdir(parents=True, exist_ok=True)
67+ fp.write_text(content)
68+ return f"Wrote {len(content)} bytes to {path}"
69+ except Exception as e:
70+ return f"Error: {e}"
8971
90-def extract_text(content) -> str:
91- if not isinstance(content, list):
92- return ""
93- texts = []
94- for block in content:
95- text = getattr(block, "text", None)
96- if text:
97- texts.append(text)
98- return "\n".join(texts).strip()
9972
73+def run_edit(path: str, old_text: str, new_text: str) -> str:
74+ try:
75+ fp = safe_path(path)
76+ content = fp.read_text()
77+ if old_text not in content:
78+ return f"Error: Text not found in {path}"
79+ fp.write_text(content.replace(old_text, new_text, 1))
80+ return f"Edited {path}"
81+ except Exception as e:
82+ return f"Error: {e}"
10083
101-def execute_tool_calls(response_content) -> list[dict]:
102- results = []
103- for block in response_content:
104- if block.type != "tool_use":
105- continue
106- command = block.input["command"]
107- print(f"\033[33m$ {command}\033[0m")
108- output = run_bash(command)
109- print(output[:200])
110- results.append({
111- "type": "tool_result",
112- "tool_use_id": block.id,
113- "content": output,
114- })
115- return results
11684
85+# -- Concurrency safety classification --
86+# Read-only tools can safely run in parallel; mutating tools must be serialized.
87+CONCURRENCY_SAFE = {"read_file"}
88+CONCURRENCY_UNSAFE = {"write_file", "edit_file"}
11789
118-def run_one_turn(state: LoopState) -> bool:
119- response = client.messages.create(
120- model=MODEL,
121- system=SYSTEM,
122- messages=state.messages,
123- tools=TOOLS,
124- max_tokens=8000,
125- )
126- state.messages.append({"role": "assistant", "content": response.content})
90+# -- The dispatch map: {tool_name: handler} --
91+TOOL_HANDLERS = {
92+ "bash": lambda **kw: run_bash(kw["command"]),
93+ "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
94+ "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
95+ "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
96+}
12797
128- if response.stop_reason != "tool_use":
129- state.transition_reason = None
130- return False
98+TOOLS = [
99+ {"name": "bash", "description": "Run a shell command.",
100+ "input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
101+ {"name": "read_file", "description": "Read file contents.",
102+ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["path"]}},
103+ {"name": "write_file", "description": "Write content to file.",
104+ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
105+ {"name": "edit_file", "description": "Replace exact text in file.",
106+ "input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
107+]
131108
132- results = execute_tool_calls(response.content)
133- if not results:
134- state.transition_reason = None
135- return False
136109
137- state.messages.append({"role": "user", "content": results})
138- state.turn_count += 1
139- state.transition_reason = "tool_result"
140- return True
110+def normalize_messages(messages: list) -> list:
111+ """Clean up messages before sending to the API.
141112
113+ Three jobs:
114+ 1. Strip internal metadata fields the API doesn't understand
115+ 2. Ensure every tool_use has a matching tool_result (insert placeholder if missing)
116+ 3. Merge consecutive same-role messages (API requires strict alternation)
117+ """
118+ cleaned = []
119+ for msg in messages:
120+ clean = {"role": msg["role"]}
121+ if isinstance(msg.get("content"), str):
122+ clean["content"] = msg["content"]
123+ elif isinstance(msg.get("content"), list):
124+ clean["content"] = [
125+ {k: v for k, v in block.items()
126+ if not k.startswith("_")}
127+ for block in msg["content"]
128+ if isinstance(block, dict)
129+ ]
130+ else:
131+ clean["content"] = msg.get("content", "")
132+ cleaned.append(clean)
142133
143-def agent_loop(state: LoopState) -> None:
144- while run_one_turn(state):
145- pass
134+ # Collect existing tool_result IDs
135+ existing_results = set()
136+ for msg in cleaned:
137+ if isinstance(msg.get("content"), list):
138+ for block in msg["content"]:
139+ if isinstance(block, dict) and block.get("type") == "tool_result":
140+ existing_results.add(block.get("tool_use_id"))
146141
142+ # Find orphaned tool_use blocks and insert placeholder results
143+ for msg in cleaned:
144+ if msg["role"] != "assistant" or not isinstance(msg.get("content"), list):
145+ continue
146+ for block in msg["content"]:
147+ if not isinstance(block, dict):
148+ continue
149+ if block.get("type") == "tool_use" and block.get("id") not in existing_results:
150+ cleaned.append({"role": "user", "content": [
151+ {"type": "tool_result", "tool_use_id": block["id"],
152+ "content": "(cancelled)"}
153+ ]})
147154
155+ # Merge consecutive same-role messages
156+ if not cleaned:
157+ return cleaned
158+ merged = [cleaned[0]]
159+ for msg in cleaned[1:]:
160+ if msg["role"] == merged[-1]["role"]:
161+ prev = merged[-1]
162+ prev_c = prev["content"] if isinstance(prev["content"], list) \
163+ else [{"type": "text", "text": str(prev["content"])}]
164+ curr_c = msg["content"] if isinstance(msg["content"], list) \
165+ else [{"type": "text", "text": str(msg["content"])}]
166+ prev["content"] = prev_c + curr_c
167+ else:
168+ merged.append(msg)
169+ return merged
170+
171+
172+def agent_loop(messages: list):
173+ while True:
174+ response = client.messages.create(
175+ model=MODEL, system=SYSTEM,
176+ messages=normalize_messages(messages),
177+ tools=TOOLS, max_tokens=8000,
178+ )
179+ messages.append({"role": "assistant", "content": response.content})
180+ if response.stop_reason != "tool_use":
181+ return
182+ results = []
183+ for block in response.content:
184+ if block.type == "tool_use":
185+ handler = TOOL_HANDLERS.get(block.name)
186+ output = handler(**block.input) if handler else f"Unknown tool: {block.name}"
187+ print(f"> {block.name}:")
188+ print(output[:200])
189+ results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
190+ messages.append({"role": "user", "content": results})
191+
192+
148193if __name__ == "__main__":
149194 history = []
150195 while True:
151196 try:
152- query = input("\033[36ms01 >> \033[0m")
197+ query = input("\033[36ms02 >> \033[0m")
153198 except (EOFError, KeyboardInterrupt):
154199 break
155200 if query.strip().lower() in ("q", "exit", ""):
156201 break
157-
158202 history.append({"role": "user", "content": query})
159- state = LoopState(messages=history)
160- agent_loop(state)
161-
162- final_text = extract_text(history[-1]["content"])
163- if final_text:
164- print(final_text)
203+ agent_loop(history)
204+ response_content = history[-1]["content"]
205+ if isinstance(response_content, list):
206+ for block in response_content:
207+ if hasattr(block, "text"):
208+ print(block.text)
165209 print()