ClaudeのCitationsは引用形式がPDF・テキスト・カスタムで変わる
Claude CitationsはPDFならページ番号、プレーンテキストなら文字インデックス、カスタムコンテンツならブロックインデックスと、ドキュメント種別ごとに引用の返り方が変わります。実装の落とし穴も解説します。
Citationsが返す引用形式はドキュメント種別で変わる
Claude APIのCitations機能は、documentブロックのcitations.enabledをtrueにするだけで、Claudeの回答に根拠テキストと出典位置を自動で付与します。見落とされがちなのは、出典位置の表現がドキュメントの種類ごとに別物になる点です。PDFはページ番号、プレーンテキストは文字位置、カスタムコンテンツはブロック番号で返ります。3つを混同してパースすると、正しく動いているのに座標がずれて見える不具合を作り込みます。
対応モデルは現行のアクティブなモデル全てです。ドキュメントはメッセージに直接埋め込めるほか、Files APIにアップロードして発行されたfile_idで参照できます。引用の対象はテキストだけで、画像からの引用にはまだ対応していません。
3種類のドキュメントと引用の返り方
テキストを抽出して文単位に分割します。返るのは1始まりのページ番号(start_page_number / end_page_number)です。
プレーンテキスト
文単位に自動分割します。返るのは0始まりの文字位置(start_char_index / end_char_index)で、ドキュメント全体の文字列に対するオフセットです。
カスタムコンテンツ
追加の分割は入らず、渡したcontent配列の要素がそのまま最小単位になります。返るのは配列の0始まりのインデックス(start_block_index / end_block_index)です。
.docxや.xlsxのようにdocumentブロックが直接扱えない形式は、プレーンテキストに変換してから渡します。すでにプレーンテキストの.csvや.mdは、text/plainを明示してFiles APIにアップロードする方法もあります。
引用されない・範囲がずれるときの切り分け
実装でつまずく症状は、原因がほぼ決まった場所にあります。症状から逆引きすると次のとおりです。
症状と最初に疑う場所
特定の一文だけ引用されない
その文が
sourceの外にないかを確認します。titleとcontextはモデルには渡りますが、引用元にはなりません。titleは長さに制限があるので、出典URLや取得日時のようなメタデータはcontextにテキストかJSON文字列で持たせます。一部のドキュメントだけCitationsを有効にしたい
Citationsは、1リクエスト内ですべての
documentに有効化するか、すべて無効にするかのどちらかです。一部だけcitations.enabled: trueにする構成は、公式の仕様の範囲外です。search_resultブロックでは、有効と無効の混在がエラーになると明記されています。PDFから引用が1件も返らない
画像として保存されたスキャンPDFは、テキストを抽出できないため引用の対象外です。
PDFがリクエストの上限を超える
ページ数は、リクエストのコンテキストウィンドウが1Mトークン未満なら100ページ、それ以上なら600ページまでです。サイズは、Claude APIでは最大32MBで、プラットフォームによって異なります。どちらもリクエスト全体にかかる上限なので、大きいPDFはFiles APIでアップロードし、
file_idで参照してペイロードを小さくします。小さな文字のページが並ぶPDFや複雑な表、図の多いPDFは、ページ数の上限に届く前にコンテキストウィンドウを使い切ることがあります。その場合はドキュメントを章ごとに分けて渡します。構造化出力を足したら400エラーになる
documentまたはsearch_resultでCitationsを有効にした状態でoutput_config.formatも指定すると、400エラーになります。詳しくはCitationsとstructured outputsの併用を参照してください。
範囲がずれる原因は半開区間の読み違い
3形式に共通する仕様が1つあります。開始側は含み、終了側は含まない半開区間です。文字インデックスとブロックインデックスは終了側が排他的で、ページ番号も終了ページが排他的です。
公式サンプルの文書The grass is green. The sky is blue.で試すと、この意味が見えます。次のPythonは、レスポンス例のstart_char_indexとend_char_indexをそのままスライスに当てた結果です。
python3 - <<'PY'
doc = "The grass is green. The sky is blue."
print(len(doc))
print(repr(doc[0:20]), repr(doc[20:36]))
# 以下のブロック配列は記事の仮例
blocks = ["First chunk", "Second chunk", "Third chunk"]
print(blocks[0:1], blocks[1:3])
PY出力は次のとおりです。
36
'The grass is green. ' 'The sky is blue.'
['First chunk'] ['Second chunk', 'Third chunk']終端を含まないスライスで、2つの引用が隙間も重なりもなく文書全体を埋めます。最初の範囲は0〜20で、末尾の空白までを含む20文字です。一方、同じレスポンスのcited_textは空白を含まないThe grass is green.(19文字)になっています。ハイライト表示をcited_textの長さで計算すると1文字ずれるので、範囲は常にインデックスから求めます。
PDFでも同じ読み方をします。start_page_number: 5とend_page_number: 6は、5ページだけを指します。最後のページ番号が欲しいときはend_page_number - 1と変換します。
ドキュメント自体を指すdocument_indexにも、3形式共通の数え方があります。そのリクエストに含まれる全メッセージのdocumentブロックを、通しで0始まりに数えます。会話履歴をまるごと送り直す実装では、ターン1で渡したドキュメントも数に入ります。2ターン目以降に履歴を間引いて送ると、同じドキュメントの番号が変わります。
PDFの引用:ページ番号は1始まり
PDFで返る引用はpage_location型です。
{
"type": "page_location",
"cited_text": "Water is essential for life.",
"document_index": 1,
"document_title": "PDF Document",
"start_page_number": 5,
"end_page_number": 6
}file_id経由でPDFを渡す場合は、次のようにリクエストします。
curl https://api.anthropic.com/v1/messages \
-H "content-type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"messages": [{
"role": "user",
"content": [
{
"type": "document",
"source": {"type": "file", "file_id": "'"$FILE_ID"'"},
"citations": {"enabled": true}
},
{"type": "text", "text": "Summarize this document."}
]
}]
}'Files APIで上げたファイルは、同じワークスペースのどのAPIキーからも参照できます。ユーザー個別・会話別には分かれていません。引用付きのアプリでは、file_idをエンドユーザーの入力から受け取らず、サーバー側で利用者とファイルの対応を管理します。
PDFは、base64エンコードしたデータ、URL、file_idの3通りで渡せます。対象はパスワードや暗号化のない標準的なPDFです。Files APIの上限は1ファイル500MB、組織全体で1TBで、500MBを超えると413のFile exceeds the 500 MB limitが返ります。
file_id方式が使えるかは、利用する面で変わります。Amazon BedrockとGoogle Cloudで使えるのは、base64の指定だけです。Microsoft Foundryでは、Azure上のデプロイでFiles APIが使えません。
プレーンテキストの引用:文字位置は0始まり
プレーンテキストで返る引用はchar_location型です。
{
"type": "char_location",
"cited_text": "The grass is green.",
"document_index": 0,
"document_title": "Example Document",
"start_char_index": 0,
"end_char_index": 20
}RAG(検索拡張生成)のパイプライン全体の組み方はClaude RAGの構築ガイドにあります。検索結果をドキュメントでなくsearch_resultブロックとして渡す方式の粒度は、search_resultの引用粒度で比較しています。
カスタムコンテンツの引用:ブロック番号は0始まり
カスタムコンテンツで返る引用はcontent_block_location型です。
{
"type": "document",
"source": {
"type": "content",
"content": [
{"type": "text", "text": "First chunk"},
{"type": "text", "text": "Second chunk"}
]
},
"citations": {"enabled": true}
}箇条書きや会話ログのように、文単位の分割では粒度が合わない資料に向いています。細かく割るほど引用箇所を狭く指せる一方、どこで割るかの設計は呼び出し側の責任です。
RAGのチャンクを渡す場合は、チャンクの中でさらに文単位の引用を許すかどうかで分かれます。許すなら、チャンクごとに1つのプレーンテキストドキュメントにします。Claudeはチャンク内の特定の文を狙って引用できます。チャンクをそれ以上分割したくないなら、content配列の要素として渡します。
3形式が混ざった応答のパース
複数のドキュメントを渡すと、応答はtextブロックの配列になり、根拠にした箇所を含むブロックだけにcitations配列が付きます。地の文と引用付きの文が交互に並ぶ形です。プレーンテキスト・PDF・カスタムコンテンツを混ぜて渡せば、char_location・page_location・content_block_locationが1つの応答に同居します。
パース処理は、次の順で組むと壊れにくくなります。
引用のパース手順
- 1
`type`で分岐する
citationsの各要素のtypeを見て、3形式のどれかを決めます。「とりあえずstart_char_indexを読む」実装は、PDFやカスタムコンテンツが混ざった時点で壊れます。 - 2
形式ごとのフィールドを読む
start_char_index、start_page_number、start_block_indexのうち、型に対応するものだけを読みます。 - 3
終端を半開区間として扱う
[start, end)で処理します。 - 4
`document_index`で元資料に戻す
送信した
documentブロックの並びと突き合わせます。
ストリーミングでは、引用がcontent_block_deltaイベントの中にcitations_deltaという専用のdelta型で届きます。1つのdeltaに入る引用は1件で、構築中のtextブロックのcitations配列へ1件ずつ追記します。型情報は非ストリーミングと同じなので、分岐のコードは両方で共通化できます。
トークンとキャッシュの扱い
引用を有効にすると、システムプロンプトへの追加とチャンク処理の分だけ入力トークンがわずかに増えます。出力側は逆に効率的です。Claudeは標準化された形式で引用位置を出力し、API側がcited_textとインデックスにパースします。cited_textは便宜上付与されるだけで、出力トークンには数えられません。次のターンに渡すときも、入力トークンにはカウントされません。
プロンプトキャッシュとは組み合わせられますが、キャッシュできるのは引用元のドキュメントのほうです。応答に含まれる引用ブロックそのものは直接キャッシュできません。documentブロックにcache_controlを付けてキャッシュします。長大な技術文書を毎ターン渡す使い方では、このcache_controlの有無がコストを左右します。Citationsはトークンカウントやバッチ処理とも併用できます。
どの形式を選ぶか
自動分割で足りるのは、Claudeが1文、または連続した複数の文(段落)で引用してくれれば十分な場合です。最小単位を自分で決めたいときは、カスタムコンテンツを選びます。
ファイルの形で決まる場面もあります。テキストを含むPDFはPDF、プレーンテキストに変換できる資料はプレーンテキストです。
まとめ
実装前に決めることは2つです。引用の最小単位を自動分割に任せるかどうかと、レスポンスをtypeで分岐するパーサーを最初から3形式分用意するかどうかです。