Guardrails, batch, and Knowledge Bases

Three Bedrock surfaces sit just outside the model that an agent platform reaches for constantly: Guardrails for content policy, batch inference for offline fleets, and Knowledge Bases for managed retrieval. Chapters 13, 14, and 34 decoded what each is for; this chapter shows the config shapes and walks a scenario through each, so the decoded concept becomes a deployable resource. As before, follow-along is illustrative, this box has no AWS account, but the shapes are faithful.

Guardrails: the config, and the agent-boundary call

A guardrail is a versioned, reusable resource you create once and apply per call. Its config composes several policy types, and seeing the shape makes the control ladder's "filter" rung concrete:

# illustrative CreateGuardrail config
bedrock.create_guardrail(
    name="hive-intake",
    contentPolicyConfig={"filtersConfig": [
        {"type": "PROMPT_ATTACK", "inputStrength": "HIGH", "outputStrength": "NONE"},
        {"type": "HATE", "inputStrength": "MEDIUM", "outputStrength": "MEDIUM"},
    ]},
    topicPolicyConfig={"topicsConfig": [
        {"name": "investment-advice", "type": "DENY",
         "definition": "Recommendations to buy or sell specific securities.",
         "examples": ["Should I buy NVDA?"]},
    ]},
    sensitiveInformationPolicyConfig={"piiEntitiesConfig": [
        {"type": "EMAIL", "action": "ANONYMIZE"},
        {"type": "CREDIT_DEBIT_CARD_NUMBER", "action": "BLOCK"},
    ]},
    contextualGroundingPolicyConfig={"filtersConfig": [
        {"type": "GROUNDING", "threshold": 0.75},
        {"type": "RELEVANCE", "threshold": 0.5},
    ]},
    blockedInputMessaging="This request was blocked by policy.",
    blockedOutputsMessaging="This response was withheld by policy.",
)

Each policy is a different job: content filters catch harm categories (note PROMPT_ATTACK, the injection-shaped-input classifier); topic policies deny subjects by natural-language definition; the sensitive-info policy either ANONYMIZEs (masks and passes) or BLOCKs PII, and the ANONYMIZE action is the one an intake pipeline wants, the document flows on, the card number does not; contextual grounding scores a response against supplied source text to flag ungrounded claims.

The wire-level insight the steering chapter argued for: you can apply a guardrail two ways, and for agents the second matters more. Attached to a Converse call via guardrailConfig, it wraps that call's input and output automatically. But an agent's most dangerous text, an injected instruction in a fetched page, arrives in a tool result mid-loop, not at the call's edges, so you call ApplyGuardrail yourself at the tool boundary:

# illustrative: check a fetched page before it enters the agent's context
verdict = bedrock.apply_guardrail(
    guardrailIdentifier="gr-abc", guardrailVersion="DRAFT",
    source="INPUT",
    content=[{"text": {"text": fetched_page,
                       "qualifiers": ["guard_content"]}}])
if verdict["action"] == "GUARDRAIL_INTERVENED":
    fetched_page = "[withheld by content policy]"     # a tool-error result

This is Chapter 37's "check at the tool boundary" made into one API call, and a newer detect-only variant streamlines exactly this per-step use inside agent loops, so the check does not require attaching a guardrail to every model call. The guardrail is still a sieve, not a wall, run it as the cheap first layer, keep the structural defenses behind it.

Batch inference: the JSONL job

The batch tier is half price for offline work, and its wire format is a file, not an API loop. You write a JSONL file to S3 where each line is one request, keyed by a recordId:

{"recordId": "shard-0001", "modelInput": {"anthropic_version": "bedrock-2023-05-31", "max_tokens": 1024, "messages": [{"role": "user", "content": "Audit this file group for bugs..."}]}}
{"recordId": "shard-0002", "modelInput": {"anthropic_version": "bedrock-2023-05-31", "max_tokens": 1024, "messages": [{"role": "user", "content": "..."}]}}

The modelInput is exactly the raw InvokeModel (Messages) body from Chapter 57. You submit a job pointing at the input and output S3 locations:

# illustrative
bedrock.create_model_invocation_job(
    jobName="capstone-a-finders-2026-07-26",
    modelId="anthropic.claude-opus-4-8",
    roleArn=BATCH_ROLE,
    inputDataConfig={"s3InputDataConfig": {"s3Uri": "s3://hive/batch/in/"}},
    outputDataConfig={"s3OutputDataConfig": {"s3Uri": "s3://hive/batch/out/"}},
)

The output is a JSONL file whose lines carry each recordId and its modelOutput, in any order, so you key results by recordId, never by position, the same key-by-id, not position discipline the queue contract taught. This is precisely how Capstone A's finder passes run: the overnight, latency-tolerant half of the fleet routes to batch at half price, and the interactive verifier panel stays on-demand. One constraint to design around: batch has a minimum record count and its own turnaround (typically within a day, not an SLA), so it is for work that genuinely can wait, which the use-case gallery warned is a smaller set than teams assume.

Knowledge Bases: managed retrieval

Chapter 34 framed agentic retrieval as an iterating tool; Bedrock Knowledge Bases is the managed pipeline that tool calls into. You point it at documents in S3, it ingests, chunks, embeds, and stores (increasingly into S3 Vectors), and exposes two operations. Retrieve returns raw chunks for the agent to reason over:

# illustrative: the retrieval an agent's "search_docs" tool wraps
kb.retrieve(
    knowledgeBaseId="KB123",
    retrievalQuery={"text": "payments-db connection pool sizing"},
    retrievalConfiguration={"vectorSearchConfiguration": {
        "numberOfResults": 8,
        "overrideSearchType": "HYBRID"}})   # SEMANTIC | HYBRID

RetrieveAndGenerate does the whole chat-RAG loop in one call (retrieve, stuff, answer with citations), which is the wrong shape for an agent that can iterate but the right shape for a one-shot lookup, so an agent uses Retrieve as a tool and reserves RetrieveAndGenerate for a sub-question it wants answered in one hop. The response's citations link each generated claim to its source chunk, the provenance the verification mesh checks against.

The one config decision that dominates retrieval quality is the chunking strategy, set on the data source: FIXED_SIZE (simple, severs context at boundaries), HIERARCHICAL (parent-child, good for an agent that drills down), SEMANTIC (splits on meaning), or NONE (you pre-chunked). The agentic-retrieval lesson applies: because an agent can iterate, prefer coarser, context-preserving chunks (HIERARCHICAL) over the tiny fragments chat-RAG favors. Ingestion is a job you trigger (StartIngestionJob) when the source changes, and GraphRAG (an auto-built entity graph) is the variant for multi-hop questions the flat vector store answers poorly.

Don't be confused: a Knowledge Base vs your memory plane. Both do vector retrieval and blur in conversation, but they are the knowledge-vs-memory split as two AWS resources. A Knowledge Base holds authored, external documents the agent does not write, ingested and retrieved, a rented chat-RAG pipeline you use as one tool. The memory plane (DynamoDB plus S3 Vectors, your own consolidation) holds earned, internal facts the agent distills from experience and must police for staleness and provenance. Use a Knowledge Base for the docs; do not pour agent memory into it, because it has no notion of the supersede-and-decay policies earned memory needs.

👉 Next: AgentCore Runtime, deployed, where the container contract from Lab 12.2 becomes a real deployment: the SDK app, the control-plane APIs, the agentcore CLI, the IAM execution role, and the Part 1 agent running in a managed microVM.