
LLM에 개인정보를 전달하지 않고 CS 문의 대응 에이전트를 만드는 법
요약
개인정보 유출 리스크를 원천 차단하기 위해 LLM에 민감 정보를 전달하지 않고 CS 문의를 처리하는 에이전트 설계 방식을 소개합니다. LLM의 역할을 분류와 문장화로 한정하고, 실제 데이터 검색과 수정은 코드가 담당하도록 구성하여 보안성을 높였습니다.
핵심 포인트
- LLM에 성명, 이메일, 직원 번호 등 개인정보를 절대 전달하지 않는 설계
- LLM은 문의 분류 및 답변 문장 생성 역할만 수행
- 데이터 검색, 진단, 업데이트 등 핵심 로직은 코드로 구현
- CS 담당자의 승인을 거치는 Human-in-the-loop 구조 채택
Canly에서 복리후생 서비스인 「Canly 복리후생」을 개발하고 있는 코이즈미입니다.
「Canly 복리후생」은 고객 기업의 직원이 이메일 주소와 직원 번호로 등록하여 사용하는 서비스입니다.
「근무지가 관리하는 번호로 본인 확인을 한다」는 이 방식의 서비스에는 업종을 불문하고 공통적인 운영 테마가 있습니다.
등록 시 번호 입력 오류나, 고용 구분 변경에 따른 번호 변경 시점에 등록 또는 로그인을 하지 못하는 분들이 일정 수 존재한다는 점입니다.
건당 대응은 정형화되어 있음에도 불구하고, 계정 조사와 근무지 기업에 대한 확인 왕복 과정이 필요하기 때문에 CS 대응 시간이 조용히 쌓여갑니다.
이 정형화된 왕복 과정을 자동화하여, CS의 업무를 「확인과 승인」으로만 한정하는 것. 그것이 이번의 목적입니다.
해결책으로서 LLM을 사용한 문의 대응 에이전트(Agent)를 만들었습니다.
다만 다루는 데이터는 고객 기업으로부터 맡은 직원의 개인정보이므로, 설계 전에 한 가지 제약을 두었습니다.
LLM에 개인정보를 전달하지 않는다.
성명도 이메일 주소도 직원 번호도 LLM으로의 입력에 포함하지 않습니다.
LLM의 역할을 「분류」와 「문장화」로 한정하고, 검색·진단·데이터 업데이트는 모두 코드가 담당하는 구성입니다.
그러면서도 원인 진단, 사용자에게 보내는 답장, 고객 기업에 대한 재직 확인, 답변에 기반한 데이터 수정까지 자동화하고 있습니다.
이 기사에서는 이 제약을 충족하기 위한 3가지 설계를 코드와 함께 소개합니다.
「LLM을 업무에 도입하고 싶지만, 개인정보가 장벽이 되어 진행하지 못하고 있다」는 팀에게 참고가 되기를 바랍니다.
대상 업무와 만든 것
대상으로 삼은 것은 「등록할 수 없음」, 「로그인할 수 없음」, 「퇴직 처리되어 있음」의 3가지 유형의 문의입니다.
원인의 대부분은 직원 번호의 불일치로 귀결됩니다.
등록 시 번호를 잘못 입력한 케이스가 대다수이며, 드물게 재직 중인 고용 구분 변경으로 번호 자체가 바뀌어 있는 케이스가 있습니다.
문의는 폼 도구인 Tayori를 통해 도착하며, 그 알림이 Slack으로 전달됩니다.
기존에는 이 알림 이후의 모든 과정이 CS 멤버의 수작업이었습니다.
- Slack 알림을 보고 관리 화면에서 계정을 조사하여 원인을 분류한다
- 사용자에게 「조사 중입니다」라고 1차 답장을 보낸다
- 고객 기업의 인사 담당자에게 재직 상황과 올바른 직원 번호를 이메일로 문의한다
- 답변이 도착하면 기업에 감사 인사를 전하며 관리 화면에서 데이터를 수정한다
- 사용자에게 완료 연락을 한다
이번에 만든 것은 이 1~5 단계를 대신 수행하는 Slack 상주 에이전트입니다.
조사와 문구 작성은 에이전트가 수행하며, CS의 업무는 「에이전트가 준비한 초안을 Slack 상에서 승인(또는 수정·거절)하는 것」으로 바뀝니다.
이메일 송신과 데이터 업데이트는 반드시 CS의 승인을 거치며, 인간의 확인 없이는 실행되지 않습니다.
도중에 판단이 서지 않는 케이스(원인을 특정할 수 없음, 답장에서 번호를 추출할 수 없음 등)는 자동 처리를 중단하고, Slack으로 CS 팀을 멘션하여 인간에게 인계합니다.
제약: LLM에 개인정보를 전달하지 않는다
이 업무에서 다루는 것은 고객 기업으로부터 맡은 직원의 개인정보입니다.
성명, 이메일 주소, 직원 번호, 재직 상황.
모두 취급에 신중을 기해야 하는 데이터이며, 외부 LLM API에 전송하는 것에 대한 심리적·설명적 허들이 높았습니다.
모델 제공 측의 「입력을 학습에 사용하지 않는다」는 규약을 근거로 전송하는 판단도 있을 수 있습니다.
하지만 저희는 규약에 대한 신뢰와는 별개의 차원에서 「애초에 전달하지 않으면 설명이 필요 없다」는 상태를 만들기로 했습니다.
유출 리스크 평가나 고객 기업에 대한 설명을 「전송하고 있지만 안전합니다」라고 구성하는 것보다, 「전송하지 않습니다」라고 말할 수 있는 편이 더 강력하기 때문입니다.
설계 방침: LLM의 역할을 「분류」와 「문장화」로 한정한다
설계 이야기를 하기 전에, 이 기사에서 「에이전트」라고 부르는 것을 명확히 해두겠습니다.
AI 에이전트라고 하면 LLM이 도구(Tool)를 가지고 상황을 보며 다음 행동을 스스로 결정하여 루프를 도는 구성을 가리키는 경우가 많을 것입니다.
저희 시스템은 그런 의미에서의 에이전트가 아닙니다.
문의에 반응하여 진단부터 데이터 업데이트까지의 업무를 자율적으로 진행하는 것은 Lambda 상의 오케스트레이터(Orchestrator) 코드이며, 에이전트성은 시스템 전체의 성질입니다.
LLM은 행동을 결정하는 주체가 아니라, 시스템에 내장된 부품으로서 작동합니다.
이 역할 분담은 제약으로부터 도출된 결과입니다.
개인정보를 전달할 수 없는 이상, LLM에게 계정 검색 결과를 보여주고 판단하게 할 수는 없습니다.
그래서 업무의 각 단계를 「코드로 작성할 수 있는가」로 분류했습니다.
계정 검색, 원인 진단, 번호 추출, 데이터 업데이트.
이것들은 모두 규칙으로 작성할 수 있으며, 게다가 코드가 더 잘하는 일입니다.
동일한 입력에는 동일한 결과를 반환하고, 테스트를 작성할 수 있으며, 나중에 감사(Audit)도 가능합니다.
분류 결과, LLM에 남은 작업은 두 가지입니다.
분류: 고객 기업으로부터 온 답장이라는 자유 문장을 읽고 '재직 중'인지 '퇴직함'인지를 판정하는 작업. "이미 재직 중이지 않습니다"와 같은 부정형이나 "◯월 말로 퇴직 예정입니다"와 같이 시제를 포함한 표기 차이는 키워드 매칭(Keyword Match) 규칙으로는 정확도를 높이기 어렵습니다. 고유명사를 마스킹(Masking)한 본문으로도 판정이 가능하며, LLM의 독해력이 실용상 거의 유일한 선택지가 되는 작업입니다 -
문장화: 비즈니스 메일의 문면을 구성하는 작업. 수신인이나 번호는 나중에 채워 넣을 수 있으므로, 생성 시점에 개인정보는 불필요합니다
문장화의 경우, 사실 고정 템플릿(Fixed Template)으로도 성립합니다.
LLM에 전달하는 것은 고정 프롬프트(Prompt)뿐이므로, 현시점의 출력은 매번 거의 동일한 문면이 되기 때문입니다.
그럼에도 LLM에 맡긴 데에는 세 가지 이유가 있습니다.
첫째는 문면 변경 비용입니다.
템플릿 방식에서는 문면 수정이 코드의 변경이지만, 프롬프트 방식이라면 일본어 지시사항을 다시 쓰는 것으로 충분합니다.
"조금 더 부드러운 톤으로"라는 CS의 요구를 문면에 반영하는 루프가 가벼워집니다.
둘째는 조건과 종류의 증가에 강하다는 점입니다.
초안의 종류나 "복구가 필요한 경우에는 이 문장을 추가한다"와 같은 조건이 늘어날수록, 템플릿은 분기(Branch)와 사이 문구 관리로 변해갑니다.
프롬프트라면 지시사항을 하나 더 추가하는 것만으로 경어(Honorifics)나 문장의 접속은 LLM이 정리해 줍니다.
셋째는 확장의 길이 열려 있다는 점입니다.
시스템은 이미 마스킹된 정보(재직 확인 답변의 요약이나 재직 상황 카테고리)를 가지고 있으며, 이를 프롬프트에 전달하면 "답변 내용에 따른 문면"으로 진화시킬 수 있습니다.
고정 템플릿에는 이러한 진화의 여지가 없습니다.
요약하자면, LLM의 독해력이 필요한 작업(분류)과 LLM에 맡기면 운용이 편해지는 작업(문장화)만을 LLM에 남기고, 그 외에는 모두 코드로 몰아넣은 분담입니다.
제약에 맞춰 용도를 타협했다기보다, 코드로 쓸 수 있는 것은 코드로 쓴다는 당연한 원칙을 제약 사항이 철저하게 만들어 준 형태입니다.
LLM은 도구를 가지지 않으며, 외부 데이터에 접근할 수단이 없습니다.
시스템 전체는 다음과 같습니다.
그리고 LLM의 입출력 경계선은 다음과 같습니다.
여기서부터 이 경계선을 지키기 위한 세 가지 설계를 살펴보겠습니다.
한마디로 말하면, 판단과 추출은 전달하지 않는다, 문면은 값을 전달하지 않고 쓰게 한다, 어쩔 수 없이 전달해야 하는 자유 문장은 다층으로 깎아낸다의 세 가지입니다.
설계 1: 판단과 추출은 LLM에게 시키지 않는다
진단 전에 문의 내용에서 성명이나 이메일 주소 등의 메타 정보(Meta Information)를 추출할 필요가 있습니다.
Tayori의 Slack 알림은 폼(Form) 유래의 고정 포맷으로, "성함:", "이메일 주소:"와 같이 라벨이 붙은 항목이 나열되므로, 라벨 뒤의 값을 정규 표현식(Regular Expression)으로 잘라낼 수 있습니다.
자유 문장의 독해가 아니라 포맷의 파싱(Parsing)이므로, 필수 항목을 가져올 수 없는 알림은 자동 처리하지 않고 사람에게 에스컬레이션(Escalation)한다는 식으로 깔끔하게 작성할 수 있습니다.
추출한 이메일 주소와 사원 번호로 코드가 관리 화면 API의 계정 검색을 수행하고, 그 결과에 기반하여 원인을 진단합니다.
"원인 분류"는 LLM이 잘할 것 같은 작업으로 보이지만, 이 진단에는 계정 검색 결과(실명, 실제 번호, 퇴직 플래그)가 필요합니다.
LLM에게 시키면 개인정보가 통째로 컨텍스트(Context)에 들어갑니다.
그래서 진단은 LLM을 사용하지 않는 결정적인 로직으로 만들었습니다.
계정 검색 결과와 문의 입력값을 코드로 작성한 규칙에 대조하여 원인 패턴을 판정합니다.
export const diagnose = (input: DiagnosisInput): Diagnosis => {
// 계정 검색 결과(실명·실제 번호·퇴직 플래그)와 문의 입력값을
// 코드로 작성한 규칙으로 대조하여 원인 패턴을 판정한다
...
규칙 기반(Rule-based)으로 만든 대가로 경계 사례(Edge Case)에서의 유연함은 상실됩니다.
이는 "판정할 수 없으면 반드시 사람에게 되돌린다"라는 안전장치로 대응하기로 했습니다.
어떤 규칙에도 해당하지 않는 문의는 자동 처리되지 않고, Slack으로 CS 팀에게 멘션(Mention)이 날아갑니다.
동일한 원칙은 플로우의 후반부에도 한 곳 더 있습니다.
고객 기업의 인사 담당자로부터 재직 확인 답장 메일이 도착하면, 거기에는 올바른 사원 번호가 적혀 있습니다.
「메일 본문에서 값을 추출하기」 또한 LLM의 전형적인 활용법이지만, 추출 대상인 사원 번호 그 자체가 전달해서는 안 되는 값입니다.
그래서 추출도 정규 표현식 (Regular Expression)으로 수행합니다.
일본어 비즈니스 메일에서 번호를 전달하는 방식은 「사원 번호: 12345」, 「올바른 번호는 12345입니다」와 같이 라벨 근처에 위치하는 경우가 거의 대부분이므로, 라벨 패턴을 열거하는 것만으로도 실용적입니다.
const TOKEN = "([A-Za-z0-9][A-Za-z0-9\\-_]*)";
const LABEL_PATTERNS: RegExp[] = [
new RegExp(`(?:新しい|正しい)?(?:従業員番号|社員番号)[^\S\r\n]*(?:は|[::])?[^\S\r\n]*${TOKEN}`, "g"),
...
추출 시에는 문의 시에 입력되었던 (잘못되었을 가능성이 높은) 번호를 제외 리스트에 넣습니다.
답장 메일에는 「12345로 등록되어 있습니다만, 올바른 번호는 67890입니다」와 같이 이전 번호가 반복되는 경우가 많기 때문에, 이를 후보에서 제외하기 위함입니다.
정규 표현식이므로, 예상치 못한 작성 방식은 놓칠 수 있습니다.
추출하지 못한 경우에는 데이터 업데이트 안을 만들지 않고 사람에게 에스컬레이션 (Escalation)하며, 후보가 여러 개 잡힌 경우에는 모든 후보를 열거한 업데이트 안을 제시하여 CS가 스레드 답장으로 확정하는 2단계 구조로 만들었습니다.
「LLM이라면 더 똑똑하게 추출할 수 있을 텐데」라는 유혹도 있었지만, 추출 정밀도 향상을 대가로 개인정보를 전달하는 것은 본말전도입니다.
진단도 추출도 관통하고 있는 것은 같은 원칙입니다.
규칙으로 쓸 수 있는 판단은 규칙으로 쓰고, 규칙에서 벗어난 것은 추측하지 않고 사람에게 넘긴다.
설계 2: 문구는 틀만 쓰게 하고, 값은 나중에 삽입한다
진단이 내려지면, 사용자에게 보내는 1차 답장과 고객 기업에 보내는 재직 확인 요청 메일 문구를 작성합니다.
이 부분이 LLM의 차례이지만, 문구에는 수신인(성명)이나 사원 번호가 들어갑니다.
그래서 LLM에는 플레이스홀더 (Placeholder)가 포함된 템플릿을 쓰게 하기로 했습니다.
LLM에 전달하는 것은 초안 유형별로 고정된 프롬프트 (Prompt)뿐이며, 문의 내용을 포함하여 가변적인 입력은 없습니다.
// 프롬프트 발췌 (사용자 대상 1차 답장 예시)
`【최우선 규칙】
당신에게는 개인정보가 전달되지 않습니다. 문구 중에서 개인정보를 언급하는 부분은,
...
제시하는 플레이스홀더 목록은 초안 유형별로 「그 단계에서 실제 값을 삽입할 수 있는 것」으로만 한정하고 있습니다.
목록에 올린 플레이스홀더는 LLM이 성실하게 사용하기 때문에, 삽입할 값이 존재하지 않는 단계의 초안에 포함하면 미해결된 표기법이 그대로 문구에 남기 때문입니다.
LLM이 반환한 문구에 코드가 실제 값을 삽입합니다.
export const fillPlaceholders = (
template: string,
values: Record<string, string | null | undefined>,
...
흐름을 도식화하면 다음과 같습니다.
부수적인 효과로 LLM의 오기입에 대한 내성도 높아졌습니다.
실제 값을 LLM에 전달하여 문구를 작성하게 하는 구성에서는, LLM이 번호를 딱 한 자리만 잘못 적는 일이 발생할 수 있습니다.
이는 겉보기에는 진짜와 구별하기 어렵고, 리뷰에서도 알아채기 힘듭니다.
이 구성에서는 문구에 실제 값이 들어가는 경로는 코드를 통한 기계적인 삽입뿐이며, LLM은 진짜 값을 모르기 때문에 「진짜와 매우 유사한 틀린 값」을 만들어낼 방법이 없습니다.
삽입 후에 플레이스홀더가 미해결 상태로 남은 경우에는 로그에 기록하고, Slack 상의 승인을 통해 CS가 알아챌 수 있도록 운영하고 있습니다.
설계 3: 어쩔 수 없이 전달해야 하는 자유문은 다층으로 마스킹한다
지금까지의 설계로 LLM에게 업무 데이터를 전달하는 부분은 단 한 곳이 되었습니다.
답장 메일이 「재직 중」인지 「퇴직 상태」인지 분류하는 것입니다.
이것은 자유문의 독해이므로, 역시 본문을 전달하지 않으면 판정할 수 없습니다.
전달하기 전에 마스킹 (Masking)을 세 층으로 겹칩니다.
첫 번째 층은 **기지값 마스킹 (Known Value Masking)**입니다.
문의 메타 정보로부터 성명, 이메일 주소, 사원 번호, 회사명은 이미 알고 있으므로, 이러한 문자열(정규 표현식으로 추출 완료된 번호 후보 포함)을 기계적으로 [MASKED]로 치환합니다.
export const maskKnownValues = (
text: string,
knownValues: Array<string | null | undefined>,
...
두 번째 층은 **패턴 마스킹 (Pattern Masking)**입니다.
기존 값 리스트에 없는 연락처, 예를 들어 인사 담당자의 서명에 있는 직통 전화번호나 이메일 주소는 첫 번째 층에서 지울 수 없습니다.
이메일 주소 형식과 전화번호 형식의 문자열을 정규 표현식 (Regular Expression)으로 추가 마스킹합니다.
export const maskContactPatterns = (text: string): string => {
return text
.replace(/[\w.+-]+@[\w-]+\.[\w.-]+/g, "[MASKED]")
...
마스킹된 본문을 받은 LLM의 역할은 재직 상태의 3가지 분류 (employed / retired / unknown)와 1~2문장의 요약뿐입니다.
프롬프트에서는 "[MASKED]의 내용을 추측하여 보완하지 말 것"을 명시하고 있습니다.
그럼에도 불구하고, 지금까지의 두 층은 기지 값(Known values)과 패턴에 기반한 베스트 에포트 (Best effort)입니다.
담당자가 답장에 예상치 못한 제3자의 성명을 적었을 경우, 기지 값 리스트에도 패턴에도 걸리지 않아 그대로 통과될 가능성이 있습니다.
이 잔여 리스크에 대한 세 번째 층으로서, Amazon Bedrock Guardrails의 sensitive information filter를 배치했습니다.
Bedrock Guardrails란
Bedrock Guardrails는 애플리케이션과 모델 사이에 끼워 넣는 AWS의 관리형 (Managed) 안전장치입니다.
앱이 모델을 호출하면, 요청은 먼저 Guardrails의 정책 평가를 거치고, 모델의 응답도 동일한 평가를 거친 후 앱으로 반환됩니다.
정책에는 유해 콘텐츠 필터링이나 화제 제한 등이 있으며, 이번에 사용한 것은 개인정보를 다루는 sensitive information filter입니다.
sensitive information filter의 탐지 방법은 두 가지가 있습니다.
성명·이메일 주소·전화번호와 같은 내장된 PII 엔티티 (기계 학습 기반의 개체명 인식 (Named Entity Recognition))와, 직접 정의하는 커스텀 정규 표현식입니다.
탐지 시 동작도 두 가지 선택지가 있어, 요청 자체를 차단하는 BLOCK과, 해당 부분만 {NAME}과 같은 태그로 교체하여 처리를 계속하는 ANONYMIZE를 선택할 수 있습니다.
여기서 짚고 넘어가야 할 점은, 이것이 프롬프트에 대한 "부탁"이 아니라, 모델 외부에서 기계적으로 실행되는 처리라는 점입니다.
LLM이 어떻게 행동하든, 설정한 정책은 입출력에 반드시 적용됩니다.
프롬프트를 통한 통제 (설계 2의 "창작해서는 안 됩니다" 등)와는 신뢰의 층위가 다른 장치입니다.
왜 "Guardrails만" 사용하지 않았는가
이 이야기만 들으면, "Guardrails가 있다면 지금까지의 설계는 불필요한 것 아닌가"라는 발상이 당연히 들 수 있습니다.
실제로 Guardrails를 주인공으로 세운다면, 지금까지의 설계 대부분을 생략하고 도구가 포함된 일반적인 에이전트 구성으로 만들 수 있었을 것입니다.
그렇게 하지 않은 이유는 두 가지가 있습니다.
첫째, 탐지 품질을 보장할 수 없기 때문입니다.
공식 문서 자체에서도 sensitive information filter를 "문맥에 의존하는, 확률적 (probabilistic)인 기계 학습 기반 솔루션"이라고 설명합니다.
확률적이기 때문에 놓치는 것이 제로(0)임을 보장할 수 없습니다.
특히 일본어 성명은 까다로운데, 예를 들어 "渡辺(와타나베)"는 문맥에 따라 인명도 되고 지명도 되며 회사명의 일부가 될 수도 있어, 개체명 인식이 인명이라고 판단할 수 있다는 보장이 없습니다.
게다가 탐지는 AWS의 관리형 모델이므로, 정밀도를 자체 테스트로 고정하거나 허점을 찾아 수정할 수도 없습니다.
따라서 코드로 확정할 수 있는 부분은 모두 코드로 확정하고 (설계 1~3), 확률적인 탐지는 보험으로만 사용하는 배치를 택했습니다.
둘째, "전달한 뒤에 지우는" 장치는 전달을 전제로 한 설계와 양립할 수 없기 때문입니다.
이는 탐지가 100% 정확하더라도 발생하는 문제입니다.
실제 값을 전달하는 구성에서는 LLM의 업무가 실제 값을 읽는 것을 바탕으로 성립됩니다.
"입력값 1234568과 등록값 1234567을 비교하여 판단해"라는 입력이 ANONYMIZE에 의해 "{NUMBER}와 {NUMBER}를 비교해"로 바뀐다면, 비교라는 업무 자체가 실행 불가능해집니다.
BLOCK을 선택하면, 개인정보를 포함한 정당한 업무 데이터가 들어올 때마다 호출이 중단됩니다.
결국 실제 값을 전달하는 구성에서는 Guardrails를 완화하여 운용할 수밖에 없으므로, "Guardrails만으로 지킨다"는 성립하지 않습니다.
태그를 붙이는 방식을 궁리하여 회피하는 길도 있어 보입니다.
하지만 "같은 값은 같은 태그로 만들고, 대응표를 수중에 보관한 뒤, 나중에 다시 되돌린다"라는 고안을 스스로 완수하면, 그것은 코드가 값을 관리하고 LLM에는 플레이스홀더(Placeholder)만 보여주는 구성, 즉 본 기사의 설계에 도달하게 됩니다.
우리의 구성은 LLM이 실제 값을 필요로 하지 않으므로, Guardrails가 무엇을 얼마나 마스킹(Masking)하더라도 망가지지 않습니다.
"전달하지 않는 설계"와 "지우는 보험"은 이 순서로 겹쳤을 때만 양쪽 모두를 풀(Full)로 활용할 수 있습니다.
설정의 요점
설정에서 의식적으로 추가한 것은 **사원 번호의 커스텀 정규 표현식 (Custom Regular Expression)**입니다.
Guardrails의 프리셋 PII 엔티티에는 NAME이나 EMAIL은 있어도, "사원 번호"와 같은 업무 고유의 식별자는 없습니다.
regex_config로 라벨이 붙은 숫자 열의 패턴을 추가하여, 도메인 고유의 식별자까지 검출 대상으로 포함했습니다.
참고로 검출 시의 액션(Action)은 ANONYMIZE(마스킹 후 계속 진행)입니다.
입력은 이미 마스킹되어 있어 검출의 대부분은 오검출(False Positive)이 될 뿐만 아니라, 태스크는 마스킹되어도 성립하므로, 이 설계에서 BLOCK으로 중단해야 할 이유는 존재하지 않습니다.
이 구성에서 Guardrails가 실제로 작동하는 장면은, 2층 구조의 마스킹을 뚫고 개인정보가 입력에 남아있을 때입니다.
역할 분담을 한마디로 요약하면, 주인공은 어디까지나 "전달하지 않는 설계"이며, Guardrails는 설계의 틈새를 빠져나간 것을 잡아내는 보험입니다.
보험을 주인공으로 만들지 않는 것이 이 구성의 뼈대입니다.
남은 리스크는 어디에 있는가
마지막으로, 이 설계에서 개인정보가 유출될 수 있는 경로가 어디에 남아있는지를 솔직하게 적겠습니다.
코드로 작성한 층(진단·추출·플레이스홀더)에는 그 경로가 없습니다.
이러한 층들도 뚫릴 수는 있지만, 뚫리는 방향이 "유출"이 아니라 "중단"이기 때문입니다.
규칙에서 벗어난 문의는 추측되지 않고 인간에게 전달되며, 해결되지 않은 플레이스홀더는 전송 전에 사람의 눈에 띄게 됩니다.
유출될 수 있는 경로는 자유 문장(Free text)을 LLM에 전달하는 단 한 곳뿐입니다.
기존 값 마스킹에도 패턴 마스킹에도 걸리지 않는 값(예를 들어 담당자가 답장에 적은, 우리가 모르는 제3자의 성명)을 Guardrails의 확률적 검출(Probabilistic Detection)마저 놓쳤을 때, 그 값이 LLM에 전달됩니다.
세 가지 놓침이 겹쳤을 때만 열리는 것, 이것이 이 설계의 잔여 리스크의 전부입니다.
자유 문장을 전달하는 곳을 분류 태스크 하나로 좁힌 것도 이 경로의 면적을 최소화하기 위함이기도 합니다.
또 하나, 스코프(Scope)의 한정에 대해서도 적어둡니다.
이 설계가 보호하는 것은 "LLM으로의 입력"뿐입니다.
Slack에 게시되는 승인 요청 초안이나 DynamoDB의 상태 관리에는 실제 값이 들어갑니다.
"AI를 도입했더니 개인정보 취급이 늘어났다"라는 점만을 설계로 제로(0)로 만들었습니다.
마치며
LLM에 개인정보를 전달하지 않는다는 제약은, 처음에는 자동화의 폭을 좁히는 족쇄처럼 보였습니다.
하지만 실제로 설계해 보니, LLM의 업무가 "분류"와 "문장화"로 좁혀지면서 책임이 명확해지고, 테스트도 쓰기 쉬워졌으며, LLM의 오기입이 실제 데이터에 혼입되는 경로도 없는 시스템이 되었습니다.
개인정보의 벽 때문에 LLM 도입이 멈춰 있는 팀은, "어떻게 안전하게 전달할까"를 고민하기 전에 "전달하지 않고 끝낼 설계는 없는가"를 검토해 보시기 바랍니다.
정형 업무의 자동화라면, LLM이 정말로 필요한 일은 생각보다 적을 것입니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기