Lesson 23: Reviewing and Editing Through the Evaluator-Optimizer Pattern
- Explaining the Evaluator-Optimizer Pattern
- Scoping the New Writing Workflow Architecture
- Why We Don’t Use a Quality Score
- The Evaluator-Optimizer vs. Reflection Pattern
- Modeling Our Review Entities
- The Review Entity
- The ArticleReviews Entity
- The SelectedText Entity
- The SelectedTextReviews Entity
- Implementing the Article Reviewer (The Evaluator)
- The System Prompt
- The Selected Text Prompt
- Example: Reviewing a Whole Article
- Example: Reviewing Selected Text
- Hooking the Reviews to the Article Writer (The Optimizer)
- Updating the Article Writer
- The Editing Prompts
- End-to-End Example
- Centralizing Our App Configuration
- Configuration Classes
- Example YAML
- Loading and Using Configuration
- Glueing Everything Into Our LangGraph Workflow
- The Workflow Definition
- Adding Short-Term Memory
- Running the New Writing Workflow
- Conclusion
- Practicing Ideas
- References
In previous lessons, we laid the foundation for our Brown writing workflow. We learned how to structure the project in Lesson 20, designed the system architecture in Lesson 21, and implemented the core drafting logic in Lesson 22 using the orchestrator-worker pattern and context engineering.
However, generating a draft is just the beginning. Any professional writer knows that the real value is created during the editing process. A first draft rarely meets all quality standards, tone requirements, and structural guidelines perfectly.
In this lesson, we will implement the Evaluator-Optimizer pattern to add a self-correcting quality assurance layer to our writing workflow. We will develop an automated review system that evaluates the generated article against our writing profiles and returns the reviews to the writer to improve the content iteratively. Finally, we will orchestrate this entire process using LangGraph’s Functional API and add short-term memory to persist our workflow state.
In this lesson, you will learn:
- The architecture of the Evaluator-Optimizer pattern and how it differs from Reflection.
- How to model review entities to capture structured feedback.
- Implementing an Article Reviewer (Evaluator) that checks content against multiple profiles.
- Upgrading the Article Writer (Optimizer) to edit content based on feedback.
- Centralizing application configuration using Pydantic and YAML.
- Orchestrating the review-edit loop with LangGraph’s Functional API and checkpointing.
Explaining the Evaluator-Optimizer Pattern
The Evaluator-Optimizer pattern is a workflow design that mimics real-world quality assurance. It separates content generation from evaluation, creating a feedback loop that iteratively refines the output until it meets specific criteria [1].
The architecture consists of two distinct components:
- Evaluator : Analyzes the output against a set of requirements and identifies specific issues.
- Optimizer : Takes the evaluator’s feedback and generates an improved version of the output.
This cycle continues until a stopping condition is met, such as reaching a maximum number of iterations or satisfying a quality threshold [2].
Image 1: A flowchart illustrating the Evaluator-Optimizer pattern applied to generating articles.
This pattern is widely used in engineering tasks where quality is non-negotiable:
- Code generation: An LLM writes code, and a separate component (static analysis or another LLM) reviews it for bugs, security flaws, or style violations.
- Video script writing: A script is generated and then reviewed for pacing and brand alignment.
- Course material creation: Lessons are drafted and then critiqued for pedagogical effectiveness.
- Financial reporting: ensuring outputs adhere to strict data formats and compliance rules.
In our context, this mirrors the professional writing process:
- The Writer creates an initial draft.
- The Reviewer provides objective feedback based on guidelines.
- The Editor (often the writer wearing a different hat) refines the draft based on that feedback.
- Steps 2 and 3 repeat until the piece is polished.
Now, let’s see how this applies to our Brown architecture.
Scoping the New Writing Workflow Architecture
In Lesson 22, we implemented a linear three-step workflow:
- Load Context : Gather guidelines, research, profiles, and examples.
- Generate Media : Create diagrams using the orchestrator-worker pattern.
- Write Article : Generate the first draft.
Now, we are extending this into a five-step workflow by adding a review-edit loop:
- Article Reviewer (Evaluator) : The
ArticleReviewernode checks the draft against the article guideline and all writing profiles (tone, style, structure). - Article Editor (Optimizer) : The
ArticleWriternode ingests the reviews and rewrites the article to fix the identified issues.
Image 2: The complete writing workflow, adding the review and edit steps to the previous pipeline.
Then, at step 6, we check if we reached the expected number of iterations. If not, we repeat steps 4 and 5 on top of the reviewed article until we reach our configured number of iterations. From our tests, we usually used two review-editing loops. Ultimately, we return the final article to the user.
💡To see how the design presented above relates to Brown's overall architecture, open the whiteboard below.
Live Whiteboard: The End-To-End Architecture of Brown: The Writing Workflow. Click here to expand.
Returning to our evaluator-optimizer design, we must address an important question: “Why rely solely on the maximum number of expected iterations rather than on an evaluation score as well?”
Why We Don’t Use a Quality Score
The standard definition of the evaluator-optimizer pattern suggests using a numerical score to decide when to stop the loop. We deliberately avoid this for two reasons.
First, writing quality is subjective and multi-dimensional. Reducing adherence to our complex style and structure profiles to a single scalar number is noisy and unreliable. An LLM might hallucinate a high score while missing critical structural requirements, or assign a low score to minor stylistic choices, leading to a noisy, unstable evaluation loop. In concrete numbers, we have around 100 rules within our profiles. Assigning a binary score of 0 or 1 to each rule and averaging them can significantly reduce the signal.
One option to solve this problem is to reduce the evaluation to a set of key characteristics we want the article to consistently follow, and compute the score only on those. Unfortunately, at this step, it won’t work for us, because we want the Reviewer to review all the profile instructions. Still, this strategy is effective for AI Evaluations, where we want to automate evaluating the entire Brown application and ensure it adheres to key features.
Second, we prioritize Human-in-the-Loop (HITL). Instead of relying on an arbitrary threshold, we conduct a fixed number of automated review cycles (e.g., two passes) to identify and correct obvious errors. Then, as we will see in Lesson 24, we present the result to a human user who can provide the final “pass” or request specific edits. This approach simplifies implementation, improves reliability, reduces token usage and latency, and ensures the final output meets human standards.
The Evaluator-Optimizer vs. Reflection Pattern
Before diving into the implementation, we want to clarify the difference between the Evaluator-Optimizer and Reflection patterns. We believe this is important because we’ve observed that many people often confuse the two.
The confusing part is that both aim to improve LLM outputs through iteration, but they do so in different ways.
The Evaluator-Optimizer Pattern uses separate LLM calls (or even distinct agents) for generation and evaluation. This separation of concerns enables us to utilize distinct prompts, contexts, or even different models for each task. For example, you might use a highly creative model for writing and a stricter, reasoning-heavy model for reviewing [2].
Image 3: The Evaluator-Optimizer vs the Reflection pattern
The Reflection Pattern , in contrast, happens within a single LLM entity that shares the same context between the generation and evaluation. In other words, the same LLM reflects on top of its own answers and tries to improve them before providing the final answer. The model is prompted to “critique its own thinking” before generating the final answer. This time, the reflection loop happens within an agent’s planning mechanism or within the LLM’s reasoning phase. While efficient, it lacks the transparency, modularity, and control afforded by a distinct evaluator node [3], [4].
Typically, the Evaluator-Optimizer pattern is employed within workflows, whereas the Reflection pattern is purely agentic. However, as the line between workflows and agents blurs, you can apply the Evaluator-Optimizer pattern to an agentic design. For example, in a swarm of agents, you would have specialized evaluator and optimizer agents, and the decision to pass information between them is handled by an orchestrator agent, rather than being hardcoded in the code as we do in a workflow. Ultimately, the most significant difference lies in whether the evaluation is conducted by the same entity (Reflection) or by a different, specialized one (Evaluator-Optimizer).
We chose the Evaluator-Optimizer pattern for Brown because:
- Transparency : We want to inspect the
Reviewsas distinct artifacts. This helps us debug why the agent decided to edit the text. - Configuration : We can use a high-temperature model for the creative writing phase and a low-temperature, deterministic model for the strict reviewing phase.
- Process : It aligns with the real-world separation between a writer and an editor.
Now that we have completed the theory and design behind the editing and reviewing loop, let’s begin implementing it.
Modeling Our Review Entities
To implement this pattern, we first need to define structured data models for our feedback. In Lesson 22, we defined most of the entities from the Brown project, such as the Article and ArticleGuideline. Now, for the reviewing logic, we have to introduce a few more.
Within Brown, we support two types of reviews to optimize for cost and speed: Whole Article Reviews and Selected Text Reviews , because often, you only need to fix a specific section, not rewrite the entire piece.
Let’s see how we can model that behavior. In the class diagram below, you can observe that we have attached an ArticleReviews class to the Article, which models a list of Reviews for a given article. Additionally, for reviewing only a single piece of text, we created the SelectedText class, which includes the SelectedTextReviews model that attaches a list of Reviews to the selected text.
Now, let’s implement each to see how this works in practice.
Image 4: Entity Relationship Diagram showing relationships between Article, ArticleReviews, Review, SelectedText, and SelectedTextReviews.
The Review Entity
A Review captures a single issue found in the text. It links the problem to a specific profile and location and provides a comment. As the Review entity is passed directly to the LLM during evaluation, it’s essential to attach a Field object to each attribute that clearly describes what each field is and how it relates to the evaluation process.
The ArticleReviews Entity
This entity aggregates all reviews for a comprehensive article review. It contains the article itself and the list of reviews.
The SelectedText Entity
To review only a part of the article, we need to define what that part is. The SelectedText entity holds the content and its position (line numbers) within the parent article. We use the first_line_number and last_line_number to help the LLM locate the selected text within the article, providing the full context for review and editing. Without this, reviewing or editing the selected text out of context can introduce issues. For example, let’s say that the RAG acronym was already introduced as “Retrieval-Augmented Generation (RAG)” at the top of the article, and it’s used as “RAG” within the selected text. If the evaluator cannot see that, it will mark it as an error.
The SelectedTextReviews Entity
Finally, this entity groups reviews specific to a text selection. It also leverages the Review entity, linking it to a particular SelectedText.
Now that we have all the entities in place, let’s move on to implementing the evaluator-optimizer pattern from scratch, starting with the evaluator.
Implementing the Article Reviewer (The Evaluator)
We use the same Node abstraction from Lesson 22 to implement the ArticleReviewer node, which generates feedback as review entities that follow the structure presented above.
The ArticleReviewer node works as follows.
1. The Reviewer takes the content to review (polymorphic: either Article or SelectedText, as they share the same behaviour) and the requirements (article guidelines and profiles). We also implement properties that make it easy to decide whether to work with an article or selected text.
Notice that the toolkit is empty. Reviewing is a pure generation task as the LLM doesn’t need external tools to critique the text. The magic happens when the proper context and prompt are provided.
2. We use an intermediate Pydantic model, ReviewsOutput, to enforce structured output from the LLM.
This decoupling keeps the LLM’s output schema simple (list[Review]) while allowing the node to return rich domain entities (ArticleReviews) that include the original article context.
3. Next, we implement the ainvoke method. This method orchestrates the assembly of the prompt and the model call. First, we construct the system prompt by formatting all profiles and guidelines into context. We also handle the logic for adding specialized instructions when reviewing a selected text segment.
Note how we call to_context() on every entity. This encapsulates the logic for formatting each profile into XML, keeping the node logic clean and scoped.
4. Next, we invoke the model to generate the reviews and package the output into the appropriate domain entity.
The System Prompt
As always, the system prompt is the brain of our node. Let’s analyze its anatomy, as we did for the article writer, following best practices for prompt structure [5]:
- Task Context : Defines the persona and the high-level overview of the task.
- Background Data : Injects the article, guidelines, and all profiles (Character, Tonality, Structure, etc.).
- Detailed Task Description : Defines the rules.
Reviewing Rulesestablishes a strict hierarchy: Special Rules (from the writing profiles) > Article Guideline (the user input) > Writing Profile > Others. This prevents the model from prioritizing minor stylistic points over core requirements.
- Chain of Thought : Wrap up the system prompt with a chain of thoughts that explicitly states the order of the instructions and reminds the LLM at the end of the context about its core task to ensure it focuses on the right thing:
We could further improve this prompt by adding a few-shot examples , such as calling the Reviewer on a sample article, manually correcting the generated reviews, and feeding those back into the prompt as examples. This strategy could significantly increase the quality and consistency of the feedback.
The Selected Text Prompt
As we want to model both reviewing the whole article or a selected piece of text, we decoupled the two logic branches into two system prompts. The selected text prompt doesn’t repeat the entire logic; it just provides extra context on where the selected text is coming from and short-circuits the Chain of Thoughts with a new set of instructions on the reviewing steps for the selected text. Otherwise, all context, including the article, article guidelines, and writing profiles, is used in both scenarios.
By placing the Chain of Thoughts at the end of the prompt chain, we effectively redirect the model’s immediate focus while keeping the global context available.
And the article generated in the previous lesson.
Then, run the Reviewer using Gemini 2.5 Flash.
The output shows specific, actionable feedback as follows:
You can see that the profile points to one of our writing profiles, the location is the name of a section from the article, and within the comment, we have the actual review feedback.
Now, let’s review only a selected piece of text.
Example: Reviewing Selected Text
We can also review just a specific portion of the article. This is useful when a user wants to check if a particular section meets the requirements without reprocessing the entire document.
- First, we extract the text we want to review and wrap it in a
SelectedTextentity.
It outputs:
- Then, we run the
ArticleReviewerexactly as before, but passing theSelectedTextobject. The node’s polymorphic design handles the rest.
The output will now contain reviews specific only to that slice of text, anchored by the line numbers.
Now, let’s hook these reviews to the article writer, the Optimizer.
Hooking the Reviews to the Article Writer (The Optimizer)
To implement the Optimizer, we don’t need a new node. We upgrade the existing ArticleWriter to serve a dual purpose: Writer (from scratch) and Editor (from reviews). This keeps all writing logic in one place.
Updating the Article Writer
- We update
__init__to accept optional reviews.
If reviews is None, it writes a new draft. If provided, it switches to editing mode.
- We update
ainvoketo construct a conversation history that simulates the review process.
By simulating the conversation history through the if ``[self.reviews](<http://self.reviews/>) branch, we create the following context:
- Here are the rules (System - initially added when writing the article the first time)
- Here is what you wrote (Assistant - the article)
- Here is what was wrong (User - the optimizer system prompt)
- Now fix it.
Let’s see what the optimizer system prompts look like.
The Editing Prompts
As before, we have a system prompt tailored to each scenario: the entire article or a selected piece of text.
The system prompt for reviewing the article is grouped as follows:
- The detailed task description.
- Special section ranking the importance of each review. We will cover human feedback in the next lesson. For now, it’s important to highlight that we always prioritize human feedback over everything else:
- The actual reviews:
- The chain of thought from the end overrides the one from the main system prompt, transforming the
ArticleWriternode from generating an article from scratch to reviewing it:
For reviewing the selected piece of text, the prompt is pretty similar to the one for editing the whole article, except for some minor details about where the selected text comes from.
The final chain of thought has to carefully instruct how to manipulate only the selected text in the context of the full article.
Now that we have all the moving pieces of the evaluator-optimizer pattern in place, such as the Article Reviewer and Editor, let’s glue them together into an end-to-end example.
E nd-to-End Example
We can now run the full loop manually to verify the logic. This example demonstrates the complete lifecycle: loading context, generating media, writing a draft, reviewing it, and editing it.
- Load Context : We initialize our loaders and fetch all necessary data.
- Generate Media : We run the media generation step (simulated here for brevity).
- Write Draft : We invoke the writer to create the first version.
It outputs:
- Review : The Reviewer analyzes the draft and produces feedback.
It outputs:
- Edit : The writer acts as an editor, applying the reviews to the draft.
- Compare : We can check the difference between the draft and the final version.
It outputs:
With the current infrastructure, it’s impossible to show how all the reviews are addressed. Let’s focus on the second one in the introduction. This is how Review 2 looked like:
It changes the article by improving the introduction:
Let’s also use a diff tool to show the difference between the first and second versions of the articles. It doesn’t really matter what tool. For example, we used this free one. Here is what was deleted from the first version:
And added to the second version:
As you can see, checking what actually changed after a review can be tedious. You have to go over the reviews, locate the changes using the location field, and use a diff tool to compare files. However, in future lessons, once we integrate Brown with Cursor/Claude through an MCP server, this process of checking changes and diffs will be fully automated.
The only step left in this lesson is to glue the workflow above with LangGraph. Before we do, let’s define our app configuration layer.
Centralizing Our App Configuration
As our system grows, hardcoding model names, hyperparameters, and paths becomes unmanageable. We need a centralized configuration system that defines all our parameters in a single configuration file and propagates it across the entire application.
To implement this, we use YAML files to store the configuration and load them with Pydantic to map the YAML into Python and define a type-safe schema.
Let’s implement this.
Configuration Classes
- Context : Defines paths to guidelines, profiles, and research that define our context.
- ToolConfig : Configures individual tools, such as the diagram generator.
- NodeConfig : Allows configuring the model and parameters for each node independently. Note how it’s leveraging the
SupportedModelsandModelConfigtypes to ensure we load only models that we support through ourbrown.modelsmodule.
- Memory : Controls the checkpointing strategy.
- AppConfig : The root configuration object that loads the config from a YAML file into a Pydantic
AppConfigobject.
Example YAML
Let’s look at an example YAML file, such as the one we used across the course, located at configs/course.yaml:
Note how we have complete control over how many review loops we do, how we configure each model, and even what infrastructure we use to load our context from. This allows us to easily experiment with different setups, such as a creative setting for the first draft and a strict, deterministic setting for reviewing and editing, optimizing the performance of each stage.
Loading and Using Configuration
We lazy-load the configuration once through a Least Recently Used (LRU) cache. Note how we control what app config to load through the settings object that loads the environment variables:
What’s the difference between the settings and the app config?
Usually, you want to load only sensitive information via env vars, since you never want to commit your credentials to git. That’s why everything that goes into your .env file is sensitive and never exposed to the public.
Meanwhile, the app config contains no sensitive information. On the contrary, you want to store and version it with Git. Also, loading the entire application config from env vars is painful because it doesn’t naturally support complex structures like dicts (it does, but it’s ugly).
That’s why you usually want two config types: one for credentials (our settings) and one for app parameters.
Also, in case you have values that rarely change, it’s totally fine to have a [constants.py](<http://constants.py/>) file that contains static objects, such as file names, character names, relative paths, etc., that aggregate in a single place all your constants. You could also add them directly into your app config as default values. Both options are valid. Ultimately, the most important design decision is to separate the settings and app config.
Now, let’s finally implement the LangGraph workflow that glues together all the steps from the end-to-end example above into a compact unit of work.
Glueing Everything Into Our LangGraph Workflow
Instead of using LangGraph Graph SDK, we use LangGraph’s Functional API to orchestrate the workflow. We chose the Functional API over the Graph SDK because it allows us to write standard Python loops and logic (if/else) while still getting state management and checkpointing [6], [7].
In the first version of Brown, we used the Graph SDK, which overcomplicated the code. Writing simple loops or conditional logic required defining complex edges and state reducers, turning what should have been minutes of coding into hours of debugging graph topology. The Functional API solves this by allowing us to use native Python control flow while leveraging LangChain’s rich ecosystem.
Let’s dig into it.
The Workflow Definition
We define the workflow using LangGraph’s Functional API @entrypoint, @task decorators, and a shared RetryPolicy. Let’s start with the @entrypoint that serves as, well…, the entrypoint to our workflow.
- Next, we define the****
_generate_article_workflowfunction that orchestrates the entire flow: load context, generate media, draft, iterative review/edit/render with progress updates, and final render to disk. It glues the end-to-end article generation workflow into a single function. We already went through this in this lesson, so we won’t split it into multiple groups.
The only element that’s new to this function is how we use the writer LangGraph object to emit custom WorkflowProgress events that inform the consumer of the workflow’s progress. Since this workflow can take ~3-5 minutes to run, sending progress messages to the consumer is a vital UX component. Nobody likes waiting for a blank screen. This way, the client can easily display progress messages to the end user or make other decisions, such as sending notifications or emails.
The WorkflowProgress pydantic object looks as follows (note that whenever we send it to the writer we have to JSON serialize it by calling .model_dump(mode="json") as the communication between the workflow and the consumer can be done through different processes.
- Now, let’s define each step at a time. Each step is decorated with the
@taskLangGraph decorator that takes theretry_policyas a parameter. This ensures that if an API call fails or a rate limit is hit, LangGraph automatically retries just that specific step without crashing the whole workflow.
The****generate_media_items step implements the orchestrator-worker pattern that generates all diagrams/snippets by fanning out tool calls from the media orchestrator, logging planned jobs, running them in parallel, and returning a typed MediaItems collection. The same logic we presented in the previous lesson.
- The
write_articlestep generates the first full draft using the writer node, combining guidelines, research, profiles, media items, and examples into anArticle.
- The****
generate_reviewstask is used within the evaluator optimizer. It runs the Reviewer on the current article to produceArticleReviewsaligned with guidelines and profiles.
- Then we have the
edit_based_on_reviewsstep that uses the writer in “edit” mode, guided by the generated reviews plus full context, to produce the updatedArticle.
- Ultimately, we can run the whole workflow as a standard LangGraph object calling
astream()for streaming intermediate steps from the@taskoutputs (by pickingstream_mode=["values"]and the writer (by pickingstream_mode=[”custom”]). This way, we can capture events as they become available, creating the streaming effect you see in chatbots, or, in this case, emit progress events or intermediate results.
This implementation follows clean code patterns by separating the app layer from the infrastructure layer. The LangGraph workflow represents our app layer, gluing together entities and nodes into usable business logic. The checkpointer acts as the infrastructure layer, providing short-term memory. Notice how the workflow doesn’t care which checkpointer we use (in-memory or SQLite), only that a checkpointer is injected at runtime. This keeps our domain logic independent of infrastructure choices. For example, we could use a different memory builder, such as build_sqlite_checkpointer and easily inject a different checkpointer without changing anything else.
Image 5: The Brown project structure.
With that in mind, let’s also see how we implemented the short-term memory layer.
Adding Short-Term Memory
We support both in-memory (for testing) and SQLite (for local persistence) checkpointers.
- In-Memory Checkpointer : Best for development and testing, as the state is lost when the process ends.
- SQLite Checkpointer : Best for local persistence, allowing you to resume between runs if the workflow fails. Even if SQLite is still a file on disk, it can easily support dozens of users during initial testing of your app (aka in PoC mode). Thus, it’s extremely powerful when first deploying your application, as it avoids setting up a database such as Postgres, which already complicates the infrastructure. It’s also super easy to use when testing and debugging locally.
So what’s the purpose of the checkpointer? It provides:
- Debugging : It saves the output from each step. Thus, we can easily run the workflow multiple times from a given step, while freezing the others. This is extremely powerful due to the non-deterministic nature of AI applications. That’s why it’s usually recommended to have one step per LLM call to scope down one non-deterministic component per step.
- Resilience : Because the output of each step is saved, we can also resume the workflow in case of failures. For example, if the script crashes during the 2nd review loop, we can resume exactly where it left off.
- Human-in-the-Loop : We can pause execution, wait for user input, and resume.
Running the New Writing Workflow
Now that we understand the LangGraph workflow, let’s finally run it. We will use the same code presented a few paragraphs above. Note how we generate a unique thread_id using UUIDs to track this specific run in the checkpointer and stream the events.
The events will look something like this:
In the output directory, we see the evolution of the article:
article_000.md: The initial draft.article_001.md: The result after the first review-edit cycle.article_002.md: The final polished version.[article.md](<http://article.md/>): The final article. In this use case, it is the same asarticle_002.md(because we run the review-edit loop only twice)
To see the article’s evolution across iterations, open the sample we ran for you, which includes all the articles, and compare them using this free tool. You can also open the notebook.ipynb, run it, and generate all the article versions yourself.
Conclusion
In this lesson, we transformed our linear writing workflow into a reliable, self-correcting system by implementing the Evaluator-Optimizer pattern. We modeled structured reviews, implemented a specialized Reviewer node, and upgraded our writer to handle editing tasks. By orchestrating this with LangGraph and adding persistence, we built a resilient engine that iteratively refines its own work.
In the next lesson, we will take this a step further by introducing Human-in-the-Loop into our workflows. We will expose our reviewing and editing capabilities as tools, allowing a human user to intervene, provide specific feedback, and guide the agent to the perfect final draft.
Practicing Ideas
Here are some ideas on how you can further extend this code:
- Improve the Reviewer : Tweak the Reviewer prompts or profiles for better results. Get a sense of how it affects the final prompts. The biggest change you could make here is to add few-shot examples to the Reviewer.
- Modify the configuration : Change models, temperatures, and num_reviews in YAML and observe output changes.
- Add a scoring mechanism to the evaluator : Even though we advised against it, it will be a good exercise to modify the evaluator-optimizer implementation and see how it behaves.
- Different AI frameworks : Replace LangGraph with PydanticAI, Google ADK, or other frameworks (clean architecture makes swapping easy).
References
- Schluntz, E., & Zhang, B. (n.d.). Building effective agents. Anthropic. anthropic.com/engineering/building-effective-agents
- Iusztin, P. (2025, February 23). Stop Building AI Agents. Use These 5 Patterns Instead. Decoding AI Magazine. decodingai.com/p/stop-building-ai-agents-use-these
- SwirlAI. (n.d.). Reflection and Working Memory. SwirlAI. newsletter.swirlai.com/p/building-ai-agents-from-scratch-part-8ca
- LangChain. (n.d.). Reflection Agents. LangChain Blog. blog.langchain.com/reflection-agents
- Moran, H., & Ryan, C. (n.d.). Prompting 101 | Code w/ Claude. YouTube. youtube.com/watch
- LangChain. (n.d.). Functional API overview. LangChain. docs.langchain.com/oss/python/langgraph/functional-api
- LangChain. (n.d.). Use the functional API. LangChain. docs.langchain.com/oss/python/langgraph/use-functional-api