AI Workflow · part 4
[Claude Code] claude-agent-sdk vs subprocess:中間 Turn 為什麼消失了
❯ cat --toc
TL;DR
claude -p subprocess 會丟掉所有中間 turn — tool call、推理、進行中的輸出 — 只回傳最終文字。遷移到 claude-agent-sdk 的 query() async generator,可以即時串流每一個 turn,還有 session resume 和並行執行。
白話版:為什麼你的 Multi-Agent 系統只顯示最後的答案
想像你同時派三個助理去研究不同的主題,但你只能看到每個人的最終報告 — 看不到他們的工作筆記、中間發現,什麼都看不到,直到他們做完。用 subprocess 跑 AI agent 就是這樣:你拿到完成品,但錯過了中間的一切。
claude-agent-sdk 透過串流介面解決這個問題。不用等 subprocess 跑完,你的 orchestrator 可以即時收到每一個中間步驟 — tool call、推理、部分輸出。這篇記錄從 subprocess 遷移到 SDK 的過程、setting_sources 這個會把數千無用 token 載入每個 sub-agent 的陷阱,以及如何處理會忽略新指令的過期 session。
前言
跑 subprocess 就像寄一封附回郵信封的信。你知道送出去什麼,最後會收到一封回覆。中間發生了什麼——思考過程、工具呼叫、草稿——你看不到。對一個需要即時顯示多個 agent 並行輸出的 orchestrator 來說,信件往返模型完全行不通。
這篇是從 claude -p subprocess 遷移到 claude-agent-sdk 的過程,改了什麼,以及 subprocess 版本靜默丟掉了什麼。
claude -p 的問題是什麼?
最初的 orchestrator 用 subprocess.Popen 呼叫 claude -p:
result = subprocess.run(
["claude", "-p", task, "--output-format", "text"],
capture_output=True, text=True
)
output = result.stdout
單次、fire-and-forget 的呼叫沒問題。其他情境都有問題。
claude -p 回傳的是什麼: 只有最後一個 agent turn 的最終文字訊息。如果 agent 做了三次工具呼叫,在呼叫之間輸出了推理文字,最後輸出了摘要——subprocess 只回傳摘要。每一個中間 turn 都被丟掉了。
對一個需要即時顯示多個 agent 輸出的 MUD 風格終端介面來說,這代表使用者看不到任何東西,直到每個 agent 跑完——然後所有輸出同時出現。跟完全沒有串流一樣。
--output-format stream-json --verbose 存在,但解析原始 event stream 很脆,session resume 要手動處理,而且還是 subprocess,有一樣的生命週期限制。
SDK 的做法如何解決這個問題?
claude-agent-sdk(Python)提供 query(),一個非同步 generator,每個 turn 產出一條訊息——包括工具呼叫之間的每一個中間 turn。
from claude_agent_sdk import ClaudeAgentOptions, query
from claude_agent_sdk.types import AssistantMessage, ResultMessage, SystemMessage, TextBlock
async def run_agent(name: str, task: str, session_id: str | None = None):
options = ClaudeAgentOptions(
system_prompt=AGENT_CONFIGS[name]["system"],
disallowed_tools=["Task", "ExitPlanMode", "AskUserQuestion"],
permission_mode="bypassPermissions",
cwd=str(BASE_DIR),
setting_sources=["project", "local"],
**({"resume": session_id} if session_id else {}),
)
captured_session_id = session_id
async for message in query(prompt=task, options=options):
if isinstance(message, SystemMessage) and message.subtype == "init":
captured_session_id = message.data.get("session_id")
elif isinstance(message, AssistantMessage):
for block in message.content:
if isinstance(block, TextBlock) and block.text.strip():
yield block.text, captured_session_id
elif isinstance(message, ResultMessage):
captured_session_id = message.session_id or captured_session_id
return captured_session_id
每個 AssistantMessage 對應 agent 的一個 turn。工具呼叫之間輸出的文字各自是一個 TextBlock。Orchestrator 即時拿到每一段中間輸出。
Session Resume
SDK 支援 session resume:傳入之前的 session_id,agent 從上次結束的地方繼續,完整 context 保留。
# 第一次跑——沒有 session
session_id = await run_agent("nanase", "check system health")
# 存 session_id
registry[name] = session_id
save_registry(registry)
# 後續跑——resume session
session_id = await run_agent(
"nanase", "what was the root cause?",
session_id=registry.get(name)
)
Session ID 從 ResultMessage.session_id 取得(優先),或 SystemMessage.data["session_id"] 作為備選。按 agent 名稱存在 registry 裡。
注意:如果 agent 的 system prompt 或輸出格式改了,要刪掉 registry。舊 session 帶著舊 context,會跟新指令衝突。一個在舊 system prompt 下訓練成輸出 NDJSON 的 session,就算新 prompt 不再要求,還是會繼續輸出 NDJSON。
如何並行執行多個 Agent?
多個 agent 用 asyncio.gather 同時跑:
assignments = [
("nanase", "check docker containers"),
("futaba", "review the diff in /tmp/patch.py"),
("shion", "what positions should I open today"),
]
results = await asyncio.gather(*[
run_agent(name, task, registry.get(name))
for name, task in assignments
])
每個 agent 獨立執行,產出輸出就 yield,orchestrator 把輸出 multiplex 到終端(或各自的顯示區)。
需要互動輸入時(agent 需要使用者回答才能繼續),用 lock 序列化:
_input_lock = asyncio.Lock()
async def prompt_user(agent_name: str, question: str) -> str:
async with _input_lock:
return await asyncio.to_thread(input, f"\n{agent_name} ❓ > ")
Lock 防止兩個 agent 同時問問題。asyncio.to_thread 讓 event loop 在等使用者輸入時不被阻塞。
Disallowed Tools
一定要設:
disallowed_tools=["Task", "ExitPlanMode", "AskUserQuestion"]
Task— 防止 sub-agent 再生 sub-agent(打破 orchestrator 的控制模型)ExitPlanMode— 防止 agent 進入 plan mode 無限等待AskUserQuestion— 防止 agent 用內建問題 UI,繞過 orchestrator 的輸入處理
沒有這些,sub-agent 可以生出自己的 sub-agent、在 plan mode 卡死、或跳出一個 orchestrator 無從回應的問題視窗。
setting_sources
setting_sources=["project", "local"]
沒有這個設定,sub-agent 會繼承 ~/.claude/CLAUDE.md 和使用者全域設定裡所有 MCP server 的 tool schema。MCP server 多的系統,這代表每個 agent turn 都要載入數千 token 的 tool schema,而那些工具 sub-agent 永遠用不到。setting_sources=["project", "local"] 限制只載入 project 層級的設定,跳過全域 MCP schema。
對頻繁呼叫的 orchestrator 影響顯著。每小時 20 次 agent 呼叫,每次 3K token 的無用 MCP schema,一小時就是 60K token 的 context 噪音。
收穫
最花時間的地方:
搞清楚 claude -p 實際上回傳什麼。文件說「跑一個 prompt,拿結果」——準確,但埋了關鍵限制:「結果」是最後一個 turn 的最終文字,不是所有 turn 的累積輸出。這不是 bug,是單次使用場景下的設計選擇。SDK 才是複雜 orchestration 的預設工具。
可以複用的診斷方法:
- Subprocess 輸出不完整或缺少工具呼叫結果 → 用了
claude -p,中間 turn 沒有回傳。遷移到 SDK。 - Agent 無視新 system prompt,明明是全新執行 → session registry 有舊 ID。刪掉 registry 檔案。
- MCP tool schema 出現在不該出現的 agent context →
setting_sources載入了~/.claude/CLAUDE.md帶進了 MCP server。設setting_sources=["project", "local"]。
記住這一件事: Subprocess 是 fire-and-forget 的正確工具。串流 async generator 是需要中間輸出、session 狀態或並行組合的正確工具。用錯了原始工具,等系統建好了再重構代價很大。
遷移清單
從 claude -p subprocess 遷移到 claude-agent-sdk:
- 把
subprocess.run(["claude", "-p", task])換成async for message in query(prompt=task, options=options)。 - 加
disallowed_tools=["Task", "ExitPlanMode", "AskUserQuestion"]。 - 需要工具存取不問 permission 的話,設
permission_mode="bypassPermissions"。 - 設
setting_sources=["project", "local"]避免載入全域 MCP schema。 - 從
ResultMessage取session_id,存進 registry。 - 後續呼叫傳
resume=session_id。 - 並行 agent 用
asyncio.gather,互動輸入用asyncio.to_thread + Lock。
同系列:Debate System — 多個 Claude 互為 Peer Reviewer · 讓 Claude Code 指令真的被遵守
常見問題
- 為什麼 claude -p subprocess 在 multi-agent 系統中會丟棄中間 turn?
- claude -p 只回傳最後一個 agent turn 的最終文字。所有中間 turn——工具呼叫、推理文字、進行中的輸出——全被丟棄。改用 claude-agent-sdk 的 async query() generator,就能即時收到每一個 turn。
- 如何用 Python 串流多個 Claude agent 的即時輸出?
- 使用 claude-agent-sdk 的 query() async generator 搭配 asyncio.gather 並行執行。每個 AssistantMessage 會 yield 該 turn 的文字 block,orchestrator 可以在 token 產生時就收到中間輸出,而非等最終結果。
- claude-agent-sdk 中設定 setting_sources=['project', 'local'] 有什麼效果?
- 防止 sub-agent 載入全域 ~/.claude/CLAUDE.md 和所有 MCP server 的 tool schema。若不設定,每次 agent 呼叫都會載入數千 token 的 schema——以每小時 20 次呼叫計算,會產生 60K token 的 context 噪音。
- 如何修復忽略新 system prompt 的 Claude sub-agent?
- 刪除 session registry 檔案。舊的 session_id 帶著過去的 context,包括該 session 中訓練好的輸出格式。解決方法:刪除 registry,讓 agent 在下次執行時建立新 session。
接著讀
- 2026-07-16[Dev Workflow] 一群 AI 交接工作卻不用重講:原來是同一套系統的兩條軸
這是 AI Workflow 系列裡『兩軸合流』這一段的收尾。回頭看整段路,前面六篇其實在蓋同一套系統的兩條軸:持久知識(我知道什麼,長效、要用再查)和即時狀態(我現在做到哪,易逝、隨時寫)。這篇講清楚兩條軸為什麼互不代替、怎麼匯流到『多 AI 交接』、又受同一套哲學管——以及那條讓一群不完全信任的模型還能安全協作的中心律:沒有收據就不信。
- 2026-07-29[Dev Workflow] Skill 裡的檢查完全寫對了,但它一次都沒跑過
一篇技術文五關全過,唯一的人類讀者一句話就命中:同一個句型在稿子裡出現了五次。查下去發現該抓的規則白紙黑字寫在 skill 裡、內容也對,但它標著「人工掃」——沒有指令、沒有 exit code、擋不住任何東西,所以從來沒運作過。門檻怎麼用五篇舊文校準、兩次真實攔截、以及一個機械檢查永遠抓不到的變體。
- 2026-07-17[Dev Workflow] 幫 AI agent 的 skill 減肥:193 個塞爆 2% 預算,砍到剩 7 個
AI Workflow 系列第 15 篇。某天早上我讀 codex 紀錄,滑到一行:skill 描述被剪短,才塞得進 2% 的 context 預算。我共用的 root 累到 193 個 skill,全載進去撐爆上限,每條描述都被截斷——我天天在用的,被剪掉是為了替我從不碰的一百多個騰位子。這篇講怎麼用 per-agent allowlist(不是刪)砍到 7 個、薄殼配深引擎壓低每個 skill 的載入成本,再用一個閘門加稽核加可逆退場區守住,不讓它肥回去。
- 2026-07-15[Dev Workflow] 搜尋找得到,卻要每個 session 重推一遍:幫 AI 記憶加一層蒸餾層
我拿一批 markdown 檔當 AI 的長期記憶,在檢索(搜尋加知識圖譜)之上再疊一層蒸餾層。檢索撈回的是原始材料,每個 session、每個 agent 都得重讀重推一遍;蒸餾層把定案結論冶煉一次、存成一檔一個 claim,之後直接取用。這篇講清楚這層是什麼、為什麼它的目的是『少解釋一次』而不是『逼大家講一樣』、以及我怎麼學會真的去信它。
不想錯過新文章?
訂閱我確保不漏接!
隨時一鍵退訂。