LM Studio를 사용하여 자동 도구 호출 Granite 구축

작성자

Caleb Palappillil

Open-source AI Software Developer Intern

Erika Russi

Data Scientist

IBM

이 튜토리얼에서는 로컬 머신에서 오픈 소스 IBM Granite 3.3-8b 명령 모델로 LM Studio를 사용합니다. 처음에는 로컬 모델을 있는 그대로 테스트한 다음 모델이 자동 도구 호출에 사용할 수 있는 Python 함수를 작성합니다. 마지막으로, AI 에이전트와 체스 게임을 하기 위한 더 많은 기능을 개발하게 될 것입니다. 이 튜토리얼은 IBM Granite 커뮤니티의 Granite Snack Cookbook GitHub에서도 Jupyter Notebook 형태로 확인할 수 있습니다.

LM Studio

LM Studio는 로컬 대규모 언어 모델(LLM)로 작업하기 위한 애플리케이션입니다. Mistral AI 모델, Google의 Gemma, Meta’s Llama 또는 DeepSeek’s R1과 같은 오픈 소스 모델을 LM Studio와 함께 사용할 수 있습니다.

초급자부터 고급 사용자까지 LM Studio를 사용하면 컴퓨터의 CPU나 GPU로도 LLM을 실행할 수 있습니다. LM Studio는 ChatGPT와 유사한 채팅형 인터페이스를 제공하여 로컬 LLM과 상호작용할 수 있습니다.

로컬 AI 모델을 사용하면 미세 조정, 추론 등을 수행할 수 있으며, 외부 API 호출(OpenAI 또는 IBM® watsonx.ai 애플리케이션 프로그래밍 인터페이스(API) 등)이나 토큰 사용량을 걱정할 필요가 없습니다. 또한 LM Studio에서는 사용자가 로컬 및 비공개로 '문서와 채팅'할 수 있습니다. 사용자는 채팅 세션에 문서를 첨부하고 문서에 대해 질문할 수 있습니다. 문서가 긴 경우, LM Studio는 쿼리를 위해 검색 증강 생성(RAG) 시스템을 구성합니다.

도구 호출

LLM은 사람과 유사한 텍스트를 이해하고 생성하는 데는 탁월하지만, 정밀한 계산, 실시간 외부 데이터 액세스 또는 잘 정의된 특정 절차의 실행이 필요한 작업에서는 종종 한계에 직면합니다. [도구 호출]을 구현하여 대규모 언어 모델(LLM)에 그들이 선택하여 호출할 수 있는 일련의 '도구', 즉 외부 함수를 제공하여 그들의 능력을 크게 확장할 수 있게 합니다. 이 튜토리얼에서는 이러한 도구를 정의하고 통합하여 LLM이 보다 안정적으로 다양한 작업을 수행할 수 있도록 하는 방법을 설명합니다.

단계

1단계. LM Studio 설치

LM Studio를 설치하기 전에 로컬 컴퓨터가 최소 시스템 요구 사항을 충족하는지 확인하세요.

그런 다음 컴퓨터 운영 체제(Windows, macOS 또는 Linux)에 적합한 설치 프로그램을 다운로드합니다. 그런 다음 다음 지침에 따라 모델을 로컬 컴퓨터에 다운로드합니다.

이 레시피에서는 Granite 3.3-8b 명령 모델을 사용하지만 원하는 LLM을 자유롭게 사용할 수 있습니다. Granite 모델을 사용하는 경우 특정 사용자/모델 문자열을ibm-granite/granite-3.3-8b-instruct-GGUF LM Studio의 공간에서Select a model to load 검색할 수 있습니다.

그 다음 LM Studio 왼쪽 상단의 녹색 아이콘을 찾아Developer LM Studio 로컬 서버를 시작합니다.Status 왼쪽 상단의 바를 전환하여Running .

2단계. 종속성 설치

먼저 LM Studio SDK 및 체스 라이브러리를 포함하여 필요한 라이브러리를 설치해야 합니다.

%pip install git+https://github.com/ibm-granite-community/utils \
    lmstudio \
    chess
import lmstudio as lms

3단계. 모델 로드

다음에 이 레시피에서 사용할 모델을 지정하겠습니다. 이 경우에는 LM Studio에서 다운로드한 모델인 Granite 3.3-8b 명령이 사용됩니다.

또한 초기 메시지와 함께 model.respond()를 호출하여 채팅을 시작합니다.

model = lms.llm("ibm-granite/granite-3.3-8b-instruct-GGUF")

print(model.respond("Hello Granite!"))

4단계. 도구 없이 계산 수행

모델에 간단한 계산을 요청하는 것부터 시작하겠습니다.

print(model.respond("What is 26.97 divided by 6.28? Don't round."))

모델이 근사치를 제공할 수는 있지만 자체적으로 몫을 계산할 수 없기 때문에 정확한 답을 반환하지는 않습니다.

5단계. Python 함수로 계산 도구 생성

이 문제를 해결하기 위해 모델에 도구를 제공할 것입니다. 도구는 추론 시 모델에 제공하는 Python 함수입니다. 모델은 사용자의 쿼리에 응답하기 위해 이러한 도구 중 하나 이상을 호출하도록 선택할 수 있습니다.

도구 작성 방법에 대한 자세한 내용은 LM Studio 문서를 참조하세요. 일반적으로 도구 함수에 적절한 이름, 정의된 입력 및 아웃풋 유형, 도구의 목적을 설명하는 설명이 있는지 확인해야 합니다. 이 모든 정보는 모델에 전달되며 쿼리에 답할 올바른 도구를 선택하는 데 도움이 될 수 있습니다.

모델이 도구로 사용할 수 있는 몇 가지 간단한 수학 함수를 작성합니다.

def add(a: float, b:float):
    """Given two numbers a and b, return a + b."""
    return a + b

def subtract(a: float, b:float):
    """Given two numbers a and b, return a - b."""
    return a - b

def multiply(a: float, b: float):
    """Given two numbers a and b, return a * b."""
    return a * b

def divide(a: float, b: float):
    """Given two numbers a and b, return a / b."""
    return a / b

def exp(a: float, b:float):
    """Given two numbers a and b, return a^b"""
    return a ** b

이제 동일한 쿼리를 다시 실행하지만 모델에 응답하는 데 도움이 되는 몇 가지 도구를 제공할 수 있습니다. 자동 도구 호출을 위해 model.act() 호출을 사용하고 만든 함수를 사용할 수 있음을 모델에 표시하겠습니다.

model.act(
  "What is 26.97 divided by 6.28? Don't round.",
  [add, subtract, multiply, divide, exp],
  on_message=print,
)

모델이 ToolCallRequest, name에서 올바른 도구를 선택하고 arguments(함수에 전달할 인수) 아래에 적절한 입력을 사용했으며 관련 없는 도구를 사용하지 않았음을 알 수 있습니다. 마지막으로 AssistantResponse, content, text 아래의 응답은 질문에 대한 정확한 답변인 모델의 응답을 보여줍니다.

딸기(strawberry)에는 몇 개의 R이 있나요?

가장 똑똑한 언어 모델조차도 당황할 수 있는 매우 간단한 질문입니다. 2024년 이전에 훈련 컷오프가 있는 거의 모든 LLM은 "딸기"라는 단어에 2개의 R만 있다고 대답합니다. 보너스로, 글자의 잘못된 위치를 환각으로 인식할 수도 있습니다.

요즘에는 대부분의 훈련 데이터 세트에서 이 특정 질문이 널리 퍼져 있기 때문에 LLM이 이 문제를 맞추는 경향이 있습니다. 그러나 LLM은 여전히 유사한 문자 계산 작업에서 실패하는 경우가 많습니다.

print(model.respond("How many Bs are in the word 'blackberry'?"))

모델이 더 잘 작동하도록 돕는 도구를 작성해 보겠습니다.

def get_letter_frequency(word: str) -> dict:
    """Takes in a word (string) and returns a dictionary containing the counts of each letter that appears in the word. """

    letter_frequencies = {}

    for letter in word:
        if letter in letter_frequencies:
            letter_frequencies[letter] += 1
        else:
            letter_frequencies[letter] = 1

    return letter_frequencies

이제 도구를 모델에 전달하고 프롬프트를 다시 실행할 수 있습니다.

model.act(
  "How many Bs are in the word 'blackberry'?",
  [get_letter_frequency],
  on_message=print,
)

get_letter_frequency() 도구를 사용하여 모델은 '블랙베리(blackberry)'라는 단어에서 ㅠ의 개수를 정확하게 계산할 수 있었습니다.

6단계. 에이전트에 대한 자동 도구 호출 구현

이 자동 도구 호출 워크플로의 가장 좋은 사용 사례 중 하나는 모델에 외부 환경과 상호 작용할 수 있는 기능을 제공하는 것입니다. 도구를 사용하여 체스를 두는 에이전트를 만들어 봅시다!

언어 모델은 체스에 대한 강력한 개념 지식을 보유하고 있을 수 있지만, 본질적으로 체스판을 이해하도록 설계되지는 않았습니다. 온라인 챗봇과 체스 게임을 하려고 하면 여러 턴 후에 탈선하여 불법적이거나 비합리적인 움직임을 보이는 경우가 많습니다.

저희는 모델이 보드를 이해하고 상호 작용하는 데 도움이 되는 여러 도구를 제공하고 있습니다.

  • legal_moves(): 현재 위치에서의 모든 합법적인 이동 목록을 제공합니다.
  • possible_captures(): 현재 위치에서 가능한 모든 캡처 목록을 제공합니다.
  • possible_checks(): 현재 위치에서 가능한 모든 검사 목록을 제공합니다.
  • get_move_history(): 지금까지 플레이된 모든 움직임 목록을 제공합니다.
  • get_book_moves(): 모든 책 이동 목록을 제공합니다.
  • make_ai_move(): 모델이 움직임을 입력할 수 있도록 하는 인터페이스

많은 양은 아니지만 모델이 환각 없이 체스 게임을 하고 지능적인 추론을 사용하여 의사 결정을 내릴 수 있으면 충분합니다.

import chess
import chess.polyglot
from IPython.display import display, SVG, clear_output
import random
import os, requests, shutil, pathlib

board = chess.Board()
ai_pos = 0

# Download book moves
RAW_URL   = ("https://raw.githubusercontent.com/"
             "niklasf/python-chess/master/data/polyglot/performance.bin")
DEST_FILE = "performance.bin"

if not os.path.exists(DEST_FILE):
    print("Downloading performance.bin …")
    try:
        with requests.get(RAW_URL, stream=True, timeout=15) as r:
            r.raise_for_status()
            with open(DEST_FILE, "wb") as out:
                shutil.copyfileobj(r.raw, out, 1 << 16)  # 64 KB chunks
    except requests.exceptions.RequestException as e:
        raise RuntimeError(f"Download failed: {e}")



def legal_moves() -> list[str]:
    """
    Returns a list of legal moves in standard algebraic notation.
    """
    return [board.san(move) for move in board.legal_moves]

def possible_captures() -> list[dict]:
    """
    Returns all legal captures with metadata:
    - san: SAN notation of the capture move.
    - captured_piece: The piece type being captured ('P','N','B','R','Q','K').
    - is_hanging: True if the captured piece was undefended before the capture.
    """
    result = []
    for move in board.generate_legal_captures():
        piece = board.piece_at(move.to_square)
        piece_type = piece.symbol().upper() if piece else "?"
        # Check defenders of the target square
        defenders = board.attackers(not board.turn, move.to_square)
        is_hanging = len(defenders) == 0  # no defenders => hanging
      
        result.append({
            "san": board.san(move),
            "captured_piece": piece_type,
            "is_hanging": is_hanging
        })
    return result

def possible_checks() -> list[dict]:
    """
    Returns all legal checking moves with metadata:
    - san: SAN notation of the checking move.
    - can_be_captured: True if after the move, the checking piece can be captured.
    - can_be_blocked: True if the check can be legally blocked.
    - can_escape_by_moving_king: True if the king can move out of check.
    """
    result = []
    for move in board.legal_moves:
        if not board.gives_check(move):
            continue
        temp = board.copy()
        temp.push(move)

        can_capture = any(
            temp.is_capture(reply) and reply.to_square == move.to_square
            for reply in temp.legal_moves
        )

        # King escapes by moving
        king_sq = temp.king(not board.turn)
        can_escape = any(
            reply.from_square == king_sq for reply in temp.legal_moves
        )

        # Blocking: legal non-capture, non-king move that resolves check
        can_block = any(
            not temp.is_capture(reply)
            and reply.from_square != king_sq
            and not temp.gives_check(reply)
            for reply in temp.legal_moves
        )

        result.append({
            "san": board.san(move),
            "can_be_captured": can_capture,
            "can_be_blocked": can_block,
            "can_escape_by_moving_king": can_escape
        })
    return result

def get_move_history() -> list[str]:
    """
    Returns a list of moves made in the game so far in standard algebraic notation.
    """
    return [board.san(move) for move in board.move_stack]

def get_book_moves() -> list[str]:
    """
    Returns a list of book moves in standard algebraic notation from performance.bin
    for the current board position. If no book moves exist, returns an empty list.
    """
    moves = []
    with chess.polyglot.open_reader("performance.bin") as reader:
        for entry in reader.find_all(board):
            san_move = board.san(entry.move)
            moves.append(san_move)
    return moves

def is_ai_turn() -> bool:
    return bool(board.turn) == (ai_pos == 0)

def make_ai_move(move: str) -> None:
    """
    Given a string representing a valid move in chess notation, pushes move onto chess board.
    If non-valid move, raises a ValueError with message "Illegal move.
    If called when it is not the AI's turn, raises a ValueError with message "Not AI's turn."
    THIS FUNCTION DIRECTLY ENABLES THE AI TO MAKE A MOVE ON THE CHESS BOARD.
    """
    if is_ai_turn():
        try:
            board.push_san(move)
        except ValueError as e:
            raise ValueError(e)
    else:
        raise ValueError("Not AI's turn.")

def make_user_move(move: str) -> None:
    """
    Given a string representing a valid move in chess notation, pushes move onto chess board.
    If non-valid move, raises a ValueError with message "Illegal move.
    If called when it is not the player's turn, raises a ValueError with message "Not player's turn."
    If valid-move, updates the board and displays the current state of the board.
    """
    if not is_ai_turn():
        try:
            board.push_san(move)
        except ValueError as e:
            raise ValueError(e)
    else:
        raise ValueError("Not player's turn.")

def print_fragment(fragment, round_index=0):
    print(fragment.content, end="", flush=True)

다음으로 AI 에이전트와 체스 경기를 설정합니다. lms.Chat() 호출을 사용하여 에이전트가 화이트 또는 블랙을 위해 플레이하는 경우에 대한 지침을 체스 AI 에이전트에 제공합니다.

chat_white = lms.Chat("""You are a chess AI, playing for white. Your task is to make the best move in the current position, using the provided tools. 
                      You should use your overall chess knowledge, including openings, tactics, and strategies, as your primary method to determine good moves. 
                      Use the provided tools as an assistant to improve your understanding of the board state and to make your moves. Always use the book moves 
                      if they are available. Be prudicious with your checks and captures. Understand whether the capturable piece is hanging, and its value in 
                      comparison to the piece you are using to capture. Consider the different ways the opponent can defend a check, to pick the best option.""")


chat_black = lms.Chat("""You are a chess AI, playing for black. Your task is to make the best move in the current position, using the provided tools. 
                      You should use your overall chess knowledge, including openings, tactics, and strategies, as your primary method to determine good moves. 
                      Use the provided tools as an assistant to improve your understanding of the board state and to make your moves. Always use the book moves 
                      if they are available. Be prudicious with your checks and captures. Understand whether the capturable piece is hanging, and its value in 
                      comparison to the piece you are using to capture. Consider the different ways the opponent can defend a check, to pick the best option.""")

마지막으로, 일치를 추적하기 위해 update_board()get_end_state()라는 두 가지 함수를 설정합니다.

앞서 도구 호출에 사용한 model.act() 호출을 사용하여 정의한 에이전트 지침(chat)과 사용 가능한 도구를 전달하고 max_prediction_rounds를 설정합니다. 이 함수는 에이전트가 특정 이동을 실행하기 위해 수행할 수 있는 최대 독립 도구 호출 수를 보여줍니다.

다음 셀을 실행하면 동작을 작성할 수 있는 빈 입력 필드가 나타납니다. 사용할 수 있는 동작이 확실하지 않은 경우 'help'를 입력하면 첫 번째 이니셜이 해당 말의 이니셜 이름을 입력하여 사용할 수 있는 동작의 표기법이 표시됩니다(\"B\"는 비숍, \"Q\"은 퀸 등, 그러나 \"K\"는 왕을 나타내기 때문에 기사는 \"N\"이며 폰에 대한 첫 번째 이니셜은 없습니다). 나열된 다음 문자와 숫자는 해당 말을 이동할 행과 열입니다. 캐슬링이나 모호한 말의 움직임과 같은 특수한 경우의 표기법에 대해서는 대수 표기법(체스) 위키피디아 페이지를 참조하세요.

행운을 빕니다!

move = 0
import chess.svg

board.reset()
ai_pos = round(random.random())

def update_board(move = move, ai_pos = ai_pos):
    """
    Updates the chess board display in the notebook.
    """
    clear_output(wait=True)  # Clear previous output
    print(f"Board after move {move+1}")
    if (ai_pos == 1):
        display(SVG(chess.svg.board(board, size=400)))
    else:
        display(SVG(chess.svg.board(board, size=400, orientation = chess.BLACK)))

def get_end_state():
    """
    Returns the end state of the chess game.
    """
    if board.is_checkmate():
        return "Checkmate!"
    elif board.is_stalemate():
        return "Stalemate!"
    elif board.is_insufficient_material():
        return "Draw by insufficient material!"
    elif board.is_seventyfive_moves():
        return "Draw by 75-move rule!"
    elif board.is_fivefold_repetition():
        return "Draw by fivefold repetition!"
    else:
        return None

clear_output(wait=True) # Clear any previous output from the cell
if (ai_pos == 1):
    display(SVG(chess.svg.board(board, size=400)))
else:
    display(SVG(chess.svg.board(board, size=400, orientation = chess.BLACK)))

# 2. Loop through moves, apply each move, clear previous output, and display new board
userEndGame = False
while True:

    if ai_pos == 0:
        # AI's turn
        model.act(
            chat_white,
            [get_move_history, legal_moves, possible_captures, possible_checks, get_book_moves, make_ai_move],
            on_message=print,
            max_prediction_rounds = 8,
        )


        if is_ai_turn(): # failsafe in case AI does not make a move
           make_ai_move(legal_moves()[0])  # Default to the first legal move if AI does not respond

        update_board(move)
        move += 1
        game_over_message = get_end_state()
        if game_over_message:
            print(game_over_message)
            break

        # User's turn
        while True:
            user_move = input("User (Playing Black): Input your move. Input 'help' to see the list of possible moves. Input 'quit' to end the game ->")
            if user_move.lower() == 'quit':
                print("Game ended by user.")
                userEndGame = True
                break
            if user_move.lower() == 'help':
                print("Possible moves:", legal_moves())
                continue
            try:
                make_user_move(user_move)
                break
            except ValueError as e:
                print(e)

        if userEndGame:
            break

        update_board(move)
        move += 1
        game_over_message = get_end_state()
        if game_over_message:
            print(game_over_message)
            break
    else:
        # User's turn
        while True:
            user_move = input("User (Playing White): Input your move. Input 'help' to see the list of possible moves. Input 'quit' to end the game ->")
            if user_move.lower() == 'quit':
                print("Game ended by user.")
                userEndGame = True
                break
            if user_move.lower() == 'help':
                print("Possible moves:", legal_moves())
                continue
            try:
                make_user_move(user_move)
                break
            except ValueError as e:
                print(e)

        if userEndGame:
            break

        update_board(move)
        move += 1
        game_over_message = get_end_state()
        if game_over_message:
            print(game_over_message)
            break

        model.act(
            chat_black,
            [get_move_history, legal_moves, possible_captures, possible_checks, get_book_moves, make_ai_move],
            max_prediction_rounds = 8,
            on_message=print,
        )

        if is_ai_turn(): # failsafe in case AI does not make a move
           make_ai_move(legal_moves()[0])  # Default to the first legal move if AI does not respond

        update_board(move)
        move += 1
        game_over_message = get_end_state()
        if game_over_message:
            print(game_over_message)
            break

요약

이 노트북에서는 도구를 통합하여 유틸리티 및 에이전트 기능을 향상시킬 수 있는 방법을 시연했습니다. LLM에 사전 정의된 외부 기능에 대한 액세스를 제공함으로써 핵심 언어 처리 능력을 초월하여 정확한 계산이나 외부 시스템과의 인터페이스와 같은 작업을 수행할 수 있음을 설명했습니다. 이 작업을 자체적으로 안정적으로 수행할 수는 없습니다. 핵심은 도구를 사용하면 LLM이 특정 하위 문제를 전문화된 루틴에 위임하여 사실적인 데이터 또는 정확한 작업에 근거하여 대응할 수 있다는 것입니다. 이 접근 방식을 사용하면 정확성이 향상될 뿐만 아니라 LLM이 더 복잡한 대화형 워크플로에 참여하여 더 다양하고 강력한 어시스턴트로 효과적으로 전환할 수 있습니다.

관련 솔루션
비즈니스용 AI 에이전트

생성형 AI로 워크플로와 프로세스를 자동화하는 강력한 AI 어시스턴트 및 에이전트를 구축, 배포, 관리하세요.

    watsonx Orchestrate 살펴보기
    IBM AI 에이전트 솔루션

    믿을 수 있는 AI 솔루션으로 비즈니스의 미래를 설계하세요.

    AI 에이전트 솔루션 살펴보기
    IBM Consulting AI 서비스

    IBM Consulting AI 서비스는 기업이 AI 활용 방식을 재구상하여 혁신을 달성하도록 지원합니다.

    인공 지능 서비스 살펴보기
    다음 단계 안내

    사전 구축된 앱과 스킬을 사용자 정의하든, AI 스튜디오를 사용하여 맞춤형 에이전틱 서비스를 구축하고 배포하든, IBM watsonx 플랫폼이 모든 것을 지원합니다.

    watsonx Orchestrate 살펴보기 watsonx.ai 살펴보기