
「지난주에 환불을 세 번 넘게 요청한 고객을 찾아 문의 내용을 정리해 줘」라는 질문은 숫자 하나로 끝나지 않습니다. 모델은 상담 이력 테이블의 행을 읽어야 하고, 그 행에는 고객 이름과 전화번호, 이메일이 들어 있습니다. 상담 메모 칸에는 고객이 불러 준 환불 계좌번호가 적혀 있기도 합니다. 이 값들은 조회 결과로 모델에 들어가는 순간 모델 프로바이더로 나가고, 대화 맥락에 실려 다음 턴에도 다시 모델로 갑니다. 답변에서 그 값을 지워도 이미 나간 값은 되돌릴 수 없습니다.
그래서 개인정보는 도구 결과가 모델 입력으로 들어가기 직전에 가립니다. 무엇을 가릴지는 스키마 분석이 정한 컬럼 분류와 값의 형식이 함께 정하고, 판정은 LLM을 거치지 않는 규칙이라 같은 결과에는 매번 같은 값이 가려집니다. 가린 값은 대화 안에서 늘 같은 토큰으로 바꿉니다. 모델은 원문 없이 토큰만으로 같은 고객을 세고, 그 고객을 다시 조회할 때는 서버가 원문을 넣습니다. 이 글은 가리는 자리와 기준, 그리고 토큰을 대화 단위로 묶은 이유를 설명합니다.
프롬프트 지시는 행이 필요한 질문을 막지 못한다
시스템 프롬프트는 개인 식별 컬럼을 이유 없이 조회하지 말고, 질문에 꼭 필요하면 가리거나 집계해서 돌려 달라고 모델에게 지시합니다. 행을 가져와 직접 세지 말고 DB에서 집계하라는 지시도 함께 있습니다. 「지난달 해지 고객 수」처럼 숫자로 끝나는 질문이라면 모델이 이 지시를 지키는 한 행 값은 오지 않습니다.
첫 문단의 질문은 다릅니다. 문의 내용을 정리하려면 행을 읽어야 하고, 세 번 넘게 요청한 사람을 가리려면 고객을 서로 구분할 값이 있어야 합니다. 모델이 전화번호 컬럼을 빼고 조회해도 상담 메모 안의 계좌번호는 따라옵니다. 게다가 모델은 시스템 프롬프트의 지시를 대부분 지키지만 항상 지키지는 않습니다(「권한은 코드로 막고, 판단은 모델에 맡긴다」). 지시는 필요 없는 조회를 줄이는 데 쓰고, 개인정보가 모델에 닿지 않게 하는 일은 코드가 맡습니다.
답변을 고쳐도 되돌릴 수 없는 일이 셋 있다
답변이 완성된 뒤 개인정보를 찾아 지우는 방법은 단순하고, 화면에 보이는 답은 지킬 수 있습니다. 하지만 모델이 원문을 한 번 읽고 나면 다음 세 가지는 답변을 고쳐도 돌아오지 않습니다.
- 원문이 모델 프로바이더로 나갔습니다. 외부 모델을 쓰는 설치라면 나간 값을 지울 방법이 없습니다. 민감한 DB를 외부 모델 대화에서 아예 빼는 방법은 「외부 모델 대화에는 내부 모델 전용 DB의 이름도 싣지 않는다」에서 다룹니다.
- 원문이 대화 맥락에 남습니다. 앞 턴의 도구 결과는 다음 턴의 모델 입력에 다시 실리므로, 대화가 이어지는 동안 원문도 턴마다 다시 나갑니다.
- 답변 말고도 값이 나가는 곳이 있습니다. 모델은 원문을 엑셀 파일에 쓰고, 메일 본문에 넣고, 이슈 댓글로 올릴 수 있습니다. 이런 출구를 한데 묶어 검사하는 구조는 「답변만 검사하면 엑셀과 이슈 댓글로 샌다, 세 경로를 한 곳에서 검사한다」에서 다룹니다.
입력에서 가리면 세 가지가 한 자리에서 풀립니다. 모델이 원문을 받지 않으니 프로바이더에도, 대화 맥락에도, 파일과 메일에도 원문을 실을 수 없습니다.
무엇을 가릴지는 컬럼 분류와 값의 형식이 함께 정한다
가릴 값은 두 기준으로 고릅니다.
첫째는 컬럼 분류입니다. 스키마 분석은 컬럼마다 개인정보를 담는지 판정해 두고, 관리자는 분석 결과 화면에서 그 판정을 고칠 수 있습니다. 개인정보로 분류된 컬럼의 값은 생김새와 상관없이 가립니다. 이름이나 주소처럼 정해진 형식이 없는 값은 이 기준으로만 찾을 수 있습니다.
둘째는 값의 형식입니다. 주민등록번호, 전화번호, 이메일, 계좌번호, 카드번호처럼 형식이 정해진 값은 어느 컬럼에서 나왔든 가립니다. 컬럼이 없는 데이터, 곧 문서 검색 결과와 커넥터(회사가 OwlAI에 연결한 외부 서비스)의 결과, 웹 페이지 본문에도 이 기준이 걸립니다. 분류되지 않은 비고 컬럼에 누군가 적어 둔 전화번호도 여기서 가려집니다.
두 기준은 서로 비는 곳을 채웁니다. 컬럼 분류는 DB 조회 결과에만 있고, 값의 형식은 형식 없는 개인정보를 알아보지 못합니다. 형식 없는 값은 스키마 분석이 컬럼의 뜻을 보고 정한 분류로 가리고, 형식 있는 값은 데이터가 어디서 왔든 형식으로 가립니다.
도구 결과가 모델 입력이 되기 직전에 가린다
도구를 실행하면 결과는 모델에 들어가기 전에 정리 단계를 차례로 거칩니다. 이진 파일 데이터는 크기 표시로 바뀌고, 접근 토큰이 박힌 전송 주소는 짧은 참조로 바뀝니다(「토큰이 든 2,000자 다운로드 주소, 모델에게는 19자 참조만 보인다」). 그다음 개인정보를 가리고, 마지막으로 큰 결과를 샌드박스 파일로 떼어 둡니다. 이 파일(스필 파일)은 결과가 너무 커서 모델이 나눠 읽거나 스크립트로 읽는 사본이라서, 가린 값으로 남아야 합니다. 그래서 파일로 떼는 일은 가린 뒤에 합니다. 순서가 바뀌면 원문이 디스크에 남습니다.
이 자리는 모든 기능에 하나뿐입니다. 채팅, 플레이북, 딥 리서치 조사원, 회의가 모두 같은 에이전트 루프를 돌고, 도구 결과는 모두 같은 실행부를 지납니다(「에이전트 루프는 하나만 두고, 기능별 동작은 훅으로 끼운다」). SQL 결과든 문서 검색 결과든 커넥터 결과든 같은 규칙으로 가려지고, 새 도구를 붙여도 그 도구의 결과는 이 자리를 지납니다.
같은 값은 한 대화 안에서 늘 같은 토큰이 된다
값을 ***로 지우기만 하면 첫 문단의 질문에 답할 수 없습니다. 네 행에 같은 고객이 있는지, 세 결과에 나온 사람이 같은 사람인지 모델이 알 길이 없어서입니다. 그래서 가린 값을 종류와 번호가 붙은 토큰으로 바꿉니다. 대화에서 처음 나온 전화번호는 [전화번호-1], 그와 다른 번호는 [전화번호-2]가 되고, 같은 번호는 대화가 끝날 때까지 늘 같은 토큰입니다. 모델은 [전화번호-3]이 환불 요청 네 건에 나온 것을 보고 그 고객을 찾아내고, 원문은 끝까지 보지 않습니다.
토큰과 원문의 대응표는 모델 밖, 서버에 대화마다 따로 보관합니다. 대화를 지우면 대응표도 함께 지웁니다. 같은 번호라도 다른 대화에서는 다른 토큰이 되므로, 여러 대화의 토큰을 모아 한 사람을 따라갈 수 없습니다.
후속 조회에는 서버가 실행 직전에 원문을 넣는다
토큰만 보는 모델에게 「그 고객의 지난 주문도 보여 줘」라고 하면, 모델은 [전화번호-3]을 조건에 넣은 SQL을 씁니다. 읽기 전용 실행 도구는 SQL을 DB로 보내기 직전에 이 대화의 대응표에서 토큰을 원문으로 되돌립니다. DB는 원래 번호로 조회하고, 돌아온 결과는 같은 규칙으로 다시 가려져 같은 토큰으로 모델에게 갑니다. 원문은 회사 DB와 서버 사이에서만 오가고 모델 입력에는 실리지 않습니다.
되돌리는 일은 그 대화의 대응표에 있는 토큰에만 일어납니다. 메일이나 이슈 댓글처럼 바깥으로 쓰는 도구에 원문을 넣을지는 「답변만 검사하면 엑셀과 이슈 댓글로 샌다, 세 경로를 한 곳에서 검사한다」에서 다루는 출구 검사가 그 도구를 쓰는 사용자의 원문 열람 권한을 보고 정합니다.
원문은 열람 권한이 있는 사람만 화면에서 펼친다
모델의 답변은 토큰이 든 채로 옵니다. 관리자가 역할에 원문 열람 권한을 준 사용자는 화면에서 토큰을 눌러 원문을 펼칠 수 있고, 권한이 없는 사용자에게는 토큰만 보입니다. 공유받은 대화를 여는 사람도 자기 권한대로 봅니다.
원문을 펼친 일과 원문이 실린 파일을 내려받은 일은 감사 기록에 남습니다. 누가 언제 어느 대화에서 원문을 열었는지가 한 줄씩 남고, 펼친 값 자체는 기록에 남지 않습니다. 관리자가 컬럼 분류를 고친 일도 설정 변경으로 남습니다.
설계에서 고른 것
판정에는 LLM 대신 규칙을 골랐습니다. 개인정보를 찾는 모델(개체명 인식)을 앞에 두면 상담 메모 문장 속 이름처럼 형식이 없는 값도 잡을 수 있어 장점이 분명합니다. 하지만 그 판정을 하려면 원문을 또 하나의 모델에 보내야 하고, 같은 결과를 넣어도 판정이 매번 같다는 보장이 없으며, 도구 결과마다 모델 호출 한 번만큼 기다려야 합니다. 형식 없는 값은 컬럼 분류가 맡으므로, 판정은 같은 입력에 같은 결과가 나오는 규칙으로 두었습니다.
덜 가리기보다 더 가리는 쪽을 골랐습니다. 계좌번호와 생김새가 같은 주문번호가 함께 가려지면 업무 답이 불편해집니다. 그래서 어느 컬럼에서 어떤 종류의 값이 몇 번 가려졌는지를 로그로 남기고, 관리자는 그 기록을 보고 컬럼을 개인정보가 아닌 업무 번호로 고칩니다. 고친 분류는 다음 조회부터 적용됩니다. 한 번 나간 원문은 되돌릴 수 없지만, 지나치게 가린 값은 설정 하나로 돌아옵니다.
사용자가 직접 입력한 값은 가리지 않습니다. 고객 번호를 쳐서 묻는 사용자에게는 그 번호로 조회하는 것이 질문의 목적이고, 그 값은 이미 사용자가 모델에게 건넨 것입니다.
토큰은 설치 전체가 아니라 대화마다 새로 매깁니다. 설치 전체에서 같은 번호에 같은 토큰을 주면 지난 대화와 이번 대화의 고객을 이을 수 있습니다. 하지만 그러면 토큰이 한 사람을 어디서나 가리키는 또 하나의 식별자가 됩니다. 대화마다 새로 매기는 대가로 대화를 넘는 비교는 토큰으로 할 수 없고, 원문 열람 권한이 있는 사람이 화면에서 합니다.
에이전트가 회사 데이터를 읽어 외부 모델에 넘기는 구조라면, 개인정보를 가리는 자리는 답변 뒤보다 도구 결과를 받는 한 곳이 맞습니다. 그 자리를 모든 기능이 함께 지나가게 만들고 원문 대응표를 모델 밖에 두면, 모델은 토큰으로 분석하고 원문은 권한 있는 사람만 봅니다.
어떤 문제를 해결하나요?
보안과 감사의 다른 글

엔지니어링 노트 · 보안과 감사
외부 모델 대화에는 내부 모델 전용 DB의 이름도 싣지 않는다
고객사 망 안에 띄운 모델과 외부 API 모델을 한 설치에 함께 두면, 쓸 모델을 정해 두는 것만으로는 사내 데이터가 밖으로 나가는 일을 막지 못합니다.

엔지니어링 노트 · 보안과 감사
도구 결과에 나온 주소도 모델이 먼저 썼다면 열지 않는다
웹 페이지를 읽거나 주소에서 파일을 받는 도구는 대화에 먼저 나온 주소만 엽니다(「권한은 코드로 막고, 판단은 모델에 맡긴다」).

엔지니어링 노트 · 보안과 감사
답변만 검사하면 엑셀과 이슈 댓글로 샌다, 세 경로를 한 곳에서 검사한다
고객 문의를 분석하는 대화에서, 원문 열람 권한이 없는 사람이 보는 답변에는 전화번호가 [전화번호-1]로 가려져 있습니다(「답변에서 지운 전화번호도 모델은 이미 읽었다, 도구 결과에서 가린다」).
