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:
2026-09-15 14:14:27 +02:00
parent 4a9e06f66e
commit 1594aa9cbd
7 changed files with 115 additions and 13 deletions
+29 -8
View File
@@ -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}]