Runtime lines

LangChain și LangGraph, trusa esențială

LangChain îți dă piese standard pentru aplicații LLM: modele de chat, mesaje, prompturi, tool-uri și retriever-e, care se îmbină toate ca Runnables. LangGraph le rulează ca graf, cu stare, bucle, memorie și aprobare umană. Parcurge pas cu pas patru programe scurte, apoi citește despre piese, tipare și întrebările pe care le pun intervievatorii.

Fiecare program de pe această pagină rulează cu adevărat, fără cheie API. Modelul este un model de chat fals din langchain_core.language_models care redă răspunsuri scriptate, deci fiecare output afișat este real. În aplicația ta scrii în schimb model = init_chat_model("provider:model-name") (cu pachetul furnizorului instalat, de exemplu langchain-openai sau langchain-anthropic), iar restul codului rămâne neschimbat.

Un chain cu LCEL

chain.pylangchain 1.4, langgraph 1.2
    Output
      Traseul stația aprinsă rulează acum
      muchiemuchie condiționată
      Valoarea dintre pași

        Piesele de bază

        Totul în LangChain implementează o singură interfață, Runnable. Îi înveți metodele o dată și poți apela, compune, face streaming și testa fiecare piesă la fel: prompturi, modele, parsere, retriever-e, tool-uri, chain-uri întregi și grafuri compilate.

        O singură interfață pentru orice Runnable

        runnables.py alege un apel ca să-i vezi output-ul
          Output

            Modele de chat și mesaje

            init_chat_model("provider:model-name") returnează un model de chat pentru orice furnizor suportat, cu același API. Un model primește o listă de mesaje și returnează un AIMessage.

            • SystemMessage: instrucțiuni pentru model.
            • HumanMessage: ce spune utilizatorul.
            • AIMessage: răspunsul. Poate conține tool_calls și usage_metadata (numărul de tokenuri).
            • ToolMessage: rezultatul unui tool, legat de apelul lui prin tool_call_id.

            În 1.x, message.text este o proprietate care dă textul simplu, iar message.content_blocks dă blocuri tipizate (text, raționament, apeluri de tool-uri, imagini) în aceeași formă pentru orice furnizor. Streamingul produce obiecte AIMessageChunk, pe care le poți aduna cu +.

            Șabloane de prompt

            ChatPromptTemplate transformă un dict într-o listă de mesaje. MessagesPlaceholder inserează o listă întreagă, de obicei istoricul conversației.

            from langchain_core.messages import AIMessage, HumanMessage
            from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
            
            prompt = ChatPromptTemplate.from_messages(
                [
                    ("system", "You are a {role}."),
                    MessagesPlaceholder("history"),
                    ("human", "{question}"),
                ]
            )
            value = prompt.invoke(
                {
                    "role": "tutor",
                    "history": [HumanMessage("Hi"), AIMessage("Hello!")],
                    "question": "What is LCEL?",
                }
            )
            print([(m.type, m.content) for m in value.to_messages()])
            [('system', 'You are a tutor.'), ('human', 'Hi'), ('ai', 'Hello!'), ('human', 'What is LCEL?')]

            Output structurat și parsere

            model.with_structured_output(Schema) returnează un Runnable care îți dă un obiect validat în loc de un mesaj. În spate folosește modul JSON nativ al furnizorului sau apelarea tool-urilor. Pasează include_raw=True ca să primești și mesajul brut, plus eventuala eroare de parsare.

            from langchain_core.language_models import GenericFakeChatModel
            from langchain_core.messages import AIMessage
            from pydantic import BaseModel
            
            
            class FakeChatModel(GenericFakeChatModel):
                def bind_tools(self, tools, **kwargs):
                    return self
            
            
            class Movie(BaseModel):
                """A movie mentioned in the text."""
            
                title: str
                year: int
            
            
            call = {"name": "Movie", "args": {"title": "Inception", "year": 2010}, "id": "c1"}
            model = FakeChatModel(messages=iter([AIMessage("", tool_calls=[call])]))
            
            extractor = model.with_structured_output(Movie)
            movie = extractor.invoke("Nolan's 2010 film about dreams")
            print(repr(movie))
            Movie(title='Inception', year=2010)

            Parserele de output fac același lucru într-un chain: StrOutputParser pentru text, JsonOutputParser și PydanticOutputParser pentru modelele fără output structurat nativ.

            Tool-uri

            @tool transformă o funcție cu tipuri într-un tool: numele vine din funcție, descrierea din docstring, iar schema argumentelor din type hints (sau dintr-un args_schema Pydantic). Modelul vede doar aceste trei lucruri, așa că scrie-le pentru model.

            • model.bind_tools([...]) îi permite modelului să decidă singur să le apeleze.
            • tool.invoke(tool_call) returnează un ToolMessage cu tool_call_id corect.
            • ToolNode rulează în paralel toate apelurile din ultimul AIMessage. Implicit, trimite înapoi modelului erorile de argumente invalide și relansează celelalte excepții; cu handle_tool_errors=True returnează orice eroare ca mesaj.

            RAG: documente, splittere, embedding-uri, retriever-e

            • Loaderele citesc sursele în obiecte Document (page_content plus metadata).
            • Text splitterele (pachetul langchain-text-splitters) le împart în chunk-uri. RecursiveCharacterTextSplitter încearcă întâi paragrafe, apoi rânduri, apoi cuvinte; chunk_size limitează mărimea fiecărui chunk, iar chunk_overlap repetă puțin text între vecini, ca o idee să nu fie tăiată în două.
            • Embedding-urile transformă textul în vectori (init_embeddings("provider:model-name")); un vector store îi păstrează și îi găsește pe cei mai apropiați.
            • store.as_retriever() îți dă un retriever: un Runnable care duce de la un șir de interogare la o listă de documente, deci intră direct într-un chain.
            from langchain_core.documents import Document
            from langchain_core.embeddings import DeterministicFakeEmbedding
            from langchain_core.language_models import FakeListChatModel
            from langchain_core.output_parsers import StrOutputParser
            from langchain_core.prompts import ChatPromptTemplate
            from langchain_core.runnables import RunnablePassthrough
            from langchain_core.vectorstores import InMemoryVectorStore
            
            docs = [
                Document("LangGraph adds state, cycles and checkpoints.", metadata={"src": "a"}),
                Document("LCEL joins Runnables with the | operator.", metadata={"src": "b"}),
            ]
            store = InMemoryVectorStore.from_documents(docs, DeterministicFakeEmbedding(size=64))
            retriever = store.as_retriever(search_kwargs={"k": 1})
            
            prompt = ChatPromptTemplate.from_template(
                "Answer from the context only.\nContext: {context}\nQuestion: {question}"
            )
            model = FakeListChatModel(responses=["(the model's grounded answer)"])
            
            
            def format_docs(found):
                return "\n\n".join(d.page_content for d in found)
            
            
            rag = (
                {"context": retriever | format_docs, "question": RunnablePassthrough()}
                | prompt
                | model
                | StrOutputParser()
            )
            print(rag.invoke("What does LangGraph add?"))

            Embedding-urile false de aici sunt vectori aleatori, deci clasamentul nu înseamnă nimic; pune în loc un model real de embedding și codul rămâne același.

            Chain, create_agent sau graf personalizat?

            Alege cea mai simplă formă care se potrivește. Fiecare formă se construiește pe cea dinainte și toate trei sunt Runnables, deci poți urca mai târziu fără să-ți rescrii tool-urile sau prompturile.

            1. Pașii sunt ficși, fără buclă?
              Un chain (LCEL)

              Prompt, model, parser, poate un retriever. Previzibil, ieftin și ușor de testat. Majoritatea aplicațiilor de extragere, clasificare, rezumare și RAG sunt chain-uri.

              prompt | model | parser
            2. Modelul alege tool-uri până termină?
              create_agent

              Bucla standard de apelare a tool-urilor, construită pe LangGraph. Adaugi comportament cu middleware în loc să rescrii bucla: aprobare umană, rezumarea istoricelor lungi, reîncercări, modele de rezervă, limite de apeluri, mascarea datelor personale (PII).

              create_agent(model, tools, middleware=[...])
            3. Ai nevoie de propriul flux de control?
              Un StateGraph personalizat

              Mai mulți agenți, ramificații, fan-out paralel, aprobare la un anumit pas, joburi de lungă durată care trebuie să supraviețuiască repornirilor. Tu proiectezi starea, nodurile și muchiile.

              StateGraph(State).add_node(...)

            create_agent construiește bucla agentului pentru tine

            Același model fals și același tool ca în explorator. Rezultatul este un graf LangGraph compilat, cu nodurile model și tools. system_prompt este trimis la fiecare apel al modelului, dar nu e salvat în stare.

            from langchain.agents import create_agent
            from langchain_core.language_models import GenericFakeChatModel
            from langchain_core.messages import AIMessage, HumanMessage
            from langchain_core.tools import tool
            
            
            class FakeChatModel(GenericFakeChatModel):
                def bind_tools(self, tools, **kwargs):
                    return self
            
            
            @tool
            def get_weather(city: str) -> str:
                """Get the current weather for a city."""
                return f"Sunny, 21 C in {city}"
            
            
            call = {"name": "get_weather", "args": {"city": "Paris"}, "id": "call_1"}
            model = FakeChatModel(
                messages=iter([AIMessage("", tool_calls=[call]), AIMessage("Sunny, 21 C.")])
            )
            
            agent = create_agent(model, tools=[get_weather], system_prompt="You report weather.")
            result = agent.invoke({"messages": [HumanMessage("Weather in Paris?")]})
            print(list(agent.get_graph().nodes))
            print([m.type for m in result["messages"]])
            ['__start__', 'model', 'tools', '__end__']
            ['human', 'ai', 'tool', 'ai']

            Middleware: hook-uri în jurul buclei

            Middleware-ul învelește apelurile de model și de tool-uri din create_agent. Cele incluse, toate în langchain.agents.middleware:

            • HumanInTheLoopMiddleware: pune pauză înaintea tool-urilor alese; un om poate aproba, edita, respinge sau răspunde.
            • SummarizationMiddleware: rezumă mesajele vechi când istoricul devine lung.
            • ModelRetryMiddleware, ToolRetryMiddleware, ModelFallbackMiddleware: reîncearcă cu backoff, apoi schimbă modelul.
            • ModelCallLimitMiddleware, ToolCallLimitMiddleware: limitează apelurile per rulare sau per thread.
            • PIIMiddleware: maschează sau blochează datele personale.

            Scrie-ți propriul middleware cu decoratorii @before_model, @after_model, @wrap_model_call, @wrap_tool_call și @dynamic_prompt.

            Și când nu-ți trebuie niciunul

            Un singur apel de model, fără tool-uri, fără regăsire și fără pași? Ajunge SDK-ul furnizorului sau un simplu init_chat_model(...).invoke(...). Framework-urile merită când schimbi furnizori, compui pași, faci streaming, tracing sau rulezi bucle cu stare. Nu adăuga straturi pe care nu le poți explica.

            Concepte de bază în LangGraph

            O aplicație LangGraph înseamnă o stare (un TypedDict, un dataclass sau un model Pydantic), noduri care o citesc și returnează actualizări, și muchii care decid ce nod rulează în continuare. Rulează în super-pași (super-steps): toate nodurile programate pentru un pas rulează (în paralel, dacă sunt mai multe), actualizările lor se combină, apoi începe pasul următor.

            Moduri de streaming

            streams.py un nod, un apel de model
              Output

                Stare și reducer-e

                Nodurile returnează actualizări parțiale, niciodată toată starea. O cheie fără reducer e suprascrisă; o cheie adnotată cu un reducer e combinată. add_messages adaugă mesajele noi la final și îl înlocuiește pe cel cu același id; așa editezi un mesaj.

                import operator
                from typing import Annotated, TypedDict
                
                from langchain_core.messages import AIMessage, HumanMessage
                from langgraph.graph import add_messages
                
                
                class State(TypedDict):
                    question: str  # no reducer: a new value overwrites the old one
                    log: Annotated[list[str], operator.add]  # reducer: lists are concatenated
                    messages: Annotated[list, add_messages]  # append, or replace by message id
                
                
                print(operator.add(["start"], ["agent"]))
                
                old = [HumanMessage("Hi", id="1"), AIMessage("Draft", id="2")]
                print([m.content for m in add_messages(old, [AIMessage("Final", id="2")])])
                print([m.content for m in add_messages(old, [AIMessage("More")])])
                ['start', 'agent']
                ['Hi', 'Final']
                ['Hi', 'Draft', 'More']

                MessagesState este starea gata făcută, doar cu messages și add_messages; moștenește din ea ca să adaugi chei.

                Noduri, muchii și muchii condiționate

                • Un nod este o funcție (sincronă sau asincronă) care primește starea și returnează un dict de actualizări.
                • add_edge(a, b) merge mereu de la a la b. START și END marchează intrarea și ieșirea.
                • add_conditional_edges(a, router) apelează router(state) și merge unde indică valoarea returnată. Dă o listă sau un dict de destinații, ca graful să poată fi desenat.
                • Un nod poate returna și Command(goto="b", update={...}), ca să actualizeze starea și să aleagă ruta în același loc.
                • O muchie înapoi spre un nod anterior formează un ciclu. Bucla asta e diferența dintre un graf și un chain.

                Cicluri și limita de recursivitate

                Fiecare super-pas se numără la recursion_limit. Când o rulare atinge limita, LangGraph aruncă GraphRecursionError în loc să ruleze la nesfârșit. O setezi per apel, în config.

                from typing import TypedDict
                
                from langgraph.errors import GraphRecursionError
                from langgraph.graph import START, StateGraph
                
                
                class State(TypedDict):
                    n: int
                
                
                builder = StateGraph(State)
                builder.add_node("loop", lambda s: {"n": s["n"] + 1})
                builder.add_edge(START, "loop")
                builder.add_edge("loop", "loop")  # a cycle with no exit
                graph = builder.compile()
                
                try:
                    graph.invoke({"n": 0}, {"recursion_limit": 5})
                except GraphRecursionError as e:
                    print(type(e).__name__, str(e).splitlines()[0])
                GraphRecursionError Recursion limit of 5 reached without hitting a stop condition. You can increase the limit by setting the `recursion_limit` config key.

                Versiunile mai vechi aveau implicit 25 de pași. În langgraph 1.2 valoarea implicită este 10.007 (LANGGRAPH_DEFAULT_RECURSION_LIMIT), iar create_agent setează 9.999, așa că pune-ți propria limită pentru agenții care pot intra în buclă.

                Checkpointere, thread-uri și time travel

                Compilezi cu un checkpointer și fiecare super-pas este salvat ca checkpoint sub thread_id din config. Asta îți dă memorie de conversație, rulări reluabile după un crash, interrupt-uri și time travel: te întorci la un checkpoint vechi, îl modifici și rulezi mai departe pe o ramură nouă.

                from typing import TypedDict
                
                from langgraph.checkpoint.memory import InMemorySaver
                from langgraph.graph import START, StateGraph
                
                
                class State(TypedDict):
                    n: int
                
                
                builder = StateGraph(State)
                builder.add_node("double", lambda s: {"n": s["n"] * 2})
                builder.add_node("inc", lambda s: {"n": s["n"] + 1})
                builder.add_edge(START, "double")
                builder.add_edge("double", "inc")
                graph = builder.compile(checkpointer=InMemorySaver())
                
                config = {"configurable": {"thread_id": "t"}}
                print(graph.invoke({"n": 5}, config))
                
                history = list(graph.get_state_history(config))  # newest first
                before_inc = next(c for c in history if c.next == ("inc",))
                print(before_inc.values)
                
                fork = graph.update_state(before_inc.config, {"n": 100})
                print(graph.invoke(None, fork))
                {'n': 11}
                {'n': 10}
                {'n': 101}

                Folosește InMemorySaver în teste și un saver pe bază de date în producție, de exemplu PostgresSaver din langgraph-checkpoint-postgres.

                Interrupt-uri

                • interrupt(value) într-un nod pune rularea pe pauză și returnează value apelantului, sub __interrupt__. Are nevoie de un checkpointer.
                • Reiei cu graph.invoke(Command(resume=answer), config) pe același thread. Nodul repornește de la prima linie, iar interrupt() returnează answer.
                • Din cauza repornirii, pune efectele secundare după apelul interrupt() sau fă-le idempotente.
                • interrupt_before și interrupt_after, setate la compilare, pun pauză în jurul unor noduri întregi; sunt utile mai ales la depanare.

                Send: map-reduce și ramuri paralele

                Când numărul de ramuri se știe abia la rulare, returnează dintr-o muchie condiționată o listă de Send(node, input). Fiecare rulează nodul cu propriul input, în paralel, în același super-pas, iar un reducer colectează rezultatele.

                import operator
                from typing import Annotated, TypedDict
                
                from langgraph.graph import START, StateGraph
                from langgraph.types import Send
                
                
                class State(TypedDict):
                    topics: list[str]
                    summaries: Annotated[list[str], operator.add]
                
                
                def fan_out(state: State):
                    return [Send("summarise", {"topic": t}) for t in state["topics"]]
                
                
                def summarise(item: dict):
                    return {"summaries": [f"summary of {item['topic']}"]}
                
                
                builder = StateGraph(State)
                builder.add_node("summarise", summarise)
                builder.add_conditional_edges(START, fan_out, ["summarise"])
                graph = builder.compile()
                
                print(graph.invoke({"topics": ["cats", "dogs", "owls"]}))
                {'topics': ['cats', 'dogs', 'owls'], 'summaries': ['summary of cats', 'summary of dogs', 'summary of owls']}

                Pentru un set fix de ramuri nu ai nevoie de Send: adaugi mai multe muchii care pleacă din același nod, iar destinațiile rulează în paralel.

                Subgrafuri

                Un graf compilat este un Runnable, deci poate fi nod în alt graf. Dacă au chei de stare comune, îl dai direct lui add_node. Dacă schemele diferă, îl apelezi dintr-o funcție-nod și mapezi starea la intrare și la ieșire. Cu subgrafuri construiești sisteme multi-agent din agenți mai mici, testabili.

                from typing import TypedDict
                
                from langgraph.graph import START, StateGraph
                
                
                class State(TypedDict):
                    text: str
                
                
                def clean(state: State):
                    return {"text": state["text"].strip()}
                
                
                inner = StateGraph(State)
                inner.add_node("clean", clean)
                inner.add_edge(START, "clean")
                cleaner = inner.compile()
                
                outer = StateGraph(State)
                outer.add_node("cleaner", cleaner)  # a compiled graph is a node
                outer.add_node("shout", lambda s: {"text": s["text"].upper()})
                outer.add_edge(START, "cleaner")
                outer.add_edge("cleaner", "shout")
                print(outer.compile().invoke({"text": "  hi  "}))
                {'text': 'HI'}

                Memorie pe termen lung: Store

                Un checkpointer ține minte un singur thread. Un Store păstrează documente JSON în namespace-uri, partajate între thread-uri: preferințele utilizatorului, fapte învățate. Nodurile ajung la el, ca și la context-ul rulării, prin argumentul Runtime.

                from dataclasses import dataclass
                
                from langgraph.graph import START, MessagesState, StateGraph
                from langgraph.runtime import Runtime
                from langgraph.store.memory import InMemoryStore
                
                
                @dataclass
                class Context:
                    user_id: str
                
                
                def remember(state: MessagesState, runtime: Runtime[Context]):
                    ns = ("users", runtime.context.user_id)
                    runtime.store.put(ns, "prefs", {"language": "Romanian"})
                    return {}
                
                
                builder = StateGraph(MessagesState, context_schema=Context)
                builder.add_node("remember", remember)
                builder.add_edge(START, "remember")
                store = InMemoryStore()
                graph = builder.compile(store=store)
                
                graph.invoke({"messages": []}, context=Context(user_id="ana"))
                print(store.get(("users", "ana"), "prefs").value)
                {'language': 'Romanian'}

                Cu un index de embedding-uri configurat, store.search(namespace, query=...) găsește amintiri după sens.

                Esențiale pentru producție

                Părțile despre care întreabă intervievatorii după ce demo-ul tău merge: să vezi ce s-a întâmplat, să reziști la furnizori instabili, să oprești buclele scăpate de sub control și să testezi fără să plătești tokenuri.

                Tracing cu LangSmith

                Setezi LANGSMITH_TRACING=true și LANGSMITH_API_KEY (și, opțional, LANGSMITH_PROJECT), iar fiecare rulare de chain, agent sau graf e înregistrată ca arbore: input-urile, output-urile, latența, consumul de tokenuri și erorile fiecărui pas. Fără modificări de cod. Ca să incluzi și funcțiile tale, învelește-le cu @traceable din pachetul langsmith.

                Evaluare cu LangSmith

                Păstrezi un dataset cu input-uri de exemplu și output-uri de referință, rulezi aplicația pe el cu evaluate(target, data=..., evaluators=[...]) și notezi fiecare rezultat cu verificări în cod sau cu un LLM judecător. Fiecare rulare este un experiment, deci poți compara prompturi și modele unul lângă altul înainte de lansare, apoi rulezi evaluatori online pe trace-urile din producție.

                Reîncercări, fallback-uri și limite de rată

                with_retry reîncearcă un Runnable cu backoff exponențial; with_fallbacks încearcă următorul Runnable dacă tot eșuează. Aici modelul principal dă mereu timeout:

                from langchain_core.language_models import FakeListChatModel
                from langchain_core.runnables import RunnableLambda
                
                attempts = []
                
                
                def flaky_model(prompt):
                    attempts.append(prompt)
                    raise TimeoutError("provider timed out")
                
                
                primary = RunnableLambda(flaky_model)
                backup = FakeListChatModel(responses=["Answer from the backup model"])
                
                model = primary.with_retry(stop_after_attempt=3).with_fallbacks([backup])
                print(model.invoke("hi").content)
                print(len(attempts))
                Answer from the backup model
                3

                Ca să rămâi sub limita de rată a furnizorului, dă-i modelului de chat rate_limiter=InMemoryRateLimiter(requests_per_second=...). În agenți, folosește middleware-ul de retry și de fallback.

                Limite și conversații lungi

                • Setează recursion_limit, iar în agenți ModelCallLimitMiddleware sau ToolCallLimitMiddleware, ca un model derutat să nu intre în buclă și să cheltuiască la nesfârșit.
                • Istoricul ajunge să depășească fereastra de context: scurtează-l cu trim_messages sau rezumă-l cu SummarizationMiddleware.
                • Folosește un checkpointer pe bază de date și alege durability ("sync", "async" sau "exit") ca să schimbi siguranță pe viteză.

                Teste cu modele false

                FakeListChatModel redă stringuri, iar GenericFakeChatModel și FakeMessagesListChatModel redau AIMessage-uri întregi, inclusiv tool_calls. Scrii dinainte ce răspunde modelul, rulezi chain-ul sau graful real și verifici mesajele, traseul parcurs și starea finală, exact ca în fiecare program de pe pagina asta. Testele unitare rămân rapide, gratuite și deterministe; calitatea răspunsurilor o verifici separat, cu evaluările din LangSmith.

                LangChain, LangGraph, LangSmith

                • LangChain: piesele (modele, mesaje, tool-uri, retrievere), LCEL și create_agent.
                • LangGraph: runtime-ul pentru fluxuri cu stare, cu bucle și durabile. create_agent rulează pe el.
                • LangSmith: tracing, evaluare și monitorizare. Merge cu sau fără celelalte două.

                De reținut

                Totul e un Runnableinvoke, batch, stream și perechile lor async merg pe orice piesă. | construiește un RunnableSequence; un dict devine un RunnableParallel.
                Mesajele sunt interfațaMesaje System, Human, AI și Tool. Un AIMessage poate cere tool-uri; fiecare ToolMessage răspunde unui singur apel, prin tool_call_id.
                Modelul nu rulează niciodată tool-uriReturnează tool_calls. Codul tău, ToolNode sau create_agent le rulează și trimite rezultatele înapoi.
                Un agent e o buclăModel, tool-uri, model, până nu mai există apeluri de tool-uri. create_agent o construiește; cu StateGraph îi dai tu forma.
                Stare plus reducer-eNodurile returnează actualizări parțiale, iar reducer-ele le combină. add_messages adaugă la final sau înlocuiește după id.
                Checkpointer plus thread_idPerechea asta îți dă memorie, interrupturi cu Command(resume=...), recuperare după crash și time travel. Un Store ține minte între threaduri.

                Întrebări de interviu: LangChain și LangGraph

                Răspunsuri scurte pe care le poți spune cu voce tare. Încearcă să răspunzi singur la fiecare întrebare înainte s-o deschizi.

                La ce folosește LangChain și când nu l-ai folosi?

                LangChain îți dă piese standard, independente de furnizor, pentru aplicații cu LLM: modele de chat, mesaje, prompturi, tool-uri, retrievere și parsarea outputului, toate compozabile ca Runnables, plus create_agent pentru agenți cu tool calling. Merită când compui pași, schimbi furnizori, faci streaming, tracing sau construiești agenți. Pentru un singur apel de model fără tool-uri sau regăsire, SDK-ul furnizorului e mai simplu și n-ar trebui să adaugi un strat pe care nu-l poți explica.

                Care e diferența dintre LangChain, LangGraph și LangSmith?

                LangChain e setul de blocuri de construcție plus create_agent, de nivel înalt. LangGraph e runtime-ul de nivel jos pentru fluxuri cu stare: grafuri cu stare, cicluri, checkpointuri, interrupturi și streaming; create_agent rulează pe el. LangSmith e platforma de observabilitate și evaluare și merge cu sau fără celelalte două.

                Ce e un Runnable și ce e LCEL?

                Un Runnable e orice are interfața standard: invoke, batch, stream și versiunile lor async. Prompturile, modelele, parserele, retrieverele, tool-urile și grafurile compilate sunt toate Runnables. LCEL, LangChain Expression Language, le compune cu |, care construiește un RunnableSequence în care outputul fiecărui pas e inputul pasului următor. Chain-ul compus e și el un Runnable, așa că primește gratis streaming, batching, async și tracing.

                Care e diferența dintre invoke, batch, stream și astream?

                invoke primește un input și returnează un output. batch primește o listă și rulează inputurile concurent pe un thread pool, limitat de max_concurrency. stream emite outputul pe bucăți, de exemplu tokenuri, imediat ce sunt gata. astream și celelalte metode cu a sunt versiunile async pentru servere asyncio, ca un apel lent de model să nu blocheze alte requesturi.

                Ce fac RunnableParallel, RunnablePassthrough și RunnableLambda?

                RunnableParallel rulează mai multe Runnables pe același input și returnează un dict cu rezultatele lor; un dict simplu într-un chain e transformat automat într-unul. RunnablePassthrough lasă inputul să treacă neschimbat, iar RunnablePassthrough.assign adaugă chei calculate într-un dict de input. RunnableLambda împachetează o funcție Python obișnuită ca să poată sta într-un chain. Chain-ul RAG clasic folosește toate trei ideile: {"context": retriever | format, "question": RunnablePassthrough()} | prompt | model.

                Care sunt tipurile de mesaje și ce înseamnă fiecare?

                SystemMessage poartă instrucțiunile, HumanMessage inputul utilizatorului, iar AIMessage răspunsul modelului, care poate include tool_calls și usage_metadata. ToolMessage poartă rezultatul unui tool și trebuie să aibă tool_call_id al apelului căruia îi răspunde. Un model de chat primește o listă de mesaje și returnează un AIMessage; în 1.x, .text îți dă textul, iar .content_blocks îți dă blocuri tipizate, cu aceeași formă pentru orice furnizor.

                Cum funcționează tool calling, pas cu pas?

                Legi tool-urile cu model.bind_tools(tools), care trimite odată cu requestul numele, descrierea și schema JSON a fiecărui tool. Modelul nu rulează nimic: returnează un AIMessage cu tool_calls, fiecare cu nume, argumente și un id. Codul tău rulează fiecare tool și adaugă un ToolMessage cu tool_call_id corespunzător, apoi apelează din nou modelul cu tot istoricul. Când modelul răspunde fără apeluri de tool-uri, ai terminat.

                bind_tools sau with_structured_output: când îl folosești pe fiecare?

                Folosește bind_tools când modelul trebuie să decidă dacă acționează și ce tool-uri apelează, ca într-un agent. Folosește with_structured_output(Schema) când vrei mereu date într-o formă fixă, de exemplu la extragere sau clasificare: impune formatul prin modul JSON al furnizorului sau printr-un apel de tool, apoi parsează rezultatul în modelul tău Pydantic, TypedDict sau dict. include_raw=True returnează în plus mesajul brut și eventuala eroare de parsare.

                Cum funcționează bucla unui agent?

                Modelul primește conversația și schemele tool-urilor. Dacă returnează apeluri de tool-uri, tool-urile rulează și rezultatele lor se adaugă ca ToolMessage-uri, apoi modelul e apelat din nou. Bucla se termină când modelul răspunde fără apeluri de tool-uri sau când o limită o oprește. În LangGraph asta înseamnă două noduri, model și tools, o muchie condiționată precum tools_condition și o muchie de la tools înapoi la model.

                Când ai folosi create_agent și când un graf LangGraph construit de mână?

                create_agent(model, tools, ...) îți dă bucla standard de tool calling peste LangGraph, cu checkpointing, streaming și middleware pentru lucruri ca aprobarea umană, rezumarea, reîncercările și limitele de apeluri. Folosește-l ori de câte ori forma potrivită e un model care alege tool-uri într-o buclă. Construiește-ți propriul StateGraph când ai nevoie de un flux de control personalizat: pași ficși amestecați cu pași de agent, mai mulți agenți, fan-out paralel sau aprobare la un anumit pas.

                Ce e middleware-ul în create_agent?

                Hookuri care rulează în jurul apelurilor de model și de tool-uri ale agentului, ca să schimbi comportamentul fără să rescrii bucla. Printre cele incluse sunt HumanInTheLoopMiddleware, SummarizationMiddleware, ModelRetryMiddleware, ModelFallbackMiddleware, ModelCallLimitMiddleware, ToolCallLimitMiddleware și PIIMiddleware. Pe ale tale le scrii cu decoratori precum @before_model, @after_model, @wrap_model_call și @dynamic_prompt.

                Ce sunt StateGraph, starea și reducer-ele? De ce add_messages?

                Un StateGraph se construiește dintr-o schemă de stare, de obicei un TypedDict. Nodurile returnează actualizări parțiale, iar reducer-ul fiecărei chei decide cum se combină o actualizare: fără reducer, valoarea se suprascrie; cu operator.add, se concatenează. add_messages adaugă mesajele noi la final și le înlocuiește pe cele cu același id, așa că nodurile pot returna doar mesajul nou, iar nodurile paralele nu se suprascriu unul pe altul. MessagesState e starea gata făcută, cu o cheie messages care îl folosește.

                Cum funcționează muchiile condiționate și ciclurile?

                add_conditional_edges(node, router) apelează router(state) după nod și merge la nodul returnat sau la END. O muchie înapoi spre un nod anterior creează un ciclu, și așa intră agenții în buclă. Un nod poate returna și Command(goto=..., update=...) ca să ruteze și să actualizeze starea într-un singur pas. Orice buclă are nevoie de o condiție de ieșire, iar limita de recursivitate e plasa de siguranță.

                Ce e limita de recursivitate și ce se întâmplă când o atingi?

                Plafonează numărul de super-pași dintr-o rulare. Când e atinsă, LangGraph aruncă GraphRecursionError în loc să ruleze la nesfârșit. O setezi per apel cu config={"recursion_limit": n}. Versiunile mai vechi aveau implicit 25; langgraph 1.2 are implicit 10.007, iar create_agent folosește 9.999, așa că pentru agenți setează-ți propria limită sau adaugă middleware de limitare a apelurilor.

                Ce face un checkpointer și ce e un thread?

                Un checkpointer salvează starea grafului după fiecare super-pas. Un thread e o conversație sau un job, identificat prin thread_id în config["configurable"]; toate checkpointurile lui sunt stocate sub acel id. Dacă apelezi din nou cu același thread, graful continuă din starea salvată, și de aici ai memoria conversației, recuperarea după crash, interrupturile și time travel. Folosește InMemorySaver în teste și un saver Postgres sau SQLite în producție.

                Ce e time travel în LangGraph?

                Pentru că fiecare pas are un checkpoint, poți lista stările trecute cu get_state_history(config). Dacă apelezi cu configul unui checkpoint mai vechi, execuția se reia din acel punct. update_state pe un checkpoint vechi creează un fork pe care îl poți rula mai departe cu alte valori. Se folosește la debugging, ca să reîncerci de la un pas greșit și ca să-i lași pe utilizatori să editeze un mesaj anterior.

                Cum funcționează interrupt și Command(resume=...) și unde e capcana?

                Un apel interrupt(value) într-un nod salvează starea și oprește rularea; apelantul primește value sub __interrupt__. Ca să continui, apelezi același thread cu Command(resume=answer). Capcana: nodul repornește de la prima linie, iar interrupt() returnează atunci answer, deci orice cod dinaintea lui rulează de două ori. Pune efectele secundare după interrupt sau fă-le idempotente. Interrupturile au nevoie de un checkpointer.

                Memorie pe termen scurt sau lung: checkpointer sau Store?

                Memoria pe termen scurt e starea unui singur thread, păstrată de checkpointer: mesajele conversației curente. Memoria pe termen lung trebuie să supraviețuiască threadurilor, de exemplu preferințele utilizatorului sau fapte aflate mai devreme, așa că merge într-un Store: documente JSON sub namespace-uri ca ("users", user_id), opțional căutabile după sens. Nodurile ajung la store prin argumentul Runtime.

                La ce folosește Send și cum funcționează ramurile paralele?

                Nodurile programate în același super-pas rulează în paralel, deci mai multe muchii care pleacă din același nod îți dau deja ramuri paralele fixe. Când numărul de ramuri se știe abia la rulare, o muchie condiționată returnează o listă de Send(node, input), iar fiecare rulează nodul cu propriul input: pasul map din map-reduce. Un reducer precum operator.add pe cheia de rezultat le colectează outputurile.

                Ce sunt subgrafurile și de ce le folosești?

                Un graf compilat e un Runnable, deci poate fi nod în alt graf. Dacă părintele și copilul au chei de stare comune, îl adaugi direct; dacă nu, îl apelezi dintr-un nod și mapezi starea la intrare și la ieșire. Cu subgrafuri construiești și testezi agenții separat și apoi îi combini, iar așa se construiesc de obicei sistemele multi-agent. Un nod dintr-un subgraf poate ruta în părinte cu Command(graph=Command.PARENT, goto=...).

                Ce moduri de streaming are LangGraph?

                values trimite starea completă după fiecare pas, iar updates doar ce a returnat fiecare nod. messages trimite tokenurile LLM-ului cu metadate care spun ce nod le-a produs, iar custom trimite orice emit nodurile cu get_stream_writer(). Mai există checkpoints, tasks și debug; dai o listă ca să primești mai multe, ca tupluri (mode, data).

                Cum faci streaming la răspunsul unui agent într-un UI web?

                Folosește astream cu stream_mode="messages" ca să primești tokenurile pe măsură ce sunt generate, de multe ori combinat cu "updates" ca să arăți progresul, de exemplu ce tool rulează. Trimite chunk-urile spre browser prin server-sent events sau printr-un WebSocket. Streamingul de tokenuri merge chiar dacă nodul apelează invoke, pentru că LangGraph ascultă callbackurile modelului.

                Cum construiești RAG cu LangChain?

                Indexarea: încarci sursele în Document-e, le împarți în chunk-uri cu un text splitter, calculezi embedding-urile chunk-urilor și le stochezi într-un vector store. Răspunsul: un retriever găsește chunk-urile cele mai similare cu întrebarea, iar un chain le pune în prompt și îi cere modelului să răspundă doar din acel context. Pentru întrebări mai grele faci din regăsire un tool, ca un agent să poată căuta de mai multe ori și să-și reformuleze interogarea.

                Cum alegi dimensiunea chunk-urilor și suprapunerea (overlap)?

                Chunk-urile trebuie să fie destul de mari cât să cuprindă o idee completă și destul de mici încât un chunk regăsit să fie în mare parte relevant; un punct de plecare obișnuit e între câteva sute și vreo mie de tokenuri. Suprapunerea (overlap), de multe ori 10–20%, repetă text între chunk-uri vecine, ca o idee tăiată la graniță să fie găsită totuși. RecursiveCharacterTextSplitter împarte după paragrafe, apoi după linii, apoi după cuvinte, ca chunk-urile să rămână naturale. Apoi măsori calitatea regăsirii pe întrebări reale și ajustezi.

                Ce e un retriever și cum diferă de un vector store?

                Un vector store stochează embedding-uri și face căutare după similaritate. Un retriever e o interfață Runnable: intră un string de interogare, iese o listă de Document-e. vector_store.as_retriever(search_kwargs={"k": 4}) împachetează un store, dar un retriever poate fi și căutare după cuvinte cheie, o căutare pe web sau un hibrid din mai multe. Fiind un Runnable, se potrivește în orice chain.

                La ce folosește LangSmith?

                Tracing: cu LANGSMITH_TRACING=true și o cheie API, fiecare rulare e înregistrată ca un arbore de pași, cu inputuri, outputuri, latență, tokenuri și erori, așa că vezi exact ce i s-a trimis modelului. Evaluare: ții dataseturi de exemple, rulezi aplicația pe ele cu evaluatori scriși în cod sau LLM-judge și compari experimentele înainte de lansare. Mai monitorizează și traficul din producție și poate rula evaluări online pe el.

                Cum gestionezi reîncercările, fallback-urile și limitele de rată?

                runnable.with_retry(stop_after_attempt=3) reîncearcă cu backoff exponențial, iar with_fallbacks([backup]) încearcă alt model sau alt chain dacă tot eșuează. Dă-i unui model de chat rate_limiter=InMemoryRateLimiter(requests_per_second=...) ca să rămâi sub limitele furnizorului. În agenți, ModelRetryMiddleware, ToolRetryMiddleware și ModelFallbackMiddleware fac aceleași lucruri.

                Cum testezi cod LangChain și LangGraph fără să apelezi un model real?

                Folosește modelele de chat false din langchain_core: FakeListChatModel redă stringuri, iar GenericFakeChatModel sau FakeMessagesListChatModel redau AIMessage-uri întregi, inclusiv apeluri de tool-uri. Rulezi chain-ul sau graful real cu un InMemorySaver și verifici mesajele, traseul parcurs și starea finală. Așa testele unitare rămân rapide și deterministe; calitatea răspunsurilor o judeci separat, cu evaluările din LangSmith.

                Ce faci cu o conversație care nu mai încape în fereastra de context?

                O scurtezi cu trim_messages, păstrând mesajul de sistem și mesajele cele mai recente; rezumi turele mai vechi într-un mesaj scurt, ce face SummarizationMiddleware pentru agenți; sau muți faptele durabile într-un Store și le regăsești când ai nevoie. Când scurtezi, nu despărți niciodată un AIMessage cu apeluri de tool-uri de ToolMessage-urile lui, altfel furnizorul respinge istoricul.

                Ce se întâmplă când un tool aruncă o excepție?

                Într-un graf, ToolNode transformă implicit erorile de argumente invalide într-un ToolMessage, ca modelul să-și poată corecta apelul, și aruncă mai departe celelalte excepții. Setează handle_tool_errors=True sau dă-i o funcție, ca să trimiți orice eroare înapoi la model ca mesaj. În create_agent, middleware-ul de retry poate reîncerca întâi tool-urile instabile.

                Ce face init_chat_model?

                Creează un model de chat dintr-un string precum "provider:model-name", încărcând pachetul de integrare potrivit, de exemplu langchain-openai sau langchain-anthropic. Apoi fiecare furnizor are aceeași interfață, așa că schimbi modelele din configurare, fără să modifici codul. Și create_agent acceptă direct același string.

                Cum funcționează sistemele multi-agent în LangGraph?

                Fiecare agent e un nod sau un subgraf cu propriul prompt și propriile tool-uri. Într-un design cu supervisor, un agent împarte munca celorlalți, de multe ori apelându-i ca tool-uri; într-un design cu handoff, agenții își predau controlul direct cu Command(goto=...). Starea comună sau mesajele duc contextul de la unul la altul. Începe cu un singur agent și adaugă alții doar când tool-urile sau instrucțiunile se separă clar.

                Fiecare output de pe pagina asta a fost afișat de programele din verify/langchain/, rulate cu langchain 1.4.3, langchain-core 1.6.7 și langgraph 1.2.14 pe Python 3.14. Modelele sunt modelele de chat false din langchain_core.language_models; id-urile mesajelor sunt omise din desene.