ADR 0012: Adottare la giunzione LLM di corespine — chat / ChatCompletion al posto di create_message / ProviderResponse
Spostare il Protocol LLMProvider nel core condiviso della famiglia (corespine) e adottare la forma chat-completions di OpenAI; aggiungere un throttle TPM attivo opzionale tramite RateLimitedProvider di corespine.
Stato: accettato · Data: 2026-06-20
Registro immutabile. Esente dal tracciamento della deriva (nessun
covers). Sostituire con un nuovo ADR, non modificare.
Correlato a 0009 (politica su framework/dipendenze) e a 0011 (adozione del python-project-standard). Rilasciato in rag-spine 0.3.0.
Contesto
RAGSpine ha sempre isolato ogni SDK esterno dietro un Protocol tipizzato, con un
core offline privo di SDK e un MockProvider predefinito (ADR 0005 /
0009). La giunzione LLMProvider era definita all'interno di ragspine
(agent/llm_provider.py) con un metodo specifico di ragspine:
def create_message(*, system, messages, tools) -> ProviderResponse: ...Due pressioni sono confluite:
- Una famiglia di pacchetti fratelli ora condivide le stesse primitive. ragspine,
spineagent(orchestrazione multi-agente) e il sottile core condivisocorespinedevono tutti comunicare con un LLM attraverso un unico contratto di provider. Duplicare un Protocol di provider su misura per ciascun pacchetto avrebbe frammentato il contratto e le giunzioni di test. corespine è il core condiviso deliberatamente sottile della famiglia, pensato esattamente per queste primitive neutrali rispetto al dominio. - La forma su misura di
create_message(*, system, messages, tools) -> ProviderResponseera gratuitamente non standard. Estrarre il prompt di sistema come argomento separato e inventare un envelopeProviderResponsedivergeva dalla forma chat-completions di OpenAI, ampiamente compresa, che gli SDK reali (e il modello mentale di ogni autore di adapter) già parlano — attrito senza alcun beneficio.
Separatamente, l'unica difesa contro i rate limit era il backoff passivo sui 429 di max_retries dell'SDK,
che reagisce dopo che un limite è stato raggiunto. Un client che voglia restare sotto un tetto TPM
contrattuale non aveva alcuna leva interna alla libreria.
Decisione
Spostare il Protocol LLMProvider in corespine e adottare la forma chat-completions di OpenAI.
ragspine assume una dipendenza runtime rigida da corespine>=0.1.0 (la sua unica non opzionale) e
riesporta la giunzione da agent/llm_provider.py, in modo che i punti di import esistenti
(from ragspine.agent.llm_provider import LLMProvider, MockProvider, AnthropicProvider) continuino a
funzionare. Concretamente:
- Il metodo unico diventa
chat(messages: list[dict[str, Any]], *, tools: list[dict[str, Any]] | None = None) -> ChatCompletion. Il prompt di sistema è semplicemente il primo messaggio{"role": "system", ...}— nessun argomento separato. ProviderResponseviene sostituito dall'envelopeChatCompletiondi corespine:choices(ciascuno unaChoiceche incapsula unResponseMessageconcontent+tool_calls), piùusage,modeleid— tutte dataclass frozen, la forma chat-completions standard.AnthropicProvidercontinua a importare in modalità lazy l'SDKanthropic, ma ora mappamessages/toolsnella forma OpenAI sull'API Messages di Anthropic e riporta la risposta in unChatCompletion.ProviderErrordiventa una sottoclasse dicorespine.CorespineError.- Il
MockProvideroffline e il loop di tool-use (provider.chat(messages, tools=tools)) vengono aggiornati in parallelo; la protezione di prevenzione delle risposte inventate resta intatta (non si è mai fidata della prosa del provider per il numero).
Aggiungere un throttle TPM attivo opzionale. Una nuova variabile d'ambiente RAGSPINE_TOKENS_PER_MINUTE (int,
default 0 = disattivato) fa sì che build_provider avvolga il provider costruito nel
RateLimitedProvider di corespine — un limite soft a finestra scorrevole che blocca prima di una chiamata chat quando la
finestra non ha budget disponibile. È complementare al max_retries passivo dell'SDK: uno resta
proattivamente sotto il tetto, l'altro recupera da uno sforamento.
Alternative considerate (respinte)
- Mantenere la giunzione su misura
create_message/ProviderResponsein ragspine. Respinta — essa frammenta il contratto attraverso la famiglia e resta inutilmente non standard. La giunzione è una primitiva neutrale rispetto al dominio da manuale, che appartiene al core condiviso. - Definire la giunzione in ragspine e far dipendere i pacchetti fratelli da ragspine. Respinta — inverte la direzione delle dipendenze (un pacchetto RAG diventerebbe una dipendenza di un pacchetto agente generico). corespine è il punto di raccolta concordato per le primitive condivise; le frecce puntano verso di esso.
- Affidarsi soltanto a
max_retriesdell'SDK per il rate limiting. Respinta — il backoff passivo non può mantenere un client sotto un tetto TPM contrattuale; i due meccanismi sono complementari, non ridondanti.
Conseguenze
- Breaking per i provider personalizzati. Chiunque abbia implementato
create_message(*, system, messages, tools) -> ProviderResponsedeve migrare achat(messages, *, tools=None) -> ChatCompletion(importando le dataclass dacorespine, oppure tramite la riesportazione inragspine.agent.llm_provider). La guida Punti di estensione contiene un esempio OpenAI completo nella nuova forma. - ragspine acquisisce esattamente una nuova dipendenza non opzionale (
corespine>=0.1.0, che a sua volta hadependenciesvuote). Il percorso predefinito offline-first e privo di SDK pesanti è preservato. - Il contratto di provider è ora condiviso in tutta la famiglia Spine, quindi un provider scritto per un pacchetto funziona per tutti.
RAGSPINE_TOKENS_PER_MINUTEè l'unica nuova superficie di configurazione; tutto il resto è interno.
ADR 0011: Adottare il python-project-standard; migrare al layout src/, mantenere il resto
Adottare lo standard interno come filosofia guida; allineare l'unico invariante strutturale delimitato (layout src/ragspine/) e accettare quattro divergenze documentate.
ADR 0013: compilatore YAML di workflow Dify → Python puro + advisor statico di ottimizzazione
Compilare un file .yml di workflow Dify in Python imperativo indipendente dai framework attraverso una IR de-Dify-izzata (parse → IR → codegen), con un advisor statico di ottimizzazione a otto regole — PyYAML dietro un nuovo extra [dify], completamente sincrono, zero nuove dipendenze nel core.