Python & AI Tutorials Logo
LangChain & LangGraph

11. Konversationelles RAG: Gedächtnis zum Retrieval hinzufügen

In Kapitel 10 haben wir die Retrieval-Qualität unseres RAG-Systems verbessert. Doch eine Einschränkung besteht weiterhin: Jede Frage wird einzeln behandelt. Wenn ein Benutzer fragt „Wie lautet Ihre Rückerstattungsrichtlinie?", findet unser System die relevanten Inhalte aus den Dokumenten und antwortet. Wenn die nächste Frage eingeht, beantwortet das System sie ohne jede Erinnerung an das vorherige Gespräch.

Schauen wir uns an, warum das in einem echten Gespräch ein Problem darstellt. Ein Benutzer fragt „Wie lautet Ihre Rückerstattungsrichtlinie?" und stellt dann die Folgefrage „Gilt das auch für digitale Produkte?" Diese Folgefrage setzt den Kontext der „Rückerstattungsrichtlinie" aus der vorherigen Runde voraus, doch der Fragetext selbst enthält keine solche Information. Wenn wir „Gilt das auch für digitale Produkte?" direkt als Suchanfrage verwenden, ruft der Retriever irrelevante Informationen zu „digitalen Produkten" ab (zum Beispiel Preise oder Spezifikationen), und das RAG-System generiert eine Antwort, die nicht der Absicht des Benutzers entspricht.

In diesem Kapitel lernen wir, wie wir dieses Problem lösen können. Wir lernen, mehrdeutige Folgefragen in vollständige Fragen umzuformulieren (rewrite), nutzen diese Technik, um ein konversationelles RAG-System (conversational RAG) zu bauen, und behandeln, wie wir den Konversationsverlauf verwalten, wenn Gespräche länger werden.

11.1) Folgefragen in vollständige Fragen umformulieren

Wie wir in der Einleitung gesehen haben, bauen Folgefragen auf dem Kontext des vorherigen Gesprächs auf, sodass Menschen dazu neigen, einen Großteil der Information wegzulassen. Dadurch ist eine Folgefrage für sich allein oft unvollständig. Wie können wir das lösen?

In Kapitel 8 haben wir gelernt, wie wir einem LLM helfen, den Gesprächskontext zu verstehen, indem wir den Konversationsverlauf zusammen mit jeder Nachricht übergeben. Denselben Ansatz können wir hier anwenden. Wir übergeben die Folgefrage zusammen mit dem Konversationsverlauf an das LLM und bitten es, sie in eine vollständige Frage umzuformulieren, die den Kontext widerspiegelt. Zum Beispiel wird die Folgefrage „Gilt das auch für digitale Produkte?" zusammen mit dem Konversationsverlauf in „Sind digitale Produkte rückerstattungsfähig?" umformuliert. Mit dieser umformulierten Frage kann die Suche die richtigen Dokumente über Rückerstattungsrichtlinien für digitale Produkte finden.

Diese Technik wird Query Rewriting genannt. Erstellen wir dafür einen System-Prompt.

python
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage
 
llm = ChatOpenAI(model="gpt-5-mini")
 
system_prompt = (
    "Given a chat history and the latest user question "
    "which might reference context in the chat history, "
    "formulate a standalone question "
    "which can be understood without the chat history. "
    "Do NOT answer the question, just reformulate it if needed "
    "and otherwise return it as is."
)

Die Kernanweisung in diesem System-Prompt lautet: „Formuliere die Folgefrage anhand des Chatverlaufs in eine vollständige Frage um." Zwei konkrete Anweisungen sind wichtig.

Erstens: „Do NOT answer the question, just reformulate it." Dies weist das LLM an, die Frage nur umzuformulieren und nicht zu beantworten. Ohne diese Anweisung neigt das LLM dazu, die Frage zu beantworten, statt sie umzuformulieren. Was wir hier wollen, ist keine Antwort, sondern eine vollständige Frage, die ohne den Chatverlauf verstanden werden kann.

Zweitens: „otherwise return it as is." Dies weist das LLM an, die Frage unverändert zu lassen, wenn sie keine Umformulierung benötigt. Ohne dies könnte das LLM die Frage unnötig umformulieren und dabei möglicherweise ihre ursprüngliche Bedeutung oder ihren Umfang verändern.

Nutzen wir nun diesen System-Prompt, um tatsächlich eine Folgefrage umzuformulieren.

python
messages = [
    SystemMessage(content=system_prompt),
    # Chatverlauf
    HumanMessage(content="What is your refund policy?"),
    AIMessage(content="All physical products may be returned within 30 days of purchase for a full refund."),
    # Folgefrage
    HumanMessage(content="Does that apply to digital products too?"),
]
 
response = llm.invoke(messages)
print(response.content)

Ausgabe:

Are digital products eligible for a refund?

Das LLM hat den Gesprächsverlauf gelesen, erkannt, dass es bei der Frage um die „Rückerstattungsrichtlinie" ging, und sie in eine vollständige Frage umformuliert. Die Suche mit dieser umformulierten Frage liefert Dokumente, die der Absicht des Benutzers entsprechen.

Im nächsten Abschnitt integrieren wir diesen Umformulierungsschritt in die RAG-Pipeline, sodass Umformulierung, Retrieval und Antwortgenerierung alle in einem einzigen Aufruf erfolgen.

11.2) Konversationelles RAG bauen

Im vorherigen Abschnitt haben wir gelernt, wie wir Folgefragen in vollständige Fragen umformulieren, indem wir den Konversationsverlauf an das LLM übergeben. Nun integrieren wir diesen Umformulierungsschritt in die RAG-Pipeline, um ein konversationelles RAG zu bauen, bei dem Umformulierung → Retrieval → Antwortgenerierung alle in einem einzigen Aufruf erfolgen.

LangChain stellt Chain-Hilfsfunktionen zum Bauen von konversationellem RAG bereit (create_history_aware_retriever, create_retrieval_chain usw.), aber diese Funktionen befinden sich im Paket langchain-classic, dessen Support im Dezember 2026 endet. Die offizielle LangChain-Dokumentation empfiehlt nun, stattdessen Agents zu verwenden.

Wir werden daher in diesem Kapitel Agents verwenden, um konversationelles RAG umzusetzen. Agents werden ausführlich in Teil V (Kapitel 15–17) behandelt, daher führen wir hier nur das ein, was für unsere Umsetzung von konversationellem RAG nötig ist.

11.2.1) Agent-Komponenten, die wir hier verwenden

In Kapitel 5 haben wir einen kurzen Blick auf das Kernkonzept von Agents geworfen. Wenn das LLM die Anfrage eines Benutzers analysiert und entscheidet, welches Tool verwendet werden soll, führt das System diese Entscheidung aus. Damals haben wir diesen Prozess manuell umgesetzt, aber LangChain stellt APIs bereit, die ihn deutlich vereinfachen. Hier eine kurze Einführung in die drei Komponenten, die wir verwenden werden.

@tool: Ein Decorator, der eine reguläre Python-Funktion in ein Tool umwandelt, das der Agent verwenden kann. Der Agent wählt und ruft basierend auf der Anfrage des Benutzers autonom das passende Tool aus seinen registrierten Tools aus.

create_agent: Eine Funktion, die ein LLM, eine Liste von Tools und einen System-Prompt entgegennimmt, um einen Agent zu erstellen. Sie handhabt den Entscheidungs-Ausführungs-Ablauf des Agents intern.

InMemorySaver: Ein Checkpointer, der den Konversationsverlauf automatisch verwaltet. Er organisiert Gespräche nach thread_id, sodass beim Aufruf des Agents mit derselben thread_id der vorherige Konversationsverlauf automatisch geladen wird.

11.2.2) Das Retrieval-Tool erstellen

Verwandeln wir zuerst die Vector-Store-Suche, die wir in Kapitel 10 gebaut haben, in ein Tool, das der Agent verwenden kann.

python
from langchain.tools import tool
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
 
# Verbindung zum in Kapitel 10 gebauten Vector Store herstellen
embedding_model = OpenAIEmbeddings(model="text-embedding-3-small")
vector_store = Chroma(
    persist_directory="data/chroma_db",
    collection_name="company_docs",
    embedding_function=embedding_model,
)
 
@tool
def retrieve_context(query: str):
    """Durchsucht Dokumente nach Inhalten, die für die Anfrage relevant sind."""
    retrieved_docs = vector_store.similarity_search(query, k=3)
    serialized = "\n\n".join(
        f"Source: {doc.metadata['source']}\nContent: {doc.page_content}"
        for doc in retrieved_docs
    )
    return serialized

Der @tool-Decorator wandelt die Funktion retrieve_context in ein Tool um, das der Agent verwenden kann. Der Agent entscheidet basierend auf der Frage des Benutzers autonom, ob er dieses Tool aufruft.

11.2.3) Den Agent erstellen

Wir übergeben das Retrieval-Tool, einen System-Prompt und einen Checkpointer an create_agent, um den Agent zu erstellen.

python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver  # Wird automatisch mit langchain installiert
 
agent = create_agent(
    model="gpt-5-mini",
    tools=[retrieve_context],
    system_prompt=(
        "You are a helpful assistant that answers questions about company policies. "
        "Use the retrieve_context tool to search for relevant information. "
        "If the retrieved context does not contain relevant information, "
        "say that you don't know. "
        "Keep the answer concise, three sentences maximum."
    ),
    checkpointer=InMemorySaver(),
)
  • model: Das LLM, das der Agent verwenden wird.
  • tools: Die Liste der Tools, die dem Agent zur Verfügung stehen. Wir registrieren das oben erstellte Dokument-Retrieval-Tool (retrieve_context).
  • system_prompt: Die Verhaltensanweisungen des Agents. Er weist den Agent an, das Retrieval-Tool zu verwenden, um Fragen zu Unternehmensrichtlinien zu beantworten, und zu sagen, dass er es nicht weiß, wenn dem abgerufenen Kontext relevante Informationen fehlen.
  • checkpointer: Verwaltet den Konversationsverlauf automatisch. InMemorySaver() speichert Gespräche im Arbeitsspeicher und handhabt automatisch den Konversationsverlauf, den wir in Kapitel 8 manuell verwaltet haben.

Ja

Nein

Benutzerfrage

Agent

Konversationsverlauf
(InMemorySaver)

Tool-Aufruf?

retrieve_context
Tool-Ausführung

Endgültige Antwort

Wenn der Agent eine Benutzerfrage erhält, zieht er den Konversationsverlauf zurate und entscheidet, ob eine Dokumentensuche im Vector Store nötig ist. Falls ja, ruft er das Tool retrieve_context auf, um relevante Dokumente abzurufen, und generiert über das LLM eine Antwort. Der Konversationsverlauf wird automatisch von InMemorySaver verwaltet.

11.2.4) Ein Gespräch über mehrere Runden ausführen

Führen wir ein tatsächliches Gespräch über zwei Runden aus, um zu überprüfen, ob es Folgefragen korrekt behandelt.

python
# thread_id ist ein Bezeichner, der Gespräche unterscheidet
# Die Verwendung derselben thread_id setzt dasselbe Gespräch fort
thread_config = {"configurable": {"thread_id": "1"}}
 
# --- Runde 1: Eine vollständige Frage ---
response1 = agent.invoke(
    {"messages": [{"role": "user", "content": "What is your refund policy?"}]},
    thread_config,
)
print("F: What is your refund policy?")
print("A:", response1["messages"][-1].content)
 
# --- Runde 2: Eine Folgefrage, die von Runde 1 abhängt ---
response2 = agent.invoke(
    {"messages": [{"role": "user", "content": "Does that apply to digital products too?"}]},
    thread_config,
)
print("\nF: Does that apply to digital products too?")
print("A:", response2["messages"][-1].content)

Ausgabe:

F: Wie lautet Ihre Rückerstattungsrichtlinie?
A: Alle physischen Produkte können innerhalb von 30 Tagen nach dem Kauf für eine
vollständige Rückerstattung zurückgegeben werden. Der Originalbeleg oder die
Bestellbestätigungs-E-Mail ist erforderlich, und die Artikel müssen sich in ihrer
Originalverpackung und in unbenutztem Zustand befinden.
Nach 30 Tagen werden Rückgaben nur gegen Guthaben akzeptiert.
 
F: Gilt das auch für digitale Produkte?
A: Digitale Produkte (Softwarelizenzen, E-Books, Online-Kurse) sind nicht
erstattungsfähig, sobald der Download- oder Zugangslink aktiviert wurde.
Sollten jedoch technische Probleme den Zugriff verhindern, können Sie sich innerhalb
von 7 Tagen an den Support wenden, um einen Ersatz oder eine Rückerstattung zu erhalten.

In der zweiten Runde haben wir „Gilt das auch für digitale Produkte?" übergeben, aber der Agent hat aus dem Konversationsverlauf erkannt, dass es sich um eine Folgefrage zur Rückerstattungsrichtlinie handelte, und den Abschnitt über digitale Produkte aus den Rückerstattungsrichtlinien-Dokumenten korrekt abgerufen.

Moment — für diesen Agenten haben wir keinen Query-Rewriting-Schritt wie in Abschnitt 11.1 hinzugefügt. Wie wurde die Folgefrage dann korrekt verarbeitet? Wenn das LLM ein Tool (eine mit @tool dekorierte Funktion) aufruft, erzeugt es die Argumente des Tools selbst. Das gilt auch für die Benutzeranfrage, die an retrieve_context übergeben wird — da das LLM den gesamten Konversationsverlauf kennt, hat es die Folgefrage vor dem Aufruf in eine vollständige, eigenständige Frage umgeschrieben. Wir haben keinen separaten Rewriting-Schritt eingerichtet, und dennoch fand Query Rewriting als Teil des Tool-Aufrufs statt.

Beachten Sie außerdem, dass wir den Konversationsverlauf nicht manuell verwalten mussten — InMemorySaver verwaltet ihn automatisch pro thread_id.

Der nächste Abschnitt behandelt das Problem, das entsteht, wenn Gespräche länger werden und der Verlauf größer wird, sowie dessen Lösung.

11.3) Längere Gespräche verwalten

Das konversationelle RAG, das wir gebaut haben, funktioniert anfangs gut, doch mit zunehmender Länge der Gespräche können Probleme auftreten. Wie wir in Kapitel 8 gelernt haben, haben LLMs eine maximale Eingabegröße, die sie in einem einzigen Aufruf verarbeiten können. Der System-Prompt, der Konversationsverlauf, die abgerufenen Dokumente und die Benutzerfrage müssen alle innerhalb dieses Limits passen.

Wenn Gespräche länger werden, nimmt der Konversationsverlauf mehr Tokens in Anspruch, überschreitet schließlich die maximale Eingabegröße und führt dazu, dass API-Aufrufe fehlschlagen. Auch die Kosten steigen mit jedem Aufruf, da pro Token abgerechnet wird. Das bedeutet, dass wir die Größe unseres Konversationsverlaufs verwalten müssen.

In Kapitel 8 haben wir dieses Problem mit einem Sliding Window gelöst: Wir behalten nur die letzten N Nachrichten und verwerfen ältere. Dasselbe Konzept gilt in einer Agent-Umgebung. create_agent unterstützt Middleware, also einen Verarbeitungsschritt, der Nachrichten verändern kann, bevor das LLM aufgerufen wird. Wir können Middleware verwenden, um alten Verlauf zu kürzen.

11.3.1) Verlauf mit Middleware begrenzen

Der @before_model-Decorator funktioniert ähnlich wie der @tool-Decorator, den wir in Abschnitt 11.2 gesehen haben. So wie @tool eine Funktion in ein Tool umwandelt, das der Agent verwenden kann, wandelt @before_model eine Funktion in Middleware um, die vor jedem LLM-Aufruf ausgeführt wird. Die umgewandelte Middleware wird aktiviert, indem sie im Parameter middleware von create_agent registriert wird.

python
from langchain.agents import create_agent, AgentState
from langchain.agents.middleware import before_model
from langchain.messages import RemoveMessage
from langgraph.graph.message import REMOVE_ALL_MESSAGES
 
@before_model
def trim_old_messages(state: AgentState, runtime) -> dict | None:
    """Entfernt alte Nachrichten vor jedem LLM-Aufruf."""
    messages = state["messages"]
    # Wenn es wenige genug Nachrichten gibt, nichts tun
    if len(messages) <= 10:
        return None
    # Nur die Systemnachricht (erste) und die 10 jüngsten Nachrichten behalten
    return {
        "messages": [
            RemoveMessage(id=REMOVE_ALL_MESSAGES),
            messages[0],     # Systemnachricht
            *messages[-10:], # Letzte 10 Nachrichten (5 Runden)
        ]
    }

AgentState ist ein Objekt, das die Zustandsdaten des Agents enthält, wobei state["messages"] die Liste der bisherigen Konversationsnachrichten enthält. Der Rückgabewert der Middleware bestimmt, wie diese Konversationsliste verändert wird.

  • Die Rückgabe von None lässt die bestehenden Zustandsdaten des Agents unverändert.
  • Die Rückgabe eines Dictionarys wendet dessen Inhalte auf die bestehende Nachrichtenliste an. Im obigen Code löscht RemoveMessage(id=REMOVE_ALL_MESSAGES) zuerst alle bestehenden Nachrichten und fügt dann nur die Systemnachricht und die 10 jüngsten Nachrichten wieder hinzu. Im Ergebnis werden nur diese Nachrichten an das LLM übergeben.

Registrieren Sie diese Middleware beim Agent:

python
agent = create_agent(
    model="gpt-5-mini",
    tools=[retrieve_context],
    system_prompt=(
        "You are a helpful assistant that answers questions about company policies. "
        "Use the retrieve_context tool to search for relevant information. "
        "If the retrieved context does not contain relevant information, "
        "say that you don't know. "
        "Keep the answer concise, three sentences maximum."
    ),
    checkpointer=InMemorySaver(),
    middleware=[trim_old_messages],  # Middleware registrieren
)

Dies ist derselbe Agent aus Abschnitt 11.2, ergänzt um middleware=[trim_old_messages]. Nun werden, egal wie lang das Gespräch wird, nur die jüngsten Nachrichten an das LLM übergeben.

11.3.2) Der Kompromiss beim Sliding Window

Wenn alte Nachrichten gekürzt werden, kann der Agent nicht mehr auf deren Inhalte zugreifen. Wenn ein Benutzer etwas anspricht, das er vor zehn Runden gefragt hat, hat der Agent keine Möglichkeit, diesen Kontext zu kennen. Dies ist eine grundlegende Einschränkung des Sliding-Window-Ansatzes.

Wenn älterer Gesprächsinhalt bewahrt werden muss, besteht eine Alternative darin, alte Nachrichten durch eine vom LLM generierte Zusammenfassung zu ersetzen, statt sie zu löschen. LangChain stellt dafür SummarizationMiddleware bereit, die wir in Teil V (ab Kapitel 15) behandeln werden, wenn wir uns mit Agent- und Graph-Architekturen befassen.