엔지니어링 노트대화 엔진

OpenAI는 응답을 기본으로 저장한다, 대화 맥락은 직접 보낸다

OpenAI Responses API는 store를 따로 정하지 않은 응답을 프로바이더 서버에 저장하고, 공식 문서는 그 응답이 적어도 30일 남는다고 밝힙니다(2026-11-03 확인). 저장된 응답은 다음 요청에서 ID 하나로 이어 쓸 수 있어 편합니다. 그런데 업무 대화에는 DB 조회 결과, 사내 문서, 회의록이 섞입니다. 이 내용이 프로바이더 서버에도 남으면, 대화가 어디에 얼마나 남는지 묻는 보안 검토에 답할 곳이 둘로 늘어납니다. 그러면 OwlAI에서 대화를 지워도 프로바이더 쪽 사본은 따로 지워야 합니다.

그래서 OwlAI는 모든 호출에 store=False를 붙이고, 이전 응답 ID도 OpenAI의 대화 객체도 쓰지 않습니다. 대화 맥락의 원본은 OwlAI의 DB 한 곳에만 두고, 호출할 때마다 그 DB에서 대화 맥락 전체를 조립해 보냅니다. 이 글은 이 설계가 무엇을 지키는지, 추론 모델에서 무엇을 더 해야 하는지, 무엇을 대가로 냈는지 다룹니다.

프로바이더에 대화를 맡기는 길은 둘이고, 둘 다 저장이 전제다

OpenAI에서 대화 상태를 서버에 두는 방법은 둘입니다. 하나는 응답 ID로 잇는 방식입니다. 다음 요청의 previous_response_id에 앞선 응답의 ID를 넣으면, 서버가 저장해 둔 앞선 입력과 출력을 이어 붙입니다. 다른 하나는 대화 객체(Conversations API)입니다. 대화 객체와 거기 붙은 항목에는 30일 보존 기간이 적용되지 않습니다.

이 방식의 장점은 분명합니다. 클라이언트는 새 입력만 보내므로 요청 본문이 작습니다. 대화 맥락을 저장하고 조립하는 코드도 필요 없습니다. 스트리밍 도중 연결이 끊겨도 응답 ID로 결과를 다시 받아 올 수 있습니다.

다만 두 방식 모두 프로바이더가 대화를 저장해야 성립합니다. store=False로 만든 응답은 서버에 남지 않으므로 다음 요청이 그 ID로 이을 수 없습니다. 공식 문서도 저장하지 않는 예시에서는 앞선 대화 전체를 다시 보냅니다. 저장을 끄는 것과 서버에 대화를 맡기는 것은 둘 중 하나만 고를 수 있습니다.

응답 ID로 잇는 방식과 매번 보내는 방식: 대화가 남는 곳의 차이
그림 1. 응답 ID로 잇는 방식과 매번 보내는 방식: 대화가 남는 곳의 차이

저장을 끄면 대화가 남는 곳은 OwlAI의 DB 하나다

OwlAI는 저장을 끄는 쪽을 골랐습니다. 모든 Responses API 호출에 store=False를 붙이므로, 프로바이더의 대화 저장소에는 OwlAI의 대화가 남지 않습니다. 대화 맥락은 OwlAI의 DB에만 있고, 구축형 설치에서 그 DB는 고객사 서버 안의 PostgreSQL입니다. 대화가 어디에 얼마나 남는지는 그 DB 하나를 보면 답할 수 있습니다.

이 설정이 정하는 범위는 좁습니다. store=False가 「프로바이더가 내용을 보지 않는다」는 뜻은 아닙니다. 외부 프로바이더의 모델을 쓰면 호출마다 대화 맥락과 질문이 그 프로바이더로 갑니다. 이 설정이 정하는 것은 응답을 프로바이더의 대화 저장소에 남기느냐뿐입니다. 프로바이더가 따로 보존하는 기록도 있습니다. OpenAI는 모든 API 사용의 남용 모니터링 기록을 기본으로 최대 30일 보존하고, 그 기록에서 고객 내용을 빼는 Zero Data Retention(ZDR)은 사전 승인을 받은 조직이나 프로젝트에만 적용됩니다(2026-11-03 확인). 어느 조건이 걸리는지는 고객사가 프로바이더와 맺은 계약과 계정 설정이 정합니다.

데이터가 아예 고객사 망 밖으로 나가면 안 되는 업무에는 망 안에 둔 프로바이더의 모델을 씁니다. 「내부 모델 전용」으로 표시한 데이터는 외부 모델로 도는 대화에 보이지 않습니다(「외부 모델 대화에는 내부 모델 전용 DB의 이름도 싣지 않는다」).

추론도 대화 상태라서 암호화된 채로 받아 두었다가 되돌려 보낸다

추론 모델에서는 이어 보낼 것이 하나 더 있습니다. 모델이 도구를 부르기 전에 한 추론도 대화 상태의 일부입니다. OpenAI는 도구 호출 뒤 다음 요청에 앞선 추론 항목을 되돌려 보내라고 권합니다. 서버에 응답을 저장하지 않는다면 이 추론을 이어 줄 곳이 클라이언트뿐입니다.

OpenAI는 저장하지 않는 호출의 응답에 추론 내용을 암호화해 싣습니다. OwlAI는 모든 호출에 include=["reasoning.encrypted_content"]를 함께 보내고, 돌아온 암호화 추론 항목을 다른 응답 항목과 함께 DB에 저장합니다. 다음 호출에는 그 항목을 받은 그대로 입력에 넣습니다. 블록을 풀 수 있는 쪽은 프로바이더뿐이고, OwlAI는 보관만 맡습니다. Anthropic 모델의 추론 블록도 같습니다. 블록에 붙은 서명을 지키려고 받은 그대로 다시 보냅니다.

세 프로바이더에 함께 통하는 길은 매번 보내기뿐이다

서버에 대화를 맡기는 방식은 OpenAI에서만 통합니다. Anthropic의 Messages API는 상태를 저장하지 않아서, 공식 문서도 매 요청에 대화 전체를 보내라고 말합니다. vLLM은 Responses API 형식을 받지만 기본 설정에서는 store를 무시합니다. 서버 설정으로 켜면 응답을 프로세스 메모리에만 쌓고, 서버가 내려갈 때까지 지우지 않습니다(vLLM 소스 주석). 디스크에 남지 않아 재시작하면 사라지고, 쌓인 만큼 메모리가 늘어납니다.

그래서 대화 맥락을 OwlAI가 들고 있는 것이 세 프로바이더 모두에 통하는 유일한 방법입니다. 대화 맥락은 어느 프로바이더로 보내든 같은 방식으로 DB에서 조립하고, 다른 것은 보내기 직전에 그 프로바이더의 형식으로 바꾸는 단계뿐입니다. 저장된 대화는 받은 원본 그대로 두고 보낼 때만 맞추므로, 한 대화 안에서 모델을 바꿔도 앞선 대화가 따라옵니다(「400으로 막힌 대화, 도구 결과는 저장할 때·모양은 보낼 때 맞춘다」). 대화를 OpenAI 서버에 맡겼다면 Anthropic 모델로 넘어가는 순간 앞선 대화를 가져올 길이 없습니다. 모델을 바꿀 수 있는 범위는 대화를 시작한 프로바이더 등급 안입니다(「외부 모델 대화에는 내부 모델 전용 DB의 이름도 싣지 않는다」).

모델이 무엇을 보고 답했는지 OwlAI의 DB가 말한다

대화 맥락이 OwlAI의 DB에 있으므로, 「그때 모델이 무엇을 보고 그렇게 답했는가」를 프로바이더에 묻지 않고 확인할 수 있습니다. 호출마다 남는 기록은 다음과 같습니다.

기록 내용
대화 노드(「고친 질문에 옛 답이 섞이지 않게 대화를 트리로 저장하기」) 그 호출에서 새로 보낸 항목과 모델이 돌려준 항목, 받은 그대로
호출 로그 실제로 보낸 입력 전체, 받은 응답 원문, 토큰 사용량, 지연 시간

이 기록은 프로바이더의 보존 기간이나 계정 권한과 상관없이 OwlAI의 DB에 남습니다.

재시도도 같은 대화 맥락에서 출발합니다. 호출이 실패해 같은 모델로 다시 보낼 때도, 사용자 개인 키가 인증에 실패해 공용 키로 한 번 바꿔 보낼 때도, 요청에는 대화 맥락 전체가 들어 있습니다. 앞선 응답이 어느 키로 만들어졌는지 따질 필요가 없습니다.

매번 보내면 커지는 것은 요청 본문이고, 입력 토큰 청구는 같다

이 설계의 비용은 요청 본문입니다. 응답 ID 하나 대신 대화 맥락 전체를 매번 보내므로, 대화가 길어질수록 요청이 커집니다. 다만 입력 토큰 청구는 두 방식이 같습니다. OpenAI 문서는 previous_response_id로 이어도 앞선 응답들의 입력 토큰이 모두 입력 토큰으로 청구된다고 밝힙니다. 서버가 앞선 대화를 이어 붙여 모델에 넣기 때문입니다.

입력이 호출마다 쌓이는 비용은 대화를 어디에 두든 같고, 그 비용을 줄이는 방법은 「한 턴의 입력 토큰은 라운드 수의 제곱으로 늘어난다」·「시스템 프롬프트 한 줄에 캐시 읽기가 0이 된다, 바뀌는 블록은 뒤로 보낸다」·「압축 임계값은 컨텍스트 윈도우가 아니라 입력 상한에서 잡는다」에서 다룹니다.

설계에서 고른 것

요청 본문의 크기를 내고 대화가 남는 곳을 한 곳으로 정했습니다. 대화 맥락의 원본은 OwlAI의 DB에만 있고, 어느 프로바이더의 모델이든 같은 경로로 부릅니다.

스트림이 끊겼을 때 결과를 건지는 방법 하나는 포기했습니다. 응답 ID로 결과를 다시 받아 오는 방법은 응답이 서버에 저장되어 있어야 통하는데, OwlAI는 응답을 저장하지 않으므로 건질 것이 없습니다. 그래서 스트림이 완료 이벤트 없이 끝나면 조회를 시도하지 않고 곧바로 스트림 끊김으로 분류합니다. 조회해 봐야 404가 돌아오고, 그 404가 요청이 잘못됐다는 오류로 분류되어 실제 원인인 스트림 끊김을 가리기 때문입니다.

서버 측 압축은 이식성에 비용을 남깁니다. OpenAI·Anthropic이 돌려주는 압축 결과는 암호화된 블록이고, 그것을 만든 프로바이더의 모델만 읽습니다. OpenAI 문서에 따르면 store=False인 호출의 서버 측 압축은 데이터를 남기지 않아서, 저장을 끈 채로 서버 측 압축을 쓸 수 있습니다. 대신 다른 프로바이더로 넘어갈 때는 압축 전 원본을 펼쳐 보내야 하므로, 모델을 바꾼 뒤 첫 요청이 커집니다(「400으로 막힌 대화, 도구 결과는 저장할 때·모양은 보낼 때 맞춘다」).

업무 데이터가 대화에 섞이고, 그 데이터가 어디에 얼마나 남는지 보안 검토에서 설명해야 하는 환경이라면 이 설계가 맞습니다. 대화가 짧고 데이터가 남는 곳을 따질 필요가 없으며 한 프로바이더만 쓴다면, 프로바이더에 대화를 맡기는 쪽이 코드가 단순합니다.

어떤 문제를 해결하나요?

대화 엔진의 다른 글