【AI大模型应用开发】2.1 Function Calling连接外部世界 - 入门与实战(1)

Function Calling是大模型连接外部世界的通道,目前出现的插件(Plugins )、OpenAI的Actions、各个大模型平台中出现的tools工具集,其实都是Function Calling的范畴。时下大火的OpenAI的GPTs,原理就是使用了Function Calling,例如联网检索、code interpreter。

本文带大家了解下Function calling,看它是如何让大模型能与外部世界连接的。

0. 接口形式

写过程序的人可能都懂接口是什么,这里再简述一下接口的形式。

  • 目前常见的接口形式:
    • 命令行(Command Line Interface),简称 CLI(DOS、Unix/Linux shell, Windows Power Shell)
    • 图形界面(Graphical User Interface),简称 GUI(Windows、MacOS、iOS、Android)
  • AI时代的接口形式:用户通过自然语言与软件或系统交互,不用再点击按钮,按标准流程操作软件
    • 语言界面(Conversational User Interface),简称 CUI,或 Natural-Language User Interface,简称 LUI
  • 未来的接口形式:
    • 脑机接口(Brain–Computer Interface),简称 BCI

以前的接口调用,我们需要给定明确的接口名称和精确的参数。大模型时代的接口调用,我们只需要给出自然语言任务,大模型自动解析出参数和调用哪个接口。

1. Function Calling在AI大模型应用中的位置 - 架构

没有Function Calling的架构:

image.png

加入Function calling之后的架构:

image.png

2. 大模型为什么需要连接外部世界

其实大模型也不是万能的,它有三大缺陷:

  • 训练数据不可能涵盖所有信息。垂直、非公开数据必有欠缺。
  • 不知道最新信息。大模型的训练周期很长,且更新一次耗资巨大。所以它不可能实时训练。GPT-3.5 的知识截至 2022 年 1 月,GPT-4 是 2023 年 4 月。
  • 没有「真逻辑」。它表现出的逻辑、推理,是训练文本的统计规律,而不是真正的逻辑。也就是说,它的结果都是有一定不确定性的,这对于需要精确和确定结果的领域,如数学等,是灾难性的,基本是不可用的。

比如算加法:

  • 把 100 以内所有加法算式都训练给大模型,它就能回答 100 以内的加法算式
  • 如果问它更大数字的加法,就不一定对了 因为它并不懂「加法」,只是记住了 100 以内的加法算式的统计规律

所以:大模型需要连接真实世界,并对接真逻辑系统,以此来控制大模型输出的不确定性和幻觉,达到我们想要的结果。

3. 实战

3.1 调用本地函数

3.1.1 定义一个自定义的本地函数,也可以是现有的库中的函数

以Python内置的sum函数为例,假设我们想让大模型使用这个函数。

sum函数介绍,接收一个列表、元组或集合:

image.png

3.1.2 告诉大模型这个函数的存在
python代码解读复制代码def get_completion(messages, model="gpt-3.5-turbo-1106"):
    response = openai.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,
        max_tokens=1024,
        tools=[
            { # 用 JSON 描述函数。可以定义多个。由大模型决定调用谁
                "type": "function",
                "function": {
                    "name": "sum",
                    "description": "计算一组数的和",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "numbers": {
                                "type": "array",
                                "items": {
                                    "type": "number"
                                }
                            }
                        }
                    }
                }
            },
        ]
    )
    return response.choices[0].message

代码解释:

  • 还是我们熟悉的openai.chat.completions.create接口,这次我们需要使用的是tools参数
  • 将本地的函数用json描述,添加到tools参数中

注意:Function Calling 中的函数与参数的描述description也是一种 Prompt。这种 Prompt 也需要调优,否则会影响函数的召回、参数的准确性,甚至让 GPT 产生幻觉

3.1.3 给一个需要使用该函数的Prompt

我们用自然语言给一个做加法的需求:

python代码解读复制代码prompt = "桌上有 2 个苹果,四个桃子和 3 本书,一共有几个水果?"

messages = [
    {"role": "system", "content": "你是一个数学家,你可以计算任何算式。"},
    {"role": "user", "content": prompt}
]
response = get_completion(messages)
messages.append(response) # 注意这一句,必须加入到上下文中,否则报错
print("=====GPT回复=====")
print(response)

运行看下这时候大模型的返回: 在这里插入图片描述 可以看到返回了函数的名称和函数的参数。

3.1.4 解析函数名称和参数

当大模型返回了需要调用的名称和参数之后,我们可以通过本地代码解析出来,然后再去调用相应函数。

python代码解读复制代码if (response.tool_calls is not None):
    for tool_call in response.tool_calls:
        print(response.tool_calls)
        print(f"调用 {tool_call.function.name} 函数,参数是 {tool_call.function.arguments}")
        if tool_call.function.name == "sum":
            # 调用 sum 函数(本地函数或库函数,非chatgpt),打印结果
            args = json.loads(tool_call.function.arguments)
            result = sum(args["numbers"])

        print("=====函数返回=====")
        print(result)

image.png

3.1.5 再次调用大模型获取最终结果

本地函数执行完得到结果后,再将这个结果给大模型,让大模型用自然语言组织起最终答案。

这里需要怎么给大模型呢?需要将函数调用结果,tool_call_id,role,name等一起加入到prompt中。

python代码解读复制代码# 把函数调用结果加入到对话历史中
messages.append(
    {
        "tool_call_id": tool_call.id,  # 用于标识函数调用的 ID
        "role": "tool",
        "name": "sum",
        "content": str(result)  # 数值result 必须转成字符串
    }
)

# 再次调用大模型
print("=====最终回复=====")
print(get_completion(messages).content)

image.png

经测试,tool_call_id和role是必须参数,name可以不要,但最好也加上。

3.1.6 完整代码
python代码解读复制代码import json
import os
from math import *
import openai
# 加载 .env 到环境变量
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())

def get_completion(messages, model="gpt-3.5-turbo-1106"):
    response = openai.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,
        max_tokens=1024,
        tools=[
            { # 用 JSON 描述函数。可以定义多个。由大模型决定调用谁
                "type": "function",
                "function": {
                    "name": "sum",
                    "description": "计算一组数的和",
                    "parameters": {
                        "type": "object",
                        "properties": {
                            "numbers": {
                                "type": "array",
                                "items": {
                                    "type": "number"
                                }
                            }
                        }
                    }
                }
            },
        ]
    )
    return response.choices[0].message

prompt = "桌上有 2 个苹果,四个桃子和 3 本书,一共有几个水果?"

messages = [
    {"role": "system", "content": "你是一个数学家,你可以计算任何算式。"},
    {"role": "user", "content": prompt}
]
response = get_completion(messages)
# 把大模型的回复加入到对话历史中
messages.append(response) # 注意这一句,必须加入到上下文中,否则报错
print("=====GPT回复=====")
print(response)

# 如果返回的是函数调用结果,则打印出来
if (response.tool_calls is not None):
    for tool_call in response.tool_calls:
        print(response.tool_calls)
        print(f"调用 {tool_call.function.name} 函数,参数是 {tool_call.function.arguments}")
        if tool_call.function.name == "sum":
            # 调用 sum 函数(本地函数或库函数,非chatgpt),打印结果
            args = json.loads(tool_call.function.arguments)
            result = sum(args["numbers"])

        print("=====函数返回=====")
        print(result)

        # 把函数调用结果加入到对话历史中
        messages.append(
            {
                "tool_call_id": tool_call.id,  # 用于标识函数调用的 ID
                "role": "tool",
                "name": "sum",
                "content": str(result)  # 数值result 必须转成字符串
            }
        )

    # 再次调用大模型
    print("=====最终回复=====")
    print(get_completion(messages).content)

3.2 多Function的调用

这里以一个查询某个地点附近某些信息的需求为例。

3.2.1 定义本地函数

这里我们需要定义自己的本地函数,不再使用Python的库函数了。

下面的代码,我们定义了两个函数。

  • get_location_coordinate用于查询某个地点的地理坐标。
  • search_nearby_pois用于查询地理坐标附近的某些信息(取决于用户输入的Keyword)
python代码解读复制代码def get_location_coordinate(location, city="北京"):
    url = f"https://restapi.amap.com/v5/place/text?key={amap_key}&keywords={location}&region={city}"
    print(url)
    r = requests.get(url)
    result = r.json()
    if "pois" in result and result["pois"]:
        return result["pois"][0]
    return None


def search_nearby_pois(longitude, latitude, keyword):
    url = f"https://restapi.amap.com/v5/place/around?key={amap_key}&keywords={keyword}&location={longitude},{latitude}"
    print(url)
    r = requests.get(url)
    result = r.json()
    ans = ""
    if "pois" in result and result["pois"]:
        for i in range(min(3, len(result["pois"]))):
            name = result["pois"][i]["name"]
            address = result["pois"][i]["address"]
            distance = result["pois"][i]["distance"]
            ans += f"{name}\n{address}\n距离:{distance}米\n\n"
    return ans

这是用的高德地图的开放接口,在使用本例之前,你需要先去高德地图开放接口的官网申请一个key,免费的。这里就不过多介绍了。

在这里插入图片描述

3.2.2 告诉大模型这两个函数的存在
python代码解读复制代码def get_completion(messages, model="gpt-3.5-turbo-1106"):
    response = openai.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,
        max_tokens=1024,
        tools=[{
            "type": "function",
            "function": {
                "name": "get_location_coordinate",
                "description": "根据POI名称,获得POI的经纬度坐标",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "POI名称,必须是中文",
                        },
                        "city": {
                            "type": "string",
                            "description": "POI所在的城市名,必须是中文",
                        }
                    },
                    "required": ["location", "city"],
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "search_nearby_pois",
                "description": "搜索给定坐标附近的poi",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "longitude": {
                            "type": "string",
                            "description": "中心点的经度",
                        },
                        "latitude": {
                            "type": "string",
                            "description": "中心点的纬度",
                        },
                        "keyword": {
                            "type": "string",
                            "description": "目标poi的关键字",
                        }
                    },
                    "required": ["longitude", "latitude", "keyword"],
                }
            }
        }]
    )
    return response.choices[0].message
3.2.3 使用示例
python代码解读复制代码prompt = "北京三里屯附近的咖啡"

messages = [
    {"role": "system", "content": "你是一个地图通,你可以找到任何地址。"},
    {"role": "user", "content": prompt}
]
response = get_completion(messages)
if (response.content is None):  # 解决 OpenAI 的一个 400 bug
    response.content = ""
messages.append(response)  # 把大模型的回复加入到对话中
print("=====GPT回复=====")
print(response)

# 如果返回的是函数调用结果,则打印出来
while (response.tool_calls is not None):
    # 1106 版新模型支持一次返回多个函数调用请求
    for tool_call in response.tool_calls:
        args = json.loads(tool_call.function.arguments)
        print(args)

        if (tool_call.function.name == "get_location_coordinate"):
            print("Call: get_location_coordinate")
            result = get_location_coordinate(**args)
        elif (tool_call.function.name == "search_nearby_pois"):
            print("Call: search_nearby_pois")
            result = search_nearby_pois(**args)

        print("=====函数返回=====")
        print(result)

        messages.append({
            "tool_call_id": tool_call.id,  # 用于标识函数调用的 ID
            "role": "tool",
            "name": tool_call.function.name,
            "content": str(result)  # 数值result 必须转成字符串
        })

    response = get_completion(messages)
    if (response.content is None):  # 解决 OpenAI 的一个 400 bug
        response.content = ""
    messages.append(response)  # 把大模型的回复加入到对话中

print("=====最终回复=====")
print(response.content)

看下执行过程和结果:

image.png

(1)首先大模型识别到应该先调用get_location_coordinate函数获取经纬度。

(2)get_location_coordinate执行结果给到大模型,大模型识别到下一步应该调用search_nearby_pois

(3)search_nearby_pois执行结果给到大模型,大模型识别到不需要调用其它函数,用自然语言组织了最终答案。

3.2.4 完整代码
python代码解读复制代码import json
import os
import openai
import requests
# 加载 .env 到环境变量
from dotenv import load_dotenv, find_dotenv
_ = load_dotenv(find_dotenv())

amap_key = os.getenv('AMAP_KEY')

def get_completion(messages, model="gpt-3.5-turbo-1106"):
    response = openai.chat.completions.create(
        model=model,
        messages=messages,
        temperature=0,
        max_tokens=1024,
        tools=[{
            "type": "function",
            "function": {

                "name": "get_location_coordinate",
                "description": "根据POI名称,获得POI的经纬度坐标",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "POI名称,必须是中文",
                        },
                        "city": {
                            "type": "string",
                            "description": "POI所在的城市名,必须是中文",
                        }
                    },
                    "required": ["location", "city"],
                }
            }
        },
        {
            "type": "function",
            "function": {
                "name": "search_nearby_pois",
                "description": "搜索给定坐标附近的poi",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "longitude": {
                            "type": "string",
                            "description": "中心点的经度",
                        },
                        "latitude": {
                            "type": "string",
                            "description": "中心点的纬度",
                        },
                        "keyword": {
                            "type": "string",
                            "description": "目标poi的关键字",
                        }
                    },
                    "required": ["longitude", "latitude", "keyword"],
                }
            }
        }]
    )
    return response.choices[0].message

def get_location_coordinate(location, city="北京"):
    url = f"https://restapi.amap.com/v5/place/text?key={amap_key}&keywords={location}&region={city}"
    print(url)
    r = requests.get(url)
    result = r.json()
    if "pois" in result and result["pois"]:
        return result["pois"][0]
    return None

def search_nearby_pois(longitude, latitude, keyword):
    url = f"https://restapi.amap.com/v5/place/around?key={amap_key}&keywords={keyword}&location={longitude},{latitude}"
    print(url)
    r = requests.get(url)
    result = r.json()
    ans = ""
    if "pois" in result and result["pois"]:
        for i in range(min(3, len(result["pois"]))):
            name = result["pois"][i]["name"]
            address = result["pois"][i]["address"]
            distance = result["pois"][i]["distance"]
            ans += f"{name}\n{address}\n距离:{distance}米\n\n"
    return ans
    

prompt = "北京三里屯附近的咖啡"

messages = [
    {"role": "system", "content": "你是一个地图通,你可以找到任何地址。"},
    {"role": "user", "content": prompt}
]
response = get_completion(messages)
if (response.content is None):  # 解决 OpenAI 的一个 400 bug
    response.content = ""
messages.append(response)  # 把大模型的回复加入到对话中
print("=====GPT回复=====")
print(response)

# 如果返回的是函数调用结果,则打印出来
while (response.tool_calls is not None):
    # 1106 版新模型支持一次返回多个函数调用请求
    for tool_call in response.tool_calls:
        args = json.loads(tool_call.function.arguments)
        print("参数:", args)

        if (tool_call.function.name == "get_location_coordinate"):
            print("Call: get_location_coordinate")
            result = get_location_coordinate(**args)
        elif (tool_call.function.name == "search_nearby_pois"):
            print("Call: search_nearby_pois")
            result = search_nearby_pois(**args)

        print("=====函数返回=====")
        print(result)

        messages.append({
            "tool_call_id": tool_call.id,  # 用于标识函数调用的 ID
            "role": "tool",
            "name": tool_call.function.name,
            "content": str(result)  # 数值result 必须转成字符串
        })

    response = get_completion(messages)
    if (response.content is None):  # 解决 OpenAI 的一个 400 bug
        response.content = ""
    print("=====GPT回复2=====")
    print(response)
    messages.append(response)  # 把大模型的回复加入到对话中

print("=====最终回复=====")
print(response.content)

4. 总结

通过本文的两个实战示例,是否已经对Function calling有了一个初步的认识?

  • 其实就是将函数说明组织成json形式告诉大模型。其中最重要的函数和参数描述,是该函数的prompt,大模型通过这个描述来确定用户的输入是否匹配该函数,是否召回该函数。
  • 大模型如果召回了某个函数,那么我们就可以在本地去解析函数名和参数去使用,从而完成大模型与外部世界的连接。

人工智能\大模型入门学习大礼包》,可以关注工棕耗:大模型星球
回🎀复:11即🉑️精准或取❕!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/872824.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C++ | Leetcode C++题解之第355题设计推特

题目&#xff1a; 题解&#xff1a; class Twitter {struct Node {// 哈希表存储关注人的 Idunordered_set<int> followee;// 用链表存储 tweetIdlist<int> tweet;};// getNewsFeed 检索的推文的上限以及 tweetId 的时间戳int recentMax, time;// tweetId 对应发送…

828华为云征文 | 华为云Flexus X实例上实现Docker容器的实时监控与可视化分析

Docker容器监控之 CAdvisorInfluxDBGranfana 需要了解 本文章主要讲述在 华为云Flexus X 实例上搭建开源的容器管理平台&#xff0c;使用的Web UI界面来简化和优化容器及集群的管理和监控选择合适的云服务器&#xff1a; 本文采用的是 华为云服务器 Flexus X 实例&#xff08;…

Prefetch文件分析

目录 介绍步骤 介绍 Prefetch&#xff08;预读取&#xff09;&#xff0c;从Windows XP开始引入&#xff0c;用来加速应用程序启动过程。Prefetch包含可执行文件的名称、文件时间戳、运行次数、上次执行时间、Hash等。Win7上记录最近128个可执行文件的信息&#xff0c;Win8-10…

正点原子STM32F103+ESP8266+DS18B20+DHT11连接阿里云

文章目录 MQTT协议1. 基础知识2. 报文形式3. 连接报文4. 心跳报文5. 订阅报文5.1. 订阅主题报文SUBSCRIBE5.2. 订阅确认SUBACK5.3. 取消订阅UNSUBSCRIBE5.4. 取消订阅确认UNSUBACK 6. 发布报文6.1. 发布消息PUBLISH6.2. 发布确认PUBACK 7. 阿里云账号创建8. 网络调试助手接入阿…

Java | Leetcode Java题解之第389题找不同

题目&#xff1a; 题解&#xff1a; class Solution {public char findTheDifference(String s, String t) {int ret 0;for (int i 0; i < s.length(); i) {ret ^ s.charAt(i);}for (int i 0; i < t.length(); i) {ret ^ t.charAt(i);}return (char) ret;} }

Matplotlib 颜色设置详解

在使用matplotlib进行颜色绘制的时候,如绘制图表、背景色或者对文字设置的时候都可以配置颜色, 以下说明主流的三种颜色使用方法 颜色名称 可以是直接使用颜色名称的字符串对color进行赋值,包括可以使用首字母缩写或者完整拼写的形式,以下为部分颜色的书写形式 缩写版 • …

Spring Boot 多数据源配置(JPA)

目录 前言 前置环境 pom yml Entity Dao Config Controller 演示 前言 一般一个系统至少有一个数据源&#xff0c;用来持久化业务数据以及查询。单个数据源的系统很常见&#xff0c;在 Spring Boot 框架下配置也很简单。在约定大于配置这个思想下&#xff0c;只需要在…

递推,CF 353D - Queue

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 353D - Queue 二、解题报告 1、思路分析 手玩一下&#xff0c;我们发现相…

[数据集][目标检测]人脸口罩佩戴目标检测数据集VOC+YOLO格式8068张3类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;8068 标注数量(xml文件个数)&#xff1a;8068 标注数量(txt文件个数)&#xff1a;8068 标注…

uniapp写的一个年月日时分秒时间选择功能

代码: <template><view><picker mode"multiSelector" :value"multiIndex" :range"multiRange" change"onMultiChange"><view class"picker">当前选择&#xff1a;{{ formattedDateTime }}</vie…

VisualStudio环境搭建C++

Visual Studio环境搭建 说明 C程序编写中&#xff0c;经常需要链接头文件(.h/.hpp)和源文件(.c/.cpp)。这样的好处是&#xff1a;控制主文件的篇幅&#xff0c;让代码架构更加清晰。一般来说头文件里放的是类的申明&#xff0c;函数的申明&#xff0c;全局变量的定义等等。源…

大路灯护眼灯有必要吗安全吗?性价比高落地护眼灯推荐

大路灯护眼灯有必要吗安全吗&#xff1f;近几年来&#xff0c;随着生活节奏的加快&#xff0c;目前青少年的近视率呈现一个直线上升的趋势&#xff0c;其中占比达到了70%以上&#xff0c;并且最令人意外的是小学生竟然也占着比较大的比重&#xff0c;这一系列的数据不仅表明着近…

MySQL(CRUD)

MySQL mysql -u root -ply MySQL的三层结构 1.安装MySQL数据库本质就是在主机安装一个数据库管理系统(DBMS),这个管理程序可以管理多个数据库. 2.一个数据库中可以创建多个表,以保存数据 SQL语句分类 1.DDL:数据定义语句[create 表,库] 2.DML:数据操作语句[增加insert,修改…

【Java】基于JWT+Token实现完整登入功能(实操)

Java系列文章目录 补充内容 Windows通过SSH连接Linux 第一章 Linux基本命令的学习与Linux历史 文章目录 Java系列文章目录一、前言二、学习内容&#xff1a;三、问题描述四、解决方案&#xff1a;4.1 认识依赖4.2 使用JWT4.3 登入实现4.4 配置拦截器4.5 获取数据 五、总结&…

EMC技术

目录 EMC 天线效应 公式 措施 EMC测试 展频技术 如何展频 OTA测试 EMC 三大要素&#xff1a;干扰源、传输介质、敏感设备。 EMI&#xff1a;Electromagnetic Interference&#xff0c;电磁干扰。 EMS&#xff1a;Electro Magnetic Susceptibility&#xff0c;电磁抗扰…

二百五十九、Java——采集Kafka数据,解析成一条条数据,写入另一Kafka中(一般JSON)

一、目的 由于部分数据类型频率为1s&#xff0c;从而数据规模特别大&#xff0c;因此完整的JSON放在Hive中解析起来&#xff0c;尤其是在单机环境下&#xff0c;效率特别慢&#xff0c;无法满足业务需求。 而Flume的拦截器并不能很好的转换数据&#xff0c;因为只能采用Java方…

JVM系列(十) -垃圾收集器介绍

一、摘要 在之前的几篇文章中,我们介绍了 JVM 内部布局、对象的创建过程、运行期的相关优化手段以及垃圾对象的回收算法等相关知识。 今天通过这篇文章,结合之前的知识,我们一起来了解一下 JVM 中的垃圾收集器。 二、垃圾收集器 如果说收集算法是内存回收的方法论,那么…

集成电路学习:什么是NOR Flash Memory非易失性闪存存储器

一、NOR Flash Memory&#xff1a;非易失性闪存存储器 NOR Flash Memory&#xff0c;即非易失性闪存存储器的一种&#xff0c;是Flash存储器的一个重要分支。Flash存储器&#xff0c;又称为闪存&#xff0c;结合了ROM&#xff08;只读存储器&#xff09;和RAM&#xff08;随机存…

Windows下Python和PyCharm的应用(三)__Numpy与矩阵

1、背景介绍 矩阵运算是Python语言的基石。 而支持矩阵运算的基础语言包就是Numpy。 参考链接&#xff1a; Python中Numpy的使用_numpy在python中的用法-CSDN博客 这篇博客介绍的numpy比我的这篇博客介绍的更加的详细。本博客只是根据本人 的实际应用&#xff0c;对最关键的…

Clean Minimalist GUI Pack (简约风格UI界面)

Unity 最简洁易用的 GUI 资源包。如果你在寻找资源商店上 UI 极简主义革命的发起者,你已经找到了。 这一极干净简约的 GUI 资源包是一款适合移动设备使用的游戏 UI 资源包,其中包含许多图标和元素,可用于创建具有简洁风格的完整游戏 UI。 功能: • 包括 3 种皮肤:深色、浅…