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)
명시 라벨이 부족하면 사용자 임베딩 → 가장 가까운 페르소나 라벨 매칭.
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 result7.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: 14audience 제한이 핵심: 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 v2의 Query 모델에 personalization 필드 추가, agent가 system prompt·스타일·scope를 모두 반영.
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 관련 주제
선행 학습 (선수)
- C17 사용자 세그멘테이션 — 분기에 쓰일 segment 정의
- C15 A/B 심화 — 페르소나 변형 검증
- C16 지능형 라우팅 — 페르소나도 arm으로 학습
- 02-1 BaseAgent v2 — Query 모델에 personalization 필드
18-LangGraph 시리즈 cross-reference
- #25 시스템 프롬프트 동적 주입 아키텍처 — 본 편의 콘텐츠 축 이론적 배경
- #21 스킬 프롬프트 아키텍처 — few-shot 카탈로그 관리
후속 (Phase C-4)
- C19 실험 파이프라인 자동화 — 가설→Bandit→A/B 사후 검증 루프
- Phase C-7 스킬 생태계 — 개인화된 스킬 풀 분리 (C28 스킬 레지스트리에서 확장)