본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 05. 21. 10:55

SDK가 전송하는 프롬프트는 당신이 작성한 프롬프트가 아닙니다

요약

개발자가 작성한 프롬프트와 SDK를 통해 실제로 서버에 전송되는 요청 본문 사이의 불일치 문제를 다룹니다. SDK가 메시지를 정규화하거나 메타데이터를 추가하는 과정에서 의도치 않은 동작이 발생할 수 있음을 지적하며, 이를 디버깅하기 위한 도구인 `agenttap`을 소개합니다.

핵심 포인트

  • SDK는 사용자가 입력한 단순 문자열을 특정 구조(예: content 블록)로 자동 정규화하여 전송함
  • 코드상의 로그와 실제 네트워크를 통해 나가는 JSON 요청 데이터는 다를 수 있음
  • SDK 내부에서 설정된 기본값이나 메타데이터가 의도치 않게 포함되어 유출될 위험이 있음
  • 네트워크 계층(Transport)에서 요청을 가로채 실제 전송 데이터를 확인하는 디버깅 방식이 필요함

Claude로부터 터무니없는 답변이 돌아왔습니다. 제 코드상의 시스템 프롬프트 (System Prompt)는 괜찮아 보였습니다. 로그에 찍힌 메시지 (Messages)들도 괜찮아 보였습니다. 그래서 client.messages.create(...) 바로 직전에 print(messages)를 추가했습니다. 여전히 괜찮았습니다. 저는 잘못된 곳을 보고 있었습니다. SDK가 요청 본문 (Request Body)을 구축하고 있었던 것입니다. 네트워크 선 (Wire)을 타고 나간 것은 제가 출력한 내용이 아니었습니다. 그래서 저는 나가는 요청을 가로채서 실제 JSON을 덤프하고, 제가 보냈다고 생각하는 것과 실제로 전송된 것을 비교(diff)할 수 있는 httpx 트랜스포트 (Transport)를 작성했습니다. 저는 그것을 agenttap이라고 불렀습니다. 여기서 제가 놓친 부분은, 제가 깔끔한 2턴 대화라고 생각했던 호출에 대해 캡처된 요청 내용입니다: { "model" : "claude-opus-4-7" , "max_tokens" : 1024 , "system" : "You are a careful code reviewer." , "messages" : [ { "role" : "user" , "content" : [ { "type" : "text" , "text" : "Review this diff: \n\n diff \n + foo \n" } ] }, { "role" : "assistant" , "content" : "Looks fine to me." }, { "role" : "user" , "content" : "What about edge cases?" } ], "metadata" : { "user_id" : "u_8821" } }. 제가 작성하지 않은 세 가지가 있었습니다: 첫 번째 사용자 메시지가 [{"type": "text", "text": ...}] 블록으로 감싸져 있었습니다. 제 코드는 일반 문자열 (Plain String)을 전달했습니다. SDK가 이를 정규화 (Normalize)한 것입니다. diff에 포함된 백틱 세 개(triple-backtick) 코드 블록이 언어 태그를 포함하여 문자 그대로 보존되었습니다. 저는 그것들을 제거하기로 되어 있는 헬퍼 (Helper) 함수를 가지고 있었습니다. metadata.user_id는 6번의 커밋 전 클라이언트 생성자 (Client Constructor)에서 설정하고 잊어버린 기본값으로부터 유출되고 있었습니다. 이 중 그 어떤 것도 제가 가진 변수의 로그에는 나타나지 않았을 것입니다. 그것들은 오직 네트워크 선 (Wire) 단계에서만 나타납니다. agenttap이 하는 일은 다음과 같습니다: pip install agenttap . 그 다음:

import httpx
from agenttap import TapTransport
from anthropic import Anthropic

tap = TapTransport ( wrap = httpx . HTTPTransport ())
client = Anthropic ( http_client = httpx . Client ( transport = tap ))
client . messages . create (
    model = " claude-opus-4-7 " ,
    max_tokens = 256 ,
    messages = [{ " role " : " user " , " content " : " hi " }],
)

for record in tap . records :
    print ( record . request_json )
    print ( record .

response_status, record.duration_ms, " ms" ) 전송 계층 (Transport)은 SDK의 하위에 위치합니다. 이 계층은 이것이 Anthropic인지 알지 못하며 상관하지도 않습니다. OpenAI의 Python SDK, Google 클라이언트, 또는 최종적으로 httpx를 호출하게 되는 그 어떤 것과도 동일한 방식으로 작동합니다. 이 계층은 호출당 네 가지를 캡처합니다: 전체 요청 URL 및 헤더 (authorization 및 x-api-key는 ***로 마스킹 처리됨), 정확한 요청 본문 바이트 (가능한 경우 JSON으로 디코딩됨), 응답 상태, 밀리초 단위의 소요 시간, 그리고 응답 본문의 복사본, 그리고 코루틴 (coroutines) 간에 정렬할 수 있도록 하는 단조 증가 시퀀스 번호 (monotonic sequence number)입니다.

재생 (Replay) 및 차이점 비교 (Diff)
제가 이것을 만든 이유는 단순히 보기 위해서만이 아니었습니다. 저는 재생하기를 원했습니다.

from agenttap import replay

캡처된 요청을 fixture로 고정합니다

tap.save("fixtures/review_call.json")

나중에, 정확한 바이트를 재생합니다

resp = replay("fixtures/review_call.json", api_key=os.environ["ANTHROPIC_API_KEY"])

그리고 두 기록의 차이점을 비교합니다:

from agenttap import diff_records
a = tap.records[0]
b = tap.records[1]
print(diff_records(a, b))

- messages[0].content[0].text: "Review this diff:..."

+ messages[0].content[0].text: "Review this PR:..."

- metadata.user_id: "u_8821"

+ metadata.user_id: "u_4410"

그 차이점 비교를 통해 지난주 프롬프트 템플릿 변경으로 인해 불필요한 줄바꿈이 추가되었던 회귀 (regression) 문제를 잡아낼 수 있었습니다. 출력 결과는 여전히 그럴싸해 보였지만, 결정론적 평가 (deterministic eval)가 어긋나고 있었습니다. 와이어 (wire) 차이 비교는 저에게 정확한 바이트를 보여주었습니다.

수치 (Numbers)
메모리에서 캡처할 때 호출당 발생하는 오버헤드: 제 노트북에서 2 KB 본문 기준 약 0.4 ms입니다. 전송 계층이 응답 본문을 버퍼링하므로 스트리밍 (streaming)은 약간 다릅니다. 스트리밍 응답을 스트리밍 상태로 유지하고 싶다면, capture_response_body=False를 전달하여 요청 측만 기록되도록 할 수 있습니다. 기본 링 버퍼 (ring buffer)는 마지막 500개의 기록을 보유합니다. tap.save_all(dir="taps/")를 사용하여 디스크로 플러시 (flush)하거나 순환 (rotate)할 수 있습니다.

이것이 해결하지 못하는 것
몇 가지 솔직한 한계점입니다. 이것은 오직 와이어 (wire)만 볼 수 있습니다. 만약 SDK가 내부적으로 재시도 (retries)를 수행하고 로거 (logger)를 하나만 설정했다면, 모든 재시도 기록이 남게 됩니다. 이는 노이즈가 될 수 있습니다.

메시지 본문에서 개인정보 (PII)를 삭제하지는 않습니다. 헤더에 포함된 자격 증명 (credentials)만 삭제합니다. 사용자 데이터를 전송하는 경우, 데이터를 영구 저장하기 전에 여전히 삭제 (redaction) 단계를 거쳐야 합니다. 스트리밍 응답 (Streaming responses)은 SSE 이벤트 스트림 (event stream)이 아니라 결합된 출력물로 캡처됩니다. 이벤트 수준의 추적 (traces)이 필요한 경우, claude-stream-rs를 사용하거나 별도의 핸들러를 연결하십시오. 삭제 목록은 보수적으로 설정되어 있습니다. 만약 사용하는 제공업체가 커스텀 인증 헤더 이름을 사용한다면, 이를 직접 추가해야 합니다: TapTransport(redact_headers=["x-my-auth"]). 만약 프로덕션 환경에서 에이전트 (agents)를 실행하면서 프로세스를 떠나는 실제 JSON을 한 번도 확인해 본 적이 없다면, 꼭 한 번 확인해 보십시오. 무언가를 발견하게 될 것입니다. 리포지토리 (Repo): https://github.com/MukundaKatta/agenttap PyPI: pip install agenttap 이것은 제가 AI 에이전트 배관 작업 (AI agent plumbing: 스냅샷, 예산, 드리프트, 복구)을 위해 발행하는 소수의 집중된 라이브러리 중 하나입니다. 실제 사고 사례들을 바탕으로 하나씩 구축되었습니다.

AI 자동 생성 콘텐츠

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

원문 바로가기
0

댓글

0