시작하기
가이드: 도구 호출 에이전트 만들기
단계 수를 제한한 루프, 병렬 도구 호출, 구조화된 최종 출력, 비용 제어를 갖춘 도구 호출 에이전트를 OpenAI 형식으로 직접 만드는 과정을 Python과 Node 코드로 따라 해 봅니다. 모델 선택 기준도 함께 짚습니다.
에이전트는 하나의 루프입니다. 모델이 대화를 읽고 하나 이상의 도구를 호출하기로 결정하면, 사용자가 이를 실행해 결과를 돌려보내고, 모델은 답할 수 있을 때까지 이를 계속합니다. api14는 도구를 지원하는 모든 모델에서 OpenAI tools 형식으로 이를 지원하므로, 모델 id만 바꾸면 같은 코드가 OpenAI, Anthropic, Google 등 여러 제공자에서 동작합니다.
이 가이드는 단계 제한, 병렬 도구 실행, 구조화된 최종 답변, 비용 제어를 갖춘 작은 에이전트를 만듭니다. 코드는 공식 openai 패키지와 base URL https://api14.com/api/v1을 사용합니다.
도구 정의
각 도구는 이름, 모델이 읽는 설명, 인자에 대한 JSON Schema로 이루어집니다. 설명은 구체적으로 작성하세요. 모델이 참고할 수 있는 문서는 이것뿐입니다.
import json, os
from openai import OpenAI
client = OpenAI(base_url="https://api14.com/api/v1", api_key=os.environ["API14_API_KEY"])
def search_orders(customer_email: str) -> list:
return [{"id": "A-1001", "status": "shipped"}] # your database query
def get_shipment(order_id: str) -> dict:
return {"order_id": order_id, "carrier": "UPS", "eta": "2026-10-09"} # your API call
TOOLS = {"search_orders": search_orders, "get_shipment": get_shipment}
tools = [
{"type": "function", "function": {
"name": "search_orders",
"description": "List a customer's orders with their status.",
"parameters": {"type": "object", "properties": {"customer_email": {"type": "string"}},
"required": ["customer_email"]},
}},
{"type": "function", "function": {
"name": "get_shipment",
"description": "Carrier and estimated delivery date for one order.",
"parameters": {"type": "object", "properties": {"order_id": {"type": "string"}},
"required": ["order_id"]},
}},
]
단계 제한이 있는 루프
반복할 때마다 별도의 요청으로 과금되므로 항상 단계 수에 상한을 두세요. 도구 오류는 프로그램을 중단시키지 말고 내용으로 모델에 돌려주세요. 모델은 보통 인자를 고쳐 스스로 복구합니다.
MAX_STEPS = 8
def run_tool(call):
try:
args = json.loads(call.function.arguments)
result = TOOLS[call.function.name](**args)
except Exception as e: # bad JSON, unknown tool, or your function failed
result = {"error": str(e)}
return {"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)}
def agent(task, model="openai/gpt-5-mini"):
messages = [
{"role": "system", "content": "You are a support agent. Use tools to look things up. Be brief."},
{"role": "user", "content": task},
]
spent = 0.0
for step in range(MAX_STEPS):
r = client.chat.completions.create(model=model, messages=messages, tools=tools, max_tokens=1000)
spent += r.usage.cost
msg = r.choices[0].message
if not msg.tool_calls:
return msg.content, spent
messages.append(msg)
messages.extend(run_tool(call) for call in msg.tool_calls)
raise RuntimeError(f"No answer after {MAX_STEPS} steps (spent ${spent:.4f})")
print(agent("Where is my order? My email is ada@example.com"))
루프 안의 모든 요청에 tools를 함께 보내세요. 요청에 도구 정의가 없으면 tool 메시지를 거부하는 제공자가 많습니다.
병렬 도구 호출
parallel_tool_calls(지원되는 곳에서는 기본으로 켜짐)를 사용하면 모델이 한 턴에 여러 호출을 요청할 수 있습니다. 예를 들어 주문 세 건에 대해 get_shipment를 한꺼번에 요청합니다. 이를 동시에 실행하고 호출마다 tool 메시지를 하나씩 추가하세요. 순서는 상관없으며, tool_call_id가 각 결과를 해당 호출에 연결합니다. Node 예시입니다.
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api14.com/api/v1", apiKey: process.env.API14_API_KEY });
const MAX_STEPS = 8;
async function agent(messages, tools, impl, model = "openai/gpt-5-mini") {
for (let step = 0; step < MAX_STEPS; step++) {
const res = await client.chat.completions.create({ model, messages, tools, max_tokens: 1000 });
const msg = res.choices[0].message;
if (!msg.tool_calls?.length) return msg.content;
messages.push(msg);
const results = await Promise.all(msg.tool_calls.map(async (call) => {
let content;
try {
content = JSON.stringify(await impl[call.function.name](JSON.parse(call.function.arguments)));
} catch (err) {
content = JSON.stringify({ error: String(err) });
}
return { role: "tool", tool_call_id: call.id, content };
}));
messages.push(...results);
}
throw new Error(`No answer after ${MAX_STEPS} steps`);
}
도구에 부수 효과가 있어 순서대로 실행해야 한다면(생성한 뒤 수정 등) parallel_tool_calls: false로 설정해 모델이 턴마다 호출을 하나씩만 요청하게 하세요.
구조화된 최종 출력
에이전트의 답변을 다른 코드가 사용한다면 스키마에 맞는 JSON을 요청하세요. 간단하고 이식성 높은 방법은 루프가 사실을 모두 모은 뒤 호출을 한 번 더 하는 것입니다(여기서 messages는 루프의 대화이며, agent()가 답변과 함께 반환하도록 하세요).
schema = {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"status": {"type": "string", "enum": ["processing", "shipped", "delivered", "unknown"]},
"eta": {"type": ["string", "null"]},
"reply": {"type": "string"},
},
"required": ["order_id", "status", "eta", "reply"],
"additionalProperties": False,
}
final = client.chat.completions.create(
model="openai/gpt-5-mini",
messages=messages + [{"role": "user", "content": "Summarize the result for our ticket system."}],
tools=tools,
tool_choice="none",
response_format={"type": "json_schema", "json_schema": {"name": "ticket", "strict": True, "schema": schema}},
)
ticket = json.loads(final.choices[0].message.content)
tool_choice: "none"은 요청에 tools를 유지하면서(기록에 tool 메시지가 있으므로 필요합니다) 추가 호출은 막습니다. 파싱한 JSON은 항상 검증하세요. Structured outputs를 참고하세요.
모델 선택
- 기능: 모델의
supported_parameters에tools가 있어야 합니다.GET /models로 확인하세요. 모델을 참고하세요. 마지막 단계에는structured_outputs또는response_format이 있는 모델을 고르세요. - 라우팅:
provider: { "require_parameters": true }를 추가하면(Python에서는extra_body로) 보내는 모든 파라미터를 지원하는 제공자로만 요청이 전달됩니다. 제공자 라우팅을 참고하세요. - 크기:
openai/gpt-5-mini같은 작은 모델은 잘 설명된 소수의 도구를 안정적이고 저렴하게 처리합니다. 에이전트의 도구가 많거나, 여러 단계에 걸친 긴 계획이 필요하거나, 자꾸 잘못된 도구를 고른다면 더 큰 모델로 옮기세요. - 내 도구로 테스트: 실제 작업 20~50개를 후보 모델 두세 개에 똑같이 실행해 성공률, 작업당 단계 수, 총
usage.cost를 비교하세요.
curl -s "https://api14.com/api/v1/models?category=text" \
| jq -r '.data[] | select(.supported_parameters | index("tools")) | "\(.id) \(.pricing.prompt)"'
비용 제어
에이전트는 작업 하나에 요청을 여러 번 보낼 수 있고, 단계마다 전체 대화가 다시 전송됩니다. 여러 수준에서 지출을 제한하세요.
- 위 예시처럼 단계 제한(
MAX_STEPS)과 요청당max_tokens. - 작업별 예산: 단계마다
usage.cost를 합산하고 기준을 넘으면 중단합니다. - 짧은 도구 결과: 모델에 필요한 필드만 반환하세요. 큰 도구 출력은 이후 모든 단계에서 입력 토큰으로 과금됩니다.
- 키별 일일 USD 한도: API 키에서 에이전트 전용 키를 만들고 일일 한도를 설정하세요. 한도에 도달하면 다음 UTC 날짜까지 요청이
429 daily_limit_reached를 반환합니다. 이 코드는 재시도하지 말고 에이전트를 중단하는 방식으로 처리하세요. - 모델 허용 목록: 키를 테스트한 모델로만 제한하세요. 그 외 모델은
403 model_not_allowed를 반환합니다. - 프롬프트 캐싱: 시스템 프롬프트와 도구 정의를 매 요청의 맨 앞에 동일하게 유지하면 캐시된 접두부 할인이 적용됩니다. 프롬프트 캐싱을 참고하세요.