refs #749: compare HMM Select AI annotation profiles

This commit is contained in:
devmrko
2026-08-11 11:01:25 +09:00
parent 2d0b5a3c1e
commit 9ce0341102
7 changed files with 526 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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초로 의미 있는 개선이 없었다. 운영 프로파일은 변경하지 않았다.

View File

@@ -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)

View File

@@ -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;
/
```
운영 프로파일은 실험 중 변경하지 않으므로 서비스 롤백은 필요 없다. 프로파일 삭제는 측정 이력과
의존성을 확인한 뒤 별도 승인하에 수행한다.

View File

@@ -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 운영 설정에는 연결하지 않는다.

View File

@@ -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'[...]'`를 사용하지 않는다.