fix(ai): B-1 deep_summary 잘린 응답 field-level regex fallback

_parse_outermost_json 도 열린 문자열 중간에 응답 끊기면 실패.
실전 MLX 응답이 entities_confirmed 내부 문자열에서 종료되는 패턴이라
detail/tldr/bullets/inconsistencies 전부 손실되던 이슈.

_regex_extract_fields helper 추가: "key":"value" 쌍 개별 매칭으로
앞쪽 완결된 필드만이라도 건진다. detail 이 응답 앞부분에 있어 잘림
지점보다 앞이면 성공.

순서:
  1. _parse_outermost_json (brace balance)
  2. parse_json_response (기존 regex)
  3. _regex_extract_fields (field-level fallback)

entities_confirmed 제거 같은 프롬프트 수정은 PR-A 영역이라 건드리지
않고, PR-B 워커에서 방어. 근본 해결은 p3c_deep_summary 에서 불필요
필드 제거 또는 max_tokens 튜닝을 policy 소유자가 결정.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-24 11:27:04 +09:00
parent 154cb1c8bd
commit 93a687a51d
+52 -2
View File
@@ -121,8 +121,13 @@ async def process(document_id: int, session: AsyncSession) -> None:
try:
# parse_json_response 는 중첩 JSON (entities_confirmed) 을 최외곽으로 오인하는
# 케이스가 있어 — deep_summary 응답에서 자주 발생 — 최외곽 추출 전용 helper 사용.
parsed = _parse_outermost_json(raw) or parse_json_response(raw) or {}
deep_out = DeepSummaryOutput.model_validate(parsed)
parsed = _parse_outermost_json(raw) or parse_json_response(raw)
if not parsed:
# 잘린 응답 fallback — field-level regex 로 detail/tldr/inconsistencies 추출
parsed = _regex_extract_fields(raw)
deep_out = DeepSummaryOutput.model_validate(parsed or {})
if not deep_out.detail and parsed and parsed.get("_fallback"):
logger.info(f"[deep] id={document_id} regex fallback parsed keys={list(parsed.keys())}")
except (ValidationError, ValueError, TypeError) as exc:
parse_error = f"parse:{type(exc).__name__}"
logger.warning(f"[deep] JSON 파싱/검증 실패 id={document_id}: {exc}")
@@ -248,6 +253,51 @@ def _parse_outermost_json(raw: str) -> dict | None:
return None
def _regex_extract_fields(raw: str) -> dict:
"""JSON parse 실패 시 field-level regex 로 detail/tldr/mode/inconsistencies 추출.
응답이 잘렸거나 중간에 문자열이 끊긴 경우에도 앞쪽에 완결된 필드는 건진다.
`"detail": ""` 처럼 key-value 쌍을 개별 매칭.
"""
def _str_field(key: str) -> str | None:
m = re.search(rf'"{key}"\s*:\s*"((?:[^"\\]|\\.)*)"', raw, re.DOTALL)
if not m:
return None
try:
# JSON string escape 복원 (\n, \\, \" 등)
return json.loads('"' + m.group(1) + '"')
except json.JSONDecodeError:
return m.group(1)
def _arr_field(key: str) -> list | None:
# 단순 문자열 배열만 지원 — bullets / inconsistencies_desc 등
m = re.search(rf'"{key}"\s*:\s*(\[[^\]]*\])', raw, re.DOTALL)
if not m:
return None
try:
return json.loads(m.group(1))
except json.JSONDecodeError:
return None
out: dict = {"_fallback": True}
mode = _str_field("mode")
if mode:
out["mode"] = mode
tldr = _str_field("tldr")
if tldr:
out["tldr"] = tldr
detail = _str_field("detail")
if detail:
out["detail"] = detail
bullets = _arr_field("bullets")
if bullets is not None:
out["bullets"] = bullets
inc = _arr_field("inconsistencies")
if inc is not None:
out["inconsistencies"] = inc
return out
def _filter_inconsistencies(items: list) -> list[dict]:
"""허용 kind 목록 (safety/news 도메인 한정) 만 통과시킨다.