LangChain ConversationalRetrievalChain Custom Prompt and Memory
To combine a LangChain ConversationalRetrievalChain custom prompt with memory, pass the memory object and put your QA prompt in combine_docs_chain_kwargs. For new code, use create_history_aware_retriever instead.
- ConversationalRetrievalChain uses two prompts: condense_question_prompt rewrites the follow-up, and the combine_docs prompt answers from context; you normally customise only the second.
- A stuff-chain QA prompt must contain exactly {context} and {question}, and memory_key must be 'chat_history' to match the condense prompt.
- RetrievalQA with memory retrieves badly on follow-ups like 'what about the second one?' because the raw follow-up is embedded.
- The current pattern is create_history_aware_retriever + create_stuff_documents_chain + create_retrieval_chain, with history passed explicitly or via RunnableWithMessageHistory.
- Key memory per session ID, otherwise every user shares one conversation.
Which chain should I use: RetrievalQA, ConversationalRetrievalChain or a history-aware chain?
The confusion comes from three generations of API that all look similar. The choice depends on whether you need follow-up questions to work and whether you are writing new code.
Short version: use the history-aware retrieval chain for anything new, keep ConversationalRetrievalChain only to maintain existing code, and use RetrievalQA only when every question stands alone.
| Approach | Memory | Follow-up handling | Status |
|---|---|---|---|
| RetrievalQA | None built in (can be bolted on) | Raw follow-up is embedded for retrieval, so it often retrieves the wrong chunks | Legacy |
| ConversationalRetrievalChain | ConversationBufferMemory or similar | Rewrites follow-up into a standalone question first | Legacy, deprecated |
| create_history_aware_retriever + create_retrieval_chain | Explicit chat_history, RunnableWithMessageHistory, or LangGraph checkpointer | Same rewrite step, but as a visible, testable component | Current recommended |
Why do I get memory or a custom prompt, but not both?
RetrievalQA is stateless: one query goes in, it retrieves, stuffs documents into a prompt, and returns an answer. It has no notion of earlier turns, so a custom prompt works but nothing is remembered.
ConversationalRetrievalChain adds a first step. An LLM call takes the chat history plus the new question and produces a standalone question, which is then used for retrieval. Only then does the answering step run.
That means two prompts exist, and people often edit the wrong one. The condense_question_prompt uses {chat_history} and {question} and only rewrites the query. The QA prompt (the combine-docs prompt) uses {context} and {question} and produces the visible answer. Your tone, language and 'only answer from context' rules belong in the QA prompt.
| Prompt | Variables | Job | Passed via |
|---|---|---|---|
| condense_question_prompt | {chat_history}, {question} | Turn a follow-up into a standalone question | condense_question_prompt= argument of from_llm |
| QA / combine-docs prompt | {context}, {question} | Answer using retrieved chunks | combine_docs_chain_kwargs={'prompt': ...} |
How do I add memory and a custom prompt to ConversationalRetrievalChain?
This is the legacy fix and it still works in versions that ship the old classes. In newer releases the legacy chains may live in the langchain-classic package, so adjust imports if you see ImportError.
The QA prompt must use exactly {context} and {question} for the default stuff chain. Other names raise a validation error about missing or extra input variables, or the context silently never reaches the model.
from langchain.chains import ConversationalRetrievalChain
from langchain.memory import ConversationBufferMemory
from langchain.prompts import PromptTemplate
QA_PROMPT = PromptTemplate(
input_variables=['context', 'question'],
template=(
'You are a support assistant. Answer ONLY from the context below. '
'If the answer is not in the context, say "I don\'t know" and do not guess.\n\n'
'Context:\n{context}\n\nQuestion: {question}\nAnswer:'
),
)
memory = ConversationBufferMemory(
memory_key='chat_history', # must match the condense prompt
return_messages=True,
output_key='answer', # needed when returning source documents
)
qa = ConversationalRetrievalChain.from_llm(
llm,
retriever=vectorstore.as_retriever(search_kwargs={'k': 4}),
memory=memory,
combine_docs_chain_kwargs={'prompt': QA_PROMPT},
return_source_documents=True,
verbose=True,
)
print(qa.invoke({'question': 'What is the refund window?'})['answer'])
print(qa.invoke({'question': 'And for digital items?'})['answer'])What is the current way: create_history_aware_retriever with a custom prompt?
The modern pattern splits the same two steps into explicit pieces. create_history_aware_retriever holds the rewrite prompt, create_stuff_documents_chain holds the QA prompt, and create_retrieval_chain joins them. Chat history is just a list of messages.
The QA prompt needs {context} (filled with documents) and {input} (the user message), plus a MessagesPlaceholder for history. The rewrite prompt needs a placeholder for history and {input}.
from langchain.chains import create_history_aware_retriever, create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
retriever = vectorstore.as_retriever(search_kwargs={'k': 4})
# 1) rewrite prompt: follow-up -> standalone question
condense_prompt = ChatPromptTemplate.from_messages([
('system', 'Given the chat history and the latest user message, rewrite it as a '
'standalone question. Do NOT answer it. Return it unchanged if already standalone.'),
MessagesPlaceholder('chat_history'),
('human', '{input}'),
])
history_aware_retriever = create_history_aware_retriever(llm, retriever, condense_prompt)
# 2) QA prompt: your custom system prompt
qa_prompt = ChatPromptTemplate.from_messages([
('system', 'Answer only from the context. If it is not there, say "I don\'t know".\n\n{context}'),
MessagesPlaceholder('chat_history'),
('human', '{input}'),
])
qa_chain = create_stuff_documents_chain(llm, qa_prompt)
rag = create_retrieval_chain(history_aware_retriever, qa_chain)
# 3) memory keyed by session
store = {}
def get_history(session_id: str):
return store.setdefault(session_id, InMemoryChatMessageHistory())
chat = RunnableWithMessageHistory(
rag, get_history,
input_messages_key='input',
history_messages_key='chat_history',
output_messages_key='answer',
)
cfg = {'configurable': {'session_id': 'user-42'}}
chat.invoke({'input': 'What is the refund window?'}, config=cfg)
res = chat.invoke({'input': 'And for digital items?'}, config=cfg)
print(res['answer'])Can I add memory to RetrievalQA, and what goes wrong?
Yes, mechanically. Put {history} in the prompt and pass the memory through chain_type_kwargs. The input_key must be 'question' so the memory knows which field is the user turn.
The catch is retrieval. RetrievalQA embeds the raw user text. A follow-up such as 'what about the second one?' has no searchable meaning on its own, so the retriever returns irrelevant chunks and the model, which can see the history but not the right documents, either hallucinates or says it does not know.
Memory in the prompt fixes how the answer reads, not what gets retrieved. Only a rewrite step fixes retrieval, which is exactly what ConversationalRetrievalChain and the history-aware retriever add.
from langchain.chains import RetrievalQA
from langchain.memory import ConversationBufferMemory
from langchain.prompts import PromptTemplate
prompt = PromptTemplate(
input_variables=['history', 'context', 'question'],
template='History:\n{history}\n\nContext:\n{context}\n\nQuestion: {question}\nAnswer:',
)
qa = RetrievalQA.from_chain_type(
llm, retriever=retriever,
chain_type_kwargs={
'prompt': prompt,
'memory': ConversationBufferMemory(input_key='question', memory_key='history'),
},
) What do the common errors mean and how do I fix them?
Most failures come from variable names and output keys, not from the model.
| Symptom | Cause | Fix |
|---|---|---|
| One output key expected, got dict_keys(['answer', 'source_documents']) | return_source_documents=True gives two outputs, and memory cannot choose which to save | Set output_key='answer' on the memory |
| Validation error about missing or extra input variables | QA prompt does not use {context} and {question} exactly | Rename variables to match the chain, and set input_variables accordingly |
| Memory has no effect, or KeyError on chat_history | memory_key differs from the placeholder name in the prompt | Use 'chat_history' everywhere |
| History is a string, not messages, in a chat prompt | return_messages left False | Set return_messages=True for message placeholders |
| Answers ignore documents | Context variable not in the prompt, or poor rewritten question | Check prompt text and print the standalone question |
| Users see each other's conversations | Single shared memory or store key | Key history by session or thread ID |
How should I choose a memory type and debug the final prompt?
Every stored turn is re-sent on each call, so cost and latency grow with conversation length, and eventually you hit the context window. Pick the memory that fits the conversation shape.
Buffer keeps everything: simplest and exact, but unbounded. Window keeps the last k turns (k of 3 to 6 is a common starting point): predictable cost, but loses early facts. Summary compresses old turns with an extra LLM call: bounded size, but adds cost and can drop details. In the modern API, windowing is just slicing the message list or using trim_messages before the call.
To debug, first print what the model actually sees. In the legacy chain set verbose=True. In the modern chain, attach a callback or use LangSmith tracing, or run the pieces separately.
- Invoke the history_aware_retriever alone with a sample history and a vague follow-up, and read the documents returned.
- Check that the rewritten standalone question names the subject explicitly; if it does not, tighten the condense prompt with 'do not answer, only rewrite'.
- Print the formatted QA prompt (qa_prompt.invoke({...})) and confirm {context} is populated.
- Open two sessions with different IDs and confirm neither sees the other's turns.
- Lower temperature to 0 for the rewrite step if the standalone questions drift.
Common questions
Should I customise condense_question_prompt or the QA prompt?
Customise the QA prompt for tone, format, language and refusal behaviour, because it produces the answer users read. Change condense_question_prompt only when rewritten questions are poor, for example when it loses product names or answers instead of rewriting.
Is ConversationBufferMemory deprecated?
Yes, the legacy memory classes and chains are deprecated in favour of passing messages explicitly, RunnableWithMessageHistory or LangGraph persistence. They still run in versions that include them, but new projects should not build on them.
Can I use a different, cheaper model for the rewrite step?
Yes. The rewrite step only needs to produce a short standalone question, so a small fast model works. Pass that model to create_history_aware_retriever and your stronger model to create_stuff_documents_chain.
How do I return source documents in the modern chain?
create_retrieval_chain already returns a context key containing the retrieved Document objects next to answer. With RunnableWithMessageHistory, keep output_messages_key='answer' so only the answer text is stored in history.
Why does my chatbot answer from general knowledge despite my prompt?
The instruction is probably too weak or placed after the context. State the rule in the system message, add the exact fallback sentence to use, and set temperature to 0. Also check that retrieval actually returned relevant chunks, since an empty context invites guessing.