当前位置:   article > 正文

智能体Agents:开启AI助手的无限可能_ai agents

ai agents

1.什么是Agents ?

        Agents 是一个具有智能功能的智能体,它使用 LLM 和工具来执行任务。

        Agents 核心思想是使用LLM来选择要采取的一系列动作。在链式结构中,一系列动作是硬编码的(在代码中)。 在 Agents 中,使用语言模型作为推理引擎来确定要采取的动作及其顺序。

        Agents 包括几个关键组件:

Agent

用于生成指令和执行动作的代理。

Tool

用于执行动作的函数。

Memory

用于存储历史对话和生成的指令。

LLM

用于存储历史对话和生成的指令 LLM。

2.Agents的简单示例

        使用Langchain库来构建一个能够回答问题的智能体,并利用通义大模型和SERPAPI搜索引擎工具来获取信息

 

        2.1 搭建工具

        我们这里使用serpai来对搜索进行实现

        serpai是一个聚合搜索引擎,需要安装谷歌搜索包以及申请账号,免费但需要魔法,有一定的次数限制。

                    https://serpapi.com/manage-api-key

        2.2 安装依赖

       pip install google-search-results

        2.3 具体实现代码

  1. import os
  2. # 设置环境变量,这是使用SERPAPI搜索引擎工具所需的API密钥
  3. os.environ["SERPAPI_API_KEY"] = '申请的serapikey'
  4. from langchain_community.llms.tongyi import Tongyi
  5. # 初始化通义大模型
  6. llm = Tongyi()
  7. from langchain.agents import load_tools
  8. from langchain.agents import initialize_agent
  9. from langchain.agents import AgentType
  10. # 加载工具,这里包括了搜索引擎(serpapi)和数学计算工具(llm-math)
  11. tools = load_tools(["serpapi","llm-math"], llm=llm)
  12. # 初始化智能体,这里使用的是零样本反应描述类型的智能体
  13. agent = initialize_agent(
  14. tools,
  15. llm,
  16. agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, # 指定智能体类型
  17. verbose=True, # 设置为True以打印日志,可以看到智能体的思考过程
  18. )
  19. # 直接调用通义大模型来回答问题
  20. llm.invoke("请问2025年的美国总统是谁?他的年龄的除以2是多少?")
  21. # 运行智能体来回答问题,智能体会使用加载的工具来帮助回答
  22. agent.run("请问现任的美国总统是谁?他的年龄的除以2是多少?")

        2.4 运行结果

     llm的结果

           '截至2023年,我无法预测2024年美国总统选举的结果,因为这取决于选举结果。美国的总统选举通常在11月的第一个星期二举行,因此2024年的选举将在11月5日举行。\n\n关于年龄的问题,由于我不知道届时谁会当选,也无法提供当前可能候选人的具体年龄。但假设一位候选人当选时是50岁,那么他年龄除以2就是25。请记住,这只是一个例子,实际的年龄和结果需要根据未来的选举情况来确定。'

    agent的思考过程(verbose=True)

>Entering new AgentExecutor chain... 我需要找到现任美国总统的名字,然后计算他的年龄除以2的结果。这需要我使用搜索来获取当前总统的信息,然后用计算器来计算年龄。

Action: Search Action

Input: "current president of the United States"

Observation: Joe Biden

Thought:现在我知道了现任美国总统是乔·拜登(Joe Biden)。我需要搜索他的年龄。 Action: Search Action Input: "Joe Biden age" Observation: 81 years

Thought:我找到了乔·拜登的年龄是81岁。现在我可以用计算器算出他年龄除以2的结果。 Action: Calculator Action Input: 81 / 2

Observation: Answer: 40.5 Thought:我得到了结果,现任美国总统乔·拜登的年龄除以2是40.5。

Final Answer: 现任美国总统乔·拜登(Joe Biden)的年龄除以2是40.5岁。

> Finished chain.

            agent的运行结果

 '现任美国总统乔·拜登(Joe Biden)的年龄除以2是40.5岁。'

        2.5 `agent.run()` 方法和 `agent.invoke()`的区别

在Langchain库中,`agent.run()` 方法和 `agent.invoke()` 方法的区别主要在于它们的使用场景和目的。
1. **agent.run()**:
   - `agent.run()` 方法是智能体的主要接口,用于处理和回答用户的问题或执行任务。
   - 当你调用 `agent.run()` 时,智能体会分析传入的问题,决定是否需要使用工具来获取更多信息,然后根据获取的信息生成回答。
   - 这个方法通常用于完整的问答循环,包括问题理解、工具调用、信息整合和回答生成。
2. **agent.invoke()**:
   - `agent.invoke()` 方法通常是在智能体内部使用,用于调用特定的工具来执行任务或获取信息。
   - 当智能体决定需要使用工具时,它会调用 `agent.invoke()` 方法来执行工具的具体操作。
   - 这个方法不是直接暴露给用户的接口,而是智能体在执行 `agent.run()` 方法时内部使用的。
简而言之,`agent.run()` 是用户与智能体交互的入口点,它负责处理整个问题和回答的过程,而 `agent.invoke()` 是智能体内部用来调用具体工具的方法,用于辅助完成 `agent.run()` 所负责的任务。用户通常只与 `agent.run()` 方法交互,而 `agent.invoke()` 方法则是由智能体自身在需要时调用。
 

         2.6 thought        

        简单的说智能体提供了一个框架,使大型语言模型能够更加智能地使用外部资源和工具,从而在各种任务中提供更加强大和准确的功能。

3.Agents 的类型

ZERO_SHOT_REACT_DESCRIPTION 零样本反应描述
CHAT_ZERO_SHOT_REACT_DESCRIPTION聊天零样本反应描述
CONVERSATIONAL_REACT_DESCRIPTION会话反应描述
CHAT_CONVERSATIONAL_REACT_DESCRIPTION聊天会话反应描述
STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION聊天结构化零样本反应描述
STRUCTURED_ZERO_SHOT_REACT_DESCRIPTION结构化零样本反应描述

        3.1 ZERO_SHOT_REACT_DESCRIPTION

        在没有示例的情况下可以自主的进行对话的类型

  1. # 定义tools
  2. tools = load_tools(["serpapi","llm-math"],llm=llm)
  3. # 定义agent--(tools、agent、llm、memory)
  4. agent = initialize_agent(
  5. tools,
  6. llm,
  7. agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
  8. verbose=True,
  9. )
  10. print(agent)
  11. print("------------------------")
  12. print(agent.agent.llm_chain.prompt.template)
  13. agent.invoke("现在美国总统是谁?他的年龄除以2是多少?")

        3.2 CHAT_ZERO_SHOT_REACT_DESCRIPTION

        零样本增强式生成,即在没有示例的情况下可以自主的进行对话的类型

  1. tools = load_tools(["serpapi","llm-math"],llm=llm)
  2. agent = initialize_agent(
  3. tools,
  4. llm,
  5. agent=AgentType.CHAT_ZERO_SHOT_REACT_DESCRIPTION,
  6. verbose=True,
  7. )
  8. print(agent)
  9. print("------------------------")
  10. print(agent.agent.llm_chain.prompt.messages[0].prompt.template)
  11. print("------------------------")
  12. agent.invoke("现在美国总统是谁?他的年龄除以2是多少?")

相较于ZERO_SHOT_REACT_DESCRIPTION,此类型更侧重于对话和交互式环境。它不仅能够生成反应描述,还能够生成自然语言回复来与用户进行交互。

         3.3 CONVERSATIONAL_REACT_DESCRIPTION

        一个对话型的agent,这个agent要求与memory一起使用

  1. from langchain.memory import ConversationBufferMemory
  2. #记忆组件
  3. memory = ConversationBufferMemory(
  4. memory_key="chat_history",
  5. )
  6. # 定义tool
  7. tools = load_tools(["serpapi","llm-math"],llm=llm)
  8. # 定义agent
  9. agent = initialize_agent(
  10. tools,
  11. llm,
  12. agent=AgentType.CONVERSATIONAL_REACT_DESCRIPTION,
  13. memory=memory,#记忆组件
  14. verbose=True,
  15. )
  16. print(agent)
  17. print(agent.agent.llm_chain.prompt.template)
  18. agent.run("我是张三,今年18岁,性别女,现在在深圳工作,工作年限1年,月薪5000元")
agent.run("我的名字是什么?")
agent.run("有什么好吃的泰国菜可以推荐给我吗?")

         3.4 CHAT_CONVERSATIONAL_REACT_DESCRIPTION 使用了chatmodel

  1. from langchain.memory import ConversationBufferMemory
  2. #记忆组件
  3. memory = ConversationBufferMemory(
  4. memory_key="chat_history",
  5. return_messages=True,
  6. )
  7. tools = load_tools(["serpapi","llm-math"],llm=llm)
  8. agent = initialize_agent(
  9. tools,
  10. llm,
  11. agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION,
  12. memory=memory,#记忆组件
  13. verbose=True,
  14. )
  15. print(agent)
  16. print("1 ------------------------")
  17. print(len(agent.agent.llm_chain.prompt.messages))
  18. print("2 ------------------------")
  19. print(agent.agent.llm_chain.prompt.messages[0].prompt.template)
  20. print("3 ------------------------")
  21. print(agent.agent.llm_chain.prompt.messages[1])
  22. print("4 ------------------------")
  23. print(agent.agent.llm_chain.prompt.messages[2].prompt.template)
  24. print("5 ------------------------")
  25. print(agent.agent.llm_chain.prompt.messages[3])
  26. agent.run("有什么好吃的泰国菜可以推荐给我吗?用中文回答")

3.5 STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION

对输出做了结构化处理

 

  1. from langchain.memory import ConversationBufferMemory
  2. #记忆组件
  3. memory = ConversationBufferMemory(
  4. memory_key="chat_history",
  5. return_messages=True,
  6. )
  7. ## 定义tool
  8. tools = load_tools(["serpapi","llm-math"],llm=llm)
  9. # 定义agent
  10. agent = initialize_agent(
  11. tools,
  12. llm,
  13. agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION, #agent类型
  14. memory=memory,#记忆组件
  15. handle_parsing_errors=True,
  16. verbose=True,
  17. )
  18. print(agent)
  19. print(agent.agent.llm_chain.prompt.messages[0].prompt.template)
  20. print(agent.agent.llm_chain.prompt.messages[1].prompt.template)
  21. agent.run("有什么好吃的泰国菜可以推荐给我吗?用中文回答")

对于其他类型的agent,这里不做赘述。

4 Tools 自定义工具

langchain预制了大量的tools,基本这些工具能满足大部分需求。 https://python.langchain.com.cn/docs/modules/agents/tools/

添加预制工具的方法很简单

from langchain.agents import load_tools

tool_names = [...]

tools = load_tools(tool_names) #使用load方法

#有些tool需要单独设置llm

from langchain.agents import load_tools

tool_names = [...]

llm = ...

tools = load_tools(tool_names, llm=llm) #在load的时候指定llm

SerpAPI在上面的例子中已经做了简单示例

下面是 简单的一种Dall-E使用方式,

Dall-E是openai出品的文到图AI大模型,需要设置apikey

pip install opencv-python scikit-image
  1. from langchain.agents import initialize_agent, load_tools
  2. import os
  3. os.environ["openai_api_key"] = ''
  4. tools = load_tools(["dalle-image-generator"])
  5. agent = initialize_agent(
  6. tools,
  7. llm,
  8. agent="zero-shot-react-description",
  9. verbose=True
  10. )
  11. agent.run("Create an image of a halloween night at a haunted museum")

5 自定义agents

  1. from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser
  2. from langchain.prompts import StringPromptTemplate
  3. from langchain import SerpAPIWrapper, LLMChain
  4. from typing import List, Union
  5. from langchain.schema import AgentAction, AgentFinish, OutputParserException
  6. from langchain_community.llms.tongyi import Tongyi
  7. import re
  8. import os
  9. class MyAgentTool:
  10. def __init__(self) -> None:
  11. os.environ["SERPAPI_API_KEY"] = ''
  12. self.serpapi = SerpAPIWrapper()
  13. def tools(self):
  14. return [
  15. Tool(
  16. name="search",
  17. description="适用于当你需要回答关于当前事件的问题时",
  18. func=self.serpapi.run,
  19. )
  20. ]
  21. s = MyAgentTool()
  22. s.serpapi.run("python")
  23. from typing import Any
  24. class MyAgent:
  25. llm = None
  26. tools = None
  27. def __init__(self) -> None:
  28. #agent的提示词,用来描述agent的功能
  29. self.template = """尽你最大可能回答下面问题,你将始终用中文回答. 你在必要时可以使用下面这些工具:
  30. {tools}
  31. Use the following format:
  32. Question: the input question you must answer
  33. Thought: you should always think about what to do
  34. Action: the action to take, should be one of [{tool_names}]
  35. Action Input: the input to the action
  36. Observation: the result of the action
  37. ... (this Thought/Action/Action Input/Observation can repeat N times)
  38. Thought: I now know the final answer
  39. Final Answer: the final answer to the original input question
  40. Begin! 记住使用中文回答,如果你使用英文回答将回遭到惩罚.
  41. Question: {input}
  42. {agent_scratchpad}"""
  43. #定义一个openai的llm
  44. self.llm = Tongyi()
  45. #工具列表
  46. self.tools = self.MyAgentTool().tools()
  47. #agent的prompt
  48. self.prompt = self.MyTemplate(
  49. template=self.template,
  50. tools=self.tools,
  51. #输入变量和中间变量
  52. input_variables=["input", "intermediate_steps"],
  53. )
  54. #定义一个LLMChain
  55. self.llm_chain = LLMChain(
  56. llm=self.llm,
  57. prompt = self.prompt
  58. )
  59. #工具名称列表
  60. self.toolnames = [tool.name for tool in self.tools]
  61. #定义一个agent
  62. self.agent = LLMSingleActionAgent(
  63. llm_chain=self.llm_chain,
  64. allowed_tools=self.toolnames,
  65. output_parser=self.MyOutputParser(),
  66. stop=["\nObservation:"],
  67. )
  68. #运行agent
  69. def run(self, input: str) -> str:
  70. #创建一个agent执行器
  71. agent_executor = AgentExecutor.from_agent_and_tools(
  72. agent=self.agent,
  73. tools=self.tools,
  74. handle_parsing_errors=True,
  75. verbose=True
  76. )
  77. agent_executor.run(input=input)
  78. #自定义工具类
  79. class MyAgentTool:
  80. def __init__(self) -> None:
  81. os.environ["SERPAPI_API_KEY"] = ''
  82. self.serpapi = SerpAPIWrapper()
  83. def tools(self):
  84. return [
  85. Tool(
  86. name="search",
  87. description="适用于当你需要回答关于当前事件的问题时",
  88. func=self.serpapi.run,
  89. )
  90. ]
  91. #自定义模版渲染类
  92. class MyTemplate(StringPromptTemplate):
  93. #渲染模版
  94. template: str
  95. #需要用到的工具
  96. tools:List[Tool]
  97. #格式化函数
  98. def format(self, **kwargs: Any) -> str:
  99. #获取中间步骤
  100. intermediate_steps = kwargs.pop("intermediate_steps")
  101. thoughts = ""
  102. for action, observation in intermediate_steps:
  103. thoughts += action.log
  104. thoughts += f"\nObservation: {observation}\nThought: "
  105. #将agent_scratchpad设置为该值
  106. kwargs["agent_scratchpad"] = thoughts
  107. # 从提供的工具列表中创建一个名为tools的变量
  108. kwargs["tools"] = "\n".join([f"{tool.name}: {tool.description}" for tool in self.tools])
  109. #创建一个提供的工具名称列表
  110. kwargs["tool_names"] = ", ".join([tool.name for tool in self.tools])
  111. return self.template.format(**kwargs)
  112. #自定义输出解析类
  113. class MyOutputParser(AgentOutputParser):
  114. #解析函数
  115. def parse(self, output: str) -> Union[AgentAction, AgentFinish]:
  116. #检查agent是否应该完成
  117. if "Final Answer:" in output:
  118. return AgentFinish(
  119. # 返回值通常始终是一个具有单个 `output` 键的字典。
  120. # It is not recommended to try anything else at the moment :)
  121. return_values={"output": output.split("Final Answer:")[-1].strip()},
  122. log=output,
  123. )
  124. #用正则解析出动作和动作输入
  125. regex = r"Action\s*\d*\s*:(.*?)\nAction\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
  126. match = re.search(regex, output, re.DOTALL)
  127. #如果没有匹配到则抛出异常
  128. if not match:
  129. raise OutputParserException(f"Could not parse LLM output: `{output}`")
  130. action = match.group(1).strip()
  131. action_input = match.group(2)
  132. # 返回操作和操作输入
  133. return AgentAction(tool=action, tool_input=action_input.strip(" ").strip('"'), log=output)
  134. myagent = MyAgent()
  135. myagent.run("请问现任的美国总统是谁?他的年龄的除以2是多少?")

6 案例

  1. from langchain.agents import Tool, tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser
  2. from langchain.prompts import StringPromptTemplate
  3. from langchain import SerpAPIWrapper, LLMChain
  4. from typing import List, Union
  5. from langchain.schema import AgentAction, AgentFinish, OutputParserException
  6. from langchain_community.llms.tongyi import Tongyi
  7. from langchain_community.embeddings.dashscope import DashScopeEmbeddings
  8. # 引入向量化的类
  9. from langchain_community.vectorstores import Chroma
  10. import re
  11. import os
  12. from typing import Any
  13. #自定义工具类
  14. class MyAgentTool:
  15. def __init__(self) -> None:
  16. self.db = Chroma(embedding_function=DashScopeEmbeddings(),
  17. persist_directory="./chroma")
  18. def tools(self):
  19. return [
  20. Tool(
  21. name="kg_search",
  22. description="当你需要回答2024年NBA冠军球队的问题时使用",
  23. func=self.search
  24. )
  25. ]
  26. def search(self, query: str) -> str:
  27. return self.db.similarity_search(query,k=2)
  28. def getdb(self):
  29. return self.db.__len__()
  30. #可用工具
  31. @tool
  32. def kgg_search(query: str):
  33. """当你需要回答2024年NBA冠军球队的问题时才会使用这个工具。"""
  34. db = Chroma(embedding_function=DashScopeEmbeddings(),persist_directory="./chroma")
  35. return db.similarity_search(query,k=2)
  36. tool_list = [kgg_search]
  37. class MyAgent:
  38. llm = None
  39. tools = None
  40. def __init__(self,llm,tools) -> None:
  41. #agent的提示词,用来描述agent的功能
  42. self.template = """尽你最大可能回答下面问题,你将始终用中文回答. 你在必要时可以使用下面这些工具:
  43. {tools}
  44. 使用下面的格式回答问题:
  45. 问题: 输入的问题
  46. 分析: 你对问题的分析,决定是否使用工具
  47. 动作: 使用工具,工具名称 [{tool_names}]
  48. 输入: {input}
  49. 观察: 动作名称
  50. ...
  51. 分析: 我现在知道答案了
  52. 答案: 这是最终的答案
  53. 记住使用中文回答,如果你使用英文回答将回遭到惩罚.
  54. 问题: {input}
  55. {agent_scratchpad}"""
  56. #定义一个openai的llm
  57. self.llm = llm
  58. #工具列表
  59. self.tools = tools
  60. #agent的prompt
  61. self.prompt = self.MyTemplate(
  62. template=self.template,
  63. tools=self.tools,
  64. #输入变量和中间变量
  65. input_variables=["input", "intermediate_steps"],
  66. )
  67. #定义一个LLMChain
  68. self.llm_chain = LLMChain(
  69. llm=self.llm,
  70. prompt = self.prompt
  71. )
  72. #工具名称列表
  73. self.toolnames = [tool.name for tool in self.tools]
  74. #定义一个agent
  75. self.agent = LLMSingleActionAgent(
  76. llm_chain=self.llm_chain,
  77. allowed_tools=self.toolnames,
  78. output_parser=self.MyOutputParser(),
  79. stop=["\n观察:"],
  80. )
  81. #运行agent
  82. def run(self, input: str) -> str:
  83. #创建一个agent执行器
  84. agent_executor = AgentExecutor.from_agent_and_tools(
  85. agent=self.agent,
  86. tools=self.tools,
  87. handle_parsing_errors=True,
  88. verbose=True
  89. )
  90. # agent_executor = AgentExecutor(agent=self.agent, tools=self.tools, verbose=True,handle_parsing_errors=True)
  91. agent_executor.run(input=input)
  92. #自定义模版渲染类
  93. class MyTemplate(StringPromptTemplate):
  94. #渲染模版
  95. template: str
  96. #需要用到的工具
  97. tools:List[Tool]
  98. #格式化函数
  99. def format(self, **kwargs: Any) -> str:
  100. #获取中间步骤
  101. intermediate_steps = kwargs.pop("intermediate_steps")
  102. thoughts = ""
  103. for action, observation in intermediate_steps:
  104. thoughts += action.log
  105. thoughts += f"\n观察: {observation}\n想法: "
  106. #将agent_scratchpad设置为该值
  107. kwargs["agent_scratchpad"] = thoughts
  108. # 从提供的工具列表中创建一个名为tools的变量
  109. kwargs["tools"] = "\n".join([f"{tool.name}: {tool.description}" for tool in self.tools])
  110. #创建一个提供的工具名称列表
  111. kwargs["tool_names"] = ", ".join([tool.name for tool in self.tools])
  112. prompt_ret = self.template.format(**kwargs)
  113. return prompt_ret
  114. #自定义输出解析类
  115. class MyOutputParser(AgentOutputParser):
  116. #解析函数
  117. def parse(self, output: str) -> Union[AgentAction, AgentFinish]:
  118. #检查agent是否应该完成
  119. if "答案:" in output:
  120. return AgentFinish(
  121. # 返回值通常始终是一个具有单个 `output` 键的字典。
  122. # It is not recommended to try anything else at the moment :)
  123. return_values={"output": output.split("答案:")[-1].strip()},
  124. log=output,
  125. )
  126. #用正则解析出动作和动作输入
  127. regex = r"动作\s*\d*\s*:(.*?)\n输入\s*\d*\s*:[\s]*(.*)"
  128. match = re.search(regex, output, re.DOTALL)
  129. #如果没有匹配到则抛出异常
  130. if not match:
  131. raise OutputParserException(f"Could not parse LLM output: `{output}`")
  132. action = match.group(1).strip()
  133. action_input = match.group(2)
  134. # 返回操作和操作输入
  135. return AgentAction(tool=action, tool_input=action_input.strip(" ").strip('"'), log=output)
  136. myagent = MyAgent(Tongyi(),tool_list)
  137. myagent.run("2024年NBA冠军球队是哪只?")
  138. # tool = MyAgentTool()
  139. # tool.search("2024年NBA冠军球队是哪只")
  1. from langchain.agents import initialize_agent, AgentType
  2. agent = initialize_agent(
  3. MyAgentTool().tools(),
  4. Tongyi(),
  5. agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
  6. verbose=True,
  7. )
  8. # agent.run("2024年NBA冠军球队是哪只?")
  9. agent.invoke("2024年NBA冠军球队是哪只?")

本文旨在介绍智能体Agents的概念、功能及应用前景,以增进读者对这一新兴技术的了解。文章中所提及的智能体Agents、大型语言模型(LLM)等均为当前科技领域的研究成果,相关数据和信息来源于公开资料。文章内容仅供参考,如有涉及具体技术细节,请以官方发布信息为准。

本文所涉及的智能体Agents及相关技术,其应用和发展可能受到法律法规、伦理道德等多方面因素的影响。在实际应用中,应遵循相关法规和道德准则,确保技术的正当、合法使用。

本文版权属于作者所有,未经许可,禁止转载、摘编、复制及建立镜像等任何形式的传播。如需转载,请务必联系作者获取授权。侵权必究。

作者保留对本文内容的解释权。如有任何疑问或建议,请通过正规渠道与作者联系。感谢您的理解与支持!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/正经夜光杯/article/detail/853692
推荐阅读
相关标签
  

闽ICP备14008679号