feat: refresh course through workflow and goal loops

This commit is contained in:
Haoran
2026-07-30 19:14:04 +08:00
parent 2dd1852d9e
commit cb8fae1bdd
125 changed files with 10882 additions and 7661 deletions

View File

@@ -1,222 +0,0 @@
# s02: Tool Use — Add a Tool, Add Just One Line
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
s01 → `s02` → [s03](../s03_permission/) → s04 → ... → s20
> *"Add a tool, add just one handler"* — The loop stays the same. Register the new tool in the dispatch map and you're done.
>
> **Harness Layer**: Tool Dispatch — Expanding the model's reach.
---
## Only One Tool: Bash
The s01 Agent has only one tool: bash. To read a file, `cat`; to write, `echo "..." > file.py`; to edit, `sed`.
The model thinks "read this file" but has to spell out `cat path/to/file`. An extra layer of translation that wastes tokens and invites errors.
---
## Overview: Tool Dispatch
![Tool Dispatch](images/tool-dispatch.en.svg)
The s01 loop is fully preserved (LLM call, stop_reason check, message append — not a single word changed). The only change is in that one line of tool execution: `run_bash()` is replaced with `TOOL_HANDLERS[block.name]()` dispatch lookup.
Adding a tool to the Agent requires just two things:
1. **Define the tool**: Add one entry to the `TOOLS` array
2. **Register the handler**: Add one mapping in the `TOOL_HANDLERS` dict
---
## From 1 Tool to 5 Tools
s01 had only bash:
```python
TOOLS = [{"name": "bash", ...}]
def run_bash(command): ...
```
s02 expands to 5 tools, each independently defined:
```python
TOOLS = [
{"name": "bash", "description": "Run a shell command.", ...},
{"name": "read_file", "description": "Read file contents.", ...},
{"name": "write_file", "description": "Write content to file.", ...},
{"name": "edit_file", "description": "Replace text in file once.", ...},
{"name": "glob", "description": "Find files by pattern.", ...},
]
```
Each tool has its own implementation function:
```python
def run_read(path, limit=None):
lines = safe_path(path).read_text().splitlines()
if limit:
lines = lines[:limit]
return "\n".join(lines)
def run_write(path, content):
safe_path(path).write_text(content)
return f"Wrote {len(content)} bytes to {path}"
def run_edit(path, old_text, new_text):
text = safe_path(path).read_text()
if old_text not in text:
return "Error: text not found"
safe_path(path).write_text(text.replace(old_text, new_text, 1))
return f"Edited {path}"
def run_glob(pattern):
import glob as g
return "\n".join(g.glob(pattern, root_dir=WORKDIR))
```
---
## Tool Dispatch
```python
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": run_read,
"write_file": run_write,
"edit_file": run_edit,
"glob": run_glob,
}
# Only one line changed in the loop — from hardcoded run_bash to dispatch lookup:
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS[block.name] # lookup
output = handler(**block.input) # call
results.append(...)
```
Adding a tool = one entry in `TOOLS` array + one line in `TOOL_HANDLERS` dict. The loop stays the same.
---
## Multiple Tool Calls
The model often returns multiple tool_use calls at once — "read a.py and b.py, then list all .py files".
The teaching version executes them one by one in the original `response.content` order. CC's approach is more complex: it slices the original order into consecutive batches, where concurrency-safe tools within a batch run in parallel, and batches are strictly sequential (see appendix).
---
## Quick Reference
| Concept | One-Liner |
|---------|-----------|
| TOOL_HANDLERS | Tool name → handler function dict. Add a tool = add one mapping line |
| Tool Definition | JSON schema telling the model "what I can do" |
| Multiple tool calls | Model may return multiple tool_use at once; teaching version executes them in original order |
| Loop Unchanged | s01's `while True` loop — not a single line changed |
---
## Changes from s01
| Component | Before (s01) | After (s02) |
|-----------|-------------|-------------|
| Tool count | 1 (bash) | 5 (+read, write, edit, glob) |
| Tool execution | Hardcoded `run_bash()` | TOOL_HANDLERS dispatch lookup |
| Path safety | None | safe_path validation (file tools only) |
| Loop | `while True` + `stop_reason` | Identical to s01 |
---
## Try It
```sh
cd learn-claude-code
python s02_tool_use/code.py
```
Try these prompts:
1. `Read the file README.md and tell me what this project is about`
2. `Create a file called test.py that prints "hello", then read it back`
3. `Find all Python files in this directory`
4. `Read both README.md and requirements.txt, then create a summary file`
What to watch for: When does the model call just one tool, and when does it call multiple at once? Are multiple tool calls executed in the correct order?
---
## What's Next
The Agent now has 5 specialized tools. File tools are protected by `safe_path`, but bash is unrestricted — `rm -rf /` still runs.
→ s03 Permission: Add a gate before tool execution — is this operation safe? Does it need user approval?
<details>
<summary>Dive into CC Source Code</summary>
> The following is based on a review of CC source code `Tool.ts`, `tools.ts`, `toolOrchestration.ts`, `toolExecution.ts`, and `StreamingToolExecutor.ts`.
### 1. Tool Definition Approach
**Teaching version**: `TOOLS` array + `TOOL_HANDLERS` dict. Definition and implementation are separate.
**CC**: Each tool is an independent object created by `buildTool()`, containing schema, validation, permissions, and execution. `getAllBaseTools()` aggregates all tools.
The teaching version's separation is clearer for teaching — readers immediately see "add a tool = two definitions".
### 2. Concurrency Safety: isConcurrencySafe()
![Tool Concurrency](images/concurrency-comparison.en.svg)
The teaching version executes tools one by one in original order, without concurrency. CC uses `isConcurrencySafe(input)` to determine concurrency — note this isn't simply "read-only vs write", but judges by specific input:
| | isReadOnly | isConcurrencySafe |
|---|---|---|
| FileRead | true | true |
| Glob | true | true |
| Bash `ls` | true | **true** ← key difference |
| Bash `rm` | false | false |
| TaskCreate | false | **true** ← modifies state but can be concurrent (introduced in s12) |
CC's Bash tool's `isConcurrencySafe` equals `isReadOnly` — read-only commands can be concurrent, write commands cannot. TaskCreate modifies task files, but each writes a different file, so it can be concurrent.
### 3. Partition Algorithm
CC's `partitionToolCalls()` (`toolOrchestration.ts:91-115`) doesn't split into two groups — it batches tool calls **by consecutive blocks**:
```
[read A, read B, glob *.py, bash "rm x", read C]
→ batch1(concurrent): [read A, read B, glob *.py]
→ batch2(serial): [bash "rm x"]
→ batch3(concurrent): [read C]
```
Consecutive concurrency-safe calls are grouped into the same batch for truly concurrent execution (`toolOrchestration.ts:152-176`, with a concurrency limit). When a non-concurrency-safe call is encountered, a new batch starts for serial execution. Batches are strictly sequential.
### 4. Validation Pipeline
Each tool call in CC goes through a strict 5-step validation (`toolExecution.ts`):
1. **Zod schema validation** (`614-680`, teaching version uses JSON Schema): parameter type/structure check
2. **Tool-level validateInput()** (`682-733`): parameter value validation (e.g., is the path within the working directory)
3. **PreToolUse hooks** (`800-862`, covered in s04): hooks can return messages, modify input, or block execution
4. **Permission check** (`921-931`, core topic of s03): canUseTool + checkPermissions → allow/deny/ask
5. **Execute tool.call()** (`1207-1222`)
The teaching version omits Zod (uses JSON Schema), omits validateInput (uses safety functions), but preserves the permission check and hook concepts.
### 5. Streaming Tool Execution
CC's `StreamingToolExecutor` (`StreamingToolExecutor.ts`) starts tools while the model is still generating — no waiting for the model to finish. `read_file` might complete while the model is still outputting "Let me analyze". The teaching version doesn't implement this, consistent with s01's goal — conceptual clarity, not peak performance.
### 6. Tool Result Persistence
Each tool has a `maxResultSizeChars` field. Results exceeding this threshold are persisted to disk, and the model sees a preview + file path. FileRead is special — set to `Infinity`, preventing file read output from being persisted again. Specifically, if FileRead's result exceeds the threshold and gets persisted, the model's next read of that persisted file would trigger another persistence → infinite loop (read file → persist → re-read → re-persist → ...).
</details>
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

View File

@@ -1,8 +1,8 @@
# s02: Tool Use — ツール一つ追加、一行追加だけ
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → `s02` → [s03](../s03_permission/) → s04 → ... → s20
s01 → `s02` → [s03](../s03_permission/) → s04 → ... → s20 → s21 → s22
> *"ツールを一つ追加、ハンドラを一つ追加"* — ループはそのまま。新しいツールをディスパッチマップに登録するだけ。
>
> **Harness レイヤー**: ツールディスパッチ — モデルが触れる範囲を拡張。

View File

@@ -1,38 +1,38 @@
# s02: Tool Use — 多加一个工具,只加一行
# s02: Tool Use — Add a Tool, Add Just One Line
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → `s02` → [s03](../s03_permission/) → s04 → ... → s20
> *"加一个工具, 只加一个 handler"* — 循环不用动, 新工具注册进 dispatch map 就行。
s01 → `s02` → [s03](../s03_permission/) → s04 → ... → s20 → s21 → s22
> *"Add a tool, add just one handler"* — The loop stays the same. Register the new tool in the dispatch map and you're done.
>
> **Harness 层**: 工具分发 — 扩展模型能触达的边界。
> **Harness Layer**: Tool Dispatch — Expanding the model's reach.
---
## 只有 bash 一个工具
## Only One Tool: Bash
s01 Agent 只有一个 bash 工具。读文件要 `cat`,写文件要 `echo "..." > file.py`,改文件要 `sed`
The s01 Agent has only one tool: bash. To read a file, `cat`; to write, `echo "..." > file.py`; to edit, `sed`.
模型想的是"读这个文件",却要拼出 `cat path/to/file`。多了一层翻译,浪费 token还容易拼错。
The model thinks "read this file" but has to spell out `cat path/to/file`. An extra layer of translation that wastes tokens and invites errors.
---
## 全局视角:工具分发
## Overview: Tool Dispatch
![Tool Dispatch](images/tool-dispatch.svg)
![Tool Dispatch](images/tool-dispatch.en.svg)
s01 的循环完全保留LLM 调用、stop_reason 判断、消息追加)。唯一的变动在工具执行那 1 行:`run_bash()` 替换为 `TOOL_HANDLERS[block.name]()` 查表分发。
The s01 loop is fully preserved (LLM call, stop_reason check, message append — not a single word changed). The only change is in that one line of tool execution: `run_bash()` is replaced with `TOOL_HANDLERS[block.name]()` dispatch lookup.
给 Agent 加一个工具只需要做两件事:
Adding a tool to the Agent requires just two things:
1. **定义工具**:在 `TOOLS` 数组里加一条描述
2. **注册处理函数**:在 `TOOL_HANDLERS` 字典里加一个映射
1. **Define the tool**: Add one entry to the `TOOLS` array
2. **Register the handler**: Add one mapping in the `TOOL_HANDLERS` dict
---
## 从 1 个工具到 5 个工具
## From 1 Tool to 5 Tools
s01 只有一个 bash
s01 had only bash:
```python
TOOLS = [{"name": "bash", ...}]
@@ -40,7 +40,7 @@ TOOLS = [{"name": "bash", ...}]
def run_bash(command): ...
```
s02 加到 5 个,每个工具都是独立定义:
s02 expands to 5 tools, each independently defined:
```python
TOOLS = [
@@ -52,7 +52,7 @@ TOOLS = [
]
```
每个工具有自己的实现函数:
Each tool has its own implementation function:
```python
def run_read(path, limit=None):
@@ -79,7 +79,7 @@ def run_glob(pattern):
---
## 工具分发
## Tool Dispatch
```python
TOOL_HANDLERS = {
@@ -90,133 +90,133 @@ TOOL_HANDLERS = {
"glob": run_glob,
}
# 循环里只改了一行——从硬编码 run_bash 变成查表:
# Only one line changed in the loop — from hardcoded run_bash to dispatch lookup:
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS[block.name] # 查表
output = handler(**block.input) # 调用
handler = TOOL_HANDLERS[block.name] # lookup
output = handler(**block.input) # call
results.append(...)
```
加一个工具 = 在 `TOOLS` 数组加一条 + 在 `TOOL_HANDLERS` 字典加一行。循环不变。
Adding a tool = one entry in `TOOLS` array + one line in `TOOL_HANDLERS` dict. The loop stays the same.
---
## 多个工具调用
## Multiple Tool Calls
模型经常一次返回多个 tool_use"读一下 a.py b.py,然后列出所有 .py 文件"。
The model often returns multiple tool_use calls at once — "read a.py and b.py, then list all .py files".
教学版按 `response.content` 原始顺序逐个执行。CC 的做法更复杂:按原始顺序切成连续 batchbatch 内并发安全的工具并行执行batch 间严格顺序(见附录)。
The teaching version executes them one by one in the original `response.content` order. CC's approach is more complex: it slices the original order into consecutive batches, where concurrency-safe tools within a batch run in parallel, and batches are strictly sequential (see appendix).
---
## 速查
## Quick Reference
| 概念 | 一句话 |
|------|--------|
| TOOL_HANDLERS | 工具名 → 处理函数的字典。加工具 = 加一行映射 |
| 工具定义 | 告诉模型"我能做什么"的 JSON schema |
| 多工具调用 | 模型可一次返回多个 tool_use教学版按原始顺序逐个执行 |
| 循环不变 | s01 `while True` 循环一行都没改 |
| Concept | One-Liner |
|---------|-----------|
| TOOL_HANDLERS | Tool name → handler function dict. Add a tool = add one mapping line |
| Tool Definition | JSON schema telling the model "what I can do" |
| Multiple tool calls | Model may return multiple tool_use at once; teaching version executes them in original order |
| Loop Unchanged | s01's `while True` loop — not a single line changed |
---
## 相对 s01 的变更
## Changes from s01
| 组件 | 之前 (s01) | 之后 (s02) |
|------|-----------|-----------|
| 工具数量 | 1 (bash) | 5 (+read, write, edit, glob) |
| 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 |
| 路径安全 | 无 | safe_path 校验(仅 file tools |
| 循环 | `while True` + `stop_reason` | 与 s01 完全一致 |
| Component | Before (s01) | After (s02) |
|-----------|-------------|-------------|
| Tool count | 1 (bash) | 5 (+read, write, edit, glob) |
| Tool execution | Hardcoded `run_bash()` | TOOL_HANDLERS dispatch lookup |
| Path safety | None | safe_path validation (file tools only) |
| Loop | `while True` + `stop_reason` | Identical to s01 |
---
## 试一下
## Try It
```sh
cd learn-claude-code
python s02_tool_use/code.py
```
试试这些 prompt
Try these prompts:
1. `Read the file README.md and tell me what this project is about`
2. `Create a file called test.py that prints "hello", then read it back`
3. `Find all Python files in this directory`
4. `Read both README.md and requirements.txt, then create a summary file`
观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确?
What to watch for: When does the model call just one tool, and when does it call multiple at once? Are multiple tool calls executed in the correct order?
---
## 接下来
## What's Next
现在 Agent 有 5 个专用工具。file tools 受 `safe_path` 保护,但 bash 不受限制,`rm -rf /` 还是能跑。
The Agent now has 5 specialized tools. File tools are protected by `safe_path`, but bash is unrestricted — `rm -rf /` still runs.
s03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗?
s03 Permission: Add a gate before tool execution — is this operation safe? Does it need user approval?
<details>
<summary>深入 CC 源码</summary>
<summary>Dive into CC Source Code</summary>
> 以下基于 CC 源码 `Tool.ts``tools.ts``toolOrchestration.ts``toolExecution.ts``StreamingToolExecutor.ts` 的核查。
> The following is based on a review of CC source code `Tool.ts`, `tools.ts`, `toolOrchestration.ts`, `toolExecution.ts`, and `StreamingToolExecutor.ts`.
### 一、工具定义方式
### 1. Tool Definition Approach
**教学版**`TOOLS` 数组 + `TOOL_HANDLERS` 字典。定义和实现分开。
**CC**:每个工具是 `buildTool()` 创建的独立对象,包含 schema、验证、权限、执行。`getAllBaseTools()` 汇总所有工具。
**Teaching version**: `TOOLS` array + `TOOL_HANDLERS` dict. Definition and implementation are separate.
**CC**: Each tool is an independent object created by `buildTool()`, containing schema, validation, permissions, and execution. `getAllBaseTools()` aggregates all tools.
教学版的分离方式对教学更清晰——读者一眼看到"加一个工具 = 两条定义"。
The teaching version's separation is clearer for teaching — readers immediately see "add a tool = two definitions".
### 二、并发安全判断:isConcurrencySafe()
### 2. Concurrency Safety: isConcurrencySafe()
![Tool Concurrency](images/concurrency-comparison.svg)
![Tool Concurrency](images/concurrency-comparison.en.svg)
教学版按原始顺序逐个执行不做并发。CC 用 `isConcurrencySafe(input)` 判断能否并发——注意这不是简单的"只读 vs 写",而是按具体输入判断:
The teaching version executes tools one by one in original order, without concurrency. CC uses `isConcurrencySafe(input)` to determine concurrency — note this isn't simply "read-only vs write", but judges by specific input:
| | isReadOnly | isConcurrencySafe |
|---|---|---|
| FileRead | true | true |
| Glob | true | true |
| Bash `ls` | true | **true**关键差异 |
| Bash `ls` | true | **true**key difference |
| Bash `rm` | false | false |
| TaskCreate | false | **true**改状态但可并发TaskCreate 在 s12 介绍) |
| TaskCreate | false | **true**modifies state but can be concurrent (introduced in s12) |
CC Bash tool `isConcurrencySafe` 等于 `isReadOnly`——只读命令可并发写命令不可。TaskCreate 虽然改了任务文件,但每次都写不同的文件,所以可以并发。
CC's Bash tool's `isConcurrencySafe` equals `isReadOnly` — read-only commands can be concurrent, write commands cannot. TaskCreate modifies task files, but each writes a different file, so it can be concurrent.
### 三、分区算法
### 3. Partition Algorithm
CC `partitionToolCalls()``toolOrchestration.ts:91-115`)不是分两组,而是把工具调用**按连续块分批**
CC's `partitionToolCalls()` (`toolOrchestration.ts:91-115`) doesn't split into two groups — it batches tool calls **by consecutive blocks**:
```
[read A, read B, glob *.py, bash "rm x", read C]
→ batch1(并发): [read A, read B, glob *.py]
→ batch2(串行): [bash "rm x"]
→ batch3(并发): [read C]
→ batch1(concurrent): [read A, read B, glob *.py]
→ batch2(serial): [bash "rm x"]
→ batch3(concurrent): [read C]
```
并发安全的连续块编入同一个 batchbatch 内真正并发执行(`toolOrchestration.ts:152-176`,有并发上限)。遇到非并发安全的就开新 batch 串行执行。batch 之间严格顺序。
Consecutive concurrency-safe calls are grouped into the same batch for truly concurrent execution (`toolOrchestration.ts:152-176`, with a concurrency limit). When a non-concurrency-safe call is encountered, a new batch starts for serial execution. Batches are strictly sequential.
### 四、验证管线
### 4. Validation Pipeline
CC 的每个工具调用经过严格的 5 步验证(`toolExecution.ts`
Each tool call in CC goes through a strict 5-step validation (`toolExecution.ts`):
1. **Zod schema 验证**`614-680`,教学版用 JSON Schema 替代):参数类型/结构检查
2. **工具级 validateInput()**`682-733`):参数值验证(如路径是否在工作区内)
3. **PreToolUse hooks**`800-862`s04 详细介绍):钩子可以返回消息、修改输入、阻止执行
4. **权限检查**`921-931`s03 的核心内容):canUseTool + checkPermissions → allow/deny/ask
5. **执行 tool.call()**`1207-1222`
1. **Zod schema validation** (`614-680`, teaching version uses JSON Schema): parameter type/structure check
2. **Tool-level validateInput()** (`682-733`): parameter value validation (e.g., is the path within the working directory)
3. **PreToolUse hooks** (`800-862`, covered in s04): hooks can return messages, modify input, or block execution
4. **Permission check** (`921-931`, core topic of s03): canUseTool + checkPermissions → allow/deny/ask
5. **Execute tool.call()** (`1207-1222`)
教学版省略了 Zod JSON Schema)、省略了 validateInput(用安全函数)、保留了权限检查和钩子概念。
The teaching version omits Zod (uses JSON Schema), omits validateInput (uses safety functions), but preserves the permission check and hook concepts.
### 五、流式工具执行
### 5. Streaming Tool Execution
CC `StreamingToolExecutor``StreamingToolExecutor.ts`)让工具在模型还在生成时就启动——不等模型说完。`read_file` 可能在模型还在输出"我来分析"的时候就跑完了。教学版不实现这个,目标和 s01 一致——概念清晰,不追求性能极致。
CC's `StreamingToolExecutor` (`StreamingToolExecutor.ts`) starts tools while the model is still generating — no waiting for the model to finish. `read_file` might complete while the model is still outputting "Let me analyze". The teaching version doesn't implement this, consistent with s01's goal — conceptual clarity, not peak performance.
### 六、工具结果持久化
### 6. Tool Result Persistence
每个工具有一个 `maxResultSizeChars` 字段。结果超过这个值就落盘,模型看到的是预览 + 文件路径。FileRead 特殊——设为 `Infinity`,防止读文件的输出又被当成文件落盘。具体来说,如果 FileRead 的结果超过阈值被落盘,模型下次读那个落盘文件时又会触发落盘 → 无限循环(读文件 → 落盘 → 再读 → 再落盘 → ...)。
Each tool has a `maxResultSizeChars` field. Results exceeding this threshold are persisted to disk, and the model sees a preview + file path. FileRead is special — set to `Infinity`, preventing file read output from being persisted again. Specifically, if FileRead's result exceeds the threshold and gets persisted, the model's next read of that persisted file would trigger another persistence → infinite loop (read file → persist → re-read → re-persist → ...).
</details>
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->

222
s02_tool_use/README.zh.md Normal file
View File

@@ -0,0 +1,222 @@
# s02: Tool Use — 多加一个工具,只加一行
[English](README.md) · [中文](README.zh.md) · [日本語](README.ja.md)
s01 → `s02` → [s03](../s03_permission/) → s04 → ... → s20 → s21 → s22
> *"加一个工具, 只加一个 handler"* — 循环不用动, 新工具注册进 dispatch map 就行。
>
> **Harness 层**: 工具分发 — 扩展模型能触达的边界。
---
## 只有 bash 一个工具
s01 的 Agent 只有一个 bash 工具。读文件要 `cat`,写文件要 `echo "..." > file.py`,改文件要 `sed`
模型想的是"读这个文件",却要拼出 `cat path/to/file`。多了一层翻译,浪费 token还容易拼错。
---
## 全局视角:工具分发
![Tool Dispatch](images/tool-dispatch.svg)
s01 的循环完全保留LLM 调用、stop_reason 判断、消息追加)。唯一的变动在工具执行那 1 行:`run_bash()` 替换为 `TOOL_HANDLERS[block.name]()` 查表分发。
给 Agent 加一个工具只需要做两件事:
1. **定义工具**:在 `TOOLS` 数组里加一条描述
2. **注册处理函数**:在 `TOOL_HANDLERS` 字典里加一个映射
---
## 从 1 个工具到 5 个工具
s01 只有一个 bash
```python
TOOLS = [{"name": "bash", ...}]
def run_bash(command): ...
```
s02 加到 5 个,每个工具都是独立定义:
```python
TOOLS = [
{"name": "bash", "description": "Run a shell command.", ...},
{"name": "read_file", "description": "Read file contents.", ...},
{"name": "write_file", "description": "Write content to file.", ...},
{"name": "edit_file", "description": "Replace text in file once.", ...},
{"name": "glob", "description": "Find files by pattern.", ...},
]
```
每个工具有自己的实现函数:
```python
def run_read(path, limit=None):
lines = safe_path(path).read_text().splitlines()
if limit:
lines = lines[:limit]
return "\n".join(lines)
def run_write(path, content):
safe_path(path).write_text(content)
return f"Wrote {len(content)} bytes to {path}"
def run_edit(path, old_text, new_text):
text = safe_path(path).read_text()
if old_text not in text:
return "Error: text not found"
safe_path(path).write_text(text.replace(old_text, new_text, 1))
return f"Edited {path}"
def run_glob(pattern):
import glob as g
return "\n".join(g.glob(pattern, root_dir=WORKDIR))
```
---
## 工具分发
```python
TOOL_HANDLERS = {
"bash": run_bash,
"read_file": run_read,
"write_file": run_write,
"edit_file": run_edit,
"glob": run_glob,
}
# 循环里只改了一行——从硬编码 run_bash 变成查表:
for block in response.content:
if block.type == "tool_use":
handler = TOOL_HANDLERS[block.name] # 查表
output = handler(**block.input) # 调用
results.append(...)
```
加一个工具 = 在 `TOOLS` 数组加一条 + 在 `TOOL_HANDLERS` 字典加一行。循环不变。
---
## 多个工具调用
模型经常一次返回多个 tool_use"读一下 a.py 和 b.py然后列出所有 .py 文件"。
教学版按 `response.content` 原始顺序逐个执行。CC 的做法更复杂:按原始顺序切成连续 batchbatch 内并发安全的工具并行执行batch 间严格顺序(见附录)。
---
## 速查
| 概念 | 一句话 |
|------|--------|
| TOOL_HANDLERS | 工具名 → 处理函数的字典。加工具 = 加一行映射 |
| 工具定义 | 告诉模型"我能做什么"的 JSON schema |
| 多工具调用 | 模型可一次返回多个 tool_use教学版按原始顺序逐个执行 |
| 循环不变 | s01 的 `while True` 循环一行都没改 |
---
## 相对 s01 的变更
| 组件 | 之前 (s01) | 之后 (s02) |
|------|-----------|-----------|
| 工具数量 | 1 (bash) | 5 (+read, write, edit, glob) |
| 工具执行 | 硬编码 `run_bash()` | TOOL_HANDLERS 查表分发 |
| 路径安全 | 无 | safe_path 校验(仅 file tools |
| 循环 | `while True` + `stop_reason` | 与 s01 完全一致 |
---
## 试一下
```sh
cd learn-claude-code
python s02_tool_use/code.py
```
试试这些 prompt
1. `Read the file README.md and tell me what this project is about`
2. `Create a file called test.py that prints "hello", then read it back`
3. `Find all Python files in this directory`
4. `Read both README.md and requirements.txt, then create a summary file`
观察重点:模型什么时候只调一个工具,什么时候一次调多个?多个工具调用的顺序和结果是否正确?
---
## 接下来
现在 Agent 有 5 个专用工具。file tools 受 `safe_path` 保护,但 bash 不受限制,`rm -rf /` 还是能跑。
s03 Permission → 在工具执行之前加一道门:这个操作安全吗?需要用户批准吗?
<details>
<summary>深入 CC 源码</summary>
> 以下基于 CC 源码 `Tool.ts`、`tools.ts`、`toolOrchestration.ts`、`toolExecution.ts`、`StreamingToolExecutor.ts` 的核查。
### 一、工具定义方式
**教学版**`TOOLS` 数组 + `TOOL_HANDLERS` 字典。定义和实现分开。
**CC**:每个工具是 `buildTool()` 创建的独立对象,包含 schema、验证、权限、执行。`getAllBaseTools()` 汇总所有工具。
教学版的分离方式对教学更清晰——读者一眼看到"加一个工具 = 两条定义"。
### 二、并发安全判断isConcurrencySafe()
![Tool Concurrency](images/concurrency-comparison.svg)
教学版按原始顺序逐个执行不做并发。CC 用 `isConcurrencySafe(input)` 判断能否并发——注意这不是简单的"只读 vs 写",而是按具体输入判断:
| | isReadOnly | isConcurrencySafe |
|---|---|---|
| FileRead | true | true |
| Glob | true | true |
| Bash `ls` | true | **true** ← 关键差异 |
| Bash `rm` | false | false |
| TaskCreate | false | **true** ← 改状态但可并发TaskCreate 在 s12 介绍) |
CC 的 Bash tool 的 `isConcurrencySafe` 等于 `isReadOnly`——只读命令可并发写命令不可。TaskCreate 虽然改了任务文件,但每次都写不同的文件,所以可以并发。
### 三、分区算法
CC 的 `partitionToolCalls()``toolOrchestration.ts:91-115`)不是分两组,而是把工具调用**按连续块分批**
```
[read A, read B, glob *.py, bash "rm x", read C]
→ batch1(并发): [read A, read B, glob *.py]
→ batch2(串行): [bash "rm x"]
→ batch3(并发): [read C]
```
并发安全的连续块编入同一个 batchbatch 内真正并发执行(`toolOrchestration.ts:152-176`,有并发上限)。遇到非并发安全的就开新 batch 串行执行。batch 之间严格顺序。
### 四、验证管线
CC 的每个工具调用经过严格的 5 步验证(`toolExecution.ts`
1. **Zod schema 验证**`614-680`,教学版用 JSON Schema 替代):参数类型/结构检查
2. **工具级 validateInput()**`682-733`):参数值验证(如路径是否在工作区内)
3. **PreToolUse hooks**`800-862`s04 详细介绍):钩子可以返回消息、修改输入、阻止执行
4. **权限检查**`921-931`s03 的核心内容canUseTool + checkPermissions → allow/deny/ask
5. **执行 tool.call()**`1207-1222`
教学版省略了 Zod用 JSON Schema、省略了 validateInput用安全函数、保留了权限检查和钩子概念。
### 五、流式工具执行
CC 的 `StreamingToolExecutor``StreamingToolExecutor.ts`)让工具在模型还在生成时就启动——不等模型说完。`read_file` 可能在模型还在输出"我来分析"的时候就跑完了。教学版不实现这个,目标和 s01 一致——概念清晰,不追求性能极致。
### 六、工具结果持久化
每个工具有一个 `maxResultSizeChars` 字段。结果超过这个值就落盘,模型看到的是预览 + 文件路径。FileRead 特殊——设为 `Infinity`,防止读文件的输出又被当成文件落盘。具体来说,如果 FileRead 的结果超过阈值被落盘,模型下次读那个落盘文件时又会触发落盘 → 无限循环(读文件 → 落盘 → 再读 → 再落盘 → ...)。
</details>
<!-- translation-sync: zh@v1, en@v0, ja@v0 -->