Jak ograniczyć koszty tokenów OpenAI w aplikacjach LangChain za pomocą semantycznego cache

0
19
Rate this post

Aplikacja oparta na LangChain zaczyna działać poprawnie, ale rachunki za wywołania OpenAI rosną szybciej niż liczba użytkowników. Analiza logów pokazuje, że pytania często różnią się tylko szykiem słów: „Jak zwrócić produkt?”, „W jaki sposób można dokonać zwrotu?” i „Czy produkt mogę oddać?” prowadzą do tej samej informacji w bazie wiedzy. Cache dokładny nie uzna ich za ten sam prompt, natomiast semantyczny cache może rozpoznać podobieństwo znaczeniowe i wykorzystać wcześniej wygenerowaną odpowiedź. [2]

To nie oznacza jednak, że wystarczy dodać bazę wektorową i wyłączyć część wywołań modelu. Największe ryzyko polega na zwróceniu odpowiedzi pozornie pasującej, lecz nieaktualnej albo dotyczącej innego przypadku. Semantyczny cache powinien być traktowany jako decyzja architektoniczna: trzeba określić, co można ponownie wykorzystać, jak długo odpowiedź pozostaje aktualna, kiedy podobieństwo jest wystarczające oraz w jakich sytuacjach system ma bezwarunkowo ominąć cache.

Przed wdrożeniem pojawiają się zwykle konkretne pytania: czy zwykłe cache’owanie promptów nie wystarczy, czy embedding nie będzie kosztował więcej niż zaoszczędzone wywołania, jaki próg podobieństwa wybrać, czy RedisSemanticCache lub GPTCache pasują do aktualnej wersji stosu, jak unieważniać stare odpowiedzi i jak sprawdzić, czy cache nie pogarsza jakości. Odpowiedzi zależą od rodzaju danych oraz od tego, czy aplikacja generuje odpowiedzi informacyjne, wykonuje operacje na danych użytkownika, czy podejmuje decyzje wymagające świeżego kontekstu.

Dlaczego zwykły cache nie wystarcza w aplikacji LangChain

Dwa różne znaczenia cache’owania zapytań do modelu

Najprostszy cache odpowiedzi LLM działa jak pamięć dokładnych wywołań. Kluczem jest pełne wejście do modelu albo jego ustabilizowana reprezentacja: treść promptu, komunikaty systemowe, wiadomości użytkownika, parametry modelu, temperatura, format odpowiedzi i czasem dodatkowe dane przekazane przez aplikację. Jeżeli kolejne wywołanie ma identyczny klucz, aplikacja zwraca zapisaną odpowiedź bez ponownego generowania.

Kod HTML wyświetlony na ekranie komputera
Źródło: Pexels | Autor: anshul kumar

W ekosystemie LangChain parametr cache może być obsługiwany przez część modeli, a aplikacja może korzystać z globalnego cache’u albo z obiektu implementującego BaseCache. Nie należy jednak utożsamiać samej obecności tego mechanizmu z cache’em semantycznym. Standardowe dopasowanie klucza nie rozumie znaczenia pytania. Dla niego różnica między „Jak anulować zamówienie?” a „Czy mogę zrezygnować z zakupu?” może oznaczać dwa całkowicie różne wpisy.

Semantyczny cache działa inaczej. Zamiast porównywać tekst znak po znaku, zamienia zapytanie na wektor reprezentujący jego znaczenie, a następnie wyszukuje wcześniej zapisane zapytania o podobnym wektorze. Gdy podobieństwo przekracza ustalony próg, system może zwrócić powiązaną odpowiedź. To rozszerza zakres potencjalnych trafień, ale jednocześnie wprowadza ocenę probabilistyczną. Podobieństwo znaczeniowe nie jest dowodem, że użytkownik oczekuje dokładnie tej samej odpowiedzi.

Dlaczego mała zmiana promptu blokuje cache dokładny

Cache dokładny jest bardzo przewidywalny, lecz jego skuteczność spada, gdy prompt zawiera elementy zmienne. Do klucza może trafić identyfikator użytkownika, aktualna data, fragment historii rozmowy, kolejność dokumentów z retrievera, nazwa produktu albo instrukcja dodana przez middleware. Nawet jeśli końcowe pytanie ma to samo znaczenie, pełny prompt będzie inny.

Przykładowo aplikacja generująca streszczenie zgłoszenia może otrzymywać takie wejścia:

  • „Skróć poniższe zgłoszenie do trzech punktów: klient nie otrzymał przesyłki.”
  • „Wypisz trzy najważniejsze informacje z tego zgłoszenia: przesyłka nie dotarła do klienta.”
  • „Stwórz krótkie streszczenie, maksymalnie trzy punkty: klient informuje o braku dostawy.”

Pod względem znaczenia zadania są bliskie. Dla cache’u dokładnego to jednak trzy różne klucze, chyba że aplikacja wcześniej normalizuje wejście. Normalizacja może pomóc, ale ma granice: usuwanie spacji, ujednolicanie wielkości liter czy stabilizowanie kolejności parametrów jest bezpieczniejsze niż ręczne przepisywanie treści do „kanonicznego” pytania. Zbyt agresywna normalizacja może z kolei połączyć pytania, które powinny pozostać rozdzielone.

Gdzie pojawia się potencjalna oszczędność

Najbardziej oczywista korzyść pojawia się wtedy, gdy trafienie w cache pozwala pominąć wywołanie modelu generującego odpowiedź. Nie jest to jednak równoznaczne z całkowitym brakiem kosztu. Semantyczny cache zwykle wymaga obliczenia embeddingu dla nowego zapytania, wyszukania podobnych rekordów oraz obsługi indeksu. Jeżeli embeddingi są generowane przez zewnętrzny model, także ta operacja może mieć koszt i opóźnienie.

W praktyce trzeba rozdzielić co najmniej cztery składniki:

  • koszt lub czas obliczenia embeddingu zapytania,
  • koszt wyszukiwania podobnych rekordów w warstwie wektorowej,
  • koszt przechowywania, aktualizowania i usuwania wpisów,
  • koszt wywołania modelu generującego odpowiedź, którego można uniknąć tylko przy bezpiecznym trafieniu.

Dlatego semantyczny cache nie gwarantuje automatycznie zmniejszenia liczby tokenów wysyłanych do OpenAI w każdej implementacji. Efekt zależy od liczby trafień, sposobu indeksowania, kosztu embeddingów, długości promptów i tego, czy odpowiedź z cache rzeczywiście zastępuje generowanie. Jeśli próg podobieństwa jest zbyt niski, rośnie liczba fałszywych trafień. Jeśli jest zbyt wysoki, system zachowuje się niemal jak cache dokładny i oszczędność może być niewielka. [3]

Kiedy semantyczne podobieństwo jest mylące

Dwa pytania mogą być podobne językowo, ale wymagać różnych odpowiedzi. „Czy mogę zwrócić produkt?” i „Czy mogę zwrócić produkt kupiony w promocji?” dzielą dużą część słów, lecz dodatkowy warunek może zmieniać wynik. Podobnie pytania „Jaki jest termin płatności?” oraz „Jaki był termin płatności dla mojego poprzedniego zamówienia?” są podobne tematycznie, ale drugie wymaga dostępu do konkretnej historii.

Ryzyko zwiększa się, gdy odpowiedź zależy od czasu. Cennik, status przesyłki, dostępność towaru, saldo konta, regulamin promocji czy informacje o bieżącej awarii nie powinny być bezrefleksyjnie przechowywane jako uniwersalne odpowiedzi. W tych przypadkach cache musi uwzględniać wersję źródła, datę publikacji, zakres obowiązywania oraz tożsamość użytkownika, a czasem powinien być całkowicie wyłączony.

Scenariusze, w których cache ma sens

Semantyczne cache’owanie dobrze pasuje do publicznego FAQ, dokumentacji produktu i instrukcji, jeżeli treść jest względnie stabilna. Użytkownicy rzadko formułują pytania identycznie, ale oczekują odpowiedzi z tej samej, ograniczonej puli tematów. Przykładem może być asystent wyjaśniający sposób konfiguracji funkcji, wymagane kroki instalacji albo zasady zwrotu opisane w aktualnej dokumentacji.

Drugą grupę stanowią asystenci obsługujący podobne zgłoszenia. Tutaj cache nie powinien być globalnym magazynem wszystkich odpowiedzi. Bezpieczniejszy jest podział według intencji, wersji procedury, języka i rodzaju klienta. Odpowiedź dotycząca procesu reklamacyjnego dla konsumenta nie musi pasować do pytania przedsiębiorcy, nawet gdy embeddingi uznają oba teksty za bliskie.

Trzecim zastosowaniem są powtarzalne zadania o ustalonym formacie: streszczenia, klasyfikacje, ekstrakcja pól i generowanie krótkich opisów. Im bardziej stabilne są instrukcja, format i źródło danych, tym łatwiej ocenić, czy ponowne wykorzystanie odpowiedzi jest akceptowalne. Przy danych dynamicznych, poufnych albo zależnych od bieżącego stanu semantyczny cache wymaga dodatkowych ograniczeń, a czasem nie daje wystarczającej przewagi nad zwykłym wywołaniem modelu.

Zbliżenie kodu na ekranie komputera podczas pracy z LangChain
Źródło: Pexels | Autor: Godfrey Atima

Cztery warianty cache’a dla aplikacji korzystającej z LangChain

Wariant 1 — cache dokładny dla identycznych promptów

Cache dokładny jest punktem odniesienia dla pozostałych rozwiązań. Aplikacja zapisuje odpowiedź pod kluczem obejmującym całe wejście oraz parametry, które mogą wpływać na wynik. W zależności od architektury trzeba uwzględnić model, temperaturę, instrukcję systemową, narzędzia, format odpowiedzi, wersję promptu i istotny kontekst przekazywany do łańcucha.

Jego największą zaletą jest przewidywalność. Jeśli klucz jest taki sam, wiadomo, dlaczego odpowiedź została znaleziona. Łatwo odtworzyć przypadek w testach, usunąć konkretny rekord i sprawdzić, czy zmiana promptu unieważnia stare dane. Ryzyko zwrócenia odpowiedzi dla tylko „podobnego” pytania jest mniejsze, ponieważ system nie wykonuje przybliżonego dopasowania.

Minusem pozostaje ograniczona liczba trafień. Różnica w kolejności słów, dodatkowy fragment historii albo zmiana wersji instrukcji może całkowicie zablokować ponowne użycie. Ten wariant sprawdza się szczególnie dobrze w zadaniach deterministycznych, batchowych i wewnętrznych procesach, gdzie wejście da się ustabilizować. Dla małej aplikacji jest często rozsądniejszym pierwszym krokiem niż od razu wdrażanie wyszukiwania wektorowego.

Dla kogo cache dokładny będzie wystarczający

To dobry wybór dla prototypu, procesu uruchamianego cyklicznie z powtarzalnymi rekordami oraz endpointu, który przyjmuje znormalizowane dane. Może również pełnić rolę pierwszej warstwy w większej architekturze. Nawet gdy później zostanie dodany semantyczny cache, dokładne dopasowanie jest tańsze, prostsze i zazwyczaj bezpieczniejsze.

Wariant 2 — semantyczny cache w osobnej warstwie wektorowej

W tym modelu nowe zapytanie jest zamieniane na embedding, a następnie porównywane z zapisanymi reprezentacjami wcześniejszych zapytań. Rekord cache’u powinien zawierać nie tylko wektor i odpowiedź. Przydatne są także metadane: wersja promptu, model, język, typ intencji, źródło danych, identyfikator dzierżawcy, czas utworzenia, czas wygaśnięcia oraz wersja dokumentów wykorzystanych do wygenerowania odpowiedzi.

Po wyszukaniu podobnych wpisów aplikacja musi podjąć decyzję. Sam fakt, że jeden rekord jest najbliższy, nie wystarcza. Potrzebny jest próg podobieństwa, a często także dodatkowe warunki filtrujące. Rekord może być semantycznie bliski, ale pochodzić ze starej wersji bazy wiedzy albo z innego zakresu uprawnień. Najbezpieczniejsza reguła brzmi: podobieństwo otwiera drogę do weryfikacji, nie zastępuje weryfikacji.

Plus stanowi możliwość ponownego wykorzystania odpowiedzi mimo różnic językowych. Cache może być współdzielony między instancjami aplikacji, co jest trudniejsze przy lokalnym magazynie pamięci. Minusy to dodatkowa infrastruktura, koszt embeddingów, opóźnienie wyszukiwania, konieczność strojenia progu i ryzyko fałszywych trafień. Wymagane jest także monitorowanie jakości, bo błędna odpowiedź może nie wywołać wyjątku technicznego i pozostać niewidoczna bez oceny treści.

Jak ograniczyć fałszywe trafienia

Nie należy opierać decyzji wyłącznie na jednym globalnym progu dla całej aplikacji. Pytania o dokumentację, klasyfikację i dane osobowe mają różną tolerancję ryzyka. Można stosować osobne progi dla intencji, dodatkowy filtr po języku i wersji źródła, ograniczenie liczby kandydatów oraz regułę, że odpowiedź z cache jest dostępna tylko wtedy, gdy rekord ma aktualny status.

Przydatne jest także przechowywanie informacji o tym, dlaczego rekord został zaakceptowany. Log powinien wskazywać wynik podobieństwa, zastosowane filtry, wersję promptu i decyzję końcową. Bez takiego śladu trudno rozstrzygnąć, czy problem wynika z embeddingu, zbyt niskiego progu, błędnego klucza tenantów czy nieaktualnego wpisu.

Wariant 3 — cache warstwowy: dokładne dopasowanie przed semantycznym

Cache warstwowy łączy dwa mechanizmy w jednej ścieżce obsługi żądania. Najpierw aplikacja sprawdza, czy istnieje odpowiedź dla identycznego klucza. Dopiero gdy nie ma dokładnego trafienia, uruchamia wyszukiwanie semantyczne. Jeśli także ono nie zwróci bezpiecznego rekordu, żądanie trafia do modelu OpenAI, a nowa odpowiedź może zostać zapisana w obu warstwach.

Kod HTML i JavaScript w edytorze Visual Studio Code
Źródło: Pexels | Autor: Antonio Batinić

Takie podejście pozwala rozdzielić przypadki łatwe od wymagających dodatkowej oceny. Dokładne dopasowanie jest szybkie i przewidywalne, natomiast warstwa semantyczna obsługuje zmienność języka. Można również ustawić różne zasady dla obu poziomów: dokładny cache może działać szerzej, a semantyczny tylko dla wybranych intencji, stabilnych dokumentów lub odpowiedzi o niskim ryzyku.

Ważna jest kolejność walidacji. Przed zwróceniem odpowiedzi aplikacja powinna sprawdzić nie tylko klucz albo podobieństwo, ale także zgodność modelu, wersji promptu, języka, uprawnień i źródła danych. W przeciwnym razie warstwowa architektura może jedynie szybciej zwracać nieaktualną lub niepasującą treść.

Zalety i ograniczenia cache’a warstwowego

  • Plus: najczęstsze, identyczne żądania są obsługiwane przez prostszą warstwę.
  • Plus: semantyczne wyszukiwanie można ograniczyć do przypadków, w których rzeczywiście przynosi wartość.
  • Plus: łatwiej stopniowo wdrażać rozwiązanie i porównywać trafienia obu warstw.
  • Minus: architektura ma więcej ścieżek decyzyjnych i wymaga spójnego unieważniania wpisów.
  • Minus: błędna konfiguracja kolejności może zwiększyć opóźnienie, gdy każde żądanie przechodzi przez zbyt wiele kontroli.

Ten wariant jest szczególnie przydatny w aplikacji, która ma jednocześnie dużą liczbę powtórzeń dokładnych i wiele pytań o podobnym znaczeniu. Nie musi jednak oznaczać, że obie warstwy będą aktywne dla każdego łańcucha. Dla zadań zależnych od bieżącego stanu można pozostawić wyłącznie cache dokładny albo całkowicie pominąć cache odpowiedzi.

Wariant 4 — cache danych pośrednich zamiast gotowej odpowiedzi modelu

Nie każda oszczędność musi polegać na przechowywaniu końcowej odpowiedzi LLM. W rozbudowanym łańcuchu można cache’ować dane pośrednie: wynik wyszukiwania dokumentów, oczyszczoną treść, znormalizowane dane wejściowe, rezultat klasyfikacji intencji albo gotowy kontekst przekazywany do kolejnego kroku.

Drewniane płytki Scrabble układające się w słowo TECH
Źródło: Pexels | Autor: Markus Winkler

Ten wariant nie zawsze eliminuje wywołanie OpenAI. Jeżeli cache dotyczy wyłącznie kontekstu, model nadal może zostać uruchomiony, ale nie trzeba ponownie wykonywać wcześniejszych operacji. Jeżeli natomiast cache’owany jest etap wymagający osobnego wywołania modelu, ponowne użycie wyniku może ograniczyć liczbę kolejnych operacji i skrócić cały łańcuch.

Największą zaletą jest mniejszy zakres odpowiedzialności pojedynczego wpisu. Zamiast uznawać całą odpowiedź za aktualną, aplikacja przechowuje konkretny, łatwiejszy do zweryfikowania element procesu. Przykładowo można ponownie wykorzystać wynik wyszukania dokumentów dla stabilnego pytania, ale przed generowaniem sprawdzić aktualność dokumentów, uprawnienia użytkownika i bieżące parametry żądania.

Kiedy cache pośredni ma sens

To rozwiązanie pasuje do aplikacji, w których jeden prompt końcowy zawiera wiele kosztownych etapów. Dobrym kandydatem jest powtarzalne wyszukiwanie w tej samej bazie wiedzy, przygotowywanie dużego kontekstu albo klasyfikowanie dokumentów przed skierowaniem ich do właściwego łańcucha. Zyski mogą pojawić się również wtedy, gdy kilka różnych funkcji korzysta z tego samego etapu przygotowania danych.

Cache pośredni jest mniej odpowiedni, gdy dane zmieniają się często albo wynik etapu zależy od szczegółów, które trudno ująć w kluczu. W takim przypadku trzeba uwzględnić między innymi wersję indeksu, zestaw filtrów, identyfikator użytkownika, zakres dostępu i czas powstania danych. Pominięcie któregoś z tych elementów może spowodować użycie kontekstu przygotowanego dla innego żądania.

W porównaniu z cache’em gotowej odpowiedzi ten wariant zwykle wymaga więcej pracy projektowej. Zespół musi określić granice poszczególnych etapów, ich wejścia i wyjścia oraz zasady unieważniania. Zyskuje jednak większą kontrolę: zmiana formatu końcowej odpowiedzi nie musi automatycznie unieważniać wszystkich zapisanych danych pośrednich.

Porównanie wariantów: koszt, trafność i ryzyko

Żaden wariant nie jest najlepszy dla każdej aplikacji LangChain. Różnią się przede wszystkim tym, co jest zapisywane, jak szeroko działa dopasowanie i jak łatwo ocenić poprawność ponownie użytego wyniku.

WariantCo jest cache’owaneTrafność dopasowaniaGłówna zaletaGłówne ryzyko lub koszt
Cache dokładnyOdpowiedź dla konkretnego klucza wejściowegoWysoka przy poprawnie zbudowanym kluczuProstota i przewidywalnośćMniejsza liczba trafień przy zmiennych promptach
Cache semantycznyOdpowiedź powiązana z podobnym znaczeniowo zapytaniemZależna od embeddingu, progu i filtrówMożliwość obsługi różnych sformułowańFałszywe trafienia, embeddingi, indeks i trudniejsza ocena jakości
Cache warstwowyNajpierw wynik dokładny, następnie ewentualnie semantycznyMożna różnicować ją według warstwy i typu zadaniaŁączy bezpieczeństwo dopasowania dokładnego z większym zasięgiem semantycznegoWięcej reguł, ścieżek i obowiązków związanych z unieważnianiem
Cache danych pośrednichKontekst, dokumenty, klasyfikacje lub inne etapy łańcuchaZależna od aktualności danych wejściowych i klucza etapuMożliwość ponownego użycia wspólnych etapów przez różne funkcjeNie zawsze eliminuje wywołanie modelu; wymaga dobrej kontroli zależności

Jak wybrać wariant dla konkretnej aplikacji

Najpierw warto ustalić, co dokładnie ma zostać ograniczone: liczbę pełnych wywołań modelu, długość promptów, koszt przygotowania kontekstu czy opóźnienie całego łańcucha. Odpowiedź determinuje miejsce, w którym cache powinien powstać. Cache końcowej odpowiedzi będzie naturalny dla FAQ, natomiast przy rozbudowanym RAG większą kontrolę może dać cache wyników pośrednich.

  • Wybierz cache dokładny, gdy wejścia są powtarzalne, wynik powinien być maksymalnie przewidywalny, a zespół dopiero zaczyna mierzyć skalę problemu.
  • Wybierz cache semantyczny, gdy użytkownicy często zadają podobne pytania w różnych formach, a domena pozwala jasno określić aktualność i zakres odpowiedzi.
  • Wybierz cache warstwowy, gdy chcesz najpierw wykorzystać bezpieczne trafienia dokładne, a dopiero później ostrożnie zwiększać zasięg przez dopasowanie semantyczne.
  • Wybierz cache danych pośrednich, gdy kilka łańcuchów korzysta z tych samych etapów przygotowania danych albo gdy gotowa odpowiedź jest zbyt zależna od bieżącego kontekstu, aby bezpiecznie ją przechowywać.

W większości aplikacji rozsądny proces zaczyna się od pomiaru i cache’a dokładnego. Pozwala on sprawdzić, które żądania rzeczywiście się powtarzają, bez wprowadzania dodatkowej niepewności semantycznego dopasowania. Dopiero gdy okaże się, że problemem są różne sformułowania tego samego zadania, można dodać warstwę semantyczną dla wybranych intencji.

Jeżeli odpowiedzi zależą od danych użytkownika, bieżącego statusu lub często zmienianej bazy wiedzy, bezpieczniejszym kierunkiem będzie cache pośredni albo bardzo wąsko zdefiniowany cache warstwowy. Natomiast dla stabilnego FAQ i podobnych zadań o stałym formacie semantyczne dopasowanie może być uzasadnione, pod warunkiem że decyzja o użyciu wpisu uwzględnia aktualność źródła i zakres dostępu.

Ostateczny wybór powinien wynikać nie z samej liczby potencjalnych trafień, lecz z relacji między wartością pominiętego wywołania a kosztem błędnej odpowiedzi. Im bardziej konsekwencje pomyłki są poważne, tym większą rolę powinny odgrywać cache dokładny, walidacja metadanych i cache etapów pośrednich. Semantyczny fallback najlepiej traktować jako kontrolowane rozszerzenie, a nie jako uniwersalny zamiennik wszystkich pozostałych mechanizmów.

Najczęściej zadawane pytania (FAQ)

Czym semantyczny cache różni się od zwykłego cache’u?

Zwykły cache rozpoznaje przede wszystkim identyczne klucze lub prompty. Semantyczny cache porównuje znaczenie zapytań na podstawie embeddingów, dlatego może znaleźć odpowiedź także dla inaczej sformułowanego pytania. [1]

Czy semantyczny cache zawsze zmniejsza koszty OpenAI?

Nie. Trzeba uwzględnić koszt embeddingów, wyszukiwania, przechowywania danych oraz liczbę bezpiecznych trafień. Oszczędność pojawia się wtedy, gdy cache rzeczywiście zastępuje kosztowne generowanie odpowiedzi.

Jaki próg podobieństwa wybrać?

Próg powinien wynikać z testów na rzeczywistych zapytaniach i zależeć od ryzyka błędnego dopasowania. Zbyt niski próg zwiększa liczbę fałszywych trafień, a zbyt wysoki ogranicza oszczędności.

Kiedy nie używać semantycznego cache’a?

Należy zachować szczególną ostrożność przy danych dynamicznych, poufnych i zależnych od użytkownika, takich jak saldo, status przesyłki, dostępność produktu czy bieżące ceny. W niektórych przypadkach cache powinien być całkowicie wyłączony.

Czy warto zacząć od cache’a dokładnego?

Tak. Cache dokładny jest prostszy, przewidywalny i łatwiejszy do testowania. Może być pierwszą warstwą rozwiązania, zanim aplikacja zostanie rozszerzona o dopasowanie semantyczne.

Źródła