Budowa chatbota CLI

5. Budowa lokalnego indeksu

5.1. Wczytanie indeksu i pierwszy ranking

Lokalne chatboty, agenci i systemy RAG z własną bazą wiedzy

Instrukcja krok po kroku · materiał uzupełniający

Budujemy chatbota w konsoli

Wyszukiwanie bez LLM

Odniesienie: M3 R4–R6   |   Czas: 25 min

Najpierw sprawdź wyszukiwanie niezależnie od generowania. Jeśli model nie dostanie potrzebnego fragmentu, sama zmiana promptu nie rozwiąże problemu.

Dopisz drugą część indeks.py

Dopisz poniższy kod bezpośrednio po funkcji build() z poprzedniego rozdziału. Gotowy plik w paczce zawiera już obie części.

def load_index():
    records = read_chunks()
    index = json.loads(INDEX.read_text(encoding="utf-8"))
    if index.get("format") != "poradnik-json-v1":
        raise ValueError("Nieobsługiwany format indeksu.")
    if index["chunks_sha256"] != file_hash(CHUNKS):
        raise ValueError("Baza zmieniła się. Zbuduj nowy indeks.")
    if index["embedding_model"] != CONFIG["embedding_model"]:
        raise ValueError("Model embeddingowy nie pasuje do indeksu.")
    installed = require_models([CONFIG["embedding_model"]])
    if installed[CONFIG["embedding_model"]] != index["embedding_digest"]:
        raise ValueError("Digest modelu embeddingowego nie pasuje. Zbuduj nowy indeks.")
    if index["chunk_ids"] != [r["chunk_id"] for r in records]:
        raise ValueError("Kolejność fragmentów nie pasuje do wektorów.")
    if len(index["vectors"]) != len(records):
        raise ValueError("Liczba wektorów nie pasuje do fragmentów.")
    if any(len(v) != index["dimension"] for v in index["vectors"]):
        raise ValueError("Niezgodny wymiar wektora w indeksie.")
    index["vectors"] = [unit(v) for v in index["vectors"]]
    return records, index


def retrieve(question, records, index, top_k):
    if type(top_k) is not int or not 1 <= top_k <= 10:
        raise ValueError("top_k musi być liczbą całkowitą od 1 do 10.")
    vector = embed(question)
    if len(vector) != index["dimension"]:
        raise ValueError("Wymiar pytania różni się od indeksu.")
    ranking = []
    for record, stored in zip(records, index["vectors"]):
        score = sum(a * b for a, b in zip(vector, stored))
        ranking.append({**record, "score": score})
    ranking.sort(key=lambda item: (-item["score"], item["chunk_id"]))
    return ranking[:top_k]


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("command", choices=("build", "search"))
    parser.add_argument("question", nargs="?")
    args = parser.parse_args()
    if args.command == "build":
        build()
    else:
        if not args.question or not args.question.strip():
            parser.error("Podaj pytanie w cudzysłowie.")
        records, index = load_index()
        for row in retrieve(args.question, records, index, CONFIG["top_k"]):
            print(f"{row['score']:.3f} | {row['chunk_id']} | {row['metadata']['section']}")
            print(row["text"], "\n")


if __name__ == "__main__":
    main()

Zbuduj indeks

.\.venv\Scripts\python.exe indeks.py build

Dla danych startowych zobaczysz kolejne komunikaty od „Embedding 1/10” do „Embedding 10/10” i informację o zapisaniu indeksu. To wywołania lokalnego modelu embeddingowego. Nie powstaje jeszcze żadna odpowiedź LLM.

Wyszukaj fragmenty

.\.venv\Scripts\python.exe indeks.py search "W jakich godzinach operator przyjmuje zgłoszenia?"
  1. Wynik zawiera score, chunk_id, sekcję i pełny tekst. Sprawdź, czy wśród top-3 jest DOC-REG-6a742ef524-0002.
  2. Zmień pytanie na parafrazę: Kiedy mogę skontaktować się z operatorem? Porównaj kolejność.
  3. Zapytaj o certyfikaty TLS. Zauważ, że mechanizm rankingowy nadal zwraca fragmenty, choć baza nie zawiera odpowiedzi.

Warto wiedzieć. Najbliższy fragment jest tylko najbliższy spośród dostępnych. Wysoki score nie potwierdza, że tekst zawiera odpowiedź. Nie ustawiamy uniwersalnego progu „prawdziwości”.

Gdy indeks już istnieje

Program zatrzyma się zamiast go nadpisać. Jeśli chcesz ponowić budowę po zmianie bazy lub modelu, zakończ chatbota, zachowaj indeks pod nową nazwą i dopiero uruchom build. Jeśli nic nie zmieniłeś, używaj istniejącego indeksu.

Punkt kontrolny

  • Indeks zawiera dziesięć wektorów dla dziesięciu rekordów.
  • Oczekiwany fragment znajduje się w rankingu dla pytania o godziny dyżuru.
  • Potrafisz obejrzeć ranking bez uruchamiania LLM.

Instrukcja uczestnika · Python i Ollama · wydanie 29.09.2026