MINERVA Phase C-4 — 개인화 전략 (프롬프트·스타일·지식 범위 조정)

세그먼트별 처치를 3축(콘텐츠·표현·범위)으로 분리하고 우선순위·Override·실험 가능 단위로 묶는다

C17 세그멘테이션이 “사용자를 어떻게 분류할지”였다면, 개인화는 “분류 결과로 무엇을 다르게 할지”이다. 본 편은 개인화의 3축(시스템 프롬프트·응답 스타일·지식 범위), 분기 방식(rule·embedding·학습 기반), Override 우선순위와 Inheritance, A/B와의 결합, over-personalization·filter bubble·privacy 함정을 정리한다. MINERVA 운영에서 개인화도 실험 대상이라는 관점이 핵심.

Agent
저자

Kwangmin Kim

공개

2026년 05월 06일

1 왜 개인화인가

C17 세그멘테이션이 “누구인지”를 정의했다면, 개인화는 “그 사람에게 무엇을 다르게 할지”이다. 사용자 한 명에게 시스템이 결정하는 것은 사실 매우 많다.

한 질의가 처리될 때 개인화될 수 있는 결정 ─────
├── 시스템 프롬프트 — 어떤 페르소나·제약·지침을 줄 것인가
├── few-shot 예시 — 어떤 예시로 응답 스타일을 유도할 것인가
├── 응답 형식 — markdown·plain·표·코드 우선순위
├── 응답 길이 — 짧은 요약 vs 자세한 설명
├── 어조 — formal·casual·step-by-step
├── 검색 범위 — 어떤 컬렉션·문서 그룹에서 검색할까
├── 도구 권한 — 어떤 툴을 사용 가능하게 할까 (read-only vs write)
├── HITL 임계값 — 언제 사람 검토를 요구할까
└── 라우팅 — 어떤 모델·reranker·프롬프트 변형을 쓸까  (C16의 영역)

이 모든 것을 사용자별로 직접 결정하는 것은 비현실적이다. 본 편은 이 결정들을 3축으로 묶고, 세그먼트가 각 축의 값을 결정하는 방식을 정리한다.

2 개인화의 3축

개인화의 3축
├── 콘텐츠 (Content) — 시스템 프롬프트, few-shot, 도메인 지침
├── 표현 (Expression) — 어조, 길이, 포맷, 코드 우선순위
└── 범위 (Scope) — 검색 컬렉션, 도구 권한, HITL 임계

세 축이 직교다. 같은 사용자에게 “프롬프트는 R&D용 + 스타일은 짧고 정량적 + 범위는 internal docs only”가 동시 적용된다.

3 축 1 — 콘텐츠 (System Prompt)

세그먼트별 시스템 프롬프트를 카탈로그로 관리.

# config/personalization/system_prompts.yaml
default: |
  너는 MINERVA, 사내 지식 어시스턴트다. 정확하고 간결하게 답한다.

departments:
  sales: |
    너는 Sales 부서를 위한 어시스턴트다.
    제품·가격·고객 사례 위주로 답한다. 기술적 깊이보다 핵심 메시지에 집중한다.
    근거는 출처와 함께 제시한다.

  rnd: |
    너는 R&D 엔지니어를 위한 어시스턴트다.
    기술적 정확도가 최우선이다. 가능하면 식·코드·논문 인용을 포함한다.
    추정과 사실을 명확히 구분해 표시한다.

  support: |
    너는 고객 Support를 위한 어시스턴트다.
    먼저 고객 컨텍스트(증상·환경)를 파악할 수 있는 질문을 제안하고,
    재현·격리·해결의 단계로 답한다.

roles:
  manager: |
    경영진/매니저를 위한 응답이다. 의사결정에 필요한 요점·리스크·다음 단계를 강조한다.
    숫자는 신뢰구간과 함께 제시한다.

lifecycle:
  onboarding: |
    이 사용자는 onboarding 단계다. 답변에 사용 팁·예시 질의·관련 가이드 링크를 함께 제시한다.
# app/personalization/system_prompt.py
def resolve_system_prompt(segment: dict) -> str:
    catalog = load_catalog()                   # YAML
    parts = [catalog["default"]]

    if dept := segment.get("department"):
        parts.append(catalog["departments"].get(dept, ""))
    if role := segment.get("role"):
        parts.append(catalog["roles"].get(role, ""))
    if stage := segment.get("lifecycle_stage"):
        parts.append(catalog["lifecycle"].get(stage, ""))

    return "\n\n".join(p for p in parts if p)

3.1 Few-shot 카탈로그

같은 시스템 프롬프트라도 few-shot 예시가 응답 스타일을 결정적으로 잡는다.

# config/personalization/fewshot.yaml
fewshot:
  rnd_engineer:
    - q: "코사인 유사도와 dot product의 차이는?"
      a: |
        코사인 유사도는 벡터 방향만, dot product는 방향+크기를 함께 본다.
        정규화된 임베딩에서는 두 값이 비례 관계: $\\cos(\\theta) = \\vec{a} \\cdot \\vec{b} / (\\|a\\|\\|b\\|)$.
        실무에서는 정규화 후 dot product로 통일하는 경우가 많다.
  sales_manager:
    - q: "올해 우리 제품의 가장 큰 차별점은?"
      a: |
        세 가지: (1) 응답 latency p95 < 2s, (2) 사내 문서 자동 인덱싱,
        (3) 부서별 권한 분리. 자세한 비교는 [경쟁사 분석 1Q] 참고.

4 축 2 — 표현 (Style)

응답 형식·길이·어조를 명시 파라미터로 분리.

# app/personalization/style.py
from pydantic import BaseModel
from typing import Literal


class ResponseStyle(BaseModel):
    tone: Literal["formal", "casual", "concise"] = "concise"
    length: Literal["short", "medium", "long"] = "medium"
    format: Literal["markdown", "plain", "table_first"] = "markdown"
    code_priority: Literal["high", "medium", "low"] = "medium"
    include_citations: bool = True
    include_disclaimer: bool = False           # 추정·불확실성 표시


STYLE_RULES = {
    ("sales", "manager"): ResponseStyle(tone="concise", length="short", format="table_first"),
    ("rnd", "engineer"): ResponseStyle(tone="formal", length="medium", code_priority="high"),
    ("support", "agent"): ResponseStyle(tone="formal", length="medium", include_citations=True),
}


def resolve_style(segment: dict) -> ResponseStyle:
    key = (segment.get("department"), segment.get("role"))
    return STYLE_RULES.get(key, ResponseStyle())

스타일은 system prompt에 자연어로 주입:

def render_prompt(segment: dict, query: str) -> str:
    style = resolve_style(segment)
    base = resolve_system_prompt(segment)
    style_instructions = (
        f"응답 길이: {style.length}. 어조: {style.tone}. 형식: {style.format}. "
        f"코드 우선도: {style.code_priority}."
    )
    return f"{base}\n\n{style_instructions}\n\n질문: {query}"

5 축 3 — 범위 (Scope)

검색 범위·도구 권한·HITL 임계값을 결정.

# app/personalization/scope.py
class AccessScope(BaseModel):
    knowledge_collections: list[str]            # ["internal", "products"]
    tool_allow: list[str]                        # ["search", "calc", "summarize"]
    tool_deny: list[str] = []                    # ["delete_doc", "send_email"]
    hitl_confidence_threshold: float = 0.6       # 이 이하면 사람 검토
    max_tokens: int = 4000


SCOPE_RULES = {
    "rnd": AccessScope(
        knowledge_collections=["internal", "patents", "papers"],
        tool_allow=["search", "calc", "code_exec"],
        hitl_confidence_threshold=0.4,           # 정확도 우선이라 더 자주 검토 요청
    ),
    "sales": AccessScope(
        knowledge_collections=["products", "case_studies", "pricing"],
        tool_allow=["search", "summarize"],
        tool_deny=["price_modify", "discount_grant"],
    ),
    "support": AccessScope(
        knowledge_collections=["faqs", "tickets", "products"],
        tool_allow=["search", "ticket_create", "ticket_update"],
        hitl_confidence_threshold=0.7,
    ),
}

이 scope가 04편 FastAPI 라우터10편 에러 전파의 도구 호출 단계에 강제된다 — 권한 위반은 보안 이슈로 즉시 차단.

6 분기 방식 — Rule·Embedding·학습

6.1 Rule 기반 (가장 단순)

위 예제 모두 — (department, role) 키 lookup. 라벨 직관적이고 디버깅 쉬움. 변경은 YAML 편집.

6.2 Embedding 기반 (semantic)

명시 라벨이 부족하면 사용자 임베딩 → 가장 가까운 페르소나 라벨 매칭.

def match_persona_by_embedding(user_topic_centroid, persona_centroids):
    sims = [cosine(user_topic_centroid, p) for p in persona_centroids]
    return argmax(sims)

6.3 학습 기반

C16 Bandit 위에 개인화도 arm으로 등록. 같은 사용자에게 두 개의 시스템 프롬프트 변형 중 어느 것의 thumbs_up이 더 좋은지 자동 학습.

# 개인화도 라우팅의 한 차원
arms = ["prompt_v1_concise", "prompt_v2_verbose", "prompt_v3_step_by_step"]
router = LinUCB(n_arms=3, n_features=26)         # context는 C17의 segment 벡터

이렇게 되면 개인화 카탈로그가 검증 단위가 된다 — 새 페르소나 도입 시 Bandit으로 explore.

7 Override 우선순위와 Inheritance

같은 사용자에게 여러 라벨이 중첩되면 어느 것이 우선인가?

# Inheritance 순서 (default → 가장 specific)
PRECEDENCE = [
    "default",
    "department",
    "role",
    "behavior_bucket",
    "topic_cluster",
    "lifecycle_stage",
    "user_override",          # 가장 우선 — 사용자가 설정에서 직접 선택
]

각 레벨은 앞 레벨을 덮어쓴다. user_override가 최우선 — 사용자 자율을 보장.

def resolve(segment: dict) -> dict:
    result = {}
    for level in PRECEDENCE:
        update = catalog.get(level, {}).get(segment.get(level))
        if update:
            result.update(update)
    return result

7.1 사용자 Override

# 사용자 설정 페이지에서 변경 가능
user_overrides:
  user_42:
    style:
      length: long              # default보다 긴 응답 선호
      include_citations: true

이 override는 DB에 저장되고 모든 자동 분기를 무력화한다. 자동 시스템이 “이 사람에게 짧게 답해야 한다”고 결정해도 사용자가 길게 원한다고 했으면 길게 답한다.

8 A/B 테스트와의 결합

개인화도 가설이고 검증해야 한다.

# experiments/exp_007.yaml — 페르소나 차별화 실험
hypothesis: |
  R&D 엔지니어에게 step-by-step 어조보다 formal+코드 우선이 thumbs_up_rate +5pp.
metrics:
  primary: thumbs_up_rate
  guardrail: [p95_latency_ms, response_token_count]
arms:
  - prompt_v_step_by_step
  - prompt_v_formal_code_first
audience:
  segment: rnd_engineer        # C17 세그먼트로 audience 제한
duration_days: 14

audience 제한이 핵심: rnd_engineer 세그먼트 안에서만 A/B → Simpson’s paradox 회피 + 통계 검정력 효율적.

22편 통계 절차 그대로 적용. 결과 좋은 arm을 personalization 카탈로그에 commit.

9 자주 발생하는 함정

9.1 Over-personalization

세그먼트 12개 × 스타일 4개 × 범위 3개 = 144개 조합. 각 조합당 사용자 50명 → 통계적으로 검증 불가.

해법: - 축별로 독립 검증 — content 분기 검증, style 분기 검증을 따로 - 작은 세그먼트에는 상위 fallback (rnd_junior가 부족하면 rnd 일반 페르소나) - 새 분기 도입 시 사용자 수 임계 (예: ≥500명 미만이면 도입 보류)

9.2 Filter Bubble

세그먼트별 검색 범위 분리가 강하면 사용자가 다른 부서 지식에 노출되지 않는다. R&D가 영업·재무 컨텍스트를 모르게 됨.

해법: - knowledge_collections에 항상 default 컬렉션 포함 (기본 사내 공지·정책) - 사용자가 명시 요청 시 다른 부서 지식 접근 (UI에 “다른 부서 자료 포함” 토글)

9.3 Privacy Leak via Personalization

사용자 A의 행동 데이터로 학습된 페르소나가 사용자 B에게 그대로 적용 → A의 패턴 노출 위험.

해법: - 개인화는 세그먼트 수준에서만 학습. 개별 사용자 ID는 학습에 들어가지 않음 - 작은 세그먼트는 k-anonymity 보장 (k=10 이상) - 사용자 override는 본인에게만 적용·저장

9.4 Personalization Drift

세그먼트는 자동 갱신되지만 페르소나 카탈로그는 사람 작성. 시간 흐르면 라벨과 실제 행동이 어긋남.

해법: - 분기마다 세그먼트별 thumbs_up_rate 모니터링 - 카탈로그 업데이트도 PR 리뷰 절차 (단순 YAML 수정도 거버넌스 대상)

9.5 Cold User Default

신규 사용자는 행동 데이터 없음 → 모든 학습 기반 분기 fallback. 충분히 좋은 default가 보장되어야.

해법: - onboarding lifecycle stage 별도 페르소나 카탈로그 - 첫 5~10 질의는 명시적 동의 후 페르소나 학습 시작

10 MINERVA 적용

# app/personalization/__init__.py
from app.personalization.system_prompt import resolve_system_prompt
from app.personalization.style import resolve_style
from app.personalization.scope import resolve_scope


def render_request(user_id: str, query: str) -> dict:
    segment = load_segment(user_id)
    return {
        "system": resolve_system_prompt(segment),
        "style": resolve_style(segment),
        "scope": resolve_scope(segment),
        "query": query,
    }

02-1 BaseAgent v2Query 모델에 personalization 필드 추가, agent가 system prompt·스타일·scope를 모두 반영.

class Query(BaseModel):
    text: str
    user_id: str
    personalization: dict | None = None       # render_request 결과

11 정리

영역 핵심
3축 Content (system prompt) · Expression (style) · Scope (검색·도구)
분기 방식 Rule (단순) · Embedding (semantic) · Bandit (학습)
우선순위 default → 세그먼트 차원들 → user_override (최우선)
Override 사용자가 자동 분기를 무력화할 수 있음 (자율 보장)
카탈로그 YAML로 관리, PR 리뷰, A/B로 검증
함정 Over-personalization·Filter Bubble·Privacy·Drift·Cold User
Bandit 결합 페르소나 변형도 arm — explore + 자동 수렴

12 응용 분야

시나리오 활용 축
R&D vs Sales 전혀 다른 응답 Content (system prompt 분기)
매니저 vs 엔지니어 길이 차이 Expression (length·format)
Sales가 가격 데이터에 read-only Scope (tool_deny)
Onboarding 사용자 가이드 강화 Content (lifecycle 페르소나)
사용자가 “더 자세히” 선호 표시 user_override
새 페르소나 도입 시 검증 A/B audience=segment + Bandit

13 관련 주제

선행 학습 (선수)

18-LangGraph 시리즈 cross-reference

  • #25 시스템 프롬프트 동적 주입 아키텍처 — 본 편의 콘텐츠 축 이론적 배경
  • #21 스킬 프롬프트 아키텍처 — few-shot 카탈로그 관리

후속 (Phase C-4)

  • C19 실험 파이프라인 자동화 — 가설→Bandit→A/B 사후 검증 루프
  • Phase C-7 스킬 생태계 — 개인화된 스킬 풀 분리 (C28 스킬 레지스트리에서 확장)

Subscribe

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