AI 제안사항 Hallucination 문제 해결 및 추출 개선

주요 변경사항:
1. AI 서비스 설정
   - claude_max_tokens: 8192 → 25000으로 증가 (회의록 통합을 위한 충분한 토큰 확보)
   - AI 서비스 타임아웃: 30초 → 60초로 증가

2. 프롬프트 개선 (consolidate_prompt.py)
   - JSON 생성 전문가 역할 추가
   - JSON 이스케이프 규칙 명시 (큰따옴표, 줄바꿈, 역슬래시)
   - Markdown 볼드체(**) 제거하여 JSON 파싱 오류 방지
   - 문자열 검증 지시사항 추가

3. JSON 파싱 개선 (claude_service.py)
   - 4단계 재시도 전략 구현:
     * 이스케이프되지 않은 개행 문자 자동 수정
     * strict=False 옵션으로 파싱
     * 잘린 응답 복구 시도
     * 제어 문자 제거 후 재시도
   - 디버깅 로깅 강화 (Input/Output Tokens, Stop Reason)
   - 파싱 실패 시 전체 응답을 파일로 저장

4. 회의 종료 로직 개선 (EndMeetingService.java)
   - 통합 회의록 생성 또는 조회 로직 추가 (userId=NULL)
   - Minutes 테이블에 전체 결정사항 저장
   - AgendaSection에 minutesId 정확히 매핑

5. 테스트 데이터 추가
   - AI 회의록 요약 테스트용 SQL 스크립트 작성
   - 3명 참석자, 3개 안건의 현실적인 회의 시나리오

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Minseo-Jo
2025-10-31 11:09:22 +09:00
parent 7e88cdceee
commit b5159ef74e
7 changed files with 486 additions and 28 deletions
+108 -3
View File
@@ -43,7 +43,7 @@ class ClaudeService:
]
# API 호출
logger.info(f"Claude API 호출 시작 - Model: {self.model}")
logger.info(f"Claude API 호출 시작 - Model: {self.model}, Max Tokens: {self.max_tokens}")
if system_prompt:
response = self.client.messages.create(
@@ -63,7 +63,12 @@ class ClaudeService:
# 응답 텍스트 추출
response_text = response.content[0].text
logger.info(f"Claude API 응답 수신 완료 - Tokens: {response.usage.input_tokens + response.usage.output_tokens}")
logger.info(
f"Claude API 응답 수신 완료 - "
f"Input Tokens: {response.usage.input_tokens}, "
f"Output Tokens: {response.usage.output_tokens}, "
f"Stop Reason: {response.stop_reason}"
)
# JSON 파싱
# ```json ... ``` 블록 제거
@@ -72,13 +77,113 @@ class ClaudeService:
elif "```" in response_text:
response_text = response_text.split("```")[1].split("```")[0].strip()
# JSON 파싱 전 전처리: 제어 문자 및 문제 문자 정리
import re
# 탭 문자를 공백으로 변환
response_text = response_text.replace('\t', ' ')
# 연속된 공백을 하나로 축소 (JSON 문자열 내부는 제외)
# response_text = re.sub(r'\s+', ' ', response_text)
result = json.loads(response_text)
return result
except json.JSONDecodeError as e:
logger.error(f"JSON 파싱 실패: {e}")
logger.error(f"응답 텍스트: {response_text[:500]}...")
logger.error(f"응답 텍스트 전체 길이: {len(response_text)}")
logger.error(f"응답 텍스트 (처음 1000자): {response_text[:1000]}")
logger.error(f"응답 텍스트 (마지막 1000자): {response_text[-1000:]}")
# 전체 응답을 파일로 저장하여 디버깅
import os
from datetime import datetime
debug_dir = "/Users/jominseo/HGZero/ai-python/logs/debug"
os.makedirs(debug_dir, exist_ok=True)
debug_file = f"{debug_dir}/claude_response_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
with open(debug_file, 'w', encoding='utf-8') as f:
f.write(response_text)
logger.error(f"전체 응답을 파일로 저장: {debug_file}")
# JSON 파싱 재시도 전략
# 방법 1: 이스케이프되지 않은 개행 문자 처리
try:
import re
# JSON 문자열 내부의 이스케이프되지 않은 개행을 찾아 \\n으로 변환
# 패턴: 큰따옴표 내부에서 "\"로 시작하지 않는 개행
def fix_unescaped_newlines(text):
# 간단한 접근: 모든 실제 개행을 \\n으로 변환
# 단, JSON 구조의 개행 (객체/배열 사이)은 유지
in_string = False
escape_next = False
result = []
for char in text:
if escape_next:
result.append(char)
escape_next = False
continue
if char == '\\':
escape_next = True
result.append(char)
continue
if char == '"':
in_string = not in_string
result.append(char)
continue
if char == '\n':
if in_string:
# 문자열 내부의 개행은 \\n으로 변환
result.append('\\n')
else:
# JSON 구조의 개행은 유지
result.append(char)
else:
result.append(char)
return ''.join(result)
fixed_text = fix_unescaped_newlines(response_text)
result = json.loads(fixed_text)
logger.info("JSON 파싱 재시도 성공 (개행 문자 수정)")
return result
except Exception as e1:
logger.warning(f"개행 문자 수정 실패: {e1}")
# 방법 2: strict=False 옵션으로 파싱
try:
result = json.loads(response_text, strict=False)
logger.info("JSON 파싱 재시도 성공 (strict=False)")
return result
except Exception as e2:
logger.warning(f"strict=False 파싱 실패: {e2}")
# 방법 3: 마지막 닫는 괄호까지만 파싱 시도
try:
last_brace_idx = response_text.rfind('}')
if last_brace_idx > 0:
truncated_text = response_text[:last_brace_idx+1]
# 개행 수정도 적용
truncated_text = fix_unescaped_newlines(truncated_text)
result = json.loads(truncated_text, strict=False)
logger.info("JSON 파싱 재시도 성공 (잘린 부분 복구)")
return result
except Exception as e3:
logger.warning(f"잘린 부분 복구 실패: {e3}")
# 방법 4: 정규식으로 문제 문자 제거 후 재시도
try:
# 제어 문자 제거 (줄바꿈, 탭 제외)
cleaned = re.sub(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]', '', response_text)
result = json.loads(cleaned, strict=False)
logger.info("JSON 파싱 재시도 성공 (제어 문자 제거)")
return result
except Exception as e4:
logger.warning(f"제어 문자 제거 실패: {e4}")
raise ValueError(f"Claude API 응답을 JSON으로 파싱할 수 없습니다: {str(e)}")
except Exception as e: