Combine graph and vector context

Graph RAG is perfect for structured facts and traversals. Vector RAG is better at recall over unstructured prose. A real service needs both. The hybrid endpoint runs both retrievers, then sends their outputs to the LLM with clear instructions on how to combine them.

Hybrid retrieval pipeline

Both retrievers run, then a synthesis call merges their outputs into one answer.

graph_rag.py
python
def ask_hybrid(self, question, top_k=5):
    graph_answer = self.ask(question)

    vector_context = []
    if self.vector_store is not None:
        try:
            vector_context = self.vector_store.query(question, top_k=top_k)
        except Exception as e:
            logger.warning("Vector lookup failed: %s", e)

    synthesized = self._synthesize(
        question=question,
        graph_answer=graph_answer.get("answer", ""),
        graph_context=graph_answer.get("graph_context"),
        vector_context=vector_context,
    )
    return {
        "question": question,
        "answer": synthesized,
        "cypher": graph_answer.get("cypher"),
        "graph_context": graph_answer.get("graph_context"),
        "vector_context": vector_context,
    }

The graph call is authoritative. The vector call is wrapped in a try so a flaky embedding or a missing collection never blocks the answer. The synthesis step receives both contexts and produces one grounded response, with full provenance returned to the caller.

graph_rag.py
python
def _synthesize(self, question, graph_answer, graph_context, vector_context):
    """Combine graph + vector evidence into one grounded answer."""
    llm = _build_chat_model()
    vector_block = "\n---\n".join(vector_context) if vector_context else "(no vector context)"
    prompt = (
        "You are answering using two complementary evidence sources.\n"
        "Prefer facts that appear in BOTH sources. If they conflict, trust the knowledge graph.\n"
        "If the answer is not supported, say you do not know.\n\n"
        f"Question: {question}\n\n"
        f"Knowledge graph answer:\n{graph_answer}\n\n"
        f"Knowledge graph context:\n{graph_context}\n\n"
        f"Vector context:\n{vector_block}\n\n"
        "Final answer:"
    )
    return llm.invoke(prompt).content

A few rules are encoded in the prompt. Prefer overlap, because facts that appear in both sources are stronger than facts in one. Trust the graph on conflicts, because structured facts have cleaner provenance. Make the model say "do not know" explicitly, because that prevents hallucination when neither source helps.

Both sources started from the same text, but they get there differently. The graph passed through structured extraction with schema enforcement, so it only contains facts the LLM was confident enough to commit to specific labels and relationships. The vector store contains raw chunks, so it might include speculative or hedging language. On a conflict, the more committed source is usually more reliable. That is why the prompt treats the graph as authoritative.

Quiz: Quiz

Loading practice…