Cos’è LangChain e perché esiste
LangChain nasce nel 2022 da un progetto personale di Harrison Chase. In pochi mesi diventa il repository GitHub più stellato del 2023 e lo strumento di riferimento per chiunque voglia costruire applicazioni basate su LLM.
L’analogia più azzeccata: LangChain è come Django o Rails per le app web. Django non ti impedisce di scrivere Python puro — ma se vuoi un ORM, un sistema di routing, template, autenticazione e admin panel, reinventarli da zero sarebbe uno spreco di tempo. Django li ha già, testati, documentati e mantenuti da una community enorme.
LangChain fa lo stesso per le applicazioni AI. Quando costruisci un’app LLM, il 60% del codice è "plumbing" — caricare documenti, fare chunking, connettersi a vector store, gestire la conversazione, parsare output strutturati, gestire errori e retry. LangChain incapsula tutto questo in astrazioni riutilizzabili.
Il problema concreto che risolve:
- Molteplicità di provider: vuoi usare Claude un giorno e GPT-4 il giorno dopo? LangChain ha interfacce uniformi per tutti i modelli principali
- Pipeline complesse: caricare PDF, fare chunking, creare embeddings, fare retrieval, costruire il prompt, chiamare l’LLM, parsare l’output — ogni step ha la sua logica, LangChain li compone
- Observability: con LangSmith hai tracing automatico di ogni step senza modificare il codice
- Ecosistema: oltre 100 document loader, integrazioni con Chroma/Pinecone/Weaviate, tool per agenti già pronti
Adottato da aziende come Elastic, Nerdio, Rakuten e migliaia di startup, LangChain è diventato lo standard de facto per chi costruisce con LLM in Python.
Architettura modulare
LangChain è costruito a strati. Ogni layer è indipendente e sostituibile:
Ogni layer in dettaglio:
- LangChain Core (Runnable Protocol): il fondamento. Definisce l’interfaccia
Runnablecon i metodi.invoke(),.stream(),.batch(),.ainvoke(). Tutto il resto implementa questa interfaccia — questa è la chiave che rende possibile la composizione con LCEL. - Loaders / Splitters / Embeddings / Vector Stores: il layer di ingestion. Qui i dati grezzi (PDF, web, database) diventano chunk vettoriali pronti per il retrieval.
- LLMs / Prompts / Memory / Output Parsers: il layer di interazione con i modelli. Gestisce le chiamate ai vari provider, i template di prompt, la memoria conversazionale e la strutturazione degli output.
- Chains / LCEL / Agents / RAG Pipeline: il layer applicativo. Qui si compongono i componenti sottostanti in flussi di lavoro completi.
LCEL — LangChain Expression Language
LCEL è il cuore di LangChain moderno, introdotto nella v0.1. L’idea fondamentale: ogni componente è un Runnable e i Runnable si compongono con l’operatore |, esattamente come le Unix pipe.
L’operatore | non è la pipe di bash — è l’override di __or__ in Python. Ogni componente riceve l’output del precedente come input e restituisce il proprio output al successivo. Il risultato è una nuova chain che può essere invocata, streamata o eseguita in batch.
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# Ogni | crea una nuova chain
prompt = ChatPromptTemplate.from_template(
"Rispondi in italiano alla domanda: {question}"
)
llm = ChatAnthropic(model="claude-3-5-sonnet-20241022")
parser = StrOutputParser()
chain = prompt | llm | parser
# Invoke sincrono — blocca fino alla risposta completa
result = chain.invoke({"question": "Cos'è il RAG?"})
# Streaming — ogni token appena disponibile
for chunk in chain.stream({"question": "Cos'è il RAG?"}):
print(chunk, end="", flush=True)
# Batch parallelo — più input in una volta, eseguiti in parallelo
results = chain.batch([
{"question": "Cos'è il RAG?"},
{"question": "Cos'è MCP?"},
{"question": "Cos'è LangGraph?"},
])python
chain.stream() invece di chain.invoke() è tutto ciò che serve per dare feedback real-time all’utente — nessun refactoring richiesto.
Composizione avanzata con RunnableParallel
LCEL non è solo sequenziale. RunnableParallel esegue più chain in parallelo e raccoglie i risultati in un dizionario:
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
# Esecuzione parallela: risposta + lunghezza della domanda
parallel = RunnableParallel(
risposta=chain,
lunghezza=lambda x: len(x["question"])
)
# Output: {"risposta": "...", "lunghezza": 42}
# RunnablePassthrough: mantieni l'input originale affiancato al risultato
chain_con_input = RunnablePassthrough.assign(
risposta=chain
)
# Output: {"question": "...", "risposta": "..."}python
RunnablePassthrough è particolarmente utile nelle RAG chain: permette di mantenere la domanda originale dell’utente mentre si aggiunge il contesto recuperato, senza doverla ricopiare esplicitamente.
Document loaders e text splitters
Prima di fare RAG o qualsiasi elaborazione su documenti, i dati devono essere caricati e preparati. LangChain ha oltre 100 document loader integrati nel pacchetto langchain-community:
from langchain_community.document_loaders import (
PyPDFLoader,
WebBaseLoader,
TextLoader,
JSONLoader,
CSVLoader,
)
# Carica un PDF — un Document per pagina
loader = PyPDFLoader("documento.pdf")
docs = loader.load()
# Scraping di una pagina web
loader = WebBaseLoader("https://theaistack.guide/rag.html")
docs = loader.load()
# Ogni Document ha .page_content e .metadata
print(docs[0].page_content) # il testo
print(docs[0].metadata) # {"source": "...", "page": 0, ...}python
Una volta caricati, i documenti devono essere suddivisi in chunk più piccoli (vedi la sezione RAG per la teoria). Il RecursiveCharacterTextSplitter è il più usato:
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # caratteri per chunk
chunk_overlap=200, # sovrapposizione per non perdere contesto
length_function=len,
# Prova questi separatori nell'ordine, usa il primo che trova
separators=["\n\n", "\n", ". ", " ", ""]
)
chunks = splitter.split_documents(docs)
# chunks è una lista di Document, ognuno con chunk_size caratteri maxpython
L’algoritmo "ricorsivo" tenta prima di splittare sui paragrafi (\n\n), poi sulle righe, poi sui periodi — cercando di mantenere unità semantiche coese. Solo come ultimo resort usa lo spazio o il taglio diretto.
Vector stores e retrieval
Con i chunk in mano, il passo successivo è trasformarli in vettori e archiviarli in un vector store per il retrieval. LangChain unifica l’interfaccia di Chroma, Pinecone, Weaviate, Qdrant, pgvector e molti altri:
from langchain_openai import OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
embeddings = OpenAIEmbeddings(model="text-embedding-3-large")
# Crea il vector store e inserisce i chunk in una sola operazione
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db"
)
# Converti in retriever con strategia di ricerca
retriever = vectorstore.as_retriever(
search_type="mmr", # Maximum Marginal Relevance: diversifica i risultati
search_kwargs={"k": 5, "fetch_k": 20}
)
relevant_docs = retriever.invoke("come funziona il chunking?")python
La strategia mmr (Maximum Marginal Relevance) è spesso preferibile alla semplice similarità coseno: evita di restituire cinque chunk quasi identici, privilegiando una selezione diversificata che copre più aspetti della query.
RAG chain completa con LCEL
Combinando retriever, prompt e LLM con LCEL si ottiene una RAG chain completa in poche righe:
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("""
Rispondi alla domanda basandoti sul contesto fornito.
Se non sai la risposta, dillo chiaramente.
Contesto: {context}
Domanda: {question}
""")
def format_docs(docs):
return "\n\n".join(doc.page_content for doc in docs)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
answer = rag_chain.invoke("Come funziona il chunking nel RAG?")python
Il dizionario {"context": retriever | format_docs, "question": RunnablePassthrough()} è un RunnableParallel implicito: recupera il contesto in parallelo mentre passa attraverso la domanda originale. Tutto prima che il prompt venga costruito.
Chains in pratica
Uno dei pattern più potenti di LangChain è la structured output chain: costringere l’LLM a rispondere in un formato JSON validato da Pydantic.
from langchain_core.output_parsers import JsonOutputParser
from langchain_core.pydantic_v1 import BaseModel, Field
class AnalisiSentimento(BaseModel):
sentimento: str = Field(description="positivo, negativo, neutro")
score: float = Field(description="score da 0 a 1")
motivo: str = Field(description="spiegazione in 1 frase")
parser = JsonOutputParser(pydantic_object=AnalisiSentimento)
chain = (
ChatPromptTemplate.from_template(
"Analizza il sentimento di questo testo. {format_instructions}\n\nTesto: {testo}"
).partial(format_instructions=parser.get_format_instructions())
| llm
| parser
)
result = chain.invoke({"testo": "Il prodotto è ottimo ma la consegna ha tardato"})
# result.sentimento = "neutro", result.score = 0.5, ...python
Il metodo .partial() pre-compila alcuni parametri del template, lasciandone altri da riempire al momento dell’invocazione. È utile per iniettare istruzioni di formato, date, configurazioni fisse senza doverle passare ogni volta.
Agenti e tools
LangChain semplifica notevolmente la creazione di agenti con tool use. Il decoratore @tool trasforma qualsiasi funzione Python in un tool che il modello può invocare:
from langchain.agents import create_tool_calling_agent, AgentExecutor
from langchain_core.tools import tool
@tool
def cerca_meteo(citta: str) -> str:
"""Restituisce le condizioni meteo attuali per una città."""
# Qui andrebbe una chiamata a una vera API meteo
return f"Milano: 18°C, parzialmente nuvoloso"
@tool
def calcola(espressione: str) -> str:
"""Calcola un'espressione matematica Python."""
return str(eval(espressione))
tools = [cerca_meteo, calcola]
prompt = ChatPromptTemplate.from_messages([
("system", "Sei un assistente utile con accesso a strumenti."),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
agent = create_tool_calling_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({"input": "Che tempo fa a Milano? E quanto fa 23 * 17?"})python
L’AgentExecutor gestisce automaticamente il loop ReAct: legge la risposta del modello, rileva le tool call, le esegue, restituisce i risultati al modello e ripete fino a quando il modello non produce una risposta finale senza tool call.
Il campo "placeholder" con "{agent_scratchpad}" è dove l’executor inietta la storia delle tool call e dei risultati nelle conversazioni intermedie — il "taccuino" dell’agente.
AgentExecutor diventa limitante. LangGraph è lo strumento giusto in quei casi — offre controllo granulare su ogni step del loop agentico.
Memory e conversational context
Per costruire chatbot che ricordano la conversazione, LangChain usa il pattern RunnableWithMessageHistory. La chiave è separare la logica di storage dalla chain — così si può passare da memoria in-memory a Redis o PostgreSQL senza cambiare la chain:
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
# Store in-memory: dizionario session_id -> ChatMessageHistory
store = {}
def get_session_history(session_id: str) -> ChatMessageHistory:
if session_id not in store:
store[session_id] = ChatMessageHistory()
return store[session_id]
# Wrap la chain con il gestore di memoria
chain_con_memory = RunnableWithMessageHistory(
chain,
get_session_history,
input_messages_key="input",
history_messages_key="history",
)
# Prima domanda — salva il contesto
chain_con_memory.invoke(
{"input": "Il mio nome è Franz"},
config={"configurable": {"session_id": "user_123"}}
)
# Seconda domanda — recupera il contesto automaticamente
risposta = chain_con_memory.invoke(
{"input": "Come mi chiamo?"},
config={"configurable": {"session_id": "user_123"}}
)
# Risposta: "Ti chiami Franz"python
Per la produzione, sostituire ChatMessageHistory con:
RedisChatMessageHistory— veloce, ideale per sessioni attivePostgresChatMessageHistory— persistente, interrogabileMongoDBChatMessageHistory— flessibile per strutture dati variabili
Per conversazioni molto lunghe, considerare la summary memory: invece di tenere tutti i messaggi, si chiede periodicamente all’LLM di comprimere la storia in un riassunto, riducendo il consumo di token mantenendo il contesto rilevante.
LangSmith — Observability e debugging
LangSmith è la piattaforma di observability ufficiale di LangChain. Il suo punto di forza: bastano tre variabili d’ambiente per attivare il tracing completo di ogni chiamata, senza modificare una riga di codice applicativo.
# Aggiungi queste variabili d'ambiente PRIMA di importare LangChain
export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=lsv2_...
export LANGCHAIN_PROJECT=my-projectbash
Da quel momento, ogni chiamata LangChain viene tracciata automaticamente. Nella dashboard di LangSmith puoi vedere:
- Trace completo: ogni step della chain, input e output di ogni componente, nidificazione dei sotto-step
- LLM calls: prompt esatto inviato al modello (con i valori interpolati), risposta ricevuta, latenza, token usati, costo stimato
- Tool invocations: quali tool sono stati chiamati, con quali argomenti, cosa hanno restituito
- Error traces: dove esattamente è avvenuto l’errore nella chain
Oltre al tracing, LangSmith offre:
- Playground: testa e modifica prompt direttamente dall’interfaccia web, senza toccare il codice
- Dataset ed evaluation: costruisci dataset di golden examples e misura automaticamente la qualità delle risposte
- Annotations: il team può annotare i trace per identificare casi problematici
- Monitoring alerts: ricevi notifiche se la latenza o il tasso di errori supera soglie definite
L’ecosistema: LangChain, LangGraph, LangServe
Il progetto LangChain è in realtà una famiglia di pacchetti con responsabilità distinte:
langchain-core: le interfacce base.Runnable,BaseMessage,Document,BaseChatModel. Dipendenza minima, senza logica applicativa.langchain: chain classiche, agent executor, utility. Il "framework" principale, dipende da core.langchain-community: integrazioni con terze parti. 100+ document loader, vector store, LLM. Mantenuta dalla community, qualità variabile.langchain-openai,langchain-anthropic, ecc.: integrazioni ufficiali, mantenute dai provider stessi o da LangChain Inc. con testing rigoroso.- LangGraph: orchestrazione di agenti stateful come grafi diretti. Il successore degli agent legacy di LangChain per casi d’uso complessi.
- LangServe: deploy di chain LangChain come API REST. Basato su FastAPI, genera automaticamente gli endpoint
/invoke,/stream,/batche la documentazione OpenAPI. - LangSmith: la piattaforma SaaS di observability, evaluation e monitoring descritta sopra.
Quando usare LangChain (e quando no)
LangChain non è la risposta a tutto. La scelta giusta dipende dalla complessità del problema:
Usare LangChain quando:
- Stai costruendo una RAG pipeline completa con loader, splitter, embeddings, retriever e generazione — LangChain fa risparmiare ore di codice boilerplate
- Devi connetterti a più provider LLM con interfaccia uniforme — cambiare da Claude a GPT-4 diventa una riga di codice
- Vuoi observability integrata con LangSmith senza modificare il codice applicativo
- Il team è già familiare con il framework — la curva di apprendimento è già stata pagata
- Stai integrando molti tool ed external service — l’ecosistema di connector è ampio
NON usare LangChain quando:
- Il task è una singola chiamata API:
anthropic.messages.create()è più chiaro e diretto di un’intera chain LangChain - Stai debuggando un problema complesso: le astrazioni nascondono cosa succede realmente — l’SDK diretto è più trasparente
- Hai requisiti di performance estremi: ogni layer di astrazione aggiunge overhead, per quanto piccolo
- Stai imparando: inizia con l’SDK del provider (Anthropic, OpenAI) per capire i fondamentali prima di aggiungere astrazioni
- Hai bisogno di agenti complessi con state: usa direttamente LangGraph
requirements.txt (langchain==0.3.x) e testa in staging prima di aggiornare.
| Scenario | LangChain | SDK diretto |
|---|---|---|
| RAG pipeline completa | ✅ Ottimo | 😰 Molto codice |
| Singola chiamata LLM | ❌ Overhead | ✅ Semplice |
| Multi-provider | ✅ Interfaccia unificata | 😰 Gestione manuale |
| Debugging | ❌ Astrazioni opache | ✅ Trasparente |
| Agenti complessi | → LangGraph | → Anthropic SDK |
| Team principianti | ❌ Curva ripida | ✅ Più comprensibile |
Per approfondire
- Per agenti con state complesso, loop condizionali e human-in-the-loop: LangGraph — Agenti Stateful come Grafi
- Per capire la teoria del retrieval che sta sotto le RAG chain: RAG — Retrieval Augmented Generation
- Per capire come connettere LangChain a tool esterni via protocollo standard: MCP — Model Context Protocol
- Per architetture multi-agente e pattern di orchestrazione: Orchestrazione & Agenti
- Per ottimizzare i prompt che usi nelle tue chain: Prompt Engineering Avanzato