Cookbook: Multi-Turn Conversations
On this page
How multi-turn support actually works
Recall from Concepts and models: ConversationHistory.get_enriched_query() only produces enrichment if a previous turn has both a non-empty query and a non-empty answer. ContextEngine.run() never sets answer on its own. This means out of the box, multi-turn enrichment is inactive unless you explicitly populate answer after each turn.
Recipe: a minimal working multi-turn loop
from zerda.context import ContextEngine, ContextEngineConfig
engine = ContextEngine(config=ContextEngineConfig())
engine.index(chunks=my_chunks, embed_fn=my_embed_fn)
def ask(query: str) -> str:
result = engine.run(query) # uses history if any turn has an answer
answer = call_my_llm(result.context, query)
# Required step: without this, get_enriched_query() will keep returning the bare query
history = engine.get_history()
if history and history.turns:
history.turns[-1].answer = answer
return answer
print(ask("ما هو RAG؟"))
print(ask("وكيف يختلف عن fine-tuning؟")) # now actually enriched with the previous Q&A
Recipe: inspecting what the engine actually saw
result = engine.run("follow-up question")
print(result.query_analysis.original_query)
# This is the ENRICHED query (with "[Previous conversation: ...]" wrapper if applicable),
# not the bare string you passed to run() — see api/engine.md's note on effective_query.
Recipe: resuming a session across processes
from zerda.context import ConversationHistory
import pickle
# Save (you must implement persistence yourself — there's no built-in serialization)
with open("session.pkl", "wb") as f:
pickle.dump(engine.get_history(), f)
# Resume in a new process
with open("session.pkl", "rb") as f:
saved_history = pickle.load(f)
new_engine = ContextEngine()
new_engine.set_conversation(saved_history)
ConversationHistory and ConversationTurn are plain @dataclass objects with no custom __reduce__/serialization logic, so standard pickle works as long as everything they reference (including any QueryAnalysis objects, which contain only primitives and enums) is itself picklable — which it is, by inspection.
Recipe: limiting how much history influences retrieval
from zerda.context import ConversationHistory
history = ConversationHistory(session_id="s1", max_turns=3) # only keep the last 3 turns
engine.set_conversation(history)
max_turns is enforced inside add_turn() — older turns are dropped automatically once the limit is exceeded.
Recipe: disabling multi-turn for a specific call
result = engine.run("a fully independent question", use_history=False, record_turn=False)
This bypasses both reading from and writing to history for this single call, without disabling multi-turn globally (config.multi_turn stays True for other calls).
Common pitfall: run_multi() and history
run_multi() forwards record_turn=False to every individual run() call internally — batched queries are never recorded into conversation history, regardless of what you pass to run_multi() itself (there's no record_turn parameter on run_multi() to override this).