Python & AI Tutorials Logo
LangChain & LangGraph

11. Conversational RAG: Menambahkan Memori ke Retrieval

Di Bab 10 kita meningkatkan kualitas retrieval dari sistem RAG kita. Tapi masih ada satu keterbatasan: setiap pertanyaan diperlakukan secara individual. Ketika pengguna bertanya "Apa kebijakan pengembalian dana Anda?", sistem kita menemukan konten relevan dari dokumen dan menjawabnya. Ketika pertanyaan berikutnya masuk, sistem menjawabnya tanpa memori tentang percakapan sebelumnya.

Mari kita lihat mengapa ini menjadi masalah dalam percakapan nyata. Seorang pengguna bertanya "Apa kebijakan pengembalian dana Anda?" lalu menindaklanjutinya dengan "Apakah itu berlaku untuk produk digital juga?" Pertanyaan lanjutan ini mengasumsikan konteks "kebijakan pengembalian dana" dari giliran sebelumnya, tetapi teks pertanyaan itu sendiri tidak memuat informasi tersebut. Jika kita menggunakan "Apakah itu berlaku untuk produk digital juga?" secara langsung sebagai query pencarian, retriever akan menarik informasi yang tidak relevan terkait "produk digital" (misalnya, harga atau spesifikasi), dan sistem RAG akan menghasilkan jawaban yang tidak sesuai dengan maksud pengguna.

Di bab ini, kita akan belajar cara menyelesaikan masalah ini. Kita akan belajar untuk menulis ulang pertanyaan lanjutan yang ambigu menjadi pertanyaan lengkap, menggunakan teknik tersebut untuk membangun sistem conversational RAG, dan membahas cara mengelola riwayat percakapan seiring percakapan menjadi semakin panjang.

11.1) Menulis Ulang Pertanyaan Lanjutan menjadi Pertanyaan Lengkap

Seperti yang kita lihat di pendahuluan, pertanyaan lanjutan dibangun di atas konteks percakapan sebelumnya, sehingga orang cenderung menghilangkan banyak informasi. Akibatnya, pertanyaan lanjutan sering kali tidak lengkap dengan sendirinya. Bagaimana kita bisa menyelesaikan ini?

Di Bab 8 kita belajar cara membantu LLM memahami konteks percakapan dengan menyertakan riwayat percakapan bersama setiap pesan. Kita bisa menerapkan pendekatan yang sama di sini. Kita menyertakan pertanyaan lanjutan bersama riwayat percakapan ke LLM, dan memintanya untuk menulis ulangnya menjadi pertanyaan lengkap yang mencerminkan konteks. Misalnya, pertanyaan lanjutan "Apakah itu berlaku untuk produk digital juga?" ditulis ulang, bersama dengan riwayat percakapan, menjadi "Apakah produk digital memenuhi syarat untuk pengembalian dana?" Dengan pertanyaan yang ditulis ulang ini, pencarian dapat menemukan dokumen yang tepat tentang kebijakan pengembalian dana untuk produk digital.

Teknik ini disebut query rewriting. Mari kita buat system prompt untuknya.

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."
)

Instruksi inti dalam system prompt ini adalah "tulis ulang pertanyaan lanjutan menjadi pertanyaan lengkap menggunakan riwayat chat." Dua arahan spesifik penting di sini.

Pertama, "Do NOT answer the question, just reformulate it." Ini memberi tahu LLM untuk hanya menulis ulang pertanyaan, bukan menjawabnya. Tanpa arahan ini, LLM cenderung menjawab pertanyaan alih-alih menulisnya ulang. Yang kita inginkan di sini bukanlah jawaban, melainkan pertanyaan lengkap yang dapat dipahami tanpa riwayat chat.

Kedua, "otherwise return it as is." Ini memberi tahu LLM untuk membiarkan pertanyaan tidak berubah jika tidak perlu ditulis ulang. Tanpa ini, LLM mungkin merangkai ulang pertanyaan secara tidak perlu, yang berpotensi mengubah makna atau cakupan aslinya.

Sekarang mari kita gunakan system prompt ini untuk benar-benar menulis ulang pertanyaan lanjutan.

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

Output:

Apakah produk digital memenuhi syarat untuk pengembalian dana?

LLM membaca riwayat percakapan, mengenali bahwa pertanyaan itu tentang "kebijakan pengembalian dana," dan menulis ulangnya menjadi pertanyaan lengkap. Mencari dengan pertanyaan yang ditulis ulang ini akan mengembalikan dokumen yang sesuai dengan maksud pengguna.

Di bagian berikutnya, kita akan mengintegrasikan langkah penulisan ulang ini ke dalam pipeline RAG sehingga penulisan ulang, retrieval, dan pembuatan jawaban semuanya terjadi dalam satu panggilan.

11.2) Membangun Conversational RAG

Di bagian sebelumnya kita belajar cara menulis ulang pertanyaan lanjutan menjadi pertanyaan lengkap dengan menyertakan riwayat percakapan ke LLM. Sekarang kita akan mengintegrasikan langkah penulisan ulang ini ke dalam pipeline RAG untuk membangun conversational RAG di mana penulisan ulang → retrieval → pembuatan jawaban semuanya terjadi dalam satu panggilan.

LangChain menyediakan utilitas chain untuk membangun conversational RAG (create_history_aware_retriever, create_retrieval_chain, dll.), tetapi fungsi-fungsi ini ada di paket langchain-classic, yang akan berakhir masa dukungannya pada Desember 2026. Dokumentasi resmi LangChain kini merekomendasikan penggunaan agen sebagai gantinya.

Oleh karena itu, kita akan menggunakan agen untuk mengimplementasikan conversational RAG di bab ini. Agen dibahas secara rinci di Bagian V (Bab 15–17), jadi di sini kita hanya akan memperkenalkan apa yang dibutuhkan untuk implementasi conversational RAG kita.

11.2.1) Komponen Agen yang Akan Kita Gunakan di Sini

Di Bab 5 kita mendapat gambaran singkat tentang konsep inti agen. Ketika LLM menganalisis permintaan pengguna dan memutuskan tool mana yang akan digunakan, sistem mengeksekusi keputusan tersebut. Saat itu kita mengimplementasikan proses ini secara manual, tetapi LangChain menyediakan API yang membuatnya jauh lebih sederhana. Berikut perkenalan singkat tentang tiga komponen yang akan kita gunakan.

@tool: Sebuah decorator yang mengubah fungsi Python biasa menjadi tool yang dapat digunakan agen. Agen secara otonom memilih dan memanggil tool yang sesuai dari tool yang terdaftar berdasarkan permintaan pengguna.

create_agent: Sebuah fungsi yang menerima LLM, daftar tool, dan system prompt untuk membuat agen. Fungsi ini menangani alur keputusan-eksekusi agen secara internal.

InMemorySaver: Sebuah checkpointer yang mengelola riwayat percakapan secara otomatis. Ia mengorganisir percakapan berdasarkan thread_id, sehingga ketika agen dipanggil dengan thread_id yang sama, ia secara otomatis memuat riwayat percakapan sebelumnya.

11.2.2) Membuat Tool Retrieval

Pertama, mari kita ubah pencarian penyimpanan vektor yang kita bangun di Bab 10 menjadi tool yang dapat digunakan agen.

python
from langchain.tools import tool
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
 
# Hubungkan ke penyimpanan vektor yang dibangun di Bab 10
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):
    """Cari dokumen untuk konten yang relevan dengan query."""
    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

Decorator @tool mengubah fungsi retrieve_context menjadi tool yang dapat digunakan agen. Agen secara otonom memutuskan apakah akan memanggil tool ini berdasarkan pertanyaan pengguna.

11.2.3) Membuat Agen

Kita menyertakan tool retrieval, system prompt, dan checkpointer ke create_agent untuk membuat agen.

python
from langchain.agents import create_agent
from langgraph.checkpoint.memory import InMemorySaver  # Terinstal otomatis bersama langchain
 
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: LLM yang akan digunakan agen.
  • tools: Daftar tool yang tersedia untuk agen. Kita mendaftarkan tool retrieval dokumen (retrieve_context) yang kita buat di atas.
  • system_prompt: Instruksi perilaku agen. Ia memberi tahu agen untuk menggunakan tool retrieval guna menjawab pertanyaan tentang kebijakan perusahaan, dan untuk mengatakan tidak tahu ketika konteks yang diambil tidak memuat informasi relevan.
  • checkpointer: Mengelola riwayat percakapan secara otomatis. InMemorySaver() menyimpan percakapan di memori, secara otomatis menangani riwayat percakapan yang kita kelola secara manual di Bab 8.

Ya

Tidak

Pertanyaan Pengguna

Agen

Riwayat Percakapan
(InMemorySaver)

Panggil tool?

Eksekusi tool
retrieve_context

Jawaban Akhir

Ketika agen menerima pertanyaan pengguna, ia berkonsultasi dengan riwayat percakapan dan memutuskan apakah pencarian dokumen di penyimpanan vektor diperlukan. Jika ya, ia memanggil tool retrieve_context untuk mengambil dokumen relevan dan menghasilkan jawaban melalui LLM. Riwayat percakapan dikelola secara otomatis oleh InMemorySaver.

11.2.4) Menjalankan Percakapan Multi-turn

Mari kita jalankan percakapan dua giliran yang sebenarnya untuk memverifikasi bahwa ia menangani pertanyaan lanjutan dengan benar.

python
# thread_id adalah identifier yang membedakan percakapan
# Menggunakan thread_id yang sama melanjutkan percakapan yang sama
thread_config = {"configurable": {"thread_id": "1"}}
 
# --- Giliran 1: Pertanyaan lengkap ---
response1 = agent.invoke(
    {"messages": [{"role": "user", "content": "What is your refund policy?"}]},
    thread_config,
)
print("Q: What is your refund policy?")
print("A:", response1["messages"][-1].content)
 
# --- Giliran 2: Pertanyaan lanjutan yang bergantung pada Giliran 1 ---
response2 = agent.invoke(
    {"messages": [{"role": "user", "content": "Does that apply to digital products too?"}]},
    thread_config,
)
print("\nQ: Does that apply to digital products too?")
print("A:", response2["messages"][-1].content)

Output:

Q: What is your refund policy?
A: All physical products may be returned within 30 days of purchase for a full refund.
The original receipt or order confirmation email is required, and items must be in
their original packaging and unused condition.
After 30 days, returns are accepted for store credit only.
 
Q: Does that apply to digital products too?
A: Digital products (software licenses, e-books, online courses) are non-refundable
once the download or access link has been activated.
However, if you experience technical issues preventing access, you can contact support
within 7 days for a replacement or refund.

Pada giliran kedua, kita menyertakan "Does that apply to digital products too?" tetapi agen mengenali dari riwayat percakapan bahwa ini adalah pertanyaan lanjutan tentang kebijakan pengembalian dana, dan secara akurat mengambil bagian produk digital dari dokumen kebijakan pengembalian dana.

Tunggu — untuk agen ini, kita tidak menambahkan langkah query rewriting seperti pada Bagian 11.1. Lalu, bagaimana pertanyaan lanjutan dapat diproses dengan benar? Ketika LLM memanggil sebuah tool (fungsi yang diberi dekorator @tool), LLM menghasilkan argumen tool tersebut secara mandiri. Ini termasuk kueri pengguna yang diteruskan ke retrieve_context — karena LLM memiliki seluruh riwayat percakapan, LLM mengubah pertanyaan lanjutan menjadi pertanyaan lengkap dan mandiri sebelum melakukan pemanggilan. Kita tidak menyiapkan langkah rewriting khusus, namun query rewriting tetap terjadi sebagai bagian dari proses pemanggilan tool.

Perhatikan juga bahwa kita tidak perlu mengelola riwayat percakapan secara manual — InMemorySaver mengelolanya secara otomatis per thread_id.

Bagian berikutnya membahas masalah yang muncul seiring percakapan menjadi lebih panjang dan riwayatnya membesar, beserta cara menyelesaikannya.

11.3) Mengelola Percakapan yang Lebih Panjang

Conversational RAG yang kita bangun bekerja dengan baik pada awalnya, tetapi masalah bisa muncul seiring percakapan menjadi lebih panjang. Seperti yang kita pelajari di Bab 8, LLM memiliki ukuran input maksimum yang dapat mereka proses dalam satu panggilan. System prompt, riwayat percakapan, dokumen yang diambil, dan pertanyaan pengguna semuanya harus muat dalam batas ini.

Seiring percakapan menjadi lebih panjang, riwayat percakapan memakan lebih banyak token, yang pada akhirnya melebihi ukuran input maksimum dan menyebabkan panggilan API gagal. Biaya juga meningkat dengan setiap panggilan karena Anda ditagih per token. Ini berarti kita perlu mengelola ukuran riwayat percakapan kita.

Di Bab 8 kita menyelesaikan masalah ini dengan sliding window: menyimpan hanya N pesan terbaru dan membuang yang lebih lama. Konsep yang sama berlaku di lingkungan agen. create_agent mendukung middleware, yaitu langkah pemrosesan yang dapat memodifikasi pesan sebelum LLM dipanggil. Kita dapat menggunakan middleware untuk memangkas riwayat lama.

11.3.1) Membatasi Riwayat dengan Middleware

Decorator @before_model bekerja serupa dengan decorator @tool yang kita lihat di Bagian 11.2. Sama seperti @tool mengubah fungsi menjadi tool yang dapat digunakan agen, @before_model mengubah fungsi menjadi middleware yang berjalan sebelum setiap panggilan LLM. Middleware yang sudah dikonversi diaktifkan dengan mendaftarkannya di parameter middleware dari create_agent.

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:
    """Hapus pesan lama sebelum setiap panggilan LLM."""
    messages = state["messages"]
    # Jika pesannya cukup sedikit, jangan lakukan apa pun
    if len(messages) <= 10:
        return None
    # Simpan hanya pesan system (pertama) dan 10 pesan terbaru
    return {
        "messages": [
            RemoveMessage(id=REMOVE_ALL_MESSAGES),
            messages[0],     # Pesan system
            *messages[-10:], # 10 pesan terakhir (5 giliran)
        ]
    }

AgentState adalah objek yang menyimpan data state agen, dengan state["messages"] berisi daftar pesan percakapan sejauh ini. Nilai kembalian middleware menentukan bagaimana daftar percakapan ini dimodifikasi.

  • Mengembalikan None membiarkan data state agen yang ada tidak berubah.
  • Mengembalikan dictionary menerapkan isinya ke daftar pesan yang ada. Pada kode di atas, RemoveMessage(id=REMOVE_ALL_MESSAGES) pertama-tama menghapus semua pesan yang ada, lalu menambahkan kembali hanya pesan system dan 10 pesan terbaru. Akibatnya, hanya pesan-pesan ini yang disertakan ke LLM.

Daftarkan middleware ini dengan agen:

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],  # Daftarkan middleware
)

Ini adalah agen yang sama dari Bagian 11.2 dengan tambahan middleware=[trim_old_messages]. Sekarang, sepanjang apa pun percakapannya, hanya pesan terbaru yang disertakan ke LLM.

11.3.2) Trade-off Sliding Window

Ketika pesan lama dipangkas, agen tidak lagi dapat merujuk ke kontennya. Jika pengguna mengungkit sesuatu yang mereka tanyakan sepuluh giliran yang lalu, agen tidak memiliki cara untuk mengetahui konteks tersebut. Ini adalah keterbatasan fundamental dari pendekatan sliding window.

Ketika konten percakapan yang lebih lama perlu dipertahankan, alternatifnya adalah mengganti pesan lama dengan ringkasan yang dihasilkan LLM alih-alih menghapusnya. LangChain menyediakan SummarizationMiddleware untuk tujuan ini, yang akan kita bahas di Bagian V (mulai Bab 15) ketika kita mendalami arsitektur agen dan graph.