Microsoft AI-103

Microsoft AI-103 시험 노트개념 정리18 MIN

LLM·소형·코드·멀티모달 모델 배포와 호출

AI-103 생성형 AI·에이전트 도메인의 첫 노트입니다. Foundry에서 모델을 배포하는 순서, 대화 완성 호출의 요청과 응답, 소형·코드·멀티모달 모델의 쓰임, 스트리밍, 구조화 출력과 함수 스키마의 기초를 정리합니다.

도메인이 다시 바뀝니다. 앞의 네 노트가 데이터를 들여와 찾고 뽑는 일이었다면, 여기부터는 모델을 불러 생성하는 일입니다. 시험에서 가장 비중이 큰 도메인이고 그 첫 자리가 기본기입니다 — 배포를 만들고, 요청을 보내고, 응답을 읽는 것. 배포 옵션의 종류와 과금은 인프라 노트에서 봤으니 여기서는 손이 가는 순서와 호출의 모양만 봅니다.

모델 배포

배포 단위

배포는 카탈로그의 모델 하나를 우리 리소스에서 부를 수 있게 이름을 붙여 세운 것입니다. 앱은 모델 이름이 아니라 배포 이름으로 부릅니다. 그래서 같은 모델을 이름을 달리해 둘 배포할 수 있고, 배포 이름은 그대로 두고 뒤의 모델 버전만 갈아 끼울 수도 있습니다. 배포마다 배포 유형(표준·프로비저닝드 등)과 분당 토큰 한도가 붙고, 한도는 리소스 전체 할당량에서 떼어 나눕니다.

배포 순서

포털에서는 이 순서입니다.

  1. Foundry 포털에서 프로젝트를 연다.
  2. 모델 카탈로그에서 모델을 고르고 「배포」를 누른다.
  3. 배포 이름, 배포 유형, 모델 버전과 업데이트 정책을 정한다.
  4. 분당 토큰 한도를 정하고 콘텐츠 필터를 고른다.
  5. 배포가 끝나면 플레이그라운드에서 한 번 불러 보고 엔드포인트를 확인한다.

카탈로그 모델은 대개 Foundry 리소스에 바로 배포해 토큰 단위로 쓰고, 일부 공개 가중치 모델은 전용 컴퓨트에 올릴 수도 있습니다. 「GPU 인프라를 관리하지 않고 쓰고 싶다」면 토큰 과금 배포, 「가중치를 우리 쪽에 두고 격리해야 한다」면 관리형 컴퓨트입니다.

대화 완성 호출

메시지 역할

대화 완성(chat completion)은 메시지 목록을 보내고 다음 메시지 하나를 받는 호출입니다. 메시지마다 역할이 붙습니다. 시스템 메시지는 모델이 지킬 규칙과 말투, 사용자 메시지는 질문, 어시스턴트 메시지는 앞서 모델이 한 답, 도구 메시지는 함수를 실행한 결과입니다. 모델은 이전 호출을 기억하지 않으므로 대화를 이어 가려면 앞의 메시지를 매번 다시 보내야 합니다 — 대화가 길어질수록 입력 토큰이 늘어나는 까닭입니다.

from azure.identity import DefaultAzureCredential
from azure.ai.projects import AIProjectClient

project = AIProjectClient(
    endpoint="https://my-foundry.services.ai.azure.com/api/projects/support",
    credential=DefaultAzureCredential(),
)
client = project.get_openai_client()

reply = client.chat.completions.create(
    model="chat-main",                      # 배포 이름
    messages=[
        {"role": "system", "content": "당신은 반품 상담원입니다. 규정에 없는 것은 모른다고 답하세요."},
        {"role": "user", "content": "해외 배송 상품도 반품할 수 있나요?"},
    ],
    temperature=0.2,
    max_tokens=300,
)
choice = reply.choices[0]
print(choice.finish_reason, reply.usage.prompt_tokens, reply.usage.completion_tokens)
print(choice.message.content)

생성 매개변수

매개변수 하는 일
temperature 낮을수록 같은 입력에 같은 답에 가깝다. 사실 응답은 낮게, 아이디어는 높게
top_p 확률이 높은 낱말부터 누적 p까지만 후보로 둔다. temperature와 둘 중 하나만 조정한다
max_tokens 출력 토큰 상한. 비용과 지연의 직접 손잡이
stop 이 문자열이 나오면 생성을 멈춘다
seed 같은 값이면 재현에 가깝다. 보장은 아니다

추론 모델은 답 전에 내부 추론 토큰을 쓰므로 출력 상한을 max_completion_tokens로 받고, 그 상한에 추론 토큰이 함께 셈해집니다. 상한을 너무 낮게 잡으면 추론만 하다가 답을 못 내고 끝납니다.

응답 구조

응답에서 볼 것은 셋입니다. 생성된 메시지, 왜 멈췄는지를 알려 주는 finish_reason, 그리고 입력·출력 토큰 수인 usage입니다. finish_reason이 stop이면 자연스럽게 끝난 것이고, length면 상한에 걸려 잘린 것, content_filter면 필터가 막은 것, tool_calls면 모델이 함수를 불러 달라고 요청한 것입니다. 「답이 문장 중간에서 끊긴다」는 length이고 손볼 곳은 출력 상한입니다.

모델 고르기

소형 언어 모델

소형 언어 모델(SLM)은 파라미터가 수십억 개 안팎으로 작아 더 싸고 빠르며, 기기나 컨테이너에 내려 돌릴 수도 있는 모델입니다. 분류·추출·짧은 요약처럼 범위가 좁고 형식이 정해진 일에 맞고, 여러 단계를 추론하거나 넓은 지식이 필요한 일에는 큰 모델이 낫습니다. 「하루 수백만 건의 짧은 문의를 다섯 범주로 나눈다, 지연과 비용이 최우선이다」는 SLM, 「복잡한 계약 조항의 충돌을 따진다」는 큰 모델입니다.

코드 모델

코드 모델은 코드 생성·설명·변환·리뷰에 맞춰 학습된 모델입니다. 같은 대화 완성 호출로 부르지만, 시스템 메시지에 언어와 스타일 규칙을 적고 출력 형식을 코드 블록 하나로 제한하는 식으로 다룹니다. 「모델이 만든 SQL을 운영 DB에 바로 실행한다」 같은 보기는 대개 오답입니다 — 생성한 코드는 실행 전에 검사합니다.

멀티모달 입력

이미지

멀티모달 모델에 이미지를 보낼 때는 사용자 메시지의 content를 문자열 대신 조각의 배열로 보냅니다. 텍스트 조각과 이미지 조각을 섞어 넣고, 이미지는 공개 URL이나 base64로 인코딩한 데이터 URL로 줍니다.

import base64

with open("damaged_box.jpg", "rb") as f:
    b64 = base64.b64encode(f.read()).decode()

reply = client.chat.completions.create(
    model="vision-main",
    messages=[{
        "role": "user",
        "content": [
            {"type": "text", "text": "상자 파손 정도를 상·중·하로 판정하고 근거를 한 문장으로 적어 주세요."},
            {"type": "image_url",
             "image_url": {"url": f"data:image/jpeg;base64,{b64}", "detail": "low"}},
        ],
    }],
)

detail은 이미지를 얼마나 자세히 볼지입니다. low는 작게 줄여 보므로 입력 토큰이 적고, high는 잘게 나눠 보므로 작은 글자나 세부까지 읽는 대신 토큰이 많이 듭니다. 파손 여부처럼 전체 인상이면 low, 라벨의 작은 글씨를 읽어야 하면 high입니다.

오디오

오디오 입력을 받는 모델에는 소리를 입력 조각으로 함께 보낼 수 있습니다. 다만 녹음 수천 건을 텍스트로 옮기는 일이라면 대화 모델보다 음성 인식 서비스나 전사 전용 모델이 싸고 정확합니다. 「녹음을 듣고 고객의 감정과 요청을 함께 판단한다」처럼 소리 자체의 정보(말투·억양)가 필요할 때가 오디오 입력의 자리입니다.

스트리밍

청크

stream=True로 부르면 응답이 다 만들어질 때까지 기다리지 않고 생성되는 대로 조각(chunk)이 옵니다. 조각마다 delta에 새로 생긴 텍스트가 들어 있어 이어 붙이면 전체 답이 됩니다. 전체 생성 시간은 같지만 첫 글자가 보이는 시간이 크게 줄어듭니다. 첫 토큰까지 0.6초, 초당 50토큰으로 300토큰을 내는 응답이면 스트리밍 없이는 0.6 + 300 ÷ 50 = 6.6초를 빈 화면으로 기다리고, 스트리밍이면 0.6초 뒤부터 글자가 흐릅니다.

stream = client.chat.completions.create(
    model="chat-main",
    messages=[{"role": "user", "content": "반품 절차를 단계별로 알려 주세요."}],
    stream=True,
    stream_options={"include_usage": True},
)
for chunk in stream:
    if chunk.choices and chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="", flush=True)
    if chunk.usage:
        print("\n토큰:", chunk.usage.total_tokens)

사용량과 필터

스트리밍에서는 토큰 사용량이 조각마다 오지 않으므로, 집계가 필요하면 마지막 조각에 사용량을 실어 달라고 따로 요청합니다(include_usage). 사용량만 실린 마지막 조각에는 선택지가 없어 위 조각이 chunk.choices부터 확인합니다. 콘텐츠 필터도 스트리밍 중에 돌기 때문에, 이미 보낸 조각 뒤에서 필터가 걸려 응답이 중간에 끊길 수 있습니다. 앱은 finish_reason을 마지막에 확인해 끊긴 이유를 사용자에게 알려야 합니다.

구조화 출력과 함수 스키마

JSON 강제

모델의 답을 프로그램이 읽어야 하면 형식을 강제합니다. JSON 모드는 응답이 유효한 JSON이라는 것만 보장하고, 구조화 출력은 우리가 준 JSON 스키마에 맞는 필드와 타입까지 보장합니다. 「주문번호·사유·환불 금액을 정해진 키로 받아 바로 DB에 넣는다」면 스키마를 주는 구조화 출력이 답이고, JSON 모드만으로는 키 이름이 바뀌거나 필드가 빠질 수 있습니다.

reply = client.chat.completions.create(
    model="chat-main",
    messages=[{"role": "user", "content": "주문 A-1042 반품 원해요. 사이즈가 안 맞아요. 39,000원 결제했어요."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "return_request",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "order_id": {"type": "string"},
                    "reason": {"type": "string", "enum": ["size", "damage", "change_of_mind"]},
                    "amount": {"type": "integer"},
                },
                "required": ["order_id", "reason", "amount"],
                "additionalProperties": False,
            },
        },
    },
)

함수 도구

함수 도구는 모델이 직접 실행하지 않고 「이 함수를 이 인자로 불러 달라」고 요청하게 하는 장치입니다. 요청에 함수 이름·설명·인자의 JSON 스키마를 tools로 실어 보내면, 모델은 필요할 때 finish_reason이 tool_calls인 응답으로 호출할 함수와 인자를 냅니다. 앱이 그 함수를 실행해 결과를 도구 메시지로 붙여 다시 보내면 모델이 그 결과로 답을 마칩니다. 도구 메시지에는 어느 요청에 대한 결과인지 가리키는 호출 id를 반드시 함께 적습니다.

tool_choice로 이 동작을 조정합니다. auto는 모델이 판단하고, none은 도구를 안 쓰게 하며, 특정 함수를 지정하면 그 함수를 반드시 부르게 합니다. 설명이 모호하면 모델이 엉뚱한 때 부르므로 「언제 부르는가」를 설명에 적습니다.

연습 문제

  1. 같은 모델 버전을 개발용과 운영용으로 따로 한도를 두고 쓰려 합니다. 알맞은 구성은?
    ① 모델 이름으로 부르고 요청마다 한도를 적는다
    ② 이름이 다른 배포 둘을 만들어 한도를 따로 준다
    ③ 시스템 메시지에 환경 이름을 적는다
    ④ 하나의 배포에서 seed로 갈라 쓴다
    ②. 앱은 배포 이름으로 부르고 한도는 배포마다 붙습니다. 같은 모델이라도 배포를 나누면 한쪽 부하가 다른 쪽 한도를 먹지 않습니다.
  2. 응답이 자주 문장 중간에서 끊기고 finish_reason이 length입니다. 고칠 곳은?
    ① temperature를 낮춘다
    ② 출력 토큰 상한을 늘리거나 답을 짧게 하라고 지시한다
    ③ 콘텐츠 필터를 끈다
    ④ 스트리밍을 켠다
    ②. length는 상한에 걸려 잘렸다는 뜻입니다. 스트리밍은 보여 주는 방식만 바꿀 뿐 상한은 그대로입니다.
  3. 첫 토큰까지 0.8초, 초당 40토큰으로 출력 400토큰을 내는 응답입니다. 스트리밍 없이 사용자가 답을 처음 보기까지와, 스트리밍으로 첫 글자를 보기까지의 시간은?
    ① 10.8초와 0.8초
    ② 10초와 0.8초
    ③ 10.8초와 10.8초
    ④ 0.8초와 10.8초
    ①. 전체는 0.8 + 400 ÷ 40 = 10.8초이고 스트리밍 없이는 그때 한꺼번에 보입니다. 스트리밍이면 첫 토큰이 나오는 0.8초부터 보입니다.
  4. 응답을 받아 order_id·reason·amount 키로 바로 DB에 넣어야 합니다. 키가 빠지면 안 됩니다. 알맞은 설정은?
    ① JSON 모드
    ② strict 스키마를 준 구조화 출력
    ③ temperature 0
    ④ 시스템 메시지에 「JSON으로 답하라」를 적는다
    ②. JSON 모드와 지시문은 유효한 JSON까지만 기대할 수 있고 키와 타입은 보장하지 않습니다.
  5. 모델이 tool_calls로 get_order(order_id="A-1042")를 요청했습니다. 앱이 다음에 할 일은?
    ① 같은 요청을 다시 보낸다
    ② 함수를 실행하고 결과를 그 호출 id를 단 도구 메시지로 붙여 다시 보낸다
    ③ 함수 결과를 사용자 메시지로 바꿔 보낸다
    ④ 모델이 함수를 실행할 때까지 기다린다
    ②. 모델은 함수를 실행하지 않습니다. 앱이 실행하고 결과를 호출 id와 함께 도구 메시지로 돌려줘야 모델이 답을 마칩니다.
  6. 제품 라벨 사진에서 작은 글씨의 제조 번호를 읽어야 하는데 모델이 자주 틀립니다. 이미지는 detail: low로 보냈습니다. 먼저 바꿀 것은?
    ① detail을 high로 올린다
    ② temperature를 올린다
    ③ 스트리밍을 켠다
    ④ stop 문자열을 지정한다
    ①. low는 이미지를 줄여 보므로 작은 글씨가 뭉개집니다. 세부를 읽어야 하면 토큰을 더 내고 high로 봅니다.

이 노트의 문항은 대부분 응답의 어느 칸을 보면 되는가로 풀립니다. 잘렸으면 finish_reason, 비싸면 usage, 함수를 부르려 하면 tool_calls이고, 그 칸을 바꾸는 손잡이가 요청 쪽 어디에 있는지를 짝지어 두면 됩니다.

Microsoft AI-103 시험 노트 전체 보기