Costruire uno stack di applicazioni LLM mantenibile e modulare con Hamilton in 13 minuti
Creare un'applicazione LLM con Hamilton in 13 minuti
Le applicazioni LLM sono flussi di dati, utilizza uno strumento appositamente progettato per esprimerli

Questo post è scritto in collaborazione con Thierry Jean ed è apparso originariamente qui.
In questo post, condivideremo come Hamilton, un framework open source, può aiutarti a scrivere codice modulare e mantenibile per il tuo stack di applicazioni di grandi modelli di linguaggio (LLM). Hamilton è ottimo per descrivere qualsiasi tipo di flusso di dati, che è esattamente ciò che fai quando costruisci un’applicazione alimentata da un LLM. Con Hamilton ottieni una solida ergonomia per la manutenzione del software, con il vantaggio aggiuntivo di poter facilmente sostituire ed valutare diversi fornitori/implementazioni per i componenti della tua applicazione. Avviso: sono uno degli autori del pacchetto Hamilton.
L’esempio che ti guideremo riprodurrà un flusso di lavoro tipico di un’applicazione LLM che utilizzeresti per popolare un database vettoriale con una certa conoscenza testuale. In particolare, tratteremo l’estrazione dei dati dal web, la creazione di rappresentazioni testuali (vettori) e l’inserimento dei dati nel database vettoriale.

Il flusso di dati dell’applicazione LLM
Per iniziare, descriviamo cosa comprende tipicamente un flusso di dati LLM. L’applicazione riceverà un piccolo input di dati (ad esempio un testo, un comando) e agirà all’interno di un contesto più ampio (ad esempio una cronologia di chat, documenti, stato). Questi dati si sposteranno attraverso diversi servizi (LLM, database vettoriale, archivio documenti, ecc.) per eseguire operazioni, generare nuovi artefatti di dati e restituire risultati finali. Nella maggior parte dei casi, questi flussi di dati vengono ripetuti più volte iterando su diversi input.
- Un’introduzione pratica agli LLM
- 3 Concetti Fondamentali sulle Strutture Dati in Python
- Sfruttare il potere dei grafi di conoscenza arricchire un LLM con dati strutturati
Alcune operazioni comuni includono:
- Convertire il testo in rappresentazioni vettoriali
- Archiviare / cercare / recuperare rappresentazioni vettoriali
- Trovare i vicini più vicini a una rappresentazione vettoriale
- Recuperare il testo per una rappresentazione vettoriale
- Determinare il contesto necessario da passare a un prompt
- Interrogare modelli con contesto derivato dal testo pertinente
- Inviare i risultati a un altro servizio (API, database, ecc.)
- …
- e concatenarli insieme!
Ora, pensiamo a tutto ciò in un contesto di produzione e immaginiamo che un utente non sia soddisfatto degli output della tua applicazione e tu voglia individuare la causa principale del problema. La tua applicazione ha registrato sia il prompt che i risultati. Il tuo codice ti consente di capire la sequenza delle operazioni. Tuttavia, non hai idea di dove le cose siano andate storte e il sistema ha prodotto un output indesiderato… Per mitigare questo, sosteniamo che sia fondamentale avere la tracciabilità degli artefatti di dati e il codice che li produce, in modo da poter risolvere rapidamente situazioni come queste.
Per aggiungere complessità al flusso di dati della tua applicazione LLM, molte operazioni sono non deterministiche, il che significa che non puoi eseguire di nuovo o invertire l’operazione per riprodurre risultati intermedi. Ad esempio, una chiamata API per generare una risposta testuale o di immagine sarà probabilmente non riproducibile anche se hai accesso allo stesso input e alla stessa configurazione (puoi mitigare alcuni di questi aspetti con opzioni come la temperatura). Questo si estende anche a certe operazioni del database vettoriale come “trova il più vicino” in cui il risultato dipende dagli oggetti attualmente memorizzati nel database. In ambienti di produzione, è proibitivo o quasi impossibile acquisire uno snapshot dello stato del database per rendere le chiamate riproducibili.
Per questi motivi, è importante adottare strumenti flessibili per creare flussi di dati robusti che ti consentano di:
- inserire facilmente vari componenti.
- vedere come i componenti si collegano tra loro.
- aggiungere e personalizzare esigenze comuni di produzione come la memorizzazione nella cache, la validazione e l’osservabilità.
- adattare la struttura del flusso alle tue esigenze senza richiedere una forte competenza ingegneristica.
- inserirti nell’ecosistema tradizionale di elaborazione dei dati e di apprendimento automatico.
In questo post forniremo una panoramica su come Hamilton soddisfa i punti 1, 2 e 4. Per i punti 3 e 5 vi rimandiamo alla nostra documentazione.
Strumenti attuali per lo sviluppo di applicazioni LLM
Lo spazio LLM è ancora in fase di sviluppo e i modelli di utilizzo e gli strumenti stanno evolvendo rapidamente. Mentre i framework LLM possono aiutarti ad iniziare, le opzioni attuali non sono testate in produzione; a nostra conoscenza, nessuna delle aziende tecnologiche più importanti sta usando i framework LLM attualmente popolari in produzione.
Non fraintendeteci, alcuni degli strumenti disponibili sono ottimi per realizzare rapidamente un prototipo di concetto! Tuttavia, riteniamo che presentino alcune carenze in due aree specifiche:
1. Come modellare il flusso di dati dell’applicazione LLM. Crediamo fermamente che il flusso di dati delle “azioni” sia meglio modellato come funzioni, piuttosto che attraverso classi orientate agli oggetti e cicli di vita. Le funzioni sono molto più semplici da comprendere, testare e modificare. Le classi orientate agli oggetti possono diventare piuttosto opache e imporre un carico mentale maggiore.
Quando si verifica un errore, i framework orientati agli oggetti ti richiedono di analizzare il codice sorgente degli oggetti per comprenderlo. Mentre con le funzioni di Hamilton, una chiara linea di dipendenza ti indica dove guardare e ti aiuta a comprendere ciò che è successo (ne parleremo più avanti)!
2. Personalizzazione/estensioni. Sfortunatamente, per modificare i framework attuali al di fuori dei limiti di ciò che rendono “facile” da fare, è necessario un solido set di competenze di ingegneria del software. Se ciò non è possibile, significa che potresti finire per uscire dal framework per una particolare logica di business personalizzata, il che potrebbe portarti involontariamente a mantenere una superficie di codice maggiore rispetto a se non avessi usato il framework in primo luogo.
Per saperne di più su questi due punti, ti segnaliamo discussioni come queste due (hacker news, reddit) in cui gli utenti parlano in dettaglio.
Sebbene Hamilton non sia un completo sostituto dei framework LLM attuali (ad esempio, non c’è un componente “agente”), ha tutti gli elementi essenziali per soddisfare le esigenze delle tue applicazioni LLM e può essere utilizzato in combinazione con essi. Se desideri un modo pulito, chiaro e personalizzabile per scrivere codice di produzione, integrare diversi componenti dello stack LLM e ottenere osservabilità sulla tua app, allora passiamo alle prossime sezioni!
Costruire con Hamilton
Hamilton è un micro-framework dichiarativo per descrivere flussi di dati in Python. Non è un nuovo framework (ha più di 3,5 anni) ed è stato utilizzato per anni nella modellazione di dati e flussi di apprendimento automatico in produzione. La sua forza sta nell’esprimere il flusso di dati e calcolo in un modo che è semplice da creare e mantenere (come fa DBT per SQL), il che lo rende molto adatto per supportare la modellazione dei dati e delle esigenze computazionali delle applicazioni LLM.

Le basi di Hamilton sono semplici e può essere esteso in molti modi; non è necessario conoscere Hamilton per trarne vantaggio da questo post, ma se sei interessato, dai un’occhiata a:
- tryhamilton.dev – un tutorial interattivo nel tuo browser!
- Trasformazioni dei dati di Pandas in Hamilton in 5 minuti
- Lineage + Hamilton in 10 minuti
- Hamilton + Airflow per la produzione
Passiamo al nostro esempio
Per aiutarti a mettere in contesto, immagina questo. Sei un piccolo team di dati incaricato di creare un’applicazione LLM per “chattare” con i documenti della tua organizzazione. Credi che sia importante valutare le architetture candidate in termini di funzionalità, profilo delle prestazioni, licenze, requisiti di infrastruttura e costi. Alla fine, sai che la principale preoccupazione della tua organizzazione è fornire i risultati più rilevanti e un’ottima esperienza utente. Il modo migliore per valutare questo è costruire un prototipo, testare diverse soluzioni e confrontarne le caratteristiche e gli output. Poi, quando passerai alla produzione, vorrai avere la certezza che il sistema possa essere facilmente mantenuto ed esaminato, per fornire in modo coerente un’ottima esperienza utente.
Con questo in mente, in questo esempio, implementeremo parte di un’applicazione LLM, nello specifico il passaggio di acquisizione dei dati per indicizzare una base di conoscenza, in cui convertiamo il testo in embedding e li archiviamo in un database vettoriale. Implementiamo questo in modo modulare con alcuni diversi servizi/tecnologie. I passaggi generali sono:
- Caricare l’insieme di dati SQuAD dallo HuggingFace Hub. Sostituireste questo con il vostro corpus di documenti preelaborati.
- Embedding delle voci di testo utilizzando la Cohere API, l’OpenAI API o la libreria SentenceTransformer.
- Archiviare gli embedding in un database vettoriale, sia LanceDB, Pinecone o Weaviate.
Se avete bisogno di saperne di più sugli embedding e sulla ricerca, vi indirizziamo ai seguenti link:
- Spiegazione degli embedding di testo – Weaviate
- Come condurre una ricerca semantica con Pinecone
Mentre stiamo seguendo questo esempio, potrebbe essere utile per voi pensare a/tenere a mente quanto segue:
- Confrontate ciò che vi mostriamo con ciò che state facendo ora. Vedete come Hamilton vi consente di curare e strutturare un progetto senza aver bisogno di un framework esplicitamente incentrato su LLM.
- Struttura del progetto e dell’applicazione. Comprendere come Hamilton imponga una struttura che vi permette di creare e mantenere uno stack modulare.
- Convinzione nell’iterazione e nella longevità del progetto. Combinando i due punti precedenti, Hamilton vi permette di mantenere più facilmente un’applicazione LLM in produzione, indipendentemente da chi l’ha creata.
Iniziamo con una visualizzazione per darvi una panoramica di ciò di cui stiamo parlando:

Ecco come apparirebbe il flusso di dati dell’applicazione LLM quando si utilizza Pinecone con sentence transformers. Con Hamilton, capire come le cose sono collegate è semplice quanto chiamare display_all_functions()
sull’oggetto driver di Hamilton.
Codice modulare
Spieghiamo le due principali modalità di implementazione del codice modulare con Hamilton utilizzando il nostro esempio per contestualizzare.
@config.when
Hamilton si concentra sulla leggibilità. Senza spiegare cosa faccia @config.when
, probabilmente potete capire che si tratta di una dichiarazione condizionale, inclusa solo quando il predicato è soddisfatto. Qui di seguito troverete l’implementazione per la conversione del testo in embedding con l’OpenAI e la Cohere API.
Hamilton riconoscerà due funzioni come implementazioni alternative grazie al decoratore @config.when
e allo stesso nome di funzione embeddings
preceduto da un doppio trattino basso (__cohere
, __openai
). Le loro firme di funzione non devono essere necessariamente identiche, il che significa che è facile e chiaro adottare implementazioni diverse.
embedding_module.py
Per questo progetto, aveva senso avere tutti i servizi di embedding implementati nello stesso file con il decoratore @config.when
, poiché ci sono solo 3 funzioni per servizio. Tuttavia, man mano che il progetto cresce in complessità, le funzioni potrebbero essere spostate in moduli separati e utilizzato invece il pattern di modularità della sezione successiva. Un altro punto da notare è che ciascuna di queste funzioni è indipendentemente testabile. Se avete esigenze specifiche, è facile incapsularle nella funzione e testarle.
Sostituzione dei moduli Python
Qui di seguito troverete l’implementazione delle operazioni di database vettoriale per Pinecone e Weaviate. Notate che gli snippet provengono da pinecone_module.py
e weaviate_module.py
e notate come le firme delle funzioni si somiglino e differiscano.
pinecone_module.py e weaviate_module.py
Con Hamilton, il flusso dei dati viene assemblato utilizzando i nomi delle funzioni e gli argomenti di input delle funzioni. Pertanto, condividendo i nomi delle funzioni per operazioni simili, i due moduli sono facilmente interscambiabili. Poiché le implementazioni di LanceDB, Pinecone e Weaviate risiedono in moduli separati, si riduce il numero di dipendenze per file e li rende più brevi, migliorando sia la leggibilità che la manutenibilità. La logica per ciascuna implementazione è chiaramente incapsulata in queste funzioni denominate, quindi l’implementazione dei test unitari è facile da realizzare per ciascun modulo rispettivo. I moduli separati ribadiscono l’idea che non dovrebbero essere caricati contemporaneamente. Il driver di Hamilton in realtà genererà un errore quando vengono trovate più funzioni con lo stesso nome, il che aiuta a imporre questo concetto.
Implicazioni per il driver
La parte chiave per l’esecuzione del codice di Hamilton è l’oggetto Driver
trovato in run.py
. Escludendo il codice per l’interfaccia a riga di comando e l’analisi degli argomenti, otteniamo:
Estratto di run.py
Il driver di Hamilton, che coordina l’esecuzione ed è ciò attraverso cui si manipola il flusso dei dati, consente la modularità attraverso tre meccanismi come si vede nell’estratto di codice sopra riportato:
- Configurazione del driver. Questa è una matrice che il driver riceve all’istanziazione contenente informazioni che dovrebbero rimanere costanti, come ad esempio quale API utilizzare o la chiave API del servizio di embedding. Questo si integra bene con un piano di comando che può passare JSON o stringhe (ad esempio un container Docker, Airflow, Metaflow, ecc.). Concretamente qui specificheremmo quale API di embedding utilizzare.
- Moduli del driver. Il driver può ricevere un numero arbitrario di moduli Python indipendenti per costruire il flusso di dati. Qui, il modulo
vector_db_module
può essere sostituito con l’implementazione desiderata del database di vettori a cui ci stiamo collegando. È anche possibile importare moduli dinamicamente tramite importlib, il che può essere utile per contesti di sviluppo rispetto a contesti di produzione e consente anche un modo basato sulla configurazione per cambiare l’implementazione del flusso di dati. - Esecuzione del driver. Il parametro
final_vars
determina quale output deve essere restituito. Non è necessario ristrutturare il proprio codice per cambiare l’output desiderato. Prendiamo ad esempio il caso di voler debuggare qualcosa nel nostro flusso di dati, è possibile richiedere l’output di qualsiasi funzione aggiungendo il suo nome afinal_vars
. Ad esempio, se si ha un output intermedio da debuggare, è facile richiederlo o interrompere completamente l’esecuzione in quel punto. Si noti che il driver può ricevere i valoriinputs
eoverrides
durante la chiamata aexecute()
; nel codice sopra riportato, ilclass_name
è uninput
al momento dell’esecuzione che indica l’oggetto di embedding che vogliamo creare e dove memorizzarlo nel nostro database di vettori.
Sintesi della modularità
In Hamilton, la chiave per abilitare i componenti intercambiabili è:
- definire funzioni con lo stesso nome e poi,
- annotarle con
@config.when
e scegliere quale utilizzare tramite la configurazione passata al driver, o, - inserirle in moduli Python separati e passare il modulo desiderato al driver.
Abbiamo appena mostrato come è possibile collegare, sostituire e chiamare vari componenti LLM con Hamilton. Non abbiamo bisogno di spiegare cos’è una gerarchia orientata agli oggetti, né di richiedere una vasta esperienza nell’ingegneria del software per seguirlo (speriamo!). Per realizzare questo, abbiamo solo dovuto abbinare i nomi delle funzioni e i loro tipi di output. Pensiamo che questo modo di scrivere e modularizzare il codice sia quindi più accessibile rispetto a quanto consentano i framework LLM attuali.
Codice di Hamilton in pratica
Per aggiungere alle nostre affermazioni, ecco alcune implicazioni pratiche della scrittura di codice di Hamilton per i flussi di lavoro LLM che abbiamo osservato:
CI/CD
Questa capacità di sostituire moduli/@config.when
significa anche che i test di integrazione in un sistema CI sono facili da considerare, poiché si ha la flessibilità e la libertà di sostituire/isolare parti del flusso di dati come desiderato.
Collaborazione
- La modularità che Hamilton permette consente di superare facilmente i confini tra team. I nomi delle funzioni e i loro tipi di output diventano un contratto, che garantisce la possibilità di apportare modifiche chirurgiche e di avere fiducia nel cambiamento, oltre a fornire la visibilità sulle dipendenze a valle con le funzionalità di visualizzazione e tracciabilità di Hamilton (come la visualizzazione iniziale che abbiamo visto). Ad esempio, è chiaro come interagire e consumare dal database di vettori.
- Le modifiche al codice sono più semplici da revisionare, perché il flusso è definito da funzioni dichiarative. Le modifiche sono autocontenute; poiché non c’è una gerarchia orientata agli oggetti da apprendere, solo una funzione da modificare. Tutto ciò “personalizzato” è supportato de facto da Hamilton.
Debugging
Quando si verifica un errore con Hamilton, è chiaro a quale codice si riferisce e a causa di come la funzione è definita, si sa dove inserirla all’interno del flusso di dati.
Prendiamo l’esempio semplice della funzione di embedding che utilizza cohere. Se ci fosse un timeout o un errore nel parsing della risposta, sarebbe chiaro che si riferisce a questo codice e dalla definizione della funzione si saprebbe dove si inserisce nel flusso.
@config.when(embedding_service="cohere")def embeddings__cohere( embedding_provider: cohere.Client, text_contents: list[str], model_name: str = "embed-english-light-v2.0",) -> list[np.ndarray]: """Convertire il testo in rappresentazioni vettoriali (embedding) utilizzando Cohere Embed API riferimento: https://docs.cohere.com/reference/embed """ response = embedding_provider.embed( texts=text_contents, model=model_name, truncate="END", ) return [np.asarray(embedding) for embedding in response.embeddings]

Suggerimenti per creare uno stack modulare di LLM
Prima di finire, ecco alcune idee per guidarti nella costruzione della tua applicazione. Alcune decisioni potrebbero non avere una scelta migliore ovvia, ma avendo un approccio corretto alla modularità ti consentirà di iterare efficientemente man mano che le esigenze evolvono.
- Prima di scrivere qualsiasi codice, disegna un DAG dei passaggi logici del tuo flusso di lavoro. Questo stabilisce le basi per definire passaggi comuni e interfacce che non sono specifici del servizio.
- Identifica i passaggi che potrebbero essere sostituiti. Essendo intenzionale con i punti di configurazione, ridurrai i rischi di generalità speculativa. Concretamente, ciò si tradurrebbe in funzioni con meno argomenti, valori predefiniti e raggruppate in moduli tematici.
- Dividi parti del tuo flusso di dati in moduli con poche dipendenze, se rilevante. Ciò porterà a file Python più brevi con meno dipendenze di pacchetti, migliorando la leggibilità e la manutenibilità. Hamilton è indifferente e può costruire il suo DAG da moduli multipli.
Per concludere e direzioni future
Grazie per essere arrivato fino a qui. Crediamo che Hamilton abbia un ruolo da svolgere nell’aiutare tutti a esprimere i loro flussi di dati, e le applicazioni LLM sono solo un caso d’uso! Riassumendo il nostro messaggio in questo post:
- È utile concepire le applicazioni LLM come flussi di dati e quindi sono perfette per l’uso di Hamilton.
- I framework LLM centrati sugli oggetti possono essere opachi e difficili da estendere e mantenere per le tue esigenze di produzione. Invece, si dovrebbero scrivere le proprie integrazioni con lo stile dichiarativo chiaro di Hamilton. In questo modo si migliorerà la trasparenza e la manutenibilità del codice, con funzioni chiare testabili, mappatura chiara degli errori di runtime alle funzioni e visualizzazione integrata del flusso di dati.
- La modularità prescritta dall’uso di Hamilton renderà la collaborazione più efficiente e fornirà la flessibilità necessaria per modificare e cambiare i flussi di lavoro LLM alla velocità con cui si muove il settore.
Ti invitiamo ora a giocare, provare e modificare l’esempio completo da soli qui. C’è un README
che spiegherà i comandi da eseguire e come iniziare. In caso contrario, stiamo lavorando per rendere ancora migliore l’esperienza di Hamilton + Applicazione LLM pensando alle seguenti cose:
- Agenti. Possiamo fornire lo stesso livello di visibilità agli agenti che abbiamo per i flussi di dati regolari di Hamilton?
- Parallelizzazione. Come possiamo semplificare l’espressione dell’esecuzione di un flusso di dati su un elenco di documenti, ad esempio. Vedi questo PR in corso per capire cosa intendiamo.
- Plugin per la memorizzazione nella cache e l’osservabilità. Si può già implementare una soluzione personalizzata di memorizzazione nella cache e osservabilità su Hamilton. Stiamo lavorando per fornire più opzioni standard di componenti comuni, ad esempio redis.
- Una sezione di flussi di dati contribuiti dagli utenti. Vediamo la possibilità di standardizzare nomi comuni per casi d’uso specifici di applicazioni LLM. In tal caso potremmo iniziare ad aggregare i flussi di dati di Hamilton e consentire alle persone di scaricarli per le proprie esigenze.
Vogliamo sentirti!
Se sei entusiasta di tutto ciò o hai opinioni forti, passa al nostro canale Slack / o lascia alcuni commenti qui! Alcune risorse per aiutarti:
📣 unisciti alla nostra community su Slack – siamo più che felici di aiutarti a rispondere alle domande che potresti avere o per farti iniziare.
⭐️ valutaci su GitHub
📝 lasciaci un problema se trovi qualcosa
Altri post di Hamilton che potrebbero interessarti:
- tryhamilton.dev – un tutorial interattivo nel tuo browser!
- Trasformazioni dei dati di Pandas in Hamilton in 5 minuti
- Lineage + Hamilton in 10 minuti
- Hamilton + Airflow per la produzione