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スタジオ

LM Studioは、ローカルの[大規模言語モデル](https://www.ibm.com/jp-ja/think/topics/large-language-models)で作業するためのアプリケーションです。(LLM)。LM Studioでは、あらゆる[オープンソース・モデル](https://www.ibm.com/jp-ja/think/topics/open-source-llms)や、[Mistral AI](https://www.ibm.com/jp-ja/think/topics/mistral-ai)モデル、[GoogleのGemma](https://www.ibm.com/jp-ja/think/topics/Google-gemma)、Meta's Llamaまたは[DeepSeek's R1](https://www.ibm.com/jp-ja/think/topics/deepseek)シリーズを使用できます。

LM Studioを使用すると、初心者から上級ユーザーまで、コンピューターのCPUまたは[GPU](https://www.ibm.com/jp-ja/think/topics/gpu)のいずれかでLLMを実行できます。LM Studioは、[ChatGPT](https://www.ibm.com/jp-ja/think/topics/chatgpt)のチャットと同様に、ローカルのLLMと対話するためのチャットのようなインターフェースを提供しています。

ローカルAIモデルを使用すると、[ファイン・チューニング](https://www.ibm.com/jp-ja/think/topics/Fine-tuneing)や、推論などを行うことが可能です。これにより、外部API(OpenAIやIBM® watsonx.aiなどのアプリケーション・プログラミング・インターフェース)やトークン使用量を気にする必要がなくなります。LM Studioを使用すると、ユーザーはローカルおよびプライベートに「ドキュメントを使用したチャット」を行うこともできます。ユーザーはチャット・セッションに文書を添付し、その文書について質問できます。文書が長い場合、LM Studioは[検索拡張生成](https://www.ibm.com/jp-ja/think/topics/retrieval-augmented-Generation)(RAG)システムを使用してクエリを実行します。

ツール呼び出し

LLMは人間のようなテキストの理解と生成には優れていますが、タスクで正確な計算、リアルタイムの外部データへのアクセス、または特定の明確に定義された手順の実行が必要な場合、しばしば制限に直面します。[ツール呼び出し](https://www.ibm.com/jp-ja/think/topics/tool-calling)を実装することで、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関数です。モデルは、ユーザーの質問に答えるために、これらのツールのうち1つ以上を呼び出すことを選択できます。

ツールの作成方法の詳細については、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」の回答には、モデルからの回答、つまり質問に対する正確な回答が表示されます。

フレームワークには何個のRがありますか。

最も賢い言語モデルでさえも困惑させる非常に単純な質問です。2024年以前にトレーニングを中断したLLMのほぼすべてのLLMは、「strawberry」という言葉にRが2つしかないと回答しています。さらに、文字の位置を間違って認識することもあります。

現在、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」に含まれるbの数を正確に数えることができました。

ステップ6. エージェントに自動ツール呼び出しを導入する

この自動ツール呼び出しワークフローの最良のユースケースの1つは、モデルに外部環境と対話する機能を提供することです。ツールを使用してチェスをプレイするエージェントを構築しましょう。

言語モデルはチェスに関する強力な概念知識を持つことができますが、本質的にはチェス盤を理解するように設計されていません。オンライン・チャットボットを使ってチェスのゲームをプレイしようとすると、数ターン後に脱却し、違法または非合理な動きになることがよくあります。

私たちはモデルがボードの理解と対話を行うのに役立ついくつかのツールを提供しています。- 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()」という2つの関数を設定します。

以前にツール呼び出しに使用した「model.act()」呼び出しを使用することで、定義したエージェント指示(「chat」)とその使用に利用できるツールを入力し「max_prediction_rounds」を確立します。この関数は、エージェントが特定の動きを実行するために実行できる独立ツール呼び出しの最大数を示します。

次のセルを実行すると、動きを書き込むための空のインプット・フィールドが表示されます。利用可能な動きがわからない場合は、「ヘルプ」と入力すると、利用可能な動きの表記が表示されます。ここで、最初のイニシャルはそのピースのイニシャル名です(「\"B\"」はビショップ、「"Q\"」はクイーン)のようになります。しかし、「\"K\"」は王を表し、「\"N\"」はナイトであり、ポーンを表す頭文字は存在しません。次の文字と数字は、そのピースを動きさせる行と列です。キャスリングや曖昧な駒の動きなどの特殊なケースの表記については、代数記法(チェス)のWikipediaページを参照してください。

頑張ってください。

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がコアとなる言語処理機能を超えて、正確な計算などのタスクを実行したり、外部システムとのインターフェースを作成したりできることを示しました。それだけでは確実に行うことはできません。重要なポイントは、ツールの使用により、LLMが特定の下位問題を特殊なルーチンに委任できるようになり、事実に基づいたデータや正確なオペレーションに基づいて対応できるようになるということです。このアプローチにより、精度が向上するだけでなく、LLMはより複雑でインタラクティブなワークフローに取り組むことができ、LLMをより多用途で強力なアシスタントに効果的に変えることができます。

関連ソリューション
ビジネス向けAIエージェント

生成AIを使用してワークフローとプロセスを自動化する強力なAIアシスタントとエージェントを構築、デプロイ、管理しましょう。

    watsonx Orchestrateの詳細はこちら
    IBM AIエージェント・ソリューション

    信頼できるAIソリューションでビジネスの未来を構築します。

    AIエージェント・ソリューションの詳細はこちら
    IBM®コンサルティング AIサービス

    IBMコンサルティングAIサービスは、企業がAIをトランスフォーメーションに活用する方法を再考するのに役立ちます。

    人工知能サービスの詳細はこちら
    次のステップ

    事前構築済みのアプリケーションとスキルをカスタマイズする場合でも、AIスタジオを使用してカスタム・エージェント・サービスを構築し、デプロイする場合でも、IBM watsonxプラットフォームが対応します。

    watsonx Orchestrateの詳細はこちら watsonx.aiの詳細はこちら