llm_client: 문법이 약간 틀린 모델 JSON 복구 (값 안 따옴표·줄바꿈·꼬리 콤마·잘린 출력), 못 읽으면 원문 보존

고객사 사내 LLM 실측: "Expecting ',' delimiter" — 설명 문장 안의 따옴표를 이스케이프하지 않았다.
_repair_json 이 문자열 안의 따옴표를 닫는 따옴표(뒤에 , : } ])와 구분해 이스케이프하고,
끝내 못 읽으면 data/llm_jobs/_badjson/ 에 원문을 남긴다.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
byeongwook.choi
2026-09-21 16:32:50 +09:00
co-authored by Claude Fable 5.1
parent 9cdefa1a49
commit d8718c858f
2 changed files with 117 additions and 5 deletions
+29
View File
@@ -274,3 +274,32 @@ def test_merge_system_folds_system_into_user(monkeypatch):
assert msgs == [{"role": "user", "content": "SYS\n\nUSR"}]
monkeypatch.setattr(settings, "llm_merge_system", False)
assert [m["role"] for m in llm_client.OpenAICompatClient().body("SYS", "USR")["messages"]] == ["system", "user"]
# ------------------------------------------------------------------ 문법이 틀린 JSON 복구
def test_extract_json_repairs_common_model_mistakes(tmp_path, monkeypatch):
from config.settings import settings
monkeypatch.setattr(settings, "data_llm_jobs", tmp_path)
# 값 안의 따옴표 미이스케이프 (고객사 실측: Expecting ',' delimiter)
bad = '{"unit_purpose_ko": "전표를 "확정" 상태로 바꾼다", "chunks": [{"kind": "db_write", "purpose_ko": "BKPF 갱신"}]}'
assert _extract_json(bad)["unit_purpose_ko"] == '전표를 "확정" 상태로 바꾼다'
# 문자열 안의 실제 줄바꿈 + 꼬리 콤마
bad2 = '{"a": "줄1\n줄2", "b": [1, 2,],}'
assert _extract_json(bad2) == {"a": "줄1\n줄2", "b": [1, 2]}
# 출력이 잘린 경우 — 열린 것을 닫아 부분 결과라도 돌려준다
cut = '{"unit_purpose_ko": "요약", "chunks": [{"kind": "select", "purpose_ko": "조회'
assert _extract_json(cut) == {"unit_purpose_ko": "요약", "chunks": [{"kind": "select", "purpose_ko": "조회"}]}
# 코드펜스 + 서문 + 따옴표 문제가 같이 있는 경우
assert _extract_json('답입니다:\n```json\n{"x": "a "b" c"}\n```')["x"] == 'a "b" c'
# 정상 JSON 은 그대로
assert _extract_json('{"ok": true}') == {"ok": True}
def test_extract_json_dumps_unreadable_content(tmp_path, monkeypatch):
from config.settings import settings
monkeypatch.setattr(settings, "data_llm_jobs", tmp_path)
with pytest.raises(json.JSONDecodeError):
_extract_json("안녕하세요! 반갑습니다.")
dumped = list((tmp_path / "_badjson").glob("*.txt"))
assert len(dumped) == 1 and "안녕하세요" in dumped[0].read_text(encoding="utf-8")