<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

   <title>Java译站</title>
   <link href="https://it.deepinmind.com/atom.xml" rel="self" type="application/atom+xml"/>
   <link href="https://it.deepinmind.com" rel="alternate" type="text/html" />
   <updated>2024-05-08T10:13:25+08:00</updated>
   <id>https://it.deepinmind.com</id>
   <author>
     <name>deepinmind</name>
     <email>spidercoco@gmail.com</email>
   </author>

   
   <entry>
     <title>使用Llama3和Ollama来增强RAG</title>
     <link href="https://it.deepinmind.com/llm/2024/05/07/improved-rag-with-llama3-and-ollama.html"/>
     <updated>2024-05-07T20:33:41+08:00</updated>
     <id>https://it.deepinmind.com/llm/2024/05/07/improved-rag-with-llama3-and-ollama</id>
     <content type="html">&lt;p&gt;在这篇文章中，我们将探讨如何利用Meta新发布的最先进的开源大型语言模型Llama-3，实现在完全本地化基础设施上的进阶版RAG（检索增强生成）。这篇文章是使用Llama-3进行进阶RAG实施的实战指南。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/5071.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;简介：
在这篇文章中，我们将创建一个进阶版的RAG，它会根据输入到管道的研究论文来回答用户查询。构建这个pipeline所使用的技术栈如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ollama嵌入模型（mxbai-embed-large）&lt;/li&gt;
&lt;li&gt;Ollama量化Llama-3 8b模型&lt;/li&gt;
&lt;li&gt;本地托管的Qdrant向量数据库&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;从技术选型中可以明显看出两个优势：成本为0，并且信息高度安全和私密。&lt;/p&gt;

&lt;h3&gt;什么是HyDE？&lt;/h3&gt;

&lt;p&gt;HyDE（Hypothetical Document Embeddings， 假设文档嵌入）源自Gao等人在2022年发表的名为&lt;a href=&quot;https://arxiv.org/pdf/2212.10496&quot;&gt;《Precise Zero-Shot Dense Retrieval without Relevance Labels》&lt;/a&gt;的论文中的创新工作。这项研究的主要目标是改进依赖语义嵌入相似性的零样本稠密检索（也就是基于嵌入向量的检索），HyDE由两个步骤来完成。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/5072.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;在第一步（步骤1）中，通过指令提示词引导大语言模型（以GPT-3为例）生成基于原始查询的假设文档。这个过程是针对查询的问题精心定制的，确保了“假设文档”的相关性。&lt;/p&gt;

&lt;p&gt;进入第二步，通过一个“无监督对比编码器”的Contriever将生成的假设文档转化为嵌入向量。这个编码器将假设文档转换为向量表示，然后用于后续的相似性搜索和检索任务。&lt;/p&gt;

&lt;p&gt;HyDE的基本功能是通过两个关键组件将文档转换为向量嵌入。第一步使用语言模型执行生成任务，旨在捕捉假设文档中的相关性，即使存在事实不准确。随后，由对比编码器管理的文档-文档相似性任务细化嵌入过程，过滤掉多余细节，提高效率。&lt;/p&gt;

&lt;p&gt;值得注意的是，HyDE的表现超过了现有的无监督稠密检索器，如Contriever。此外，它在多样化的任务和语言上展示出与经过微调的检索器相当的性能表现。这种方法将稠密检索简化为两个连贯的任务，标志着基于语义嵌入的检索方法的重大进步。&lt;/p&gt;

&lt;p&gt;实现：&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;from llama_index.core import (
    SimpleDirectoryReader,
    VectorStoreIndex,
    StorageContext,
    Settings,
    get_response_synthesizer)
from llama_index.core.query_engine import RetrieverQueryEngine, TransformQueryEngine
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.schema import TextNode, MetadataMode
from llama_index.vector_stores.qdrant import QdrantVectorStore
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core.indices.query.query_transform import HyDEQueryTransform
import qdrant_client
import logging
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;初始化&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# load the local data directory and chunk the data for further processing
docs = SimpleDirectoryReader(input_dir=&amp;quot;data&amp;quot;, required_exts=[&amp;quot;.pdf&amp;quot;]).load_data(show_progress=True)
text_parser = SentenceSplitter(chunk_size=512, chunk_overlap=100)

text_chunks = []
doc_ids = []
nodes = []
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;创建向量数据库以便存储嵌入向量。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;# Create a local Qdrant vector store
logger.info(&amp;quot;initializing the vector store related objects&amp;quot;)
client = qdrant_client.QdrantClient(host=&amp;quot;localhost&amp;quot;, port=6333)
vector_store = QdrantVectorStore(client=client, collection_name=&amp;quot;research_papers&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;加载嵌入及大语言模型。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;# local vector embeddings model
logger.info(&amp;quot;initializing the OllamaEmbedding&amp;quot;)
embed_model = OllamaEmbedding(model_name=&amp;#39;mxbai-embed-large&amp;#39;, base_url=&amp;#39;http://localhost:11434&amp;#39;)
logger.info(&amp;quot;initializing the global settings&amp;quot;)
Settings.embed_model = embed_model
Settings.llm = Ollama(model=&amp;quot;llama3&amp;quot;, base_url=&amp;#39;http://localhost:11434&amp;#39;)
Settings.transformations = [text_parser]
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;创建节点、向量存储、HyDE transformer并进行查询。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;logger.info(&amp;quot;enumerating docs&amp;quot;)
for doc_idx, doc in enumerate(docs):
    curr_text_chunks = text_parser.split_text(doc.text)
    text_chunks.extend(curr_text_chunks)
    doc_ids.extend([doc_idx] * len(curr_text_chunks))

logger.info(&amp;quot;enumerating text_chunks&amp;quot;)
for idx, text_chunk in enumerate(text_chunks):
    node = TextNode(text=text_chunk)
    src_doc = docs[doc_ids[idx]]
    node.metadata = src_doc.metadata
    nodes.append(node)

logger.info(&amp;quot;enumerating nodes&amp;quot;)
for node in nodes:
    node_embedding = embed_model.get_text_embedding(
        node.get_content(metadata_mode=MetadataMode.ALL)
    )
    node.embedding = node_embedding

logger.info(&amp;quot;initializing the storage context&amp;quot;)
storage_context = StorageContext.from_defaults(vector_store=vector_store)
logger.info(&amp;quot;indexing the nodes in VectorStoreIndex&amp;quot;)
index = VectorStoreIndex(
    nodes=nodes,
    storage_context=storage_context,
    transformations=Settings.transformations,
)

logger.info(&amp;quot;initializing the VectorIndexRetriever with top_k as 5&amp;quot;)
vector_retriever = VectorIndexRetriever(index=index, similarity_top_k=5)
response_synthesizer = get_response_synthesizer()
logger.info(&amp;quot;creating the RetrieverQueryEngine instance&amp;quot;)
vector_query_engine = RetrieverQueryEngine(
    retriever=vector_retriever,
    response_synthesizer=response_synthesizer,
)
logger.info(&amp;quot;creating the HyDEQueryTransform instance&amp;quot;)
hyde = HyDEQueryTransform(include_original=True)
hyde_query_engine = TransformQueryEngine(vector_query_engine, hyde)

logger.info(&amp;quot;retrieving the response to the query&amp;quot;)
response = hyde_query_engine.query(
    str_or_query_bundle=&amp;quot;what are all the data sets used in the experiment and told in the paper&amp;quot;)
print(response)

client.close()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;上述代码首先把日志级别配置成INFO，以便查看日志，然后从本地目录加载PDF数据，并将其分割成文本块。它设置了一个Qdrant向量存储来存储研究论文的嵌入向量，并初始化了一个Ollama文本嵌入模型，用来将文本生成嵌入。设置好全局配置，再处理文本块并将其与文档ID关联起来。再根据这些块创建文本节点，保留元数据，并使用Ollama模型为这些节点生成嵌入。然后设置存储上下文，以便在Qdrant向量存储中索引文本嵌入，以便后续进行检索。配置完向量检索器用来检索相似的嵌入向量，再初始化一个查询引擎用于处理查询。同时还设置了一个HyDE查询transformer，用于增强查询处理。最后，执行了一个查询，以检索有关论文实验中提到的数据集相关的信息，并输出响应。&lt;/p&gt;

&lt;p&gt;输出：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/5073.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;结论：
通过利用Meta的Llama-3等先进的模型，结合HyDE的改进方法以及Ollama的能力，我们可以构建出优秀的RAG管道。通过细致地微调关键的超参数，如top&lt;em&gt;k、chunk&lt;/em&gt;size和chunk_overlap，可以达到更高的准确性和效率。通过结合先进的工具的和精心的优化，能够释放系统的全部潜能，确保解决方案的创新性和领先性，同时还最大程度保障了隐私和安全。&lt;/p&gt;
</content>
   </entry>
   
   <entry>
     <title>使用LangChain来实现大模型agent</title>
     <link href="https://it.deepinmind.com/llm/2024/04/08/intro-to-llm-agents-with-langchain-when-rag-is-not-enough.html"/>
     <updated>2024-04-08T20:33:41+08:00</updated>
     <id>https://it.deepinmind.com/llm/2024/04/08/intro-to-llm-agents-with-langchain-when-rag-is-not-enough</id>
     <content type="html">&lt;h3&gt;agent介绍&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/1.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;我们先从LLM Agent的各种不同示例开始讲起。虽然很多人在热议这个话题，但很少有人经常在使用它；通常，我们所认为的agent只是大语言模型。来看一个简单的任务，如搜索足球比赛结果并将其保存为CSV文件。可以比较下几种可用的工具：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GPT-4结合搜索和插件：如这里的&lt;a href=&quot;https://chat.openai.com/share/2ecd61a9-dbd9-4287-aa75-14618106a34c&quot;&gt;聊天记录&lt;/a&gt;所示，由于代码错误，GPT-4未能完成任务。&lt;/li&gt;
&lt;li&gt;AutoGPT使用&lt;a href=&quot;https://evo.ninja/&quot;&gt;https://evo.ninja/&lt;/a&gt; 至少可以生成CSV的格式（尽管不理想）。&lt;/li&gt;
&lt;li&gt;AgentGPT&lt;a href=&quot;https://agentgpt.reworkd.ai/&quot;&gt;https://agentgpt.reworkd.ai/&lt;/a&gt;：它把这个任务看成是合成数据生成器，但这并不是我们想要的，聊天记录在&lt;a href=&quot;https://agentgpt.reworkd.ai/agent?id=clsyfh1t101t9jv08yaof890k&quot;&gt;这里&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;由于可用的工具并不理想，我们先从头开始学习构建agent的基本原理。我参考了这篇文章作为结构参考，但添加了更多示例。&lt;/p&gt;

&lt;p&gt;步骤1：规划&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/2.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;你可能已经接触过各种旨在提升大语言模型效果的技术，比如提供提示，甚至开玩笑地威胁它们。一种流行的技术被称为“链式思维”，它会模型去逐步思考，从而允许自我修正。这种方法已经演变成更高级的形式，如&lt;a href=&quot;https://www.promptingguide.ai/techniques/consistency&quot;&gt;“自我一致性的链式思维”&lt;/a&gt;以及更为通用的“思想树”，它会创建、重新评估以及合并多个思想来提供输出。&lt;/p&gt;

&lt;p&gt;在这个教程中，我会经常用到Langsmith，来实现LLM应用程序的产品化。例如，在构建思想树提示词时，我会将子提示词保存在提示仓储中并加载它们：&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;from langchain import hub
from langchain.chains import SequentialChain

cot_step1 = hub.pull(&amp;quot;rachnogstyle/nlw_jan24_cot_step1&amp;quot;)
cot_step2 = hub.pull(&amp;quot;rachnogstyle/nlw_jan24_cot_step2&amp;quot;)
cot_step3 = hub.pull(&amp;quot;rachnogstyle/nlw_jan24_cot_step3&amp;quot;)
cot_step4 = hub.pull(&amp;quot;rachnogstyle/nlw_jan24_cot_step4&amp;quot;)

model = &amp;quot;gpt-3.5-turbo&amp;quot;

chain1 = LLMChain(
    llm=ChatOpenAI(temperature=0, model=model),
    prompt=cot_step1,
    output_key=&amp;quot;solutions&amp;quot;
)

chain2 = LLMChain(
    llm=ChatOpenAI(temperature=0, model=model),
    prompt=cot_step2,
    output_key=&amp;quot;review&amp;quot;
)

chain3 = LLMChain(
    llm=ChatOpenAI(temperature=0, model=model),
    prompt=cot_step3,
    output_key=&amp;quot;deepen_thought_process&amp;quot;
)

chain4 = LLMChain(
    llm=ChatOpenAI(temperature=0, model=model),
    prompt=cot_step4,
    output_key=&amp;quot;ranked_solutions&amp;quot;
)

overall_chain = SequentialChain(
    chains=[chain1, chain2, chain3, chain4],
    input_variables=[&amp;quot;input&amp;quot;, &amp;quot;perfect_factors&amp;quot;],
    output_variables=[&amp;quot;ranked_solutions&amp;quot;],
    verbose=True
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;在这里，你可以看到这样推理的结果。这里想强调的是，正确地定义推理步骤并在像Langsmith这样的LLMOps系统中对其进行版本控制的重要性。你还可以在公开存储库中看到其他流行的推理技术示例，例如ReAct或self-ask-with-search。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;prompt = hub.pull(&amp;quot;hwchase17/react&amp;quot;)
prompt = hub.pull(&amp;quot;hwchase17/self-ask-with-search&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;其他值得关注的方法包括：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reflexion（Shinn &amp;amp; Labash 2023）：它是一个框架，使得agent可以具备动态记忆和自我反思能力，以提升推理技能。&lt;/li&gt;
&lt;li&gt;Hindsight链（CoH；Liu等人，2023）通过明确地向模型展示一系列带有反馈的过去输出，鼓励模型改进自己的输出。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;步骤2：记忆&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/3.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;我们可以将大脑中的不同记忆类型映射到LLM智能体架构中的组件&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;感觉记忆：这部分记忆捕捉即时感官输入，如我们看到、听到或感受到的。在提示工程和AI模型的上下文中，提示词充当瞬时输入，类似于瞬间触摸或感觉。它是触发模型处理的初始刺激。&lt;/li&gt;
&lt;li&gt;短期记忆：短期记忆暂时保存与当前任务或对话相关的信息。在提示工程中，这相当于保留最近的聊天历史。这种记忆使智能体在整个交互过程中保持上下文和连贯性，确保响应与当前对话对齐。在代码中，你通常将其添加为对话历史。&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain.agents import AgentExecutor
from langchain.agents import create_openai_functions_agent

llm = ChatOpenAI(model=&amp;quot;gpt-3.5-turbo&amp;quot;, temperature=0)
tools = [retriever_tool]
agent = create_openai_functions_agent(
    llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)

message_history = ChatMessageHistory()
agent_with_chat_history = RunnableWithMessageHistory(
    agent_executor,
    lambda session_id: message_history,
    input_messages_key=&amp;quot;input&amp;quot;,
    history_messages_key=&amp;quot;chat_history&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;长期记忆：长期记忆存储事实知识和程序指令。在AI模型中，这其实就是用于训练和微调的数据。此外，长期记忆支持RAG框架的操作，使智能体能够访问并整合学习到的信息到其响应中。它就像是一个全面的知识库，agent基于它来生成有意义且相关的输出。在代码中，通常来说就是向量化数据库：&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings

loader = WebBaseLoader(&amp;quot;https://neurons-lab.com/&amp;quot;)
docs = loader.load()
documents = RecursiveCharacterTextSplitter(
    chunk_size=1000, chunk_overlap=200
).split_documents(docs)
vector = FAISS.from_documents(documents, OpenAIEmbeddings())
retriever = vector.as_retriever()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;步骤3：工具&lt;/h3&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/4.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;实践中，智能体的增强可以通过添加独立的推理线（可以是另一个领域特定的LLM或其他机器学习模型，用于图像分类）或者基于规则、API来进行。&lt;/p&gt;

&lt;p&gt;ChatGPT插件和OpenAI API函数调用是LLMs实际使用工具的典型例子。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;内置的Langchain工具：Langchain有多种内置工具，从互联网搜索、Arxiv工具包到Zapier和Yahoo Finance。我们将尝试使用Tavily提供的互联网搜索进行一个简单的实验：&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;from langchain.utilities.tavily_search import TavilySearchAPIWrapper
from langchain.tools.tavily_search import TavilySearchResults

search = TavilySearchAPIWrapper()
tavily_tool = TavilySearchResults(api_wrapper=search)

llm = ChatOpenAI(model_name=&amp;quot;gpt-3.5-turbo&amp;quot;, temperature=0.0)
agent_chain = initialize_agent(
    [retriever_tool, tavily_tool],
    llm,
    agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True,
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;自定义工具：定义自己的工具也非常简单。用一个计算字符串长度的工具来举个例。需要使用@tool装饰器来让Langchain知道它，然后，还需要提供输入和输出的类型，最重要的是三个引号之间的函数注释——这将告诉智能体这个工具能做什么，并将其与其他工具的描述进行比较：&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;from langchain.pydantic_v1 import BaseModel, Field
from langchain.tools import BaseTool, StructuredTool, tool

@tool
def calculate_length_tool(a: str) -&amp;gt; int:
    &amp;quot;&amp;quot;&amp;quot;The function calculates the length of the input string.&amp;quot;&amp;quot;&amp;quot;
    return len(a)

llm = ChatOpenAI(model_name=&amp;quot;gpt-3.5-turbo&amp;quot;, temperature=0.0)
agent_chain = initialize_agent(
    [retriever_tool, tavily_tool, calculate_length_tool],
    llm,
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True,
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;你可以在&lt;a href=&quot;https://github.com/Rachnog/intro_to_llm_agents/blob/main/3_tools.ipynb&quot;&gt;这个脚本&lt;/a&gt;中看到它是怎么工作的，但也会看到一个错误——它没有拉取到Neurons Lab公司的正确描述，尽管调用的长度计算函数是对的，但最终结果还是错误的。&lt;/p&gt;

&lt;h3&gt;步骤4：整合到一起&lt;/h3&gt;

&lt;p&gt;我在&lt;a href=&quot;https://towardsdatascience.com/intro-to-llm-agents-with-langchain-when-rag-is-not-enough-7d8c08145834#:%7E:text=of%20architecture%20together-,in%20this%20script,-.%20Notice%2C%20how%20we&quot;&gt;这个脚本&lt;/a&gt;中提供了一个组合在一起的干净版本。注意，我们要能轻松地识别及定义出：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;所有类型的工具（搜索、自定义工具等）&lt;/li&gt;
&lt;li&gt;所有类型的记忆（对应感观的是提示词、短期记忆的是消息历史以及提示词内的sketchpad，长期记忆则对应向量数据库的检索）&lt;/li&gt;
&lt;li&gt;任意类型的规划策略（作为从LLMOps系统中提取的提示词的一部分）&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;最终的智能体定义看起来会是这样的：&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;llm = ChatOpenAI(model=&amp;quot;gpt-3.5-turbo&amp;quot;, temperature=0)
agent = create_openai_functions_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
agent_with_chat_history = RunnableWithMessageHistory(
    agent_executor,
    lambda session_id: message_history,
    input_messages_key=&amp;quot;input&amp;quot;,
    history_messages_key=&amp;quot;chat_history&amp;quot;,
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;如脚本的输出所示（你也可以自己运行它），它解决了上一部分与工具相关的问题。这和直接调用大模型有什么区别呢，我们定义了一个完整的架构，在这里短期记忆起着关键作用，我们的智能体获得了消息历史和推理结构中的sketchpad，这使得它能够获取正确的网站描述并计算其长度。&lt;/p&gt;

&lt;h3&gt;最后&lt;/h3&gt;

&lt;p&gt;希望这次对LLM智能体架构核心元素的介绍能够设计出用于认知任务的实用机器人。最后，我想再次强调拥有智能体的所有元素的重要性。正如我们所看到的，缺少短期记忆或工具的不完整描述可能会干扰智能体的推理，甚至对于像摘要生成和长度计算这样非常简单的任务也会提供错误答案。&lt;/p&gt;
</content>
   </entry>
   
   <entry>
     <title>如何根据LLM的参数估算内存（显存）使用量</title>
     <link href="https://it.deepinmind.com/llm/2024/04/01/parameters-and-memory-estimation.html"/>
     <updated>2024-04-01T20:33:41+08:00</updated>
     <id>https://it.deepinmind.com/llm/2024/04/01/parameters-and-memory-estimation</id>
     <content type="html">&lt;h1&gt;简介&lt;/h1&gt;

&lt;p&gt;基于Transformer架构的大型语言模型（LLMs）已经变得越来越普遍，例如Mistral AI团队推出的&lt;a href=&quot;https://huggingface.co/mistralai/Mistral-7B-v0.1&quot;&gt;Mistral 7B模型&lt;/a&gt;。理解其推理、微调和训练时的内存需求对于有效部署和使用这些模型至关重要。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;推理内存估算：&lt;/strong&gt;，对于一个70亿参数（7B）的模型，预计需要的内存需求如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;浮点精度：28GB&lt;/li&gt;
&lt;li&gt;BF16精度：14GB&lt;/li&gt;
&lt;li&gt;int8精度：7GB&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个估算逻辑可以等比例应用于其他版本。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;训练所需内存：&lt;/strong&gt;，保守的估计是，训练所需的内存是具有相同参数数量和类型的推理内存的四倍。例如，使用浮点精度训练一个7B模型大约需要112GB（28GB * 4）。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;训练大语言模型的内存要求：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;梯度&lt;/strong&gt;：所需的梯度内存等于参数的数量。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优化器状态&lt;/strong&gt;：优化器状态所需的内存取决于使用的优化器类型。

&lt;ul&gt;
&lt;li&gt;使用AdamW优化器，需要的内存是参数数量的两倍。&lt;/li&gt;
&lt;li&gt;使用SGD优化器，需要的内存相当于参数数量。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;使用LoRA/QloRA技术的内存使用情况，以LoRA为例：&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;LoRA涉及在原始模型上运行推理并训练一个较小的模型，以实现与训练原始参数几乎相同的效果。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;例如，如果需要微调大小为1024x512的参数，使用LoRA并选择Rank为8，只需要微调以下数量的参数：1024x8 + 512x8。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;这个过程需要使用原始参数量运行一次推理（不需要梯度和优化器状态），但在计算过程中仍需要一些内存来存储数据。总内存使用量是这些需求的总和。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;背景知识&lt;/h2&gt;

&lt;p&gt;在计算机内存/磁盘存储中，基本单位是字节，系统基于1024。单位包括KB、MB、GB和TB。重要的是不要将其与十进制系统混淆：1 KB = 1024 字节；1 MB = 1024 KB；1 GB = 1024 MB。&lt;/p&gt;

&lt;h3&gt;参数计数&lt;/h3&gt;

&lt;p&gt;以Mistral-7B为例，“7B”表示LLM有70亿个参数。&lt;/p&gt;

&lt;h3&gt;数据类型&lt;/h3&gt;

&lt;p&gt;对于训练好的模型，参数类型可能包括：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;浮点（float）&lt;/strong&gt;：32位浮点数，4字节&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;半精度（half/BF16）&lt;/strong&gt;：16位浮点数，2字节&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;int8&lt;/strong&gt;：8位整数，1字节&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;int4&lt;/strong&gt;：4位整数，0.5字节&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;参数类型所需的存储量越小，性能通常越低。&lt;/p&gt;

&lt;h3&gt;推理使用内存估算&lt;/h3&gt;

&lt;p&gt;虽然其他因素也在占用内存，但推理期间使用内存的主要是参数。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;比如，Mistral-7B-BF16模型需要的内存等于参数数量乘以类型大小：70亿参数 * 2字节 = 140亿字节。因此，140亿字节 = 14 * 1,000 * 1,000 * 1,000 / 1024 / 1024 / 1024 ≈ 13 GB（考虑1000/1024）³ ≈ 0.93。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;注1：(1000/1024)³ ≈ 0.93  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;注2：为了估算目的，简单地将此比率视为1。因此，对于7B-BF16模型，内存需求大约是7 * 2 = 14 GB。这个估算略高于精确计算，但更实用，因为推理需要超出参数之外的额外内存。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;假设要估算&lt;code&gt;llama2-13B&lt;/code&gt;模型的内存需求，对应各种类型的分别是：float：13 * 4 = 52 GB，half/BF16：13 * 2 = 26 GB，int8：13 GB，int4：13 * 0.5 = 6.5 GB&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;训练内存使用估算&lt;/h2&gt;

&lt;p&gt;为了确保训练期间模型收敛，参数类型不能是int8或int4。通常使用float，对于稍低的性能，BF16也是一种选择。&lt;/p&gt;

&lt;p&gt;由于反向传播、Adam优化和Transformer架构等因素，训练所需的内存通常是相同大小LLM推理所需的3到4倍。一个保守的估计是使用4作为因子进行计算。&lt;/p&gt;

&lt;p&gt;假设要训练Qwen-7B模型，所需的内存为：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;对于float类型：7（10亿参数）* 4（float的字节数）* 4 = 112 GB&lt;/li&gt;
&lt;li&gt;对于half/BF16类型参数：7（10亿参数）* 2（每个BF16参数字节数）* 4 = 56 GB&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;结论&lt;/h2&gt;

&lt;p&gt;理解像Mistral-7B这样的模型的内存需求量对于优化其部署和使用至关重要。对于考虑使用云计算服务进行模型训练和推理的人来说更是如此，因为它会影响到硬件的选择和整体成本。&lt;/p&gt;
</content>
   </entry>
   
   <entry>
     <title>什么是1-bit LLM</title>
     <link href="https://it.deepinmind.com/llm/2024/03/29/what-is-1bit-llm.html"/>
     <updated>2024-03-29T20:33:41+08:00</updated>
     <id>https://it.deepinmind.com/llm/2024/03/29/what-is-1bit-llm</id>
     <content type="html">&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/2024032911073901.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;生成式AI领域正在飞速发展，最新加入这个快速演进领域的是一1比特LLMs。你可能不相信，但它可以改变很多事情，并有助于消除与LLMs相关的一些最大挑战，尤其是它们庞大尺寸问题。&lt;/p&gt;

&lt;p&gt;通常情况下（不总是这样），无论是LLMs还是逻辑回归等机器学习模型，其权重都以32位浮点数或16位浮点数的形式存储。&lt;/p&gt;

&lt;p&gt;这就是为什么我们无法在本地系统和生产环境中使用GPT等大型模型的原因。因为这些模型具有大量权重，由于权重的高精度值导致模型体积庞大。&lt;/p&gt;

&lt;p&gt;假设我们有一个名为“MehulGPT”的LLM，它有70亿个参数（类似于Mistral或Llama-7B），使用32位精度（4字节）。该模型将占用的总内存为：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;总内存 = 单个权重的大小 * 权重数量&lt;br&gt;
总内存 = 4字节 * 7,000,000,000&lt;br&gt;
总内存 = 28,000,000,000字节&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;将其转换为GB，我们得到：&lt;br&gt;
- 总内存 = 28,000,000,000字节 / 1024³ 字节每GB&lt;br&gt;
总内存 ≈ 26.09 GB  &lt;/p&gt;

&lt;p&gt;这是一个巨大的体积，因此许多设备都无法使用它，包括手机，因为它们没有这么大的存储空间或硬件能力来运行这些模型。&lt;/p&gt;

&lt;p&gt;那么，如何使LLMs适用于小型设备和手机呢？&lt;/p&gt;

&lt;h4&gt;&lt;strong&gt;1比特LLMs&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;在1比特LLMs中，仅使用1比特（即0或1）来存储权重参数，而传统LLMs使用32/16比特。这大大减少了总体积，从而使小型设备也能使用LLMs。假设是“MehulGPT”的1比特版本。这次占用的内存为：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;总内存 = 单个权重的大小 * 权重数量&lt;br&gt;
总内存 = 0.125字节 * 7,000,000,000&lt;br&gt;
总内存 = 875,000,000字节&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;将其转换为千兆字节（GB），我们得到：  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;总内存 = 875,000,000字节 / 1024³ 字节每GB&lt;br&gt;
总内存 ≈ 0.815 GB&lt;br&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;1比特 = 0.125字节
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;因此，节省了大量的计算和存储资源。&lt;/p&gt;

&lt;h4&gt;这是不是和量化类似？&lt;/h4&gt;

&lt;p&gt;有的读者可能不了解量化，它是一种通过降低权重的精度来减小模型大小的方法，例如从32位减少到8位，从而减小4倍的大小。使用的位数越低，模型的大小就越小，但性能也会受到影响。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;1比特LLMs类似于量化思想，但有所不同。在量化中，我们降低了精度（所以如果一个权重值是2.34567890656373…，它可能会被减少到2.3456）。&lt;/p&gt;

&lt;p&gt;在1比特LLM中，每个权重将仅由二进制运算符（0,1）表示，因此模型更为精简。进行了一些主要的架构更改，以确保与传统LLMs相比，性能不受影响。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;&lt;strong&gt;BitNet b1.58&lt;/strong&gt;&lt;/h4&gt;

&lt;p&gt;BitNet b1.58是首款1比特LLM，目前使用1.58比特/权重（因此并非严格的1比特LLM），其中权重可以有3个可能的值（-1,0,1）。&lt;/p&gt;

&lt;p&gt;对于1.58比特：
1）权重只有-1,0,1的值。
2）由于值仅为-1,0,1，因此不需要乘法操作。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/2024032911095402.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;

&lt;p&gt;根据论文中所说：&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;BitNet b1.58在困惑度和终端任务性能方面与16位浮点数LLM基线相当。

它提供了更快的处理速度，并且比传统模型使用更少的GPU内存。

模型最小化矩阵乘法中的乘法操作，提高了优化和效率。

包括用于系统级优化的量化函数，并集成了类似于LLaMA的组件，如RMSNorm和SwiGLU。
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;注意：我暂时避开了上面提到的术语，因为解释它们需要另写一篇文章。&lt;/p&gt;

&lt;p&gt;该模型目前尚未公开，因此还没有被公开测试。但是，这看起来非常有前景，如果它声称的优点属实，我们将迎来一场盛宴！&lt;/p&gt;
</content>
   </entry>
   
   <entry>
     <title>Agent是如何工作的：概念及LangChain实现</title>
     <link href="https://it.deepinmind.com/llm/2024/03/26/how-agent-works.html"/>
     <updated>2024-03-26T20:33:41+08:00</updated>
     <id>https://it.deepinmind.com/llm/2024/03/26/how-agent-works</id>
     <content type="html">&lt;h1&gt;LLM在自主agent领域的应用&lt;/h1&gt;

&lt;p&gt;LLM（大型语言模型）在自主Agent领域的应用受到了广泛关注。你可能已经在诸如&lt;strong&gt;Auto-GPT&lt;/strong&gt;、&lt;strong&gt;BabyAGI&lt;/strong&gt;等流行应用中了解过它们的用法，这些应用几乎每天都层出不穷。&lt;/p&gt;

&lt;p&gt;理解这些应用的基本原理并不复杂，因为大多数工具的工作流程大致相同。&lt;/p&gt;

&lt;h3&gt;使用代理背后的直观思路&lt;/h3&gt;

&lt;p&gt;让我们首先从高层次上理解流程，我们将根据需要引出相关的概念。&lt;/p&gt;

&lt;p&gt;使用Agent的想法很简单。我们已经有了理解人类语言并能够相当好地进行推理的模型。这些模型包括像&lt;strong&gt;GPT&lt;/strong&gt;这样的模型以及其他开源替代品。到目前为止，这些模型仅限于“讨论”如何完成事情，无论是编写构建应用程序的代码还是列出执行诸如设立慈善基金等操作的步骤。如果我们能让它们根据这些智能采取行动会怎样呢？&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/20240325141241.jpg&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;

&lt;p&gt;在软件工程的世界里，实现此类动作的传统方式是通过&lt;strong&gt;API（应用程序编程接口）&lt;/strong&gt;。API可以将执行特定操作的应用或服务的能力暴露给其他软件或前端（供人类使用）。这让我们想到，如果模型也能使用这些API，它就有可能根据自己的知识采取行动。但如何让模型知道有哪些API可用，更重要的是，API能做什么呢？&lt;/p&gt;

&lt;p&gt;你只需要告诉它！&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/20240325142609.jpg&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;

&lt;p&gt;由于大语言模型强大的自然语言理解能力，很容易向它们解释API的功能以及在何种情况下应该使用。&lt;/p&gt;

&lt;p&gt;在这种情况下，LLM只需输出API的名称和应提供什么输入以获得期望的输出。然后，我们可以轻松设计逻辑，利用这些知识调用下游API，用正确的变量传递参数，并将输出返回给LLM进一步处理（它要么调用另一个API，要么直接以自然语言形式将结果返回给用户）。这个过程会反复进行，直到最后输出一个容易理解的最终输出。&lt;/p&gt;

&lt;h2&gt;深入探索&lt;/h2&gt;

&lt;p&gt;你可以将上述情况视为多个可独立解决的问题的组合。让我们再次回顾一下流程，这次我们会更技术地去介绍相关概念。&lt;/p&gt;

&lt;p&gt;粗略地说，上述流程可以分解为以下组件之间的交互：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;LLM模型&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编排器&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/20240326094106.jpg&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;

&lt;p&gt;以上每个组件在&lt;strong&gt;LangChain&lt;/strong&gt;（一个开源的LLM框架）中都会对应一个抽象概念。抽象只是一个接口，用于正式定义实体的责任和属性。我们将逐一了解这种结构能带来的好处。&lt;/p&gt;

&lt;h4&gt;🤖 Agent&lt;/h4&gt;

&lt;p&gt;Agent本质上是LLM模型的包装器，它可以接收需要传递给模型的输入。它们实际上是用LLM链构建的，也就是包含模型和一些附加元素（如提示模板）的管道。如果你对这些术语感到陌生，我建议你阅读LangChain关于此主题的文档。然而，对于理解Agent工作原理来说，这并非必不可少的知识，只要不超出本文其余部分的范围，你可以自由跳过。&lt;/p&gt;

&lt;p&gt;那么，Agent有什么特别之处，为什么需要它们而不是仅仅使用LLM链呢？Agent定义了一些额外的属性，使执行变得更加简单。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;通过预定义的提示词来控制模型所需的输出，这些提示词可以打包到Agent的概念中。下面是一个聊天Agent的提示词示例，它输出的格式后续可由该Agent或下游功能来进行解析。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;# flake8: noqa
SYSTEM_MESSAGE_PREFIX = &amp;quot;&amp;quot;&amp;quot;Answer the following questions as best you can. You have access to the following tools:&amp;quot;&amp;quot;&amp;quot;
FORMAT_INSTRUCTIONS = &amp;quot;&amp;quot;&amp;quot;The way you use the tools is by specifying a json blob.
Specifically, this json should have a `action` key (with the name of the tool to use) and a `action_input` key (with the input to the tool going here).

The only values that should be in the &amp;quot;action&amp;quot; field are: {tool_names}

The $JSON_BLOB should only contain a SINGLE action, do NOT return a list of multiple actions. Here is an example of a valid $JSON_BLOB:

&amp;gt;&amp;gt;&amp;gt;
{
  &amp;quot;action&amp;quot;: $TOOL_NAME,
  &amp;quot;action_input&amp;quot;: $INPUT
}
&amp;gt;&amp;gt;&amp;gt;
ALWAYS use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action:
&amp;gt;&amp;gt;&amp;gt;
$JSON_BLOB
&amp;gt;&amp;gt;&amp;gt;

Observation: the result of the action
... (this Thought/Action/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question&amp;quot;&amp;quot;&amp;quot;
SYSTEM_MESSAGE_SUFFIX = &amp;quot;&amp;quot;&amp;quot;Begin! Reminder to always use the exact characters `Final Answer` when responding.&amp;quot;&amp;quot;&amp;quot;
HUMAN_MESSAGE = &amp;quot;{input}\n\n{agent_scratchpad}&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;包含可以参与中间步骤并负责从这些步骤构建“scratchpad”或上下文的函数，并将其与上述提示词一起传递给模型，使它能够“思考”整个过程或目标。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;def get_full_inputs(
        self, intermediate_steps: List[Tuple[AgentAction, str]], **kwargs: Any
    ) -&amp;gt; Dict[str, Any]:
        &amp;quot;&amp;quot;&amp;quot;Create the full inputs for the LLMChain from intermediate steps.&amp;quot;&amp;quot;&amp;quot;
        thoughts = self._construct_scratchpad(intermediate_steps)
        new_inputs = {&amp;quot;agent_scratchpad&amp;quot;: thoughts, &amp;quot;stop&amp;quot;: self._stop}
        full_inputs = {**kwargs, **new_inputs}
        return full_inputs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;上面的函数定义在Agent类中，它构建了一个将传递给模型的输入，由一系列思考的scratchpad组成，这些scratchpad是按照与这个agent特定的初始提示词相符的方式构建的。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;列出特定类型的代理能够使用的工具，这有助于后续进行验证。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;@classmethod
    def _validate_tools(cls, tools: Sequence[BaseTool]) -&amp;gt; None:
        if len(tools) != 2:
            raise ValueError(f&amp;quot;Exactly two tools must be specified, but got {tools}&amp;quot;)
        tool_names = {tool.name for tool in tools}
        if tool_names != {&amp;quot;Lookup&amp;quot;, &amp;quot;Search&amp;quot;}:
            raise ValueError(
                f&amp;quot;Tool names should be Lookup and Search, got {tool_names}&amp;quot;
            )
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这是LangChain中一个agent定义的代码片段，它验证传递给agent的工具是否被命名为“Lookup”和“Search”，这是这种特定类型的agent所期望的。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;像AgentFinish和AgentAction这样的概念是LangChain的一部分，它们与agent一起使用，帮助区分响应的类别。例如，类型为AgentFinish的响应表明agent已经得出了结论。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;👨‍💻 API&lt;/h4&gt;

&lt;p&gt;这部分不需要太多解释。API使得可以通过外部服务执行各种任务。在LangChain中，有一个&lt;code&gt;Tool&lt;/code&gt;抽象，你可以定义一个函数，它可以接收输入，内部通过任何自定义的逻辑去调用外部服务、获取输出并返回。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;class BingSearchRun(BaseTool):
    &amp;quot;&amp;quot;&amp;quot;Tool that adds the capability to query the Bing search API.&amp;quot;&amp;quot;&amp;quot;

    name = &amp;quot;Bing Search&amp;quot;
    description = (
        &amp;quot;A wrapper around Bing Search. &amp;quot;
        &amp;quot;Useful for when you need to answer questions about current events. &amp;quot;
        &amp;quot;Input should be a search query.&amp;quot;
    )
    api_wrapper: BingSearchAPIWrapper

    def _run(self, query: str) -&amp;gt; str:
        &amp;quot;&amp;quot;&amp;quot;Use the tool.&amp;quot;&amp;quot;&amp;quot;
        return self.api_wrapper.run(query)

    async def _arun(self, query: str) -&amp;gt; str:
        &amp;quot;&amp;quot;&amp;quot;Use the tool asynchronously.&amp;quot;&amp;quot;&amp;quot;
        raise NotImplementedError(&amp;quot;BingSearchRun does not support async&amp;quot;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;上面的代码是Bing搜索工具。它具有以下特点：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;一个run函数，包含使用工具时调用的逻辑。您可以看到它使用了一个BingSearchAPIWrapper的run函数，这是进行对Bing的API调用的地方。&lt;/li&gt;
&lt;li&gt;每个工具都附带一个自然语言描述，说明了应该使用该工具的情况。这有助于模型确定何时选择一个特定的工具而不是另一个。ChatGPT中的插件也依赖类似的理念。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;🧙 编排器&lt;/h4&gt;

&lt;p&gt;这里说的编排器是指能够控制agent、用户和工具之间执行流程的实体。这包括：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;接收用户输入。&lt;/li&gt;
&lt;li&gt;将输入传递给agent（模型），同时提供正确的输入、提示词和过去的记忆上下文。&lt;/li&gt;
&lt;li&gt;获取输出，指示使用哪个工具以及使用什么输入。&lt;/li&gt;
&lt;li&gt;调用所需的工具或API，用输入获取响应，并将其返回给模型。&lt;/li&gt;
&lt;li&gt;根据工具输出将自然语言响应传递给用户。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;LangChain中的&lt;code&gt;AgentExecutor&lt;/code&gt;是我们的编排器，它来执行上述所有任务。&lt;/p&gt;

&lt;p&gt;用你想要的agent类型以及希望agent选择使用的工具来对它进行初始化。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;tools = [
    Tool(
        name = &amp;quot;Current Search&amp;quot;,
        func=search.run,
        description=&amp;quot;useful for when you need to answer questions about current events or the current state of the world. the input to this should be a single search term.&amp;quot;
    ),
]

agent_executor = initialize_agent(
    tools,
    llm,
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    callback_manager=manager,
    verbose=True,
)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;AgentExecutor做的事就是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;它里面的函数会去驱动agent循环地和工具进行交互。下面的代码展示了触发循环的&lt;em&gt;call函数以及从agent返回输出的&lt;/em&gt;take&lt;em&gt;next&lt;/em&gt;step函数。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;def _call(self, inputs: Dict[str, str]) -&amp;gt; Dict[str, Any]:
        &amp;quot;&amp;quot;&amp;quot;Run text through and get agent response.&amp;quot;&amp;quot;&amp;quot;
        ...
        # We now enter the agent loop (until it returns something).
        while self._should_continue(iterations, time_elapsed):
            next_step_output = self._take_next_step(
                name_to_tool_map, color_mapping, inputs, intermediate_steps
            )


def _take_next_step(
        ...
    ) -&amp;gt; Union[AgentFinish, List[Tuple[AgentAction, str]]]:
        &amp;quot;&amp;quot;&amp;quot;Take a single step in the thought-action-observation loop.

        Override this to take control of how the agent makes and acts on choices.
        &amp;quot;&amp;quot;&amp;quot;
        # Call the LLM to see what to do.
        output = self.agent.plan(intermediate_steps, **inputs)
        # If the tool chosen is the finishing tool, then we end and return.
        if isinstance(output, AgentFinish):
            return output
        ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;保存中间步骤，以便对代理（模型）的每个请求都可以使用，agent（模型）根据这些步骤回答问题。&lt;/li&gt;
&lt;li&gt;验证所提供的工具是否是选定的agent所支持或需要的。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;总结&lt;/h3&gt;

&lt;p&gt;了解了这些概念后，你现在可以理解LangChain提供的多种agent类型，并开始尝试不同的工具，看看它们如何协同工作。&lt;/p&gt;

&lt;p&gt;你还可以将学到的这些用于最近出现的基于agent的新应用中。如果想了解其他工具的功能和实现细节，以便理解它们的工作原理并根据需要去修改，建议去看一下LangChain的博客，里面列出了&lt;strong&gt;Auto-GPT&lt;/strong&gt;、&lt;strong&gt;BabyAGI&lt;/strong&gt;等流行应用的特性及一些实现细节。&lt;/p&gt;
</content>
   </entry>
   
   <entry>
     <title>双显卡运行Qwen1.5-72B-int4量化版本</title>
     <link href="https://it.deepinmind.com/llm/2024/03/23/qwen-1.5-int4-double-graphics-cards.html"/>
     <updated>2024-03-23T21:33:41+08:00</updated>
     <id>https://it.deepinmind.com/llm/2024/03/23/qwen-1.5-int4-double-graphics-cards</id>
     <content type="html">&lt;p&gt;我本机配置是4090，3090分别一张，共48G显存。&lt;/p&gt;

&lt;h3&gt;下载Qwen1.5&lt;/h3&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;git clone https://github.com/QwenLM/Qwen.git
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;安装相关依赖&lt;/h3&gt;

&lt;p&gt;按照官方文档安装相关依赖的库。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;cd Owen/
pip install -r requirements.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;我这边自己的环境是
Python 3.11
PyTorch 2.2.1
Transformers 4.37.0
Cuda 12.2&lt;/p&gt;

&lt;h3&gt;下载模型&lt;/h3&gt;

&lt;p&gt;可以从modelscope下载。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;git clone https://www.modelscope.cn/qwen/Qwen1.5-14B-Chat-GPTQ-Int4.git
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;记得先把git lfs安装好了。&lt;/p&gt;

&lt;p&gt;原来看别的文章说还要单独指定调整各层的使用的显卡，目前的版本看是不需要了，直接回到刚才Qwen的工程目录下&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;python openai_api.py -c /path/to/model/Qwen1.5-14B-Chat-GPTQ-Int4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;就可以运行起来了。&lt;/p&gt;
</content>
   </entry>
   
   <entry>
     <title>深入学习机器学习中module 'd2l.torch' has no attribute 'train_ch3'报错的问题</title>
     <link href="https://it.deepinmind.com/llm/2024/03/20/d2l-torch-no-attribute-train-ch3.html"/>
     <updated>2024-03-20T20:33:41+08:00</updated>
     <id>https://it.deepinmind.com/llm/2024/03/20/d2l-torch-no-attribute-train-ch3</id>
     <content type="html">&lt;p&gt;网上搜了下，主要是最新版本和书里的版本不一致的问题，可以到https://github.com/d2l-ai/d2l-zh/blob/master/d2l/torch.py把这个torch.py文件下载下来，到指定的python的库下面把对应文件替换掉就好。&lt;/p&gt;

&lt;p&gt;我这边是在./Library/Python/3.8/lib/python/site-packages/d2l/下面&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;__init__.py jax.py      mxnet.py    tensorflow.py   torch.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;把torch.py文件替换掉就可以了。&lt;/p&gt;
</content>
   </entry>
   
   <entry>
     <title>cuda及pytorch对应版本安装</title>
     <link href="https://it.deepinmind.com/llm/2024/03/11/flash-attn-cuda-cudnn-pytorch-install.html"/>
     <updated>2024-03-11T21:33:41+08:00</updated>
     <id>https://it.deepinmind.com/llm/2024/03/11/flash-attn-cuda-cudnn-pytorch-install</id>
     <content type="html">&lt;p&gt;由于单机两张卡，4090+2080Ti,在跑qwen1.5-14B模型的时候用到了两张卡，其中提示到flash-attn2只支持ampere或更新的GPU，查了下发现它暂时还不支持2080，搜了下网上说可以降到flash-attn1.x。于是尝试了pip install flash-attn=1.0.9.&lt;/p&gt;

&lt;p&gt;但报错，看了下是cuda版本不对导致的。于是打算重新安装，升级到cuda12.2。&lt;/p&gt;

&lt;h3&gt;安装cudatoolkit&lt;/h3&gt;

&lt;p&gt;选择驱动、toolkit对应的版本 https://developer.nvidia.com/cuda-toolkit-archive&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;wget https://developer.download.nvidia.com/compute/cuda/12.2.2/local_installers/cuda_12.2.2_535.104.05_linux.run
sudo sh cuda_12.2.2_535.104.05_linux.run
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;保存并加载环境变量&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;export PATH=/usr/local/cuda-12.2/bin${PATH:+:${PATH}}
export LD_LIBRARY_PATH=/usr/local/cuda-12.2/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

source ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;安装cuDNN&lt;/h3&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install cudnn-cuda-12
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3&gt;安装pytorch&lt;/h3&gt;

&lt;p&gt;https://pytorch.org/get-started/locally/
&lt;code&gt;
conda install pytorch torchvision torchaudio pytorch-cuda=12.1 -c pytorch -c nvidia
&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;flash-attn&lt;/h3&gt;

&lt;p&gt;不过最后试了下，flash-attention还是没OK，目前看qwen1.5的config.json里也没有关闭flash-attn的选项了，估计只能等它更新或者flash-attn支持2080了（看官方说应该后续会支持)。&lt;/p&gt;
</content>
   </entry>
   
   <entry>
     <title>Java IO类型</title>
     <link href="https://it.deepinmind.com/io/2022/01/24/java-sockets-i-o-blocking-non-blocking-and-asynchronous.html"/>
     <updated>2022-01-24T21:33:41+08:00</updated>
     <id>https://it.deepinmind.com/io/2022/01/24/java-sockets-i-o-blocking-non-blocking-and-asynchronous</id>
     <content type="html">&lt;h3&gt;介绍&lt;/h3&gt;

&lt;p&gt;描述IO类型时经常会交替地使用非阻塞、异步等术语，但这两个词是有着很大的区别的。本文将从理论和实践两个方面来说明下Java编程里的非阻塞和异步IO。&lt;/p&gt;

&lt;p&gt;TCP和UDP协议使用了套接字进行双端通信。Java的套接字 API则是底层操作系统具体实现的的适配器。兼容POSIX规范的操作系统（如Unix, Linux, Mac OS X, BSD, Solaris， AIX等）中使用的socket通信被称作伯克利套接字（Berkeley sockets）。Windows中的套接字叫winsock，它也是基于伯克利套接字，但增加了额外的功能用于支持windows的编程模型。&lt;/p&gt;

&lt;h4&gt;POSIX定义&lt;/h4&gt;

&lt;p&gt;本文使用的是POSIX规范中的简化版定义。&lt;/p&gt;

&lt;p&gt;阻塞线程————等待特定条件以便能继续执行的线程。&lt;/p&gt;

&lt;p&gt;阻塞————套接字的一种属性，所有对该套接字的调用都要等待所请求的动作执行完成后才返回。&lt;/p&gt;

&lt;p&gt;非阻塞————套接字的一种属性，当所请求的动作无法在可预期的时间内完成，对该套接字的调用无需等待直接返回。&lt;/p&gt;

&lt;p&gt;同步IO操作————它会阻塞所请求的线程，直到IO操作完成。&lt;/p&gt;

&lt;p&gt;异步IO操作————它本身不会导致请求线程阻塞，也就是说线程和IO操作可以并行执行。&lt;/p&gt;

&lt;p&gt;因此，根据POSIX规范，非阻塞和异步的区别显而易见：&lt;/p&gt;

&lt;p&gt;非阻塞————这是套接字的一种属性，对该套接字的调用无需等待直接返回。&lt;/p&gt;

&lt;p&gt;异步IO操作————这是IO操作的一种属性，该IO操作可以和请求线程并行执行。&lt;/p&gt;

&lt;h3&gt;IO模型&lt;/h3&gt;

&lt;p&gt;在兼容POSIX的操作系统上最常见的IO模型如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;阻塞IO&lt;/li&gt;
&lt;li&gt;非阻塞IO&lt;/li&gt;
&lt;li&gt;IO多路复用&lt;/li&gt;
&lt;li&gt;信号驱动（signal-driven ）IO&lt;/li&gt;
&lt;li&gt;异步IO&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;阻塞IO&lt;/h4&gt;

&lt;p&gt;在阻塞IO中，应用发起阻塞的系统调用，直到内核接受到数据，并将数据从内核空间拷贝到用户空间。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/0_cZoBWeg61ktOz_6F.png&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;优点：容易实现。&lt;/li&gt;
&lt;li&gt;缺点：线程被阻塞。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;非阻塞IO&lt;/h4&gt;

&lt;p&gt;非阻塞IO中，应用程序发起系统调用后，会立即返回如下其一的结果：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果IO操作能立即完成，则返回数据&lt;/li&gt;
&lt;li&gt;如果IO操作不能立即完成，则返回错误码，告知应用IO操作会阻塞或者设备暂时不可用。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;应用程序可以不停地循环等待（重复发起系统调用），直至IO操作完成。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/0_6MG53wgJeawlcvsz.png&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;优点：应用程序不会阻塞&lt;/li&gt;
&lt;li&gt;缺点：
应用在完成前要不断等待，会导致多余的用户态和内核态的上下文切换；
该模型可能会带来额外的IO延迟，因为在内核数据可用到数据被应用读取之间可能会存在时间间隔。&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;IO多路复用&lt;/h4&gt;

&lt;p&gt;在IO多路复用模型（也被称为带阻塞通知的非阻塞IO）中，应用会发起一次阻塞的select系统调用，来监视多个IO描述符上的活动。可以针对某个描述符的特定的IO操作（连接，读/写，发生错误等），在IO状态就绪时获得通知。当select调用返回时，至少会有一个描述符上的操作是准备就绪的，应用可以发起一次非阻塞的调用，将数据从内核空间复制到用户空间。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/0_iy3o3Zt7frjBLD8L.png&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;优点：可以通过一个线程同时在多个描述符上执行IO操作。&lt;/li&gt;
&lt;li&gt;缺点：
应用仍然会在select系统调用时发生阻塞。
并非所有操作系统都能很好的支持该模式&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;信号驱动IO&lt;/h4&gt;

&lt;p&gt;在信号驱动IO模型中，应用程序会发起一次非阻塞的调用，注册一个信号通知的handler。当某个描述符的特定IO操作就绪时，会生成一个信号来通知应用。然后这个注册的handler会将数据从内核拷贝到用户态。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/0_izKWlpsFtiwe7Go7.png&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;优点：应用不会阻塞。
性能比较不错。&lt;/li&gt;
&lt;li&gt;缺点：并非所有操作系统都能支持该模式&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;异步IO&lt;/h4&gt;

&lt;p&gt;在异步IO模型（也被称为重叠IO， overlapped I/O）中，应用发起非阻塞调用，来发起一次内核的后台操作。当操作完成时（内核已经接收到数据并将其从内核空间拷贝到用户空间），会发起一次完成的回调来结束这次IO操作。&lt;/p&gt;

&lt;p&gt;异步IO和信号驱动IO的不同之处在于信号驱动IO中，内核告诉应用IO操作可以开始了，但在异步IO中，内核告诉应用IO操作已经完成了。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://guanzhiapp.oss-cn-shanghai.aliyuncs.com/images/0_V4DURyt10-H6iwzJ.png&quot; alt=&quot;图片&quot;&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;优点：应用不会阻塞；性能最优。&lt;/li&gt;
&lt;li&gt;缺点：实现最复杂；并非所有操作系统都能支持的很好。&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;Java IO API&lt;/h3&gt;

&lt;p&gt;Java的输入输出API是用stream(InputStream, OutputStream)来代表阻塞的、单向的数据流。&lt;/p&gt;

&lt;h4&gt;Java NIO API&lt;/h4&gt;

&lt;p&gt;Java NIO是基于Channel, Buffer, Selector来实现的，通过这些类来驱动操作系统的底层的IO操作。&lt;/p&gt;

&lt;p&gt;Channel类代表了和一个能够进行IO操作（读或写）的实体（可以是硬件设备，文件，socket或者软件组件等）的连接。&lt;/p&gt;

&lt;p&gt;和stream是单向的不同，channel是一个双向的通道。&lt;/p&gt;

&lt;p&gt;Buffer类是一个定长的数据容器，有对应的方法用来读写数据。Channel中的所有数据交互都必须通过Buffer，不能直接操作：发送给Channel的数据，要先写入Buffer中，要从Channel中接收的数据也要读入到Buffer中。&lt;/p&gt;

&lt;p&gt;stream是面向字节的，而channel是面向块数据的。面向字节的IO更简单，但对某些IO对象而言操作效率比较低。面向块的IO速度更快，但实现起来也更复杂。&lt;/p&gt;

&lt;p&gt;Selector类可以在一次调用中订阅多个SelectableChannel对象的事件。当订阅的事件触发时，Selector类会将事件分发给对应的处理器来执行。&lt;/p&gt;

&lt;h4&gt;Java NIO2 API&lt;/h4&gt;

&lt;p&gt;Java NIO2是基于异步通道(AsynchronousServerSocketChannel, AsynchronousSocketChannel等)来实现的，可以实现异步IO操作（连接，读写，错误处理）。&lt;/p&gt;

&lt;p&gt;异步通道提供了两套机制来进行异步IO操作。第一种是返回一个java.util.concurrent.Future对象，用来代表一个挂起的操作，可以透过它来查询和获取结果。第二种是给对应的IO操作传入一个java.nio.channels.CompletionHandler对象，当IO操作完成或者失败时，会执行对应的代码。两种方式都是等效的。&lt;/p&gt;

&lt;p&gt;异步通道提供了一套平台无关的标准化地执行异步IO操作的方式。然而，Java API究竟能挖掘出多少底层操作系统的异步化能力的支持，这要取决于对该平台的支持好不好了。&lt;/p&gt;

&lt;h4&gt;socket实现的echo服务器&lt;/h4&gt;

&lt;p&gt;下面将会使用Java的套接字接口，来实现一个echo应用的服务端和客户端，以实现上述提到的这几种IO模型。这个应用工作原理如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;有一个服务端监听着TCP的7000端口；&lt;/li&gt;
&lt;li&gt;客户端应用通过一个动态的TCP端口来连接服务端的socket&lt;/li&gt;
&lt;li&gt;客户端从控制台读取输入信息后，通过socket将对应的字节发送给服务端&lt;/li&gt;
&lt;li&gt;服务端接收到后，将字节流再发回给客户端。&lt;/li&gt;
&lt;li&gt;客户端收到返回的信息后将它打印到控制台上。&lt;/li&gt;
&lt;li&gt;当客户端收到的字节流长度等于它发送的长度，它会断开和服务端的连接。&lt;/li&gt;
&lt;li&gt;当服务端接收到某个特殊的字符串时，它会停止监听端口。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这里面字符串和字节流间采用的是UTF—8编码进行转换。&lt;/p&gt;

&lt;p&gt;下面只列出了服务端的代码摘要，完成代码将在后面的链接中提供。&lt;/p&gt;

&lt;h4&gt;阻塞式IO&lt;/h4&gt;

&lt;p&gt;下面的例子实现的是阻塞式IO的版本。&lt;/p&gt;

&lt;p&gt;ServerSocket.accept方法会一直阻塞直到边接建立。InputStream.read则会阻塞直到输入的数据可用为止，或者客户端连接断开。OutputStream.write也会一直阻塞直到输出数据写入完毕。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;public class IoEchoServer {
   public static void main(String[] args) throws IOException {
       ServerSocket serverSocket = new ServerSocket(7000);
       while (active) {
           Socket socket = serverSocket.accept(); // blocking
           InputStream is = socket.getInputStream();
           OutputStream os = socket.getOutputStream();
           int read;
           byte[] bytes = new byte[1024];
           while ((read = is.read(bytes)) != -1) { // blocking
               os.write(bytes, 0, read); // blocking
           }
           socket.close();
       }
       serverSocket.close();
   }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;阻塞的NIO版本&lt;/h4&gt;

&lt;p&gt;下面会用J   ava的NIO来实现一个阻塞版IO。&lt;/p&gt;

&lt;p&gt;ServerSocketChannel和SocketChannel都默认采用阻塞模式。ServerSocketChannel.accept方法会阻塞直到连接建立并返回一个SocketChannel对象。ServerSocket.read会一直阻塞直到数据可用，或连接断开。ServerSocket.write也会阻塞直到数据写入成功。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;public class NioBlockingEchoServer {
   public static void main(String[] args) throws IOException {
       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
       serverSocketChannel.bind(new InetSocketAddress(&amp;quot;localhost&amp;quot;, 7000));
       while (active) {
           SocketChannel socketChannel = serverSocketChannel.accept(); // blocking
           ByteBuffer buffer = ByteBuffer.allocate(1024);
           while (true) {
               buffer.clear();
               int read = socketChannel.read(buffer); // blocking
               if (read &amp;lt; 0) {
                   break;
               }
               buffer.flip();
               socketChannel.write(buffer); // blocking
           }
           socketChannel.close();
       }
       serverSocketChannel.close();
   }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;非阻塞的NIO版本&lt;/h4&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;public class NioNonBlockingEchoServer {
   public static void main(String[] args) throws IOException {
       ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
       serverSocketChannel.configureBlocking(false);
       serverSocketChannel.bind(new InetSocketAddress(7000));
       while (active) {
           SocketChannel socketChannel = serverSocketChannel.accept(); // non-blocking
           if (socketChannel != null) {
               socketChannel.configureBlocking(false);
               ByteBuffer buffer = ByteBuffer.allocate(1024);
               while (true) {
                   buffer.clear();
                   int read = socketChannel.read(buffer); // non-blocking
                   if (read &amp;lt; 0) {
                       break;
                   }
                   buffer.flip();
                   socketChannel.write(buffer); // can be non-blocking
               }
               socketChannel.close();
           }
       }
       serverSocketChannel.close();
   }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4&gt;多路复用NIO&lt;/h4&gt;

&lt;p&gt;下面是NIO实现的多路复用IO模型。&lt;/p&gt;

&lt;p&gt;初始化过程中，多个ServerSocketChannel对象会配置成非阻塞划式，并通过SelectionKey.OP_ACCEPT注册到同一个Selector对象里，表明对连接创建成功的事件感兴趣。&lt;/p&gt;

&lt;p&gt;在主循环里，Selector.select方法会一直阻塞直到至少有一个注册事件发生了。Selector.selectedKeys方法会返回一组SelectionKey对象，里面包含已发生的事件。通过迭代SelectionKey对象，可以知道到底发生了什么IO事件（连接，或者读/写）以及事件关联的是哪个套接字。&lt;/p&gt;

&lt;p&gt;SelectionKey对象只是提示对应的通道已经准备好进行某个IO操作了，但并不确保结果。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;public class NioMultiplexingEchoServer {
   public static void main(String[] args) throws IOException {
       final int ports = 8;
       ServerSocketChannel[] serverSocketChannels = new ServerSocketChannel[ports];
       Selector selector = Selector.open();
       for (int p = 0; p &amp;lt; ports; p++) {
           ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
           serverSocketChannels[p] = serverSocketChannel;
           serverSocketChannel.configureBlocking(false);
           serverSocketChannel.bind(new InetSocketAddress(&amp;quot;localhost&amp;quot;, 7000 + p));
           serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
       }
       while (active) {
           selector.select(); // blocking
           Iterator&amp;lt;SelectionKey&amp;gt; keysIterator = selector.selectedKeys().iterator();
           while (keysIterator.hasNext()) {
               SelectionKey key = keysIterator.next();
               if (key.isAcceptable()) {
                   accept(selector, key);
               }
               if (key.isReadable()) {
                   keysIterator.remove();
                   read(selector, key);
               }
               if (key.isWritable()) {
                   keysIterator.remove();
                   write(key);
               }
           }
       }
       for (ServerSocketChannel serverSocketChannel : serverSocketChannels) {
           serverSocketChannel.close();
       }
   }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;当SelectionKey对象告知连接建立的事件已发生后，会通过ServerSocketChannel.accept方法调用（可以是非阻塞式的）来获取连接。完成了之后，会创建出一个新的非阻塞模式下的SocketChannel对象，并使用SelectionKey.OP_READ注册到同一个Selector上，声明对该通道的读事件感兴趣。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;private static void accept(Selector selector, SelectionKey key) throws IOException {
       ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
       SocketChannel socketChannel = serverSocketChannel.accept(); // can be non-blocking
       if (socketChannel != null) {
           socketChannel.configureBlocking(false);
           socketChannel.register(selector, SelectionKey.OP_READ);
       }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;当SelectionKey提示发生了读事件时，会调用SocketChannel.read方法（也可以是非阻塞的）来将数据从SocketChannel中读取到新的ByteBuffer对象中。然后再用SelectionKey.OP_WRITE将该SocketChannel注册到同一个Selector对象上，表明现在对写事件感兴趣。注册写事件时也会用到这个ByteBuffer对象。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;private static void read(Selector selector, SelectionKey key) throws IOException {
       SocketChannel socketChannel = (SocketChannel) key.channel();
       ByteBuffer buffer = ByteBuffer.allocate(1024);
       socketChannel.read(buffer); // can be non-blocking
       buffer.flip();
       socketChannel.register(selector, SelectionKey.OP_WRITE, buffer);
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;当SelectionKeys提示写事件发生时，会调用SocketChannel.write方法（可以是非阻塞的），将SelectionKey.attachment方法返回的ByteBuffer对象中所提取出来的数据中写入到SocketChannel。最后通过SocketChannel.close关闭连接。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;private static void write(SelectionKey key) throws IOException {
       SocketChannel socketChannel = (SocketChannel) key.channel();
       ByteBuffer buffer = (ByteBuffer) key.attachment();
       socketChannel.write(buffer); // can be non-blocking
       socketChannel.close();
   }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;每次读写完成后，都会移除SelectionKey对象以免被重复使用。但接受连接的SelectionKey不会被移除，确保还能用于后续的操作。&lt;/p&gt;

&lt;h4&gt;异步的NIO2&lt;/h4&gt;

&lt;p&gt;下面的例子会通过Java NIO2来实现一个异步IO的版本。这里用到了AsynchronousServerSocketChannel, AsynchronousSocketChannel以及完成事件handler的机制。&lt;/p&gt;

&lt;p&gt;AsynchronousServerSocketChannel.accept方法会初始化一个异步的接受连接的操作。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;public class Nio2CompletionHandlerEchoServer {
   public static void main(String[] args) throws IOException {
       AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
       serverSocketChannel.bind(new InetSocketAddress(7000));
       AcceptCompletionHandler acceptCompletionHandler = new AcceptCompletionHandler(serverSocketChannel);
       serverSocketChannel.accept(null, acceptCompletionHandler);
       System.in.read();
   }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;当连接建立时，会调用AcceptCompletionHandler，然后通过AsynchronousSocketChannel.read(ByteBuffer destination, A attachment, CompletionHandler&lt;Integer,? super A&gt; handler)方法初始化一个异步的读操作，数据将从向AsynchronousSocketChannel读取到一个新的ByteBuffer对象中。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;class AcceptCompletionHandler implements CompletionHandler&amp;lt;AsynchronousSocketChannel, Void&amp;gt; {
   private final AsynchronousServerSocketChannel serverSocketChannel;
   AcceptCompletionHandler(AsynchronousServerSocketChannel serverSocketChannel) {
       this.serverSocketChannel = serverSocketChannel;
   }
   @Override
   public void completed(AsynchronousSocketChannel socketChannel, Void attachment) {
       serverSocketChannel.accept(null, this); // non-blocking
       ByteBuffer buffer = ByteBuffer.allocate(1024);
       ReadCompletionHandler readCompletionHandler = new ReadCompletionHandler(socketChannel, buffer);
       socketChannel.read(buffer, null, readCompletionHandler); // non-blocking
   }
   @Override
   public void failed(Throwable t, Void attachment) {
       // exception handling
   }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;当读操作完成时，会调用ReadCompletionHandler类，再通过AsynchronousSocketChannel.write(ByteBuffer source, A attachment, CompletionHandler&lt;Integer,? super A&gt; handler)方法初始化一个异步写操作，将数据从ByteBuffer中写入到AsynchronousSocketChannel。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;class ReadCompletionHandler implements CompletionHandler&amp;lt;Integer, Void&amp;gt; {
   private final AsynchronousSocketChannel socketChannel;
   private final ByteBuffer buffer;
   ReadCompletionHandler(AsynchronousSocketChannel socketChannel, ByteBuffer buffer) {
       this.socketChannel = socketChannel;
       this.buffer = buffer;
   }
   @Override
   public void completed(Integer bytesRead, Void attachment) {
       WriteCompletionHandler writeCompletionHandler = new WriteCompletionHandler(socketChannel);
       buffer.flip();
       socketChannel.write(buffer, null, writeCompletionHandler); // non-blocking
   }
   @Override
   public void failed(Throwable t, Void attachment) {
       // exception handling
   }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;写操作完成时会调用WriteCompletionHandler，再通过AsynchronousSocketChannel.close方法关闭连接。&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;class WriteCompletionHandler implements CompletionHandler&amp;lt;Integer, Void&amp;gt; {
   private final AsynchronousSocketChannel socketChannel;
   WriteCompletionHandler(AsynchronousSocketChannel socketChannel) {
       this.socketChannel = socketChannel;
   }
   @Override
   public void completed(Integer bytesWritten, Void attachment) {
       try {
           socketChannel.close();
       } catch (IOException e) {
           // exception handling
       }
   }
   @Override
   public void failed(Throwable t, Void attachment) {
       // exception handling
   }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;在这个例子中，并没有用到attachment对象，因为所有需要用到的对象（AsynchronousSocketChannel, ByteBuffer）都在构造方法中传递进来了。&lt;/p&gt;

&lt;h3&gt;结论&lt;/h3&gt;

&lt;p&gt;socket通信中如何选择IO模型取决于网络通信的情况。如果IO操作比较耗时但不频繁，异步IO会是一个不错的选择。然后如果都是短而快的IO操作，同步IO可能会更好一些，能减少内核调用的处理开销。&lt;/p&gt;

&lt;p&gt;尽管Java针对不同的操作系统提供了一套标准化的Socket通信的机制，但执行性能仍然和具体的平台实现密切相关。可以看下&lt;a href=&quot;http://www.kegel.com/c10k.html&quot;&gt;这篇文章&lt;/a&gt;来了解下不同实现的区别。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://github.com/aliakh/demo-sockets-io-nio-nio2&quot;&gt;这里&lt;/a&gt;是文中例子的完整的代码实现。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://liakh-aliaksandr.medium.com/java-sockets-i-o-blocking-non-blocking-and-asynchronous-fb7f066e4ede&quot;&gt;英文原文链接&lt;/a&gt;&lt;/p&gt;
</content>
   </entry>
   
   <entry>
     <title>容易混淆的CAP及ACID定义</title>
     <link href="https://it.deepinmind.com/concurrent/2022/01/23/the-confusing-cap-and-acid-wording.html"/>
     <updated>2022-01-23T18:33:41+08:00</updated>
     <id>https://it.deepinmind.com/concurrent/2022/01/23/the-confusing-cap-and-acid-wording</id>
     <content type="html">&lt;h3&gt;容易混淆的CAP和ACID概念&lt;/h3&gt;

&lt;p&gt;CAP及ACID都有一些共同的概念：例如原子性，一致性等等。但也会带来一些问题，这些术语名字虽然是一样的，但背后的含义完全不一样。CAP是分布式系统的理论引申出来的，而ACID指的是数据库系统。而分布式数据库会同时提到CAP和ACID，这就产生了很多困惑。当有人提到“不能放弃一致性”，这到底意味着什么呢？我们先来看下ACID和CAP分别的定义是什么。&lt;/p&gt;

&lt;h3&gt;ACID与CAP——回顾&lt;/h3&gt;

&lt;p&gt;ACID里的要素最早是70年代提出的，最终在1983年构成了ACID这个专用术语。这里面：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A指的是原子性（Atomicity）&lt;/li&gt;
&lt;li&gt;C是一致性（Consistency）&lt;/li&gt;
&lt;li&gt;I是隔离性（Isolation）&lt;/li&gt;
&lt;li&gt;D是持久性（Durability）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CAP是Eric Brewer在2000年提出的猜想，在2002年由Seth Gilbert与Nancy Lynch完成了证明。这里面：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;C指的是一致性（Consistency）&lt;/li&gt;
&lt;li&gt;A指的是可用性（Availability）&lt;/li&gt;
&lt;li&gt;P指的是分区容错性（Partition-tolerance）&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;概念澄清&lt;/h3&gt;

&lt;p&gt;先来看张大图。&lt;/p&gt;

&lt;table&gt;&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&quot;text-align: left&quot;&gt;名词&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;数据库&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;CAP&lt;/th&gt;
&lt;th style=&quot;text-align: left&quot;&gt;是否存在概念混淆&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;事务&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;一系列操作&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;未使用&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;持久化&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;一旦事务提交，它的修改就是永久性的。&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;未使用&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;一致性&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;数据的一致性约束（数据类型，关系等）&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;对CAP而言，一致性其实指的是“原子一致性（Atomic Consistency）”。它是一种一致性模型，后面会讲到&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;同一名词，不同概念&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;隔离性&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;事务虽然是并行执行的，但对事务A来说，其它事务要么在前，要么在后&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;CAP中虽然没有使用，但ACID中的这个隔离性在CAP的一致性模型中有体现&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;不同名词，同一概念&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;原子性&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;要么全部发生，要么都不发生&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;在CAP中，原子性指是一致性模型，该模型用于证明CAP的理论。&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;同一名词，不同概念&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;可用性&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;数据库里这个概念不太常用。即使提到，和CAP中的也不一样，比如说，数据库的可用性并不要求&lt;strong&gt;所有&lt;/strong&gt;非故障节点都要响应。&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;分布式系统中的每一个非故障节点对其所收到的请求都必须能够响应。&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;同词同概念，但定义不同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&quot;text-align: left&quot;&gt;分区&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;不常使用该概念。如果提到则和CAP中的含义一样。&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;节点间会产生分区，分区间所有的消息均会丢失。&lt;/td&gt;
&lt;td style=&quot;text-align: left&quot;&gt;无&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

&lt;p&gt;下面我们再来展开一下细节部分：关于分布式数据库还有一些其它会引发其它歧义的地方。&lt;/p&gt;

&lt;h4&gt;事务（仅在ACID中）&lt;/h4&gt;

&lt;p&gt;事务指的是一组操作。任意的操作都可以读写多条数据。满足了ACID的定义，这组操作可以看作是单一操作。这并不是CAP要实现的目标，它要定义的是针对同一数据的多个操作，通常是同样的操作。&lt;/p&gt;

&lt;h4&gt;持久性（仅在ACID中）&lt;/h4&gt;

&lt;p&gt;“一旦事务成功提交，它的修改就是永久的，即使发生故障数据也不会丢失”（Once a transaction completes successfully, its changes to the state survive failures），这个定义已经很清晰了，但关于故障的描述还是不清晰的。解决办法通常是使用冗余存储：单节点上多块磁盘，和/或多节点，和/或多套部署。“数据不会丢失”（原话是“Survive“）并不意味着可用性：也就是说后续只要能够恢复数据就算满足要求。&lt;/p&gt;

&lt;p&gt;CAP本身并不提及持久性，它里面是隐含了持久性的要求的：CAP主要讲的是分区，而不是节点故障。&lt;/p&gt;

&lt;h4&gt;CAP中用可用性&lt;/h4&gt;

&lt;p&gt;在CAP中，可用性意味着分区系统中的每一个非故障节点都能正确处理请求。许多分布式系统也声称自己是分区下可用的，但只有&lt;strong&gt;部分&lt;/strong&gt;非故障节点能处理请求。这些系统并不满足CAP概念下的可用性。&lt;/p&gt;

&lt;h4&gt;CAP中的一致性和原子性&lt;/h4&gt;

&lt;p&gt;CAP中的一致性是原子一致性（Atomic Consistency）的简称。原子一致性是一种一致性模型。这个模型定义的是如何组织系统内的多个操作。具体是什么操作取决于是什么样的CAP系统。比如说，一个事务型系统里的一致性模型，事务的“提交”就是一个操作。CAP的证明是基于一个分布式共享内存模型来完成的，这个模型是由Lynch定义的，并包含了读/写/ACK。&lt;/p&gt;

&lt;p&gt;一致性模型的选型并不简单。有多种可选方式，因为这里面有多种权衡取舍的考量：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;这个模型是不是简单易用。这也取决于应用程序本身：某类一致性模型对某类应用会更容易使用。&lt;/li&gt;
&lt;li&gt;内存模型的的实现效率。这通常也取决于硬件以及物理部署。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;事实上ACID和CAP所采用的一致性模型都比较简单：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;顺序一致性，正如Lamport所定义的：“从应用程序的表现来看，就好像所有进程的内存访问都是分开并依次执行的。”&lt;/li&gt;
&lt;li&gt;原子一致性（也称线性一致性）除了满足顺序一致性外还要满足实时性的约束：“线性一致性和顺序一致性不同的是，它假设有一个对所有进程可见的全局时间。每个操作都对应一个时间段，从开始调用的时间到返回响应的时间，而操作则发生在这个时间段内的某个时间点。”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CAP所说的一致性就是原子一致性，这只是一个简称。CAP中的原子性（操作的顺序）和ACID中的原子性（全部发生或没发生）根本就不是同一个概念。&lt;/p&gt;

&lt;h4&gt;ACID中的一致性&lt;/h4&gt;

&lt;p&gt;ACID中的一致性指的是数据一致性。比如说，SQL数据库通常会实现如下的功能：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;这个字段不为空&lt;/li&gt;
&lt;li&gt;这个字段是一个数值&lt;/li&gt;
&lt;li&gt;这个字段引用了别的表中的另一个字段&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;数据库是不会允许破坏该约束的事务成功提交的。这就是ACID中的一致性的约束。它和CAP中的定义并不相同。&lt;/p&gt;

&lt;p&gt;还需要注意的是，数据库（不管是否是SQL数据库），不会实现所有的一致性约束。这里引用下Gray和Reuter说的一段话：“底层系统没有办法检查所有的一致性约束。很多约束一开始都没有正式支持。”&lt;/p&gt;

&lt;p&gt;再用下他们的话总结下ACID中的一致性定义就是：“需要记住的是事务范式中定义的一致性主要是句法层面的定义。”&lt;/p&gt;

&lt;h3&gt;ACID中的原子性&lt;/h3&gt;

&lt;p&gt;定义上看“要么都执行，要么都不执行”已经很直白了：比如说如下的事务，这是用伪代码模拟的两个账户间的转账交易：&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;begin
    val1 = read(account1)
    val2 = read(account2)
    newVal1 = val1 - 100
    newVal2 = val2 + 100
    write(account1, newVal1)
    write(account2, newVal2)
commit
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;原子性意味着这两个账户要么全都更新，要么全都不更新。如果账户1成功而账户2失败了，这两次写操作都会回滚。&lt;/p&gt;

&lt;p&gt;然而，ACID中的原子性并不代表事务间是彼此隔离的。也就是说，尽管出现以下现象，你也可以声称自己是满足ACID中的原子性的：
- 事务中写的值在提交前就对其它事务可见。
- 在事务中读到的值可能会被其它事务修改。如果多次读取该值，结果可能会不一样。&lt;/p&gt;

&lt;p&gt;比方说，上述提交的事务在许多SQL数据库中可能产生出现如下结果：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;账户1开始时余额为1000,账户2为0&lt;/li&gt;
&lt;li&gt;两个转账事务并行运行&lt;/li&gt;
&lt;li&gt;正常应该是账户1有800,账户2是200,但结果账户却是900。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因为原子性并不等同于ACID中的隔离性：原子性并不意味着事务是隔离的。&lt;/p&gt;

&lt;h3&gt;隔离性&lt;/h3&gt;

&lt;p&gt;Gray和Reuter对隔离性的定义是“尽管事务是并发执行的，但对每个事务T而言，其它事务要么在它之前执行，要么在它之后执行。”这就和CAP一样，它也定义出了一个一致性模。有了这层定义，任何事务都是完全隔离有。很容易理解也很容易使用。&lt;/p&gt;

&lt;p&gt;然而，隔离性很难有效地实现，数据库系统通常都要放松某些约束条件。结果就是ACID中的隔离性产生了诸多级别，包括“串行化（serializable）”，“可重复读（repeatable read）”，“读提交（read committed）”，“读未提交（read uncommitted）”。&lt;/p&gt;

&lt;p&gt;默认的级别一般都是“读提交”：事务只能读到已经提交的数据。&lt;/p&gt;

&lt;p&gt;这个看起来不难理解，不过也有几点要关注下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;正如Hellerstein、Stonebraker和Hamilton在论文中所提到的：”这两种定义（Gray早期的版本和ANSI官方定义的版本）都假设使用了锁进行并发控制，而不是用乐观锁或多版本控制。这说明这个定义在语义上是有问题的。“&lt;/li&gt;
&lt;li&gt;对于一个给定的隔离级别而言，所有数据库的表现都不尽相同。这点Martin Kleppmann在&lt;a href=&quot;https://martin.kleppmann.com/2014/11/25/hermitage-testing-the-i-in-acid.html&quot;&gt;他的文章&lt;/a&gt;中也提到了。&lt;/li&gt;
&lt;li&gt;隔离性不仅影响了功能正确性，还会影响到技术层面的正确性：大多数据库都用到了锁。并发执行会产生死锁“：各个事务间非预期的依赖或不受控制的依赖，会导致所有事务都会需要其它事务释放它所持有的资源。这种情况下，数据库引擎会停掉其中的一个事务，尽管这个事务从功能上或者语义上来看都是正确的可被执行的。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;现在数据库的使用者们需要理解更多关于数据库一致性模型的知识：他们需要理解自己所使用的数据库引擎究竟是如何实现的。&lt;/p&gt;

&lt;p&gt;我们再来看下上述的例子。如果一个数据库默认实现的是”读提交“，结果可能会是错误的，最终账户中会多产生了钱。这种情况可以通过显式使用锁来解决。使用了锁的伪代码会是这样的：&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;code class=&quot;language-text&quot; data-lang=&quot;text&quot;&gt;&lt;span&gt;&lt;/span&gt;begin
    val1 = readAndLock(account1)
    val2 = readAndLock(account2)
    newVal1 = val1 - 100
    newVal2 = val2 + 100
    write(account1, newVal1)
    write(account2, newVal2)
commit // release all locks
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;正如Java并发编程那样，使用了锁之后会带来一系列的复杂问题。而数据库本身也增加了锁使用的复杂性，比如锁的范围很广（页锁，表锁。。），锁还可以由数据库引擎动态升级。有的事务只是为了读取不可变数据，可能会因为死锁或性能原因而采用“读未提交”的隔离级别。如果在系统运行时什么地方不小心修改了这些不可变数据，则可能会引发复杂的问题。&lt;/p&gt;

&lt;p&gt;可以看到CAP中的C和ACID中的I是非常类似的。但CAP作为一个定理，可以严格遵循这个一致性模型，而数据库实现由于性能的要求，不得不增加了多种级别的参数化配置，使用者不得不去理解数据库的隔离机制最终是如何实现的。&lt;/p&gt;

&lt;h3&gt;结论&lt;/h3&gt;

&lt;p&gt;ACID中的4个定义，有三个的含义和CAP中的对应的概念是不一样的，这会让人混淆不清。并且这种混淆不仅限于字面上的重复概念，还需要深入实现并发应用程序的实现细节来理解其中的差异。实际上这就是让不少人转向“NoSQL“的原因之一。&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;http://blog.thislongrun.com/2015/03/the-confusing-cap-and-acid-wording.html&quot;&gt;英文原文链接&lt;/a&gt;&lt;/p&gt;
</content>
   </entry>
   

</feed>
