Knowledge Bases (managed RAG)
A Bedrock Knowledge Base is Ch 3's RAG pipeline, managed: embeddings, vector store, chunking and retrieval behind two calls. You bring S3 docs; AWS keeps the index in sync.
- AWS credentials (
aws configure) + Bedrock model access enabled in your region +pip install boto3 - AWS credentials (
aws configure) +pip install boto3
Learning objectives
- Explain what a Bedrock Knowledge Base gives you over hand-rolled RAG (Ch 3).
- Create a KB backed by OpenSearch Serverless and ingest documents from S3.
- Query with
Retrieve(chunks only) andRetrieveAndGenerate(answer). - Decide when managed RAG beats owning the pipeline yourself.
code/aws4-bedrock-kb-rag/ in the course, with a README on how to run it. Read here, run there — no need to copy-paste.Managed RAG vs. the hand-built pipeline essential
In Ch 3 you built RAG from scratch: chunk, embed, store, retrieve, stuff the prompt. A Bedrock Knowledge Base manages all of that — embeddings, a vector store, chunking, and retrieval — behind two API calls. You bring documents in S3; AWS keeps the index in sync.
Create the KB and ingest essential
A KB needs: an S3 data source, an embeddings model, and a vector store (OpenSearch Serverless is the default). We create it here with boto3; W7 shows the Terraform/CDK version you would actually use in production.
create_kb.pyimport boto3
agent = boto3.client("bedrock-agent", region_name="us-east-1") # KB control plane
kb = agent.create_knowledge_base(
name="support-docs",
roleArn="arn:aws:iam::123456789012:role/bedrock-kb-role", # needs S3 + OpenSearch access
knowledgeBaseConfiguration={
"type": "VECTOR",
"vectorKnowledgeBaseConfiguration": {
"embeddingModelArn": "arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v2:0"
},
},
storageConfiguration={
"type": "OPENSEARCH_SERVERLESS",
"opensearchServerlessConfiguration": {
"collectionArn": "arn:aws:aoss:us-east-1:123456789012:collection/abc123",
"vectorIndexName": "support-index",
"fieldMapping": {"vectorField": "vec", "textField": "text", "metadataField": "meta"},
},
},
)
print("KB id:", kb["knowledgeBase"]["knowledgeBaseId"])
This creates a Knowledge Base (KB) — Amazon's managed version of the RAG pipeline you hand-built in Ch 3. Instead of writing code to chunk, embed and store documents, you describe what you want and AWS runs the machinery. This one block declares all three pieces a KB needs: where documents live, which model turns text into vectors, and where those vectors are stored.
boto3.client("bedrock-agent", ...)opens a connection to the AWS service that manages Knowledge Bases (the "control plane" — the part that creates and configures things).region_name="us-east-1"picks the AWS data-centre region.agent.create_knowledge_base(...)is the single call that builds the KB. Everything inside the parentheses is configuration passed as named arguments.nameis a human label;roleArnis an AWS IAM role — an identity with permission to read your S3 documents and write to the vector store. The KB acts as this role when it works.knowledgeBaseConfigurationsays the KB is aVECTORstore and names the embedding model (amazon.titan-embed-text-v2) — the model that converts each chunk of text into a list of numbers (a vector) capturing its meaning.storageConfigurationpoints at OpenSearch Serverless, the default vector database.fieldMappingtells it which fields hold the vector, the original text, and metadata.collectionArnis the unique address of that store.print("KB id:", ...)pulls the new KB's ID out of the reply. You'll reuse this ID in every later call, so note it down.
What the output means: On success AWS prints something like KB id: KB123. That ID is the handle you pass to the ingestion and query calls below. The ARNs and IDs shown are placeholders — swap in your own.
Try this: Read the three sub-configs top to bottom and match them to the KB's three needs: S3 docs (added next), embedding model (embeddingModelArn), and vector store (opensearchServerlessConfiguration). Seeing all three named here is the whole point.
After attaching an S3 data source you trigger an ingestion job to embed and index. Re-run it whenever the documents change.
ingest.pyimport boto3
agent = boto3.client("bedrock-agent", region_name="us-east-1")
job = agent.start_ingestion_job(
knowledgeBaseId="KB123",
dataSourceId="DS456",
)
print("ingestion job:", job["ingestionJob"]["status"]) # STARTING -> IN_PROGRESS -> COMPLETE
Creating the KB in W4.1 built the empty container. Your documents are not indexed yet. An ingestion job is the step that actually reads the files from S3, chunks them, calls the embedding model on each chunk, and writes the resulting vectors into OpenSearch. You run this once after setup, and again every time the documents change.
- We reconnect to
bedrock-agent— the same control-plane client as W4.1, because starting an ingestion job is a management action. agent.start_ingestion_job(...)kicks off the indexing work.knowledgeBaseIdis the KB from W4.1;dataSourceIdidentifies the S3 data source you attached to it (KB123/DS456are placeholders).- The job runs asynchronously — the call returns immediately with a starting status rather than waiting for all documents to finish.
print(...job["ingestionJob"]["status"])shows that first status.
What the output means: You'll see a status like STARTING. Behind the scenes it moves STARTING → IN_PROGRESS → COMPLETE (the comment spells this out). You would poll the job again later, or check the console, to confirm it reached COMPLETE before querying.
Try this: Think about why re-running matters: if you add a new support doc to S3 tomorrow, the KB won't know about it until you start another ingestion job. Managed RAG syncs on demand, not by magic.
Query: Retrieve vs RetrieveAndGenerate intermediate
retrieve returns the matching chunks — use it when you want to control the prompt (feed chunks to Claude yourself). retrieve_and_generate does retrieval + answering in one call, with citations.
query_kb.pyimport boto3
rt = boto3.client("bedrock-agent-runtime", region_name="us-east-1") # KB data plane
# One-call RAG: retrieves, then answers with Claude, with citations.
resp = rt.retrieve_and_generate(
input={"text": "How do I reset a customer's password?"},
retrieveAndGenerateConfiguration={
"type": "KNOWLEDGE_BASE",
"knowledgeBaseConfiguration": {
"knowledgeBaseId": "KB123",
"modelArn": "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20241022-v2:0",
},
},
)
print(resp["output"]["text"])
for c in resp["citations"]:
for ref in c["retrievedReferences"]:
print(" source:", ref["location"])
Now we ask a question against the indexed KB. Bedrock offers two query styles: retrieve returns just the matching chunks (you then write your own prompt to Claude), and retrieve_and_generate — used here — does the whole RAG loop in one call: find relevant chunks, feed them to Claude, and return a written answer plus citations pointing back to the sources.
boto3.client("bedrock-agent-runtime", ...)connects to the data plane — the part that uses the KB to answer queries (as opposed to the control plane that built it). Different job, different client.rt.retrieve_and_generate(...)is the one-call RAG.input={"text": "How do I reset a customer's password?"}is the user's question in plain English.retrieveAndGenerateConfigurationtells it how to answer:type: KNOWLEDGE_BASEmeans "use a KB",knowledgeBaseIdpicks which KB, andmodelArnchooses the model that writes the answer — here Claude 3.5 Sonnet.print(resp["output"]["text"])prints Claude's final written answer, grounded in your documents.- The
for c in resp["citations"]loop walks the citations. Each citation listsretrievedReferences— the actual document chunks used — and we print eachref["location"], i.e. where that fact came from. This is how you prove the answer isn't made up.
What the output means: You get a natural-language answer to the password question, followed by one or more source: lines showing which S3 documents backed it. Citations are the key advantage of managed RAG — grounding and traceability come free.
Try this: Decide which call you'd want for two tasks: (1) a chatbot that shows a polished answer — retrieve_and_generate; (2) a custom prompt where you control every instruction to Claude — plain retrieve, then build the prompt yourself.
Exercise W4.1 — Compare to Ch 3
Context: The honest way to judge a managed Knowledge Base is to race it against the hand-built RAG pipeline you already wrote, over the same documents. The interesting answer is rarely 'which is more accurate' — it's 'which cost less to maintain'.
Your task: Point a Bedrock KB at the same documents from your Ch 3 RAG lab, ask five questions through both retrieve_and_generate and your hand-built pipeline, and compare where they differ and which was less work to maintain.
Requirements:
- Ingest the identical Ch 3 corpus into a Knowledge Base
- Run the same five questions through both the managed KB and the hand-built pipeline
- Compare answers and citations side by side, noting where they diverge
- Judge each on operational cost — which required less code and upkeep
- Draw a concrete conclusion about when the managed KB's lost control is worth the saved ops
💡 Hint: Hold the corpus and the questions fixed so the only variables are the retrieval stack and the maintenance effort — that's what makes the comparison fair.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Bedrock Knowledge Bases collapse the entire RAG pipeline — retrieve, generate, and cite — into a single managed call. It's the fastest path from 'documents in S3' to a grounded answer without building any retrieval yourself.
Your task: Ask a question against a Knowledge Base with retrieve_and_generate and print the grounded answer.
Requirements:
- Use the
bedrock-agent-runtimeclient (the KB data plane) - Pass the question via
input={"text": ...} - Configure
type: "KNOWLEDGE_BASE"with aknowledgeBaseIdand a generation model ARN - Print the grounded answer from
output.text
💡 Hint: Note the third client family: management is bedrock-agent, plain inference is bedrock-runtime, but KB queries go through bedrock-agent-runtime.
Show solution
bedrock-agent-runtime is the data plane. retrieve_and_generate retrieves, answers, and cites in one call.
import boto3
rt = boto3.client("bedrock-agent-runtime", region_name="us-east-1")
resp = rt.retrieve_and_generate(
input={"text": "What is our refund window?"},
retrieveAndGenerateConfiguration={
"type": "KNOWLEDGE_BASE",
"knowledgeBaseConfiguration": {
"knowledgeBaseId": "KB123456",
"modelArn": "arn:aws:bedrock:us-east-1::foundation-model/"
"anthropic.claude-3-5-sonnet-20241022-v2:0",
},
},
)
print(resp["output"]["text"])
Context: A grounded answer you can't trace isn't much better than a guess. Knowledge Bases return citations that map each answer back to the exact source chunks in S3 — surfacing them is what makes the answer auditable.
Your task: Extend the retrieve_and_generate call to also print the source document location of each citation.
Requirements:
- Keep printing the generated answer from
output.text - Iterate the
citationslist on the response - For each citation, walk its
retrievedReferences - Print each reference's
location(the source S3 doc)
💡 Hint: Citations are nested two levels deep — a list of citations, each holding a list of retrievedReferences, each with its own location.
Show solution
Citations trace the answer back to source S3 docs via retrievedReferences[].location.
import boto3
rt = boto3.client("bedrock-agent-runtime", region_name="us-east-1")
resp = rt.retrieve_and_generate(
input={"text": "What is our refund window?"},
retrieveAndGenerateConfiguration={
"type": "KNOWLEDGE_BASE",
"knowledgeBaseConfiguration": {
"knowledgeBaseId": "KB123456",
"modelArn": "arn:aws:bedrock:us-east-1::foundation-model/"
"anthropic.claude-3-5-sonnet-20241022-v2:0",
}})
print(resp["output"]["text"])
for c in resp.get("citations", []):
for ref in c.get("retrievedReferences", []):
print("source:", ref["location"])
Context: Sometimes the managed generation is too rigid — you want your own system prompt, reranking, or model. Retrieve hands you just the raw chunks so you own everything downstream, trading convenience for control.
Your task: Use retrieve (not retrieve_and_generate) to fetch raw chunks, build your own prompt from them, and explain when you'd choose this split.
Requirements:
- Call
retrievewith aknowledgeBaseIdand avectorSearchConfiguration(e.g.numberOfResults) - Pull the chunk text out of
retrievalResults[].content.text - Assemble the chunks into your own context/prompt to send to
conversewith your chosen model - Justify the choice: pick retrieve-only when you need control over prompt, reranking, or model that the managed call hides
💡 Hint: retrieve stops at the chunks — generation is now your job, which is exactly the point when the one-call version is too much of a black box.
Show solution
retrieve returns chunks only, so you control the prompt, reranking, and model. Choose it when the managed generation is too rigid.
import boto3
rt = boto3.client("bedrock-agent-runtime", region_name="us-east-1")
r = rt.retrieve(
knowledgeBaseId="KB123456",
retrievalConfiguration={"vectorSearchConfiguration": {"numberOfResults": 5}},
retrievalQuery={"text": "refund window"},
)
chunks = [x["content"]["text"] for x in r["retrievalResults"]]
prompt = "Context:\n" + "\n---\n".join(chunks) + "\n\nQuestion: ..."
# then send `prompt` to converse() with your own system prompt/model
print(len(chunks), "chunks retrieved")
Context: Standing up a Knowledge Base wires three things in order — an S3 data source, an embedding model, and a vector store — and then ingestion runs asynchronously. Knowing it's async is what stops you from querying an empty index.
Your task: Sketch create_knowledge_base with a Titan embedding model plus an OpenSearch Serverless store, kick off an ingestion job, and note its async status lifecycle.
Requirements:
- Use the
bedrock-agentcontrol-plane client and supply aroleArn - Configure a
VECTORKB with an embedding model ARN (e.g.amazon.titan-embed-text-v2:0) - Point
storageConfigurationat an OpenSearch Serverless collection with a vector index and field mapping - Start ingestion with
start_ingestion_joband read itsstatus - Recognize the async lifecycle:
STARTING→IN_PROGRESS→COMPLETE
💡 Hint: Creation returns immediately with a KB id, but the documents aren't queryable until the separate ingestion job finishes — poll its status, don't assume.
Show solution
A KB needs three things wired in order: an S3 data source, an embedding model ARN, and a vector store. Ingestion is async.
import boto3
agent = boto3.client("bedrock-agent", region_name="us-east-1")
kb = agent.create_knowledge_base(
name="support-kb",
roleArn="arn:aws:iam::123:role/kb-role",
knowledgeBaseConfiguration={"type": "VECTOR",
"vectorKnowledgeBaseConfiguration": {"embeddingModelArn":
"arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v2:0"}},
storageConfiguration={"type": "OPENSEARCH_SERVERLESS",
"opensearchServerlessConfiguration": {
"collectionArn": "arn:aws:aoss:...",
"vectorIndexName": "kb-index",
"fieldMapping": {"vectorField":"v","textField":"t","metadataField":"m"}}},
)
kb_id = kb["knowledgeBase"]["knowledgeBaseId"]
job = agent.start_ingestion_job(knowledgeBaseId=kb_id, dataSourceId="DS123")
print(job["ingestionJob"]["status"]) # STARTING -> IN_PROGRESS -> COMPLETE
Context: Managed KB versus hand-built RAG is a control-vs-ops trade, not a quality one. A KB owns chunk size, retrieval count, and reranking for you; you give up those knobs in exchange for far less operational work. Encoding the rule makes the decision defensible.
Your task: Encode rag_choice(need_chunk_control, need_custom_rerank, want_low_ops) returning 'managed-kb' or 'hand-built', with a justification.
Requirements:
- Needing chunk control or custom reranking forces
'hand-built'— those are knobs the KB hides - Wanting low ops (and no special control) favours
'managed-kb' - The default leans
'managed-kb'when nothing demands the extra control - Run offline; verify the branches with example inputs
- Capture the trade-off in a comment: managed KB owns chunk/embed/store, you own less
💡 Hint: Check the control-requiring conditions first — if any knob the KB hides is required, hand-built wins before low-ops preference even enters the picture.
Show solution
A managed KB owns chunk size, retrieval count, and reranking; you trade that control for far less operational work.
def rag_choice(need_chunk_control, need_custom_rerank, want_low_ops):
if need_chunk_control or need_custom_rerank:
return "hand-built" # you need the knobs the KB hides
if want_low_ops:
return "managed-kb" # AWS owns chunk/embed/store
return "managed-kb"
print(rag_choice(False, False, True)) # managed-kb
print(rag_choice(True, False, True)) # hand-built
Context: For a compliance use case, an answer without a source is worse than no answer. Grounding is only enforceable if you actually inspect the citations and refuse when none came back — the model's confidence is not evidence.
Your task: Wrap retrieve_and_generate so any response with no citations is rejected with a safe refusal message instead of being returned.
Requirements:
- Pull all
retrievedReferencesout of the response'scitations - If there are zero references, return a safe refusal and mark it
refused: True— do not surface the model's text - If references exist, return the answer plus the list of source
locations andrefused: False - The guard logic is pure and testable offline against fake grounded and ungrounded responses
- Enforce the rule in code — asking the prompt to 'please cite' is not a guarantee
💡 Hint: The presence of a citation, not the wording of the answer, is your signal — flatten citations to their references and treat an empty list as an automatic refusal.
Show solution
Grounding is only enforceable if you check the citations; no source means no answer.
def enforce_citations(resp):
cites = resp.get("citations", [])
refs = [r for c in cites for r in c.get("retrievedReferences", [])]
if not refs:
return {"answer": "I can't answer without a cited source.",
"sources": [], "refused": True}
return {"answer": resp["output"]["text"],
"sources": [r["location"] for r in refs], "refused": False}
# offline test with a fake grounded response and an ungrounded one:
grounded = {"output": {"text": "30 days."},
"citations": [{"retrievedReferences": [{"location": "s3://kb/faq.md"}]}]}
print(enforce_citations(grounded)["refused"]) # False
print(enforce_citations({"output":{"text":"guess"}})["refused"]) # True
✓ Checkpoint — you can move on when you can…
- Explain what a Knowledge Base manages that Ch 3 made you build.
- List the three things a vector KB needs (data source, embeddings model, vector store).
- Choose between
retrieveandretrieve_and_generatefor a given task. - Explain why the provisioning order pushed us toward IaC (W7).
Knowledge check check yourself
A Bedrock Knowledge Base needs three things to exist as a vector store. What are they, and which client (control plane vs data plane) creates versus queries it?
Show answer
When would you use Retrieve versus RetrieveAndGenerate against a KB?