n4nAI

LlamaIndex SimpleChatEngine vs ContextChatEngine

Technical comparison of LlamaIndex SimpleChatEngine and ContextChatEngine — capabilities, trade-offs, and when to use each.

n4n Team6 min read1,264 words

Audio narration

Coming soon — every post will get a voice note here.

If you’re evaluating llamaindex simplechatengine vs contextchatengine for a production system, the decision comes down to whether you need conversation history to influence retrieval. SimpleChatEngine treats every turn as independent; ContextChatEngine rewrites the user’s latest message using chat history before querying your index. That single architectural difference cascades into latency, token cost, retrieval quality, and failure modes. Here’s the breakdown.

Core architecture

SimpleChatEngine is a thin wrapper around an LLM call with a system prompt. It maintains an in-memory ChatMemoryBuffer (or whatever memory implementation you inject) and stuffs the last N messages into the context window alongside your system prompt. No index, no retriever, no reranking. The prompt template looks roughly like:

from llama_index.core.chat_engine import SimpleChatEngine
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)

engine = SimpleChatEngine.from_defaults(
    llm=llm,
    memory=memory,
    system_prompt="You are a helpful assistant with access to internal docs.",
)

ContextChatEngine adds a retrieval step. Before calling the LLM, it takes the user’s latest message plus recent history, runs a condense question prompt through the LLM to produce a standalone query, hits your vector index (or any BaseRetriever), fetches top-k nodes, then feeds those nodes plus the full conversation history into a final response synthesis prompt.

from llama_index.core.chat_engine import ContextChatEngine
from llama_index.core import VectorStoreIndex
from llama_index.core.memory import ChatMemoryBuffer
from llama_index.llms.openai import OpenAI

llm = OpenAI(model="gpt-4o-mini")
index = VectorStoreIndex.from_documents(docs)
memory = ChatMemoryBuffer.from_defaults(token_limit=3000)

engine = ContextChatEngine.from_defaults(
    llm=llm,
    retriever=index.as_retriever(similarity_top_k=4),
    memory=memory,
    system_prompt="You are a support agent. Use the retrieved context to answer.",
)

The condense prompt is the critical piece. Default template:

Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.

Chat History:
{chat_history}

Follow Up Input: {question}
Standalone question:

If the user asks “What about pricing?” after discussing Enterprise tiers, the condense step rewrites it to “What is the pricing for Enterprise tiers?” — which your retriever can actually match.

Retrieval quality and hallucination surface

SimpleChatEngine has zero retrieval. It hallucinates freely unless your system prompt and context window happen to contain the right facts. Use it only when:

  • The model’s parametric knowledge is sufficient (general coding help, creative writing, reasoning tasks)
  • You’re prototyping and haven’t built an index yet
  • You explicitly want the model to not ground in external data

ContextChatEngine grounds responses in retrieved nodes. The synthesis prompt receives {context_str} (concatenated node text) and {chat_history}. This reduces hallucination on domain-specific questions but introduces new failure modes:

  • Condense drift: If the condense LLM misinterprets a vague follow-up (“and the other one?”), the rewritten query retrieves irrelevant chunks.
  • Context overflow: Retrieved nodes + full history + system prompt can exceed the model’s context window. LlamaIndex truncates history first, then nodes, but you lose either conversation continuity or grounding.
  • Stale retrieval: The condense step uses the same LLM as synthesis. If that model is weak at query rewriting (e.g., a small local model), retrieval quality tanks.

Latency and token economics

Dimension SimpleChatEngine ContextChatEngine
LLM calls per turn 1 2 (condense + synthesis)
Input tokens per turn system + history + user condense(history + user) + synthesis(system + history + retrieved)
Output tokens per turn response only condense query + response
Retrieval latency 0 vector search + optional rerank
Typical p50 latency (gpt-4o-mini, 4k context) ~400ms ~900-1400ms
Cost per 1k turns (est., gpt-4o-mini pricing) ~$0.15 ~$0.45-0.65

The second LLM call dominates. If you’re serving user-facing chat with SLAs under 1s, SimpleChatEngine is safer. ContextChatEngine can be optimized:

  • Use a smaller/faster model for condense (e.g., gpt-4o-mini for condense, gpt-4o for synthesis)
  • Cache condensed queries for repeated follow-ups
  • Lower similarity_top_k to 2-3 if your chunks are dense
  • Disable condense entirely for single-turn queries (detect via history length)
# Custom condense with a cheaper model
from llama_index.core.chat_engine import CondenseQuestionChatEngine
from llama_index.llms.openai import OpenAI

cheap_llm = OpenAI(model="gpt-4o-mini")
expensive_llm = OpenAI(model="gpt-4o")

engine = CondenseQuestionChatEngine.from_defaults(
    llm=expensive_llm,           # synthesis
    condense_llm=cheap_llm,      # query rewriting
    retriever=index.as_retriever(similarity_top_k=3),
    memory=memory,
)

Note: ContextChatEngine is an alias for CondenseQuestionChatEngine in current LlamaIndex versions. The class hierarchy is BaseChatEngine → CondenseQuestionChatEngine → ContextChatEngine (deprecated alias). Use CondenseQuestionChatEngine directly for the condense_llm parameter.

Memory and conversation continuity

Both engines use ChatMemoryBuffer by default, which implements a token-aware sliding window. You can swap in ChatSummaryMemoryBuffer to summarize older turns instead of dropping them:

from llama_index.core.memory import ChatSummaryMemoryBuffer

memory = ChatSummaryMemoryBuffer.from_defaults(
    llm=llm,
    token_limit=4000,
    summary_token_limit=500,
)

With SimpleChatEngine, summarization preserves high-level context but loses detail — fine for chit-chat, bad for multi-step debugging where the user references “that error from three messages ago.”

With ContextChatEngine, memory interacts with retrieval in subtle ways. The condense prompt sees the full memory buffer (summarized or raw). If you use ChatSummaryMemoryBuffer, the condense LLM receives a summary of early turns plus raw recent turns. This usually works well, but verify your condense prompt handles summarized history gracefully. The default template doesn’t distinguish; you may want a custom template:

from llama_index.core.prompts import PromptTemplate

custom_condense = PromptTemplate(
    "Conversation summary (older turns):\n{summary}\n\n"
    "Recent conversation:\n{recent_history}\n\n"
    "Follow Up Input: {question}\n"
    "Standalone question:"
)

engine = CondenseQuestionChatEngine.from_defaults(
    llm=llm,
    retriever=retriever,
    memory=memory,
    condense_prompt=custom_condense,
)

Streaming and UX

Both engines support stream_chat and astream_chat. The difference is what streams:

  • SimpleChatEngine streams the final response token-by-token. Clean UX.
  • ContextChatEngine streams only the synthesis step. The condense + retrieval phase blocks before the first token appears. Users see a 500-1500ms blank pause, then streaming starts.

If that pause is unacceptable, you have two options:

  1. Show a “thinking…” indicator during the condense+retrieve phase (easy, honest)
  2. Run condense+retrieve speculatively while the user types (hard, requires frontend cooperation)
# Async streaming with progress callback
from llama_index.core.callbacks import CallbackManager, LlamaDebugHandler

debug = LlamaDebugHandler()
callback_manager = CallbackManager([debug])

engine = ContextChatEngine.from_defaults(
    llm=llm,
    retriever=retriever,
    memory=memory,
    callback_manager=callback_manager,
)

# In your handler, inspect debug.get_llm_inputs_outputs() to time each phase
response = await engine.astream_chat("What's the refund policy?")
async for token in response.async_response_gen():
    print(token, end="", flush=True)

Customization surface

SimpleChatEngine exposes:

  • system_prompt
  • memory implementation
  • prompt_template (the final prompt fed to LLM)
  • llm

ContextChatEngine adds:

  • retriever (any BaseRetriever — vector, BM25, hybrid, kg, custom)
  • condense_prompt / condense_llm
  • response_synthesis_mode (compact, refine, tree_summarize, simple_summarize)
  • node_postprocessors (rerankers, filters, metadata extraction)
  • verbose logging for debugging retrieval

The response_synthesis_mode matters when retrieved context exceeds the synthesis window. compact (default) stuffs as many nodes as fit, then calls LLM once. refine iterates: answer with first node, then refine with each additional node — slower but handles arbitrary context length. tree_summarize builds a hierarchical summary. Choose based on your chunk size and token budget.

from llama_index.core.postprocessor import LLMRerank

reranker = LLMRerank(
    choice_batch_size=5,
    top_n=2,
    llm=llm,
)

engine = ContextChatEngine.from_defaults(
    llm=llm,
    retriever=index.as_retriever(similarity_top_k=10),
    node_postprocessors=[reranker],
    response_synthesis_mode="compact",
    memory=memory,
)

Failure modes and debugging

SimpleChatEngine fails quietly: confident wrong answers. Add verbose=True to see the exact prompt sent to the LLM.

ContextChatEngine fails visibly but opaquely. Common issues:

Symptom Likely cause Fix
Irrelevant retrieval Condense prompt too generic Custom condense prompt with domain examples
“I don’t know” despite relevant docs Synthesis prompt ignores context Check response_synthesis_mode; verify context_str in prompt
Truncated answers mid-sentence Context window exceeded Reduce similarity_top_k, shorten chunks, increase token_limit
Condense hallucinates entities Weak condense LLM Use stronger condense_llm or few-shot condense prompt
Latency spikes Vector index cold start / large top-k Warm index, lower similarity_top_k, add caching

Enable verbose=True and callback_manager with LlamaDebugHandler to trace every LLM call, retrieval, and prompt. Log the condensed query and retrieved node IDs for every turn — this is the single most valuable debug artifact.

from llama_index.core.callbacks import LlamaDebugHandler, CallbackManager

debug = LlamaDebugHandler()
callback_manager = CallbackManager([debug])

engine = ContextChatEngine.from_defaults(
    llm=llm,
    retriever=retriever,
    memory=memory,
    callback_manager=callback_manager,
    verbose=True,
)

response = engine.chat("How do I reset my API key?")

# After the call:
for event in debug.get_llm_inputs_outputs():
    print(f"Prompt: {event.prompt}")
    print(f"Response: {event.response}")
    print("---")

# Retrieve the condensed query and nodes
condense_events = [e for e in debug.get_llm_inputs_outputs() if "standalone question" in e.prompt.lower()]
print("Condensed query:", condense_events[0].response if condense_events else "N/A")

When to reach for something else

Both engines are single-turn reasoning loops. They don’t support:

  • Tool use / function calling — use OpenAIAgent or ReActAgent
  • Multi-step planning — use ReActAgent or FunctionCallingAgent
  • Structured output enforcement — use OpenAIAgent with Pydantic output parser
  • Parallel retrieval across multiple indexes — build a custom retriever or use RouterRetriever

If your use case is “chat over docs” with occasional tool calls (lookup order status, create ticket), start with OpenAIAgent and give it a QueryEngineTool wrapping your index. The agent decides when to retrieve vs. when to call tools. SimpleChatEngine and ContextChatEngine are retrieval-only; they cannot act.

Which to choose

SimpleChatEngine when:

  • Parametric knowledge is enough (coding assistant, brainstorming, general Q&A)
  • You need sub-500ms p99 latency
  • You’re building a prototype and haven’t indexed data yet
  • Conversation history is purely for tone/context, not factual grounding
  • Cost per conversation must stay minimal

ContextChatEngine (CondenseQuestionChatEngine) when:

  • Users ask follow-ups that require retrieval (“What about the Enterprise plan?” after discussing pricing)
  • Hallucination on domain facts is unacceptable (support, legal, medical, internal tools)
  • You have a quality vector index and retriever already
  • You can tolerate 1-2s latency for the first token
  • You need reranking, hybrid search, or metadata filtering on retrieved context

Neither when:

  • You need tool use, structured output, or multi-step reasoning → OpenAIAgent / ReActAgent
  • You need to route across multiple indexes conditionally → custom router + QueryEngineTool
  • You’re building a RAG pipeline with complex post-processing (citation extraction, conflict detection) → build a custom QueryEngine and wrap in an agent

The llamaindex simplechatengine vs contextchatengine decision is fundamentally a retrieval question. If the user’s next question depends on something you must look up, use ContextChatEngine. If the model can answer from its weights plus a system prompt, SimpleChatEngine is faster, cheaper, and simpler. Don’t add retrieval infrastructure until you have a measured need for it.

Tagsllamaindexchat-enginecomparisoncontext

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All llamaindex chat engines & memory posts →