Welcome to Day 28 of #30DaysOfLangChain – LangChain 0.3 Edition! So far, we’ve built powerful individual agents, but what happens when a problem is too complex for a single AI? Or when you need diverse perspectives, error checking, and iterative improvement? This is where advanced multi-agent patterns come into play!

Today, we’ll dive into how multiple specialized LLM agents can collaborate as a team, leveraging LangGraph’s unique capabilities to orchestrate sophisticated workflows like task delegation, self-correction, and achieving consensus.

Beyond the Single Agent: Why Multi-Agent Systems?

While a single LLM agent with tools can achieve a lot, complex problems often benefit from a “divide and conquer” approach:

  • Specialization: Different agents can be experts in specific domains or tasks, each with tailored prompts, tools, or even fine-tuned models.
  • Robustness & Reliability: By having multiple agents contribute or critique, you can reduce the risk of hallucination or errors from a single LLM.
  • Complex Workflows: Break down a large problem into manageable sub-tasks that can be delegated and combined.
  • Iterative Refinement & Self-Correction: Agents can critique each other’s outputs, leading to cycles of improvement.
  • Diverse Perspectives: Simulate brainstorming sessions or debates to generate more comprehensive or creative solutions.

Common Multi-Agent Architectures

  1. Hierarchical Agents (Supervisor/Worker): A high-level “supervisor” agent receives a complex query and delegates sub-tasks to specialized “worker” agents. The supervisor then aggregates their responses.
  2. Debate Agents / Consensus: Multiple agents are given a task or question and are prompted to “debate” or offer different perspectives. A final “judge” or “consensus” agent synthesizes their arguments into a final output.
  3. Iterative Refinement Loops: Agents perform a task, then pass their output to a “critique” or “refinement” agent, which suggests improvements. The original agent (or a new one) then attempts to incorporate the feedback, repeating the cycle until a satisfactory result is achieved.

LangGraph for Orchestrating Agent Teams

LangGraph’s StateGraph is the perfect tool for building these complex multi-agent systems. Its ability to:

  • Define nodes for each agent or step.
  • Manage a shared state that evolves as information flows between agents.
  • Use conditional edges to dynamically control the flow based on agent outputs.
  • Support cycles (loops) for iterative processes.

…makes it incredibly powerful for orchestrating sophisticated, stateful agentic workflows.

Project: An Idea Generation Team (Brainstorm, Critique, Refine)

Today, we’ll implement a simplified multi-agent “idea generation” team. Our team will consist of three “agents” (nodes in LangGraph):

  1. Brainstormer: Generates initial ideas based on a prompt.
  2. Critique Agent: Reviews the generated ideas, pointing out weaknesses or areas for improvement.
  3. Refiner Agent: Takes the original ideas and the critiques to produce an improved version.

This process will loop for a set number of iterations, demonstrating iterative refinement and self-correction.

Before you run the code:

  • Install necessary libraries: pip install langchain-openai langchain-ollama langgraph python-dotenv
  • Ensure your OPENAI_API_KEY is set if using OpenAI.
  • If using Ollama, ensure it’s running and you’ve pulled your desired chat model (e.g., ollama pull llama2).
import os
from dotenv import load_dotenv
from typing import TypedDict, Annotated, List
import operator

from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from langgraph.graph import StateGraph, END

load_dotenv()

# --- Configuration for LLM ---
LLM_PROVIDER = os.getenv("LLM_PROVIDER", "ollama").lower() # 'openai' or 'ollama'
OLLAMA_MODEL_CHAT = os.getenv("OLLAMA_MODEL_CHAT", "llama2").lower()
OPENAI_MODEL_CHAT = os.getenv("OPENAI_MODEL_CHAT", "gpt-3.5-turbo")

# --- Initialize LLM ---
def get_llm():
    if LLM_PROVIDER == "openai":
        if not os.getenv("OPENAI_API_KEY"):
            raise ValueError("OPENAI_API_KEY not set for OpenAI provider.")
        return ChatOpenAI(model=OPENAI_MODEL_CHAT, temperature=0.7) # Higher temp for creativity
    elif LLM_PROVIDER == "ollama":
        try:
            llm = ChatOllama(model=OLLAMA_MODEL_CHAT, temperature=0.7) # Higher temp for creativity
            llm.invoke("test", config={"stream": False})
            return llm
        except Exception as e:
            raise RuntimeError(f"Error connecting to Ollama chat LLM '{OLLAMA_MODEL_CHAT}': {e}") from e
    else:
        raise ValueError(f"Invalid LLM provider: {LLM_PROVIDER}. Must be 'openai' or 'ollama'.")

llm = get_llm()

# --- Define the Agent State ---
# This state will be passed between agents and updated by them.
class AgentState(TypedDict):
    """
    Represents the state of our multi-agent workflow.
    - initial_topic: The user's initial request/topic.
    - ideas: A list of generated ideas (strings).
    - critiques: A list of critiques for the current ideas.
    - iteration: Current iteration count for refinement.
    - max_iterations: Maximum allowed iterations for refinement.
    - messages: Langchain messages for conversation history (optional, but good for context)
    """
    initial_topic: str
    ideas: Annotated[List[str], operator.add]
    critiques: Annotated[List[str], operator.add]
    iteration: int
    max_iterations: int
    messages: Annotated[List[BaseMessage], operator.add]


# --- Agent Node Functions ---

def brainstorm_ideas(state: AgentState):
    """
    Brainstormer Agent: Generates initial ideas.
    """
    print("---BRAINSTORMING IDEAS---")
    topic = state["initial_topic"]
    current_ideas = state.get("ideas", [])

    prompt_template = ChatPromptTemplate.from_messages([
        ("system", "You are a creative brainstorming expert. Generate 3-5 distinct and innovative ideas based on the user's topic. Each idea should be a concise sentence or two. If there are existing ideas, try to generate new ones or variations, but don't just repeat them."),
        ("human", f"Topic: {topic}\nExisting ideas (avoid repeating): {current_ideas if current_ideas else 'None'}\nGenerate new ideas:")
    ])
    
    chain = prompt_template | llm | StrOutputParser()
    new_ideas_str = chain.invoke({"topic": topic, "current_ideas": current_ideas})
    
    # Simple parsing: split by lines, filter empty ones
    new_ideas = [idea.strip() for idea in new_ideas_str.split('\n') if idea.strip()]
    
    print(f"Generated Ideas: {new_ideas}")
    return {"ideas": new_ideas, "messages": [AIMessage(content=f"Generated initial ideas for '{topic}'.")]}

def critique_ideas(state: AgentState):
    """
    Critique Agent: Evaluates the generated ideas for flaws or areas of improvement.
    """
    print("---CRITIQUING IDEAS---")
    topic = state["initial_topic"]
    ideas = state["ideas"]
    
    # Combine all current ideas into a single string for critiquing
    ideas_text = "\n".join([f"- {idea}" for idea in ideas])

    prompt_template = ChatPromptTemplate.from_messages([
        ("system", "You are a critical thinking agent. Review the following ideas for a given topic. "
                   "Identify weaknesses, potential issues, missing aspects, or areas for improvement. "
                   "Provide constructive feedback for each idea or overall. Be specific and concise. "
                   "If ideas are generally good, suggest ways to make them even better or more unique."),
        ("human", f"Topic: {topic}\nIdeas to critique:\n{ideas_text}\n\nProvide your critique:")
    ])
    
    chain = prompt_template | llm | StrOutputParser()
    critique_str = chain.invoke({"topic": topic, "ideas_text": ideas_text})
    
    print(f"Critique: {critique_str}")
    return {"critiques": [critique_str], "messages": [AIMessage(content=f"Provided critique for ideas.")]}

def refine_ideas(state: AgentState):
    """
    Refiner Agent: Incorporates critiques to improve the ideas.
    """
    print("---REFINING IDEAS---")
    topic = state["initial_topic"]
    ideas = state["ideas"]
    critiques = state["critiques"]
    iteration = state["iteration"] + 1 # Increment iteration count

    # Combine all current ideas and critiques
    ideas_text = "\n".join([f"- {idea}" for idea in ideas])
    critiques_text = "\n".join([f"- {critique}" for critique in critiques])

    prompt_template = ChatPromptTemplate.from_messages([
        ("system", f"You are an idea refiner. Your goal is to improve the provided ideas based on the critiques. "
                   f"Generate 3-5 refined and enhanced ideas for the topic, taking into account all feedback. "
                   f"Focus on addressing the weaknesses and incorporating suggestions. "
                   f"Current Iteration: {iteration}"),
        ("human", f"Topic: {topic}\nOriginal Ideas:\n{ideas_text}\n\nCritiques:\n{critiques_text}\n\nGenerate Refined Ideas (3-5 concise sentences):")
    ])
    
    chain = prompt_template | llm | StrOutputParser()
    refined_ideas_str = chain.invoke({"topic": topic, "ideas_text": ideas_text, "critiques_text": critiques_text, "iteration": iteration})

    refined_ideas = [idea.strip() for idea in refined_ideas_str.split('\n') if idea.strip()]
    
    print(f"Refined Ideas: {refined_ideas}")
    # Clear critiques for the next round, and update ideas and iteration count
    return {"ideas": refined_ideas, "critiques": [], "iteration": iteration, "messages": [AIMessage(content=f"Refined ideas based on critique.")]}

def decide_next_step(state: AgentState) -> str:
    """
    Decides the next step in the workflow based on iteration count.
    """
    iteration = state["iteration"]
    max_iterations = state["max_iterations"]
    
    print(f"---DECIDING NEXT STEP (Iteration: {iteration}/{max_iterations})---")
    if iteration >= max_iterations:
        print("Max iterations reached. Ending.")
        return "end"
    else:
        print("Continuing to critique.")
        return "critique"


# --- Build the LangGraph Workflow ---
workflow = StateGraph(AgentState)

# Add nodes for each agent/step
workflow.add_node("brainstorm", brainstorm_ideas)
workflow.add_node("critique", critique_ideas)
workflow.add_node("refine", refine_ideas)

# Set the entry point
workflow.set_entry_point("brainstorm")

# Define edges
workflow.add_edge("brainstorm", "critique") # After brainstorming, always critique
workflow.add_edge("critique", "refine")    # After critique, always refine

# Define conditional edge for refinement loop
# After refinement, decide whether to loop back for more critique or end
workflow.add_conditional_edges(
    "refine",
    decide_next_step, # Function to determine next node
    {
        "critique": "critique", # Loop back to critique for another round
        "end": END              # End the graph
    }
)

# Compile the graph
app = workflow.compile()

# --- Run the Multi-Agent Team ---
if __name__ == "__main__":
    initial_topic = "Marketing campaign ideas for a new eco-friendly smart home device."
    max_iterations = 2 # Number of refinement cycles

    print(f"\n--- Starting Multi-Agent Idea Generation for: '{initial_topic}' ---")

    # The initial state passed to the graph
    inputs = {
        "initial_topic": initial_topic,
        "ideas": [],
        "critiques": [],
        "iteration": 0,
        "max_iterations": max_iterations,
        "messages": [HumanMessage(content=f"Generate ideas for: {initial_topic}")]
    }

    # Stream the output for better visibility of steps
    for s in app.stream(inputs):
        print(s)
        print("------")

    print("\n--- Final Ideas After Multi-Agent Collaboration ---")
    final_state = app.invoke(inputs) # Get the final state after execution
    for i, idea in enumerate(final_state["ideas"]):
        print(f"Idea {i+1}: {idea}")
    print(f"\nTotal Iterations: {final_state['iteration']}")

Code Explanation & Key Takeaways:

  1. AgentState (TypedDict): This is the heart of LangGraph’s state management. We define a TypedDict to explicitly type our shared state.
    • initial_topic: The user’s original request.
    • ideas: A list that will accumulate and be updated with brainstormed and refined ideas. We use Annotated[List[str], operator.add] for lists, meaning new items will be appended.
    • critiques: A list for critiques, cleared after each refinement round.
    • iteration, max_iterations: To control the looping refinement process.
    • messages: Optional, but useful for keeping a conversation history or for LangSmith tracing.
  2. Agent Node Functions (brainstorm_ideas, critique_ideas, refine_ideas):
    • Each function takes the state as input.
    • They use specific ChatPromptTemplates tailored to their role (creative generation, critical analysis, improvement).
    • They invoke the llm and StrOutputParser to get their output.
    • Crucially, each function returns a dictionary of updates to the state. For example, brainstorm_ideas returns {"ideas": new_ideas}, which updates the ideas list in the shared state. refine_ideas also increments iteration and clears critiques.
  3. decide_next_step (Conditional Edge Function):
    • This function determines the next node in the graph based on the current state.
    • It checks iteration against max_iterations to decide whether to loop back to critique (for another refinement cycle) or to END the graph.
  4. Building the StateGraph:
    • workflow = StateGraph(AgentState): Initializes the graph with our defined state.
    • workflow.add_node(...): Adds each agent function as a node.
    • workflow.set_entry_point("brainstorm"): The first node to execute.
    • workflow.add_edge(...): Defines unconditional transitions (e.g., from brainstorm to critique).
    • workflow.add_conditional_edges(...): This is where the magic of multi-agent orchestration shines! Based on the output of decide_next_step (which inspects the state), the graph dynamically transitions to critique or END.
  5. Running the Graph (app.stream(inputs)): We stream the execution to see the updates at each step, making the multi-agent flow transparent.

This advanced multi-agent pattern showcases how LangGraph empowers you to design complex, iterative, and collaborative AI workflows, moving far beyond simple chain execution.


Key Takeaway

Day 28 was an exciting deep dive into advanced multi-agent patterns with LangGraph! We moved beyond single-agent capabilities to build a collaborative “idea generation team” featuring brainstorming, critiquing, and iterative refinement. This project demonstrated how LangGraph’s state management and conditional edges enable sophisticated task delegation, self-correction loops, and achieving better results through agent team collaboration. This is the future of complex AI systems!

Leave a comment

I’m Arpan

I’m a Software Engineer driven by curiosity and a deep interest in Generative AI Technologies. I believe we’re standing at the frontier of a new era—where machines not only learn but create, and I’m excited to explore what’s possible at this intersection of intelligence and imagination.

When I’m not writing code or experimenting with new AI models, you’ll probably find me travelling, soaking in new cultures, or reading a book that challenges how I think. I thrive on new ideas—especially ones that can be turned into meaningful, impactful projects. If it’s bold, innovative, and GenAI-related, I’m all in.

“The future belongs to those who believe in the beauty of their dreams.”Eleanor Roosevelt

“Imagination is more important than knowledge. For knowledge is limited, whereas imagination embraces the entire world.”Albert Einstein

This blog, MLVector, is my space to share technical insights, project breakdowns, and explorations in GenAI—from the models shaping tomorrow to the code powering today.

Let’s build the future, one vector at a time.

Let’s connect