Wszystkie posty
aiAI-Powered

OpenAI API w praktyce: function calling, embeddings i RAG

Kod produkcyjny dla OpenAI API: function calling, embeddings, vector database, RAG pipeline. Konkretne przykłady, realne pułapki, koszty. Dla developerów, nie marketerów.

6 min czytaniaAktualizacja: 18 czerwca 2026

OpenAI API w praktyce: function calling, embeddings i RAG

Ten post to kod produkcyjny, nie hello world. Po dwóch latach budowania aplikacji z OpenAI API mam sprawdzone wzorce, które używam w co drugim projekcie. Są gotowe do wklejenia i uruchomienia.

Function calling: stabilny kontrakt z modelem

Function calling to najważniejsza funkcja OpenAI API dla developerów. Pozwala wymusić, żeby model zwrócił strukturę zamiast chaotycznego tekstu. W produkcji jest niezastąpiony.

Podstawowy wzorzec (TypeScript)

import OpenAI from "openai";
import { z } from "zod";

const client = new OpenAI();

// Definiuj schemat tego co model ma zwrócić
const TaskSchema = z.object({
  action: z.enum(["send_email", "create_task", "search_docs", "none"]),
  args: z.record(z.string(), z.any()),
  reasoning: z.string().max(500),
});

type Task = z.infer<typeof TaskSchema>;

async function classifyIntent(userMessage: string): Promise<Task> {
  const completion = await client.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: `Jesteś asystentem. Klasyfikuj intencję użytkownika
        w jedną z 4 akcji. Bądź zwięzły.`,
      },
      { role: "user", content: userMessage },
    ],
    tools: [
      {
        type: "function",
        function: {
          name: "execute_action",
          description: "Wykonaj akcję na podstawie intencji użytkownika",
          parameters: {
            type: "object",
            properties: {
              action: {
                type: "string",
                enum: ["send_email", "create_task", "search_docs", "none"],
              },
              args: { type: "object", additionalProperties: true },
              reasoning: { type: "string" },
            },
            required: ["action", "args", "reasoning"],
          },
        },
      },
    ],
    tool_choice: { type: "function", function: { name: "execute_action" } },
  });

  const toolCall = completion.choices[0].message.tool_calls?.[0];
  if (!toolCall) throw new Error("No tool call returned");

  // WAŻNE: waliduj output modelu zanim go użyjesz
  const parsed = TaskSchema.parse(JSON.parse(toolCall.function.arguments));
  return parsed;
}

Trzy rzeczy, które robią różnicę:

  1. tool_choice: { type: "function", ... } — wymusza, żeby model zawsze wywołał tę funkcję. Bez tego model może odpowiedzieć tekstem zamiast JSON-em.

  2. z.parse() na output — model może zwrócić JSON, który nie pasuje do schematu. Walidacja pozwala to złapać. Bez niej masz bugi, które objawiają się w produkcji losowo.

  3. reasoning w output — pole wymuszone w schemacie. Model musi uzasadnić swoją decyzję, co zmniejsza halucynacje. Bez tego model zgaduje akcję losowo, z tym — myśli przed decyzją.

Pułapka: function calling + streaming

Function calling nie streamuje się dobrze. Output jest albo pełny JSON, albo nic. Jeśli potrzebujesz streamingu (long-form text), użyj dual call: pierwszy call zwraca akcję (function calling), drugi zwraca tekst (streaming). Dwa razy droższe, ale jedyny sposób na łączenie obu.

Embeddings: zamiana tekstu na wektory

Embeddings to fundament wyszukiwania semantycznego i RAG. Zamieniają tekst na 1536-wymiarowy wektor (dla text-embedding-3-small). Teksty o podobnym znaczeniu mają podobne wektory.

Generowanie embeddingów (cache!)

import { createHash } from "crypto";
import { Redis } from "@upstash/redis";

const redis = Redis.fromEnv();

async function getEmbedding(text: string): Promise<number[]> {
  // Cache hit: 0ms, $0
  const cached = await redis.get<number[]>(`emb:${hashText(text)}`);
  if (cached) return cached;

  // Cache miss: 200ms, $0.00000002
  const response = await openai.embeddings.create({
    model: "text-embedding-3-small",
    input: text,
  });

  const vector = response.data[0].embedding;
  await redis.set(`emb:${hashText(text)}`, vector, { ex: 60 * 60 * 24 * 30 });
  return vector;
}

function hashText(text: string): string {
  return createHash("sha256").update(text).digest("hex").slice(0, 16);
}

Cache'owanie embeddingów jest kluczowe dla kosztów. Jeśli user pyta "jak działa X" dwa razy — drugi embedding to darmowy cache hit. W moich aplikacjach 60-80% zapytań to cache hit.

Magazynowanie: pgvector vs Pinecone

Dla 95% polskich SaaS-ów: pgvector w Supabase wystarcza.

-- Migration: enable pgvector
CREATE EXTENSION IF NOT EXISTS vector;

-- Table with embeddings
CREATE TABLE documents (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  content TEXT NOT NULL,
  metadata JSONB DEFAULT '{}',
  embedding vector(1536),
  created_at TIMESTAMPTZ DEFAULT now()
);

-- Index for fast similarity search
CREATE INDEX documents_embedding_idx
ON documents
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_KEY!
);

async function findSimilarDocuments(
  queryEmbedding: number[],
  matchThreshold = 0.7,
  matchCount = 5
) {
  const { data, error } = await supabase.rpc("match_documents", {
    query_embedding: queryEmbedding,
    match_threshold: matchThreshold,
    match_count: matchCount,
  });

  if (error) throw error;
  return data;
}

// SQL function
// CREATE FUNCTION match_documents(
//   query_embedding vector(1536),
//   match_threshold float,
//   match_count int
// ) RETURNS SETOF documents AS $$
//   SELECT * FROM documents
//   WHERE 1 - (documents.embedding <=> query_embedding) > match_threshold
//   ORDER BY documents.embedding <=> query_embedding
//   LIMIT match_count;
// $$ LANGUAGE sql;

<=> to operator odległości cosinusowej w pgvector. Im bliżej 0, tym bardziej podobne. Threshold 0.7 = "dość podobne".

RAG pipeline: production-ready

RAG to połączenie retrieval (wyszukiwanie semantyczne) + generation (LLM). Wzorzec, który używam w większości aplikacji:

async function ragQuery(userQuestion: string): Promise<string> {
  // 1. Embedding pytania (cache'owany)
  const questionEmbedding = await getEmbedding(userQuestion);

  // 2. Retrieval: top-5 najbardziej podobnych fragmentów
  const relevantDocs = await findSimilarDocuments(questionEmbedding, 0.7, 5);

  if (relevantDocs.length === 0) {
    return "Nie znam odpowiedzi na to pytanie. Mogę pomóc w inny sposób?";
  }

  // 3. Konstrukcja kontekstu z cytatami
  const context = relevantDocs
    .map((doc, i) => `[${i + 1}] ${doc.content}`)
    .join("\n\n");

  // 4. Prompt z kontekstem i instrukcją cytowania
  const completion = await openai.chat.completions.create({
    model: "gpt-4o-mini",
    messages: [
      {
        role: "system",
        content: `Jesteś asystentem. Odpowiadaj TYLKO na podstawie
        dostarczonego kontekstu. Jeśli kontekst nie zawiera odpowiedzi,
        powiedz to wprost. ZAWSZE cytuj źródło używając numeru [N] przy
        każdym fakcie.`,
      },
      {
        role: "user",
        content: `Kontekst:\n${context}\n\nPytanie: ${userQuestion}`,
      },
    ],
    temperature: 0.2, // niska = bardziej deterministyczny
  });

  return completion.choices[0].message.content!;
}

Trzy elementy, które robią różnicę:

  1. Threshold similarity 0.7 — poniżej tego dokumenty są szumem. Bez thresholda model dostaje random dokumenty i halucynuje.

  2. Numeracja cytatów [N] — model cytuje źródła. Użytkownik widzi skąd info. Możesz potem zweryfikować w UI ("Źródło: dokument 3").

  3. Temperatura 0.2 — dla RAG chcesz deterministyczne odpowiedzi, nie kreatywne. Temperatura 0.7+ daje halucynacje, bo model "domyśla się" gdy kontekst jest niepełny.

Kiedy RAG NIE działa

RAG nie jest magiczną różdżką. Nie działa gdy:

  • Dokumenty są nieaktualne — RAG zwróci stary, sprzeczny z rzeczywistością content. Rozwiązanie: regularnie re-embeduj dokumenty, dodaj dateModified jako filter.
  • Pytanie jest zbyt ogólne — top-5 fragmentów nie pokrywa tematu. Rozwiązanie: query expansion (przeformułuj pytanie na 3 warianty, szukaj każdego).
  • Knowledge base jest mały (< 100 dokumentów) — lepiej wkleić wszystko do promptu, RAG jest overengineering.

Koszty w produkcji: realne liczby

Dla aplikacji 10k użytkowników / miesiąc, 3 pytania / user:

| Komponent | Koszt / miesiąc | |-----------|-----------------| | Embeddings (cache 70% hit) | $0.30 | | Vector storage (Supabase) | $0 (w darmowym planie) | | GPT-4o-mini (RAG, avg 1k tokenów) | $45 | | GPT-4o (precyzyjne odpowiedzi) | $300 | | Total (mini) | $45-50 | | Total (full) | $300-350 |

Wniosek: GPT-4o-mini wystarcza do 80% zastosowań RAG. Dopiero gdy potrzebujesz niuansów (prawne, medyczne) — przeskakuję na GPT-4o.

Debugging: 5 narzędzi, które używam codziennie

  1. LangSmith — tracing dla OpenAI, pełna widoczność prompts i responses. Płatne ($39/m) ale warte.
  2. Helicone — proxy OpenAI, logowanie tokenów i kosztów. Darmowy tier wystarcza do startu.
  3. OpenAI Playground — testowanie promptów bez kodu, porównywanie modeli obok siebie.
  4. pgvector Studio (lokalny GUI) — przeglądanie wektorów w bazie, sprawdzanie czy embedding ma sens.
  5. Własny eval set — 20-30 pytań z oczekiwanymi odpowiedziami, uruchamiam po każdej zmianie promptu. Bez tego optymalizujesz na ślepo.

Co dalej

Ten post to fundament. Kolejne kroki:

  • Function calling + persystencja — agenty, które pamiętają poprzednie wywołania
  • RAG z re-ranking — drugi model poprawia ranking retrieval
  • Streaming + function calling — dwa modele w tandemie dla UX

Jeśli budujesz aplikację z OpenAI API i potrzebujesz review architektury — napisz do mnie. Mam sprawdzone wzorce dla 8 różnych verticals (e-commerce, edukacja, B2B lead gen, HR, support, content generation, code assistance, document analysis).

Powiązane posty:

Tagi:#ai#openai#rag#function-calling#embeddings

Najczęściej zadawane pytania

Czym różni się RAG od zwykłego promptu z kontekstem?
RAG (Retrieval-Augmented Generation) automatycznie wybiera relevantne fragmenty z bazy wiedzy i wkleja je do promptu. Zwykły prompt z kontekstem to statyczne wklejenie całego dokumentu — działa do ~10k tokenów, potem model traci fokus. RAG skaluje się do milionów dokumentów, bo retrieval zwraca top-5 najtrafniejszych fragmentów zamiast wszystkiego.
Jaki model embeddingów OpenAI wybrać w 2025?
Dla większości przypadków: text-embedding-3-small (1536 wymiarów, $0.02 / 1M tokenów). Dla wysokiej jakości: text-embedding-3-large (3072 wymiarów, $0.13 / 1M tokenów) — warto dla krytycznych zastosowań (medyczne, prawne). Nie używaj już text-embedding-ada-002 — jest przestarzały i droższy.
Ile kosztuje RAG w produkcji?
Dla aplikacji z 10k użytkownikami/miesiąc, 3 pytania/user, średnio 500 tokenów retrieval + 500 tokenów completion: ~$45-60/miesiąc na GPT-4o-mini. GPT-4o: ~$300-450. Dodaj embeddings (cache'owane): $1-3/miesiąc. Storage wektorów (Pinecone/Supabase): $0-70/miesiąc zależnie od skali. Realny budżet: $50-500/miesiąc dla MVP.
Czy potrzebuję Pinecone czy mogę użyć PostgreSQL?
Możesz użyć PostgreSQL z pgvector — to wystarcza do ~1M wektorów. Powyżej 10M: rozważ dedykowaną bazę (Pinecone, Qdrant, Weaviate). Moje doświadczenie: pgvector + Supabase obsługuje 95% polskich SaaS-ów, jest tańszy i nie wymaga osobnej infrastruktury.

Powiązane posty