tool loop 의 한 cycle 은 세 단계야. 먼저 context 를 보내 모델에게 tool_calls 또는 텍스트를 받아. 텍스트면 끝내고, tool_calls 면 각 handler 를 실행해 결과를 모아. 그 결과를 다시 모델에 보내 텍스트나 다음 tool_call 을 받고, tool_call 이 없어질 때까지 반복해.
세 단계를 코드에 그대로 드러내
1단계: responses.create(input, tools=[...]) 를 호출하고 output 이 tool_call 인지 텍스트인지 확인해.
2단계: 각 tool_call 의 name 과 arguments 로 handler 를 실행하고 결과를 저장해.
3단계: 결과를 Responses 의 function_call_output 또는 Chat Completions 의 tool role 메시지로 보내. 텍스트가 나올 때까지 반복해.
반복 횟수에는 반드시 상한을 둬
max_tool_iterations=10 처럼 명시적인 상한을 설정해. 잘못된 prompt 는 도구끼리 끝없이 오가는 loop 를 만들 수 있어. 상한에 닿으면 분명한 오류와 함께 중단하는 circuit breaker 로 사용해.
매 단계를 기록해야 다시 재생할 수 있어
iteration 마다 tool, arguments, 결과를 JSONL 에 기록해. 그러면 실제 loop 를 CI 에서 replay 하는 운영 테스트의 입력으로 쓸 수 있어.
Code
Full tool loop (Responses)·python
import json
from openai import OpenAI
client = OpenAI()
def get_weather(location, units="celsius"):
return {"location": location, "temperature": 22, "condition": "sunny", "units": units}
def search_news(query):
return [{"title": f"News about {query}", "url": "https://news.example.com"}]
TOOLS_MAP = {"get_weather": get_weather, "search_news": search_news}
tools = [
{"type": "function", "name": "get_weather", "description": "Get current weather",
"parameters": {"type": "object", "properties": {"location": {"type": "string"},
"units": {"type": ["string", "null"], "enum": ["celsius", "fahrenheit"]}},
"required": ["location"], "additionalProperties": False}, "strict": True},
{"type": "function", "name": "search_news", "description": "Search recent news",
"parameters": {"type": "object", "properties": {"query": {"type": "string"}},
"required": ["query"], "additionalProperties": False}, "strict": True},
]
input_items = [{"role": "user", "content": "Weather in Tokyo and any AI news?"}]
while True:
response = client.responses.create(model="gpt-5.4", tools=tools, input=input_items)
input_items.extend(response.output)
tool_calls = [i for i in response.output if i.type == "function_call"]
if not tool_calls:
print(response.output_text)
break
for tc in tool_calls:
result = TOOLS_MAP[tc.name](**json.loads(tc.arguments))
input_items.append({
"type": "function_call_output",
"call_id": tc.call_id,
"output": json.dumps(result),
})