tutorial tehnic · C6

LangChain — composiție și standardizare pentru aplicații LLM

Un apel direct la un model lingvistic este simplu. Greu este tot ce îl înconjoară: cum îi dai contextul potrivit, cum reutilizezi prompturile, cum schimbi providerul fără să rescrii codul. LangChain este stratul care standardizează aceste piese și le face compozabile.

🐍 Python ≥ 3.10 📦 langchain ≥ 0.3 📄 14 secțiuni ⏱ ~40 min

01De ce LangChain?

Un model lingvistic, luat singur, primește text și produce text. Atât. Pentru o aplicație reală mai ai nevoie de structură în jurul lui: prompturi care se pot reutiliza, acces la date proprii, parsare a răspunsului, posibilitatea de a schimba modelul. Fără un strat de organizare, fiecare dintre aceste piese se scrie manual și se rescrie la fiecare modificare.

Sunt trei probleme concrete care apar repede:

  • LLM-ul singur nu are structură. Răspunsul vine ca text liber. Dacă ai nevoie de JSON sau de un câmp anume, îl extragi manual de fiecare dată.
  • Prompturile sunt greu de reutilizat. Un prompt construit cu f-string trăiește lipit de codul care îl apelează. Nu poate fi testat, versionat sau partajat separat.
  • Fiecare provider are alt API. Codul scris pentru OpenAI nu rulează pe Anthropic sau pe un model local fără rescriere.

LangChain rezolvă aceste trei probleme printr-un singur principiu: oferă componente standardizate — pentru prompturi, modele, regăsire de date, memorie și parsare — și le conectează între ele cu operatorul |.

Aplicația ta chatbot · agent · cercetare LangChain strat de composiție prompt | model | date | output CE STANDARDIZEAZĂ Modelsinterfață unică pentru orice LLM Promptsșabloane reutilizabile, separate de cod Retrievalloader, splitter, vector store, retriever Memorycontext conversațional pe termen lung Outputparsare în JSON, Pydantic, text curat
Fig. 1 — LangChain se așază între aplicație și componentele de care are nevoie un sistem LLM: modele, prompturi, regăsire de date, memorie și parsarea răspunsului.

Formularea scurtă: LangChain nu adaugă inteligență, adaugă structură. Transformă un set de apeluri ad-hoc într-un set de componente standardizate care se pot conecta, testa și înlocui independent.

02Ce NU este LangChain

La fel de important ca definiția este delimitarea. LangChain este des prezentat ca soluția pentru orice, ceea ce duce la așteptări greșite. Patru clarificări:

Nu

Nu face modelul mai inteligent

Capacitatea de raționament a răspunsului vine din model și din contextul furnizat. LangChain doar organizează fluxul către model.

Nu

Nu este un agent autonom

Un chain LangChain execută pașii pe care i-ai definit. Nu decide singur ce să facă în continuare — acea logică ține de orchestrare.

Nu

Nu este o bază de date vectorială

LangChain se integrează cu FAISS, Chroma sau Pinecone, dar nu le înlocuiește. Stocarea și căutarea vectorilor rămân în vector store.

Nu

Nu înlocuiește LangGraph

Pentru fluxuri cu stare, bucle și ramificări se folosește LangGraph. LangChain acoperă composiția liniară a componentelor.

Ce este, de fapt: un framework de composiție și standardizare. Îți dă piese compatibile între ele și o sintaxă pentru a le lega. Restul — inteligența, datele, decizia — vine de la model, de la corpus și de la designul tău.

03Componentele principale

LangChain este organizat în jurul a șase familii de componente. Toate respectă aceeași interfață, ceea ce le face interschimbabile și compozabile.

Componenta 1

Models

Interfață unificată pentru orice LLM. ChatOpenAI, ChatAnthropic, ChatOllama — același cod, provider diferit.

Componenta 2

Prompts

PromptTemplate separă structura promptului de valorile concrete. Variabilele devin contractul componentei.

Componenta 3

Retrieval

DocumentLoader, TextSplitter, VectorStore, Retriever — lanțul de regăsire a datelor proprii pentru RAG.

Componenta 4

Chains (LCEL)

Orice componentă este un Runnable. Operatorul | le leagă: prompt | llm | parser.

Componenta 5

Memory

Păstrarea contextului între apeluri. ConversationBufferMemory sau istoricul atașat la un chain.

Componenta 6

Output parsers

StrOutputParser, JsonOutputParser, parsere Pydantic — transformă textul modelului în date verificabile.

Restul tutorialului parcurge aceste componente în ordinea în care apar într-o aplicație: întâi modelul și promptul, apoi composiția cu LCEL, apoi regăsirea datelor.

04Models — interfață unificată

Fiecare provider de modele are propriul SDK, cu propriile nume de funcții și parametri. LangChain ascunde aceste diferențe în spatele unei interfețe comune. Practic, schimbi providerul înlocuind o singură linie — restul codului rămâne neatins.

models.py — același cod, modele diferite
# Fiecare clasă vine din pachetul ei, dar expune aceeași interfață Runnable
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_ollama import ChatOllama

# Trei modele, trei provideri — interfață identică
llm_openai    = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
llm_anthropic = ChatAnthropic(model="claude-haiku-4-5", temperature=0.3)
llm_local     = ChatOllama(model="llama3.1", temperature=0.3)

# Aceeași metodă .invoke() pentru oricare dintre ele
llm = llm_openai
raspuns = llm.invoke("Rezumă în două propoziții ce este RAG.")
print(raspuns.content)

De ce contează: la curs poți porni cu un model gratuit sau local și poți trece la un model comercial fără să modifici promptul, chain-ul sau logica aplicației. Singura linie care se schimbă este cea care construiește obiectul llm.

Modelul rămâne, totuși, doar o piesă. Un model fără un prompt structurat produce text generic. Următoarea componentă rezolvă exact asta.

05Prompts și PromptTemplate

Un prompt construit cu f-string funcționează, dar are un defect: este lipit de codul care îl apelează. Nu poate fi reutilizat de altă funcție, nu poate fi testat separat și nu declară explicit ce variabile așteaptă. PromptTemplate rezolvă aceste trei lucruri.

De ce PromptTemplate și nu f-string

Cu un PromptTemplate, variabilele din șablon devin contractul API al componentei: oricine folosește șablonul știe exact ce trebuie să furnizeze. Șablonul este un obiect de sine stătător — îl poți importa, testa și refolosi în mai multe locuri.

prompts.py — from_template() vs PromptTemplate()
from langchain_core.prompts import PromptTemplate

# Varianta 1 — from_template(): variabilele sunt deduse automat din {...}
tpl = PromptTemplate.from_template(
    "Răspunde la întrebare folosind contextul.\n"
    "Context: {context}\n"
    "Întrebare: {intrebare}"
)

# Varianta 2 — constructorul: declari explicit variabilele de intrare
tpl = PromptTemplate(
    template="Context: {context}\nÎntrebare: {intrebare}",
    input_variables=["context", "intrebare"],
)

# Ambele se completează la fel — format() returnează promptul final ca text
prompt = tpl.format(context="RAG = regăsire + generare.",
                    intrebare="Ce înseamnă RAG?")

from_template() este forma recomandată: citește variabilele direct din șablon. Constructorul explicit este util când vrei să declari și să verifici lista de variabile — util la șabloane mari.

Exemplu concret pentru EchoChamber

În proiectul EchoChamber, promptul agentului RAG are trei blocuri, fiecare cu rolul lui. Acestea sunt exact cele trei variabile ale șablonului:

echochamber — structura promptului C6
template = PromptTemplate.from_template("""
{agent_system}

[STIMULUS]
{input_text}

[COMENTARII SIMILARE]
{retrieved_context}
""")
# {agent_system}      → rolul discursiv din role_XX.yaml (vocea agentului)
# {input_text}        → știrea/afirmația nouă, aceeași pentru toți agenții
# {retrieved_context} → fragmentele top-k recuperate din FAISS

Cele trei variabile nu sunt arbitrare. agent_system controlează vocea, input_text este textul de interpretat, retrieved_context ancorează răspunsul în corpus. Promptul devine auditabil: se vede exact ce a primit modelul.

06LCEL — operatorul pipe |

LCEL (LangChain Expression Language) este sintaxa prin care se leagă componentele. Ideea centrală e simplă: orice componentă LangChain — prompt, model, parser, retriever — implementează aceeași interfață, numită Runnable. Pentru că toate au aceeași interfață, pot fi conectate cu operatorul |, unde ieșirea fiecărei piese devine intrarea următoarei.

Protocolul Runnable

Un Runnable expune patru metode standard. Le ai pe toate, gratuit, pentru orice componentă și pentru orice chain compus din componente:

.invoke()

Un input, un output

Execuția simplă, sincronă. Forma pe care o folosești cel mai des.

.stream()

Răspuns în fragmente

Emite bucăți pe măsură ce sosesc — util pentru afișare progresivă.

.batch()

Mai multe inputuri

Procesează o listă de inputuri, în paralel unde se poate.

.ainvoke()

Execuție asincronă

Varianta async a lui invoke, pentru aplicații cu trafic mare.

chain.py — composiție cu operatorul pipe
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

prompt = PromptTemplate.from_template("Explică pe scurt: {tema}")
llm    = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()

# Chain-ul: trei Runnable legate cu | într-un singur Runnable
chain = prompt | llm | parser

# Apelarea chain-ului — primește un dict cu variabilele promptului
rezultat = chain.invoke({"tema": "ce este un embedding"})
print(rezultat)  # text curat, fără obiectul AIMessage din jur
input | PromptTemplate | LLM | OutputParser output ieșirea fiecărei componente devine intrarea următoarei
Fig. 2 — Un chain LCEL: patru Runnable legate cu operatorul pipe. Datele curg de la stânga la dreapta, fără cod de legătură scris manual.

Composabilitatea este avantajul real: dacă vrei alt model, înlocuiești llm; dacă vrei alt format de ieșire, înlocuiești parser. Restul chain-ului rămâne identic. Componenta se schimbă, structura nu.

07Retrieval — RAG cu LangChain

RAG (Retrieval-Augmented Generation) înseamnă: înainte de a genera răspunsul, modelul primește fragmente relevante dintr-un corpus propriu. LangChain organizează partea de regăsire într-un lanț de cinci pași.

DocumentLoader citește sursele TextSplitter taie în chunk-uri Embeddings text → vectori VectorStore FAISS — index Retriever întoarce top-k
Fig. 3 — Lanțul de regăsire: de la sursele brute la un retriever care, pentru o interogare, întoarce cele mai relevante k fragmente.

Primii patru pași — încărcare, tăiere, embedding, indexare — se rulează o singură dată, la construirea vector store-ului. Doar ultimul, regăsirea, se rulează la fiecare întrebare. În proiectul EchoChamber, primii patru pași au fost deja făcuți în C5; în C6 pornim direct de la indexul FAISS salvat.

build_index.py — construirea vector store-ului (o singură dată)
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS

docs     = TextLoader("data/corpus.txt", encoding="utf-8").load()
chunks   = RecursiveCharacterTextSplitter(chunk_size=500,
                                          chunk_overlap=50).split_documents(docs)
embed    = HuggingFaceEmbeddings(model_name="paraphrase-multilingual-MiniLM-L12-v2")
store    = FAISS.from_documents(chunks, embed)
store.save_local("assets/vectorstores/agent")   # indexul rămâne pe disc
rag_chain.py — chain RAG complet cu LCEL
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate

store     = FAISS.load_local("assets/vectorstores/agent", embed,
                              allow_dangerous_deserialization=True)
retriever = store.as_retriever(search_kwargs={"k": 5})

prompt = PromptTemplate.from_template(
    "Răspunde folosind doar contextul.\n"
    "Context: {context}\nÎntrebare: {question}"
)

# Retrieverul alimentează variabila {context}; întrebarea trece neschimbată
chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt | llm | StrOutputParser()
)
raspuns = chain.invoke("Ce spune corpusul despre alegeri?")

De reținut: calitatea răspunsului depinde de calitatea fragmentelor recuperate. Dacă retrieverul întoarce context slab, și răspunsul va fi slab — RAG nu corectează o regăsire proastă.

08Primul chain — fără LLM

Înainte de a integra un model, înțelegem mecanica LCEL cu un exemplu pur Python care rulează fără niciun API key. O funcție obișnuită devine Runnable prin RunnableLambda — și poate fi legată cu | ca orice altă componentă.

first_chain.py — exemplu complet, fără API key
from langchain_core.runnables import RunnableLambda

# Trei funcții simple — fiecare ia un input și întoarce un output
def curata(text: str) -> str:
    return text.strip().lower()

def numara_cuvinte(text: str) -> dict:
    return {"text": text, "cuvinte": len(text.split())}

def formateaza(d: dict) -> str:
    return f"'{d['text']}' are {d['cuvinte']} cuvinte"

# Le transformăm în Runnable și le legăm cu | — exact ca un chain cu LLM
chain = (
    RunnableLambda(curata)
    | RunnableLambda(numara_cuvinte)
    | RunnableLambda(formateaza)
)

print(chain.invoke("  Salut Lume Mare  "))
# → 'salut lume mare' are 3 cuvinte

Chain-ul leagă trei funcții obișnuite și le execută în ordine, fiecare primind ieșirea precedentei. Niciun model, niciun API key — doar mecanica LCEL. Când înlocuiești o funcție cu un PromptTemplate și un LLM, principiul rămâne identic.

09Chain cu LLM

Acum înlocuim funcțiile cu un prompt real, un model real și un parser. Structura chain-ului — legarea cu | — rămâne aceeași. Acesta este un chain RAG minimal funcțional.

chain_llm.py — chain RAG minimal
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

# 1. Componentele
prompt = PromptTemplate.from_template(
    "Ești un asistent care răspunde strict pe baza contextului.\n"
    "Context:\n{context}\n\nÎntrebare: {intrebare}\nRăspuns:"
)
llm    = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
parser = StrOutputParser()

# 2. Chain-ul
chain = prompt | llm | parser

# 3. Apelarea — context-ul vine din regăsire, întrebarea de la utilizator
raspuns = chain.invoke({
    "context": "FAISS este o bibliotecă pentru căutare de similaritate.",
    "intrebare": "La ce folosește FAISS?",
})
print(raspuns)

Cele trei componente sunt independente. Schimbă llm și ai alt model. Schimbă parser cu un JsonOutputParser și primești JSON în loc de text. Chain-ul în sine — prompt | llm | parser — nu se modifică.

10Memory — context conversațional

Un apel LLM este, implicit, fără memorie: modelul nu știe ce s-a discutat la apelul anterior. Pentru un chatbot care trebuie să țină firul conversației, ai nevoie de un mecanism care păstrează istoricul și îl reintroduce în prompt la fiecare tură.

memory.py — istoric conversațional minimal
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

istoric = InMemoryChatMessageHistory()

# Înfășurăm chain-ul existent cu un strat care reține mesajele
chain_cu_memorie = RunnableWithMessageHistory(
    chain,
    lambda session_id: istoric,    # de unde se ia istoricul
)

# Fiecare apel adaugă mesajele la istoric și le reintroduce la următorul
cfg = {"configurable": {"session_id": "curs-c6"}}
chain_cu_memorie.invoke({"intrebare": "Ce este un retriever?"}, config=cfg)
chain_cu_memorie.invoke({"intrebare": "Și cum diferă de un loader?"}, config=cfg)
# Al doilea apel are acces la primul — modelul știe despre ce e vorba
Când se folosește

Conversație cu fir

Chatbot, asistent în mai multe ture, orice flux unde răspunsul depinde de ce s-a spus înainte.

Când NU se folosește

Apeluri independente

Agentul RAG din C6 răspunde la fiecare stimulus separat. Nu are nevoie de memorie — fiecare răspuns este de sine stătător.

Memoria are un cost: istoricul intră în prompt și consumă context. Pentru EchoChamber C6, agentul este intenționat stateless — fiecare răspuns trebuie să fie comparabil cu al celorlalți agenți, deci nu poate depinde de un istoric propriu.

11LangChain vs LangGraph

LangChain și LangGraph sunt complementare, nu alternative. LangChain organizează componente într-un flux liniar. LangGraph organizează controlul fluxului: stare, bucle, ramificări. LangChain este fundația peste care se construiește LangGraph.

DimensiuneLangChainLangGraph
Model mentalComposiție liniară: A | B | CGraf cu stare: noduri, muchii, condiții
FluxDrum unic, fără întoarcereRamificări, bucle, reluare
StareDatele curg între componenteStare persistentă, citită și scrisă de noduri
DeciziiNu — pașii sunt fixațiDa — un router decide nodul următor
Potrivit pentruRAG cu rol, chain prompt-model-parserAgentic RAG, critic, HITL, multi-agent
În cursC6 — agentul RAG simpluC7 — orchestrarea agenților

Regula de alegere este simplă. Dacă fluxul tău este input → regăsire → prompt → model → răspuns, LangChain este suficient. Dacă ai nevoie ca sistemul să decidă singur dacă reia regăsirea, să verifice răspunsul sau să se oprească pentru un input uman — atunci treci la LangGraph. C6 rămâne la primul caz; C7 introduce al doilea.

12Studiu de caz — EchoChamber (Cursul C6)

În EchoChamber, fiecare bulă discursivă este reprezentată de un agent RAG. C6 construiește acest agent: model + rol + context. Rolul vine dintr-un fișier YAML, contextul din indexul FAISS construit în C5, iar PromptTemplate leagă cele două.

Nivelul 2 din taxonomia RAG

RAG poate fi văzut ca o scară de complexitate. C6 implementează nivelul 2 — RAG cu rol: regăsire fixă, urmată de generare controlată de un rol discursiv. Fluxul rămâne stabil și previzibil. Nivelul 3 — agentul decide singur când și ce caută — este subiectul lui C7, cu LangGraph.

assets/roles/role_XX.yaml — rolul discursiv al agentului
anti_sistem:
  slug: anti_sistem
  name: "Anti-sistem"
  color: "#FF8A65"
  system: |
    Ești un comentator politic român.
    Cum vorbești: ton direct, critic, ironic uneori.
    Reguli: folosești comentariile ca inspirație;
            maxim 3 propoziții; fără inventare.

Rolul nu descrie o persoană — descrie un pattern de interpretare observat în date. slug este identificatorul, name eticheta de afișare, system este constrângerea retorică completă care va popula variabila agent_system din prompt.

core/agent.py — agentul RAG C6 ca chain LangChain
import yaml
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

# 1. Rolul discursiv din YAML
role = yaml.safe_load(open("assets/roles/roles.yaml"))["anti_sistem"]

# 2. Șablonul C6 — trei blocuri, trei variabile
template = PromptTemplate.from_template("""
{agent_system}

[STIMULUS]
{input_text}

[COMENTARII SIMILARE]
{retrieved_context}
""")

# 3. Chain-ul: prompt | model | parser
chain = template | llm | StrOutputParser()

# 4. Răspunsul agentului — context-ul vine din FAISS (retrieve_context din C5)
results, retrieved_context = retrieve_context(input_text, k=5)
raspuns = chain.invoke({
    "agent_system":      role["system"],
    "input_text":        input_text,
    "retrieved_context": retrieved_context,
})
input FAISS top-k rol YAML PromptTemplate 3 variabile LLM răspuns PromptTemplate leagă rolul, stimulusul și contextul recuperat într-un singur prompt auditabil
Fig. 4 — Fluxul agentului RAG C6: regăsire din FAISS, rol din YAML, composiție prin PromptTemplate, generare cu LLM. Toate pieseele sunt vizibile și verificabile.

Ce produce C6: primul răspuns real ancorat în corpus, cu identitate discursivă definită și context recuperat transparent. Fiecare răspuns este auditabil — se știe rolul, se știu fragmentele, se știe promptul. PromptTemplate este piesa care face acest lucru reutilizabil pentru toți cei cinci agenți ai echipei.

13Exercițiu practic

Sarcina: construiește un chain RAG cu PromptTemplate pentru un agent, pornind de la indexul FAISS din C5.

  1. Încarcă rolul și indexul

    Citește role_XX.yaml pentru agentul ales și încarcă vector store-ul FAISS salvat în C5.

  2. Definește PromptTemplate

    Trei variabile: agent_system, input_text, retrieved_context. Folosește from_template().

  3. Compune chain-ul

    Leagă template | llm | StrOutputParser() și apelează-l cu un stimulus politic.

  4. Testează două inputuri diferite

    Verifică dacă răspunsul rămâne ancorat în context și în voce, indiferent de stimulus.

Întrebări de notat după execuție:

  • Ce se întâmplă dacă schimbi modelul (llm)? Vocea agentului se schimbă sau rămâne dată de rol?
  • Ce se întâmplă dacă schimbi parserul cu un JsonOutputParser? Ce trebuie modificat în prompt?
  • Dacă fragmentele recuperate sunt slabe, se vede în răspuns? Unde intervii — la retriever sau la prompt?
  • Același chain funcționează pentru alt agent doar schimbând role? De ce este asta avantajul lui PromptTemplate?

14Concluzie

LangChain standardizează fluxul, nu face modelul mai inteligent. Oferă componente compatibile — modele, prompturi, retrievere, parsere — și o sintaxă, LCEL, prin care se conectează. Inteligența rămâne în model și în context; LangChain dă structura.

Conceptele acoperite — Models, PromptTemplate, Runnable, LCEL, Retrieval, Memory — sunt fundația pe care se construiește orice aplicație LLM serioasă. În C6, ele se concretizează într-un agent RAG cu rol: model + rol + context, legate prin PromptTemplate, testabile în aplicație.

Cursul C7 duce mai departe: când agentul trebuie să decidă singur dacă reia regăsirea, să-și verifice răspunsul sau să se oprească pentru un input uman, composiția liniară din LangChain nu mai este suficientă — se trece la orchestrarea cu stare din LangGraph.

Capacități LangChain neacoperite în C6, disponibile pentru explorare independentă: RunnableParallel pentru execuție concurentă, RunnableBranch pentru rutare, parsere Pydantic pentru output structurat validat, integrarea cu LangSmith pentru observabilitate.


Resurse și surse

Sursele de mai jos au stat la baza tutorialului. Sunt recomandate pentru aprofundare, în ordinea utilității.

Documentație oficială
LangChain — PromptTemplate (API reference)
Documentație oficială
LangChain — Build a RAG agent (tutorial)
Pinecone
LangChain Expression Language Explained
Pinecone
Prompt Engineering and LLMs with LangChain
GeeksforGeeks
LangChain Expression Language (LCEL)
Codecademy
Getting Started with LangChain Prompt Templates
DEV Community · YoungTee
LangChain Fundamentals: the Mental Model
Medium · Snehitha
A Beginner Guide: LangChain Chains, LCEL, Runnable
Medium · Anil Goyal
Building a Simple RAG System with LangChain & FAISS
GeeksforGeeks
RAG with LangChain — pipeline complet