0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Bedrock Knowledge Base の Retrieve は生チャンクを返さなかった — S3 Vectors で RAG 3方式実測比較

0
Posted at

TL;DR

同一の Knowledge Base(S3 Vectors バックエンド)に対して、同じ 2 ターンの会話を 3 つの実装方式で実行し、挙動を比較しました。

  • KB の Retrieve API は「生チャンク」を返しません。ベクトルインデックスに入っているのは 228〜690 字のチャンクなのに、Retrieve は最大 8,120 字に結合されたチャンクを返してきました。入力トークンは直叩き比で 3.3 倍、そのぶん回答は深くなります。
  • フルマネージドの RetrieveAndGenerate(デフォルト設定)は、本実験では**「〜だけ詳しく」という絞り込みのフォローアップに追従できませんでした**(毎回全体を再説明する)。分かれ目は「会話履歴を誰が持つか」です。
  • RAG のレイテンシの 98% は生成でした(Q1 の実測ペア: 生成 22.6s vs 検索 0.5s)。検索基盤(S3 Vectors)の速度は個人〜中規模用途ではボトルネックになりません。
  • 検証環境一式(Terraform + boto3 CLI)は GitHub で公開しています: bedrock-lab (phase2 タグ)

この記事で扱うこと・扱わないこと

Knowledge Base + S3 Vectors の構築手順そのものは扱いません。構築は先行記事が充実しています(@zumax さんの統合手順@hayao_k さんの CloudFormation 実装@takeyan さんの考慮点整理など。末尾の参考にまとめています)。

本記事が扱うのは**「作った後」の話**です。出来上がった同じベクトルインデックスを

  1. RetrieveAndGenerate API(検索も生成も履歴管理もフルマネージド)
  2. Retrieve API + Converse API(検索だけマネージド、プロンプト合成と履歴は自前)
  3. S3 Vectors 直叩き(埋め込み生成→ベクトル検索→合成まで全部自前)

の 3 方式で叩くと、同じ質問への挙動がどう変わるかを実測します。この 3 択の実測比較は探した範囲で見つからなかったので、一次データとして残します。

実験設定

3方式の構成比較
図1: 3方式のデータフロー(検索・プロンプト合成・会話履歴をどこまで自前でやるか)

項目 内容
ベクトルストア S3 Vectors(Titan Text Embeddings V2 / 1024 次元 / cosine)
Knowledge Base チャンキングは KB デフォルト(約 300 トークン・文境界を尊重)
投入データ 筆者の Qiita 全 35 記事 + 検証リポジトリの docs 3 本 = 38 ドキュメント(日本語 Markdown)
メタデータ サイドカー JSON(title / source_url / published / tags)を各ドキュメントに付与
生成モデル jp.anthropic.claude-sonnet-4-6(3 方式共通)
検索件数 top_k=5(3 方式共通)
リージョン ap-northeast-1(東京)
時期 2026年7月

ユースケースは「自分が書いた技術記事・運用メモを検索する個人版ナレッジ検索」です。投入データはすべて公開済み記事なので、そのまま記事にできます(再現用の取得スクリプトもリポジトリの scripts/build_kb_docs.py に置いてあります)。

会話は次の 2 ターンで固定しました。

  • Q1: 「ALB の 502 エラーの切り分け方法は?」(初回質問)
  • Q2: 「その 3 層のうち Target Group 層だけ詳しく」(絞り込みのフォローアップ

Q2 が今回の実験の肝です。ナレッジ検索を実際に使うとき、1 問 1 答で終わることはまずなく、「今の回答のここだけ深掘りして」という会話になります。このとき前のターンの文脈を誰がどう保持しているかが、方式によってまったく違います。

3方式の実装

コードは要点のみ抜粋します(全文はリポジトリの client/ 参照)。

方式①: RetrieveAndGenerate(フルマネージド)

1 回の API 呼び出しで検索→プロンプト合成→生成まで完結します。マルチターンは sessionId を渡すだけで、会話文脈は KB 側が管理します。

kwargs = {
    "input": {"text": user_text},
    "retrieveAndGenerateConfiguration": {
        "type": "KNOWLEDGE_BASE",
        "knowledgeBaseConfiguration": {
            "knowledgeBaseId": kb_id,
            "modelArn": model_arn,  # jp. プロファイルの ARN を指定可能
        },
    },
}
if session_id:              # 2ターン目以降はこれだけで文脈維持
    kwargs["sessionId"] = session_id
resp = client.retrieve_and_generate(**kwargs)
session_id = resp.get("sessionId")

方式②: Retrieve + Converse(検索だけマネージド)

検索は KB の Retrieve API に任せ、返ってきたチャンクを自分でプロンプトに組み立てて Converse API に渡します。会話履歴は自前のリストです。

resp = agent_client.retrieve(
    knowledgeBaseId=kb_id,
    retrievalQuery={"text": query},
    retrievalConfiguration={"vectorSearchConfiguration": {"numberOfResults": 5}},
)
chunks = resp["retrievalResults"]  # ← ここで返るものが後述の発見 1

# 履歴 + 今回だけ資料付きメッセージ (資料は履歴に積まない)
messages = list(history)
messages.append({"role": "user", "content": [{"text": build_user_message(query, chunks)}]})
resp = runtime_client.converse_stream(modelId=MODEL_ID, messages=messages,
                                      system=[{"text": RAG_SYSTEM_PROMPT}], ...)

1 点だけ設計上の工夫があります。検索で取得した資料は送信時のプロンプトにだけ注入し、履歴には生の質問と回答だけを積みます。資料ごと履歴に積むと入力トークンが毎ターン数千ずつ膨張していくためです。

方式③: S3 Vectors 直叩き(フル自前)

KB を経由せず、埋め込み生成(Titan V2)→ S3 Vectors の QueryVectors → プロンプト合成まで全部自分でやります。生成と履歴管理は方式②と同じです。

# 1. クエリを埋め込みベクトルに変換
body = json.dumps({"inputText": query, "dimensions": 1024, "normalize": True})
resp = runtime_client.invoke_model(modelId="amazon.titan-embed-text-v2:0", body=body)
vector = json.loads(resp["body"].read())["embedding"]

# 2. ベクトルインデックスを直接検索
resp = s3vectors_client.query_vectors(
    vectorBucketName=bucket, indexName=index,
    queryVector={"float32": vector},
    topK=5, returnDistance=True,
    returnMetadata=True,  # ← QueryVectors に加えて GetVectors 権限が要る (おまけ参照)
)
# チャンク本文はメタデータの AMAZON_BEDROCK_TEXT に入っている

結果

① RetrieveAndGenerate ② Retrieve + Converse ③ S3 Vectors 直叩き
Q2 の絞り込みに追従できたか ❌ 3 層全体を再説明 ✅ Target Group 層のみ深掘り ✅ Target Group 層のみ深掘り
会話履歴の持ち主 KB(sessionId、制御不可) 自前リスト(制御可) 自前リスト(制御可)
返ってきたチャンク 不明(内部処理) 3 件・最大 8,120 字(結合済み) 5 件・228〜690 字(生チャンク)
入力トークン(Q1) 不明(内部処理) 6,081 1,820
検索レイテンシ(Q1) —(内訳非公開) 514ms 358ms(埋め込み 199 + 検索 158)
生成レイテンシ(Q1) —(内訳非公開) 22.6s 12.1s
回答の深さ(Q2) ◎ 判定の機構まで説明 ○ 要点は正確
実装コード量 数十行 大(埋め込み・スコア解釈も自前)

発見

1. KB の Retrieve は生チャンクを返さなかった

タイトルの件です。チャンキング設定は KB デフォルト(約 300 トークン)なので、インデックスに格納されているチャンク — S3 Vectors のメタデータ AMAZON_BEDROCK_TEXT に入っている単位で、本記事ではこれを「生チャンク」と呼びます — は、方式③の直叩きで見えるとおり 228〜690 字です。ところが同じインデックスへの同じクエリで、方式②の Retrieve API は最大 8,120 字のチャンクを返してきました。KB が検索ヒット箇所の前後を結合・拡張して返しているようです(階層チャンキングは未設定なので、いわゆる親チャンク返却とも別の挙動。正確な仕組みは公式ドキュメントに記載が見当たらず、追加検証課題です)。

この差は下流に連鎖します。方式②は入力トークンが方式③の 3.3 倍(6,081 vs 1,820)、生成レイテンシ約 2 倍。そのかわり文脈が厚いぶん回答は明確に深くなりました(Q2 で方式②はヘルスチェック判定ラグの機構まで説明、方式③は要点のみ)。

つまり Retrieve API を使うことは、単に「検索をマネージドに任せる」のではなく、「トークンを多めに払って文脈の厚い材料をもらう」という取引です。コスト最適化で方式③に切り替えると、静かに回答品質も変わります。ここを実測で意識できたのが今回一番の収穫でした。

2. 「〜だけ詳しく」に答えられるかは、会話履歴を誰が持つかで決まる

Q2「Target Group 層だけ詳しく」に対して、方式①だけが 3 層全体を律儀に再説明しました。sessionId による文脈維持は効いている(「その 3 層」は解決できている)のに、です。

方式①は毎ターン検索が走り、生成が検索チャンクに支配されます。生成プロンプトのテンプレート差し替え(textPromptTemplate)という調整手段はあるものの、会話履歴そのものは KB 管理で、履歴の中身を前提にした制御はできません(本実験の①はデフォルト設定です)。一方②③は履歴が自前のリストなので、システムプロンプトに「直前の回答と重複する説明は繰り返さない。絞り込みの指示には該当部分のみ深掘りする」と書けて、期待どおりに動きます。

手軽さの①、制御性の②③。1 問 1 答なら①で十分ですが、対話型のナレッジ検索を作るなら、絞り込みフォローアップは最初に試すべきテストケースです。

3. フォローアップ時、検索はほぼ働いていない

Q2 の検索スコアは激減しました(方式③: 0.77→0.37、方式②: 0.88→0.68。無関係な記事もヒット)。「Target Group 層だけ詳しく」のような短い絞り込み文は、検索クエリとしては弱いのです。それでも②③の回答が正確だったのは、会話履歴が回答を支えていたからです。

マルチターン RAG で「毎ターン素朴に再検索」は無駄が多い、という学びです。会話文脈を織り込んだ検索クエリの生成(クエリ書き換え)や、検索スキップ判定に改善余地があります。このあたりは会話型 QA の研究領域としても確立しています(参考に論文リンクを置きました)。

4. レイテンシの 98% は生成

検索は 200〜500ms、生成は 9〜23 秒(方式② Q1 の計測ペアでは生成 22.6s / 検索 0.51s ≒ 97.8%)。RAG の体感速度を決めるのは検索基盤ではなく生成側(モデル選択・出力長・ストリーミング表示の有無)でした。

S3 Vectors は「低頻度アクセス向け・サブ秒クエリ」という位置づけで OpenSearch Serverless より遅いとされますが、この構造なら個人〜中規模用途で検索側の差は誤差です。固定費ゼロ(従量のみ)で KB のバックエンドになる S3 Vectors は、ホームラボ用途では文句なしでした。

5. 自前化するほど IAM が太る

方式①②は bedrock:Retrieve* + 生成モデルの InvokeModel で済みますが、方式③は追加で埋め込みモデルの InvokeModels3vectors:QueryVectors が要ります。さらに returnMetadata=True を付けると s3vectors:GetVectors も必要でした(AccessDenied で発見。QueryVectors だけでは足りません)。

マネージドを剥がすことは、権限管理の責任を自分で引き受けることでもあります。

6. スコアの意味が方式で違う

KB Retrieve の score は「大きいほど近い」正規化済みの値、S3 Vectors の distance は cosine 距離で「小さいほど近い」。体系が別物なので、方式②で作った閾値ロジックを方式③に移植するとそのままでは壊れます。

使い分けの結論

用途 方式
単発の質問応答をとにかく速く作る ① RetrieveAndGenerate
マルチターン対話・プロンプトを制御したい ② Retrieve + Converse(バランス型。検索品質は KB に任せて制御を握る)
RAG の内部を理解する・検索を自作したい(クエリ書き換え、リランキング等) ③ S3 Vectors 直叩き

おまけ: 構築時につまずいた 2 点

同期が 38 件中 30 件失敗 — non-filterable キーは「2 つ」必要

初回のデータ同期で 38 ドキュメント中 30 件がこのエラーで失敗しました。

Filterable metadata must have at most 2048 bytes

S3 Vectors のインデックスを自作して KB に接続する場合、AMAZON_BEDROCK_TEXT(チャンク本文)と AMAZON_BEDROCK_METADATA(KB の内部管理 JSON)の両方を non-filterable に指定する必要があります(公式ドキュメント記載の要件)。片方だけだと、もう片方がフィルタ可能メタデータ扱いになり 2,048 バイト制限に当たります。

resource "aws_s3vectors_index" "kb" {
  # ...
  metadata_configuration {
    non_filterable_metadata_keys = ["AMAZON_BEDROCK_TEXT", "AMAZON_BEDROCK_METADATA"]
  }
}

この制約自体は既出です(@hayao_k さんの記事でも「実質必須」と指摘されています)。ここで付け加えたいのは日本語データだと顕在化が早いことです。UTF-8 の日本語は 1 字 3 バイトなので、690 字の生チャンクは本文だけで約 2,070 バイト。300 トークンという小さめのデフォルトチャンクですら制限を超えます。「全部失敗」ではなく 30/38 という中途半端な失敗(短いチャンクだけ成功)になるのも、原因を紛らわしくするポイントでした。

QueryVectors の returnMetadata には GetVectors 権限が要る

発見 5 の再掲ですが、単体でハマりどころなので手順として残します。s3vectors:QueryVectors を許可しても、returnMetadata=True 付きの呼び出しは AccessDenied になります。s3vectors:GetVectors を併せて許可してください。

実はこれ、QueryVectors のリファレンスの Permissions に明記されています(メタデータフィルタまたは returnMetadata には両方の権限が必要、無ければ 403)。エラーを踏んでから読み直して気づきました。API リファレンスは Permissions 欄まで読んでから IAM を書く、という基本の再確認でした。

まとめ

学び 内容
Retrieve の返すもの 生チャンクでなく結合チャンク。トークン 3.3 倍と引き換えに回答が深い
絞り込みフォローアップ デフォルトの①は追従できず、履歴を自前で持つ②③は追従できた
フォローアップ時の検索 スコア激減。回答を支えるのは履歴
レイテンシ 98% は生成。検索基盤の速度は個人〜中規模では誤差
IAM 自前化するほど太る。returnMetadata には GetVectors も必要
自作インデックス non-filterable キーは 2 つ必須。日本語データは顕在化が早い

Terraform 一式・3 方式のクライアント実装・計測ログの取り方は GitHub で公開しています。

前回(Phase 1)の記事はこちら: 動いていた Bedrock の Claude が数分で使えなくなった — モデルアクセス有効化を CLI で完結する手順とハマりどころ

参考

AWS 公式

先行記事(構築手順はこちらが詳しいです)

同事象の報告・関連研究

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?