Wprowadzenie
Czas złożyć wszystko w całość. Z części pierwszej mamy teorię (chunki, embeddingi, indeks), z drugiej — klocki LlamaIndex (Document, Settings, VectorStoreIndex, query engine). Dziś opakowujemy je w proste API HTTP: jeden endpoint przyjmuje ścieżkę do pliku Markdown na dysku i dodaje go do indeksu w pamięci, drugi przyjmuje pytanie i zwraca odpowiedź modelu wraz ze źródłami. Świadomie bez bazy wektorowej, bez persystencji, bez zaawansowanych strategii — najprostszy RAG, jaki ma sens.
Przygotowanie środowiska
1
2
| python -m venv .venv
.venv\Scripts\pip install fastapi uvicorn llama-index-core llama-index-llms-openrouter llama-index-embeddings-huggingface
|
Klucz do OpenRouter ustawiamy wyłącznie jako zmienną środowiskową — nigdy w kodzie:
1
| $env:OPENROUTER_API_KEY = "twój-klucz"
|
Kompletny kod — main.py
Całość mieści się w jednym pliku:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| import os
from pathlib import Path
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from llama_index.core import Document, Settings, VectorStoreIndex
from llama_index.embeddings.huggingface import HuggingFaceEmbedding
from llama_index.llms.openrouter import OpenRouter
# --- konfiguracja modeli (część 2 serii) ---
Settings.embed_model = HuggingFaceEmbedding(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
Settings.llm = OpenRouter(
api_key=os.environ["OPENROUTER_API_KEY"],
model="anthropic/claude-haiku-4.5",
)
app = FastAPI(title="Najprostszy RAG")
# indeks w pamięci — startuje pusty, znika przy restarcie
index = VectorStoreIndex.from_documents([])
zaindeksowane: list[str] = []
class IndexRequest(BaseModel):
path: str # ścieżka do pliku .md na lokalnym dysku
class AskRequest(BaseModel):
question: str
top_k: int = 3 # ile fragmentów trafi do modelu
@app.post("/index")
def index_file(req: IndexRequest):
plik = Path(req.path)
if not plik.is_file():
raise HTTPException(404, f"Nie znaleziono pliku: {req.path}")
if plik.suffix.lower() != ".md":
raise HTTPException(400, "Obsługiwane są tylko pliki Markdown (.md)")
tekst = plik.read_text(encoding="utf-8")
dokument = Document(text=tekst, metadata={"plik": plik.name})
index.insert(dokument)
zaindeksowane.append(plik.name)
return {"status": "ok", "plik": plik.name, "znakow": len(tekst)}
@app.get("/documents")
def list_documents():
return {"dokumenty": zaindeksowane}
@app.post("/ask")
def ask(req: AskRequest):
if not zaindeksowane:
raise HTTPException(400, "Indeks jest pusty — najpierw dodaj dokument przez /index")
query_engine = index.as_query_engine(similarity_top_k=req.top_k)
wynik = query_engine.query(req.question)
return {
"odpowiedz": wynik.response,
"zrodla": [
{
"plik": node.metadata.get("plik"),
"trafnosc": round(node.score, 3),
"fragment": node.text[:200],
}
for node in wynik.source_nodes
],
}
|
Uruchomienie:
1
| .venv\Scripts\uvicorn main:app --port 8000
|
Jak to działa — krok po kroku
Wszystko, co tu widać, było już w poprzednich częściach — nowe jest tylko opakowanie w HTTP:
/index — wczytuje plik z dysku, opakowuje w Document z nazwą pliku w metadanych i woła index.insert(). LlamaIndex sam tnie tekst na chunki i liczy embeddingi. Walidujemy istnienie pliku i rozszerzenie, zwracając czytelne błędy HTTP./documents — zwraca listę zaindeksowanych plików; przydatne, żeby sprawdzić, co RAG w ogóle „widzi"./ask — tworzy query engine, zadaje pytanie i zwraca odpowiedź modelu razem ze źródłami: nazwą pliku, wynikiem podobieństwa i początkiem fragmentu. Dzięki temu odpowiedź da się zweryfikować — to jedna z największych zalet RAG nad „gołym" LLM.
Modele żyją w Settings (embeddingi lokalnie, LLM przez OpenRouter), a klucz API wchodzi przez os.environ — kod można spokojnie wrzucić do publicznego repozytorium.
Testujemy
FastAPI daje interaktywną dokumentację pod http://127.0.0.1:8000/docs, ale można też z terminala:
1
2
3
4
5
6
7
| curl -X POST http://127.0.0.1:8000/index \
-H "Content-Type: application/json" \
-d '{"path": "C:/notatki/procedury.md"}'
curl -X POST http://127.0.0.1:8000/ask \
-H "Content-Type: application/json" \
-d '{"question": "Jak zresetować hasło użytkownika?"}'
|
Pierwsze wywołanie /index może chwilę potrwać — model embeddingów pobiera się przy pierwszym użyciu.
Czego tu celowo nie ma
Żeby całość pozostała zrozumiała, pominęliśmy wszystko, co w produkcyjnym RAG prędzej czy później się pojawia: persystencję indeksu (po restarcie indeksujemy od nowa), bazę wektorową, autoryzację API, obsługę innych formatów niż Markdown czy strojenie parametrów cięcia na chunki. Każda z tych rzeczy to naturalne rozszerzenie tego kodu — ale żadna nie jest potrzebna, żeby zrozumieć, jak działa RAG.
Podsumowanie
Trzy wpisy i mamy działający RAG: teoria (chunki, embeddingi, indeks), klocki LlamaIndex i na koniec ~80 linijek Pythona z dwoma endpointami. To dobry punkt startowy do eksperymentów z własnymi dokumentami — a jeśli myślisz o czymś podobnym w swojej firmie, odezwij się do nas.