Skip to content

FCC RAG Pipeline Guide

Retrieval-Augmented Generation (RAG) in FCC is more than vector search against a document collection. It is a persona-aware, vocabulary-grounded retrieval pipeline that respects the federation namespace of the deployed project and feeds grounded context into the SimulationEngine. GraphRAG — the graph-native variant — is being staged for v1.4.0 and already has scaffolding in the codebase.

The reference code lives at src/fcc/rag/:

  • chunking.pyDocumentChunker with 6 strategies
  • retriever.pySemanticRetriever, RetrievalResult
  • pipeline.pyRAGPipeline with persona-aware prompt construction
  • integration.py — wiring to the simulation engine
  • unified_pipeline.py — federated retrieval across namespaces

Supporting modules:

  • src/fcc/search/SearchIndex, PersonaSearchIndex, ActionSearchIndex, EmbeddingProvider
  • src/fcc/knowledge/ — the knowledge graph the GraphRAG variant walks

The Pipeline

flowchart LR
    Docs[Document<br/>Corpus] --> C[DocumentChunker]
    C --> E[EmbeddingProvider]
    E --> I[SearchIndex]
    Q[Query +<br/>Persona ID] --> R[SemanticRetriever]
    I --> R
    V[Vocabulary<br/>Grounding] --> R
    R --> P[RAGPipeline]
    P --> Grounded[Grounded<br/>Response]

    classDef doc fill:#fff9c4,stroke:#f57f17;
    classDef fcc fill:#c8e6c9,stroke:#2e7d32;
    classDef out fill:#e3f2fd,stroke:#0d47a1;
    class Docs,V,Q doc;
    class C,E,I,R,P fcc;
    class Grounded out;

Chunking Strategies

DocumentChunker supports six strategies, chosen per corpus:

Strategy When to use
fixed_size Uniform text, predictable behaviour
sentence Prose documents with punctuation
paragraph Technical documentation
markdown_section Docs-as-code (FCC's own guides are ideal candidates)
code_block Source code corpora
semantic Long narratives where meaning crosses paragraph boundaries
from fcc.rag.chunking import DocumentChunker

chunker = DocumentChunker(strategy="markdown_section", chunk_size=1024, overlap=128)
chunks = chunker.chunk(text, metadata={"source": "ophiuchus/guides/patient_intake.md"})

For FCC's own documentation (Markdown), markdown_section preserves header context and produces well-scoped chunks.


Embedding Providers

The EmbeddingProvider protocol decouples the pipeline from any specific model:

from fcc.search.embeddings import EmbeddingProvider, MockEmbeddingProvider

# Default mock provider — 384-dim, deterministic, no network
provider = MockEmbeddingProvider()

# Or wrap sentence-transformers
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-small-en-v1.5")

class BGEProvider:
    def embed(self, texts: list[str]) -> list[list[float]]:
        return model.encode(texts, normalize_embeddings=True).tolist()
    def dimension(self) -> int:
        return 384

# Or delegate to a managed API via the AI provider plugin matrix

The mock provider is always available and gives byte-identical outputs — ideal for tests and for offline reproducibility.


Indexing a Corpus

from fcc.rag.chunking import DocumentChunker
from fcc.search.embeddings import MockEmbeddingProvider
from fcc.search.index import SearchIndex

chunker = DocumentChunker(strategy="markdown_section")
embeddings = MockEmbeddingProvider()
index = SearchIndex(embedding_provider=embeddings)

# Ingest one document
for chunk in chunker.chunk(text, metadata={"source": "ophiuchus/guide.md"}):
    index.add(chunk.text, metadata=chunk.metadata)

# Persist
index.save("/tmp/ophiuchus.idx")

The index format is portable; load it later without rebuilding.


Persona-Aware Retrieval

The defining feature of the FCC RAG pipeline is persona-aware retrieval — the query prompt is annotated with the persona's R.I.S.C.E.A.R. spec, and retrieval biases toward chunks that match the persona's expected inputs and constraints.

from fcc.rag.pipeline import RAGPipeline
from fcc.rag.retriever import SemanticRetriever
from fcc.personas.registry import PersonaRegistry
from fcc.search.embeddings import MockEmbeddingProvider

registry = PersonaRegistry.from_package_data()
retriever = SemanticRetriever(index, embedding_provider=MockEmbeddingProvider())
pipeline = RAGPipeline(retriever=retriever, persona_registry=registry)

result = pipeline.query(
    question="What are the prerequisites for discharge planning?",
    persona_id="OPH-DC",   # Ophiuchus Discharge Coordinator
)
print(result.answer)
for chunk in result.sources:
    print(f"  [{chunk.score:.3f}] {chunk.metadata['source']}")

Under the hood the pipeline:

  1. Looks up the persona spec from the registry
  2. Annotates the query with persona inputs / constraints / role
  3. Retrieves top-k chunks
  4. Re-ranks using persona-weight features
  5. Constructs a persona-style prompt and generates the answer

Vocabulary Grounding

Add a vocabulary grounding to filter and re-rank chunks against known concepts:

from fcc.rag.retriever import SemanticRetriever, VocabularyGrounding

grounding = VocabularyGrounding.from_namespace("ophiuchus")
retriever = SemanticRetriever(
    index,
    embedding_provider=MockEmbeddingProvider(),
    grounding=grounding,
)

With grounding active:

  • Chunks whose embedding does not intersect any vocabulary concept are down-weighted
  • Concept-bearing chunks are boosted
  • The answer prompt includes vocabulary-ID hints for the LLM

This is the mechanism by which a domain-specific RAG pipeline stays inside its domain rather than hallucinating cross-domain content.


Federated RAG

UnifiedPipeline queries across multiple namespaces in a single call:

from fcc.rag.unified_pipeline import UnifiedPipeline

unified = UnifiedPipeline.from_package_data()

# Query across ophiuchus + pyxis simultaneously (e.g. for healthcare-finance topics)
result = unified.query(
    question="How does HSA account reimbursement interact with diagnosis codes?",
    namespaces=["ophiuchus", "pyxis"],
    persona_id="FED-COORD",
)

The UnifiedPipeline walks the federation registry, dispatches sub-queries to each namespace's index, and re-ranks globally. Results carry a namespace attribute so you can weigh or filter per-domain.


GraphRAG (Roadmap in v1.4.0)

FCC already ships the knowledge graph (src/fcc/knowledge/) that GraphRAG walks. The staged pattern for v1.4.0:

  1. Index documents as before
  2. Parallel to the vector index, extract entities and link them to the knowledge graph
  3. At query time, walk the knowledge graph from query-relevant entities to collect contextual neighbours
  4. Retrieve chunks that mention either the query entities or their graph neighbours
  5. Re-rank by combined vector similarity + graph proximity

The knowledge-graph substrate is already in place: 9 node types, 9 edge types, RDF/OWL/SKOS/JSON-LD serializers, federated graph. The v1.4.0 work is wiring the retriever to walk it at query time.

Preview the graph today:

from fcc.knowledge.graph import KnowledgeGraph
from fcc.knowledge.builders import build_full_fcc_graph

graph = build_full_fcc_graph()
print(f"Nodes: {graph.node_count()}, edges: {graph.edge_count()}")
neighbours = graph.neighbours("OPH-DC", max_depth=2)

Evaluating the Pipeline

Wire a RAG evaluation into the CLEAR+ benchmark suite:

fcc evaluate rag \
    --index /tmp/ophiuchus.idx \
    --questions tests/fixtures/rag_questions.jsonl \
    --output /tmp/rag_eval.json

CLEAR+ measures seven dimensions: Cost, Latency, Efficacy, Assurance, Reliability, Coverage, Explainability. For RAG specifically, Efficacy and Explainability are the two to watch — a pipeline that retrieves fast but produces unverifiable answers is not usable.

Treat a drop in Efficacy > 5% as a regression and gate releases on the benchmark.


Integration with the Simulation Engine

Wire the RAG pipeline into the simulation engine so personas receive grounded context automatically:

from fcc.rag.integration import attach_rag_to_simulation
from fcc.simulation.engine import SimulationEngine

engine = SimulationEngine(registry=registry, mode="ai")
attach_rag_to_simulation(engine, pipeline)

engine.run(scenario)

Every action invocation now gets grounded context from the pipeline; the persona's prompt is prefixed with the retrieved chunks. See the integration demo notebook and the unified-pipeline demo.


Common Pitfalls

1. Forgetting grounding. A vanilla RAG pipeline against a domain corpus will hallucinate domain-adjacent content. Always wire a vocabulary grounding for production.

2. Mixing namespaces without federation metadata. Two chunks from ophiuchus and pyxis that mention "Account" are about different things. Use federated RAG or restrict the query to a single namespace.

3. Over-chunking. 128-token chunks lose context. 2048-token chunks dilute relevance. Start at 512-1024 and tune with the CLEAR+ benchmark.

4. Ignoring reproducibility. The mock provider and a deterministic seed give bit-identical output across runs. Use them in tests; use a real provider only at runtime.

5. Not measuring. Without a benchmark suite, you cannot tell whether a new chunk strategy helped or hurt. Wire CLEAR+ into CI.