Issue 15 | Manual Graph Editing: Manually Correcting Agent Hallucinations via update_state
🎯 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:
- Understand the core mechanism of
update_state: Master how to precisely locate and modify the runtime state of a specific thread in LangGraph. - Implement human intervention in Agent workflows: Learn to simulate human review and correct Agent outputs via
update_state, effectively addressing the "hallucination" problem. - Build more robust AI applications: Introduce a critical Human-In-The-Loop (HITL) step to your AI Content Agency, improving content quality and reliability.
- Master debugging and backtracking techniques: Use
update_stateas 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:
thread_id(orconfig): This tells LangGraph which specific conversation or task thread's state you want to modify. In LangGraph, every independentinvokeorstreamcall corresponds to a uniquethread_id. Thisthread_idis the key LangGraph uses to track and persist the state of each session.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).as_node(Optional, defaults toFalse): This parameter is quite advanced. If set toTrue, 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 asFalse.
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_stateprovides 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_stateis 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_statewithout 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:2pxDiagram Explanation:
- User Input: The beginning of everything, a content creation request.
- Planner Agent: Responsible for generating a content outline based on the request.
- Researcher Agent: Collects data based on the outline and writes
research_outputinto the state. - 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. - Yes (Hallucination!): If hallucinations or errors are found in the
research_output, we enter the manual correction flow. - Manual Correction (Call
update_state): In this step, we manually callgraph.update_state(), passing in the current thread'sthread_idand a dictionary containing the correctedresearch_output, directly overwriting the erroneous information in the state. - Writer Agent: Whether the
Researcher's output passes directly or undergoes manual correction, theWriterwill write the first draft based on the latest, correctresearch_outputin the current state. - Editor Agent: Polishes and proofreads the first draft.
- 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:
AgentStateDefinition: We defined anAgentStateto carry data throughout the workflow, includinguser_query,research_output,writing_draft,final_content, andmessages.Annotatedcombined withoperator.addensures that certain fields (likeresearch_outputandwriting_draft) can have content appended to them.- 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 onresearch_output.editor_node: Polishes the first draft.
- Building LangGraph: We built the
StateGraphas usual, defining nodes and edges. SqliteSaver: IntroducingSqliteSaveris to persist the state of each thread, so we can accurately retrieve and modify the state viathread_id.- Simulated Execution:
- First Run: We first run the graph normally once.
researcher_nodewill intentionally output incorrect market size data. You will see that bothwriter_nodeandeditor_nodecreate 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 callapp.update_state().- The
configparameter specifies whichthread_id's state we want to modify. - The second parameter is a dictionary
{"research_output": "..."}, which tells LangGraph to update theresearch_outputfield of the current thread to the new value we provide.
- The
- Second Run: After the state is corrected, we call
app.invoke()again. Because thethread_idis the same, LangGraph loads the state we just corrected. At this time, whenwriter_nodeexecutes again (or rather, if it hasn't executed yet, it will see the new state), it will see theresearch_outputthat we manually corrected, thereby generating the correct article.
- First Run: We first run the graph normally once.
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.
Thread ID Confusion:
- Pitfall:
update_statemust specify the correctthread_id. If you accidentally modify the wrongthread_idduring development, you will be modifying the state of another session, leading to hard-to-track issues. - Avoidance: Always be clear about which
thread_idyou are operating on. In actual applications,thread_idis usually bound to the user session ID. During debugging, you can print thethread_idinconfigto confirm. - Advanced: For
streammode, eachchunkreturns aconfigcontaining the currentthread_id.
- Pitfall:
Understanding State Overwrite vs. Merge:
- Pitfall: The dictionary passed into
update_statewill 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, theresearch_outputfield might need to append multiple research results), but you directly overwrite it, you might lose historical information. - Avoidance: Carefully understand the
TypedDictdefinition of each field in yourAgentStateand the aggregation operation ofAnnotated(likeoperator.add). If appending is needed, ensure the value you pass toupdate_stateis the result of merging with the existing value, or that your state definition itself supports appending. In our example,research_outputisAnnotated[str, operator.add], so every node return appends. Butupdate_statedefaults to overwriting, so you need to manually fetch the old value and merge it.
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.# 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})
- Pitfall: The dictionary passed into
Side Effects of
as_node=True:- Pitfall: When
as_node=True,update_statewill be treated as
- Pitfall: When