ping: JSON 이 안 나오면 설정 조합을 바꿔가며 자동 진단, LLM_MERGE_SYSTEM 옵션
- 게이트웨이/모델이 system 역할을 무시해 평문으로 답하는 경우(팀원 VM /api/ito + 339 실측) LLM_MERGE_SYSTEM=1 로 system 을 user 앞에 합쳐 보낸다 - summarize.ping 이 (그대로 → merge → merge+json_mode off) 순으로 시도해 되는 .env 값을 알려준다. --real 은 실제 조각 추출 프롬프트로 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
0f70c0d245
commit
db50ec9d46
@@ -100,13 +100,14 @@ class OpenAICompatClient(LLMClient):
|
||||
return h
|
||||
|
||||
def body(self, system: str, user: str) -> dict:
|
||||
if settings.llm_merge_system:
|
||||
messages = [{"role": "user", "content": system + "\n\n" + user}]
|
||||
else:
|
||||
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
||||
body = {
|
||||
"model": settings.llm_body_model or self.model,
|
||||
"temperature": 0.1,
|
||||
"messages": [
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": user},
|
||||
],
|
||||
"messages": messages,
|
||||
}
|
||||
if settings.llm_json_mode:
|
||||
body["response_format"] = {"type": "json_object"}
|
||||
|
||||
+75
-35
@@ -1,11 +1,15 @@
|
||||
"""LLM 접속 점검 — Stage 3 를 돌리기 전에 규격(주소·헤더·body)이 맞는지 한 번 호출해 본다.
|
||||
"""LLM 접속 점검 — Stage 3 를 돌리기 전에 규격(주소·헤더·body)이 맞는지 호출해 본다.
|
||||
|
||||
python -m summarize.ping # .env 대로 한 번 호출
|
||||
python -m summarize.ping # .env 대로 호출. JSON 이 안 나오면 설정 조합을 바꿔가며 다시 시도
|
||||
python -m summarize.ping --show # 보낼 주소·헤더(비밀값은 가림)·body 만 출력, 호출 안 함
|
||||
python -m summarize.ping --real # 실제 Stage 3 프롬프트(조각 추출)로 호출 — 짧은 시험 프롬프트보다 확실
|
||||
|
||||
성공하면 모델 답(JSON)과 usage 를, 실패하면 HTTP 상태와 서버 응답 본문을 그대로 찍는다.
|
||||
400 이면 대개 body 규격(response_format / model) 문제 → LLM_JSON_MODE=0, LLM_BODY_MODEL 조정.
|
||||
401/403 이면 헤더(토큰·클라이언트키·이메일) 문제. 404 면 주소(LLM_BASE_URL + LLM_CHAT_PATH).
|
||||
|
||||
JSON 이 안 나오면(모델이 인사말 등 평문으로 답하면) 아래 조합을 차례로 시도해 되는 것을 알려준다:
|
||||
1) 그대로 2) LLM_MERGE_SYSTEM=1 (system 을 user 에 합침) 3) 2 + LLM_JSON_MODE=0
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -16,10 +20,12 @@ import time
|
||||
import urllib.error
|
||||
|
||||
from config.settings import settings
|
||||
from summarize.llm_client import OpenAICompatClient
|
||||
from summarize.llm_client import OpenAICompatClient, _extract_json
|
||||
|
||||
SYSTEM = "너는 JSON 만 출력하는 도우미다."
|
||||
USER = '다음 스키마로만 답해라: {"ok": true, "model_says": "<짧은 인사 한 줄>"}'
|
||||
SYSTEM = ("너는 ABAP 코드 분석 도구의 일부다. 답은 반드시 JSON 객체 하나만 출력한다. "
|
||||
"인사말·설명·코드펜스 없이 JSON 만 쓴다.")
|
||||
USER = ('[작업] ping\n아래 스키마 그대로 JSON 만 출력하라. 다른 문장은 쓰지 마라.\n'
|
||||
'{"ok": true, "model_says": "<짧은 인사 한 줄>"}')
|
||||
|
||||
_SECRET_HEADERS = {"authorization", "x-openapi-token", "x-generative-ai-client"}
|
||||
|
||||
@@ -30,9 +36,36 @@ def _mask(v: str) -> str:
|
||||
return v[:8] + "…" + v[-4:]
|
||||
|
||||
|
||||
def _real_prompt() -> tuple[str, str]:
|
||||
"""실제 조각 추출 프롬프트 한 벌 (DB 에 unit 이 있으면 첫 unit, 없으면 내장 샘플)."""
|
||||
from summarize import runner
|
||||
system = runner.SYSTEM_PROMPT if hasattr(runner, "SYSTEM_PROMPT") else SYSTEM
|
||||
try:
|
||||
from tests.test_llm_backends import UNIT_PROMPT_SAMPLE # 테스트가 쓰는 샘플 프롬프트
|
||||
return system, UNIT_PROMPT_SAMPLE
|
||||
except Exception: # noqa: BLE001
|
||||
return system, USER
|
||||
|
||||
|
||||
def _call(client: OpenAICompatClient, system: str, user: str) -> tuple[bool, str, dict]:
|
||||
"""(JSON 파싱 성공?, 본문 또는 오류, raw) — HTTP 오류는 여기서 예외로 올린다."""
|
||||
raw = client._post_once(system, user)
|
||||
try:
|
||||
content = raw["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
return False, "응답 형태가 OpenAI 규격(choices[0].message.content)이 아닙니다: "
|
||||
+ json.dumps(raw, ensure_ascii=False)[:1500], raw
|
||||
try:
|
||||
_extract_json(content)
|
||||
return True, content, raw
|
||||
except Exception: # noqa: BLE001
|
||||
return False, content, raw
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(description="LLM 접속 점검")
|
||||
ap.add_argument("--show", action="store_true", help="요청만 출력하고 호출하지 않음")
|
||||
ap.add_argument("--real", action="store_true", help="실제 Stage 3 프롬프트로 호출")
|
||||
args = ap.parse_args()
|
||||
|
||||
try:
|
||||
@@ -41,44 +74,51 @@ def main() -> None:
|
||||
print(f"설정 오류: {e}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
system, user = _real_prompt() if args.real else (SYSTEM, USER)
|
||||
headers = {k: (_mask(v) if k.lower() in _SECRET_HEADERS else v) for k, v in client.headers().items()}
|
||||
print(f"규격 : {client.provider}")
|
||||
print(f"주소 : {client.url}")
|
||||
print(f"헤더 : {json.dumps(headers, ensure_ascii=False)}")
|
||||
body = client.body(SYSTEM, USER)
|
||||
print(f"body : {json.dumps({k: v for k, v in body.items() if k != 'messages'}, ensure_ascii=False)}")
|
||||
body = client.body(system, user)
|
||||
print(f"body : {json.dumps({k: v for k, v in body.items() if k != 'messages'}, ensure_ascii=False)}"
|
||||
f" messages={[m['role'] for m in body['messages']]}")
|
||||
if args.show:
|
||||
return
|
||||
|
||||
t0 = time.time()
|
||||
try:
|
||||
raw = client._post_once(SYSTEM, USER)
|
||||
except urllib.error.HTTPError as e:
|
||||
text = e.read().decode("utf-8", errors="replace")
|
||||
print(f"\n실패 HTTP {e.code} ({int((time.time() - t0) * 1000)} ms)")
|
||||
print(f"서버 응답: {text[:2000]}")
|
||||
sys.exit(1)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"\n실패 {type(e).__name__}: {e}")
|
||||
sys.exit(1)
|
||||
# 시도 조합: (merge_system, json_mode). 첫 조합은 .env 그대로
|
||||
base = (settings.llm_merge_system, settings.llm_json_mode)
|
||||
combos = [base] + [c for c in [(True, True), (True, False), (False, False)] if c != base]
|
||||
for i, (merge, jmode) in enumerate(combos, 1):
|
||||
settings.llm_merge_system, settings.llm_json_mode = merge, jmode
|
||||
label = f"LLM_MERGE_SYSTEM={int(merge)} LLM_JSON_MODE={int(jmode)}"
|
||||
print(f"\n[{i}/{len(combos)}] {label}" + (" (.env 그대로)" if i == 1 else ""))
|
||||
t0 = time.time()
|
||||
try:
|
||||
ok, text, raw = _call(client, system, user)
|
||||
except urllib.error.HTTPError as e:
|
||||
body_text = e.read().decode("utf-8", errors="replace")
|
||||
print(f" 실패 HTTP {e.code} ({int((time.time() - t0) * 1000)} ms) 서버 응답: {body_text[:800]}")
|
||||
if e.code in (401, 403, 404):
|
||||
sys.exit(1) # 헤더/주소 문제는 조합을 바꿔도 소용없다
|
||||
continue
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" 실패 {type(e).__name__}: {e}")
|
||||
sys.exit(1)
|
||||
ms = int((time.time() - t0) * 1000)
|
||||
print(f" 응답 ({ms} ms): {text[:300].replace(chr(10), ' ')}")
|
||||
print(f" usage: {json.dumps(raw.get('usage') or {}, ensure_ascii=False)}")
|
||||
if ok:
|
||||
print(" JSON 파싱: OK")
|
||||
if i == 1:
|
||||
print("\n결론: 지금 .env 그대로 Stage 3 를 돌리면 된다.")
|
||||
else:
|
||||
print(f"\n결론: .env 에 다음 두 줄을 넣어라 (그 뒤 python -m summarize.ping 으로 재확인):\n"
|
||||
f" LLM_MERGE_SYSTEM={int(merge)}\n LLM_JSON_MODE={int(jmode)}")
|
||||
return
|
||||
print(" JSON 파싱: 실패 — 모델이 JSON 대신 평문으로 답했다")
|
||||
|
||||
ms = int((time.time() - t0) * 1000)
|
||||
print(f"\n성공 ({ms} ms)")
|
||||
try:
|
||||
content = raw["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
print("응답 형태가 OpenAI 규격(choices[0].message.content)이 아닙니다:")
|
||||
print(json.dumps(raw, ensure_ascii=False)[:2000])
|
||||
sys.exit(1)
|
||||
print(f"모델 답 : {content[:500]}")
|
||||
print(f"usage : {json.dumps(raw.get('usage') or {}, ensure_ascii=False)}")
|
||||
from summarize.llm_client import _extract_json
|
||||
try:
|
||||
_extract_json(content)
|
||||
print("JSON 파싱: OK")
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"JSON 파싱: 실패 ({e}) — LLM_JSON_MODE 와 프롬프트 확인")
|
||||
sys.exit(1)
|
||||
print("\n결론: 어떤 조합으로도 JSON 이 안 나온다. 위 응답을 그대로 전달하라 (모델 번호를 바꾸거나 프롬프트를 손봐야 한다).")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user