Skip to content

fix: tagging JSON 파싱 prefix 흡수 (assistant prefill + 슬라이스 fallback)#19

Merged
gyesswhat merged 1 commit into
mainfrom
fix/#18-tagging-json-parse-prefix
May 28, 2026
Merged

fix: tagging JSON 파싱 prefix 흡수 (assistant prefill + 슬라이스 fallback)#19
gyesswhat merged 1 commit into
mainfrom
fix/#18-tagging-json-parse-prefix

Conversation

@gyesswhat

@gyesswhat gyesswhat commented May 28, 2026

Copy link
Copy Markdown
Contributor

요약

  • Haiku 응답 앞에 붙는 도입어·markdown prefix 때문에 char 0 unexpected JSON 파싱 실패가 24h 164회 발생하던 운영 이슈를 assistant prefill + {...} 슬라이스로 흡수.

변경 사항

  • 기능
  • 버그 수정
  • 리팩토링
  • 문서
  • 테스트
  • 기타

코드

  • _extract_json_payload 신규 — 코드펜스 strip 후 첫 { ~ 마지막 } 슬라이스. 도입어·꼬리말 흡수.
  • _parse_and_validate_strip_code_fence_extract_json_payload 호출.
  • run 의 1차/2차 호출 모두 messages 마지막에 {"role": "assistant", "content": "{"} prefill 추가. 응답 처리 시 _ASSISTANT_PREFILL + raw 로 합쳐 파싱 → 첫 글자 { 100% 보장.
  • _MAX_TOKENS 250 → 350 — prefix 흡수 마진 + max_tokens 잘림 1건 (운영 실측) 방지.
  • tagging.validation_failed 로그에 raw_head (응답 앞 80자) 추가 — 패치 후 재발생 모니터링용. STAR 본문(PII) 미포함이라 안전. 안정 확인되면 별도 PR 로 제거 예정.

테스트

  • 신규: _extract_json_payload 6 케이스 (clean / 한국어 prefix / markdown prefix+suffix / 공백 / 코드펜스 / 브레이스 없음 passthrough).
  • 신규: _parse_and_validate_handles_intro_prefix — 도입어 응답 e2e 파싱.
  • 기존 mock 응답들 prefill 가정에 맞춰 { 제거 (헤더 _HAPPY_LLM_RAW 등). 코멘트로 의미 명시.

테스트

  • 로컬 테스트 완료
  • 테스트 방법:
    • uv run pytest → 77 passed
    • uv run ruff check app/ tests/ → All checks passed
    • uv run ruff format --check app/ tests/ → 변경 없음
    • uv run mypy app → pre-existing anthropic stubs 누락 1건 (본 변경과 무관)

스크린샷/로그

운영 실측 (stg groute-ai docker logs, 2026-05-28 기준):

24h validation_failed: 164회
- ~95%: "JSON 파싱 실패: unexpected character: line 1 column 1 (char 0)"
- ~5%: "허용 태그 풀 외 태그 포함" (corrective로 거의 회복)

실패 응답 output_tokens: 134, 146, 157, 187 (정상 41~56 대비 3~4배)
1건: stop_reason=max_tokens, output=250 (도입어로 토큰 다 쓰고 JSON `{` 도 못 찍음)

→ 도입어/markdown prefix 가 원인. 본 PR 머지 + 배포 후 tagging.validation_failed 빈도 추이 모니터링.

롤백/리스크

  • 리스크 포인트:
    1. assistant prefill 적용으로 LLM 응답 분포 미세 변화 가능 (첫 글자 강제 → 후속 토큰 분포 약간 다를 수 있음). 단 출력 형식(JSON 한 줄)은 동일하므로 contract 영향 없음.
    2. _extract_json_payload{ ~ } 슬라이스는 본문에 중첩 brace ({ ... { ... } ... }) 가 들어와도 가장 바깥 { 와 가장 마지막 } 로 잘 잡힘. detailTags 응답 스키마는 nested object 없으므로 안전.
    3. _MAX_TOKENS 350 으로 +40% 늘어남 → 비용 영향 미미 (output 단가 $5/Mtok, 평균 응답 ~50 tok 이라 실질 인상은 거의 0).
  • 롤백 방법: revert 커밋 → 재배포. 코드 변경이 v2_postscore.py 한 파일에 몰려있어 revert 안전.

관련 이슈

Closes #18

Summary by CodeRabbit

릴리스 노트

  • 버그 수정

    • 태그 지정 시스템의 데이터 추출 안정성 개선
    • JSON 파싱 오류에 대한 처리 강화
  • 테스트

    • 관련 테스트 케이스 확대 및 검증 로직 강화

Review Change Stack

@gyesswhat gyesswhat added fix 버그, 오류, 잘못된 동작 수정 status:review 작업 완료, 리뷰 또는 머지 대기 중 priority:high 즉시 처리 필요, 지연 시 영향 큼 labels May 28, 2026
@gyesswhat gyesswhat self-assigned this May 28, 2026
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
📝 Walkthrough

🚀 Walkthrough

Haiku 4.5 응답의 도입어 prefix로 인한 JSON 파싱 실패 문제를 해결하기 위해 LLM 프롬프팅 및 파싱 파이프라인을 재구성합니다. Assistant prefill 메시지 주입, 강화된 JSON 추출 함수, 토큰 한도 증가를 조합하여 안정성을 개선하고, 이를 검증하는 단위/통합 테스트를 확장합니다.


📋 Changes

v2_postscore JSON 파싱 강화

Layer / File(s) Summary
파싱 파라미터 및 JSON 추출 유틸리티
app/services/tagging/v2_postscore.py
토큰 한도를 250→350으로 증가, _ASSISTANT_PREFILL 상수 ("{") 추가, 백틱·도입어·공백을 제거하고 {~} 슬라이싱으로 JSON object 본문만 추출하는 _extract_json_payload() 함수 신규 추가.
검증 로직에 강화된 추출 함수 통합
app/services/tagging/v2_postscore.py
_parse_and_validate가 기존 _strip_code_fence 대신 _extract_json_payload를 사용하여 도입어가 붙은 응답도 JSON으로 파싱 가능하게 변경.
LLM 호출 흐름에 assistant prefill 메시지 적용
app/services/tagging/v2_postscore.py
run()의 1차·2차 llm.create_message 호출에서 user + assistant(prefill "{") 메시지 구성으로 변경. 원문을 _ASSISTANT_PREFILL + 추출된_텍스트로 재조립, 검증 실패 로깅에 raw_head (첫 80자) 추가.
테스트 기초 설정 및 상수 업데이트
tests/test_tagging.py
_extract_json_payload import 추가, 모든 기존 mock 응답을 prefill-완성 형식 ("detailTags": ...}) 으로 일괄 변경.
기존 시나리오 mock 응답 일괄 업데이트
tests/test_tagging.py
corrective retry, 이중 검증 실패, 갯수 위반 회복, 검증 실패 로그 등 기존 테스트 시나리오 mock을 새 포맷으로 조정.
_extract_json_payload 및 도입어 처리 단위 테스트 추가
tests/test_tagging.py
도입어·markdown 코드펜스·공백 제거 케이스를 매개변수화하여 테스트, _parse_and_validate가 도입어 prefix 붙은 입력도 정상 파싱하는지 검증.

🔄 Sequence Diagram

sequenceDiagram
    participant Caller as 호출자
    participant run as run()
    participant LLM as Claude Haiku 4.5
    participant extract as _extract_json_payload
    participant validate as _parse_and_validate
    
    Caller->>run: tag_request 호출
    
    rect rgba(100, 150, 200, 0.5)
    Note over run,validate: 1차 LLM 호출
    run->>LLM: messages=[user, assistant("{")]
    LLM-->>run: "detailTags": [...] (도입어 생략)
    run->>run: first_raw = "{" + extract_text(...)
    run->>validate: "{detailTags": [...]} 전달
    validate->>extract: JSON 추출 시도
    extract-->>validate: 정제된 JSON object
    validate-->>run: 검증 성공
    end
    
    rect rgba(200, 100, 100, 0.5)
    Note over run,validate: 검증 실패 시 2차 재시도
    run->>LLM: messages=[user, assistant("{")]
    LLM-->>run: "detailTags": [...] (또 도입어)
    run->>validate: "{detailTags": [...]} 전달
    validate-->>run: 검증 성공 또는 최종 실패
    end
    
    run-->>Caller: tag_result 반환
Loading

📊 Code Review Effort

🎯 3 (Moderate) | ⏱️ ~25분

이유:

  • 논리 밀도: JSON 추출 로직(_extract_json_payload)은 중간 복잡도 — 백틱 처리, 슬라이싱, 엣지 케이스(빈 응답, 중괄호 없음) 검증 필요
  • 파일 범위: 2개 파일, 하나는 핵심 로직(v2_postscore.py), 하나는 테스트(test_tagging.py) — 관련성 높음
  • 변경 패턴: 기존 메서드 수정(동일 위치 3곳) + 새 함수 추가 + mock 응답 반복 업데이트 — 반복성 있어 검토 용이
  • 운영 임팩트: 높음(#18 issue로 stg/prod 실패율 164회/24h) — 검증 테스트 커버리지 중요

🔗 Possibly Related PRs

  • 2026-KUSITMS-GLIT/2026-KUSITMS-GLIT-AI#14: v2_postscore.py_parse_and_validate를 같은 함수에서 수정하는 다른 PR — 카테고리 narrowing 규칙 변경과 이번 JSON 추출 강화가 상호작용할 수 있음.

🏷️ Suggested Labels

test, priority:mid


👥 Suggested Reviewers

  • parkdo0918
  • hyhy-j

🎭 Poem

🔍 Haiku의 도입어, 중괄호로 이기고
큰 토큰 품에 안기고
추출기가 백틱 닦아낸다 ✨
{detailTags} 오롯한 JSON이
파싱 실패 100%에서 0%로 — 해결! 🚀

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.73% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목이 핵심 변경사항(assistant prefill + JSON 슬라이스 fallback으로 파싱 오류 해결)을 명확하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 충실히 따르며, 요약·변경사항·테스트·리스크·롤백·이슈 링크를 모두 포함하고 있습니다.
Linked Issues check ✅ Passed PR 코드 변경이 #18의 모든 핵심 목표(도입어 흡수, assistant prefill, {…} 슬라이스, max_tokens 상향, 모니터링 로그)를 충분히 충족합니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #18의 범위 내에 있으며, v2_postscore.py 파싱 파이프라인과 관련 테스트에만 집중되어 있습니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/#18-tagging-json-parse-prefix

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/test_tagging.py (1)

275-317: ⚡ Quick win

🧪 JSON 잘림(닫는 } 누락) 실패 케이스도 1개 추가하면 더 안전합니다.

현재 성공 케이스 커버는 충분히 좋습니다. 여기에 max_tokens 잘림처럼 불완전 JSON 입력이 들어왔을 때 _v2_parse_and_validateTaggingValidationError로 안정적으로 실패하는 경계 테스트를 하나 고정해두면 회귀 방지 효과가 큽니다.

예시 테스트
+def test_v2_postscore_parse_and_validate_rejects_truncated_payload() -> None:
+    raw = '다음과 같습니다: {"detailTags": ["`#원인분석`","`#검증및테스트`"'
+    with pytest.raises(TaggingValidationError, match="JSON 파싱 실패"):
+        _v2_parse_and_validate(raw)

As per coding guidelines "tests/**: 테스트 코드를 검토한다. - 해피 패스뿐 아니라 경계값·실패 케이스도 커버하는지 확인".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_tagging.py` around lines 275 - 317, Add a negative boundary test
that passes an incomplete/truncated JSON (e.g. missing the closing brace)
through _v2_parse_and_validate and assert it raises TaggingValidationError; this
verifies the parser fails reliably on max_tokens-like JSON truncation. Put the
new test alongside existing tests (near
test_v2_postscore_parse_and_validate_handles_intro_prefix) and use a raw string
that mirrors real responses (e.g. prefix like "다음과 같습니다: " + '{"detailTags":
["`#원인분석`","`#검증및테스트`"' ) so _v2_extract_json_payload is exercised before
_v2_parse_and_validate raises TaggingValidationError.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/test_tagging.py`:
- Around line 275-317: Add a negative boundary test that passes an
incomplete/truncated JSON (e.g. missing the closing brace) through
_v2_parse_and_validate and assert it raises TaggingValidationError; this
verifies the parser fails reliably on max_tokens-like JSON truncation. Put the
new test alongside existing tests (near
test_v2_postscore_parse_and_validate_handles_intro_prefix) and use a raw string
that mirrors real responses (e.g. prefix like "다음과 같습니다: " + '{"detailTags":
["`#원인분석`","`#검증및테스트`"' ) so _v2_extract_json_payload is exercised before
_v2_parse_and_validate raises TaggingValidationError.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a340da6-d6df-48b0-8476-b93fe73d2c0a

📥 Commits

Reviewing files that changed from the base of the PR and between 8062bea and faf0730.

📒 Files selected for processing (2)
  • app/services/tagging/v2_postscore.py
  • tests/test_tagging.py

@gyesswhat
gyesswhat merged commit 897ee96 into main May 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix 버그, 오류, 잘못된 동작 수정 priority:high 즉시 처리 필요, 지연 시 영향 큼 status:review 작업 완료, 리뷰 또는 머지 대기 중

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: tagging v2_postscore 응답 첫 글자가 { 아님 → JSON 파싱 100% 실패 (stg/prod 운영 영향)

1 participant