Podręcznik uczestnika z objaśnieniami, przykładami i ćwiczeniami do samodzielnej nauki do Modułu 5

7. Integracja własnego asystenta

Połączysz elementy tak, aby odmowa lub wyjątek nie omijały zakończenia tury. Koordynator odpowiada za kolejność, a poszczególne funkcje zachowują wąskie zadania.

1. Przepływ pojedynczej wiadomości

  1. begin_turn: identyfikatory, czas i wyzerowanie znacznika próby.
  2. Walidacja wiadomości i kontrola zakresu.
  3. Ograniczona historia i rozwiązanie pytania zależnego.
  4. Propozycja modelu oraz normalizacja, ewentualnie zakończenie bez narzędzia.
  5. Złożenie wywołania z zaufanych danych, walidacja i wykonanie.
  6. Ślad narzędzia, złożenie wyniku i finalize_turn.

2. Konfiguracja aplikacji ma pierwszeństwo

Do pliku kod/moj_m5.py: build_tool_call()

def build_tool_call(decision, resolved, *, top_k=DEFAULT_TOP_K, similarity_threshold=DEFAULT_SIMILARITY_THRESHOLD):
    args = deepcopy(decision.get('arguments', {}))
    if decision['action'] == 'answer_from_documents':
        args.update(question=resolved['standalone_question'], top_k=top_k, similarity_threshold=similarity_threshold)
    return {'action': decision['action'], 'arguments': args}

Dla RAG selektor nie zmienia jawnie wybranego top_k ani progu. Wykorzystujemy pytanie z resolvera. args.update() zastępuje te trzy pola, ale pozostawia nieznane dodatkowe pola, aby walidator mógł je odrzucić. Ich ciche usunięcie ukrywałoby niezgodną propozycję.

Test R6: zaufane dane zastępują propozycję parametrów

decision = {"action": "answer_from_documents", "arguments": {
    "question": "inna treść", "similarity_threshold": None}}
resolved = {"standalone_question": "Jak skonfigurować TLS?"}
call = m.build_tool_call(decision, resolved, similarity_threshold=1.0)
assert call["arguments"]["question"] == resolved["standalone_question"]
assert call["arguments"]["similarity_threshold"] == 1.0

3. Koordynator tury

To kompletna definicja do ostatniego miejsca WARSZTAT. Pomocnicze _base_turn(), _finish_early(), build_validation_error_result(), build_assistant_result() i append_tool_trace() już istnieją. _finish_early() też korzysta z Twojego finalize_turn(). Wszystkie kontrolowane zakończenia zwiększają numer i zachowują ślad.

Do pliku kod/moj_m5.py: assistant_step()

def assistant_step(user_message, state, *, top_k=DEFAULT_TOP_K, similarity_threshold=DEFAULT_SIMILARITY_THRESHOLD, use_selector=True):
    turn = begin_turn(state)
    result = _base_turn(user_message, turn)
    try:
        clean = validate_chat_message(user_message)
    except (TypeError, ValueError) as error:
        return finalize_turn(state, build_validation_error_result(error, turn), turn)
    result['validation'] = {'status': 'valid'}
    if _forbidden(clean):
        result['decision'] = {'status': 'rejected', 'origin': 'application_policy'}
        return _finish_early(state, result, turn, 'rejected', 'forbidden_action', 'DocAssist opisuje procedury, ale nie wykonuje operacji systemowych ani wysyłania danych.')
    if 'metadan' in clean.casefold() and (not re.search('\\bDOC-[A-Z]+\\b', clean)):
        result['decision'] = {'status': 'clarification_needed', 'origin': 'application_policy'}
        return _finish_early(state, result, turn, 'clarification_needed', 'missing_document_id', 'Podaj jawny identyfikator dokumentu, np. DOC-INSTR.')
    history = get_recent_history(state['history'])
    resolved = resolve_followup_question(clean, history)
    result['resolution'] = resolved
    result['conversation'].update(standalone_question=resolved['standalone_question'], history_used=resolved['history_used'])
    result['diagnostics']['trace'] += ['history', 'resolve']
    if resolved['resolution_status'] == 'clarification_needed':
        return _finish_early(state, result, turn, 'clarification_needed', resolved['reason'], resolved['clarification_question'])
    if is_last_sources_request(clean) and (not state['last_sources']):
        result['decision'] = {'status': 'clarification_needed', 'origin': 'application_state'}
        return _finish_early(state, result, turn, 'clarification_needed', 'missing_previous_sources', NO_SOURCES_MESSAGE)
    result['diagnostics']['trace'].append('decide')
    if use_selector:
        try:
            selection = propose_action(clean, history, build_state_summary(state), TOOL_SPECS, standalone_question=resolved['standalone_question'])
        except Exception:
            logger.exception('Błąd selektora')
            return _finish_early(state, result, turn, 'error', 'generation_error', 'Nie udało się uzyskać propozycji działania.')
        decision = normalize_action_proposal(selection)
        result['diagnostics']['selection'] = selection
    else:
        decision = {'status': 'action_proposed', 'action': 'show_last_sources' if is_last_sources_request(clean) else 'answer_from_documents', 'arguments': {}, 'message': None, 'origin': 'fixed_flow_r3'}
    result['decision'] = decision
    if decision['status'] != 'action_proposed':
        ds = decision['status']
        reason = 'forbidden_action' if ds == 'rejected' else ds
        return _finish_early(state, result, turn, 'error' if ds == 'parse_error' else 'clarification_needed' if ds == 'clarification_needed' else 'rejected', reason, decision.get('message') or 'Niepoprawna propozycja działania.')
    if decision['action'] == 'get_document_metadata':
        explicit_ids = re.findall('\\bDOC-[A-Z]+\\b', clean)
        if decision['arguments'].get('document_id') not in explicit_ids:
            return _finish_early(state, result, turn, 'rejected', 'invalid_arguments', 'Propozycja metadanych nie wskazuje identyfikatora z bieżącej wiadomości.')
    call = build_tool_call(decision, resolved, top_k=top_k, similarity_threshold=similarity_threshold)
    result['diagnostics']['trace'] += ['build_call', 'tool']
    tool_result = execute_tool_call(call, state)
    append_tool_trace(tool_result, turn)
    combined = build_assistant_result(user_message, resolved, decision, tool_result, turn)
    combined['diagnostics'] = result['diagnostics']
    if combined['rag']:
        combined['diagnostics']['trace'].append('rag')
    combined['diagnostics']['trace'].append('build')
    return finalize_turn(state, combined, turn)

Kontrole przed selektorem obsługują rozpoznane żądanie operacji systemowej, brak document_id i brak poprzednich źródeł. To reguły prototypu, nie uniwersalny filtr języka. Możliwości ogranicza też rejestr: brak w nim powłoki. Dla metadanych dodatkowo porównujemy zaproponowane ID z bieżącą wiadomością, aby model nie wybrał innego znanego dokumentu.

use_selector=False uruchamia stały przepływ R3 bez modelu wybierającego narzędzie. Domyślna ścieżka dodaje selektor. Nie ma automatycznego ponawiania: niepoprawną propozycję zapisujemy i kończymy turę zamiast ukrywać błąd kolejną próbą.

4. Komendy obsłuż przed turą

Wejście Działanie
/exit lub /koniec Koniec interfejsu bez RAG.
/debug Przełączenie diagnostyki, bez nowej tury.
/sources Odczyt źródeł jako komenda, bez nowej tury.
/clear Nowy conversation_id i numer od 1; czyszczenie historii i źródeł, zachowanie plików.
Pokaż źródła. Naturalna wiadomość z osobnym result_id.

Gotowe parse_interface_input(), handle_interface_command(), process_interface_input() i run_assistant() łączą interfejs z koordynatorem. Do wykonania komendy użyj process_interface_input(). Dosłowne /clear przekazane wprost do assistant_step() nie przejdzie przez parser komend i może zostać potraktowane jak pytanie.

5. Własna implementacja i scenariusze pakietu

Po uzupełnieniu wszystkich funkcji uruchom w warsztacie część „Scenariusze własnego modułu”. Adapter scenariusz_warsztatu() przekazuje do istniejących scenariuszy właśnie moj_m5. Sama zmiana importu w standardowym notebooku nie zmieniłaby wewnętrznej referencji używanej przez m5_exercises.

11 scenariuszy R6; po przygotowaniu STUDENT w notebooku

from sprawdz_moj_m5 import scenariusz_warsztatu
from m5_exercises import save_review, save_integration_report, check_submission
OUT = m.new_run(STUDENT, "6_ksiazka")
MODE = "checkpoint"  # albo jawnie "ollama" dla nowych wywołań
m.configure(STUDENT, mode=MODE, output_dir=OUT, save_raw=True)
wyniki = scenariusz_warsztatu(m, 6, OUT)
for r in wyniki:
    print(r["case_id"], r["status"], r["reason"])

Każda próba otrzymuje nowy katalog. Powtórny scenariusz w tym samym OUT jest blokowany. Pełny zapis włączamy jawnie dla fikcyjnych danych. Zwykłe ślady przechowują skrót pytania; functional_tests.jsonl i surowe_wyniki.jsonl pozwalają odczytać pełną treść. Nie dodawaj danych poufnych do udostępnianych prób.

6. Oceń rzeczywiste zakończenia

Przypadek z pilotażu Obserwacja Wniosek
tura_2: stan usługi show_last_sources i answered. Błędna intencja mimo poprawnego wykonania.
tura_3: failed Odmowa; brak potrzebnego chunk_id w sources. Oceń pobrany kontekst osobno od generatora.
tls_domyslnie answered z odmową. Nie zamieniaj na no_usable_results.
tls_prog below_threshold, rag.similarity_threshold=1.0. Kod zastosował próg mimo None w propozycji.
zabronione / puste rejected przed narzędziem. Kontrola zakresu i wejścia.
po_clear / zrodla_po_clear clarification_needed. Nie użyto starego stanu.

decision.status=action_proposed oznacza propozycję, tool.status=tool_completed wykonanie, a rag.status=answered tekst wygenerowany przez RAG. Zewnętrzne answered może też oznaczać odczyt źródeł lub metadanych. Sprawdź reason, wybraną akcję i treść przed uznaniem wyniku za poprawny.

7. Zapis oceny i raportu

Przy każdym result_id uzupełnij obserwacja, dowod i wniosek. Dowodem jest konkretne pole lub fragment źródła, nie ogólne „działa”. Raport ma pola granice, dowody i przekazanie_do_M6. Funkcje nie zapiszą wersji zawierającej UZUPEŁNIJ. Po zmianie komentarza uruchom tylko ocenę i zapis, bez nowych prób modelu.

Zapis oceny tej samej próby OUT

# Po uzupełnieniu oceny i raportu w notebooku:
zapis_oceny = save_review(OUT, wyniki, oceny)
zapis_raportu = save_integration_report(OUT, raport)
if zapis_oceny and zapis_raportu:
    print(check_submission(OUT))

Ćwiczenie 6. Złóż prototyp i pokaż dowody

  1. Uzupełnij build_tool_call() i assistant_step(), sprawdź etap R6.
  2. Uruchom scenariusze dla własnego modułu: dialog, TLS, restart, puste wejście i reset.
  3. Wskaż poprawne zakończenie bez odpowiedzi oraz błąd modelu, jeśli wystąpił. Zachowaj go w logu.
  4. Zapisz oceny, raport i notebook; uruchom kontrolę kompletności.
  5. Opcjonalnie w nowej próbie Ollama zmień jeden element promptu lub resolvera i oceń te same przypadki.

ENHANCER+ · Politechnika Warszawska · podręcznik do samodzielnej pracy · M5