fix: 일정 생성 LLM 보정 병렬화 - #22
Conversation
OpenAI 보정은 일정 생성의 보조 단계라서 옵션별 호출을 순차 대기하면 클라이언트 타임아웃을 유발할 수 있다. 기본 일정 후보 생성은 기존 순서를 유지하고, LLM 보정만 병렬 실행해 응답 대기 시간을 단일 호출 상한에 가깝게 제한한다. Constraint: 일정 생성 HTTP 요청이 약 30초 부근에서 클라이언트에 의해 중단됨\nRejected: OpenAI timeout만 15초로 확대 | 순차 호출 구조에서는 다시 30초 이상 지연 가능\nConfidence: high\nScope-risk: moderate\nDirective: deterministic 후보 생성 순서는 중복 회피에 영향을 주므로 병렬화하지 말고 LLM 보정 단계만 병렬화할 것\nTested: ./gradlew test --tests 'com.tripsync.infrastructure.llm.OpenAiClientTest' --tests 'com.tripsync.application.consensus.ConsensusServiceTest' --tests 'com.tripsync.application.schedule.ScheduleServiceTest' --no-daemon --max-workers=1\nTested: docker compose up -d --build server; tripsync-server health=healthy\nNot-tested: 실제 OpenAI 네트워크 성공 응답의 운영 latency 분포
Heyaaz
left a comment
There was a problem hiding this comment.
Nogada PR Review
Verdict
❌ Blocking 이슈 1개 있습니다. 병합은 보류가 맞아 보입니다.
Blocking
- LLM 보정 결과 기준의 옵션 간 장소 중복 방지가 깨질 수 있습니다.
ConsensusService.buildScheduleOptions에서 balanced, individual은 prepareOption(...) 직후 deterministic slot 기준으로 rememberUsedPlacesByOrder(...)를 호출하고, 실제 LLM 보정은 이후 async { refinePreparedOption(...) }로 병렬 실행됩니다. 그래서 LLM이 balanced의 특정 order를 deterministic 선택지 A에서 shortlist 후보 B로 바꾸면, individual/discovery 준비 단계는 B를 이미 사용된 장소로 알 수 없습니다. 결과적으로 같은 order에서 여러 추천 옵션이 같은 장소 B를 고를 수 있습니다.
이전 순차 구조에서는 보정까지 끝난 최종 slot을 기준으로 다음 옵션의 avoid set을 만들었는데, 지금 구조는 병렬화를 위해 그 정보가 사라진 상태입니다. saveGeneratedOptions.ensureUniqueSlots(...)도 한 option 내부 중복만 보정하므로, 옵션 간 중복은 막지 못합니다.
권장 수정 방향:
- 병렬 LLM 보정 후 최종 slot들을 기준으로 옵션 간 중복을 한 번 더 조정하거나,
- 각 옵션의 LLM 후보 shortlist를 만들 때 이전 옵션의 전체 shortlist/후보까지 soft avoid로 넣거나,
- 병렬화 대상은 유지하되, 최종 결과 post-process에서 order별 option 간 중복을 검증/대체하는 테스트를 추가하는 쪽이 안전합니다.
검증
/tmp/tripsync_server_pr22_verifyfresh detached worktree에서./gradlew clean test --no-daemon --no-build-cache성공- 테스트 XML 기준: 64 tests / 0 failures / 0 errors / 0 skipped
git diff --check origin/main...HEAD통과
참고
- 테스트 종료 시 Testcontainers/Hikari/PostgreSQL shutdown warning이 출력됐지만, Gradle 결과와 XML 요약은 정상 성공이라 별도 blocking으로 보지 않았습니다.
[Heyaaz의 Nogada]
병렬 LLM 보정은 deterministic 준비 단계의 avoid set 이후에 최종 장소를 바꿀 수 있어 같은 순번의 추천 옵션이 같은 장소로 수렴할 수 있다. 병렬화는 유지하되 최종 보정 결과를 order별로 한 번 더 검사하고 대체 후보로 조정한다. Constraint: LLM 보정은 옵션별 병렬 실행되어 이전 옵션의 최종 LLM 선택을 다음 옵션 준비 단계에서 알 수 없음\nRejected: 다시 전체 옵션 생성을 순차화 | 타임아웃 개선 효과 상실\nConfidence: high\nScope-risk: moderate\nDirective: 옵션 간 중복 회피는 deterministic 준비 단계와 최종 LLM 후처리 양쪽에서 유지할 것\nTested: ./gradlew test --tests 'com.tripsync.infrastructure.llm.OpenAiClientTest' --tests 'com.tripsync.application.consensus.ConsensusServiceTest' --tests 'com.tripsync.application.schedule.ScheduleServiceTest' --no-daemon --max-workers=1\nTested: docker compose up -d --build server; tripsync-server health=healthy\nNot-tested: 실제 운영 OpenAI 응답 분포에서의 대체 빈도
Heyaaz
left a comment
There was a problem hiding this comment.
Nogada PR Review
Verdict
✅ 승인 가능 — 이전 blocking 이슈는 해결된 것으로 봤습니다.
확인한 내용
- LLM 보정 병렬화 후
resolveCrossOptionPlaceCollisions로 최종 보정된 슬롯 기준의 옵션 간 장소 중복을 다시 정리합니다. - 이전에 막았던 케이스(LLM이 여러 옵션에서 같은 orderIndex의 같은 후보 장소를 선택하는 경우)를 테스트가 직접 재현합니다.
- OpenAI timeout 설정과 병렬 fallback 동작도 테스트로 확인됩니다.
검증
git diff --check origin/main...HEAD통과./gradlew clean test --no-daemon --no-build-cache성공- 테스트 XML: 65 tests / 0 failures / 0 errors / 0 skipped
비고: 테스트 종료 시 Testcontainers/Hikari shutdown 경고가 있었지만, Gradle exit code와 XML 결과가 모두 정상이라 비차단으로 봤습니다.
[Heyaaz의 Nogada]
Summary
Changes
Verification
./gradlew test --tests 'com.tripsync.infrastructure.llm.OpenAiClientTest' --tests 'com.tripsync.application.consensus.ConsensusServiceTest' --tests 'com.tripsync.application.schedule.ScheduleServiceTest' --no-daemon --max-workers=1docker compose up -d --build servertripsync-server health=healthyNotes