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

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

MAHAWIR IWANOWSKI · WROCŁAW

Przyszłość organizacji — budowana tam, gdzie psychologia spotyka inżynierię, renderowana w kodzie, dostarczana z intencją.

Przyszłość Organizacji w Kodzie

AI · Fullstack · Psychology

© 2026 Mahawir Iwanowski · Wszystkie prawa zastrzeżone.