AI 工程

Claude Code 2.1.239 拆解:`/claude-api upgrade` 一鍵遷移,與三個「不報錯」的靜默失效

2026-08-22 這天,anthropics/claude-code 推了 2.1.239。這個版本的 changelog 有 68 條,是近期最長的一次。但真正值得你花十分鐘讀完的,不是那 68 條裡面最顯眼的新功能,而是三個「靜默失效」類型的修正——它們不會報錯、不會警告,只會安靜地什麼都不做

如果你在 Windows 上寫過 subagent、在 monorepo 裡用過 worktree、或是專案還釘在 anthropic 0.x SDK,這一版大概率修掉了你曾經懷疑過自己、最後歸咎於「AI 就是不穩」的某個問題。

這篇把三件事拆開講清楚:/claude-api upgrade 這個新的一鍵遷移子指令實際上做了什麼、.worktreeinclude**/ 為什麼會靜默匹配不到、以及 UTF-8 BOM 怎麼讓你的 agent 檔案憑空消失。每一段都給可以直接照做的指令。


一、/claude-api upgrade:Anthropic Python SDK 0.x → 1.x 的可執行遷移腳本

為什麼現在需要它

先講背景。Anthropic 的 Python SDK anthropic2026-08-20 發布了 1.0.0(PyPI 上 info.version1.0.0,upload_time 2026-08-20T19:58:58)。這是這個套件第一次跨主版號,帶了幾個會直接讓程式炸掉的破壞性變更:

  • requires-python>=3.9 提到 >=3.10
  • HTTP 層從 httpx 換成 httpx2(PyPI 上 anthropic 1.0.0 的依賴寫的是 httpx2<3,>=2.0.0
  • 舊的 Text Completions API(/v1/complete)整組移除
  • temperature / top_p / top_k 從 messages 方法簽章移除

httpx2 這個名字第一次看到會讓人心裡一驚——這是不是搶註冊的假套件?我去 PyPI 查了:httpx2 的 project_urls 指向 github.com/pydantic/httpx2,author 欄位是 Tom Christie <tom@tomchristie.com>,也就是 httpx 原作者本人,目前由 Pydantic 團隊維護,版本線從 2.0 起算(我查的時候最新是 2.12.0)。所以它是官方的維護分支,不是 typosquat。Claude Code 內附的遷移文件甚至專門叮嚀 agent 要在報告裡寫一行 provenance,因為「reviewers and supply-chain scanners flag unfamiliar package names as possible typosquats」——這個細節看得出來寫的人真的踩過這個場面。

它不是「幫你改一改」,是一份 13 步的執行腳本

2.1.239 的 changelog 這樣寫:

Added /claude-api upgrade to migrate Python projects from anthropic 0.x to 1.x, and updated the skill's Python reference for 1.x (timeouts use anthropic.Timeout, not httpx.Timeout)

我沒有在正式專案上跑過這個遷移,但我把 2.1.239 的 binary 裡打包的那份 python/claude-api/sdk-upgrade.md 完整撈出來讀了一遍(約 29 KB)。它的設計方式很值得學:它不是給模型一段「請幫使用者升級 SDK」的模糊指令,而是一份 Step 0 到 Step 12 的可執行 checklist,開頭第一句就是:

If you arrived via /claude-api upgrade: this is the right file. Execute the steps below in order — do not summarize them back to the user.

這句「do not summarize them back to the user」是整份文件的靈魂。它在對抗 coding agent 最常見的失敗模式:讀完一份長文件,然後跟你講一遍它讀到什麼,而不是動手做。

實際流程長這樣:

Step 0 — 先確認範圍、現況版本、目標版本。 文件明確規定:如果請求沒指名檔案或目錄,就問一個問題、給三個選項(整個工作目錄/某個子目錄/指定檔案清單)然後upgradeupgrade python、「幫我升到 v1」全部算範圍不明。另外它要求在寫任何 pin 之前,先用 pip index versions anthropiccurl -s https://pypi.org/pypi/anthropic/json 確認 1.x 真的發布了——不准憑印象寫版號

Step 1 — 用 grep 建 inventory,這份清單同時是最後的驗收表。 文件給了一張 signal 表,每個訊號對應到後面的哪一步。摘幾個關鍵的:

grep 訊號 它會抓到什麼
import httpx, from httpx 可能把 httpx 物件交給 SDK 的模組
respx, pytest_httpx, vcr, HTTPXClientInstrumentor 會靜默停止攔截 SDK 流量的 mock / APM
with_raw_response raw response 呼叫點
completions.create, HUMAN_PROMPT, AI_PROMPT 已移除的 Text Completions
temperature, top_p, top_k 已移除的取樣參數
AnthropicBedrock( 可能依賴舊 region fallback 的 Bedrock client

它還要求對每個 hit 先分類再動手:SDK 呼叫點(改)/同名但無關(不動,例如恆溫器的 temperature 變數、urllib.parse)/測試(改,但要保持測試有意義)/文件與 notebook(改,.ipynb 要改 cell source 字串並保持 JSON 合法)。這個「先分類再編輯」的紀律,是你自己寫 migration prompt 時最該抄走的一招。

Step 3 是最容易漏的一步。 httpxhttpx2 只在物件跨越 SDK 邊界時才要改:timeout=30.0 這種純值完全不用動,但 httpx.Timeouthttpx.Limits、transport、整個 httpx.Clienthttp_client= 傳進去,就必須來自 httpx2——舊 httpx 的 client 傳進去會在建構時直接 TypeError。文件特別點名:你自己寫的中介層也算,例如 class TracingTransport(httpx.BaseTransport) 子類、wrapper 內層委派的 httpx.HTTPTransport()httpx.Auth flow、event_hooks callable 的型別註記,全部要 re-base。

最好的做法其實是直接用 SDK 自己的 re-export,連 import 都省掉:

# 這四個都已經是 httpx2-based,簽章不變
from anthropic import (
    Timeout,
    DefaultHttpxClient,
    DefaultAsyncHttpxClient,
    DefaultAioHttpClient,
)

這就是 changelog 那句「timeouts use anthropic.Timeout, not httpx.Timeout」的意思。

而如果你的專案是應用程式(不是函式庫),還有一招更省事:

# entry point 的最前面幾行,必須在任何東西 import httpx / httpcore 之前
import httpx2

httpx2.alias_httpx()

import httpx  # 現在這就是 httpx2 模組

兩條硬規則:它必須跑在任何 httpx / httpcore import 之前(否則 RuntimeError;重複呼叫是 no-op);而且只給應用程式用,絕不要塞進函式庫的 import path

這裡有個真正陰險的坑值得單獨標紅:respxpytest-httpxvcrpy、OpenTelemetry 的 HTTPXClientInstrumentor、Sentry 的 httpx integration,全部是靠 patch httpx 運作的。升到 1.x 之後它們 import 依然成功、測試依然綠、trace 依然有輸出——只是再也看不到 SDK 的請求了。沒有任何東西會報錯。 修法是同一個 alias_httpx(),但要早於它們載入。pytest 的作法是掛成 early plugin:

# tests/_alias_httpx.py
import httpx2

httpx2.alias_httpx()
# pyproject.toml
[tool.pytest.ini_options]
addopts = "-p tests._alias_httpx"
pythonpath = ["."]

注意是 merge 進既有的 addopts,不是覆蓋掉。

Step 4 — .with_raw_response 的回傳型別換了。 以前是 LegacyAPIResponse,現在是 APIResponse / AsyncAPIResponse。兩個後果:async client 上 parse() / json() / text() / read() 變成 coroutine 要 await;而且 .text.content 在同步 client 上也變成方法了

0.x 1.x 同步 1.x async
r.parse() r.parse() await r.parse()
r.text r.text() await r.text()
r.content r.read() await r.read()
.headers / .status_code / .request_id 不變 不變(純屬性,不要 await)

Step 6 — temperature / top_p / top_k 被移除,但這不是模型不支援了。 文件講得很清楚:它們是從 1.x 的簽章消失,不是從 API 消失。傳了會 TypeError。如果你的呼叫釘的是還接受這些參數的舊模型、而且程式明確依賴這個設定(有文件化的 determinism 需求、正在做 temperature A/B),正確做法是搬進 extra_body

# Before
client.messages.create(..., model="claude-sonnet-4-6", temperature=0.2)

# After(僅限:釘的模型確實接受,且程式確實依賴)
client.messages.create(..., model="claude-sonnet-4-6", extra_body={"temperature": 0.2})

哪些模型還吃這組參數,是模型層的問題不是 SDK 層的問題。依照那份文件的描述:Opus 4.7 以後的模型只要請求帶了這三個之一(即使是預設值)就回 400;4.6 / 4.5 那條線(Opus 4.6、Sonnet 4.6、Opus 4.5、Sonnet 4.5、Haiku 4.5)以及仍在服務的已棄用 Claude 4 模型則還接受。

Step 10 — Bedrock 現在強制要 region。 AnthropicBedrock() 以前沒設 region 會警告並 fallback 到 us-east-1,現在直接在建構時丟 ValueError。文件對 agent 下了一條很好的指令:如果判斷不出部署環境有沒有提供 region,不准亂猜一個填進去,要列進報告請使用者確認。這條「不確定就升級成人類決策,而不是編一個看起來合理的值」的規則,值得抄進你自己所有的 migration prompt。

怎麼用(照做版)

# 1. 先升到 2.1.239 以上
claude update && claude --version

# 2. 在專案根目錄開 session,明確給範圍,不要給模糊指令
claude
> /claude-api upgrade python src/

# 3. 想先看不想改,就講清楚
> /claude-api upgrade python src/ — 只出報告和 diff,先不要寫檔

小提醒:upgrade 目前只有 Python 有 sdk-upgrade.md。文件明寫,如果偵測到的語言沒有對應的升級指南,agent 應該直說「沒有 bundled 指南」並指向該 SDK 的 CHANGELOG,不准拿 Python 那份改編。所以 TypeScript 專案打這個指令,正確行為是被婉拒。

另外別搞混同一個 skill 的三個子指令:migrate 是換模型prompt-audit 是稽核舊模型時代的 prompt 寫法upgrade 才是換 SDK 主版號


二、.worktreeinclude**/ 為什麼會靜默匹配不到

.worktreeinclude 複製流程與 2.1.239 修正點

先講 .worktreeinclude 是什麼

Claude Code 的 worktree(claude --worktree feature-xEnterWorktree 工具、subagent 的 isolation: worktree)本質是 git worktree add,所以它是一份乾淨的 checkout——你的 .envconfig/secrets.json 這些被 gitignore 的檔案,在新 worktree 裡不存在。

官方文件的解法是在專案根目錄放一個 .worktreeinclude

.env
.env.local
config/secrets.json

文件寫得很精確:「The file uses .gitignore syntax. Only files that match a pattern and are also gitignored are copied, so tracked files are never duplicated.」——注意這是一個交集:既符合你的 pattern、又確實被 git 忽略。這個設計是對的(避免重複複製已追蹤檔案),但也正是 bug 的來源。

機制拆解

我把 2.1.239 binary 裡 copyWorktreeIncludeFiles 的實作撈出來讀了。流程是四步:

  1. .worktreeinclude,去掉空行與 # 註解,建一個 gitignore matcher。
  2. git ls-files --others --ignored --exclude-standard --directory 取得候選清單。
  3. 把清單拆成「檔案」和「目錄」。檔案直接用 matcher 過濾;目錄則要先判斷值不值得展開,值得的才用 git ls-files ... -- <那些目錄> 再列一次。
  4. 逐一複製,跳過 symlink,也跳過會經由 committed symlink 逃出 worktree 的目標路徑。

關鍵在第 2 步那個 --directory 旗標。它會把整個都被忽略的目錄摺疊成一行。我在本機開了個 repo 實測:

$ cat .gitignore
secrets/
.env
build/

$ git ls-files --others --ignored --exclude-standard --directory
.env
build/
packages/
packages/api/
packages/api/secrets/
secrets/

$ git ls-files --others --ignored --exclude-standard      # 不加 --directory
.env
build/out.txt
packages/api/secrets/api.json
secrets/prod.json

看到了嗎——加了 --directorysecrets/prod.jsonpackages/api/secrets/api.json 根本不在候選清單裡,只剩下 secrets/ 這種目錄項。Claude Code 必須自己決定要不要鑽進去。

bug 長在哪

2.1.239 之前,決定「要不要展開這個目錄」的判斷大致只有兩條:pattern 的字面開頭是不是這個目錄名、或 pattern 在第一個萬用字元之前的字面前綴是不是這個目錄的前綴(程式碼裡是 m.search(/[*?[]/) 且要求 index 大於 0)。

於是 **/secrets/*.json 這種 pattern 就死得很安靜:

  • "**/secrets/*.json".startsWith("secrets/")false
  • 第一個萬用字元在 index 0,所以「字面前綴」是空字串,index > 0 的條件不成立 → 跳過
  • matcher 拿 secrets/ 這個目錄本身去比 **/secrets/*.json → 也不匹配(pattern 要的是目錄裡的檔案)

三條都不成立 → 這個目錄不展開 → 裡面的 .json 從頭到尾沒進過候選清單 → 複製了 0 個檔案,沒有任何警告。你只會在新 worktree 裡發現 secrets 不見了,然後開始懷疑是不是自己 pattern 寫錯。

changelog 對這條的描述是:

Fixed .worktreeinclude patterns starting with **/ silently matching nothing when the target lived in a gitignored directory

修法

2.1.239 在判斷式裡加了第三條:把 pattern 開頭的 **/ 剝掉,取剩下的第一個路徑片段的字面部分,拿去跟這個摺疊目錄的每一個路徑片段做(不分大小寫的)比對——沒有萬用字元就比相等、有萬用字元就比前綴。

套回上面的例子:**/secrets/*.json → 剝掉 **/secrets/*.json → 第一段是 secrets(無萬用字元)→ secrets/ 的片段是 ["secrets"],命中;packages/api/secrets/ 的片段是 ["packages","api","secrets"],也命中。兩個目錄都展開,兩個 .json 都被複製。

給你的可操作結論

  1. 升到 2.1.239 之後,回頭檢查你 .worktreeinclude 裡所有 **/ 開頭的 pattern——它們以前可能一直是空轉的。
  2. 想少踩坑,能寫具體路徑就別寫 **/config/secrets.json 這種寫法從第一版就走 m.startsWith(dir) 那條,一直都有效。
  3. 每次改完 .worktreeinclude,用這條指令自己驗一次候選清單(這是 Claude Code 內部跑的第一條指令,你可以完全複製):
    git ls-files --others --ignored --exclude-standard --directory
    

    如果你要的檔案沒出現在這裡、而它的上層目錄被摺疊成一行,那就是需要「展開判斷」的那條路徑。

  4. 兩個 pattern 之外的硬限制,文件寫得很明白,別浪費時間 debug:.worktreeinclude 裡的 symlink 一律跳過;而只要你設了 WorktreeCreate hook 取代預設 git 邏輯,--worktree 就完全不處理 .worktreeinclude,要在 hook script 裡自己複製。

三、UTF-8 BOM 讓你的 agent / skill / command 憑空消失

第三條靜默失效,是三個裡面最便宜、也最容易中的:

Fixed agents, skills, and commands whose .md file starts with a UTF-8 BOM being silently ignored

Claude Code 靠 .md 開頭的 --- 判斷 YAML frontmatter。UTF-8 BOM 是三個位元組 EF BB BF,加在最前面之後,第一行就不是 --- 而是 ---,frontmatter 解析失敗,整個檔案被跳過。不是報錯,是跳過——你的 /my-command 就是不存在,.claude/agents/reviewer.md 就是不出現在 agent 清單裡。

誰會不小心加上 BOM?主要是 Windows:PowerShell 的 Out-FileSet-Content 在舊版預設寫 BOM、記事本另存 UTF-8 預設帶 BOM、部分編輯器的「UTF-8 with BOM」選項、以及某些從 Excel 匯出的流程。這也是為什麼這個 bug 對 Windows 使用者殺傷力遠大於 macOS / Linux。

我在本機驗了偵測與修復指令:

# 偵測:列出 .claude 底下所有開頭是 BOM 的 .md
find .claude -name '*.md' -exec sh -c \
  'head -c3 "$1" | od -An -tx1 | grep -q "ef bb bf" && echo "BOM: $1"' _ {} \;

# 一次修掉
find .claude -name '*.md' -exec sh -c \
  'head -c3 "$1" | od -An -tx1 | grep -q "ef bb bf" && { sed -i "1s/^\xEF\xBB\xBF//" "$1"; echo "fixed: $1"; }' _ {} \;

單檔快速確認也可以直接用 file:帶 BOM 會顯示 Unicode text, UTF-8 (with BOM) text,乾淨的顯示 ASCII text

順帶一提,我試過 grep -rlI $'\xef\xbb\xbf' 這種寫法,在我的環境下抓不到(locale 因素),別依賴它——上面那個 head -c3 | od 的組合才穩。

根治的作法是在 repo 裡加一條 .gitattributes,並且在 Windows 端把 PowerShell 的預設編碼釘死:

# PowerShell 7+,寫進 $PROFILE
$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8NoBOM'
$PSDefaultParameterValues['Set-Content:Encoding'] = 'utf8NoBOM'

如果團隊有 CI,最省事的是加一個 pre-commit 檢查,把上面那條 find 指令的偵測版接上 exit code。


四、其他三條你應該知道的修正

2.1.239 那 68 條裡,還有幾條的影響面比看起來大:

Bedrock 串流被 proxy 剝掉 Content-Type,會讓 API 呼叫數量默默翻倍。 changelog 原文:「Fixed Bedrock streaming behind proxies that strip the response Content-Type header, which silently doubled billed API calls by re-running every turn non-streaming」。這條是真金白銀——如果你在企業 proxy 後面走 Bedrock,過去的帳單可能有一半是重跑的非串流請求。這是三個「靜默」bug 裡唯一會直接反映在成本上的。

Esc 的 race condition 可能讓 agent 重複執行動作。 「Fixed a race where pressing Esc with a prompt queued could let the next turn finish early, leaving the session idle while Claude was still working and letting a later resubmit repeat actions」——排隊了 prompt 又按 Esc,session 看起來閒置但 Claude 還在跑,之後重送會重複動作。如果你有「按 Esc 打斷後重新送出」的習慣,這條值得留意。

/cost 的成本估算改了。 「Cost estimates (/cost, status line, --max-budget-usd) now include the 1.1× US-only-inference premium for data-residency workspaces」——如果你在 data-residency workspace,過去 /cost 少報了 10%。這不是漲價,是估算對齊實際計費。


五、限制與誠實標註

幾個我要講清楚的邊界:

  • 我沒有在真實專案上跑過 /claude-api upgrade 上面關於它行為的描述,全部來自兩個一手來源:2.1.239 changelog,以及我從 2.1.239 binary 裡完整撈出的那份 python/claude-api/sdk-upgrade.md。實際跑起來 agent 會不會嚴格照 Step 0 問範圍,我沒有實測數據。
  • git ls-files --directory 的摺疊行為、以及 BOM 的偵測/修復指令,是我在本機實跑驗證過的。 worktree 那段的「修法邏輯」則是讀 binary 裡的實作推出來的,不是官方文件敘述,所以我盡量描述到可驗證的層次(哪個判斷式、哪個條件不成立),而不是宣稱 Anthropic 內部怎麼想。
  • anthropic 1.0.0 的版本、日期、依賴,來自 PyPI JSON API 的一手資料version: 1.0.0upload_time: 2026-08-20T19:58:58requires_python: >=3.10httpx2<3,>=2.0.0)。SDK 的破壞性變更清單以 anthropic-sdk-python repo 的 MIGRATION.md 為準——Claude Code 內附那份文件自己也寫了「如果兩者衝突,以 MIGRATION.md 為準並在報告裡說明」。
  • 關於哪些模型還接受 temperature,我引用的是 Claude Code 內附文件的敘述,不是我實測 API 回應。 真要遷移,請自己打一次 API 確認。

六、對工程團隊的意義:一份可以今天就做的清單

2.1.239 三個靜默失效的檢查清單

這一版真正的訊號不是「多了什麼功能」,而是 Anthropic 花了 68 條的篇幅在補「不報錯的失敗」。對 coding agent 這種工具,靜默失效比 crash 危險得多:crash 你會修,靜默失效你會歸咎於「模型今天狀態不好」,然後在錯誤的方向上浪費一個下午。

今天就可以做的五件事:

  1. claude update,確認 claude --version ≥ 2.1.239。
  2. 掃 BOM:把上面那條 find 指令跑一次,順便掃 ~/.claude/ 和團隊共用的 plugin repo。有 Windows 成員的團隊尤其該做,並考慮加進 CI。
  3. .worktreeinclude:跑一次 git ls-files --others --ignored --exclude-standard --directory,確認你要帶進 worktree 的檔案,其上層目錄有沒有被摺疊。把所有 **/ 開頭的 pattern 標記出來重新測一次;能改成具體路徑就改掉。
  4. Bedrock + 企業 proxy 的團隊,回頭看一下這個月的 API 呼叫量。 如果有非預期的翻倍,很可能就是那個 Content-Type 的問題。
  5. anthropic 還在 0.x 的專案,先跑一次唯讀版的 /claude-api upgrade python <dir> — 只出報告和 diff,把 Step 1 的 inventory 拿到手。就算你最後決定自己改,那份 grep 清單也已經幫你把範圍框好了——特別是 respx / pytest-httpx / OpenTelemetry 那條,靠人工 review 幾乎不可能想到。

最後一個 meta 層級的收穫:如果你自己在寫 migration 類型的 skill 或 prompt,sdk-upgrade.md 這份文件是很好的範本。它做對的三件事——開頭就禁止 agent 只做摘要先 grep 建 inventory 再分類再編輯把「不確定的值」升級成人類決策而不是讓模型編一個——換到任何一個大規模程式碼變更任務都成立。


來源

整理:DataAgent · Coding Agent 實戰教學

發表迴響

%d 位部落客按了讚: