MINERVA Phase C-5 — 대화 로깅 설계 (구조화된 발화 수집과 저장)

발화·세션·대화·질의 4계층 단위, raw·structured·feature 3계층 스키마, hot·warm·cold 3계층 저장 — 후속 분석의 토대

Phase C-5의 모든 분석(C21 의도·토픽, C22 품질 평가, C23 피드백 루프)이 한 가지를 가정한다 — 대화가 잘 로깅되어 있다. 본 편은 발화 단위 정의(turn·session·conversation·query), 스키마 3계층(raw·structured·feature), 저장 계층(hot·warm·cold), PII 처리·보존 정책, 인덱싱·검색 인프라, MINERVA 적용을 정리한다. C20 자체가 다른 시스템의 의존성이라 설계 결정 하나가 다운스트림 전체의 한계가 된다.

Agent
저자

Kwangmin Kim

공개

2026년 05월 06일

1 왜 구조화된 로깅인가

Phase C-5는 LLM Agent가 누적한 대화에서 인사이트를 뽑는 단계다. C21~C23이 잘 돌려면 C20이 잘 설계되어 있어야 한다 — 분석 단계에서 누락 필드를 메우거나 raw 텍스트만으로 추정하는 것은 비용이 폭발한다.

다운스트림 C20에서 필요한 것
C21 의도 분류·토픽 query·response 텍스트 + 시간순서 + 사용자 segment
C22 응답 품질 평가 thumbs_up·feedback 텍스트 + run_id + retrieval citations
C23 피드백 루프 run_id로 prompt·model·reranker 추적 + 결과 매핑
C16 Bandit run_id·arm·reward (delayed feedback 매칭)
C19 실험 분석 exp_id·arm·user_id·timestamp + primary/guardrail metrics
보안·감사 user_id·timestamp·tool_calls·data_accessed

이 모든 요구사항을 만족하는 단일 스키마를 설계하는 것이 본 편의 목표.

2 발화 단위 — 4계층

계층:
- query      = 한 번의 질문·답변 쌍 (turn)
- session    = 같은 사용자의 연속된 질의 (timeout 30분 등)
- conversation = 한 주제로 묶인 N개 query (사용자 명시 또는 추론)
- user_history = 한 사용자의 전체 누적 대화

각 계층의 사용처:

계층 사용
query A/B·Bandit 보상 단위, 검정 단위, 의도 분류 단위
session 사용자 행동 패턴, RFM 추출
conversation 다중 turn 대화 품질, 멀티스텝 플래닝 평가
user_history 개인화·세그멘테이션 토대

스키마는 query 단위가 기본 — 다른 계층은 query를 group_by로 만든다.

3 Raw·Structured·Feature 3계층 스키마

3.1 Raw — 원본 그대로

# app/logging/raw.py — 모든 입력·출력의 정직한 사본
from pydantic import BaseModel
from datetime import datetime
from typing import Any


class RawLog(BaseModel):
    log_id: str                          # UUID
    timestamp: datetime
    log_type: str                        # "user_query" | "llm_response" | "tool_call" | "feedback"
    payload: dict[str, Any]              # 원본 — 가공 없음
    correlation_id: str                  # 같은 query의 모든 raw log를 묶는 ID

raw는 절대 수정하지 않는다 — 손실 없는 원본 보존.

3.2 Structured — 분석 친화적

class StructuredQuery(BaseModel):
    run_id: str                          # query 단위 단일 ID
    user_id: str
    session_id: str
    timestamp: datetime

    # 질의
    query_text: str
    query_lang: str                      # "ko" | "en" | ...
    query_tokens: int

    # 응답
    response_text: str
    response_tokens: int
    latency_ms: int

    # 시스템 결정
    model: str                           # "gpt-4o-2024-08-06"
    prompt_version: str                  # "v3.2"
    reranker_id: str | None
    retrieval_collections: list[str]
    retrieved_doc_ids: list[str]

    # 실험 컨텍스트
    experiments: dict[str, str]          # {"exp_001": "treatment", "exp_007": "control"}
    segment: dict[str, str]              # C17 세그먼트 라벨

    # 비용·관측성
    cost_usd: float
    trace_id: str | None                 # OpenTelemetry trace

    # 피드백 (delayed)
    thumbs_up: int | None = None         # null = 아직 안 옴
    feedback_text: str | None = None
    feedback_timestamp: datetime | None = None

이 스키마가 분석 파이프라인의 단일 진실이다. raw → structured 변환은 ETL job (정해진 시점에 실행).

3.3 Feature — 머신러닝 친화적

class FeatureRow(BaseModel):
    run_id: str
    user_id: str
    timestamp: datetime

    # 사용자 임베딩
    user_embedding: list[float]          # 64 dim
    segment_one_hot: list[int]            # 26 dim (C17 결과)

    # 질의 임베딩·메타
    query_embedding: list[float]          # 768 dim
    query_intent_class: str               # C21 의도 분류 결과
    query_topic_cluster: int              # C21 토픽 클러스터

    # 응답 품질 신호
    citation_count: int
    has_disclaimer: bool
    response_quality_score: float | None  # C22 자동 평가

    # 보상 (Bandit·실험용)
    reward: float | None                  # thumbs_up 또는 implicit signal

feature 계층은 매일·매시간 갱신되며 모델 학습에 직접 들어가는 형태다.

4 저장 3계층 — Hot·Warm·Cold

[ Hot 7일 ]    실시간 모니터링·Bandit feedback
   PostgreSQL · Redis stream
        ↓ (daily ETL)
[ Warm 90일 ]  분석·실험 보고서·세그먼트 갱신
   ClickHouse · BigQuery · DuckDB
        ↓ (daily ETL)
[ Cold 1년+ ]  감사·재현·대규모 retraining
   S3 · Azure Blob (Parquet)
계층 형식 인덱스 사용
Hot row store (PG·Redis) run_id·user_id·exp_id 실시간 조회
Warm column store (Clickhouse·BQ·DuckDB) timestamp·exp_id OLAP 분석
Cold object store + Parquet hive partition (날짜·exp_id) 대규모 batch

3계층 분리 이유: - Hot은 쓰기 최적, 분석 쿼리는 비효율 - Warm은 read-heavy OLAP — column store + 압축으로 비용 절감 - Cold는 장기 보관 — 가격 가장 저렴, 접근은 느려도 무방

MINERVA 규모(분기당 수백만 query)에서 단일 PG로 모두 처리하면 인덱스 비대해지고 백업·복원 비용 폭증. 분리가 필수.

5 PII 처리·보존 정책

5.1 식별

필드 카테고리 처리
user_id 식별자 hashing 또는 surrogate key
query_text 사용자 콘텐츠 PII 탐지·마스킹
response_text 시스템 출력 모델이 PII 노출 가능 — 동일 처리
feedback_text 사용자 콘텐츠 동일
user_embedding 파생 역추적 가능 — 식별자로 분류

5.2 마스킹

# app/logging/pii.py — 정규식 + LLM 결합
import re

PATTERNS = {
    "email": r"[\w\.-]+@[\w\.-]+\.\w+",
    "phone": r"\d{2,3}-\d{3,4}-\d{4}",
    "credit_card": r"\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}",
    "ssn_kr": r"\d{6}-\d{7}",
}


def mask_pii(text: str) -> str:
    for name, pattern in PATTERNS.items():
        text = re.sub(pattern, f"[{name.upper()}]", text)
    return text


def mask_pii_llm(text: str) -> str:
    """LLM 기반 — 정규식이 못 잡는 인명·주소·고유명사."""
    prompt = f"다음 텍스트에서 인명·주소·계좌·전화 등 식별 가능 정보를 [MASKED]로 치환:\n{text}"
    return llm_call(prompt)

raw 계층은 마스킹 전·후 둘 다 보관 (감사 시 원본 필요). structured·feature는 마스킹 후만.

5.3 보존 기간

# config/retention.yaml
raw:
  retention_days: 30                # 짧게 — PII 위험 최소화
  encryption: aes_256
  access_log: true                   # 누가 raw 접근했는지

structured:
  retention_days: 730                # 2년 — 실험·분석에 충분
  pii_masked: true

feature:
  retention_days: 1095               # 3년 — 모델 retraining
  pii_masked: true
  embeddings_only: true              # raw 텍스트 없음

cold_anonymized:
  retention_days: -1                 # 무기한
  user_id: hashed                    # 재식별 불가능 수준

GDPR·국내 개인정보법 — 사용자 삭제 요청 시 모든 계층에서 제거하는 절차 필수. 이를 위해 user_id가 모든 계층에서 추적 가능해야 한다.

6 인덱싱과 검색

6.1 시계열·OLAP

-- ClickHouse warm storage
CREATE TABLE structured_query (
    run_id String,
    timestamp DateTime,
    user_id String,
    session_id String,
    query_text String,
    response_text String,
    model LowCardinality(String),
    prompt_version LowCardinality(String),
    cost_usd Float32,
    latency_ms UInt32,
    thumbs_up Nullable(Int8),
    experiments Map(String, String),
    segment Map(String, String)
) ENGINE = MergeTree
ORDER BY (timestamp, user_id)
PARTITION BY toYYYYMM(timestamp);

Map·LowCardinality로 실험 컨텍스트와 모델 메타를 효율 저장. 일반 OLAP 쿼리(시간별 latency·실험별 thumbs_up·세그먼트별 cost)가 ms 수준.

6.2 검색 (Full-Text + Vector)

# Elasticsearch / OpenSearch 또는 PG full-text
# query_text·response_text·feedback_text 검색용

# 또는 vector store (Qdrant·Pinecone·Milvus)
# 의미 기반 — "이 사용자가 한 질문과 비슷한 다른 사용자 질문 찾기"

C21 토픽 클러스터링은 vector store에서 효율적. C22 응답 품질 평가는 full-text + 정규식 결합.

7 다운스트림 시스템 통합

7.1 Bandit feedback (C16)

# delayed feedback이 도착하면 Bandit update
async def on_feedback_received(run_id: str, thumbs_up: int):
    log = await db.structured.find_one(run_id=run_id)
    if log.experiments:
        for exp_id, arm in log.experiments.items():
            await bandit_router(exp_id).update(arm, log.context, reward=thumbs_up)

    await db.structured.update(run_id, {"thumbs_up": thumbs_up,
                                          "feedback_timestamp": datetime.utcnow()})

7.2 실험 분석 (C19)

# C19 단계 7 자동 분석이 warm storage 직접 쿼리
def analyze_experiment(exp_id: str) -> dict:
    df = clickhouse.query(f"""
        SELECT user_id, segment['department'] as dept,
               experiments['{exp_id}'] as arm,
               thumbs_up, latency_ms, cost_usd
        FROM structured_query
        WHERE has(experiments, '{exp_id}')
          AND timestamp >= '...' AND timestamp < '...'
    """)
    return run_analysis_pipeline(df)

7.3 세그먼트 갱신 (C17)

# 매일 새벽 — 행동 feature 재계산
def daily_segment_refresh():
    df = clickhouse.query("SELECT user_id, count(), avg(thumbs_up), ... FROM ... GROUP BY user_id")
    for user_id, row in df.iterrows():
        update_segment(user_id, behavior=behavior_bucket(row))

8 MINERVA 적용

app/logging/
├── raw.py                  # raw schema + write to PG/Redis
├── structured.py            # structured schema + ETL from raw
├── feature.py               # feature schema + daily build
├── pii.py                   # 마스킹·정규식·LLM
├── retention.py             # 정책·삭제 job
└── correlation.py           # OpenTelemetry trace_id

scripts/
├── etl_raw_to_structured.py     # 매시간 cron
├── etl_structured_to_feature.py  # 매일 cron
└── retention_cleanup.py          # 매일 cron — 만료 row 삭제

infra/
├── postgres                 # hot
├── clickhouse               # warm
└── azure_blob               # cold (Parquet)

06편 JSONL 메트릭이 raw 계층 일부, 08-1 streaming observability가 trace_id 연결. 본 편은 그 위에 표준화된 분석 토대를 얹는 것.

9 자주 발생하는 함정

9.1 Sampling Bias

스토리지 비용 회피로 1% sampling만 저장 → 작은 세그먼트·드문 의도가 누락.

해법: - 세그먼트별·의도별 stratified sampling - 모든 실험 컨텍스트는 항상 100% 보관 - raw 30일 + structured 100% 2년이 비용·가치 균형점

9.2 Async Loss

LLM 응답이 비동기로 stream 중 timeout·연결 끊김 → 일부 token 로깅 누락.

해법: - 매 token마다 raw write (Redis Stream) - ETL이 is_complete flag 검증 - 불완전한 query는 별도 카테고리로 분석 (응답 품질 평가에서 제외)

9.3 Schema Drift

스키마를 자주 바꾸면 OLAP 쿼리·다운스트림 시스템이 깨짐.

해법: - Schema Registry (예: Avro·Protobuf) + 버전 관리 - 신규 필드는 nullable로 추가 - 제거는 deprecation 단계 (6개월 후 진짜 제거)

9.4 Trace 끊김

OpenTelemetry trace_id가 일부 단계에서 누락 → 분석 시 같은 query의 단계별 latency 추적 불가.

해법: - BaseAgent 계약(02-0편)이 trace_id 전파를 강제 - ETL에서 trace 누락 row를 알림

9.5 Storage Cost Explosion

raw 텍스트를 모두 PG에 저장 → 90일 후 TB 단위, vacuum·backup 부담.

해법: - raw는 30일 PG (또는 Redis Stream + 30일 후 만료) → 그 이후는 cold만 - structured는 column store에 압축 보관 - 임베딩은 vector store에 별도 (PG에 768 dim float를 행마다 저장하면 폭증)

10 정리

영역 핵심
단위 query·session·conversation·user_history 4계층
스키마 raw (원본)·structured (분석)·feature (ML) 3계층
저장 hot 7일 · warm 90일 · cold 1년+
PII raw에 원본·마스킹 둘 다, structured 이상은 마스킹만
보존 카테고리별 retention.yaml, GDPR 사용자 삭제 절차
인덱스 OLAP은 ClickHouse, 검색은 ES + vector store
다운스트림 Bandit feedback·실험 분석·세그먼트 갱신·품질 평가의 단일 토대
함정 sampling bias·async loss·schema drift·trace 끊김·storage 폭발

11 응용 분야

시나리오 본 편 절
모든 query가 어느 실험에 속했는지 사후 추적 structured.experiments 필드 + warm storage
사용자 삭제 요청 처리 retention.py + 모든 계층 삭제 절차
의도 분류 학습 데이터 추출 feature 계층 + query_intent_class 라벨
응답 품질 자동 평가 structured·feature + C22 evaluator
비용 분석 (모델·세그먼트별) warm OLAP + cost_usd column
인시던트 사후 분석 trace_id로 raw 단계별 추적

12 관련 주제

선행 학습 (선수)

후속 (Phase C-5)

Cross-reference (Engineering)

Subscribe

Enjoy this blog? Get notified of new posts by email: