How to Build a Self-Correcting Multi-Agent AI Product with LangGraph and AWS
An architecture based on specialized agents that automatically generate, evaluate, and improve responses, with persistence, security, observability, and execution in production on AWS.

Most generative Artificial Intelligence applications start simply: the user submits a request, a language model processes the prompt, and a response is returned.
User → Application → LLM → Response
This model works well for experimentation, simple assistants, and some internal use cases.
The problem arises when we want to turn this idea into a product.
A product needs to do more than generate responses. It needs to control quality, maintain history, handle failures, protect data, control costs, monitor executions, authenticate users, and log what happened during processing.
It is in this context that the architecture of this article comes into play.
The proposal is to build a Self-Correcting Multi-Agent AI Product, in which different agents take on specific responsibilities within a workflow coordinated by LangGraph.
The main flow will be:
Generate → Evaluate → Correct → Evaluate Again → Approve
Instead of relying entirely on the first response produced by an LLM, we introduce a quality process within the application itself.
The product we are building
Our product allows a user to submit a topic, a request, or content and receive as a result a response that has gone through an automatic process of generation, evaluation, and correction.
The first version will have three main agents:
Writer Agent → Reviewer Agent → Correction Agent
Each agent has a specific responsibility.
The Writer Agent produces the first version of the content.
The Reviewer Agent evaluates the result using quality criteria.
The Correction Agent uses the feedback received to improve the response.
After correction, the content returns to the Reviewer for a new evaluation.
This process continues until the response is approved or the maximum number of revisions is reached.
The product can also present to the user:
- final response;
- quality score;
- evaluated criteria;
- number of revisions;
- feedback from agents;
- execution time;
- model used;
- token consumption;
- estimated cost;
- execution history;
- approval status.
The experience shifts from simply being:
Prompt → Response
to being:
Request → Generation → Evaluation → Correction → Validation → Result
The user no longer interacts only with a model and begins to use a quality-controlled AI system.
High-level architecture
The proposed architecture for the product uses the following components:
| Layer | Technology |
|---|---|
| Frontend | React / Next.js |
| Backend | FastAPI |
| Orchestration | LangGraph |
| Inference | Amazon Bedrock |
| Authentication | Amazon Cognito |
| Persistence | PostgreSQL / Amazon RDS |
| Workflow State | LangGraph Checkpointer |
| Security | IAM, Secrets Manager, and Bedrock Guardrails |
| Observability | Amazon CloudWatch |
| Containerization | Docker |
| Registry | Amazon ECR |
| Deploy | AWS App Runner |
| Infrastructure | Terraform |
| CI/CD | GitHub Actions |
The main flow of the application will be:
User → React / Next.js → Amazon Cognito → FastAPI → LangGraph → Agents → LLM Provider → Amazon Bedrock
Surrounding this flow, we will have persistence, security, governance, observability, and continuous delivery mechanisms.
Why use multiple agents?
A traditional application based on LLM typically concentrates several responsibilities within the same prompt.
We can ask the model to:
- write content;
- be technical;
- be clear;
- validate its own response;
- check for examples;
- correct issues;
- maintain a certain length;
- confirm that all requirements have been met.
It is possible to work this way.
The problem is that we are concentrating generation, evaluation, and correction within the same step.
From an engineering perspective, we can separate these responsibilities.
Produce → Evaluate → Correct
This is the same principle of separation of responsibilities used in traditional software.
Each agent now has a specific and predictable function.
Writer Agent
The first agent is responsible for the initial generation of content.
It receives:
Topic + Context + Requirements → Initial Draft
For example:
"Explain hexagonal architecture to an intermediate developer and provide a practical example.
We can define some responsibilities for this agent:
- produce technical content;
- explain objectively;
- use examples;
- stay focused on the subject;
- respect size limits;
- follow the requested format;
- avoid unnecessary information.
The result produced by the agent is stored in the workflow state.
class WorkflowState(TypedDict):
topic: str
draft: str
feedback: str
decision: str
score: float
revision_count: int
The Writer Agent does not need to decide if its own response is good.
That responsibility belongs to the next agent.
Reviewer Agent
The Reviewer Agent acts as a Quality Gate.
Its goal is to evaluate the content produced by the Writer Agent.
A simple implementation could return:
{
"decision": "PASS"
}
or:
{
"decision": "REVISE"
}
For a professional product, we can go further.
The Reviewer can produce a structured evaluation:
{
"decision": "REVISE",
"score": 0.74,
"criteria": {
"clarity": 0.91,
"relevance": 0.89,
"completeness": 0.68,
"technical_quality": 0.72
},
"issues": [
"The example is not detailed enough.",
"The explanation of dependency inversion is incomplete."
],
"suggestions": [
"Add an example with Ports and Adapters.",
"Better explain the direction of dependencies."
]
}
Now the product has something much more interesting than a simple opinion from the model.
We have a structured evaluation, which can be stored, analyzed, and used by the workflow itself.
The Reviewer as a quality mechanism
We can establish objective rules for approval.
For example:
Overall Score ≥ 0.85
We can also create additional criteria:
- clarity ≥ 0.80;
- relevance ≥ 0.90;
- technical quality ≥ 0.80;
- completeness ≥ 0.80.
The application now combines two mechanisms:
LLM Evaluation + Deterministic Rules
Example:
approved = (
score >= 0.85
and clarity >= 0.80
and relevance >= 0.90
and technical_quality >= 0.80
)
This combination is important.
We are not simply asking the model if it considers the response good.
We are using Artificial Intelligence within a mechanism controlled by the application.
Correction Agent
When the Reviewer Agent identifies problems, we enter the correction process.
The Correction Agent receives:
Original Topic + Current Response + Identified Problems + Suggestions from the Reviewer
For example:
"Identified Problem: the explanation about ports and adapters was not clear enough.
"Suggestion: add an example showing a repository interface and a PostgreSQL implementation.
The agent uses this information to produce a new version.
Draft v1 → Reviewer → Feedback → Reviser → Draft v2
After that, the new version returns to the Reviewer Agent.
The self-correction loop
This is the central part of the architecture.
The workflow has a cycle.
Writer → Reviewer → Reviser → Reviewer
If the content is approved:
Writer → Reviewer → PASS → Final Result
If the content needs improvements:
Writer → Reviewer → REVISE → Reviser → Reviewer
If there are still problems:
Reviewer → Reviser → Reviewer → Reviser → Reviewer
This pattern can be summarized as:
Generate → Evaluate → Improve → Evaluate
Or, conceptually:
Actor → Critic → Refiner
This cycle transforms simple generation into a controlled improvement process.
Why LangGraph?
Traditional pipelines tend to be linear.
A → B → C → D
Our workflow is not linear.
The Reviewer Agent needs to dynamically decide what the next step will be.
When the response is approved:
Reviewer → PASS → Final Result
When there is a need for correction:
Reviewer → REVISE → Reviser → Reviewer
We have:
- shared state;
- conditional decisions;
- cycles;
- checkpoints;
- multiple agents;
- termination conditions;
- possibility of resumption.
LangGraph takes on the role of workflow orchestrator.
It is not responsible for generating content.
Its responsibility is to control the execution of agents and determine how the state evolves during the process.
Shared state among agents
All agents work on a shared state.
We can represent this state as follows:
class WorkflowState(TypedDict):
topic: str
draft: str
feedback: str
decision: str
score: float
revision_count: int
During an execution, we might have:
{
"topic": "Hexagonal Architecture",
"draft": "Hexagonal architecture is...",
"feedback": "Detail Ports and Adapters.",
"decision": "REVISE",
"score": 0.76,
"revision_count": 1
}
Each agent modifies only what belongs to its responsibility.
The flow now works on an explicit representation of the execution state.
Controlling the number of revisions
A self-correcting product needs to have limits.
Otherwise, we could generate an indefinite cycle:
Reviewer → Reviser → Reviewer → Reviser → Reviewer → ...
This represents:
More calls to the LLM → More tokens → More cost → Higher latency
Therefore, we can establish:
MAX_REVISIONS = 3
The logic can follow this rule:
Reviewer → PASS → Final Result
or:
Reviewer → REVISE → Check limit → Reviser
If the limit has been reached:
Reviewer → Maximum limit reached → End execution
There is an important difference.
Reaching the maximum limit of revisions does not mean that the response has been approved.
The API should make this explicit.
{
"status": "MAX_REVISIONS_REACHED",
"approved": false,
"score": 0.81,
"revision_count": 3
}
This transparency is important for a professional product.
Workflow persistence
During a proof of concept, we can store the state only in memory.
In production, this is not sufficient.
Imagine an ongoing execution:
Writer → Reviewer → Reviser
At this moment, the container is restarted.
Without persistence, the execution is lost.
We need to be able to recover the workflow.
LangGraph Checkpointer
The solution involves using checkpoints.
LangGraph → Checkpointer → PostgreSQL
In AWS, we can use:
LangGraph → Checkpointer → Amazon RDS PostgreSQL
After each step, the state can be persisted.
Writer → Checkpoint → Reviewer → Checkpoint → Reviser → Checkpoint
Each execution receives an identifier.
thread_id
Example:
a32887cb-36a1-4272
This allows us to reconstruct the state of an execution and continue processing after failures.
It also opens up possibilities for functionalities such as:
- pause and resume;
- human-in-the-loop;
- recovery after failures;
- replay;
- auditing;
- debugging;
- execution history.
Checkpoint is not a business database
There is an important separation.
The Checkpointer should store the operational state of the workflow.
The application database should store the product data.
These are different responsibilities.
We can create tables such as:
users
projects
executions
execution_steps
evaluations
model_usage
prompt_versions
An executions table could have:
executions
id
user_id
thread_id
topic
status
approved
final_answer
final


