Aller au contenu

Pipeline RAG

AKKO embarque un pipeline RAG (Retrieval-Augmented Generation) prêt à l'emploi qui reste 100 % à l'intérieur du cluster. Les embeddings sont calculés par nomic-embed-text via Ollama, stockés dans l'extension pgvector de PostgreSQL, puis récupérés par qwen2.5:3b servi au travers de LiteLLM.

Le notebook interactif se trouve dans notebooks/rag-pipeline-demo.ipynb, monté en lecture seule dans chaque pod JupyterHub.

Pourquoi RAG sur AKKO

  • Souverain — aucun appel à OpenAI, Pinecone, Weaviate Cloud ou Anthropic. Modèles et vecteurs ne quittent jamais le cluster.
  • Composable — les mêmes tables pgvector sont interrogeables depuis Trino (postgresql.akko.kb_chunks) et exposées aux agents via MCP.
  • Économiquenomic-embed-text est un modèle de 137 Mo qui embedde plusieurs milliers de chunks par minute en CPU.
  • Auditable — chaque appel d'embedding transite par LiteLLM, qui journalise l'appelant (X-User-Id) et le modèle utilisé.

Architecture

flowchart LR
    subgraph Ingest[Ingestion]
        DOC[Documents sources<br/>PDF, Markdown, HTML, export Confluence]
        CHUNK[Chunker LangChain<br/>RecursiveCharacterTextSplitter<br/>chunk_size=1000, overlap=200]
        EMB[Ollama<br/>nomic-embed-text<br/>768 dims]
        PG[(PostgreSQL<br/>pgvector · index HNSW)]
    end
    subgraph Query[Requête]
        Q[Question utilisateur]
        EMBQ[Ollama<br/>nomic-embed-text]
        SEARCH[pgvector<br/>cosinus top-k=5]
        PROMPT[Constructeur de prompt<br/>contexte + question]
        LLM[Ollama<br/>qwen2.5:3b]
        ANS[Réponse + citations]
    end
    DOC --> CHUNK --> EMB --> PG
    Q --> EMBQ --> SEARCH
    PG --> SEARCH
    SEARCH --> PROMPT
    Q --> PROMPT
    PROMPT --> LLM --> ANS

Modèle de stockage

Définition de la table PostgreSQL (provisionnée automatiquement par postgres-init, voir postgres/init/05-pgvector.sql) :

CREATE EXTENSION IF NOT EXISTS vector;

CREATE TABLE akko.kb_chunks (
  id            BIGSERIAL PRIMARY KEY,
  source        TEXT        NOT NULL,    -- ex. 'docs/handbook.pdf'
  chunk_index   INT         NOT NULL,
  content       TEXT        NOT NULL,
  tokens        INT         NOT NULL,
  embedding     vector(768) NOT NULL,
  metadata      JSONB       NOT NULL DEFAULT '{}'::jsonb,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Index HNSW pour une recherche cosinus sous-linéaire
CREATE INDEX kb_chunks_hnsw ON akko.kb_chunks
  USING hnsw (embedding vector_cosine_ops)
  WITH (m = 16, ef_construction = 64);

CREATE INDEX kb_chunks_source_idx ON akko.kb_chunks (source);

Accès via Trino : postgresql.akko.kb_chunks — même table, exposée par le connecteur PostgreSQL. Les colonnes vector sont promues en ARRAY<DOUBLE> dans Trino : akko_ai_similarity et akko_ai_search fonctionnent directement.

Pipeline d'ingestion

# notebooks/rag-pipeline-demo.ipynb — simplifié
from langchain_community.vectorstores import PGVector
from langchain_ollama import OllamaEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter

splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
emb      = OllamaEmbeddings(
    base_url=os.environ["OLLAMA_HOST"],   # http://akko-ollama:11434
    model="nomic-embed-text",
)

store = PGVector(
    collection_name="akko.kb_chunks",
    connection_string=os.environ["POSTGRES_URL"],
    embedding_function=emb,
)

for doc in documents:
    chunks = splitter.split_documents([doc])
    store.add_documents(chunks)

Chaque variable d'environnement est injectée par le spawner (jupyterhub_config.py). Aucun hostname en dur.

Pipeline de requête

from langchain_ollama import ChatOllama
from langchain.prompts import PromptTemplate

chat = ChatOllama(
    base_url=os.environ["OLLAMA_HOST"],
    model="qwen2.5:3b",
    temperature=0,
)

retriever = store.as_retriever(search_type="similarity", search_kwargs={"k": 5})
prompt = PromptTemplate.from_template(
    "Réponds à la question en utilisant uniquement le contexte ci-dessous. "
    "Cite les sources sous la forme [source:chunk_index].\n"
    "Contexte :\n{context}\n\nQuestion : {question}\nRéponse :"
)

chain = prompt | chat
docs  = retriever.invoke(question)
ctx   = "\n\n".join(f"[{d.metadata['source']}:{d.metadata['chunk_index']}] {d.page_content}" for d in docs)
ans   = chain.invoke({"context": ctx, "question": question})

Configuration

Valeurs Helm clés (helm/akko/values.yaml) :

akko-ollama:
  models:
    - name: qwen2.5-coder:7b
    - name: qwen2.5:3b
    - name: nomic-embed-text

akko-postgresql-data:
  extensions:
    - pgvector
    - postgis

akko-litellm:
  routes:
    - model_name: nomic-embed-text
      litellm_params:
        model: ollama/nomic-embed-text
        api_base: http://akko-ollama:11434
Variable (env notebook) Défaut Rôle
OLLAMA_HOST http://akko-ollama:11434 Ollama direct (contourne LiteLLM pour la vitesse)
LITELLM_HOST http://akko-litellm:4000 Chemin auditable (recommandé pour les jobs partagés)
POSTGRES_URL postgresql://alice:...@akko-postgresql-data:5432/akko PostgreSQL de données (pgvector)
RAG_EMBED_MODEL nomic-embed-text Modèle d'embedding
RAG_CHAT_MODEL qwen2.5:3b Modèle de génération
RAG_TOP_K 5 Top-k du retriever

RBAC

Rôle Insérer des chunks Interroger les chunks Créer une collection KB
akko-admin oui oui oui
akko-engineer oui oui oui
akko-analyst non (lecture seule) oui non
akko-user non oui (PII masquées) non
akko-viewer non oui (KB publiées uniquement) non

Tous les accès sont appliqués par OPA au-dessus des règles standard du connecteur PostgreSQL de Trino (voir Admin / RBAC). La colonne akko.kb_chunks.metadata peut contenir une clé project utilisée par les rowFilters pour restreindre la visibilité inter-équipes.

Notes de performance

  • Index HNSW — passe la recherche de O(n) à ~O(log n). Sur 1 M de chunks, le p95 reste sous 50 ms.
  • Embedding par lot — appeler store.add_documents([batch]) avec 32-64 chunks pour saturer Ollama sans exploser la mémoire.
  • Stratégie de ré-ingestion — supprimer par source, re-découper, ré-embedder. pgvector n'a pas de tombstones : un vacuum hebdomadaire tourne via pg_cron (akko-postgresql-data).
  • Préférer akko_ai_search en SQL — si la question est un littéral, le plugin Trino AI cache l'embedding de la requête par worker (voir IA / Fonctions Trino).

Points d'intégration

  • JupyterHubnotebooks/rag-pipeline-demo.ipynb est la référence.
  • ADEN — quand AKKO_RAG_ENABLED=true, ADEN appelle le retriever avant le prompt de génération SQL pour injecter du savoir métier (descriptions de tables, règles).
  • MCP Trino — les agents peuvent exécuter execute_query sur postgresql.akko.kb_chunks pour chercher des documents.
  • Superset — un dashboard dédié (AKKO RAG insights) trace les embeddings (UMAP) pour surveiller la dérive.

Dépannage

Symptôme Cause probable Correctif
could not find extension "vector" pgvector absent du cluster de données Vérifier les values akko-postgresql-data ; l'image akko-postgres:2026.03 embarque pgvector
Ingestion très lente (> 30 s / chunk) Modèle non chargé en mémoire Ollama kubectl exec -it deploy/akko-ollama -- ollama pull nomic-embed-text
HTTP 401 sur OllamaEmbeddings Notebook sans clé virtuelle LiteLLM Utiliser OLLAMA_HOST directement, ou définir LITELLM_KEY dans l'env du spawner
Les réponses répètent la question Chunks retournés non pertinents Augmenter RAG_TOP_K, ajuster chunk_size/overlap, ré-indexer
Reconstruction HNSW bloque les inserts CREATE INDEX sur une table peuplée Utiliser CREATE INDEX CONCURRENTLY ou créer l'index avant l'ingestion massive

Voir aussi