Blog
AI23 min czytania

LangChain.js — budowa aplikacji LLM w TypeScripcie

Wersja 1.0 wywróciła LangChaina do góry nogami: zamiast łańcuchów jest jedna funkcja `createAgent` i middleware, które opakowuje pętlę agenta. Pokazujemy aktualne API, komplet wbudowanych middleware wraz z domyślnymi wartościami, strumieniowanie, pamięć wątków, RAG oraz wpięcie agenta w aplikację Laravela jako kontenera obok Saila. Plus rachunek za utrzymanie: tempo wydań, otwarte zgłoszenia i miejsca, w których framework przeszkadza zamiast pomagać.

Prototyp aplikacji z modelem językowym powstaje w godzinę. Bierzesz SDK dostawcy, wysyłasz prompt, dostajesz tekst. Kłopot zaczyna się dzień później, kiedy prototyp ma zostać usługą. Trzeba dołożyć narzędzia, które model może wywołać, historię rozmowy, która nie rozsadzi okna kontekstu, akceptację człowieka przed zwrotem pieniędzy klientowi, ponowienia po błędzie 429, twardy limit wywołań, żeby zapętlony agent nie wygenerował rachunku na kilkaset dolarów przy jednym zapytaniu, i przełącznik dostawcy, bo za miesiąc okaże się, że konkurencyjny model robi to samo o połowę taniej.

Każda z tych rzeczy z osobna to jeden dzień pracy. Razem to jest framework — tylko napisany naprędce, bez testów i bez dokumentacji, utrzymywany przez jedną osobę w zespole.

LangChain.js (github.com/langchain-ai/langchainjs, licencja MIT) jest tym frameworkiem napisanym za nas. Warto o nim porozmawiać właśnie teraz, bo projekt ma za sobą reset, który unieważnił większość tego, co o nim wiadomo z internetu: wersja 1.0 wyrzuciła z głównego pakietu łańcuchy, na których opierały się wszystkie poradniki z lat 2023–2024, i zastąpiła je jedną funkcją plus warstwą middleware. Poniżej aktualne API, komplet wbudowanych middleware, wpięcie w stack oparty o Laravela — i uczciwy rachunek za utrzymanie.

Wersja 1.0 to nie było podbicie numerka

LangChain i LangGraph dostały wersje 1.0 22 października 2025, jednocześnie w Pythonie i w JavaScripcie. Zmiana była zerwaniem ciągłości, nie kolejnym krokiem:

  • Główny pakiet langchain zawiera dziś pętlę agenta i nic poza nią. Punktem wejścia jest createAgent, który zastąpił zarówno stary AgentExecutor, jak i createReactAgent z LangGraph,
  • cała reszta historii przeniosła się do @langchain/classicLLMChain, RetrievalQAChain, ConversationalRetrievalQAChain, indeksowanie, retrievery i eksporty społecznościowe. Import zmienia się z langchain/chains na @langchain/classic/chains i kod dalej działa, ale to jest ścieżka wyjścia, nie ścieżka rozwoju,
  • middleware jako mechanizm dostosowywania zachowania agenta w każdym punkcie pętli — to jest właściwa nowość w tej wersji i powód, dla którego warto ją znać,
  • standardowe bloki treści — jednolita własność contentBlocks na wiadomościach, dająca ten sam dostęp do śladów rozumowania, cytowań i wywołań narzędzi niezależnie od dostawcy,
  • obietnica stabilności — żadnych zmian łamiących zgodność do wersji 2.0.
Praktyczna konsekwencja tego resetu jest taka, że materiały o LangChainie starsze niż koniec 2025 roku opisują framework, którego już nie ma. Wpisy o LLMChain, SequentialChain i „prompt | model | parser” dotyczą API przeniesionego do pakietu zgodnościowego. Jeśli ktoś w zespole odbił się od LangChaina dwa lata temu, to odbił się od czegoś innego niż to, co jest dzisiaj.

Co jest w ekosystemie i w jakich wersjach

Repozytorium jest monorepo, a pakiety wersjonują się niezależnie. Stan na dziś, prosto z rejestru npm — wszystkie na licencji MIT:

  • langchain1.5.10 (opublikowana 20 sierpnia 2026), pętla agenta, tool(), initChatModel, wbudowane middleware,
  • @langchain/core1.2.9, zależność równorzędna (peer dependency): abstrakcje wiadomości, modeli i narzędzi,
  • @langchain/langgraph1.4.13, silnik grafowy pod spodem, wraz z @langchain/langgraph-checkpoint od trwałości stanu,
  • @langchain/classic1.0.46, warstwa zgodności z API sprzed v1,
  • @langchain/openai1.5.11, @langchain/anthropic1.5.9, @langchain/ollama1.3.0 i kilkadziesiąt innych integracji,
  • @langchain/community1.1.29, integracje utrzymywane przez społeczność,
  • deepagents1.13.2, warstwa wyżej: system plików dla agenta, podagenci, umiejętności.

Sam pakiet langchain ciągnie za sobą niewiele: zod (w wersji 3.25.76 lub nowszej albo 4), langsmith, oraz dwa pakiety LangGraph. Wymagany jest Node.js 20 lub nowszy; testowane są linie 20.x, 22.x i 24.x. Poza Node’em kod chodzi w Cloudflare Workers, Deno, Bun, w Next.js, w Supabase Edge Functions i w przeglądarce — z zastrzeżeniem, że klucza dostawcy w przeglądarce nie umieszczamy.

Pierwszy agent w dwudziestu linijkach

Instalacja to główny pakiet, rdzeń, integracja wybranego dostawcy i zod do opisu schematów:

npm install langchain @langchain/core @langchain/anthropic zod

Agent w LangChainie 1.x to model wywołujący narzędzia w pętli, dopóki zadanie nie zostanie zamknięte. Definicja narzędzia i definicja agenta to dwie funkcje:

import { createAgent, tool } from "langchain";
import * as z from "zod";

const statusZamowienia = tool(
  async ({ orderId }) => {
    const res = await fetch(`https://erp.internal/orders/${orderId}`);

    return JSON.stringify(await res.json());
  },
  {
    name: "get_order_status",
    description: "Zwraca status zamówienia na podstawie jego numeru",
    schema: z.object({
      orderId: z.string().describe("Numer zamówienia, np. 55321"),
    }),
  },
);

const agent = createAgent({
  model: "anthropic:claude-sonnet-4-6",
  tools: [statusZamowienia],
  systemPrompt: "Jesteś asystentem obsługi klienta. Odpowiadasz po polsku i zwięźle.",
});

const wynik = await agent.invoke({
  messages: [{ role: "user", content: "Co z zamówieniem 55321?" }],
});

console.log(wynik.messages.at(-1)?.text);

Trzy rzeczy z tego fragmentu warto wyłapać od razu.

Model podaje się napisem "dostawca:model" — bez importowania klasy, bez konfigurowania klienta. Identyfikatory modeli zmieniają się co kilka tygodni (w samym repozytorium wsparcie dla nowych wariantów dochodzi w tempie kilku commitów miesięcznie), więc konkretną nazwę bierz z dokumentacji dostawcy, a nie z poradnika. Alternatywnie podaje się gotową instancję klasy — o tym niżej.

Opis narzędzia to część promptu. Pole description i .describe() na polach schematu trafiają do modelu — to na ich podstawie decyduje, czy i z jakimi argumentami wywołać funkcję. Lakoniczny opis jest najczęstszą przyczyną tego, że agent „nie chce korzystać z narzędzia”.

Walidacja argumentów jest wymuszona schematem zod. Model, który wymyśli pole spoza schematu albo poda liczbę jako tekst, dostaje błąd walidacji zamiast przepuścić śmieci do funkcji sięgającej po ERP.

Modele: jedna abstrakcja na wszystkich dostawców

Poza napisem "dostawca:model" jest jawna inicjalizacja przez initChatModel, która przyjmuje wspólny zestaw parametrów niezależnie od dostawcy:

import { initChatModel } from "langchain";

const model = await initChatModel("openai:gpt-5.5", {
  temperature: 0,
  maxTokens: 2000,
  timeout: 60_000,
  maxRetries: 6,
});

Domyślne maxRetries wynosi 6 i obejmuje błędy sieciowe, limity zapytań (429) i błędy serwera (5xx) — ale już nie 401 czy 404, i słusznie, bo ponawianie błędnego klucza nie ma sensu. Jest też droga przez klasy dostawców, gdy potrzebna jest opcja specyficzna dla jednego API:

import { ChatAnthropic } from "@langchain/anthropic";

const model = new ChatAnthropic({
  model: "claude-sonnet-4-6",
  maxRetries: 10,
  timeout: 120_000,
});

Na obiekcie modelu dostajesz też trzy rzeczy, które przy własnej integracji trzeba by dopisać samodzielnie:

  • model.batch([...], { maxConcurrency: 5 }) — kolejka wielu zapytań z kontrolą współbieżności. Przy klasyfikacji tysiąca zgłoszeń to jest różnica między jedną linijką a własnym mechanizmem puli,
  • model.withStructuredOutput(Schemat) — odpowiedź walidowana schematem zod zamiast parsowania JSON-a z tekstu,
  • model.profile — deklarowane możliwości modelu (rozmiar okna kontekstu, obsługa obrazów, wywoływanie narzędzi). Przydaje się, gdy aplikacja pozwala klientowi wybrać model i musi w locie ukryć funkcje, których ten model nie ma.

Middleware — jedyny powód, żeby sięgnąć po framework

Tu jest sedno wersji 1.0. Pętla agenta ma trzy kroki: wywołaj model, wykonaj wybrane przez niego narzędzia, zakończ, gdy model przestanie sięgać po narzędzia. Middleware wpina się w każdy z tych punktów przez zestaw haków:

  • beforeAgent i afterAgent — raz na całe uruchomienie,
  • beforeModel i afterModel — wokół każdego wywołania modelu,
  • wrapModelCall i wrapToolCall — haki opakowujące: dostają żądanie oraz funkcję domykającą łańcuch, więc mogą wywołanie zmodyfikować, powtórzyć, podmienić albo w ogóle nie dopuścić.

Różnica między beforeModel a wrapModelCall jest ta sama co między listenerem a dekoratorem: pierwszy obserwuje i modyfikuje stan, drugi kontroluje samo wywołanie. Ponowienie po błędzie da się napisać wyłącznie jako wrapModelCall.

Co dostajesz gotowe

Wbudowanych middleware jest kilkanaście i to jest lista, na którą warto spojrzeć, zanim zacznie się cokolwiek pisać samodzielnie — bo mniej więcej połowa z nich to funkcje, które i tak trafiłyby do backlogu:

  • summarizationMiddleware — streszcza starszą część rozmowy, gdy zbliża się limit kontekstu. Konfiguruje się modelem streszczającym, progiem (trigger liczony w tokenach, wiadomościach albo ułamku okna) i liczbą wiadomości zachowywanych w całości (keep, domyślnie 20). Pilnuje przy tym, żeby nie rozdzielić pary wiadomość-narzędzie od jej wyniku,
  • contextEditingMiddleware — zamiast streszczać, czyści stare wyniki narzędzi. Domyślna strategia ClearToolUsesEdit zaczyna działać po przekroczeniu 100 000 tokenów i zostawia trzy ostatnie wyniki. Przy agencie, który dwadzieścia razy pobiera po pół megabajta JSON-a, to jest tańsze niż streszczanie,
  • toolCallLimitMiddleware — limit wywołań narzędzi na wątek (threadLimit) i na pojedyncze uruchomienie (runLimit), opcjonalnie dla wskazanego narzędzia,
  • modelCallLimitMiddleware — to samo dla wywołań modelu, z domyślnym exitBehavior: "end". To jest bezpiecznik przeciwko pętli, w której agent w kółko wywołuje to samo narzędzie,
  • modelRetryMiddleware — ponowienia z odczekiwaniem wykładniczym (maxRetries 2, backoffFactor 2.0, initialDelayMs 1000),
  • toolRetryMiddleware — to samo dla narzędzi, z możliwością wskazania, które błędy kwalifikują się do ponowienia (retryOn),
  • modelFallbackMiddleware — kolejka modeli zapasowych podawana po przecinku. Gdy główny dostawca ma awarię, ruch idzie na następnego z listy,
  • humanInTheLoopMiddleware — zatrzymuje pętlę i czeka na akceptację człowieka przed wywołaniem wskazanych narzędzi (interruptOn). Wymaga skonfigurowanego checkpointera, bo stan musi przetrwać między żądaniami,
  • piiMiddleware i piiRedactionMiddleware — wykrywanie danych osobowych ze strategią redact, mask, hash albo block, domyślnie także na wejściu (applyToInput),
  • llmToolSelectorMiddleware — gdy agent ma czterdzieści narzędzi, tańszy model wybiera najpierw kilka pasujących do pytania, a dopiero potem rusza model główny. Zbija to zużycie kontekstu na samych definicjach narzędzi,
  • todoListMiddleware — dokłada agentowi narzędzie write_todos i prowadzenie własnej listy zadań, bez żadnej konfiguracji,
  • toolEmulatorMiddleware — udaje wykonanie wskazanych narzędzi modelem językowym. Do testów, gdy nie chcesz w CI dzwonić do prawdziwego ERP-a,
  • anthropicPromptCachingMiddleware i bedrockPromptCachingMiddleware — jawne sterowanie cache promptu u dostawców, którzy je udostępniają,
  • openAIModerationMiddleware, dynamicSystemPromptMiddleware, providerToolSearchMiddleware, toolErrorMiddleware — moderacja treści, prompt systemowy budowany w locie, wyszukiwanie po stronie dostawcy i obsługa błędów narzędzi.

Składa się je jak warstwy — kolejność ma znaczenie, bo każde kolejne opakowuje poprzednie:

import {
  createAgent,
  humanInTheLoopMiddleware,
  modelCallLimitMiddleware,
  piiMiddleware,
  summarizationMiddleware,
  toolCallLimitMiddleware,
  toolRetryMiddleware,
} from "langchain";
import { MemorySaver } from "@langchain/langgraph";

const agent = createAgent({
  model: "anthropic:claude-sonnet-4-6",
  tools: [statusZamowienia, zwrotPieniedzy],
  middleware: [
    piiMiddleware("email", { strategy: "redact", applyToInput: true }),
    summarizationMiddleware({ model: "openai:gpt-5.5", keep: 20 }),
    toolRetryMiddleware({ maxRetries: 2 }),
    toolCallLimitMiddleware({ runLimit: 10 }),
    modelCallLimitMiddleware({ runLimit: 8 }),
    humanInTheLoopMiddleware({ interruptOn: { refund_order: true } }),
  ],
  checkpointer: new MemorySaver(),
});

To jest dziesięć linijek konfiguracji zamiast tygodnia pisania warstwy, która i tak byłaby gorsza. Agent z tego przykładu ma redakcję adresów e-mail, automatyczne streszczanie długiej rozmowy, ponowienia narzędzi, dwa niezależne bezpieczniki kosztowe i wymuszoną akceptację człowieka przed zwrotem pieniędzy.

Własne middleware

createMiddleware przyjmuje nazwę i wybrane haki. Najczęstszy przypadek u nas to telemetria — wpięcie czasu wywołania modelu w ten sam system metryk, do którego raportuje reszta aplikacji:

import { createMiddleware } from "langchain";

const pomiarCzasu = createMiddleware({
  name: "PomiarCzasu",
  wrapModelCall: async (request, handler) => {
    const start = performance.now();

    try {
      return await handler(request);
    } finally {
      metryki.histogram("agent.model_call_ms", performance.now() - start);
    }
  },
});

Middleware może też rozszerzyć stan agenta o własne pola przez stateSchema — to droga na przekazanie przez całą pętlę identyfikatora dzierżawcy, licznika czy flagi, których pętla domyślnie nie zna.

Pamięć, wątki i przerwania

Agent domyślnie nie pamięta niczego między wywołaniami. Ciągłość rozmowy daje checkpointer plus identyfikator wątku podawany przy każdym wywołaniu:

import { MemorySaver } from "@langchain/langgraph";

const agent = createAgent({
  model: "anthropic:claude-sonnet-4-6",
  tools: [statusZamowienia],
  checkpointer: new MemorySaver(),
});

const config = { configurable: { thread_id: rozmowaId } };

await agent.invoke(
  { messages: [{ role: "user", content: "Co z zamówieniem 55321?" }] },
  config,
);

await agent.invoke(
  { messages: [{ role: "user", content: "A kiedy dotrze?" }] },
  config,
);

Drugie wywołanie zna kontekst pierwszego, bo trafia w ten sam thread_id. MemorySaver trzyma stan w pamięci procesu i nie nadaje się na produkcję — przy dwóch instancjach usługi za load balancerem połowa zapytań trafi do procesu, który tej rozmowy nie widział. Na produkcji używa się checkpointera opartego o bazę; w naszym przypadku to jest ten sam PostgreSQL, który obsługuje aplikację.

Ponieważ pod spodem siedzi LangGraph, ze stanu wynikają za darmo trzy dalsze rzeczy: trwałość (proces może paść w połowie i wątek wznowi się z ostatniego punktu), przerwania (agent zatrzymuje się na akceptacji człowieka i czeka dowolnie długo, bo stan leży w bazie, nie w pamięci) oraz cofanie — możliwość wznowienia wątku z wcześniejszego punktu i puszczenia go inną ścieżką. Ostatnie bywa nieocenione przy diagnozowaniu rozmowy, która skończyła się źle.

Osobno od stanu wątku istnieje context — dane na jedno uruchomienie, opisane przez contextSchema: identyfikator użytkownika, dzierżawa, flagi funkcji. Nie trafia do historii rozmowy i nie zjada kontekstu modelu, a jest dostępny dla narzędzi i middleware.

Strumieniowanie

Agent, który myśli osiem sekund i dopiero potem coś pokazuje, jest w interfejsie nie do obrony. agent.stream() przyjmuje streamMode o pięciu wartościach:

  • "messages" — pojedyncze tokeny z modelu wraz z metadanymi węzła; to jest tryb do pisania odpowiedzi na żywo,
  • "updates" — zmiany stanu po każdym kroku pętli. Do pokazywania „szukam w bazie zamówień…”,
  • "values" — pełny stan po każdym kroku,
  • "custom" — dane emitowane przez samo narzędzie przez config.writer(), np. postęp długiego zadania,
  • "debug" — komplet zdarzeń do diagnostyki.

Tryby da się łączyć, podając tablicę — wynik zmienia się wtedy w pary [tryb, dane]. Wersja najprostsza:

for await (const [token] of await agent.stream(
  { messages: [{ role: "user", content: pytanie }] },
  { streamMode: "messages" },
)) {
  process.stdout.write(token.text ?? "");
}

Odpowiedź o wymuszonej strukturze

Klasyfikacja zgłoszeń, wyciąganie danych z faktury, ocena treści — wszędzie tam nie chce się tekstu, tylko obiektu. responseFormat przyjmuje schemat zod, a wynik ląduje w structuredResponse:

import * as z from "zod";

const Klasyfikacja = z.object({
  kategoria: z.enum(["reklamacja", "zapytanie", "spam"]),
  pilnosc: z.number().min(1).max(5),
  uzasadnienie: z.string(),
});

const agent = createAgent({
  model: "openai:gpt-5.5",
  tools: [statusZamowienia],
  responseFormat: Klasyfikacja,
});

const { structuredResponse } = await agent.invoke({
  messages: [{ role: "user", content: trescZgloszenia }],
});

W wersji 1.0 generowanie struktury zostało wciągnięte do głównej pętli agenta — wcześniej wymagało dodatkowego wywołania modelu. Przy tysiącu zgłoszeń dziennie to jest tysiąc wywołań mniej.

RAG bez łańcuchów

Wyszukiwanie po własnych dokumentach jest w v1 rozegrane inaczej niż w v0, gdzie służył do tego gotowy łańcuch. Dokumentacja wyróżnia trzy podejścia:

  1. RAG dwuetapowy — najpierw wyszukanie, potem generowanie. Przewidywalny, jedno wywołanie modelu, najniższe opóźnienie. Do wyszukiwarki po dokumentacji to zwykle wystarcza,
  2. RAG agentowy — wyszukiwanie jest narzędziem, a model sam decyduje, kiedy i o co zapytać. Droższy i wolniejszy, ale radzi sobie z pytaniami wymagającymi kilku różnych zapytań,
  3. RAG hybrydowy — sztywny pierwszy krok plus agentowe doszukiwanie, gdy pierwszy nie wystarczył.

Wariant agentowy to po prostu kolejne narzędzie — żadnej osobnej abstrakcji nie ma:

import { tool } from "langchain";
import * as z from "zod";

const szukajWBazieWiedzy = tool(
  async ({ pytanie }) => {
    const wyniki = await vectorStore.similaritySearch(pytanie, 4);

    return wyniki.map((d) => d.pageContent).join("\n\n---\n\n");
  },
  {
    name: "szukaj_w_bazie_wiedzy",
    description: "Przeszukuje wewnętrzną dokumentację i zwraca najtrafniejsze fragmenty",
    schema: z.object({
      pytanie: z.string().describe("Pytanie w języku naturalnym"),
    }),
  },
);

Cegiełki do zbudowania vectorStore są w osobnych pakietach: @langchain/textsplitters do dzielenia dokumentów, integracja dostawcy embeddingów, no i sama baza wektorowa. Wspierane są między innymi MemoryVectorStore (do testów), pgvector i Qdrant (@langchain/qdrant 1.0.3). Wybór pgvectora ma tę zaletę, że nie dokłada kolejnej usługi do docker-compose.yml — wektory leżą w tej samej bazie co reszta aplikacji, w jednej kopii zapasowej i jednej transakcji.

Jak to wpiąć w aplikację Laravela

LangChain.js jest biblioteką Node’a i nie ma odpowiednika w PHP, który dorównywałby jej zakresem. Przepisywanie aplikacji na Node z tego powodu byłoby jednak ruchem odwrotnym do zamierzonego — sprawdza się układ, w którym agent jest osobnym kontenerem obok pozostałych usług Saila, a Laravel rozmawia z nim po HTTP.

Usługa agenta w docker-compose.yml, bez wystawiania portu na zewnątrz — ma być widoczna wyłącznie z sieci wewnętrznej:

services:
    agent:
        build: ./agent
        environment:
            ANTHROPIC_API_KEY: '${ANTHROPIC_API_KEY}'
            DATABASE_URL: '${AGENT_DATABASE_URL}'
            LANGSMITH_TRACING: 'false'
        networks:
            - sail
        expose:
            - '3001'

Sam serwis to kilkanaście linijek — tu na Hono, ze strumieniowaniem przez SSE:

import { Hono } from "hono";
import { streamSSE } from "hono/streaming";
import { createAgent } from "langchain";

const app = new Hono();

const agent = createAgent({
  model: "anthropic:claude-sonnet-4-6",
  tools: [statusZamowienia, szukajWBazieWiedzy],
  middleware: [toolRetryMiddleware({ maxRetries: 2 }), modelCallLimitMiddleware({ runLimit: 8 })],
  checkpointer,
});

app.post("/agent/stream", async (c) => {
  const { message, threadId } = await c.req.json();

  return streamSSE(c, async (stream) => {
    for await (const [token] of await agent.stream(
      { messages: [{ role: "user", content: message }] },
      { streamMode: "messages", configurable: { thread_id: threadId } },
    )) {
      await stream.writeSSE({ data: token.text ?? "" });
    }
  });
});

export default { port: 3001, fetch: app.fetch };

Po stronie Laravela adres usługi idzie do konfiguracji, żeby dało się ją podmienić bez ruszania kodu:

// config/services.php
'agent' => [
    'url' => env('AGENT_URL', 'http://agent:3001'),
    'token' => env('AGENT_TOKEN'),
],
namespace App\Services;

use Illuminate\Support\Facades\Http;

class AgentClient
{
    public function __construct(
        private readonly string $baseUrl,
        private readonly ?string $token,
    ) {}

    public function ask(string $message, string $threadId): string
    {
        return Http::withToken($this->token)
            ->timeout(120)
            ->connectTimeout(5)
            ->post($this->baseUrl.'/agent', [
                'message' => $message,
                'threadId' => $threadId,
            ])
            ->throw()
            ->json('reply');
    }
}

Kilka decyzji z tego układu warto uzasadnić, bo każda wynika z konkretnej pułapki:

  • timeout(120) przy connectTimeout(5). Agent, który wykonuje pięć wywołań narzędzi, potrafi mielić minutę i to jest normalne. Odróżnienie „usługa nie odpowiada” od „usługa pracuje” wymaga dwóch osobnych limitów, bo przy jednym wspólnym albo zabijasz działające zapytania, albo czekasz dwie minuty na kontener, który nie wstał,
  • zapytania synchroniczne tylko tam, gdzie użytkownik czeka. Wszystko, co da się zrobić w tle — klasyfikacja zgłoszeń, streszczanie, wzbogacanie danych — idzie na kolejkę Laravela, po jednym zadaniu na dokument. RateLimited na tym zadaniu chroni przed wyczerpaniem limitu dostawcy modelu przy nagłym imporcie,
  • thread_id zarządzany po stronie Laravela. Identyfikator wątku jest kluczem do historii rozmowy, więc trzymamy go w bazie razem z rekordem konwersacji i uprawnieniami. Usługa agenta nie ma własnego pojęcia o tym, kto jest zalogowany, i nie powinna mieć,
  • brak wystawionego portu. Kontener agenta trzyma klucz API dostawcy modelu. Wystawienie go poza sieć wewnętrzną oznacza oddanie tego klucza — a przy strumieniowaniu przez SSE łatwo o tym zapomnieć, bo kusi, żeby przeglądarka łączyła się z agentem bezpośrednio. Strumień przepuszczamy przez Laravela, który sprawdza uprawnienia.

Obserwowalność, czyli LangSmith i jego brak

Pakiet langsmith jest zwykłą zależnością langchain, więc jest zainstalowany zawsze. Zbieranie śladów jest jednak domyślnie wyłączone i włącza się je zmiennymi środowiskowymi:

LANGSMITH_TRACING=true
LANGSMITH_API_KEY=lsv2_...
LANGSMITH_PROJECT=obsluga-klienta
LANGSMITH_ENDPOINT=https://eu.api.smith.langchain.com

Warto o tym wiedzieć z dwóch powodów. Po pierwsze, LangSmith jest bardzo dobrym narzędziem do diagnozowania agentów — pokazuje pełne drzewo wywołań, prompty, wyniki narzędzi i koszt każdego kroku, a bez tego debugowanie pętli agenta sprowadza się do czytania logów. Po drugie, to jest usługa komercyjna, do której wysyłasz treść rozmów. Przy danych klientów to jest decyzja do podjęcia świadomie, z uwzględnieniem RODO i umowy powierzenia, a nie zmienna wklejona do .env, bo tak było w poradniku. Endpoint da się przestawić na region europejski (jak wyżej) albo na instancję własną — plan self-hosted jest osobną pozycją w cenniku.

Alternatywa dla zespołów, które nie chcą wysyłać niczego na zewnątrz: własne middleware wrapModelCall raportujące do systemu metryk, który już masz. Nie da to drzewa wywołań, ale da czasy, koszty i liczbę kroków — a to zwykle wystarcza do zauważenia, że coś jest nie tak.

Rachunek za utrzymanie

Framework, który zdejmuje z zespołu tydzień pracy, dokłada w zamian własne koszty. Warto je znać przed decyzją, a nie po.

Tempo wydań jest wysokie. Między 8 lipca a 20 sierpnia 2026 pakiet langchain dostał osiem wydań; w tygodniu 12–14 sierpnia były trzy, jedno po drugim. Obietnica braku zmian łamiących zgodność do wersji 2.0 obowiązuje i jest dotrzymywana, ale w praktyce oznacza to, że package-lock.json trzeba pilnować, a aktualizacje robić świadomie, z testami — nie automatem od bota.

Zgłoszeń jest dużo. W repozytorium wisi 243 otwartych zgłoszeń i 313 otwartych pull requestów. Część z nich dotyczy rzeczy, na które można się realnie natknąć — na przykład tego, że wewnętrzne wywołania modelu wykonywane przez summarizationMiddleware i llmToolSelectorMiddleware trafiają do strumienia widocznego dla interfejsu (zgłoszenia #9455 i #10042). Przy strumieniowaniu tokenów wprost do przeglądarki użytkownik zobaczy wtedy fragmenty streszczenia albo wyboru narzędzi zamiast odpowiedzi. Filtrowanie po metadanych węzła w trybie "messages" rozwiązuje problem, ale trzeba o nim wiedzieć zawczasu.

Dokumentacja goni kod. Przy tym tempie zdarza się, że przykład w dokumentacji odnosi się do nazwy modelu albo opcji sprzed kilku wersji. Referencja API na reference.langchain.com jest generowana z kodu i jest wiarygodniejsza niż strony poradnikowe.

Abstrakcja kosztuje przy diagnozowaniu. Gdy agent zachowuje się dziwnie, między Twoim kodem a API dostawcy stoi pętla LangGraph, warstwa middleware i warstwa integracji. Bez włączonych śladów albo własnej telemetrii wnioskowanie o przyczynie jest trudniejsze niż przy bezpośrednim wywołaniu SDK.

Kiedy tego nie brać

  • Jedno wywołanie modelu, jeden prompt, żadnych narzędzi. Wywołaj SDK dostawcy bezpośrednio. Dokładanie frameworka do jednego fetch-a to koszt bez korzyści,
  • Twarde wymagania opóźnieniowe. Pętla agenta z middleware ma swój narzut; przy budżecie liczonym w dziesiątkach milisekund liczy się każda warstwa,
  • Zespół bez kompetencji w Node.js. Utrzymanie osobnej usługi w drugim języku to realny koszt organizacyjny. Jeśli w firmie nikt nie chce jej dotykać, lepsze będzie wywoływanie API dostawcy wprost z PHP, nawet kosztem ręcznego pisania tego, co framework ma gotowe,
  • Rozmowa w przeglądarce z kluczem po stronie klienta. Biblioteka owszem, działa w przeglądarce — ale klucz dostawcy w kodzie frontendu to nie jest kwestia architektury, tylko rachunku, który przyjdzie.

Stan projektu

Liczby, na podstawie których warto podejmować decyzję:

  • 18,1 tys. gwiazdek i 3355 forków, kod w TypeScripcie, licencja MIT,
  • repozytorium powstało w lutym 2023, ostatnie commity z dziś — w tym tygodniu doszło wsparcie dla nowych modeli obu głównych dostawców,
  • ponad 440 osób na liście kontrybutorów,
  • 243 otwarte zgłoszenia i 313 otwartych pull requestów,
  • za projektem stoi spółka LangChain, która zarabia na LangSmith i LangGraph Platform — biblioteka open source jest jej lejkiem, nie projektem pobocznym. To oznacza stabilne finansowanie rozwoju i jednocześnie stałe ciążenie w stronę płatnych usług w dokumentacji.

Nasza ocena: to jest projekt żywy, dobrze utrzymany i po v1 wreszcie spójny. Główny zarzut wobec LangChaina z lat 2023–2024 — że jest workiem abstrakcji, w którym prostą rzecz robi się przez pięć warstw — został adresowany wprost, przez wyrzucenie tych abstrakcji do osobnego pakietu. To, co zostało, jest małe i ma czytelną odpowiedzialność: pętla, narzędzia, middleware, stan.

Podsumowanie

LangChain.js po wersji 1.0 przestał być frameworkiem do składania łańcuchów, a stał się warstwą uruchomieniową dla agentów. Jeśli budujesz coś, co wywołuje model w pętli i sięga po narzędzia, robi to za Ciebie lepiej, niż zrobiłbyś sam w rozsądnym czasie.

Co dostajesz:

  • createAgent jako jedno API — model, narzędzia, prompt systemowy i middleware w jednym wywołaniu, bez klas wykonawczych i bez łańcuchów,
  • narzędzia opisane schematem zod, z walidacją argumentów i opisem trafiającym wprost do promptu,
  • kilkanaście gotowych middleware — streszczanie kontekstu, czyszczenie starych wyników narzędzi po 100 tys. tokenów, ponowienia modelu i narzędzi, modele zapasowe, limity wywołań, redakcja danych osobowych, akceptacja człowieka przed wskazanym narzędziem, emulacja narzędzi w testach,
  • trwały stan z LangGraph — wątki po thread_id, wznawianie po awarii, przerwania na akceptację i cofanie rozmowy do wcześniejszego punktu,
  • pięć trybów strumieniowania, w tym tokeny i zdarzenia własne emitowane przez narzędzia,
  • odpowiedzi o wymuszonej strukturze wciągnięte do głównej pętli, bez dodatkowego wywołania modelu,
  • jedna abstrakcja na wszystkich dostawców — zmiana modelu to zmiana napisu "dostawca:model", a nie przepisywanie integracji.

O czym pamiętać: MemorySaver jest wyłącznie do testów i na produkcji potrzebny jest checkpointer w bazie; tempo wydań jest wysokie, więc aktualizacje robi się z testami, a nie automatem; wewnętrzne wywołania modelu z summarizationMiddleware i llmToolSelectorMiddleware potrafią wyciec do strumienia widocznego w interfejsie (otwarte zgłoszenia #9455 i #10042); LangSmith jest domyślnie wyłączony i włączenie go oznacza wysyłanie treści rozmów do usługi zewnętrznej — przy danych klientów jest to decyzja wymagająca sprawdzenia podstawy prawnej, nie zmiana w .env. Materiały starsze niż koniec 2025 roku opisują nieaktualne API.

Licencja: cały ekosystem — langchain, @langchain/core, @langchain/langgraph, @langchain/classic, integracje dostawców i deepagents — jest na MIT. To licencja permisywna bez zastrzeżeń przy użyciu komercyjnym, również w oprogramowaniu zamkniętym i w projektach wdrażanych u klienta; nie wymaga udostępniania kodu ani nie nakłada obowiązków wykraczających poza zachowanie noty licencyjnej. Płatne są usługi wokół biblioteki — LangSmith i platforma wdrożeniowa — ale korzystanie z samego frameworka ich nie wymaga i działa bez konta w czymkolwiek.