경우에 따라 개인 정보 보호 또는 보안상의 이유로 트레이스의 입력 및 출력이 로깅되지 않도록 방지해야 할 수 있습니다. LangSmith는 트레이스의 입력 및 출력이 LangSmith 백엔드로 전송되기 전에 필터링할 수 있는 방법을 제공합니다.
트레이스의 입력 및 출력을 완전히 숨기려면 애플리케이션 실행 시 다음 환경 변수를 설정할 수 있습니다:
LANGSMITH_HIDE_INPUTS=true
LANGSMITH_HIDE_OUTPUTS=true
이는 LangSmith SDK(Python 및 TypeScript)와 LangChain 모두에서 작동합니다.
또한 특정 Client 인스턴스에 대해 이 동작을 사용자 정의하고 재정의할 수 있습니다. 이는 Client 객체의 hide_inputs 및 hide_outputs 매개변수를 설정하여 수행할 수 있습니다(TypeScript에서는 hideInputs 및 hideOutputs).
아래 예제에서는 hide_inputs 및 hide_outputs 모두에 대해 단순히 빈 객체를 반환하지만, 필요에 따라 이를 사용자 정의할 수 있습니다.
import openai
from langsmith import Client
from langsmith.wrappers import wrap_openai
openai_client = wrap_openai(openai.Client())
langsmith_client = Client(
hide_inputs=lambda inputs: {}, hide_outputs=lambda outputs: {}
)
# 생성된 트레이스에는 메타데이터가 있지만 입력은 숨겨집니다
openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
],
langsmith_extra={"client": langsmith_client},
)
# 생성된 트레이스에는 숨겨진 입력 및 출력이 없습니다
openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Hello!"},
],
)
입력 및 출력의 규칙 기반 마스킹
이 기능은 다음 LangSmith SDK 버전에서 사용할 수 있습니다:
- Python: 0.1.81 이상
- TypeScript: 0.1.33 이상
입력 및 출력의 특정 데이터를 마스킹하려면 create_anonymizer / createAnonymizer 함수를 사용하고 클라이언트 인스턴스를 생성할 때 새로 생성된 익명화 도구를 전달할 수 있습니다. 익명화 도구는 정규식 패턴 목록과 대체 값으로 구성하거나 문자열 값을 받아서 반환하는 함수로 구성할 수 있습니다.
익명화 도구는 LANGSMITH_HIDE_INPUTS = true인 경우 입력에 대해 건너뜁니다. LANGSMITH_HIDE_OUTPUTS = true인 경우 출력에 대해서도 마찬가지입니다.
그러나 입력 또는 출력이 클라이언트로 전송되는 경우, anonymizer 메서드는 hide_inputs 및 hide_outputs에 있는 함수보다 우선합니다. 기본적으로 create_anonymizer는 최대 10개의 중첩 레벨까지만 확인하며, 이는 max_depth 매개변수를 통해 구성할 수 있습니다.
from langsmith.anonymizer import create_anonymizer
from langsmith import Client, traceable
import re
# 정규식 패턴 목록과 대체 값으로 익명화 도구 생성
anonymizer = create_anonymizer([
{ "pattern": r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}", "replace": "<email-address>" },
{ "pattern": r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", "replace": "<UUID>" }
])
# 또는 함수로 익명화 도구 생성
email_pattern = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}")
uuid_pattern = re.compile(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
anonymizer = create_anonymizer(
lambda text: email_pattern.sub("<email-address>", uuid_pattern.sub("<UUID>", text))
)
client = Client(anonymizer=anonymizer)
@traceable(client=client)
def main(inputs: dict) -> dict:
...
익명화 도구는 페이로드를 처리하기 전에 JSON으로 직렬화하므로 복잡한 정규식이나 큰 페이로드를 사용할 경우 성능 저하가 발생할 수 있습니다.
이전 버전의 LangSmith SDK는 hide_inputs 및 hide_outputs 매개변수를 사용하여 동일한 효과를 얻을 수 있습니다. 이러한 매개변수를 사용하여 입력 및 출력을 보다 효율적으로 처리할 수도 있습니다.
import re
from langsmith import Client, traceable
# 이메일 주소 및 UUID에 대한 정규식 패턴 정의
EMAIL_REGEX = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}"
UUID_REGEX = r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
def replace_sensitive_data(data, depth=10):
if depth == 0:
return data
if isinstance(data, dict):
return {k: replace_sensitive_data(v, depth-1) for k, v in data.items()}
elif isinstance(data, list):
return [replace_sensitive_data(item, depth-1) for item in data]
elif isinstance(data, str):
data = re.sub(EMAIL_REGEX, "<email-address>", data)
data = re.sub(UUID_REGEX, "<UUID>", data)
return data
else:
return data
client = Client(
hide_inputs=lambda inputs: replace_sensitive_data(inputs),
hide_outputs=lambda outputs: replace_sensitive_data(outputs)
)
inputs = {"role": "user", "content": "Hello! My email is [email protected] and my ID is 123e4567-e89b-12d3-a456-426614174000."}
outputs = {"role": "assistant", "content": "Hi! I've noted your email as [email protected] and your ID as 123e4567-e89b-12d3-a456-426614174000."}
@traceable(client=client)
def child(inputs: dict) -> dict:
return outputs
@traceable(client=client)
def parent(inputs: dict) -> dict:
child_outputs = child(inputs)
return child_outputs
parent(inputs)
단일 함수의 입력 및 출력 처리
process_outputs 매개변수는 Python의 경우 LangSmith SDK 버전 0.1.98 이상에서 사용할 수 있습니다.
클라이언트 수준의 입력 및 출력 처리 외에도 LangSmith는 @traceable 데코레이터의 process_inputs 및 process_outputs 매개변수를 통해 함수 수준의 처리를 제공합니다.
이러한 매개변수는 특정 함수의 입력 및 출력이 LangSmith에 로깅되기 전에 변환할 수 있는 함수를 받습니다. 이는 페이로드 크기를 줄이거나, 민감한 정보를 제거하거나, 특정 함수에 대해 객체가 LangSmith에서 직렬화되고 표현되는 방식을 사용자 정의하는 데 유용합니다.
다음은 process_inputs 및 process_outputs 사용 방법의 예제입니다:
from langsmith import traceable
def process_inputs(inputs: dict) -> dict:
# inputs는 키가 인수 이름이고 값이 제공된 인수인 딕셔너리입니다
# 처리된 입력이 포함된 새 딕셔너리를 반환합니다
return {
"processed_key": inputs.get("my_cool_key", "default"),
"length": len(inputs.get("my_cool_key", ""))
}
def process_outputs(output: Any) -> dict:
# output은 함수의 직접 반환 값입니다
# 출력을 딕셔너리로 변환합니다
# 이 경우 "output"은 정수입니다
return {"processed_output": str(output)}
@traceable(process_inputs=process_inputs, process_outputs=process_outputs)
def my_function(my_cool_key: str) -> int:
# 함수 구현
return len(my_cool_key)
result = my_function("example")
이 예제에서 process_inputs는 처리된 입력 데이터가 포함된 새 딕셔너리를 생성하고, process_outputs는 LangSmith에 로깅하기 전에 출력을 특정 형식으로 변환합니다.
프로세서 함수에서 소스 객체를 변경하지 않는 것이 좋습니다. 대신 처리된 데이터가 포함된 새 객체를 생성하고 반환하세요.
비동기 함수의 경우 사용법이 유사합니다:
@traceable(process_inputs=process_inputs, process_outputs=process_outputs)
async def async_function(key: str) -> int:
# 비동기 구현
return len(key)
이러한 함수 수준 프로세서는 둘 다 정의된 경우 클라이언트 수준 프로세서(hide_inputs 및 hide_outputs)보다 우선합니다.
빠른 시작
규칙 기반 마스킹을 다양한 익명화 도구와 결합하여 입력 및 출력에서 민감한 정보를 제거할 수 있습니다. 이 가이드에서는 정규식, Microsoft Presidio 및 Amazon Comprehend를 사용하는 방법을 다룹니다.
정규식
아래 구현은 완전하지 않으며 일부 형식이나 엣지 케이스를 놓칠 수 있습니다. 프로덕션에서 사용하기 전에 모든 구현을 철저히 테스트하세요.
정규식을 사용하여 LangSmith로 전송되기 전에 입력 및 출력을 마스킹할 수 있습니다. 아래 구현은 이메일 주소, 전화번호, 전체 이름, 신용 카드 번호 및 주민등록번호를 마스킹합니다.
import re
import openai
from langsmith import Client
from langsmith.wrappers import wrap_openai
# 다양한 PII에 대한 정규식 패턴 정의
SSN_PATTERN = re.compile(r'\b\d{3}-\d{2}-\d{4}\b')
CREDIT_CARD_PATTERN = re.compile(r'\b(?:\d[ -]*?){13,16}\b')
EMAIL_PATTERN = re.compile(r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b')
PHONE_PATTERN = re.compile(r'\b(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b')
FULL_NAME_PATTERN = re.compile(r'\b([A-Z][a-z]*\s[A-Z][a-z]*)\b')
def regex_anonymize(text):
"""
정규식 패턴을 사용하여 텍스트의 민감한 정보를 익명화합니다.
Args:
text (str): 익명화할 입력 텍스트입니다.
Returns:
str: 익명화된 텍스트입니다.
"""
# 민감한 정보를 자리 표시자로 대체합니다
text = SSN_PATTERN.sub('[REDACTED SSN]', text)
text = CREDIT_CARD_PATTERN.sub('[REDACTED CREDIT CARD]', text)
text = EMAIL_PATTERN.sub('[REDACTED EMAIL]', text)
text = PHONE_PATTERN.sub('[REDACTED PHONE]', text)
text = FULL_NAME_PATTERN.sub('[REDACTED NAME]', text)
return text
def recursive_anonymize(data, depth=10):
"""
데이터 구조를 재귀적으로 순회하여 민감한 정보를 익명화합니다.
Args:
data (any): 익명화할 입력 데이터입니다.
depth (int): 과도한 재귀를 방지하기 위한 현재 재귀 깊이입니다.
Returns:
any: 익명화된 데이터입니다.
"""
if depth == 0:
return data
if isinstance(data, dict):
anonymized_dict = {}
for k, v in data.items():
anonymized_value = recursive_anonymize(v, depth - 1)
anonymized_dict[k] = anonymized_value
return anonymized_dict
elif isinstance(data, list):
anonymized_list = []
for item in data:
anonymized_item = recursive_anonymize(item, depth - 1)
anonymized_list.append(anonymized_item)
return anonymized_list
elif isinstance(data, str):
anonymized_data = regex_anonymize(data)
return anonymized_data
else:
return data
openai_client = wrap_openai(openai.Client())
# 익명화 함수로 LangSmith 클라이언트 초기화
langsmith_client = Client(
hide_inputs=recursive_anonymize, hide_outputs=recursive_anonymize
)
# 생성된 트레이스에는 메타데이터가 있지만 입력 및 출력은 익명화됩니다
response_with_anonymization = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "My name is John Doe, my SSN is 123-45-6789, my credit card number is 4111 1111 1111 1111, my email is [email protected], and my phone number is (123) 456-7890."},
],
langsmith_extra={"client": langsmith_client},
)
# 생성된 트레이스에는 익명화되지 않은 입력 및 출력이 있습니다
response_without_anonymization = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "My name is John Doe, my SSN is 123-45-6789, my credit card number is 4111 1111 1111 1111, my email is [email protected], and my phone number is (123) 456-7890."},
],
)
익명화된 실행은 LangSmith에서 다음과 같이 표시됩니다:
익명화되지 않은 실행은 LangSmith에서 다음과 같이 표시됩니다:
Microsoft Presidio
아래 구현은 사용자와 LLM 간에 교환되는 메시지의 민감한 정보를 익명화하는 방법에 대한 일반적인 예제를 제공합니다. 완전하지 않으며 모든 경우를 고려하지 않습니다. 프로덕션에서 사용하기 전에 모든 구현을 철저히 테스트하세요.
Microsoft Presidio는 데이터 보호 및 비식별화 SDK입니다. 아래 구현은 Presidio를 사용하여 LangSmith로 전송되기 전에 입력 및 출력을 익명화합니다. 최신 정보는 Presidio의 공식 문서를 참조하세요.
Presidio 및 spaCy 모델을 사용하려면 다음을 설치하세요:
pip install presidio-analyzer
pip install presidio-anonymizer
python -m spacy download en_core_web_lg
또한 OpenAI를 설치하세요:
import openai
from langsmith import Client
from langsmith.wrappers import wrap_openai
from presidio_anonymizer import AnonymizerEngine
from presidio_analyzer import AnalyzerEngine
anonymizer = AnonymizerEngine()
analyzer = AnalyzerEngine()
def presidio_anonymize(data):
"""
사용자가 보내거나 모델이 반환한 민감한 정보를 익명화합니다.
Args:
data (any): 익명화할 데이터입니다.
Returns:
any: 익명화된 데이터입니다.
"""
message_list = (
data.get('messages') or [data.get('choices', [{}])[0].get('message')]
)
if not message_list or not all(isinstance(msg, dict) and msg for msg in message_list):
return data
for message in message_list:
content = message.get('content', '')
if not content.strip():
print("Empty content detected. Skipping anonymization.")
continue
results = analyzer.analyze(
text=content,
entities=["PERSON", "PHONE_NUMBER", "EMAIL_ADDRESS", "US_SSN"],
language='en'
)
anonymized_result = anonymizer.anonymize(
text=content,
analyzer_results=results
)
message['content'] = anonymized_result.text
return data
openai_client = wrap_openai(openai.Client())
# 익명화 함수로 langsmith 클라이언트 초기화
langsmith_client = Client(
hide_inputs=presidio_anonymize, hide_outputs=presidio_anonymize
)
# 생성된 트레이스에는 메타데이터가 있지만 입력 및 출력은 익명화됩니다
response_with_anonymization = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "My name is Slim Shady, call me at 313-666-7440 or email me at [email protected]"},
],
langsmith_extra={"client": langsmith_client},
)
# 생성된 트레이스에는 익명화되지 않은 입력 및 출력이 있습니다
response_without_anonymization = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "My name is Slim Shady, call me at 313-666-7440 or email me at [email protected]"},
],
)
익명화된 실행은 LangSmith에서 다음과 같이 표시됩니다:
익명화되지 않은 실행은 LangSmith에서 다음과 같이 표시됩니다:
Amazon Comprehend
아래 구현은 사용자와 LLM 간에 교환되는 메시지의 민감한 정보를 익명화하는 방법에 대한 일반적인 예제를 제공합니다. 완전하지 않으며 모든 경우를 고려하지 않습니다. 프로덕션에서 사용하기 전에 모든 구현을 철저히 테스트하세요.
Comprehend는 개인 식별 정보를 감지할 수 있는 자연어 처리 서비스입니다. 아래 구현은 Comprehend를 사용하여 LangSmith로 전송되기 전에 입력 및 출력을 익명화합니다. 최신 정보는 Comprehend의 공식 문서를 참조하세요.
Comprehend를 사용하려면 boto3를 설치하세요:
또한 OpenAI를 설치하세요:
AWS에서 자격 증명을 설정하고 AWS CLI를 사용하여 인증해야 합니다. 여기의 지침을 따르세요.
import openai
import boto3
from langsmith import Client
from langsmith.wrappers import wrap_openai
comprehend = boto3.client('comprehend', region_name='us-east-1')
def redact_pii_entities(text, entities):
"""
감지된 엔티티를 기반으로 텍스트의 PII 엔티티를 수정합니다.
Args:
text (str): PII를 포함하는 원본 텍스트입니다.
entities (list): 감지된 PII 엔티티 목록입니다.
Returns:
str: PII 엔티티가 수정된 텍스트입니다.
"""
sorted_entities = sorted(entities, key=lambda x: x['BeginOffset'], reverse=True)
redacted_text = text
for entity in sorted_entities:
begin = entity['BeginOffset']
end = entity['EndOffset']
entity_type = entity['Type']
# 엔티티 유형에 따라 수정 자리 표시자 정의
placeholder = f"[{entity_type}]"
# 텍스트의 PII를 자리 표시자로 대체
redacted_text = redacted_text[:begin] + placeholder + redacted_text[end:]
return redacted_text
def detect_pii(text):
"""
AWS Comprehend를 사용하여 주어진 텍스트에서 PII 엔티티를 감지합니다.
Args:
text (str): 분석할 텍스트입니다.
Returns:
list: 감지된 PII 엔티티 목록입니다.
"""
try:
response = comprehend.detect_pii_entities(
Text=text,
LanguageCode='en',
)
entities = response.get('Entities', [])
return entities
except Exception as e:
print(f"Error detecting PII: {e}")
return []
def comprehend_anonymize(data):
"""
사용자가 보내거나 모델이 반환한 민감한 정보를 익명화합니다.
Args:
data (any): 익명화할 입력 데이터입니다.
Returns:
any: 익명화된 데이터입니다.
"""
message_list = (
data.get('messages') or [data.get('choices', [{}])[0].get('message')]
)
if not message_list or not all(isinstance(msg, dict) and msg for msg in message_list):
return data
for message in message_list:
content = message.get('content', '')
if not content.strip():
print("Empty content detected. Skipping anonymization.")
continue
entities = detect_pii(content)
if entities:
anonymized_text = redact_pii_entities(content, entities)
message['content'] = anonymized_text
else:
print("No PII detected. Content remains unchanged.")
return data
openai_client = wrap_openai(openai.Client())
# 익명화 함수로 langsmith 클라이언트 초기화
langsmith_client = Client(
hide_inputs=comprehend_anonymize, hide_outputs=comprehend_anonymize
)
# 생성된 트레이스에는 메타데이터가 있지만 입력 및 출력은 익명화됩니다
response_with_anonymization = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "My name is Slim Shady, call me at 313-666-7440 or email me at [email protected]"},
],
langsmith_extra={"client": langsmith_client},
)
# 생성된 트레이스에는 익명화되지 않은 입력 및 출력이 있습니다
response_without_anonymization = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "My name is Slim Shady, call me at 313-666-7440 or email me at [email protected]"},
],
)
익명화된 실행은 LangSmith에서 다음과 같이 표시됩니다:
익명화되지 않은 실행은 LangSmith에서 다음과 같이 표시됩니다: