Zespoły, które chcą asystenta nad własnymi dokumentami, zwykle zadają to samo pierwsze pytanie: wytrenować model na naszych dokumentach czy podłączyć model do nich? Dla bazy wiedzy domyślna odpowiedź brzmi: podłączyć, czyli to, co robi retrieval-augmented generation (RAG), i zanim wybierze się cokolwiek innego, zbudować zbiór testowy z prawdziwych pytań. Ta notatka prostymi słowami wyjaśnia oba podejścia, podaje tabelę decyzyjną, a potem krok po kroku opisuje dwutygodniowy test, który można przeprowadzić z jednym inżynierem i jedną osobą znającą dokumenty.
RAG i fine-tuning prostymi słowami
Model językowy wie to, co było w jego danych treningowych. Nie zna Państwa polityki zwrotów, cennika z zeszłego miesiąca ani wewnętrznej procedury zatwierdzania dostawcy.
RAG rozwiązuje to w momencie pytania. Gdy ktoś zadaje pytanie, system przeszukuje dokumenty, bierze kilka fragmentów, które wyglądają na najbardziej trafne, i przekazuje je modelowi z instrukcją: odpowiedz na podstawie tych fragmentów i wskaż, których użyłeś. Dokumenty zostają tam, gdzie są. Wystarczy zmienić politykę, ponownie ją zaindeksować, a następna odpowiedź uwzględni zmianę. Technikę opisano w pracy badawczej z 2020 roku, która połączyła generator tekstu z przeszukiwalnym indeksem dokumentów; główna idea od tego czasu się nie zmieniła.
Fine-tuning kontynuuje trenowanie modelu na przykładach danych wejściowych i oczekiwanych wyników. Zmienia nawyki modelu: ton, strukturę, słownictwo, sposób wykonywania jednego wąskiego zadania. To słaby sposób na uczenie faktów, które się zmieniają. Każda zmiana źródła wymaga nowego treningu, model nie potrafi wskazać, skąd wzięła się odpowiedź, a wszystko, czego się nauczył, jest dostępne dla każdego, kto może go używać.
Większość firm już tak pracuje. Raport Menlo Ventures o generatywnej AI w przedsiębiorstwach z grudnia 2025 roku wskazał projektowanie promptów jako najczęstszy sposób dostosowywania modeli przez firmy, a RAG jako drugi; fine-tuning i reinforcement learning pozostawały niszą, używaną głównie przez zespoły z czołówki.
Ile to kosztuje. Poniższy dwutygodniowy test kosztuje czas jednego inżyniera i kilka dni eksperta dziedzinowego, plus użycie modelu, które w skali zbioru testowego jest niewielkie. Na produkcji koszt działania wynika z jednego wzoru: pytania miesięcznie × tokeny na pytanie × cena za token. Dla ilustracji: 10 000 pytań miesięcznie, z których każde wysyła pięć fragmentów po około 400 tokenów plus 500 tokenów instrukcji i pytania, a dostaje odpowiedź o długości 300 tokenów, to 10 000 × 2 800 = 28 milionów tokenów. Przy poglądowej uśrednionej cenie 5 USD za milion tokenów daje to 140 USD miesięcznie. Należy użyć aktualnych cen swojego dostawcy; liczy się wzór. Jeśli system ma zostać zbudowany, a nie tylko przetestowany, nasz opublikowany przedział dla wewnętrznego copilota RAG to 8k–18k USD w ciągu 4–8 tygodni, a strona z cennikiem pokazuje stawki, na których się opiera.
Tabela decyzyjna
| Czego potrzeba | RAG | Fine-tuning |
|---|---|---|
| Wiedza, która zmienia się co tydzień albo codziennie | Zmiany są widoczne po następnym ponownym indeksowaniu | Każda zmiana wymaga nowego treningu i nowej ewaluacji |
| Link do źródła przy każdej odpowiedzi | Wbudowany: odpowiedź cytuje fragmenty, których użyła | Brak wiarygodnego sposobu, by wskazać, skąd pochodzi odpowiedź |
| Różne osoby mogą widzieć różne dokumenty | Fragmenty są filtrowane według uprawnień użytkownika, zanim zobaczy je model | Wszystko, co zostało wytrenowane, jest widoczne dla każdego użytkownika modelu |
| Stały format wyniku albo firmowy ton | Możliwy dzięki instrukcjom i przykładom w prompcie, czasem niekonsekwentny | Dobre dopasowanie |
| Niskie opóźnienie | Dodaje krok wyszukiwania i dłuższy prompt | Bez dodatkowego kroku; mniejszy dostrojony model może być też szybszy |
| Koszt aktualizacji | Ponowne zaindeksowanie zmienionych dokumentów | Przygotowanie przykładów, ponowny trening, ponowna ewaluacja |
| Dane potrzebne na start | Dokumenty i zbiór testowy prawdziwych pytań | Przykładowe dane wejściowe z idealnymi wynikami sprawdzonymi przez ludzi, plus ten sam zbiór testowy |
Warto policzyć, które wiersze mają znaczenie w danym przypadku. Dla bazy wiedzy zwykle rozstrzygają pierwsze trzy: treść się zmienia, ludzie chcą sprawdzić źródło, a nie każdy może czytać wszystko.
Fine-tuning ma swoje miejsce w węższym zestawie przypadków: odpowiedzi muszą zawsze mieć ścisłą strukturę; zadanie o dużym wolumenie, na przykład kierowanie zgłoszeń do kategorii, mogłoby działać na mniejszym, tańszym modelu; albo ton musi odpowiadać przewodnikowi stylu, którego prompt nie utrzymuje. Nawet wtedy zwykle działa on na wierzchu wyszukiwania, a nie zamiast niego, jako dostrojony model, który nadal czyta wyszukane fragmenty.
Domyślnie więc najpierw RAG plus zbiór ewaluacyjny. Fine-tuning później, dla formatu, tonu albo wąskiego zadania, i tylko jeśli zbiór testowy pokaże, że pozostałe błędy są właśnie tego rodzaju.
Dwutygodniowy test
Test zastępuje opinie liczbą. Na końcu jest wynik na własnych pytaniach i lista błędów pogrupowanych według przyczyny, co mówi, czy budować, co poprawić i czy fine-tuning w ogóle by pomógł.
Krok 1: zebrać prawdziwe pytania
Należy wziąć pytania, które ludzie faktycznie zadali: zgłoszenia do helpdesku, wątki z wewnętrznego czatu, logi wyszukiwarki w intranecie, pytania, na które właściciel polityki odpowiada mailem. Celem jest 150–300 pytań. Warto uwzględnić 10–15% takich, na które dokumenty nie odpowiadają, bo poprawna odmowa jest częścią zadania.
Dlaczego tyle: przy 200 pytaniach i rzeczywistej trafności około 80% margines błędu przy 95% wynosi około ±5,5 punktu procentowego (1,96 × √(0,8 × 0,2 / 200) ≈ 0,055). Mniejszy zbiór nie odróżni od siebie dwóch wersji systemu.
Dla każdego pytania osoba znająca materiał zapisuje oczekiwaną odpowiedź i wskazuje dokument, z którego powinna pochodzić. Jedna linia JSON na pytanie:
{"id": "q041", "question": "Who approves a new supplier above the purchasing threshold?", "expected": "Procurement lead and finance, both in writing.", "doc": "supplier-onboarding", "groups": ["procurement"], "answerable": true}
Każda odpowiedź dostaje następnie jedną z czterech etykiet:
- correct: poprawna, kompletna i cytuje właściwy dokument;
- partly: poprawna, ale niepełna, albo poprawna z błędnym lub brakującym cytowaniem;
- wrong: stwierdza coś fałszywego albo odpowiada na pytanie, na które dokumenty nie odpowiadają;
- declined: stwierdza, że nie może odpowiedzieć.
Odmowa to celowo osobna etykieta. Przy pytaniu, na które da się odpowiedzieć, jest to pudło, ale tańsze niż błędna odpowiedź, a biznes decyduje, ile jednych i drugich może zaakceptować. Ten próg należy uzgodnić przed pierwszym przebiegiem, tak samo jak metrykę uzgadnia się przed trenowaniem modelu.
Ekspert dziedzinowy ręcznie ocenia pierwsze 50 odpowiedzi. Potem resztę może ocenić drugi model, porównując z oczekiwanymi odpowiedziami, o ile jego etykiety zgadzają się z etykietami eksperta na tych 50. Obie praktyki są powszechne: w ankiecie LangChain wśród 1 340 praktyków budujących agentów, przeprowadzonej w listopadzie i grudniu 2025 roku, 59,8% stosowało przegląd przez ludzi, 53,3% używało modelu jako sędziego, a 52,4% prowadziło ewaluacje offline na zbiorach testowych.
Krok 2: zaindeksować fragmenty razem z uprawnieniami
Dokumenty należy dzielić według nagłówków na fragmenty po kilkaset słów. Przy każdym fragmencie trzeba zachować id dokumentu, tytuł, link i grupy dostępu. Uprawnienia należą do fragmentu od początku; dodanie ich później oznacza ponowne zaindeksowanie wszystkiego.
Krok 3: wyszukiwanie hybrydowe z reciprocal rank fusion
Wyszukiwanie słów kluczowych (BM25) znajduje dokładne terminy, takie jak kody produktów, numery formularzy i skróty. Wyszukiwanie po embeddingach znajduje parafrazy: „wolne”, gdy w polityce jest „urlop wypoczynkowy”. Każde z nich przeoczy to, co złapie drugie, więc warto używać obu i łączyć rankingi.
Reciprocal rank fusion (RRF) łączy według pozycji, a nie według wyniku, więc dwóch skal punktacji nigdy nie trzeba kalibrować: każdy fragment dostaje 1 / (k + rank) z każdej listy, a wartości się sumuje. Praca z 2009 roku, która wprowadziła tę metodę, ustaliła k na 60 i uznała tę wartość za bliską optymalnej, choć niekrytyczną.
import numpy as np
from rank_bm25 import BM25Okapi
def tokenize(text):
return text.lower().split()
class HybridIndex:
def __init__(self, chunks, embed):
# chunks: dicts with "text", "doc", "groups" (a set); embed: list[str] -> unit vectors
self.chunks, self.embed = chunks, embed
self.bm25 = BM25Okapi([tokenize(c["text"]) for c in chunks])
self.vectors = np.asarray(embed([c["text"] for c in chunks]))
def search(self, query, user_groups, k=5, depth=50, rrf_k=60):
allowed = [i for i, c in enumerate(self.chunks) if c["groups"] & user_groups]
lexical = self.bm25.get_scores(tokenize(query))
semantic = self.vectors @ np.asarray(self.embed([query]))[0]
fused = {}
for scores in (lexical, semantic):
ranked = sorted(allowed, key=lambda i: scores[i], reverse=True)[:depth]
for rank, i in enumerate(ranked, start=1):
fused[i] = fused.get(i, 0.0) + 1.0 / (rrf_k + rank)
top = sorted(fused, key=fused.get, reverse=True)[:k]
return [{**self.chunks[i], "similarity": float(semantic[i])} for i in top]
embed to dowolna funkcja embeddingów, hostowana albo open source; jej zamiana zmienia jeden argument. Filtr uprawnień działa przed rankingiem, więc fragment, którego użytkownik nie może otworzyć, nigdy nie trafia do modelu.
Krok 4: odpowiedzieć z cytowaniami albo odmówić
Dwie kontrole powstrzymują system przed zmyślaniem. Przed wywołaniem modelu: jeśli najlepszy fragment nie jest wystarczająco podobny do pytania, system odmawia, niczego nie generując. Po wywołaniu: każda odpowiedź musi cytować fragmenty, a każdy cytowany numer musi istnieć. Wszystko inne liczy się jako odmowa.
import re
PROMPT = """Answer the question using only the numbered passages below.
After each sentence, cite the passages you used, for example [2].
If the passages do not contain the answer, reply with exactly: NOT_FOUND
{passages}
Question: {question}"""
def answer(question, index, user_groups, llm, min_similarity=0.35):
hits = index.search(question, user_groups)
if not hits or max(h["similarity"] for h in hits) < min_similarity:
return {"status": "declined", "text": None, "docs": []}
passages = "\n\n".join(f"[{n}] ({h['doc']}) {h['text']}" for n, h in enumerate(hits, 1))
text = llm(PROMPT.format(passages=passages, question=question)).strip()
cited = {int(n) for n in re.findall(r"\[(\d+)\]", text)}
if text == "NOT_FOUND" or not cited or not cited <= set(range(1, len(hits) + 1)):
return {"status": "declined", "text": None, "docs": []}
return {"status": "answered", "text": text, "docs": [hits[n - 1]["doc"] for n in sorted(cited)]}
llm to dowolna funkcja, która przyjmuje prompt i zwraca tekst, więc model można wymienić bez dotykania tego kodu. Próg 0.35 to wartość zastępcza: skale podobieństwa różnią się między modelami embeddingów. Należy ustawić go na zbiorze testowym tak, żeby pytania bez odpowiedzi były odrzucane bez utraty zbyt wielu pytań, na które odpowiedź istnieje.
Krok 5: pętla oceniania
import json
from collections import Counter
def run_eval(path, index, llm, judge):
items = [json.loads(line) for line in open(path)]
labels, found = Counter(), 0
for item in items:
groups = set(item["groups"])
if item["answerable"]:
hits = index.search(item["question"], groups)
found += item["doc"] in {h["doc"] for h in hits}
result = answer(item["question"], index, groups, llm)
if not item["answerable"]:
label = "correct" if result["status"] == "declined" else "wrong"
elif result["status"] == "declined":
label = "declined"
else: # judge returns "correct", "partly" or "wrong"
label = judge(item["question"], item["expected"], result["text"])
labels[label] += 1
answerable = sum(1 for item in items if item["answerable"])
print({k: round(v / len(items), 3) for k, v in labels.items()})
print("expected document in top 5:", round(found / answerable, 3))
Druga liczba jest równie ważna jak pierwsza. Jeśli właściwego dokumentu nie było w pierwszej piątce, model nie miał szans, a poprawka leży w wyszukiwaniu albo w dokumentach, nie w modelu. Etykiety z każdego przebiegu warto zapisywać z datą i opisem wprowadzonej zmiany, żeby każdą zmianę porównywać z tym samym punktem odniesienia.
Tydzień 1 i tydzień 2
Tydzień 1: punkt odniesienia.
- Dni 1–2: zebrać i spisać pytania razem z właścicielami dokumentów; uzgodnić próg akceptacji.
- Dzień 3: zaindeksować dokumenty; uruchomić najpierw samo wyszukiwanie słów kluczowych, potem hybrydowe, i zapisać trafność top-5 dla obu.
- Dni 4–5: pierwszy pełny przebieg; ekspert ocenia 50 odpowiedzi; sprawdzić względem nich model-sędziego; ocenić resztę.
Wynikiem tygodnia 1 jest liczba i lista błędów pogrupowanych według czterech przyczyn: dokumentu brakuje albo jest nieaktualny; wyszukiwanie pominęło właściwy fragment; model miał właściwy fragment, a mimo to odpowiedział źle; system powinien był odmówić, a nie odmówił.
Tydzień 2: poprawki w kolejności od największych.
- Zacząć od największej przyczyny. Brakujące lub nieaktualne dokumenty to zadanie treściowe dla ich właścicieli. Pominięcia w wyszukiwaniu zwykle ustępują po lepszym chunkingu, dodaniu tytułów dokumentów i nagłówków do każdego fragmentu albo synonimów dla wewnętrznego żargonu. Złe odpowiedzi przy dobrych fragmentach ustępują po zmianach promptu, a czasem po zmianie modelu.
- Po każdej zmianie uruchomić ponownie pełny zbiór. Zmianę zachować tylko wtedy, gdy wynik przesuwa się o więcej niż margines błędu.
- Próg odmowy dostroić na końcu, na ostatecznej konfiguracji wyszukiwania.
Pod koniec tygodnia 2 decyzja jest zwykle jasna. Jeśli odsetek błędnych odpowiedzi jest poniżej uzgodnionego progu, buduje się wersję produkcyjną, a zbiór testowy zostaje jej bramką wydań. Jeśli pozostałe błędy dotyczą formatu albo tonu, a fakty są poprawne, to jest przypadek, w którym warto przetestować fine-tuning, a zbiór testowy już istnieje, żeby pokazać, czy pomógł. Jeśli większość błędów to brakujące dokumenty, problemem jest baza wiedzy i żaden wybór modelu tego nie naprawi.
W tej kolejności pracujemy przy budowie copilotów RAG: najpierw prawdziwe pytania, potem wyszukiwanie, potem wynik, który każde wydanie musi pobić.
Źródła
- Menlo Ventures, 2025: The State of Generative AI in the Enterprise, grudzień 2025 — https://menlovc.com/perspective/2025-the-state-of-generative-ai-in-the-enterprise/
- LangChain, State of Agent Engineering, grudzień 2025 — https://www.langchain.com/state-of-agent-engineering
- NeurIPS 2020 (preprint arXiv), Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks, maj 2020 — https://arxiv.org/abs/2005.11401
- ACM SIGIR 2009, Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods, lipiec 2009 — https://doi.org/10.1145/1571941.1572114