모델을 추가할 때는 답변 텍스트 외의 계약도 맞춰야 한다

AI Chat에 Claude를 추가하면서 유지해야 할 것은 기존 화면의 대화 흐름이었다. 화면은 답변의 생성·진행·완료 이벤트와 웹 검색 인용, 코드 실행 결과를 같은 방식으로 처리한다. 모델 호출이 성공해도 공급자마다 다른 스트림과 사용량을 그대로 넘기면, 모델을 바꿀 때마다 화면과 통계의 해석도 달라진다.

Claude 초기 통합은 기존 BaseStreamService 아래에 AnthropicStreamService를 연결해 공통 형식을 유지했다. 모델의 provider 값으로 서비스 경로를 고르고, Claude의 블록 이벤트를 제품의 SSE 이벤트와 저장 필드로 바꿨다. Gemini가 이미 무상태 공급자의 선례를 제공했으므로, 기존 공통 계층을 확장한 작업이었다.

사용량도 그대로 복사할 수 없었다. 당시 Claude 응답의 input_tokens는 캐시 쓰기·읽기 토큰을 제외한 값이다. 공통 입력 사용량은 세 값을 더해 복원했다. 캐시를 적용한 요청에서 비캐시 토큰만 통계에 넣으면, 실제 입력의 일부가 월별 사용량 판정에서 빠진다.

전체 입력 토큰 = 비캐시 입력 + 캐시 쓰기 + 캐시 읽기

초기 공통 타입의 cached_tokens에는 캐시 읽기만 담겼다. 쓰기와 읽기에 서로 다른 비용이 붙는다는 사실까지 이 한 필드로 설명할 수는 없었고, 캐시 쓰기·읽기·비캐시 값을 별도로 로그에 남겼다.

같은 요청의 도구 재개와 다음 질문의 이력을 나눈다

OpenAI는 previous_response_id로 이전 응답을 연결하지만, 당시 Claude Messages 경로는 대화 이력을 매 요청 조립해야 했다. 최초 설계는 공급자의 원본 응답 블록을 저장해 다시 보내는 방식이었다. 이후 이전 답변을 저장된 텍스트로 재조립하는 방식으로 바꿨다.

이 변경은 원본 블록을 어느 때나 버려도 된다는 판단이 아니다. 구현은 진행 중인 서버 도구가 pause_turn으로 멈추면, 그 요청에서 받은 assistant 블록을 가공하지 않고 다음 호출에 붙여 재개한다. 다음 사용자 질문을 위한 과거 이력은 별도로 저장된 답변 텍스트에서 만든다. 웹 검색 인용과 코드 실행 내역은 제품의 전용 필드에 남긴다.

재개 요청마다 초기 생성 이벤트를 다시 보내거나 인용 목록을 비우면 화면에도 중복·유실이 생길 수 있다. 생성 이벤트의 전송 여부와 인용·코드 누적기는 재개 반복문 밖에서 유지한다. 무한 재개를 막는 상한도 두지만, 상한에 도달하면 경고 로그를 남기고 그때까지의 텍스트를 저장한다. 기존 화면 형식을 유지했다는 사실이 모든 답변의 완결성을 보장하지는 않는다.

이력을 보내는 범위에도 비용이 있었다. 초기 구현은 최근 20개 메시지와 첨부 파일의 크기 합계로 범위를 제한했다. 만료된 첨부의 외부 파일 ID를 다시 보내는 대신 안내 텍스트를 넣었다. 이 방식은 오래된 대화의 문맥을 제한하며, 파일 크기 상한도 모델의 토큰 한도와 같은 단위가 아니다.

최근 메시지 창이 움직이면 캐시의 앞부분도 바뀐다

무상태 요청은 같은 지시문과 이전 대화, 첨부를 반복해서 보낸다. 캐시를 붙이면 반복 입력의 비용을 줄일 수 있지만, 매번 달라지는 앞부분을 계속 쓰면 캐시 쓰기 비용이 오히려 부담이 된다. 초기 정책은 운영 대화의 길이와 질문 간격, 당시 쓰기 단가를 함께 검토해 TTL과 재사용할 입력 범위를 정했다. 캐시 표시는 두 위치에 나눴다.

초기 통합의 캐시 표시 조건
위치포함하는 입력표시 조건
system의 마지막 블록앞에 놓인 도구 정의와 시스템 지시문이력 창과 별개로 표시
현재 user 메시지의 마지막 블록앞의 대화 이력과 현재 첨부·질문조회한 과거 메시지가 한도보다 적을 때 표시

과거 이력이 한도 안에 있으면 대화는 앞부분을 유지하며 뒤에 늘어난다. 반면 최근 20개 창이 잘리기 시작하면 오래된 메시지가 빠지면서 매번 앞부분이 달라진다. 구현은 조회 결과가 한도에 닿았을 때 두 번째 표시를 생략했다.

이 조건은 추가 개수 조회 없이 기존 조회 결과만 사용한다. 정확히 20개이고 아직 잘리지 않은 경계에서도 표시를 생략하므로, 캐시 기회를 한 번 놓칠 수 있다. 당시 기본 TTL은 5분이며 1시간 옵션은 사용하지 않았다. 당시 요율에서는 5분 캐시 쓰기가 일반 입력의 1.25배, 1시간 쓰기가 2배여서 유지 시간을 늘리는 선택에도 비용이 있었다.

정책 검토는 경성대 운영의 최근 90일 대화 15,232개를 대상으로 했다. 10턴 이하 대화는 83.3%, 모든 턴 간격이 5분 이내인 대화는 71.7%였다. 이는 짧은 이력 창과 TTL을 판단하는 분포이며, 실제 캐시 적중률이나 전체 운영비 절감률은 아니다. 단일 턴 대화 비중은 당시 별도로 측정하지 않았다.

입력 토큰 수와 입력 비용은 같은 숫자가 아니다

당시 캐시 검토 기록에는 같은 4턴 대화의 사용량 비교가 있다. 아래 표는 대화 이력의 캐시 적용 후 비캐시·쓰기·읽기 토큰과, 기록된 요율을 적용한 입력 비용을 보여 준다. 비용의 단위는 일반 입력 토큰 단가를 1로 둔 토큰 상당량이다. 원화나 달러로 청구된 금액이 아니다.

4턴 기록 · 토큰 사용량과 일반 입력 단가로 환산한 입력 비용
턴캐시 미적용 입력
(토큰)
적용 후 비캐시
(토큰)
캐시 쓰기
(토큰)
캐시 읽기
(토큰)
적용 후 입력 비용
(토큰 상당량)
187687600876
21,19721,19601,497
31,50223061,196504.1
41,77322681,502487.2
합계5,3488821,7702,6983,364.3

계산은 비캐시 입력에 1배, 캐시 쓰기에 1.25배, 읽기에 0.1배를 적용한다.

캐시 미적용 입력 비용: 5,348 × 1 = 5,348
캐시 적용 입력 비용: 882 + 1,770 × 1.25 + 2,698 × 0.1 = 3,364.3
입력 비용 감소율: (5,348 − 3,364.3) ÷ 5,348 = 약 37.1%

적용 후 처리한 입력 토큰의 합계는 882 + 1,770 + 2,698 = 5,350이다. 토큰 수 자체를 37.1% 줄인 결과가 아니라, 반복 입력에 다른 요율을 적용한 결과다. 첫 쓰기에는 더 높은 단가를 지불하므로, 후속 읽기가 없는 짧은 대화까지 같은 비율로 절감된다고 볼 수 없다.

별도 첨부 검증에서는 250KB 한국어 자료의 110,315토큰이 후속 턴에서 캐시 쓰기에서 읽기로 전환된 기록이 있다. 이 첨부 실험과 4턴 표, 운영 대화 15,232개의 분포는 서로 다른 자료다. 4턴의 감소율에는 출력 비용과 전체 운영비가 포함되지 않는다.

초기 이력 제한과 후속 요약 정책은 다른 변경이다

테스트는 SDK에 보낼 system 블록, 짧은 이력의 마지막 캐시 표시, 한도에 닿았을 때의 생략, 첨부 뒤 텍스트 블록의 표시를 확인한다. 공급자 스트림을 모킹한 검사는 재개 중 생성 이벤트와 인용 결과의 중복·유실도 다룬다. 외부 캐시가 실제로 적중했는지는 이런 요청·이벤트 검사만으로 증명할 수 없다.

비용 표와 첨부 결과는 강병관이 작성한 당시 제품 검토 기록의 사용량을 근거로 한다. 37.1%는 그 표에 기록된 요율을 적용한 재계산이며, 공급자 청구 내역을 대조한 수치는 아니다. 제품의 실제 배포·사용량 자료와 테스트가 확인하는 요청 형태를 같은 수준의 증거로 취급하지 않는다.

공통 화면 형식을 유지하되, 진행 중인 도구의 원본 상태와 다음 질문의 저장 이력은 구분했다. 캐시도 반복 전송하는 입력 중 앞부분이 유지되는 범위에 적용했다. 최근 20개 창과 조건부 캐시는 초기 통합 당시의 정책이다. 후속 기능 브랜치에서는 요약 이후 전체 이력과 캐시 경계를 맞추며 이력 제한과 날짜 위치를 바꿨다. 미완료 응답과 무차감 호출의 사용량 정책도 별도의 후속 변경이다. 4턴 계산은 초기 캐시 정책의 결과이며, 요약을 적용한 장기 대화의 비용을 대표하지 않는다.