Home / Strands 프로덕션 / 스트리밍과 재시도
Note

스트리밍과 재시도

Async Iterator, Callback Handler, SSE, ModelRetryStrategy

⏱ 30분 100 / 189

스트리밍과 재시도

Async Iterator, Callback Handler, SSE, ModelRetryStrategy

에이전트를 프로덕션에 올린다

프롬프트 캐싱·스트리밍·배포 타겟 — 비용과 속도 최적화

프롬프트 캐싱

SystemContentBlock · cache_tools · 비용 90% 절감

PART 2 · 프롬프트 캐싱

프롬프트 캐싱

매 호출 반복되는 prefix를 캐시해 입력 토큰 비용을 절감하는 메커니즘

프롬프트 캐싱

시스템 프롬프트·도구 정의처럼 매 호출 동일한 prefix를 캐시하여 입력 토큰 과금을 최소화합니다.

  • cache_tools="default" — 도구 정의 전체 캐시 (5분 TTL)
  • cache_config=CacheConfig(strategy="auto") — 멀티턴 메시지 자동 캐싱
  • 시스템 프롬프트 명시 캐싱 — SystemContentBlock의 cachePoint 마커
  • cache hit — 캐시 읽기 비용은 입력 토큰의 10%
takeaway

첫 호출은 cache write 비용이 추가되지만, 이후 hit부터는 prefix 재처리 없이 절감된 비용으로 처리됩니다.

PART 2 · 프롬프트 캐싱

cache_tools로 도구 정의 캐싱

도구 정의는 한 줄 옵션 — 멀티턴 자동(CacheConfig)·시스템 프롬프트 명시(cachePoint)까지 대상별 선언

python
from strands import Agent
from strands.models import BedrockModel, CacheConfig
from strands.types.content import SystemContentBlock

model = BedrockModel(
    model_id="us.anthropic.claude-sonnet-4-6",
    cache_tools="default",  # 도구 정의 전체를 캐시 (5분 TTL)
    # 멀티턴 메시지 자동 캐싱 — 마지막 user 메시지 끝에 캐시 포인트 자동 배치
    cache_config=CacheConfig(strategy="auto"),
)

agent = Agent(
    model=model,
    # 시스템 프롬프트 명시 캐싱 — cachePoint 마커까지가 캐시 prefix
    system_prompt=[
        SystemContentBlock(text="""You are a production data analyst.
    You have access to SQL databases, S3 data lakes, and monitoring dashboards.
    Always validate queries before execution and explain your reasoning."""),
        SystemContentBlock(cachePoint={"type": "default"}),
    ],
    tools=[query_database, read_s3, check_metrics, generate_report],
)

# 첫 호출: cache write (약간의 추가 비용)
# 이후 호출: cache hit → 시스템 프롬프트 + 도구 정의 비용 90% 절감
result = agent("지난주 매출 리포트를 생성해줘")
PART 2 · 프롬프트 캐싱

캐싱 동작 원리

Cache Point 마커 기준으로 prefix 일치 여부를 판단하는 hit/miss 흐름

  1. 1
    Cache Point 선언

    SystemContentBlock에 cachePoint: { type: "default" }를 삽입. 이 마커까지의 토큰이 캐시 키의 prefix가 됨.

  2. 2
    Prefix 해싱

    프로바이더가 시스템 프롬프트 + 도구 정의를 해싱하여 캐시 키 생성. 동일 prefix면 같은 키로 매핑.

  3. 3
    Cache Hit 판정

    TTL 내에 동일 키가 존재하면 hit — 캐시된 KV 상태를 재사용하여 prefix 토큰 재처리 생략.

  4. 4
    비용 정산

    Hit 시 캐시 읽기 비용만 과금 (일반 입력 토큰의 10%). Miss 시 cache write 비용 25% 추가 후 다음 호출부터 절감.

takeaway

hit이면 캐시 읽기 비용 10%만 과금되고, miss면 write 비용 25%가 추가됩니다 — TTL 안에 같은 prefix로 다시 호출하는 것이 절감의 조건입니다.

PART 2 · 프롬프트 캐싱

캐싱 조건 비교

Anthropic API 직접 호출과 Bedrock 경유 시 캐싱 동작 차이

조건Anthropic APIAmazon Bedrock
최소 토큰1,024 이상 (Sonnet 4.6 기준)모델별 최소 토큰 존재 (1,024/4,096 — Bedrock 문서 기준)
TTL5분 (사용 시 갱신)5분~1시간 (모델별 상이)
cache_tools="default"✓ 지원 (5분 TTL)✓ 지원
cache_config (멀티턴 메시지 자동)✓ CacheConfig(strategy="auto")✓ 동일 인터페이스
시스템 프롬프트 (수동 cachePoint)✓ SystemContentBlock✓ 동일 인터페이스
Cache Write 비용입력 토큰의 25% 추가 (5분 TTL 기준 — 1시간 TTL은 +100%)입력 토큰의 25% 추가 (5분 TTL 기준)
Cache Read 비용입력 토큰의 10%입력 토큰의 10%
교차 세션 공유동일 workspace 내 공유 (2026-02 변경)동일 계정 내 공유
takeaway

Strands 캐싱 인터페이스는 Anthropic API와 Bedrock에서 동일합니다 — 최소 토큰과 TTL 같은 캐싱 조건만 프로바이더별로 다릅니다.

스트리밍

AsyncIterator · Callback · SSE · Retry

PART 3 · 스트리밍

스트리밍

토큰 단위 수신 인터페이스부터 브라우저 전달까지의 실시간 경로

스트리밍

전체 응답 완료를 기다리지 않고 생성 즉시 토큰을 전달하여 체감 지연을 제거합니다.

  • Callback Handler — callable 하나로 이벤트 수신
  • stream_async — async for 루프의 AsyncIterator
  • SSE 파이프라인 — Agent → Callback → Transport → 브라우저
  • TTFT 최소화 — 첫 토큰 즉시 표시
takeaway

AsyncIterator와 Callback 이중 인터페이스로 토큰을 받고, SSE 파이프라인이 브라우저까지 실시간 전달합니다.

PART 3 · 스트리밍

AsyncIterator + Callback 스트리밍

토큰 단위 수신을 위한 두 가지 인터페이스 — 이터레이터와 이벤트 콜백

python
import asyncio
from strands import Agent

queue = asyncio.Queue()

# 방법 1: Callback Handler — callable 하나로 이벤트 수신
def sse_handler(**kwargs):
    """토큰을 SSE 형식으로 클라이언트에 푸시"""
    if "data" in kwargs:  # 텍스트 델타
        queue.put_nowait(f"data: {kwargs['data']}\n\n")
    tool_use = kwargs.get("current_tool_use")
    if tool_use:          # 도구 호출 이벤트
        queue.put_nowait(f"event: tool\ndata: {tool_use['name']}\n\n")

agent = Agent(callback_handler=sse_handler)

# 방법 2: AsyncIterator — async for 루프 (이벤트는 dict)
async def stream_response(agent: Agent, prompt: str):
    async for event in agent.stream_async(prompt):
        if "data" in event:
            yield f"data: {event['data']}\n\n"
        elif event.get("current_tool_use"):
            name = event["current_tool_use"]["name"]
            yield f"event: tool\ndata: {name}\n\n"
    yield "data: [DONE]\n\n"
PART 3 · 스트리밍

Polling vs Streaming 비교

응답 수신 방식에 따른 지연·리소스·UX 차이

Polling (동기 대기)

전체 응답 완료 후 반환

  • TTFT = 전체 생성 시간 (수 초~수십 초)
  • 클라이언트가 빈 화면에서 대기
  • 구현 단순 — request/response 한 쌍
  • 네트워크 타임아웃 위험 (30초+ 응답)

Streaming (토큰 단위)

생성 즉시 토큰 전달

  • TTFT 최소화 — 첫 토큰 즉시 표시
  • 타이핑 효과로 체감 지연 제거
  • SSE/WebSocket 연결 관리 필요
  • 긴 응답도 타임아웃 없이 안정 수신
takeaway

스트리밍은 SSE/WebSocket 연결 관리 비용을 치르는 대신 첫 토큰 지연과 타임아웃 위험을 동시에 제거합니다.

PART 3 · 스트리밍

SSE 파이프라인 구조

Agent에서 브라우저까지 토큰이 전달되는 4단계 흐름

  1. 1
    Agent 추론

    Strands Agent가 모델 API를 스트리밍 모드로 호출. 모델이 토큰을 생성할 때마다 chunk 이벤트 발생.

  2. 2
    Token Callback

    callback_handler로 전달한 함수가 "data" 키의 텍스트 델타를 수신. SSE 형식(data: ...)으로 변환하여 응답 큐에 적재.

  3. 3
    SSE Transport

    FastAPI StreamingResponse / Lambda Function URL이 Content-Type: text/event-stream으로 청크 전송. Keep-alive 유지.

  4. 4
    Client Render

    EventSource API가 토큰을 수신할 때마다 DOM에 append. 타이핑 효과로 실시간 표시. 연결 끊김 시 자동 재연결.

takeaway

네 단계 중 개발자가 작성하는 것은 callback 함수와 SSE 엔드포인트뿐입니다 — 나머지는 모델과 브라우저가 처리합니다.

PART 3 · 스트리밍

SSE 장면 — 토큰이 흐르는 화면

같은 순간의 양쪽 모습 — 와이어의 델타 이벤트와 브라우저의 부분 출력






와이어 — GET /chat/stream


HTTP/1.1 200 OK

Content-Type: text/event-stream · keep-alive

event: tool

data: get_weather

data: 서울은

data:  지금 맑고

data:  기온은 24

data: [DONE]  ← 생성이 끝나면 마지막 이벤트






app.example.com/chat


서울 날씨 어때?

🔧 도구 호출 — get_weather

서울은 지금 맑고 기온은 24


EventSource가 델타를 받을 때마다 DOM에 append — 전체 응답을 기다리지 않고 첫 토큰부터 즉시 표시됩니다 (TTFT 최소화).



좋은 에이전트는 빠르고 저렴해야 합니다.

캐싱 · 스트리밍 · 배포 — 프로덕션 에이전트의 3대 요소