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
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 buildDla 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?"- Wynik zawiera score, chunk_id, sekcję i pełny tekst. Sprawdź, czy wśród top-3 jest
DOC-REG-6a742ef524-0002. - Zmień pytanie na parafrazę:
Kiedy mogę skontaktować się z operatorem?Porównaj kolejność. - 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