Building a Self‑Learning LangGraph Memory System with Feedback Loops and Dynamic Prompts
This article walks through the design and implementation of a two‑layer memory architecture for LangGraph agents, covering short‑term and long‑term stores, various storage back‑ends, prompt engineering, utility functions, node definitions, human‑in‑the‑loop interrupt handling, and how user feedback is captured and used to continuously update the agent’s behavior.
Modern AI agents often need a double‑layer memory system: a short‑term store that tracks the current conversation and a long‑term store that persists knowledge across sessions. The article explains this architecture and shows how LangGraph provides built‑in components for both layers.
Memory Stores
Three storage options are demonstrated:
InMemoryStore – a pure‑Python dictionary that lives only for the process lifetime. Example:
from langgraph.store.memory import InMemoryStore
in_memory_store = InMemoryStore()Local dev store – uses langgraph dev to pickle data to the local file system, providing persistence between notebook runs.
Production store – a PostgreSQL + pgvector backend that offers durable vector storage and semantic search.
Prompt Engineering
The tutorial defines a series of system and user prompts that embed the current memory contents. Key prompts include triage_system_prompt, triage_user_prompt, and the main agent_system_prompt_hitl_memory. These prompts reference placeholders such as {background}, {triage_instructions}, and {response_preferences} which are filled at runtime from the memory store.
Utility Functions
Helper functions are provided to keep the workflow clear:
# Parse the raw email dict
def parse_email(email_input: dict) -> tuple[str, str, str, str]:
return (
email_input["author"],
email_input["to"],
email_input["subject"],
email_input["email_thread"],
)
# Convert parsed fields into markdown for the LLM
def format_email_markdown(subject, author, to, email_thread):
return f"**Subject**: {subject}
**From**: {author}
**To**: {to}
{email_thread}
---"Two core memory‑access functions are introduced:
# Retrieve a memory entry or create it with a default value
def get_memory(store, namespace, default_content=None):
user_preferences = store.get(namespace, "user_preferences")
if user_preferences:
return user_preferences.value
else:
store.put(namespace, "user_preferences", default_content)
return default_content
# Update a memory entry using a dedicated LLM
def update_memory(store, namespace, messages):
user_preferences = store.get(namespace, "user_preferences")
memory_updater_llm = llm.with_structured_output(UserPreferences)
result = memory_updater_llm.invoke([
{"role": "system", "content": MEMORY_UPDATE_INSTRUCTIONS.format(
current_profile=user_preferences.value, namespace=namespace)},
] + messages)
store.put(namespace, "user_preferences", result.user_preferences)Node Definitions
The workflow is built from three main node types:
triage_router – classifies an incoming email using a structured RouterSchema and decides whether to respond, ignore, or notify. It injects the latest triage_preferences from memory into the system prompt.
llm_call – the reasoning node of the response agent. It fetches response_preferences and cal_preferences from memory, builds the full prompt, and calls the LLM with tool bindings.
interrupt_handler – a human‑in‑the‑loop node that presents each proposed tool call (e.g., write_email, schedule_meeting) to the user and processes four possible responses: edit, response, ignore, or accept. Each branch calls update_memory with context‑specific messages, ensuring the feedback is stored.
A small helper should_continue routes the graph to either the interrupt handler or the END state based on whether the LLM emitted a Done tool.
Assembling the Graph
Two sub‑graphs are compiled:
# Response agent sub‑graph
agent_builder = StateGraph(State)
agent_builder.add_node("llm_call", llm_call)
agent_builder.add_node("interrupt_handler", interrupt_handler)
agent_builder.add_edge(START, "llm_call")
agent_builder.add_conditional_edges(
"llm_call",
should_continue,
{"interrupt_handler": "interrupt_handler", END: END},
)
agent_builder.add_edge("interrupt_handler", "llm_call")
response_agent = agent_builder.compile()
# Overall workflow
overall_workflow = (
StateGraph(State, input=StateInput)
.add_node("triage_router", triage_router)
.add_node("triage_interrupt_handler", triage_interrupt_handler)
.add_node("response_agent", response_agent)
.add_edge(START, "triage_router")
.add_edge("triage_router", "response_agent")
.add_edge("triage_router", "triage_interrupt_handler")
.add_edge("triage_interrupt_handler", "response_agent")
)
email_assistant = overall_workflow.compile()Testing the Feedback Loop
Two test scenarios illustrate how the system learns:
Baseline (accept) – the user accepts the agent’s proposed schedule_meeting (45 min) and write_email without editing. After the run, all memory namespaces retain their default values, confirming that mere acceptance does not trigger learning.
Edit‑based learning – the user edits the schedule_meeting call to a 30‑minute meeting with a shorter subject, then edits the drafted email. The update_memory node records these changes, and the stored cal_preferences and response_preferences are updated with rules such as “prefer 30‑minute meetings” and “use the revised subject line”. Subsequent runs will automatically apply these preferences.
Utility display_memory_content prints the current state of each namespace, making it easy to verify that feedback has been incorporated.
Learning Cycle Overview
The article distills the continuous‑learning mechanism into four steps:
Feedback is the trigger – only when the user edits or provides a response does the learning path start.
Call the dedicated memory‑manager LLM – the MEMORY_UPDATE_INSTRUCTIONS prompt guides a specialized LLM to analyse the feedback.
Perform a precise update – the manager adds new rules while preserving existing ones, never overwriting the whole profile.
Inject updated knowledge on the next run – the refreshed preference strings are fetched from the Store and embedded in the agent’s prompts, altering future behaviour.
This loop enables a generic LangGraph agent to evolve into a personalized assistant over time.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Data STUDIO
Click to receive the "Python Study Handbook"; reply "benefit" in the chat to get it. Data STUDIO focuses on original data science articles, centered on Python, covering machine learning, data analysis, visualization, MySQL and other practical knowledge and project case studies.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
