Usa LM Studio per il tool calling automatico con Granite

Autori

Caleb Palappillil

Open-source AI Software Developer Intern

Erika Russi

Data Scientist

IBM

In questo tutorial dettagliato, utilizzerai LM Studio con il modello di istruzioni open source IBM Granite 3.3-8b sul tuo computer locale. Inizialmente, testerai il modello locale così com'è e quindi scriverai le funzioni Python che il modello può utilizzare per il tool calling automatico. Infine, svilupperai altre funzioni per giocare una partita a scacchi con un agente AI. Questo tutorial è disponibile anche nel Granite Snack Cookbook su GitHub dell'IBM Granite Community sotto forma di Jupyter Notebook.

LM Studio

LM Studio è un'applicazione per lavorare con modelli linguistici di grandi dimensioni (LLM) in locale. È possibile utilizzare qualsiasi modello open source con LM Studio, come i modelli Mistral AI, Gemma di Google, Llama di Meta o la serie R1 di DeepSeek.

Utilizzando LM Studio, gli utenti principianti e più avanzati possono eseguire gli LLM con la CPU del proprio computer o anche con una GPU. LM Studio offre un'interfaccia simile a una chat per interagire con gli LLM locali in modo simile alla chat di ChatGPT.

Con un modello AI locale, è possibile effettuare la messa a punto, l'inferenza e molto altro ancora senza doversi preoccupare di chiamate API esterne, (come le application programming interface o API di OpenAI o IBM watsonx.ai) o del consumo di token. LM Studio consente inoltre agli utenti di "chattare con i documenti" localmente e privatamente. Un utente può allegare un documento a una sessione di chat e porre domande sul documento. Nei casi in cui il documento sia molto lungo, LM Studio imposta un sistema di retrieval-augmented generation (RAG) per gestire le query in modo efficiente.

Tool calling

Sebbene i modelli linguistici di grandi dimensioni (LLM) eccellano nella comprensione e generazione di testo simile a quello umano, spesso incontrano limiti quando i compiti richiedono calcoli precisi, accesso a dati esterni in tempo reale o l'esecuzione di procedure specifiche e ben definite. Implementando il tool calling, forniamo agli LLM una serie di "strumenti" (funzioni esterne) che possono scegliere di chiamare e che possono estendere significativamente le loro funzionalità. Questo tutorial dimostrerà come definire questi strumenti e integrarli, consentendo al LLM di eseguire una gamma più ampia di attività con maggiore affidabilità.

Passaggi

Passaggio 1. Installare LM Studio

Prima di installare LM Studio, verificare che il computer locale soddisfi i requisiti minimidi sistema.

Successivamente, scarica il programma di installazione appropriato per il sistema operativo del tuo computer (Windows, macOS o Linux). Quindi, segui queste istruzioni per scaricare i modelli sul tuo computer locale.

Per questa ricetta utilizzeremo il modello di istruzione Granite 3.3-8b, ma puoi usare qualsiasi LLM di tua scelta. Se stai utilizzando il modello Granite, puoi cercare la stringa specifica utente/modelloibm-granite/granite-3.3-8b-instruct-GGUF nelSelect a model to load spazio di LM Studio.

Quindi, avvia il server locale di LM Studio navigando verso il verdeDeveloper icona in alto a sinistra di LM Studio. Attiva laStatus barra in alto a sinistra perRunning .

Passaggio 2. Installare le dipendenze

Anzitutto dobbiamo installare le librerie necessarie, tra cui l'SDK di LM Studio e la libreria di scacchi.

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

Passaggio 3. Caricare il modello

Di seguito specificheremo il modello che vogliamo utilizzare in questa ricetta. Nel nostro caso, sarà il modello che abbiamo scaricato in LM Studio, Granite 3.3-8b instruct.

Inizieremo anche a chattare con esso chiamando model.respond() con un messaggio iniziale.

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

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

Passaggio 4. Eseguire un calcolo senza strumenti

Cominciamo chiedendo al modello di eseguire un calcolo semplice.

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

Sebbene il modello possa fornire una buona approssimazione, non restituirà la risposta esatta perché non è in grado di calcolare il quoziente autonomamente.

Passaggio 5. Creare strumenti di calcolo con funzioni Python

Per risolvere questo problema, forniremo al modello gli strumenti. Gli strumenti sono funzioni Python che forniamo al modello al momento dell'inferenza. Il modello può scegliere di chiamare uno o più di questi strumenti per rispondere alla query dell'utente.

Per ulteriori informazioni su come scrivere gli strumenti, consulta la documentazione di LM Studio. In generale, dovresti assicurarti che le funzioni dei tuoi strumenti abbiano un nome appropriato, tipi di input e output definiti e una descrizione che spieghi lo scopo dello strumento. Tutte queste informazioni vengono passate al modello e possono aiutarlo a selezionare lo strumento corretto per rispondere alla tua query.

Scriveremo diverse semplici funzioni matematiche per il modello da utilizzare come strumenti:

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

Ora, possiamo rieseguire la stessa query ma fornire al modello alcuni strumenti per aiutarlo a rispondere. Useremo la chiamata model.act() per il tool calling automatico e indicheremo al modello che può utilizzare le funzioni che abbiamo creato.

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

Possiamo osservare che il modello è stato in grado di selezionare lo strumento corretto in ToolCallRequest, name, ha utilizzato gli input appropriati in arguments (gli argomenti da passare alla funzione) ed ha evitato l'uso di strumenti irrilevanti. Infine, la risposta in AssistantResponse, content, text mostra la risposta del modello, una risposta esatta alla domanda.

Quante R ci sono in "strawberry" (fragola)?

Una domanda molto semplice che sconcerta anche i modelli linguistici più intelligenti. Quasi ogni singolo LLM con un limite di formazione prima del 2024 risponde che ci sono solo 2 R nella parola "strawberry". In più, potrebbe anche avere allucinazioni su posizioni errate per le lettere.

Al giorno d'oggi, gli LLM tendono a rispondere correttamente a questa domanda specifica, semplicemente perché la loro viralità li ha portati nella maggior parte dei set di dati di addestramento. Tuttavia, gli LLM falliscono ancora comunemente in attività simili al conteggio delle lettere.

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

Proviamo a scrivere uno strumento per aiutare il modello a fare un lavoro migliore.

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

Ora possiamo passare lo strumento al modello ed eseguire di nuovo il prompt.

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

Utilizzando lo strumento get_letter_frequency() il modello è stato in grado di contare con precisione il numero di b nella parola 'blackberry'.

Passaggio 6. Implementare il tool calling automatico per un agente

Uno dei migliori casi d'uso di questo workflow per il tool calling automatico è dare al modello la capacità di interagire con il suo ambiente esterno. Costruiamo un agente che utilizza strumenti per giocare a scacchi!

Sebbene i modelli linguistici possano avere una solida conoscenza concettuale degli scacchi, non sono intrinsecamente progettati per comprendere una scacchiera. Se provi a giocare a scacchi con un chatbot online, spesso questo deraglia dopo diversi turni, facendo mosse illegali o irrazionali.

Stiamo fornendo al modello diversi strumenti che lo aiutano a comprendere e interagire con la scacchiera.

  • legal_moves(): fornisce un elenco di tutte le mosse legali nella posizione attuale
  • possible_captures(): fornisce un elenco di tutte le catture possibili nella posizione corrente
  • **possible_checks () **: fornisce un elenco di tutti gli scacchi possibili nella posizione corrente
  • get_move_history(): fornisce un elenco di tutte le mosse giocate finora
  • get_book_moves(): fornisce un elenco di tutte le mosse di apertura conosciute
  • make_ai_move(): un'interfaccia per consentire al modello di inserire la sua mossa

Non è molto, ma è sufficiente perché il modello possa giocare una partita completa di scacchi senza generare risposte errate e utilizzare un ragionamento intelligente per prendere le sue decisioni.

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)

Ora prepareremo la partita a scacchi con un agente AI. Utilizzando la chiamata lms.Chat(), forniremo istruzioni al nostro agente AI degli scacchi per quando l'agente AI gioca per il bianco o il nero.

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.""")

Infine, imposteremo due funzioni per tracciare la partita: update_board()e get_end_state().

Utilizzando la chiamata model.act() che abbiamo impiegato in precedenza per la chiamata degli strumenti, forniremo le istruzioni dell'agente (chat) che abbiamo definito, gli strumenti disponibili per il suo utilizzo e stabiliremo un max_prediction_rounds. Questa funzione mostra il numero massimo di chiamate di strumenti indipendenti che l'agente può effettuare per eseguire una mossa specifica.

Dopo aver eseguito la cella successiva, dovrebbe apparire un campo di input vuoto in cui scrivere le tue mosse. Se non sai bene quali sono le mosse disponibili, digita `help` e verranno visualizzate le notazioni delle mosse disponibili, dove la prima iniziale è il nome del pezzo (\"B\" è l'alfiere (bishop), \"Q\" è la regina (queen), e così via. Ma \"N\" è il cavaliere (knight) poiché \"K\" rappresenta il re (king) e nessuna iniziale è per un pedone). La lettera e il numero successivi elencati rappresentano la riga e la colonna in cui spostare il pezzo. Per la notazione dei casi speciali, come l'arrocco o le mosse ambigue dei pezzi, consulta la pagina di Wikipedia sulla notazione algebrica (scacchi).

Buona fortuna!

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

Riepilogo

In questo notebook abbiamo dimostrato come l'integrazione degli strumenti possa migliorare l'utilità e le funzionalità degli LLM. Abbiamo dimostrato che fornendo a un LLM l'accesso a funzioni esterne predefinite, può trascendere le sue funzionalità di base per eseguire attività come calcoli accurati o interfacciarsi con sistemi esterni. Non può farlo in modo affidabile da solo. Il risultato principale è che l'uso degli strumenti consente agli LLM di delegare problemi secondari specifici a routine specializzate, consentendo loro di basare le risposte su dati fattuali o operazioni precise. Questo approccio non solo migliora la precisione, ma consente anche agli LLM di impegnarsi in un workflow più complesso e interattivo, trasformandoli efficacemente in assistenti più versatili e potenti.

Soluzioni correlate
Agenti AI per il Business

Crea, implementa e gestisci assistenti e agenti AI potenti che automatizzano workflow e processi con l'AI generativa.

    Scopri watsonx Orchestrate
    Soluzioni per agenti AI IBM

    Costruisci il futuro della tua azienda con soluzioni AI di cui puoi fidarti.

    Esplora le soluzioni basate su agenti AI
    Servizi AI di IBM Consulting

    I servizi di AI di IBM Consulting aiutano a reinventare il modo in cui le aziende lavorano con l'AI per la trasformazione.

    Esplora i servizi di intelligenza artificiale
    Prossimi passi

    Sia che tu scelga di personalizzare app e competenze precostituite o di creare e implementare servizi di agenti personalizzati utilizzando uno studio di AI, la piattaforma IBM watsonx è la soluzione che fa per te.

    Scopri watsonx Orchestrate Esplora watsonx.ai