키워드 필터는 "죽고싶다"는 막지만
"이제 다 끝내려고"는 놓칩니다.
임베딩은 의미를 고차원 벡터로 표현해
표현을 바꿔도 같은 의도를 잡아냅니다.
text-embedding-3-small은 다국어를 동일한 벡터 공간에 투영합니다.
"죽고 싶다"와 "I want to die"가
같은 리스크 클러스터에 위치합니다.
동형암호(CKKS)는 내적(dot product)을 평문 없이 계산합니다. 코사인 유사도 = 정규화 벡터의 내적 → 임베딩이 곧 HE 연산의 입력입니다.
레이블된 샘플로 임계값(threshold)과 최소 마진을 F1 최적화 기반으로 자동 튜닝합니다. 키워드 가중치 수동 조정보다 훨씬 체계적입니다.
| 방법 | 의미 이해 | 다국어 | HE 호환 | 우회 저항성 |
|---|---|---|---|---|
| 키워드 필터 | ✗ | ✗ | ✓ | ✗ |
| LLM 프롬프트 분류 | ✓ | ✓ | ✗ (평문 필요) | ✓ |
| Fine-tuned 분류기 | ✓ | △ | ✗ | ✓ |
| OpenAI Embedding + HE | ✓ | ✓ | ✓ | ✓ |
서버가 연산하려면 반드시 복호화해야 합니다.
복호화 순간 평문이 서버 메모리에 존재합니다.
서버는 암호문 상태에서 덧셈·내적을 계산합니다.
복호화는 비밀키를 가진 클라이언트만 가능.
사용자 메시지를 OpenAI text-embedding-3-small API로 1,536차원 실수 벡터로 변환합니다. 이 단계까지는 평문입니다.
// 평문 단계: 클라이언트 디바이스에서만 실행 vec = openai.embed("죽고 싶어서 힘들어") // → [0.021, -0.103, 0.847, ...] (1536차원)
공개 컨텍스트(public context)로 벡터를 암호화합니다. 이 시점부터 원문 복원은 비밀키 없이 불가능합니다.
// 암호화: 공개키만 필요, 비밀키 불필요 enc_vec = ts.ckks_vector(pub_ctx, vec) payload = base64(enc_vec.serialize()) // → 서버로 전송되는 것: 암호문 blob
서버는 평문 위험 프로파일 벡터(공개)와 암호화된 사용자 벡터의 내적을 계산합니다. 내적 결과도 암호화 상태입니다.
// 서버: 평문을 절대 보지 않음 // risk_vec = 공개된 위험 프로파일 중심 벡터 enc_score = enc_vec.dot(risk_vec) // enc_score는 여전히 암호문 — 서버 접근 불가
비밀 컨텍스트(secret key)로 스칼라 점수만 복호화합니다. 원본 메시지는 복원 불가능하며 오직 "위험도 점수"만 얻습니다.
// 복호화: 비밀키 보안 영역에서만 score = enc_score.decrypt()[0] # → 0.73 alert = score >= threshold # → ALERT // 원문 "죽고 싶어서..." 는 절대 복원되지 않음
artifacts/public_context.tenseal누구나 소유 가능, 암호화 전용secure/secret_context.tenseal보안 영역 로컬 파일, .gitignore 필수text-embedding-3-small은 100개 이상 언어를 동일한 고차원 공간에 투영합니다. 한국어 "죽고 싶다"와 영어 "I want to die"가 인접한 벡터를 가집니다.
CKKS 내적 연산에 최적화된 차원수. L2 정규화 후 내적 = 코사인 유사도. 추가 변환 없이 HE 파이프라인에 직접 연결됩니다.
모델 버전을 고정하면 임베딩 공간이 변하지 않습니다. 위험 프로파일 DB와 사용자 임베딩이 항상 같은 공간을 공유하는 것이 보장됩니다.
임베딩은 의미 측정, LLM은 경보 설명 생성에만 사용합니다. 임베딩은 평문 노출 없이 HE 파이프라인 진입, LLM은 복호화된 구조화 데이터만 처리합니다.
실제 TenSEAL CKKS 백엔드와 연결된 라이브 데모입니다.
서버는 암호문만 연산하며 원문 메시지를 절대 볼 수 없습니다.