Klocki LangChain i graf w LangGraph

Michał Feliksik
3 min read
Klocki LangChain i graf w LangGraph

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.