Claudeへの指示、XMLとJSONと自然言語で差は出るのか——自然言語で十分じゃない?
Claudeへのプロンプト、XMLで書いたほうがいいらしい。JSONで書いたほうがいいらしい。自然言語で十分じゃない?
仲間から「JSONプロンプティング」の記事が共有されて、「普段どんなフォーマットでプロンプト書いてる?」って話になった。自分は自然言語でしか書いてない。
Anthropicの公式ドキュメントには「ClaudeはXMLタグを構造的な区切りとして認識するよう訓練されている」とある。XMLでの構造化を推奨してる。
推奨はわかった。でも自分のIDEで、普段の開発で、実際に差が出るのか。差が出ないなら構造化する手間が無駄になる。
気になったので、同じ指示を自然言語・XML・JSONの3フォーマットで投げてみた。計6回。結論だけ知りたい人はこちら。
同じ指示を3フォーマットで投げた
Express + TypeScriptのメモ管理APIを用意した。ユーザーとノートのCRUDがある小さなプロジェクト。
検証環境はAntigravity(GoogleのVSCode fork)+ Claude拡張。API直叩きじゃなくて、普段の開発と同じIDE上で試してる。
2つの場面を比べた。
- バグ修正(シンプルな指示): 3つのバグを直す
- 機能追加(複雑な指示): JWT認証をゼロから追加する
3フォーマット × 2場面 = 計6回。毎回 git checkout でコードを初期状態に戻して、Claudeのチャットもクリアしてからやり直してる。プロンプトの中身は同じ。フォーマットだけ変えた。
バグ修正: 差、出なかった
3つのバグを直してもらう。存在しないIDで500エラーになる、削除時に存在しないIDでも200が返る、存在しないuserIdでもノートが作れる。
自然言語は1段落にまとめて投げた。
GET /api/users/999 にリクエストすると500エラーになる。存在しないユーザーIDのときに
適切なエラーレスポンスを返すように修正して。ついでに、DELETE /api/users/:id も
存在しないIDのとき404を返すようにして、POST /api/notes でuserIdが存在しない
ユーザーを指してたら400エラーにして。
XMLは <bug> タグで症状・期待動作を分けた。
<task>バグ修正</task>
<bugs>
<bug>
<endpoint>GET /api/users/:id</endpoint>
<symptom>存在しないIDで500エラーになる</symptom>
<expected>404とエラーメッセージを返す</expected>
</bug>
<bug>
<endpoint>DELETE /api/users/:id</endpoint>
<symptom>存在しないIDでも200で空レスポンスを返す</symptom>
<expected>404とエラーメッセージを返す</expected>
</bug>
<bug>
<endpoint>POST /api/notes</endpoint>
<symptom>存在しないuserIdでもノートが作成できる</symptom>
<expected>400とエラーメッセージを返す</expected>
</bug>
</bugs>
JSONも同じ構造。
{
"task": "バグ修正",
"bugs": [
{
"endpoint": "GET /api/users/:id",
"symptom": "存在しないIDで500エラーになる",
"expected": "404とエラーメッセージを返す"
},
{
"endpoint": "DELETE /api/users/:id",
"symptom": "存在しないIDでも200で空レスポンスを返す",
"expected": "404とエラーメッセージを返す"
},
{
"endpoint": "POST /api/notes",
"symptom": "存在しないuserIdでもノートが作成できる",
"expected": "400とエラーメッセージを返す"
}
]
}
結果。3フォーマットとも 3つのバグを全部直した 。

自然言語で投げたときの回答。XML・JSONもほぼ同じ画面が返ってきた
修正箇所もコードの構造もほぼ同じ。エラーメッセージの文言が微妙に違うくらい("Invalid userId" vs "User not found" vs "userId does not reference an existing user")。
唯一の差は、自然言語とXMLが修正後に npx tsc --noEmit で型チェックを走らせたのに、JSONだけ走らせなかったこと。修正自体は正しかったから問題にはならなかったけど、地味な差ではある。
指示がシンプルだと、フォーマットはただの飾り。
機能追加: ここで差が開いた
同じJWT認証の追加を3フォーマットで指示した。ログインエンドポイント、認証ミドルウェア、ルートの保護、トークンからuserIdを取るとか、要件が6つある。
自然言語は1段落にまとめて投げた。
このAPIにJWT認証を追加して。ログインエンドポイント POST /api/auth/login を作って、
emailとpasswordを受け取ってJWTトークンを返すようにして。ユーザーのデータに
passwordフィールドを追加して。トークンの有効期限は1時間。メモのCRUD操作
(GET /api/notes 以外)はログイン必須にして、認証ミドルウェアで保護して。
トークンがないか無効なら401を返して。ノート作成時はトークンからuserIdを取得して、
リクエストボディのuserIdは無視して。
XMLは <requirements> <protection> <constraints> で区切った。
<task>JWT認証の追加</task>
<requirements>
<login>
<endpoint>POST /api/auth/login</endpoint>
<request>emailとpasswordを受け取る</request>
<response>JWTトークンを返す</response>
<expiry>1時間</expiry>
</login>
<user_data>
<change>Userにpasswordフィールドを追加</change>
</user_data>
</requirements>
<protection>
<middleware>認証ミドルウェアを作成</middleware>
<protected_routes>
<route>POST /api/notes</route>
<route>PUT /api/notes/:id</route>
<route>DELETE /api/notes/:id</route>
</protected_routes>
<public_routes>
<route>GET /api/notes</route>
<route>GET /api/notes/:id</route>
</public_routes>
<unauthorized>トークンがないか無効なら401を返す</unauthorized>
</protection>
<constraints>
<constraint>ノート作成時はトークンからuserIdを取得し、リクエストボディのuserIdは無視する</constraint>
</constraints>
JSONはXMLと同じ構造をキーバリューで表現。
{
"task": "JWT認証の追加",
"requirements": {
"login": {
"endpoint": "POST /api/auth/login",
"request": "emailとpasswordを受け取る",
"response": "JWTトークンを返す",
"expiry": "1時間"
},
"user_data": {
"change": "Userにpasswordフィールドを追加"
}
},
"protection": {
"middleware": "認証ミドルウェアを作成",
"protected_routes": [
"POST /api/notes",
"PUT /api/notes/:id",
"DELETE /api/notes/:id"
],
"public_routes": [
"GET /api/notes",
"GET /api/notes/:id"
],
"unauthorized": "トークンがないか無効なら401を返す"
},
"constraints": [
"ノート作成時はトークンからuserIdを取得し、リクエストボディのuserIdは無視する"
]
}
書く手間は自然言語が圧倒的に楽。XMLとJSONは構造を考える時間がかかるけど、要件の漏れに自分で気づけるメリットもある。タグを埋めてるうちに「保護するルートとしないルート、ちゃんと分けて指示しなきゃ」ってなる。
基本要件は全員クリア
3フォーマットとも、ログインエンドポイント、認証ミドルウェア、ルートの保護、型チェックのパスまでできた。ここまでは差がない。
差が出たのはその先
| 観点 | 自然言語 | XML | JSON |
|---|---|---|---|
| 所要時間 | 2分8秒 | 3分5秒 | 2分32秒 |
| JWT_SECRETの扱い | ハードコード | 環境変数対応 | 環境変数対応 |
| 設定の分離 | なし(middleware内) | config.ts分離 | auth.ts内にまとめ |
| パスワード漏洩対策 | POSTのみ | GET + POST両方 | GET + POST両方 |
| curl動作確認例 | なし | あり | なし |
| 依存パッケージ追加 | npm install | npm install | package.json手動編集 |
| 未使用import整理 | なし | 削除 | なし |
一番わかりやすかったのがパスワード漏洩への対応。
Userにpasswordフィールドを追加したことで、GET /api/users がパスワードを返してしまう問題が生まれる。これはプロンプトのどのフォーマットにも書いてない。指示の外にある問題。
自然言語で投げたとき、Claudeの回答にはこう書いてあった。
既存の GET /api/users がパスワードを返してしまう(既知の課題)ですが、今回のスコープ外として手を入れていません。

末尾に「スコープ外として手を入れていません」の一文が見える
問題に気づいてはいる。でも「スコープ外」として見送り。
XMLではどうか。GETとPOST両方でパスワードを除外してる。言い訳なし。黙って直してた。

パスワード漏洩への言い訳はない。POST/GETでパスワードを露出しないよう調整、と書いてある
JSONもGET・POST両方で対策してたけど、依存パッケージの追加を npm install じゃなくpackage.jsonの手動編集で済ませてた。動くけど package-lock.json との整合性がちょっと怖い。
なぜ構造化すると「行間を読む」のか
パスワード漏洩を直したのはXMLとJSON、つまり構造化した2つ。自然言語だけが見送ってる。
自然言語は要件が1段落に流れてるから、Claudeは「書いてあることを順番にやる」モードになりやすい。指示の境界が曖昧なぶん、明示されてないことは後回しにする。
XMLやJSONはタグやキーで要件が区切られてる。各ブロックが何を担当してるかはっきりしてるぶん、そこからはみ出した問題(パスワード漏洩みたいな副作用)が目につきやすくなったんじゃないか。
さらにXMLはその先まで踏み込んでた。設定ファイルの分離、curlの動作確認例、未使用importの削除。XMLもJSONも構造化の恩恵はある。それでもXMLがもう一歩先を行ったのは、Anthropicの訓練が効いてるのかもしれない。
仮説の域を出ないけど、今回の実験では構造化が「Claudeに情報を渡す」だけじゃなくて「Claudeの視野を広げる」効果があると感じた。
複雑さで切り替えればいい
6回の実験から見えたのはシンプルな話。
「ここを直して」で済むシンプルな指示なら、フォーマットで差は出ない。仕様と制約がセットになる複雑な指示は、タグやキーで区切ると精度が上がる。
仕様と制約がセットになる指示が来たらタグで区切る。それ以外は自然言語でいい。
自然言語で十分だった?
| シンプルな指示(バグ修正) | 複雑な指示(機能追加) | |
|---|---|---|
| 自然言語 | 全部直せた。十分 | 基本要件はクリア。ただし副作用を「スコープ外」で見送り |
| XML / JSON | 同じ結果。差なし | 副作用も黙って修正。XMLはさらに設定分離やcurl例まで |
十分だった。ほとんどの場面では。
じゃあXMLとJSONどっちがいいのか。正直、大きな差はなかった。XMLかJSONかより、構造化したかどうかのほうがずっと大きい。強いて言えば、XMLのほうが自然言語に近い感覚で書ける。タグ名を自由につけられるし、値にそのまま日本語の文章を入れられる。
構造化で差が出たなら、自然言語でも箇条書きで要件を整理すれば近い効果が出るかもしれない。それでも精度が出ないときは、XMLやJSONを試してみる価値がある。特にAPIで使うプロンプトは一度書いたらテンプレとして繰り返し使うから、構造化の手間が活きやすい。
Discussion