Runtime API
SigV4 인증 기반 Converse/InvokeModel, 추론 파라미터, 멀티모달, 스트리밍
Amazon Bedrock Runtime API
InvokeModel · 추론 파라미터 · Converse · 함수 호출 · 멀티모달
모든 호출은 Runtime을 지난다
InvokeModel부터 Converse · 함수 호출 · 멀티모달까지
API 호출 방식
InvokeModel vs Converse · 선택 기준 · 코드 패턴
Bedrock Runtime 엔드포인트 개요
bedrock-runtime = SigV4 인증 기반 데이터 플레인 — 요청 하나가 지나는 길
"이 회의록을 3줄로 요약해 줘"
meeting-notes.txt
요청 — 프롬프트 + 문서
→
BEDROCK RUNTIME — 한 번의 호출
① boto3 클라이언트
→
② SigV4 서명
③ Runtime 엔드포인트
→
④ 모델 추론
IAM 정책(bedrock:InvokeModel)이 곧 접근 제어 · Converse / InvokeModel 모두 이 데이터 플레인을 지나 100+ 파운데이션 모델에 닿음
→
1. 3분기 예산 확정
2. 출시 일정 9월 합의
3. QA 담당 지정
usage 입력 1,873 · 출력 96 토큰
응답 — output + usage (예시 수치)
기본은 AWS 자격 증명(SigV4)과 IAM 정책입니다 — API Key(Bearer)도 발급 가능하지만, Mantle과 달리 필수가 아닙니다.
InvokeModel vs Converse
같은 bedrock-runtime, 다른 추상화 수준
InvokeModel API
- 모델별 네이티브 포맷
- 요청 본문 전체 변경 필요
- 이미지 생성 전용 · 비디오는 StartAsyncInvoke
- 모델 고유 기능 완전 접근
- 저수준 제어로 최적화 가능
- 추천: 미디어 생성, 고급 제어
InvokeModel은 각 모델의 네이티브 JSON 포맷을 그대로 전달합니다. 모델을 바꾸면 코드를 재작성해야 하지만, 모든 모델 고유 파라미터에 접근할 수 있습니다.
Converse API
- 통합 시그니처 (모델 무관)
- modelId만 바꾸면 모델 전환
- Tool Use 내장 지원
- 스트리밍: converse_stream()
- 멀티모달 입력 자동 처리
- 추천: 범용 용도, 신규 프로젝트
Converse API는 메시지를 지원하는 모든 모델에 통합된 표준 인터페이스입니다. 모델을 바꿀 때 코드를 수정할 필요가 없고, Tool Use와 스트리밍을 기본으로 지원합니다.
추상화 수준의 차이입니다 — 통합 인터페이스는 Converse, 네이티브 제어는 InvokeModel입니다.
API 선택 가이드
Converse를 먼저 선택, InvokeModel은 필요할 때만
Converse 선택
- 신규 프로젝트 (모델 변경 유연성 필요)
- 프로토타이핑 단계
- Tool Use / 함수 호출 사용
- Multi-turn 대화형 애플리케이션
언제 — 기본 선택지
InvokeModel 선택
- 이미지 생성 (Nova Canvas)
- 비디오 생성 (Nova Reel — StartAsyncInvoke 비동기, EOL 2026-09-30 예정)
- 모델 고유 파라미터 전체 제어 필요
- 성능 최적화 (비용/지연시간)
언제 — 특수한 요구사항
하이브리드
- 메인은 Converse 사용
- InvokeModel은 미디어 생성만 사용
- 코드 복잡도 증가 — 주의
언제 — 두 API 모두 필요한 경우
기본 선택지는 Converse입니다 — InvokeModel은 미디어 생성과 저수준 제어가 필요할 때만 씁니다. Mantle 계열까지 포함한 5가지 API 전체 선택은 API 개요 모듈 기준입니다.
Bedrock Runtime 호출
boto3 클라이언트 초기화 — 하나의 클라이언트로 두 API를 모두 호출
import boto3
# ① Bedrock Runtime 클라이언트 생성 — Invoke · Converse 공용
bedrock_runtime = boto3.client(
'bedrock-runtime',
region_name='us-east-1'
)
# ② 모델별 네이티브 포맷 그대로 — InvokeModel (PART 2)
response = bedrock_runtime.invoke_model(
modelId='us.anthropic.claude-sonnet-4-6',
body=native_json, # 모델마다 다른 JSON
)
# ③ 모든 모델 통합 인터페이스 — Converse (PART 4)
response = bedrock_runtime.converse(
modelId='us.anthropic.claude-sonnet-4-6',
messages=[{'role': 'user', 'content': [{'text': '서울의 인구는?'}]}],
)
- 클라이언트는 하나 — invoke_model()과 converse() 모두 같은 bedrock-runtime
- Invoke — body에 모델별 네이티브 JSON을 그대로 전달
- Converse — messages 통합 시그니처, 모델 무관 동일 코드
InvokeModel API
모델별 네이티브 포맷 · 저수준 호출
InvokeModel — 동기 vs 스트리밍
InvokeModel도 두 가지 — 한 번에 받는 invoke_model · 흘려받는 invoke_model_with_response_stream
케이션
0s · 요청 전송
6s · 전체 응답 한 번에
Runtime
케이션
0s · 같은 요청
0.5s · chunk — 첫 델타 "1."
2s · 델타 연속 수신 — 화면 갱신
6s · 마지막 chunk — stop_reason
Runtime
총 생성 시간은 같고 첫 글자 체감(6s vs 0.5s, 예시)이 다릅니다 — chunk 델타 JSON까지 모델별 포맷이라 모델을 바꾸면 파서도 다시 씁니다.
실물 요청·응답 — 네이티브 포맷
같은 회의록 요약 요청 — 필드 이름부터 응답 구조까지 전부 모델별 포맷
같은 요청, 같은 정보인데 필드 이름과 구조가 전부 모델별입니다 — 이 차이를 흡수하는 것이 Converse입니다.
요청 — 네이티브 JSON (json.dumps 필수)
body = json.dumps({
# Anthropic 전용 필수
'anthropic_version': 'bedrock-2023-05-31',
# 네이티브에선 필수 (Converse는 옵션)
'max_tokens': 512,
'messages': [{'role': 'user',
'content': '이 회의록을 3줄로 요약해 줘'}],
})
response = bedrock_runtime.invoke_model(
modelId='us.anthropic.claude-sonnet-4-6',
body=body)
응답 (예시 수치) — 이것도 모델별 포맷
{"content": [{"type": "text",
"text": "1. 3분기 예산 확정
2. 출시 일정 9월 합의
3. QA 담당 지정"}],
// 스네이크 케이스 — Converse는 stopReason
"stop_reason": "end_turn",
// 이름까지 다름 — Converse는 inputTokens
"usage": {"input_tokens": 1873,
"output_tokens": 96}}
InvokeModel 호출
모델별 JSON 포맷으로 요청 — 응답도 모델별 파싱 필요
import json
# Anthropic Claude - InvokeModel
body = json.dumps({
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1024,
"messages": [
{"role": "user", "content": "Python으로 피보나치 함수 작성"}
]
})
response = bedrock_runtime.invoke_model(
modelId='us.anthropic.claude-sonnet-4-6',
body=body
)
# 응답 파싱 (Anthropic 포맷)
output = json.loads(response['body'].read())
code = output['content'][0]['text']
print(code)
- body는 모델별 네이티브 JSON 문자열 — anthropic_version 같은 모델 고유 필드 포함
- 응답도 모델별 포맷 — response['body'].read()를 직접 파싱
- 모델을 바꾸면 요청 본문과 파싱 코드를 함께 재작성
InvokeModel 스트리밍 호출
같은 body, 메서드만 교체 — chunk를 순회하며 모델별 델타를 파싱
response = bedrock_runtime.invoke_model_with_response_stream(
modelId='us.anthropic.claude-sonnet-4-6',
body=body # 동기 호출과 같은 네이티브 JSON
)
# response['body']는 EventStream — chunk 단위로 순회
for event in response['body']:
chunk = json.loads(event['chunk']['bytes'])
# Anthropic 스트리밍 포맷 — 타입별 분기
if chunk['type'] == 'content_block_delta':
print(chunk['delta']['text'], end='', flush=True)
elif chunk['type'] == 'message_delta':
stop_reason = chunk['delta']['stop_reason']
- 요청 body는 동기 호출과 동일 — 메서드만 _with_response_stream
- chunk['bytes'] 안의 델타 JSON도 모델별 포맷 — 타입 이름부터 Anthropic 전용
- content_block_delta에서 텍스트, message_delta에서 stop_reason
모델별 네이티브 포맷
같은 요청, 같은 512 토큰 상한 — JSON 모양은 네 가지
'max_tokens': 512,
'messages': [{'role': 'user',
'content': '회의록을 요약해 줘'}]}
'content': [{'text': '회의록을 요약해 줘'}]}],
'inferenceConfig': {'maxTokens': 512}}
'max_gen_len': 512}
// messages 배열 없이 단일 문자열
'content': '회의록을 요약해 줘'}],
'max_tokens': 512}
// OpenAI Chat Completions 스키마
같은 InvokeModel이라도 요청·응답 스키마가 프로바이더마다 다릅니다 — 모델을 바꿀 때마다 본문과 파싱 코드를 다시 쓰는 이 번거로움을 하나로 통합한 것이 Converse API입니다.
추론 파라미터
다음 토큰 선택 · Temperature · Top-P · Max Tokens
다음 토큰 선택 — 파라미터의 개입 지점
모델은 확률 분포에서 다음 토큰을 고른다 — Temperature·Top-P가 조절하는 대상
파라미터는 세 지점에 개입합니다 — ① Temperature 분포의 뾰족함(Softmax 전 로짓 나누기) · ② Top-P 후보 컷라인 · ③ Max Tokens · stopSequences 반복을 멈추는 조건. 컷라인 통과 후보 중 추첨(샘플링)은 모델의 몫입니다.
① Temperature — 분포의 뾰족함
Softmax 전에 로짓을 T로 나눕니다 — 낮으면 확정적, 높으면 다양 (0~1, 예시 수치)
같은 로짓, 다른 T — 분포 자체를 바꾸는 첫 개입 · 기본값은 모델별 상이라 명시 지정 권장
② Top-P — 후보 컷라인
확률 내림차순으로 누적 P에 도달할 때까지만 후보로 — 나머지는 제외
분포가 뾰족하면 후보가 줄고 평평하면 늘어나는 가변 컷 — 그래서 Temperature와 둘 중 하나만 조정하는 것이 예측 가능한 기본
③ Max Tokens · Stop Sequences — 멈추는 조건
생성이 끝나는 세 가지 길 — stopReason으로 어느 길이었는지 확인
maxTokens는 생성분만 제한합니다 — 입력 + 생성이 컨텍스트 윈도우를 넘을 수는 없음 · 어느 길로 끝났는지는 stopReason이 알려줌
Converse API 구현
호출 흐름 · 실물 요청과 응답 · 스트리밍 · 특징
Converse — 동기 vs 스트리밍
한 번에 받는 converse · 흘려받는 converse_stream — 짝은 InvokeModel과 동일
케이션
0s · 요청 전송
6s · 전체 응답 한 번에
Runtime
케이션
0s · 같은 요청
0.5s · contentBlockDelta — "1."
2s · 델타 연속 수신 — 화면 갱신
6s · messageStop — stopReason
Runtime
총 생성 시간은 같고 첫 글자 체감(6s vs 0.5s, 예시)이 다릅니다 — 사용자가 화면에서 기다리면 스트리밍, 서버끼리 주고받으면 동기 일괄입니다.
실물 요청·응답
같은 회의록 요약 요청 — Converse는 모델이 바뀌어도 이 구조 그대로
요청도 응답도 모델 무관 동일 구조 — 모델 전환은 modelId 교체뿐입니다.
요청 — 통합 스키마 (모델 무관 동일)
response = bedrock_runtime.converse(
modelId='us.anthropic.claude-sonnet-4-6',
messages=[{'role': 'user', 'content': [
{'text': '이 회의록을 3줄로 요약해 줘'},
# 문서 첨부도 content 블록 하나
{'document': {'format': 'txt',
'name': 'meeting-notes',
'source': {'bytes': meeting_notes}}},
]}],
inferenceConfig={'temperature': 0.7,
'maxTokens': 512},
)
응답 (예시 수치) — 이 구조도 모델 무관
{"output": {"message": {"role": "assistant",
"content": [{"text": "1. 3분기 예산 확정
2. 출시 일정 9월 합의
3. QA 담당 지정"}]}},
// end_turn 정상 · tool_use면 함수 호출 루프
"stopReason": "end_turn",
// 과금·모니터링 기준
"usage": {"inputTokens": 1873,
"outputTokens": 96,
"totalTokens": 1969}}
Converse API 기본 호출
동기 요청으로 전체 응답을 한 번에 수신
response = bedrock_runtime.converse(
modelId='us.anthropic.claude-sonnet-4-6',
messages=[
{
'role': 'user',
'content': [{'text': '다음 리스트를 JSON으로 변환해주세요:\n• Apple\n• Banana'}]
}
],
inferenceConfig={
'temperature': 0.7,
'maxTokens': 512,
}
)
output = response['output']['message']['content'][0]['text']
print(output)
# {"fruits": ["Apple", "Banana"]}
- messages 배열 — role + content 블록 구조는 모델과 무관하게 동일
- inferenceConfig — temperature·maxTokens를 표준 필드로 지정
- 응답 경로 고정 — output.message.content[0].text, 모델 전환 시에도 그대로
Converse 스트리밍 호출
메서드만 converse_stream으로 — 이벤트 스키마도 모델 무관 동일
response = bedrock_runtime.converse_stream(
modelId='us.anthropic.claude-sonnet-4-6',
messages=[{'role': 'user',
'content': [{'text': '이 회의록을 3줄로 요약해 줘'}]}],
)
# 이벤트 루프 — 통합 스키마라 모델이 바뀌어도 이 코드 그대로
for event in response['stream']:
if 'contentBlockDelta' in event:
print(event['contentBlockDelta']['delta']['text'],
end='', flush=True)
elif 'messageStop' in event:
stop_reason = event['messageStop']['stopReason']
- 요청은 동기 호출과 동일 — 메서드만 converse_stream
- contentBlockDelta — 텍스트 델타 · messageStop — stopReason
- usage는 마지막 metadata 이벤트에 — Invoke와 달리 이벤트 이름이 모델 무관
Converse 특징
메시지 지원 전 모델에 동일한 인터페이스
모델 교체 자유
modelId만 바꾸면 다른 모델로 즉시 전환
Tool Use 내장
toolConfig 필드로 함수 정의 후 자동 호출 관리
스트리밍 지원
converse_stream()으로 토큰 단위 즉시 수신
멀티모달 자동화
이미지/비디오/문서를 자동으로 처리
네 가지 특징의 공통분모는 모델 독립성입니다 — 인터페이스 하나로 모델·모달리티·도구를 통일합니다.
함수 호출
Function Calling · Tool Use 루프 · ReAct
함수 호출 vs Function Calling
차이는 누가 호출을 결정하는가 — 코드가 아닌 모델의 판단
전통적 함수 호출
개발자가 코드로 결정
- 호출 시점·함수·인자를 코드에 미리 작성
- if/else 분기 — 결정론적, 예외 없음
- 자연어 입력을 해석할 수 없음
- 새 분기 추가 = 코드 수정 · 재배포
Function Calling
모델이 런타임에 판단
- 호출 여부·함수·인자를 모델이 상황을 보고 결정
- 자연어 → 구조화된 인자(JSON) 변환
- "서울 날씨 어때?" → get_weather(city=서울)
- 새 기능 추가 = 코드가 아니라 도구 정의 추가
모델은 판단만, 실행은 앱이 — 함수를 부를지 말지를 모델이 정하고, 실제 실행 권한은 여전히 코드에 있습니다.
Function Calling 개념
모델은 호출 의도만 반환 — 함수 실행은 모델이 아닌 앱의 몫
이 왕복 한 번이 Function Calling입니다 — 왕복을 반복하며 추론과 행동을 잇는 패턴이 ReAct입니다.
Function Calling 구현
LLM이 외부 함수 호출 의도를 JSON으로 반환 — Tool Use
# ① Tool 정의 — toolConfig.tools[].toolSpec 구조
tool_config = {
"tools": [{
"toolSpec": {
"name": "get_weather",
"description": "도시의 현재 날씨 조회", # 모델의 선택 근거
"inputSchema": {"json": {"type": "object",
"properties": {"city": {"type": "string"}}, "required": ["city"]}},
}
}]
}
# ② Converse 호출 — toolConfig로 도구 목록 전달
response = bedrock_runtime.converse(
modelId='us.anthropic.claude-sonnet-4-6',
messages=[{'role': 'user', 'content': [{'text': '서울의 날씨는?'}]}],
toolConfig=tool_config,
)
# ③ 응답 분석 — 모델은 호출 "의도"(name·input)만 JSON으로 반환
for content in response['output']['message']['content']:
if content.get('toolUse'):
print(content['toolUse']['name'], content['toolUse']['input'])
- toolSpec — name · description · inputSchema(JSON Schema)로 도구 선언, description이 모델의 선택 근거
- toolConfig — Converse 호출에 도구 목록을 실어 전달
- toolUse — 모델은 호출 의도(name·input)만 반환, 실제 실행은 개발자 몫 — 다음 슬라이드의 ReAct 루프로 이어짐
ReAct 루프
Reason → Act → Observe → 반복 — 에이전트의 핵심 메커니즘
루프의 종료 조건은 stopReason입니다 — tool_use면 반복, end_turn이면 종료로 에이전트가 굴러갑니다.
ReAct 루프 코드
ReAct 루프를 코드로 — while + stopReason + toolResult
messages = [{'role': 'user', 'content': [{'text': '서울의 날씨는?'}]}]
while True:
# ① Reason — 모델이 현재 messages를 보고 다음 행동 결정
response = bedrock_runtime.converse(
modelId='us.anthropic.claude-sonnet-4-6',
messages=messages,
toolConfig=tool_config)
messages.append(response['output']['message'])
# ② 종료 조건 — end_turn이면 최종 응답
if response['stopReason'] != 'tool_use':
break
# ③ Act — 모델이 반환한 도구를 개발자가 실제 실행
for content in response['output']['message']['content']:
if content.get('toolUse'):
tu = content['toolUse']
result = run_tool(tu['name'], tu['input'])
# ④ Observe — toolResult를 messages에 추가해 재전달
messages.append({'role': 'user', 'content': [{
'toolResult': {'toolUseId': tu['toolUseId'],
'content': [{'json': result}]}}]})
- while + stopReason — tool_use면 반복, end_turn이면 break로 종료
- Reason → Act — 모델은 의도만 반환, run_tool() 실행은 개발자 코드
- toolResult (Observe) — toolUseId로 호출과 결과를 짝지어 messages에 재전달
멀티모달
이미지·문서 입력 · Canvas 이미지 생성
멀티모달 워크플로우
분석과 생성은 다른 API — 방향이 갈리는 두 트랙
입력 · 분석
Converse
- 이미지·비디오·문서를 content 블록으로 입력
- 모델이 내용을 이해하고 설명·지시 생성
- 텍스트와 같은 층위 — 기존 대화 코드 그대로
생성
InvokeModel · 비동기 API
- 이미지 — InvokeModel(Nova Canvas)
- 비디오 — StartAsyncInvoke/GetAsyncInvoke (Nova Reel, EOL 2026-09-30)
- 결과는 base64 — 디코딩 후 저장 또는 S3 업로드
분석 입력은 Converse, 이미지 생성은 InvokeModel(Canvas), 비디오 생성은 비동기 API(Reel) — API가 역할을 나눕니다.
멀티모달 입력 — 이미지 분석
이미지도 content 블록 하나 — text와 나란히 넣으면 끝
with open('sales-chart.png', 'rb') as f:
image_bytes = f.read()
response = bedrock_runtime.converse(
modelId='us.anthropic.claude-sonnet-4-6',
messages=[{'role': 'user', 'content': [
{'text': '이 차트의 추세를 두 줄로 설명해줘'},
{'image': {'format': 'png',
'source': {'bytes': image_bytes}}},
]}],
)
print(response['output']['message']['content'][0]['text'])
- content 배열에 image 블록 추가 — 텍스트와 같은 층위
- format — png · jpeg · gif · webp
- 문서는 document 블록(PDF·txt·docx 등), 비디오는 video 블록 — 같은 패턴
Nova Canvas 생성 코드
이미지 생성은 InvokeModel — Canvas 네이티브 포맷으로 요청
body = json.dumps({
'taskType': 'TEXT_IMAGE',
'textToImageParams': {'text': '노을 지는 서울 스카이라인, 수채화'},
'imageGenerationConfig': {
'numberOfImages': 1, 'width': 1024, 'height': 1024,
},
})
response = bedrock_runtime.invoke_model(
modelId='amazon.nova-canvas-v1:0', body=body)
output = json.loads(response['body'].read())
image_b64 = output['images'][0] # base64 — 디코드 후 저장/S3 업로드
- 생성 모델은 Converse 미지원 — InvokeModel 네이티브 포맷 전용
- taskType — TEXT_IMAGE 외 인페인팅·배경 교체 등 과업별 상이
- 프롬프트 구체성이 품질을 가름 — 피사체·스타일·조명까지 명시
- 응답은 base64 이미지 배열 — 비디오(Nova Reel)는 StartAsyncInvoke 비동기
실습 — InvokeModel · Converse API
두 Runtime API를 직접 호출합니다 — 네이티브 포맷부터 Tool Use 챗봇까지
학습 목표
- InvokeModel — 네이티브 포맷 호출과 스트리밍 응답 처리
- Nova Canvas 이미지 · Nova Reel 비디오 생성
- Converse — 멀티턴 대화 · 스트리밍 · Tool Use
- Streamlit 챗봇 UI로 두 API 통합 마무리
InvokeModel · 추론 파라미터 · Converse · 함수 호출 · 멀티모달 — 운영 패턴은 추론 인프라 모듈에서