length-Retry: abgeschnittene Antworten sind die q-024-Ursache, kein Thinking (D14)
- Diagnose: q-024-Flakiness war KEIN Think-Ghost (thinking-Feld leer, keine Tags), sondern num_predict=1024 — lange belegte Antworten brachen bei done_reason=length ab (3/3 Sondenlaeufe), Zitationen wurden unvollstaendig, CITE_RE matchte partielle IDs -> Verletzung -> Regenerierungs-Eskalation -> UNCERTAIN. - Fix: num_predict 1024 -> 2048; OllamaClient.chat_full() liefert (content, done_reason); chat_with_length_retry() wiederholt bei length einmal mit 2x Budget (technischer Retry, kein Regel- Regenerierungszaehler) - im Antwort-, Map- und Regenerierungspfad. - Voll-Eval (46 Fragen): Zitier-Praezision 100 %, Verweigerung korrekt 100 % (46/46) - erstmals alle M3-Gates erfuellt; erwartete Quelle 92,7 %; Latenz mean 39,6 s / p95 78 s (vollstaendige statt abgeschnittener Antworten). q-024: 4/4 stabil. - Tests 59 -> 60. Report lokal data/eval-qwen38-lengthfix.json.
This commit is contained in:
@@ -168,6 +168,18 @@ q-015 antwortet mit KV-Abhängigkeit + Rückfrage. Eval (46 Fragen):
|
||||
Zitier-Präzision 97,8 % · Verweigerung 97,8 % (Gate ✓) · erwartete
|
||||
Quelle 90,2 % · Latenz mean 34,2 s. Report: `data/eval-qwen38-stage2.json`.
|
||||
|
||||
**Output-Budget + length-Retry (2026-09-15, D14):** Die q-024-Flakiness
|
||||
war kein Thinking, sondern `num_predict=1024`: lange belegte Antworten
|
||||
wurden bei `done_reason=length` abgeschnitten → unvollständige Zitationen
|
||||
→ Verletzungs-/Eskalationsspirale. Fix: `num_predict=2048`, `chat_full()`
|
||||
liefert `done_reason`, bei `length` ein technischer Retry mit 2× Budget
|
||||
(kein Regel-Regenerierungs-Zähler). **Voll-Eval: Zitier-Präzision
|
||||
100 % · Verweigerung korrekt 100 %** (46/46, alle M3-Gates erstmals
|
||||
voll erfüllt) · erwartete Quelle 92,7 % · Latenz mean 39,6 s / p95 78 s.
|
||||
Report: `data/eval-qwen38-lengthfix.json`. Offene Restfälle: keine
|
||||
Fehlverweigerungen/Verletzungen mehr; 3-4 Fragen zitiert gültige,
|
||||
aber nicht die erwartete Quelle (q-015-Klasse).
|
||||
|
||||
## Dateien
|
||||
|
||||
```
|
||||
|
||||
+3
-1
@@ -51,7 +51,9 @@ class Config:
|
||||
# (KV-Cache @32k ~6-8 GB -> gesamt ~21 GB, Budget ok)
|
||||
max_context_chars: int = 90_000 # Budget fuer User-Content; niedrig gerankte
|
||||
# Bloecke werden weggelassen statt trunciert
|
||||
num_predict: int = 1024
|
||||
num_predict: int = 2048 # Output-Budget; 1024 schnitt lange belegte
|
||||
# Antworten ab (done_reason=length, q-024-Fall)
|
||||
# -> unvollstaendige Zitationen
|
||||
think: bool = False # Thinking per Request abschalten (Latenz)
|
||||
chat_timeout_s: float = 300.0
|
||||
embed_timeout_s: float = 240.0
|
||||
|
||||
+29
-8
@@ -227,20 +227,41 @@ def answer_question(
|
||||
{"role": "user", "content": build_user_content(question, results)},
|
||||
]
|
||||
|
||||
def chat(msgs):
|
||||
return strip_think(
|
||||
client.chat(
|
||||
def chat(msgs, num_predict: int | None = None):
|
||||
"""Ein Chat-Zug; liefert (gestrippter Content, done_reason).
|
||||
Bei done_reason='length' (Antwort bei num_predict abgeschnitten)
|
||||
sind Zitationen ggf. unvollstaendig — der Aufrufer ruft einmal mit
|
||||
doppeltem Budget neu (technischer Retry, kein Regel-Regeneration)."""
|
||||
budget = num_predict or cfg.num_predict
|
||||
if hasattr(client, "chat_full"):
|
||||
raw, done_reason = client.chat_full(
|
||||
cfg.answer_model,
|
||||
msgs,
|
||||
temperature=cfg.temperature,
|
||||
num_ctx=cfg.num_ctx,
|
||||
num_predict=cfg.num_predict,
|
||||
num_predict=budget,
|
||||
think=cfg.think,
|
||||
)
|
||||
)
|
||||
else:
|
||||
raw = client.chat(
|
||||
cfg.answer_model,
|
||||
msgs,
|
||||
temperature=cfg.temperature,
|
||||
num_ctx=cfg.num_ctx,
|
||||
num_predict=budget,
|
||||
think=cfg.think,
|
||||
)
|
||||
done_reason = "stop"
|
||||
return strip_think(raw), done_reason
|
||||
|
||||
def chat_with_length_retry(msgs):
|
||||
final, done_reason = chat(msgs)
|
||||
if done_reason == "length":
|
||||
final, done_reason = chat(msgs, num_predict=cfg.num_predict * 2)
|
||||
return final
|
||||
|
||||
if map_messages is not None:
|
||||
summary = chat(map_messages)
|
||||
summary = chat_with_length_retry(map_messages)
|
||||
if summary:
|
||||
messages = [
|
||||
{"role": "system", "content": SYSTEM_PROMPT},
|
||||
@@ -255,7 +276,7 @@ def answer_question(
|
||||
]
|
||||
# leerer Map-Output -> Fallback: messages bleibt die Einzelantwort
|
||||
|
||||
final = chat(messages)
|
||||
final = chat_with_length_retry(messages)
|
||||
violations = validate_answer(final, allowed)
|
||||
regenerations = 0
|
||||
if violations:
|
||||
@@ -267,7 +288,7 @@ def answer_question(
|
||||
"Beantworte die Frage erneut und zitiere nur diese IDs — oder verweigere "
|
||||
f"mit dem vorgesehenen Satz („{REFUSAL_MESSAGE}“)."
|
||||
)
|
||||
retry = chat(
|
||||
retry = chat_with_length_retry(
|
||||
messages
|
||||
+ [{"role": "assistant", "content": final},
|
||||
{"role": "user", "content": warn}]
|
||||
|
||||
+18
-4
@@ -74,10 +74,14 @@ class OllamaClient:
|
||||
|
||||
# -- Chat ---------------------------------------------------------------
|
||||
|
||||
def chat(self, model: str, messages: list[dict],
|
||||
temperature: float = 0.1, num_ctx: int = 16384,
|
||||
num_predict: int = 1024, think: bool = False) -> str:
|
||||
"""POST /api/chat, stream=False; `think`-Flag mit 404/400-Fallback."""
|
||||
def chat_full(self, model: str, messages: list[dict],
|
||||
temperature: float = 0.1, num_ctx: int = 16384,
|
||||
num_predict: int = 1024,
|
||||
think: bool = False) -> tuple[str, str]:
|
||||
"""POST /api/chat; liefert (content, done_reason). done_reason ==
|
||||
'length' bedeutet: Antwort wurde bei num_predict abgeschnitten —
|
||||
Zitationen koennen dann unvollstaendig sein (Think-Ghost-Ursache
|
||||
q-024, D13-Follow-up)."""
|
||||
body: dict[str, Any] = {
|
||||
"model": model,
|
||||
"messages": messages,
|
||||
@@ -105,4 +109,14 @@ class OllamaClient:
|
||||
content = msg.get("content") or ""
|
||||
if not content.strip():
|
||||
raise OllamaError(f"empty response from {model} (keys: {list(data.keys())})")
|
||||
return content, str(data.get("done_reason") or "stop")
|
||||
|
||||
def chat(self, model: str, messages: list[dict],
|
||||
temperature: float = 0.1, num_ctx: int = 16384,
|
||||
num_predict: int = 1024, think: bool = False) -> str:
|
||||
"""POST /api/chat, stream=False; `think`-Flag mit 404/400-Fallback."""
|
||||
content, _ = self.chat_full(
|
||||
model, messages, temperature=temperature, num_ctx=num_ctx,
|
||||
num_predict=num_predict, think=think,
|
||||
)
|
||||
return content
|
||||
Reference in New Issue
Block a user