fix(projekt-matching): disable thinking for structured vLLM calls; retry non-JSON responses

With the vLLM qwen3 reasoning parser active, json_schema guided decoding
plus thinking degenerates: runs burn the whole 65k context
(finish_reason "length", ~63k completion tokens) and return empty or
truncated content. chat_template_kwargs {"enable_thinking": false} fixes
it (extract answers in ~35 s). Also: retry chat_json up to 3x on
non-JSON content, and two prompt calibrations verified against both
gate examples — Nice signal words bind only to their own line (following
unmarked lines stay Must), and compound requirements with clear evidence
for one part rate "unknown" instead of "no".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
tlg
2026-07-09 15:48:55 +02:00
parent 20796b3a68
commit cef4041500

View File

@@ -1,7 +1,12 @@
"""vLLM (OpenAI-compatible) calls with response_format json_schema + German prompts.
The model is a REASONING model: max_tokens stays UNSET so long thinking
chains cannot truncate the final answer (65k context window).
max_tokens stays UNSET so nothing can truncate the final answer (65k
context window). Thinking is DISABLED via chat_template_kwargs: with the
vLLM qwen3 reasoning parser active, json_schema guided decoding plus
thinking degenerates — the run burns the entire context window
(finish_reason "length", ~63k completion tokens) and returns empty or
truncated content (verified live in Task 13). Without thinking the same
extract call answers in ~35 s with valid schema-conformant JSON.
"""
from __future__ import annotations
@@ -86,7 +91,12 @@ Kategorie Misc.
"plus"). Misc = Rahmenbedingungen und alles, was weder Muss noch \
Wunsch-Qualifikation ist. Explizite Abschnittsüberschriften haben Vorrang \
vor Signalwörtern; "idealerweise" INNERHALB einer Must-Zeile lässt die \
Zeile Must bleiben.
Zeile Must bleiben. Nice-Signalwörter gelten NUR für die Zeile, in der \
sie selbst stehen — sie färben NIE auf folgende Zeilen ab: Jede Zeile \
eines Anforderungs-Abschnitts OHNE eigenes Nice-Signalwort ist Must, \
auch wenn direkt davor Nice-Zeilen stehen. Beispiel: Auf "Idealerweise \
zusätzlich Erfahrung mit X" (Nice) folgt "Erfahrung mit Y" ohne \
Signalwort → Y ist Must.
- miscType nur für Misc-Zeilen relevant (sonst "other"): start = \
Projektstart/Verfügbarkeit (startDate als ISO-Datum YYYY-MM-DD, wenn ein \
konkretes Datum genannt ist, sonst null); workload = Auslastung \
@@ -117,7 +127,9 @@ Proof-of-Concept-Erfahrung deckt "produktiven Betrieb" NICHT ab; \
"mehrjährig" wörtlich nehmen; ein Produktname (z. B. "Azure DevOps \
Server") belegt KEINE Cloud-Plattform-Erfahrung.
- "unknown" NUR bei echter Teilevidenz, wenn die Entscheidung von Wissen \
abhängt, das nur der Kandidat selbst hat.
abhängt, das nur der Kandidat selbst hat. Zusammengesetzte Anforderungen \
("X sowie Y", "X und Y"): klare Evidenz für einen Teil und keine \
Gegen-Evidenz für den anderen → "unknown", nicht "no".
- "wie z. B."-Aufzählungen: gleichwertige Alternativen zählen als Abdeckung \
(Beispiel: Ollama/llama.cpp/Transformers decken "LLM-Inference-Stacks wie \
z. B. vLLM, TGI, Triton" ab).
@@ -126,6 +138,7 @@ z. B. vLLM, TGI, Triton" ab).
def _post(base, model, messages, schema):
body = {"model": model, "messages": messages, "temperature": 0.1,
"chat_template_kwargs": {"enable_thinking": False},
"response_format": {"type": "json_schema", "json_schema": {
"name": "out", "schema": schema}}}
return requests.post(f"{base.rstrip('/')}/chat/completions", json=body,