Podręcznik uczestnika z objaśnieniami, przykładami i ćwiczeniami do samodzielnej nauki do Modułu 5
6. Kontrolowane wykonanie narzędzia
Tekst modelu nie może wybierać dowolnej funkcji Pythona. Wykonawca korzysta z jawnego rejestru, sprawdza argumenty i stan, a po wywołaniu kontroluje wynik oraz przechwytuje błędy.
1. Walidacja to więcej niż obecność pola
Do pliku kod/moj_m5.py: validate_answer_arguments()
def validate_answer_arguments(arguments, state):
_fields(arguments, {'question', 'top_k', 'similarity_threshold'}, {'question'})
try:
q = validate_chat_message(arguments['question'], MAX_QUESTION_LENGTH)
except (TypeError, ValueError) as e:
raise ToolValidationError('invalid_question') from e
k = arguments.get('top_k', DEFAULT_TOP_K)
th = arguments.get('similarity_threshold', DEFAULT_SIMILARITY_THRESHOLD)
if type(k) is not int or not 1 <= k <= MAX_TOP_K:
raise ToolValidationError('invalid_top_k')
if th is not None and (type(th) not in (int, float) or not math.isfinite(th) or (not -1 <= th <= 1)):
raise ToolValidationError('invalid_threshold')
return {'question': q, 'top_k': k, 'similarity_threshold': th}
Gotowe _fields() wymaga słownika, odrzuca nieznane klucze i wykrywa brak wymaganych. top_k musi być dokładnie int od 1 do 5: bool nie jest dopuszczalny mimo dziedziczenia po int w Pythonie. Próg to None lub skończona liczba od -1 do 1. Sprawdzenie math.isfinite odrzuci NaN również przy bezpośrednim wywołaniu, które ominęło parser modelu.
Test R5: niepoprawne argumenty, bez RAG
state = m.create_initial_state()
for args in [
{"question": "Stan?", "top_k": True},
{"question": "Stan?", "top_k": 6},
{"question": "Stan?", "command": "whoami"}
]:
try:
m.validate_answer_arguments(args, state)
except m.ToolValidationError as error:
print(error.code)
Odczyt źródeł wymaga pustych argumentów i istniejących last_sources. Metadane wymagają znanego document_id. Te walidatory są już gotowe. Brak źródeł jest brakiem danych potrzebnych do działania, a nie dowodem nieistnienia narzędzia.
2. Ustal postać i limit wyniku
Do pliku kod/moj_m5.py: normalize_tool_output()
def normalize_tool_output(output):
if not isinstance(output, dict):
raise ToolValidationError('invalid_output')
try:
text = json.dumps(output, ensure_ascii=False, allow_nan=False)
except (ValueError, TypeError) as e:
raise ToolValidationError('invalid_output') from e
if len(text) > MAX_RESULT_CHARS:
raise ToolValidationError('result_too_large')
return deepcopy(output)
Wynik ma być słownikiem zapisywalnym w JSON bez NaN i Infinity. MAX_RESULT_CHARS=12000 ogranicza długość serializowanego tekstu. Kontrola następuje po wykonaniu funkcji: nie ogranicza czasu generacji ani wcześniej zużytej pamięci. Limit znaków nie jest limitem tokenów.
3. Jedna próba, również odrzucona
Do pliku kod/moj_m5.py: execute_tool_call()
def execute_tool_call(tool_call, state):
start = time.perf_counter()
name = None
args = {}
def finish(status, data=None, code=None):
return make_tool_result(name, status, data, {'code': code} if code else None, round((time.perf_counter() - start) * 1000, 3), args)
if state['tool_attempted_this_turn']:
return finish('tool_rejected', code='attempt_limit')
state['tool_attempted_this_turn'] = True
if not isinstance(tool_call, dict) or set(tool_call) != {'action', 'arguments'}:
return finish('invalid_arguments', code='invalid_envelope')
name = tool_call['action']
if not isinstance(name, str) or name not in TOOL_REGISTRY:
name = name if isinstance(name, str) else None
return finish('unknown_tool', code='unknown_tool')
try:
args = TOOL_VALIDATORS[name](tool_call['arguments'], state)
except ToolStateError as e:
return finish('clarification_needed', code=e.code)
except ToolValidationError as e:
return finish('invalid_arguments', code=e.code)
try:
output = TOOL_REGISTRY[name](state=state, **args) if name == 'show_last_sources' else TOOL_REGISTRY[name](**args)
output = normalize_tool_output(output)
return finish('tool_completed', data=output)
except ToolValidationError as e:
return finish('tool_error', code=e.code)
except Exception:
logger.exception('Błąd narzędzia %s', name)
return finish('tool_error', code='tool_execution_failed')
Znacznik ustawiamy przed sprawdzeniem koperty i nazwy. Po błędnej próbie nie wykonamy po cichu kolejnej w tej samej turze. Nazwa jest kluczem słownika, nie treścią do eval(). Dla show_last_sources wykonawca przekazuje zaufany stan aplikacji; model nie dostarcza własnego state.
| Sytuacja | tool.status | Wywołanie funkcji |
|---|---|---|
| Nazwa spoza rejestru | unknown_tool | Nie. |
| Złe argumenty | invalid_arguments | Nie. |
| Brak źródeł | clarification_needed | Nie. |
| Poprawne dane i wynik | tool_completed | Tak, raz. |
| Wyjątek lub zbyt duży wynik | tool_error | Tak; błąd w trakcie lub po wywołaniu. |
| Następna próba w turze | tool_rejected | Nie ponownie. |
4. Jak dowieść braku wykonania
Sam napis „odrzucono” nie dowodzi, że funkcja nie została wcześniej wywołana. Podmień RAG na dubler dopisujący argumenty do listy. Sprawdź status i długość listy. Przywróć funkcję w finally, także gdy test się nie powiedzie. Wynik dublera nie jest wynikiem modelu.
Test: odrzucona próba także zużywa limit
calls = []
original = m.TOOL_REGISTRY["answer_from_documents"]
def fake_rag(**kwargs):
calls.append(kwargs)
return {"status": "answered", "answer": "Dubler", "sources": []}
try:
m.TOOL_REGISTRY["answer_from_documents"] = fake_rag
state = m.create_initial_state()
m.begin_turn(state)
bad = m.execute_tool_call({
"action": "answer_from_documents",
"arguments": {"question": "Stan?", "top_k": True}
}, state)
assert bad["status"] == "invalid_arguments"
assert calls == []
again = m.execute_tool_call({
"action": "answer_from_documents",
"arguments": {"question": "Stan?"}
}, state)
assert again["status"] == "tool_rejected"
assert calls == []
finally:
m.TOOL_REGISTRY["answer_from_documents"] = original
execute_tool_call() zwraca wynik, ale sam nie zapisuje tool_trace.jsonl. Robi to wywołujący kod, znający ID rozmowy, tury i wyniku. Dzięki temu również wczesny return, np. unknown_tool, ma ślad. W R6 append_tool_trace() występuje bezpośrednio po wykonawcy; w macierzy R5 po każdej testowanej próbie.
Ćwiczenie 5. Zbuduj granicę wykonania
- Uzupełnij trzy funkcje R5 i uruchom test etapu.
- Przeanalizuj w macierzy R5 nieznane narzędzie, bool, dodatkowe pole, ścieżkę zamiast ID, wyjątek i duży wynik.
- Wyjaśnij różnicę między tool_error i invalid_arguments na podstawie momentu wywołania.
- Uzasadnij 14 wpisów śladu przy 13 przypadkach oraz wspólny result_id dwóch prób.
ENHANCER+ · Politechnika Warszawska · podręcznik do samodzielnej pracy · M5