fix(llm): pusta odpowiedz modelu to blad, nie pusta strona
Objaw zgloszony przez uzytkownika: w Kalendarzu horoskop wyswietla sie
poprawnie, w Interpretacjach zapytanie wychodzi, wraca — i NIC sie nie
pokazuje. Bez zadnego komunikatu.
Przyczyna: model potrafi oddac pusta tresc (finish_reason=length,
completion_tokens=0), a generate() zwracalo wtedy pusty tekst BEZ bledu.
Widok sprawdza {% if prompt_result.horoscope %} -> falsz -> nie renderuje nic,
a llm_error nie jest ustawiony -> zero wyjasnienia. Cicha awaria.
Asymetria miedzy ekranami wynika z rozmiaru promptu: natalny (13 obiektow x
fasety x opisy z bazy) wypelnia okno kontekstu modelu lokalnego i na odpowiedz
nie zostaje miejsca; okresowy jest mniejszy i sie miesci.
- wspolny straznik _require_text() dla obu dostawcow: pusta lub bialoznakowa
odpowiedz podnosi LLMError,
- komunikat PROWADZI DO PRZYCZYNY: podaje finish_reason i zuzycie tokenow oraz
radzi zmniejszyc budzet promptu / zwiekszyc num_ctx / LLM_MAX_TOKENS,
- Anthropic sprowadzony do wspolnego ksztaltu diagnostyki (input/output_tokens).
Dzieki temu uzytkownik widzi powod ORAZ gotowy prompt do recznego uzycia.
Zweryfikowane na zywym stosie z atrapa modelu oddajaca pusta tresc: zamiast
pustej strony pojawia sie pelny komunikat z diagnostyka. Testy: 148 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -49,6 +49,39 @@ def _post_with_retry(url: str, headers: dict, payload: dict, timeout: float) ->
|
||||
raise LLMError(f"Nie udało się wywołać modelu: {last}")
|
||||
|
||||
|
||||
def _require_text(text: str, usage: dict, finish_reason: str | None, max_tokens: int) -> str:
|
||||
"""Pusta odpowiedź modelu MUSI być błędem, nie pustym tekstem.
|
||||
|
||||
Inaczej mamy cichą awarię: warstwa wyżej dostaje `horoscope=""`, widok nic nie
|
||||
renderuje i NIC nie tłumaczy użytkownikowi, dlaczego strona wróciła pusta.
|
||||
Najczęstsza przyczyna: prompt wypełnił okno kontekstu modelu, więc na odpowiedź
|
||||
nie zostało miejsca (`finish_reason=length`, `completion_tokens=0`) — dotyczy to
|
||||
zwłaszcza obszernych promptów natalnych na modelach lokalnych o małym kontekście.
|
||||
"""
|
||||
if text and text.strip():
|
||||
return text
|
||||
|
||||
powod = []
|
||||
if finish_reason:
|
||||
powod.append(f"finish_reason={finish_reason}")
|
||||
if usage:
|
||||
pt, ct = usage.get("prompt_tokens"), usage.get("completion_tokens")
|
||||
if pt is not None:
|
||||
powod.append(f"tokeny promptu={pt}")
|
||||
if ct is not None:
|
||||
powod.append(f"tokeny odpowiedzi={ct}")
|
||||
szczegoly = f" ({', '.join(powod)})" if powod else ""
|
||||
|
||||
rada = (
|
||||
"Najczęstsza przyczyna: prompt nie zmieścił się w oknie kontekstu modelu i na "
|
||||
"odpowiedź nie zostało miejsca. Zmniejsz budżet promptu (zwięzły), zwiększ okno "
|
||||
"kontekstu modelu (w Ollamie num_ctx) albo podnieś LLM_MAX_TOKENS."
|
||||
if finish_reason == "length" or (usage or {}).get("completion_tokens") == 0
|
||||
else "Model przyjął żądanie, ale nie wygenerował treści."
|
||||
)
|
||||
raise LLMError(f"Model zwrócił pustą odpowiedź{szczegoly}. {rada}")
|
||||
|
||||
|
||||
class ChatCompletionsProvider(LLMProvider):
|
||||
"""Protokół OpenAI `/chat/completions` — lokalny serwer modelu ORAZ OpenAI."""
|
||||
|
||||
@@ -78,12 +111,15 @@ class ChatCompletionsProvider(LLMProvider):
|
||||
self.timeout,
|
||||
)
|
||||
try:
|
||||
text = data["choices"][0]["message"]["content"]
|
||||
choice = data["choices"][0]
|
||||
text = choice["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError) as e:
|
||||
raise LLMError(f"Nieoczekiwany kształt odpowiedzi modelu: {str(data)[:300]}") from e
|
||||
usage = data.get("usage") or {}
|
||||
text = _require_text(text, usage, choice.get("finish_reason"), max_tokens)
|
||||
return Completion(
|
||||
text=text, model=data.get("model", self.model), provider=self.name,
|
||||
leaves_lan=self.leaves_lan, usage=data.get("usage") or {},
|
||||
leaves_lan=self.leaves_lan, usage=usage,
|
||||
)
|
||||
|
||||
def health(self) -> dict:
|
||||
@@ -131,9 +167,15 @@ class AnthropicProvider(LLMProvider):
|
||||
text = "".join(b.get("text", "") for b in data["content"] if b.get("type") == "text")
|
||||
except (KeyError, TypeError) as e:
|
||||
raise LLMError(f"Nieoczekiwany kształt odpowiedzi modelu: {str(data)[:300]}") from e
|
||||
usage = data.get("usage") or {}
|
||||
# Anthropic nazywa to inaczej — sprowadzamy do wspólnego kształtu dla diagnostyki
|
||||
norm = {"prompt_tokens": usage.get("input_tokens"),
|
||||
"completion_tokens": usage.get("output_tokens")}
|
||||
text = _require_text(text, {k: v for k, v in norm.items() if v is not None},
|
||||
data.get("stop_reason"), max_tokens)
|
||||
return Completion(
|
||||
text=text, model=data.get("model", self.model), provider=self.name,
|
||||
leaves_lan=True, usage=data.get("usage") or {},
|
||||
leaves_lan=True, usage=usage,
|
||||
)
|
||||
|
||||
def health(self) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user