使用 LM Studio 构建自动工具调用 Granite

作者

Caleb Palappillil

Open-source AI Software Developer Intern

Erika Russi

Data Scientist

IBM

本教程将指导您在本地计算机上使用 LM Studio 搭配开源 IBM Granite 3.3-8b instruct 模型。您将首先测试基础模型性能,随后编写可供模型调用的 Python 函数实现自动工具调用。最后,您将开发更多功能,实现与 AI 智能体国际象棋对弈。本教程同步收录于 IBM Granite 社区的 Granite Snack Cookbook GitHub,以 Jupyter Notebook 形式提供。

LM Studio

LM Studio 是一个用于处理本地 [大型语言模型] (LLM) (https://www.ibm.com/cn-zh/think/topics/large-language-models) 的应用程序。您可以在 LM Studio 中使用任何 [开源模型] (https://www.ibm.com/cn-zh/think/topics/open-source-llms),如 [Mistral AI] (https://www.ibm.com/cn-zh/think/topics/mistral-ai) 模型、 [Google's Gemma] (https://www.ibm.com/cn-zh/think/topics/google-gemma)、Meta's Llama 或 [DeepSeek's R1] (https://www.ibm.com/cn-zh/think/topics/deepseek) 系列。

借助 LM Studio,初学者和高级用户都可以使用计算机的 CPU 甚至 GPU 来运行 LLM (https://www.ibm.com/cn-zh/think/topics/gpu)。LM Studio 提供了一个类似聊天的界面,可与本地 LLM 进行交互,类似于 ChatGPT 的聊天功能。

使用本地 AI 模型,您可以 进行微调推理 等操作,而无需担心外部 API 调用(如 OpenAI 或 IBM watsonx.ai 应用程序编程接口 (API))或令牌使用情况。LM Studio 还允许用户在本地和私下“与文档聊天”。用户可以将文档附加到聊天会话并询问有关该文档的问题。如果文档很长,LM Studio 将设置 [检索增强生成](https://www.ibm.com/cn-zh/think/topics/retrieval-augmented- Generation)(RAG) 系统进行查询。

工具调用

虽然 LLM 擅长理解和生成类似人类文本的文本,但当任务需要精确计算、访问实时外部数据或执行特定的明确定义程序时,它们往往会面临限制。通过实施 工具调用,我们为 LLM 配备了一套“工具”(外部函数)他们可以选择调用这些函数,从而显著扩展功能。本教程将演示如何定义并集成这些工具,使 LLM 能够以更高的可靠性执行更广泛的任务。

步骤

第 1 步: 安装 LM Studio

在安装 LM Studio 之前,请检查本地计算机是否满足最低系统 要求

接下来, 下载适合您计算机操作系统(Windows、macOS 或 Linux)的安装程序。然后,按照以下 说明将模型下载到本地计算机。

本示例将采用 Granite 3.3-8b instruct 模型,您也可自由选择任何 LLM。如果您使用的是 Granite 模型,则可以ibm-granite/granite-3.3-8b-instruct-GGUFSelect 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 instruct 模型。我们还将通过调用 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”下的响应显示来自模型的响应,即问题的准确答案。

###草莓单词中有几个 R?

这个简单问题曾难倒最聪明的语言模型。在 2024 年前训练的 LLM 几乎都回答“strawberry”中只有 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”中字母 b 的数量。

第 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()' 调用,我们将向为执白或执黑的国际象棋智能体提供对弈指令。

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()”如果使用之前的工具调用,我们将为代理提供我们定义的指令(“聊天”)、可供其使用的工具并建立 “max_prediction_rounds”。此函数显示代理为执行特定棋步可以进行独立工具调用的最大数量。

运行下一个单元后,系统将显示一个空白输入框,供您输入棋步走法。如果不确定当前可用的合法棋步,请输入“help”指令,系统将显示可用棋步的代数记谱说明:其中首字母代表棋子名称的缩写(例如 “B” 代表象,“Q” 代表后,以此类推;但 “N” 代表马,因为 “K” 已用于表示王,而兵则不标注首字母)。随后列出的字母与数字组合,表示该棋子应移动至的目标行与列坐标。关于王车易位或歧义棋步等特殊情形的记谱规则,请参阅维基百科的国际象棋代数记谱法相关页面

祝您对弈顺利!

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 能够参与更复杂的交互式工作流,有效地将其转变为更通用、更强大的助手。

相关解决方案
商用 AI 智能体

构建、部署和管理强大的 AI 助手和智能体,运用生成式 AI 实现工作流和流程自动化。

    探索 watsonx Orchestrate
    IBM AI 智能体解决方案

    借助值得信赖的 AI 解决方案,您可以勾勒未来业务发展蓝图。

    深入了解 AI 智能体解决方案
    IBM Consulting AI 服务

    IBM Consulting AI 服务有助于重塑企业利用 AI 实现转型的方式。

    探索人工智能服务
    采取下一步行动

    无论您是选择定制预构建的应用程序和技能,还是使用 AI 开发平台构建和部署定制代理服务,IBM watsonx 平台都能满足您的需求。

    1. 探索 watsonx Orchestrate
    2. 深入了解 watsonx.ai