Issue 19 | Cross-Agent Shared and Private State Walls: Information Penetration and Isolation
Welcome back, AI architects, to our "LangGraph Multi-Agent Expert Course". I am your old friend.
In the previous 18 episodes, our "AI Content Agency" has begun to take shape. The Planner strategizes, the Researcher frantically scrapes data across the web, the Writer writes furiously, and the Editor is strictly impartial. Looking at the screen full of successful logs, do you feel like you can pop the champagne and start taking orders to make money?
Hold on. Yesterday, a student posted a 100,000-Token error log in the group chat in the middle of the night, asking me in frustration: "Teacher, why did my Writer suddenly output a snippet of HTTP 404 Not Found and BeautifulSoup parsing failed code right in the middle of writing the article?"
I took one look at his architecture and slapped my thigh: "Bro, you let the Writer see the trash can in the Researcher's kitchen!"
In default LangGraph tutorials, everyone is used to passing a global State (usually containing a messages list) from beginning to end. This is like a company where everyone shares a single WeChat group: when the Researcher encounters network timeouts, parsing gibberish, and self-correction intermediate processes while looking up information, it all gets posted in this group. When the Writer wants to write an article based on the materials, they have to dig through hundreds of garbage chat logs to find useful information. This not only leads to a Token cost explosion, but also triggers severe LLM Hallucination.
Today, we are going to solve this core pain point in multi-agent architectures: State Isolation. We will introduce the concept of a "private state wall" to the Agency, preventing the Writer from seeing the Researcher's intermediate garbage error logs, refactoring the State, and isolating dirty data!
🎯 Learning Objectives for this Episode
Through this practical session, you will master the following advanced skills:
- Break the Global State Superstition: Understand why "sharing everything" is a disaster for complex Multi-Agent systems.
- Build a Subgraph Private State Wall: Utilize LangGraph's subgraph feature to create a "black-box workspace" for the Researcher.
- Implement State Penetration and Mapping: Master how to precisely control and only allow the Researcher's refined
clean_summaryto penetrate back to the global state for the Writer to use. - Reduce Token Consumption and Improve Stability: Use architectural means to physically isolate dirty data and improve the output quality of downstream Agents.
📖 Principle Analysis
In software engineering, we emphasize "high cohesion, low coupling" and the "principle of least privilege". The same applies to Agent architectures.
In a traditional single-graph structure, all Nodes are mounted on the same StateGraph and share the same TypedDict.
If the Researcher needs to perform 3 web searches and 2 anti-scraping retries, these intermediate states (such as raw_html, search_errors) will pile up in the global state.
Our breakthrough solution is: introducing Subgraphs.
We will upgrade the Researcher into an independent subgraph. It has its own ResearcherState. In this subgraph, the Researcher can freely make mistakes, retry, and handle gibberish. Once it finishes the dirty work and generates a clean "Research Summary", it only returns this summary to the parent graph (the global Agency) through the "state channel".
Look at the architecture diagram below. Once you understand it, you will grasp today's core philosophy:
graph TD
subgraph Global_Agency_State [Global State Area]
direction TB
G_Topic[Task Topic: topic]
G_Summary[Research Summary: research_summary]
G_Draft[First Draft: draft]
end
Planner(Planner Node
Assigns Tasks) --> Researcher_Subgraph
subgraph Researcher_Subgraph [Researcher Private Workspace Subgraph]
direction TB
R_State[(Private State ResearcherState)]
R_State -.contains.-> R_Raw[Raw Web Data raw_html]
R_State -.contains.-> R_Err[Retry Errors error_logs]
R_State -.contains.-> R_Steps[Intermediate Thought Process scratchpad]
Search(Search Node) --> Scrape(Scraper Node)
Scrape --"Error Retry"--> Search
Scrape --> Summarize(Distill Node)
end
Researcher_Subgraph --"Information Penetration: Only returns clean summary"--> G_Summary
G_Summary --> Writer(Writer Node
Writes based on summary)
style Global_Agency_State fill:#f9f9f9,stroke:#333,stroke-width:2px
style Researcher_Subgraph fill:#e6f7ff,stroke:#1890ff,stroke-width:2px,stroke-dasharray: 5 5
style G_Summary fill:#d9f7be,stroke:#52c41a
style R_Err fill:#ffccc7,stroke:#f5222dDiagram Explanation:
- The dashed box represents the Researcher's private workspace (Subgraph). The
raw_htmlanderror_logsinside are completely invisible to the outside (black box). - The green node
G_Summaryis the only information that penetrates the private wall. - When the Writer node is working, the Researcher's
error_logswill absolutely not appear in its context, thereby ensuring the purity of the creation.
💻 Practical Code Drill
Enough talk, show me the code. We will use Python and the latest LangGraph API to implement this refactoring. Please read the bilingual comments in the code carefully; they are the essence of this practice.
Step 1: Define Two Sets of State (Global and Private)
First, we must distinguish between the "public square" and the "private booth" at the code level.
from typing import TypedDict, List, Annotated
import operator
from langgraph.graph import StateGraph, START, END
# ==========================================
# 1. Define Global Agency State
# This is the clean context shared by Planner, Writer, and Editor
# ==========================================
class AgencyState(TypedDict):
topic: str
# Core: Only store the refined summary here, no intermediate garbage
research_summary: str
draft: str
final_article: str
# ==========================================
# 2. Define Researcher's Private State
# Its task is to turn the topic into a research_summary
# ==========================================
class ResearcherState(TypedDict):
# Input inherited from the global state
topic: str
# --- The following is Dirty/Private Data ---
# Use Annotated and operator.add to accumulate intermediate logs, but never leak them to the global state
search_queries: Annotated[List[str], operator.add]
raw_html_snippets: Annotated[List[str], operator.add]
error_logs: Annotated[List[str], operator.add]
retry_count: int
# Final output
research_summary: str
Step 2: Build the Researcher Subgraph
Next, we wrap the Researcher into an independent Graph. It can do whatever it wants internally, as long as it spits out the research_summary in the end.
# Mock: Search and scraper node with errors and dirty data
def search_and_scrape(state: ResearcherState):
print(" [Researcher] Scraping dirty data across the web...")
topic = state["topic"]
# Simulating the generation of garbage logs and intermediate waste
mock_html = f"<html><body>Lots of messy data about {topic}...</body></html>"
mock_error = "HTTP 404: Image not found during scraping."
return {
"search_queries": [f"Deep dive {topic}"],
"raw_html_snippets": [mock_html],
"error_logs": [mock_error],
"retry_count": state.get("retry_count", 0) + 1
}
# Mock: Distill a clean summary from dirty data
def distill_information(state: ResearcherState):
print(" [Researcher] Filtering dirty data, distilling core summary...")
# Only here does the LLM read the messy raw_html_snippets
dirty_data_size = len(str(state.get("raw_html_snippets", [])))
error_count = len(state.get("error_logs", []))
# Mock the LLM distillation process
clean_summary = f"[Clean Research Summary]: The core points about {state['topic']} are XYZ. Filtered {dirty_data_size} bytes of dirty data and {error_count} error logs."
return {"research_summary": clean_summary}
# Assemble the Researcher Subgraph
researcher_builder = StateGraph(ResearcherState)
researcher_builder.add_node("search_and_scrape", search_and_scrape)
researcher_builder.add_node("distill_information", distill_information)
researcher_builder.add_edge(START, "search_and_scrape")
researcher_builder.add_edge("search_and_scrape", "distill_information")
researcher_builder.add_edge("distill_information", END)
# Compile the subgraph
researcher_graph = researcher_builder.compile()
Step 3: Build the Global Graph and Embed the Subgraph
Now, it's time to witness the magic. In the global Agency Graph, we mount the compiled researcher_graph from above as a regular Node.
Key Knowledge Point: When LangGraph executes a subgraph, it passes the parent graph's State into the subgraph's State (matching by key name, e.g., topic will be passed in). When the subgraph finishes execution (reaches END), it returns the subgraph's final output State to the parent graph, also overwriting or appending based on key name matching.
# Mock: Planner Node
def planner_node(state: AgencyState):
print(f"\n[Planner] Received task topic: {state['topic']}")
return {"topic": state["topic"]}
# Mock: Writer Node
def writer_node(state: AgencyState):
# Key observation: Can the Writer see the error_logs?
print("\n[Writer] Preparing to start writing...")
# Deliberately try to fetch dirty data to see if it's accessible
if "error_logs" in state: # type: ignore
print(" [Writer Crashed] Oops! I saw the error logs, my prompt is polluted!")
else:
print(" [Writer Ecstatic] Awesome! My context is extremely clean, without any garbage data!")
summary = state.get("research_summary", "")
print(f" [Writer] Received reference materials: {summary}")
draft = f"This is a brilliant first draft written based on {summary}."
return {"draft": draft}
# Assemble the Global Agency Graph
agency_builder = StateGraph(AgencyState)
agency_builder.add_node("planner", planner_node)
# Add the compiled subgraph directly as a node! (LangGraph's magical feature)
agency_builder.add_node("researcher_team", researcher_graph)
agency_builder.add_node("writer", writer_node)
agency_builder.add_edge(START, "planner")
agency_builder.add_edge("planner", "researcher_team")
agency_builder.add_edge("researcher_team", "writer")
agency_builder.add_edge("writer", END)
agency_graph = agency_builder.compile()
Step 4: Run and Verify
Let's run it and see the power of state isolation.
if __name__ == "__main__":
print("=== 🚀 AI Content Agency Started (Episode 19 State Isolation Version) ===\n")
initial_state = {"topic": "2024 AI Agent Development Trends"}
# Run the global graph
final_state = agency_graph.invoke(initial_state)
print("\n=== 🏁 Run Finished, Checking Global Final State ===")
for key, value in final_state.items():
print(f"-> {key}: {value}")
Console Output:
=== 🚀 AI Content Agency Started (Episode 19 State Isolation Version) ===
[Planner] Received task topic: 2024 AI Agent Development Trends
[Researcher] Scraping dirty data across the web...
[Researcher] Filtering dirty data, distilling core summary...
[Writer] Preparing to start writing...
[Writer Ecstatic] Awesome! My context is extremely clean, without any garbage data!
[Writer] Received reference materials: [Clean Research Summary]: The core points about 2024 AI Agent Development Trends are XYZ. Filtered 61 bytes of dirty data and 1 error logs.
=== 🏁 Run Finished, Checking Global Final State ===
-> topic: 2024 AI Agent Development Trends
-> research_summary: [Clean Research Summary]: The core points about 2024 AI Agent Development Trends are XYZ. Filtered 61 bytes of dirty data and 1 error logs.
-> draft: This is a brilliant first draft written based on [Clean Research Summary]: The core points about 2024 AI Agent Development Trends are XYZ. Filtered 61 bytes of dirty data and 1 error logs..
See that? The keys raw_html_snippets and error_logs do not exist at all in the global final_state! The garbage generated by the Researcher in the subgraph is perfectly sealed within the subgraph's lifecycle and vanishes into thin air. The Writer receives a highly pure research_summary.
Pitfalls & Avoidance Guide
As your mentor, I not only want to teach you how to write code, but also how to troubleshoot. When implementing the "state wall", beginners are most likely to fall into the following three pitfalls:
💣 Pitfall 1: Key Name Mismatch Leading to "Silent Drop"
Symptom: The Researcher subgraph clearly ran successfully, but the research_summary received by the Writer is empty.
Cause: When LangGraph returns from a subgraph to the parent graph, it updates strictly according to the Key names. If the dictionary returned by the subgraph is called summary, but the global state calls it research_summary, LangGraph will simply discard this mismatched key, and will not throw an error!
Avoidance: Make absolutely sure that the key names in ResearcherState that need to penetrate are exactly the same as the key names in AgencyState.
💣 Pitfall 2: Mindless Appending to the Global Message List
Symptom: Many people like to put a messages: Annotated[list, add] in the global state. Then, all LLM calls in the subgraph also stuff things into this messages list. As a result, after one run, the global messages inflates to 50,000 Tokens.
Cause: Even if you use a subgraph, if you write the subgraph's internal messages to a key named messages that shares the same name as the global one, the dirty data will still penetrate!
Avoidance: Rename the subgraph's conversation history key. For example, call the global one agency_messages and the subgraph one researcher_internal_messages. The distillation node finally generates only one clean AIMessage, returning it in the format of {"agency_messages": [clean_msg]}.
💣 Pitfall 3: Excessive Nesting Leading to Debugging Hell
Symptom: For extreme isolation, the graph is nested 5 layers deep: Agency -> ResearcherTeam -> WebScraper -> ErrorHandler... When an error finally occurs, the Traceback is as long as an endless scroll. Cause: Over-engineering. Avoidance: Don't overdo it. Usually, a Global Graph + 1 layer of Subgraph is enough to handle 90% of business scenarios. If finer isolation is needed, prioritize handling it inside standard Python functions rather than mindlessly adding LangGraph nodes.
📝 Summary of this Episode
Students, today we have completed a cognitive upgrade at the architectural level.
In multi-agent systems, "what Agents can see from each other" is just as important as "what Agents can do." Without state isolation, your system is like an amateur setup with no departmental divisions, where everyone is shouting in a single large hall. Today, through LangGraph's Subgraph feature, we built a "private state wall" for the Researcher. The dirty work is digested within the wall, and only the most refined value (Summary) penetrates back to the global state.
This not only saves a massive amount of Token costs and greatly reduces the hallucination rate, but also gives your code enterprise-grade maintainability.
Teaser for Next Episode: Now our Writer can get clean data to write drafts, but what if what it writes is still a pile of nonsense with a strong "AI flavor"? In Episode 20, we will introduce the Human-in-the-loop mechanism for the Editor. I will teach you how to make LangGraph "pause" at critical nodes, waiting for the boss's (your) approval before continuing execution.
Make sure you all type out today's code after class. See you next episode! Class dismissed!