Issue 15 | Manual Graph Editing: Manually Correcting Agent Hallucinations via update_state

Updated on 4/14/2026

🎯 Learning Objectives for This Issue

Architects and students, welcome back to the LangGraph Multi-Agent Expert Course! In the last issue, we delved into how to enable Agents to make flexible decisions in complex workflows. In this issue, we are not talking about decision-making; we are talking about "error correction"—and specifically, "manual error correction."

Imagine this: your Researcher Agent works hard to look up a bunch of information, but it "hallucinates" and gets a key piece of data wrong. The Writer Agent foolishly starts creating based on this incorrect information, ultimately producing a draft with "factual errors." This is simply a nightmare for a content creation agency! What can we do? Let the Editor Agent catch it? That's too late! We need a way to "turn back time" and manually correct the Agent's output before the error spreads, or even after it has occurred.

In this issue, we will dive deep into a powerful and highly practical feature in LangGraph: update_state. It acts like the ultimate reviewer holding a "red pen" in your content agency, able to intervene at any time to correct Agent output errors and ensure the entire process stays on the right track.

Through this issue, you will be able to:

  1. Understand the core mechanism of update_state: Master how to precisely locate and modify the runtime state of a specific thread in LangGraph.
  2. Implement human intervention in Agent workflows: Learn to simulate human review and correct Agent outputs via update_state, effectively addressing the "hallucination" problem.
  3. Build more robust AI applications: Introduce a critical Human-In-The-Loop (HITL) step to your AI Content Agency, improving content quality and reliability.
  4. Master debugging and backtracking techniques: Use update_state as a powerful debugging tool to quickly correct Agent behavior during the development phase and accelerate iteration.

📖 Principle Analysis

Students, we all know that the core of LangGraph is "State". What Agents pass between nodes is this continuously evolving global state. Typically, state modification is accomplished by nodes returning new state values after execution. But today, the update_state method we are introducing steps outside this conventional "node execution -> return new state" flow, allowing us to directly modify the current state of a specified thread from the outside, from a "God's-eye view."

What is this like? Your Researcher Agent, while researching an article, writes "the AI market size is 100 billion" as "1 trillion". This error has already entered your LangGraph state and is about to be passed to the Writer. Under normal circumstances, the Writer would start writing based on this erroneous "1 trillion" data. The role of update_state is to allow you, like a super editor, to rush directly into the state repository and manually change that incorrect research_output field from "1 trillion" back to "100 billion" before the Writer even starts, or after you discover the issue when the Writer is halfway through. Then, the Writer Agent can continue working based on this corrected, accurate data.

Isn't it a lot like finding a flaw in a photo in Photoshop and directly opening the layer to make local adjustments, rather than having to reshoot from scratch? Therefore, I vividly call it "manual retouching."

Core Parameters and Mechanisms of update_state

The update_state method is typically called on a CompiledGraph instance and requires the following key information:

  1. thread_id (or config): This tells LangGraph which specific conversation or task thread's state you want to modify. In LangGraph, every independent invoke or stream call corresponds to a unique thread_id. This thread_id is the key LangGraph uses to track and persist the state of each session.
  2. state: This is a dictionary containing the state key-value pairs you wish to update. The passed dictionary will be merged with the target thread's current state (usually a shallow merge, meaning it overwrites if the key exists, or adds it if it doesn't).
  3. as_node (Optional, defaults to False): This parameter is quite advanced. If set to True, LangGraph will treat this state update as the output of a "node". This means the update might trigger conditional edges in the graph, leading to further execution of the graph. However, in our "manual error correction" scenario today, we usually just want to modify the state and then perhaps manually decide whether the next step is to re-execute a node or let the flow continue, so it is generally kept as False.

Why is update_state necessary?

  • Addressing Agent Hallucination: This is the most direct application. LLMs are not perfect; they make mistakes. When Agents are built on LLMs, these errors are brought into the workflow. update_state provides a "safety valve" for human intervention.
  • Human-In-The-Loop (HITL): In certain critical stages, such as content review or important decision-making, human experts are needed for final confirmation or correction. update_state is the cornerstone for implementing this collaboration model.
  • Debugging and Development: When developing complex Agent workflows, Agent behavior does not always meet expectations. With update_state, you can quickly correct intermediate states and test different scenarios without running the entire process from scratch, greatly accelerating development efficiency.
  • Iterative Optimization: When you find that a specific output pattern of an Agent needs fine-tuning, you can make temporary corrections via update_state without modifying the Agent's code itself, providing a buffer for subsequent Agent optimization.

Mermaid Diagram: Content Creation Workflow under Human Intervention

Let's use a Mermaid diagram to intuitively understand the role of update_state in our AI Content Agency project.

graph TD
    A[User Input: Content Creation Request] --> B(Planner Agent: Plan Content Outline)
    B --> C(Researcher Agent: Collect Data)
    C -- research_output --> D{Human Review Point: Error Found?}
    D -- Yes (Hallucination!) --> E[Manual Correction: Call update_state]
    E -- Update state.research_output --> F(Writer Agent: Write First Draft)
    D -- No (Correct) --> F
    F --> G(Editor Agent: Polish and Proofread)
    G --> H[Output: Final Content]

    style A fill:#f9f,stroke:#333,stroke-width:2px
    style E fill:#ffc,stroke:#a00,stroke-width:2px,color:#a00
    style D fill:#fcf,stroke:#333,stroke-width:2px

Diagram Explanation:

  1. User Input: The beginning of everything, a content creation request.
  2. Planner Agent: Responsible for generating a content outline based on the request.
  3. Researcher Agent: Collects data based on the outline and writes research_output into the state.
  4. Human Review Point (Error Found?): This is the key step we introduce. At this point, we can selectively pause or inspect the Researcher's output.
  5. Yes (Hallucination!): If hallucinations or errors are found in the research_output, we enter the manual correction flow.
  6. Manual Correction (Call update_state): In this step, we manually call graph.update_state(), passing in the current thread's thread_id and a dictionary containing the corrected research_output, directly overwriting the erroneous information in the state.
  7. Writer Agent: Whether the Researcher's output passes directly or undergoes manual correction, the Writer will write the first draft based on the latest, correct research_output in the current state.
  8. Editor Agent: Polishes and proofreads the first draft.
  9. Output: Final high-quality content.

Through this process, update_state acts like a powerful "correction tape," able to precisely correct previous errors at any stage of the content production chain, ensuring downstream Agents receive the most accurate information.

💻 Practical Code Drill (Specific Application in the Agency Project)

Alright, enough theory. Let's get our hands dirty and see how to practically apply update_state in our AI Content Agency project.

Our scenario is: The Researcher Agent, while querying a trending topic (like "Generative AI market size"), accidentally provides incorrect or outdated data. As the "CTO and Editor-in-Chief of the AI Content Agency," we must personally intervene, correct this error, and then let the Writer Agent continue creating based on the corrected data.

import operator
from typing import Annotated, TypedDict, List
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.sqlite import SqliteSaver
import os

# Ensure the OpenAI API key is set
# os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY" # Please replace with your actual key

# 1. Define Graph State
# -----------------------------------------------------------
class AgentState(TypedDict):
    """
    Define the global state for LangGraph.
    This state will be passed and modified between various Agent nodes.
    """
    user_query: str  # The user's original content creation request
    research_output: Annotated[str, operator.add]  # Researcher's output, may contain multiple pieces of info, aggregated using operator.add
    writing_draft: Annotated[str, operator.add]  # First draft written by the Writer, aggregated using operator.add
    final_content: str  # The final produced content
    messages: Annotated[List[BaseMessage], operator.add] # Conversation history, facilitating context passing between Agents

# 2. Define Agent Node Functions
# -----------------------------------------------------------

# Simulate an LLM
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)

def planner_node(state: AgentState) -> AgentState:
    """
    Planner Agent Node: Plans the content outline based on the user query.
    """
    print("\n--- Planner Agent is planning the content ---")
    user_query = state["user_query"]
    messages = state.get("messages", [])
    
    # Build planning prompt
    prompt = f"""
    You are an experienced content planner.
    The user's request is: '{user_query}'.
    Please provide a detailed content outline for this article, including main chapters and key points.
    """
    
    # Call LLM for planning
    response = llm.invoke([HumanMessage(content=prompt)])
    plan = response.content
    
    print(f"Planning result:\n{plan}")
    
    # Update state
    return {
        "messages": [AIMessage(content=f"Planning result:\n{plan}")],
        "research_output": f"Planning Outline:\n{plan}\n", # Put the planning result into research_output for subsequent Agents to reference
        "writing_draft": "", # Clear the writing draft to prepare for the Writer
    }

def researcher_node(state: AgentState) -> AgentState:
    """
    Researcher Agent Node: Collects data based on the planning outline, simulating hallucination.
    """
    print("\n--- Researcher Agent is collecting data ---")
    current_messages = state["messages"]
    
    # Simulate the research process and intentionally introduce a "hallucinated" data point
    # Assume the user query is about "Generative AI market size"
    if "Generative AI market size" in state["user_query"] or "生成式 AI 市场规模" in state["user_query"]:
        research_info = """
        According to the latest research, the generative AI market size is expected to **reach $50 billion by 2025**.
        Main drivers include: technological advancements, enterprise digital transformation needs, and the explosion of emerging application scenarios.
        (Note: A lower, potentially incorrect or outdated data point is intentionally set here to demonstrate update_state)
        """
        print("The researcher intentionally made a mistake: The market size data might be wrong!")
    else:
        research_info = "This is research information about other topics..."

    # Update state
    return {
        "messages": current_messages + [AIMessage(content=f"Research result:\n{research_info}")],
        "research_output": research_info,
    }

def writer_node(state: AgentState) -> AgentState:
    """
    Writer Agent Node: Writes the first draft based on the research results.
    """
    print("\n--- Writer Agent is writing the first draft ---")
    user_query = state["user_query"]
    research_output = state["research_output"]
    current_messages = state["messages"]
    
    # Build writing prompt
    prompt = f"""
    You are a professional article writer.
    User request: '{user_query}'
    The researcher provided the following materials:
    {research_output}
    
    Based on these materials, please write a first draft of an article about '{user_query}'.
    Be sure to cite the data and key information provided by the researcher.
    """
    
    # Call LLM for writing
    response = llm.invoke([HumanMessage(content=prompt)])
    draft = response.content
    
    print(f"Writing first draft:\n{draft[:200]}...") # Print partial content
    
    # Update state
    return {
        "messages": current_messages + [AIMessage(content=f"Writing first draft:\n{draft}")],
        "writing_draft": draft,
    }

def editor_node(state: AgentState) -> AgentState:
    """
    Editor Agent Node: Polishes and proofreads the first draft.
    """
    print("\n--- Editor Agent is proofreading and polishing ---")
    user_query = state["user_query"]
    writing_draft = state["writing_draft"]
    current_messages = state["messages"]
    
    # Build editing prompt
    prompt = f"""
    You are a rigorous content editor.
    This is the first draft of the article about '{user_query}':
    {writing_draft}
    
    Please make the following modifications to this article:
    1. Correct grammatical and spelling errors.
    2. Optimize sentence structures to make them more fluent and professional.
    3. Ensure the content logic is clear and arguments are strong.
    4. Check factual accuracy (although we are mainly simulating polishing here, in reality, an editor would do fact-checking).
    
    Please return the final polished article.
    """
    
    # Call LLM for editing
    response = llm.invoke([HumanMessage(content=prompt)])
    final_content = response.content
    
    print(f"Final content:\n{final_content[:200]}...") # Print partial content
    
    # Update state
    return {
        "messages": current_messages + [AIMessage(content=f"Final content:\n{final_content}")],
        "final_content": final_content,
    }

# 3. Build LangGraph Workflow
# -----------------------------------------------------------
def build_graph():
    workflow = StateGraph(AgentState)

    # Add nodes
    workflow.add_node("planner", planner_node)
    workflow.add_node("researcher", researcher_node)
    workflow.add_node("writer", writer_node)
    workflow.add_node("editor", editor_node)

    # Set edges
    workflow.add_edge(START, "planner")
    workflow.add_edge("planner", "researcher")
    workflow.add_edge("researcher", "writer")
    workflow.add_edge("writer", "editor")
    workflow.add_edge("editor", END)
    
    # Use SqliteSaver to persist state, allowing us to get thread_id and modify state
    memory = SqliteSaver.from_conn_string(":memory:")
    
    # Compile graph
    app = workflow.compile(checkpointer=memory)
    return app

# 4. Simulate Execution and Manual Correction
# -----------------------------------------------------------
if __name__ == "__main__":
    app = build_graph()
    
    user_input = "Please write an article about the Generative AI market size and its future trends."
    
    print("--- First Run: Researcher Agent makes a mistake ---")
    
    # Run the graph and get thread_id
    # config is a dictionary that can contain thread_id and thread_ts (timestamp)
    # We only pass thread_id here; LangGraph will automatically generate a UUID as thread_id
    # Or we can explicitly specify one
    config = {"configurable": {"thread_id": "gen_ai_market_report_1"}}
    
    # First run, let the Researcher Agent make a mistake
    initial_output = app.invoke(
        {"user_query": user_input, "messages": [HumanMessage(content=user_input)]},
        config=config
    )
    
    print("\n--- First Run Results (Researcher Mistake Version) ---")
    print(f"Research Output: {initial_output['research_output']}")
    print(f"Writing Draft (based on wrong data):\n{initial_output['writing_draft'][:200]}...")
    print(f"Final Content (based on wrong data):\n{initial_output['final_content'][:200]}...")
    
    # Assume our human review caught the Researcher's error
    print("\n--- Human review found Researcher hallucination: Market size data is wrong! ---")
    wrong_data = "reach $50 billion by 2025"
    correct_data = "expected to reach $1.1 trillion by 2030 (according to the latest Grand View Research report)"
    
    print(f"Original wrong data: '{wrong_data}'")
    print(f"Corrected to right data: '{correct_data}'")
    
    # Key step: Use update_state to correct the state
    # We directly modify the 'research_output' field
    # Note: Here we only updated 'research_output', but we could also update other fields, or even add new ones
    app.update_state(
        config, # Use the thread_id from the previous run
        {"research_output": f"Planning Outline:\n{initial_output['research_output']}\nCorrected Research Result:\n{correct_data}\n"}
        # For demonstration convenience, we directly overwrite research_output here. In reality, finer merge logic might be needed,
        # or planning and research results could be stored separately when the Researcher node returns.
    )
    
    print("\n--- State manually corrected. Now re-running Writer and Editor Agents ---")
    
    # Re-run starting from the Writer node (or continue after Researcher, depending on where you want to rollback)
    # To demonstrate the effect of update_state, we let the graph continue execution from the Writer node
    # LangGraph will continue from where it stopped last time, but since we modified the state, the Writer will see the new state
    
    # Here we simulate that the Writer has already run, but we modified the Researcher's output,
    # and we want the Writer to regenerate based on the new data. So we call invoke again, and LangGraph continues from the latest state.
    # Actually, to ensure the Writer re-executes, more complex control flow might be needed,
    # such as routing the state back to the Researcher or Writer node after the Editor finds an error.
    # But for a direct demonstration of update_state's effect, we just need to modify the state and let subsequent nodes continue.
    
    # For a clearer demonstration, we simulate "rolling back" to the state before the writer node (i.e., after modifying research_output)
    # In reality, `invoke` always starts execution from the current latest state until END
    # If you want to precisely start from a certain node, you need to use `stream` or lower-level controls
    # Here, we modified the state, and invoking again will make all subsequent nodes execute based on the new state.
    
    # Re-fetch the state to confirm state.research_output has been updated
    current_state_after_update = app.get_state(config).values
    print(f"\nCorrected Research Output (read from state): {current_state_after_update['research_output']}")
    
    # Run the entire graph again; Writer and Editor will use the corrected data
    # Note: We didn't explicitly "rollback" to a node here, but let the graph continue running from the current state
    # Because we modified research_output, the Writer will see this new value on its next execution.
    # LangGraph's invoke defaults to continuing from where the last execution ended (or the first unfinished node),
    # but if the entire graph has already executed to END, invoking again might restart it.
    # To demonstrate `update_state` affecting subsequent nodes, we just let it continue.
    
    # A more rigorous approach would be:
    # 1. Introduce a conditional edge after Researcher to determine if human review is needed.
    # 2. If needed, enter a "Manual Correction" node, which internally calls update_state.
    # 3. After correction, route back to the Writer node.
    # But for the core demonstration of `update_state` in this issue, we call it directly externally.
    
    # Simply call invoke again to let Writer and Editor run again based on the new state
    # Actually, if the graph has ended, invoke will start a new execution, but since the thread_id is the same,
    # it will load the old state and continue from there.
    # This also means the Writer and Editor will recalculate.
    print("\n--- Second Run: Based on corrected Researcher output ---")
    final_output_corrected = app.invoke(
        {"user_query": user_input, "messages": [HumanMessage(content=user_input)]},
        config=config
    )
    
    print("\n--- Second Run Results (Corrected Version) ---")
    print(f"Research Output: {final_output_corrected['research_output']}")
    print(f"Writing Draft (based on right data):\n{final_output_corrected['writing_draft'][:200]}...")
    print(f"Final Content (based on right data):\n{final_output_corrected['final_content'][:200]}...")
    
    # Verify if the corrected data was used
    assert correct_data in final_output_corrected['research_output']
    assert wrong_data not in final_output_corrected['writing_draft'] # Ensure Writer didn't use old data
    assert correct_data in final_output_corrected['writing_draft'] # Ensure Writer used new data
    
    print("\n--- Verification Successful: Writer and Editor Agents have continued working based on the corrected data! ---")

Code Breakdown:

  1. AgentState Definition: We defined an AgentState to carry data throughout the workflow, including user_query, research_output, writing_draft, final_content, and messages. Annotated combined with operator.add ensures that certain fields (like research_output and writing_draft) can have content appended to them.
  2. Agent Node Functions:
    • planner_node: Responsible for generating the content outline.
    • researcher_node: This is where we intentionally introduce "hallucination". When the user query involves "Generative AI market size", it returns outdated or incorrect data.
    • writer_node: Writes the first draft based on research_output.
    • editor_node: Polishes the first draft.
  3. Building LangGraph: We built the StateGraph as usual, defining nodes and edges.
  4. SqliteSaver: Introducing SqliteSaver is to persist the state of each thread, so we can accurately retrieve and modify the state via thread_id.
  5. Simulated Execution:
    • First Run: We first run the graph normally once. researcher_node will intentionally output incorrect market size data. You will see that both writer_node and editor_node create content based on this incorrect data.
    • Manual Correction: This is the core! We simulate a human review discovering the incorrect data in research_output. Then, we call app.update_state().
      • The config parameter specifies which thread_id's state we want to modify.
      • The second parameter is a dictionary {"research_output": "..."}, which tells LangGraph to update the research_output field of the current thread to the new value we provide.
    • Second Run: After the state is corrected, we call app.invoke() again. Because the thread_id is the same, LangGraph loads the state we just corrected. At this time, when writer_node executes again (or rather, if it hasn't executed yet, it will see the new state), it will see the research_output that we manually corrected, thereby generating the correct article.

Through this practical drill, you clearly saw how update_state acts as a "line-jumper" during LangGraph's execution, altering the data flow and correcting Agent errors.

Pitfalls and Avoidance Guide

The update_state feature is powerful, but if used improperly, it can also create pitfalls. As your senior mentor, I must give you a heads-up.

  1. Thread ID Confusion:

    • Pitfall: update_state must specify the correct thread_id. If you accidentally modify the wrong thread_id during development, you will be modifying the state of another session, leading to hard-to-track issues.
    • Avoidance: Always be clear about which thread_id you are operating on. In actual applications, thread_id is usually bound to the user session ID. During debugging, you can print the thread_id in config to confirm.
    • Advanced: For stream mode, each chunk returns a config containing the current thread_id.
  2. Understanding State Overwrite vs. Merge:

    • Pitfall: The dictionary passed into update_state will be merged with the existing state. If the key you pass already exists in the LangGraph state, the new value will overwrite the old value. If you expect an append rather than an overwrite (for example, the research_output field might need to append multiple research results), but you directly overwrite it, you might lose historical information.
    • Avoidance: Carefully understand the TypedDict definition of each field in your AgentState and the aggregation operation of Annotated (like operator.add). If appending is needed, ensure the value you pass to update_state is the result of merging with the existing value, or that your state definition itself supports appending. In our example, research_output is Annotated[str, operator.add], so every node return appends. But update_state defaults to overwriting, so you need to manually fetch the old value and merge it.
      # Incorrect example: Direct overwrite, might lose the Planner's planning result
      # app.update_state(config, {"research_output": correct_data})
      
      # Correct example: Fetch old state, then merge the newly corrected data
      current_state = app.get_state(config).values
      updated_research_output = current_state.get("research_output", "") + f"\n[Manual Correction]: {correct_data}\n"
      app.update_state(config, {"research_output": updated_research_output})
      
      In our example code, for simplicity, I directly demonstrated overwriting, but the comments also reminded that finer merge logic might be needed in actual applications.
  3. Side Effects of as_node=True:

    • Pitfall: When as_node=True, update_state will be treated as