Budowa chatbota CLI
6. Połączenie wyszukiwania z odpowiedzią
6.1. Funkcja ask_rag i źródła odpowiedzi
Lokalne chatboty, agenci i systemy RAG z własną bazą wiedzy
Instrukcja krok po kroku · materiał uzupełniający
Budujemy chatbota w konsoli
Kompletny rag.py
Ustal jeden format wyniku. Interfejs będzie mógł pokazywać status, odpowiedź i źródła bez odgadywania znaczenia tekstu wygenerowanego przez model.
Dopisz drugą część rag.py
Dopisz kod do pliku z poprzedniego rozdziału. W pakiecie jest już złożony plik.
def ask_rag(question, records, index):
result = {"result_id": uuid.uuid4().hex,
"created_at": datetime.now(timezone.utc).isoformat(),
"question": question, "status": "error", "answer": "",
"sources": [], "ranking": [], "warnings": [], "request": None,
"prompt_version": "poradnik-rag-v1"}
if not isinstance(question, str) or not 1 <= len(question.strip()) <= CONFIG["question_chars"]:
result.update(status="invalid_input", answer="Pytanie musi mieć 1–1000 znaków.")
return result
try:
result["ranking"] = retrieve(question, records, index, CONFIG["top_k"])
sources, context = select_context(result["ranking"])
if not sources:
result.update(status="insufficient_context", reason="no_accepted_context",
answer="Brak fragmentów dopuszczonych do kontekstu.")
return result
result["sources"] = sources
messages = [{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"KONTEKST:\n{context}\n\nPYTANIE:\n{question}"}]
result["request"] = {"model": CONFIG["chat_model"], "messages": messages,
"stream": False, "options": {k: CONFIG[k] for k in
("temperature", "seed", "num_ctx", "num_predict")}}
raw = chat(messages)
result.update(status="answered", answer=raw["message"]["content"], raw_response=raw)
cited = set(re.findall(r"
", result["answer"]))
known = {row["label"] for row in sources}
result["citation_check"] = {"cited": sorted(cited), "unknown": sorted(cited - known),
"semantic_verification": "manual_required"}
if cited - known:
result["warnings"].append("Nieznana etykieta źródła.")
if not cited:
result["warnings"].append("Brak cytowania; oceń ręcznie, czy odpowiedź jest odmową.")
if raw.get("done_reason") == "length":
result["warnings"].append("Możliwe ucięcie odpowiedzi przez num_predict.")
except (RuntimeError, ValueError, KeyError, TypeError) as exc:
result.update(status="error", reason=str(exc), answer="Nie udało się wykonać zapytania.")
return result
def format_sources(sources):
lines = []
for row in sources:
meta = row["metadata"]
location = meta.get("section") or "brak sekcji"
page = meta.get("page_number") or meta.get("page_start")
if page:
location += f", str. {page}"
lines.append(f"[{row['label']}] {meta['source']} | wersja {meta['version']} | "
f"{location} | {row['chunk_id']}")
return "\n".join(lines) or "Brak źródeł w bieżącej turze."Co zawiera wynik
| Pole | Jak je odczytać |
|---|---|
| result_id | Identyfikator jednego wywołania. Wskazuj go przy omawianiu błędu. |
| ranking | Znalezione top-k przed progiem i budżetem kontekstu. Nie jest to pełny ranking całej bazy. |
| sources | Pełne teksty i metadane fragmentów wysłanych do LLM. |
| request / raw_response | Dokładne żądanie generowania i surowa odpowiedź serwera. |
| citation_check | Kontrola istnienia etykiet; znaczenie cytowania ocenia człowiek. |
| warnings | Sygnały do sprawdzenia, np. nieznana etykieta lub możliwe ucięcie odpowiedzi. |
Odczytaj status poprawnie
| Status | Znaczenie |
|---|---|
| answered | Model wygenerował tekst. Może to być dobra odpowiedź, błąd albo tekstowa odmowa z powodu braku informacji. |
| insufficient_context | Do promptu nie dopuszczono żadnego fragmentu; generowanie nie zostało wywołane. |
| invalid_input | Wiadomość jest pusta lub zbyt długa. |
| error | Nie udało się wykonać etapu technicznego. Zobacz pole reason. |
Jest to prostszy kontrakt niż pełny asystent M5–M6. Nie dodajemy statusów dla wyboru narzędzia czy doprecyzowania, ponieważ ta wersja ich nie implementuje. Status answered nie jest automatyczną oceną jakości.
Punkt kontrolny
- Plik rag.py zawiera obie części.
- Potrafisz wyjaśnić, dlaczego sama etykieta [S1] nie wystarcza do uznania odpowiedzi za poprawną.
Instrukcja uczestnika · Python i Ollama · wydanie 29.09.2026