# Smilegate 게임 대상 계약 통합 설계 ## 1. 배경 현재 `game_query_plan`은 게임을 찾지 못하면 `STOP_NO_MATCH`, 여러 게임이면 `FAN_OUT`을 반환한다. Portal의 ReAct 모델이 이 지시를 종료 조건으로 해석하거나 일부 게임만 다음 도구로 전달하면서 다음 문제가 발생했다. - 게임명이 없는 전체 매출 질의가 SQL 생성 전에 중단된다. - 여러 게임 중 일부만 매칭되면 매칭된 게임의 조회도 생략될 수 있다. - 계획 결과를 전달해도 NL2SQL worker가 질문 전체를 다시 단일 게임 벡터검색하여 범위를 축소할 수 있다. - 게임별 사용자 마스터 물리 객체를 모델이 추측해야 한다. ## 2. 목표 질문의 게임 대상을 `NONE`, `SINGLE`, `MULTI`, `ALL` 네 가지로 통일하고, 모든 유형에서 원 질문과 전체 계획 결과를 `smilegate_fewshot_nl2sql`에 한 번 전달한다. 계획 단계는 조회 중단이나 SQL 조립을 결정하지 않는다. ## 3. 계약 `game_query_plan`은 ReAct가 판단에 사용할 고정 JSON 계약을 반환한다. 자연어 설명, 후보 검색 원문, 기존 호환 필드는 진단 영역으로 분리하며 실행 판단은 아래 권위 필드만 사용한다. ```json { "contractVersion": "1.0", "scopeType": "MULTI", "targets": [ { "mention": "사용자가 언급한 게임명", "matchStatus": "MATCHED", "dataStatus": "AVAILABLE", "gameKey": "카탈로그 키 또는 null", "gameId": "카탈로그 식별자 또는 null", "userMasterObjectName": "승인 객체 또는 null" } ], "dataEligibleTargets": [], "unresolvedTargets": [] } ``` 고정 enum은 다음과 같다. - `scopeType`: `NONE`, `SINGLE`, `MULTI`, `ALL` - `matchStatus`: `NOT_APPLICABLE`, `MATCHED`, `UNMATCHED` - `dataStatus`: `NOT_APPLICABLE`, `AVAILABLE`, `UNAVAILABLE` `scopeType`은 질문에 언급된 범위만 나타낸다. 실행 가능성이나 SQL 조립 방식을 암시하지 않는다. `targets`는 언급 순서를 유지하고, `dataEligibleTargets`는 현재 질의에 사용할 수 있는 카탈로그 대상만, `unresolvedTargets`는 미매칭 또는 데이터 객체가 없는 대상을 보존한다. ReAct는 이 계약 전체를 관찰한 뒤 공통 팩트 단일 호출, 대상별 호출, 또는 빈 결과 응답을 선택한다. 기존 API 호환 기간에는 아래 필드도 반환할 수 있지만 ReAct의 실행 판단에 사용하지 않는다: `targetType`, `status`, `matchedGames`, `supportedGames`, `mentionResults`, `dataIneligibleTargets`, `unmatchedGames`, `nextAction`, `executionMode`. 기존 필드의 의미는 다음과 같다. | 필드 | 내용 | |---|---| | `targetType` | 호환용 `scopeType` 별칭 | | `status` | 호환용 요약 상태. ReAct 실행 분기에 사용하지 않음 | | `targets` | 언급 순서를 보존한 게임 대상 배열 | | `nextAction` | 호환용 안내. 실행을 강제하지 않음 | | `executionMode` | 호환용 안내. 실행을 강제하지 않음 | 각 `targets` 항목은 다음 값을 가진다. | 필드 | 내용 | |---|---| | `mention` | 질문에서 추출한 게임명. `NONE`은 `null` | | `status` | 호환용 대상 상태 | | `matchStatus` | `NOT_APPLICABLE`, `MATCHED`, `UNMATCHED` | | `dataStatus` | `NOT_APPLICABLE`, `AVAILABLE`, `UNAVAILABLE` | | `gameKey`, `gameId` | DB 카탈로그가 반환한 게임 식별자 | | `gamePrefix` | DB 카탈로그가 반환한 prefix | | `gameName`, `aliases` | 표시명과 별칭 | | `userMasterObjectName` | 현재 Select AI 승인 객체 중 해당 게임의 사용자 마스터 물리 객체. 없으면 `null` | | `cosineDistance` | 후보 검색 거리. 적용되지 않으면 `null` | | `reasonCode`, `reason` | 매칭 또는 미매칭 근거 | 유형별 처리 방식은 다음과 같다. - `NONE`: 게임 대상 항목 하나를 모든 게임 필드가 `null`인 상태로 전달한다. 공통 거래·기준 객체 질의는 계속하되 prefix 전용 객체를 임의 선택하지 않는다. - `SINGLE`: 질문에서 언급된 대상 한 건을 전달한다. 미매칭이면 게임 필드가 `null`인 대상도 그대로 다음 도구에 전달한다. - `MULTI`: 언급된 대상 모두를 전달한다. 매칭·미매칭을 함께 보존한다. - `ALL`: 게임 카탈로그의 활성 게임을 모두 반환한다. 사용자 마스터 물리 객체가 없는 게임도 `null`로 보존한다. ## 4. DB 설계 `SG_GAME_CATALOG`에 `USER_MASTER_OBJECT_NAME`을 추가한다. - 값은 `COMN_GAME_ALIAS_BAS`/운영 게임 registry의 `GAME_PREFIX`와 현재 `USER_CLOUD_AI_PROFILE_ATTRIBUTES.object_list`, `USER_OBJECTS`를 조합해 계산한다. - 공통 정책이나 애플리케이션 코드에는 특정 게임명, prefix, 테이블명을 넣지 않는다. - 운영 registry에만 존재하는 게임도 카탈로그에 동기화하고 임베딩을 생성한다. - 게임별 물리 객체가 없으면 컬럼은 `null`이다. `SG_GAME_CATALOG_SEARCH`는 물리 객체명을 후보 결과에 포함한다. `SG_GAME_QUERY_PLAN`은 추출된 mention마다 후보 선택 결과를 `targets`로 만들며, `ALL`은 벡터 선택 없이 전체 카탈로그를 사용한다. ## 5. MCP와 NL2SQL 설계 `game_query_plan` 도구 설명은 “항상 먼저 호출하고 결과를 원 질문과 함께 다음 도구에 전달”하도록 바꾼다. `smilegate_fewshot_nl2sql`의 입력은 다음과 같다. - `prompt`: 사용자가 입력한 원 질문 - `queryPlan`: 바로 앞 `game_query_plan`의 전체 JSON - `scopeGameKey`: 하위 호환용 선택 값. 새 흐름에서는 사용하지 않는다. NL2SQL worker는 전달된 `queryPlan`을 권위 있는 게임 범위로 사용한다. 계획이 있으면 질문 전체를 다시 게임 카탈로그 벡터검색하지 않는다. - 공통 객체에 게임 식별 컬럼이 있으면 질문 의미에 따라 `IN`과 `GROUP BY`를 사용할 수 있다. - 대상별 물리 객체가 다르면 제공된 객체만 사용해 `UNION ALL`할 수 있다. - 물리 객체가 `null`인 대상을 임의 객체로 대체하지 않는다. - `NONE`에서 생성 SQL이 prefix 전용 객체를 참조하면 실행 전에 차단한다. - 최종 SQL은 기존과 같이 단일 `SELECT`/`WITH`, read-only transaction, 최대 행수와 timeout 제한을 적용한다. ## 6. Portal ReAct 설계 Portal은 도구 호출 순서를 Java/Python 조건문으로 강제하지 않는다. 외부 환경변수의 system prompt와 MCP 도구 설명으로 다음 절차를 안내한다. 1. `game_query_plan`을 호출한다. 2. `targetType`과 상관없이 원 질문, 전체 `queryPlan`으로 `smilegate_fewshot_nl2sql`을 한 번 호출한다. 3. SQL, 실행 결과, 매칭·미매칭 대상을 함께 설명한다. ## 7. 검증 운영 MCP에서 다음 네 질문군을 실제 호출한다. | 유형 | 검증 내용 | |---|---| | `NONE` | 게임명이 없는 전체 매출 질의가 공통 테이블로 실행되고 게임 필터가 임의 추가되지 않는다. | | `SINGLE` | 한 게임의 AU 질의가 해당 게임 식별자/물리 객체로 실행된다. | | `MULTI` | 지원·미지원 게임을 함께 질문해 지원 결과와 미지원 상태가 한 답변에 보존된다. | | `ALL` | 전체 게임 질의가 전체 카탈로그 대상을 받아 공통 객체 그룹 또는 게임별 객체 결합 SQL로 실행된다. | 추가 검증: - Maven 테스트 - Portal Python 테스트 - MCP `tools/list` schema/description 확인 - 생성 SQL에서 `NONE`의 prefix 전용 객체 차단 확인 - 운영 서비스 health와 Portal ReAct 실제 응답 확인 ## 8. Few-shot 참조 거버넌스 Few-shot 원문 SQL은 실행 이력 그대로 검색하지 않는다. 특히 특정 게임의 물리 객체명이 포함된 예제는 다른 게임 또는 게임 미지정 질의의 벡터 후보가 될 수 있으므로, 예제는 검증·정규화·승인 단계를 거쳐야 한다. `SG_QA_VECTOR_EXAMPLE`에는 다음 참조 메타데이터를 둔다. | 필드 | 내용 | |---|---| | `REFERENCE_STATUS` | `DRAFT`, `APPROVED`, `RETIRED`. 검색은 `APPROVED`만 사용한다. | | `REFERENCE_KIND` | `SQL_TEMPLATE`, `NO_TARGET`, `OBJECT_UNAVAILABLE`, `METADATA_POLICY` | | `TARGET_TYPE` | `NONE`, `SINGLE`, `MULTI`, `ALL`, `ANY`. 현재 game query plan과 일치하는 예제만 벡터 순위에 포함한다. | | `OBJECT_ROLE` | 물리 테이블명이 아닌 `GAME_USER_MASTER` 등 논리 객체 역할 | | `INSPECTION_STATUS`, `INSPECTION_NOTE` | 전수 실행·정책 검토 결과와 사유 | | `VERIFIED_AT`, `VERIFIED_BY` | 승인 감사 이력 | 물리 테이블명은 Few-shot의 의미 규칙으로 저장하지 않는다. `SQL_TEMPLATE`은 `` 같은 논리 placeholder를 사용하며, 실제 객체는 오직 현재 `queryPlan.targets[*].userMasterObjectName`에서만 해석한다. `NO_TARGET`과 `OBJECT_UNAVAILABLE` 예제는 범위 경계 안내와 검증된 빈 결과 SQL 템플릿을 함께 프롬프트에 포함한다. 이 템플릿은 애플리케이션에서 직접 실행하거나 답변으로 반환하지 않으며, Select AI가 현재 질문의 범위를 해석해 SQL을 생성할 때만 Few-shot 근거로 사용한다. `NONE`은 게임이 선택되지 않았다는 범위 정보이며 질의 불가 상태가 아니다. 각 경계 예제는 `OBJECT_ROLE`로 적용 대상을 밝힌다. 예를 들어 게임별 사용자 마스터 경계는 공통 판매·환불 같은 공통 객체 질의에 적용하지 않는다. 공통 객체로 답할 수 있는 질문은 게임 중립 SQL로 계속 실행하고, 게임별 객체가 본질적으로 필요한 질문만 빈 결과 또는 게임 식별 요청으로 처리한다. `UNMATCHED`와 `OBJECT_STATUS=UNAVAILABLE`는 `NONE`과 별도 계약이다. 요청한 게임 전용 논리 객체를 현재 카탈로그에서 확인할 수 없다는 뜻이므로 다른 게임의 객체로 대체하지 않는다. 해당 객체가 반드시 필요한 연산이면 실행 가능한 읽기 전용 0행 결과로 종료한다. 이때 `NULL`을 담은 합성 1행은 빈 결과로 취급하지 않는다. 이 규칙은 특정 게임·prefix·물리 객체에 의존하지 않으며, Java 분기가 아니라 Select AI 프로파일의 `additional_instructions`로 관리한다. 애플리케이션은 계획 전달과 read-only·객체 범위 검증만 담당한다. 공통 거래처럼 게임 식별 컬럼이 있는 객체는 게임 식별자를 SQL 리터럴로 직접 넣지 않는다. 활성 게임 별칭 카탈로그를 통해 허용 식별자를 구하는 서브쿼리 또는 `EXISTS` 조건을 사용하며, 카탈로그에는 있으나 해당 공통 객체의 데이터가 없는 경우는 다른 게임으로 대체하지 않고 빈 결과 또는 집계 `NULL`을 그대로 보존한다. 기존 예제는 삭제하지 않는다. 실행 가능성, 게임 범위 일치, 물리 객체 의존성을 전수 검사해 `RETIRED`로 분리하고, 검증된 정규화 예제만 `APPROVED`로 전환한다. ### 고객 질답 47건 후보화·승인 규칙 `SG_AI_QA_QUESTION`의 고객 Excel 47건은 `SOURCE_TYPE=CUSTOMER_QA_BENCHMARK`, `SOURCE_CASE_ID`로 벡터 예제에 한 번씩 적재한다. 초기 상태는 모두 `DRAFT`이고, 질문·기대 포인트·기준 SQL을 embedding 문서로 사용한다. 이 상태의 원문은 운영 검색 결과에 절대 포함되지 않는다. `SG_QA_VECTOR_APPROVE_VERIFIED_BENCHMARK`는 최신 Portal ReAct 실행이 다음을 모두 만족한 후보만 자동으로 `APPROVED`로 전환한다. 1. 고객 지원 범위가 `SUPPORTED`일 것 2. 최신 실행이 `COMPLETED`이며 고객 기준 판정이 `PASS`일 것 3. 실행 가능한 읽기 전용 SQL이 있을 것 승인 시 최신 실행 SQL과 응답으로 embedding을 재생성하고, 실행 이력의 query plan에서 확인된 `TARGET_TYPE`을 함께 보관한다. `WARN`·`FAIL`·미지원·SQL 미생성 후보는 `DRAFT / REVIEW`로 남긴다. 따라서 개선 전 SQL이 Few-shot 근거로 노출되지 않으며, 각 검색 결과는 `SOURCE_CASE_ID`로 고객 기준 항목까지 추적할 수 있다. ## 9. 변경 이력 - Redmine: `#739` - Backoffice branch: `smilegate` - Portal branch: `main` - 2026-07-28: `STD-03`의 `NO_TARGET` 예제에 검증된 빈 결과 SQL 템플릿을 Few-shot 근거로 노출했다. 운영 Portal ReAct에서 `game_query_plan → fewshot_nl2sql` 경로로 생성·실행한 SQL은 `SELECT CAST(NULL AS NUMBER) ... FROM DUAL WHERE 1 = 0`이었고, 답변 이력 `422`는 `PASS`로 기록됐다. 직접 정답 실행 분기는 두지 않는다. - 2026-07-28: `NONE`을 질의 차단으로 해석하지 않도록 경계 예제에 논리 객체 역할을 부여하고, 공통 객체 질의에는 경계 SQL을 일반화하지 않는 프롬프트 규칙을 추가했다. - 2026-07-29: ReAct가 자유 설명문이나 중복 요약 필드를 해석하지 않도록, 게임 범위 계획의 권위 JSON 계약을 `contractVersion`, `scopeType`, `targets[*].matchStatus`, `targets[*].dataStatus`, `dataEligibleTargets`, `unresolvedTargets`로 정리했다. 이 계약은 게임명·prefix·물리 객체를 애플리케이션에 고정하지 않으며, 다음 도구 호출 횟수와 방식은 ReAct가 현재 질문과 계획 결과로 판단한다.