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 さんの考慮点整理など。末尾の参考にまとめています)。
本記事が扱うのは**「作った後」の話**です。出来上がった同じベクトルインデックスを
- RetrieveAndGenerate API(検索も生成も履歴管理もフルマネージド)
- Retrieve API + Converse API(検索だけマネージド、プロンプト合成と履歴は自前)
- S3 Vectors 直叩き(埋め込み生成→ベクトル検索→合成まで全部自前)
の 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 で済みますが、方式③は追加で埋め込みモデルの InvokeModel と s3vectors: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 公式
- Using S3 Vectors with Amazon Bedrock Knowledge Bases (S3 User Guide) — non-filterable 2 キー要件の一次情報
- Metadata filtering / Limitations (S3 Vectors) — フィルタ可能メタデータ 2KB 制限
- Query a knowledge base and retrieve data (Amazon Bedrock) — Retrieve / RetrieveAndGenerate API
- How content chunking works for knowledge bases (Amazon Bedrock) — チャンキング戦略
- S3Vectors.Client.query_vectors (boto3) — returnMetadata に GetVectors 権限が必要な旨の Permissions 記載
先行記事(構築手順はこちらが詳しいです)
- S3 VectorsとBedrock Knowledge Basesを統合してみた (@zumax)
- Amazon Bedrock Knowledge Bases + S3 Vectors を CloudFormation で実装する (@hayao_k) — 2048B 制約と NonFilterableMetadataKeys の指摘あり
- S3 VectorsとKnowledge Baseを組み合わせたRAG構築における考慮点 (@takeyan) — メタデータ設計の考慮点
- 既存の S3 Vectors を Bedrock Knowledge Bases で利用する方法 (@revsystem)
同事象の報告・関連研究