LlamaIndex as_chat_engine() — mode selection matters more than tutorials suggest
A chat engine is a query engine plus conversation memory. LlamaIndex offers several chat modes, each with different failure characteristics on follow-up questions and multi-turn conversations.
Quick fix (TL;DR)
condense_plus_context for anything real, and (c) capping token_limit on memory to prevent context bloat.Real error messages you'll see
These are the exact strings returned by the LlamaIndex framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.
# User: "What is the refund policy?" # Bot: "Refunds within 30 days, minus shipping." # User: "What about for damaged items?" # Bot: "I don't have information about damaged items." # ← condense rewrote to "damaged items policy" losing "refund" context
# User asks about API v2 endpoints # Bot answers correctly using v2 docs # User asks about pagination — bot mixes v1 and v2 pagination # ← old retrieved chunks from v2 turn are still in context
# SimpleChatEngine ignores the index; pure LLM chat # useful for debug but easy to ship by accident
Reference
Chat mode comparison
| Mode | How it retrieves | Best for |
|---|---|---|
condense_question | Rewrites follow-up as standalone, then retrieves | Search-like chat where each turn is a query |
context | Retrieves on raw user message; stuffs context | Conversational RAG with follow-up context |
condense_plus_context | Best of both — rewrites AND retrieves fresh each turn | Recommended for most production apps |
simple | No retrieval; pure LLM chat | Debug / fallback / no-RAG baseline |
react | ReAct agent with query engine as tool | When retrieval is one of many actions |
openai | OpenAI function-calling agent with query engine as tool | OpenAI-specific tool orchestration |
best | Auto: openai on OpenAI models, react otherwise | When you want the framework to pick |
Root causes, ranked by frequency
Based on developer reports across LlamaIndex forums, GitHub issues, and Discord community during 2025–2026.
- 25%condense_question loses context. Condensation drops the previous topic; retrieval misses relevant chunks.
- 18%context mode keeps stale nodes in prompt. Prior-turn context bleeds into follow-up answers.
- 14%Memory not bounded. Conversation grows without limit; every turn pays for all prior context.
- 10%Wrong mode for use case. condense used for casual chat; context used for search-like queries.
- 8%System prompt lost in condense. condense rewrites lose the system-prompt persona.
- 7%ReAct chat mode + no tools. Wraps query engine unnecessarily; adds latency.
- 7%Streaming broken in condense mode. Rewrite step is not streaming; UI shows nothing.
- 11%Chat history not persisted. Every session starts fresh even though user expects continuity.
Fixes — copy-paste solutions
Use condense_plus_context for most production apps
condense_plus_context rewrites the question to be standalone (good for retrieval), keeps the full conversation for the LLM (good for answer quality). It is the safest default.
from llama_index.core.memory import ChatMemoryBuffer from llama_index.core import VectorStoreIndex # ... index built already ... # Bounded memory — prevents unbounded context growth memory = ChatMemoryBuffer.from_defaults(token_limit=4000) chat_engine = index.as_chat_engine( chat_mode="condense_plus_context", memory=memory, similarity_top_k=6, system_prompt=( "You are a helpful assistant with access to a knowledge base. " "When you can, cite the source document." ), verbose=True, # dev-time: see the rewrite + retrieval ) # --- Multi-turn conversation --- response = chat_engine.chat("What is the refund policy?") print(response) # Refunds within 30 days, minus shipping costs. [source: refund_policy.pdf] response = chat_engine.chat("What about damaged items?") print(response) # For damaged items, refunds are processed within 5 business days # and cover full shipping cost. [source: refund_policy.pdf, section 3.2] # Access chat history for msg in chat_engine.chat_history: print(f"{msg.role}: {msg.content[:80]}") # Reset when switching topics or on user request chat_engine.reset()
verbose=True flag prints the rewritten standalone question at each turn. Watch this in dev — bad rewrites are the biggest source of "why did the follow-up work poorly?" bugs.Bound memory and manage session state
Always set a token_limit on ChatMemoryBuffer. For production, persist chat history to disk / DB across sessions.
from llama_index.core.memory import ChatMemoryBuffer, ChatSummaryMemoryBuffer from llama_index.llms.openai import OpenAI from llama_index.core.llms import ChatMessage, MessageRole # --- Simple buffer with token limit --- memory = ChatMemoryBuffer.from_defaults(token_limit=3000) # --- Summary memory for very long conversations --- llm = OpenAI(model="gpt-4o-mini") summary_memory = ChatSummaryMemoryBuffer.from_defaults( token_limit=3000, summarize_prompt="Summarize the conversation so far, preserving all facts.", llm=llm, ) # --- Load prior conversation from your DB --- def load_chat_history(user_id: str, conversation_id: str) -> list[ChatMessage]: # Fetch from Postgres/Redis/etc. rows = fetch_history_rows(user_id, conversation_id) return [ChatMessage(role=r["role"], content=r["content"]) for r in rows] # --- Reconstruct the chat engine with loaded history --- prior_msgs = load_chat_history("u_alice", "c_20260803") memory = ChatMemoryBuffer.from_defaults( token_limit=3000, chat_history=prior_msgs, ) chat_engine = index.as_chat_engine( chat_mode="condense_plus_context", memory=memory, similarity_top_k=6, ) # Continue where the user left off response = chat_engine.chat("Can you elaborate on your last answer?") print(response) # --- Save updated history back after the turn --- def save_chat_history(user_id, conversation_id, messages): for msg in messages: upsert_row(user_id, conversation_id, msg.role, msg.content) save_chat_history("u_alice", "c_20260803", chat_engine.chat_history)
ChatSummaryMemoryBuffer which summarizes older turns automatically. Trades some fidelity for bounded cost — useful for support-agent style workloads.Debug bad follow-up handling by inspecting the rewrite
Enable verbose mode or call the internals directly to see what standalone question was generated. Adjust the condense prompt if needed.
from llama_index.core.chat_engine import CondensePlusContextChatEngine from llama_index.core.memory import ChatMemoryBuffer from llama_index.llms.openai import OpenAI llm = OpenAI(model="gpt-4o-mini") memory = ChatMemoryBuffer.from_defaults(token_limit=3000) # --- Custom condense prompt for better follow-up handling --- CUSTOM_CONDENSE_PROMPT = """ Given the following conversation and a follow-up question, rewrite the follow-up question to be a standalone question that captures all relevant context. Rules: 1. If the follow-up refers to something in prior turns (e.g. "what about X?"), INCLUDE the original topic in the rewrite. 2. If the follow-up is a fresh topic, use it as-is. 3. Preserve every named entity, product, and number mentioned in prior turns that's relevant to the follow-up. Chat history: {chat_history} Follow-up: {question} Standalone question: """ from llama_index.core.prompts import PromptTemplate chat_engine = CondensePlusContextChatEngine.from_defaults( retriever=index.as_retriever(similarity_top_k=6), llm=llm, memory=memory, condense_prompt=PromptTemplate(CUSTOM_CONDENSE_PROMPT), verbose=True, # prints the condensed question every turn ) # --- Follow-up handling test --- chat_engine.chat("What is the refund policy?") # Bot: Refunds within 30 days... response = chat_engine.chat("What about for damaged items?") # You will see the printed rewrite: # > "What is the refund policy for damaged items?" # instead of # > "What is the damaged items policy?" # --- Manually inspect the condensed question --- # For deeper debugging, subclass and log every step class DebugChatEngine(CondensePlusContextChatEngine): def _condense_question(self, chat_history, question): standalone = super()._condense_question(chat_history, question) print(f"[condense] {question!r} -> {standalone!r}") return standalone
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Use
condense_plus_contextas the default chat mode for production RAG chat. - Always set
token_limiton the memory buffer. - Persist chat history to a DB; do not rely on in-process memory.
- For very long conversations, use
ChatSummaryMemoryBuffer. - Enable
verbose=Truein dev to observe the rewrite step. - Test with representative follow-up patterns before shipping — pronouns, "what about X", "and Y?".
- Provide a "reset conversation" affordance to the user; call
chat_engine.reset().
Frequently asked questions
condense_question rewrites the follow-up into a standalone question, then retrieves. context retrieves on the raw follow-up and includes prior turns in the LLM prompt as history. condense_plus_context does both — usually the right default.chat_engine.stream_chat("...") — returns a StreamingAgentChatResponse. Iterate response.response_gen for chunks. Streaming works best with context mode; condense modes have a non-streaming rewrite step.response.source_nodes contains the retrieved nodes. Use for citation UI. Some modes (react agent) expose intermediate steps via response.sources.ChatMemoryBuffer is in-process; you persist it yourself. LangChain's RunnableWithMessageHistory pairs with pluggable backends (SQL, Redis) natively.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.