diff --git a/database/adb/85_hmm_hr_select_ai_no_annotation_profile.sql b/database/adb/85_hmm_hr_select_ai_no_annotation_profile.sql new file mode 100644 index 0000000..f9d570c --- /dev/null +++ b/database/adb/85_hmm_hr_select_ai_no_annotation_profile.sql @@ -0,0 +1,159 @@ +-- Redmine #749 +-- Create an isolated HMM HR Select AI profile with annotations disabled. +-- The source profile is never modified. + +SET SERVEROUTPUT ON SIZE UNLIMITED +WHENEVER SQLERROR EXIT SQL.SQLCODE + +DECLARE + c_source_profile CONSTANT VARCHAR2(128) := 'HMM_HR_DATA_GPT54_PROFILE'; + c_target_profile CONSTANT VARCHAR2(128) := 'HMM_HR_DATA_GPT54_NOANN_PROFILE'; + + l_attributes CLOB; + l_object_list CLOB; + l_provider VARCHAR2(4000); + l_credential_name VARCHAR2(4000); + l_model VARCHAR2(4000); + l_region VARCHAR2(4000); + l_compartment_id VARCHAR2(4000); + l_max_tokens NUMBER; + l_temperature NUMBER; + l_profile_count PLS_INTEGER; + l_difference_count PLS_INTEGER; + l_annotations VARCHAR2(30); + + FUNCTION attribute_clob(p_name IN VARCHAR2) RETURN CLOB IS + l_value CLOB; + BEGIN + SELECT attribute_value + INTO l_value + FROM user_cloud_ai_profile_attributes + WHERE profile_name = c_source_profile + AND attribute_name = p_name; + RETURN l_value; + END attribute_clob; + + FUNCTION attribute_text(p_name IN VARCHAR2) RETURN VARCHAR2 IS + BEGIN + RETURN DBMS_LOB.SUBSTR(attribute_clob(p_name), 4000, 1); + END attribute_text; +BEGIN + SELECT COUNT(*) + INTO l_profile_count + FROM user_cloud_ai_profiles + WHERE profile_name = c_source_profile + AND status = 'ENABLED'; + + IF l_profile_count <> 1 THEN + RAISE_APPLICATION_ERROR(-20080, 'Enabled source Select AI profile was not found.'); + END IF; + + l_provider := attribute_text('provider'); + l_credential_name := attribute_text('credential_name'); + l_model := attribute_text('model'); + l_region := attribute_text('region'); + l_compartment_id := attribute_text('oci_compartment_id'); + l_object_list := attribute_clob('object_list'); + l_max_tokens := TO_NUMBER( + attribute_text('max_tokens'), + '9999999999', + 'NLS_NUMERIC_CHARACTERS=''.,''' + ); + l_temperature := TO_NUMBER( + attribute_text('temperature'), + '9999999990D999999999', + 'NLS_NUMERIC_CHARACTERS=''.,''' + ); + + SELECT COUNT(*) + INTO l_profile_count + FROM user_cloud_ai_profiles + WHERE profile_name = c_target_profile; + + IF l_profile_count = 0 THEN + SELECT JSON_OBJECT( + 'provider' VALUE l_provider, + 'credential_name' VALUE l_credential_name, + 'model' VALUE l_model, + 'region' VALUE l_region, + 'oci_compartment_id' VALUE l_compartment_id, + 'object_list' VALUE l_object_list FORMAT JSON, + 'max_tokens' VALUE l_max_tokens, + 'temperature' VALUE l_temperature, + 'annotations' VALUE 'false' FORMAT JSON + RETURNING CLOB + ) + INTO l_attributes + FROM dual; + + DBMS_CLOUD_AI.CREATE_PROFILE( + profile_name => c_target_profile, + attributes => l_attributes, + status => 'enabled', + description => 'HMM HR GPT-5.4 mini comparison profile with annotations disabled' + ); + DBMS_OUTPUT.PUT_LINE('PROFILE_CREATED|' || c_target_profile); + ELSE + DBMS_OUTPUT.PUT_LINE('PROFILE_EXISTS|' || c_target_profile); + END IF; + + SELECT COUNT(*) + INTO l_difference_count + FROM ( + SELECT attribute_name, attribute_value + FROM user_cloud_ai_profile_attributes + WHERE profile_name = c_source_profile + AND attribute_name <> 'annotations' + ) source_attributes + FULL OUTER JOIN ( + SELECT attribute_name, attribute_value + FROM user_cloud_ai_profile_attributes + WHERE profile_name = c_target_profile + AND attribute_name <> 'annotations' + ) target_attributes + ON target_attributes.attribute_name = source_attributes.attribute_name + WHERE source_attributes.attribute_name IS NULL + OR target_attributes.attribute_name IS NULL + OR DBMS_LOB.COMPARE( + source_attributes.attribute_value, + target_attributes.attribute_value + ) <> 0; + + SELECT LOWER(TRIM(DBMS_LOB.SUBSTR(attribute_value, 30, 1))) + INTO l_annotations + FROM user_cloud_ai_profile_attributes + WHERE profile_name = c_target_profile + AND attribute_name = 'annotations'; + + IF l_difference_count <> 0 OR l_annotations <> 'false' THEN + RAISE_APPLICATION_ERROR( + -20081, + 'Comparison profile differs from source beyond annotations=false.' + ); + END IF; + + DBMS_OUTPUT.PUT_LINE( + 'PROFILE_VALIDATED|source=' || c_source_profile + || '|target=' || c_target_profile + || '|annotations=false|other_attribute_differences=0' + ); +END; +/ + +SELECT profile_name, status, description + FROM user_cloud_ai_profiles + WHERE profile_name IN ( + 'HMM_HR_DATA_GPT54_PROFILE', + 'HMM_HR_DATA_GPT54_NOANN_PROFILE' + ) + ORDER BY profile_name; + +SELECT profile_name, attribute_name, attribute_value + FROM user_cloud_ai_profile_attributes + WHERE profile_name IN ( + 'HMM_HR_DATA_GPT54_PROFILE', + 'HMM_HR_DATA_GPT54_NOANN_PROFILE' + ) + ORDER BY profile_name, attribute_name; + +EXIT SUCCESS diff --git a/database/adb/86_hmm_select_ai_annotation_benchmark.sql b/database/adb/86_hmm_select_ai_annotation_benchmark.sql new file mode 100644 index 0000000..1bfa302 --- /dev/null +++ b/database/adb/86_hmm_select_ai_annotation_benchmark.sql @@ -0,0 +1,103 @@ +-- Redmine #749 +-- Compare Select AI prompt and SQL generation with annotations enabled/disabled. +-- The Korean prompt is reconstructed from UTF-8 Base64 inside Oracle. + +SET SERVEROUTPUT ON SIZE UNLIMITED +SET FEEDBACK OFF +SET VERIFY OFF +WHENEVER SQLERROR EXIT SQL.SQLCODE + +DECLARE + c_annotation_profile CONSTANT VARCHAR2(128) := 'HMM_HR_DATA_GPT54_PROFILE'; + c_no_annotation_profile CONSTANT VARCHAR2(128) := 'HMM_HR_DATA_GPT54_NOANN_PROFILE'; + c_prompt_base64 CONSTANT VARCHAR2(4000) := + '7J2067KIIOuLrCDtjIDsm5Drs4Qg7Zy06rCAIOyCrOyaqSDtmITtmansnYQg67O07Jes7KSY'; + + l_prompt VARCHAR2(4000) := + UTL_I18N.RAW_TO_CHAR( + UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(c_prompt_base64)), + 'AL32UTF8' + ); + + PROCEDURE run_one( + p_run_no IN PLS_INTEGER, + p_action IN VARCHAR2, + p_profile_name IN VARCHAR2 + ) IS + l_started PLS_INTEGER; + l_elapsed_ms PLS_INTEGER; + l_result CLOB; + l_result_head VARCHAR2(32767); + l_result_hash VARCHAR2(128); + l_valid_sql VARCHAR2(1) := '-'; + BEGIN + l_started := DBMS_UTILITY.GET_TIME; + l_result := DBMS_CLOUD_AI.GENERATE( + l_prompt, + p_profile_name, + p_action + ); + l_elapsed_ms := (DBMS_UTILITY.GET_TIME - l_started) * 10; + l_result_head := DBMS_LOB.SUBSTR(l_result, 32767, 1); + SELECT RAWTOHEX(STANDARD_HASH(l_result_head, 'SHA256')) + INTO l_result_hash + FROM dual; + + IF p_action = 'showsql' THEN + IF REGEXP_LIKE(LTRIM(l_result_head), '^(SELECT|WITH)[[:space:]]', 'i') THEN + l_valid_sql := 'Y'; + ELSE + l_valid_sql := 'N'; + END IF; + END IF; + + DBMS_OUTPUT.PUT_LINE( + 'BENCHMARK|run=' || p_run_no + || '|action=' || p_action + || '|profile=' || p_profile_name + || '|elapsed_ms=' || l_elapsed_ms + || '|result_chars=' || DBMS_LOB.GETLENGTH(l_result) + || '|valid_sql=' || l_valid_sql + || '|result_sha256=' || l_result_hash + ); + EXCEPTION + WHEN OTHERS THEN + l_elapsed_ms := (DBMS_UTILITY.GET_TIME - l_started) * 10; + DBMS_OUTPUT.PUT_LINE( + 'BENCHMARK_ERROR|run=' || p_run_no + || '|action=' || p_action + || '|profile=' || p_profile_name + || '|elapsed_ms=' || l_elapsed_ms + || '|error=' || REPLACE(SUBSTR(SQLERRM, 1, 500), '|', '/') + ); + END run_one; + + PROCEDURE run_pair(p_run_no IN PLS_INTEGER, p_action IN VARCHAR2) IS + BEGIN + IF MOD(p_run_no, 2) = 1 THEN + run_one(p_run_no, p_action, c_annotation_profile); + run_one(p_run_no, p_action, c_no_annotation_profile); + ELSE + run_one(p_run_no, p_action, c_no_annotation_profile); + run_one(p_run_no, p_action, c_annotation_profile); + END IF; + END run_pair; +BEGIN + DBMS_OUTPUT.PUT_LINE( + 'BENCHMARK_START|prompt_utf8_bytes=' || + UTL_RAW.LENGTH(UTL_I18N.STRING_TO_RAW(l_prompt, 'AL32UTF8')) + ); + + FOR run_no IN 1 .. 3 LOOP + run_pair(run_no, 'showprompt'); + END LOOP; + + FOR run_no IN 1 .. 3 LOOP + run_pair(run_no, 'showsql'); + END LOOP; + + DBMS_OUTPUT.PUT_LINE('BENCHMARK_END'); +END; +/ + +EXIT SUCCESS diff --git a/docs/design/749-hmm-select-ai-no-annotation-profile/README.md b/docs/design/749-hmm-select-ai-no-annotation-profile/README.md new file mode 100644 index 0000000..1134428 --- /dev/null +++ b/docs/design/749-hmm-select-ai-no-annotation-profile/README.md @@ -0,0 +1,55 @@ +# HMM Select AI annotation 제외 프로파일 비교 + +Redmine: #749 + +상태: Approved + +구현 대상: `database/adb/85_hmm_hr_select_ai_no_annotation_profile.sql`, +`database/adb/86_hmm_select_ai_annotation_benchmark.sql` + +## 목적 + +HMM HR Select AI에서 Oracle table/column annotation을 프롬프트에 포함할 때와 제외할 때의 응답 +시간과 SQL 생성 결과를 같은 조건으로 비교한다. 운영 프로파일과 MCP 설정은 변경하지 않는다. + +## 범위와 결정사항 + +- 기준 프로파일은 `HMM_HR_DATA_GPT54_PROFILE`이며 현재 `annotations=true`다. +- 비교 프로파일은 `HMM_HR_DATA_GPT54_NOANN_PROFILE`로 만들고 `annotations=false`를 명시한다. +- provider, credential, model, region, compartment, object list, temperature, max tokens는 동일하게 유지한다. +- 같은 한국어 질문으로 `showprompt` 크기·시간과 `showsql` 생성 시간·성공 여부를 교차 반복 측정한다. +- 한국어 질문은 SQLcl에 직접 넣지 않고 UTF-8 Base64를 Oracle 안에서 복원한다. +- 측정 결과만으로 운영 프로파일을 자동 전환하지 않는다. + +## 전체 구성 + +```text +동일 HR 질문 + ├─ HMM_HR_DATA_GPT54_PROFILE ─ annotations=true ─ showprompt/showsql + └─ HMM_HR_DATA_GPT54_NOANN_PROFILE ─ annotations=false ─ showprompt/showsql + │ + 시간·prompt 크기·SQL 비교 +``` + +두 경로의 유일한 의도적 차이는 `annotations`다. 기존 선사 Federation 프로파일은 이미 annotation을 +사용하지 않으므로 이번 A/B 대상이 아니다. + +## 성공 기준 + +- 두 프로파일의 비교 대상 속성이 `annotations` 외에는 같다. +- 각 프로파일이 같은 질문에 유효한 읽기 전용 SQL을 생성한다. +- 측정 회차별 시간, 성공 여부, 생성 SQL 길이를 남긴다. +- 기존 프로파일과 운영 `BACKOFFICE_SELECT_AI_PROFILE` 값이 바뀌지 않는다. + +## 문서 지도 + +- [비교 설계](architecture.md) +- [생성·측정·롤백 절차](cookbook.md) +- [측정 결과](results.md) +- [문제 해결](troubleshooting.md) + +## 현재 상태 + +2026-08-11 비교 프로파일 생성과 3회 교차 측정을 완료했다. 현재 HR object list에는 실제 +annotation이 0건이어서 prompt 크기는 34자만 줄었고, `showsql` 평균은 annotation 사용 2.453초, +제외 2.483초로 의미 있는 개선이 없었다. 운영 프로파일은 변경하지 않았다. diff --git a/docs/design/749-hmm-select-ai-no-annotation-profile/architecture.md b/docs/design/749-hmm-select-ai-no-annotation-profile/architecture.md new file mode 100644 index 0000000..cbb59d3 --- /dev/null +++ b/docs/design/749-hmm-select-ai-no-annotation-profile/architecture.md @@ -0,0 +1,49 @@ +# 비교 설계 + +[개요](README.md) · [실행 절차](cookbook.md) · [측정 결과](results.md) · [문제 해결](troubleshooting.md) + +## 비교 대상 + +| 속성 | annotation 사용 | annotation 제외 | +|---|---|---| +| 프로파일 | `HMM_HR_DATA_GPT54_PROFILE` | `HMM_HR_DATA_GPT54_NOANN_PROFILE` | +| provider | `oci` | 동일 | +| model | `openai.gpt-5.4-mini` | 동일 | +| object list | HMM HR 5개 객체 | 동일 | +| temperature | `0.1` | 동일 | +| max tokens | `1500` | 동일 | +| annotations | `true` | `false` | + +credential 이름과 compartment OCID는 비밀값은 아니지만 환경 종속값이므로 문서에 복사하지 않고 +DB 원본 프로파일을 기준으로 검증한다. credential 원문이나 토큰은 조회·출력하지 않는다. + +## 측정 방식 + +```text +run 1: annotation → no annotation +run 2: no annotation → annotation +run 3: annotation → no annotation +``` + +호출 순서를 번갈아 배치해 첫 호출 캐시와 일시적 모델 지연이 한쪽에만 몰리는 현상을 줄인다. +각 호출은 `DBMS_CLOUD_AI.GENERATE(prompt, profile, action)` 한 번의 경과 시간을 +`DBMS_UTILITY.GET_TIME`으로 측정한다. + +- `showprompt`: 모델에 전달되는 metadata prompt 크기와 구성 비용 비교 +- `showsql`: 실제 SQL 생성 시간과 성공 여부 비교 + +생성 SQL은 실행하지 않는다. SQL 생성 품질은 `SELECT`/`WITH` 시작 여부, 길이와 주요 객체 사용을 +검토한다. VPD 결과 행 비교는 운영 프로파일 전환을 결정한 뒤 별도 MCP 회귀 검증에서 수행한다. + +## 안전 경계 + +- 기존 프로파일에는 `SET_ATTRIBUTE`, disable, drop을 실행하지 않는다. +- 비교 프로파일이 이미 있으면 덮어쓰지 않고 정의를 검증한다. +- 벤치마크는 DDL/DML을 생성하거나 실행하지 않고 `showprompt`와 `showsql`만 호출한다. +- 새 프로파일은 운영 MCP 설정에 자동 연결하지 않는다. + +Oracle 공식 문서에 따르면 `annotations=true`는 table/column annotation을 LLM metadata에 포함하며, +프로파일 속성은 `DBA_CLOUD_AI_PROFILE_ATTRIBUTES`에서 확인할 수 있다. + +- [Oracle DBMS_CLOUD_AI package](https://docs.oracle.com/en/database/oracle/oracle-database/26/arpls/dbms_cloud_ai1.html) +- [Oracle DBMS_CLOUD_AI views](https://docs.oracle.com/en-us/iaas/autonomous-database-serverless/doc/dbms-cloud-ai-views.html) diff --git a/docs/design/749-hmm-select-ai-no-annotation-profile/cookbook.md b/docs/design/749-hmm-select-ai-no-annotation-profile/cookbook.md new file mode 100644 index 0000000..b95b426 --- /dev/null +++ b/docs/design/749-hmm-select-ai-no-annotation-profile/cookbook.md @@ -0,0 +1,65 @@ +# 생성·측정·롤백 절차 + +[개요](README.md) · [비교 설계](architecture.md) · [측정 결과](results.md) · [문제 해결](troubleshooting.md) + +## 1. 준비 + +- ADMIN으로 HMM ADB에 연결한다. +- `HMM_HR_DATA_GPT54_PROFILE`이 `ENABLED`이고 `annotations=true`인지 확인한다. +- 운영 `BACKOFFICE_SELECT_AI_PROFILE` 값을 기록하되 credential과 비밀번호는 출력하지 않는다. + +성공 판정: 원본 프로파일과 전체 속성 목록을 읽을 수 있다. + +실패 시: [원본 프로파일이 없거나 소유자가 다름](troubleshooting.md#원본-프로파일이-없거나-소유자가-다름) + +## 2. 비교 프로파일 생성 + +```text +database/adb/85_hmm_hr_select_ai_no_annotation_profile.sql +``` + +스크립트는 비교 프로파일이 없을 때만 생성한다. 생성 후 원본과 비교 프로파일의 속성을 조회해 +`annotations` 외의 차이가 없는지 확인한다. + +성공 판정: `HMM_HR_DATA_GPT54_NOANN_PROFILE`이 `ENABLED`, `annotations=false`다. + +실패 시: [비교 프로파일이 이미 있지만 속성이 다름](troubleshooting.md#비교-프로파일이-이미-있지만-속성이-다름) + +## 3. A/B 측정 + +```text +database/adb/86_hmm_select_ai_annotation_benchmark.sql +``` + +동일 질문을 UTF-8 Base64에서 복원하고, 두 프로파일의 `showprompt`와 `showsql`을 번갈아 3회씩 +호출한다. `BENCHMARK|...` 행을 결과 파일에 저장하고 평균·중앙값을 계산한다. + +성공 판정: + +- 두 프로파일 모두 3회 결과가 있다. +- `showsql`이 읽기 전용 SQL을 반환한다. +- 시간과 prompt/SQL 길이를 비교할 수 있다. + +실패 시: [호출이 간헐적으로 실패하거나 시간이 크게 흔들림](troubleshooting.md#호출이-간헐적으로-실패하거나-시간이-크게-흔들림) + +## 4. 운영 전환 판단 + +이번 실험에서는 운영 값을 바꾸지 않는다. 속도 개선이 반복 확인되고 SQL 품질이 유지될 때만 +별도 변경으로 `BACKOFFICE_SELECT_AI_PROFILE` 전환과 MCP VPD 회귀 검증을 수행한다. + +2026-08-11 측정에서는 의미 있는 속도 개선이 확인되지 않았으므로 전환하지 않았다. 자세한 수치는 +[측정 결과](results.md)를 본다. + +## 5. 롤백 + +비교 프로파일이 문제를 일으키면 먼저 disable한다. + +```sql +BEGIN + DBMS_CLOUD_AI.DISABLE_PROFILE('HMM_HR_DATA_GPT54_NOANN_PROFILE'); +END; +/ +``` + +운영 프로파일은 실험 중 변경하지 않으므로 서비스 롤백은 필요 없다. 프로파일 삭제는 측정 이력과 +의존성을 확인한 뒤 별도 승인하에 수행한다. diff --git a/docs/design/749-hmm-select-ai-no-annotation-profile/results.md b/docs/design/749-hmm-select-ai-no-annotation-profile/results.md new file mode 100644 index 0000000..685f7c9 --- /dev/null +++ b/docs/design/749-hmm-select-ai-no-annotation-profile/results.md @@ -0,0 +1,49 @@ +# 측정 결과 + +[개요](README.md) · [비교 설계](architecture.md) · [실행 절차](cookbook.md) · [문제 해결](troubleshooting.md) + +## 결론 + +현재 HMM HR 데이터에서는 annotation 제외에 따른 일관된 속도 개선이 확인되지 않았다. +annotation 사용 프로파일의 object list에 실제 table/column annotation이 0건이기 때문이다. +운영 프로파일은 기존 값을 유지한다. + +## 측정 조건 + +| 항목 | 값 | +|---|---| +| 측정일 | 2026-08-11 | +| DB 연결 | ADMIN / `hmmaipoc_low` | +| 모델 | `openai.gpt-5.4-mini` | +| 질문 | 이번 달 팀원별 휴가 사용 현황 조회 | +| 반복 | action별·profile별 3회, 호출 순서 교차 | +| annotation 사용 | `HMM_HR_DATA_GPT54_PROFILE` | +| annotation 제외 | `HMM_HR_DATA_GPT54_NOANN_PROFILE` | + +한국어 질문은 UTF-8 Base64로 전달해 Oracle에서 복원했다. `showsql` 결과는 실행하지 않았다. + +## 결과 요약 + +| action | 프로파일 | 평균 | 중앙값 | 최소 | 최대 | 결과 크기 | +|---|---|---:|---:|---:|---:|---:| +| `showprompt` | annotation 사용 | 693.3ms | 130ms | 120ms | 1,830ms | 3,188자 | +| `showprompt` | annotation 제외 | 196.7ms | 130ms | 120ms | 340ms | 3,154자 | +| `showsql` | annotation 사용 | 2,453.3ms | 2,430ms | 2,410ms | 2,520ms | 522~547자 | +| `showsql` | annotation 제외 | 2,483.3ms | 2,370ms | 2,350ms | 2,730ms | 522~533자 | + +`showprompt` 첫 회차의 1,830ms는 이후 120~130ms로 줄어 초기 metadata cache 영향으로 판단한다. +중앙값은 두 프로파일 모두 130ms다. `showsql`은 annotation 제외 프로파일이 평균 기준 30ms 느리고, +중앙값 기준 60ms 빠르므로 측정 편차 범위다. + +## 품질 확인 + +- `showsql` 6회 모두 `SELECT` 또는 `WITH`로 시작하는 유효한 읽기 전용 SQL을 반환했다. +- 회차별 SQL hash가 달라 생성 SQL은 완전히 결정적이지 않았다. +- 두 프로파일의 prompt 크기 차이는 34자, 약 1.1%다. +- HR object list 5개 테이블의 `ALL_ANNOTATIONS_USAGE` 집계는 0건, 0자다. + +## 판단 + +현재 조건에서는 `annotations=false` 프로파일로 운영 전환할 근거가 없다. annotation metadata가 실제로 +많은 다른 object list에서는 결과가 달라질 수 있으므로, 그 경우 같은 스크립트로 다시 측정해야 한다. +비교 프로파일은 후속 시험용으로 유지하되 MCP 운영 설정에는 연결하지 않는다. diff --git a/docs/design/749-hmm-select-ai-no-annotation-profile/troubleshooting.md b/docs/design/749-hmm-select-ai-no-annotation-profile/troubleshooting.md new file mode 100644 index 0000000..fee65ba --- /dev/null +++ b/docs/design/749-hmm-select-ai-no-annotation-profile/troubleshooting.md @@ -0,0 +1,46 @@ +# 문제 해결 + +[개요](README.md) · [비교 설계](architecture.md) · [실행 절차](cookbook.md) · [측정 결과](results.md) + +## 원본 프로파일이 없거나 소유자가 다름 + +**증상**: `HMM_HR_DATA_GPT54_PROFILE` 조회 결과가 없거나 `CREATE_PROFILE` 권한 오류가 발생한다. + +**원인**: 다른 DB 또는 다른 사용자로 접속했거나 프로파일 소유자가 ADMIN이 아니다. + +**확인**: `DBA_CLOUD_AI_PROFILES`에서 owner와 profile name을 확인한다. + +**해결**: HMM ADB 지갑·service alias와 ADMIN 연결을 다시 확인한다. 이름이 비슷한 프로파일을 임의로 +대체하지 않는다. + +## 비교 프로파일이 이미 있지만 속성이 다름 + +**증상**: 비교 프로파일은 존재하지만 모델, object list, temperature 또는 annotations 값이 설계와 다르다. + +**원인**: 이전 실험의 프로파일이 같은 이름으로 남아 있다. + +**확인**: `DBA_CLOUD_AI_PROFILE_ATTRIBUTES`에서 두 프로파일을 나란히 비교한다. + +**해결**: 기존 비교 프로파일을 덮어쓰지 않는다. 먼저 disable하고 이력·의존성을 확인한 뒤 새 버전 +이름을 사용하거나 별도 승인으로 정리한다. + +## 호출이 간헐적으로 실패하거나 시간이 크게 흔들림 + +**증상**: 같은 프로파일에서도 회차별 `showsql` 시간이 크게 다르거나 LLM 오류가 발생한다. + +**원인**: 모델 endpoint 부하, 첫 호출 metadata cache, 생성형 SQL의 비결정성일 수 있다. + +**확인**: 두 프로파일을 번갈아 호출했는지, 각 3회 이상 결과가 있는지, 오류 코드가 프로파일 설정 +오류인지 외부 모델 오류인지 구분한다. + +**해결**: 실패 회차를 숨기지 않고 기록한다. 같은 조건으로 추가 회차를 실행하고 평균뿐 아니라 +중앙값·최솟값·최댓값을 함께 비교한다. + +## 한국어 질문이 깨져 SQL 품질이 달라짐 + +**증상**: 질문의 한글이 깨지거나 관련 없는 SQL이 생성된다. + +**원인**: SQLcl을 통해 한국어 literal을 직접 전달했다. + +**해결**: UTF-8 질문을 Base64 ASCII로 전달하고 Oracle에서 `UTL_I18N.RAW_TO_CHAR`로 복원한다. +직접 SQL string 또는 `q'[...]'`를 사용하지 않는다.