自分の運用しているリポジトリを grep して、該当 0 件という結果に一度は安心しました。
安心してから、手が止まりました。プロンプトの本体はコードの中ではなく、Console 側に保存されている——そのことに気づいたからです。コードを検索して何も出てこないことは、影響がないことの証明にはなりません。
残り3日です。移行の手順そのものより、何を数え、何を手元に持ち直すかを先に決めます。
8月17日に止まるのは、3つのエンドポイントと保存済みの資産です
2026年7月17日の告知から31日という短い予告で、次の2つが同じ日に終わります。
| 対象 | 期日 | 終了後の挙動 |
|---|---|---|
| 旧 Workbench(platform.claude.com/workbench) | 2026年8月17日 | アクセス終了。保存済みプロンプト・変数・評価は新しい Workbench では扱えません |
/v1/experimental/generate_prompt | 2026年8月17日 | リクエストはエラーを返します |
/v1/experimental/improve_prompt | 2026年8月17日 | 同上 |
/v1/experimental/templatize_prompt | 2026年8月17日 | 同上 |
後継のエンドポイントは案内されていません。つまり「呼び先を差し替える」形の移行ではなく、その処理を自分の側に持つ形の移行になります。
保存済みのデータは、Console のバナーと Organizational Settings からエクスポートできます。移行作業のうち、これだけは取り返しがつきません。先に済ませてください。
コードの grep は、影響範囲の半分しか教えてくれません
まず数えます。3つのエンドポイントは URL 文字列で呼ばれていることがほとんどなので、1行で足ります。
grep -rIn -E "experimental/(generate|improve|templatize)_prompt" \
--include='*.{py,ts,tsx,js,mjs,sh,yml,yaml,json}' .個人開発で運用している4サイト分のソースに対して実行し、ヒットは 0 件でした。プロンプトはすべてリポジトリ内のテキストファイルに置き、スクリプトから読ませる形にしていたためです。
ただ、この 0 件で安心してよかったのは、たまたま Workbench 側にプロンプトを保存していなかったからにすぎません。Workbench で書いて、そこから手でコピーして使っていた場合、コードには痕跡が残りません。この場合の依存は「文面そのものがそこにしかない」という形で存在します。
コード検索と合わせて、次の2つを見てください。
- Console の Usage ページから使用量 CSV をエクスポートする(API キー別・モデル別の実トラフィックが出ます)。設定ファイルは書き換え忘れで嘘をつきますが、実際のトラフィックは嘘をつきません
- Workbench に保存したプロンプトを、ファイルとしてリポジトリに移す。移した時点で、この先どのツールが終了しても影響を受けなくなります
2 のほうが本質的だと考えています。今回の終了で失われるのは機能ではなく、保管場所を他人に預けていたことへの請求書だからです。
templatize_prompt は、20行ほどのスクリプトで置き換えられます
templatize_prompt は、具体値の入ったプロンプトを {{VARIABLE}} 形式のテンプレートへ変換する処理でした。決定的な処理なので、モデルに頼む必要はありません。手元で持てます。
#!/usr/bin/env python3
"""具体値の入ったプロンプトを再利用可能なテンプレートへ変換する。"""
import argparse, json, re, sys
def templatize(prompt: str, values: dict[str, str]) -> tuple[str, list[str]]:
# 長い値から先に置換する。短い値が長い値の一部を先に食うのを防ぐため。
ordered = sorted(values.items(), key=lambda kv: len(kv[1]), reverse=True)
template, used = prompt, []
for name, value in ordered:
if not value:
continue
if value not in template:
print(f"warning: value for {name} not found in prompt: {value!r}", file=sys.stderr)
continue
template = template.replace(value, "{{" + name + "}}")
used.append(name)
return template, used
def fill(template: str, values: dict[str, str]) -> str:
return re.sub(r"\{\{(\w+)\}\}", lambda m: values.get(m.group(1), m.group(0)), template)
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("prompt_file")
ap.add_argument("values_file", help='JSON: {"VARIABLE_NAME": "プロンプト中の実際の文字列"}')
args = ap.parse_args()
prompt = open(args.prompt_file, encoding="utf-8").read()
values = json.load(open(args.values_file, encoding="utf-8"))
template, used = templatize(prompt, values)
# 逆変換で元に戻らなければ、置換のどこかが壊れている
if fill(template, values) != prompt:
print("round-trip check failed: template does not restore the original", file=sys.stderr)
return 1
print(template)
print(f"\n--- variables: {', '.join(used)} ---", file=sys.stderr)
return 0
if __name__ == "__main__":
raise SystemExit(main())入力にこのプロンプトを与えます。
あなたはECサイトの問い合わせ対応担当です。
商品「ワイヤレスイヤホン Model A」について、配送が遅れているという問い合わせに、
丁寧な日本語で3文以内で返信してください。注文番号は ORD-20260814-0031 です。変数の定義はこうです。
{
"PRODUCT_NAME": "ワイヤレスイヤホン Model A",
"ISSUE": "配送が遅れている",
"ORDER_ID": "ORD-20260814-0031",
"MAX_SENTENCES": "3"
}実行結果です。
あなたはECサイトの問い合わせ対応担当です。
商品「{{PRODUCT_NAME}}」について、{{ISSUE}}という問い合わせに、
丁寧な日本語で{{MAX_SENTENCES}}文以内で返信してください。注文番号は {{ORDER_ID}} です。
--- variables: PRODUCT_NAME, ORDER_ID, ISSUE, MAX_SENTENCES ---置換の順序が結果を変えました
書き始めたときは、辞書を順に回して replace すれば済むと思っていました。実際に別のプロンプトで試すまでは。
2026年3月の売上について、3文以内で要約してください。MAX_SENTENCES に "3" を割り当てた状態で、長さを考えずに置換すると、こうなります。
2026年{{MAX_SENTENCES}}月の売上について、{{MAX_SENTENCES}}文以内で要約してください。日付の「3」まで変数になりました。テンプレートとしては一見それらしく、埋め直すと文が壊れます。長い値から順に置換するだけでこの事故は消えますが、気づくきっかけがないのが厄介なところです。
だから逆変換のチェックを最後に入れています。テンプレートに値を埋め直して元の文と一致しなければ、置換のどこかが壊れています。5行のこの確認が、静かな破損を出口で止めてくれます。1文字や2文字の値を変数にするときは、そもそも変数にしない選択も検討してください。
improve_prompt は、メタプロンプトを自分で持つほうが長持ちします
improve_prompt の中身は、要するに「プロンプトを推敲させるプロンプト」です。Messages API から自分で呼べば、推敲の観点を自分で決められます。実験的エンドポイントに預けていたときは、この観点がブラックボックスでした。
#!/usr/bin/env python3
"""プロンプトを Claude に推敲させる。improve_prompt の置き換え。"""
import argparse, json, os, urllib.error, urllib.request
ENDPOINT = "https://api.anthropic.com/v1/messages"
META_PROMPT = """あなたはプロンプトの推敲役です。渡されたプロンプトを、
次の観点だけを直して書き直してください。意味を変えないこと。
1. 指示の順序を「役割 → 入力 → 制約 → 出力形式」に並べ替える
2. 曖昧な語(適切に・いい感じに 等)を検証可能な条件に置き換える
3. 出力形式を明示する(形式の指定がない場合のみ追加する)
4. プレースホルダはそのまま残す
書き直したプロンプトだけを出力してください。前置きや説明は書かないでください。"""
def build_payload(prompt: str, model: str) -> dict:
# temperature / top_p / top_k は入れない。既定値以外を指定すると
# Opus 4.7 以降と Sonnet 5 では 400 が返る。
return {
"model": model,
"max_tokens": 2000,
"system": META_PROMPT,
"messages": [{"role": "user", "content": prompt}],
}
def call(payload: dict, api_key: str) -> str:
req = urllib.request.Request(
ENDPOINT,
data=json.dumps(payload).encode("utf-8"),
headers={
"content-type": "application/json",
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
},
)
try:
with urllib.request.urlopen(req, timeout=120) as res:
body = json.load(res)
except urllib.error.HTTPError as e:
raise SystemExit(f"HTTP {e.code}: {e.read().decode('utf-8', 'replace')}")
except urllib.error.URLError as e:
raise SystemExit(f"network error: {e.reason}")
return "".join(b.get("text", "") for b in body.get("content", []) if b.get("type") == "text")
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("prompt_file")
ap.add_argument("--model", default="claude-sonnet-5")
ap.add_argument("--dry-run", action="store_true", help="送信せずにリクエスト本文を表示する")
args = ap.parse_args()
prompt = open(args.prompt_file, encoding="utf-8").read().strip()
payload = build_payload(prompt, args.model)
if args.dry_run:
print(json.dumps(payload, ensure_ascii=False, indent=2))
return 0
api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key:
raise SystemExit("ANTHROPIC_API_KEY が設定されていません")
print(call(payload, api_key))
return 0
if __name__ == "__main__":
raise SystemExit(main())--dry-run を付けると、送信せずにリクエスト本文だけを確認できます。移行作業では、まずこれで形を見てから鍵を通すほうが安全です。
{
"model": "claude-sonnet-5",
"max_tokens": 2000,
"system": "あなたはプロンプトの推敲役です。...",
"messages": [
{
"role": "user",
"content": "レビューをいい感じに要約して。"
}
]
}鍵が未設定のときは、送信前に ANTHROPIC_API_KEY が設定されていません で終了します(終了コード 1)。無人実行に組み込むなら、この形で早く落ちるほうが後始末が楽になります。
Workbench の設定をそのまま移すと 400 が返ります
ここが移行で最もつまずきやすい箇所だと感じています。Workbench の画面には temperature のスライダーがありました。同じ値を移行先のコードに書き写したくなります。
ところが temperature・top_p・top_k は、Opus 4.7 以降のモデルと Sonnet 5 では既定値以外を指定すると 400 が返ります。しかも SDK のリクエスト型にはこれらのフィールドが残っているため、型チェックは通ります。壊れるのは実行時、それも移行を終えたつもりになった後です。
上のコードでこれらを一切渡していないのは、そのためです。振る舞いを寄せたい場合は、パラメータではなくメタプロンプト側の条件文で指定してください。API 呼び出しの基本形から確認したい方は、Claude API クイックスタート が最短です。
8月17日までのチェックリスト
| 順番 | やること | 確認方法 |
|---|---|---|
| 1 | 保存済みプロンプト・変数・評価をエクスポートする | Console のバナー / Organizational Settings。取り返しがつかないので最初に行う |
| 2 | 3つのエンドポイントの呼び出しを数える | 上の grep コマンド。0 件でも次へ進む |
| 3 | 使用量 CSV で実トラフィックを確認する | Console の Usage → Export。API キー別・モデル別に出ます |
| 4 | テンプレート化を手元のスクリプトに置き換える | 逆変換チェックが通ること |
| 5 | 推敲処理を Messages API 経由に置き換える | --dry-run で本文を確認してから鍵を通す |
| 6 | 移行先のコードに temperature 等を書き写していないか確認する | 対象モデルへ実際に1回投げる。型チェックでは検出できません |
廃止の期日が近いときは、期日から逆算した順番でしか進みません。私はこの並び順を崩さないようにしています。エクスポートだけは今日のうちに終わらせてください。それさえ済んでいれば、残りは8月17日を過ぎてからでも取り戻せます。
同じ「予告つきの廃止」への備えとしては、Opus 4.7 の fast mode 廃止で無人ジョブを落とさない事前検査と移行(プレミアム記事)に、廃止前後で挙動が変わる箇所の測り方をまとめています。今回のように「エラーになるもの」と「黙って振る舞いが変わるもの」は、備え方が別になります。
私自身、今回のことでプロンプトの置き場所を見直しました。お読みいただきありがとうございました。