개인정보를 남기지 않고 프롬프트 로깅하기
LLM 앱을 디버깅하려면 프롬프트 로그가 필요하지만 원본 PII를 그대로 남겨둘 수는 없다. Comprehend로 저장 전에 마스킹하고, 로그가 일정에 맞춰 만료되도록 보존 기간을 설정하라.

프롬프트를 로깅하지 않고는 LLM 애플리케이션을 디버깅할 수 없고, 그 프롬프트가 이름, 이메일, 계좌번호로 가득하다면 그대로 보관할 수도 없다. 사용자는 개인 식별 정보를 입력창에 그대로 타이핑하므로, 잘못된 응답을 이해하는 데 도움을 주는 로그는 동시에 잘못된 보존 기간과 잘못된 접근 통제로 CloudWatch나 S3에 앉아 있는, 점점 커지는 규제 대상 데이터 저장소이기도 하다. 해법은 로깅을 멈추는 것이 아니다. 저장 전에 마스킹하고, 남는 것에는 보존 정책을 두어 디버깅 가치는 살아남고 부채는 남지 않도록 하는 것이다.
재구성은 이렇다. 프롬프트 로그는 디버깅 편의가 아니라 데이터 수집 표면이다. 원본 프롬프트를 디스크에 쓰는 순간 우리는 그것을 담은 데이터가 어떤 규제 아래 있든 그 데이터를 수집한 것이 된다. 로그 파이프라인을 감사가 요구할 때 나중에 정리하는 곳이 아니라, 들어오는 길목에서 PII가 제거되는 곳으로 다루어라.
나가는 길이 아니라 들어오는 길에서 마스킹하라
PII를 제거할 수 있는 유일하게 신뢰할 만한 지점은 기록되기 전이다. 저장 후 마스킹한다는 것은 원본 데이터가 이미 저장 상태로 존재했고, 이미 복제되었고, 이미 백업 안에 들어갔다는 뜻이며, "나중에 삭제한다"는 감사자가 받아들이는 통제가 아니다. 요청과 로그 싱크 사이에 마스킹 단계를 두어, 저장소에 도달하는 값이 애초부터 민감하지 않았던 값이 되게 하라.
Amazon Comprehend는 탐지의 기본 요소를 제공한다. DetectPiiEntities는 텍스트를 실시간으로 검사해 발견한 각 PII 엔티티, 그 유형, 문자 오프셋, 신뢰도 점수를 반환한다. 그 오프셋을 사용해 로깅하기 전에 해당 구간을 치환한다. Comprehend는 두 가지 마스킹 모드를 지원한다. 각 엔티티를 그 유형으로 치환해서 "Jane Doe"가 [NAME]이 되게 하는 방식은 로그를 읽을 수 있게 유지하고, 문자를 고정된 기호로 마스킹하는 방식도 있다. 유형으로 치환하는 쪽이 대개 옳다. 신원은 제거하면서도 프롬프트의 형태는 디버깅을 위해 보존하기 때문이다.
entities = comprehend.detect_pii_entities(Text=prompt, LanguageCode="en")
redacted = prompt
for e in sorted(entities["Entities"], key=lambda x: x["BeginOffset"], reverse=True):
redacted = redacted[:e["BeginOffset"]] + f"[{e['Type']}]" + redacted[e["EndOffset"]:]
log.write(redacted) # only the redacted form is ever persisted
뒤에서부터 이어 붙여야 문자열을 다시 쓰는 동안 앞쪽 오프셋이 유효하게 유지된다. 탐지는 실시간 호출이며, 대량의 과거 데이터에 대해서는 Comprehend가 비동기 마스킹 작업도 제공하지만, 새로운 기록을 보호하는 것은 실시간 경로다.
탐지는 확률적이므로 놓칠 것을 전제로 설계하라
Comprehend는 기계학습 탐지기이며, 이는 재현율이 100퍼센트 미만이고 우리가 하한선을 정해야 하는 신뢰도 점수를 갖는다는 뜻이다. 낮은 임계값은 공격적으로 마스킹해서 정상적인 텍스트를 훼손할 수 있고, 높은 임계값은 예외 사례의 PII를 통과시킬 수 있다. 두 가지 습관이 이를 안전하게 만든다.
- 결정적 패스를 한 겹 더하라. 신용카드 번호, 주민번호, 일부 계좌 형식처럼 고정된 형식을 가진 구조화된 식별자에는 정규식이 확률적 모델이 놓칠 수 있는 것을 잡아내며, 매번 같은 방식으로 잡아낸다. 둘 다 돌려라.
- 거짓 양성 쪽으로 마스킹하라. 디버그 로그에서는 과도한 마스킹이 가독성을 조금 희생시킬 뿐이다. 부족한 마스킹은 데이터 보호 사고를 초래한다. 임계값을 너무 많이 제거하는 쪽으로 편향시켜라.
Amazon Bedrock Guardrails도 모델 경계에서 민감 정보를 필터링할 수 있으며, 이는 상호 보완적이다. Guardrails는 요청과 응답 경로를 보호하고, Comprehend는 우리 자신의 로그에 쓰는 것을 보호한다. 위험이 있는 지점에 맞는 것을 써라.
보존 기간은 통제의 나머지 절반이다
마스킹은 로그가 담는 내용을 줄이고, 보존 기간은 마스킹된 형태조차 얼마나 오래 사는지를 제한한다. 마스킹된 프롬프트는 위험이 낮을 뿐 제로는 아니며, 디버그 로그의 자연스러운 유효 수명은 몇 년이 아니라 몇 주 단위로 측정된다. 싱크에서 명시적으로 보존 기간을 설정하라.
- 일정에 맞춰 만료시켜라. 정의된 기간 이후 삭제하는 CloudWatch 로그 그룹 보존 설정이나 S3 수명 주기 규칙은 아무도 기억하지 않아도 오래된 로그가 스스로 나가떨어지게 한다.
- 보존 기간을 목적에 맞춰라. 운영 디버깅은 30일에서 90일 이상을 필요로 하는 경우가 드물다. 특정 이유로 더 긴 기간이 필요하다면 그 이유를 명시하고 그 보존 기간을 필요한 로그로 국한하라.
- 접근을 잠가라. 마스킹된 로그라도 범위가 좁혀진 IAM을 받을 자격이 있다. 원본 프롬프트 로그를 읽을 수 있는 사람의 집합은 작고 이름이 명시되어 있어야 한다.
결론
프롬프트 로깅과 PII 보호는 서로 충돌하지 않는다. 같은 파이프라인의 두 단계일 뿐이다. Comprehend로 기록 전에 마스킹하되, 고정 형식 식별자를 위한 결정적 패스를 한 겹 더하고 임계값을 과도한 마스킹 쪽으로 편향시켜라. 그런 다음 남은 것에 보존 정책을 두어, 마스킹된 로그가 느슨한 접근 뒤에서 영원히 쌓이는 대신 일정에 맞춰 만료되게 하라. 이 둘을 모두 하면 앱을 고치는 데 필요한 디버깅 신호는 유지하면서 규제 대상 데이터는 저장 상태로 남지 않는다. 앱을 고치는 데 도움을 주는 로그가 유출 보고서에 등장하는 로그가 되어서는 안 된다.
이어서 읽으면 좋은 글
- IAM for LLM Apps: Least Privilege When the Caller Is a Model, 이 로그들이 닿는 데이터에 누가, 무엇이 접근할 수 있는지 범위를 정하는 글.
- Multi-Tenant LLM Apps: Isolating Customers on a Shared Model, 한 테넌트의 데이터와 로그를 다른 테넌트의 손이 닿지 않는 곳에 두는 방법에 관한 글.
로그 파이프라인, 보존, 접근 통제의 플랫폼 측면은 ercan.cloud의 클라우드 현장 노트에서 다루고, 허브는 ercanermis.com에 있다.
Ercan의 다른 글
같은 저자, 다른 영역의 사이트 두 개.