Zacznij tworzyć GenAI z AI SDK

GenAI z AI SDK

Naucz się AI SDK od Vercela i zacznij wdrażać AI do swoich aplikacji biznesowych. Od podstawowych przykładów do agentów i telemetrii

OdAleksander Patschek

Wstęp

Postanowiłem napisać ten poradnik do AI SDK, by ułatwić ci start w tworzeniu aplikacji opartych o LLM. Teraz jest idealny czas, by się zacząć tym bawić, bo są już narzędzia, które pomagają to robić, pojawiają się pierwsze wzorce, modele są potężniejsze i widać chęć biznesu, by rozwijać aplikacje o elementy AI.

Ten poradnik jest darmowy więc jeśli ci się podoba moja praca to zachęcam do wsparcia w postaci symbolicznej kawy

Opis wersji

  • v0.1 - Podstawowe informacje o AI SDK i przykłady w kodzie
  • v0.2 - Dodano generowanie obrazków

Dlaczego AI SDK?

Zanim przejdę do właściwego mięsa, to chcę odpowiedzieć na pytanie: Dlaczego AI SDK a nie X?. Według mnie możesz używać, czego chcesz. Mnie AI SDK przypadło bardzo do gustu i jak dla mnie to jest najlepsza aktualnie biblioteka do budowania aplikacji GenAI.

Przemawia za tym kilka rzeczy:

  • Prostota. Jak za chwilę zobaczysz, API tej biblioteki jest banalnie proste i nie wymaga bardzo złożonych opisów. Nie czuję też typowego overenginnering i tworzenia super abstrakcji. Wszystko jest proste i przemyślane
  • Wspólny interfejs dla wielu modeli. Zmiana modelu to jest jedna linijka kodu. Dzięki zastosowaniu integracji jako zewnętrzne biblioteki bardzo łatwo można podmieniać modele w locie
  • Mnogość opcji. Pomimo tego, że jest to prosta abstrakcja, to daje duże możliwości: proste odpowiedzi, streaming, wykorzystanie narzędzi, generowanie obrazków, telemetria i kolejne są dokładane
  • Stoi za tym duża firma. Jest to ważny aspekt, bo wsparcie Vercela daje większą szansę, że projekt będzie rozwijany i aktualizowany na bieżąco. Takie biblioteki muszą być często i szybko aktualizowane, by być na bieżąco ze zmianami w AI.

Natomiast elementy, które tutaj opisuję są na tyle uniwersalne, że zastosujesz je do dowolnej biblioteki. Eksperymentuj i wybierz to, co jest najlepsze dla ciebie.

Ja też skupiam się w tym ebooku tylko na części serwerowej, a jest jeszcze część frontendowa AI SDK, które pomaga budować interfejsy pod AI. Ale to może w innej publikacji.

Setup

Do podstawowego setupu musisz wybrać model, na jakim będziesz pracować. Ja osobiście testuję wszystko na Google Gemini, bo jest darmowe. Jeśli masz kredyty na OpenAI, Anthropic albo czymś innym - to bez problemu możesz tego użyć. Wszystkich obsługiwanych provider'ów znajdziesz pod tym linkiem.

1npm i ai @ai-sdk/google
2

I tyle wystarczy, by zacząć pracę. Polecam też zainstalować sobie Bun i za jego pomocą uruchamiać pliki. Dzięki temu masz zapewnione wsparcie dla TypeScript i ma kompatybilność z node_modules.

Stwórz też plik .env i wrzuć następujące zmienne

1GOOGLE_GENERATIVE_AI_API_KEY=
2

AI SDK

Basic completion

Opis

Najbardziej podstawowym zastosowaniem dowolnego modelu AI, będzie sytuacja gdy pytamy o coś model i oczekujemy prostej odpowiedzi na bazie wiedzy modelu. W przypadku tego podejścia nie dodajemy żadnej nowej wiedzy jak w przypadku RAG.

W tym podejściu w prompcie podajemy wszystkie informacje na temat interesującego nas zagadnienia, ale nie sięgamy po dodatkowe dane np: prosimy o wygenerowanie odpowiedzi do danego pytania, możemy podać przykłady, ale są one statyczne.

Przykłady użycia

Pomimo tego, że te podejście wydaje się ograniczone ma swoje zastosowania w podstawowych zastosowaniach modeli LLM.

  • Proste operacje na tekście - tłumaczenia, zmiana tonu, korekta błędów,
  • Proste analizy - sentyment tekstu, czy tekst mieści się w kategorii,
  • Ogólna generacja - generowanie szablonów, generycznych tekstów
  • Generacja na podstawie danych wejściowych - generowanie odpowiedzi na maila itd

Oprócz tego te podejście stanowi idealny punkt startu to innych bardziej zaawansowanych podejść.

Kod

TypeScript
1import { generateText } from 'ai';
2import { google } from '@ai-sdk/google';  
3
4const system = `
5Jesteś specjalistą od języka polskiego. Twoim zadaniem jest poprawienie tekstu. Odpowiadasz tylko poprawionym tekstem. Nie możesz dodawać własnych elementów`;
6  
7const fixText = 'ala ma tży psy krulika kota.';
8const prompt = `Tekst do poprawienia: ${fixText}`;
9
10
11const completion = await generateText({
12system: system,
13prompt: prompt,
14model: google('gemini-2.0-flash')
15});
16
17console.log(completion.text);
18

Image Generation

Opis

Zastosowanie, które jest mniej popularne w standardowych aplikacjach biznesowych, ale daje sporo radości przy nauce. Generowanie obrazków jest coraz lepsze i coraz łatwiejsze.

Bardzo istotne jest, by opisać możliwie jak najwięcej szczegółów, które pomogą wygenerować obrazek. Można nawet połączyć to z Basic Completion i pozwolić AI by samo rozwinęło opis o więcej szczegółów.

Przykłady użycia

Tutaj na razie nie widzę dużej ilości zastosowań w przypadku wdrażania w projekty, ale to nie znaczy, że nie ma:

  • Opcja generowania obrazków w profilu
  • Automatyczne generowanie obrazków do postów
  • Specyficzne aplikacje, które generują obrazki

Kod

Uwaga. W przykładzie skupiłem się na części AI i w przypadku wdrożeń produkcyjnych warto inaczej zrobić pętlę generowania obrazków (tutaj działa to sekwencyjnie i przy większej ilości obrazków będzie działać wolno)
TypeScript
1import { generateText } from 'ai';
2import { google } from '@ai-sdk/google';
3import fs from 'fs';
4import path from 'path';
5
6
7const saveImageToFile = async (base64Data: string, filename: string) => {
8    const base64Image = base64Data.split(';base64,').pop() || base64Data;
9    const imageBuffer = Buffer.from(base64Image, 'base64');
10
11    const dir = path.dirname(filename);
12    if (!fs.existsSync(dir)) {
13        fs.mkdirSync(dir, { recursive: true });
14    }
15
16    await fs.promises.writeFile(filename, imageBuffer);
17    console.log(`Image saved to ${filename}`);
18};
19
20const result = await generateText({
21    model: google('gemini-2.0-flash-exp'),
22    prompt: `Generate an image of the Eiffel tower with fireworks in the background.`,
23    providerOptions: {
24        google: { responseModalities: ['TEXT', 'IMAGE'] },
25    },
26});
27
28for (const file of result.files) {
29    let i = 0;
30    if (file.mimeType.startsWith('image/')) {
31        const outputPath = path.join(__dirname, '.', `image_${i}.png`);
32        await saveImageToFile(file.base64, outputPath);
33        i++
34    }
35}

Streaming completion

Opis

Jest to rozwinięcie przykładu powyżej i różnicą jest sposób otrzymania danych. W Basic completion czekamy aż LLM utworzy całą odpowiedź. W zależności od pytania, prompta i modelu może to trwać kilka-kilkanaście sekund.

Aby skrócić ten czas, możemy zwracać częściowy tekst, dzięki czemu tworzymy wrażenie pisania na żywo. Ma to minus w postaci gorszej moderacji tekstu co było widać po czacie DeepSeek gdzie najpierw generuje odpowiedź a dopiero potem ją analizował i usuwał.

Przykłady użycia

Najczęściej stosujemy gdy obawiamy się, że generowanie całej odpowiedzi będzie trwać zbyt długo lub chcemy poprawić UX aplikacji

Kod

TypeScript
1import { streamText } from 'ai';
2import { google } from '@ai-sdk/google';
3
4const system = `
5Jesteś specjalistą od języka polskiego. Twoim zadaniem jest poprawienie tekstu. Odpowiadasz tylko poprawionym tekstem. Nie możesz dodawać własnych elementów`;
6
7const fixText = 'ala ma tży psy krulika kota.';
8
9const prompt = `Tekst do poprawienia: ${fixText}`;
10
11const stream = streamText({
12    system: system,
13    prompt: prompt,
14    model: google('gemini-2.0-flash'),
15    onError: (error) => {
16        console.error('Error:', error);
17    }
18});
19
20// Process chunks as they arrive
21for await (const chunk of stream.textStream) {
22    console.log(chunk)
23}
24
25console.log('Full text:', stream.text);
26

Structured outputs

Structured outputs są potężnym narzędziem, które można zastosować w wielu różnych sytuacjach. Najbardziej będą pomocne gdy potrzebujemy konkretnego kształtu odpowiedzi np.: w celu wywołania API albo dalszego procesowania danych.

Przykłady użycia

  • Parsowanie danych i dokumentów - wyciąganie z luźnego tekstu konkretnych informacji
  • Generowanie danych w konkretnym formacie
  • Przygotowanie danych do konkretnego API

Single item

Kod

TypeScript
1import { generateObject } from 'ai';
2import { google } from '@ai-sdk/google';
3import {z} from 'zod';
4
5const userSchema = z.object({
6    name: z.string(),
7    email: z.string().describe("email in example.com domain"),
8    confirmed: z.boolean(),
9});
10
11const system = `You are generating fake data for tests`;
12const prompt = `Generate fake user data`;
13
14
15const completion = await generateObject({
16    system: system,
17    prompt: prompt,
18    model: google('gemini-1.5-pro-latest'),
19    schema: userSchema,
20    output: 'object',
21});
22
23console.log(completion.object);
24

Array

Kod

TypeScript
1import { generateObject } from 'ai';
2import { google } from '@ai-sdk/google';
3import {z} from 'zod';
4
5const userSchema = z.object({
6    name: z.string(),
7    email: z.string(),
8    confirmed: z.boolean(),
9});
10
11const system = `You are generating fake data for tests`;
12const prompt = `Generate 5 fake user data`;
13
14
15const completion = await generateObject({
16    system: system,
17    prompt: prompt,
18    model: google('gemini-1.5-pro-latest'),
19    schema: userSchema,
20    output: 'array',
21});
22
23console.log(completion.object);
24

Enum

Opis

Enum jest przykładem zapytania, gdzie oczekujemy jednej odpowiedzi z grupy dozwolonych wartości. Jest to szczególnie przydatne przy zadaniach klasyfikacyjnych

Przykłady użycia

  • Analiza tekstu np.: sprawdzanie sentymentu
  • Odpowiedzi TAK/NIE np.: sprawdzanie, czy wiadomość jest w konteście
  • przy korzystaniu z LLM-as-a-jugde do sprawdzania halucynacji

Kod

TypeScript
1import { generateObject } from 'ai';
2import { google } from '@ai-sdk/google';
3
4const system = `Check what is the setiment of the text`;
5
6const checkSentiment = async (prompt: string) => {
7    const completion = await generateObject({
8        system: system,
9        prompt: prompt,
10        model: google('gemini-1.5-pro'),
11        output: 'enum',
12        enum: ['positive', 'negative', 'neutral']
13    });
14
15    console.log(`{propmpt} -> ${completion.object}`);
16}
17
18checkSentiment("This book is amazing");
19checkSentiment("This book is terrible");
20checkSentiment("This book is ok");
21

Tools

Opis

Jedno z bardziej zaawansowanych oraz przydatnych zastosowań modeli AI. Jeśli dany model obsługuje narzędzia, to możemy rozszerzać możliwości modelu w nieskończoność. Tutaj ogranicza nas wyobraźnia oraz umiejętność napisania narzędzia.

Główna rola narzędzi to rozszerzanie możliwości modeli. Jeśli model zauważy, że brakuje mu jakiś danych to będzie halucynował. Gdy ma dostęp do konkretnych narzędzi może je użyć by uzupełnić braki w wiedzy

Przykłady użycia

  • Wyszukiwanie aktualnych informacji np.: wiadomości, pogody, przeszukiwanie internetu
  • Uzupełnianie wiedzy np.: odpytywanie bazy danych o konkretne dane
  • Wykorzystywanie istniejących rozwiązań np.: zamiana lokalizacji na koordynaty.

Kod

TypeScript
1import { generateObject, generateText, tool } from 'ai';
2import { google } from '@ai-sdk/google';
3import {z} from 'zod';
4
5export const fetchLocation = async (location: string) => {
6  const response = await fetch(`https://us1.locationiq.com/v1/search.php?key=${process.env.LOCATIONIQ_API_KEY}&q=${location}&format=json`);
7  const data = await response.json();
8
9  return data[0];
10};
11
12const prompt = `What is the location of Katowice, Poland`;
13
14
15const completion = await generateText({
16    prompt: prompt,
17    model: google('gemini-1.5-pro-latest'),
18    tools: {
19        location: tool({
20          description: 'Get the location of a place',
21          parameters: z.object({
22            location: z.string().describe('The location to get the lat&lon for'),
23          }),
24          execute: async ({ location }) => {
25            return fetchLocation(location)
26          }
27        }),
28      },
29});
30
31console.dir(completion, {depth: null});
32

Simple Agent

Opis

Agenci są aktualnie najbardziej zaawansowaną formą aplikacji GenAI. W założeniu agent ma być niezależnym bytem który podejmie określone akcje na podstawie danych . Czyli nie prowadzimy za rączkę algorytmu ale dajemy narzędzia i pozwalamy by sam rozwiązał problem. Daje to sporo elastyczności i pozwala rozwiązywać niedeterministyczne problemy, które były ciężkie do zrobienia. Rolą programisty jest dostarczenie odpowiednich narzędzi.

Kod

Jest wiele różnych sposób na stworzenie agentów i tutaj mówimy o tym najprostszym opartym o wykorzystywanie dostępnych narzędzi. W AI SDK możemy to uzyskać dostarczając narzędzia i ustawiając maxSteps na wartość inną niż 1.

TypeScript
1import { generateText, tool } from 'ai';
2import { google } from '@ai-sdk/google';
3import {z} from 'zod';
4
5export const fetchLocation = async (location: string) => {
6  const response = await fetch(`https://us1.locationiq.com/v1/search.php?key=${process.env.LOCATIONIQ_API_KEY}&q=${location}&format=json`);
7  const data = await response.json();
8
9  return data[0];
10};
11
12export const fetchWeather = async (lat: string, lon: string) => {
13  const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m&forecast_days=1&timezone=Europe%2FWarsaw`);
14  const data = await response.json();
15
16  return data;
17};
18
19
20const prompt = `What is the weather in Katowice, Poland`;
21
22const completion = await generateText({
23    prompt: prompt,
24    model: google('gemini-2.0-flash'),
25    tools: {
26        location: tool({
27          description: 'Get the location of a place',
28          parameters: z.object({
29            location: z.string().describe('The location to get the lat&lon for'),
30          }),
31          execute: async ({ location }) => {
32            return fetchLocation(location)
33          }
34        }),
35        weather: tool({
36          description: 'Get the weather for a location',
37          parameters: z.object({
38            lat: z.string().describe('The lat of location'),
39            lon: z.string().describe('The lon of location'),
40          }),
41          execute: async ({ lat, lon }) => {
42            return fetchWeather(lat, lon)
43          }
44        }),
45      },
46    maxSteps:5
47});
48
49console.dir(completion, {depth: null});
50

Telemetry

Opis

Telemetria to mój konik w ostatnim czasie i nie wyobrażam sobie budowania aplikacji LLM bez tego. W przypadku gdy tworzymy tradycyjne systemy to są one deterministyczne - jeśli wrzucimy konkretne dane to dostaniemy zawsze takie same dane na wyjściu. W przypadku LLM tak nie jest. A jeśli dorzucimy agentów i niedeterministyczne uruchamianie narzędzi to mamy prosty przepis na katastrofę przy analizie błędów.

AI SDK daje opcje uruchomienia telemetrii dla zapytań dzięki czemu możemy wykorzystać dowolne narzędzi dla OpenTelemetry. Ale dobrze jest wykorzystać dedykowane narzędzia pod LLM które dostarczą więcej informacji. Ja polecam Langfuse ale możesz wykorzystać inne.

Dzięki temu po wykonaniu zapytania mamy automatycznie dostępne logi co się działo i można to przeanalizować jeśli jest potrzeba.

Kod

Wymagane env'y

1LANGFUSE_SECRET_KEY="sk-lf-"
2LANGFUSE_PUBLIC_KEY="pk-lf-"
3LANGFUSE_BASEURL="https://cloud.langfuse.com"
4
TypeScript
1import { generateText, tool } from 'ai';
2import { google } from '@ai-sdk/google';
3import { z } from 'zod';
4
5import { NodeSDK } from "@opentelemetry/sdk-node";
6import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
7import { LangfuseExporter } from "langfuse-vercel";
8
9const sdk = new NodeSDK({
10  traceExporter: new LangfuseExporter(),
11  instrumentations: [getNodeAutoInstrumentations()],
12});
13
14sdk.start();
15
16
17//fetch location from LocationIq API
18const fetchLocation = async (location: string) => {
19  const response = await fetch(`https://us1.locationiq.com/v1/search.php?key=${process.env.LOCATIONIQ_API_KEY}&q=${location}&format=json`)
20  const data = await response.json()
21
22  return data[0]
23}
24
25// fetch weather based on lat&lon from open-meteo
26const fetchWeather = async (lat: string, lon: string) => {
27  const response = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m&forecast_days=1&timezone=Europe%2FWarsaw`)
28  const data = await response.json()
29
30  return data
31}
32
33
34const prompt = `What is the weather in Katowice, Poland`;
35
36const completion = await generateText({
37  prompt: prompt,
38  model: google('gemini-2.0-flash'),
39  tools: {
40    location: tool({
41      description: 'Get the location of a place',
42      parameters: z.object({
43        location: z.string().describe('The location to get the lat&lon for'),
44      }),
45      execute: async ({ location }) => {
46        return fetchLocation(location)
47      }
48    }),
49    weather: tool({
50      description: 'Get the weather for a location',
51      parameters: z.object({
52        lat: z.string().describe('The lat of location'),
53        lon: z.string().describe('The lon of location'),
54      }),
55      execute: async ({ lat, lon }) => {
56        return fetchWeather(lat, lon)
57      }
58    }),
59  },
60  maxSteps: 5,
61  experimental_telemetry: {
62    isEnabled: true,
63  }
64});
65
66console.dir(completion.text, { depth: null });
67
68await sdk.shutdown();
69