# 툴 호출, 처음부터 끝까지 > tool_choice "auto"로 둔 왕복 한 번입니다. 툴을 요청받아 실행하고 결과를 돌려보냅니다. Source: https://kiln.bricksum.com/docs/ko/cookbook/tool-calling tool_choice를 "auto"로 둔 왕복 한 번입니다. 모델이 툴을 요청하면 코드가 실행하고, 그 결과를 tool 메시지로 돌려보내 최종 답을 받습니다. 이 레시피는 auto만 다룹니다. 툴 호출 여부는 모델이 결정하며, 강제 tool_choice는 다루지 않습니다. ## 빠른 시작 **Python** ```python import json, os from openai import OpenAI client = OpenAI(base_url="https://api.bricksum.com/v1", api_key=os.environ["KILN_API_KEY"]) tools = [{"type": "function", "function": { "name": "get_weather", "description": "Current weather for a city", # required "parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}, }}] messages = [{"role": "user", "content": "What's the weather in Seoul?"}] first = client.chat.completions.create(model="qwen3-32b", messages=messages, tools=tools, tool_choice="auto", max_tokens=800) choice = first.choices[0] # The model may answer without calling the tool, so this is a check, not an index. if not choice.message.tool_calls: print(f'no tool call, finish_reason: "{choice.finish_reason}"') print("answer:", choice.message.content) else: # One reply can carry several tool calls; answer every one of them. messages.append(choice.message) for call in choice.message.tool_calls: print("tool call:", call.function.name, call.function.arguments) messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps({"temp_c": 27})}) final = client.chat.completions.create(model="qwen3-32b", messages=messages, max_tokens=800) print("final:", final.choices[0].message.content) ``` **TypeScript** ```javascript import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.bricksum.com/v1", apiKey: process.env.KILN_API_KEY }); const tools: OpenAI.ChatCompletionTool[] = [{ type: "function", function: { name: "get_weather", description: "Current weather for a city", // required parameters: { type: "object", properties: { city: { type: "string" } }, required: ["city"] }, }}]; const messages: OpenAI.ChatCompletionMessageParam[] = [ { role: "user", content: "What's the weather in Seoul?" }, ]; const first = await client.chat.completions.create({ model: "qwen3-32b", messages, tools, tool_choice: "auto", max_tokens: 800 }); // The model may answer without calling the tool, so this is a check, not an index. const calls = first.choices[0].message.tool_calls ?? []; if (calls.length === 0) { console.log('no tool call, finish_reason: "' + first.choices[0].finish_reason + '"'); console.log("answer:", first.choices[0].message.content); } else { // One reply can carry several tool calls; answer every one of them. messages.push(first.choices[0].message); for (const call of calls) { if (call.type !== "function") continue; console.log("tool call:", call.function.name, call.function.arguments); messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify({ temp_c: 27 }) }); } const final = await client.chat.completions.create({ model: "qwen3-32b", messages, max_tokens: 800 }); console.log("final:", final.choices[0].message.content); } ``` ## 확인 스크립트를 실행하세요. 첫 번째 호출의 message.tool_calls에는 city를 담은 JSON 인자와 함께 get_weather가 들어 있습니다(항목이 여러 개일 수 있으며 각각에 tool 메시지를 보냅니다). 두 번째 호출의 message.content는 모델이 직접 작성한 문장이라 실행할 때마다 표현이 달라지지만, 전달한 온도(27)를 언급합니다. **expected output** ```text tool call: get_weather { "city": "Seoul" } final: The current temperature in Seoul is **27 °C**. ``` ## 설정 참고 - `tool_choice` = `"auto"` — 모델이 툴 호출 여부를 결정합니다. "required"나 함수를 지정하는 강제 선택은 지원하지 않습니다. qwen3-32b에서는 강제 툴 선택이 명확한 에러 없이 실패합니다(2026-09-26 실측). tool_choice를 "required"로 보내면 본문이 평문 "Internal Server Error"인 HTTP 500이 오고, 함수를 지정하면 arguments가 빈 200(finish_reason "stop")이 옵니다. 둘 다 4xx가 아니므로 재시도로 해결되지 않습니다. tool_choice는 "auto"만 보내세요. - `tools[].function.description` = `required` — 모든 툴에 필요합니다. 없으면 400으로 거부됩니다. - `parallel_tool_calls` = `not applied` — 모델 페이지에서 병렬 툴 호출이 지원으로 표시된 모델에서만 적용되며, qwen3-32b는 해당하지 않습니다. gpt-oss-120b 실측에서는 200으로 수락되고, 모델이 응답마다 툴을 최대 하나만 호출했습니다. qwen3-32b에서는 parallel_tool_calls 설정이 적용되지 않습니다. 이는 응답에 툴 호출이 하나만 온다는 뜻이 아닙니다. parallel_tool_calls: false를 보내도 한 응답에 툴 호출이 여러 건 올 수 있습니다. tool_calls의 모든 항목에 각각 tool 메시지로 답하세요. - `max_tokens` = `800` — reasoning 토큰도 이 값 안에서 과금됩니다. 값이 작으면 tool_calls가 나오기 전에 finish_reason이 "length"로 끝날 수 있습니다. ## 제한 사항 - 강제 tool_choice("required" 또는 특정 함수 지정)는 지원하지 않습니다. 이 레시피는 "auto"만 다룹니다. qwen3-32b에서는 강제 툴 선택이 명확한 에러 없이 실패합니다(2026-09-26 실측). tool_choice를 "required"로 보내면 본문이 평문 "Internal Server Error"인 HTTP 500이 오고, 함수를 지정하면 arguments가 빈 200(finish_reason "stop")이 옵니다. 둘 다 4xx가 아니므로 재시도로 해결되지 않습니다. tool_choice는 "auto"만 보내세요. - qwen3-32b에서는 parallel_tool_calls 설정이 적용되지 않습니다. 이는 응답에 툴 호출이 하나만 온다는 뜻이 아닙니다. parallel_tool_calls: false를 보내도 한 응답에 툴 호출이 여러 건 올 수 있습니다. tool_calls의 모든 항목에 각각 tool 메시지로 답하세요. ## 문제 해결 - `HTTP 500 Internal Server Error` — 요청의 tool_choice가 "required"입니다. qwen3-32b에서는 4xx 대신 평문 500으로 실패합니다(2026-09-26 실측). tool_choice를 "auto"로 보내세요. 같은 요청을 재시도해도 같은 500이 옵니다. - `"arguments": ""` — 요청이 tool_choice에서 함수를 지정했습니다. qwen3-32b에서는 arguments가 빈 200(finish_reason "stop")이 와서(2026-09-26 실측) JSON.parse / json.loads가 실패합니다. tool_choice를 "auto"로 보내고 어떤 툴을 기대하는지 프롬프트에 설명하세요. arguments를 파싱하기 전에 tool_calls를 확인하세요. - `"message": "missing or malformed API key"` — KILN_API_KEY가 설정되지 않았거나 만료됐거나, 스크립트를 실행하는 셸에 export되지 않았습니다. 콘솔 → API 키에서 새 키를 발급하고 다시 export하세요. - `"code": "model_not_found"` — model 필드가 내 키로 GET /v1/models를 호출했을 때 반환되는 ID가 아닙니다. 모델 페이지나 GET /v1/models에서 목록을 확인하고, 그 ID 중 하나(예: "qwen3-32b")로 model을 설정하세요. - `finish_reason: "length"` — 모델이 툴 호출 여부를 판단하는 reasoning을 끝내기 전에 max_tokens에 도달했습니다. 퀵스타트가 보내는 800보다 크게, 예를 들어 1600으로 첫 번째 create() 호출의 max_tokens를 올리세요. 참고 이 페이지의 스니펫은 2026-09-21에 openai 3.16.2 (Python 3.14.7), openai 7.20.0 (Node 24.12.0) 환경에서 Kiln API로 직접 실행해 확인했습니다. 확인하지 않은 부분: 이 실행은 gpt-oss-120b에서 했습니다. 페이지는 이제 qwen3-32b을 사용하며, 이 모델로는 아직 실행하지 않아 아래 예상 출력과 다를 수 있습니다. 두 퀵스타트 탭의 툴 미호출 분기는 이 실행 이후에 추가됐으며, 스테이징이 아닌 로컬 스텁 서버로만 확인했습니다. 모든 툴 호출에 답하는 반복문은 그 뒤에 추가됐으며(#760) API로 실행하지 않았습니다.