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

Python · 18 lines
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

Python · 4 lines
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

Python · 13 lines
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

Python · 4 lines
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

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

See also

Related pages