Learn Claude Code

学习路径对比

比较两个章节之间新增了什么能力、为什么在这里引入,以及学习时该先盯住哪条主线。

学习跃迁

先决定你要比较哪一步升级

这页优先帮助你理解能力边界的变化,而不是先把你拖进源码细节里。

一键对比入口

先用这些最稳的比较入口,不必每次手选两章

这些按钮优先覆盖最值得反复看的相邻升级和阶段切换,适合第一次理解章节边界,也适合读到一半开始混时快速重启。

学习跃迁

Agent 循环工具使用

这是紧邻的一步升级,最适合按教程顺序学习系统是如何自然长出来的。

从 A 带走

没有循环,就没有 agent。

B 新引入

加一个工具, 只加一个 handler

推进关系

这是紧邻的一步升级,最适合按教程顺序学习系统是如何自然长出来的。

学完 B 后

在不改主循环的前提下,自己加一个新工具。

Agent 循环

没有循环,就没有 agent。

130 LOC1 tools核心闭环

工具使用

加一个工具, 只加一个 handler

169 LOC4 tools核心闭环
相隔章节

1

B 中新增工具

3

共有工具

1

新增实现面

5

跃迁诊断

这是最稳的一步升级

A 和 B 相邻,最适合看“系统刚刚多了一条什么分支、一个什么状态容器、为什么现在引入它”。

更稳的读法

先看执行流,再看架构图,最后再决定要不要往下看源码 diff。

这次跳跃前最值得先补的桥接资料

跳读辅助

从 Agent 循环 跳到 工具使用 前,先补这几张图

对比页不只是告诉你“多了什么”,还应该告诉你为了消化这次跃迁,哪些结构地图和机制展开最值得先看。

主线执行对比

先看一条请求在两章之间是怎么变的:新的分支出现在哪里,哪些结果会回流到主循环,哪些部分只是侧车或外部车道。

Agent 循环

读图方式

先看主线回流,再看左右分支

从上往下看时间顺序,中间通常是主线,左右是分支、隔离车道或恢复路径。真正重要的不是节点有多少,而是这一章新增的分叉与回流在哪里。

本章先盯住

先盯住 `messages`、`tool_use` 和 `tool_result` 如何闭环回流。

最容易混

不要把“模型会思考”和“系统能行动”混成一回事,真正让它能行动的是 loop。

学完要会

手写一个最小但真实可运行的 agent loop。

节点图例

入口

这轮从哪里开始进入系统。

主处理

系统内部稳定推进的一步。

分叉判断

系统在这里决定往哪条分支走。

子流程 / 外部车道

常见于外部执行、侧车流程或隔离车道。

回流 / 结束

这轮在这里结束或回到主循环。

分支 / 侧车

权限分支、自治扫描、后台槽位、worktree 车道常在这里展开。

主线

系统当前回合反复回到的那条路径。

分支 / 侧车

权限分支、自治扫描、后台槽位、worktree 车道常在这里展开。

用户输入模型调用tool_use?执行 Bash追加结果输出

虚线边框通常表示子流程或外部车道;箭头标签说明当前分叉为什么发生。

工具使用

读图方式

先看主线回流,再看左右分支

从上往下看时间顺序,中间通常是主线,左右是分支、隔离车道或恢复路径。真正重要的不是节点有多少,而是这一章新增的分叉与回流在哪里。

本章先盯住

先盯住 `ToolSpec`、`dispatch map` 和 `tool_result` 的对应关系。

最容易混

工具 schema 不是执行函数本身;一个是给模型看的说明,一个是代码里的处理器。

学完要会

在不改主循环的前提下,自己加一个新工具。

节点图例

入口

这轮从哪里开始进入系统。

主处理

系统内部稳定推进的一步。

分叉判断

系统在这里决定往哪条分支走。

子流程 / 外部车道

常见于外部执行、侧车流程或隔离车道。

回流 / 结束

这轮在这里结束或回到主循环。

分支 / 侧车

权限分支、自治扫描、后台槽位、worktree 车道常在这里展开。

主线

系统当前回合反复回到的那条路径。

分支 / 侧车

权限分支、自治扫描、后台槽位、worktree 车道常在这里展开。

用户输入模型调用tool_use?工具分发bash / read / write / edit追加结果输出

虚线边框通常表示子流程或外部车道;箭头标签说明当前分叉为什么发生。

架构视图

先看模块边界和协作关系,再决定要不要往下钻实现细节。

Agent 循环

这章在系统里真正新增了什么

LoopState + tool_result 回流

第一章先建立最小闭环:用户输入进入 messages[],模型决定要不要调工具,结果再回写到同一条循环里。

主线执行

真正把系统往前推的那条执行主线。

Agent 循环

新增

每轮都走一次调用模型 -> 处理输出 -> 再决定是否继续。

状态容器

真正需要被系统记住和回写的结构。

messages[]

新增

所有用户、助手和工具结果都累积在这里。

tool_result 回流

新增

真正让 agent 能行动的是工具结果会回到下一轮推理。

关键记录结构

这些不是实现细枝末节,而是开发者自己重建系统时最应该抓住的状态容器。

LoopState新增

最小可运行会话状态。

Assistant 内容新增

模型本轮输出。

主回流路径

1

用户消息进入 messages[]

2

模型产出 tool_use 或文本

3

工具结果回写到下一轮

工具使用

这章在系统里真正新增了什么

工具规格 + 分发映射

这一章把“会调一个工具”升级成“能稳定路由很多工具”,主循环不变,工具层长出来。

主线执行

真正把系统往前推的那条执行主线。

稳定主循环

主循环继续只管模型调用与结果回写。

控制面

决定怎么运行、何时放行、何时转向。

ToolSpec 目录

新增

把工具能力描述给模型看。

分发映射

新增

按工具名把调用路由到对应 handler。

状态容器

真正需要被系统记住和回写的结构。

tool_input

新增

模型传入的结构化工具参数。

关键记录结构

这些不是实现细枝末节,而是开发者自己重建系统时最应该抓住的状态容器。

ToolSpec新增

schema + 描述。

分发条目新增

工具名到函数的映射。

主回流路径

1

模型说要调哪个工具

2

dispatch map 找到 handler

3

handler 输出 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()