44 KiB
메모리·지식·페르소나 통합 아키텍처 설계서
한신대 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은 보수적 유지(안전).