1 究竟啥是“tool use”?

“工具使用”描述的是:

  • LLM输出文本,请求调用函数(LLM无法直接执行代码)
  • 你的代码执行这些函数
  • 你的代码将结果反馈给LLM

2 重要提示

为“工具使用”选择的模型会极大影响性能。选择模型时的一些通用指导原则:

  • 并非所有模型都能智能自主使用工具
  • 模型参数越多越好(如70亿参数的模型通常比30亿参数模型表现更好)
  • 很多情况下,https://model.lmstudio.ai/download/lmstudio-community/Qwen2.5-7B-Instruct-GGUF 模型表现良好
  • 这些指导原则可能演变

3 Python示例

Python聊天机器人示例,通过类似OpenAI调用chat.completions API的方式来使用“工具”,使LM Studio模型能够查询维基百科。

3.1 前提条件

  • PC安装Python
  • 使用pip install openai安装OpenAI Python包(仅用作LM Studio的包装器)

3.2 步骤

  1. 加载一个模型
  2. 启动服务器
  3. 使用“另存为Python文件”按钮保存下面的示例代码
  4. 将保存后显示的命令复制并粘贴到你的终端中,然后按回车键tool-use-example.py
"""
LM Studio工具使用演示:维基百科查询聊天机器人
展示LM Studio模型如何查询维基百科
"""

# 标准库导入
import itertools
import json
import shutil
import sys
import threading
import time
import urllib.parse
import urllib.request

# 第三方库导入
from openai import OpenAI

# 初始化LM Studio客户端
client = OpenAI(base_url="http://127.0.0.1:1234/v1", api_key="lm-studio")
MODEL = "llama-4-maverick-17b-128e-instruct"


def fetch_wikipedia_content(search_query: str) -> dict:
    """为给定的搜索查询获取维基百科内容"""
    try:
        # 搜索最相关的文章
        search_url = "https://en.wikipedia.org/w/api.php"
        search_params = {
            "action": "query",
            "format": "json",
            "list": "search",
            "srsearch": search_query,
            "srlimit": 1,
        }

        url = f"{search_url}?{urllib.parse.urlencode(search_params)}"
        with urllib.request.urlopen(url) as response:
            search_data = json.loads(response.read().decode())

        if not search_data["query"]["search"]:
            return {
                "status": "error",
                "message": f"未找到关于“{search_query}”的维基百科文章",
            }

        # 从搜索结果中获取规范化的标题
        normalized_title = search_data["query"]["search"][0]["title"]

        # 现在使用规范化的标题获取实际内容
        content_params = {
            "action": "query",
            "format": "json",
            "titles": normalized_title,
            "prop": "extracts",
            "exintro": "true",
            "explaintext": "true",
            "redirects": 1,
        }

        url = f"{search_url}?{urllib.parse.urlencode(content_params)}"
        with urllib.request.urlopen(url) as response:
            data = json.loads(response.read().decode())

        pages = data["query"]["pages"]
        page_id = list(pages.keys())[0]

        if page_id == "-1":
            return {
                "status": "error",
                "message": f"未找到关于“{search_query}”的维基百科文章",
            }

        content = pages[page_id]["extract"].strip()
        return {
            "status": "success",
            "content": content,
            "title": pages[page_id]["title"],
        }

    except Exception as e:
        return {"status": "error", "message": str(e)}


# 为LM Studio定义工具
WIKI_TOOL = {
    "type": "function",
    "function": {
        "name": "fetch_wikipedia_content",
        "description": (
            "搜索维基百科并获取最相关文章的简介。"
            "如果用户询问的内容很可能在维基百科上,始终使用此工具。"
            "如果用户的搜索查询中有拼写错误,在搜索前进行纠正。"
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "search_query": {
                    "type": "string",
                    "description": "用于查找维基百科文章的搜索查询",
                },
            },
            "required": ["search_query"],
        },
    },
}


# 用于显示模型处理状态的类
class Spinner:
    def __init__(self, message="正在处理..."):
        self.spinner = itertools.cycle(["-", "/", "|", "\\"])
        self.busy = False
        self.delay = 0.1
        self.message = message
        self.thread = None

    def write(self, text):
        sys.stdout.write(text)
        sys.stdout.flush()

    def _spin(self):
        while self.busy:
            self.write(f"\r{self.message} {next(self.spinner)}")
            time.sleep(self.delay)
        self.write("\r\033[K")  # 清除该行

    def __enter__(self):
        self.busy = True
        self.thread = threading.Thread(target=self._spin)
        self.thread.start()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.busy = False
        time.sleep(self.delay)
        if self.thread:
            self.thread.join()
        self.write("\r")  # 将光标移到行首


def chat_loop():
    """
    主聊天循环,处理用户输入并处理工具调用。
    """
    messages = [
        {
            "role": "system",
            "content": (
                "你是一个可以检索维基百科文章的助手。"
                "当被问及某个主题时,你可以检索维基百科文章并引用其中的信息。"
            ),
        }
    ]

    print(
        "助手:"
        "你好!我可以访问维基百科来帮助回答你关于历史、科学、人物、地点或概念的问题——或者我们也可以聊其他任何话题!"
    )
    print("(输入'quit'退出)")

    while True:
        user_input = input("\n你:").strip()
        if user_input.lower() == "quit":
            break

        messages.append({"role": "user", "content": user_input})
        try:
            with Spinner("正在思考..."):
                response = client.chat.completions.create(
                    model=MODEL,
                    messages=messages,
                    tools=[WIKI_TOOL],
                )

            if response.choices[0].message.tool_calls:
                # 处理所有工具调用
                tool_calls = response.choices[0].message.tool_calls

                # 将所有工具调用添加到消息中
                messages.append(
                    {
                        "role": "assistant",
                        "tool_calls": [
                            {
                                "id": tool_call.id,
                                "type": tool_call.type,
                                "function": tool_call.function,
                            }
                            for tool_call in tool_calls
                        ],
                    }
                )

                # 处理每个工具调用并添加结果
                for tool_call in tool_calls:
                    args = json.loads(tool_call.function.arguments)
                    result = fetch_wikipedia_content(args["search_query"])

                    # 以格式化的方式打印维基百科内容
                    terminal_width = shutil.get_terminal_size().columns
                    print("\n" + "=" * terminal_width)
                    if result["status"] == "success":
                        print(f"\n维基百科文章:{result['title']}")
                        print("-" * terminal_width)
                        print(result["content"])
                    else:
                        print(
                            f"\n获取维基百科内容时出错:{result['message']}"
                        )
                    print("=" * terminal_width + "\n")

                    messages.append(
                        {
                            "role": "tool",
                            "content": json.dumps(result),
                            "tool_call_id": tool_call.id,
                        }
                    )

                # 流式传输工具调用后的响应
                print("\n助手:", end=" ", flush=True)
                stream_response = client.chat.completions.create(
                    model=MODEL, messages=messages, stream=True
                )
                collected_content = ""
                for chunk in stream_response:
                    if chunk.choices[0].delta.content:
                        content = chunk.choices[0].delta.content
                        print(content, end="", flush=True)
                        collected_content += content
                print()  # 流式传输完成后换行
                messages.append(
                    {
                        "role": "assistant",
                        "content": collected_content,
                    }
                )
            else:
                # 处理常规响应
                print("\n助手:", response.choices[0].message.content)
                messages.append(
                    {
                        "role": "assistant",
                        "content": response.choices[0].message.content,
                    }
                )

        except Exception as e:
            print(
                f"\n与LM Studio服务器聊天时出错!\n\n"
                f"请确保:\n"
                f"1. LM Studio服务器在127.0.0.1:1234(主机名:端口)运行\n"
                f"2. 已下载模型“{MODEL}”\n"
                f"3. 已加载模型“{MODEL}”,或者已启用即时模型加载功能\n\n"
                f"错误详情:{str(e)}\n"
                "有关更多信息,请参阅https://lmstudio.ai/docs/basics/server"
            )
            exit(1)


if __name__ == "__main__":
    chat_loop()

启动服务器,运行上述脚本,与一个能够查询维基百科的模型进行聊天:

-> % python tool-use-example.py

助手:你好!我可以访问维基百科来帮助回答你关于历史、科学、人物、地点或概念的问题——或者我们也可以聊其他任何话题!
(输入'quit'退出)

你:

4 使用curl的示例

一个模型使用curl工具请求调用工具的情况。

curl http://127.0.0.1:1234/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama-4-maverick-17b-128e-instruct",
    "messages": [{"role": "user", "content": "What dell products do you have under $50 in electronics?"}],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "search_products",
          "description": "根据各种标准搜索产品目录。每当客户询问产品的可用性、价格或规格时,使用此工具。",
          "parameters": {
            "type": "object",
            "properties": {
              "query": {
                "type": "string",
                "description": "搜索词或产品名称"
              },
              "category": {
                "type": "string", 
                "description": "用于筛选的产品类别",
                "enum": ["electronics", "clothing", "home", "outdoor"]
              },
              "max_price": {
                "type": "number",
                "description": "以美元为单位的最高价格"
              }
            },
            "required": ["query"],
            "additionalProperties": false
          }
        }
      }
    ]
  }'

/v1/chat/completions识别的所有参数都将生效,并且可用工具的数组应在tools字段中提供。

如果模型认为用户的消息最好通过调用工具来完成,那么在响应字段choices[0].message.tool_calls中将会提供一个工具调用请求对象的数组。

顶级响应对象的finish_reason字段也将填充为"tool_calls"。

上述curl请求的一个示例响应如下:

{
  "id": "chatcmpl-zbpvqy4k1sax34dv62a2",
  "object": "chat.completion",
  "created": 1745678823,
  "model": "llama-4-maverick-17b-128e-instruct",
  "choices": [
    {
      "index": 0,
      "logprobs": null,
      "finish_reason": "tool_calls",
      "message": {
        "role": "assistant",
        "tool_calls": [
          {
            "id": "169722375",
            "type": "function",
            "function": {
              "name": "search_products",
              "arguments": "{\"category\":\"electronics\",\"max_price\":50,\"query\":\"Dell\"}"
            }
          }
        ]
      }
    }
  ],
  "usage": {
    "prompt_tokens": 387,
    "completion_tokens": 30,
    "total_tokens": 417
  },
  "stats": {},
  "system_fingerprint": "llama-4-maverick-17b-128e-instruct"
}%

上述响应可以理解为模型在说:

“请调用search_products函数,参数如下:

  • query参数为’dell’
  • category参数为’electronics’
  • max_price参数为’50’

然后把结果返回给我”

需要解析tool_calls字段来调用实际的函数或API,如Python示例代码中所示。

高层次流程

┌──────────────────────────┐
│ SETUP: LLM + Tool list   │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│    Get user input        │◄────┐
└──────────┬───────────────┘     │
           ▼                     │
┌──────────────────────────┐     │
│ LLM prompted w/messages  │     │
└──────────┬───────────────┘     │
           ▼                     │
     Needs tools?                │
      │         │                │
    Yes         No               │
      │         │                │
      ▼         └────────────┐   │
┌─────────────┐              │   │
│Tool Response│              │   │
└──────┬──────┘              │   │
       ▼                     │   │
┌─────────────┐              │   │
│Execute tools│              │   │
└──────┬──────┘              │   │
       ▼                     ▼   │
┌─────────────┐          ┌───────────┐
│Add results  │          │  Normal   │
│to messages  │          │ response  │
└──────┬──────┘          └─────┬─────┘
       │                       ▲
       └───────────────────────┘
┌──────────────────────────┐
│ 设 置:LLM + 工具列表   │
└──────────┬───────────────┘
           ▼
┌──────────────────────────┐
│    获取用户输入            │◄────┐
└──────────┬───────────────┘     │
           ▼                     │
┌──────────────────────────┐     │
│ 用消息提示LLM       │     │
└──────────┬───────────────┘     │
           ▼                     │
     需要工具吗?                │
      │         │                │
    是          否               │
      │         │                │
      ▼         └────────────┐   │
┌─────────────┐              │   │
│工具响应      │              │   │
└──────┬──────┘              │   │
       ▼                     │   │
┌─────────────┐              │   │
│执行工具      │              │   │
└──────┬──────┘              │   │
       ▼                     ▼   │
┌─────────────┐          ┌───────────┐
│将结果添加到消息│          │  常规响应   │
└──────┬──────┘          └─────┬─────┘
       │                       ▲
       └───────────────────────┘

5 工具列表和函数

  1. 编写供模型使用的函数
  2. 通过/v1/chat/completions请求体的tools字段向模型提供这些函数及其参数的列表

如Python中:

# 函数定义
def search_products(query: str, category: str = None, max_price: float = None):
    """
    根据标准在产品目录中搜索产品
    参数:
        query:搜索词
        category:产品类别筛选条件
        max_price:最高价格筛选条件
    返回:
        搜索结果
    """
    # 此处为具体实现
    pass

# 相应的工具列表定义
tools = [
    {
        "type": "function",
        "function": {
            "name": "search_products",
            "description": "根据各种标准搜索产品目录",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "搜索词或产品名称"
                    },
                    "category": {
                        "type": "string",
                        "description": "用于筛选的产品类别",
                        "enum": ["electronics", "clothing", "home", "outdoor"]
                    },
                    "max_price": {
                        "type": "number",
                        "description": "以美元为单位的最高价格"
                    }
                },
                "required": ["query"]
            }
        }
    }
]

一般使用流程:

  1. 随API请求发送tools列表
  2. 解析模型的tool_calls响应
  3. 使用提供的参数执行匹配的函数
  4. 将结果返回给模型
Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐