Podstawy LangChain: co rozwiązuje i kiedy można go całkowicie pominąć.
Dowiedz się, co dokładnie oferuje LangChain, dlaczego narzędzia z wieloma dostawcami skłaniają zespoły do jego wykorzystania, jakie pakiety należy zainstalować oraz kiedy lepszym wyborem jest prosty wrapper.
Źródła: [Day 11/100] LangChain 101: Why It Exists, and When You Should (and Should Not) Use It
Agent składający się z pięćdziesięciu linijek przestaje mieć pięćdziesiąt linijek
Dodanie nowego narzędzia do małego, ręcznie tworzonego agenta jest tanie. Piszesz funkcję, rejestrujesz ją w słowniku, opisujesz ją w schemacie, a sam pętla nigdy się nie zmienia. Ta część pracy nad agentem sprawia przyjemność i wprowadza ludzi w błąd, sprawiając, że myślą, iż cała praca pozostanie tak mała.
Wtedy pojawiają się prawdziwe wymagania. Koniec API OpenAI od czasu do czasu zwraca błąd 503, więc konieczne są próby ponowne z opóźnieniem. Czasami usługa dostawcy jest niedostępna przez dwadzieścia minut, dlatego potrzebny jest drugi dostawca jako alternatywa. Użytkownik czekający osiem sekund na zakończenie czterech powiązanych wywołań założy, że aplikacja się zamarła, więc dodaje się strumieniowanie tokenów. Usługa poniżej w łańcuchu odrzuca odpowiedź, ponieważ jedno pole zostało zwrócone jako ciąg znaków zamiast liczby, więc teraz waliduje się strukturalny wyjście. A gdy dziesięć osób korzysta z niej jednocześnie, blokujące wywołania przestają być do przyjęcia, więc wszystko musi być asynchroniczne.
Wszystko to buduje się na bazie surowych bibliotek openai i requests, a agent stopniowo rozrasta się do około pięciuset linii kodu. Bardzo niewiele z tego kodu dotyczy tego, co agent faktycznie robi – chodzi tu o parsowanie JSON, strategię exponential backoff oraz konwertowanie między formatami wiadomości, które każdy dostawca opracował samodzielnie. Każdy zespół musi pisać takie elementy infrastruktury, a nikt nie chce tego robić ponownie.
To właśnie tę infrastrukturę oferuje produkt LangChain. Poniżej znajduje się opis tego, czym naprawdę jest ta biblioteka, problemów, które ma rozwiązywać, oraz, równie ważne, sytuacji, w których jej dodanie jest błędem.
Czym jest (a czym nie jest) biblioteka
LangChain nie uruchamia twojego agenta za ciebie, nie hostuje żadnych zasobów i nie poprawia zdolności rozumowania modelu. Traktuj to jako zestaw wspólnych interfejsów wraz z gotowymi implementacjami elementów aplikacji opartej na LLM, które w przeciwnym razie musiałbyś napisać sam.
Jeśli już stworzyłeś prosty agenta, to większość tych elementów została już zbudowana pod różnymi nazwami. Lista wiadomości, które przekazujesz, odpowiada ChatMessageHistory. Schemat narzędzia staje się Tool lub zwykłą funkcją ozdobioną tagiem @tool. Pętla while, która nieustannie wywołuje model, dopóki ten nie przestanie prosić o narzędzia, to właśnie to robił AgentExecutor. Natomiast bezpośrednie wywołanie client.chat.completions.create przekształca się w ChatModel. Nic tutaj nie jest koncepcyjnie nowe. Zmienia się to, że te elementy mają teraz wspólną strukturę, którą już wykorzystują tysiące innych projektów, a ich kombinacje są już gotowe do użycia.
Problemy, dla których został stworzony
Zacznij od wywoływania narzędzi, ponieważ jasno pokazuje to cały bałagan. OpenAI dołącza element tool_calls do wiadomości asystenta i oczekuje wyniku w postaci wiadomości z atrybutem role: „tool”. Anthropic wysyła bloki zawierające tool_use i oczekuje bloków tool_result w następnej turze użytkownika. Gemini używa elementów function_call oraz function_response. To ta sama koncepcja, trzy API, trzy różne formaty, które nie pasują do siebie.
Przy jednym dostawcy to nigdy nie stanowi problemu. Gdy potrzebujesz drugiego – być może dlatego, że pierwszy ma awarię lub po prostu stał się droższy – musisz albo pisać i utrzymywać trzy adaptery, albo korzystać z kodu, który już to obsługuje. To był pierwotny powód powstania LangChain: jedna interfejs ChatModel, w której metody .invoke() i .bind_tools() działają tak samo, niezależnie od tego, która firma produkuje tokeny. Przejście z ChatOpenAI na ChatAnthropic polega jedynie na zmianie importu, a nie na przepisywaniu mechanizmu obsługi wiadomości.
Kompozycja to drugi problem. Wprowadzanie promptu do modelu, następnie modelu do parsera, a potem parsera do innego modelu oznacza konieczność dodawania kodu łączącego na każdej granicy, a ten kod musi być przepisywany za każdym razem, gdy struktura łańcucha zmienia się. LangChain ujednolica te granice. Mechanizmem tego jest LCEL – język wyrażeń LangChain, który zasługuje na osobny opis.
Trzecim problemem jest prosta możliwość ponownego użycia. Agent, który robi coś więcej niż tylko wywołuje model, potrzebuje elementów już stworzonych przez innych: narzędzi do ładowania około czterdziestu formatów dokumentów, klientów do dziesiątek baz danych wektorowych, polityk ponawiania prób, funkcji strumieniowania, grupowania danych oraz mechanizmów śledzenia. Dobrze utrzymywana integracja jest prawie zawsze lepsza niż parser PDF napisany w piątek, który teraz trzeba debugować.
Co otrzymujesz, nie pisząc tego od nowa
Jedna interfejs modelu. ChatOpenAI, ChatAnthropic, ChatGoogleGenerativeAI oraz ChatOllama przeznaczone do modeli lokalnych wszystkie udostępniają metody .invoke(), .stream() i .bind_tools(). Twój kod nie musi już wiedzieć, kto trenował model użyty podczas wywołania.
Komponowalne elementy budulcowe. Dzięki operatorowi łączącemu w LCEL możesz napisać prompt | model | parser i otrzymać jeden obiekt, który zachowuje się jak każdy z jego składników. Może przesyłać dane strumieniowo, przetwarzać je pakietowo oraz uruchamiać operacje asynchroniczne bez dodatkowej pracy.
Odporność jako wywołanie metody. Każdy obiekt typu Runnable posiada metody .with_retry() i .with_fallbacks(), dzięki czemu nie musisz ręcznie pisać pętli ponawiania prób ani mechanizmu przełączania dostawców.
Striming i asynchroniczność wszędzie. Każdy obiekt typu Runnable posiada metody .stream(), .ainvoke(), .astream() oraz .batch(), nawet jeśli oryginalny obiekt, który go otacza, nigdy nie obsługiwał ich w sposób natywny.
Szeroki katalog integracji. Narzędzia do ładowania dokumentów, przechowywania wektorów, wyszukiwania, wykonywania kodu oraz narzędzia do pracy z bazami danych. Wiele z nich jest utrzymywanych przez samych dostawców, a wszystkie korzystają z tej samej interfejsu Runnable.
Jednoznaczna droga do LangGraph. Gdy twój agent potrzebuje pętli, zapisanego stanu lub kroku zatwierdzenia przez człowieka, którego nie da się zaprogramować w liniowej łańcuchu, pętla sterująca przenosi się do LangGraph. Jest on zbudowany na tych samych podstawowych abstrakcjach, więc to, czego nauczysz się w LangChain, można bezpośrednio zastosować tutaj.
Kiedy go pominąć
Większość wprowadzeń pomija tę część, a to właśnie ona pozwala uniknąć największych problemów później.
Jeden dostawca, jeden wywołanie, na zawsze. Jeśli cały produkt polega na „wyśłaniu tego zapytania do GPT-4o i otrzymaniu tekstu”, prosty własny wrapper jest łatwiejszy do odczytania i naprawiania niż zależność wymagająca setek dodatkowych pakietów.
Musisz widzieć każde żądanie. Abstrakcje są przydatne właśnie dlatego, że ukrywają szczegóły, a to ukrywanie kosztuje cię czasu, gdy coś się psuje. Przeszukiwanie ścieżki wywołań przez sześć warstw wrapperów typu Runnable, zanim dotrzesz do rzeczywistego błędu, to strata czasu. Zespoły, które chcą czytać każdą linię kodu oraz żądania HTTP, często korzystają bezpośrednio z surowego SDK.
Ścieżki krytyczne pod względem opóźnień. Każda warstwa dodaje pewien nakład pracy. Obok wywołania modelu trwającego 500 ms lub więcej ten nakład jest zwykle bez znaczenia. Natomiast na ścieżkach o wysokim obciążeniu, gdzie liczy się każdy milisekundę, czasem ma to znaczenie.
Aktualizacje bez fiksacji wersji. LangChain regularnie wprowadza zmiany, które mogą zakłócić funkcjonowanie aplikacji. Projekt oparty na najnowszej wersji w końcu może napotkać zmienione nazwy importów lub ustawienia domyślne. Zablokuj tę wersję jak każdą inną zależność i aktualizuj ją wtedy, gdy sam to zdecydujesz, a nie wtedy, gdy to zrobi za ciebie pip.
To wcale nie oznacza, że nigdy nie powinieneś używać LangChain. Oznacza to, że decyzję należy podejmować indywidualnie dla każdego projektu, świadomie, a nie z przyzwyczajenia.
Który pakiet jest który
LangChain jest podzielony na kilka pakietów, więc warto wiedzieć, jakie funkcje pełni każdy z nich, zanim cokolwiek zainstalujesz.
langchain-corezawiera podstawowe abstrakcje:Runnable,BaseChatModel,BaseTool, szablony zapytań oraz parsery wyników. Ma niewiele zależności, a prawie wszystko inne jest oparte na nim.langchaindodaje łańcuchy, agenty oraz logikę pozyskiwania danych na bazie jądra, nie wiążąc użytkownika z konkretnym dostawcą.langchain-communitygromadzi integracje z trzecich stron. Niektóre z nich są doskonałe, a inne porzucone, ponieważ każdy może wnieść swój wkład. Sprawdź, kiedy ostatnio zaktualizowano daną integrację, zanim zaczniesz jej używać w produkcji.- Pakiety partnerskie takie jak
langchain-openai,langchain-anthropicilangchain-google-genaisą utrzymywane równolegle z własnym SDK każdego dostawcy. Zainstaluj tylko te, których faktycznie używasz. langchain-classicto miejsce, gdzie w wersji 1 znajdowały się konstruktory agentów z czasów przed wersją 1.0:AgentExecutor,create_tool_calling_agentoraz starycreate_react_agent. Istniejący kod nadal działa, ale nowe agenty nie powinny być tworzone w tym miejscu.langgraphto odrębny pakiet oparty nalangchain-core, który zarządza pętlą sterującą dla wszystkich przypadków, które nie są liniowe. Od wersjilangchainv1 zalecany budownik agentów,langchain.agents.create_agent, jest w nim implementowany bezpośrednio.
Aby zacząć, potrzebujesz tylko trzech pakietów:
uv add langchain langchain-openai python-dotenv
Zamiana surowego klienta na ChatOpenAI
Nie oczekuj niczego spektakularnego na tym etapie. Celem jest zastąpienie zwykłego klienta OpenAI obudową modelu czatowego LangChain i sprawdzenie, czy otrzymasz tę samą odpowiedź.
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model="gpt-4o", temperature=0)
response = llm.invoke("What is the weather in Paris?")
print(response.content)
llm.invoke(...) zwraca obiekt AIMessage zamiast surowego słownika, ale .content dostarcza tekst w taki sam sposób jak .choices[0].message.content. W głębi sprawy ChatOpenAI nadal korzysta z tego samego endpointu OpenAI, który użyłbyś ręcznie, a model w ogóle się nie zmienił. Różnica polega na tym, że llm jest teraz obiektem typu Runnable, więc metody .stream(), .batch() i .ainvoke() już istnieją, a ty ich nie pisałeś.
for chunk in llm.stream("Tell me one fact about Paris."):
print(chunk.content, end="", flush=True)
Zainstaluj to, a tokeny będą przychodzić po jednym pakiecie. Nigdy nie pisałeś obsługi strumieniowej, a ten mały pętla stanowi praktycznie całe uzasadnienie dla tej biblioteki.
Kilka słów na temat nazw modeli: przykłady używają "gpt-4o" jako zamiennika. Lista dostawców zmienia się szybciej, niż jakikolwiek tutorial może za tym nadążyć, więc traktuj każdą wartość model="..." jako zastępczą i podstaw to, co oferuje twój dostawca, gdy uruchamiasz kod. Zasady działania pozostają takie same.
Spróbuj to na swoim własnym agencie
- Dodaj
langchainorazlangchain-openaido swojego projektu za pomocąuv add. - W kroku związanych z modelem w twoim agencie zastąp surowe wywołanie
OpenAI()funkcjąChatOpenAIi nie zmieniaj reszty pętli. Sprawdź, czy agent zachowuje się dokładnie tak samo jak wcześniej. - Wezwij
llm.stream(...)dla prostej pytania bez użycia żadnych narzędzi i wydrukuj każdy fragment w momencie jego otrzymania. Teraz masz możliwość strumieniowania danych dla metody, której nigdy nie zaimplementowałeś.
Twój agent nie staje się mądrzejszy po tej zmianie. Zyskałeś natomiast fundament, dzięki któremu ponawianie prób, mechanizmy awaryjne, strumieniowanie danych oraz zmiana dostawcy kosztują tylko jedno wezwanie metody, zamiast setek dodatkowych linijek kodu.