2025. 09. 26.
FriendliAI의 완전관리형 모델 서빙 제품인 Friendli Dedicated Endpoints는 안정적이고 편리한 모델 서빙을 지원하며, 여러 기능 중 하나로 함수 호출 설정이 자동으로 감지·활성화되는 기능을 제공한다.
본 게시글은 FriendliAI와 직접적인 연관이 없습니다, 글에 내용과 설명 모두 개인적인 의견이며, FriendliAI의 의견을 대변하지 않습니다.
예를 들어, Qwen/Qwen3-30B-A3B
모델의 경우 아래 순서에 따라서 배포를 진행한 경우 자동으로 함수 호출이 가능한 엔드포인트가 배포되게 된다.
모델 선택 후 Features > Tool call: Supported 확인
배포 후 Endpoint Overview에서 Endpoint ID 확인 (e.g., dep0hyjaasjus3o
)
FRIENDLI_TOKEN
발급 후 API 요청으로 테스트 (강조된 부분을 주의하여 본인의 값으로 수정)
curl https://api.friendli.ai/dedicated/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $FRIENDLI_TOKEN" \
-d '{
"model": "dep0hyjaasjus3o",
"messages": [
{"role": "user", "content": "서울 날씨 알려줘"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "도시의 현재 날씨를 가져옴",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}
}
}
],
"tool_choice": "auto"
}' | jq .choices[].message
함수 호출 결과 확인
{
"content": "\n\n",
"reasoning_content": "\nOkay, the user is asking for the weather in Seoul. Let me check the tools available. There's a function called get_weather that takes a location parameter. Since the user mentioned \"서울\" which is Seoul, I need to call that function with the location set to Seoul. I should make sure the arguments are correctly formatted in JSON. Alright, the required parameter is location, so I'll structure the tool call accordingly.\n",
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"location\": \"서울\"}",
"name": "get_weather"
},
"id": "call_XIl0NYYMh9jRLhUdhF2OtTjE",
"type": "function"
}
]
}
이렇게 어떠한 추가적인 설정 없이 함수 호출이 가능한 엔드포인트를 배포할 수 있다는 것을 확인할 수 있다.
이러한 기능을 이용하다 보면, 일부 모델의 경우 도구 호출이 지원되지 않는다고 표시되는 경우가 있습니다. 알려진 일반적인 경우는 다음과 같습니다.
해당 모델의 chat template에 tool call
를 랜더링 하기 위한 로직이 없는 경우 (e.g., google/gemma-3-27b-it
)
모델에는 문제가 없지만, 엔진에서 지원되지 않는 유형의 도구 호출인 경우 (e.g., zai-org/GLM-4.5-Air
)
이러한 이유로 지원되지 않는다고 표시된다고 해서, Dedicated 에서 완전히 도구 호출을 사용하지 못한다는 의미는 아닙니다.
FDE에서는 chat template를 기반으로 tool call type를 자동으로 감지하고, 설정하기 때문에 chat template과 tool calling에 대한 약간에 이해만 있다면 사용자 정의 chat template를 이용하여 문제를 해결하거나 우회하여 도구 호출이 가능하도록 엔드포인트를 설정할 수 있습니다.
이러한 문제를 해결하기 위해 엔진에서 지원하는 형식의 tool call format으로 랜더링을 수행하는 chat template를 모델에 맞게 작성하는 과정을 수행해야합니다.
아래에서 설명하는 기능은 고급 기능으로, 의도치 않는 모델 함수 호출 성능 저하를 일으킬 수 있습니다.
우선, 기존의 모델 chat template를 분석하여, instruct, vision, reasoning를 위한 탬플릿 포맷을 정의합니다.
google/gemma-3-27b-it
처럼 기존에 탬플릿에 tool call을 위한 로직이 없는 경우, 그대로 사용하면 됩니다.
{{- bos_token -}}
{%- if messages[0]["role"] == "system" -%}
{%- if messages[0]["content"] is string -%}
{%- set first_user_prefix = messages[0]["content"] + "\n\n" -%}
{%- else -%}
{%- set first_user_prefix = messages[0]["content"][0]["text"] + "\n\n" -%}
{%- endif -%}
{%- set loop_messages = messages[1:] -%}
{%- else -%}
{%- set first_user_prefix = "" -%}
{%- set loop_messages = messages -%}
{%- endif -%}
{%- for message in loop_messages -%}
{%- if message["role"] == "user" != (loop.index0 % 2 == 0) -%}
{{- raise_exception("Conversation roles must alternate user/assistant/user/assistant/...") -}}
{%- endif -%}
{%- if message["role"] == "assistant" -%}
{%- set role = "model" -%}
{%- else -%}
{%- set role = message["role"] -%}
{%- endif -%}
{{- "<start_of_turn>" + role + "\n" + (first_user_prefix if loop.first else "") -}}
{%- if message["content"] is string -%}
{{- message["content"] | trim -}}
{%- elif message["content"] is iterable -%}
{%- for item in message["content"] -%}
{%- if item["type"] == "image" -%}
{{- "<start_of_image>" -}}
{%- elif item["type"] == "text" -%}
{{- item["text"] | trim -}}
{%- endif -%}
{%- endfor -%}
{%- else -%}
{{- raise_exception("Invalid content type") -}}
{%- endif -%}
{{- "<end_of_turn>\n" -}}
{%- endfor -%}
{%- if add_generation_prompt -%}
{{- "<start_of_turn>model\n" -}}
{%- endif -%}
{{- "[gMASK]<sop>" -}}
{%- macro visible_text(content) -%}
{%- if content is string -%}
{{- content -}}
{%- elif content is iterable and content is not mapping -%}
{%- for item in content -%}
{%- if item is mapping and item.type == "text" -%}
{{- item.text -}}
{%- elif item is string -%}
{{- item -}}
{%- endif -%}
{%- endfor -%}
{%- else -%}
{{- content -}}
{%- endif -%}
{%- endmacro -%}
{%- set ns = namespace(last_user_index=-1) -%}
{%- for m in messages -%}
{%- if m.role == "user" -%}
{%- set ns.last_user_index = loop.index0 -%}
{%- endif -%}
{%- endfor -%}
{%- for m in messages -%}
{%- if m.role == "user" -%}
{{- "<|user|>\n" -}}
{{- visible_text(m.content) -}}
{{- "/nothink" if enable_thinking is defined and not enable_thinking and not visible_text(m.content).endswith("/nothink") else "" -}}
{%- elif m.role == "assistant" -%}
{{- "<|assistant|>" -}}
{%- set reasoning_content = "" -%}
{%- set content = visible_text(m.content) -%}
{%- if m.reasoning_content is string -%}
{%- set reasoning_content = m.reasoning_content -%}
{%- elif "</think>" in content -%}
{%- set reasoning_content = content.split("</think>")[0].rstrip("\n").split("<think>")[-1].lstrip("\n") -%}
{%- set content = content.split("</think>")[-1].lstrip("\n") -%}
{%- endif -%}
{%- if loop.index0 > ns.last_user_index and reasoning_content -%}
{{- "\n<think>" + reasoning_content.strip() + "</think>" -}}
{%- else -%}
{{- "\n<think></think>" -}}
{%- endif -%}
{%- if content.strip() -%}
{{- "\n" + content.strip() -}}
{%- endif -%}
{%- elif m.role == "system" -%}
{{- "<|system|>\n" -}}
{{- visible_text(m.content) -}}
{%- endif -%}
{%- endfor -%}
{%- if add_generation_prompt -%}
{{- "<|assistant|>" -}}
{{- "\n<think></think>" if enable_thinking is defined and not enable_thinking else "" -}}
{%- endif -%}
{{- "[gMASK]<sop>" -}}
- {%- if tools -%}
- {{- "<|system|>\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>\n" -}}
- {%- for tool in tools -%}
- {{- tool | tojson(ensure_ascii=False) -}}
- {{- "\n" -}}
- {%- endfor -%}
- {{- "</tools>\n\nFor each function call, output the function name and arguments within the following XML format:\n<tool_call>{function-name}\n<arg_key>{arg-key-1}</arg_key>\n<arg_value>{arg-value-1}</arg_value>\n<arg_key>{arg-key-2}</arg_key>\n<arg_value>{arg-value-2}</arg_value>\n...\n</tool_call>" -}}
- {%- endif -%}
{%- macro visible_text(content) -%}
{%- if content is string -%}
{{- content -}}
{%- elif content is iterable and content is not mapping -%}
{%- for item in content -%}
{%- if item is mapping and item.type == "text" -%}
{{- item.text -}}
{%- elif item is string -%}
{{- item -}}
{%- endif -%}
{%- endfor -%}
{%- else -%}
{{- content -}}
{%- endif -%}
{%- endmacro -%}
{%- set ns = namespace(last_user_index=-1) -%}
{%- for m in messages -%}
{%- if m.role == "user" -%}
{%- set ns.last_user_index = loop.index0 -%}
{%- endif -%}
{%- endfor -%}
{%- for m in messages -%}
{%- if m.role == "user" -%}
{{- "<|user|>\n" -}}
{{- visible_text(m.content) -}}
{{- "/nothink" if enable_thinking is defined and not enable_thinking and not visible_text(m.content).endswith("/nothink") else "" -}}
{%- elif m.role == "assistant" -%}
{{- "<|assistant|>" -}}
{%- set reasoning_content = "" -%}
{%- set content = visible_text(m.content) -%}
{%- if m.reasoning_content is string -%}
{%- set reasoning_content = m.reasoning_content -%}
{%- elif "</think>" in content -%}
{%- set reasoning_content = content.split("</think>")[0].rstrip("\n").split("<think>")[-1].lstrip("\n") -%}
{%- set content = content.split("</think>")[-1].lstrip("\n") -%}
{%- endif -%}
{%- if loop.index0 > ns.last_user_index and reasoning_content -%}
{{- "\n<think>" + reasoning_content.strip() + "</think>" -}}
{%- else -%}
{{- "\n<think></think>" -}}
{%- endif -%}
{%- if content.strip() -%}
{{- "\n" + content.strip() -}}
{%- endif -%}
- {%- if m.tool_calls -%}
- {%- for tc in m.tool_calls -%}
- {%- if tc.function -%}
- {%- set tc = tc.function -%}
- {%- endif -%}
- {{- "\n<tool_call>" + tc.name -}}
- {{- "\n" -}}
- {%- set _args = tc.arguments -%}
- {%- for (k, v) in _args.items() -%}
- {{- "<arg_key>" -}}
- {{- k -}}
- {{- "</arg_key>\n<arg_value>" -}}
- {{- v | tojson(ensure_ascii=False) if v is not string else v -}}
- {{- "</arg_value>\n" -}}
- {%- endfor -%}
- {{- "</tool_call>" -}}
- {%- endfor -%}
- {%- endif -%}
- {%- elif m.role == "tool" -%}
- {%- if m.content is string -%}
- {%- if loop.first or messages[loop.index0 - 1].role != "tool" -%}
- {{- "<|observation|>" -}}
- {%- endif -%}
- {{- "\n<tool_response>\n" -}}
- {{- m.content -}}
- {{- "\n</tool_response>" -}}
- {%- else -%}
- {{- "<|observation|>" -}}
- {%- for tr in m.content -%}
- {{- "\n<tool_response>\n" -}}
- {{- tr.output if tr.output is defined else tr -}}
- {{- "\n</tool_response>" -}}
- {%- endfor -%}
- {%- endif -%}
{%- elif m.role == "system" -%}
{{- "<|system|>\n" -}}
{{- visible_text(m.content) -}}
{%- endif -%}
{%- endfor -%}
{%- if add_generation_prompt -%}
{{- "<|assistant|>" -}}
{{- "\n<think></think>" if enable_thinking is defined and not enable_thinking else "" -}}
{%- endif -%}
이렇게 함수 호출이 제거된 순수한 버전의 chat template를 생성한 후, 다시 tool call 관련된 랜더링 로직을 추가하는데, 이때 사용하는 로직은 엔진에서 지원하는 tool call format을 기반으로 하면 된다.
여러가지 유형이 있지만, 그 중 가장 일반적인 Hermes Function Calling Standard 포맷 등을 사용하면 된다.
{{- bos_token -}}
{%- if tools -%}
{%- if messages[0]["role"] == "system" -%}
{%- if messages[0]["content"] is string -%}
{%- set system_content = messages[0]["content"] -%}
{%- else -%}
{%- set system_content = messages[0]["content"][0]["text"] -%}
{%- endif -%}
{%- set first_user_prefix = system_content + "\n\n# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" -%}
{%- for tool in tools -%}
{%- set first_user_prefix = first_user_prefix + "\n" + (tool | tojson) -%}
{%- endfor -%}
{%- set first_user_prefix = first_user_prefix + "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>\n\n" -%}
{%- set loop_messages = messages[1:] -%}
{%- else -%}
{%- set first_user_prefix = "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" -%}
{%- for tool in tools -%}
{%- set first_user_prefix = first_user_prefix + "\n" + (tool | tojson) -%}
{%- endfor -%}
{%- set first_user_prefix = first_user_prefix + "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>\n\n" -%}
{%- set loop_messages = messages -%}
{%- endif -%}
{%- else -%}
{%- if messages[0]["role"] == "system" -%}
{%- if messages[0]["content"] is string -%}
{%- set first_user_prefix = messages[0]["content"] + "\n\n" -%}
{%- else -%}
{%- set first_user_prefix = messages[0]["content"][0]["text"] + "\n\n" -%}
{%- endif -%}
{%- set loop_messages = messages[1:] -%}
{%- else -%}
{%- set first_user_prefix = "" -%}
{%- set loop_messages = messages -%}
{%- endif -%}
{%- endif -%}
{%- for message in loop_messages -%}
{%- if message["role"] == "tool" -%}
{%- if loop.first or (loop_messages[loop.index0 - 1]["role"] != "tool") -%}
{{- "<start_of_turn>user\n" -}}
{%- endif -%}
{{- "<tool_response>\n" -}}
{%- if message["content"] is string -%}
{{- message["content"] | trim -}}
{%- else -%}
{{- message["content"][0]["text"] | trim -}}
{%- endif -%}
{{- "\n</tool_response>" -}}
{%- if loop.last or (loop_messages[loop.index0 + 1]["role"] != "tool") -%}
{{- "<end_of_turn>\n" -}}
{%- endif -%}
{%- elif message["role"] in ["user", "assistant"] -%}
{%- if message["role"] == "user" != ((loop.index0 - (loop_messages[:loop.index0] | selectattr("role", "equalto", "tool") | list | length)) % 2 == 0) -%}
{{- raise_exception("Conversation roles must alternate user/assistant/user/assistant/... (excluding tool messages)") -}}
{%- endif -%}
{%- if message["role"] == "assistant" -%}
{%- set role = "model" -%}
{%- else -%}
{%- set role = message["role"] -%}
{%- endif -%}
{{- "<start_of_turn>" + role + "\n" + (first_user_prefix if loop.first and role == "user" else "") -}}
{%- if message["content"] is string -%}
{{- message["content"] | trim -}}
{%- elif message["content"] is iterable -%}
{%- for item in message["content"] -%}
{%- if item["type"] == "image" -%}
{{- "<start_of_image>" -}}
{%- elif item["type"] == "text" -%}
{{- item["text"] | trim -}}
{%- endif -%}
{%- endfor -%}
{%- else -%}
{{- raise_exception("Invalid content type") -}}
{%- endif -%}
{%- if message["role"] == "assistant" and message.get("tool_calls") -%}
{%- for tool_call in message["tool_calls"] -%}
{{- "\n" -}}
{%- if tool_call.get("function") -%}
{%- set tool_call = tool_call["function"] -%}
{%- endif -%}
{{- "<tool_call>\n{\"name\": \"" -}}
{{- tool_call["name"] -}}
{{- "\", \"arguments\": " -}}
{%- if tool_call["arguments"] is string -%}
{{- tool_call["arguments"] -}}
{%- else -%}
{{- tool_call["arguments"] | tojson -}}
{%- endif -%}
{{- "}\n</tool_call>" -}}
{%- endfor -%}
{%- endif -%}
{{- "<end_of_turn>\n" -}}
{%- endif -%}
{%- endfor -%}
{%- if add_generation_prompt -%}
{{- "<start_of_turn>model\n" -}}
{%- endif -%}
{{- "[gMASK]<sop>" -}}
{# --- Tools Definition Block from the new template --- #}
{%- if tools %}
{{- '<|system|>\n' }}
{%- if messages[0].role == 'system' %}
{{- messages[0].content + '\n\n' }}
{%- endif %}
{{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
{%- for tool in tools %}
{{- "\n" }}
{{- tool | tojson }}
{%- endfor %}
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>" }}
{%- else %}
{%- if messages[0].role == 'system' %}
{{- '<|system|>\n' + messages[0].content }}
{%- endif %}
{%- endif %}
{# --- Main message loop structure from the original template --- #}
{%- macro visible_text(content) -%}
{%- if content is string -%}
{{- content -}}
{%- elif content is iterable and content is not mapping -%}
{%- for item in content -%}
{%- if item is mapping and item.type == "text" -%}
{{- item.text -}}
{%- elif item is string -%}
{{- item -}}
{%- endif -%}
{%- endfor -%}
{%- else -%}
{{- content -}}
{%- endif -%}
{%- endmacro -%}
{%- set ns = namespace(last_user_index=-1) -%}
{%- for m in messages -%}
{%- if m.role == "user" -%}
{%- set ns.last_user_index = loop.index0 -%}
{%- endif -%}
{%- endfor -%}
{%- for m in messages -%}
{%- if m.role == "user" -%}
{{- "<|user|>\n" -}}
{{- visible_text(m.content) -}}
{{- "/nothink" if enable_thinking is defined and not enable_thinking and not visible_text(m.content).endswith("/nothink") else "" -}}
{%- elif m.role == "assistant" -%}
{{- "<|assistant|>" -}}
{%- set reasoning_content = "" -%}
{%- set content = visible_text(m.content) -%}
{%- if m.reasoning_content is string -%}
{%- set reasoning_content = m.reasoning_content -%}
{%- elif "</think>" in content -%}
{%- set reasoning_content = content.split("</think>")[0].rstrip("\n").split("<think>")[-1].lstrip("\n") -%}
{%- set content = content.split("</think>")[-1].lstrip("\n") -%}
{%- endif -%}
{%- if loop.index0 > ns.last_user_index and reasoning_content -%}
{{- "\n<think>" + reasoning_content.strip() + "</think>" -}}
{%- else -%}
{{- "\n<think></think>" -}}
{%- endif -%}
{%- if content.strip() -%}
{{- "\n" + content.strip() -}}
{%- endif -%}
{# --- Tool Call Rendering Block from the new template --- #}
{%- if m.tool_calls -%}
{%- for tool_call in m.tool_calls -%}
{%- if (loop.first and content.strip()) or (not loop.first) %}
{{- '\n' }}
{%- endif %}
{%- if tool_call.function -%}
{%- set tool_call = tool_call.function -%}
{%- endif %}
{{- '<tool_call>\n{"name": "' }}
{{- tool_call.name }}
{{- '", "arguments": ' }}
{%- if tool_call.arguments is string %}
{{- tool_call.arguments }}
{%- else %}
{{- tool_call.arguments | tojson }}
{%- endif %}
{{- '}\n</tool_call>' }}
{%- endfor %}
{%- endif -%}
{# --- Tool Response Rendering Block from the new template --- #}
{%- elif m.role == "tool" -%}
{{- '\n<tool_response>\n' }}
{{- visible_text(m.content) }}
{{- '\n</t' + 'ool_response>' }}
{%- elif m.role == "system" and not loop.first -%}
{{- "<|system|>\n" -}}
{{- visible_text(m.content) -}}
{%- endif -%}
{%- endfor -%}
{# --- Generation prompt from the original template --- #}
{%- if add_generation_prompt -%}
{{- "<|assistant|>" -}}
{{- "\n<think></think>" if enable_thinking is defined and not enable_thinking else "" -}}
{%- endif -%}
{{- "[gMASK]<sop>" -}}
+ {# --- Tools Definition Block from the new template --- #}
+ {%- if tools %}
+ {{- '<|system|>\n' }}
+ {%- if messages[0].role == 'system' %}
+ {{- messages[0].content + '\n\n' }}
+ {%- endif %}
+ {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
+ {%- for tool in tools %}
+ {{- "\n" }}
+ {{- tool | tojson }}
+ {%- endfor %}
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>" }}
+ {%- else %}
+ {%- if messages[0].role == 'system' %}
+ {{- '<|system|>\n' + messages[0].content }}
+ {%- endif %}
+ {%- endif %}
+
+ {# --- Main message loop structure from the original template --- #}
{%- macro visible_text(content) -%}
{%- if content is string -%}
{{- content -}}
{%- elif content is iterable and content is not mapping -%}
{%- for item in content -%}
{%- if item is mapping and item.type == "text" -%}
{{- item.text -}}
{%- elif item is string -%}
{{- item -}}
{%- endif -%}
{%- endfor -%}
{%- else -%}
{{- content -}}
{%- endif -%}
{%- endmacro -%}
{%- set ns = namespace(last_user_index=-1) -%}
{%- for m in messages -%}
{%- if m.role == "user" -%}
{%- set ns.last_user_index = loop.index0 -%}
{%- endif -%}
{%- endfor -%}
{%- for m in messages -%}
{%- if m.role == "user" -%}
{{- "<|user|>\n" -}}
{{- visible_text(m.content) -}}
{{- "/nothink" if enable_thinking is defined and not enable_thinking and not visible_text(m.content).endswith("/nothink") else "" -}}
{%- elif m.role == "assistant" -%}
{{- "<|assistant|>" -}}
{%- set reasoning_content = "" -%}
{%- set content = visible_text(m.content) -%}
{%- if m.reasoning_content is string -%}
{%- set reasoning_content = m.reasoning_content -%}
{%- elif "</think>" in content -%}
{%- set reasoning_content = content.split("</think>")[0].rstrip("\n").split("<think>")[-1].lstrip("\n") -%}
{%- set content = content.split("</think>")[-1].lstrip("\n") -%}
{%- endif -%}
{%- if loop.index0 > ns.last_user_index and reasoning_content -%}
{{- "\n<think>" + reasoning_content.strip() + "</think>" -}}
{%- else -%}
{{- "\n<think></think>" -}}
{%- endif -%}
{%- if content.strip() -%}
{{- "\n" + content.strip() -}}
{%- endif -%}
+ {# --- Tool Call Rendering Block from the new template --- #}
+ {%- if m.tool_calls -%}
+ {%- for tool_call in m.tool_calls -%}
+ {%- if (loop.first and content.strip()) or (not loop.first) %}
+ {{- '\n' }}
+ {%- endif %}
+ {%- if tool_call.function -%}
+ {%- set tool_call = tool_call.function -%}
+ {%- endif %}
+ {{- '<tool_call>\n{"name": "' }}
+ {{- tool_call.name }}
+ {{- '", "arguments": ' }}
+ {%- if tool_call.arguments is string %}
+ {{- tool_call.arguments }}
+ {%- else %}
+ {{- tool_call.arguments | tojson }}
+ {%- endif %}
+ {{- '}\n</tool_call>' }}
+ {%- endfor %}
+ {%- endif -%}
+ {# --- Tool Response Rendering Block from the new template --- #}
+ {%- elif m.role == "tool" -%}
+ {{- '\n<tool_response>\n' }}
+ {{- visible_text(m.content) }}
+ {{- '\n</t' + 'ool_response>' }}
- {%- elif m.role == "system" -%}
+ {%- elif m.role == "system" and not loop.first -%}
{{- "<|system|>\n" -}}
{{- visible_text(m.content) -}}
{%- endif -%}
{%- endfor -%}
+ {# --- Generation prompt from the original template --- #}
{%- if add_generation_prompt -%}
{{- "<|assistant|>" -}}
{{- "\n<think></think>" if enable_thinking is defined and not enable_thinking else "" -}}
{%- endif -%}
위와 같은 방식으로 탬플릿의 tool call logic를 지원되는 형식으로 강제로 변환하고, 탬플릿에 있는 프롬포트를 조정하여, 해당 모델과 잘 동작하는 탬플릿을 생성하면 된다.
이렇게 수정한 탬플릿을 이용하여 배포하기 위해서는 다음과 같은 과정을 거쳐야한다.
모델 선택 후 not supported 확인
"Custom chat template" 선택 후 작성한 chat template 붙혀넣기
"Override" 선택 후 tool call 정보가 "unknown" 으로 나오는지 확인
배포 후 Overview에서 tool call이 "supported"로 변경되었는지 확인
이전과 동일하게 API 호출을 통해 테스트
curl https://api.friendli.ai/dedicated/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $FRIENDLI_TOKEN" \
-d '{
"model": "depp771mey8y1se",
"messages": [
{"role": "user", "content": "서울 날씨 알려줘"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "도시의 현재 날씨를 가져옴",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"}
},
"required": ["location"]
}
}
}
],
"tool_choice": "auto"
}' | jq .choices[].message
함수 호출 결과 확인
{
"content": "\n\n서울의 현재 날씨를 확인해 드리겠습니다.\n",
"reasoning_content": "사용자가 서울의 날씨를 알려달라고 요청했습니다. 제가 가진 `get_weather` 함수를 사용할 수 있습니다. 이 함수는 location 파라미터가 필요하며, 사용자가 \"서울\"이라는 위치를 명시적으로 언급했습니다.\n\n함수를 호출할 때 \"서울\"을 location 파라미터로 전달해야 합니다.",
"role": "assistant",
"tool_calls": [
{
"function": {
"arguments": "{\"location\": \"서울\"}",
"name": "get_weather"
},
"id": "call_oaNZe3tgKLLgv5aUMFQNBTRr",
"type": "function"
}
]
}
이렇게 기존에 not supported라고 나온 glm-4.5-air 모델에서도 tool call이 가능해졌음을 확인할 수 있다.
이를 응용하면 아직 지원되지 않는 모델들에 대해서도 Dedicated Endpoint를 통해 tool call을 가능하게 할 수 있다. (무려 함수 호출 학습을 하지 않는 mistralai/Mistral-7B-Instruct-v0.1
같은 모델에서도 어느 정도 준수한 성능을 보여준다)
Friendli의 Dedicated Endpoint를 사용하여 모델을 배포하는 과정은 매우 간단하고, 자동으로 도구 호출 설정이 지원되는 경우 아무런 노력 없이 도구 호출이 지원되어 Agentic workflow를 지원할 수 있다는 것을 알 수 있다.
또한 도구 호출을 아직 지원하지 않거나, 모델이 도구 호출을 지원하지 않더라고 Custom chat template 기능을 이용하여 직접 chat template를 작성하고 도구 호출을 메뉴얼 하게 활성화 할 수 있다는 사실을 알 수 있었다.