mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-09-20 12:13:38 +08:00
Fix empty tool-use response handling
This commit is contained in:
@@ -25,12 +25,12 @@
|
||||
|
||||

|
||||
|
||||
一つの `while True` ループ — モデルがツールを呼べば続き、呼ばなければ停止。全体でたった 2 つのシグナル:
|
||||
一つの `while True` ループ — モデルがツールを呼べば続き、呼ばなければ停止。ループは response の content block を直接確認する:
|
||||
|
||||
| シグナル | 意味 | ループの動作 |
|
||||
|----------|------|-------------|
|
||||
| `stop_reason == "tool_use"` | モデルが「ツールが必要」と挙手 | 実行 → 結果を戻す → 続行 |
|
||||
| `stop_reason != "tool_use"` | モデルが「完了」と宣言 | ループ終了 |
|
||||
| `tool_use` block を含む | モデルがツール呼び出しを要求 | 実行 → 結果を戻す → 続行 |
|
||||
| `tool_use` block を含まない | モデルがツールを呼ばなかった | ループ終了 |
|
||||
|
||||
---
|
||||
|
||||
@@ -57,22 +57,26 @@ response = client.messages.create(
|
||||
|
||||
```python
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
tool_calls = [
|
||||
block for block in response.content if block.type == "tool_use"
|
||||
]
|
||||
if not tool_calls:
|
||||
return
|
||||
```
|
||||
|
||||
実際の `tool_use` block だけが実行段階に進むため、空の tool result メッセージは追加されない。
|
||||
|
||||
**ステップ 4**:モデルが要求したツールを実行し、結果を収集する。
|
||||
|
||||
```python
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
for block in tool_calls:
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
```
|
||||
|
||||
**ステップ 5**:ツールの結果を新しいメッセージとして追加し、ステップ 2 に戻る。
|
||||
@@ -92,22 +96,24 @@ def agent_loop(messages):
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
|
||||
if response.stop_reason != "tool_use":
|
||||
tool_calls = [
|
||||
block for block in response.content if block.type == "tool_use"
|
||||
]
|
||||
if not tool_calls:
|
||||
return
|
||||
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
for block in tool_calls:
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
messages.append({"role": "user", "content": results})
|
||||
```
|
||||
|
||||
30 行未満 — これが最小実行可能な agent harness のカーネルだ。これは知能そのものではなく、モデルが継続的に行動できるための最小ランタイムフレームワーク。モデルが決定し(ツールを呼ぶか、どれを呼ぶか)、harness が実行を担う(ツールを呼び出し、結果を新しいメッセージとして追加する)。次の 16 章はすべてこのループの上に仕組みを積み重ねていく。ループ自体は永遠に変わらない。
|
||||
30 行あまり — これが最小実行可能な agent harness のカーネルだ。これは知能そのものではなく、モデルが継続的に行動できるための最小ランタイムフレームワーク。モデルが決定し(ツールを呼ぶか、どれを呼ぶか)、harness が実行を担う(ツールを呼び出し、結果を新しいメッセージとして追加する)。次の 16 章はすべてこのループの上に仕組みを積み重ねていく。ループ自体は永遠に変わらない。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@ Every round-trip, you're the middle layer. Automating that is what this chapter
|
||||
|
||||

|
||||
|
||||
A `while True` loop: keep going when the model calls a tool, stop when it doesn't. The entire process hinges on two signals:
|
||||
A `while True` loop: keep going when the model calls a tool, stop when it doesn't. The loop checks the response content blocks directly:
|
||||
|
||||
| Signal | Meaning | Loop Action |
|
||||
|--------|---------|-------------|
|
||||
| `stop_reason == "tool_use"` | Model raises hand: "I need a tool" | Execute → feed result back → continue |
|
||||
| `stop_reason != "tool_use"` | Model says: "I'm done" | Exit loop |
|
||||
| Contains a `tool_use` block | Model requests a tool call | Execute → feed result back → continue |
|
||||
| Contains no `tool_use` block | Model did not call a tool | Exit loop |
|
||||
|
||||
---
|
||||
|
||||
@@ -57,22 +57,26 @@ response = client.messages.create(
|
||||
|
||||
```python
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
tool_calls = [
|
||||
block for block in response.content if block.type == "tool_use"
|
||||
]
|
||||
if not tool_calls:
|
||||
return
|
||||
```
|
||||
|
||||
Only concrete `tool_use` blocks enter the execution stage, so the loop never appends an empty tool-result message.
|
||||
|
||||
**Step 4**: Execute the tool the model requested and collect the results.
|
||||
|
||||
```python
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
for block in tool_calls:
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
```
|
||||
|
||||
**Step 5**: Append the tool results as a new message and go back to Step 2.
|
||||
@@ -92,22 +96,24 @@ def agent_loop(messages):
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
|
||||
if response.stop_reason != "tool_use":
|
||||
tool_calls = [
|
||||
block for block in response.content if block.type == "tool_use"
|
||||
]
|
||||
if not tool_calls:
|
||||
return
|
||||
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
for block in tool_calls:
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
messages.append({"role": "user", "content": results})
|
||||
```
|
||||
|
||||
Under 30 lines — that's the minimal runnable agent harness kernel. It's not intelligence itself, but the smallest runtime framework that lets the model keep acting. The model decides (whether to call a tool, which one), the harness executes (calls the tool and appends the result as a new message). The next 16 chapters all add mechanisms on top of this loop. The loop itself never changes.
|
||||
Just over 30 lines — that's the minimal runnable agent harness kernel. It's not intelligence itself, but the smallest runtime framework that lets the model keep acting. The model decides (whether to call a tool, which one), the harness executes (calls the tool and appends the result as a new message). The next 16 chapters all add mechanisms on top of this loop. The loop itself never changes.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -25,12 +25,12 @@
|
||||
|
||||

|
||||
|
||||
一个 `while True` 循环,模型调用工具就继续,不调用就停。整个过程只有两个信号:
|
||||
一个 `while True` 循环,模型调用工具就继续,不调用就停。循环直接检查响应里的内容块:
|
||||
|
||||
| 信号 | 含义 | 循环动作 |
|
||||
|------|------|---------|
|
||||
| `stop_reason == "tool_use"` | 模型举手说"我要用工具" | 执行 → 结果喂回去 → 继续 |
|
||||
| `stop_reason != "tool_use"` | 模型说"我做完了" | 退出循环 |
|
||||
| 包含 `tool_use` block | 模型要求调用工具 | 执行 → 结果喂回去 → 继续 |
|
||||
| 不包含 `tool_use` block | 模型没有调用工具 | 退出循环 |
|
||||
|
||||
---
|
||||
|
||||
@@ -57,22 +57,26 @@ response = client.messages.create(
|
||||
|
||||
```python
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
tool_calls = [
|
||||
block for block in response.content if block.type == "tool_use"
|
||||
]
|
||||
if not tool_calls:
|
||||
return
|
||||
```
|
||||
|
||||
只有实际存在的 `tool_use` block 才会进入执行阶段,因此不会追加空的工具结果消息。
|
||||
|
||||
**第 4 步**:执行模型要求的工具,收集结果。
|
||||
|
||||
```python
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
for block in tool_calls:
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
```
|
||||
|
||||
**第 5 步**:把工具结果作为新消息追加,回到第 2 步。
|
||||
@@ -92,22 +96,24 @@ def agent_loop(messages):
|
||||
)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
|
||||
if response.stop_reason != "tool_use":
|
||||
tool_calls = [
|
||||
block for block in response.content if block.type == "tool_use"
|
||||
]
|
||||
if not tool_calls:
|
||||
return
|
||||
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
for block in tool_calls:
|
||||
output = run_bash(block.input["command"])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
messages.append({"role": "user", "content": results})
|
||||
```
|
||||
|
||||
不到 30 行,这就是最小可运行的 agent harness 内核。它为模型提供持续行动的最小运行框架:模型负责决策(要不要调工具、调哪个),harness 负责执行(调用工具,把结果作为新消息追加)。后面 16 个章节都在这个循环上叠加机制,循环本身始终不变。
|
||||
三十多行,这就是最小可运行的 agent harness 内核。它为模型提供持续行动的最小运行框架:模型负责决策(要不要调工具、调哪个),harness 负责执行(调用工具,把结果作为新消息追加)。后面 16 个章节都在这个循环上叠加机制,循环本身始终不变。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ s01_agent_loop.py - The Agent Loop
|
||||
|
||||
The entire secret of an AI coding agent in one pattern:
|
||||
|
||||
while stop_reason == "tool_use":
|
||||
while True:
|
||||
response = LLM(messages, tools)
|
||||
if response contains no tool_use:
|
||||
break
|
||||
execute tools
|
||||
append results
|
||||
|
||||
@@ -93,21 +95,23 @@ def agent_loop(messages: list):
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
|
||||
# If the model didn't call a tool, we're done
|
||||
if response.stop_reason != "tool_use":
|
||||
tool_calls = [
|
||||
block for block in response.content if block.type == "tool_use"
|
||||
]
|
||||
if not tool_calls:
|
||||
return
|
||||
|
||||
# Execute each tool call, collect results
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
print(f"\033[33m$ {block.input['command']}\033[0m")
|
||||
output = run_bash(block.input["command"])
|
||||
print(output[:200])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
for block in tool_calls:
|
||||
print(f"\033[33m$ {block.input['command']}\033[0m")
|
||||
output = run_bash(block.input["command"])
|
||||
print(output[:200])
|
||||
results.append({
|
||||
"type": "tool_result",
|
||||
"tool_use_id": block.id,
|
||||
"content": output,
|
||||
})
|
||||
|
||||
# Feed tool results back, loop continues
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
@@ -45,15 +45,15 @@
|
||||
<line x1="310" y1="176" x2="450" y2="176" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="380" y="194" fill="#475569" font-size="11" text-anchor="middle">Model reads message history</text>
|
||||
<text x="380" y="210" fill="#475569" font-size="11" text-anchor="middle">Decision: Need a tool?</text>
|
||||
<text x="380" y="228" fill="#64748b" font-size="10" text-anchor="middle">Returns stop_reason signal</text>
|
||||
<text x="380" y="228" fill="#64748b" font-size="10" text-anchor="middle">Returns content blocks</text>
|
||||
|
||||
<!-- Arrow: LLM → Decision (down) -->
|
||||
<line x1="380" y1="236" x2="380" y2="276" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== Decision Diamond ===== -->
|
||||
<polygon points="380,280 470,316 380,352 290,316" fill="#fff8f0" stroke="#d97706" stroke-width="2"/>
|
||||
<text x="380" y="312" fill="#92400e" font-size="12" font-weight="600" text-anchor="middle">stop_reason</text>
|
||||
<text x="380" y="326" fill="#92400e" font-size="10" text-anchor="middle">== "tool_use"?</text>
|
||||
<text x="380" y="312" fill="#92400e" font-size="12" font-weight="600" text-anchor="middle">tool_use block</text>
|
||||
<text x="380" y="326" fill="#92400e" font-size="10" text-anchor="middle">present?</text>
|
||||
|
||||
<!-- Arrow: No → End (right) -->
|
||||
<line x1="470" y1="316" x2="540" y2="316" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow-green)"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.4 KiB |
@@ -45,15 +45,15 @@
|
||||
<line x1="310" y1="176" x2="450" y2="176" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="380" y="194" fill="#475569" font-size="11" text-anchor="middle">モデルがメッセージ履歴を読む</text>
|
||||
<text x="380" y="210" fill="#475569" font-size="11" text-anchor="middle">判断:ツールが必要か?</text>
|
||||
<text x="380" y="228" fill="#64748b" font-size="10" text-anchor="middle">stop_reason シグナルを返す</text>
|
||||
<text x="380" y="228" fill="#64748b" font-size="10" text-anchor="middle">content block を返す</text>
|
||||
|
||||
<!-- 矢印:LLM → 判定(下) -->
|
||||
<line x1="380" y1="236" x2="380" y2="276" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== 判定ダイヤモンド ===== -->
|
||||
<polygon points="380,280 470,316 380,352 290,316" fill="#fff8f0" stroke="#d97706" stroke-width="2"/>
|
||||
<text x="380" y="312" fill="#92400e" font-size="12" font-weight="600" text-anchor="middle">stop_reason</text>
|
||||
<text x="380" y="326" fill="#92400e" font-size="10" text-anchor="middle">== "tool_use"?</text>
|
||||
<text x="380" y="312" fill="#92400e" font-size="12" font-weight="600" text-anchor="middle">tool_use block</text>
|
||||
<text x="380" y="326" fill="#92400e" font-size="10" text-anchor="middle">あり?</text>
|
||||
|
||||
<!-- 矢印:いいえ → 終了(右) -->
|
||||
<line x1="470" y1="316" x2="540" y2="316" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow-green)"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
@@ -45,15 +45,15 @@
|
||||
<line x1="310" y1="176" x2="450" y2="176" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="380" y="194" fill="#475569" font-size="11" text-anchor="middle">模型阅读消息历史</text>
|
||||
<text x="380" y="210" fill="#475569" font-size="11" text-anchor="middle">判断:需要工具吗?</text>
|
||||
<text x="380" y="228" fill="#64748b" font-size="10" text-anchor="middle">返回 stop_reason 信号</text>
|
||||
<text x="380" y="228" fill="#64748b" font-size="10" text-anchor="middle">返回内容块</text>
|
||||
|
||||
<!-- 箭头:LLM → 判断(向下) -->
|
||||
<line x1="380" y1="236" x2="380" y2="276" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== 判断菱形 ===== -->
|
||||
<polygon points="380,280 470,316 380,352 290,316" fill="#fff8f0" stroke="#d97706" stroke-width="2"/>
|
||||
<text x="380" y="312" fill="#92400e" font-size="12" font-weight="600" text-anchor="middle">stop_reason</text>
|
||||
<text x="380" y="326" fill="#92400e" font-size="10" text-anchor="middle">== "tool_use"?</text>
|
||||
<text x="380" y="312" fill="#92400e" font-size="12" font-weight="600" text-anchor="middle">tool_use block</text>
|
||||
<text x="380" y="326" fill="#92400e" font-size="10" text-anchor="middle">存在?</text>
|
||||
|
||||
<!-- 箭头:否 → 结束(向右) -->
|
||||
<line x1="470" y1="316" x2="540" y2="316" stroke="#16a34a" stroke-width="2" marker-end="url(#arrow-green)"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Reference in New Issue
Block a user