📚 시리즈 — Databricks LLMOps
- ① AI Gateway — LLM 폴백·게이트웨이 프록시
- ② Prompt Registry — 프롬프트 버전·alias 관리
- ③ MLflow Tracing — LangGraph 관측·PII 마스킹 — 현재 글
본 가이드는 Databricks 위에서 동작하는 LangGraph 기반 RAG agent 의 모든 호출 (LLM·도구·노드) 을 MLflow Tracing 으로 자동 캡처하고, 클라이언트 단에서 한국어 PII 를 마스킹한 뒤 trace 백엔드로 전송하는 표준 절차를 정의합니다. 데이터 평면 외부로 원본 PII 가 나가지 않도록 mlflow.tracing.configure(span_processors=...) hook 을 사용하며, MLflow Trace UI 에서 span 트리·input/output·검색·필터를 활용해 운영 가시성을 확보합니다.
1. 개요
1.1 목적
RAG agent 가 사용자 질문을 처리하면서 만들어지는 모든 내부 호출 (의도 분류·LLM·도구·검색) 을 trace 로 자동 캡처하여, 운영 가시성 과 재현 가능한 디버깅 을 동시에 확보합니다. 동시에 trace 안에 포함될 수 있는 개인정보(PII) 가 백엔드 저장소로 흘러가지 않도록 클라이언트 단에서 redact 합니다.
1.2 MLflow Tracing 이란
MLflow Tracing 은 LLM/Agent 애플리케이션의 실행을 span 트리 형태로 기록하는 관찰 도구입니다. mlflow.langchain.autolog() 한 줄로 LangChain·LangGraph 호출이 자동 캡처되며, 별도 계측 없이 다음 정보를 얻을 수 있습니다.
- 사용자 입력·LLM 응답 메시지
- 그래프 노드별 input/output (state 변환)
- LLM 호출의 토큰 사용량·latency·model 이름
- 도구 호출·검색 결과·에러 trace
- 전체 실행 latency 및 노드별 분포
1.3 전체 구조도

핵심은 redact 가 client-side 에서 일어난다 는 점입니다. PII 가 포함된 원본 span 은 클라이언트 메모리에만 존재했다가, processor 가 마스킹한 사본만 백엔드로 전송됩니다.
1.4 airag-rag 가이드와의 관계
본 가이드는 RAG 품질 ① 평가 (Tistory) 의 선행 단계 입니다. Tracing 은 RAG agent 호출의 모든 span 을 관찰·수집하며, Evaluation 은 수집된 trace 를 입력 데이터로 받아 답변 품질을 채점합니다. 두 가이드는 데이터 흐름상 직렬 관계이고, 운영에서는 보통 같이 적용합니다.
1.5 PII 보호 계층
A항공 airag 시스템의 PII 보호는 3개 계층 으로 나뉘며, 본 가이드는 그 중 Trace 계층을 담당합니다. 각 계층은 독립적으로 동작하므로 모두 정확히 적용해야 PII 누출을 막을 수 있습니다.
| 계층 | 정본 가이드 | 방식 | 가역성 |
|---|---|---|---|
| DB 저장 | 앱 데이터 모델 가이드 §4 개인정보 암호화 (D3) | AES-256-GCM + KMS CMK envelope encryption | 복호화 가능 |
| Trace 저장 | 본 가이드 | span_processors regex/Presidio redact |
비가역 |
| Log 저장 | (별도 가이드 없음, 앱 배포 가이드 참고) | nginx/사이드카 access log redact | 비가역 |
DB 는 본인 인증·환불 등 후속 처리를 위해 복호화가 필요하므로 reversible 암호화를 사용하지만, Trace·Log 는 디버깅·관찰 목적이라 원본 복원이 불필요하여 비가역 redact 가 적절합니다.
1.6 버전 요구사항
mlflow.tracing.configure(span_processors=...)API 는 MLflow 3.x 정식 입니다. 본 가이드는 MLflow 3.x 기준으로 작성되었으며, A항공 airag 환경은 신규 시스템이라 3.x 를 표준으로 채택합니다.버전 확인:
bash pip show mlflow | grep -E "^(Name|Version)"또는 노트북 내에서import mlflow; print(mlflow.__version__).
1.7 원본 PII 는 어디에도 저장되지 않습니다
본 가이드의 가장 중요한 보안 특성은 원본 PII 가 MLflow Trace 백엔드에 영구 저장되지 않는다 는 점입니다. span_processors 의 실행 위치는 클라이언트 (RAG agent 프로세스) 메모리이며, 마스킹은 백엔드 전송 이전 에 일어납니다. Trace UI 가 "마스킹된 형태로 보여준다" 가 아니라, 백엔드에 원본이 도달조차 하지 않는 구조입니다.
시점별 원본 PII 의 위치
| 시점 | 위치 | 영구 저장? |
|---|---|---|
| 1. 사용자 입력 직후 | HTTPS 네트워크 → RAG agent 프로세스 메모리 | 메모리에만 |
| 2. LangGraph 실행 중 | state 객체·LLM 호출 인자 (메모리) | 메모리에만 |
| 3. autolog → span 생성 직후 | span 객체 (메모리, redact 전) | 메모리에만 |
4. span_processors 실행 후 |
마스킹된 사본 (메모리) | 원본은 GC 대상 |
| 5. 백엔드 전송 | 마스킹된 데이터만 네트워크 통과 | — |
| 6. Trace UI 조회 | 마스킹된 데이터만 영구 저장 | 비가역 |
흔한 오해
다음 두 패턴은 보안 특성이 다릅니다.
| 패턴 | 동작 | 원본 위치 |
|---|---|---|
본 가이드 (span_processors client-side) |
클라이언트에서 redact 후 전송 | 저장 안 됨 |
| 백엔드 마스킹 (다른 시스템) | 원본 저장 후 UI 표시 단계에서 마스킹 | DB 에 원본 저장됨 |
본 가이드는 전자 입니다. Databricks/MLflow 백엔드로 원본이 나가지 않는 것이 핵심 가치입니다.
Q&A
Q1. 원본 PII 가 디스크에 일순간이라도 저장되나요?
아니요. span_processors 가 등록되어 있다면 span 객체는 메모리 안에서 redact 된 뒤 직렬화되어 네트워크로 나갑니다. 디스크 (로컬·EBS·MLflow 백엔드 모두) 에 원본이 쓰이지 않습니다.
Q2. process crash 시 메모리 dump 로 원본이 노출될 수 있나요?
이론적으로 가능합니다 — core dump 가 활성화된 상태에서 crash 가 발생하면 메모리 안 PII 가 dump 파일로 남을 수 있습니다. 운영 환경 (Databricks Apps / ECS Fargate) 은 기본적으로 core dump 가 비활성화되어 있어 위험이 낮지만, 별도로 점검해주세요.
Q3. 다른 경로 (DB·log) 로 원본이 흘러갈 가능성은?
가능합니다. 본 가이드는 Trace 계층만 다루며, 다른 계층은 별도 가이드 책임입니다 — §1.5 PII 보호 계층 표 참조. 운영 환경은 세 계층 (DB·Trace·Log) 모두 에 마스킹/암호화를 적용해야 PII 가 완전히 보호됩니다.
Q4. span_processors 등록 이전 에 만들어진 trace 는 어떻게 됩니까?
이미 백엔드로 전송된 trace 는 마스킹되지 않은 상태로 저장되어 있습니다. 운영 첫 배포 직후 §7.5 leak SOP 를 즉시 실행하고, 누출된 trace 가 발견되면 mlflow.client.delete_trace() 로 삭제하거나 backfill 스크립트로 사후 redact 합니다.
Q5. FMAPI LLM 호출 시 PII 가 외부로 나가지 않나요?
Databricks FMAPI 호출은 Databricks 내부 통신입니다. Front-end PrivateLink (D4) 가 적용된 환경에서는 고객 VPC → Databricks 의 단일 VPCE 안에서 처리되어 인터넷을 통과하지 않습니다. 다만 외부 LLM 벤더 (OpenAI·Anthropic 직접 API 등) 를 사용하면 그 벤더로 PII 가 전송되므로 별도 검토가 필요합니다 (네트워크(PrivateLink) 가이드 참조).
2. 핵심 개념
2.1 Span 구조와 타입
Trace 는 root span 1개 + 자식 span 다수로 이루어진 트리입니다. LangGraph 실행 시 자동 생성되는 주요 span 타입은 다음과 같습니다.
| Span 타입 | 의미 |
|---|---|
CHAIN |
LangGraph 전체 그래프 또는 sub-chain 실행 |
CHAT_MODEL |
LLM 호출 (요청 메시지·응답·토큰·model 이름) |
TOOL |
도구 (function) 호출 |
AGENT |
LangChain Agent 또는 LangGraph 노드 |
RETRIEVER |
RAG 검색 단계 (Vector Search 등) |
다음은 본 가이드 §4 의 한국어 챗봇 그래프가 생성하는 span 트리입니다.

2.2 autolog 자동 캡처 메커니즘
mlflow.langchain.autolog()
위 한 줄을 호출하면 LangChain·LangGraph 의 모든 chain·LLM·tool 호출이 자동으로 span 으로 기록됩니다. 캡처되는 항목은 다음과 같습니다.
- 각 span 의 input·output (state, messages, prompt)
- LLM 호출의 model 이름, 토큰 사용량, latency
- exception trace (있으면)
- LangGraph 의 노드별 state 변환
기본 동작은 모든 호출을 동기적으로 기록 합니다. 트래픽이 많을 경우 MLFLOW_TRACE_SAMPLING_RATIO 환경변수로 샘플링 비율 조절이 가능합니다 (§8.3 참조).
2.3 span_processors 후처리 hook
mlflow.tracing.configure(span_processors=[fn1, fn2, ...]) 로 등록한 함수는 각 span 이 백엔드로 전송되기 직전 클라이언트 단에서 실행됩니다. 함수 시그니처는 다음과 같습니다.
from mlflow.entities.span import Span
def fn(span: Span) -> None:
# span.inputs, span.outputs, span.attributes 를 in-place 로 수정
...
- 반환값 없이
span을 in-place 로 수정 - 등록된 순서대로 sequential 적용
- 클라이언트에서 실행되므로 원본 PII 가 백엔드로 전송되지 않음
- hook 등록 해제는
mlflow.tracing.configure(span_processors=[])또는mlflow.tracing.reset()
3. 빠른 시작
다음 5줄로 30초 안에 trace 를 시작할 수 있습니다.
import mlflow
mlflow.set_experiment("/Users/user@example.com/my-experiment")
mlflow.langchain.autolog()
# 이후 모든 LangChain/LangGraph 호출이 자동으로 trace 됨
result = graph.invoke({"messages": [...]})
실행 후 Databricks 좌측 사이드바 Experiments → 해당 experiment → Traces 탭에서 결과를 확인합니다 (§6 참조).
PII 마스킹을 함께 적용하려면 §5 의 redactor 를 정의한 뒤 mlflow.tracing.configure(span_processors=[redactor]) 를 추가로 호출합니다. 이 호출은 반드시 graph.invoke 이전에 이루어져야 합니다.
4. LangGraph 통합
4.1 그래프 구조와 trace 매핑
LangGraph 의 각 노드는 별도 span 으로 기록되며, 노드 안에서 발생하는 LLM·tool 호출은 자식 span 으로 중첩됩니다. 본 가이드 예제 그래프는 다음과 같이 단순한 2-노드 구조입니다.

각 노드 안에서 ChatDatabricks.invoke() 가 한 번씩 호출되므로, 최종 span 트리는 §2.1 다이어그램과 일치합니다.
4.2 한국어 챗봇 예제 (코드 발췌)
전체 코드는 source/databricks-misc/mlflow-tracing-demo/01_mlflow_trace_korean_pii_masking.py 에 있습니다. 워크스페이스 실행 사본 위치는 §10 참고 자료를 참조해주세요.
from typing import TypedDict, Annotated
from operator import add
from langgraph.graph import StateGraph, START, END
from langchain_core.messages import HumanMessage, SystemMessage
from databricks_langchain import ChatDatabricks
llm = ChatDatabricks(endpoint="databricks-claude-sonnet-4-6", temperature=0.2)
class ChatState(TypedDict):
messages: Annotated[list, add]
intent: str
def classify(state: ChatState) -> ChatState:
user_text = state["messages"][-1].content
resp = llm.invoke([
SystemMessage(content=(
"당신은 항공사 고객 문의 분류기입니다. "
"사용자 질문을 다음 중 하나로 분류하세요: "
"[예약조회, 마일리지, 환불, 일반문의]. "
"분류 결과만 한 단어로 답하세요."
)),
HumanMessage(content=user_text),
])
return {"messages": [], "intent": resp.content.strip()}
def answer(state: ChatState) -> ChatState:
user_text = state["messages"][-1].content
intent = state["intent"]
resp = llm.invoke([
SystemMessage(content=(
f"당신은 A항공 고객센터 상담원입니다. "
f"분류된 의도는 '{intent}' 입니다. "
f"친절한 한국어 존댓말로 2-3문장으로 답변하세요."
)),
HumanMessage(content=user_text),
])
return {"messages": [resp], "intent": intent}
builder = StateGraph(ChatState)
builder.add_node("classify", classify)
builder.add_node("answer", answer)
builder.add_edge(START, "classify")
builder.add_edge("classify", "answer")
builder.add_edge("answer", END)
graph = builder.compile()
4.3 실행 결과
autolog 활성화 상태에서 graph.invoke() 를 한 번 호출하면 trace 1개가 생성됩니다. 본 가이드 검증 시 측정값:
| 항목 | 값 |
|---|---|
| Trace ID | tr-a51f24a3b91ee3038181b375df62d647 (예시) |
| Token count | 1,200 ~ 1,335 |
| 전체 latency | 8.6 ~ 18.6초 (FMAPI 응답 시간 의존) |
| 생성된 span 수 | 5개 (root + classify + answer + 2× ChatDatabricks) |
5. 한국어 PII 마스킹
5.1 마스킹 대상 PII
본 가이드는 한국 시장에서 흔히 노출되는 5종 + 1종 (여권) PII 를 regex 로 식별합니다.
| 라벨 | 대상 | 예시 |
|---|---|---|
EMAIL |
이메일 주소 | minjun.kim@example.com |
RRN |
주민등록번호 | 900101-1234567 |
PHONE_MB |
휴대전화 | 010-1234-5678 |
PHONE_LL |
일반전화 | 02-1234-5678 |
CARD |
신용카드 번호 | 4321-8765-2109-6543 |
PASSPORT |
한국 여권번호 | M12345678 |
이름·주소·사번 등은 regex 로 안전하게 식별하기 어렵습니다. §8.5 알려진 한계와 향후 과제 (Presidio + KoBERT NER 검토) 를 참조해주세요.
5.2 regex 패턴 정의
import re
KO_PII_PATTERNS = [
("EMAIL", re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")),
("RRN", re.compile(r"\b\d{6}[- ]?[1-4]\d{6}\b")),
("PHONE_MB", re.compile(r"\b01[016789][- ]?\d{3,4}[- ]?\d{4}\b")),
("PHONE_LL", re.compile(r"\b0\d{1,2}[- ]?\d{3,4}[- ]?\d{4}\b")),
("CARD", re.compile(r"\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b")),
("PASSPORT", re.compile(r"\b[MmSsRrOoDdGg]\d{8}\b")),
]
def scrub_text(text: str) -> str:
"""문자열에서 한국어 PII를 [REDACTED:<TYPE>] 로 치환."""
if not isinstance(text, str):
return text
for label, pattern in KO_PII_PATTERNS:
text = pattern.sub(f"[REDACTED:{label}]", text)
return text
5.3 span processor 함수
LangGraph state 는 dict/list/tuple/str 이 중첩된 구조라, span 의 inputs·outputs 를 안전하게 redact 하려면 재귀적으로 모든 문자열을 scrub 해야 합니다.
from typing import Any
from mlflow.entities.span import Span
def deep_scrub(obj: Any) -> Any:
"""dict/list/tuple/str 어떤 구조든 재귀적으로 PII 마스킹."""
if isinstance(obj, str):
return scrub_text(obj)
if isinstance(obj, dict):
return {k: deep_scrub(v) for k, v in obj.items()}
if isinstance(obj, list):
return [deep_scrub(v) for v in obj]
if isinstance(obj, tuple):
return tuple(deep_scrub(v) for v in obj)
return obj
def korean_pii_redactor(span: Span) -> None:
"""MLflow span processor: 모든 span의 inputs/outputs/attributes 를 마스킹."""
try:
if span.inputs is not None:
span.set_inputs(deep_scrub(span.inputs))
if span.outputs is not None:
span.set_outputs(deep_scrub(span.outputs))
for key, value in list((span.attributes or {}).items()):
if isinstance(value, str):
span.set_attribute(key, scrub_text(value))
except Exception as e:
# 마스킹 실패가 trace 자체를 깨뜨리지 않도록 swallow
print(f"[redactor] skip span {span.name}: {e}")
try/except 로 감싼 이유는, redactor 가 한 span 에서 실패해도 다른 span 의 캡처와 사용자 요청 처리를 막지 않기 위함입니다. 운영에서는 swallow 대신 알람으로 연결합니다 (§8.4 참조).
5.4 처리기 등록
mlflow.tracing.configure(span_processors=[korean_pii_redactor])
중요: 본 호출은 반드시
graph.invoke()이전에 이루어져야 합니다. 등록 이후 생성되는 span 부터 적용되므로, 앱 진입점 (FastAPI startup, Databricks Apps 시작 함수 등) 에서 한 번 호출하는 것이 권장됩니다.
5.5 마스킹 전/후 비교
검증용으로 동일한 질문 (이메일·휴대전화·주민번호·카드번호 포함) 을 마스킹 없이 / 적용한 채로 각각 1회 실행해 비교했습니다.
BEFORE (마스킹 미적용):
{
"messages": [{
"content": "안녕하세요, 김민준입니다. 제 이메일은 minjun.kim@example.com 이고 휴대전화는 010-1234-5678 입니다. 주민등록번호 900101-1234567 로 예약한 7월 인천-도쿄 항공권 환불 가능할까요? 결제는 카드 4321-8765-2109-6543 으로 했습니다.",
"type": "human"
}],
"intent": ""
}
AFTER (마스킹 적용):
{
"messages": [{
"content": "안녕하세요, 김민준입니다. 제 이메일은 [REDACTED:EMAIL] 이고 휴대전화는 [REDACTED:PHONE_MB] 입니다. 주민등록번호 [REDACTED:RRN] 로 예약한 7월 인천-도쿄 항공권 환불 가능할까요? 결제는 카드 [REDACTED:CARD] 으로 했습니다.",
"type": "human"
}],
"intent": ""
}
| PII 타입 | 원본 | 마스킹 결과 |
|---|---|---|
| 이메일 | minjun.kim@example.com |
[REDACTED:EMAIL] |
| 휴대전화 | 010-1234-5678 |
[REDACTED:PHONE_MB] |
| 주민번호 | 900101-1234567 |
[REDACTED:RRN] |
| 카드 | 4321-8765-2109-6543 |
[REDACTED:CARD] |
이름 김민준 은 §5.1~§5.5 의 regex 5종 대상이 아니라 그대로 노출됩니다. 인명·주소·사번 같은 평문 컨텍스트 PII 는 §5.6 LLM 기반 마스킹으로 보완할 수 있습니다.
5.6 LLM 기반 마스킹 (선택)
regex 5종 (EMAIL/RRN/PHONE/CARD/PASSPORT) 은 형식이 명확한 PII 만 잡습니다. 한국어 인명·주소·사번 처럼 평문 컨텍스트 안에 있는 PII 는 LLM 기반 마스킹으로 보완하는 패턴을 함께 적용할 수 있습니다. 본 절은 선택 사항 으로, regex 만으로 충분한 환경은 §5.5 까지로 종결해도 됩니다.
5.6.1 2단계 redact 구조

5.6.2 모델 선정
PII 마스킹은 단순 분류·치환 작업 이라 추론력보다 latency·비용이 중요합니다. Databricks FMAPI 의 databricks-claude-haiku-4-5 가 적합합니다.
| 후보 | Latency (한국어 짧은 입력) | 비용 | 한국어 PII 정확도 | 적합도 |
|---|---|---|---|---|
databricks-claude-haiku-4-5 |
200~500ms | 가장 저렴 | 충분 (단순 작업) | ✓ 권장 |
databricks-claude-sonnet-4-6 |
1~3초 | 중간 | 매우 높음 | 본 LLM 과 동급이라 의미 X |
databricks-meta-llama-3-3-70b-instruct |
500ms~1.5초 | 저렴 | 충분 | 대안 |
5.6.3 구현 코드
from databricks_langchain import ChatDatabricks
from langchain_core.messages import SystemMessage, HumanMessage
from mlflow.tracing import disable as tracing_disable
# 본 RAG agent 의 LLM 과 별도 인스턴스
pii_llm = ChatDatabricks(
endpoint="databricks-claude-haiku-4-5",
temperature=0,
max_tokens=2048,
)
REDACT_PROMPT = """다음 텍스트에서 개인정보를 [REDACTED:<TYPE>] 로 치환하세요.
TYPE 종류: NAME(인명), ADDRESS(주소), EMPLOYEE_ID(사번), LOCATION(위치).
이미 [REDACTED:*] 로 마스킹된 부분은 그대로 두세요.
원문의 의미·문장 구조는 유지하세요.
치환 결과만 출력하세요. 부연 설명 금지.
원문: {text}
"""
def llm_redact(text: str) -> str:
"""LLM 기반 PII 마스킹 — regex 가 못 잡은 평문 컨텍스트 PII 보완."""
if not text or len(text) < 5:
return text
# 재귀 trace 방지 — redactor 안의 LLM 호출이 또 redactor 를 부르지 않도록
with tracing_disable():
resp = pii_llm.invoke([
SystemMessage(content="당신은 한국어 PII 마스킹 도구입니다."),
HumanMessage(content=REDACT_PROMPT.format(text=text)),
])
return resp.content.strip()
def korean_pii_redactor_v2(span: Span) -> None:
"""1차 regex + 2차 LLM 2단계 redact."""
try:
if span.inputs is not None:
scrubbed = deep_scrub(span.inputs) # 1차: regex
scrubbed = _apply_recursive(scrubbed, llm_redact) # 2차: LLM
span.set_inputs(scrubbed)
if span.outputs is not None:
scrubbed = deep_scrub(span.outputs)
scrubbed = _apply_recursive(scrubbed, llm_redact)
span.set_outputs(scrubbed)
except Exception as e:
print(f"[redactor] skip span {span.name}: {e}")
def _apply_recursive(obj: Any, fn) -> Any:
"""deep_scrub 와 같은 구조로 모든 문자열에 fn 적용."""
if isinstance(obj, str):
return fn(obj)
if isinstance(obj, dict):
return {k: _apply_recursive(v, fn) for k, v in obj.items()}
if isinstance(obj, list):
return [_apply_recursive(v, fn) for v in obj]
if isinstance(obj, tuple):
return tuple(_apply_recursive(v, fn) for v in obj)
return obj
tracing_disable() 컨텍스트 매니저로 redactor 안의 Haiku 호출이 trace 에 잡히지 않도록 막는 것이 핵심입니다. 안 그러면 redactor → LLM 호출 → autolog → span 생성 → redactor → ... 무한 재귀가 발생합니다.
등록은 §5.4 와 동일하며 함수 이름만 교체합니다.
mlflow.tracing.configure(span_processors=[korean_pii_redactor_v2])
5.6.4 Databricks 환경의 이점
| 항목 | 내용 |
|---|---|
| 외부 API 키 불필요 | UC Service Credential 또는 Apps SP OAuth 로 인증 (SP OAuth 토큰 가이드) |
| Front-end PrivateLink 통과 | airag-gw → Apps → FMAPI 호출이 모두 단일 VPCE 안에서 처리 (D4, 네트워크(PrivateLink) 가이드) |
| PII 외부 유출 없음 | 클라이언트 → FMAPI 가 모두 Databricks 내부 — 외부 LLM 벤더로 PII 가 나가지 않음 |
| AI Gateway 통제 | rate limit·audit log·content filter 표준 가드 적용 |
| UC 거버넌스 | 모델 사용 추적·권한 관리 통합 |
5.6.5 운영 주의점
| 주의점 | 내용 | 해결 |
|---|---|---|
| 재귀 trace | redactor 안 LLM 호출도 autolog 가 trace 함 → 무한 재귀 | tracing_disable() 컨텍스트 매니저 필수 |
| Latency | 요청당 LLM 호출 1회 추가 (Haiku 4.5 기준 200~500ms) | 짧은 입력은 LLM skip (길이 임계치 필터), 또는 비동기 후처리 (§5.6.7) |
| 비용 | LLM 호출 비용 증가 | regex 가 1차로 잡은 뒤 남은 텍스트만 LLM 적용 |
| 환각 | LLM 이 PII 아닌 걸 PII 로 보거나 (false positive) 놓치거나 (false negative) | temperature=0 + 프롬프트에 예시 추가 + 정기 회귀 검증 (§8.2) |
5.6.6 비용 추산
A항공 운영 가정: cabin/cargo 일 요청 1,000건, 평균 input 500자.
| 항목 | 값 |
|---|---|
| 1 요청당 redact 호출 수 | 2 (input + output) |
| 1 요청당 Haiku 토큰 사용 | 약 500 input + 500 output = ~1,000 토큰 |
| 일 누적 토큰 | 1,000건 × 2 × 1,000 = 2M 토큰 |
| Haiku 4.5 단가 (input·output 평균 가정) | 약 $1 / 1M 토큰 |
| 일 비용 | 약 $2/일 (월 $60) |
작은 부담으로 인명·주소·사번까지 정확하게 마스킹 가능합니다. 트래픽이 늘면 5.6.7 비동기 후처리로 전환을 검토합니다.
5.6.7 비동기 후처리 패턴 (대안)
요청 latency 영향을 0 으로 두고 싶다면, LLM redact 를 trace 저장 후 백엔드 정기 후처리 로 빼는 방식이 가능합니다.

- 동기 경로 (사용자 요청) 는 regex 만 → latency 영향 0
- 백엔드 job (Databricks Jobs scheduler) 이 일 1회 어제 trace 를 모두 가져와 LLM redact 후 업데이트
- 단점: regex 가 못 잡은 PII 가 수 시간 동안 백엔드에 남아 있음 — 감사 정책에 따라 채택 가능성 결정
본 패턴은 향후 과제 (CI 자동 leak 테스트) 와 함께 검토할 수 있습니다.
6. Trace UI 활용
6.1 Experiments 페이지 접근
Databricks 좌측 사이드바 AI/ML → Experiments 메뉴에서 모든 experiment 목록을 확인할 수 있습니다. mlflow.set_experiment() 에서 지정한 경로 (예: /Users/.../mlflow-trace-korean-pii) 의 항목을 클릭해 진입합니다.

6.2 Trace 리스트 탭
Experiment 안에서 Observability → Traces 탭으로 이동하면 실행된 모든 trace 가 시간순으로 리스트됩니다. 각 행에 Trace ID·Request·Response·Tokens·Execution time·Request time·State 가 표시됩니다.

검증 결과 6개 trace 중 위 3개 (최근) 는 [REDACTED:*] 가 적용되어 있고, 아래 3개 (이전) 는 마스킹 적용 전이라 원본이 그대로 노출되어 있습니다. 마스킹 hook 이 운영 첫 배포 직후부터 등록되어야 한다 는 점을 시각적으로 보여주는 사례입니다.
6.3 Span tree 시각화
Trace 한 건을 클릭하면 상세 다이얼로그가 열립니다. Details & Timeline 탭에서 그래프 노드와 span 의 매핑이 시각화됩니다.

좌측 패널의 그래프에서 LangGraph → classify → ChatDatabricks 와 LangGraph → answer → ChatDatabricks 두 경로가 보입니다. 노드를 클릭하면 우측 패널이 해당 span 의 input·output·attributes·events 로 전환됩니다.
6.4 BEFORE 마스킹 — PII 노출 확인
마스킹 hook 없이 실행한 trace 의 Summary 탭입니다. Inputs 의 messages 와 Outputs 모두에 PII 원본 (minjun.kim@example.com, 010-1234-5678, 900101-1234567, 4321-8765-2109-6543) 이 그대로 보입니다.

이 상태에서는 Trace UI 에 접근 권한이 있는 모든 사용자 (해당 워크스페이스 사용자 + 관리자) 가 PII 를 그대로 볼 수 있어, 감사·내부통제 관점에서 위험합니다.
6.5 AFTER 마스킹 — redact 확인
korean_pii_redactor 를 등록한 뒤 실행한 trace 의 Summary 탭입니다. 동일한 위치의 PII 가 모두 [REDACTED:EMAIL], [REDACTED:PHONE_MB], [REDACTED:RRN], [REDACTED:CARD] 로 치환되어 있습니다.

LLM 응답 (Outputs 부분) 도 동일하게 마스킹됩니다. LLM 이 사용자 입력 PII 를 그대로 echo 하는 경우가 흔하기 때문에, input 만 마스킹하면 leak 가 발생 한다는 점을 보여줍니다. korean_pii_redactor 는 set_outputs 도 함께 처리하므로 양방향 안전합니다.
6.6 Trace 검색·필터
Traces 탭 상단의 Filters 버튼을 누르면 컬럼별 필터 (Field/Operator/Value) 를 추가할 수 있습니다. 운영 환경에서 특정 사용자·세션·기간의 trace 만 격리해 보거나, 특정 응답 길이 이상의 trace 만 검토할 때 사용합니다.

상단 자유 검색창 (Search traces by id, request, or response) 으로 PII 잔존 여부를 빠르게 확인할 수도 있습니다 — 예를 들어 01[016789] 패턴을 검색해 매칭되는 trace 가 있으면 마스킹 누락입니다 (운영 leak SOP, §7.5·§8.1 참조).
7. airag-apps (cabin·cargo) 적용
본 절은 source/airag-apps/ke-airag-app 운영 라인의 RAG agent (cabin·cargo) 에 본 가이드를 적용하는 절차입니다.
7.1 RAG agent 진입점 초기화 순서
Databricks Apps 의 RAG agent 코드 진입점 (FastAPI app startup, 또는 main 모듈 import 시점) 에서 다음 순서로 호출합니다.

순서가 반드시 위와 같아야 합니다. span_processors 등록은 첫 graph.invoke() 이전이어야 첫 요청부터 마스킹이 적용됩니다.
7.2 span_processors 등록 시점
FastAPI 기반 앱의 경우:
# main.py (앱 진입점)
import os, mlflow
from fastapi import FastAPI
from .pii import korean_pii_redactor # §5.3 의 함수
app = FastAPI()
@app.on_event("startup")
def setup_tracing():
mlflow.set_experiment(os.environ["MLFLOW_EXPERIMENT_NAME"])
mlflow.langchain.autolog()
mlflow.tracing.configure(span_processors=[korean_pii_redactor])
모듈 import 시점 초기화도 가능하지만, 테스트 가능성을 위해 startup 이벤트가 권장됩니다.
7.3 환경변수
운영 배포 시 다음 환경변수를 설정합니다.
| 변수 | 값 예시 | 설명 |
|---|---|---|
MLFLOW_EXPERIMENT_NAME |
/Users/.../airag-cabin-prod |
cabin/cargo 별로 분리 권장 |
MLFLOW_TRACE_SAMPLING_RATIO |
1.0 |
초기 100% 캡처. 트래픽 증가 시 조절 |
7.4 app.yaml 변경 사항
Databricks Apps app.yaml 에 위 환경변수를 추가하고, Service Principal 의 권한 scope 에 MLflow tracking 권한이 포함되어 있어야 합니다. 신규 배포라면 user_api_scopes 또는 SP grants 갱신이 필요합니다 (향후 과제 user_api_scopes 자동화 후속과 연계).
env:
- name: MLFLOW_EXPERIMENT_NAME
value: /Users/.../airag-cabin-prod
- name: MLFLOW_TRACE_SAMPLING_RATIO
value: "1.0"
7.5 검증 절차 — leak SOP
운영 첫 배포 직후 반드시 다음 검증을 실행합니다.
import re
import mlflow
PII_RE = re.compile(
r"01[016789][- ]?\d{3,4}[- ]?\d{4}" # 휴대전화
r"|\d{6}[- ]?[1-4]\d{6}" # 주민번호
r"|\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}" # 카드
r"|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}" # 이메일
)
df = mlflow.search_traces(
experiment_ids=[EXPERIMENT_ID],
max_results=100,
order_by=["timestamp DESC"],
)
leaks = []
for _, row in df.iterrows():
trace = mlflow.get_trace(row["trace_id"])
for span in trace.data.spans:
blob = str(span.inputs) + str(span.outputs)
if PII_RE.search(blob):
leaks.append((row["trace_id"], span.name))
assert not leaks, f"PII leak detected: {leaks}"
print(f"OK: {len(df)} traces inspected, no leak")
- 운영 첫 배포 후 1회 즉시 실행
- 마스킹 규칙 변경 시 매번 실행 (§8.2 회귀 검증)
- 가능하면 CI 파이프라인에 통합 (
향후 과제CI 자동 leak 테스트 후속)
7.6 Feedback 통합
피드백 통합 가이드 (Draft) 의 MLflow Assessment 는 trace_id 로 trace 와 연결됩니다. 사용자가 thumbs down 을 누르면 해당 trace 의 마스킹된 input·output 을 Trace UI 에서 확인할 수 있어, PII 노출 없이 응답 품질 문제를 디버깅할 수 있습니다. 상세 흐름은 피드백 통합 가이드 를 참조해주세요.
8. 운영 적용
8.1 leak 검증 SOP
§7.5 의 leak 검증 스크립트를 다음 시점에 실행합니다.
- 운영 첫 배포 직후 1회 즉시 (필수)
- 마스킹 규칙 변경 후 1회 (§8.2 와 함께)
- 정기 (주간·월간) 회귀 검증
- 신규 PII 카테고리 추가 시
검증 실패 (leak 발견) 시 즉시 마스킹 규칙을 보강하고, 누출된 trace 는 MLflow API 로 삭제하거나 redact 처리합니다.
8.2 마스킹 규칙 변경 후 회귀 검증
KO_PII_PATTERNS 에 신규 패턴을 추가하거나 기존 패턴을 수정한 뒤에는 다음을 확인합니다.
- 신규 패턴에 매칭되는 샘플 데이터로 trace 1건 생성
mlflow.search_traces로 해당 trace 조회 → 마스킹 적용 확인- 기존 패턴 회귀 — §7.5 SOP 전체 재실행
8.3 샘플링
트래픽이 많을 때는 MLFLOW_TRACE_SAMPLING_RATIO 환경변수로 trace 비율을 조절할 수 있습니다.
| 트래픽 | 권장 비율 |
|---|---|
| 개발·PoC | 1.0 (100%) |
| 초기 운영 | 1.0 (감사·디버깅 우선) |
| 안정화 후 | 0.1 ~ 0.3 (10~30%) |
| 부하 테스트 | 0.0 (off) |
샘플링은 trace 생성 단계 에서 제어되며, 마스킹은 생성된 trace 에만 적용됩니다.
8.4 회전·알람
korean_pii_redactor 함수 안의 try/except 는 마스킹 실패가 trace 자체를 깨뜨리지 않도록 swallow 합니다. 운영에서는 다음을 추가하세요.
print(...)를 logger.error 또는 CloudWatch metric publish 로 교체- redactor 실패 metric 이 임계치 초과 시 PagerDuty/Slack 알람
- regex 패턴 자체는 컨테이너 이미지에 포함 — 변경 시 재배포로 회전
8.5 알려진 한계
regex 기반 마스킹은 형식이 명확한 PII 에는 강하지만, 다음 영역은 잡지 못합니다.
- 한국어 인명 (
김민준,이서영등) — 보통 명사와 구분이 어려움 - 주소 (
서울시 강남구 ...) — 형식이 다양하고 보통 명사와 혼재 - 사번·회원번호 — 형식이 조직마다 다름
- 위치 정보 — 의미적 식별 필요
- 음성 전사 텍스트 — 띄어쓰기·표기 불일치
이 영역은 다음 두 방향으로 보완 가능합니다.
- 즉시 적용 가능: §5.6 LLM 기반 마스킹 (Databricks FMAPI Haiku 4.5) — 인명·주소·사번 등 평문 컨텍스트 PII 를 정확도 높게 잡음
- 후속 검토:
향후 과제(Microsoft Presidio + KoBERT NER) — 자체 모델 호스팅 패턴으로 LLM 호출 의존성 제거 가능
본 가이드 검증 환경 (§부록 A) 에서는 §5.2 의 regex 5종까지만 적용했습니다. 한국어 인명·주소까지 마스킹이 필요한 운영 환경은 §5.6 의 2단계 redact 구조 채택을 권장합니다.
9. 트러블슈팅
9.1 search_traces() got an unexpected keyword argument 'experiment_names'
증상:
mlflow.search_traces(experiment_names=[...])
# TypeError: search_traces() got an unexpected keyword argument 'experiment_names'
원인: MLflow 3.x 부터 experiment_names 인자는 experiment_ids 로 변경되었습니다.
해결:
EXPERIMENT = mlflow.set_experiment(EXPERIMENT_PATH)
EXPERIMENT_ID = EXPERIMENT.experiment_id
mlflow.search_traces(experiment_ids=[EXPERIMENT_ID]) # OK
9.2 Trace 가 UI 에 보이지 않음
증상: graph.invoke() 가 정상 실행됐으나 Traces 탭에 아무것도 없음.
확인 항목:
mlflow.langchain.autolog()가 호출됐는지mlflow.set_experiment(path)의 path 가 정확한지 (대소문자, 슬래시)- Experiment 권한 — Service Principal 또는 사용자가 해당 experiment 에
CAN_EDIT권한이 있는지 - 네트워크 — MLflow Tracking 서버 (Databricks 백엔드) 로의 outbound 허용 여부
9.3 마스킹이 일부만 적용됨
증상: 일부 trace 는 [REDACTED:*] 가 잘 들어갔는데, 다른 trace 는 PII 원본이 남아 있음.
원인 1: mlflow.tracing.configure(span_processors=...) 호출이 첫 graph.invoke() 이후에 일어남.
해결: 앱 startup hook 으로 옮기고, 모듈 import 시점에 invoke 가 되지 않도록 보장.
원인 2: 등록 후 다른 코드가 mlflow.tracing.reset() 또는 mlflow.tracing.configure(span_processors=[]) 를 호출해 hook 을 비웠을 수 있음.
해결: grep 으로 mlflow.tracing.configure 호출처 전부 확인 후 단일 진입점으로 통일.
9.4 Korean 인명 마스킹이 안 됨
증상: 김민준 같은 한국 이름이 [REDACTED:*] 로 치환되지 않음.
원인: §8.5 알려진 한계. regex 만으로는 인명을 안전하게 식별 불가.
해결:
- 단기: 시스템 프롬프트에서 LLM 에게 "이름 등 추가 PII 를 echo 하지 마라" 요청
- 중기: Presidio 의 Korean recognizer 또는 KoBERT NER 모델 추가 (
향후 과제) - 장기: 입력 단계 (UI/API gateway) 에서 PII 입력 자체를 차단·경고
9.5 LLM redactor 가 무한 재귀로 hang
증상: §5.6 의 korean_pii_redactor_v2 적용 후 응답이 돌아오지 않거나, 한 trace 안에 동일한 LLM 호출 span 이 수십~수백 개 중첩되어 보임.
원인: mlflow.langchain.autolog() 가 활성화된 상태에서 redactor 안의 pii_llm.invoke() 호출이 또 span 을 생성하고, 그 span 에 다시 redactor 가 호출되는 무한 재귀.
해결: redactor 안의 LLM 호출을 mlflow.tracing.disable() 컨텍스트 매니저로 감싸기.
from mlflow.tracing import disable as tracing_disable
def llm_redact(text: str) -> str:
with tracing_disable():
resp = pii_llm.invoke(...)
return resp.content.strip()
자세한 패턴은 §5.6.3 참조.
9.6 Trace 크기 한도 초과
증상: 매우 긴 input 또는 attachment 가 포함된 span 에서 attribute too large 류 경고 또는 truncation.
원인: OpenTelemetry span attribute 의 기본 한도 (보통 32KB).
해결:
korean_pii_redactor안에서 길이 truncate 함께 적용 (예:text[:30000])- 첨부물·이미지는 별도 저장소에 두고 trace 에는 reference 만 기록
- LangGraph state 안에 큰 binary 데이터를 두지 말 것
10. 참고 자료
MLflow 공식 문서
- Redacting Sensitive Data from Traces | MLflow
- mlflow.tracing API Reference
- PR #16344 — Support PII masking via custom span post-processor
- Tracing FAQ | MLflow
📚 시리즈 — Databricks LLMOps
- ① AI Gateway — LLM 폴백·게이트웨이 프록시
- ② Prompt Registry — 프롬프트 버전·alias 관리
- ③ MLflow Tracing — LangGraph 관측·PII 마스킹 — 현재 글
'Databricks > MLflow' 카테고리의 다른 글
| Databricks LLMOps ② Prompt Registry — UC 기반 프롬프트 버전·alias 관리 (0) | 2026.07.06 |
|---|
댓글