Skip to main content
Back to all posts AI Engineering
• 8 min read • Hafiz Mateen Afzal • ...

Building a Multi-Agent Customer Support System with Amazon Bedrock AgentCore and Strands Agents

How I replaced a manual support workflow with an orchestrator, four specialist agents, parallel RAG, guardrails, memory and full tracing, and what I learned along the way.

Quick Links: Project Code on GitHub • LinkedIn • GitHub

The problem

Imagine an e-commerce company that gets thousands of support requests every day: Where is my order? Can I return this? What is the warranty on electronics? Today a human agent looks up the order, opens the policy documents, checks the customer's account, and types a reply. It is slow, inconsistent and easy to get wrong.

That was the scenario for my project in the Future AWS Agent Engineer program, a Udacity Nanodegree sponsored by AWS. The fictional company, NovaMart, needed an AI layer that could understand a request, gather facts from several data sources, make decisions using company policy, and write a professional reply, all without human intervention, and with full visibility into every decision the AI makes.

The result was a production-style system built with Amazon Bedrock AgentCore and the Strands Agents SDK. It scored 120/120 on the automated test suite and passed project review. This article walks through how it works and what I learned.


The big idea: a team of agents, not one big chatbot

The obvious first design is one large agent with every tool attached. It works for demos and falls apart in practice: the prompt grows huge, the agent picks the wrong tool, and you cannot tell why it did what it did.

Instead, I used an Orchestrator → Workers pattern. One agent decides who should handle a request. Specialists each do one job well.

Architecture overview

Agent Model Temperature Job
Orchestrator Claude Haiku 4.5 0.0 Routes requests and manages shared state. It never answers the customer itself.
Inventory Claude Sonnet 4.5 0.1 Looks up orders and customer tiers in DynamoDB. Reports facts, never decisions.
Policy Claude Sonnet 4.5 0.2 Answers policy questions using parallel RAG over three Knowledge Bases.
Refund Claude Sonnet 4.5 0.1 Decides return eligibility and starts the return.
Communication Claude Sonnet 4.5 0.3 Writes the final warm, professional customer reply.

The model choices are deliberate. Routing is a simple classification task, so the fast, cheap Haiku model at temperature 0 gives deterministic decisions. The workers do actual reasoning and writing, so they get Sonnet. Temperatures follow the job: near zero where accuracy matters (data lookups, refund decisions), a little higher where natural language matters (the final reply).


Routing rules the Orchestrator enforces

The Orchestrator's system prompt encodes six rules:

  1. Every request starts with initialize_session.
  2. Order status, returns and refunds go to the Inventory agent first, then the Refund agent for returns.
  3. Policy questions (return windows, shipping rates, warranty terms) go to the Policy agent.
  4. Account questions like "what is my tier?" go to Inventory, never to Policy, because the policy agent only knows policy text, not customer data.
  5. Math questions skip Inventory, Policy and Refund and go straight to the Communication agent.
  6. Every request ends with the Communication agent composing the final reply.

Rule 6 is the most important. The Orchestrator is never allowed to write the customer-facing answer itself. That keeps the tone consistent and makes the last step of every trace identical, which is also useful for debugging.

Three example requests show the paths:

Customer says Route
"I want to return my order ORD-27176" Orchestrator → Inventory → Refund → Communication
"What is the return policy for premium customers?" Orchestrator → Policy (3 parallel retrievers) → Communication
"How much are 5 items at $29.99 with 10% off?" Orchestrator → Communication

Shared state without stepping on each other

Agents need to hand findings to each other. Rather than passing giant blobs through prompts, every worker writes its output to a WorkflowState record in DynamoDB, keyed by session ID. The Refund agent reads what the Inventory agent found; the Communication agent reads everything.

Because several agents can touch the same record, updates use optimistic locking: each write includes the version it expects to find, and fails if someone else changed the record in the meantime. This prevents one agent from silently overwriting another's results.

Every routing tool follows the same pattern:

def _run_worker(column, session_id, worker, prompt):
    state = _read_workflow_state(session_id)
    old_version = int(state['version'])

    result_text = str(worker(prompt))
    _update_workflow_state(session_id, {column: result_text}, old_version)
    return result_text

Read the current state, run the worker, write the result back with the expected version. Keeping this in one shared helper meant five routing tools stayed small and consistent.

One small design decision paid off: the order lookup tool also returns a days_since_order field. Language models do not reliably know today's date, so computing the number of days in the data layer lets the Refund agent apply the 30-day (Standard) or 60-day (Premium) return window without guessing.


Parallel RAG: three Knowledge Bases, searched at once

NovaMart's policies live in three domains: returns, shipping and warranty. Each has its own Amazon Bedrock Knowledge Base, backed by S3 Vectors with Titan Text Embeddings V2.

The Policy agent does not search them one after another. It is a coordinator that owns three small retriever sub-agents, one per Knowledge Base, and a single tool, search_all_policies, that fires all three in parallel using a thread pool:

with ThreadPoolExecutor(max_workers=3) as executor:
    futures = {
        executor.submit(_run_retriever, domain, agent, query): domain
        for domain, agent in retrievers.items()
    }
    for future in as_completed(futures):
        ...

The coordinator then synthesizes the three result sets into one grounded answer. In my test run, the full retrieval step, including the three model calls, finished in about 8 to 9 seconds. The retrievers run at temperature 0.0 so retrieval is deterministic; the coordinator runs at 0.2 for readable synthesis.


Guardrails: safe without being annoying

A customer-facing agent needs boundaries. I used Amazon Bedrock Guardrails with four layers:

  • Content filters: sexual, violence and hate at HIGH strength; insults and misconduct at MEDIUM.
  • PII handling: block credit card numbers and US Social Security numbers; anonymize emails and phone numbers.
  • Denied topics: competitor products, pricing negotiations and legal threats.
  • Profanity: the managed word list.

The interesting part was the topic policy. A naive definition of "pricing negotiation" also blocks harmless questions like "How much are 5 items at $29.99 with 10% off?" because the words look similar to haggling. I wrote the definition so that haggling or requests to change an advertised price are blocked, while plain arithmetic on a stated price and discount is explicitly allowed. The test suite and my live scenarios confirmed that the math question gets answered while negotiation, competitor and legal-threat examples are blocked.

Two other details mattered. I used the guardrail's newer Standard tier with a cross-region profile for the topic policy, and I published a numbered version instead of attaching the DRAFT. A numbered version cannot change underneath a running deployment, which is exactly what you want in production.


Memory and observability

Memory. I configured AgentCore Memory with a session-summary strategy and seven-day event retention, so the system can carry context across turns of a conversation.

Observability. An agent system you cannot see into is a liability. The deployed runtime sends reasoning chains, tool calls and responses to CloudWatch Logs at INFO level, and AWS X-Ray traces every request at 100% sampling. The X-Ray trace map shows the real call chain: the client reaches the NovaMart Orchestrator, which fans out to the Inventory, Refund, Policy and Communication agents and the three Knowledge Base nodes.

X-Ray trace map of the multi-agent system

Seeing the graph made the architecture feel real. It also confirmed that the routing rules were doing what I intended: the math question never touched Inventory or Policy.


Deploying to AgentCore Runtime

The agents run on AgentCore Runtime, deployed with the AgentCore CLI. A single command builds the agent graph, creates the guardrail, packages and deploys the runtime, configures memory and turns on observability. The runtime uses public networking and HTTP, and receives the Knowledge Base IDs and guardrail ID as environment variables.

The supporting infrastructure (DynamoDB tables, the S3 buckets, IAM role, log group) comes from a CloudFormation stack, so the whole environment can be created and torn down repeatably. That mattered a lot, because I was working in a temporary AWS sandbox with expiring credentials.


Testing: the suite and the live scenarios

The project includes an automated test suite covering every layer: agent construction and tools, model selection, the guardrail's policies, the runtime's configuration, memory, the three Knowledge Bases (including a real parallel retrieval query) and observability. The final result was 120/120.

Beyond the suite, I ran the three live scenarios through the system and checked the traces: the return request flowed Inventory → Refund → Communication, the policy question triggered three parallel retrievals, and the math question went straight to the Communication agent.


Things that tripped me up

Every project has friction. These were mine:

  • Windows tooling. After installing the AgentCore CLI with npm, the command was not recognized until I added npm's global folder to the PATH.
  • Temporary credentials. The sandbox keys expire, so I set them as environment variables in a single terminal and ran every command from there. When a session ended, I re-pasted fresh keys and continued from the step that failed.
  • Noisy parallel output. When three sub-agents stream at once, the terminal shows interleaved text. The final per-agent results print cleanly afterwards, and the tests confirm the content, but it is a good reminder that concurrency changes how output should be displayed.
  • Guardrail false positives. The pricing topic definition took thought, as described above.
  • Cleanup. At the end, the lab's own permissions blocked some of the Bedrock cleanup calls. Cleaning up your cloud resources promptly is a habit worth building.

What I learned

  1. Clear boundaries beat clever prompts. Each agent has a narrow job, a short prompt and a few tools with good docstrings. The agents choose tools by reading those docstrings, so writing them well is real engineering work.
  2. Put facts in the data layer. Computing days_since_order in the lookup tool removed a whole class of date-related mistakes.
  3. Parallelism is easy to add when agents are independent. Three retrievers with separate Knowledge Bases scale out naturally.
  4. Guardrails need testing from both sides. Check that bad inputs are blocked and that legitimate ones still pass.
  5. Traces change how you debug. In a multi-agent system, a trace map tells you more in ten seconds than reading logs for ten minutes.

What I would build next

The project brief suggests a few extensions I would like to try:

  • Run adversarial tests against the guardrail (prompt injection, competitor and legal-threat prompts) and document how each is handled.
  • Add persistent chat sessions with a DynamoDB session store.
  • Build a CloudWatch dashboard for invocation counts, latency per agent and guardrail trigger frequency.
  • Add Amazon Cognito authentication and a React front end so the terminal-only system becomes a user-facing application.

Wrapping up

Building NovaMart's support system taught me that a good agent system is mostly good software design: narrow responsibilities, shared state with clear rules, safety at the edges and visibility into everything. The models are powerful, but the architecture around them decides whether the system is reliable.

The full code, README and screenshots are on GitHub: NovaMart-Multi-Agent-E-commerce-RAG.

Thank you to Udacity and AWS for the Future AWS Agent Engineer program. Project brief, starter code, infrastructure template and tests were provided by Udacity; the agent implementation is mine. If you are learning agents or RAG on AWS, I would love to hear what you are building.

Tags: AWS Amazon Bedrock AgentCore Strands Agents RAG Multi-Agent Systems Generative AI

Tags: #AWS #Amazon Bedrock #AgentCore #Strands Agents #Multi-Agent Systems #RAG