GPT-6 Astra Function Calling은 AI 에이전트의 핵심이다. 모델이 어떤 도구를 호출하고 어떤 인수를 전달할지 결정하면 런타임이 부작용을 실행하고 결과를 모델에 돌려줘 추론을 계속한다. 루프가 올바르면 Astra는 자율적으로 API를 호출하고, 데이터베이스를 읽고, 커스텀 로직을 실행할 수 있다. 잘못되면 루프가 막히거나 도구 권한이 제어 불능이 된다. "Function Calling이 무엇인지"는 프로토콜 비교에, "API 키를 어떻게 얻는지"는 API 튜토리얼에 있다. 이 글은 "돌아가는"에서 "안전하게 배포 가능한" 수준으로 올리는 세 가지 핵심 도구 패턴을 다룬다.
1단계: 도구 스키마 정의
도구 스키마는 모델에게 "이 도구의 이름, 목적, 파라미터"를 알려주는 JSON 객체다. 날씨 HTTP API를 호출하는 도구 예제를 보자.
import json
from openai import OpenAI
client = OpenAI()
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Get current weather for a city via HTTP API.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "City name, e.g. Tokyo, London, Shanghai",
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Default celsius.",
},
},
"required": ["city"],
"additionalProperties": False,
},
}
]
description은 도구가 무엇을 하는지 동사구로 작성한다. 모델은 이 필드를 읽고 도구를 호출할지 결정한다. 필수 파라미터는 required에 나열하고, additionalProperties: false로 예기치 않은 키를 차단한다. 여러 도구를 같은 리스트에 넣어도 된다. 모델이 한 턴에 여러 도구를 병렬 호출할 수 있으므로 루프는 모든 tool_call을 처리해야 한다.
2단계: 완전한 도구 루프
요청 전송 → output의 function_call 감지 → 실행 → 결과를 input에 추가 → 재전송, function_call이 사라질 때까지 while로 반복한다. if로 한 번만 처리하면 최종 텍스트 답변이 오지 않는다.
import json
import requests
from openai import OpenAI
client = OpenAI()
def call_weather_api(city: str, unit: str = "celsius") -> dict:
"""Replace with your real HTTP endpoint."""
url = "https://api.example.com/weather"
r = requests.get(url, params={"city": city, "unit": unit}, timeout=5)
r.raise_for_status()
return r.json() # e.g. {"city": "Tokyo", "temp": 22, "condition": "sunny"}
# Initial request — model may call tools
response = client.responses.create(
model="gpt-6-astra",
input="What is the weather in Tokyo right now?",
tools=tools,
reasoning={"effort": "low"},
)
# Tool loop: keep going until model stops calling tools
while True:
tool_calls = [o for o in response.output if o.type == "function_call"]
if not tool_calls:
break # model is done — final answer is in response.output_text
tool_results = []
for call in tool_calls:
args = json.loads(call.arguments)
if call.name == "get_weather":
result = call_weather_api(**args)
else:
result = {"error": f"Unknown tool: {call.name}"}
tool_results.append({
"type": "function_call_output",
"call_id": call.call_id,
"output": json.dumps(result, ensure_ascii=False),
})
# Append history + results and continue
response = client.responses.create(
model="gpt-6-astra",
input=response.output + tool_results,
tools=tools,
reasoning={"effort": "low"},
)
print(response.output_text)
구현 주의사항. ① response.output은 여러 타입이 섞인 리스트다. type == "function_call"만 필터링한다. ② 계속할 때는 response.output + tool_results를 input으로 전달한다. 이력 없이 tool_results만 전달하면 모델이 컨텍스트를 잃는다. ③ call_id는 tool_call과 tool_result에서 일치해야 한다. ④ 반환값은 json.dumps로 문자열화한다.
name 필드를 알려진 allowlist로 검증하고, 인수를 타입 체크하고, HTTP 도구에는 타임아웃과 도메인 allowlist를 추가한다. DB 도구는 사전 정의된 SQL만 실행하며 모델 출력을 SQL에 이어붙이지 않는다. 디스크 쓰기나 셸 실행 도구는 원격 Mac에서 격리 실행한다.
3단계: 읽기 전용 데이터베이스 도구
안전한 DB 도구의 3요소: 읽기 전용 권한(INSERT/UPDATE/DELETE 금지), 파라미터화된 쿼리(인젝션 방지), SQL allowlist(임의 쿼리 방지).
import sqlite3
from typing import Any
# Only these parameterized queries can be executed — no raw user SQL
ALLOWED_QUERIES: dict[str, str] = {
"get_order": "SELECT id, status, total, created_at FROM orders WHERE id = ?",
"list_orders": "SELECT id, status, total FROM orders WHERE user_id = ? LIMIT 20",
}
def query_db(query_name: str, params: list[Any]) -> list[dict]:
"""Execute an allowlisted, parameterized read-only query.
Never accepts raw SQL strings from the model.
"""
sql = ALLOWED_QUERIES.get(query_name)
if sql is None:
raise ValueError(f"Query '{query_name}' not in allowlist")
con = sqlite3.connect("orders.db", check_same_thread=False)
con.row_factory = sqlite3.Row
try:
rows = con.execute(sql, params).fetchall()
return [dict(r) for r in rows]
finally:
con.close()
# Schema exposed to the model
db_tool = {
"type": "function",
"name": "query_db",
"description": "Query the orders DB. Only read-only allowlisted queries.",
"parameters": {
"type": "object",
"properties": {
"query_name": {
"type": "string",
"enum": list(ALLOWED_QUERIES.keys()),
"description": "Name of the allowlisted query",
},
"params": {
"type": "array",
"items": {"type": ["string", "number"]},
"description": "Positional parameters for the query",
},
},
"required": ["query_name", "params"],
},
}
운영 환경에서는 커넥션 풀, 쿼리 타임아웃, SQL에 하드코딩된 행 수 제한, 민감 컬럼 마스킹이 필요하다. 모델에 반환하는 것은 요약이어야 하며, 테이블 전체의 raw dump가 아니다.
함수 도구 vs 호스팅 도구
| 차원 | 함수 도구 (type: function) | 호스팅 도구 (OpenAI 운영) |
|---|---|---|
| 실행 위치 | 내 런타임 (로컬 / 원격 Mac) | OpenAI 인프라 |
| 적합한 경우 | 프라이빗 API, 내부 DB, 커스텀 로직 | 웹 검색, 파일 검색, 코드 실행, shell, Computer Use |
| 개발 비용 | 스키마 + 실행 함수 + 루프를 직접 구현 | 도구 이름만 지정하면 OpenAI가 실행 |
| 과금 | 토큰만 | 토큰 + 도구 호출 추가 요금 |
| 격리 요건 | 쓰기 도구는 실행 환경 격리 필요 | OpenAI가 샌드박스 관리 |
두 타입을 같은 요청에서 혼합할 수 있다. 호스팅 도구 결과는 자동으로 컨텍스트에 들어가므로 수동 제출 불필요. 함수 도구 결과는 function_call_output으로 수동 제출해야 한다.
결정 매트릭스
| 필요한 것 | 선택 | 이유 |
|---|---|---|
| 사내 REST API 호출 | 함수 도구 | 프라이빗 네트워크는 OpenAI 인프라에서 도달 불가 |
| 내부 DB 쿼리 (읽기 전용) | 함수 도구 + SQL allowlist | 프라이빗 데이터. 접근 권한은 직접 관리 |
| 공개 웹 실시간 검색 | 호스팅 web_search_preview | 크롤링과 랭킹은 OpenAI가 담당 |
| 모델에 Python 실행 요청 | 호스팅 code_interpreter | OpenAI 샌드박스에서 실행. 로컬 리소스 불필요 |
| 모델이 데스크톱/브라우저 제어 | 호스팅 computer_use + 격리 Mac | 가시 데스크톱 필요. 오작동 방지를 위해 격리 |
| 디스크 쓰기 shell 명령 실행 | 함수 도구 + 원격 Mac 격리 | 쓰기 권한은 폐기 가능한 환경에만 |
추천 스택
A — 개인 개발자: 로컬 + 함수 도구 (HTTP API, 디스크 비쓰기). 스키마와 루프를 확립하고 call_id를 로깅한다. 단일 도구 루프가 안정되면 멀티 도구로 확장한다.
B — 소규모 팀: 함수 도구는 내부 서비스에 토큰 인증으로 연결. DB 도구는 읽기 전용 + 커넥션 풀 + 쿼리 타임아웃. 호스팅 도구는 태스크 유형별로 활성화. 도구 실행 로직은 단위 테스트로 커버.
C — 엔터프라이즈: 함수 도구는 API 게이트웨이 경유 + 감사 로그. DB는 읽기 전용 레플리카 + 행 수준 보안. Computer Use는 폐기 가능한 격리 Mac 노드에서 실행. 노드 월 비용은 가격 페이지, 계정은 고객센터.
흔한 실수
- 루프를 한 번만 실행 (if 사용): 최종 텍스트 답변이 도달하지 않는다.
- 이력 없이 결과 제출: input에 tool_results만 전달하면 모델이 컨텍스트를 잃는다.
- 모델 출력을 SQL/셸에 직접 연결: allowlist + 파라미터화된 쿼리로 방지한다.
- HTTP 도구 타임아웃 미설정: 다운스트림 서비스가 멈추면 에이전트 전체가 중단된다.
- 도구가 대용량 데이터 반환: 결과가 컨텍스트에 들어가 272K 요금 업리프트를 유발한다.
액션 플랜: 7단계
- 텍스트 경로가 먼저 동작하는지 확인 (API 튜토리얼 참조).
- 첫 함수 도구 스키마 작성. description은 동사구, required 설정, additionalProperties: false.
- 도구 실행 함수 구현. HTTP 도구에 타임아웃, DB 도구에 allowlist + 파라미터화 쿼리.
- tools 파라미터 포함해 Responses 요청을 보내고 response.output에 function_call이 나타나는지 확인.
- while True 루프 구현. call_id 일치시켜 function_call_output 제출, input에 이력 포함.
- 호스팅 도구 (web_search, code_interpreter) 필요 시 추가. 그 결과는 수동 제출 불필요.
- 디스크 쓰기, 셸 실행, 브라우저 제어 도구는 격리 원격 Mac으로 이동, 세션 종료 후 폐기.
FAQ
Function Calling은 Responses API가 필수인가?
예. 도구 호출, 호스팅 도구, 비동기 루프는 Responses API가 필수다.
요청당 도구 수 한도는?
엄격한 소수 제한은 없지만 도구가 많을수록 프롬프트 토큰 비용이 오른다. 현재 태스크에 필요한 도구만 전달하라.
SQL 인젝션을 방지하려면?
? 플레이스홀더 파라미터화 쿼리와 SQL allowlist를 조합한다. 모델 출력을 SQL에 직접 이어붙이지 마라.
함수 도구와 호스팅 도구의 선택 기준은?
프라이빗 API, 내부 DB, 커스텀 로직은 함수 도구. 웹 검색, 코드 실행, Computer Use는 호스팅 도구.
도구 호출 후 최종 답변을 스트리밍할 수 있나?
가능하다. 비스트리밍 루프를 먼저 안정화한 후 stream=True를 추가하라.
결론
GPT-6 Astra Function Calling의 핵심 세 가지: 명확한 description의 스키마, 이력 포함 결과 제출 while 루프, allowlist + 파라미터화 쿼리의 DB 도구와 쓰기 도구 격리. 원격 Mac 노드는 대여 페이지와 가격 페이지에서, 계정 문의는 고객센터.