연결 라이프사이클
연결 라이프사이클
realtime 모델은 하나의 영구 프로바이더 연결을 사용해요. 백엔드가 그 세션과 사용자까지의 미디어 브리지를 소유하고(프론트엔드 연결 참고), 재연결 정책은 애플리케이션 이벤트 루프를 바꾸지 않고 끊어진 연결과 프로바이더 세션 한도를 회복할 수 있어요.
출처: 문서
본문
세션 라이프사이클
stateDiagram-v2
[*] --> Connecting: session() opens
Connecting --> Listening: handshake complete
Listening --> UserTurn: speech detected /<br>audio committed
UserTurn --> ModelResponse: turn detection /<br>create_response()
ModelResponse --> ToolCalls: model calls a tool
ToolCalls --> ModelResponse: result returned
ModelResponse --> Listening: turn complete
Listening --> Reconnecting: connection drops
ModelResponse --> Reconnecting: connection drops
Reconnecting --> Listening: redial succeeds
Reconnecting --> [*]: attempts exhausted
Listening --> [*]: close()
세션을 여는 것은 프로바이더 핸드셰이크를 수행하고, 그 후 세션은 입력을 듣기 시작해요. 턴 감지(또는 수동 푸시-투-토크 제어)는 사용자 턴을 모델 응답으로 옮기고, RealtimeTurnCompleteEvent가 턴 경계를 표시하고 세션이 다시 듣기 전에 툴 호출을 순환할 수 있어요. 끊어진 연결은 아래 재연결 루프로 들어가고(회복 시 RealtimeSessionReconnectEvent 방출), close()(또는 async with 블록을 떠나) 세션이 끝날 때까지, 끊는 툴에서 포함해 계속돼요.
연결과 핸드셰이크
연결은 session() 컨텍스트에 들어갈 때 열리고, 공유 handshake_timeout 설정(기본 30초)은 명시적 핸드셰이크가 있는 프로바이더(OpenAI, Azure OpenAI, xAI)에서 세션이 각 realtime 프로토콜 핸드셰이크 이벤트를 기다리는 시간을 바운드해요. 타임아웃된 핸드셰이크는 RealtimeError를 발생시키고, 거부된 WebSocket 업그레이드는 ModelHTTPError를 발생시켜요(오류 참고).
재연결
reconnect 공유 설정을 ReconnectPolicy로 설정하면 지수 백오프로 재접속하고, 구성을 다시 적용하며, RealtimeSessionReconnectEvent를 방출해요. 다른 realtime 모델 설정처럼 모델의 기본이거나 한 세션에 전달할 수 있어요:
from pydantic_ai import Agent
agent = Agent()
realtime = agent.realtime(
'openai:gpt-realtime',
model_settings={'reconnect': {'max_attempts': 5}},
)
max_attempts는 한 번의 드롭에 대한 재시도를 바운드해요. max_reconnects는 전체 세션에 걸친 회복을 바운드해서, 연결을 반복적으로 받고 닫는 엔드포인트가 영원히 재접속하지 못하게 해요.
정책이 없으면 예상치 못한 프로바이더 종료가 세션 이터레이터에서 RealtimeError를 발생시켜요.
WebRTC 사이드밴드에서는 같은 정책이 예상치 못한 드롭에 적용되지만, 깨끗한 종료는 브라우저가 끊는 것으로 취급돼요. 사이드밴드는 제어 채널이므로, 정상 종료는 reconnect 정책이 설정되어 있어도 세션 오류나 재연결 시도 없이 이터레이션을 끝내요. 종료 프레임만으로는 끊는 것과 WebSocket을 종료하는 프록시가 호출 중간에 사이드밴드를 깨끗하게 닫는 것(재시작이나 정상 로테이션)을 구별할 수 없어요. 후자는 브라우저가 프로바이더와 계속 말하는 동안 에이전트 측을 끝내요. 그것들을 reconnect 정책이 덮도록 의존하지 말고 인프라 레이어에서 그런 연결을 드레인하세요.
상태 복원
OpenAI와 Azure OpenAI는 크로스 연결 서버 상태가 없어서 Pydantic AI가 로컬 메시지 이력을 새 세션으로 재생해요. 이전 전사 턴은 살아남고, 진행 중인 오디오는 살아남지 못해요.
Gemini와 xAI는 네이티브 인프로세스 세션 재개를 사용하며, reconnect 정책이 있으면 자동으로 활성화돼요(정책과 함께 명시적 google_enable_session_resumption=False는 대화를 조용히 잃는 대신 UserError를 발생시켜요). Gemini 재개 설정 참고. 그들의 핸들은 메모리에만 있고 다른 프로세스를 위해 영속할 수 없어요.
RealtimeSessionReconnectEvent.state_restored는 재연결이 턴을 끊지 않고 대화를 이어갔는지 보고해요.
드롭이 진행 중에 잡은 응답이 어떻게 처리되는지는 메커니즘에 달려 있어요. 네이티브 재개(xAI)에서는 기록된 응답이 단순히 열린 채 유지돼요. 새 연결의 출력이 그것을 계속하고, 턴은 평소처럼 응답 종결부로 완료되며, state_restored는 True로 남아요. Gemini는 서버가 재개 핸들을 발급하면(연결 직후) True를 보고하지만(그 전 드롭은 False를 보고하고 실행 중 툴을 취소), 잘린 응답을 인터럽트된 응답으로 닫고(부분 전사를 이력에 유지) RealtimeSessionReconnectEvent 후 다음 입력까지 조용해요.
로컬 재생(OpenAI, Azure OpenAI)은 완성된 턴만 복원해, 소켓이 떨어졌을 때 진행 중인 응답은 계속할 수 없어요. 세션은 이벤트를 방출하기 전에 그것을 정리해요. 부분 응답이 중단된 응답이 되고, 실행 중 툴 호출은 취소 반환을 얻으며, 턴은 경계를 기다리는 큐에 넣은 메시지가 여전히 플러시되도록 끝나요. 그리고 state_restored는 턴이 끊겼다고 말하기 위해 False예요. 요청됐지만 스트리밍을 시작하지 않은 답은 대신 새 연결에서 다시 요청되고, 진행 중인 것이 없는 드롭은 호출 전체를 그대로 복원해요. 둘 다 출력을 잃지 않으므로 state_restored는 True로 남아요.
프로바이더 세션 한도
프로바이더는 개별 연결 기간을 상한을 두어요. 재연결 정책은 애플리케이션이 그 한도를 살아남는 방법이기도 해요. 정확한 한도와 프로바이더 동작은 바뀔 수 있으므로 프로바이더 페이지가 표준이에요:
Gemini는 한도 직전에 GoAway를 보내지만 Pydantic AI는 현재 연결이 끊긴 후에만 재연결하므로, 긴 호출은 턴 중간에 잠깐 끊길 수 있어요.
호출 끝내기
async with 블록을 떠나면 세션이 닫혀요. 다른 곳에서(워치독, 정지 버튼, 또는 툴) 끊으려면 어떤 태스크에서든 close()를 await 하세요. 태스크가 기다리는 동안 취소되어도 teardown은 완료까지 실행되고, 동시 close()와 async with 종료가 같은 teardown을 기다리므로 블록을 떠날 때 세션은 완전히 닫혀 있어요. 세션이 반복되는 동안 루프는 끝나고, async with 블록을 떠나는 것은 예외를 발생시키지 않으며, session.result는 확정돼요.
유휴 타임아웃이나 최대 호출 시간 같은 외부 정책을 위해 close()를 호출하는 워치독 태스크를 실행하세요:
import asyncio
from pydantic_ai import Agent
from pydantic_ai.realtime import RealtimeSession
agent = Agent(instructions='You are a helpful voice assistant.')
async def close_after(session: RealtimeSession, seconds: float) -> None:
await asyncio.sleep(seconds)
await session.close()
async def main():
async with agent.realtime('openai:gpt-realtime').session() as session:
watchdog = asyncio.create_task(close_after(session, 300))
try:
async for event in session:
... # handle events as usual; the loop ends when the watchdog closes the session
finally:
watchdog.cancel()
절대 타임아웃이 아니라 유휴 타임아웃에서는 RealtimeInputSpeechStartEvent와 RealtimeInputSpeechEndEvent에서 워치독을 리셋하세요. 프로파일이 emits_input_speech_events를 선언하는 프로바이더가 보내는 이벤트예요. Gemini처럼 그 플래그가 설정되지 않은 곳에서는 자체 입력 경로에서도(침묵이 아닌 오디오를 보낼 때마다) 워치독을 리셋하고, 어시스턴트 PartDeltaEvent 활동과 RealtimeTurnCompleteEvent에서도 리셋하세요. 출력에서만 리셋하면 사람이 문장 중간에 끊겨요. 입력 전사가 켜져 있으면 stream_transcripts()의 사용자 파트도 작동해요.
오류
Realtime 세션은 표준 Pydantic AI 예외 계층을 사용해요:
| 예외 | 발생 시점 |
|---|---|
UserError |
애플리케이션이 지원되지 않는 작업을 요청하거나, 호환되지 않는 설정을 전달하거나, 자격증명이 없거나, 세션을 잘못 사용 |
ModelHTTPError |
프로바이더가 HTTP 상태로 WebSocket 업그레이드를 거부; Gemini는 1007·1008 같은 WebSocket 종료 코드도 status_code로 매핑하는 반면, OpenAI 프로토콜 프로바이더는 핸드셰이크 내 거부에 RealtimeError를 사용 |
RealtimeError |
연결 실패, 타임아웃, 예상치 못한 종료, 잘못된 프레임, 재연결 시도 소진 |
UsageLimitExceeded |
구성된 사용량 한도 초과 |
RealtimeError는 ModelAPIError를 상속하므로 except ModelAPIError는 HTTP와 비 HTTP 프로바이더 실패를 함께 다뤄요.
복구 가능한 실패는 이벤트로 도착해요. 프로바이더 작업에는 RealtimeSessionErrorEvent, 실패한 사용자 전사 하나에는 RealtimeInputTranscriptionErrorEvent. 두 이벤트 후에도 세션은 사용 가능해요.
실패는 가능하면 책임 있는 호출에서 드러나요. 실패한 send_audio()는 거기서 발생해요. 수신 루프와 툴 실패는 세션이 어떻게 소비되는지에 따라 표면화돼요:
- 이벤트 스트림이 반복되는 동안 실패는
async for에서 발생해요. - 이벤트 스트림이 전혀 반복되지 않았을 때(오디오나 전사 뷰만 소비) 뷰가 끝나고
async with블록이 종료될 때(close()에서) 실패가 발생해요. - 반복을 시작했다가 멈춘 소비자는 듣기를 멈추기로 선택한 것이므로, 이후 실패는 그것을 대신해 발생하지 않아요.
증상 우선 디버깅은 문제 해결을 참고하세요.