티스토리 뷰
서론
batch 모드는 상용 LLM 서비스에서 일반적으로 제공하는 여러 개의 요청을 묶어 보내면 천천히 처리해서 결과를 돌려주는 대신 비용을 반으로 줄여주는 서비스입니다. 보통 LLM을 활용한 서비스를 만들때 성능평가를 하거나, LLM을 활용한 데이터셋을 확보하기 위해 사용합니다.
Gemini 모델이 성능이 상당히 좋아졌습니다. 이제는 대부분의 상용 LLM 성능을 압도하고 있고, 또 같은 성능의 GPT 모델보다 가격도 훨씬 저렴합니다. 이러한 이점으로 Gemini를 써보고 싶은데 구글의 문서화는 항상 어지럽습니다..... batch의 경우에는 코드로 정리해둔 글도 없길래, 직접 열댓번 날려보면서 어떻게 작성해야하는지 정리했습니다.
gemini batch 모드 공식 문서
이 글에서는 jsonl 파일로 system prompt (페르소나 지정 프롬프트)와 json schame 등의 다양한 설정값을 지정해서 batch 모드 요청을 python 코드로 보내는 방법을 서술하고 있습니다.
최하단에는 모듈화가 야악간 부족한 batch 작업 코드가 있으니 참고해주세요.
[목차]
- quick start: batch 모드를 활용하는 전체 흐름 설명
- task 작업 만들기: "batch task"에 대한 설명
- source의 형식 2가지
- GenerateContentRequest 객체
- user prompt, system prompt, temperature, 출력 형식, thinking budget
quick start
전체적인 흐름은 다음과 같습니다.
- 먼저 gemini api key 발급 홈페이지에서 키 발급을 받고 요금 설정을 한 후, (관련해서는 다른 블로그를 참고해보세요)
- 관련 packeage를 설치하고 ->
pip install google-genai
- batch task jsonl 파일을 만든 후 (예시 코드 중 make_batch_tasks_file 참고)
- file api로 업로드하고, 해당 업로드 정보를 src에 지정해 batch 작업을 실행합니다. (예시 코드 중 create_batch 참고)
- batch 작업이 끝났는지 확인하다가 (예시 코드 중 check_batch_status 참고)
- 끝나면 결과 파일을 다운받습니다. (예시 코드 중 save_batch_result 참고)
나는 코드를 보고 이해하는 게 빠르다, 하시는 분은 밑 설명 다 패스하고 예시 코드만 보셔도 됩니다. 예시 코드가 좀 길어서 제일 하단에 두었습니다. 참고해주세요.
quick start 부분은 해당 문서를 가져다 썼습니다.
기본 import 및 설정
from google import genai
from google.genai import types
client = genai.Client(api_key="{여기에 api key 입력}")
batch task jsonl 파일 생성
batch 작업 리스트인, batch task file을 jsonl 형식으로 생성합니다.
해당 리스트를 보고 llm이 답변을 생성합니다.
jsonl 파일과 각 task의 형식에 대해서는 입력 - task 만들기 파트에서 설명합니다.
with open("my-batch-requests.jsonl", "w") as f:
requests = [
{"key": "request-1", "request": {"contents": [{"parts": [{"text": "Describe the process of photosynthesis."}]}]}},
{"key": "request-2", "request": {"contents": [{"parts": [{"text": "What are the main ingredients in a Margherita pizza?"}]}]}}
]
for req in requests:
f.write(json.dumps(req) + "\n")
file api로 업로드
batch task file을 쓰기 위해서 어디에 올리는 코드인 것 같습니다.
uploaded_file = client.files.upload(
file='my-batch-requests.jsonl',
config=types.UploadFileConfig(display_name='my-batch-requests', mime_type='jsonl')
)
batch 실행 - create batch
위에서 업로드한 파일을 source로 지정하고, 모델(ex: gemini-2.5-flash, gemini-2.0-flash-lite 등)은 원하는 걸로 작성한 후 batch 작업을 실행합니다.
실행 후 받아오는 file_batch_job은 실행시킨 batch에 대한 정보를 담아둔 객체입니다. 이 정보가 없어지면 해당 배치 작업의 진행상황이나 결과를 알 수 없게 됩니다.
file_batch_job = client.batches.create(
model="gemini-2.5-flash",
src=uploaded_file.name,
config={
'display_name': "file-upload-job-1",
},
)
print(f"Created batch job: {file_batch_job.name}")
batch 진행상황 확인
batch는 결과를 (돌리는 작업의 개수에 따라 편차가 크기만,) 10-20분 정도 늦게 줍니다.
batch가 끝났는지, 안끝났는지, 오류가 났는지 확인하려면 아래 코드를 돌려보면 됩니다.
진행 중이면 JOB_STATE_PENDING
으로, 성공적으로 끝나면 JOB_STATE_SUCCEEDED
가 출력됩니다.
create batch 이후 가지고 있는 batch에 대한 정보 file_batch_job
의 name 속성을 clinet.batches.get
에 파라미터로 넘겨 실행시키면, 현재 batch의 정보를 알려줍니다.
job_name = file_batch_job.name
batch_job = client.batches.get(name=job_name)
print(f"Polling status for job: {job_name}")
print(f"Current state: {batch_job.state.name}")
if batch_job.state.name == 'JOB_STATE_FAILED':
print(f"Error: {batch_job.error}")
batch 결과 확인
진행상황 확인 중 JOB_STATE_SUCCEEDED
가 출력됐다면, 결과를 받아올 준비가 됐다는 뜻입니다.
다음 코드를 실행시키면 결과 파일인 batch_job_results.jsonl
을 받아올 수 있습니다.
job_name = file_batch_job.name
batch_job = client.batches.get(name=job_name)
if batch_job.state.name == 'JOB_STATE_SUCCEEDED':
# If batch job was created with a file
if batch_job.dest and batch_job.dest.file_name:
# Results are in a file
print("Downloading result file content...")
file_content = client.files.download(file=batch_job.dest.file_name)
with open(f'batch_job_results.jsonl', 'wb') as file:
file.write(file_content)
입력 - task 만들기
LLM이 처리할 일(task)를 어떤 형식으로 구성해야 하는지 구체적으로 설명합니다.
어떻게 요청을 보내야 하는가? - source의 형식
현재 대부분의 batch 모드의 경우, 대부분 요청을 여러개 담은 jsonl이라는 파일을 보내는 형식을 취합니다.
jsonl이란, json 형식의 데이터 여러개를 한 줄 단위로 구분하는 형식의 파일입니다. 예를 들자면 다음과 같습니다.
{key: 1, data: "data1"}
{key: 2, data: "data2"}
{key: 3, data: "data3"}
...
gemini의 경우 요청을 보낼 수 있는 방법이 2가지 입니다.
- Inline Requests:
GenerateContentRequest
객체 리스트를 source로 해서 보냄 - Input File:
GenerateContentRequest
객체 + key 데이터 등을 포함한 데이터를 json으로 바꿔서, JSONL 파일로 보냄
개인적으로 2번을 추천드립니다. 내가 보낸 input의 내용을 파일로 남겨두는게 생각보다 도움이 됩니다(테스트를 여러 번 하다보면 방금 내가 제대로 된 input을 보냈는지 까먹습니다......)
GenerateContentRequest에 대해서는 아래에서 설명합니다.
GenerateContentRequest 객체란?
위에서 계속 언급되는 GenerateContentRequest는, gemini가 답해야할 하나의 질문을 객체화한 데이터 입니다.
예를 들어, 내가 보낼 질문의
- 페르소나 프롬프트(system prompt)가 "너는 고양이야. 고양이 소리로만 대답할 수 있어"이고,
- 모델에 할 질문(user prompt)이 "왜 고양이는 그루밍을 해?"이라면,
GenerateContentRequest는 다음과 같이 구성하면 됩니다.
{
"contents": [
{
"role": "user",
"parts": [{"text": "왜 고양이는 그루밍을 해?"}]
}
],
"systemInstruction": {
"parts": [{"text": "너는 고양이야. 고양이 소리로만 대답할 수 있어."}]
}
}
참고로, 원래 GenerateContentRequest에는 사용하는 Gemini 모델의 이름(ex: gemini-2.0-flash, gemini-2.5-flash-pro ..)를 담는 model 필드도 필수로 있어야 한다고 적혀있습니다. 그런데 batch mode에서는 생략을 하나봅니다.
이 객체를 통해 모델에게 할 질문 뿐 아니라, 모델이 생각을 할지말지 결정하거나, 모델이 내뱉을 출력의 형식과 글자 수 제한을 정할 수도 있고, temperature를 조절해줄 수 있습니다. 자세한 건 아래에서 서술하겠습니다.
user prompt 외에는 선택적인 항목이여서 꼭 사용할 필요는 없습니다.
(+ 설명한 것 외에, 후보 답변을 여러 개 생성하게하기, 모델의 생성에 규약 두기 등 유용한 설명이 많으니 문서를 한번 꼭 살펴보는걸 추천합니다)
모델한테 질문하기 - user prompt
GenerateContentRequest.contents[]
메인 prompt(gemini한테 할 질문)를 여기 적으면 됩니다.
사실 이후에 서술할 내용을 다 여기 적어도 생각보다 잘 작성해주긴합니다.
GenerateContentRequest{
"contents": [
{
"role": "user",
"parts": [{"text": "왜 고양이는 그루밍을 해?"}]
}
]
}
너는 어떤 생각을 가진 모델이다! - system prompt
GenerateContentRequest.systemInstruction
system prompt는 모델의 페르소나를 정하는 prompt입니다.
예를 들어, "너는 10년차 개발자야. react 코드는 발로 짤 수 있어" 같은 문장을 적는 곳입니다.이걸 적냐 안 적냐의 차이가 꽤나 크다고 하네요.
GenerateContentRequest{
"systemInstruction": {
"parts": [{"text": "너는 고양이야. 고양이 소리로만 대답할 수 있어."}]
}
}
답변의 다양성 정도 설정하기 - temperature
GenerateContentRequest.generatedConfig.temperature
temperature는 모델이 답변할 때의 자유도를 높게 줄지, 낮게 줄지 설정하는 수치(gemini의 경우, 0에서 2사이의 소수)입니다.
예를 들어 모델에게 "제일 좋아하는 동물이 뭐야?"라고 질문하면, temperature 0일 때 가장 흔한 답변(ex "고양이"), temperature 1일 때 특이한 답변(ex "인간"), temperature 2일 때 정말 특이한 답변(ex 생뚱맞게 중국어를 할 수도 있음)을 하게 됩니다.
GenerateContentRequest{
"generatedConfig": {
"temperature": 0.8
}
}
출력의 형식 제한하기(json으로만 답해라) - responseMimeType, responseJsonSchema
GenerateContentRequest.generatedConfig.responseMimeType
responseMimeType이 json인 경우, GenerateContentRequest.generatedConfig.responseJsonSchema
llm을 api로 사용하는 사람들은 보통 서비스에 얘를 집어넣거나, 생성한 데이터를 다량으로 수집해 가공하는 목적을 가집니다. 이를 위해서는 모델이 형식화된 출력을 내놓는 편이 좋은데, 이를 위해 구글이 마련해준 설정값입니다. 이 글의 메인은 사실상 얘라고 생각합니다.
먼저 responseMimeType은 출력의 형식(mime type 중 하나)을 그냥 텍스트(text/plain
), JSON(application/json
), enum(text/x.enum
) 등으로 지정해줄 수 있는 값입니다.
이걸 json으로 설정해주면, 모델이 출력을 json 형식으로만 뱉게 됩니다.
출력 json의 구체적인 schema도 responseMimeSchema을 통해 설정해줄 수 있습니다. 공식 문서 - structed output 공식 문서 - 자세한 스키마 형식 지정법 자세히 설명하자면,
내부에서 type
을 통해 객체의 형식을 지정하고, 관련된 필드를 적어서 객체의 값을 설명하면 됩니다.
예를 들어, 어떤 대상(강아지, 모니터, 개나리 등)를 주면, 모델이 물체에 대한
- (color) 빨강, 파랑, 노랑 중 대상에 가장 가까운 색 - enum
- (is_alive) 대상이 살아있나? - boolean
- (relative_objects) 최대 5개의 대상과 관련있는 것들 - array(string) 최대 5개
- (relative_objects_length) 관련있는 대상의 개수 - integer
- (explain_your_response) 위와 같이 판단한 이유 - string
속성들을 json화해서 출력하게 하고 싶다면,
GenerateContentRequest{
"generatedConfig": {
"responseMimeType": "application/json",
"responseSchema": {
"type": "OBJECT",
"properties": {
"explain_your_response": {"type": "STRING"},
"color": {"type": "STRING", "enum": ["빨강", "파랑", "노랑"]},
"is_alive": {"type": "BOOLEAN"},
"relative_objects": {
"type": "OBJECT",
"properties": {
"relative_objects": {"type": "ARRAY", "items": {"type": "STRING"}, "maxItems": "5"},
"relative_objects_length": {"type": "INTEGER"}
}
},
"required": [ "color", "is_alive", "relative_objects", "explain_your_response" ] // 모든 속성을 필수적으로 포함할 것
}
}
}
이제 대상을 "개나리"라고 해서 물어봤을 때, 모델은 다음과 같은 형식으로만 답하게 됩니다.
{
"explain_your_reason": "개나리는 봄을 대표하는 관목으로, 선명한 노란색 꽃을 피우는 것이 가장 큰 특징이며, 이는 빨강이나 파랑보다 노랑과 가장 가깝습니다. 개나리는 비록 동물처럼 움직이지는 않지만, 광합성, 생장, 개화 등 생물학적 기능을 갖춘 살아 있는 식물입니다. 이로 인해 개나리는 살아 있다고 판단됩니다. 또한 개나리는 봄, 꽃, 노란색, 산수유(비슷한 시기에 피는 노란 꽃) 등과 밀접한 연관이 있으며, 관련 있는 대상은 총 4개입니다. ",
"color": "노랑",
"is_alive": true,
"relative_objects": {
"relative_objects": ["봄", "꽃", "노란색", "산수유"],
"relative_objects_length": 4
}
}
모델이 내부적으로 얼마나 생각할 수 있게 할까? - thinkingBudge
GenerateContentRequest.generatedConfig.thinkingConfig.thinkingBudget
'let's think step by step'이라는 문장을 프롬프트에 넣으니 LLM의 성능이 급격하게 상승했다는 이야기를 들을 적 있나요? 이는 프롬프트 기법 중 zero shot CoT로, 이를 기반으로 CoT(모델이 문제를 풀 때, 중간 단계를 명확히 밝히면서 단계적으로 추론하는 방식)이 프롬프트 기법으로 유행했습니다.
이 기법으로 인해 성능이 꽤 많이 늘어나서, 요새 상용 LLM은 이걸 아예 내부적으로 넣어버립니다. 이러한 모델을 보통 thinking model이라고 하는데, gemini의 경우 gemini-2.5-pro
, gemini-2.5-flash
가 thinking model에 해당합니다.
이 특정 모델들이 내부적으로 생각에 쓸 수 있는 토큰의 수를 thinking budget이라고 합니다.
근데 이 토큰도 요금에 포함됩니다. CoT가 굳이 필요없는 간단한 작업을 하고 있거나, 이 요금도 줄이고 싶다하면 얘를 0으로 설정해버릴 수 있습니다.
GenerateContentRequest{
"generatedConfig": {
"thinkingConfig": {
"thinkingBudget": 0,
}
}
}
예시 코드 전체
해당 코드에서는
1. dataframe을 기반으로 task를 하나씩 생성해 batch task 파일을 만들고,
2. batch를 실행시킨 후
3. 결과를 다시 dataframe에 넣어 파일로 저장하는 (+추가적으로 발생한 오류를 저장해줌)
작업을 포함합니다.
함수 모음 코드
from google import genai
from google.genai import types
import json
from json import JSONDecodeError
client = genai.Client(
api_key="{여기에 api key 입력}",
)
def make_batch_tasks_file(df, result_name):
# batch 입력 파일(batch_tasks.jsonl) 생성
def row_to_json(index, row):
return {
"key": f"request-{index}",
"request": {
"contents": [
{
"role": "user",
"parts": [{
"text": f"""{row['object']}가 대상일 때, 다음에 대해 답해줘
- (color) 빨강, 파랑, 노랑 중 대상에 가장 가까운 색
- (is_alive) 대상이 살아 움직이나?
- (relative_objects) 대상과 관련있는 것들
- (relative_objects_length) 관련있는 대상의 개수
- (explain_your_response) 위와 같이 판단한 이유"""
}]
}
],
"systemInstruction": {
"parts": [{"text": "너는 10년 동안 물체, 생물에 대해 분류해온 전문가야."}]
},
"generationConfig": {
"responseMimeType": "application/json",
"responseSchema": {
"type": "OBJECT",
"properties": {
"color": {"type": "STRING", "enum": ["빨강", "파랑", "노랑"]},
"is_alive": {"type": "BOOLEAN"},
"relative_objects": {
"type": "OBJECT",
"properties": {
"relative_objects": {"type": "ARRAY", "items": {"type": "STRING"}, "maxItems": "5"},
"relative_objects_length": {"type": "INTEGER"}
}
},
"explain_your_response": {"type": "STRING"},
},
"required": [ "color", "is_alive", "relative_objects", "explain_your_response" ]
},
"temperature": 0.4,
"thinkingConfig": { # thinking 설정은 gemini-2.5-flash, gemini-2.5-pro에서만 지원됩니다.
"thinkingBudget": 0,
},
}
}
}
tasks = [row_to_json(index, row) for index, row in df.iterrows()]
file_name = f"batch_tasks_{result_name}.jsonl"
with open(file_name, 'w') as file:
for obj in tasks:
file.write(json.dumps(obj) + '\n')
def create_batch(result_name):
# File API에 파일 업로드
uploaded_file = client.files.upload(
file=f'batch_tasks_{result_name}.jsonl',
config=types.UploadFileConfig(display_name=f'batch_tasks{result_name}', mime_type='jsonl')
)
# batch 작업 생성
file_batch_job = client.batches.create(
model="gemini-2.0-flash",
src=uploaded_file.name,
config={
'display_name': "file-upload-job-{}".format(result_name),
},
)
return file_batch_job
def check_batch_status(file_batch_job):
# batch 작업 상태 확인
job_name = file_batch_job.name
batch_job = client.batches.get(name=job_name)
print(f"Polling status for job: {job_name}")
print(f"Current state: {batch_job.state.name}")
if batch_job.state.name == 'JOB_STATE_FAILED':
print(f"Error: {batch_job.error}")
def save_batch_result(df, file_batch_job, result_name):
# 응답 파일 저장
job_name = file_batch_job.name
batch_job = client.batches.get(name=job_name) # Initial get
if batch_job.state.name == 'JOB_STATE_SUCCEEDED':
# If batch job was created with a file
if batch_job.dest and batch_job.dest.file_name:
# Results are in a file
print("Downloading result file content...")
file_content = client.files.download(file=batch_job.dest.file_name)
with open(f'batch_job_results_{result_name}.jsonl', 'wb') as file:
file.write(file_content)
parse_fails = []
# 응답 dataframe에 추가
with open(f'batch_job_results_{result_name}.jsonl', 'r', encoding='utf-8') as f:
for line in f:
res = json.loads(line)
row_id = int(res['key'][8:]) # 추출한 id 값
if 'error' in res:
# 요청에 에러가 있는 경우
parse_fails.append({ 'id': row_id, 'response': res })
continue
# 그 외의 프롬프트가 거부된 경우
if 'response' not in res or 'candidates' not in res['response']:
parse_fails.append({ 'id': row_id, 'response': res })
continue
if not res['response']['candidates']:
parse_fails.append({ 'id': row_id, 'response': res })
continue
candidate = res['response']['candidates'][0]
parts = candidate.get('content', {}).get('parts')
if not parts:
parse_fails.append({ 'id': row_id, 'response': res })
continue
# 정상적인 입력
try:
json_obj = json.loads(parts[0]['text'])
df.at[row_id, 'explain_your_response'] = json_obj.get("explain_your_response", None)
df.at[row_id, 'color'] = json_obj.get("color", None)
df.at[row_id, 'is_alive'] = json_obj.get("is_alive", None)
df.at[row_id, 'relative_objects'] = str(json_obj.get("relative_objects", None))
except JSONDecodeError:
print(parts[0]['text'])
parse_fails.append({
'id': row_id,
'response': res
})
df.to_csv(f'batch_results_{result_name}.csv', index=False)
# parse_fails 저장
with open(f'parse_fails_{result_name}.json', 'w', encoding='utf-8') as f:
json.dump(parse_fails, f, ensure_ascii=False, indent=4)
실행 코드
batch 생성
import pandas as pd
objects = ['벚꽃', '고양이', '연필', '해바라기', '컴퓨터', '참새', '자동차', '사과', '로봇', '선풍기']
df = pd.DataFrame(objects, columns=['object'])
result_name = 'objects'
make_batch_tasks_file(df, result_name)
file_batch_job = create_batch(result_name)
check_batch_status(file_batch_job)
batch 상태 확인
check_batch_status(file_batch_job)
batch 결과 파일, 결과 csv, 실패 기록 저장
check_batch_status(file_batch_job)
결과 파일은 다음과 같이 생성됩니다.
- batch_job_results_objects.jsonl: 배치 결과 파일
- parse_fails_objects.json: 실패 기록
- batch_results_objects: 결과가 저장된 dataframe 파일
object,explain_your_response,color,is_alive,relative_objects 벚꽃,"벚꽃은 분홍색에 가까우며, 분홍색은 빨강색의 범주에 포함될 수 있다고 판단했습니다. 벚꽃은 살아있는 식물이므로 is_alive는 true입니다. 벚꽃과 관련된 대상은 벚꽃나무, 벚꽃잎, 봄, 꽃놀이 등이 있습니다.",빨강,True,"{'relative_objects': ['벚꽃나무', '벚꽃잎', '봄', '꽃놀이'], 'relative_objects_length': 4}" 고양이,"고양이의 털 색깔은 다양하지만, 노란색(또는 갈색, 주황색) 고양이가 흔히 발견됩니다. 고양이는 살아있는 동물입니다. 고양이와 관련된 대상은 고양이의 생활과 관련된 것들입니다.",노랑,True,"{'relative_objects': ['쥐', '생선', '고양이 캔', '털실', '장난감 쥐'], 'relative_objects_length': 5}" 연필,"연필은 다양한 색상으로 만들어지지만, 나무 연필의 경우 노란색으로 칠해지는 경우가 많고, 심을 보호하기 위해 노란색으로 칠해진 경우가 많습니다.",노랑,False,"{'relative_objects': ['지우개', '종이', '샤프', '책상', '필통'], 'relative_objects_length': 5}" 해바라기,해바라기는 일반적으로 노란색 꽃잎을 가지고 있습니다.,노랑,True,"{'relative_objects': ['태양', '씨앗', '꿀벌', '해바라기 기름', '밭'], 'relative_objects_length': 5}" 컴퓨터,"컴퓨터는 일반적으로 검정색, 흰색, 회색, 파란색을 띄며, 그중 파란색이 가장 흔하게 사용되는 색상 중 하나입니다. 컴퓨터는 생명이 없는 물체입니다. 컴퓨터는 다양한 부품과 주변 기기와 관련이 있습니다.",파랑,False,"{'relative_objects': ['모니터', '키보드', '마우스', '프린터', '스피커'], 'relative_objects_length': 5}" 참새,"참새는 갈색, 검은색, 흰색, 노란색을 띕니다. 이 중 노란색이 가장 가깝습니다.",노랑,True,"{'relative_objects': ['새', '둥지', '알', '지렁이', '곡식'], 'relative_objects_length': 5}" 자동차,"자동차는 다양한 색상이 있지만, 빨강, 파랑, 노랑 중에서 빨간색 자동차가 흔히 보이기 때문에 가장 가깝다고 판단했습니다. 자동차는 기계 장치이므로 살아 움직이지 않습니다. 자동차와 관련된 대상으로는 도로, 운전자, 연료 등이 있습니다.",빨강,False,"{'relative_objects': ['도로', '운전자', '연료', '타이어', '핸들'], 'relative_objects_length': 5}" 사과,"사과는 일반적으로 빨간색, 노란색, 초록색을 띕니다. 파란색 사과는 존재하지 않습니다. 사과는 살아있는 생물이 아닌 과일입니다. 사과는 나무, 잼, 주스와 관련이 있습니다.",빨강,False,"{'relative_objects': ['나무', '잼', '주스'], 'relative_objects_length': 3}" 로봇,"로봇은 다양한 색상으로 만들어질 수 있지만, 파란색은 로봇의 외형에 흔히 사용되는 색상 중 하나입니다. 로봇은 기계 장치이므로 살아 움직이지 않습니다. 로봇과 관련된 대상은 로봇의 종류, 사용 목적, 제조사 등에 따라 매우 다양할 수 있습니다.",파랑,False,"{'relative_objects': ['인공지능', '기계', '컴퓨터', '센서', '액추에이터'], 'relative_objects_length': 5}" 선풍기,"선풍기는 주로 플라스틱이나 금속으로 만들어지며, 파란색은 선풍기에서 흔히 볼 수 있는 색상 중 하나입니다. 빨강, 파랑, 노랑 중 선풍기의 색깔로 가장 흔한 것은 파랑입니다. 선풍기는 전기의 힘으로 작동하는 기계 장치이므로 살아있는 것으로 간주하지 않습니다. 선풍기와 관련된 대상으로는 전기, 바람, 날개, 모터 등이 있습니다.",파랑,False,"{'relative_objects': ['전기', '바람', '날개', '모터'], 'relative_objects_length': 4}"