LangGraph: workflow-uri cu stare, routere și agenți multipli
De la agent RAG individual la thread multi-agent controlat. LangGraph nu este un model AI — este un framework pentru controlul fluxului: stare, noduri, muchii, decizii, bucle, oprire. În C7 îl folosim pentru a construi un thread în care mai mulți agenți văd conversația anterioară și sunt coordonați de un router.
01De ce avem nevoie de LangGraph
În C6 am construit un agent RAG individual. Un text politic intra în sistem, agentul recupera context din FAISS și producea un răspuns. Totul se întâmpla într-un singur apel — nu existau decizii intermediare, nici reluări, nici alți agenți în joc.
În C7 cerința se schimbă. Vrem mai mulți agenți care intervin într-o conversație: fiecare are rolul lui, fiecare vede ce s-a spus înainte, iar ordinea intervențiilor nu este fixă. Problema nu mai este cum generăm un răspuns, ci cum controlăm pașii: cine vorbește primul, cine urmează, când ne oprim, ce se păstrează între intervenții.
Generarea de text rămâne treaba modelului (din C6). LangGraph adaugă controlul: stare partajată, decizii pe baza ei, repetare până la o condiție de oprire. Aceasta este diferența fundamentală față de C6.
02Ce este LangGraph
LangGraph este un framework pentru construirea de workflow-uri AI ca grafuri. Un graf are noduri, muchii și o stare comună. Fiecare nod este o funcție Python care primește starea curentă și returnează modificările aduse. Muchiile definesc cum trece fluxul de la un nod la altul — fie fix („mereu A → B"), fie pe baza unei decizii („dacă X, atunci A; altfel B").
Mesajul central. LangGraph nu face modelul mai inteligent. Face fluxul explicit, controlabil și inspectabil. La fiecare pas se vede ce stare a citit nodul, ce a modificat și ce decizie a luat. Pentru cercetare socială asta înseamnă auditabilitate completă — putem reconstrui pas cu pas cum s-a ajuns la o conversație simulată.
Trei consecințe practice ale acestei alegeri arhitecturale, pentru un proiect ca EchoChamber:
- Reproducibilitate. Cu același state inițial și aceleași noduri, graful produce aceeași execuție. Conversațiile simulate pot fi rerulate și verificate.
- Modularitate. Schimbi un nod, restul rămâne neatins. Înlocuiești routerul round-robin cu un router LLM — graful funcționează în continuare.
- Inspecție. La fiecare pas poți printa starea. Nu este o cutie neagră — este un grafic Python pe care îl execuți pas cu pas.
03De la chain la graph
În cursurile anterioare am întâlnit două forme mai simple de a apela un model. LangGraph este forma a treia. Tabelul de mai jos arată când este suficientă fiecare.
| Tip de flux | Formă | Când este suficient |
|---|---|---|
| LLM call | input → LLM → output | răspuns unic, fără context structurat |
| Chain liniar | input → pas 1 → pas 2 → output | pași ficși, ordine cunoscută dinainte |
| LangGraph | state → node → decision → node / END | decizii, bucle, agenți multipli, stare |
Un chain este bun când ordinea pașilor este fixă și cunoscută dinainte. Un graph este necesar când sistemul trebuie să decidă la runtime ce se întâmplă mai departe — cine intervine, când se oprește, dacă reia un pas.
Concret în C7: nu putem ști dinainte ce va spune fiecare agent. Dar putem să spunem grafului cum să decidă cine vorbește la fiecare pas și când să se oprească. Aceste reguli devin parte din graf, nu din codul aplicației.
04Concepte fundamentale LangGraph
Înainte să construim primul graf, avem nevoie de un vocabular minim. Cinci concepte acoperă tot ce vom face în C7.
| Concept | Definiție simplă | Exemplu C7 |
|---|---|---|
State | datele care circulă prin graf | stimulus, messages, current_turn |
Node | funcție Python care modifică state-ul | router_node, agent_node |
Edge | trecere fixă între noduri | agent_node → router |
Conditional edge | alegere pe baza state-ului | router → agent sau router → END |
START / END | intrarea și ieșirea grafului | START → router; router → END |
State este memoria temporară a grafului. Toate nodurile citesc din ea și returnează ce modifică. Două noduri nu comunică direct între ele — comunicarea se face prin state. Această alegere face graful inspectabil: la orice moment, întreaga stare este vizibilă.
05Primul graf — fără LLM
Începem fără LLM ca să separăm mecanica LangGraph de generarea textului. Exemplul: un graf care numără cuvintele dintr-un text. Niciun apel către un model, niciun API key.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
# 1. State — datele care circulă prin graf
class TextState(TypedDict):
text: str
count: int
# 2. Node — funcție care primește state și returnează modificări
def count_words(state: TextState) -> dict:
n = len(state["text"].split())
return {"count": n} # doar ce s-a schimbat
# 3. Construim graful
workflow = StateGraph(TextState)
workflow.add_node("count_words", count_words)
workflow.add_edge(START, "count_words")
workflow.add_edge("count_words", END)
# 4. Compilăm și rulăm
graph = workflow.compile()
result = graph.invoke({"text": "LangGraph orchestrează workflow-uri AI.", "count": 0})
print(result)
# → {'text': 'LangGraph orchestrează workflow-uri AI.', 'count': 4}
Patru pași, fiecare cu un rol clar: definim ce date circulă (State), ce face un pas (Node), cum se leagă pașii (edges), apoi compilăm și rulăm. Toate grafurile LangGraph respectă această structură, oricât de complexe ar deveni.
Notă despre return. Nodul returnează doar câmpurile pe care le modifică, nu state-ul întreg. LangGraph aplică automat actualizările peste state-ul existent. text rămâne neschimbat fiindcă nodul nu l-a returnat.
06Graf cu decizie
Acum adăugăm prima decizie. În funcție de lungimea textului, graful merge la short_response sau la long_response. Aceasta este o muchie condițională.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
class LengthState(TypedDict):
text: str
result: str
# Noduri — ramurile
def short_response(state: LengthState) -> dict:
return {"result": "Text scurt."}
def long_response(state: LengthState) -> dict:
return {"result": "Text lung."}
# Routerul — decide ramura, returnează numele etichetei
def choose_path(state: LengthState) -> str:
if len(state["text"].split()) < 10:
return "short"
return "long"
# Construim graful
workflow = StateGraph(LengthState)
workflow.add_node("short_response", short_response)
workflow.add_node("long_response", long_response)
# Muchie condițională cu mapping explicit etichetă → nod
workflow.add_conditional_edges(
START,
choose_path,
{"short": "short_response", "long": "long_response"},
)
workflow.add_edge("short_response", END)
workflow.add_edge("long_response", END)
graph = workflow.compile()
graph.invoke({"text": "Salut lume.", "result": ""})
# → {'text': 'Salut lume.', 'result': 'Text scurt.'}
Mapping-ul explicit contează. Dicționarul {"short": "short_response", "long": "long_response"} spune lui LangGraph: „dacă choose_path returnează 'short', mergi la nodul short_response". Fără mapping, vizualizarea grafului nu poate desena muchiile corect.
Muchia condițională este mecanismul prin care un graf decide. Tot restul C7 se construiește în jurul ei: routerul nostru este o funcție exact ca choose_path, doar că în loc de „short" / „long" returnează numele agentului care urmează să vorbească.
07Vizualizarea grafului
După compile(), LangGraph poate desena automat graful ca diagramă Mermaid. Este util pentru a verifica vizual că workflow-ul este construit corect — mai ales când avem multe noduri și muchii condiționate.
from IPython.display import Image, display
# Imaginea PNG generată din graf (necesită conexiune pentru rendering)
display(Image(graph.get_graph().draw_mermaid_png()))
# Fallback: textul Mermaid brut, dacă rendering-ul nu merge
print(graph.get_graph().draw_mermaid())
Vizualizarea ajută să verificăm dacă workflow-ul este construit corect, înainte să-l rulăm. Dacă o muchie condițională nu apare în diagramă, înseamnă că mapping-ul lipsește sau este greșit. Cel mai bun debug este vizual.
Mai jos, varianta textuală Mermaid generată din graful cu decizie. Aceasta este reprezentarea pe care o desenează draw_mermaid_png():
---
config:
flowchart:
curve: linear
---
graph TD;
__start__([<p>__start__</p>]):::first
short_response(short_response)
long_response(long_response)
__end__([<p>__end__</p>]):::last
__start__ -.-> short_response;
__start__ -.-> long_response;
short_response --> __end__;
long_response --> __end__;
08Ce este un thread conversațional
În C7 nu vrem un singur răspuns — vrem o conversație. O conversație, din punct de vedere al codului, este o listă de mesaje păstrată în State. Fiecare intervenție adaugă un mesaj la listă. Routerul citește lista ca să decidă cine urmează.
Un mesaj are o structură minimă:
| Câmp | Rol |
|---|---|
agent | numele afișabil al agentului (ex. „Anti-sistem") |
slug | identificatorul unic, folosit de router (ex. anti_sistem) |
handle | handle-ul stilizat ca pe rețele sociale (ex. @LibertateRO99) |
text | conținutul intervenției, generat de agentul RAG |
turn | numărul de ordine al intervenției în thread |
HANDLES = {
"anti_sistem": "@LibertateRO99",
"conspirationist": "@AdevarulViu",
"pro_european": "@EuroOptimistRO",
}
messages = [
{"agent": "Anti-sistem", "slug": "anti_sistem",
"text": "Instituțiile par din nou rupte de oameni.", "turn": 1},
{"agent": "Pro-european", "slug": "pro_european",
"text": "Trebuie să discutăm pe baza procedurilor și a dovezilor.", "turn": 2},
]
def thread_to_text(messages: list) -> str:
lines = []
for m in messages:
handle = HANDLES.get(m["slug"], m["slug"])
lines.append(f"Turn {m['turn']} — {handle}: {m['text']}")
return "\n".join(lines)
print(thread_to_text(messages))
# Turn 1 — @LibertateRO99: Instituțiile par din nou rupte de oameni.
# Turn 2 — @EuroOptimistRO: Trebuie să discutăm pe baza procedurilor...
Pentru moment, mesajele sunt construite manual. Mai târziu, fiecare agent își va genera mesajul prin generate_agent_response() din C6. Funcția thread_to_text() este utilă atât pentru afișare, cât și pentru a trimite thread-ul anterior către un agent care urmează să răspundă.
09ThreadState — structura de date a conversației
ThreadState este State-ul specific lui C7. Toate câmpurile sunt simple: șiruri, liste, întregi. Fiecare nod citește din ThreadState și actualizează câmpurile relevante.
from typing import TypedDict
class ThreadState(TypedDict):
stimulus: str # inputul politic inițial
messages: list # mesajele produse până acum
active_slugs: list # agenții care participă la thread
total_turns: int # câte intervenții vrem în total
current_turn: int # câte au fost deja produse
next_slug: str # agentul ales pentru intervenția următoare
provider: str # "gemini" sau "deepseek"
k: int # câte fragmente FAISS recuperăm
| Câmp | Rol | Cine îl modifică |
|---|---|---|
stimulus | textul politic — fix pe toată durata thread-ului | nimeni (inițializat) |
messages | lista de mesaje a conversației | agent_node |
active_slugs | agenții participanți, în ordinea round-robin | nimeni (inițializat) |
total_turns | bugetul de intervenții | nimeni (inițializat) |
current_turn | contorul de intervenții — citit de router | agent_node |
next_slug | slug-ul agentului ales pentru tura curentă | router_node |
provider | furnizorul LLM | nimeni (inițializat) |
k | parametrul top-k pentru retriever | nimeni (inițializat) |
Diviziunea muncii. Routerul setează doar next_slug. Agent node-ul adaugă la messages și incrementează current_turn. Restul câmpurilor rămân neschimbate pe tot parcursul thread-ului. Această separare clară face debugging-ul ușor.
10Router simplu — round-robin
Routerul este inima sistemului multi-agent. El decide la fiecare pas cine vorbește și când se oprește thread-ul. Pentru C7 obligatoriu folosim round-robin — agenții vorbesc pe rând, în ordinea din active_slugs. Este simplu, stabil și explicabil.
def pick_next_round_robin(active_slugs: list, current_turn: int) -> str:
"""Alege agentul următor: tura N → al N-lea agent (modulo nr. agenți)."""
return active_slugs[current_turn % len(active_slugs)]
def router_node(state: ThreadState) -> dict:
"""Setează next_slug în state, sau '__end__' dacă thread-ul s-a terminat."""
if state["current_turn"] >= state["total_turns"]:
return {"next_slug": "__end__"}
next_slug = pick_next_round_robin(
state["active_slugs"],
state["current_turn"],
)
return {"next_slug": next_slug}
def route_decision(state: ThreadState) -> str:
"""Muchia condițională: citește next_slug și-l returnează ca etichetă."""
return state["next_slug"]
Trei funcții, trei roluri. pick_next_round_robin este pură — primește două argumente, returnează un slug. Testabilă izolat. router_node aplică logica pe state. route_decision este folosită ca conditional edge și citește doar next_slug. Separarea face routerul ușor de înlocuit (vezi secțiunea 15).
11Agent node — apelul către agentul C6
Routerul decide cine vorbește. Agent node-ul decide ce spune — printr-un apel către agentul RAG construit în C6. Nu rescriem nimic din C6: doar îl chemăm cu un input extins care include și thread-ul anterior.
cine vorbește
Citește current_turn și active_slugs. Setează next_slug. Nu apelează LLM-ul.
ce spune agentul
Citește stimulus și messages. Apelează generate_agent_response(). Returnează mesajul nou și incrementează current_turn.
from core.agent import generate_agent_response
def make_agent_node(slug: str):
"""Factory: returnează un nod specializat pentru agentul `slug`."""
def agent_node(state: ThreadState) -> dict:
thread_ctx = thread_to_text(state["messages"])
# input extins: stimulusul + thread-ul anterior
extended_input = f"""[STIMULUS]
{state['stimulus']}
[THREAD ANTERIOR]
{thread_ctx if thread_ctx else "Prima intervenție."}
[SARCINĂ]
Răspunde ca agentul tău. Dacă există mesaje anterioare, reacționează la ele."""
# apel către agentul RAG din C6
result = generate_agent_response(
agent_slug=slug,
input_text=extended_input,
provider=state["provider"],
k=state["k"],
)
new_turn = state["current_turn"] + 1
new_msg = {
"agent": result["agent_name"],
"slug": slug,
"text": result["response"],
"turn": new_turn,
}
# întoarcem messages cu mesajul nou adăugat la sfârșit
return {
"messages": state["messages"] + [new_msg],
"current_turn": new_turn,
}
return agent_node
De ce factory. Avem 3-5 agenți. Un nod per agent. make_agent_node("anti_sistem") produce un nod care apelează agentul anti-sistem. make_agent_node("pro_european") produce alt nod. Alternativa — un singur nod care primește slug-ul prin state — ar amesteca routarea cu execuția și ar face graful mai greu de citit.
12Graful multi-agent complet
Conectăm router_node, route_decision și nodurile produse de make_agent_node într-un graf canonic. Fiecare agent se întoarce la router după ce a vorbit — pentru că routerul este singurul care știe dacă thread-ul continuă sau s-a oprit.
from langgraph.graph import StateGraph, START, END
def build_graph(active_slugs: list[str]):
workflow = StateGraph(ThreadState)
# nodul router
workflow.add_node("router", router_node)
# câte un nod pentru fiecare agent activ
for slug in active_slugs:
workflow.add_node(slug, make_agent_node(slug))
workflow.add_edge(slug, "router") # agent → router (întotdeauna)
# intrare
workflow.add_edge(START, "router")
# muchia condițională: router → un agent sau END
mapping = {slug: slug for slug in active_slugs} | {"__end__": END}
workflow.add_conditional_edges("router", route_decision, mapping)
return workflow.compile()
Topologia este aceeași indiferent de câți agenți avem — adăugarea unui al patrulea sau al cincilea agent înseamnă doar adăugarea unui nou nod și a unui mapping nou. Graful crește în lățime, nu în complexitate.
13Rulăm primul thread multi-agent
Compilăm graful și îl invocăm cu un state inițial. LangGraph se ocupă de restul: rulează routerul, decide următorul agent, îl rulează, se întoarce la router, repetă până când contorul ajunge la total_turns.
graph = build_graph(["anti_sistem", "conspirationist", "pro_european"])
initial_state = {
"stimulus": "CCR a decis anularea alegerilor după suspiciuni privind influențe externe.",
"messages": [],
"active_slugs": ["anti_sistem", "conspirationist", "pro_european"],
"total_turns": 4,
"current_turn": 0,
"next_slug": "",
"provider": "gemini",
"k": 3,
}
result = graph.invoke(initial_state)
for m in result["messages"]:
print(f"Turn {m['turn']} — {m['agent']}: {m['text']}")
Inputul este același pentru toți studenții, ca să putem compara conversațiile produse. Recomandat: „CCR a decis anularea alegerilor după suspiciuni privind influențe externe.". Trei agenți, patru ture, provider Gemini sau DeepSeek.
14Afișarea thread-ului
După graph.invoke(), result["messages"] conține lista de mesaje produse. O afișăm ca thread cu carduri colorate per agent — același stil pe care îl folosește aplicația finală. Tabelul Pandas nu este vizualizarea principală: thread-ul are sens ca succesiune de intervenții, nu ca rânduri într-o tabelă.
Patru intervenții, ordine A → B → C → A (round-robin), fiecare agent vede thread-ul anterior și reacționează. Textele de mai sus sunt reprezentative — outputul concret depinde de modelul ales și de fragmentele FAISS recuperate pentru fiecare rol.
Rulează thread-ul cu același stimulus, dar inversează ordinea din active_slugs. Compară rezultatele: prima intervenție stabilește cadrul interpretativ pentru tot ce urmează?
15Router LLM — extensie opțională
Round-robin este robust, dar mecanic — agenții vorbesc în ordine fixă indiferent de ce s-a spus. Pentru conversații mai dinamice, putem înlocui routerul cu un apel LLM care alege agentul potrivit pe baza thread-ului. Aceasta este o extensie, nu o cerință obligatorie C7.
| Router | Avantaj | Risc |
|---|---|---|
| round-robin | stabil, explicabil, reproducibil | poate părea mecanic în conversații lungi |
| LLM router | mai flexibil, contextual | poate alege repetat același agent · cost LLM la fiecare tură |
Regulă obligatorie pentru router LLM. Nu alege același agent de două ori la rând. Fără această constrângere, modelul tinde să continue cu agentul curent („pentru că tocmai a vorbit despre subiectul potrivit") — și conversația devine monolog.
from core.agent import make_llm
def router_node_llm(state: ThreadState) -> dict:
if state["current_turn"] >= state["total_turns"]:
return {"next_slug": "__end__"}
active = state["active_slugs"]
if not state["messages"]:
return {"next_slug": active[0]}
# constrângerea hard: nu alege agentul care tocmai a vorbit
last_slug = state["messages"][-1]["slug"]
candidates = [s for s in active if s != last_slug] or active
prompt = f"""[STIMULUS]
{state["stimulus"]}
[THREAD]
{thread_to_text(state["messages"])}
[Agenți disponibili]
{chr(10).join(f"- {s}" for s in candidates)}
Alege agentul care ar trebui să răspundă următorul.
Răspunde DOAR cu slug-ul exact."""
llm = make_llm(provider=state["provider"], temperature=0.1)
chosen = llm.invoke(prompt).content.strip().lower()
return {"next_slug": chosen if chosen in candidates else candidates[0]}
Restul grafului rămâne identic. Schimbi un singur nod — router_node → router_node_llm — și toți agenții continuă să funcționeze. Aceasta este modularitatea LangGraph: politica de routing este o componentă interschimbabilă.
16De la notebook la core/graph.py
Notebook-ul este pentru învățare și explorare. Aplicația are nevoie de funcții reutilizabile, importabile din alte module. Mutăm logica de mai sus într-un singur fișier core/graph.py cu un API minimal.
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from core.agent import generate_agent_response
# --- State ---------------------------------------------------------
class ThreadState(TypedDict):
stimulus: str; messages: list; active_slugs: list
total_turns: int; current_turn: int; next_slug: str
provider: str; k: int
# --- Helpers -------------------------------------------------------
def thread_to_text(messages: list) -> str: ...
# --- Router --------------------------------------------------------
def router_node(state): ...
def route_decision(state): return state["next_slug"]
# --- Agent node factory --------------------------------------------
def make_agent_node(slug: str): ...
# --- Builder + runner ---------------------------------------------
def build_graph(active_slugs): ...
def run_thread(stimulus: str, active_slugs: list,
total_turns: int = 4, provider: str = "gemini",
k: int = 3) -> dict:
graph = build_graph(active_slugs)
return graph.invoke({
"stimulus": stimulus, "messages": [],
"active_slugs": active_slugs, "total_turns": total_turns,
"current_turn": 0, "next_slug": "",
"provider": provider, "k": k,
})
Test rapid din terminal — fiecare membru al echipei rulează această comandă ca să verifice că core/graph.py funcționează izolat:
python -m core.graph \
--agents anti_sistem conspirationist pro_european \
--text "CCR a decis anularea alegerilor după suspiciuni privind influențe externe." \
--turns 4 \
--provider gemini
17Integrare minimă în aplicație
app.py nu trebuie să cunoască LangGraph. Singurul lucru pe care îl face este să apeleze run_thread() și să afișeze mesajele. Aceasta este toată integrarea pentru C7.
from core.graph import run_thread
# În tab-ul „Multi-agent thread"
def on_run_click(stimulus, active_slugs, total_turns, provider):
result = run_thread(
stimulus=stimulus,
active_slugs=active_slugs,
total_turns=total_turns,
provider=provider,
)
return result["messages"] # UI le afișează ca thread cu carduri
Principiu de design. Aplicația este o interfață peste run_thread(). Toată logica LangGraph rămâne în core/graph.py. Dacă schimbi routerul sau topologia grafului, app.py nu se modifică deloc.
18Etică și limite
Agenții nu sunt persoane reale. Răspunsurile generate nu sunt opinii autentice ale vreunei grupări politice. Sistemul simulează poziții discursive construite din corpus — atât tehnic, cât și editorial. Orice prezentare publică a output-ului trebuie să clarifice această distincție.
| Dimensiune | Risc | Control minim |
|---|---|---|
| Transparență | agenții pot părea persoane reale | disclaimer explicit: agenți simulați |
| Factualitate | răspunsurile pot părea adevăr verificat | separăm răspunsul de contextul RAG în UI |
| Date | corpusul poate conține date personale | minimizare și curățare la C3 |
| Amplificare | conversația poate escalada conflictul | limită de runde + review uman |
| Reprezentare | rolurile pot fi confundate cu grupuri reale | etichetăm „poziții discursive construite" |
| Responsabilitate | outputul poate fi reutilizat greșit | docs/ethics_and_limits.md + README |
Scrie un disclaimer de 3–4 fraze pentru aplicația EchoChamber. Include: (1) ce este sistemul, (2) ce nu este, (3) pe ce date se bazează, (4) cum trebuie interpretat output-ul. Salvează în docs/ethics_and_limits.md.
19Seminar C7 — EchoChamber
Toată mecanica LangGraph de mai sus se concretizează în seminar într-o singură sesiune practică. La final, echipa are un thread multi-agent funcțional integrat în aplicație.
Ce construim concret
- Notebook
notebooks/student_XX/C7_01_langgraph_multi_agent_thread.ipynb— parcurgem mecanica LangGraph, construim graful, rulăm un thread, afișăm rezultatul cu cardurile colorate per agent. - Backend reutilizabil
core/graph.pycu API-ul minimal:ThreadState,router_node,make_agent_node,build_graph,run_thread. Importabil dinapp.py. - Tab multi-agent în aplicație
Tab nou minimal: utilizatorul alege stimulusul, agenții și numărul de ture; apasă „Run"; thread-ul apare ca succesiune de carduri.
- Disclaimer și etică
docs/ethics_and_limits.md— scriem o pagină scurtă cu disclaimer-ul și limitele sistemului. Aceasta este obligatorie înainte de orice demo public.
Cum se leagă de C6
C7 nu rescrie C6. LangGraph orchestrează agentul C6. Funcția generate_agent_response() din core/agent.py rămâne neatinsă; o chemăm dintr-un nod LangGraph. Dacă agentul C6 nu funcționează izolat, graful C7 nu are ce să controleze.
Test de validare înainte de a începe. python -m core.agent --agent anti_sistem --text "..." --provider gemini trebuie să returneze un răspuns ancorat în corpus. Dacă nu merge, repară C6 înainte de a continua cu C7.
20Livrabile C7
Notebook + interpretare
notebooks/student_XX/C7_01_langgraph_multi_agent_thread.ipynb
· 1 thread multi-agent rulat
· 3 agenți, 4 ture, același stimulus
· mini-interpretare (3–5 fraze) despre dinamica conversației
Backend + app + docs
· core/graph.py cu run_thread() funcțional
· app/app.py cu tab multi-agent minimal
· docs/ethics_and_limits.md
· README.md actualizat
Criteriu de succes. La final, oricine descarcă repo-ul poate rula aplicația local, alege 3 agenți, pune un stimulus și obține un thread multi-agent în UI. Atât. Nu cerem dashboard, metrici complexe sau evaluare automată.
21Team work session — 60 de minute
Scop. Stabilizăm aplicația și pregătim trecerea de la Agent RAG simplu (C6) la thread multi-agent (C7). La final, echipa trebuie să poată rula aplicația local și să genereze un thread funcțional pentru cel puțin un grup de agenți.
Regula principală. Nu lucrează doi membri pe același fișier comun în același timp. Pentru fiecare fișier comun, un singur membru este responsabil.
Împărțirea rolurilor
Roles lead
Verifică structura YAML, prezența câmpurilor slug, name, emoji, color, system. Rulează testul de încărcare.
Retrieval lead
Verifică existența index.faiss și index.pkl pentru agentul testat. Rulează python -m core.retriever ca test terminal.
Agent backend lead
Verifică generate_agent_response(). Rulează python -m core.agent --agent anti_sistem --text "..." --provider gemini.
LangGraph / workflow lead
Construiește sau verifică core/graph.py. Rulează python -m core.graph cu agenții, textul și provider-ul. Output așteptat: lista de mesaje.
App lead
Pornește python app/app.py. Verifică tab-urile existente. Adaugă tab multi-agent care apelează run_thread().
GitHub / issue lead
Verifică issues C6 deschise. Coordonează commit-urile pe fișierele comune. Nu modifică fișierele dacă alt membru lucrează pe ele.
Ordinea de verificare
Echipa nu începe cu aplicația. Ordinea este: roles → retriever → agent → graph → app → GitHub. Dacă un pas nu merge, nu treceți la următorul.
# Toate testele folosesc același input
INPUT="CCR a decis anularea alegerilor după suspiciuni privind influențe externe."
AGENT="anti_sistem" # sau agentul pentru care echipa are sigur vectorstore
python -m core.retriever --agent $AGENT --query "$INPUT" --k 5
python -m core.agent --agent $AGENT --text "$INPUT" --provider gemini
python -m core.graph --agents anti_sistem conspirationist pro_european \
--text "$INPUT" --turns 4 --provider gemini
Ce NU facem azi
UI redesign
Tab-ul multi-agent este minimal. Restul interfeței rămâne neatinsă.
Dashboard, metrici complexe
Fără grafice, fără statistici. Doar thread-ul afișat ca succesiune de carduri.
RAGAS
Evaluarea automată RAG nu intră în C7. Rămâne demo profesor sau C8+.
n8n, MCP
Automatizări externe — nu fac parte din scopul C7. Demo separat dacă rămâne timp.
Scraping web
Stimulusul vine manual. RSS-ul rămâne pentru extensii viitoare.
Multi-agent debate complet
Round-robin cu 3 agenți, 4 ture, atât. Conversații lungi rămân pentru C8+.
Raport scurt la final
DONE C7 team work session
roles.yaml works: yes / no
retriever works: yes / no
agent.py works: yes / no
graph.py / notebook works: yes / no
app multi-agent tab works: yes / no
ethics_and_limits.md: yes / no
Observații:
- [orice problemă rămasă deschisă]
- [ce trebuie reluat în sesiunea următoare]
Resurse
- LangGraph — Tutorial tehnic didactic (referință teoretică detaliată: State, Node, Edge, Conditional Edge, Checkpointing, HITL)
- Documentație oficială LangGraph — docs.langchain.com/oss/python/langgraph/graph-api
- LangGraph 101 — exemple oficiale GitHub: github.com/langchain-ai/langgraph-101
- Notebook-ul seminarului C7:
notebooks/student_XX/C7_01_langgraph_multi_agent_thread.ipynb
Concluzie. LangGraph nu face modelul mai inteligent. Face fluxul explicit, controlabil și inspectabil. În C6 am produs răspunsuri individuale. În C7 am construit un thread multi-agent — agenții au roluri, văd conversația, sunt coordonați de un router. Aceasta este fundația pentru aplicația finală din C8.