search_resultの引用粒度を細かくする方法
search_resultのcontent配列を複数のtextブロックに分けると、Claudeの引用はブロック単位に細分化されます。分割の仕組みと実際の挙動を扱います。
search_resultコンテンツブロックで返る引用の粒度は、content配列に入れるtextブロックの分け方で決まります。長文を1ブロックにまとめると、引用は常にその全文です。段落や項目ごとに複数のtextブロックへ分けると、Claudeはブロック単位で引用範囲を絞ります。
引用が長すぎるのは、textブロックが最小単位だから
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つのブロックに複数のトピックを詰めると、一文だけを使った回答でも引用は無関係な文まで含む全文になります。
この挙動は公式ドキュメントの応答例にそのまま出ています。APIキーの発行方法だけを述べた文に付く引用のcited_textが、「Rate limits: 1000 requests per hour…」の文まで含んでいるのです。2つの話題が同じ1ブロックに入っているためで、回答の文には使われていない部分が出典として貼られます。
引用の粒度を変える手段は、このcontentの分け方です。
start_block_indexとend_block_indexの読み方
上の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 | 意味引用範囲の先頭ブロックのインデックス(0始まり) |
end_block_index | 意味引用範囲の終端。排他的なので、実際に含まれる最後のブロックはend_block_index - 1 |
cited_text | 意味content[start_block_index:end_block_index]を連結した文字列 |
どちらのインデックスも文字位置ではなくブロックの位置です。end_block_indexは常にstart_block_indexより大きい値になります。
Pythonのスライスと同じ読み方なので、返ってきた引用はcontentから手元で再現できます。
def cited_text(result, start, end):
return "".join(b["text"] for b in result["content"][start:end])
cited_text(result, 1, 2)
# 'Rate Limits: The API allows 1000 requests per hour per key.'
cited_text(result, 0, 3) == "".join(b["text"] for b in result["content"])
# True3ブロックのうち2番目だけなら1, 2、全部なら0, 3です。
応答側で引用がどう並ぶか
引用は、応答のtextブロックごとのcitations配列に入ります。公式の応答例は3つのtextで構成されています。1つ目は「APIキーはAuthorizationヘッダーに入れる」という主張で、引用が1件付きます。2つ目は「To set this up from scratch, you'll need to」というつなぎの文で、引用はありません。3つ目は別のsearch_resultを指す引用付きです。引用が付くのは、渡した内容に基づく主張の部分だけです。
引用範囲は複数ブロックにまたがることもあります。上の3ブロックで「レート制限」と「エラー処理」の両方を使った文なら、範囲はstart_block_index: 1, end_block_index: 3です。cited_textは2ブロックを連結した文字列になります。複数のsearch_resultを渡したときは、1つ目がsearch_result_index: 0、2つ目が1です。番号は出現順で、複数のメッセージにまたがっても通し番号になります。
cited_textが想定より長いときは、まず該当するsearch_resultのcontentの要素数を見ます。要素が1つなら、その1ブロックが全文として返っているだけで、分割し直せば絞れます。
逆にcited_textが文の途中で切れたように見えるときは、切った位置が原因です。段落を文の途中で機械的に分けると、1文の前半と後半が別ブロックになります。片方のブロックだけが引用されたときは、文が途中で切れて見えます。切り目は文や項目の境界に合わせます。
分割前と分割後で、同じ回答の引用はこう変わる
Rate Limitsだけを使った回答の引用
1ブロックにまとめた場合
認証・レート制限・エラー処理の説明が1つのtextに入っているため、cited_textは3つのトピックすべてを含みます。レート制限について答えているのに、認証やエラー処理まで出典として貼られます。
トピックごとに3ブロックへ分けた場合
start_block_index: 1, end_block_index: 2となり、cited_textはレート制限の1文だけです。回答の主張と引用範囲が一致します。
分割の手順と、どの単位で切るか
粒度を変えるときの作業は、検索結果をcontentへ入れる前のテキスト加工に尽きます。
引用粒度を調整する流れ
- 1
引用させたい最小の主張を決める
段落か、FAQの1項目か、条文の1条か。それ単体で読者に見せてよい単位がブロックの大きさになります。
- 2
その単位でtextブロックに切る
import re def to_search_result(source, title, markdown): paras = [p.strip() for p in re.split(r"\n\s*\n", markdown) if p.strip()] return { "type": "search_result", "source": source, "title": title, "content": [{"type": "text", "text": p} for p in paras], "citations": {"enabled": True}, }空行で段落に分ける例です。上の3行のドキュメントを渡すと、
contentは3ブロックの配列になります。 - 3
固定の検索結果で引用範囲を試す
ユーザーメッセージに検索結果を直接含めると、ツールを組まなくても分け方ごとの
cited_textを比べられます。 - 4
本番のツールが同じ構造を返すようにする
ツール結果の側でも
contentの構造は同じです。分け方が固まってから実装すれば手戻りがありません。
切る単位の目安は次のとおりです。
- 段落単位: 長い解説記事を渡す場合。1段落=1ブロックで段落レベルの引用になる
- 項目単位: FAQやAPIリファレンスのように「見出し+説明」が繰り返される構造。1項目=1ブロックで項目レベルの引用になる
- 文単位: 引用の正確性を最優先する場合。1文=1ブロックまで細分化できるが、
contentの要素数が増え、ブロックごとのJSON構造も増える
引用の正確さが必要な範囲(FAQの回答や規約の条文など)だけを細かく切り、地の文の解説は段落単位に留めると、構築と保守の手間を抑えられます。
search_resultを渡す経路は、カスタムツールが実行時に返す方法と、取得済みの結果をユーザーメッセージに直接含める方法の2通りです。どちらでも粒度はcontentの構造で決まります。公式ドキュメントも後者の用途の1つに「引用の挙動のテスト」を挙げています。
文単位で自動的に切ってほしいときは、プレーンテキストドキュメント
search_resultは渡したブロックをそのまま引用の単位にします。文ごとの自動分割はありません。一方、Citations機能のプレーンテキストドキュメントとPDFは、内容が文単位に自動で分割されます。
| 形式 | 引用の単位 | 引用の位置情報 |
|---|---|---|
| search_result | 引用の単位渡したtextブロック | 引用の位置情報ブロックのインデックス範囲 |
| カスタムコンテンツドキュメント | 引用の単位渡したブロック(追加の分割なし) | 引用の位置情報ブロックのインデックス範囲 |
| プレーンテキストドキュメント | 引用の単位文(自動分割) | 引用の位置情報文字位置の範囲 |
Citationsのドキュメントには、使い分けが書かれています。RAGのチャンクから特定の文を引用させたいなら、チャンクをプレーンテキストドキュメントに入れます。追加の分割をしたくなければ、カスタムコンテンツにします。search_resultの特徴は、sourceとtitleが検索結果の単位で付き、検索エンジンやナレッジベースの返り値をそのまま引用元として扱える点です。ドキュメント形式ごとの引用の違いはClaudeのCitationsは引用形式がPDF・テキスト・カスタムで変わるにまとめています。
分割しても変わらないもの
ブロックをいくつに分けても、次の項目は検索結果の単位のままです。
- sourceとtitle: search_result全体に1つずつしか設定できません。3ブロックのどれが引用されても、citationオブジェクトの
sourceとtitleは同じ値です。ブロックごとに別の出典を持たせたいなら、contentを分けるのではなく別のsearch_resultとして渡します - cache_control: search_resultブロックに付ける設定です。
content内のtextブロックのフィールドはtypeとtextで、cache_controlは定義されていません
{
"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の分け方とは別に、search_result全体には守る制約があります。分割設計をリクエスト全体で統一していないと、細かく切った側だけが動かない事態になります。
- 引用の有効・無効はリクエスト内で揃える:
citations.enabledはsearch_resultごとに書きますが、既定は無効です。有効のものと無効のものが混在するとエラーになります contentに入れられるのはテキストだけ: 画像などのメディアは入れられません。細かく切る対象はテキストです- search_resultを置けるのはユーザーメッセージ側だけ: ツール結果の中もユーザーメッセージ側に含まれます。アシスタントメッセージに置くとリクエストは拒否されます
- Web検索ツールと併用するときは全ブロックで引用を有効にする: 同じリクエストでWeb検索ツールを有効にしている場合、すべての
search_resultでcitations.enabled: trueが必要です - tool_resultの中はsearch_resultだけにする: 1つでもsearch_resultがあれば、そのtool_resultのブロックはすべてsearch_resultでなければなりません。ツール経由で補足の説明文を添えたいときは、いずれかのsearch_resultの
contentの中にテキストブロックとして入れます。独立したテキストブロックとして並べることはできません。補足文も引用の対象になります。ユーザーメッセージの直下なら、search_resultとテキストや画像を自由に混在できます
検索が失敗した場合やヒットがなかった場合は、エラーを返す代わりに{"type": "text", "text": "No results found."}のようなプレーンテキストを返すと、Claudeがその旨を説明して会話が続きます。
どのモデル・提供面で使えるか
search_resultコンテンツブロックは、Claude API・Amazon Bedrock・Google Cloudで利用できます。ベータヘッダーは不要で、標準のMessages APIの一部です。モデル側ではClaude Haiku 3だけが非対応で、それ以外の現行モデルは引用付きのsearch_resultに対応しています。
トークンコストへの影響
cited_textは出力トークンに数えられません。次のターンに引用つきの応答を渡し直したときも、入力トークンには数えられません。ブロックを細かく分けて引用範囲を絞っても、cited_textの分だけ課金が増えることはありません。
contentの配列そのものは入力として送るため、入力トークンの対象です。Citationsのドキュメントによると、引用を有効にするとシステムプロンプトの追加と分割のぶん、入力トークンがわずかに増えます。Citations全体のトークンの数え方はClaude Citationsのトークンコストの仕組みで扱っています。
まとめ
引用を絞りたいときは、答えに使われうる最小の主張が1ブロックに収まっているかを見ます。FAQの1項目や規約の1条のように、それだけで出典として見せてよい単位まで切れば、引用はその単位に絞られます。文ごとの自動分割が欲しい場合はsearch_resultではなくプレーンテキストドキュメントが向き、ブロックごとに出典を変えたい場合は別のsearch_resultに分けます。