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

602 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 메모리·지식·페르소나 통합 아키텍처 설계서
> 한신대 AI 심리상담 시뮬레이션 훈련 플랫폼 · **단일 종합 설계서**
> 작성 2026-06-25 · 트웬티온스 산학협력
> 종합 대상 4개 영역: ① 회기 간 연속성 메모리(최우선 갭) · ② 지식 데이터베이스(KB) · ③ 페르소나 일관성 · ④ 3-AI 메모리 뷰/정보비대칭/종단평가
> 상위 설계: [`MASTERPLAN.md`](./MASTERPLAN.md) + [`redteam/MASTERPLAN_REVISIONS.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)
```sql
-- ════ 불변 정체성 (람다 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, 체크포인트)
```sql
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)
```sql
-- 마스터플랜 §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)
```sql
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)
```sql
-- 케이스 = 한 가상내담자의 다회기 전체 (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 스키마, 독립)
```sql
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)
```sql
-- 발화별 평가 (마스터플랜 §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
-- 하이브리드 검색 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`](./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은 보수적 유지(안전).