KO ▾

OpenAI SDK를 사용한 Ollama API 도구 호출 예제

도구 호출은 모델이 코드에 함수를 실행하도록 요청한 후 결과를 답변에 사용하는 기능입니다. 아래는 호스팅된 엔드포인트를 대상으로 한 OpenAI SDK를 사용한 완전한 예제이며, 스트리밍과 가장 흔한 실패 원인을 다룹니다.

업데이트됨

모델이 루프를 통해 함수에 작업을 전달하고 결과를 받는 과정의 일러스트레이션

주요 포인트

  1. tools에 JSON 스키마로 함수를 보내세요; 모델은 텍스트 대신 tool_calls로 응답합니다.
  2. 함수를 실행하고 결과를 role tool 메시지로 추가한 후 API를 다시 호출합니다.
  3. 모델이 잘못되거나 예상치 못한 JSON을 반환할 수 있으므로 방어적으로 인수를 구문 분석해야 합니다.
  4. 스트리밍된 도구 호출은 인덱스별로 연결해야 하는 조각으로 도착합니다.

루프의 구조

호스팅된 엔드포인트에서 작동하는 ollama API 함수 호출 예제를 찾고 있다면, 형식은 표준 OpenAI 형식과 같습니다. 함수는 tools 배열에 설명합니다. 모델은 텍스트로 응답하거나 함수와 해당 인수를 명명하는 tool_calls 목록을 반환합니다. 모델은 아무것도 실행하지 않습니다. 귀하의 코드가 함수를 실행하고 출력을 다시 보내며, 모델이 최종 응답을 작성합니다.

도구 사용의 한 라운드 트립은 두 API 호출을 만듭니다:

  1. 사용자 메시지와 도구 정의를 보냅니다.
  2. tool_calls를 포함하는 어시스턴트 메시지를 받습니다. 각 호출을 로컬에서 실행합니다.
  3. 원본 메시지, 어시스턴트 메시지, 그리고 각 호출당 결과와 함께 role: "tool" 메시지를 하나씩 전송합니다.
  4. 최종 텍스트 답변을 받습니다.

예제는 Base URL https://api.ollamaapi.com/v1과 모델 ID uncensored를 사용합니다. 엔드포인트는 호스팅 서비스이므로 키는 API 키 페이지에서 가져옵니다. Base URL, 키 및 모델 이름을 변경하면 로컬 Ollama 서버에서도 동일한 코드가 실행되지만, 로컬 모델이 도구를 지원해야 합니다.

완전한 Python 예제

예제는 모델에 작은 사전으로 지원되는 하나의 함수 get_order_status을 제공합니다. 실제 애플리케이션은 이때 데이터베이스나 내부 서비스에 쿼리할 것입니다.

import json
from openai import OpenAI

client = OpenAI(base_url="https://api.ollamaapi.com/v1", api_key="YOUR_KEY")

ORDERS = {"A1001": "shipped", "A1002": "processing", "A1003": "delivered"}

def get_order_status(order_id: str) -> str:
    return ORDERS.get(order_id, "not found")

tools = [{
    "type": "function",
    "function": {
        "name": "get_order_status",
        "description": "Look up the status of an order by its ID.",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "Order ID such as A1001"}
            },
            "required": ["order_id"],
        },
    },
}]

messages = [{"role": "user", "content": "Where is order A1002?"}]

first = client.chat.completions.create(
    model="uncensored",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)
msg = first.choices[0].message

if msg.tool_calls:
    messages.append(msg)  # keep the assistant turn with its tool_calls
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_order_status(**args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": result,
        })
    final = client.chat.completions.create(
        model="uncensored",
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)
else:
    print(msg.content)

여기서는 세 가지 세부 사항이 중요합니다. tool_calls을 포함하는 어시스턴트 역할 메시지는 도구 결과 이전의 이력에 다시 포함되어야 합니다. 그렇지 않으면 다음 요청이 거부되거나 혼란스러워집니다. 각 도구 메시지는 해당 호출의 tool_call_id를 포함해야 합니다. 그리고 두 번째 요청에는 여전히 tools를 포함해야 합니다. 모델이 첫 번째 결과를 본 후 다른 함수를 호출하려 할 수 있기 때문입니다.

curl로 동일한 요청

curl로 테스트하면 SDK가 제외되므로 문제가 코드에 있는지 요청에 있는지 불확실할 때 도움이 됩니다.

curl https://api.ollamaapi.com/v1/chat/completions \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "uncensored",
    "messages": [{"role": "user", "content": "Where is order A1002?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_order_status",
        "description": "Look up the status of an order by its ID.",
        "parameters": {
          "type": "object",
          "properties": {"order_id": {"type": "string"}},
          "required": ["order_id"]
        }
      }
    }],
    "tool_choice": "auto"
  }'

성공적인 첫 번째 응답에는 finish_reason이 tool_calls로 설정되고, content이 비어 있거나 null이며, tool_calls 배열이 있습니다. arguments 필드는 중첩된 객체가 아닌 JSON을 포함하는 문자열이므로 Python 예제에서 json.loads를 호출합니다.

tool_choice 필드는 요청의 강제성을 제어합니다. auto은 모델이 결정하도록 합니다. 함수 이름을 명시하는 객체를 전달하면 해당 특정 함수를 요청합니다. 특별한 이유가 없다면 auto를 사용하세요. 필요 없는 메시지에 호출을 강제하면 허위 인수가 생성됩니다.

Node에서의 동일한 루프

Node 버전은 공식 openai 패키지를 사용하고 동일한 단계를 따릅니다. 이 버전은 Promise.all로 한 턴에 여러 호출을 처리합니다.

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.ollamaapi.com/v1",
  apiKey: process.env.API_KEY,
});

const handlers = {
  get_order_status: async ({ order_id }) =>
    ({ A1001: "shipped", A1002: "processing" })[order_id] ?? "not found",
};

const tools = [{
  type: "function",
  function: {
    name: "get_order_status",
    description: "Look up the status of an order by its ID.",
    parameters: {
      type: "object",
      properties: { order_id: { type: "string" } },
      required: ["order_id"],
    },
  },
}];

const messages = [{ role: "user", content: "Where is order A1001?" }];
const first = await client.chat.completions.create({
  model: "uncensored", messages, tools,
});
const msg = first.choices[0].message;

if (msg.tool_calls?.length) {
  messages.push(msg);
  const results = await Promise.all(msg.tool_calls.map(async (call) => {
    const args = JSON.parse(call.function.arguments);
    const out = await handlers[call.function.name](args);
    return { role: "tool", tool_call_id: call.id, content: String(out) };
  }));
  messages.push(...results);
  const final = await client.chat.completions.create({
    model: "uncensored", messages, tools,
  });
  console.log(final.choices[0].message.content);
} else {
  console.log(msg.content);
}

도구 호출 스트리밍

stream: true로 하면 응답이 SSE로 도착하고 도구 호출은 조각으로 전달됩니다. 각 청크에는 delta.tool_calls의 index를 가진 항목이 포함됩니다. 첫 번째 조각은 호출 ID와 함수 이름을 포함합니다. 이후 조각은 arguments 문자열의 일부를 추가합니다. 인덱스별로 조각을 연결하고 스트림이 끝나면 JSON을 파싱합니다.

stream = client.chat.completions.create(
    model="uncensored",
    messages=[{"role": "user", "content": "Where is order A1003?"}],
    tools=tools,
    stream=True,
)

calls = {}
for chunk in stream:
    if not chunk.choices:
        continue
    delta = chunk.choices[0].delta
    for part in (delta.tool_calls or []):
        slot = calls.setdefault(part.index, {"id": "", "name": "", "args": ""})
        if part.id:
            slot["id"] = part.id
        if part.function and part.function.name:
            slot["name"] = part.function.name
        if part.function and part.function.arguments:
            slot["args"] += part.function.arguments

for slot in calls.values():
    print(slot["name"], json.loads(slot["args"]))

스트리밍된 요청은 스트림이 끝날 때까지 동시성 슬롯을 유지합니다. 전체 키는 동시에 8개 요청과 분당 300개를 허용하며, 시험용 키는 2개와 60개를 허용합니다. 여러 사용자를 위해 도구 루프를 병렬로 실행하는 경우 각 열린 스트림을 첫 번째 숫자에 대해 계산하세요.

주의할 점 및 해결 방법

형식이 잘못되었거나 누락된 인수

이 모델은 오픈 웨이트 모델이며, 오픈 모델은 때때로 유효하지 않은 JSON인 인수를 반환하거나 필수 필드를 누락하거나 추가적인 필드를 생성할 수 있습니다. json.loads을 try 블록으로 감싸고 스키마에 따라 유효성을 검사한 후 실패할 경우 오류를 도구 메시지로 전송합니다. 모델은 보통 다음 턴에서 스스로를 수정합니다. 고집스러운 실패가 무한히 계속되지 않도록 루프를 몇 번의 반복으로 제한합니다.

너무 모호한 설명

모델은 이름과 설명을 읽어서 함수를 선택합니다. "데이터 가져오기"와 같은 설명은 모델이 작업할 내용을 전혀 제공하지 못합니다. 함수가 수행하는 작업, 입력의 의미 및 매개변수 설명에 예제 값을 포함하세요.

너무 많은 도구

모든 도구 정의는 모든 요청에서 입력 토큰으로 계산됩니다. 1M 입력 토큰당 $0.15의 가격으로, 150 토큰의 도구 10개가 추가되면 요청당 1,500 토큰이 추가되어 $0.000225가 되며, 이는 작지만 대량 사용 시 0이 아닙니다. 더 중요한 것은, 긴 메뉴는 선택의 정확도를 떨어뜨린다는 것입니다. 현재 단계에 관련 있는 도구만 제공하세요.

너무 큰 도구 결과

반환하는 모든 것이 입력으로 다시 공급됩니다. 큰 데이터베이스 행이나 웹 페이지를 모델이 필요한 필드로 잘라내세요. 컨텍스트 창은 총 100,000 토큰이므로 매우 큰 결과는 대화를 밀어낼 수 있습니다.

JSON 모드와 도구 혼합

JSON 모드(response_format과 json_object)를 사용할 수 있지만, 함수 호출을 원하는 턴이 아닌 최종 답변에 사용하십시오. 이를 신뢰하기 전에 자체 프롬프트에서 조합을 테스트하십시오.

로컬 Ollama에서 전환하고 확인할 사항

로컬 Ollama 서버를 대상으로 첫 도구 루프를 만들었다면 거의 변경할 것이 없습니다. Base URL을 https://api.ollamaapi.com/v1로 바꾸고 실제 키를 사용하며 모델을 uncensored로 설정하세요. /api/chat 같은 네이티브 Ollama 경로는 여기에서 존재하지 않습니다. 오직 POST /v1/chat/completions과 GET /v1/models만 제공되므로 네이티브 API용 코드는 OpenAI 스타일 호출로 변경해야 합니다.

출시 전에 다음 체크리스트를 실행하세요:

  • GET /v1/models로 키가 작동하는지 확인하세요.
  • 모든 호출에 대해 X-Request-Id 헤더를 로그에 기록하여 실패한 호출을 추적할 수 있도록 합니다.
  • 무인 도구 루프를 실행할 때 응답 헤더의 X-Balance-USD를 확인하세요.
  • HTTP 429를 처리하세요: rate_limited 또는 concurrency에서 대기하고, 과부하 시 Retry-After: 5를 준수하세요.
  • 루프 반복에 하드 캡을 설정합니다.

도구 루프는 사용자 질문당 최소 두 번의 요청을 수행하므로 비용은 이에 따라 증가합니다. 예를 들어, 요청당 1,200 입력 토큰과 150 출력 토큰을 가진 2,000개의 루프와 두 번의 요청은 총 4,000개의 요청, 4.8M 입력 토큰 및 0.6M 출력 토큰으로 이어집니다. 이는 $0.72 더하여 $0.30, 즉 총 $1.02입니다. 가격 책정 페이지에는 충전 옵션이 있으며, 문서에는 지원되는 요청 필드가 나열되어 있습니다. 신규 사용자는 카드 없이도 $0.50 체험 크레딧으로 이를 모두 시도할 수 있습니다.

질문과 답변

엔드포인트가 함수 호출을 지원합니까?

네. 도구와 tool_choice를 받으며 tool_calls를 반환하고 델타로 스트리밍하며 결과를 role tool 메시지로 받습니다.

도구 호출 후 두 번째 요청이 실패하는 이유는 무엇입니까?

일반적인 원인은 어시스턴트 메시지가 누락되었거나 일치하는 tool_call_id가 없는 도구 메시지입니다. tool_calls를 포함하는 어시스턴트 턴을 먼저 추가한 후, 각 호출에 대해 하나씩 도구 메시지를 추가하세요.

모델이 한 턴에 여러 도구를 호출할 수 있나요?

하나 이상의 항목이 있는 목록을 반환할 수 있으므로 모든 항목에 대해 반복하여 각 항목에 대한 결과를 보내야 합니다. 목록에 단일 항목만 있다고 가정하지 마세요.

이를 위해 Ollama 라이브러리가 필요합니까?

아닙니다. 공식 OpenAI SDK만으로도 충분합니다. Base URL을 엔드포인트로 설정하고 uncensored 모델을 사용하세요.

키는 양식 하나만 작성하면 받을 수 있습니다

계정을 생성하고 키를 복사한 후 기본 URL을 변경하세요. 설정은 이것으로 끝입니다.

API 키 받기문서 읽기