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.
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 |.
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 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 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 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 î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.
Models
Interfață unificată pentru orice LLM. ChatOpenAI, ChatAnthropic, ChatOllama — același cod, provider diferit.
Prompts
PromptTemplate separă structura promptului de valorile concrete. Variabilele devin contractul componentei.
Retrieval
DocumentLoader, TextSplitter, VectorStore, Retriever — lanțul de regăsire a datelor proprii pentru RAG.
Chains (LCEL)
Orice componentă este un Runnable. Operatorul | le leagă: prompt | llm | parser.
Memory
Păstrarea contextului între apeluri. ConversationBufferMemory sau istoricul atașat la un chain.
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.
# 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.
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:
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:
Un input, un output
Execuția simplă, sincronă. Forma pe care o folosești cel mai des.
Răspuns în fragmente
Emite bucăți pe măsură ce sosesc — util pentru afișare progresivă.
Mai multe inputuri
Procesează o listă de inputuri, în paralel unde se poate.
Execuție asincronă
Varianta async a lui invoke, pentru aplicații cu trafic mare.
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
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.
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.
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
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ă.
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.
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ă.
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
Conversație cu fir
Chatbot, asistent în mai multe ture, orice flux unde răspunsul depinde de ce s-a spus înainte.
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.
| Dimensiune | LangChain | LangGraph |
|---|---|---|
| Model mental | Composiție liniară: A | B | C | Graf cu stare: noduri, muchii, condiții |
| Flux | Drum unic, fără întoarcere | Ramificări, bucle, reluare |
| Stare | Datele curg între componente | Stare persistentă, citită și scrisă de noduri |
| Decizii | Nu — pașii sunt fixați | Da — un router decide nodul următor |
| Potrivit pentru | RAG cu rol, chain prompt-model-parser | Agentic RAG, critic, HITL, multi-agent |
| În curs | C6 — agentul RAG simplu | C7 — 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.
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.
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,
})
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.
-
Încarcă rolul și indexul
Citește
role_XX.yamlpentru agentul ales și încarcă vector store-ul FAISS salvat în C5. -
Definește PromptTemplate
Trei variabile:
agent_system,input_text,retrieved_context. Foloseștefrom_template(). -
Compune chain-ul
Leagă
template | llm | StrOutputParser()și apelează-l cu un stimulus politic. -
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.