Szanuję Vercel’a za to, jaką uwagę poświęca tematowi telemetrii. Również ich biblioteka do komunikacji z ai (Vercel AI SDK) ma wsparcie dla obserwowalności. W tym poście chcę pokazać, jak można z tego skorzystać, by mieć więcej informacji na temat tego, co się dzieje w kodzie.
Vercel AI SDK
Vercel AI SDK skalda się z dwóch części: Core i UI. Ta druga pozwala szybciej budować interfejsy, natomiast w tym poście się skupię na Core, który dostarcza interfejs do komunikacji z różnymi modelami AI. Ta wszechstronność jest jedną z zalet tej biblioteki. Mamy wsparcie dla wszystkich głównych dostawców oraz sporo integracji od społeczności. Dzięki temu możemy łatwo wymienić model i eksperymentować z różnymi dostawcami.
Przykładowe API Z Hono.js
Żeby móc testować telemetrię to stworzyłem bardzo proste API przy pomocy Hono.js. Hono jest bardzo ciekawą i prostą biblioteką do tworzenia API w Node.js. Opiera się na standardach, ma wsparcie dla wszystkich runnerów, można uruchamiać na edge, TS itd. Używam często do szybkich PoC a ostatnio bawię się tez tym poważniej.
Minimalny kod serwera wygląda następująco:
import {Hono} from 'hono' import {serve} from '@hono/node-server' const app = new Hono() // tutaj za chwilę dodamy endpointy const port = 3500 console.log(`Server is running on port ${port}`) serve({ fetch: app.fetch, port })Po uruchomieniu komendy
tsx --env-file=.env src/index.tsMamy serwer gotowy do użytku pod adresem: http://localhost:3000
Integracja z Vercel AI SDK
Integracja z biblioteką Vercel AI SDK jest banalnie prosta i sprowadza się do instalacji odpowiedniej zależności npm.
npm i aiTeraz trzeba tylko dodać odpowiedni kawałek kodu odpowiedzialnym za generowanie odpowiedzi.
import {generateText} from 'ai'; import {bedrock} from '@ai-sdk/amazon-bedrock'; //wcześniejszy kod app.get('/ai', async (c) => { const {text} = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), prompt: 'Make a joke about programming', }); return c.text(text) })W zależności od wybranego modelu musimy dodać odpowiednie zmienne środowiskowe oraz bibliotekę do obsługi zapytań. Ja standardowo korzystam z Amazon Bedrock, więc w pliku .env trzymam AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY i oczywiście AWS_REGION. jeżeli chodzi o npm to musiałem dodać
npm i @ai-sdk/amazon-bedrockJeśli wszystko zostało skonfigurowane poprawnie to po wejściu na stronę http://localhost:3500/ai dostaniemy nasz oczekiwany dowcip.
Dodajmy telemetrię do LLM’a
Zanim dodamy telemetrię jedna ważna uwaga. Zgodnie z dokumentacją jest to eksperymentalna funkcjonalność i może się jeszcze zmienić w przyszłości. Telemetria jest dodawana na poziomie zapytania, a nie na poziomie całego systemu. Jest to dobre rozwiązanie, bo możemy dokładnie sterować, tym co chcemy śledzić, a co nie. Z minusów, to musimy to dodawać do każdego zapytania, ale łatwo to objeść robiąc własną abstrakcję.
const {text} = await generateText({ model: bedrock('anthropic.claude-3-haiku-20240307-v1:0'), prompt: 'Make a joke about programming', experimental_telemetry: {isEnabled: true} });Jedna zmienna i tyle. Do pełni szczęścia potrzebujemy jeszcze reszty konfiguracji do zbierania metryk OpenTelemetry. Wystarczy, iż zmodyfikujemy nasz plik następująco.
//pozostałe importy import {NodeSDK} from "@opentelemetry/sdk-node"; import {getNodeAutoInstrumentations} from "@opentelemetry/auto-instrumentations-node"; import {OTLPTraceExporter} from '@opentelemetry/exporter-trace-otlp-http'; import {Resource} from '@opentelemetry/resources'; import {ATTR_SERVICE_NAME} from '@opentelemetry/semantic-conventions'; const sdk = new NodeSDK({ resource: new Resource({ [ATTR_SERVICE_NAME]: 'hono-api', }), traceExporter: new OTLPTraceExporter({}), instrumentations: [ getNodeAutoInstrumentations({ '@opentelemetry/instrumentation-fs': { enabled: false, }, }), ] }); const app = new Hono() //reszta pliku sdk.start() //musimy uruchomić telemtrię zanim uruchomimy serwer serve({ fetch: app.fetch, port })To, co dodałem to konfiguracja OpenTelemetry dla Node wraz z auto-instrumentations-node, która dodaje podstawowe elementy do śledzenia. Najważniejszym elementem tej konfiguracji jest opcja traceExporter, która konfiguruje, gdzie wysyłamy nasze dane. Do celów testowych można skorzystać z Jaeger, ale jest mnóstwo innych serwisów, które przyjmują dane z OpenTelemetry.
Aby uruchomić Jaeger, najprościej skorzystać z Docker’a.
docker run --rm --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 5778:5778 \ -p 9411:9411 \ jaegertracing/jaeger:2.1.0Na koniec ostatnia rzecz. Musimy dodać odpowiednią zmienną w pliku .env, by skonfigurować wysyłanie logów.
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318LLM w OpenTelemetry
Teraz jak musimy wykonać zapytanie do naszego API, by dostać odpowiednie dane. Po wykonaniu zapytania wejdź na adres http://localhost:16686 (o ile korzystasz z Jaeger) i po chwili powinny się pojawić logi.
Po kliknięciu interesującego nas zapytania dostaniemy dodatkowe informacje.
I tu się pokazuje cała magia OpenTelemetry. Widać od razu co się działo po odpytaniu endpointa /ai. Jak wejdziemy w szczegóły konkretnych wywołań, to widzimy więcej danych np.: ile tokenów, jaki promet poszedł, co przyszło z powrotem itd. Dzięki temu debuggowanie aplikacji jest dużo prostsze i jeżeli coś pójdzie nie tak, to wiemy na którym etapie. Teraz to drzewko jest bardzo proste, ale wyobraź sobie bardziej zaawansowane przypadki, gdy potrzebne jest więcej niż jedno zapytanie do LLM’a.
Im bardziej skomplikowany system, tym lepsze efekty można osiągnąć dzięki poprawniej skonfigurowanej telemetrii.
Langfuse
Z Jaeger (i innymi systemami do OpenTelemetry) jest mały problem. Są bardzo ogólne i stworzone z myślą o standardowych aplikacjach. Na szczęście powstają rozwiązania dedykowane pod LLM. Jednym z bardziej interesujących jest Langfuse, który oprócz tracingu dodaje też zarządzanie promptami, ich ewaluację, testy itd. Dodatkowo Langfuse idealnie się integruje z Vercel AI SDK i OpenTelemetry.
Aby rozpocząć eksportowanie danych do Langfuse, musimy zmienić eksporter w ustawieniach OpenTelemetry
traceExporter: new LangfuseExporter(),oraz ustawić odpowiednie zmienne w pliku .env (odpowiednie wartości znajdziesz w panelu Langfuse)
LANGFUSE_SECRET_KEY="sk-lf-..." LANGFUSE_PUBLIC_KEY="pk-lf-..." LANGFUSE_BASEURL="https://cloud.langfuse.com"Teraz, jeżeli wykonamy zapytanie, to dostaniemy dane w serwisie Langfuse (trzeba poczekać parę minut aż będą widoczne)
Jak widać, dostajemy informacje o wywołaniach LLM. Warto zwrócić uwagę na dodatkowe informacje w postaci ceny, które nie były dostępne w Jaeger. Dane są też lepiej sformatowane, bo faktycznie nacisk jest położny na LLM’y. Z tego poziomu możemy też skorzystać z dodatkowych opcji dostarczanych przez Langfuse.
Ale jest tylko jeden mały problem - Langfuse zbiera tylko informacje na temat wywołań LLM. Czyli tracimy dodatkowe informacje z OpenTelemetry na temat innych elementów systemu. Rozwiązanie?
Możemy wysyłać logi do obu systemów.
Aby to było możliwe trzeba dokonać paru zmian w kodzie
const sdk = new NodeSDK({ resource: new Resource({ [ATTR_SERVICE_NAME]: 'hono-api', }), spanProcessors: [ new BatchSpanProcessor(new OTLPTraceExporter({})), new BatchSpanProcessor(new LangfuseExporter()), ], instrumentations: [ getNodeAutoInstrumentations({ '@opentelemetry/instrumentation-fs': { enabled: false, }, }), ] });Skorzystałem z opcji spanProcessors, która daje możliwość skonfigurowania więcej niż jednego eksportera. Teraz logi będą wysłane do obu serwisów - Langfuse używa tylko tych AI, a zwykły system OpenTelemetry zbiera wszystko.
Tutaj warto zwrócić uwagę na identyczne traceID. Dzięki temu możemy połączyć dane z obu serwisów i rozszerzyć naszą wiedzę, jeżeli będziemy tego potrzebowali.
Podsumowanie
Ostatnio jestem wielkim fanem OpenTelemetry i pomogło mi w paru sytuacjach. W przypadku LLM’ów bardzo mi tego brakowało rok temu i mega się cieszę, iż można to teraz zaimplementować. Wiedza co się dzieje w systemie, jest często konieczna, by móc go ulepszać. A w połączeniu z innymi funkcjonalnościami Langfuse można iterować szybciej niż normalnie. Dlatego do tematu Langfuse wrócę w innym poście.