vignette/docs/MEMORY_KNOWLEDGE_PERSONA_DESIGN.md
Yun Chan a0311c5957
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s
회기 무발화 0턴 분리, 자기예측 락 불변식 및 TDD 회귀 검증 완료
2026-09-08 23:28:06 +09:00

44 KiB
Raw Blame History

메모리·지식·페르소나 통합 아키텍처 설계서

한신대 AI 심리상담 시뮬레이션 훈련 플랫폼 · 단일 종합 설계서 작성 2026-06-25 · 트웬티온스 산학협력 종합 대상 4개 영역: ① 회기 간 연속성 메모리(최우선 갭) · ② 지식 데이터베이스(KB) · ③ 페르소나 일관성 · ④ 3-AI 메모리 뷰/정보비대칭/종단평가 상위 설계: MASTERPLAN.md + redteam/MASTERPLAN_REVISIONS.md(BLUE TEAM 패치). 본 문서는 두 상위 문서의 확정 전제를 깨지 않는 하위 통합 설계다.


0. 확정 전제 (바꾸지 않음) + 설계 철학

0.1 변경 불가 기술 전제

항목 확정값
엔진 로컬 claude -p 상주 멀티턴 풀(--input-format stream-json), Messages API는 가용성 폴백
RAG BGE-M3(dense+sparse+ColBERT 단일모델) + pgvector(HNSW) + 하이브리드 + Anthropic Contextual Retrieval + BGE-reranker-v2-m3
DB NAS PostgreSQL 16 + pgvector, RLS 이중강제
백엔드 FastAPI(상태머신·가드레일·엔진 어댑터 소유)
3-AI 상담사AI(보조/선택) · 내담자AI · 백그라운드 평가AI
정보비대칭 DB visible_to[] 컬럼이 강제(프롬프트 신뢰 X), 인간 RBAC×cohort와 2-레이어 AND

0.2 통합 설계 철학 — 람다 자신의 메모리 시스템에서 차용한 3대 원리

이 플랫폼의 메모리·지식·페르소나는 람다(이 AI)의 실제 운영 메모리 구조를 1차 레퍼런스로 삼는다. 검증된 운영 패턴이라 추측이 아니다.

람다 시스템 원리 통합 설계 적용
계층 압축 (daily 원본 → weekly 요약 → monthly 요약, 원본 archive 절대보존, 숫자 무손실) 메모리 4계층(working/episodic/summary/semantic) + KB 3계층(청크/Contextual prefix/community 요약). 원본 append-only, 압축은 파생 테이블에만
역할 분리 (rag-memory=BGE-M3 sqlite 시맨틱 ↔ graphify=GraphRAG 구조, "둘 다 유지, 충돌 X, 질문 유형에 맞게 선택") 에피소드 메모리(내담자 일관성) ↔ 정적 지식 KB를 물리적으로 다른 스키마·다른 검색정책으로 분리. 충돌 시 우선순위 룰로 결정
부트의 큰그림→세부 읽기 (monthly→weekly→daily, "본문 인용 필요할 때만 직접 Read") 회기 시작 회상도 case_digest(큰그림)→직전 summary(중간)→episodic recall(세부) 순으로 토큰 예산 배분. 매 발화 전체 덤프 금지
IDENTITY/SOUL 불변 + daily 가변 (정체성이 일지 압축에 오염 안 됨) 페르소나 = 불변 카드(persona_card) + 가변 메모리(persona_session_memory)를 다른 테이블·다른 프롬프트 레이어·다른 권한으로 물리 분리 → drift 구조적 차단

0.3 4대 대원칙 (전 영역 관통)

  • P1. 원본은 절대 삭제 안 한다turns는 append-only(람다 archive). "망각"은 물리삭제가 아니라 회상 우선순위 배제(salience 하위 → 검색 랭킹 하위 → system 미주입).
  • P2. 숫자(상태 수치)는 무손실로 넘긴다 — working state(저항·라포·openness)는 LLM 요약이 아니라 결정론 상태머신이 DB 컬럼으로 직접 carry-over. LLM에 수치를 맡기면 hallucination.
  • P3. 큰 그림 → 세부 순서로 회상 — 토큰 예산을 case→summary→episodic 순으로 배분.
  • P4. 압축은 LLM, carry-over는 결정론 — 서사(narrative)는 LLM 압축(손실 허용), 상태 수치·pinned fact는 코드가 직접 복사(무손실).

1. 통합 메모리 계층도 (working/episodic/semantic/summary + 지식KB)

═══════════════════════════════════════════════════════════════════════════════
  통합 메모리·지식 아키텍처 — 5개 저장 계층 (메모리 4 + 지식 1), 물리 분리
═══════════════════════════════════════════════════════════════════════════════

  계층            수명          저장소(스키마.테이블)       채우는 주체     람다 대응물
─────────────────────────────────────────────────────────────────────────────────
 ┌──── 에피소드 메모리 (내담자 연속성) ── app 스키마 ────────────────────────────┐
 │① WORKING      회기 내        app.session_state          상태머신(결정론)   L3+L4    │
 │  회기내상태    (체크포인트)    +단기버퍼(JSONB)            매 턴 UPSERT       상태머신 │
 │                                                                                   │
 │② EPISODIC     영구           app.turns +                발화 로깅(원자)    daily    │
 │  회기별사건    (append-only)  app.turn_embedding(HNSW)   회기 중 임베딩      (원본)   │
 │                                                                                   │
 │③ SUMMARY      영구           app.session_summary        회기종료 압축(LLM)  weekly   │
 │  회기종료요약                 (+digest 임베딩)            회기당 1행                  │
 │                                                                                   │
 │④ SEMANTIC     영구(evolving) app.case_profile           케이스누적 압축     monthly  │
 │  누적임상프로파일              + app.pinned_fact          회기마다 갱신/병합   +MEMORY  │
 └───────────────────────────────────────────────────────────────────────────────┘
 ┌──── 정적 지식 KB (DSM/이론/taxonomy) ── kb 스키마 (독립, FK 없이 read-only) ──┐
 │⑤ KNOWLEDGE    영구(버전드)   kb.chunk(멀티벡터 HNSW)    오프라인 인덱싱     graphify │
 │  정적임상지식                 +kb.document +kb.source    (배치, 런타임 아님)  references│
 │  3계층: 원본청크 → Contextual prefix → community 요약(예산초과 fallback)            │
 └───────────────────────────────────────────────────────────────────────────────┘

─────────────────────────────────────────────────────────────────────────────────
  데이터 흐름:
  회기종료:  ②turns ──압축(LLM)──► ③session_summary ──병합(LLM+rule)──► ④case_profile
                                                                       + pinned_fact(rule)
  회기시작:  ④case_profile+pinned ──┐
            ③직전 summary         ──┼─로드(큰그림→세부)──► 내담자AI system L2-EP
            ②episodic recall(top-k)─┘   (BGE-M3 하이브리드+reranker, ~1.5~2K토큰)
            ⑤KB 청크 ───────────────────로드(정책별 필터)──► system L2-KB (캐시친화)

  모순 시 우선순위: pinned(L4) > 에피소드(이미 한 말) > 상태머신수치 > KB > LLM사전지식
  ("KB는 사전이지 판사가 아니다" — 내담자는 에피소드를 따르고 KB는 보정용)
═══════════════════════════════════════════════════════════════════════════════

1.1 계층별 역할 한 줄 정의

  • ① WORKING: 상태머신 결정론 수치(stage·effective_openness·rapport_credit·resistance·ideation_stage) + 최근 K턴 버퍼. 매 턴 체크포인트 UPSERT(프로세스 죽어도 복원).
  • ② EPISODIC: 발화=최소 원자단위, 저장 시 BGE-M3 임베딩 → 회기 시작 회상의 검색 대상. append-only(재귀학습·audit 원천).
  • ③ SUMMARY: 회기당 1행. 구조화 수치 snapshot(무손실) + narrative(LLM 압축) 분리. 다음 회기 carry-over 1차 단위.
  • ④ SEMANTIC: 케이스 전체 궤적 누적 압축(CCD 추정·호소문제·라포궤적·치료동맹). pinned_fact=절대 흘리면 안 되는 핵심(모순 처리 기준점).
  • ⑤ KNOWLEDGE: DSM/이론/taxonomy/논평. 버전드, 거의 불변. 3-AI가 같은 물리 테이블을 다른 정책으로 검색.

2. 회기 라이프사이클 시퀀스

═══════════════════════════════════════════════════════════════════════════════
  회기 라이프사이클: 회기시작 회상 → 진행(턴 사이클) → 회기종료 압축 → 다음 회기
═══════════════════════════════════════════════════════════════════════════════

【A. 회기 시작 — 회상 + 상태 복원】 (람다 부트 monthly→weekly→daily 차용)
────────────────────────────────────────────────────────────────────────────
 학습자가 P1 내담자와 N번째 회기 시작
   │
   ├─1. case_profile + pinned_fact 로드 (큰 그림, ~400+300토큰)  [P3]
   │      load_pinned_facts(case_id, status IN stable/evolving/locked)
   │
   ├─2. 직전 session_summary 로드 (중간, ~500토큰)
   │      {digest, open_threads, homework, end_state}
   │
   ├─3. episodic recall (세부, top-5, ~600토큰)
   │      query = open_threads + homework (+학습자 첫 발화)
   │      → hybrid_search(case_id 스코프, dense+sparse RRF, top-50)
   │      → BGE-reranker-v2-m3 → top-5
   │
   ├─4. 상태 복원 (carry-over, 결정론)  [P2]
   │      init_session_state_from_history():
   │        stage = '라포' (매 회기 라포부터 재시작 — 상담 구조)
   │        rapport_credit = prev.rapport_credit × 0.7 (주1회 간격 일부 이월)
   │        resistance = inter_session_drift(prev, alliance)
   │        ideation_stage = prev (보수적 유지 — 안전 R5)
   │
   ├─5. KB 정적 청크 로드 (페르소나 증상 단서, 정책 필터)
   │      visible_to='client' AND sensitivity<=1, 본문 비노출(행동단서만)
   │
   └─6. 상주 claude -p 프로세스 점유 + system 주입 (불변, cache_control)
          L0 가드레일 + L1 페르소나카드(불변) + L2-EP 회상 + L2-KB 지식
          → session_state.recall_context에 캐시 (회기 내 재검색 0)

【B. 회기 진행 — 턴 사이클】 (마스터플랜 §2.2, 매 발화)
────────────────────────────────────────────────────────────────────────────
 학습자 발화 U_t
   │
   ├─ [입력 가드레일] Presidio PII 마스킹 + 위기분류(실제위기 vs 연기 분기)
   ├─ [라우터] route_memory_vs_knowledge(turn, ai_role)  [람다 "필요할때만 Read"]
   │     needs_episodic = 지시어("아까/저번에") OR role=client OR 단계전이
   │     needs_knowledge = role=evaluator OR 임상용어 OR role=counselor
   ├─ [상태머신] effective_openness(t) 계산 (결정론, LLM 아님)  [P2]
   │     = clamp(stage.openness + rapport_credit×unlock_rate - resistance×decay)
   ├─ [모순 검사] handle_potential_contradiction(new_turn, pinned)
   │     locked 사실모순 → 차단·재생성 / clinical 변화 → 수용·갱신+이력
   ├─ [내담자 AI] stdin JSON 주입 → 페르소나 응답 (CCD 직접노출 금지)
   │     L3 상태수치 + L4 fact hard-pin + L5 episodic recall + L6 최근N턴
   ├─ [출력 가드레일] 자살수단 차단, ideation≤3, drift 감지(임베딩 일관성≥0.79)
   ├─ [평가 AI 2-tier]
   │     fast-loop(로컬/Haiku): U_t만 4차원 태깅 (실시간)
   │     deep-loop(Opus 상주): 단계전환·회기말 0-5 채점 + 대안발화
   ├─ [working 갱신] session_state UPSERT (체크포인트, 단기버퍼 append)
   └─ [로깅] turns → Postgres + 임베딩 (재귀학습 JSONL 원천)
   ↑ K턴 반복 (recall_context는 회기 내 재사용, 재검색 안 함)

【C. 회기 종료 — 압축】 (람다 daily→weekly 컴프레션, 비동기 비블로킹)
────────────────────────────────────────────────────────────────────────────
 stage='정리' 완료 OR 명시 종료
   │
   ├─(A) 무손실 carry-over: end_state = working 종료 snapshot (코드 복사, LLM 미경유)  [P4]
   ├─(B) salience 산출 → 망각/유지 결정
   │      compute_salience(t): pinned/위기/최초개방/정서급변 ↑, 잡담 ↓
   │      salience ≥ 0.5 → episodic 영구유지(검색 우선), 미만 → digest 흡수
   ├─(C) narrative 압축(LLM, 상주 claude -p 재사용): digest 6~10문장 + open_threads
   │      pinned 사실은 프롬프트 hard-pin → digest 보존 강제
   │      입력 = turns.text_masked만 (F-03 마스킹 강제)
   ├─(D) digest 임베딩 + session_summary UPSERT + episodic 임베딩 백필
   ├─(E) case_profile 병합: rapport_trajectory append, CCD 추정 갱신,
   │      case_digest 재압축(N회기마다 or major shift), alliance EWMA
   ├─(F) pinned_fact 모순/갱신 처리 + pinned_fact_history 이력화
   └─(G) RAG 동기화 (람다 storeDocument→embed): 다음 회기 회상 대비
   │
   ├─ 상주 프로세스 회수 (누적 컨텍스트 리셋 → 말투표류 D-c 회기경계서 차단)
   └─ 종단 평가: learner_profile EWMA(현재수준)+slope(성장속도) 갱신

【D. 다음 회기】 → 【A】로 복귀 (case_profile·summary·pinned 누적된 상태로)
            토큰 ~4K 고정 (100회기여도 선형증가 X)
═══════════════════════════════════════════════════════════════════════════════

3. 통합 Postgres 스키마 (메모리·지식·페르소나·평가, DDL 수준)

스키마 4분할: app(운영·메모리·페르소나) / kb(정적 지식, 독립) / audit(append-only 감사) / ds(재귀학습). Phase 0에 전부 선반영(nullable), cross-session·KB 채우기는 Phase 2a(MASTERPLAN_REVISIONS §3.5 재마이그레이션 폭탄 방지 원칙).

3.1 페르소나 — 불변 카드 (app.persona_card)

-- ════ 불변 정체성 (람다 IDENTITY/SOUL, 버전드, 교수만 편집) ════
CREATE TABLE app.persona_card (
  persona_id      UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  code            TEXT NOT NULL,                    -- 'P1','P2','P3'
  version         INT  NOT NULL DEFAULT 1,          -- 편집 시 +1 (진행 세션은 옛 version 핀)
  status          TEXT NOT NULL DEFAULT 'draft'
                    CHECK (status IN ('draft','review','approved','archived')),
  display_name    TEXT NOT NULL,
  difficulty      TEXT NOT NULL CHECK (difficulty IN ('easy','moderate','hard')),
  theory_target   TEXT[],                           -- {'humanistic','cbt'}
  demographics    JSONB NOT NULL,                   -- 범주화(재식별 방지 F-25): age_band,sex,grade...
  presenting      JSONB NOT NULL,                   -- 표층 호소(내담자가 입으로 말함)
  history         JSONB NOT NULL,                   -- 과거사
  big5            JSONB NOT NULL,                   -- {O,C,E,A,N} 0~1 (말투·반응 수학적 앵커)
  resistance      JSONB NOT NULL,                   -- {base_resistance,unlock_rate,decay_floor,silence_prob,deflection_prob}
  speech_style    JSONB NOT NULL,                   -- {register,avg_sentence_len,fillers,honorific,verbal_tics}
  affect_baseline JSONB NOT NULL,                   -- {negative_affect,hopelessness,anhedonia,sleep,anxiety,suicide_ideation_stage} hard상한=3
  ccd             JSONB NOT NULL,                   -- Patient-Ψ 8요소 (★직접발화 금지 R4, 행동으로만)
  dsm5_dimensional JSONB NOT NULL,                  -- criteria_behavior_matrix (진단명 비노출, 내부 엄밀성 F-22)
  source_provenance TEXT NOT NULL,                  -- '0615 합성변형' (원문 미적재 F-05)
  is_synthetic    BOOL NOT NULL DEFAULT TRUE,
  created_by      UUID, approved_by UUID, approved_at TIMESTAMPTZ,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (code, version)
);
CREATE INDEX idx_persona_card_status ON app.persona_card(status) WHERE status='approved';

-- 음성 매핑 (provider-agnostic, 카드 버전 정합)
CREATE TABLE app.persona_voice_map (
  persona_id  UUID NOT NULL REFERENCES app.persona_card(persona_id) ON DELETE CASCADE,
  version     INT  NOT NULL,
  voice_id    TEXT NOT NULL,                        -- 'voice_p1_teen_m'
  provider    TEXT NOT NULL CHECK (provider IN ('openai','higgs','melotts')),
  base_params JSONB NOT NULL,
  prosody_map JSONB NOT NULL,                       -- 상태(stage/affect/resistance)→prosody 함수
  PRIMARY KEY (persona_id, version)
);

-- 상담사 AI 페르소나 (데모/self-play용, 이론 충실도 일관)
CREATE TABLE app.counselor_profile (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code TEXT,
  theory TEXT CHECK (theory IN ('humanistic','cbt','integrative')),
  skill_level TEXT, reasoning_chain JSONB,          -- CoE: PCT/CBT 추론체인
  allowed_microskills TEXT[], speech_style JSONB
);

3.2 메모리 ① WORKING (app.session_state, 체크포인트)

CREATE TABLE app.session_state (
  session_id      UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
  stage           TEXT NOT NULL DEFAULT '라포' CHECK (stage IN ('라포','탐색','개입','정리')),
  turn_seq        INT  NOT NULL DEFAULT 0,
  effective_openness REAL NOT NULL DEFAULT 0.15 CHECK (effective_openness BETWEEN 0 AND 1),
  rapport_credit  REAL NOT NULL DEFAULT 0.0,        -- 회기말 carry 대상 (0.7 이월)
  resistance      REAL NOT NULL DEFAULT 0.65 CHECK (resistance BETWEEN 0 AND 1),
  ideation_stage  SMALLINT NOT NULL DEFAULT 1 CHECK (ideation_stage BETWEEN 1 AND 5),
  affect_state    JSONB NOT NULL DEFAULT '{}',
  short_buffer    JSONB NOT NULL DEFAULT '[]',      -- 최근 K턴 rolling window
  recall_context  JSONB,                            -- 회기시작 1회 로드, 회기내 재사용 캐시
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

3.3 메모리 ② EPISODIC (app.turns 확장 + app.turn_embedding)

-- 마스터플랜 §3.2 turns에 연속성 컬럼 추가
ALTER TABLE app.turns
  ADD COLUMN IF NOT EXISTS salience    REAL DEFAULT 0.0,    -- 망각/우선순위
  ADD COLUMN IF NOT EXISTS is_pinned   BOOL DEFAULT FALSE,  -- pinned_fact 승격 발화
  ADD COLUMN IF NOT EXISTS contradicts UUID[] DEFAULT '{}', -- 모순추적: 뒤집은 과거 turn_id[]
  -- 음성 paralinguistic (MASTERPLAN_REVISIONS §3.5 선반영)
  ADD COLUMN IF NOT EXISTS audio_ref   TEXT,
  ADD COLUMN IF NOT EXISTS silence_ms  INT,
  ADD COLUMN IF NOT EXISTS speech_rate REAL,
  ADD COLUMN IF NOT EXISTS barge_in    BOOL;

CREATE TABLE app.turn_embedding (
  turn_id     UUID PRIMARY KEY REFERENCES app.turns(id) ON DELETE CASCADE,
  case_id     UUID NOT NULL,                        -- denormalized: 케이스 스코프 회상
  session_id  UUID NOT NULL, seq INT NOT NULL,
  dense       vector(1024) NOT NULL,                -- BGE-M3 dense
  sparse      JSONB,                                -- BGE-M3 sparse {token_id:weight}
  context_prefix TEXT,                              -- Contextual Retrieval 1문장
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_turn_emb_hnsw ON app.turn_embedding
  USING hnsw (dense vector_cosine_ops) WITH (m=16, ef_construction=64);
CREATE INDEX idx_turn_emb_case ON app.turn_embedding (case_id, session_id);

3.4 메모리 ③ SUMMARY (app.session_summary)

CREATE TABLE app.session_summary (
  session_id   UUID PRIMARY KEY REFERENCES app.sessions(id) ON DELETE CASCADE,
  case_id      UUID NOT NULL, session_no INT NOT NULL,
  -- (A) 무손실 carry-over (상태 수치 snapshot, LLM 미경유)  [P2]
  end_state    JSONB NOT NULL,                      -- {stage,openness,rapport_credit,resistance,ideation_stage,affect}
  rapport_delta REAL,
  -- (B) narrative 압축 (LLM, 손실 허용)
  digest       TEXT NOT NULL,                       -- 6~10문장, 다음 회기 system 주입
  open_threads JSONB NOT NULL DEFAULT '[]',         -- 미해결 [{topic,last_stance,raised_session_no}]
  homework     JSONB, emotional_arc TEXT,
  -- (C) 검색용
  digest_embedding vector(1024),
  token_count  INT, compressed_by TEXT,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (case_id, session_no)
);
CREATE INDEX idx_sess_sum_case ON app.session_summary (case_id, session_no DESC);
CREATE INDEX idx_sess_sum_emb  ON app.session_summary
  USING hnsw (digest_embedding vector_cosine_ops) WITH (m=16, ef_construction=64);

3.5 메모리 ④ SEMANTIC (app.case_profile + app.pinned_fact)

-- 케이스 = 한 가상내담자의 다회기 전체 (persona_id=템플릿, case=학습자별 인스턴스)
CREATE TABLE app.case_profile (
  case_id         UUID PRIMARY KEY,
  persona_id      TEXT NOT NULL,                    -- 시드 페르소나
  learner_id      UUID NOT NULL,                    -- ★ 학습자별 독립 연속체
  last_session_no INT  NOT NULL DEFAULT 0,
  ccd_estimate    JSONB NOT NULL DEFAULT '{}',      -- CCD 8요소 현재추정 +confidence
  presenting_arc  JSONB NOT NULL DEFAULT '[]',      -- 호소문제 궤적
  rapport_trajectory JSONB NOT NULL DEFAULT '[]',   -- [{session_no,end_rapport,end_openness}]
  alliance_level  REAL DEFAULT 0.2,                 -- 치료동맹 누적
  case_digest     TEXT NOT NULL DEFAULT '',         -- 전체 궤적 8~12문장 (큰그림, 람다 monthly)
  digest_embedding vector(1024),
  updated_at      TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- pinned facts (람다 MEMORY.md, 절대 흘리면 안 되는 핵심, 모순 기준점)
CREATE TABLE app.pinned_fact (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  case_id     UUID NOT NULL REFERENCES app.case_profile(case_id) ON DELETE CASCADE,
  key         TEXT NOT NULL,                        -- 'name','family','suicide_attempt_history'
  value       TEXT NOT NULL,                        -- 가명처리/마스킹된
  fact_type   TEXT NOT NULL CHECK (fact_type IN ('identity','history','relationship','clinical','agreement')),
  status      TEXT NOT NULL DEFAULT 'stable' CHECK (status IN ('stable','evolving','contradicted','locked')),
  source_turn UUID, confidence REAL NOT NULL DEFAULT 1.0,
  version     INT NOT NULL DEFAULT 1, updated_session_no INT,
  updated_at  TIMESTAMPTZ NOT NULL DEFAULT now(),
  UNIQUE (case_id, key)
);
CREATE INDEX idx_pinned_case ON app.pinned_fact (case_id, status);

-- pinned 변경 이력 (append-only, 모순/갱신 audit + 평가AI 정답신호)
CREATE TABLE app.pinned_fact_history (
  id BIGSERIAL PRIMARY KEY, fact_id UUID NOT NULL, case_id UUID NOT NULL,
  old_value TEXT, new_value TEXT,
  reason TEXT,                                      -- 'contradiction','clarification','progression'
  session_no INT, turn_id UUID, created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

3.6 지식 ⑤ KB (kb 스키마, 독립)

CREATE SCHEMA kb;
CREATE EXTENSION IF NOT EXISTS vector;
CREATE EXTENSION IF NOT EXISTS pg_trgm;

-- 출처 + 라이선스 (DSM-5 저작권 1차 통제)
CREATE TABLE kb.source (
  source_id   TEXT PRIMARY KEY,                     -- 'dsm5','theory','taxonomy_0615'
  title       TEXT NOT NULL,
  kb_kind     TEXT NOT NULL CHECK (kb_kind IN
                ('diagnostic','theory','technique','taxonomy','supervisor_pattern','template','ko_context')),
  license_class CHAR(1) NOT NULL CHECK (license_class IN ('A','B','C','D')),  -- C=저작권민감 D=미성년파생
  origin_path TEXT, citation TEXT,
  external_llm_ok BOOLEAN NOT NULL DEFAULT false,   -- C/D면 false → 국내 라우팅
  created_at  TIMESTAMPTZ DEFAULT now()
);

-- 문서 + 버전 (graphify content_hash 증분 인덱싱 차용)
CREATE TABLE kb.document (
  doc_id      BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  source_id   TEXT NOT NULL REFERENCES kb.source(source_id),
  doc_uri     TEXT NOT NULL, version INT NOT NULL DEFAULT 1,
  content_hash TEXT NOT NULL,                       -- SHA256 → 변경감지(증분)
  superseded_by BIGINT REFERENCES kb.document(doc_id),
  is_active   BOOLEAN NOT NULL DEFAULT true, indexed_at TIMESTAMPTZ,
  UNIQUE (source_id, doc_uri, version)
);

-- 청크 + 멀티벡터 (검색 본체)
CREATE TABLE kb.chunk (
  chunk_id    BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  doc_id      BIGINT NOT NULL REFERENCES kb.document(doc_id) ON DELETE CASCADE,
  source_id   TEXT NOT NULL, kb_kind TEXT NOT NULL,  -- 비정규화(필터 가속)
  seq INT NOT NULL, heading_path TEXT,
  chunk_text  TEXT NOT NULL,                        -- 표시·LLM 주입(prefix 미포함)
  context_prefix TEXT,                              -- Contextual Retrieval 주입문
  embedding   vector(1024),                         -- BGE-M3 dense
  sparse_vec  JSONB, colbert_vecs JSONB,            -- 멀티벡터(단일 호출 산출)
  visible_to  TEXT[] NOT NULL DEFAULT '{client,counselor,evaluator}',  -- 정보비대칭
  sensitivity SMALLINT NOT NULL DEFAULT 0,          -- 0공개 1비공개내부 2평가전용 3원천격리
  label_id    BIGINT,                               -- taxonomy면 technique_label_def FK
  meta        JSONB DEFAULT '{}',                   -- {dsm_category,theory,bias_weight...}
  token_count INT, created_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_chunk_hnsw ON kb.chunk USING hnsw (embedding vector_cosine_ops) WITH (m=16, ef_construction=64);
CREATE INDEX idx_chunk_fts  ON kb.chunk USING gin (to_tsvector('simple', chunk_text));
CREATE INDEX idx_chunk_trgm ON kb.chunk USING gin (chunk_text gin_trgm_ops);
CREATE INDEX idx_chunk_visible ON kb.chunk USING gin (visible_to);
CREATE INDEX idx_chunk_route ON kb.chunk (kb_kind, sensitivity);

3.7 평가 + 종단 추적 + 드리프트 감사 (app + audit)

-- 발화별 평가 (마스터플랜 §3.2, visible_to로 학습자 비노출)
CREATE TABLE app.feedback_scores (
  turn_id UUID NOT NULL, dimension TEXT NOT NULL,   -- 공감/개방질문/검증/이론부합
  score REAL,                                       -- 0-5 (CTRS 0-6)
  rationale TEXT, top1_score REAL,                  -- CRAG 게이트(임계미달→관찰프레이밍 F-06)
  visible_to TEXT[] NOT NULL DEFAULT '{evaluator}',
  PRIMARY KEY (turn_id, dimension)
);

-- 종단 학습자 프로파일 (회기 거듭하며 나아지는지 — 윤찬 직접 답)
CREATE TABLE app.learner_profile (
  learner_id  UUID PRIMARY KEY,
  -- 4차원 EWMA(현재수준) + slope(성장속도) 동시
  dim_ewma    JSONB NOT NULL DEFAULT '{}',          -- {empathy:0.6, open_q:0.5, validation:..., theory:...}
  dim_slope   JSONB NOT NULL DEFAULT '{}',          -- 성장속도 (잘하는데정체 vs 못하지만급성장 구분)
  persistent_gaps JSONB DEFAULT '[]',               -- deep-loop만 맥락화 코칭 (fast-loop 절대 미주입)
  session_count INT DEFAULT 0, updated_at TIMESTAMPTZ DEFAULT now()
);

-- 페르소나 드리프트 감사 (KPI 임베딩일관성≥0.79 측정)
CREATE TABLE audit.persona_drift_log (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  session_id UUID NOT NULL, turn_id UUID, persona_id UUID NOT NULL,
  drift_type TEXT NOT NULL,                         -- 'big5_violation'|'ccd_leak'|'fact_contradiction'|'style_drift'
  embedding_sim REAL, detail JSONB,
  severity TEXT CHECK (severity IN ('warn','block')),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- KB 검색 로그 (환각측정·캐시검증, 람다 cost.json 차용)
CREATE TABLE kb.retrieval_log (
  id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  session_id UUID, turn_id UUID, ai_role TEXT NOT NULL,
  query_text TEXT, policy TEXT NOT NULL, hit_chunk_ids BIGINT[],
  rerank_scores REAL[], top1_score REAL, used_in_answer BOOLEAN,
  latency_ms INT, created_at TIMESTAMPTZ DEFAULT now()
);

핵심 설계 결정 — (persona_id, learner_id) 복합키: 한 페르소나(P1)를 20명이 각자 연습한다. 같은 카드(불변) + 다른 연속체(가변). case_profile.learner_id가 "이 학습자에게 이 내담자가 겪은 회기 히스토리"를 정의 → 학습자 간 기억 오염 차단.


4. 3-AI 정보비대칭이 메모리·KB 레이어에서 강제되는 방식

4.1 핵심 원리 — 위험한 건 turn 표면이 아니라 파생 메모리

turn 자체는 대화 표면이라 덜 위험하다. 위험한 건 CCD·정답라벨·점수·종단요약 같은 파생 메모리. 여기에만 visible_to[]를 달고, 컨텍스트 조립기가 역할별로 물리적으로 다른 쿼리 분기를 탄다.

2-레이어 강제 (MASTERPLAN_REVISIONS §5.1 F-30):
 ┌─ 레이어1: AI 정보비대칭 (current_ai_view enum) ── visible_to[] WHERE 강제
 │    CLIENT_AI 분기엔 load_ccd()·load_evaluator_answer() 함수 자체가 없음
 │    → "필터 누락"이 아니라 "코드 경로 부재"가 1차 방어
 └─ 레이어2: 인간 RBAC×cohort (current_role) ── RLS DB레벨 방어선
      인간이 turn 읽을 때 두 게이트 AND (단일 미들웨어 + RLS 이중강제)

4.2 메모리 레이어 — 3-AI 뷰 격리 매트릭스

메모리/KB 항목 ② 내담자 AI ① 상담사 AI(보조) ③ 평가 AI
① working(상태수치) 자기 stage·openness만 (수치 주입, 메타노출 X) 표면 대화만 전체 (저항엔진 검증)
② episodic(자기 발화) 자기 케이스 recall만 (case 스코프 강제) 회상 없음(표면만) 전체 (정답 대조)
③ session_summary digest·pinned는 "자기기억"으로만 표현 미노출 전체 + open_threads
④ case_profile.ccd_estimate 절대 비노출 (R4 메타노출 금지) 절대 비노출 전체 (정답 CCD)
④ pinned_fact(clinical) 자기 사실로만 미노출 전체 (변화 정답신호)
⑤ KB diagnostic(DSM) 내부 파라미터 (본문 비노출, 행동단서만) 차단 전체
⑤ KB taxonomy 정답라벨 차단 (sensitivity≥2) 차단 전체 (채점 기준)
learner_profile 차단 차단 deep-loop만 (fast 미주입, 사전편향 차단)

4.3 KB 레이어 — 같은 물리 테이블, 다른 retrieval 정책 4-튜플

검색 정책 = 사전필터(WHERE) + 회수 가중치 + 리랭킹 목표 + 주입 방식.

차원 ② 내담자 AI ① 상담사 AI ③ 평가 AI
사전필터 kb_kind IN (diagnostic,theory,technique) AND 'client'=ANY(visible_to) AND sensitivity<=1 kb_kind IN (theory,technique,microskill,ko_context) AND 'counselor'=ANY(visible_to) AND sensitivity=0 'evaluator'=ANY(visible_to) AND sensitivity<=2 (taxonomy·supervisor 정답 포함)
회수 가중치 dense 0.7 / sparse 0.3 (증상 의미) dense 0.5 / sparse 0.5 (기법명) sparse 0.6 / dense 0.4 (라벨명·논평)
주입 방식 본문 비노출, "행동단서 요약"만 본문+예시 본문+label_id+bias_weight
절대 차단 sensitivity≥2, 타 페르소나 CCD DSM diagnostic, 평가정답 (전부 봄, 학습자 비노출은 응답 가공단)
-- 하이브리드 검색 SQL 계약 (정책별 파라미터화, visible_to DB 강제)
WITH dense AS (
  SELECT chunk_id, 1-(embedding <=> :q_dense) AS s_dense FROM kb.chunk
  WHERE kb_kind=ANY(:kinds) AND :role=ANY(visible_to) AND sensitivity<=:sens_max
  ORDER BY embedding <=> :q_dense LIMIT 50),
sparse AS (
  SELECT chunk_id, ts_rank_cd(to_tsvector('simple',chunk_text),:q_terms) AS s_sparse FROM kb.chunk
  WHERE kb_kind=ANY(:kinds) AND :role=ANY(visible_to) AND sensitivity<=:sens_max
    AND to_tsvector('simple',chunk_text) @@ :q_terms LIMIT 50)
SELECT c.chunk_id, c.chunk_text, c.context_prefix, c.label_id,
       COALESCE(d.s_dense,0)*:w_dense + COALESCE(s.s_sparse,0)*:w_sparse AS fused
FROM kb.chunk c LEFT JOIN dense d USING(chunk_id) LEFT JOIN sparse s USING(chunk_id)
WHERE d.chunk_id IS NOT NULL OR s.chunk_id IS NOT NULL
ORDER BY fused DESC LIMIT :k;  -- → BGE-reranker-v2-m3 top-5

4.4 누수 방지 자동 테스트 (F-17, 람다 직접검증)

  • T1 RLS 부정테스트: 타 AI뷰 SET → 0행 반환 확인
  • T2 조립기 화이트리스트: CLIENT_AI 분기에 ccd/정답 로드 함수 부재 정적검증
  • T3 CCD-메타 누설: "제 핵심신념은…" 정규식 게이트 + 분류기
  • T4 cross-trainee 누수: 학습자A의 case가 B에 새지 않는지 (case_id 스코프)
  • 람다 직접: IDOR 403 스크린샷, ai_view 위조 요청 차단 검증

5. 기존 마스터플랜 §3.6과의 정합/대체점

마스터플랜 §3.6 항목 본 설계 정합 / 대체
페르소나 메모리=하이브리드(상태수치+rolling summary+episodic 벡터) 4계층(working/episodic/summary/semantic)으로 구체화 정합·확장 (§3.6 추상을 테이블·압축 알고리즘으로 실체화)
피드백 근거=하이브리드+리랭킹(+CRAG) KB ③평가 경로 = 하이브리드+Contextual+reranker, top1_score 게이트 정합 (CRAG를 ③경로에만 우선 도입)
지식=정적 KB+Contextual Retrieval kb 스키마 + 3계층(청크/prefix/community) + license_class 정합·확장 (DSM 저작권 통제 추가)
임베딩=BGE-M3 단일모델 멀티벡터 episodic·summary·KB 전부 BGE-M3 정합 (동일 모델 재사용)
pgvector(HNSW) 모든 벡터 테이블 HNSW 정합
하이브리드 dense+sparse top-50 episodic·KB 동일 RRF 정합
GraphRAG=2차(이론 교차개념) 본 설계도 graphify 2차 (theory KB 커질 때만) 정합
(§3.6 미언급) 회기 간 연속성 본 설계가 신규 추가 (summary/case_profile/recall/모순처리) 대체점=신규 보강 (§3.6은 단일회기 메모리만, 본 설계가 cross-session 채움)
(§3.6 미언급) rag_chunks vs episodic 분리 에피소드(app)↔정적KB(kb) 물리 스키마 분리 대체점=명확화 (§3.6 rag_chunks를 KB 전용으로 좁히고 메모리는 별도)

핵심 대체점: 마스터플랜 §3.6은 "페르소나 메모리=하이브리드 메모리"라고만 했지 회기 간 연속성(cross-session)을 다루지 않았다. 본 설계가 summary→case_profile 계층 압축 + 회기 시작 회상 + 모순 처리를 신규로 채운다. §3.6의 RAG 스택 자체는 하나도 바꾸지 않는다(BGE-M3/pgvector/하이브리드/Contextual/reranker 그대로).


6. 20주·n=1 제약 하 MVP 메모리 범위

6.1 Phase 매핑 (단일 로드맵 정합, 산발 task 금지)

항목 Phase 소유(REVISIONS §6.1) DoD 검증(람다 직접)
스키마 전체 선반영 (§3 전부, nullable) Phase 0 람다 직접(인프라) 마이그레이션 통과 + nullable 확인
페르소나 카드 4테이블 + 음성맵 Phase 0 람다 직접 DDL 적용
① working + 체크포인트 Phase 1 A2(상태머신) 프로세스 kill 후 상태 복원 재현
② episodic + 임베딩 Phase 1 A1(엔진)+데이터 발화 저장 시 임베딩 적재 DB쿼리
⑤ KB (하이브리드+Contextual+reranker) Phase 1 A1+데이터 ②/③ 다른 청크 회수 SQL 대조
③ 회기종료 압축 Phase 2a A2+데이터 회기 완주→summary 생성, 토큰예산 실측
④ case_profile+pinned+모순처리 Phase 2a A2+데이터 2회기→carry-over+모순처리 재현
회기시작 회상 Phase 2a A1+B(UI카드) N회기째 "지난번~" 정확 회상 + 정답대조
종단 learner_profile EWMA+slope Phase 2a~3 D(데이터) 다회기 성장곡선 대시보드

6.2 MVP 메모리 범위 결정 (정직한 단계화)

  • Phase 1 = 단일 회기 메모리만: working(상태머신 carry-over 체크포인트) + episodic(발화 임베딩) + KB(정적 지식). cross-session 없음. 0615를 1회기 episodic 시드로.
  • Phase 2a = cross-session 추가: summary 압축 + case_profile 누적 + 회기 시작 회상 + 모순 처리. 다회기 진화는 데이터가 아니라 결정론 상태머신 + 합성 시나리오로 보장.
  • 스키마는 Phase 0에 전부 선반영 (재마이그레이션 폭탄 방지).

6.3 n=1 정직 표기 (절대 "검증됨" 보류)

  • 0615은 사실상 단일 회기 사례 → 다회기 연속성 학습 코퍼스 없음. 압축 품질(정보보존율)·회상 정확도는 0615로 검증 불가.
  • Phase 1엔 룰 기반으로 시작(salience 룰, inter_session_drift 룰, EWMA α=0.4·FAST_WINDOW=6 전부 가정값).
  • 정량검증은 Phase 3 파일럿(20명×다회기)에서 first measure — "압축 전후 사실 보존율 / 다음회기 회상 정확도 / 성장곡선 타당도". 그 전엔 "검증됨" 표기 금지(feedback_measure_before_report).
  • 추가 축어록 수급이 전제 — n=1 해소는 한신대 공문(블로킹 의존성, F-04)에 달림.

7. 핵심 리스크 (통합)

# 리스크 영향 완화
M1 압축 정보손실로 페르소나 일관성 균열 페르소나 붕괴 pinned 무손실+episodic 검색 백업, digest는 서사용. Phase3 보존율 측정
M2 상태수치 LLM hallucination 저항엔진 오작동 end_state는 코드 복사(P4), LLM 미경유
M3 n=1로 압축/회상/성장곡선 품질 검증 불가 정량근거 부재 정직표기, 룰 시작, Phase3 first measure. α·window·TTFT 전부 "실측 필요"
M4 모순처리 오판 (정당변화↔버그 혼동) 잘못 차단/오염 fact_type별 분기(locked만 차단), 전건 이력화 사후교정
M5 회상 검색 오류 (엉뚱한 과거발화) "지난번~" 환각 reranker top-5, open_threads 우선쿼리, case 스코프 강제
M6 CCD 메타노출 (회상·KB가 정답 누설) 추론훈련 무력화 visible_to 분리, 내담자="자기기억"만, KB 본문 비노출(단서만), 출력 가드레일
M7 회기 경계 상태 리셋 버그 진화 손실 carry-over 명시(0.7 이월), rapport_trajectory DB 회귀테스트
M8 페르소나 drift (가변이 불변 덮음) 페르소나 깨짐 불변 카드 압축 대상 제외, COMPRESS_SYSTEM hard rule+금칙어 정규식 이중방어
M9 DSM-5 verbatim 저작권 (license C) 법적 책임 원문 미적재 환언본만, verbatim 검출기 게이트, 국내 라우팅
M10 한국어 sparse/BM25 토크나이징 (PG simple 형태소 미분리) 검색 품질 pg_trgm+BGE sparse 병용, 부족 시 mecab-ko 2차. 실측 전 "해결됨" 금지
M11 정보비대칭 누수 (내담자가 정답청크 봄) 시뮬레이션 가치 0 visible_to+sensitivity DB WHERE 강제, 코드경로 부재 1차방어, T1~T4 자동테스트
M12 마스킹 게이트 우회 (claude -p stdin 직송) IRB·법적 C/D 청크 external_llm_ok=false, Presidio 선통과(F-03), KB는 인덱싱 시점 환언
M13 압축/HNSW 재인덱싱 비용 누적 예산 정액 한계비용 0, content_hash 증분, case_digest 재압축 N회기마다, salience 입력축소
M14 임베딩 일관성 0.79 임계가 임의값 KPI 무근거 self-play 회귀로 분포 측정 후 확정, 파일럿 전 "검증됨" 금지

8. 통합 요약 (한 장)

  • 5계층: ①working(상태머신 수치+버퍼, 코드 carry-over·체크포인트) ②episodic(turns+BGE-M3, append-only) ③summary(회기종료 LLM 압축 ~600토큰) ④semantic(case_profile evolving+pinned_fact) ⑤knowledge(kb 스키마, 3-AI 다른 정책). 메모리(app)↔지식(kb) 물리 스키마 분리.
  • 회기 라이프사이클: 시작(case_digest→summary→episodic recall, 큰그림→세부, ~4K 고정) → 진행(턴 사이클, 상태머신 결정론, 모순검사) → 종료(수치 코드복사+서사 LLM압축+salience 망각/유지+case 병합) → 다음 회기(누적 상태).
  • 회기 간 연속성(최우선 갭): 상태 수치 carry-over(rapport 0.7 이월, ideation 보수적 유지), pinned hard-pin 무손실, 모순 처리(locked=차단·재생성 / clinical=수용·갱신+이력), 람다 daily→weekly→monthly 압축 직이식.
  • 페르소나 일관성: 불변 카드 + 가변 메모리 물리 분리(L1 캐시 vs L3~L6 턴주입), drift=가변이 불변 덮을 때 → 구조적 차단. 음성 prosody=텍스트 말투와 같은 상태머신 입력 동기화.
  • 정보비대칭: 파생 메모리(CCD·정답·점수·종단)에만 visible_to, 코드경로 부재 1차방어+RLS 2차, 인간 RBAC×cohort AND. KB는 같은 테이블 다른 4-튜플 정책.
  • §3.6 정합: BGE-M3/pgvector/하이브리드/Contextual/reranker 스택 그대로, 회기 간 연속성만 신규 보강.
  • MVP·n=1: Phase 0 스키마 전체 선반영, Phase 1 단일회기, Phase 2a cross-session. 0615 검증 불가→룰 시작+Phase3 first measure, 추가 축어록 수급(공문 블로킹) 전제. "검증됨" 표기 보류.

부록. 종합 근거 자료 (절대경로)

  • 마스터플랜: MASTERPLAN.md (§2 3-AI, §3 데이터모델, §3.6 RAG)
  • BLUE TEAM 패치: …/적대검증_2026-06-25/MASTERPLAN_REVISIONS.md (§1 stream-json 상주풀, §3 마스킹·스키마, §5 visible_to/RBAC 레이어분리, §6 RACI·DoD검증)
  • 데이터 원천: …/데이터/README.md (0615 n=1 시드, 파란색=발화별 기법라벨+슈퍼바이저논평 taxonomy)
  • 람다 메모리 시스템(1차 레퍼런스): …/Lambda/BOOT.md(부트 큰그림→세부), …/Lambda/procedures/memory-compression.md(daily→weekly→monthly 압축 알고리즘), …/CLAUDE.md(graphify=GraphRAG / rag-memory=BGE-M3 sqlite 역할분리)
  • KB 자료원: …/Lambda/references/counseling/(00~07 DSM·이론·마이크로스킬·위기·한국맥락, 출처인용·가드레일 완비 환언본)
  • 자매 설계서: …/설계/연속성메모리_정보비대칭_종단평가_설계서.md(3-AI 메모리뷰 상세)

설계 차별점 4줄: ① 같은 물리 KB(kb.chunk) 하나를 visible_to+sensitivity+정책 매트릭스로 3-AI가 다르게 검색(DB 강제, 코드경로 부재 1차방어). ② 람다 daily→weekly→monthly 계층압축을 메모리 4계층(working/episodic/summary/semantic) + KB 3계층(청크/prefix/community)으로 이식. ③ 람다 rag-memory↔graphify 역할분리를 에피소드 메모리↔정적 KB 물리분리로, 모순 시 "KB는 사전이지 판사가 아니다"(에피소드>KB) 우선순위로 페르소나 일관성 보호. ④ 회기 간 연속성=수치는 결정론 carry-over(무손실)+서사는 LLM 압축(손실허용)+pinned hard-pin, ideation은 보수적 유지(안전).