이전 글에 이어서 작성합니다.
지난번 제작한 DecisionRAG(모의)는 1회의 검색에 약 10초가 소요되었습니다.
실용성이 낮다고 판단되어 레이턴시(latency) 개선을 진행했습니다.
온라인 용도이므로 원래라면 Python으로 실행 방식을 전환하는 방법도 있었지만, Genie Agent에서 사용하고 싶었기 때문에 SQL UC 함수 형태는 유지하면서 레이턴시 개선에 집중했습니다.
그 결과, 동일한 질문에 대해 약 10초 → 3.6초로 단축되었습니다.
개선을 위해 ai_decide의 호출 횟수 감소도 시도했지만, 결국 탐색의 '단계(段数)'를 줄이는 것이 더 효과적이었습니다.
ai_decide는 Beta 기능입니다. 또한, 후반부에서 탐색 단계를 하나 생략했기 때문에 정확도에 미치는 영향은 아직 검증되지 않았습니다.
지난번의 약 10초 측정은 XXS SQL Warehouse 환경이었고, 이번에는 노트북(서버리스)에서 측정한 것이므로 이전 결과와는 참고용으로만 봐주시기 바랍니다.
이전 글의 개선 방안으로 다음과 같은 내용을 작성했습니다.
이번에는 ai_decide 함수를 각 레코드에 대해 배치적으로 Noul(Yes/No)로 판별하는 설계를 했기 때문에, 함수 호출 횟수가 많았습니다. 또한, 아마도 ai_decide 함수는 현재 병렬 실행되지 않을 가능성이 높으므로, 호출 횟수가 많을수록 시간이 오래 걸리는 구조입니다. 특히 첫 번째 태그 탐색을 Choice 방식으로 판별하면 호출 횟수를 1회로 줄일 수 있어 레이턴시를 줄일 수 있을 것입니다.
그 외에 함수 호출 횟수를 어떻게 줄이느냐가 레이턴시(와 비용)에 기여할 것이므로, 트리 구조 자체의 재설계까지 검토하는 것이 개선 포인트라고 생각합니다.
이에 대해서는 '아마도 이렇게 아닐까'라는 가설을 세웠고, 이를 기반으로 개선 작업을 진행했습니다.
질문은 모두 Databricks ai_decide 함수 사용법은 무엇인가요? (threshold => 0.2, 그 외는 기본값)였습니다.
| 버전 | 변경 내용 | ai_decide 단계 | 호출 횟수 | 실행 시간 |
|---|
| 이전 | 모든 계층 noul | 4단계 | 200~250회 | 약 10초 (제대로 측정하지 않음) |
| ... | | | | |
지난번에는 L1에서 모든 태그(166개)를 noul로 하나씩 판별했습니다. ai_decide의 choice는 모든 레이블의 확률 분포를 반환하므로, 태그를 선택지로 사용하면 1회로 끝낼 수 있을 것입니다. | | | | |
다만, ai_decide의 두 번째 인자(questions)는 상수여야 하므로, 태그 목록에서 동적으로 생성할 수는 없었습니다.
그래서 레이블은 t001부터 t255까지의 번호로 상수로 작성하고, 각 번호 내부의 내용(태그 설명)은 state 측의 options에 매번 조립하여 전달하는 방식으로 시도했습니다.
l1_decision AS (
SELECT ai_decide(
to_json(named_struct(
...
모델은 존재하지 않는 번호(t169 이후)에도 확률을 반환하기 때문에 (선택지로 전달했으므로 당연함), 실제로 존재하는 태그에만 한정하여 합산 후 1로 정규화했습니다.
이 방법으로 7.9초가 되었습니다. (여러 번 측정한 평균)
동일한 방식으로 L2(후보 20개 기사)와 L3(약 50개 섹션)도 choice를 각각 1회씩 사용해 보았습니다. L4는 임계값으로 판별하고 싶기 때문에 noul 방식을 유지했습니다.
ai_decide는 호출할 때마다 결과가 달라질 수 있기 때문에, 동일한 CTE(Common Table Expression)를 두 번 참조하여 재계산되면 계층 간에 불일치가 발생합니다. 그래서 후보 배열과 ai_decide의 결과를 같은 집계 행에 담아 그 1행을 전개했습니다.
l2_decision AS (
SELECT
collect_list(struct(slot, article_id, title, tag_score)) AS cands,
...
호출은 질문당 8회 (약 76회의 1/10)가 되었지만, 실행 시간은 6.6초였습니다. 여러 번 실행하여 평균을 내니 10%가 조금 넘는 개선이었습니다.
좀 더 빨라질 거라고 생각했습니다.
호출 횟수를 줄여도 예상만큼 단축되지 않았기 때문에, 드디어 쿼리 프로파일(query profile)에서 어느 부분에 시간이 걸리는지 확인해 보았습니다 (처음에 봤어야 했는데).
choice 버전(6.51초)의 내역은 다음과 같았습니다.
| 구분 | 시간 |
|---|
| ai_decide의 스테이지(4개) | 3.47초 |
| ... | |
| 결합이나 윈도우 함수 계산은 순식간이었고, ai_decide를 제외한 약 2.9초는 거의 '대기 시간'이었습니다. 각 층이 이전 층의 결과를 기다리기 때문에 스테이지가 직렬로 배열되며, 전환할 때마다 공백 시간이 발생합니다. 특히 L1 이전에 태그 집계를 기다리는 공백이 0.75초 있었습니다. | |
이에 개선 방침을 '단(Layer)과 스테이지 전환 감소'로 결정하고, 다음 세 가지를 수정한 decision_rag_search_fast를 만들었습니다.
L1의 선택지를 사전에 계산: 태그 번호와 설명문을 정리한 options 한 줄을 파이프라인(SDP)에서 테이블 형태로 미리 준비합니다.
- tag_score를 배열 함수로 계산: 기사의 태그 배열과 L1의 확률 맵으로부터 계산하여, 기사×태그 결합 및 윈도우 함수 사용을 없앱니다.
- L2 생략: tag_score 상위 10개 기사의 모든 섹션을 L3의 choice를 통해 직접 선택합니다 (L1 → L3 → L4의 3단계).
L1의 options는 질문에 의존하지 않기 때문에, 이전 파이프라인에 다음 두 개의 Materialized View를 추가했습니다.
| 테이블 | 내용 |
|---|
tag_slots | 태그별 번호(t001부터, 기사 수 많은 순)와 선택지 설명문 (태그명・건수・대표 제목 3개) |
tag_options | L1에 그대로 전달할 한 줄. options (번호 → 설명문)와 slot_to_tag (번호 → 태그) 두 개의 맵 |
변환 처리는 로컬에서도 테스트할 수 있도록 utilities에 함수로 분리했습니다.
from pyspark.sql import DataFrame, Window
from pyspark.sql import functions as F
MAX_SLOTS = 255 # ai_decide의 choice 선택지 상한선
...
파이프라인 측에서는 이를 Materialized View로 정의하기만 하면 됩니다.
더불어, L3의 선택지에 사용되는 설명문 (기사 제목 / 헤드라인: 본문 시작 부분)도 sections에 option_text 컬럼으로 추가했습니다.
from utilities.tree_views import build_tag_options, build_tag_slots
OPTION_TITLES = int(spark.conf.get("decision_rag.option_titles", "3"))
@dp.materialized_view(
...
완성된 tag_options는 다음과 같은 한 줄 형태입니다.
options : {"t001": "databricks(…건): …", "t002": "…", …}
slot_to_tag : {"t001": "databricks", "t002": "…", …}
n_tags : 168
함수 측의 L1은 이 한 줄을 읽어 ai_decide에 전달하기만 하면 됩니다. 반환된 확률 역시 map_filter로 실제 존재하는 번호만 걸러내고, 한 줄 상태 그대로 '태그 → 확률' 맵(tag_p)으로 변환합니다. 탐색 시 태그 집계나 윈도우 함수가 사라지므로, L1 이전의 공백 시간이 줄어들 것이라는 목표입니다.
l1_decision AS (
SELECT o.slot_to_tag,
ai_decide(to_json(named_struct('question', q, 'options', o.options)), '…L1용 choice 상수…') AS d
...
기사의 태그 배열과 L1의 확률 맵(한 줄)으로부터 계산합니다. 기사×태그 결합 및 윈도우 함수는 필요 없어집니다.
scored AS (
SELECT
article_id, title, url,
...
tag_score 상위 10개 기사의 모든 섹션(이번에는 103개)을 선택지로 삼아 L3의 choice를 한 번 호출합니다. 작성 방식은 Step2의 L2와 동일하게, 후보 배열과 ai_decide 결과를 한 줄로 통합하는 형태입니다.
decision_rag_search_fast 전체 (길어서 접기)
CREATE OR REPLACE FUNCTION decision_rag_search_fast(
q STRING COMMENT '질문 문장',
n_candidates INT DEFAULT 10 COMMENT 'tag_score로 남길 후보 기사 수(이 기사의 섹션이 L3의 선택지가 됨)',
...
노트북에서 다음과 같이 만든 UC 함수를 실행합니다. (변수 S에는 catalog.schema 문자열이 들어 있습니다)
display(spark.sql(f"""
SELECT result_rank, title, heading, top_tags,
round(tag_score, 3) AS tag_score, round(p_head, 3) AS p_head, round(p_body, 3) AS p_body, url
...
"""))```
결과는 4건이었고, 실행 시간은 **3.64초**였습니다. 프로파일링 결과는 다음과 같습니다.
| 구분 | choice 버전 | fast 버전 |
|---|---|---|
| 합계 | 6.51초 | 3.64초 |
| ... |
L1 이전의 공백은 0.75초에서 0.27초로 줄었습니다. L3는 선택지가 103개로 늘어나 약간 느려졌지만, L2 전체를 생략한 효과가 더 큽니다.
SQL 함수 자체만으로도 3초 전후가 하한선일 것 같습니다. Genie Agent 측에서도 몇 초가 걸리기 때문에 개인적으로는 하나의 단계로 충분해 보입니다.
이전에 사용했던 Genie Agent에 `decision_rag_search_fast`를 SQL 함수로 추가(기존 함수와 교체)해 보았습니다.
다음과 같은 질문과 사고 과정이 도출됩니다.
얻게 된 답변은 이렇습니다.
음, Databricks를 칭찬하는 글을 많이 써본 기억이 없는데...
궁금해서 좀 더 파고들어 봤습니다.
내용적으로는 제 기사의 경향이 드러난 결과였습니다.
`ai_decide`로 만든 DecisionRAG(모의) 검색을 UC 함수 상태 그대로 빠르게 만들어서 Genie Agent에서 사용해 보았습니다.
`ai_decide` 호출 횟수 감소도 고속화에 기여했지만, 그 외 요소들도 크며 탐색 구조 설계가 중요합니다.
그렇다고는 해도 L2를 생략한 영향이나 번호 라벨 방식의 정확도는 아직 확인하지 못했습니다(평가용 질문을 만드는 것이 생각보다 힘듭니다). 비교해 보니 약간의 정확도 하락은 있는 것 같습니다. 제대로 측정해야겠지만 그 부분은 다음번(?)에 하겠습니다.
또한, 이번에는 기사 수가 많지 않아서 이번 방식만으로 충분하다고 생각했지만, 더 많은 문서를 다루는 케이스도 시도해 보고 싶습니다. 완전히 다른 구조 설계가 필요할 것 같습니다.
그 외에 Lakehouse//RT가 AI Functions를 지원하게 되면 훨씬 낮은 레이턴시가 될 것 같아 기대됩니다. 1~2초 정도의 레이턴시가 된다면 온라인 용도로도 충분한 속도가 될 것이라 예상합니다. 빨리 대응해 주세요! 빨리!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34