본문으로 건너뛰기

사무소 학습 루프 (ADR 0018)

너홀로프로의 핵심 정체성: 자기 사무소 데이터로 학습한 AI 가 자기 사무소만 도와준다 — 변호사가 그 학습을 인지하고 신뢰할 수 있게.

본 문서는 그 정체성을 실제 코드로 어떻게 닫았는지 (Capture → Visibility → Use → Quality loop) 4 layer 의 파일 위치·계약·시그널을 한 곳에 묶는 SSoT 다.

4 layer 구조

┌──────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐
│ 1. Capture │ → │ 2. Visibility │ → │ 3. Use │ → │ 4. Quality loop │
│ (변호사 작업 흔적) │ │ (학습 가시화) │ │ (AI prompt inject) │ │ (만족도 → 정렬) │
└──────────────────────┘ └──────────────────────┘ └─────────────────────┘ └──────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
finalize·refine chip·card·modal selectAuthored aggregateExemplar
(draftDiffs + (편집기 chip / Exemplars + Quality (up/down)
refinementFeedback) 대시보드 카드 / buildExemplarPrompt → quality Map
모달 source 링크) Section → selection 가중치

1. Capture — 변호사 작업 흔적 누적

저장소:

원칙:

  • PII 안전 — 본문은 redact strict 통과한 토큰만 영속, 변호사가 검증 가능한 metadata (제목·일자·legacyDocId) 만 클라이언트로 노출.
  • Tenant 격리 — tenants/{tid}/... 하위 경로만 사용. cross-tenant 노출 절대 금지.

2. Visibility — 학습 가시화

변호사가 "이 사무소 AI 가 학습하고 있다" 를 관념이 아니라 숫자·링크로 인지할 수 있어야 신뢰가 형성된다.

Surfaces:

  • 대시보드 카드 DashboardFirmLearningCard
    • 누적 학습 본 수 · 최근 7일 · 평균 편집 강도 · 주력 docKind top 3
    • 다듬기 만족도 footer (👍/👎 카운트 + 만족도 % + 학습 자료 활용도)
    • cold-start CTA — totalLearned=0 신규 사무소용
  • 에디터 chip InPlatformDocEditor
    • "사무소 학습 N건 · NN%" — 같은 docKind 의 누적 + 만족도 (표본 ≥ 3)
  • AI 다듬기 모달 exemplar list AiRefineModal
    • inject 된 사례 제목·일자 + /legacy/{legacyDocId} 새 창 링크 (트러스트 클로저)

Server Actions:

  • getFirmLearningStatsAction — draftDiffs 200 건 fetch · 통계 산출
  • getRefinementFeedbackStatsAction — 만족도 footer 데이터
  • getKindRefinementSatisfactionAction — 특정 docKind chip 데이터 (표본 ≥ 3)

3. Use — AI prompt inject

경로:

변호사가 "AI 다듬기" 클릭
→ InPlatformDocEditor.handleAiRefine
→ fetchAuthoredExemplarsAction (legacyDocs 50건 fetch)
→ selectAuthoredExemplars (docKind 매칭 · quality 가중 정렬 · top 2)
→ buildExemplarPromptSection (perExemplarCharBudget 1500 · totalCharBudget 4000)
→ systemPrompt 에 inject 된 채로 generateText 호출
→ AiRefineModal 에 결과 + exemplar 메타 전달

파일:

원칙:

  • 순수 함수 분리 — selection · prompt build 는 Firestore 의존 없는 pure 함수. 결정론 보장 (legacyDocId 사전식 tie-break).
  • 토큰 예산 안전 마진 — 한국어 1 char ≈ 0.7-1 token, 4000 자 budget 으로 평균 ~3000 token 안정.
  • graceful degrade — 학습 자료 부재해도 다듬기 자체는 진행. exemplarsSection="" 으로 폴백.

4. Quality loop — 만족도가 selection 에 자동 반영

학습 루프의 마지막 layer. 변호사가 👎 한 사례는 다음번 다듬기에서 자동 후순위로 밀린다.

알고리즘:

// 1. refinementFeedback 의 최근 100 건 fetch + docKind 필터
// 2. aggregateExemplarQuality(records, { halfLifeDays: 90 }) → Map<legacyDocId, {up, down, total}>
// - 시간 가중: 90일 전 평가는 weight 0.5, 180일 전은 0.25 (분기 단위 적응)
// 3. selectAuthoredExemplars 에 quality 인자 전달
// 정렬 우선순위:
// 1순위: qualityScore = (up - down) / total ∈ [-1, +1] (표본 < 3 은 0 중립)
// 2순위: finalizedAt desc
// 3순위: legacyDocId 사전식

파일:

원칙:

  • 표본 임계값 qualityMinSample=3 — 1-2 건 평가는 노이즈로 취급, score 0 (중립). 변호사의 우발적 클릭이 학습 흐름 흔드는 것 방지.
  • quality 미전달 호환 — 인자 없으면 date-only 정렬 유지. 기존 caller 영향 없음.
  • graceful degrade — feedback fetch 실패 시 quality undefined → date 정렬로 자연 fallback.

안티패턴

  • 본문을 클라이언트로 노출 — exemplar 메타에 redactedText 포함하지 말 것. PII 토큰만 backend 사용.
  • cross-tenant 후보 fetchlegacyDocsRef(otherTenantId) 절대 금지. Server Action 의 session.tenantId 만 사용.
  • 표본 1건으로 가중치 적용qualityMinSample 임계값을 의도적으로 0/1 로 낮추지 말 것 (테스트 외).
  • finalize 후 학습 트리거 누락finalizeDocument_pendingLearningFeed 플래그가 Cloud Function 임베딩 트리거. 이 필드를 set 하지 않으면 학습 자료 누적 안 됨.

검증 테스트

  • apps/web/app/(workspace)/dashboard/_lib/__tests__/refinement-feedback-aggregate.test.ts (15)
  • apps/web/app/(workspace)/docs/_lib/__tests__/authored-exemplars.test.ts (23)
  • apps/web/app/(workspace)/docs/_actions/__tests__/fetch-authored-exemplars-action.test.ts (12)
  • apps/web/__tests__/business-logic-documents.test.ts (10) — finalize · draftDiffs 캡처

후속 확장 후보

  • per-exemplar 만족도 ops 노출 (master admin 이 cross-tenant 학습 품질 관전)
  • 카테고리별 학습 quality dashboard (사무소 단위 docKind 별 satisfaction breakdown)
  • V2 검색 (ADR 0017) RAG path 에도 동일 quality loop 적용