Funk-lab/ansible/roles/rag/files/rag-query
alkatrazz 0d4dfcc3f5 feat(rag): embedder dédié nomic-embed-text (768) au lieu de qwen3-8b
funk-docs utilisait le modèle de CHAT qwen3-8b (:1234, 4096 dim) comme
embedder → similarités quasi indiscernables (tous les scores ~0.96, ranking
médiocre). Bascule sur l'instance dédiée nomic-embed-text (:1238, 768 dim) —
la même que la mémoire STT — déjà identifiée comme roadmap dans le README.

rag-ingest ET rag-query alignés (même modèle/dim). Seuil rag-query abaissé
0.60→0.40 (nomic étale les scores plus bas). Collection recréée en 768.

Mesure : scores désormais étalés 0.61-0.74, et les bons docs ressortent en
tête (dnsmasq→dnsmasq.md, nftables→phase gateway, wedge→llama_server.md).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 21:29:38 +02:00

79 lines
2.4 KiB
Python

#!/usr/bin/env python3
"""
Interroge la base vectorielle Qdrant avec une question en langage naturel.
Usage: rag-query "ma question" [--top N]
Retourne les passages de documentation les plus pertinents.
"""
import sys
import json
import os
import urllib.request
QDRANT_URL = os.environ.get("QDRANT_URL", "http://127.0.0.1:6333")
# Doit matcher rag-ingest : embedder dédié nomic-embed-text (gpu-01 :1238, dim 768).
EMBED_URL = os.environ.get("EMBED_URL", "http://192.168.10.20:1238/v1/embeddings")
EMBED_MODEL = os.environ.get("EMBED_MODEL", "nomic-embed-text")
COLLECTION = os.environ.get("RAG_COLLECTION", "funk-docs")
# nomic étale les scores cosinus plus bas que qwen3-8b (qui saturait ~0.96) → seuil
# plus permissif pour ne pas écarter de bons passages. Surchargeable via RAG_MIN_SCORE.
MIN_SCORE = float(os.environ.get("RAG_MIN_SCORE", "0.40"))
def _post(url, data):
req = urllib.request.Request(
url,
data=json.dumps(data).encode(),
headers={"Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read())
def embed(text):
result = _post(EMBED_URL, {"model": EMBED_MODEL, "input": text})
return result["data"][0]["embedding"]
def search(question, top=5):
vector = embed(question)
result = _post(f"{QDRANT_URL}/collections/{COLLECTION}/points/search", {
"vector": vector,
"limit": top,
"with_payload": True,
"score_threshold": MIN_SCORE,
})
return result["result"]
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: rag-query \"question\" [--top N]", file=sys.stderr)
sys.exit(1)
question = sys.argv[1]
top = 5
if "--top" in sys.argv:
idx = sys.argv.index("--top")
try:
top = int(sys.argv[idx + 1])
except (IndexError, ValueError):
pass
try:
results = search(question, top)
except Exception as e:
print(f"ERREUR: {e}", file=sys.stderr)
sys.exit(1)
if not results:
print(f"Aucun résultat pertinent pour : {question}")
sys.exit(0)
print(f"=== Contexte RAG — {len(results)} résultat(s) pour : {question} ===\n")
for i, r in enumerate(results, 1):
p = r["payload"]
score = r["score"]
print(f"--- [{i}] {p['file']} § {p['section']} (score: {score:.3f}) ---")
print(p["text"][:700])
print()