search_resultの引用粒度を細かくする方法
search_resultのcontent配列を複数のtextブロックに分けると、Claudeの引用はブロック単位に細分化されます。分割の仕組みと実際の挙動を扱います。
search_resultコンテンツブロックで返す引用の粒度は、content配列に入れるtextブロックの分け方だけで決まります。1つの配列に長文をまとめて入れると、引用は常にその全文を返します。段落や項目ごとに複数のtextブロックへ分割すると、Claudeはブロック単位で引用範囲を絞り込みます。
引用の最小単位はtextブロック1つ
search_resultのcontentは、type: "text"のブロックを並べた配列です。
{
"type": "search_result",
"source": "https://docs.company.com/api-guide",
"title": "API Documentation",
"content": [
{ "type": "text", "text": "Authentication: All API requests require an API key." },
{ "type": "text", "text": "Rate Limits: The API allows 1000 requests per hour per key." },
{ "type": "text", "text": "Error Handling: The API returns standard HTTP status codes." }
],
"citations": { "enabled": true }
}Claudeが引用を返すとき、実際に返るのはブロック単位のスライスであって、ブロック内の一部分ではありません。1つのtextブロックに複数のトピックを詰め込むと、そのうちの一文だけを使った回答でも、引用は無関係な文まで含めた全文になります。これがsearch_resultの引用における唯一の粒度パラメータです。
start_block_indexとend_block_indexの読み方
Claudeが上の3ブロックのうち「Rate Limits」の行を使って回答すると、返る引用オブジェクトは次の形になります。
{
"type": "search_result_location",
"cited_text": "Rate Limits: The API allows 1000 requests per hour per key.",
"source": "https://docs.company.com/api-guide",
"title": "API Documentation",
"search_result_index": 0,
"start_block_index": 1,
"end_block_index": 2
}| フィールド | 意味 |
|---|---|
search_result_index | 意味リクエスト内の何番目のsearch_resultブロックが引用されたか(0始まり) |
start_block_index | 意味引用範囲の先頭ブロックのインデックス |
end_block_index | 意味引用範囲の終端(排他的、つまり実際に含まれる最後のブロックはend_block_index - 1) |
cited_text | 意味content[start_block_index:end_block_index]を連結した文字列そのもの |
start_block_indexとend_block_indexはブロック単位の範囲を指すインデックスであって、文字位置ではありません。
分割前後で引用がどう変わるか
同じ内容を1ブロックにまとめた場合と、3つに分割した場合の違いを並べると、粒度の効果がはっきりします。
| content配列の構成 | Rate Limitsだけを使った回答の引用範囲 |
|---|---|
| 1ブロックに全文をまとめる | Rate Limitsだけを使った回答の引用範囲認証・レート制限・エラー処理の説明文すべてがcited_textになる |
| トピックごとに3ブロックへ分割 | Rate Limitsだけを使った回答の引用範囲start_block_index: 1, end_block_index: 2でレート制限の1文だけが返る |
分割していない場合、読者から見ると「レート制限について答えているのに、認証やエラー処理の説明まで出典として貼られる」という結果になります。トピック単位・段落単位でcontentを切り分けておくほど、回答の主張と引用範囲が一致しやすくなります。
どの単位で分割すればよいか
公式ドキュメントは「テキストブロックが引用可能な最小単位」であることだけを定義しており、分割の粒度そのものに固定ルールはありません。実務では次の基準で切り分けます。
- 段落単位: 長い解説記事を検索結果として渡す場合、1段落=1ブロックにすると段落レベルの引用になる
- 項目単位: FAQやAPIリファレンスのように「見出し + 説明」が繰り返される構造なら、1項目=1ブロックにすると項目レベルの引用になる
- 文単位: 引用の正確性を最優先する場合は1文=1ブロックまで細分化できるが、
content配列の要素数が増えるほどリクエストのペイロードは大きくなる
分割の前に押さえておく制約
contentの分割はブロック単位で自由に増やせますが、search_result全体には分割とは別の制約がいくつかあります。
- 引用の有効・無効はリクエスト単位で統一する:
citations.enabledはsearch_resultごとに設定できますが、1つのリクエストに含まれるすべてのsearch_resultで有効・無効を揃える必要があります。一部だけ有効・一部だけ無効という混在はエラーになります - テキスト以外は
contentに入れられない: 画像や他のメディアはsearch_resultのcontent配列でサポートされていません。細かく分割する対象はあくまでテキストです - search_resultはユーザーメッセージにしか置けない: ツール結果の中を含め、ユーザーメッセージ側でのみ使えます。アシスタントメッセージにsearch_resultを含めるとリクエストは拒否されます
- Web検索ツールと併用する場合は引用を全ブロックで有効にする: 同じリクエストでWeb検索ツールを有効にしているときは、すべての
search_resultブロックでcitations.enabled: trueにする必要があります
これらは粒度そのものを制限するものではありませんが、contentを分割する設計をリクエスト全体で統一しておかないと、粒度を細かくした側だけが動かずエラーになる、という事態を招きます。
ツール経由でも直接指定でも粒度の効き方は同じ
search_resultをClaudeに渡す方法には2通りあります。カスタムツールが実行時に検索結果を返す「ツール呼び出し」経由と、事前に取得済みの検索結果をユーザーメッセージに直接含める「トップレベルcontent」としての指定です。どちらの方法で渡しても、content配列をブロック単位に分割すれば同じように引用の粒度が細かくなります。粒度を左右するのはcontent配列の構造だけで、どちらの経路でsearch_resultを渡したかには依存しません。
この性質を利用すると、動的なツール呼び出しを組む前に、まずトップレベルcontentとして固定の検索結果を渡し、contentをどう分割すれば期待する引用範囲になるかを手元で検証できます。公式ドキュメントもトップレベルcontentの用途の1つとして「引用の挙動をテストする」ことを挙げています。分割の粒度が固まってから、同じcontent構造を返すツールを実装すれば、本番のツール呼び出しフローに手戻りなく組み込めます。
分割の単位とキャッシュの単位は別物
cache_controlはsearch_resultブロック自体に付ける設定で、content配列の中の個々のtextブロックには付きません。
{
"type": "search_result",
"source": "https://docs.company.com/guide",
"title": "User Guide",
"content": [
{ "type": "text", "text": "Authentication: All API requests require an API key." },
{ "type": "text", "text": "Rate Limits: The API allows 1000 requests per hour per key." }
],
"citations": { "enabled": true },
"cache_control": { "type": "ephemeral" }
}つまり、引用の粒度を細かくするためにcontentをいくつのtextブロックに分けても、キャッシュの単位はsearch_result全体のままです。分割を細かくしてもキャッシュのヒット率やキャッシュ書き込み単位には影響しません。この2つは別のパラメータで制御される別の概念だと分けて考えます。
補足用のテキストを追加したい場合の制約もあります。ユーザーメッセージの中ではsearch_resultブロックは他のブロック(テキストや画像など)と自由に混在できますが、ツール結果(tool_result)の中は厳格で、1つでもsearch_resultが含まれていれば、そのtool_resultのすべてのブロックがsearch_resultでなければなりません。ツール経由で返した検索結果に補足の説明文を添えたい場合は、独立したテキストブロックとして並べるのではなく、いずれかのsearch_resultのcontent配列の中にテキストブロックとして含めます。こうすると、その補足文自体も引用可能なブロックの1つとして扱われます。
公式が推奨する分割の指針
公式ドキュメントのベストプラクティスは、分割の目的をはっきり「引用の境界を細かくするため」と位置付けています。長いコンテンツを論理的なテキストブロックに分けることが、Claudeに細かい引用境界を与える方法だと明記されており、これは本記事で説明したstart_block_index/end_block_indexの仕組みそのものです。あわせて、ソースURLは恒久的なものを使う・タイトルは内容を正確に反映させる・フォーマットをアプリケーション全体で統一する、という3点も分割設計とセットで推奨されています。検索が失敗したりヒットしなかったりした場合は、エラーを発生させる代わりに{"type": "text", "text": "No results found."}のようなプレーンテキストブロックを返すと、Claudeがその旨を説明しつつ会話が継続します。
カスタムコンテンツドキュメントと同じモデル
この「テキストブロックが最小引用単位」という設計は、search_result固有のものではなく、Citations機能のカスタムコンテンツドキュメントでも共通です。search_resultはRAGアプリケーション向けに自分のデータをClaudeに検索結果として渡す形ですが、引用の粒度制御という点では同じ考え方が適用されます。Citations全体でのフォーマット別の挙動差はClaudeのCitationsは引用形式がPDF・テキスト・カスタムで変わるで扱っています。
どの提供面で使えるか
search_resultコンテンツブロックは、Claude API・Amazon Bedrock・Google Cloud(Vertex AI)のいずれの提供面でも利用できます。分割による粒度制御の挙動そのものは、content配列の構造だけで決まる仕様なので、どの提供面を使っていても同じように働きます。提供面ごとの違いを心配する必要があるのはリクエストの送り先やSDKの初期化方法であって、引用の粒度設計そのものではありません。一方でモデル側には対応・非対応の差があり、Haiku 3はsearch_resultに非対応です。分割の粒度をどれだけ細かくしても、対応モデルを使っていなければ引用は返りません。
トークンコストへの影響
cited_textは出力トークンとしてカウントされないため、ブロックを細かく分けて引用範囲を絞っても、その分課金が増えることはありません。ただしcontent配列そのものは入力コンテンツとして送信されるため、入力トークンの計算対象です。Citations機能全体のトークンの数え方はClaude Citationsのトークンコストの仕組みで扱っています。
sourceとtitleは分割しても引用ごとに同じ値が付く
contentを細かく分割しても、sourceとtitleはsearch_result全体に対して1つずつしか設定できません。3つのブロックに分割したsearch_resultのうち、どのブロックが引用されても、citationオブジェクトのsourceとtitleは同じ値になります。粒度が変わるのはcited_text・start_block_index・end_block_indexだけで、引用の出典表示そのものは常にsearch_result単位です。ブロックごとに別々の出典を持たせたい場合は、contentを分割するのではなく、別のsearch_resultとして分けて渡す必要があります。
まとめ
分割の設計は、引用させたい最小の主張が1ブロックに収まっているかで決めます。段落やFAQの1項目のように、それ単体で読者に見せてよい単位までブロックを切り分ければ、Claudeの引用はその単位に自然と絞り込まれます。逆に無関係な文をまとめたまま残すと、答えに使われた一文だけでなくその全文が出典として貼られます。分割の細かさに公式の上限はなく、引用の正確さが必要な箇所(FAQの回答や規約の条文など)から着手するのが実務での進め方です。