Python, de la zero Lecția 59 / 60

AI vs ML in 2026: cand apelezi un LLM, cand antrenezi

Decizia care nu exista acum cinci ani: folosesti un model gazduit, faci fine-tune unuia open sau antrenezi propriul model?

Cu câțiva ani în urmă, „a face ML” avea o formă clară. Adunai date etichetate, le curățai, antrenai un model, îl evaluai, îl puneai în producție în spatele unui API. Bucla întreagă lua luni, iar mare parte din buget mergea pe munca de date. Lumea aia încă există, dar acum e o submulțime a unui peisaj mult mai dezordonat.

În 2026, prima întrebare la un proiect nou nu e „ce model antrenez?”. E „chiar trebuie să antrenez ceva?”. Pentru o clasă enormă și în creștere de probleme, răspunsul corect e: îndrepți un LLM găzduit spre input și ceri politicos. Pentru altă clasă: faci fine-tune unui model open-source pe hardware ieftin. Pentru o a treia, pipeline-ul clasic pe care l-ai învățat în modulul 9 încă e corect. A alege dintre ele e cea mai consecventă decizie de design din domeniu chiar acum.

Lecția asta e un cadru de decizie, cu cod. Niciun algoritm nou, doar judecată câștigată cu greu despre ce unealtă iei de pe raft.

Arborele de decizie

Trei căsuțe. Alege-o pe cea mai din stânga în care se potrivește problema ta.

Apelează un LLM găzduit (Claude, GPT-5, Gemini) când:

  • Task-ul e ceva ce un om deștept ar putea face primind input-ul ca text, fără antrenament special. Draftuire, sumarizare, extracție, clasificare pe categorii noi, reformatare, raționament ușor.
  • Latența tolerează 1-3 secunde.
  • Nu ai nevoie de determinism bit-cu-bit sau de explicabilitate strictă.
  • Volumul e mic-spre-mediu, să zicem < 1M apeluri API pe zi sau trafic în rafale imprevizibile.
  • Nu ai sau nu poți obține destule date de antrenare etichetate ca să faci fine-tune cu sens.
  • Costul-pe-apel (în prezent undeva între 0,001 $ și 0,05 $ în funcție de model și lungime) se potrivește economiei tale unitare.

Fă fine-tune unui model open-source (un Llama, Mistral, Qwen sau o bază specifică unui domeniu) când:

  • Ai un task specializat recurent cu cel puțin câteva sute de exemple etichetate.
  • Costurile cu LLM găzduit la volumul tău fac un model fine-tunat de 7B pe propriul GPU evident mai ieftin.
  • Cerințele de latență sunt strânse (< 200 ms): inferența locală pe hardware-ul tău bate dus-întors la un API.
  • Confidențialitatea, rezidența datelor sau reglementările interzic trimiterea datelor în afară.
  • Task-ul e suficient de îngust încât un mic specialist bate un generalist gigant.

Antrenează de la zero (sau antrenează un model ML non-LLM de la zero) când:

  • Construiești modele de embedding pentru retrieval; un encoder mai mic, mai rapid, ajustat pe domeniu de obicei câștigă.
  • Task-ul e previziune de serii temporale, recomandare, detecție de anomalii sau altă problemă tabulară/secvențială unde LLM-urile pur și simplu nu au forma potrivită. Modulul 9 încă se aplică aici, neatins.
  • Lucrezi într-o modalitate nouă (date de senzori proprietari, imagini specifice unui domeniu, secvențe biologice) unde nu există un model pre-antrenat util.

Marea majoritate a „proiectelor ML” din 2026 cad în primele două căsuțe. O fracțiune semnificativă, dar mai mică, rămâne în a treia, iar a recunoaște când ML-ul clasic încă învinge e parte din meserie.

Șablonul hibrid: LLM-uri ca adaptoare

Cele mai robuste arhitecturi de producție nu tratează LLM-ul ca pe sistemul întreg. Îl folosesc ca pe un adaptor care schimbă forma, care stă la graniță, primind input nestructurat și mizerabil (e-mailuri în text liber, transcrieri vocale, documente scanate) și emițând date structurate ordonate pe care un pipeline clasic în aval le poate gestiona.

from anthropic import Anthropic
import json

client = Anthropic()

PROMPT = """You will receive a customer support email. Extract:
- intent: one of [refund, technical_issue, account_question, other]
- urgency: one of [low, medium, high]
- mentions_competitor: boolean
- contains_legal_threat: boolean

Reply with ONLY a JSON object, no prose."""

def classify_email(body: str) -> dict:
    msg = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=200,
        system=PROMPT,
        messages=[{"role": "user", "content": body}],
    )
    return json.loads(msg.content[0].text)

Output-ul ăla apoi alimentează un sistem de rutare determinist, un warehouse SQL, dashboard-uri, alerte. Partea imprevizibilă, înțelegerea limbajului natural, e izolată în spatele unei granițe tipate. Avalul rămâne codul plictisitor, testabil, auditabil pe care l-ai exersat decenii.

Ăsta e șablonul care chiar câștigă în producție: LLM la margini, inginerie clasică la mijloc. Obții magia unde ai nevoie și predictibilitatea unde ai nevoie de aia.

RAG: șablonul dominant pentru „răspunde la întrebări despre datele mele”

Aproape orice cerință de tipul „construiește-mi un chatbot pentru documentația noastră” e de fapt o cerere pentru retrieval-augmented generation. LLM-ul nu cunoaște datele tale private; nu poți încăpea toate datele în fereastra de context; fine-tuning-ul oricum nu e o cale grozavă de a adăuga cunoștințe. Soluția:

  1. Împarți documentele în chunk-uri.
  2. Calculezi un embedding pentru fiecare chunk.
  3. Stochezi embedding-urile într-un index vectorial.
  4. La interogare, faci embedding întrebării utilizatorului, găsești top-k cele mai similare chunk-uri și le bagi în prompt.

Atât. LLM-ul răspunde din contextul recuperat. Cincizeci de linii de Python:

import os
from pathlib import Path
from sentence_transformers import SentenceTransformer
import chromadb
from anthropic import Anthropic

# 1. Modelul de embedding si vector store
embedder = SentenceTransformer("BAAI/bge-small-en-v1.5")
chroma = chromadb.PersistentClient(path="./rag_store")
coll = chroma.get_or_create_collection("docs")

# 2. Indexeaza documentele
def chunk(text: str, size: int = 600, overlap: int = 100) -> list[str]:
    chunks = []
    i = 0
    while i < len(text):
        chunks.append(text[i:i + size])
        i += size - overlap
    return chunks

def index_folder(folder: str) -> None:
    docs, ids, metas = [], [], []
    for path in Path(folder).rglob("*.md"):
        text = path.read_text(encoding="utf-8")
        for j, c in enumerate(chunk(text)):
            docs.append(c)
            ids.append(f"{path.stem}-{j}")
            metas.append({"source": str(path)})
    embeddings = embedder.encode(docs, normalize_embeddings=True).tolist()
    coll.upsert(ids=ids, documents=docs, embeddings=embeddings, metadatas=metas)

# 3. Interogare
client = Anthropic()

def answer(question: str, k: int = 4) -> str:
    q_emb = embedder.encode([question], normalize_embeddings=True).tolist()
    hits = coll.query(query_embeddings=q_emb, n_results=k)
    context = "\n\n---\n\n".join(hits["documents"][0])
    sources = [m["source"] for m in hits["metadatas"][0]]

    msg = client.messages.create(
        model="claude-opus-4-7",
        max_tokens=600,
        system=(
            "Answer the question using ONLY the provided context. "
            "If the answer isn't in the context, say so."
        ),
        messages=[{
            "role": "user",
            "content": f"<context>\n{context}\n</context>\n\nQuestion: {question}",
        }],
    )
    return msg.content[0].text + f"\n\nSources: {sorted(set(sources))}"

if __name__ == "__main__":
    index_folder("./my_docs")
    print(answer("How does our refund policy handle digital goods?"))

Ăsta e un sistem RAG funcțional. Trei componente: un model de embedding (bge-small-en-v1.5 e excelent și mic), un vector store (ChromaDB e ok până la ~1M chunk-uri; pentru mai mult, uită-te la Qdrant, Weaviate sau pgvector) și un apel la LLM care consumă contextul recuperat.

Framework-uri ca LangChain și LlamaIndex împachetează toate astea și adaugă streaming, agenți, rescriere de query, căutare hibridă. Sunt utile, dar grele; pentru o primă versiune, abordarea directă de 50 de linii de mai sus e adesea mai clară de debugat. Adaugi un framework când chiar ai nevoie de feature-urile lui, nu din oficiu.

Ce ajustezi într-un sistem RAG real, în ordine aproximativă a impactului:

  1. Strategia de chunking. Splituri naive pe dimensiune fixă funcționează, dar splittere recursive care respectă granițele de propoziții și paragrafe funcționează mai bine. Splittere conștiente de markdown pentru markdown.
  2. Retrieval. Top-k pură pe cosinus e baseline-ul. Căutarea hibridă (BM25 + dense) o bate. Rerankarea cu un cross-encoder (de exemplu bge-reranker-v2-m3) pe primii 20-50 de candidați e încă un boost mare.
  3. Calitatea modelului de embedding. Un embedder modern de engleză e ok; pentru corpuri multilingve sau de domeniu, schimbă pe un model antrenat pe datele potrivite.
  4. Template-ul de prompt. Spune modelului ce contează ca context, ce să facă atunci când răspunsul nu e prezent, ce format vrei.
  5. LLM-ul în sine. Treci la un model mai mare doar după ce de mai sus sunt ajustate; retrieval-ul e de obicei strangularea, nu generatorul.

Matematica costurilor

O întrebare comună care conduce decizia: la ce volum un model fine-tunat propriu bate plata per apel?

Un calcul aproximativ. Să presupunem că task-ul tău are în medie 800 de tokeni de input și 300 de tokeni de output per apel. Cu un model găzduit de tier frontier la, să zicem, 5 $/M input + 25 $/M output, fiecare apel costă:

(800 / 1_000_000) * 5 + (300 / 1_000_000) * 25
= 0.0040 + 0.0075
= $0.0115 per call

La 1M apeluri/lună: 11.500 $/lună. La 10M apeluri/lună: 115.000 $/lună.

Un model fine-tunat de 7B self-hosted pe o singură instanță A100 (în prezent ~1,50 $/oră rezervat) costă ~1.100 $/lună. Poate face confortabil milioane de apeluri dacă bugetul tău de latență permite. Punctul de break-even e undeva între 100K-200K apeluri pe lună, dacă un 7B fine-tunat e destul de bun la task-ul tău.

Deci pentru muncă de volum mic/mediu, găzduit e mai ieftin și mai bun la calitate. Pentru muncă specializată de volum mare, deținerea modelului câștigă. Punctul de încrucișare se tot mută: prețurile găzduite scad ~30% pe an, iar ponderile open devin ~30% mai bune la aceeași dimensiune, deci re-rulează calculul la fiecare șase luni. Decizia nu e fixă.

Când ML-ul clasic încă învinge, și e mai des decât crede lumea

O parte zgomotoasă din industrie se comportă ca și cum totul ar fi acum un LLM. Nu. Problemele numerice, tabulare și de serii temporale încă bat LLM-urile la acuratețe, latență, cost și explicabilitate, de obicei cu ordine de mărime pe fiecare axă.

Concret:

  • Predicția churn-ului dintr-un tabel cu trăsături despre clienți. Gradient-boosted trees, de fiecare dată. (Vezi lecția 54.)
  • Previziune de cerere / serii temporale. Prophet, ARIMA sau un model deep antrenat specific pe serii temporale. Un LLM e o pereche de pantofi de clovn pentru așa ceva.
  • Detecție de anomalii pe metrici. Isolation forests, statistical control charts, detectoare dedicate.
  • Sisteme de recomandare. Modele two-tower, factorizare de matrice, learned-to-rank. LLM-urile sunt uneori folosite ca rerankere, dar niciodată ca retriever-ul de bază.
  • Computer vision pe o taxonomie fixă. Un fine-tune de ConvNeXt sau ViT bate prompting-ul VLM la cost și acuratețe.

Regula de aproximare: când input-ul e natural un vector de numere sau un rând structurat, ML-ul clasic de obicei câștigă. Când input-ul e natural un paragraf de text liber sau o imagine pe care ai descrie-o în cuvinte, un LLM are avantajul. Mare parte din munca de data engineering trăiește în prima categorie.

Principiul: LLM-urile ca multiplicator de forță, nu ca înlocuitor

Echipele care scot cea mai mare valoare din AI în 2026 nu sunt cele care înlocuiesc fiecare componentă cu un LLM. Sunt cele care obișnuiau să ia trei săptămâni ca să construiască un pipeline și acum o fac în trei zile, pentru că LLM-ul s-a ocupat de bucățile mizerabile, în formă de text, parsarea, clasificarea, sumarizarea, care obișnuiau să ceară ajustare manuală.

Ingineria nu a dispărut. Munca de date nu a dispărut. Nevoia de teste, monitorizare, scheme și control al versiunii nu a dispărut. Ce a dispărut a fost o categorie specifică de fricțiune la granița dintre input uman nestructurat și procesare structurată de mașină. Categoria aia era enormă. Eliminarea ei mută ce e posibil. Nu elimină nevoia de inginerie software.

Dacă iei un singur lucru din lecția asta: întrebarea la fiecare proiect nou nu mai e „ce model construim?”. E „unde trăiește problema asta de fapt pe spectrul de la prompt la fine-tune la de-la-zero?”. Alegerea corectă te duce de la „nu poate fi făcută în bugetul nostru” la „livrăm trimestrul viitor”. Alegerea greșită arde bani pe care nu trebuia să îi cheltui, în orice direcție.

Ce urmează

Asta e penultima lecție a cursului. Modulul 10 a acoperit fundamentele deep learning-ului (lecțiile 56-57), workflow-ul transfer learning + Hugging Face (lecția 58) și decizia AI-vs-ML (asta). Lecția următoare e capstone-ul: o privire înapoi la ce ai construit pe parcursul celor 60 de lecții și o privire înainte la unde să mergi mai departe.


Referințe: Anthropic Python SDK (https://docs.anthropic.com/en/api/client-sdks), OpenAI Python SDK (https://platform.openai.com/docs/libraries), sentence-transformers (https://www.sbert.net/), documentația ChromaDB (https://docs.trychroma.com/), modelele de embedding BAAI BGE pe Hugging Face, documentația LlamaIndex (https://docs.llamaindex.ai/). Consultat 2026-05-01.

Caută