Home Fondamenti Token Modelli AI Deep Learning Tecniche RAG RAG Avanzato MCP Orchestrazione LangChain LangGraph Prompt Engineering Usare l'AI ChipsBot News

LangChain: Il Framework per Applicazioni LLM

LangChain è il framework Python più usato al mondo per costruire applicazioni con LLM. Dai pipeline RAG agli agenti, dall’orchestrazione al monitoring — tutto ciò che serve per passare dal prototipo alla produzione.

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:

┌─────────────────────────────────────────────────┐ │ LangChain Application │ ├─────────────────────────────────────────────────┤ │ Chains / LCEL │ Agents │ RAG Pipeline │ ├─────────────────────────────────────────────────┤ │ LLMs │ Prompts │ Memory │ Output Parsers │ ├─────────────────────────────────────────────────┤ │ Loaders │ Splitters │ Embeddings │ Vector Stores │ ├─────────────────────────────────────────────────┤ │ LangChain Core (Runnable Protocol) │ └─────────────────────────────────────────────────┘

Ogni layer in dettaglio:

  • LangChain Core (Runnable Protocol): il fondamento. Definisce l’interfaccia Runnable con 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
Tip: Streaming gratuito con LCEL LCEL gestisce automaticamente lo streaming, il batch parallelo e l’async. Scrivere 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.

Agenti complessi: usa LangGraph Per agenti con state complesso, flussi condizionali, human-in-the-loop o architetture multi-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 attive
  • PostgresChatMessageHistory — persistente, interrogabile
  • MongoDBChatMessageHistory — 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, /batch e 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
Stabilità API: versiona sempre le dipendenze LangChain ha storicamente avuto problemi di instabilità delle API — aggiornamenti breaking tra versioni minori. In produzione, fissa sempre la versione esatta nel 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