Generation を引用付きで書く — Anthropic Citations API と cross-encoder reranker

(更新: 2026-05-24) by ZeroZawa

2026-05-24 改訂: 本シリーズは Ollama + Qwen3 で完全ローカル再現できる構成に作り直しました。reranker (cross-encoder) と検索スコアは qwen3-embedding:0.6b + ms-marco-MiniLM-L-6-v2実測した値 です。ただし本記事後半の Anthropic Citations API は Anthropic 固有機能 なので、その節だけは Anthropic キーが必要です (構造は --mock でも確認できます)。題材は架空企業「ナギサ・パートナーズ」の社内 wiki です。

Part 1 で「動くけど使えない」失敗を 3 グループに整理し、Part 2 で hybrid+filter により旧版 (archived) を top-5 から追い出しました。けれど Part 2 末尾で天井を 2 つ残したはずです。意味的に近い trap (LT 大会の評価基準) が hybrid でむしろ 2 位に上がる こと (グループ A-2)、そして 「どの doc が根拠か」を読者が検証できない こと (グループ C-1)。本記事 (Part 3) では前者を cross-encoder reranker で top-20 から top-3 まで絞り、後者を Anthropic Citations API で doc_id 付きの grounding に置き換えます。

Part 2 で旧版は落ちたものの、意味的 trap の残存と引用喪失が Generation 側に残り、Part 3 で cross-encoder reranker と Anthropic Citations API の 2 段で対処する流れを示すインフォグラフィック

ただし最初に正直なことを書きます。Citations API は 「引用文が source 内に必ず存在する」 ことを構造的に保証するだけで、「主張が正しい」 ことまでは保証しません。cross-encoder reranker も off-the-shelf モデルでは domain mismatch で外れることがあります。本記事は 2 つの打ち手が 何を解き、何を解かないか を観察する立場で、客観評価は Part 4 で RAGAs に持ち込みます。

Part 2 までで残った 2 つの構造的課題

Part 2 で打った 3 手 (heading-aware chunking / BM25+dense hybrid / status filter) は retrieval 側の話でした。Generation 側に持ち込んだ瞬間、別の 2 つの問題が顔を出します。

課題 A-2 — 意味的に近い trap は hybrid で「落ちない」どころか上がる

Part 2 で、クエリ「Mirage 開発でコードレビューに必要な approve 数は?」を 3 段で観察しました。正解は mirage-architecture-v3 の「コードレビュー基準」節 (2 人以上の approve)。ところが nagisa-lt-evaluation (社内 LT 大会の「評価基準」) が 意味的に近い trap として食い込みます。

stagetrap (nagisa-lt-evaluation) の rank
Part 1: dense only4
Part 2: hybrid+filter2 (悪化)

dense では 4 位だった trap が、hybrid で 2 位に上がって います。BM25 が「評価」「基準」という表層語を強く拾い、RRF が dense と合算するからです。status filter は archived を落とせても、status=active の trap には無力。これは表層構文の影響を強く受ける bi-encoder の構造的弱点1で、retrieval を真面目にしても 同じ層では届かない天井 です。

課題 C-1 — 「動く citation」と「使える citation」は別物

Part 1 で ### mirage-architecture-v3#00 のような chunk_id ラベルを context に入れたのに、生成側の出力に chunk_id が残らないことを観察しました。prompt engineering で「[citation: doc_id] 形式で引用を出力してください」と指示すれば部分的には動きますが、citation 文字列が 架空 になる (corpus 内に存在しない言い回しに置換される) リスクが残ります2

社内ナレッジボットの利用者にとってこれは致命的です。「認証は Keycloak です」という回答が現行 mirage-architecture-v3 から来たのか、旧版 mirage-architecture-v2-archive から来たのか — 一見同じ単語でも、根拠ドキュメントによって信頼性は桁違いに変わります。

Cross-encoder reranker で top-20 から top-3 を絞る

最初の打ち手は cross-encoder reranker です。Part 2 hybrid+filter 出力の top-20 を入力に、別モデルで再採点して top-3 に絞ります。このモデルはローカルで動くので、Ollama の 0 円経路でもそのまま再現できます。

なぜ bi-encoder では届かないか — 相互作用の有無

Part 1-2 で使った qwen3-embedding:0.6bbi-encoder 1 です。query と doc を 独立に ベクトル化して cosine 類似度を取る方式で、「query と doc を組み合わせた時の特徴」は見えません。query 単体の文脈と、doc 単体の文脈が、それぞれ高次元空間でどう配置されるか、だけしか分かりません。

cross-encoder はここが本質的に違います。query と doc を 1 度に同じ transformer に流す ので、相互注意 (cross-attention) で「コードレビューのクエリに対し、LT 大会の評価基準は『評価』が表層一致するが内容は無関係」を判定できます3

代償は計算量です。bi-encoder は corpus を 1 度埋め込めば検索は近似最近傍探索で O(1) per query ですが、cross-encoder は クエリごとに全候補との pair を forward pass するので O(k) per query になります。だから bi-encoder で候補を絞ってから cross-encoder で rerank が production パターンの定石になりました3

日本語には多言語 cross-encoder を — bge-reranker-v2-m3

reranker のモデル選択で最優先すべきは 対象言語への適合 です。本記事は BAAI/bge-reranker-v2-m3 を採用します。100 以上の言語で学習された多言語 cross-encoder で、日本語の query-passage ペアを正しく採点できます4

実は最初、英語ベンチで定番の cross-encoder/ms-marco-MiniLM-L-6-v2 (22.7M, 軽量, Apache 2.0) を試しました。英語 web search では優秀なモデルです。ところが 日本語 corpus では top-20 → top-3 の絞り込みがほぼ的外れになり、関連 chunk を落として Part 4 の aggregate スコアをむしろ下げました (Context Recall が hybrid の 0.87 から 0.66 に regression)。しかもこれは、Part 3 の単一クエリの eyeball test では まったく気づけませんでした — 後述の通り、観察したクエリでは「trap が下がった」ように見えたのです。失敗が判明したのは Part 4 で 30 件を測ったときです。

教訓は明確です。reranker は対象言語で学習されたモデルを使う。英語ベンチの NDCG が高くても、日本語で同じ性能が出るとは限りません。off-the-shelf を盲信せず、自分のデータで測る — まさに本シリーズが Part 4 で示すことの実例になりました。bge-reranker-v2-m3 は 568M とやや重い (ms-marco の約 25 倍) ですが、ローカル CPU/MPS でも top-20 規模なら実用範囲で、Ollama の 0 円経路にそのまま乗ります (RAG_RERANKER_MODEL で差し替え可)。この「英語 reranker が日本語で aggregate を壊した」顛末は §「それでも残るもの」で再訪します。

trap の rank は reranker のモデルで逆転する

実測 (observe-q3) で、nagisa-lt-evaluation の rank が各段でどう動くかを観察しました:

off-the-shelf reranker の罠 — 同じ trap でもモデルで評価が逆転する

「コードレビューの approve 数」クエリでの lt-evaluation (LT 大会評価基準) trap の rank。rank が高いほど trap を下位へ追いやれている (top-5 圏外が理想)

0123456789↑ trap doc の rank (高いほど trap を遠ざけた)P1: denseP2: hybrid+filterP3: bge-m3 (多言語)P3: ms-marco (英語)stage / reranker
英語 ms-marco は trap を 9 位まで落とし eyeball では大勝利に見えるが、Part 4 の aggregate スコアをむしろ悪化させた。多言語 bge は 3 位までと地味だが aggregate を改善する。trap doc: nagisa-lt-evaluation。qwen3-embedding:0.6b で実測

dense で 4 位だった trap は、hybrid+filter で 2 位に 悪化 します (BM25 が「評価」「基準」の表層を拾う)。ここで cross-encoder を 2 段目に挟むのですが、どのモデルを選ぶかで結果が逆転 します。

  • ms-marco (英語): trap を 9 位 (top-5 圏外) まで突き落とす。「コードレビュー」と「LT 大会の評価基準」を区別できたように見え、単一クエリの eyeball では大勝利です。
  • bge-m3 (多言語、本記事の採用): trap は 3 位までしか下がらない。地味です。

直感的には ms-marco の圧勝に見えます。ところが — これが本シリーズで最も大事な瞬間なのですが — Part 4 で 30 件を測ると評価が逆転 します。ms-marco は単一 trap を派手に落とす一方、日本語の chunk 全体をほぼランダムに並べ替えて関連 chunk を取りこぼし、aggregate の Context Recall を hybrid の 0.87 から 0.66 に下げて しまうのです。bge は per-query では地味でも、aggregate の Context Precision を押し上げます (Part 4 で詳説)。

1 つのクエリの見栄えで reranker を選んではいけない。これは Part 4「測定」のクライマックスへの最良の伏線です。bge での rerank 後 top-3 は mirage-architecture-v3#コードレビュー基準 を先頭に据え、正解文書を確実に上位へ集めます。

Anthropic Citations API で引用を構造化する

2 つ目の打ち手は Anthropic Citations API 5 です。prompt engineering で [citation: doc_id] を要求するアプローチとは別の層で動きます。ここは Anthropic 固有機能なので、ローカル Ollama 経路では --mock で構造だけ確認し、実際の引用付き生成は Anthropic キーを使います。

3 種の document source — RAG では custom content がほぼ正解

Citations API には 3 種類の document source type があり、それぞれ chunking と citation の粒度が違います5

TypeChunkingCitation formatRAG 適性
text (plain text)自動 sentence chunkingchar_location (0-indexed char range)RAG chunk より細かい粒度
application/pdf自動 sentence chunkingpage_location (1-indexed page)PDF 原本のまま
content (custom content)追加 chunking なしcontent_block_location (0-indexed block)RAG chunks に最適

公式 docs から直接引用します:

If you want to customize any additional chunking, you can put RAG chunks into custom content document(s). 5

本記事は custom content を選びます。理由は単純で、Part 1-2 で確立した chunk_id ベースの観察可能性を Generation 側まで維持したい から。1 chunk = 1 block で渡せば、start_block_index がそのまま chunk_id に対応します。

documents block の組み立てと cited_text の戻り

top-3 rerank 結果を anthropic SDK の messages content に変換します。最小例:

import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-opus-4-7",
max_tokens=1024,
messages=[
{
"role": "user",
"content": [
{
"type": "document",
"source": {
"type": "content",
"content": [
{"type": "text", "text": chunk.body}
for chunk in top3_chunks
],
},
"title": top3_chunks[0].chunk_id,
"context": f"status={top3_chunks[0].doc_status}",
"citations": {"enabled": True},
},
{"type": "text", "text": "Mirage 開発でコードレビューに必要な approve 数は?"},
],
}
],
)

ポイントは 3 つ:

  • title に chunk_id (mirage-architecture-v3#コードレビュー基準#00 など) を入れると、response の document_title で chunk_id が直接戻る
  • context は LLM には渡るが cited_text の対象外 (公式に明記)5。doc 全体のステータス情報や version を入れる場所として最適。Part 2 で抽出した status=active / archived をここに置けば、UI で「現行か旧版か」を出せる
  • cited_textoutput tokens に数えない。subsequent turn で送り返しても input tokens に数えない5。prompt engineering で長い citation を要求するより、コスト的にも有利

知っておくべき 2 つの非対称制約

Citations API には設計レベルの非対称制約が 2 つあります。両方とも公式に明記されています5

  • all-or-none: 「citations must be enabled on all or none of the documents within a request」。一部の document だけ citations 有効、は不可
  • Structured Outputs と非互換: output_config.format を同時指定すると 400 error。citation block と text block の interleaving が strict JSON schema と矛盾するため

逆に tool use / function calling とは併用可能 で、prompt caching とも併用可能 です (cache_control: { type: "ephemeral" } を document block に付与)。本シリーズで言えば、Part 5 で運用視点で扱う prompt caching と組み合わせて、ドキュメント側だけキャッシュする pattern が成立します。

rerank → documents → 引用付き回答 の 1 本のパイプライン

ここまでの 2 つの打ち手は別々に動くわけではありません。retrieve → rerank → documents block → claude.messages.create で 1 本につながります。companion repo の examples/generation/run.py がこの順序を encapsulate します。

prompt_builder.py の責務

src/rag/prompt_builder.py は単純な変換器です。入力は top-3 rerank 後の chunks (Part 2 hybrid+filter の出力 → cross-encoder の出力)、出力は anthropic SDK の messages content 配列。やることは:

  • chunk metadata から title (chunk_id) と context (doc status) を抽出
  • chunk body をそのまま content[].text に詰める (custom content なので追加 chunking しない)
  • system prompt として「Cite specific chunks from the provided documents when stating facts.」を付与

引用付き回答の戻り例 (Anthropic 経路)

クエリ「Mirage 開発でコードレビューに必要な approve 数は?」を Anthropic 経路で叩いた response の構造 (代表例):

response.content = [
{"type": "text", "text": "Mirage のコードレビューでは、PR をマージするのに "},
{
"type": "text",
"text": "2 人以上の approve が必須です (うち 1 人は他事業部の reviewer が望ましい)",
"citations": [
{
"type": "content_block_location",
"cited_text": "2 人以上の approve 必須 (うち 1 人は同事業部、もう 1 人は他事業部 reviewer が望ましい)",
"document_index": 0,
"document_title": "mirage-architecture-v3#コードレビュー基準#00",
"start_block_index": 0,
"end_block_index": 1,
}
],
},
{"type": "text", "text": "。1 PR あたり 400 LOC までが目安です。"},
]

document_title に chunk_id がそのまま戻り、cited_text で source 内の実テキストがコピペレベルで取れます。これは prompt engineering では構造的に保証できない property です — citation 文字列が corpus 内に必ず存在する ことを API 側が parse 時に担保しているからです5。LT 大会の評価基準 (trap) は rerank で top-3 から外れているので、そもそも document block に入らず、引用される余地もありません。

「approve は 2 人」の根拠が 現行 mirage-architecture-v3 のコードレビュー基準節 から来たことを、UI で document_title を見出しとして表示するだけで読者に開示できます。

それでも残るもの — Part 4 で測定する 3 つの距離

ここまでの 2 つの打ち手で、Part 2 で観察した天井は 構造的に縮みました。けれど距離はゼロではありません。3 つの「残るもの」を Part 4 への伏線として置きます。

「正しい citation」と「正しい claim」の差

Citations API が保証するのは citation 文字列が source 内に存在する ことだけです5。「claim が citation で実際に support される」かは依然として LLM の判断に委ねられます。

Stanford の 2025 研究は、purpose-built な legal RAG であっても 17-34% のクエリで hallucinate すると報告しました6。citation の文字列は valid でも、claim がそれを正しく要約していなければ、結局は誤った回答に valid citation が紐付くだけです。Part 4 では RAGAs の Faithfulness (claim が context に裏付けされているか) で、ここを定量化します。

reranker が言語・ドメインで外れる時

本記事は最初に英語 ms-marco を試し、日本語 corpus で aggregate を悪化させた ので多言語 bge-m3 に切り替えました (§「日本語には多言語 cross-encoder を」)。これは「reranker は対象言語・ドメインで学習されたモデルを使う」という原則の、自分の足で踏んだ生きた実例です。NDCG の高い英語ベンチ番長が、日本語ではほぼランダムソートになり得ます。

bge-m3 でも万能ではありません。法務 / 医療 / 専門 jargon が密な領域では off-the-shelf の精度が落ちます7。その場合は domain-specific な query-document pair に正解 label を付けて fine-tuning するか、LLM-as-reranker のような動的な代替を検討します。本シリーズの corpus 規模では fine-tuning は不要ですが、production で使うときの判断軸として頭に置いておいてください。

context 爆発は top-3 で同時に緩和される副作用

Part 1 で挙げたグループ C-2「context 爆発」は、本 Part の打ち手で 副作用的に同時緩和 されます。top-20 から top-3 に絞れば:

  • LLM context への入力 token は 85% 削減 (20 → 3)
  • 「Lost in the Middle」(Liu et al., TACL 2023)8 の U-shape 性能低下を回避できる確率が上がる
  • follow-up の Hsieh らの calibration 手法9は U-shape を補正できますが、短く絞った context のほうが構造的にトラブルが少ない

ただし top-3 が 常に最適 ではありません。「2023 と 2025 の経費上限の差は?」のような 多文書統合が必要なクエリ では top-k を増やす判断が要ります。これも Part 4 の Context Recall で評価する伏線として置いておきます。

companion repo と次回 Part 4

本 Part の実装は companion repo の part-03 tag で再現できます。Part 1-2 と同じパターン:

Terminal window
ollama pull qwen3:8b && ollama pull qwen3-embedding:0.6b
git clone https://github.com/zawazawa5809/rag-fundamentals-companion.git
cd rag-fundamentals-companion
git checkout part-03
uv sync --frozen
echo "RAG_PROVIDER=ollama" >> .env
uv run python -m examples.generation.run --observe-q3 # trap の rank 推移を実測
uv run python -m examples.generation.run --mock # Citations の構造を確認 (キー不要)

実装ファイルは src/rag/reranker.py (cross-encoder wrapper、ローカル動作) / src/rag/prompt_builder.py (custom content documents の組み立て) / examples/generation/run.py (Part 2 hybrid+filter → rerank → Citations のエンドツーエンド) の 3 つです。reranker までは Ollama 経路で 0 円再現でき、Citations の引用付き生成だけ Anthropic キーが要ります (RAG_PROVIDER=anthropic_openai)。

ここまでで Part 1 のグループ A (trap) と C (引用喪失・context 爆発) がそれぞれ「部分解決」されました。次回 Part 4 では:

  • 30 クエリ × 厳密 golden set
  • RAGAs の 4 指標 (Faithfulness / Answer Relevance / Context Precision / Context Recall)
  • Part 1-3 の改善が 客観的なスコア改善 として再確認されるか

をクライマックスとして扱います。本 Part の Citations / reranker が「感覚的にはよくなった」ことを、Part 4 で 数字に翻訳 します。


シリーズ全体: 今更聞けない RAG の作り方、評価の仕方

次回 Part 4: 「評価 (クライマックス) — RAGAs 4 指標で Part 1-3 の改善を客観評価する」

参考文献

Footnotes

  1. Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks — Reimers & Gurevych, EMNLP 2019. bi-encoder の表層 bias (Part 1, 2 でも引用) 2

  2. Attribution Techniques for Mitigating Hallucinated Information in RAG Systems: A Survey — 2026 survey。citation 系手法の分類と limitation

  3. Retrieve & Re-Rank — sentence-transformers — 公式 2 段 pipeline 解説。cross-encoder の cross-attention と計算量トレードオフ 2

  4. BAAI/bge-reranker-v2-m3 - Hugging Face — 多言語 cross-encoder reranker (約 568M, XLM-RoBERTa-large ベース, 100+ 言語対応)。比較対象の英語専用 ms-marco-MiniLM-L-6-v2 は 22.7M / Apache 2.0 と軽量だが日本語では精度が出ない

  5. Citations - Claude API Docs — 公式仕様。citations.enabled / 3 source types / cited_text の input/output token 非計上 / Structured Outputs 非互換 / prompt caching 互換性を一次参照 2 3 4 5 6 7 8

  6. Hallucination-Free? Assessing the Reliability of Leading AI Legal Research Tools — Stanford 2025。purpose-built RAG でも 17-34% hallucinate

  7. Cross-Encoder Reranking in Practice: What Cosine Similarity Misses — 2026-04。off-the-shelf cross-encoder の domain mismatch 実証

  8. Lost in the Middle: How Language Models Use Long Contexts — Liu et al., TACL 2023。U-shape 性能、関連情報が中央で大幅劣化

  9. Found in the Middle: Calibrating Positional Attention Bias Improves Long Context Utilization — Hsieh et al., ACL Findings 2024。calibration で長 context retrieval を +15pp 改善