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

Odniesienie: M4 R5, R6   |   Czas: 25 min

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"(S\d+)", 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

PoleJak je odczytać
result_idIdentyfikator jednego wywołania. Wskazuj go przy omawianiu błędu.
rankingZnalezione top-k przed progiem i budżetem kontekstu. Nie jest to pełny ranking całej bazy.
sourcesPełne teksty i metadane fragmentów wysłanych do LLM.
request / raw_responseDokładne żądanie generowania i surowa odpowiedź serwera.
citation_checkKontrola istnienia etykiet; znaczenie cytowania ocenia człowiek.
warningsSygnały do sprawdzenia, np. nieznana etykieta lub możliwe ucięcie odpowiedzi.

Odczytaj status poprawnie

StatusZnaczenie
answeredModel wygenerował tekst. Może to być dobra odpowiedź, błąd albo tekstowa odmowa z powodu braku informacji.
insufficient_contextDo promptu nie dopuszczono żadnego fragmentu; generowanie nie zostało wywołane.
invalid_inputWiadomość jest pusta lub zbyt długa.
errorNie 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