向量检索增强chatglm生成

news2024/11/24 19:17:20

背景:

基于chatglm构建agnet:chatglm实现Agent控制 - 知乎

前面一篇文章已经介绍了如何去搭建LLM Agent控制系统,也简单介绍了如何去构建Toolset和构建Action。但是在上篇文章中Toolset其实是基于搜索api构建的,从这篇文章开始后面几篇文章会围绕具体的工具展开介绍如何搭建专业工具。这篇文章介绍的是如何构建临时文件填充工具:向量检索。

向量检索有两大部分:内容存储部分、内容检索部分。

开始细节讲解之前,先来一个整体例子介绍:何谓向量化:

## 
# 导入分割文本的工具,并把上面给出的解释分成文档块

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 100,
    chunk_overlap  = 20,
)
explanation = '''Autoencoder(自动编码器)是一种非常有趣的数学模型,就像一个魔法盒子,可以帮助我们理解数据是如何在空间中转换和变换的。

这个魔法盒子里有两个部分:编码器(Encoder)和解码器(Decoder)。

首先,让我们来看一下编码器。它是一个把输入数据(比如一张图片、一段视频或者一篇文章)变得更容易看懂的神奇机器。它将输入数据压缩成一个更小的空间,这样原始数据中的信息就会更加强烈地保留下来。

接下来是解码器。它是一个把编码器压缩后的结果恢复成原始数据的神奇机器。它将编码器得到的结果还原成输入数据(也就是我们刚刚压缩过的数据),这样解码器就得到了和原始数据完全一样的输出。

那么,为什么说Autoencoder能够提高数据处理的效率呢?

因为通过有效的数据压缩和恢复,Autoencoder能够减少数据量,从而更快地处理和分析数据。这就好像把一个大箱子变成了一个更小的箱子,虽然里面东西的总量没有变,但是可以更轻松地拿取和移动箱子。

所以,Autoencoder是一个非常有趣的数学模型,它可以帮助我们更好地理解数据在空间中的转换和变换。
'''

texts = text_splitter.create_documents([explanation])

切割完后数据如下:

对切割完的数据embedding:

from langchain.embeddings import HuggingFaceEmbeddings
       
model_name = "nghuyong/ernie-3.0-xbase-zh"
#model_name = "nghuyong/ernie-3.0-nano-zh"
#model_name = "shibing624/text2vec-base-chinese"
#model_name = "GanymedeNil/text2vec-large-chinese"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': False}
hf = HuggingFaceEmbeddings(
   model_name=model_name,
   model_kwargs=model_kwargs,
   encode_kwargs=encode_kwargs,
   cache_folder = "/root/autodl-tmp/ChatGLM2-6B/llm_model"
)
query_result = hf.embed_query(texts[0].page_content)
print(query_result)

embbding后的数据如下:

技术点:

数据加载

使用文档加载器从文档源加载数据。 文档是一段文本和关联的元数据。 例如,有一些文档加载器可以加载简单的 .txt 文件、加载任何网页的文本内容,甚至加载 YouTube 视频的脚本。

文档加载器公开了一个“加载”方法,用于从配置的源将数据加载为文档。 它们还可以选择实现“延迟加载”,以便将数据延迟加载到内存中。

数据类型

txt

from langchain.document_loaders import TextLoader

loader = TextLoader("./index.md")
loader.load()

pdf

#pip install pypdf
from langchain.document_loaders import PyPDFLoader

loader = PyPDFLoader("example_data/layout-parser-paper.pdf")
pages = loader.load_and_split()
pages[0]

数据分块

RecursiveCharacterTextSplitter:对于一般文本,推荐使用此文本分割器。 它由字符列表参数化。 它尝试按顺序分割它们,直到块足够小。 默认列表为 ["\n\n", "\n", " ", ""]。 这样做的效果是尝试将所有段落(然后是句子,然后是单词)尽可能长时间地放在一起,因为这些通常看起来是语义相关性最强的文本片段。

文本如何分割:按字符列表

如何测量块大小:按字符数

实现代码:

from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    # Set a really small chunk size, just to show.
    chunk_size = 100,
    chunk_overlap  = 20,
    length_function = len,
)
texts = text_splitter.create_documents([explanation])
print(texts[0])

CharacterTextSplitter:是最简单的方法。 这基于字符(默认为“\n\n”)进行分割,并按字符数测量块长度。

文本如何分割:按单个字符

如何测量块大小:按字符数

from langchain.text_splitter import CharacterTextSplitter
from langchain.document_loaders import TextLoader

loader = TextLoader("/root/autodl-tmp/ChatGLM2-6B/read.txt")
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)

文本embbeding处理

embbeding的作用就是对上面切好块的数据块,用预训练好的文本模型来做一道编码转换,压成有语意的向量。后面在检索的时候,就是通过对检索的问题做Embbding向量化,然后通过问题向量和存储的数据做相似度计算;把相关的数据捞出来做后续处理。

from langchain.embeddings import HuggingFaceEmbeddings
       
model_name = "nghuyong/ernie-3.0-xbase-zh"
#model_name = "nghuyong/ernie-3.0-nano-zh"
#model_name = "shibing624/text2vec-base-chinese"
#model_name = "GanymedeNil/text2vec-large-chinese"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': False}
hf = HuggingFaceEmbeddings(
   model_name=model_name,
   model_kwargs=model_kwargs,
   encode_kwargs=encode_kwargs,
   cache_folder = "/root/autodl-tmp/ChatGLM2-6B/llm_model"
)

数据存储向量库

存储到本地:

from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import FAISS

# 导入分割文本的工具,并把上面给出的解释分成文档块

from langchain.text_splitter import RecursiveCharacterTextSplitter

loader = TextLoader("/root/autodl-tmp/ChatGLM2-6B/read.txt")
documents = loader.load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
docs = text_splitter.split_documents(documents)
db = FAISS.from_documents(docs, hf)

如果数据量大,且数据是需要长期使用的,而不是临时使用用完就丢,数据可以存储到es:

import os
import shutil
from elasticsearch import Elasticsearch
from langchain.vectorstores import ElasticKnnSearch
from langchain.document_loaders import TextLoader
from langchain.text_splitter import CharacterTextSplitter
from configs.params import ESParams
from embedding import Embeddings
from typing import Dict


def _default_knn_mapping(dims: int) -> Dict:
    """Generates a default index mapping for kNN search."""
    return {
        "properties": {
            "text": {"type": "text"},
            "vector": {
                "type": "dense_vector",
                "dims": dims,
                "index": True,
                "similarity": "cosine",
            },
        }
    }


def load_file(filepath, chunk_size, chunk_overlap):
    loader = TextLoader(filepath, encoding='utf-8')
    documents = loader.load()
    text_splitter = CharacterTextSplitter(separator='\n', chunk_size=chunk_size, chunk_overlap=chunk_overlap)
    docs = text_splitter.split_documents(documents)
    return docs


class ES:
    def __init__(self, embedding_model_path):
        self.es_params = ESParams()
        self.client = Elasticsearch(['{}:{}'.format(self.es_params.url, self.es_params.port)],
                                    basic_auth=(self.es_params.username, self.es_params.passwd),
                                    verify_certs=False)
        self.embedding = Embeddings(embedding_model_path)
        self.es = ElasticKnnSearch(index_name=self.es_params.index_name, embedding=self.embedding,
                                   es_connection=self.client)

    def doc_upload(self, file_obj, chunk_size, chunk_overlap):
        try:
            if not self.client.indices.exists(index=self.es_params.index_name):
                dims = len(self.embedding.embed_query("test"))
                mapping = _default_knn_mapping(dims)
                self.client.indices.create(index=self.es_params.index_name, body={"mappings": mapping})
            filename = os.path.split(file_obj.name)[-1]
            file_path = 'data/' + filename
            shutil.move(file_obj.name, file_path)
            docs = load_file(file_path, chunk_size, chunk_overlap)
            self.es.add_documents(docs)
            return "插入成功"
        except Exception as e:
            return e

    def exact_search(self, query, top_k):
        result = []
        similar_docs = self.es.similarity_search_with_score(query, k=top_k)
        for i in similar_docs:
            result.append({
                'content': i[0].page_content,
                'source': i[0].metadata['source'],
                'score': i[1]
            })
        return result

    def knn_search(self, query, top_k):
        result = []
        query_vector = self.embedding.embed_query(query)
        similar_docs = self.es.knn_search(query=query, query_vector=query_vector, k=top_k)
        hits = [hit for hit in similar_docs["hits"]["hits"]]
        for i in hits:
            result.append({
                'content': i['_source']['text'],
                'source': i['_source']['metadata']['source'],
                'score': i['_score']
            })
        return result

    def hybrid_search(self, query, top_k, knn_boost):
        result = []
        query_vector = self.embedding.embed_query(query)
        similar_docs = self.es.knn_hybrid_search(query=query, query_vector=query_vector, knn_boost=knn_boost,
                                                 query_boost=1 - knn_boost, k=top_k)
        hits = [hit for hit in similar_docs["hits"]["hits"]]
        for i in hits:
            result.append({
                'content': i['_source']['text'],
                'source': i['_source']['metadata']['source'],
                'score': i['_score']
            })
        result = result[:top_k]
        return result


from configs.params import ESParams
from elasticsearch import Elasticsearch

es_params = ESParams()
index_name = es_params.index_name

# %% 初始化ES对象
client = Elasticsearch(['{}:{}'.format(es_params.url, es_params.port)],
                       basic_auth=(es_params.username, es_params.passwd),
                       verify_certs=False)

# %% 连通测试
client.ping()

# %% 检查索引是否存在
index_exists = client.indices.exists(index=index_name)

# %% 新建索引
response = client.indices.create(index=index_name, body=mapping)

# %% 插入数据
response = client.index(index=index_name, id=document_id, document=data)

# %% 更新
rp = client.update(index=index_name, id=document_id, body={"doc": data})

# %% 检查文档是否存在
document_exists = client.exists(index=index_name, id=document_id)

# %% 根据ID删除文档
response = client.delete(index=index_name, id=document_id)

用户检索

检索转写

基于距离的向量数据库检索在高维空间中嵌入(表示)查询,并根据“距离”查找相似的嵌入文档。 但是,如果查询措辞发生细微变化,或者嵌入不能很好地捕获数据的语义,检索可能会产生不同的结果。 有时会进行及时的工程/调整来手动解决这些问题,但这可能很乏味。

MultiQueryRetriever 通过使用 LLM 从不同角度为给定的用户输入查询生成多个查询,从而自动执行提示调整过程。 对于每个查询,它都会检索一组相关文档,并采用所有查询之间的唯一并集来获取更大的一组潜在相关文档。 通过对同一问题生成多个视角,MultiQueryRetriever 或许能够克服基于距离的检索的一些限制,并获得更丰富的结果集。

from langchain.retrievers.multi_query import MultiQueryRetriever

llm = ChatGLM(model_path="/root/autodl-tmp/ChatGLM2-6B/llm_model/models--THUDM--chatglm2-6b/snapshots/8eb45c842594b8473f291d0f94e7bbe86ffc67d8")
llm.load_model()

retriever_from_llm = MultiQueryRetriever.from_llm(
    retriever=db.as_retriever(), llm=llm
)
query = "chatglm是什么?"
len(retriever_from_llm.get_relevant_documents(query=query))

从多个角度去生成了可能的提问方式:

这部分功能是怎么实现的呢,其实很简单就是让LLM大模型对输入的问题从多个角度来生成可能的问法。你可以通过下面的代码来按自己要求生成自己的问法。把下面代码中 QUERY_PROMPT的template改成你自己的控制约束就行了。

from typing import List
from langchain import LLMChain
from pydantic import BaseModel, Field
from langchain.prompts import PromptTemplate
from langchain.output_parsers import PydanticOutputParser


# Output parser will split the LLM result into a list of queries
class LineList(BaseModel):
    # "lines" is the key (attribute name) of the parsed output
    lines: List[str] = Field(description="Lines of text")


class LineListOutputParser(PydanticOutputParser):
    def __init__(self) -> None:
        super().__init__(pydantic_object=LineList)

    def parse(self, text: str) -> LineList:
        lines = text.strip().split("\n")
        return LineList(lines=lines)


output_parser = LineListOutputParser()

QUERY_PROMPT = PromptTemplate(
    input_variables=["question"],
    template="""You are an AI language model assistant. Your task is to generate five 
    different versions of the given user question to retrieve relevant documents from a vector 
    database. By generating multiple perspectives on the user question, your goal is to help
    the user overcome some of the limitations of the distance-based similarity search. 
    Provide these alternative questions seperated by newlines.
    Original question: {question}""",
)
llm = ChatGLM(model_path="/root/autodl-tmp/ChatGLM2-6B/llm_model/models--THUDM--chatglm2-6b/snapshots/8eb45c842594b8473f291d0f94e7bbe86ffc67d8")
llm.load_model()

# Chain
llm_chain = LLMChain(llm=llm, prompt=QUERY_PROMPT, output_parser=output_parser)

# Other inputs
question = "What are the approaches to Task Decomposition?"

检索结果整理

如果只是把问题做embbding,然后通过相似度检索召回文本,代码实现如下。

query = "chatglm是什么?"
docs = db.similarity_search(query)

检索回来的结果如下:

如果是直接把召回的结果作为答案,那这个肯定达不到我们希望的效果。所以比然要对召回的结果做处理,一般的处理其实就是让LLM模型,利用召回的数据作为参考资料,回答我们的问题。其实这背后的逻辑就是对信息做了过滤,把相关的数据权重加重,然后让模型基于加重权重的数据做回答。

Stuff检索框架

填充文档链(“填充”如“填充”或“填充”)是最直接的文档链。 它需要一个文档列表,将它们全部插入到提示中,并将该提示传递给LLM模型。该链非常适合文档较小且大多数调用只传递少量文档的应用程序。

实现代码如下:

from langchain.chains import RetrievalQA
ruff = RetrievalQA.from_chain_type(
    llm=llm, chain_type="stuff", retriever=db.as_retriever()
)
ruff.run(query)

生成结果如下:

refine检索框架

细化文档链通过循环输入文档并迭代更新其答案来构建响应。 对于每个文档,它将所有非文档输入、当前文档和最新的中间答案传递给 LLM 链以获得新答案。

由于 Refine 链一次仅将单个文档传递给 LLM,因此它非常适合需要分析的文档数量多于模型上下文的任务。 明显的权衡是,该链将比 Stuff 文档链进行更多的 LLM 调用。 还有一些任务很难迭代完成。 例如,当文档频繁相互交叉引用或一项任务需要来自许多文档的详细信息时,Refine 链可能会表现不佳。

from langchain.chains import RetrievalQA
ruff = RetrievalQA.from_chain_type(
    llm=llm, chain_type="refine", retriever=db.as_retriever()
)
ruff.run(query)

生成结果如下:

maprecude信息聚合检索框架

MapReduce 文档链首先将 LLM 链单独应用于每个文档(Map 步骤),将链输出视为新文档。 然后,它将所有新文档传递到单独的组合文档链以获得单个输出(Reduce 步骤)。 它可以选择首先压缩或折叠映射的文档,以确保它们适合组合文档链(这通常会将它们传递给LLM)。 如有必要,该压缩步骤会递归执行。

实现代码如下:

from langchain.chains import RetrievalQA
ruff = RetrievalQA.from_chain_type(
    llm=llm, chain_type="map_reduce", retriever=db.as_retriever()
)
ruff.run(query)

生成效果如下:

用户自定义检索框架

上面介绍了几种端到端query向量数据库生成答案的方式,但往往这样的端到端生成方法无法满足我们需要。我们要如何去改进提高呢,要回答这个问题就要知道基于向量库生成答案到底做了什么事。拆看代码看其实就两部分:1.通过语意相似度召回相关资料 2.把召回的相关资料做了摘要总结生成答案。可以通过下面一个示例代码,看如何来把两个部分合成一个。

# 导入LLMChain并定义一个链,用语言模型和提示作为参数。

from langchain.chains import LLMChain
chain = LLMChain(llm=llm, prompt=prompt)

# 只指定输入变量来运行链。
print(chain.run("autoencoder"))
# 定义一个第二个提示

second_prompt = PromptTemplate(
    input_variables=["ml_concept"],
    template="把{ml_concept}的概念描述转换成用500字向我解释,就像我是一个五岁的孩子一样",
)
chain_two = LLMChain(llm=llm, prompt=second_prompt)

# 用上面的两个链定义一个顺序链:第二个链把第一个链的输出作为输入

from langchain.chains import SimpleSequentialChain
overall_chain = SimpleSequentialChain(chains=[chain, chain_two], verbose=True)

# 只指定第一个链的输入变量来运行链。
explanation = overall_chain.run("autoencoder")
print(explanation)

所以我们如果要定制化解决方案,其实只要把摘要生成答案部分加入策略就行了。具体代码如下,只要把你的约束策略放到prompt_template就定制化满足你的要求了。

from langchain.prompts import PromptTemplate

prompt_template = """Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.

{context}

Question: {question}
Helpful Answer:"""
PROMPT = PromptTemplate(
    template=prompt_template, input_variables=["context", "question"]
)

chain = load_summarize_chain(llm, chain_type="stuff", prompt=PROMPT)
chain.run(docs)

把上面模块封装成Tool

#把基于向量库的检索生成封装成Tool
ruff = RetrievalQA.from_chain_type(
    llm=llm, chain_type="stuff", retriever=db.as_retriever()
)

tools = [
    Tool(
        name="Ruff QA System",
        func=ruff.run,
        description="useful for when you need to answer questions about ruff (a python linter). Input should be a fully formed question.",
    ),
]

#挂到Agent上测试效果
agent = initialize_agent(
    tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)

agent.run(
    "What did biden say about ketanji brown jackson in the state of the union address?"
)

小结:

1.总体介绍了基于向量检索的框架,主要分为两大块:内容存储、内容检索

2.具体介绍了内容存储部分技术细节:数据加载模块、数据切块模块、数据embbeding模块、数据存储模块及代码实现

3.具体介绍了内容检索部分:向量相似度召回+基于上下文生成问题答案,实现原理和实现代码

4.介绍了如何把向量检索生成封装成tool供agnet使用

项目代码:https://github.com/liangwq/Chatglm_lora_multi-gpu/tree/main/APP_example/chatglm_agent

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++ stack和queue 模拟实现

stack和queue 模拟实现 模拟栈实现模拟队实现 模拟栈实现 1 栈是一种容器适配器,专门设计用于后进先出的后进先出环境,在这种环境中,元素只从容器的一端插入和提取。 2 栈是作为容器适配器实现的,这些适配器是使用特定容器类的封装…

获取gitlab上项目最近更新时间

获取gitlab上项目列表过程及脚本_xiaodaiwang的博客-CSDN博客使用Python及shell,获取gitlab上项目列表过程及脚本https://blog.csdn.net/xiaodaiwang/article/details/131781316?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rI…

【字符串编码解码问题】

字符串中编码解码问题 1.编码 byte[] getBytes():使用平台的默认字符集将该String编码为一系列字节,将结果存储到新的字节数组中byte[] getBytes(String charsetName):使用指定的字符集将该String编码为一系列字节,将结果存储到…

Minecraft 1.20.x Forge模组开发 02.物品栏+方块+物品

我们本次在1.20中添加一个属于自己模组的物品栏、物品和方块。 效果演示 效果演示 效果演示 1.在项目中新建一个int包,用于存放所有注册类,在init包中新建ItemTabInit类: ItemTabInit.java package com.joy187.re8joymod.init

【半监督医学图像分割 2023 CVPR】PatchCL

文章目录 【半监督医学图像分割 2023 CVPR】PatchCL摘要1. 简介2. 相关工作2.1 半监督学习2.2 对比学习 3. 方法3.1 类感知补丁采样3.2 伪标记引导对比损失3.3 总体学习目标3.4 伪标号生成与求精 4. 实验5. 结果 【半监督医学图像分割 2023 CVPR】PatchCL 论文题目:…

行为型模式 - 模板方法模式

概述 在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的具体实现还未知,或者说某些步骤的实现与具体的环境相关。 例如&#…

JAVA ---- 经典排序算法

目录 一. 插入排序 1. 直接插入排序 代码演示 2.希尔排序( 缩小增量排序 ) 二. 选择排序 1.直接选择排序 代码: 2. 堆排序 代码 三. 交换排序 1. 冒泡排序 代码 2. 快速排序 代码(有注释): 动图来自网…

Mysql教程(四):DML学习

Mysql教程(四):DML学习 前言 DML-介绍 DML英文全称是Data Manipulation Language数据库操作语言,用来对数据库中表的数据记录进行增删改查。 添加数据(INSERT)修改数据(UPDATE)删除…

Java 串口通讯 Demo

为什么写这篇文章 之前职业生涯中遇到的都是通过tcp协议与其他设备进行通讯,而这个是通过串口与其他设备进行通讯,意识到这里是承重板的连接,但实际上比如拉力、压力等模拟信号转换成数字信号的设备应该是有相当一大部分是通过这种方式通讯的…

为你精选5款体验极佳的原型设计工具!

在绘制原型图的过程中,使用一款的简单易操作的原型设计工具是非常重要的,本文精选了5款好用的原型工具与大家分享,一起来看看吧! 1、即时设计 即时设计是国内很多设计师都在用的原型设计工具,同时它也是国产的原型设…

JMeter正则表达式提取器和JSON提取器基础用法,小白必会!

最近在利用JMeter做接口自动化测试,正则表达式提取器和JSON提取器用的还挺多,想着分享下,希望对大家的接口自动化测试项目有所启发。 在 JMeter 中,正则表达式和 JSON 提取器都是用于从响应数据中提取所需内容,但它们的…

界面控件Telerik UI for WinForms R2 2023——发布全新的热图控件

Telerik UI for WinForms拥有适用Windows Forms的110多个令人惊叹的UI控件。所有的UI for WinForms控件都具有完整的主题支持,可以轻松地帮助开发人员在桌面和平板电脑应用程序提供一致美观的下一代用户体验。 在本文中,我们将揭秘一下Telerik UI for W…

vue数组对象快速获取最大值和最小值(linq插件各种常用好用方法),提高开发效率

需求:因后端传入的数据过多,前端需要在数组中某一值的的最大值和最小值计算,平常用的最多的不就是遍历之后再比对吗,或者用sort方法等实现,同事交了我一招,一句话就可以获取到数组对象中最大值和最小值&…

【Docker】Docker基本概念

Docker基本概念 1.Docker概述1.1 Docker是什么?1.2 Docker的宗旨1.3 容器的优点1.4 Docker与虚拟机的区别1.5 容器在内核中支持的两种技术1.6 namespace的六大类型 2.Docker核心概念2.1 镜像2.2 容器2.3 仓库 3. 知识点总结3.1 Docker是什么?3.2 容器和虚…

Fortinet Accelerate 2023·中国区巡展收官丨让安全成就未来

7月18日,2023 Fortinet Accelerate Summit在上海成功举办!这亦象征着“Fortinet Accelerate2023中国区巡展”圆满收官。Fortinet携手来自多个典型行业的百余位代表客户,以及亚马逊云科技、Telstra - PBS 太平洋电信、Tenable等多家生态合作伙…

RT-Thread 学习-Env开发环境搭建(一)

Env是什么 Env 是 RT-Thread 推出的开发辅助工具,针对基于 RT-Thread 操作系统的项目工程,提供编译构建环境、图形化系统配置及软件包管理功能。 其内置的 menuconfig 提供了简单易用的配置剪裁工具,可对内核、组件和软件包进行自由裁剪&…

PROFIBUS-DP主站转ETHERNET/IP网关ethernet有哪些协议

远创智控YC-DPM-EIP是自主研发的一款PROFIBUS-DP主站功能的通讯网关。该产品主要功能是将各种PROFIBUS-DP从站接入到ETHERNET/IP网络中。 1, 本网关连接到PROFIBUS总线中作为主站使用,连接到ETHERNET/IP总线中作为从站使用。 1.2 产品特点 ◆ PROFIBUS-DP/V0 协议…

AtcoderABC243场

A - Shampoo A - Shampoo ] 题目大意 高桥家有三个人:高桥、他的父亲和他的母亲。每个人每晚都在浴室洗头发。他们按照顺序使用AA、BB和CC毫升的洗发水。 问,今天早上瓶子里有VV毫升的洗发水。在不重新装满的情况下,谁会第一个用完洗发水洗头…

【Maven四】——maven聚合和继承

系列文章目录 Maven之POM介绍 maven命令上传jar包到nexus 【Maven二】——maven仓库 【Maven三】——maven生命周期和插件 聚合和继承 系列文章目录前言一、什么是maven的聚合和继承&why二、聚合三、继承1.可继承的POM元素2.依赖管理3.插件管理 四、聚合与继承的关系五、约…

java电子病历系统源码

电子病历系统采取结构化与自由式录入的新模式,自由书写,轻松录入。化实现病人医疗记录(包含有首页、病程记录、检查检验结果、医嘱、手术记录、护理记录等等。)的保存、管理、传输和重现,取代手写纸张病历。不仅实现了…