大模型实战:如何使用图数据库提高向量搜索精确度?

文本嵌入和向量搜索技术可以帮助我们根据文档的含义及其相似性来检索文档。但当需要根据日期或类别等特定标准来筛选信息时,这些技术就显得力不从心。

为了解决这个问题,我们可以引入元数据过滤或过滤向量搜索,这允许我们根据用户的特定需求来缩小搜索范围。喜欢本文记得收藏、关注、点赞。

在这里插入图片描述

技术交流

技术要学会分享、交流,不建议闭门造车。一个人可以走的很快、一堆人可以走的更远。

成立了大模型面试和技术交流群,相关源码、资料、技术交流&答疑,均可加我们的交流群获取,群友已超过2000人,添加时最好的备注方式为:来源+兴趣方向,方便找到志同道合的朋友。

方式①、微信搜索公众号:机器学习社区,后台回复:加群
方式②、添加微信号:mlc2040,备注:来自CSDN + 技术交流

例如,用户可能想要了解 2021 年实施的新政策。通过使用元数据过滤器,系统可以先筛选出 2021 年的文档,然后在这些文档中执行向量相似性搜索,以找到与用户兴趣最相关的文档。这种先进行元数据过滤再执行向量搜索的两步策略,能够显著提高搜索的相关性和准确性。

近期,Neo4j 引入了基于节点属性的 LangChain 元数据过滤支持。由于图形数据库能够存储复杂的结构化和非结构化数据,我们可以利用这些数据来执行更精细的元数据过滤。

在这里插入图片描述

以一个包含文章和组织信息的数据集为例,文章节点包含了文本和嵌入值,而与文章相关联的组织节点则包含了日期、情感、作者等更多信息。通过这些信息,我们可以构建复杂的查询,以回答如

  • Rod Johnson 所在的公司是否实施了新的在家工作政策?

  • Neo4j 投资的公司是否有负面新闻?

  • 与为现代汽车供应的公司相关的供应链问题是否有任何值得注意的新闻?

等问题。

在本篇博客中,Tomaz Bratanic 将向我们展示如何结合 LangChain 和 OpenAI 函数调用代理来实现基于图的元数据过滤。

概览

我们将使用 Neo4j 托管的公共演示服务器上的 companies 图数据集。您可以通过以下凭据访问该数据集:

URI: https://demo.neo4jlabs.com:7473/browser/
用户名: companies
密码: companies
数据库: companies

在这里插入图片描述

数据集的完整模式包括以 Organization 节点为中心的丰富信息,涵盖供应商、竞争对手、位置、董事会成员等。此外,还有提及特定组织的文章及其相应的文本块。

我们将实现一个 OpenAI 代理,它可以根据用户输入动态生成 Cypher 语句,并从图形数据库检索相关文本块。这个工具将提供四个可选输入参数:

  • 主题:用户感兴趣的特定信息或主题。

  • 组织:用户希望查询信息的组织。

  • 国家:用户感兴趣的组织的国家。

  • 情感:文章的情感倾向。

我们将根据这些输入参数动态构建相应的 Cypher 语句,从图形数据库检索相关信息,并利用大型语言模型(LLM)生成最终答案。

要跟随代码实践,您将需要一个 OpenAI API 密钥。

功能实现

我们从设置 Neo4j 的连接凭证和相关连接开始。

import os

os.environ["OPENAI_API_KEY"] = "sk-"
os.environ["NEO4J_URI"] = "neo4j+s://demo.neo4jlabs.com"
os.environ["NEO4J_USERNAME"] = "companies"
os.environ["NEO4J_PASSWORD"] = "companies"
os.environ["NEO4J_DATABASE"] = "companies"

embeddings = OpenAIEmbeddings()
graph = Neo4jGraph()
vector_index = Neo4jVector.from_existing_index(
 embeddings,
 index_name="news"
)

我们使用 OpenAI 的文本嵌入技术,您需要一个 API 密钥来使用它。接下来,我们定义了与 Neo4j 的连接,这使我们能够执行任意的 Cypher 语句。最后,我们创建了一个 Neo4jVector 连接,它可以通过查询现有的向量索引来检索信息。目前,我们不能将向量索引与预过滤方法结合使用,只能与后过滤方法结合使用。但本文将专注于预过滤方法与全面向量相似性搜索的结合使用。

本文的核心是一个名为 get_organization_news 的函数,它能够根据用户的需求动态生成 Cypher 查询语句并检索相关信息。为了清晰起见,我将代码分成了多个部分。

  • 首先,我们定义了一组输入参数,这些参数都是可选的文字输入。特别地,topic 参数用来在文档中搜索特定的信息。在实际应用中,我们会将 topic 参数的值用于向量相似性搜索。另外三个参数则用于展示预过滤的方法。如果所有预过滤参数都没有提供,我们可以直接利用现有的向量索引来检索相关文档。如果提供了预过滤参数,我们会开始构建一个基础的 Cypher 查询语句,这个语句将用于后续的预过滤元数据方法。我们使用 CYPHER runtime = parallel parallelRuntimeSupport=all 指令来告诉 Neo4j 数据库,在可能的情况下使用 并行运行时。然后,我们准备一个匹配语句来选择 Chunk 节点和它们关联的 Article 节点。
def get_organization_news(
 topic: Optional[str] = None,
 organization: Optional[str] = None,
 country: Optional[str] = None,
 sentiment: Optional[str] = None,
) -> str:
 # 如果没有预过滤条件,我们可以直接使用向量索引进行搜索
 if topic and not organization and not country and not sentiment:
     return vector_index.similarity_search(topic)
 # 使用并行运行时(如果可用)
 base_query = (
     "CYPHER runtime = parallel parallelRuntimeSupport=all "
     "MATCH (c:Chunk)<-[:HAS_CHUNK]-(a:Article) WHERE "
 )
 where_queries = []
 params = {"k": 5}  # 设置要检索的文本块数量
  • 接下来,我们动态地向 Cypher 语句添加元数据过滤器。我们从 Organization 过滤器开始。
if organization:
    # 将组织名称映射到数据库中的候选项
    candidates = get_candidates(organization)
    if len(candidates) > 1:  # 如果候选选项太多,则需要用户进一步明确
        return f"请明确指出用户指的是以下哪个组织:{candidates}"
    # 添加一个过滤条件,筛选出提及特定组织的 articles
    where_queries.append(f"EXISTS {{(a)-[:MENTIONS]->(:Organization {{name: $organization}})}}")
    # 将组织名称作为参数传入
    params["organization"] = candidates[0]

如果系统识别出用户感兴趣的特定组织,我们会使用 get_candidates 函数将该组织的名称映射到数据库中的候选项。如果找到多个匹配项,我们会要求用户进一步明确。如果没有找到多个匹配项,我们会添加一个过滤条件,筛选出提及特定组织的 articles。为了安全起见,我们使用参数化查询而不是直接拼接查询字符串。

  • 随后,我们处理用户可能基于提及的组织的国家进行预过滤的情况。
if country:
    # 由于国家名称标准化,不需要额外的映射
    where_queries.append(f"EXISTS {{(a)-[:MENTIONS]->(:Organization)-[:IN_CITY]->()-[:IN_COUNTRY]->(:Country {{name: $country}})}}")
    params["country"] = country

由于国家名称通常是标准化的,我们不需要将国家名称映射到数据库中的值,因为大型语言模型(LLM)已经熟悉大多数国家的名称。

  • 随后,我们处理情感元数据的过滤。
if sentiment:
    if sentiment == "positive":
        where_queries.append("a.sentiment > $sentiment")
        params["sentiment"] = 0.5
    else:
        where_queries.append("a.sentiment < $sentiment")
        params["sentiment"] = -0.5

我们要求 LLM 仅接受正面或负面两种情感输入值,并将这些值映射到适当的过滤器上。

  • 对于 topic 参数,我们采取了略有不同的处理方式,因为它不用于预过滤,而是用于向量相似性搜索。
if topic:  # 执行向量比较
    vector_snippet = (
        "WITH c, a, vector.similarity.cosine(c.embedding,$embedding) AS score "
        "ORDER BY score DESC LIMIT toInteger($k)"
    )
    params["embedding"] = embeddings.embed_query(topic)
else:  # 只返回最新的数据
    vector_snippet = "WITH c, a ORDER BY a.date DESC LIMIT toInteger($k)"

如果系统识别出用户对新闻中的特定主题感兴趣,我们使用主题输入的文本嵌入来找到最相关的文档。如果没有识别出特定主题,我们简单地返回最新的几篇文章,并避免向量相似性搜索。

  • 最后,我们将 Cypher 语句组合起来,并用它来从数据库中检索信息。
return_snippet = "RETURN '#title ' + a.title + '\n#date ' + toString(a.date) + '\n#text ' + c.text AS output"

complete_query = (
    base_query + " AND ".join(where_queries) + vector_snippet + return_snippet
)

# 从数据库检索信息
data = graph.query(complete_query, params)
print(f"Cypher: {complete_query}\n")
# 在打印前安全地移除嵌入
params.pop('embedding', None)
print(f"参数: {params}")
return "###文章: ".join([el["output"] for el in data])

我们通过组合所有查询片段来构建最终的 complete_query。然后,我们使用动态生成的 Cypher 语句从数据库检索信息并返回结果。让我们通过一个示例输入来看看生成的 Cypher 语句。

get_organization_news(
  organization='neo4j',
  sentiment='positive',
  topic='远程工作'
)

# Cypher: CYPHER runtime = parallel parallelRuntimeSupport=all
# MATCH (c:Chunk)<-[:HAS_CHUNK]-(a:Article) WHERE 
# EXISTS {(a)-[:MENTIONS]->(:Organization {name: $organization})} AND 
# a.sentiment > $sentiment 
# WITH c, a, vector.similarity.cosine(c.embedding,$embedding) AS score 
# ORDER BY score DESC LIMIT toInteger($k) 
# RETURN '#title ' + a.title + '\n#date ' + toString(a.date) + '\n#text ' + c.text AS output

# 参数: {'k': 5, 'organization': 'Neo4j', 'sentiment': 0.5}

动态查询生成按预期工作,能够从数据库中检索到相关的信息。

构建新闻信息代理工具

接下来,我们将创建一个代理工具,用于处理新闻信息查询。首先,我们需要为输入参数编写一些说明。

fewshot_examples = """{输入:Google员工的健康福利在新闻中有哪些?查询:健康福利}
{输入:关于Google的最新正面新闻是什么?查询:无}
{输入:有关VertexAI和Google的新闻有哪些?查询:VertexAI}
{输入:关于Google的新产品有哪些新闻?查询:新产品}
"""

class NewsInput(BaseModel):
    topic: Optional[str] = Field(
        description="除了组织、国家和情感倾向之外,如果您对其他特定信息或话题感兴趣,请告诉我们。以下是一些示例:"
        + fewshot_examples
    )
    organization: Optional[str] = Field(
        description="您希望了解信息的组织名称"
    )
    country: Optional[str] = Field(
        description="您感兴趣的组织的所在国家。请使用正式的国家名称,例如‘美利坚合众国’或‘法国’。"
    )
    sentiment: Optional[str] = Field(
        description="您想要查询的文章情感倾向", enum=["正面", "负面"]
    )

在定义预过滤参数时,我遇到了一些困难,特别是如何让 topic 参数按预期工作。为了解决这个问题,我提供了一些示例,帮助语言模型更好地理解用户的需求。同时,我们还向模型提供了关于国家名称格式的指导,并对情感倾向选项进行了枚举。

现在,我们可以定义一个自定义工具,为其指定一个名称和一段包含使用说明的描述。

class NewsTool(BaseTool):
    name = "新闻信息工具"
    description = (
        "当你需要在新闻中查找相关信息时,这个工具会非常有用。"
    )
    args_schema:Type[BaseModel] = NewsInput

    def _run(
        self,
        topic: Optional[str] = None,
        organization: Optional[str] = None,
        country: Optional[str] = None,
        sentiment: Optional[str] = None,
        run_manager: Optional[CallbackManagerForToolRun] = None,
    ) -> str:
        ""“使用这个工具来获取新闻信息。”""
        return get_organization_news(topic, organization, country, sentiment)

最后,我们需要定义一个代理执行器。这里,我使用了之前实现的 OpenAI 代理的 LCEL 实现。

llm = ChatOpenAI(temperature=0, model="gpt-4-turbo", streaming=True)
tools = [NewsTool()]

llm_with_tools = llm.bind(functions=[format_tool_to_openai_function(t) for t in tools])

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            “你是一个乐于助人的助手,可以找到关于电影的信息并进行推荐。如果工具需要进一步的问题,请确保向用户询问以获得澄清。确保在后续问题中包含任何需要澄清的可用选项。只做用户明确请求的事情。”
        ),
        MessagesPlaceholder(variable_name="chat_history"),
        ("user", "{input}"),
        MessagesPlaceholder(variable_name="agent_scratchpad"),
    ]
)

agent = (
    {
        "input": lambda x: x["input"],
        "chat_history": lambda x: _format_chat_history(x["chat_history"])
        if x.get("chat_history")
        else [],
        "agent_scratchpad": lambda x: format_to_openai_function_messages(
            x["intermediate_steps"]
        ),
    }
    | prompt
    | llm_with_tools
    | OpenAIFunctionsAgentOutputParser()
)

agent_executor = AgentExecutor(agent=agent, tools=tools)

这个代理工具可以用于检索新闻信息。我们还添加了 聊天记录 消息占位符,这样代理就可以进行对话,并允许提出后续问题和回复。

实施测试

让我们尝试几个查询,看看生成的 Cypher 语句和参数是什么样的。

agent_executor.invoke(
  {"输入": "关于 neo4j 的一些正面新闻是什么?"}
)

# Cypher: CYPHER runtime = parallel parallelRuntimeSupport=all 
# MATCH (c:Chunk)<-[:HAS_CHUNK]-(a:Article) WHERE 
# EXISTS {(a)-[:MENTIONS]->(:Organization {name: $organization})} AND 
# a.sentiment > $sentiment WITH c, a 
# ORDER BY a.date DESC LIMIT toInteger($k) 
# RETURN '#标题 ' + a.title + '日期 ' + toString(a.date) + '文本 ' + c.text AS output
# 参数: {'k': 5, 'organization': 'Neo4j', 'sentiment': 0.5}

生成的 Cypher 语句是有效的。由于没有指定具体的主题,它返回了提到 Neo4j 的最后五篇正面文章的文本块。让我们尝试一个更复杂的例子:

agent_executor.invoke(
   {"输入": "关于法国公司的员工幸福感,有哪些最新的负面新闻?"}
)

# Cypher: CYPHER runtime = parallel parallelRuntimeSupport=all 
# MATCH (c:Chunk)<-[:HAS_CHUNK]-(a:Article) WHERE 
# EXISTS {(a)-[:MENTIONS]->(:Organization)-[:IN_CITY]->()-[:IN_COUNTRY]->(:Country {name: $country})} AND 
# a.sentiment < $sentiment 
# WITH c, a, vector.similarity.cosine(c.embedding,$embedding) AS score 
# ORDER BY score DESC LIMIT toInteger($k) 
# RETURN '#标题 ' + a.title + '日期 ' + toString(a.date) + '文本 ' + c.text AS output
# 参数: {'k': 5, 'country': 'France', 'sentiment': -0.5, 'topic': '员工幸福感'}

语言模型代理正确地生成了预过滤参数,并且还识别出了一个特定的“员工幸福感”主题。这个主题被用作向量相似性搜索的输入,使我们能够进一步优化检索过程。

总结

在这篇博客文章中,我们实现了基于图的元数据过滤器的示例,以提高向量搜索的准确性。数据集拥有广泛且相互关联的选项,这允许进行更精细的预过滤查询。结合图数据表示和语言模型的函数调用功能,可以动态生成 Cypher 语句,从而为结构化过滤器提供了几乎无限的可能性。

此外,你的代理可以拥有检索非结构化文本的工具,如本文所示,以及能够检索结构化信息的其他工具,这使得知识图谱成为许多 RAG应用的理想解决方案。

通俗易懂讲解大模型系列

  • 重磅消息!《大模型面试宝典》(2024版) 正式发布!

  • 重磅消息!《大模型实战宝典》(2024版) 正式发布!

  • 做大模型也有1年多了,聊聊这段时间的感悟!

  • 用通俗易懂的方式讲解:大模型算法工程师最全面试题汇总

  • 用通俗易懂的方式讲解:不要再苦苦寻觅了!AI 大模型面试指南(含答案)的最全总结来了!

  • 用通俗易懂的方式讲解:我的大模型岗位面试总结:共24家,9个offer

  • 用通俗易懂的方式讲解:大模型 RAG 在 LangChain 中的应用实战

  • 用通俗易懂的方式讲解:ChatGPT 开放的多模态的DALL-E 3功能,好玩到停不下来!

  • 用通俗易懂的方式讲解:基于扩散模型(Diffusion),文生图 AnyText 的效果太棒了

  • 用通俗易懂的方式讲解:在 CPU 服务器上部署 ChatGLM3-6B 模型

  • 用通俗易懂的方式讲解:ChatGLM3-6B 部署指南

  • 用通俗易懂的方式讲解:使用 LangChain 封装自定义的 LLM,太棒了

  • 用通俗易懂的方式讲解:基于 Langchain 和 ChatChat 部署本地知识库问答系统

  • 用通俗易懂的方式讲解:Llama2 部署讲解及试用方式

  • 用通俗易懂的方式讲解:一份保姆级的 Stable Diffusion 部署教程,开启你的炼丹之路

  • 用通俗易懂的方式讲解:LlamaIndex 官方发布高清大图,纵览高级 RAG技术

  • 用通俗易懂的方式讲解:为什么大模型 Advanced RAG 方法对于AI的未来至关重要?

  • 用通俗易懂的方式讲解:基于 Langchain 框架,利用 MongoDB 矢量搜索实现大模型 RAG 高级检索方法

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

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

相关文章

开源AI智能名片商城小程序:深度解读IMC(IP、MarTech、Content)视角

在数字化浪潮中&#xff0c;私域流量的运营已成为企业不可或缺的增长引擎。而开源AI智能名片商城小程序&#xff0c;则是以一种全新的视角——IMC&#xff08;IP、MarTech、Content&#xff09;&#xff0c;为企业打开私域流量运营的新篇章。今天&#xff0c;我们就来一起深入解…

Leetcode-17.04. 消失的数字

面试题 17.04. 消失的数字 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/missing-number-lcci/ 目录 面试题 17.04. 消失的数字 - 力扣&#xff08;LeetCode&#xff09; 题目 解题(注释) 第一种方法 第二种方法 第三种方法 题目 数组nums包含…

【GAMES 101】图形学入门——着色(Shading)

定义&#xff1a;将不同材质内容应用于不同物体对象上的过程。着色只考虑着色点的存在&#xff0c;不考虑其他物体的遮挡等&#xff0c;因此不考虑阴影处理 一些前期内容的定义&#xff1a; 着色点&#xff08;Shading Point&#xff09;观测方向&#xff08;Viewer Directio…

vue 脚手架 创建vue3项目

创建项目 命令&#xff1a;vue create vue-element-plus 选择配置模式&#xff1a;手动选择模式 (上下键回车) 选择配置&#xff08;上下键空格回车&#xff09; 选择代码规范、规则检查和格式化方式: 选择语法检查方式 lint on save (保存就检查) 代码文件中有代码不符合 l…

抄表自动化的实现与优势

1.界定与简述 抄表自动化是一种当代关键技术&#xff0c;致力于取代传统的手动式抄表方法&#xff0c;通过远程数据数据采集解决&#xff0c;完成电力工程、水、气等公用事业电力仪表的全自动载入。这一系统利用先进的感应器、物联网技术(IoT)设备及数据数据分析工具&#xff…

西圣全新磁吸无线充电宝强势上线:打开充电新方式,摆脱续航焦虑

在移动互联时代&#xff0c;智能手机、平板电脑等电子设备已经成为我们生活不可或缺的一部分。但随之而来的是电池续航问题的困扰&#xff0c;用户往往需要在户外、旅途或日常生活中频繁地充电。为了解决这一问题&#xff0c;充电宝作为便携式的移动充电设备&#xff0c;已经成…

leetCode61. 旋转链表

leetCode61. 旋转链表 题目思路&#xff1a;见如图所示 代码展示 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* Li…

触发器的启用和禁用

Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 在 Oracle 数据库中&#xff0c;所创建的触发器可以根据情况&#xff0c;灵活修改它的状态&#xff0c;使其有效或者无效&#xff0c;即启用或者禁用。 其语法格式如下所示。…

Mac好用又好看的终端iTerm2 + oh-my-zsh

Mac好用又好看的终端iTerm2 1. iTerm2的下载安装2. oh-my-zsh的安装2.1 官网安装方式2.2 国内镜像源安装方式 3. oh-my-zsh配置3.1 存放主题的路径3.2 存放插件的路径3.3 配置文件路径 1. iTerm2的下载安装 官网下载&#xff1a; iTerm2 2. oh-my-zsh的安装 oh-my-zsh是一…

Spirng 当中 Bean的作用域

Spirng 当中 Bean的作用域 文章目录 Spirng 当中 Bean的作用域每博一文案1. Spring6 当中的 Bean的作用域1.2 singleton 默认1.3 prototype1.4 Spring 中的 bean 标签当中scope 属性其他的值说明1.5 自定义作用域&#xff0c;一个线程一个 Bean 2. 总结:3. 最后&#xff1a; 每…

使用Ollama和OpenWebUI在CPU上玩转Meta Llama3-8B

2024年4月18日&#xff0c;meta开源了Llama 3大模型[1]&#xff0c;虽然只有8B[2]和70B[3]两个版本&#xff0c;但Llama 3表现出来的强大能力还是让AI大模型界为之震撼了一番&#xff0c;本人亲测Llama3-70B版本的推理能力十分接近于OpenAI的GPT-4[4]&#xff0c;何况还有一个4…

React的路由

1. 什么是前端路由 一个路径 path 对应一个组件 component 当我们在浏览器中访问一个 path 的时候&#xff0c;path 对应的组件会在页面中进行渲染 2. 创建路由开发环境 # 使用CRA创建项目 npm create-react-app react-router-pro# 安装最新的ReactRouter包 npm i react-ro…

人工智慧时代的引擎:揭开机器人核心零部件的奥秘

机器人核心零部件技术现状及趋势 工业机器人是我国制造业的“顶冠明珠”&#xff0c;在机器人核心零部件的研发制造上&#xff0c;我国在很多方面已经接近国际顶尖水平&#xff0c;但一些核心技术仍无法满足复杂高端领域应用需求&#xff0c;如精密减速器的传动精度与寿命间竞争…

「玻尔曾孙」领衔!超辐射原子,重塑全球精准测时——

超辐射原子能够帮助我们以前所未有的精度测量时间。在哥本哈根大学最近的一项研究中&#xff0c;研究人员开发了一种新的测量时间间隔&#xff08;秒&#xff09;的方法&#xff0c;这种方法克服了目前最先进原子钟面临的一些限制。 这一成就有望在多个领域产生深远影响&#x…

C语言进阶|双链表

✈链表的分类 链表的结构非常多样&#xff0c;以下情况组合起来就有8种&#xff08;2x22&#xff09;链表结构: 虽然有这么多的链表的结构&#xff0c;但是我们实际中最常用还是两种结构&#xff1a;单链表和双向带头循环链表 1.无头单向非循环链表&#xff1a;结构简单&…

[iOS]使用CocoaPods发布公开库

1.检查库名是否已被占用 选择库名时&#xff0c;尽量选择具有描述性并且独特的名字&#xff0c;这不仅可以避免命名冲突&#xff0c;还可以帮助用户更好地理解库的用途和功能。 在实际创建和发布 CocoaPods 库之前&#xff0c;确实应该检查库名是否已经被占用&#xff0c;以避…

文献速递:深度学习医学影像心脏疾病检测与诊断---利用深度学习进行动态心脏PET的自动帧间患者运动校正

Title 题目 Automatic Inter-frame Patient Motion Correction for Dynamic Cardiac PET Using Deep Learning 利用深度学习进行动态心脏PET的自动帧间患者运动校正 01 文献速递介绍 OSITRON正电子发射断层扫描&#xff08;PET&#xff09;心肌灌注成像已被证明相较于其他…

基于SkyEye运行Android——应用最为广泛的移动设备操作系统

01.Android简介 Android&#xff08;安卓&#xff09;是一种基于Linux内核&#xff08;不包含GNU组件&#xff09;的开源操作系统&#xff0c;最初由安迪鲁宾开发&#xff0c;主要支持手机。2005年8月由Google收购注资&#xff1b;2007年11月&#xff0c;Google与84家硬件制造…

SpringCloud项目打包的镜像下载成功后docker ps找不到

一、问题&#xff1a; 当我们项目打包发布成镜像时&#xff0c;在docker 下载运行镜像后&#xff0c;docker ps找不到下载的镜像 但是docker ps -a可以查看。打印日志docker logs 容器id 遇到错误信息 "no main manifest attribute, in app.jar"&#xff0c;这意味…