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.
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ę:
-
tool_choice: { type: "function", ... }— wymusza, żeby model zawsze wywołał tę funkcję. Bez tego model może odpowiedzieć tekstem zamiast JSON-em. -
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. -
reasoningw 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 pgvectorCREATE EXTENSION IF NOT EXISTS vector;
-- Table with embeddingsCREATE 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 searchCREATE INDEX documents_embedding_idxON documentsUSING 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ę:
-
Threshold similarity 0.7 — poniżej tego dokumenty są szumem. Bez thresholda model dostaje random dokumenty i halucynuje.
-
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”).
-
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
dateModifiedjako 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
- LangSmith — tracing dla OpenAI, pełna widoczność prompts i responses. Płatne ($39/m) ale warte.
- Helicone — proxy OpenAI, logowanie tokenów i kosztów. Darmowy tier wystarcza do startu.
- OpenAI Playground — testowanie promptów bez kodu, porównywanie modeli obok siebie.
- pgvector Studio (lokalny GUI) — przeglądanie wektorów w bazie, sprawdzanie czy embedding ma sens.
- 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:
Najczęściej zadawane pytania
Czym różni się RAG od zwykłego promptu z kontekstem?
Jaki model embeddingów OpenAI wybrać w 2025?
Ile kosztuje RAG w produkcji?
Czy potrzebuję Pinecone czy mogę użyć PostgreSQL?
Powiązane posty
- aiAI
Multi-agent systemy AI w 2025: architektura, narzędzia, pułapki
Jak budować produkcyjne systemy multi-agent w 2025: Hermes-style orkiestracja, LangChain, autogen, evaluacja. Praktyczny przewodnik z kodem i realnymi pułapkami.
5 min - aiAI
GEO vs SEO: jak optymalizować treści pod wyszukiwarki AI w 2025
Generative Engine Optimization to nowa dyscyplina SEO. Jak pisać treści, które cytuje ChatGPT, Perplexity i Gemini. Praktyczne techniki z przykładami kodu i danymi.
5 min