Wprowadzenie
W pierwszej części rozrysowaliśmy mapę: LangChain dostarcza klocki, LangGraph spina je w graf retrieve → generate. Dziś każdy klocek dostaje swój kod — dokładnie w takiej formie, w jakiej użyjemy go w finałowym API.
Instalacja w środowisku wirtualnym:
1
2
| python -m venv .venv
.venv\Scripts\pip install langchain langchain-openai langchain-huggingface langgraph
|
Model czatu — LLM przez OpenRouter
OpenRouter udostępnia API zgodne z formatem OpenAI, więc wystarczy klasa ChatOpenAI ze zmienionym adresem bazowym:
1
2
3
4
5
6
7
8
9
10
11
| import os
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
api_key=os.environ["OPENROUTER_API_KEY"], # klucz TYLKO ze zmiennej środowiskowej
base_url="https://openrouter.ai/api/v1",
model="anthropic/claude-haiku-4.5",
)
odpowiedz = llm.invoke("Przywitaj się jednym zdaniem.")
print(odpowiedz.content)
|
invoke() to uniwersalna metoda wywołania w LangChain — spotkamy ją w każdym komponencie, także w gotowym grafie.
Embeddingi — lokalnie, za darmo
Tak jak poprzednio, do embeddingów używamy małego modelu uruchamianego lokalnie:
1
2
3
4
5
| from langchain_huggingface import HuggingFaceEmbeddings
embeddings = HuggingFaceEmbeddings(
model_name="sentence-transformers/all-MiniLM-L6-v2"
)
|
Text splitter — cięcie dokumentu na chunki
W LlamaIndex cięcie działo się automatycznie; w LangChain robimy to jawnie:
1
2
3
4
5
6
7
| from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # docelowy rozmiar chunka (w znakach)
chunk_overlap=200, # zakładka między sąsiednimi chunkami
)
chunki = splitter.split_text("...długi tekst dokumentu...")
|
RecursiveCharacterTextSplitter tnie „z wyczuciem": najpierw próbuje po akapitach, potem po zdaniach, na końcu po znakach — żeby nie rozcinać myśli w połowie. Zakładka (chunk_overlap) sprawia, że informacja z granicy dwóch chunków nie ginie.
Vector store — indeks w pamięci
Magazyn embeddingów z wyszukiwaniem podobieństwa. Zostajemy przy najprostszym wariancie — w pamięci procesu:
1
2
3
4
5
6
7
8
9
10
11
12
13
| from langchain_core.documents import Document
from langchain_core.vectorstores import InMemoryVectorStore
vector_store = InMemoryVectorStore(embeddings)
dokumenty = [
Document(page_content=chunk, metadata={"plik": "notatki.md"})
for chunk in chunki
]
vector_store.add_documents(dokumenty)
# wyszukiwanie: embedding pytania + najbliższe chunki
znalezione = vector_store.similarity_search("Jak zresetować hasło?", k=3)
|
Document w LangChain pełni tę samą rolę co w LlamaIndex: treść plus słownik metadanych, dzięki któremu wiadomo, skąd pochodzi fragment.
StateGraph — pierwszy graf w LangGraph
Ostatni klocek. Graf w LangGraph składa się ze stanu (słownik z ustaloną strukturą — stąd TypedDict), węzłów (funkcje) i krawędzi (kolejność):
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
| from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
question: str
context: list # znalezione chunki
answer: str
def retrieve(state: State):
znalezione = vector_store.similarity_search(state["question"], k=3)
return {"context": znalezione} # węzeł zwraca TYLKO to, co zmienia
def generate(state: State):
fragmenty = "\n\n".join(doc.page_content for doc in state["context"])
prompt = (
"Odpowiedz na pytanie wyłącznie na podstawie poniższych fragmentów.\n\n"
f"Fragmenty:\n{fragmenty}\n\nPytanie: {state['question']}"
)
odpowiedz = llm.invoke(prompt)
return {"answer": odpowiedz.content}
graf = (
StateGraph(State)
.add_node("retrieve", retrieve)
.add_node("generate", generate)
.add_edge(START, "retrieve")
.add_edge("retrieve", "generate")
.add_edge("generate", END)
.compile()
)
wynik = graf.invoke({"question": "Jak zresetować hasło?"})
print(wynik["answer"])
|
Zasada działania jest prosta: invoke() wchodzi do grafu ze stanem początkowym, każdy węzeł czyta stan i zwraca słownik z polami, które aktualizuje, a LangGraph skleja to w całość i podaje dalej. Po generate stan zawiera już answer — i to on wraca jako wynik.
Podsumowanie
Pięć klocków: ChatOpenAI wskazany na OpenRouter, HuggingFaceEmbeddings, RecursiveCharacterTextSplitter, InMemoryVectorStore i StateGraph z węzłami retrieve/generate. Każdy z osobna to kilka linijek — a razem to już kompletny, działający RAG. W ostatniej części dołożymy do tego FastAPI: endpoint indeksujący pliki Markdown z dysku i endpoint odpytujący graf.