
에이전트 스팬(Agent Span)에서 무엇을 캡처하고 무엇을 마스킹(Redact)해야 하는가
요약
LLM 에이전트의 관측성(Observability)을 확보하기 위해 스팬(Span) 설계 시 결정(Decision) 기록과 개인정보(PII) 마스킹 사이의 균형을 맞추는 방법을 다룹니다. 에이전트의 루프를 추적하기 위해 ID, 이름, 설명, 버전과 같은 필수 속성을 정의하는 가이드를 제공합니다.
핵심 포인트
- 에이전트 스팬 설계 시 의사결정 기록과 개인정보 마스킹의 균형이 필수적임
- 운영 효율을 위해 ID, Name, Description, Version 속성을 반드시 포함해야 함
- 모델 교체나 프롬프트 수정에 따른 성능 퇴보를 방지하기 위해 버전 관리가 중요함
- 단순 응답뿐만 아니라 모델의 도구 호출 등 '결정' 과정을 기록해야 함
- 도서: Observability for LLM Applications — Tracing, Evals, and Shipping AI You Can Trust
- 저자의 다른 저서: Agents in Production — The AI Engineer's Library (2권 시리즈)의 동반 도서
- 내 프로젝트: Hermes IDE | GitHub — Claude Code 및 기타 AI 코딩 도구를 사용하여 작업하는 개발자를 위한 IDE
- 나: xgabriel.com | GitHub
당신은 호출(paged)을 받습니다. 지원 에이전트(support agent)가 고객 이메일에 잘못된 환불 금액을 작성했습니다. 당신은 트레이싱(tracing) 백엔드를 열고, 궤적(trajectory)을 찾아 스팬(span)을 읽기 시작합니다. 모든 스팬은 초록색입니다. 모든 도구 호출(tool call)이 반환되었습니다. 그런데 세 번째 턴의 chat 스팬을 보니, 프롬프트에 전체 신용카드 번호가 포함된 고객의 원문 메시지가 그대로 들어있습니다. 이제 이 정보는 당신의 관측성(observability) 벤더, 로그 파이프라인, 그리고 방금 Slack에 붙여넣은 스크린샷에까지 복사되었습니다.
하나의 트레이스(trace) 안에 두 가지 문제가 있습니다. 스팬이 '의사결정(decision)'을 기록하지 않았기 때문에 에이전트가 어디서 잘못되었는지 알 수 없었습니다. 그리고 스팬이 '페이로드(payload)'를 있는 그대로 기록했기 때문에 개인정보(PII)가 유출되었습니다. 이 두 가지 실패는 서로 반대되는 방향을 향하고 있으며, 적절한 균형을 잡는 것이 에이전트를 위한 스팬 설계의 핵심입니다.
나머지는 어떤 속성(attribute)을 원문 그대로 기록해도 안전한지, 그리고 어떤 속성이 프로세스를 떠나기 전에 마스킹(redaction) 과정을 거쳐야 하는지를 아는 것입니다.
실제로 쿼리하게 될 네 가지 속성부터 시작하세요
에이전트 턴(agent turn)은 단 한 번의 LLM 호출이 아닙니다. 그것은 루프(loop)입니다: 모델이 결정하고, 도구가 실행되고, 결과가 돌아오고, 모델이 다시 결정합니다. 전체 루프를 감싸는 스팬이 당신의 invoke_agent 부모(parent)가 됩니다. 여기에 네 가지 속성을 부착하면 나중에 거의 모든 운영상의 질문에 답할 수 있습니다.
from opentelemetry import trace
tracer = trace.get_tracer("support-agent")
...
id는 버그를 보고할 때 사용합니다. name은 UI에 표시되는 레이블입니다. description은 전체 프롬프트가 아니라 시스템 프롬프트의 한 줄 요약입니다. version은 사람들이 건너뛰고 나중에 후회하는 항목입니다. 에이전트는 조용히 퇴보(regress)합니다: 프롬프트 수정, 화이트리스트에 추가된 새 도구, 두 개의 마이너 개정판 사이의 모델 교체 등. 평가 점수가 떨어지면 가장 먼저 하는 일은 gen_ai.agent.version으로 필터링하여 에이전트가 변경되었는지 확인하는 것입니다. 이것 없이는 추측만 할 수 있습니다.
응답뿐 아니라 결정(decision)을 기록하세요
일반적인 LLM 호출의 경우, 신경 써야 할 속성은 모델이 말한 내용입니다. 하지만 에이전트의 chat 스팬에서는 모델이 사용자에게 보여지는 답변을 생성하는 경우는 드뭅니다. 대신 '결정(decision)'을 생성합니다: 이 인수로 이 도구를 호출하라, 저 에이전트에게 넘기라, 또는 멈추라는 것입니다. 그 결정이야말로 기록할 가치가 있는 것이며 구조를 가지고 있습니다.
span.set_attribute("gen_ai.agent.step", 3)
span.set_attribute("gen_ai.agent.decision", "call_tool")
span.set_attribute(...)
decision은 하니스가 정의하고 모든 곳에서 사용하는 작고 고정된 어휘(vocabulary)로 유지하세요: call_tool, handoff, final_answer, reflect, stop. decision.reason도 짧게 유지하고, 가능하다면 모델 자체의 추론(rationale)에서 파생시키세요. 이 속성들은 아직 OpenTelemetry GenAI 컨벤션에 안정적이지는 않지만, 에이전트 백엔드에서는 이미 gen_ai.agent.* 아래에서 읽고 있으며, 나중에 평가 파이프라인은 gen_ai.agent.decision으로 그룹화하여 궤적(trajectories)의 점수를 매깁니다. 이 속성이 방출되지 않았다면 해당 쿼리는 불가능합니다.
부모 스팬에 대해서도 한 가지 더 말씀드리자면: 루프를 빠져나갈 때 gen_ai.agent.step.count를 설정하세요. 이것은 최종 턴(turn) 수를 나타내는 하나의 정수이며,
하위 chat 스팬(span)은 실행 비용을 산정할 수 있게 해주는 다음 수치들을 유지합니다:
gen_ai.request.model, gen_ai.usage.input_tokens,
gen_ai.usage.output_tokens. execute_tool 스팬은
gen_ai.tool.name 및 gen_ai.tool.call.id를 유지합니다. 이 중 어느 것도
민감한 정보가 아닙니다. 모두 기록하세요.
페이로드(Payloads)에서 문제가 발생합니다
위의 속성들은 메타데이터(metadata)입니다. 모든 프롬프트(prompt)와 모든 완료(completion) 내용을 전체 그대로 기록하고 싶은 유혹이 생길 수 있습니다. 기록하지 않았던 단 한 번의 상황이 바로 당신에게 꼭 필요했던 트레이스(trace)였을 것이기 때문입니다. 하지만 전체 충실도(full-fidelity) 버전을 유지하려는 유혹을 뿌리치십시오. 두 가지 이유가 있습니다.
첫 번째는 크기입니다. 에이전트의 루프 후반부에 있는 chat 스팬은 누적된 컨텍스트(context)로 인해 수만 개의 토큰(token)을 포함할 수 있습니다. 대부분의 스팬 익스포터(span exporter)와 백엔드(backend)는 제한을 초과하는 속성을 조용히 잘라내거나(truncate) 삭제합니다 (OpenTelemetry의 기본값은 128개의 속성 및 구성 가능한 값당 길이 제한입니다). 모든 스팬에 40 KB의 프롬프트 문자열이 포함된다면, 이는 데이터 수집 비용(ingest bill)을 폭증시키고 정작 중요한 작은 속성들을 유실하게 만드는 원인이 됩니다.
두 번째는 유출입니다. 프롬프트는 사용자의 개인정보(PII)가 존재하는 바로 그 지점입니다. 이를 가공 없이 기록하면 이름, 이메일, 카드 번호, 건강 정보 등을 해당 데이터의 처리 범위(scope)에 포함되지 않았던 제3자 벤더(third-party vendor)로 복사하게 되는 셈입니다.
따라서 페이로드가 스팬에 담기기 전에 제한을 두십시오. 마커(marker)와 함께 투박하게 잘라내는 것이 제한 없는 문자열을 사용하는 것보다 낫습니다:
MAX_CHARS = 2048
def clip(text: str, limit: int = MAX_CHARS) -> str:
...
디버깅 가능성(debuggability)을 위해 잘라낸 페이로드를 기록하되, 잘린 스팬을 짧은 스팬으로 오해하지 않도록 실제 길이를 별도의 속성으로 기록하십시오:
span.set_attribute("gen_ai.prompt.length", len(prompt))
span.set_attribute("gen_ai.prompt.preview", clip(prompt))
이제 스팬은 입력값이 9,000자였다는 것을 알려주면서 동시에 처음 2,000자를 보여줍니다. 이는 전체 데이터를 네트워크를 통해 전송하지 않고도 입력된 내용의 형태를 파악하기에 대개 충분합니다.
트레이스를 무력화하지 않으면서 PII를 마스킹(Redact)하기
길이 제한(Truncation caps) 크기입니다. 이는 첫 2,000자 내의 카드 번호를 제거하지 않습니다. set_attribute에 도달하기 전에 값에 대해 실행되는 마스킹(redaction) 패스가 필요합니다. 핵심은 데이터의 _정체성(identity)_을 마스킹하면서도 그 _형태(shape)_는 유지하는 것입니다. 왜냐하면 형태가 트레이스를 디버깅 가능하게 만들기 때문입니다. '이메일 형식이 잘못되었습니다'라는 메시지는 마스킹 후에도 여전히 보고 싶은 실제 버그입니다.
import re
PATTERNS = {
...
"charge card 4111 1111 1111 1111 for a.b@x.io"를 이 과정을 통해 전달하면 "charge card [CARD] for [EMAIL]"가 반환됩니다. 문장 구조, 도구 의도(tool intent), 그리고 카드와 이메일이 존재했다는 사실은 유지되었습니다. 값만 제거된 것입니다. 이를 보는 검토자는 잘 구성된 요청과 규제 대상 데이터가 없다는 것을 확인합니다.
마스킹을 먼저 실행하고 그 다음 크기 제한을 적용하도록 두 패스를 연결하세요:
def safe(text: str) -> str:
return clip(redact(text))
...
정규 표현식(Regex)은 형식화된 클래스, 즉 카드, 이메일, 전화번호, IBAN, SSN을 포착합니다. 자유 텍스트 이름이나 집 주소는 포착하지 못하므로, 이를 상한선이 아닌 하한선으로 간주하세요. 더 어려운 클래스의 경우, 내보내기(export) 전에 Claude를 사용하여 가벼운 구조화된 출력 패스(structured-output pass)로 스팬을 태그하거나, 기록해도 안전한 도구 인자 목록(allowlist)을 사용하세요. lookup_order 도구의 order_id는 원본 그대로 유지하는 것이 괜찮지만, billing_address는 그렇지 않습니다.
경계에서 한 번만 수행하기
코드베이스 전반에 걸쳐 safe()를 여기저기 사용하는 것은 잊힌 호출 하나가 누출(leak)을 의미합니다. 마스킹을 사용자 지정 SpanProcessor에 넣어 프롬프트 및 완료 미리보기(prompt and completion previews)가 나갈 때마다 이를 통과하도록 하세요. 이렇게 하면 호출자가 잊더라도 안전합니다:
from opentelemetry.sdk.trace import SpanProcessor
SENSITIVE = {"gen_ai.prompt.preview",
...
파이프라인 내의 exporter(내보내기 도구) 이전에 이를 등록하면, 개발자가 safe() 호출을 잊어버리더라도 모든 에이전트와 모든 도구에 걸쳐 해당 두 필드가 마스킹(Redact)됩니다. 이는 SENSITIVE에 포함된 키들만 다루므로, 기록하는 다른 자유 형식(free-text) 속성이 있다면 해당 세트에 추가하십시오. 동일한 아이디어가 TypeScript에서도 작동하며, 실제 OTLP exporter에 위임하기 전에 span.attributes를 매핑하는 커스텀 SpanExporter 래퍼(wrapper)를 통해 구현할 수 있습니다.
균형 (The balance)
비용이 저렴한 속성들은 전체를 그대로 기록해도 안전하며, 이는 15분짜리 디버깅 세션과 2시간짜리 디버깅 세션 사이의 차이를 만들어냅니다. 반면 페이로드(Payloads)는 export(내보내기) 경계에서 제한(cap)과 마스킹(redaction) 과정을 거쳐야 하는 대상입니다. 이 구분을 제대로 해낸다면, 고객의 카드 번호는 보여주지 않으면서도 에이전트가 무엇을 결정했는지는 보여주는 트레이스(trace)를 얻을 수 있습니다. 즉, 새벽 3시에 실제로 Slack에 붙여넣을 수 있는 트레이스를 얻게 되는 것입니다.
만약 에이전트를 구축하고 있다면, Agents in Production에서 이러한 스팬(span)을 방출하는 하네스(harness)와 그 아래의 도구 루프(tool loop)에 대해 자세히 다룹니다. 만약 이를 중심으로 트레이싱(tracing), 평가(evals), 비용 회계(cost accounting)를 연결하고 있다면, Observability for LLM Applications가 동반 도서가 될 것입니다. 이 두 권은 _The AI Engineer's Library_의 두 절반이며, 이와 같은 스팬 설계는 그 두 권이 만나는 이음새와 같습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기