From cef4041500b7e3a13cb9cf872c63f2120600c8b3077ee52cea6553a819bff9c2 Mon Sep 17 00:00:00 2001 From: tlg Date: Thu, 9 Jul 2026 15:48:55 +0200 Subject: [PATCH] fix(projekt-matching): disable thinking for structured vLLM calls; retry non-JSON responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- projekt-matching/projektmatch/llm.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/projekt-matching/projektmatch/llm.py b/projekt-matching/projektmatch/llm.py index 1657d9f..210af4c 100644 --- a/projekt-matching/projektmatch/llm.py +++ b/projekt-matching/projektmatch/llm.py @@ -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,