【大模型系列篇】解锁谷歌AI Agent智能体技术白皮书,跨入智能体时代

智能体时代

人类擅长处理复杂和微妙的模式识别任务。 然而能做到这一点,我们往往需要借助书籍、搜索或计算器之类的工具来补充我们头脑中的先验知识, 从而做出最终的判断。正如人类一样,生成式 AI 模型也可以通过训练使用工具,来获取实时信息或建议现实世界中的行动。

  • 模型可以利用数据库查询工具来获取特定信息,如获取客户的购物历史,然后给出购物建议。
  • 根据用户的查询,模型可以调用相应 API,替用户回复电子邮件或完成金融交易。

为此,模型不仅需要访问外部工具,还要能够自主规划和执行任务。 这种将推理、逻辑和访问外部信息相结合的方式,都与生成式 AI 模型相关联,从而引出了对“智能体(Agent)”的概念,即一个扩展了生成式 AI 模型出厂能力的程序。

什么是 Agent?

宽泛地来讲,智能体是一种应用程序,它能通过观察周围世界,并利用其可支配的工具来实现特定目标。智能体具备自主性,能够在没有人类干预的情况下独立行动,甚至在没有明确的人类指令集时,也能主动推理下一步如何实现最终目标。

  • Agent 是有自主能力的(autonomous),只要提供了合适的目标,它们就能独立行动,无需人类干预;
  • 即使是模糊的人类指令,Agent 也可以推理出它接下来应该做什么,并采取行动,最终实现其目标。

在 AI 领域,Agent 是一个非常通用的概念。 指的是基于生成式 AI 模型能够实现的 Agents。

智能体的行为、动作和决策由其内部的认知架构驱动。一个典型的智能体的认知架构构成包括三个核心组件,如图所示:

 

模型(model)

指的是用作 Agent 中用来做核心决策的语言模型(LM)。

  • 可以是一个或多个任何大小的模型,能够遵循基于指令的推理和逻辑框架,如 ReAct、Chain-of-Thought、Tree-of-Thoughts
  • 可以是通用的、多模态的,或根据特定 Agent 架构的需求微调得到的模型。
  • 可以通过“能展示 Agent 能力的例子或数据集”来进一步微调模型,例如 Agent 在什么上下文中使用什么工具,或者执行什么推理步骤。

工具(tool)

基础模型在文本和图像生成方面非常强大,但无法与外部世界联动极大限制了它们的能力。 工具的出现解决了这一问题。有了工具,Agent 便能够与外部数据和服务互动,大大扩展了它们的行动范围。

工具可以有多种形式,常见是 Web API 方式,即 GET、POST、PATCH 和 DELETE 方法。 例如,结合用户信息和获取天气数据的 tool,Agent 可以为用户提供旅行建议。

有了工具,Agent 可以访问和处理现实世界的信息,这使它们能够支撑更专业的系统,如检索增强生成(RAG),显著扩展了 Agent 的能力。

编排(orchestration)

编排描述了一个循环过程:Agent 如何接收信息,如何进行内部推理,如何使用推理来结果来指导其下一步行动或决策。

  • 一般来说,这个循环会持续进行,直到 Agent 达到其目标或触发停止条件。
  • 编排的复杂性跟 Agent 及其执行的任务直接相关,可能差异很大。 例如,一些编排就是简单的计算和决策规则,而其他的可能包含链式逻辑、额外的机器学习算法或其他概率推理技术。

智能体 vs. 模型

模型 智能体
知识范围 知识仅限于其训练数据。 通过工具连接外部系统,能够在模型自带的知识之外,实时、动态扩展知识。
状态与记忆 无状态,每次推理都跟上一次没关系,除非在外部给模型加上会话历史或上下文管理能力。 有状态,自动管理会话历史,根据编排自主决策进行多轮推理。
原生工具 非原生工具实现 有,自带工具和对工具的支持能力。
原生逻辑层 无。需要借助提示词工程或使用推理框架(CoT、ReAct 等)来形成复杂提示,指导模型进行预测。 有,原生认知架构,内置 CoT、ReAct 等推理框架或 LangChain 等编排框架。

认知架构:智能体如何运作?

打个比方:厨师做菜

        想象厨房中一群忙碌的厨师。他们的职责是根据顾客的菜单,为顾客烹制相应的菜品。 这就涉及到我们前面提到的“规划 —— 执行 —— 调整”循环。具体来说, 厨师们需要执行以下步骤,

  1. 收集信息(输入):顾客点的菜,后厨现有的食材等等;
  2. 推理(思考):根据收集到的信息,判断可以做哪些菜;
  3. 做菜(行动):包括切菜、加调料、烹炒等等。

        在以上每个阶段,厨师都根据需要进行调整 —— 例如某些食材不够用了,或者顾客反馈好吃或难吃了 —— 进而不断完善他们的计划。 这个信息接收、规划、执行和调整(information intake, planning, executing, and adjusting)的循环描述的就是一个厨师用来实现其目标的特定认知架构。

智能体推理框架

智能体可以使用认知架构,通过迭代处理信息、做出明智的决策以及根据先前的输出来改进下一步的行动,从而达到最终目标。智能体认知架构的核心是编排层,负责维护记忆、状态、推理和计划。它利用快速发展的提示工程领域和相关框架来指导推理和计划,使智能体能够更有效地与其环境互动并完成任务。提示工程框架和语言模型任务规划领域的研究正在迅速发展,产生了各种有希望的方法。虽然这不是一个详尽的列表,但以下是本文发布时一些最流行的框架和推理技术:

  • ReAct,一种提示工程框架,为语言模型提供了一种思维过程策略,使其能够对用户查询进行推理并采取行动,无论有没有上下文示例。ReAct 提示已被证明优于多个 SOTA 基线,并提高了人类对大语言模型的可操作性和可信度。
  • Chain-of-Thought (CoT),一种提示工程框架,通过中间步骤实现推理能力。CoT 有各种子技术,包括自洽性、主动提示和多模态 CoT,它们各自根据具体应用具有优势和劣势。
  • Tree-of-Thoughts (ToT),一种提示工程框架,非常适合探索或战略前瞻任务。它概括了思维链提示,并允许模型探索各种思维链,这些思维链充当使用语言模型进行一般问题解决的中间步骤。

ReAct 的示例

智能体可以利用上述推理技术之一,或许多其他技术,来选择给定用户请求的最佳下一步行动。例如,让我们考虑一个被编程为使用 ReAct 框架来为用户查询选择正确行动和工具的智能体。事件的顺序可能如下所示:

  1. 用户向 Agent 发送查询。

  2. Agent 开始 ReAct sequence。

  3. Agent 提示模型,要求其生成下一个 ReAct 步骤及其相应的输出:

    • 问题:提示词 + 用户输入的问题

    • 思考:模型的想法:下一步应该做什么

    • 行动:模型的决策:下一步要采取什么行动。这里就是可以引入工具的地方, 例如,行动可以是 [Flights, Search, Code, None] 中的一个,前三个代表模型可以选择的已知工具,最后一个代表“无工具选择”。

    • 行动的输入:模型决定是否要向工具提供输入,如果要提供,还要确定提供哪些输入

    • 观察:行动/行动输入序列的结果。根据需要,这个思考/行动/行动输入/观察(thought / action / action input / observation)可能会重复 N 次。

    • 最终答案:模型返回对原始用户查询的最终答案。

  4. ReAct 循环结束,并将最终答案返回给用户。

编排层中使用 ReAct 推理的示例智能体

模型、工具和智能体配置协同工作,根据用户的原始查询向用户提供基于事实的简洁响应。虽然模型可以根据其先前的知识猜测(幻觉)答案,但它却使用工具(航班)来搜索实时外部信息。此附加信息提供给模型,使其能够根据真实的事实数据做出更明智的决策,并将此信息总结返回给用户。

总而言之,智能体响应的质量可以直接与模型推理和执行各种任务的能力相关联,包括选择正确工具的能力以及工具的定义程度。就像厨师用新鲜食材烹制菜肴并关注客户反馈一样,智能体依靠健全的推理和可靠的信息来提供最佳结果

工具:模型通往现实世界的关键

虽然语言模型很擅长处理信息,但它们缺乏直接感知和影响现实世界的能力。 在需要与外部系统或数据联动的情况下,这些模型的实用性就很低了。某种意义上说, 语言模型的能力受限于它们的训练数据中覆盖到的信息。那么,如何赋予模型与外部系统进行实时、上下文感知的互动能力呢? 目前有几种方式:

  • Functions

  • Extensions

  • Data Stores

  • Plugins

虽然名称各异,但它们都统称为工具(tools)。 工具是将基础模型与外部世界连接起来的桥梁。能够连接到外部系统和数据之后,智能体便能够执行更广泛的任务,并且结果更加准确和可靠。 例如,工具使智能体能够调整智能家居设置、更新日程、从数据库中获取用户信息或根据特定指令发送电子邮件。

谷歌模型能够与之交互的主要工具有三种:扩展、函数和数据存储。通过配备工具之后,智能体解锁了理解真实世界和在真实世界中做出行动的超能力, 从而打开了各种新应用场景和可能性的大门。

扩展

扩展以标准化的方式桥接了 API 与智能体,使智能体能够无缝执行 API,无需关心其底层实现。例如,若要构建一个帮助用户预订航班的智能体,使用 Google Flights API 获取航班信息时,通过扩展,智能体可以利用示例学习如何使用 API 端点以及成功调用 API 所需的参数。

传统方法是实现自定义代码,该代码将接收传入的用户查询,解析查询以获取相关信息,然后进行 API 调用。例如,在航班预订用例中,用户可能会说“我想预订从奥斯汀到苏黎世的航班”。在这种情况下,我们的自定义代码解决方案需要从用户查询中提取“奥斯汀”和“苏黎世”作为相关实体,然后再尝试进行 API 调用。但是,如果用户说“我想预订飞往苏黎世的航班”,而没有提供出发城市,该怎么办?如果没有所需的数据,API 调用将失败,并且需要实现更多代码才能捕获此类边缘和极端情况。这种方法维护性和扩展性都很差,并且在任何超出已实现自定义代码范围的情况下都容易崩溃。

另一种更具弹性的方法是使用扩展。扩展通过以下方式弥合智能体和 API 之间的差距:

  1. 使用示例教导智能体如何使用 API。

  2. 教导智能体成功调用API所需的参数或参数。

扩展可以独立于智能体进行制作,但应作为智能体配置的一部分提供。智能体在运行时使用模型和示例来决定哪个扩展(如果有)适合解决用户的查询。这突出了扩展的一个关键优势,即其内置的示例类型(built-in example types),允许智能体动态选择最适合任务的扩展。如下图所示:

智能体、扩展和 API 之间的一对多关系

将其视为软件开发人员在解决用户问题和提供解决方案时决定使用哪些 API 的方式。如果用户想要预订航班,开发人员可能会使用 Google Flights API。如果用户想知道离他们最近的咖啡店在哪里,开发人员可能会使用 Google Maps API。同样,智能体/模型堆栈使用一组已知的扩展来决定哪个最适合用户的查询。如果您想了解扩展的实际应用,可以在 Gemini 应用程序中尝试它们,方法是转到“设置”>“扩展”,然后启用您想要测试的任何扩展。例如,您可以启用 Google Flights 扩展,然后询问 Gemini “显示下周五从奥斯汀飞往苏黎世的航班”。

Extensions 示例

为了简化扩展的使用,Google 提供了一些开箱即用的扩展,可以快速导入到您的项目中,并以最少的配置使用。以 Google 的 Code Interpreter extension 作为例子,从自然语言描述生成和运行 Python 代码。

import vertexai
import pprint

PROJECT_ID = "YOUR_PROJECT_ID"
REGION = "us-central1"

vertexai.init(project=PROJECT_ID, location=REGION)

from vertexai.preview.extensions import Extension

extension_code_interpreter = Extension.from_hub("code_interpreter")

CODE_QUERY = """Write a python method to invert a binary tree in O(n) time."""
response = extension_code_interpreter.execute(
    operation_id="generate_and_execute",
    operation_params={"query": CODE_QUERY}
)

print("Generated Code:")
pprint.pprint(response['generated_code'])

输出如下:

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def invert_binary_tree(root):
    """Inverts a binary tree."""
    if not root:
        return None
    # Swap the left and right children recursively
    root.left, root.right = invert_binary_tree(root.right), invert_binary_tree(root.left)
    return root

# Example usage:
# Construct a sample binary tree
root = TreeNode(4)
root.left = TreeNode(2)
root.right = TreeNode(7)
root.left.left = TreeNode(1)
root.left.right = TreeNode(3)
root.right.left = TreeNode(6)
root.right.right = TreeNode(9)

# Invert the binary tree
inverted_root = invert_binary_tree(root)

函数

在软件工程领域,“函数”被定义为完成特定任务且可以根据需要重复使用的自包含代码模块。当软件开发人员编写程序时,他们通常会创建许多函数来执行各种任务。他们还会定义何时调用函数A与函数B的逻辑,以及预期的输入和输出。

函数在智能体领域的工作方式非常相似,但我们可以用模型代替软件开发人员。模型可以接收一组已知的函数,并根据其规范决定何时使用每个函数以及函数需要哪些参数。

还是以前面的 Google Flights 为例,可以看出 Function 与 Extension 的不同:

函数与扩展(Extensions)在几个方面有所不同,最显著的是:

  1. 模型只输出一个函数及其参数,但不进行实时的API调用。

  2. 函数在客户端执行,而扩展在智能体端执行

时序图

展示函数调用生命周期的序列图

Functions 示例

定义 Function

def display_cities(cities: list[str], preferences: Optional[str] = None):
    """Provides a list of cities based on the user's search query and preferences.

    Args:
        preferences (str): The user's preferences for the search, like skiing, beach, restaurants, bbq, etc.
        cities (list[str]): The list of cities being recommended to the user.

    Returns:
        list[str]: The list of cities being recommended to the user.
    """
    return cities

接下来,初始化模型和工具函数 Function Calling ,然后将用户的查询和工具传递给模型。

from vertexai.generative_models import GenerativeModel, Tool, FunctionDeclaration

model = GenerativeModel("gemini-1.5-flash-001")
display_cities_function = FunctionDeclaration.from_func(display_cities)
tool = Tool(function_declarations=[display_cities_function])

message = "I’d like to take a ski trip with my family but I’m not sure where to go. "
res = model.generate_content(message, tools=[tool])

print(f"Function Name: {res.candidates[0].content.parts[0].function_call.name}")
print(f"Function Args: {res.candidates[0].content.parts[0].function_call.args}")

效果:

> Function Name: display_cities
> Function Args: {'preferences': 'skiing', 'cities': ['Aspen', 'Vail', 'Park City']}

总结起来,Function 提供了一个简单的框架,使应用程序开发人员能够

  • 对数据流和系统执行进行细粒度的控制。

  • 利用 Agent 和模型生成结构化的信息,方便作为下一步的输入。

数据存储

语言模型就像一个大图书馆,其中包含了其训练数据(信息)。但与真实世界的图书馆不同的是, 这个图书馆是静态的 —— 不会更新,只包含其最初训练时的知识。 而现实世界的知识是不断在演变的,所以静态模型在解决现实世界问题时就遇到了挑战

数据存储通过提供对更动态、最新信息的访问来解决语言模型知识静态的限制,确保模型的响应基于事实性和相关性。数据存储允许开发者以原始格式向智能体提供额外数据,无需耗时的数据转换、模型重新训练或微调。数据存储将传入文档转换为一组向量数据库嵌入,智能体可以使用这些嵌入来提取所需信息。

实现与应用

在生成式人工智能智能体的背景下,数据存储通常被实现为向量数据库,开发者希望智能体在运行时能够访问该数据库。虽然我们在此不会深入探讨向量数据库,但需要理解的关键点是,它们以向量嵌入的形式存储数据,这是一种高维向量或所提供数据的数学表示。最近,数据存储与语言模型结合使用的最显著的例子之一是基于检索增强生成(RAG)的应用程序的实现。这些应用程序旨在通过让模型访问各种格式的数据来扩展模型知识的广度和深度,例如:

智能体和数据存储之间的一对多关系,可以表示各种类型的预索引数据

  • 网站内容

  • 诸如 PDF、Word 文档、CSV、电子表格等格式的结构化数据

  • 诸如 HTML、PDF、TXT 等格式的非结构化数据

每个用户请求和 Agent 响应循环的基本过程通常如图所示

  1. 用户查询被发送到嵌入模型(embedding 模型)以生成查询的嵌入。

  2. 将查询嵌入与向量数据库的内容进行匹配,本质上就是在计算相似度。

  3. 匹配的内容以文本格式从向量数据库中检索出来,并发送回智能体。

  4. 智能体接收用户查询和检索到的内容,然后制定响应或操作。

  5. 向用户发送最终响应。

Data Stores 示例

最终的结果是一个应用程序,它允许智能体通过向量搜索将用户的查询与已知的数据存储进行匹配,检索原始内容,并将其提供给编排层和模型以进行进一步处理。下一个操作可能是向用户提供最终答案,或者执行额外的向量搜索以进一步细化结果。

ReAct 推理/规划的示例 RAG 应用程序

通过定向学习提升模型性能

有效使用模型的关键之一是其在生成输出时选择正确工具的能力,尤其是在生产中大规模使用工具时。虽然一般训练有助于模型发展这项技能,但现实世界的场景通常需要超出训练数据的知识。可以将其想象为基本烹饪技能和掌握特定菜系之间的区别。两者都需要基本的烹饪知识,但后者需要有针对性的学习才能获得更细致的结果。 为了帮助模型获取特定知识,存在几种方法:

  • 上下文学习(In-Context Learning):这种方法是在推理时为通用模型提供提示、工具和少量示例,使其能够 “即时” 学习如何以及何时为特定任务使用这些工具,ReAct 框架是自然语言中这种学习方法的一个例子。

  • 基于检索的上下文学习(Retrieval-Based In-Context Learning):该技术通过从外部存储器(例如 Vertex AI 扩展中的“示例存储”或前面提到的基于数据存储的 RAG 架构)中检索最相关的信息、工具和相关示例来动态填充模型提示。

  • 基于微调的学习(Fine-Tuning Learning):在推理之前,使用更大的特定示例数据集来训练模型,这有助于模型在接收任何用户查询之前了解何时以及如何应用某些工具。

方式

类比

In-context learning

厨师收到了一个特定的食谱(提示词)、一些食材(相关工具)和一些示例菜肴(少量示例)。基于这些信息和厨师已经具备的常规烹饪知识,“即时学习”如何准备最符合菜单和客户偏好的菜品。

Retrieval-based in-context learning

厨房里有一个储藏室(外部 Data Storage),里面有各种食材和食谱(示例和工具)。厨师可以从储藏室中自主选择更符合用户饮食偏好的食材和食谱,做出让用户更满意的菜品。

Fine-tuning based learning

把厨师送回学校学习新的菜系(在大量的特定示例数据集上进行训练)。如果希望厨师在特定菜系(知识领域)中表现出色,这种方法非常合适。

每种方法在速度、成本和延迟方面都各有优缺点,需要看实际需求组合使用。

基于LangChain快速创建Agent

LangChain 和 LangGraph 是两个流行的开源库,允许用户通过将逻辑、推理和工具调用序列“链接”在一起来构建自定义智能体,以回答用户的查询。我们将使用gemini-1.5-flash-001 模型和一些简单的工具来回答。

我们正在使用的工具是 SerpAPI(用于 Google 搜索)和 Google Places API。

from langgraph.prebuilt import create_react_agent
from langchain_core.tools import tool
from langchain_community.utilities import SerpAPIWrapper
from langchain_community.tools import GooglePlacesTool

os.environ["SERPAPI_API_KEY"] = "XXXXX"
os.environ["GPLACES_API_KEY"] = "XXXXX"

@tool
def search(query: str):
    """使用 SerpAPI 运行 Google 搜索。"""
    search = SerpAPIWrapper()
    return search.run(query)

@tool
def places(query: str):
    """使用 Google Places API 运行 Google Places 查询。"""
    places = GooglePlacesTool()
    return places.run(query)

model = ChatVertexAI(model="gemini-1.5-flash-001")
tools = [search, places]

query = "德克萨斯长角牛队上周的足球比赛对手是谁?对方球队的体育场地址是什么?"  
Agent = create_react_agent(model, tools)
input = {"messages": [("human", query)]}

for s in Agent.stream(input, stream_mode="values"):
    message = s["messages"][-1]
    if isinstance(message, tuple):
        print(message)
    else:
        message.pretty_print()

其中用到的工具包括:SerpAPI(用于 Google 搜索)、Google Places API。运行效果如下:

==================== Human Message ====================
德克萨斯长角牛队上周的橄榄球比赛对手是谁?对方球队的体育场地址是什么?
==================== Ai Message ====================
工具调用:搜索
参数:
query: 德克萨斯长角牛队橄榄球赛程
==================== Tool Message ====================
名称:搜索
{...结果:“NCAA一级橄榄球赛,佐治亚州,日期...”}
==================== Ai Message ====================
德克萨斯长角牛队上周与佐治亚斗牛犬队比赛。
工具调用:地点
参数:
query: 佐治亚斗牛犬队体育场
==================== Tool Message ====================
名称:地点
{...桑福德体育场地址:100 Sanford...}
==================== Ai Message ====================
佐治亚斗牛犬队体育场的地址是美国佐治亚州雅典市桑福德大道100号,邮编30602。

虽然这是一个相当简单的智能体示例,但它展示了模型、编排和工具所有组件协同工作以实现特定目标的基本要素。 

谷歌 Vertex AI Agent 创建生产应用

最后,我们来看看这些组件如何在像 Vertex AI Agent 和生成式剧本这样的 Google 规模的托管产品中结合在一起。

基于 Vertex AI 平台构建的端到端智能体示例架构

虽然这份白皮书探讨了智能体的核心组件,但构建生产级应用需要将它们与用户界面、评估框架和持续改进机制等其他工具集成。Google 的 Vertex AI 平台通过提供一个完全托管的环境简化了这一过程,该环境包含了前面提到的所有基本要素。借助自然语言界面,开发人员可以快速定义其智能体的关键要素 - 目标、任务指令、工具、用于任务委派的子智能体和示例 - 以轻松构建所需的系统行为。此外,该平台还提供了一套开发工具,用于测试、评估、衡量智能体性能、调试和提高已开发智能体的整体质量。这使得开发人员可以专注于构建和改进他们的智能体,而基础设施、部署和维护的复杂性则由平台本身管理。

在上图中,我们提供了一个基于 Vertex AI 平台构建的智能体示例架构,该架构使用了 Vertex Agent Builder、Vertex Extensions、Vertex Function Calling 和 Vertex Example Store 等多种功能。该架构包含了生产就绪应用所需的许多组件。

总结

在这份白皮书中,我们讨论了生成式人工智能智能体的基础构建模块、它们的构成以及以认知架构形式实现它们的有效方法。这份白皮书的一些主要结论包括:

  1. 智能体通过利用工具访问实时信息、建议现实世界的行动以及自主规划和执行复杂任务,扩展了语言模型的能力。智能体可以利用一个或多个语言模型来决定何时以及如何转换状态,并使用外部工具来完成模型自身难以或不可能完成的任何数量的复杂任务。

  2. 智能体运行的核心是编排层,这是一种认知架构,它构建了推理、规划、决策并指导其行动。诸如ReAct、思维链(CoT)和思维树(ToT)等各种推理技术为编排层提供了一个框架,用于接收信息、执行内部推理并生成明智的决策或响应。

  3. 诸如扩展、函数和数据存储之类的工具是智能体通往外部世界的钥匙,使它们能够与外部系统交互并访问超出其训练数据范围的知识。扩展(Extensions)提供了智能体和外部 API 之间的桥梁,从而能够执行 API 调用并检索实时信息。函数(Functions)通过分工为开发人员提供了更细致的控制,允许智能体生成可在客户端执行的函数参数。数据存储(Data Stores )为智能体提供了对结构化或非结构化数据的访问,从而支持数据驱动的应用程序。

智能体的未来充满令人兴奋的进步,我们才刚刚开始触及可能性的表面。随着工具变得更加复杂,推理能力得到增强,智能体将被赋予解决日益复杂问题的能力。此外,“智能体链”的战略方法将继续获得发展势头。通过结合专门的智能体(每个智能体都擅长特定的领域或任务),我们可以创建一种“混合智能体专家”方法,能够在各个行业和问题领域提供卓越的成果。

重要的是要记住,构建复杂的智能体架构需要迭代的方法。实验和改进是为特定业务案例和组织需求找到解决方案的关键。由于支撑其架构的基础模型的生成性质,没有两个智能体是相同的。然而,通过利用这些基础组件中每一个的优势,我们可以创建有影响力的应用程序,扩展语言模型的功能并驱动现实世界的价值。

猜你喜欢

转载自blog.csdn.net/Jackie_vip/article/details/146201305
今日推荐