序文
過去 6 か月間、ChatGPT の人気が LLM 全体の方向性を直接刺激しましたが、結局のところ、LLM は過去の経験データに基づいて事前トレーニングされており、最新の知識やそれぞれのプライベートな知識を取得することはできません。企業。
- ChatGPT plus版では、最新の知見を得るためにbing検索機能が統合されており、一部のモデルでは「さまざまなAIモデルやツールを連携させるlangchain」と位置付けられるbing機能を呼び出します。
- 企業のプライベートナレッジを処理するために、オープンソースモデルに基づいて微調整することも、ベクターデータベースとラングチェーンに統合されたLLMに基づいてローカルナレッジベースQ&Aを構築することもできます(独自性は何ですか)ベクトル データベースの説明はここにありますか?たとえば、従来のデータベースの画像検索にはキーワードによる検索が含まれますが、ベクトル データベースでは、セマンティクスを通じて画像内の同じまたは類似のベクトルを検索し、結果を表示します)。
したがって、ますます多くの人が langchain に注目し、データベース、ナレッジ グラフ、LLM の組み合わせアプリケーションを直接促進する LLM と組み合わせ始めています (詳細については、次の記事を参照してください: ナレッジ グラフの実践入門: KG とは何かLLMとKG/DBの実践的な組み合わせまで)
この記事は説明に重点を置いています
- LangChain と Langchain の全体的な構造とは何ですか?
- langchain-ChatGLM プロジェクトの主要なソース コードを解釈することは、単にツールとして使用するだけではなく、ツールの原理をよりよく理解することでツールをよりスムーズに使用できるようになります。多くのプロジェクトと技術的なポイントが含まれているため、最初は理解するのが難しく、最初は
混乱しやすかったですが、幸いなことに、プロジェクトのプロセスを段階的に追跡した後、明確なコード構造を全員に提示できましたその
過程で、langchain-ChatGLM プロジェクトに連絡してから全体のソース コードを整理して明確に記述するまでに 1 週間近くかかりましたが、この記事を読めば 1 日もかからずに理解できるかもしれません (効率がほぼ 7 倍向上します)これがこの記事の価値と重要性の 1 つです。 - langchain-ChatGLM プロジェクトのアップグレード バージョン: langchain-Chatchat
読んでいる途中でご質問がございましたら、お気軽にメッセージを残していただければ、時間内に一つずつ返信/回答させていただき、一緒に議論し、深く掘り下げていきます。
パート 1 : LangChain とは: LLM のプラグイン/関数ライブラリ
1.1 ラングチェーンの全体構造
平たく言えば、いわゆるlangchain(公式Webサイトのアドレス、GitHubのアドレス)は、AIで一般的に使用される多くの機能をライブラリにカプセル化し、さまざまな商用モデルAPIやオープンソースモデルを呼び出すためのインターフェースを備えており、以下のコンポーネントをサポートしています。
初めてこれに触れる友人は、あまりにも多くのコンポーネントを見て混乱するかもしれません (非常に多くのものがカプセル化されており、LLM に必要なすべての機能/ツールをカプセル化したいように感じます)。ラングチェーン ライブラリ全体は、基本層、機能層、アプリケーション層の 3 つの主要な層に分かれています。
1.1.1 基本層: モデル、LLM、インデックス
1.1.1.1 モデル: モデル
OpenAIの各種API/GPT-4など、さまざまなタイプのモデルとモデル統合により、
APIを介して質疑応答を完了するなど、さまざまな基本モデルに統一されたインターフェイスが提供されます。
import os
os.environ["OPENAI_API_KEY"] = '你的api key'
from langchain.llms import OpenAI
llm = OpenAI(model_name="text-davinci-003",max_tokens=1024)
llm("怎么评价人工智能")
得られた答えは以下のとおりです
1.1.1.2 LLMS層
この層は、主に次のようなモデル層機能とサービス指向の出力機能のカプセル化に重点を置いています。
- さまざまな LLM モデル管理プラットフォーム: モデルの豊富さと使いやすさを重視
- 統合されたサービス機能製品: すぐに使用できることを重視
- 差別化された機能: たとえば、プロンプト管理 (プロンプト管理、プロンプト最適化、プロンプト直列化を含む)、共有リソースに基づくモデル実行モードなどに焦点を当てます。
たとえば、Google の PaLM Text API、またはllms/openai.py ファイルの下
model_token_mapping = {
"gpt-4": 8192,
"gpt-4-0314": 8192,
"gpt-4-0613": 8192,
"gpt-4-32k": 32768,
"gpt-4-32k-0314": 32768,
"gpt-4-32k-0613": 32768,
"gpt-3.5-turbo": 4096,
"gpt-3.5-turbo-0301": 4096,
"gpt-3.5-turbo-0613": 4096,
"gpt-3.5-turbo-16k": 16385,
"gpt-3.5-turbo-16k-0613": 16385,
"text-ada-001": 2049,
"ada": 2049,
"text-babbage-001": 2040,
"babbage": 2049,
"text-curie-001": 2049,
"curie": 2049,
"davinci": 2049,
"text-davinci-003": 4097,
"text-davinci-002": 4097,
"code-davinci-002": 8001,
"code-davinci-001": 8001,
"code-cushman-002": 2048,
"code-cushman-001": 2048,
}
1.1.1.3 索引:ベクトル方式、KG方式
ユーザーのプライベート テキスト、画像、PDF などのさまざまなドキュメント (外部データとモデルの相互作用を可能にする構造化ドキュメントに相当) を保存および取得するには、Vector プランと KG プランの 2 つの具体的な実装プランがあります。
インデックスのベクトル方式
Vector ソリューションの場合: ファイルは最初にチャンクに分割され、次にチャンクがそれぞれエンコード、保存、取得されます。このコード ファイルを参照できます: langchain/libs/langchain/langchain/indexes/vectorstore.py このコード
ファイル順次実装されます。
モジュールのインポート: さまざまな型チェック、データ構造、事前定義されたクラスと関数がインポートされ、次に、関数 _get_default_text_splitter と 2 つのクラス VectorStoreIndexWrapper および VectorstoreIndexCreator
が実装されます。
_get_default_text_splitter関数:
これは、ブロック間に重複があるサイズ 1000 のブロックにテキストを再帰的に分割できるデフォルトのテキスト スプリッターを返すプライベート関数です。
# 默认的文本分割器函数
def _get_default_text_splitter() -> TextSplitter:
return RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
次はVectorStoreIndexWrapperクラスです。
これは主にベクトル ストア (Vector Store) へのアクセスとクエリを便利にするためのラッパー クラスです。
- Vectorstore: ベクター ストレージ オブジェクトの属性
vectorstore: VectorStore # 向量存储对象 class Config: """Configuration for this pydantic object.""" extra = Extra.forbid # 额外配置项 arbitrary_types_allowed = True # 允许任意类型
- query: 質問文字列を受け取り、ベクトル ストアにクエリを実行して答えを取得するメソッド
上に表示されるエクストラクタについて説明します# 查询向量存储的函数 def query( self, question: str, # 输入的问题字符串 llm: Optional[BaseLanguageModel] = None, # 可选的语言模型参数,默认为None retriever_kwargs: Optional[Dict[str, Any]] = None, # 提取器的可选参数,默认为None **kwargs: Any # 其他关键字参数 ) -> str: """Query the vectorstore.""" # 函数的文档字符串,描述函数的功能 # 如果没有提供语言模型参数,则使用OpenAI作为默认语言模型,并设定温度参数为0 llm = llm or OpenAI(temperature=0) # 如果没有提供提取器的参数,则初始化为空字典 retriever_kwargs = retriever_kwargs or {} # 创建一个基于语言模型和向量存储提取器的检索QA链 chain = RetrievalQA.from_chain_type( llm, retriever=self.vectorstore.as_retriever(**retriever_kwargs), **kwargs ) # 使用创建的QA链运行提供的问题,并返回结果 return chain.run(question)
抽出器はまず質問に関連する文書または断片を大規模なコーパスから取得し、生成器はこれらの取得した文書に基づいて回答を生成します。
エクストラクターは、次のようなさまざまなテクノロジーに基づいて作成できます。
a. キーワードベースの検索: キーワード マッチングを使用して関連ドキュメントを検索します
b. ベクトル空間モデル: ドキュメントとクエリの両方をベクトルとして表し、それらの間の類似性を計算することで関連ドキュメントを検索します c
. 深層学習ベースの方法: 事前トレーニングされたニューラルを使用しますd. ネットワーク モデル (BERT、RoBERTa など) を使用してドキュメントとクエリをベクトルにエンコードし、類似度計算を実行します
d. インデックス作成方法: 逆インデックスなど、検索エンジンで一般的に使用され、迅速に検索できるテクノロジです。特定の単語やフレーズ
これらの方法を単独で使用することも、組み合わせて使用することもでき、検索の精度と速度を向上させることができます。 - query_with_sources: クエリに似ていますが、クエリ結果に関連するデータ ソースも返します。
# 查询向量存储并返回数据源的函数 def query_with_sources( self, question: str, llm: Optional[BaseLanguageModel] = None, retriever_kwargs: Optional[Dict[str, Any]] = None, **kwargs: Any ) -> dict: """Query the vectorstore and get back sources.""" llm = llm or OpenAI(temperature=0) # 默认使用OpenAI作为语言模型 retriever_kwargs = retriever_kwargs or {} # 提取器参数 chain = RetrievalQAWithSourcesChain.from_chain_type( llm, retriever=self.vectorstore.as_retriever(**retriever_kwargs), **kwargs ) return chain({chain.question_key: question})
最後に、 VectorstoreIndexCreatorクラスがあります。
ベクトルストレージインデックスを作成するクラスです
- Vectorstore_cls: 使用されるベクトル ストレージ クラス、デフォルトは Chroma
簡略化されたベクター ストアは、各行がアイテム (ドキュメント、画像、文章など) を表し、各アイテムに関連付けられた高次元ベクトルを持つ大きなテーブルまたはデータベースと考えることができます。ベクトルの次元は、使用される埋め込みモデルに応じて、数十から数千の範囲になります。vectorstore_cls: Type[VectorStore] = Chroma # 默认使用Chroma作为向量存储类
次に例を示します。アイテムID ベクトル(高次元空間内) 1 [0.34、-0.2、0.5、...] 2 [-0.1、0.3、-0.4、...] ... ... - embedding: 使用される埋め込みクラス。デフォルトは OpenAIEmbeddings です。
embedding: Embeddings = Field(default_factory=OpenAIEmbeddings) # 默认使用OpenAIEmbeddings作为嵌入类
- text_splitter: テキストを分割するためのテキスト スプリッター
text_splitter: TextSplitter = Field(default_factory=_get_default_text_splitter) # 默认文本分割器
- from_loaders: 指定されたローダーのリストからベクトル ストレージ インデックスを作成します。
# 从加载器创建向量存储索引的函数 def from_loaders(self, loaders: List[BaseLoader]) -> VectorStoreIndexWrapper: """Create a vectorstore index from loaders.""" docs = [] for loader in loaders: # 遍历加载器 docs.extend(loader.load()) # 加载文档 return self.from_documents(docs)
- from_documents: 指定されたドキュメントのリストからベクトル ストレージ インデックスを作成します。
# 从文档创建向量存储索引的函数 def from_documents(self, documents: List[Document]) -> VectorStoreIndexWrapper: """Create a vectorstore index from documents.""" sub_docs = self.text_splitter.split_documents(documents) # 分割文档 vectorstore = self.vectorstore_cls.from_documents( sub_docs, self.embedding, **self.vectorstore_kwargs # 从文档创建向量存储 ) return VectorStoreIndexWrapper(vectorstore=vectorstore) # 返回向量存储的包装对象
インデックスKGプラン_
KG ソリューションの場合: この部分は LLM を使用してファイル内のトリプルを抽出し、その後の取得のためにそれらを KG として保存します。このコード ファイルを参照できます: langchain/libs/langchain/langchain/indexes /graph.py
"""Graph Index Creator.""" # 定义"图索引创建器"的描述
# 导入相关的模块和类型定义
from typing import Optional, Type # 导入可选类型和类型的基础类型
from langchain import BasePromptTemplate # 导入基础提示模板
from langchain.chains.llm import LLMChain # 导入LLM链
from langchain.graphs.networkx_graph import NetworkxEntityGraph, parse_triples # 导入Networkx实体图和解析三元组的功能
from langchain.indexes.prompts.knowledge_triplet_extraction import ( # 从知识三元组提取模块导入对应的提示
KNOWLEDGE_TRIPLE_EXTRACTION_PROMPT,
)
from langchain.pydantic_v1 import BaseModel # 导入基础模型
from langchain.schema.language_model import BaseLanguageModel # 导入基础语言模型的定义
class GraphIndexCreator(BaseModel): # 定义图索引创建器类,继承自BaseModel
"""Functionality to create graph index.""" # 描述该类的功能为"创建图索引"
llm: Optional[BaseLanguageModel] = None # 定义可选的语言模型属性,默认为None
graph_type: Type[NetworkxEntityGraph] = NetworkxEntityGraph # 定义图的类型,默认为NetworkxEntityGraph
def from_text(
self, text: str, prompt: BasePromptTemplate = KNOWLEDGE_TRIPLE_EXTRACTION_PROMPT
) -> NetworkxEntityGraph: # 定义一个方法,从文本中创建图索引
"""Create graph index from text.""" # 描述该方法的功能
if self.llm is None: # 如果语言模型为None,则抛出异常
raise ValueError("llm should not be None")
graph = self.graph_type() # 创建一个新的图
chain = LLMChain(llm=self.llm, prompt=prompt) # 使用当前的语言模型和提示创建一个LLM链
output = chain.predict(text=text) # 使用LLM链对文本进行预测
knowledge = parse_triples(output) # 解析预测输出得到的三元组
for triple in knowledge: # 遍历所有的三元组
graph.add_triple(triple) # 将三元组添加到图中
return graph # 返回创建的图
async def afrom_text( # 定义一个异步版本的from_text方法
self, text: str, prompt: BasePromptTemplate = KNOWLEDGE_TRIPLE_EXTRACTION_PROMPT
) -> NetworkxEntityGraph:
"""Create graph index from text asynchronously.""" # 描述该异步方法的功能
if self.llm is None: # 如果语言模型为None,则抛出异常
raise ValueError("llm should not be None")
graph = self.graph_type() # 创建一个新的图
chain = LLMChain(llm=self.llm, prompt=prompt) # 使用当前的语言模型和提示创建一个LLM链
output = await chain.apredict(text=text) # 异步使用LLM链对文本进行预测
knowledge = parse_triples(output) # 解析预测输出得到的三元组
for triple in knowledge: # 遍历所有的三元组
graph.add_triple(triple) # 将三元组添加到图中
return graph # 返回创建的图
さらに、インデックスを作成するには、次の機能が必要です。
- ドキュメント ローダーは、ドキュメントをロードするための標準インターフェイスであり、
Arxiv、電子メール、Excel、Markdown、PDF (ChatPDF などのアプリケーションに使用できる)、Youtube などのさまざまな形式のドキュメントやデータ ソースと統合されます。 docstore (
wikipedia.py
およびその他の
document_transformersが含まれます) - 埋め込み( langchain/libs/langchain/langchain/embeddings ) には、さまざまなコード ファイルに反映されるさまざまな埋め込みアルゴリズムが含まれます:
elasticsearch.py、google_palm.py、gpt4all.py、huggingface.py、huggingface_hub.py、
llamacpp.py、minimax .py、modelscope_hub.py、mosaicml.py
openai.py
、sentence_transformer.py、spacey_embeddings.py、tensorflow_hub.py、vertexai.py
1.1.2 機能層: チェーン、メモリ、ツール
基本レイヤーがコア能力を提供する場合、能力レイヤーはこれらの能力に手、足、脳をインストールし、チェーン、メモリ、ツールを含むすべてを記憶してトリガーする能力を与えます。
- チェーン: つまり
、リンクは、プロンプト テンプレート、言語モデル、出力パーサーなどのさまざまなコンポーネントへの一連の呼び出しと同等であり、連携してユーザー入力の処理、応答の生成、出力の処理を行います
。これは、さまざまなニーズに応じてさまざまな実行ロジックを抽象化およびカスタマイズすることに相当します。チェーンは相互にネストして、シリアルに実行できます。この層を通じて、LLM の機能をさまざまな業界にリンクできます。たとえば、Elasticsearch データベースとの対話: elasticsearch_database
、例 ナレッジ グラフの質問と回答に基づく: graph_qaのコード ファイル: chains/graph_qa/base.pyは、ナレッジ グラフに基づく質問と回答システムを実装します。具体的な手順は次のとおりです:まず、次に従ってナレッジ グラフ内の関連情報を検索します。抽出されたエンティティ。これは self.graph.get_entity_knowledge(entity)によって実現され、エンティティに関連するすべての情報がトリプルの形式で返されます。その後、すべてのトリプルが結合されてコンテキストが形成されます。最後に、質問とコンテキストそれらをまとめて qa_chain に入力すると、最終的な回答が得られます
たとえば、コードを自動的に生成して実行できます: llm_mathなど。entities = get_entities(entity_string) # 获取实体列表。 context = "" # 初始化上下文。 all_triplets = [] # 初始化三元组列表。 for entity in entities: # 遍历每个实体 all_triplets.extend(self.graph.get_entity_knowledge(entity)) # 获取实体的所有知识并加入到三元组列表中。 context = "\n".join(all_triplets) # 用换行符连接所有的三元组作为上下文。 # 打印完整的上下文。 _run_manager.on_text("Full Context:", end="\n", verbose=self.verbose) _run_manager.on_text(context, color="green", end="\n", verbose=self.verbose) # 使用上下文和问题获取答案。 result = self.qa_chain( {"question": question, "context": context}, callbacks=_run_manager.get_child(), ) return {self.output_key: result[self.qa_chain.output_key]} # 返回答案
プライベート ドメイン データの場合: qa_with_sources。コード ファイルchains/qa_with_sources/vector_db.py は、 ベクトル データベースを使用した質問への回答です。コアは次の 2 つの関数
reduce_tokens_below_limit
_get_docs# 定义基于向量数据库的问题回答类 class VectorDBQAWithSourcesChain(BaseQAWithSourcesChain): """Question-answering with sources over a vector database.""" # 定义向量数据库的字段 vectorstore: VectorStore = Field(exclude=True) """Vector Database to connect to.""" # 定义返回结果的数量 k: int = 4 # 是否基于token限制来减少返回结果的数量 reduce_k_below_max_tokens: bool = False # 定义返回的文档基于token的最大限制 max_tokens_limit: int = 3375 # 定义额外的搜索参数 search_kwargs: Dict[str, Any] = Field(default_factory=dict) # 定义函数来根据最大token限制来减少文档 def _reduce_tokens_below_limit(self, docs: List[Document]) -> List[Document]: num_docs = len(docs) # 检查是否需要根据token减少文档数量 if self.reduce_k_below_max_tokens and isinstance( self.combine_documents_chain, StuffDocumentsChain ): tokens = [ self.combine_documents_chain.llm_chain.llm.get_num_tokens( doc.page_content ) for doc in docs ] token_count = sum(tokens[:num_docs]) # 减少文档数量直到满足token限制 while token_count > self.max_tokens_limit: num_docs -= 1 token_count -= tokens[num_docs] return docs[:num_docs]
たとえば、SQL データ ソース: sql_databaseの場合は、次のコード ファイル: chains/sql_database/query.pyに注目できます。# 获取相关文档的函数 def _get_docs( self, inputs: Dict[str, Any], *, run_manager: CallbackManagerForChainRun ) -> List[Document]: question = inputs[self.question_key] # 从向量存储中搜索相似的文档 docs = self.vectorstore.similarity_search( question, k=self.k, **self.search_kwargs ) return self._reduce_tokens_below_limit(docs)
たとえば、モデル ダイアログ: chat_modelsには、次のコード ファイルが含まれます: __init__.py、anthropopic.py、azure_openai.py 、base .py、fake.py、google_palm.py、human.py、jinachat.py、openai.py、promptlayer_openai.py、vertexai.py
さらに、さらに目を引くものがあります:
constitutional_ai : 最終結果に対するバイアス、コンプライアンス問題を処理するロジックにより、最終結果が値
llm_checkerに準拠していることが保証されます。LLM は出力に問題のないロジックがあるかどうかを自動的に検出できます。 - メモリ:
簡単に言うと、モデルと対話するときにコンテキストの状態を保存し、長期記憶を処理するために使用されます。具体的には、
この層には主に 2 つのコア ポイントがあります:
チェーンの実行中に入力と出力を記憶し、構造的に保存します。次のインタラクションのコンテキストを提供します。この部分を Redis に保存するだけで、
インタラクション履歴に基づいてナレッジ グラフを構築し、関連情報に基づいて正確な結果を提供できます。対応するコード ファイルは次のとおりです: memory/kg.py# 定义知识图谱对话记忆类 class ConversationKGMemory(BaseChatMemory): """知识图谱对话记忆类 在对话中与外部知识图谱集成,存储和检索对话中的知识三元组信息。 """ k: int = 2 # 考虑的上下文对话数量 human_prefix: str = "Human" # 人类前缀 ai_prefix: str = "AI" # AI前缀 kg: NetworkxEntityGraph = Field(default_factory=NetworkxEntityGraph) # 知识图谱实例 knowledge_extraction_prompt: BasePromptTemplate = KNOWLEDGE_TRIPLE_EXTRACTION_PROMPT # 知识提取提示 entity_extraction_prompt: BasePromptTemplate = ENTITY_EXTRACTION_PROMPT # 实体提取提示 llm: BaseLanguageModel # 基础语言模型 summary_message_cls: Type[BaseMessage] = SystemMessage # 总结消息类 memory_key: str = "history" # 历史记忆键 def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]: """返回历史缓冲区。""" entities = self._get_current_entities(inputs) # 获取当前实体 summary_strings = [] for entity in entities: # 对于每个实体 knowledge = self.kg.get_entity_knowledge(entity) # 获取与实体相关的知识 if knowledge: summary = f"On {entity}: {'. '.join(knowledge)}." # 构建总结字符串 summary_strings.append(summary) context: Union[str, List] if not summary_strings: context = [] if self.return_messages else "" elif self.return_messages: context = [ self.summary_message_cls(content=text) for text in summary_strings ] else: context = "\n".join(summary_strings) return {self.memory_key: context} @property def memory_variables(self) -> List[str]: """始终返回记忆变量列表。""" return [self.memory_key] def _get_prompt_input_key(self, inputs: Dict[str, Any]) -> str: """获取提示的输入键。""" if self.input_key is None: return get_prompt_input_key(inputs, self.memory_variables) return self.input_key def _get_prompt_output_key(self, outputs: Dict[str, Any]) -> str: """获取提示的输出键。""" if self.output_key is None: if len(outputs) != 1: raise ValueError(f"One output key expected, got {outputs.keys()}") return list(outputs.keys())[0] return self.output_key def get_current_entities(self, input_string: str) -> List[str]: """从输入字符串中获取当前实体。""" chain = LLMChain(llm=self.llm, prompt=self.entity_extraction_prompt) buffer_string = get_buffer_string( self.chat_memory.messages[-self.k * 2 :], human_prefix=self.human_prefix, ai_prefix=self.ai_prefix, ) output = chain.predict( history=buffer_string, input=input_string, ) return get_entities(output) def _get_current_entities(self, inputs: Dict[str, Any]) -> List[str]: """获取对话中的当前实体。""" prompt_input_key = self._get_prompt_input_key(inputs) return self.get_current_entities(inputs[prompt_input_key]) def get_knowledge_triplets(self, input_string: str) -> List[KnowledgeTriple]: """从输入字符串中获取知识三元组。""" chain = LLMChain(llm=self.llm, prompt=self.knowledge_extraction_prompt) buffer_string = get_buffer_string( self.chat_memory.messages[-self.k * 2 :], human_prefix=self.human_prefix, ai_prefix=self.ai_prefix, ) output = chain.predict( history=buffer_string, input=input_string, verbose=True, ) knowledge = parse_triples(output) # 解析三元组 return knowledge def _get_and_update_kg(self, inputs: Dict[str, Any]) -> None: """从对话历史中获取并更新知识图谱。""" prompt_input_key = self._get_prompt_input_key(inputs) knowledge = self.get_knowledge_triplets(inputs[prompt_input_key]) for triple in knowledge: self.kg.add_triple(triple) # 向知识图谱中添加三元组 def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None: """将此对话的上下文保存到缓冲区。""" super().save_context(inputs, outputs) self._get_and_update_kg(inputs) def clear(self) -> None: """清除记忆内容。""" super().clear() self.kg.clear() # 清除知识图谱内容
- ツール層、ツール。
実際、Chains 層は LLM + プロンプトに基づいて特定のロジックを実行できます。ただし、Chain を使用してすべてのロジックを実装することが非現実的である場合は、Tools 層を通じて実現することもできます。Tools 層は検索、Wikipedia、天気予報、ChatGPTサービスなど、スキルとして理解するのがより合理的です。
1.1.3 アプリケーション層:エージェント
- エージェント:
つまり、基本層と機能層によって、さまざまな楽しく価値のあるサービスを構築することができます。具体的にはエージェントです。エージェントは、
LLM にリクエストを送信し、アクションを実行するエージェントとして機能します。そして、結果が表示されるまで確認します。 LLM が処理できないタスク (検索や計算など、ChatGPT のようなプラグインに加え、bing や計算機を呼び出す機能があります)
。たとえば、エージェントは Wikipedia を使用して Barack の誕生日を見つけることができます。オバマ大統領、その後、計算機を使って 2023 年の彼の年齢を調べてください
さらに、Wikipedia の場合は、次のコード ファイルに注目してください: langchain/docstore/wikipedia.py ...# pip install wikipedia from langchain.agents import load_tools from langchain.agents import initialize_agent from langchain.agents import AgentType tools = load_tools(["wikipedia", "llm-math"], llm=llm) agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True) agent.run("奥巴马的生日是哪天? 到2023年他多少岁了?")
langchain の最終的な全体的な技術アーキテクチャを以下の図に示します (高解像度の大きな画像を表示します。さらに、別のアーキテクチャ図もここにあります)。
1.2 langchain の応用例: インターネット検索 + ドキュメント Q&A
ただし、理論的な説明だけを読んでも、langchain が何に使用されるのか理解できないかもしれません。理解を容易にするために、ここでは langchain の応用例をいくつか示します。
1.2.1 Google を検索して回答を返す
Serpapi の助けを借りて実装する必要があり、Serpapi は Google 検索の API インターフェースを提供するためです。
そのため、まず Serpapi 公式 Web サイト (https://serpapi.com/) にアクセスしてユーザーを登録し、それをコピーして API キーを生成し、環境変数に設定します。
import os
os.environ["OPENAI_API_KEY"] = '你的api key'
os.environ["SERPAPI_API_KEY"] = '你的api key'
次に、コードを書き始めます
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.llms import OpenAI
from langchain.agents import AgentType
# 加载 OpenAI 模型
llm = OpenAI(temperature=0,max_tokens=2048)
# 加载 serpapi 工具
tools = load_tools(["serpapi"])
# 如果搜索完想再计算一下可以这么写
# tools = load_tools(['serpapi', 'llm-math'], llm=llm)
# 如果搜索完想再让他再用python的print做点简单的计算,可以这样写
# tools=load_tools(["serpapi","python_repl"])
# 工具加载后都需要初始化,verbose 参数为 True,会打印全部的执行详情
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
# 运行 agent
agent.run("What's the date today? What great events have taken place today in history?")
1.2.2 50 行未満のコードで文書会話ロボットを実装する
ご存知のとおり、ChatGPT でトレーニングされたデータは 2021 年までしか更新されていないため、(検索関数 bing を呼び出さない限り) インターネットの最新の知識を知りません。また、「LangChain + ChatGPT の API」を使用すると、より少ない労力で実装できます。 50 行を超えるコード 既存のドキュメントを使用した会話型ボット
2022 年に更新されたすべてのコンテンツが2022.txtドキュメントに存在すると仮定すると、次のコードを通じて、ChatGPT は 2022 年の質問への回答をサポートできます。
原理も非常にシンプルです。
- ユーザー入力/プロンプトをベクトル化する
- ドキュメントのセグメント化
- 文書の分割
- テキストのベクトル化
ベクトル間の類似性を計算できるのは、ベクトル化後にのみです。 - ベクトル化されたテキストはベクトル データベースに保存されます
- ユーザーの入力/プロンプトに従って、ベクトル データ内の回答を検索します (回答は、プロンプト/入力とテキスト内の関連する段落ベクトル間の類似性の一致に基づいて決定されます)
- 最後に、LLM を通じて答えが返されます。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import os # 导入os模块,用于操作系统相关的操作
import jieba as jb # 导入结巴分词库
from langchain.chains import ConversationalRetrievalChain # 导入用于创建对话检索链的类
from langchain.chat_models import ChatOpenAI # 导入用于创建ChatOpenAI对象的类
from langchain.document_loaders import DirectoryLoader # 导入用于加载文件的类
from langchain.embeddings import OpenAIEmbeddings # 导入用于创建词向量嵌入的类
from langchain.text_splitter import TokenTextSplitter # 导入用于分割文档的类
from langchain.vectorstores import Chroma # 导入用于创建向量数据库的类
# 初始化函数,用于处理输入的文档
def init():
files = ['2022.txt'] # 需要处理的文件列表
for file in files: # 遍历每个文件
with open(f"./data/{file}", 'r', encoding='utf-8') as f: # 以读模式打开文件
data = f.read() # 读取文件内容
cut_data = " ".join([w for w in list(jb.cut(data))]) # 对读取的文件内容进行分词处理
cut_file = f"./data/cut/cut_{file}" # 定义处理后的文件路径和名称
with open(cut_file, 'w') as f: # 以写模式打开文件
f.write(cut_data) # 将处理后的内容写入文件
# 新建一个函数用于加载文档
def load_documents(directory):
# 创建DirectoryLoader对象,用于加载指定文件夹内的所有.txt文件
loader = DirectoryLoader(directory, glob='**/*.txt')
docs = loader.load() # 加载文件
return docs # 返回加载的文档
# 新建一个函数用于分割文档
def split_documents(docs):
# 创建TokenTextSplitter对象,用于分割文档
text_splitter = TokenTextSplitter(chunk_size=1000, chunk_overlap=0)
docs_texts = text_splitter.split_documents(docs) # 分割加载的文本
return docs_texts # 返回分割后的文本
# 新建一个函数用于创建词嵌入
def create_embeddings(api_key):
# 创建OpenAIEmbeddings对象,用于获取OpenAI的词向量
embeddings = OpenAIEmbeddings(openai_api_key=api_key)
return embeddings # 返回创建的词嵌入
# 新建一个函数用于创建向量数据库
def create_chroma(docs_texts, embeddings, persist_directory):
# 使用文档,embeddings和持久化目录创建Chroma对象
vectordb = Chroma.from_documents(docs_texts, embeddings, persist_directory=persist_directory)
vectordb.persist() # 持久化存储向量数据
return vectordb # 返回创建的向量数据库
# load函数,调用上面定义的具有各个职责的函数
def load():
docs = load_documents('./data/cut') # 调用load_documents函数加载文档
docs_texts = split_documents(docs) # 调用split_documents函数分割文档
api_key = os.environ.get('OPENAI_API_KEY') # 从环境变量中获取OpenAI的API密钥
embeddings = create_embeddings(api_key) # 调用create_embeddings函数创建词嵌入
# 调用create_chroma函数创建向量数据库
vectordb = create_chroma(docs_texts, embeddings, './data/cut/')
# 创建ChatOpenAI对象,用于进行聊天对话
openai_ojb = ChatOpenAI(temperature=0, model_name="gpt-3.5-turbo")
# 从模型和向量检索器创建ConversationalRetrievalChain对象
chain = ConversationalRetrievalChain.from_llm(openai_ojb, vectordb.as_retriever())
return chain # 返回该对象
# 调用load函数,获取ConversationalRetrievalChain对象
chain = load()
# 定义一个函数,根据输入的问题获取答案
def get_ans(question):
chat_history = [] # 初始化聊天历史为空列表
result = chain({ # 调用chain对象获取聊天结果
'chat_history': chat_history, # 传入聊天历史
'question': question, # 传入问题
})
return result['answer'] # 返回获取的答案
if __name__ == '__main__': # 如果此脚本作为主程序运行
s = input('please input:') # 获取用户输入
while s != 'exit': # 如果用户输入的不是'exit'
ans = get_ans(s) # 调用get_ans函数获取答案
print(ans) # 打印答案
s = input('please input:') # 获取用户输入
// 更新予定
パート 2: LangChain + ChatGLM-6B に基づくローカル ナレッジ ベース Q&A (7 月 23 日の初版)
2.1 主要な手順: LangChain+LLM を介してローカル ナレッジ ベース Q&A を実装する方法
2023 年 7 月に、GitHub 上のlangchain のアイデアを 使用して実装されたローカル ナレッジ ベースに基づく質問と回答のアプリケーションがありました : langchain-ChatGLM (これはGitHub アドレスです。もちろん、現在 Vicuna-13b をサポートしている同様のプロジェクトがあります。LangChain- ChatGLM-Webui ) の目標は、中国語のシナリオやオープンソース モデルに適しており、オフラインで実行できるナレッジ ベースの質問と回答のソリューションを構築することです。
- このプロジェクトは、 GanymedeNil のプロジェクト document.aiと Alex Zhangji によって作成された ChatGLM-6B プル リクエストに触発さ れ、プロセス全体を通じてオープン ソース モデルを使用して実装できるローカル ナレッジ ベース Q&A アプリケーションを確立しました。ChatGLM-6B、 ClueAI/ChatYuan-large-v2 などの大規模言語モデルへのアクセスをサポートするようになりました。
- このプロジェクトでは、デフォルトの埋め込み選択は GanymedeNil/text2vec-large-chineseで、デフォルトの LLM 選択は ChatGLM-6Bです。上記のモデルに依存して、このプロジェクトはすべてのオープン ソースモデルを使用してオフライン プライベート デプロイメントを実現できます。
このプロジェクトの実装原理は次の図に示されています (ドキュメントベースの質疑応答と同様に、プロセスには次のものが含まれます: 1 ドキュメントの読み込み -> 2 ドキュメントの読み取り -> 3/4 ドキュメントの分割 -> 5/6 テキストのベクトル化 -> 8/9 質問のベクトル化 -> 10 ドキュメント ベクトル内の質問ベクトルに最も類似した上位 k を照合 -> 11/12/13 一致したテキストがコンテキストおよび質問としてプロンプトに追加される -> 14/15 LLM 生成の回答に送信 )
-
第 1 段階: ファイルのロード - ファイルの読み取り -テキスト スプリッター
ファイルのロード: ローカルに保存されているナレッジ ベース ファイルを読み取るステップです
ファイルの読み取り: ロードされたファイルの内容を読み取り、通常はテキスト形式の
テキスト スプリッター (テキスト スプリッター) ) : 特定のルールに従ってテキストを分割します (段落、文、単語など)。以下はサンプル コードです (langchain-ChatGLM プロジェクトのソース コードではありません)。def _load_file(self, filename): # 判断文件类型 if filename.lower().endswith(".pdf"): # 如果文件是 PDF 格式 loader = UnstructuredFileLoader(filename) # 使用 UnstructuredFileLoader 加载器来加载 PDF 文件 text_splitor = CharacterTextSplitter() # 使用 CharacterTextSplitter 来分割文件中的文本 docs = loader.load_and_split(text_splitor) # 加载文件并进行文本分割 else: # 如果文件不是 PDF 格式 loader = UnstructuredFileLoader(filename, mode="elements") # 使用 UnstructuredFileLoader 加载器以元素模式加载文件 text_splitor = CharacterTextSplitter() # 使用 CharacterTextSplitter 来分割文件中的文本 docs = loader.load_and_split(text_splitor) # 加载文件并进行文本分割 return docs # 返回处理后的文件数据
-
第 2 段階: テキストのベクトル化 (埋め込み) - ベクトル データベースに保存
テキストのベクトル化 (埋め込み) : これには通常、NLP 特徴抽出が含まれます。セグメント化されたテキストは数値ベクトルに変換できます。# 初始化方法,接受一个可选的模型名称参数,默认值为 None def __init__(self, model_name=None) -> None: if not model_name: # 如果没有提供模型名称 # 使用默认的嵌入模型 # 创建一个 HuggingFaceEmbeddings 对象,模型名称为类的 model_name 属性 self.embeddings = HuggingFaceEmbeddings(model_name=self.model_name)
ベクトルデータベースに保存: テキストをベクトル化した後、データベースのベクトルストアに保存します ( FAISS、FAISS については次のセクションで詳しく説明します)
def init_vector_store(self): persist_dir = os.path.join(VECTORE_PATH, ".vectordb") # 持久化向量数据库的地址 print("向量数据库持久化地址: ", persist_dir) # 打印持久化地址 # 如果持久化地址存在 if os.path.exists(persist_dir): # 从本地持久化文件中加载 print("从本地向量加载数据...") # 使用 Chroma 加载持久化的向量数据 vector_store = Chroma(persist_directory=persist_dir, embedding_function=self.embeddings) # 如果持久化地址不存在 else: # 加载知识库 documents = self.load_knownlege() # 使用 Chroma 从文档中创建向量存储 vector_store = Chroma.from_documents(documents=documents, embedding=self.embeddings, persist_directory=persist_dir) vector_store.persist() # 持久化向量存储 return vector_store # 返回向量存储
load_knownlege の実装は次のとおりです。
def load_knownlege(self): docments = [] # 初始化一个空列表来存储文档 # 遍历 DATASETS_DIR 目录下的所有文件 for root, _, files in os.walk(DATASETS_DIR, topdown=False): for file in files: filename = os.path.join(root, file) # 获取文件的完整路径 docs = self._load_file(filename) # 加载文件中的文档 # 更新 metadata 数据 new_docs = [] # 初始化一个空列表来存储新文档 for doc in docs: # 更新文档的 metadata,将 "source" 字段的值替换为不包含 DATASETS_DIR 的相对路径 doc.metadata = {"source": doc.metadata["source"].replace(DATASETS_DIR, "")} print("文档2向量初始化中, 请稍等...", doc.metadata) # 打印正在初始化的文档的 metadata new_docs.append(doc) # 将文档添加到新文档列表 docments += new_docs # 将新文档列表添加到总文档列表 return docments # 返回所有文档的列表
-
第 3 段階: 質問のベクトル化
ユーザーのクエリや質問をベクトルに変換するもので、テキストのベクトル化と同じ方法を使用して、同じ空間内で比較できるようにする必要があります。 -
第 4 段階: 質問ベクトルに最も類似する上位 k 個のテキスト ベクトルを照合します。
このステップは情報検索の核心です。コサイン類似度、ユークリッド距離などを計算することにより、質問ベクトルに最も近いテキストが見つかります。def query(self, q): """在向量数据库中查找与问句向量相似的文本向量""" vector_store = self.init_vector_store() docs = vector_store.similarity_search_with_score(q, k=self.top_k) for doc in docs: dc, s = doc yield s, dc
-
第 5 段階: 一致したテキストがコンテキストおよび質問としてプロンプトに追加されます。
これは、一致したテキストを使用して、言語モデルへの入力用の質問に関連するコンテキストを形成します。 -
ステージ 6: LLM に送信して回答を生成します。最後に、質問とコンテキストを言語モデル (GPT シリーズなど) に送信し、ナレッジ クエリ (コード ソース)
などの回答を生成させます。class KnownLedgeBaseQA: # 初始化 def __init__(self) -> None: k2v = KnownLedge2Vector() # 创建一个知识到向量的转换器 self.vector_store = k2v.init_vector_store() # 初始化向量存储 self.llm = VicunaLLM() # 创建一个 VicunaLLM 对象 # 获得与查询相似的答案 def get_similar_answer(self, query): # 创建一个提示模板 prompt = PromptTemplate( template=conv_qa_prompt_template, input_variables=["context", "question"] # 输入变量包括 "context"(上下文) 和 "question"(问题) ) # 使用向量存储来检索文档 retriever = self.vector_store.as_retriever(search_kwargs={"k": VECTOR_SEARCH_TOP_K}) docs = retriever.get_relevant_documents(query=query) # 获取与查询相关的文本 context = [d.page_content for d in docs] # 从文本中提取出内容 result = prompt.format(context="\n".join(context), question=query) # 格式化模板,并用从文本中提取出的内容和问题填充 return result # 返回结果
ご覧のとおり、langchain + LLM を組み合わせるこの方法は、LLM のインテリジェントな対話機能を使用して社内のプライベート Q&A システムを構築するために、一部の垂直分野や大規模なグループ会社に特に適しています。また、個人が一部の英語論文について特に Q&A を行うのにも適しています。たとえば、人気のあるオープンソース プロジェクト: ChatPDF は、ドキュメント処理の観点から見ると、実装プロセスは次のとおりです (画像ソース)。
2.2 Facebook AI 類似性検索 (FAISS): 効率的なベクトル類似性検索
Faiss の正式名は Facebook AI 類似性検索 (公式紹介ページ、GitHub アドレス). 大規模な類似性検索問題に対処するために FaceBook の AI チームによって開発されたツールです. C++ で書かれており、Python インターフェイスを備えています。 10億レベルのインデックスを処理し、ミリ秒レベルの検索パフォーマンスを実現
簡単に言えば、Faiss の仕事は、独自の候補ベクトル セットをインデックス データベースにカプセル化し、TopK の類似ベクトルを取得するプロセスを高速化することです。インデックスの一部は GPU 構築もサポートしています。
2.2.1 類似ベクトルTopKのFaiss検索の基本処理
類似ベクトル TopK の Faiss 検索プロジェクトは、基本的に 3 つのステップに分けることができます。
- ベクターライブラリを取得する
import numpy as np d = 64 # 向量维度 nb = 100000 # index向量库的数据量 nq = 10000 # 待检索query的数目 np.random.seed(1234) xb = np.random.random((nb, d)).astype('float32') xb[:, 0] += np.arange(nb) / 1000. # index向量库的向量 xq = np.random.random((nq, d)).astype('float32') xq[:, 0] += np.arange(nq) / 1000. # 待检索的query向量
- faiss を使用してインデックスを構築し、ベクトルをインデックスに追加します。インデックスの
構築には、総当たり検索メソッド FlatL2 が使用されます。L2 は、構築されたインデックスで使用される類似性測定方法を表します。L2 ノルム、つまりユークリッド距離です。import faiss index = faiss.IndexFlatL2(d) print(index.is_trained) # 输出为True,代表该类index不需要训练,只需要add向量进去即可 index.add(xb) # 将向量库中的向量加入到index中 print(index.ntotal) # 输出index中包含的向量总数,为100000
- faiss インデックスを使用して、TopK の類似クエリを検索して取得します。
k = 4 # topK的K值 D, I = index.search(xq, k)# xq为待检索向量,返回的I为每个待检索query最相似TopK的索引list,D为其对应的距离 print(I[:5]) print(D[-5:])
印刷結果は次のとおりです:
>>>
[[ 0 393 363 78]
[ 1 555 277 364]
[ 2 304 101 13]
[ 3 173 18 182]
[ 4 288 370 531]]
[[ 0. 7.17517328 7.2076292 7.2511625 3]
[0 .6.32356453 6.6845808 6.79994535]
[ 0. 5.79640865 6.39173603 7.28151226]
[ 0. 7.27790546 7.52798653 7.66284657]
[ 0. 6.7638 03 48 7.29512024 7.36881447]]
2.2.2 FAISS のさまざまなインデックス構築方法
インデックスメソッドとパラメータ渡しメソッドは次のように構築できます。
dim, measure = 64, faiss.METRIC_L2
param = 'Flat'
index = faiss.index_factory(dim, param, measure)
- dim はベクトル次元です
- 最も重要なことは param パラメータです。これはインデックスに渡されるパラメータであり、構築する必要があるインデックスのタイプを表します。
- Measureは測定方法で、現在はユークリッド距離と内積、つまり内積の2種類に対応しています。したがって、コサイン類似度を計算するには、vecs を正規化し、内積メトリックを使用するだけで済みます。
この記事によると、faiss は現在、次の 8 つの測定方法を正式にサポートしています。
- METRIC_INNER_PRODUCT(内积)
- METRIC_L1 (マンハッタン距離)
- METRIC_L2 (ユークリッド距離)
- METRIC_Linf (無限ノルム)
- METRIC_Lp (p ノルム)
- METRIC_BrayCurtis (BC 非類似度)
- METRIC_Canberra (ランダム距離/キャンベラ距離)
- METRIC_JensenShannon (JS ダイバージェンス)
2.2.2.1フラット: 暴力的な回収
- 利点: この方法は最も正確で、すべての Faiss インデックスの中で最も高い再現率を持っています。
- 欠点: 速度が遅い、メモリ使用量が多い。
- 使用法: ベクトル候補セットは 500,000 以内と非常に小さく、メモリが不足していません。
- 注: これらはすべて暴力的な検索ですが、faiss の暴力的な検索速度は一般のプログラマが作成した暴力的な検索よりもはるかに速いため、役に立たないというわけではありません。暴力的な検索ニーズがある学生には引き続き faiss を使用することをお勧めします。
- ビルド方法:
dim, measure = 64, faiss.METRIC_L2
param = 'Flat'
index = faiss.index_factory(dim, param, measure)
index.is_trained # 输出为True
index.add(xb) # 向index中添加向量
2.2.2. 2 IVFx Flat: 逆暴力検索
- 利点: IVF は主に反転のアイデアを使用します。文書検索シナリオにおける反転技術は、kw の後にその単語を含む多くの文書が続くことを意味します。kw の数は文書よりもはるかに小さいため、検索時間を短縮します。ベクトルで反転を使用するにはどうすればよいですか? 各クラスターの中心の下にあるベクトル ID を取り出し、各中心 ID の後ろに多数の非中心ベクトルをぶら下げることができます。ベクトルをクエリするたびに、最も近い中心 ID を見つけて、これらの中心の下にある非中心をそれぞれ検索します。ベクター。検索範囲を減らすことで検索効率が向上します。
- デメリット:速度はあまり速くありません。
- 使用方法: Flat に比べて検索速度が大幅に向上するため、数百万のベクトルを使用することをお勧めします。
- パラメータ: IVFx の x は、K-means クラスタリング中心の数です。
- ビルド方法:
dim, measure = 64, faiss.METRIC_L2
param = 'IVF100,Flat' # 代表k-means聚类中心为100,
index = faiss.index_factory(dim, param, measure)
print(index.is_trained) # 此时输出为False,因为倒排索引需要训练k-means,
index.train(xb) # 因此需要先训练index,再add向量
index.add(xb)
2.2.2.3 PQx: 積の量子化
- 利点: 通常の検索を改善するために積定量化の方法が使用され、ベクトルの次元が x 個のセグメントに分割され、各セグメントが個別に検索され、最終的な TopK はベクトルの各セグメントの検索結果を交差することによって取得されます。したがって、非常に高速で、メモリ使用量が少なく、再現率が比較的高くなります。
- 短所: 暴力的検索と比較して、再現率がさらに低下します。
- 用途: メモリが非常に不足しているため、高速な検索速度が必要ですが、再現率はそれほど重要ではありません。
- パラメータ: PQx の x は、ベクトルを分割するセグメントの数です。したがって、x はベクトルの次元で割り切れる必要があります。x が大きいほど、セグメンテーションが詳細になり、時間の計算量が高くなります。
- ビルド方法:
dim, measure = 64, faiss.METRIC_L2
param = 'PQ16'
index = faiss.index_factory(dim, param, measure)
print(index.is_trained) # 此时输出为False,因为倒排索引需要训练k-means,
index.train(xb) # 因此需要先训练index,再add向量
index.add(xb)
2.2.2.4 IVFxPQy 逆積量子化
- 利点: この方法は業界で広く使用されており、すべての指標が許容されます。積定量化方法は、IVF の k 平均法を改善するために使用されます。ベクトルの次元は x セグメントに分割され、k 平均値は個別に取得されます。各セグメントごとに。
- 短所: 数百の学派の長所を組み合わせたものですが、数百の学派の欠点も併せ持っています。
- 使用法: 一般に、あらゆる面で特別な極端な要件がない場合は、この方法が最も推奨されます。
- パラメータ: IVFx、PQy、x と y は上記と同じです。
- ビルド方法:
dim, measure = 64, faiss.METRIC_L2
param = 'IVF100,PQ16'
index = faiss.index_factory(dim, param, measure)
print(index.is_trained) # 此时输出为False,因为倒排索引需要训练k-means,
index.train(xb) # 因此需要先训练index,再add向量 index.add(xb)
2.2.2. 5 LSH 局所性依存ハッシュ
- 原則: ハッシュは誰にとっても馴染みのあるものです。ベクトルでも検索を高速化するためにハッシュを使用できます。ここで話しているハッシュは、従来のハッシュとは異なり、衝突を回避しようとする局所性敏感ハッシュ (LSH) を指します。ハッシュは衝突に依存して近傍を見つけます。高次元空間内の 2 つの点が非常に近い場合、その 2 つの点をハッシュしてバケットに分割するハッシュ関数を設計して、それらのハッシュ バケットの値が同じになる可能性が高くなります。ポイント間の距離が大きい場合、それらのハッシュ バケット値が同じである確率は非常に小さくなります。
- 利点: トレーニングは非常に高速で、バッチ インポートをサポートし、インデックスに使用されるメモリはほとんどなく、取得も高速です。
- デメリット:再現率が非常に低い。
- 使用法: 候補ベクトル ライブラリは非常に大きく、オフラインで取得できるため、メモリ リソースが不足しています。
- ビルド方法:
dim, measure = 64, faiss.METRIC_L2
param = 'LSH'
index = faiss.index_factory(dim, param, measure)
print(index.is_trained) # 此时输出为True
index.add(xb)
2.2.2 .6 HNSWx
- 長所: この手法はグラフ検索をベースに改良した手法であり、検索速度が非常に速く、10億秒で検索結果が得られ、再現率はFlatとほぼ同等で、なんと97%に達します。検索の時間計算量は loglogn であり、候補ベクトルの大きさをほとんど無視できます。また、バッチ インポートもサポートしています。これはオンライン タスクに非常に適しており、ミリ秒レベルのエクスペリエンスを提供します。
- 欠点: インデックスの構築が非常に遅く、多くのメモリを消費します (Faiss では最大で、元のベクトルが占有するメモリ サイズよりも大きくなります)。
- パラメータ: HNSWx の x は、グラフを構築するときに各ポイントに接続されるノードの最大数です。x が大きいほど、構成はより複雑になり、クエリの精度が高くなります。そしてもちろん、インデックスの構築時間は遅くなります。 x は 4 ~ 64 の任意の整数です。
- 使用法: メモリを気にせず、インデックスを構築するのに十分な時間があります。
- ビルド方法:
dim, measure = 64, faiss.METRIC_L2
param = 'HNSW64'
index = faiss.index_factory(dim, param, measure)
print(index.is_trained) # 此时输出为True
index.add(xb)
2.3 プロジェクトの展開: Q&A 用のローカルナレッジベースを構築するための langchain + ChatGLM-6B
2.3.1 導入プロセス 1: 複数の使用モードをサポートする
LLM モデルは、実際のビジネス ニーズに応じて選択できます。このプロジェクトで使用されている ChatGLM-6B、その GitHub アドレスは: https://github.com/THUDM/ChatGLM-6B
ChatGLM-6B はオープン ソースであり、中国語と中国語をサポートしています。英語のバイリンガル会話言語モデル。一般言語モデル (GLM) アーキテクチャに基づいており、62 億のパラメーターがあります。モデル量子化テクノロジーと組み合わせることで、ユーザーはそれを民生グレードのグラフィックス カードにローカルに展開できます (INT4 量子化レベルでは最低 6GB のビデオ メモリが必要です)。
ChatGLM-6B は ChatGPT と同様のテクノロジーを使用しており、中国語の質疑応答と対話に最適化されています。約 1T の識別子を使用した中国語と英語のバイリンガル トレーニングを経て、教師付き微調整、フィードバック セルフサービス、ヒューマン フィードバック強化学習、その他のテクノロジーによって補完された後、62 億パラメータの ChatGLM-6B は、次の内容と完全に一致する回答を生成することができました。人間の好み。
- 新しい python3.8.13 環境を作成します (モデル ファイルは引き続き使用できます)
conda create -n langchain python==3.8.13
- アイテムをプルする
git clone https://github.com/imClumsyPanda/langchain-ChatGLM.git
- ディレクトリを入力してください
cd langchain-ChatGLM
- インストール要件.txt
conda activate langchain pip install -r requirements.txt
- 現在の環境でサポートされている langchain の最新バージョンは 0.0.166 です。0.0.174 をインストールできない場合は、まず 0.0.166 をインストールしてください。
構成ファイルのパスを変更します。vi configs/model_config.py
- chatglm-6b のパスを独自のパスに設定します
“chatglm-6b”: { “name”: “chatglm-6b”, “pretrained_model_name”: “/data/sim_chatgpt/chatglm-6b”, “local_model_path”: None, “provides”: “ChatGLM”
- 実行するコード ファイルを変更します: webui.py
vi webui.py
- 前回の起動関数の share を True に設定し、inbrowser を True に設定します
- webui.pyファイルを実行する
ネットワークに問題があり、パブリック リンクを作成できない可能性があります。クラウドサーバーとローカルポートをマッピングできます。参考: https://www.cnblogs.com/monologuesmw/p/14465117.htmlpython webui.py
対応する出力:
占有ビデオメモリ:約15G
2.3.2 導入プロセス 2: 複数のコミュニティでのオンライン エクスペリエンスのサポート
プロジェクトのアドレス: https://github.com/thomas-yanxin/LangChain-ChatGLM-Webui
HUggingFace コミュニティのオンライン体験: https://huggingface.co/spaces/thomas-yanxin/LangChain-ChatLLM
さらに、ModelScope Magic Community や Flying Paddle AIStudio Community などのオンライン体験もサポートしています。
- プロジェクトをダウンロード
git clone https://github.com/thomas-yanxin/LangChain-ChatGLM-Webui.git
- ディレクトリを入力してください
cd LangChain-ChatGLM-Webui
- 必要なパッケージをインストールする
pip install -r requirements.txt pip install gradio==3.10
- config.pyを変更する
init_llm = "ChatGLM-6B" llm_model_dict = { "chatglm": { "ChatGLM-6B": "/data/sim_chatgpt/chatglm-6b",
- app.py ファイルを変更し、launch 関数の share を True に設定し、inbrowser を True に設定して
webui.py ファイルを実行します。python webui.py
ビデオメモリは約13Gを占有します
3 番目の部分は、行ごとの詳細な分析です: langchain-ChatGLM (初版は 7 月 23 日) プロジェクトのソース コード解釈
langchain-ChatGLM プロジェクトのアーキテクチャ図をもう一度確認してみましょう (画像ソース)
プロジェクトは主に次のモジュールで構成されていることがわかります。
- chains:chains/local_doc_qa などの作業リンク実装は、ローカル ドキュメントに基づいて Q&A を実装します。
- configs: 設定ファイルのストレージ
- Knowledge_base/content: アップロードされた元のファイルの保存に使用されます
- loader: ドキュメントローダーの実装クラス
- モデル: llm のインターフェイス クラスと実装クラス。オープン ソース モデルのストリーミング出力サポートを提供します。
- textsplitter: テキストセグメンテーションの実装クラス
- Vectorstores: ベクトル ライブラリ ファイル、つまりローカル ナレッジ ベース オントロジーを保存するために使用されます。
- ..
次に、読者が一目で理解しやすいように、
- 基本的に「以下のプロジェクトのコードのすべての行」に中国語のコメントを追加しました。
- また、理解をスムーズにするために、各コード フォルダーを解釈する順序をプロジェクト プロセスに従って 1 つずつ展開します (上の図の GitHub 上で各コード フォルダーが表示されている順序ではありません)。
ご質問がございましたら、いつでもコメントを残していただけます
3.1 エージェント:custom_agent/bing_search
3.1.1エージェント/custom_agent.py
from langchain.agents import Tool # 导入工具模块
from langchain.tools import BaseTool # 导入基础工具类
from langchain import PromptTemplate, LLMChain # 导入提示模板和语言模型链
from agent.custom_search import DeepSearch # 导入自定义搜索模块
# 导入基础单动作代理,输出解析器,语言模型单动作代理和代理执行器
from langchain.agents import BaseSingleActionAgent, AgentOutputParser, LLMSingleActionAgent, AgentExecutor
from typing import List, Tuple, Any, Union, Optional, Type # 导入类型注释模块
from langchain.schema import AgentAction, AgentFinish # 导入代理动作和代理完成模式
from langchain.prompts import StringPromptTemplate # 导入字符串提示模板
from langchain.callbacks.manager import CallbackManagerForToolRun # 导入工具运行回调管理器
from langchain.base_language import BaseLanguageModel # 导入基础语言模型
import re # 导入正则表达式模块
# 定义一个代理模板字符串
agent_template = """
你现在是一个{role}。这里是一些已知信息:
{related_content}
{background_infomation}
{question_guide}:{input}
{answer_format}
"""
# 定义一个自定义提示模板类,继承自字符串提示模板
class CustomPromptTemplate(StringPromptTemplate):
template: str # 提示模板字符串
tools: List[Tool] # 工具列表
# 定义一个格式化函数,根据提供的参数生成最终的提示模板
def format(self, **kwargs) -> str:
intermediate_steps = kwargs.pop("intermediate_steps")
# 判断是否有互联网查询信息
if len(intermediate_steps) == 0:
# 如果没有,则给出默认的背景信息,角色,问题指导和回答格式
background_infomation = "\n"
role = "傻瓜机器人"
question_guide = "我现在有一个问题"
answer_format = "如果你知道答案,请直接给出你的回答!如果你不知道答案,请你只回答\"DeepSearch('搜索词')\",并将'搜索词'替换为你认为需要搜索的关键词,除此之外不要回答其他任何内容。\n\n下面请回答我上面提出的问题!"
else:
# 否则,根据 intermediate_steps 中的 AgentAction 拼装 background_infomation
background_infomation = "\n\n你还有这些已知信息作为参考:\n\n"
action, observation = intermediate_steps[0]
background_infomation += f"{observation}\n"
role = "聪明的 AI 助手"
question_guide = "请根据这些已知信息回答我的问题"
answer_format = ""
kwargs["background_infomation"] = background_infomation
kwargs["role"] = role
kwargs["question_guide"] = question_guide
kwargs["answer_format"] = answer_format
return self.template.format(**kwargs) # 格式化模板并返回
# 定义一个自定义搜索工具类,继承自基础工具类
class CustomSearchTool(BaseTool):
name: str = "DeepSearch" # 工具名称
description: str = "" # 工具描述
# 定义一个运行函数,接受一个查询字符串和一个可选的回调管理器作为参数,返回DeepSearch的搜索结果
def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None):
return DeepSearch.search(query = query)
# 定义一个异步运行函数,但由于DeepSearch不支持异步,所以直接抛出一个未实现错误
async def _arun(self, query: str):
raise NotImplementedError("DeepSearch does not support async")
# 定义一个自定义代理类,继承自基础单动作代理
class CustomAgent(BaseSingleActionAgent):
# 定义一个输入键的属性
@property
def input_keys(self):
return ["input"]
# 定义一个计划函数,接受一组中间步骤和其他参数,返回一个代理动作或者代理完成
def plan(self, intermedate_steps: List[Tuple[AgentAction, str]],
**kwargs: Any) -> Union[AgentAction, AgentFinish]:
return AgentAction(tool="DeepSearch", tool_input=kwargs["input"], log="")
# 定义一个自定义输出解析器,继承自代理输出解析器
class CustomOutputParser(AgentOutputParser):
# 定义一个解析函数,接受一个语言模型的输出字符串,返回一个代理动作或者代理完成
def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:
# 使用正则表达式匹配输出字符串,group1是调用函数名字,group2是传入参数
match = re.match(r'^[\s\w]*(DeepSearch)\(([^\)]+)\)', llm_output, re.DOTALL)
print(match)
# 如果语言模型没有返回 DeepSearch() 则认为直接结束指令
if not match:
return AgentFinish(
return_values={"output": llm_output.strip()},
log=llm_output,
)
# 否则的话都认为需要调用 Tool
else:
action = match.group(1).strip()
action_input = match.group(2).strip()
return AgentAction(tool=action, tool_input=action_input.strip(" ").strip('"'), log=llm_output)
# 定义一个深度代理类
class DeepAgent:
tool_name: str = "DeepSearch" # 工具名称
agent_executor: any # 代理执行器
tools: List[Tool] # 工具列表
llm_chain: any # 语言模型链
# 定义一个查询函数,接受一个相关内容字符串和一个查询字符串,返回执行器的运行结果
def query(self, related_content: str = "", query: str = ""):
tool_name =这段代码的主要目的是建立一个深度搜索的AI代理。AI代理首先通过接收一个问题输入,然后根据输入生成一个提示模板,然后通过该模板引导AI生成回答或进行更深入的搜索。现在,我将继续为剩余的代码添加中文注释
```python
self.tool_name
result = self.agent_executor.run(related_content=related_content, input=query ,tool_name=self.tool_name)
return result # 返回执行器的运行结果
# 在初始化函数中,首先从DeepSearch工具创建一个工具实例,并添加到工具列表中
def __init__(self, llm: BaseLanguageModel, **kwargs):
tools = [
Tool.from_function(
func=DeepSearch.search,
name="DeepSearch",
description=""
)
]
self.tools = tools # 保存工具列表
tool_names = [tool.name for tool in tools] # 提取工具列表中的工具名称
output_parser = CustomOutputParser() # 创建一个自定义输出解析器实例
# 创建一个自定义提示模板实例
prompt = CustomPromptTemplate(template=agent_template,
tools=tools,
input_variables=["related_content","tool_name", "input", "intermediate_steps"])
# 创建一个语言模型链实例
llm_chain = LLMChain(llm=llm, prompt=prompt)
self.llm_chain = llm_chain # 保存语言模型链实例
# 创建一个语言模型单动作代理实例
agent = LLMSingleActionAgent(
llm_chain=llm_chain,
output_parser=output_parser,
stop=["\nObservation:"],
allowed_tools=tool_names
)
# 创建一个代理执行器实例
agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True)
self.agent_executor = agent_executor # 保存代理执行器实例
3.1.2 エージェント/bing_search.py
#coding=utf8
# 声明文件编码格式为 utf8
from langchain.utilities import BingSearchAPIWrapper
# 导入 BingSearchAPIWrapper 类,这个类用于与 Bing 搜索 API 进行交互
from configs.model_config import BING_SEARCH_URL, BING_SUBSCRIPTION_KEY
# 导入配置文件中的 Bing 搜索 URL 和 Bing 订阅密钥
def bing_search(text, result_len=3):
# 定义一个名为 bing_search 的函数,该函数接收一个文本和结果长度的参数,默认结果长度为3
if not (BING_SEARCH_URL and BING_SUBSCRIPTION_KEY):
# 如果 Bing 搜索 URL 或 Bing 订阅密钥未设置,则返回一个错误信息的文档
return [{"snippet": "please set BING_SUBSCRIPTION_KEY and BING_SEARCH_URL in os ENV",
"title": "env inof not fould",
"link": "https://python.langchain.com/en/latest/modules/agents/tools/examples/bing_search.html"}]
search = BingSearchAPIWrapper(bing_subscription_key=BING_SUBSCRIPTION_KEY,
bing_search_url=BING_SEARCH_URL)
# 创建 BingSearchAPIWrapper 类的实例,该实例用于与 Bing 搜索 API 进行交互
return search.results(text, result_len)
# 返回搜索结果,结果的数量由 result_len 参数决定
if __name__ == "__main__":
# 如果这个文件被直接运行,而不是被导入作为模块,那么就执行以下代码
r = bing_search('python')
# 使用 Bing 搜索 API 来搜索 "python" 这个词,并将结果保存在变量 r 中
print(r)
# 打印出搜索结果
3.2 モデル: モデルとドキュメント ローダー ローダーが含まれています
- モデル: llm のインターフェイス クラスと実装クラス。オープン ソース モデルのストリーミング出力サポートを提供します。
- loader: ドキュメントローダーの実装クラス
3.2.1 モデル/chatglm_llm.py
from abc import ABC # 导入抽象基类
from langchain.llms.base import LLM # 导入语言学习模型基类
from typing import Optional, List # 导入类型标注模块
from models.loader import LoaderCheckPoint # 导入模型加载点
from models.base import (BaseAnswer, # 导入基本回答模型
AnswerResult) # 导入回答结果模型
class ChatGLM(BaseAnswer, LLM, ABC): # 定义ChatGLM类,继承基础回答、语言学习模型和抽象基类
max_token: int = 10000 # 最大的token数
temperature: float = 0.01 # 温度参数,用于控制生成文本的随机性
top_p = 0.9 # 排序前0.9的token会被保留
checkPoint: LoaderCheckPoint = None # 检查点模型
# history = [] # 历史记录
history_len: int = 10 # 历史记录长度
def __init__(self, checkPoint: LoaderCheckPoint = None): # 初始化方法
super().__init__() # 调用父类的初始化方法
self.checkPoint = checkPoint # 赋值检查点模型
@property
def _llm_type(self) -> str: # 定义只读属性_llm_type,返回语言学习模型的类型
return "ChatGLM"
@property
def _check_point(self) -> LoaderCheckPoint: # 定义只读属性_check_point,返回检查点模型
return self.checkPoint
@property
def _history_len(self) -> int: # 定义只读属性_history_len,返回历史记录的长度
return self.history_len
def set_history_len(self, history_len: int = 10) -> None: # 设置历史记录长度
self.history_len = history_len
def _call(self, prompt: str, stop: Optional[List[str]] = None) -> str: # 定义_call方法,实现模型的具体调用
print(f"__call:{prompt}") # 打印调用的提示信息
response, _ = self.checkPoint.model.chat( # 调用模型的chat方法,获取回答和其他信息
self.checkPoint.tokenizer, # 使用的分词器
prompt, # 提示信息
history=[], # 历史记录
max_length=self.max_token, # 最大长度
temperature=self.temperature # 温度参数
)
print(f"response:{response}") # 打印回答信息
print(f"+++++++++++++++++++++++++++++++++++") # 打印分隔线
return response # 返回回答
def generatorAnswer(self, prompt: str,
history: List[List[str]] = [],
streaming: bool = False): # 定义生成回答的方法,可以处理流式输入
if streaming: # 如果是流式输入
history += [[]] # 在历史记录中添加新的空列表
for inum, (stream_resp, _) in enumerate(self.checkPoint.model.stream_chat( # 对模型的stream_chat方法返回的结果进行枚举
self.checkPoint.tokenizer, # 使用的分词器
prompt, # 提示信息
history=history[-self.history_len:-1] if self.history_len > 1 else [], # 使用的历史记录
max_length=self.max_token, # 最大长度
temperature=self.temperature # 温度参数
)):
# self.checkPoint.clear_torch_cache() # 清空缓存
history[-1] = [prompt, stream_resp] # 更新最后一个历史记录
answer_result = AnswerResult() # 创建回答结果对象
answer_result.history = history # 更新回答结果的历史记录
answer_result.llm_output = {"answer": stream_resp} # 更新回答结果的输出
yield answer_result # 生成回答结果
else: # 如果不是流式输入
response, _ = self.checkPoint.model.chat( # 调用模型的chat方法,获取回答和其他信息
self.checkPoint.tokenizer, # 使用的分词器
prompt, # 提示信息
history=history[-self.history_len:] if self.history_len > 0 else [], # 使用的历史记录
max_length=self.max_token, # 最大长度
temperature=self.temperature # 温度参数
)
self.checkPoint.clear_torch_cache() # 清空缓存
history += [[prompt, response]] # 更新历史记录
answer_result = AnswerResult() # 创建回答结果对象
answer_result.history = history # 更新回答结果的历史记录
answer_result.llm_output = {"answer": response} # 更新回答结果的输出
yield answer_result # 生成回答结果
3.2.2 モデル/shared.py
このファイルの機能は、LLM をリモートで呼び出すことです。
import sys # 导入sys模块,通常用于与Python解释器进行交互
from typing import Any # 从typing模块导入Any,用于表示任何类型
# 从models.loader.args模块导入parser,可能是解析命令行参数用
from models.loader.args import parser
# 从models.loader模块导入LoaderCheckPoint,可能是模型加载点
from models.loader import LoaderCheckPoint
# 从configs.model_config模块导入llm_model_dict和LLM_MODEL
from configs.model_config import (llm_model_dict, LLM_MODEL)
# 从models.base模块导入BaseAnswer,即模型的基础类
from models.base import BaseAnswer
# 定义一个名为loaderCheckPoint的变量,类型为LoaderCheckPoint,并初始化为None
loaderCheckPoint: LoaderCheckPoint = None
def loaderLLM(llm_model: str = None, no_remote_model: bool = False, use_ptuning_v2: bool = False) -> Any:
"""
初始化 llm_model_ins LLM
:param llm_model: 模型名称
:param no_remote_model: 是否使用远程模型,如果需要加载本地模型,则添加 `--no-remote-model
:param use_ptuning_v2: 是否使用 p-tuning-v2 PrefixEncoder
:return:
"""
pre_model_name = loaderCheckPoint.model_name # 获取loaderCheckPoint的模型名称
llm_model_info = llm_model_dict[pre_model_name] # 从模型字典中获取模型信息
if no_remote_model: # 如果不使用远程模型
loaderCheckPoint.no_remote_model = no_remote_model # 将loaderCheckPoint的no_remote_model设置为True
if use_ptuning_v2: # 如果使用p-tuning-v2
loaderCheckPoint.use_ptuning_v2 = use_ptuning_v2 # 将loaderCheckPoint的use_ptuning_v2设置为True
if llm_model: # 如果指定了模型名称
llm_model_info = llm_model_dict[llm_model] # 从模型字典中获取指定的模型信息
if loaderCheckPoint.no_remote_model: # 如果不使用远程模型
loaderCheckPoint.model_name = llm_model_info['name'] # 将loaderCheckPoint的模型名称设置为模型信息中的name
else: # 如果使用远程模型
loaderCheckPoint.model_name = llm_model_info['pretrained_model_name'] # 将loaderCheckPoint的模型名称设置为模型信息中的pretrained_model_name
loaderCheckPoint.model_path = llm_model_info["local_model_path"] # 设置模型的本地路径
if 'FastChatOpenAILLM' in llm_model_info["provides"]: # 如果模型信息中的provides包含'FastChatOpenAILLM'
loaderCheckPoint.unload_model() # 卸载模型
else: # 如果不包含
loaderCheckPoint.reload_model() # 重新加载模型
provides_class = getattr(sys.modules['models'], llm_model_info['provides']) # 获取模型类
modelInsLLM = provides_class(checkPoint=loaderCheckPoint) # 创建模型实例
if 'FastChatOpenAILLM' in llm_model_info["provides"]: # 如果模型信息中的provides包含'FastChatOpenAILLM'
modelInsLLM.set_api_base_url(llm_model_info['api_base_url']) # 设置API基础URL
modelInsLLM.call_model_name(llm_model_info['name']) # 设置模型名称
return modelInsLLM # 返回模型实例
// 更新予定..
3.3 configs: 設定ファイルの保存model_config.py
import torch.cuda
import torch.backends
import os
import logging
import uuid
LOG_FORMAT = "%(levelname) -5s %(asctime)s" "-1d: %(message)s"
logger = logging.getLogger()
logger.setLevel(logging.INFO)
logging.basicConfig(format=LOG_FORMAT)
# 在以下字典中修改属性值,以指定本地embedding模型存储位置
# 如将 "text2vec": "GanymedeNil/text2vec-large-chinese" 修改为 "text2vec": "User/Downloads/text2vec-large-chinese"
# 此处请写绝对路径
embedding_model_dict = {
"ernie-tiny": "nghuyong/ernie-3.0-nano-zh",
"ernie-base": "nghuyong/ernie-3.0-base-zh",
"text2vec-base": "shibing624/text2vec-base-chinese",
"text2vec": "GanymedeNil/text2vec-large-chinese",
"m3e-small": "moka-ai/m3e-small",
"m3e-base": "moka-ai/m3e-base",
}
# Embedding model name
EMBEDDING_MODEL = "text2vec"
# Embedding running device
EMBEDDING_DEVICE = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
# supported LLM models
# llm_model_dict 处理了loader的一些预设行为,如加载位置,模型名称,模型处理器实例
# 在以下字典中修改属性值,以指定本地 LLM 模型存储位置
# 如将 "chatglm-6b" 的 "local_model_path" 由 None 修改为 "User/Downloads/chatglm-6b"
# 此处请写绝对路径
llm_model_dict = {
"chatglm-6b-int4-qe": {
"name": "chatglm-6b-int4-qe",
"pretrained_model_name": "THUDM/chatglm-6b-int4-qe",
"local_model_path": None,
"provides": "ChatGLM"
},
"chatglm-6b-int4": {
"name": "chatglm-6b-int4",
"pretrained_model_name": "THUDM/chatglm-6b-int4",
"local_model_path": None,
"provides": "ChatGLM"
},
"chatglm-6b-int8": {
"name": "chatglm-6b-int8",
"pretrained_model_name": "THUDM/chatglm-6b-int8",
"local_model_path": None,
"provides": "ChatGLM"
},
"chatglm-6b": {
"name": "chatglm-6b",
"pretrained_model_name": "THUDM/chatglm-6b",
"local_model_path": None,
"provides": "ChatGLM"
},
"chatglm2-6b": {
"name": "chatglm2-6b",
"pretrained_model_name": "THUDM/chatglm2-6b",
"local_model_path": None,
"provides": "ChatGLM"
},
"chatglm2-6b-int4": {
"name": "chatglm2-6b-int4",
"pretrained_model_name": "THUDM/chatglm2-6b-int4",
"local_model_path": None,
"provides": "ChatGLM"
},
"chatglm2-6b-int8": {
"name": "chatglm2-6b-int8",
"pretrained_model_name": "THUDM/chatglm2-6b-int8",
"local_model_path": None,
"provides": "ChatGLM"
},
"chatyuan": {
"name": "chatyuan",
"pretrained_model_name": "ClueAI/ChatYuan-large-v2",
"local_model_path": None,
"provides": None
},
"moss": {
"name": "moss",
"pretrained_model_name": "fnlp/moss-moon-003-sft",
"local_model_path": None,
"provides": "MOSSLLM"
},
"vicuna-13b-hf": {
"name": "vicuna-13b-hf",
"pretrained_model_name": "vicuna-13b-hf",
"local_model_path": None,
"provides": "LLamaLLM"
},
# 通过 fastchat 调用的模型请参考如下格式
"fastchat-chatglm-6b": {
"name": "chatglm-6b", # "name"修改为fastchat服务中的"model_name"
"pretrained_model_name": "chatglm-6b",
"local_model_path": None,
"provides": "FastChatOpenAILLM", # 使用fastchat api时,需保证"provides"为"FastChatOpenAILLM"
"api_base_url": "http://localhost:8000/v1" # "name"修改为fastchat服务中的"api_base_url"
},
"fastchat-chatglm2-6b": {
"name": "chatglm2-6b", # "name"修改为fastchat服务中的"model_name"
"pretrained_model_name": "chatglm2-6b",
"local_model_path": None,
"provides": "FastChatOpenAILLM", # 使用fastchat api时,需保证"provides"为"FastChatOpenAILLM"
"api_base_url": "http://localhost:8000/v1" # "name"修改为fastchat服务中的"api_base_url"
},
# 通过 fastchat 调用的模型请参考如下格式
"fastchat-vicuna-13b-hf": {
"name": "vicuna-13b-hf", # "name"修改为fastchat服务中的"model_name"
"pretrained_model_name": "vicuna-13b-hf",
"local_model_path": None,
"provides": "FastChatOpenAILLM", # 使用fastchat api时,需保证"provides"为"FastChatOpenAILLM"
"api_base_url": "http://localhost:8000/v1" # "name"修改为fastchat服务中的"api_base_url"
},
}
# LLM 名称
LLM_MODEL = "chatglm-6b"
# 量化加载8bit 模型
LOAD_IN_8BIT = False
# Load the model with bfloat16 precision. Requires NVIDIA Ampere GPU.
BF16 = False
# 本地lora存放的位置
LORA_DIR = "loras/"
# LLM lora path,默认为空,如果有请直接指定文件夹路径
LLM_LORA_PATH = ""
USE_LORA = True if LLM_LORA_PATH else False
# LLM streaming reponse
STREAMING = True
# Use p-tuning-v2 PrefixEncoder
USE_PTUNING_V2 = False
# LLM running device
LLM_DEVICE = "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu"
# 知识库默认存储路径
KB_ROOT_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "knowledge_base")
# 基于上下文的prompt模版,请务必保留"{question}"和"{context}"
PROMPT_TEMPLATE = """已知信息:
{context}
根据上述已知信息,简洁和专业的来回答用户的问题。如果无法从中得到答案,请说 “根据已知信息无法回答该问题” 或 “没有提供足够的相关信息”,不允许在答案中添加编造成分,答案请使用中文。 问题是:{question}"""
# 缓存知识库数量,如果是ChatGLM2,ChatGLM2-int4,ChatGLM2-int8模型若检索效果不好可以调成’10’
CACHED_VS_NUM = 1
# 文本分句长度
SENTENCE_SIZE = 100
# 匹配后单段上下文长度
CHUNK_SIZE = 250
# 传入LLM的历史记录长度
LLM_HISTORY_LEN = 3
# 知识库检索时返回的匹配内容条数
VECTOR_SEARCH_TOP_K = 5
# 知识检索内容相关度 Score, 数值范围约为0-1100,如果为0,则不生效,经测试设置为小于500时,匹配结果更精准
VECTOR_SEARCH_SCORE_THRESHOLD = 0
NLTK_DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "nltk_data")
FLAG_USER_NAME = uuid.uuid4().hex
logger.info(f"""
loading model config
llm device: {LLM_DEVICE}
embedding device: {EMBEDDING_DEVICE}
dir: {os.path.dirname(os.path.dirname(__file__))}
flagging username: {FLAG_USER_NAME}
""")
# 是否开启跨域,默认为False,如果需要开启,请设置为True
# is open cross domain
OPEN_CROSS_DOMAIN = False
# Bing 搜索必备变量
# 使用 Bing 搜索需要使用 Bing Subscription Key,需要在azure port中申请试用bing search
# 具体申请方式请见
# https://learn.microsoft.com/en-us/bing/search-apis/bing-web-search/create-bing-search-service-resource
# 使用python创建bing api 搜索实例详见:
# https://learn.microsoft.com/en-us/bing/search-apis/bing-web-search/quickstarts/rest/python
BING_SEARCH_URL = "https://api.bing.microsoft.com/v7.0/search"
# 注意不是bing Webmaster Tools的api key,
# 此外,如果是在服务器上,报Failed to establish a new connection: [Errno 110] Connection timed out
# 是因为服务器加了防火墙,需要联系管理员加白名单,如果公司的服务器的话,就别想了GG
BING_SUBSCRIPTION_KEY = ""
# 是否开启中文标题加强,以及标题增强的相关配置
# 通过增加标题判断,判断哪些文本为标题,并在metadata中进行标记;
# 然后将文本与往上一级的标题进行拼合,实现文本信息的增强。
ZH_TITLE_ENHANCE = False
3.4 ローダー: ドキュメントのロードとテキスト変換
3.4.1 ローダー/pdf_loader.py
# 导入类型提示模块,用于强化代码的可读性和健壮性
from typing import List
# 导入UnstructuredFileLoader,这是一个从非结构化文件中加载文档的类
from langchain.document_loaders.unstructured import UnstructuredFileLoader
# 导入PaddleOCR,这是一个开源的OCR工具,用于从图片中识别和读取文字
from paddleocr import PaddleOCR
# 导入os模块,用于处理文件和目录
import os
# 导入fitz模块,用于处理PDF文件
import fitz
# 导入nltk模块,用于处理文本数据
import nltk
# 导入模型配置文件中的NLTK_DATA_PATH,这是nltk数据的路径
from configs.model_config import NLTK_DATA_PATH
# 设置nltk数据的路径,将模型配置中的路径添加到nltk的数据路径中
nltk.data.path = [NLTK_DATA_PATH] + nltk.data.path
# 定义一个类,UnstructuredPaddlePDFLoader,该类继承自UnstructuredFileLoader
class UnstructuredPaddlePDFLoader(UnstructuredFileLoader):
# 定义一个内部方法_get_elements,返回一个列表
def _get_elements(self) -> List:
# 定义一个内部函数pdf_ocr_txt,用于从pdf中进行OCR并输出文本文件
def pdf_ocr_txt(filepath, dir_path="tmp_files"):
# 将dir_path与filepath的目录部分合并成一个新的路径
full_dir_path = os.path.join(os.path.dirname(filepath), dir_path)
# 如果full_dir_path对应的目录不存在,则创建这个目录
if not os.path.exists(full_dir_path):
os.makedirs(full_dir_path)
# 创建一个PaddleOCR实例,设置一些参数
ocr = PaddleOCR(use_angle_cls=True, lang="ch", use_gpu=False, show_log=False)
# 打开pdf文件
doc = fitz.open(filepath)
# 创建一个txt文件的路径
txt_file_path = os.path.join(full_dir_path, f"{os.path.split(filepath)[-1]}.txt")
# 创建一个临时的图片文件路径
img_name = os.path.join(full_dir_path, 'tmp.png')
# 打开txt_file_path对应的文件,并以写模式打开
with open(txt_file_path, 'w', encoding='utf-8') as fout:
# 遍历pdf的所有页面
for i in range(doc.page_count):
# 获取当前页面
page = doc[i]
# 获取当前页面的文本内容,并写入txt文件
text = page.get_text("")
fout.write(text)
fout.write("\n")
# 获取当前页面的所有图片
img_list = page.get_images()
# 遍历所有图片
for img in img_list:
# 将图片转换为Pixmap对象
pix = fitz.Pixmap(doc, img[0])
# 如果图片有颜色信息,则将其转换为RGB格式
if pix.n - pix.alpha >= 4:
pix = fitz.Pixmap(fitz.csRGB, pix)
# 保存图片
pix.save(img_name)
# 对图片进行OCR识别
result = ocr.ocr(img_name)
# 从OCR结果中提取文本,并写入txt文件
ocr_result = [i[1][0] for line in result for i in line]
fout.write("\n".join(ocr_result))
# 如果图片文件存在,则删除它
if os.path.exists(img_name):
os.remove(img_name)
# 返回txt文件的路径
return txt_file_path
# 调用上面定义的函数,获取txt文件的路径
txt_file_path = pdf_ocr_txt(self.file_path)
# 导入partition_text函数,该函数用于将文本文件分块
from unstructured.partition.text import partition_text
# 对txt文件进行分块,并返回分块结果
return partition_text(filename=txt_file_path, **self.unstructured_kwargs)
# 运行入口
if __name__ == "__main__":
# 导入sys模块,用于操作Python的运行环境
import sys
# 将当前文件的上一级目录添加到Python的搜索路径中
sys.path.append(os.path.dirname(os.path.dirname(__file__)))
# 定义一个pdf文件的路径
filepath = os.path.join(os.path.dirname(os.path.dirname(__file__)), "knowledge_base", "samples", "content", "test.pdf")
# 创建一个UnstructuredPaddlePDFLoader的实例
loader = UnstructuredPaddlePDFLoader(filepath, mode="elements")
# 加载文档
docs = loader.load()
# 遍历并打印所有文档
for doc in docs:
print(doc)
// 更新予定..
3.5 textsplitter : ドキュメントの分割
3.5.1 textsplitter/ali_text_splitter.py
ali_text_splitter.pyのコードは以下の通りです
# 导入CharacterTextSplitter模块,用于文本切分
from langchain.text_splitter import CharacterTextSplitter
import re # 导入正则表达式模块,用于文本匹配和替换
from typing import List # 导入List类型,用于指定返回的数据类型
# 定义一个新的类AliTextSplitter,继承自CharacterTextSplitter
class AliTextSplitter(CharacterTextSplitter):
# 类的初始化函数,如果参数pdf为True,那么使用pdf文本切分规则,否则使用默认规则
def __init__(self, pdf: bool = False, **kwargs):
# 调用父类的初始化函数,接收传入的其他参数
super().__init__(**kwargs)
self.pdf = pdf # 将pdf参数保存为类的成员变量
# 定义文本切分方法,输入参数为一个字符串,返回值为字符串列表
def split_text(self, text: str) -> List[str]:
if self.pdf: # 如果pdf参数为True,那么对文本进行预处理
# 替换掉连续的3个及以上的换行符为一个换行符
text = re.sub(r"\n{3,}", r"\n", text)
# 将所有的空白字符(包括空格、制表符、换页符等)替换为一个空格
text = re.sub('\s', " ", text)
# 将连续的两个换行符替换为一个空字符
text = re.sub("\n\n", "", text)
# 导入pipeline模块,用于创建一个处理流程
from modelscope.pipelines import pipeline
# 创建一个document-segmentation任务的处理流程
# 用的模型为damo/nlp_bert_document-segmentation_chinese-base,计算设备为cpu
p = pipeline(
task="document-segmentation",
model='damo/nlp_bert_document-segmentation_chinese-base',
device="cpu")
result = p(documents=text) # 对输入的文本进行处理,返回处理结果
sent_list = [i for i in result["text"].split("\n\t") if i] # 将处理结果按照换行符和制表符进行切分,得到句子列表
return sent_list # 返回句子列表
その中で、注目すべき点は次の 3 つです。
- use_document_segmentation パラメータは、ドキュメントのセマンティック セグメンテーションを使用するかどうかを指定します。
ここで採用されているドキュメント セマンティック セグメンテーション モデルは、DAMO アカデミーによってオープンソース化されています: nlp_bert_document-segmentation_chinese-base (これは彼の論文です) - さらに、ドキュメントのセマンティック セグメンテーションにモデルを使用する場合は、以下をインストールする必要があります。
modelscope[nlp]:pip install "modelscope[nlp]" -f https://modelscope.oss-cn-beijing.aliyuncs.com/releases/repo.html
- また、3 つのモデルが使用されることを考慮すると、低構成の GPU には適していない可能性があるため、ここではモデルを CPU にロードして計算しますが、必要に応じてデバイスを独自のグラフィックス カード ID に置き換えることもできます。
3.6 ナレッジベース: ユーザーがアップロードしたファイルを保存し、それらを定量化します。
Knowledge_bas の下には 2 つのファイルがあります。1 つはコンテンツで、ユーザーがアップロードしたオリジナルのファイルです。Vector_store は、ベクトル ライブラリ ファイル、つまりローカルのナレッジ ベース オントロジーを保存するために使用されます。コンテンツは人によって異なるため、誰がアップロードしても、 Vector_store の下には、index.faiss と Index.pkl の 2 つのファイルがあります。
3.7 チェーン: ベクトル検索/マッチング
前述したように、本節冒頭の図の「FAISSインデックス、FAISS検索」の「FAISS」は、大規模な高次元ベクトル空間における類似性検索を効率的に行うためにFacebook AIによって起動されたライブラリです。スケール データ セット 特定のベクトルに最も類似したベクトルを迅速に見つけることは、推奨システム、自然言語処理、画像検索などの多くの AI アプリケーションの重要な部分です。
3.7.1chains /modules/vectorstores.pyファイル: クエリ ベクトル クエリに基づいて、ベクトル データベース内のクエリに類似したテキスト ベクトルを検索します。
主に、FAISS (Facebook AI 類似性検索) の使用と、次の主なメソッドを含む FAISS ベクトル ストレージ クラス (FAISSVS、FAISSVS クラスは FAISS クラスから継承) の定義について説明します。
- max_marginal_relevance_search
クエリ ステートメントを指定すると、まずクエリ ステートメントを埋め込みベクトル「embedding = self.embedding_function(query)」に変換してから、 MMR 検索用のmax_marginal_relevance_search_by_vector 関数を呼び出します。
max_marginal_relevance_search_by_vector は# 使用最大边际相关性返回被选中的文本 def max_marginal_relevance_search( self, query: str, # 查询 k: int = 4, # 返回的文档数量,默认为 4 fetch_k: int = 20, # 用于传递给 MMR 算法的抓取文档数量 **kwargs: Any, ) -> List[Tuple[Document, float]]: # 查询向量化 embedding = self.embedding_function(query) # 调用:max_marginal_relevance_search_by_vector docs = self.max_marginal_relevance_search_by_vector(embedding, k, fetch_k) return docs
、Maximal Marginal Relevance (MMR) メソッドを使用して、指定された埋め込みベクトルを通じて関連テキストを返します。MMR は、
クエリ結果の多様性と関連性を解決するアルゴリズムです。具体的には、返されたテキストが可能な限り類似しているだけでなく、クエリで、返されるテキストのセットをできるだけ多様なものにしたい場合# 使用最大边际相关性返回被选中的文档,最大边际相关性旨在优化查询的相似性和选定文本之间的多样性 def max_marginal_relevance_search_by_vector( self, embedding: List[float], k: int = 4, fetch_k: int = 20, **kwargs: Any ) -> List[Tuple[Document, float]]: # 使用索引在文本中搜索与嵌入向量相似的内容,返回最相似的fetch_k个文本的得分和索引 scores, indices = self.index.search(np.array([embedding], dtype=np.float32), fetch_k) # 通过索引从文本中重构出嵌入向量,-1表示没有足够的文本返回 embeddings = [self.index.reconstruct(int(i)) for i in indices[0] if i != -1] # 使用最大边际相关性算法选择出k个最相关的文本 mmr_selected = maximal_marginal_relevance( np.array([embedding], dtype=np.float32), embeddings, k=k ) selected_indices = [indices[0][i] for i in mmr_selected] # 获取被选中的文本的索引 selected_scores = [scores[0][i] for i in mmr_selected] # 获取被选中的文本的得分 docs = [] for i, score in zip(selected_indices, selected_scores): # 对于每个被选中的文本索引和得分 if i == -1: # 如果索引为-1,表示没有足够的文本返回 continue _id = self.index_to_docstore_id[i] # 通过索引获取文本的id doc = self.docstore.search(_id) # 通过id在文档库中搜索文本 if not isinstance(doc, Document): # 如果搜索到的文本不是Document类型,抛出错误 raise ValueError(f"Could not find document for id {_id}, got {doc}") docs.append((doc, score)) # 将文本和得分添加到结果列表中 return docs # 返回结果列表
- __from は、
テキストと対応する埋め込みベクトルのセットから FAISSVS インスタンスを作成するために使用されます。このメソッドは、まず FAISS インデックスを作成して埋め込みベクトルを追加し、次に各埋め込みベクトルに関連付けられたテキストを保存するテキスト ストアを作成します。# 从给定的文本、嵌入向量、元数据等信息构建一个FAISS索引对象 def __from( cls, texts: List[str], # 文本列表,每个文本将被转化为一个文本对象 embeddings: List[List[float]], # 对应文本的嵌入向量列表 embedding: Embeddings, # 嵌入向量生成器,用于将查询语句转化为嵌入向量 metadatas: Optional[List[dict]] = None, **kwargs: Any, ) -> FAISS: faiss = dependable_faiss_import() # 导入FAISS库 index = faiss.IndexFlatIP(len(embeddings[0])) # 使用FAISS库创建一个新的索引,索引的维度等于嵌入文本向量的长度 index.add(np.array(embeddings, dtype=np.float32)) # 将嵌入向量添加到FAISS索引中 # quantizer = faiss.IndexFlatL2(len(embeddings[0])) # index = faiss.IndexIVFFlat(quantizer, len(embeddings[0]), 100) # index.train(np.array(embeddings, dtype=np.float32)) # index.add(np.array(embeddings, dtype=np.float32)) documents = [] for i, text in enumerate(texts): # 对于每一段文本 # 获取对应的元数据,如果没有提供元数据则使用空字典 metadata = metadatas[i] if metadatas else {} # 创建一个文本对象并添加到文本列表中 documents.append(Document(page_content=text, metadata=metadata)) # 为每个文本生成一个唯一的ID index_to_id = {i: str(uuid.uuid4()) for i in range(len(documents))} # 创建一个文本库,用于存储文本对象和对应的ID docstore = InMemoryDocstore( {index_to_id[i]: doc for i, doc in enumerate(documents)} ) # 返回FAISS对象 return cls(embedding.embed_query, index, docstore, index_to_id)
上記がこのコードの主な内容ですが、FAISS と MMR を使用すると、大量のテキストの中から特定のクエリに対して最も関連性の高いテキストを見つけることができます。
3.7.2chains /local_doc_qa.pyコード ファイル: ベクトル検索
- パッケージとモジュールのインポート
コードの先頭は、ファイル ローダー、テキスト スプリッター、モデル構成、一部の Python 組み込みモジュールやその他のサードパーティ ライブラリなど、必要な Python パッケージとモジュールをインポートする一連の import ステートメントです。 - HuggingFaceEmbeddings クラスのハッシュ メソッドを書き換えます。このコードでは、 _embeddings_hash
という名前の関数を定義し、それを HuggingFaceEmbeddings クラスの __hash__ メソッドに割り当てます。この目的は、HuggingFaceEmbeddings オブジェクトをハッシュできるようにすることです。つまり、オブジェクトを辞書キーとして使用したり、コレクションに追加したりできます。 - ベクトル メモリのロードでは、 load_vector_storeという名前の関数
が定義されます。この関数は、ローカルからベクトル メモリをロードし、FAISS クラスのオブジェクトを返すために使用されます。lru_cache デコレータが使用されます。これにより、最近使用された CACHED_VS_NUM 結果をキャッシュし、コード効率を向上させることができます。 - ファイル ツリー走査
ツリー関数は、指定されたディレクトリ内のすべてのファイルを走査し、すべてのファイルの完全なパスとファイル名を含むリストを返す再帰関数です。指定されたファイルまたはディレクトリを無視することができます - ファイルのロード:
load_file関数は、ファイル拡張子に基づいて適切なローダーとテキスト スプリッターを選択し、ファイルをロードして分割します。 - リマインダーの生成:
generate_prompt関数は、関連するドキュメントとクエリに基づいてリマインダーを生成するために使用されます。リマインダー テンプレートは、prompt_template パラメータによって提供されます。 - ドキュメントリストの作成
search_result2docs# 创建一个空列表,用于存储文档 def search_result2docs(search_results): docs = [] # 对于搜索结果中的每一项 for result in search_results: # 创建一个文档对象 # 如果结果中包含"snippet"关键字,则其值作为页面内容,否则页面内容为空字符串 # 如果结果中包含"link"关键字,则其值作为元数据中的源链接,否则源链接为空字符串 # 如果结果中包含"title"关键字,则其值作为元数据中的文件名,否则文件名为空字符串 doc = Document(page_content=result["snippet"] if "snippet" in result.keys() else "", metadata={"source": result["link"] if "link" in result.keys() else "", "filename": result["title"] if "title" in result.keys() else ""}) # 将创建的文档对象添加到列表中 docs.append(doc) # 返回文档列表 return docs
その後、LocalDocQA という名前のクラスが定義されます。これは主にドキュメントベースの質問と回答のタスクに使用されます。ドキュメント ベースの質問と回答タスクの主な機能は、指定された一連のドキュメント (ここではナレッジ ベースと呼びます) とユーザーが入力した質問に基づいて回答を返すことです。LocalDocQA クラスの主なメソッドは次のとおりです。
- init_cfg() : このメソッドは、llm_model (回答の生成に使用される言語モデル) の self.llm への割り当て、HuggingFace ベースの埋め込みモデルの self.embeddings への割り当て、入力パラメーター top_k の self.top_k への割り当てなど、いくつかの変数を初期化します。
- init_knowledge_vector_store() : このメソッドは、知識ベクトル ライブラリの初期化を担当します。まず入力ファイル パスをチェックし、パス内のファイルごとにファイルのコンテンツを Document オブジェクトに読み込み、次にこれらのドキュメントを埋め込みベクトルに変換して、ベクトル ライブラリに保存します。
- one_knowledge_add() : このメソッドは、新しいナレッジ ドキュメントをナレッジ ベースに追加するために使用されます。入力されたタイトルとコンテンツを Document オブジェクトとして作成し、それを埋め込みベクトルに変換して、ベクトル ライブラリに追加します。
- get_knowledge_based_answer() : このメソッドは、指定されたナレッジ ベースとユーザーが入力した質問に基づいて回答を生成します。まず、ユーザーが入力した質問に基づいてナレッジ ベース内で最も関連性の高いドキュメントを検索し、次に関連するドキュメントとユーザーの質問を含むプロンプトを生成し、そのプロンプトを llm_model に渡して回答を生成します。上記で実装されました
:similarity_search_with_score - get_knowledge_based_conent_test() : このメソッドはテスト用であり、入力クエリで最も関連性の高いドキュメントとクエリ プロンプトを返します
# クエリ クエリ コンテンツ
# vs_path ナレッジ ベース パス
# chunk_conent コンテキストの関連付けを有効にするかどうか
# core_threshold 検索一致スコアしきい値
# Vector_search_top_k 検索ナレッジBase コンテンツ項目の数、デフォルトの検索結果は 5
# chunk_sizes 単一コンテンツの接続コンテキスト長と一致します
def get_knowledge_based_conent_test(self, query, vs_path, chunk_conent,
core_threshold=VECTOR_SEARCH_SCORE_THRESHOLD,
Vector_search_top_k=VECTOR_SEARCH_TOP_K, chunk_size=CHUNK_SIZE): - get_search_result_based_answer() : このメソッドは get_knowledge_based_answer() に似ていますが、ここでは bing_search の結果がナレッジ ベースとして使用されます。
ご覧のとおり、この関数と上記の関数の主な違いは、この関数は検索エンジンの検索結果を直接使用して回答を生成するのに対し、上記の関数はクエリ類似性検索を使用して最も関連性の高いテキストを検索し、それに基づいて検索を行うことです。これらのテキストは回答を生成しますdef get_search_result_based_answer(self, query, chat_history=[], streaming: bool = STREAMING): # 对查询进行 Bing 搜索,并获取搜索结果 results = bing_search(query) # 将搜索结果转化为文本的形式 result_docs = search_result2docs(results) # 生成用于提问的提示语 prompt = generate_prompt(result_docs, query) # 通过 LLM(长语言模型)生成回答 for answer_result in self.llm.generatorAnswer(prompt=prompt, history=chat_history, streaming=streaming): # 获取回答的文本 resp = answer_result.llm_output["answer"] # 获取聊天历史 history = answer_result.history # 将聊天历史中的最后一项的提问替换为当前的查询 history[-1][0] = query # 组装回答的结果 response = {"query": query, "result": resp, "source_documents": result_docs} # 返回回答的结果和聊天历史 yield response, history
。この bing_search はセクション 3.1.2 で定義されています。 - 次に、ベクトル ストアからファイルを削除する、ファイルを更新する、およびファイルを一覧表示するための3 つのメソッド
delete_file_from_vector_store
update_file_from_vector_store
list_file_from_vector_store をそれぞれ示します。# 删除向量存储中的文件 def delete_file_from_vector_store(self, filepath: str or List[str], # 文件路径,可以是单个文件或多个文件列表 vs_path): # 向量存储路径 vector_store = load_vector_store(vs_path, self.embeddings) # 从给定路径加载向量存储 status = vector_store.delete_doc(filepath) # 删除指定文件 return status # 返回删除状态 # 更新向量存储中的文件 def update_file_from_vector_store(self, filepath: str or List[str], # 需要更新的文件路径,可以是单个文件或多个文件列表 vs_path, # 向量存储路径 docs: List[Document],): # 需要更新的文件内容,文件以文档形式给出 vector_store = load_vector_store(vs_path, self.embeddings) # 从给定路径加载向量存储 status = vector_store.update_doc(filepath, docs) # 更新指定文件 return status # 返回更新状态 # 列出向量存储中的文件 def list_file_from_vector_store(self, vs_path, # 向量存储路径 fullpath=False): # 是否返回完整路径,如果为 False,则只返回文件名 vector_store = load_vector_store(vs_path, self.embeddings) # 从给定路径加载向量存储 docs = vector_store.list_docs() # 列出所有文件 if fullpath: # 如果需要完整路径 return docs # 返回完整路径列表 else: # 如果只需要文件名 return [os.path.split(doc)[-1] for doc in docs] # 用 os.path.split 将路径和文件名分离,只返回文件名列表
__main__
コードの一部は、LocalDocQA クラスのインスタンス化と使用例です。
- まず、llm_model_ins オブジェクトを初期化します。
- 次に、LocalDocQA のインスタンスを作成し、その init_cfg() メソッドを呼び出して初期化します。
- その後、クエリとナレッジ ベースへのパスを指定します。
- 次に、get_knowledge_based_answer() メソッドまたは get_search_result_based_answer() メソッドを呼び出してクエリに基づいて回答を取得し、回答とソース ドキュメント情報を出力します。
3.7.3チェーン/text_load.py
以下に示すように、チェーン フォルダーの下には最後のプロジェクト ファイル (langchain-ChatGLM/text_load.py at master · imClumsyPanda/langchain-ChatGLM · GitHub)もあります。
import os
import pinecone
from tqdm import tqdm
from langchain.llms import OpenAI
from langchain.text_splitter import SpacyTextSplitter
from langchain.document_loaders import TextLoader
from langchain.document_loaders import DirectoryLoader
from langchain.indexes import VectorstoreIndexCreator
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Pinecone
#一些配置文件
openai_key="你的key" # 注册 openai.com 后获得
pinecone_key="你的key" # 注册 app.pinecone.io 后获得
pinecone_index="你的库" #app.pinecone.io 获得
pinecone_environment="你的Environment" # 登录pinecone后,在indexes页面 查看Environment
pinecone_namespace="你的Namespace" #如果不存在自动创建
#科学上网你懂得
os.environ['HTTP_PROXY'] = 'http://127.0.0.1:7890'
os.environ['HTTPS_PROXY'] = 'http://127.0.0.1:7890'
#初始化pinecone
pinecone.init(
api_key=pinecone_key,
environment=pinecone_environment
)
index = pinecone.Index(pinecone_index)
#初始化OpenAI的embeddings
embeddings = OpenAIEmbeddings(openai_api_key=openai_key)
#初始化text_splitter
text_splitter = SpacyTextSplitter(pipeline='zh_core_web_sm',chunk_size=1000,chunk_overlap=200)
# 读取目录下所有后缀是txt的文件
loader = DirectoryLoader('../docs', glob="**/*.txt", loader_cls=TextLoader)
#读取文本文件
documents = loader.load()
# 使用text_splitter对文档进行分割
split_text = text_splitter.split_documents(documents)
try:
for document in tqdm(split_text):
# 获取向量并储存到pinecone
Pinecone.from_documents([document], embeddings, index_name=pinecone_index)
except Exception as e:
print(f"Error: {e}")
quit()
3.8 ベクターストア:MyFAISS.py
次のコードに示すように、2 つのファイル、1 つは __init__.py (コードは 1 行: from .MyFAISS import MyFAISS)、もう 1 つは MyFAISS.py
# 从langchain.vectorstores库导入FAISS
from langchain.vectorstores import FAISS
# 从langchain.vectorstores.base库导入VectorStore
from langchain.vectorstores.base import VectorStore
# 从langchain.vectorstores.faiss库导入dependable_faiss_import
from langchain.vectorstores.faiss import dependable_faiss_import
from typing import Any, Callable, List, Dict # 导入类型检查库
from langchain.docstore.base import Docstore # 从langchain.docstore.base库导入Docstore
# 从langchain.docstore.document库导入Document
from langchain.docstore.document import Document
import numpy as np # 导入numpy库,用于科学计算
import copy # 导入copy库,用于数据复制
import os # 导入os库,用于操作系统相关的操作
from configs.model_config import * # 从configs.model_config库导入所有内容
# 定义MyFAISS类,继承自FAISS和VectorStore两个父类
class MyFAISS(FAISS, VectorStore):
次に以下の関数を一つずつ実装していきます
3.8.1 クラスの初期化関数を定義します: __init__
# 定义类的初始化函数
def __init__(
self,
embedding_function: Callable,
index: Any,
docstore: Docstore,
index_to_docstore_id: Dict[int, str],
normalize_L2: bool = False,
):
# 调用父类FAISS的初始化函数
super().__init__(embedding_function=embedding_function,
index=index,
docstore=docstore,
index_to_docstore_id=index_to_docstore_id,
normalize_L2=normalize_L2)
# 初始化分数阈值
self.score_threshold=VECTOR_SEARCH_SCORE_THRESHOLD
# 初始化块大小
self.chunk_size = CHUNK_SIZE
# 初始化块内容
self.chunk_conent = False
3.8.2 seperate_list: リストを複数のサブリストに分解する
# 定义函数seperate_list,将一个列表分解成多个子列表,每个子列表中的元素在原列表中是连续的
def seperate_list(self, ls: List[int]) -> List[List[int]]:
# TODO: 增加是否属于同一文档的判断
lists = []
ls1 = [ls[0]]
for i in range(1, len(ls)):
if ls[i - 1] + 1 == ls[i]:
ls1.append(ls[i])
else:
lists.append(ls1)
ls1 = [ls[i]]
lists.append(ls1)
return lists
3.8.3 minimumity_search_with_score_by_vector、入力ベクトルに基づいて最も近い k 個のテキストを検索します。
類似性_search_with_score_by_vector 関数は、ベクトルによる類似性検索を実行するために使用され、指定された埋め込みベクトルに最も類似したテキストと対応するスコアを返します。
# 定义函数similarity_search_with_score_by_vector,根据输入的向量,查找最接近的k个文本
def similarity_search_with_score_by_vector(
self, embedding: List[float], k: int = 4
) -> List[Document]:
# 调用dependable_faiss_import函数,导入faiss库
faiss = dependable_faiss_import()
# 将输入的列表转换为numpy数组,并设置数据类型为float32
vector = np.array([embedding], dtype=np.float32)
# 如果需要进行L2归一化,则调用faiss.normalize_L2函数进行归一化
if self._normalize_L2:
faiss.normalize_L2(vector)
# 调用faiss库的search函数,查找与输入向量最接近的k个向量,并返回他们的分数和索引
scores, indices = self.index.search(vector, k)
# 初始化一个空列表,用于存储找到的文本
docs = []
# 初始化一个空集合,用于存储文本的id
id_set = set()
# 获取文本库中文本的数量
store_len = len(self.index_to_docstore_id)
# 初始化一个布尔变量,表示是否需要重新排列id列表
rearrange_id_list = False
# 遍历找到的索引和分数
for j, i in enumerate(indices[0]):
# 如果索引为-1,或者分数小于阈值,则跳过这个索引
if i == -1 or 0 < self.score_threshold < scores[0][j]:
# This happens when not enough docs are returned.
continue
# 如果索引存在于index_to_docstore_id字典中,则获取对应的文本id
if i in self.index_to_docstore_id:
_id = self.index_to_docstore_id[i]
# 如果索引不存在于index_to_docstore_id字典中,则跳过这个索引
else:
continue
# 从文本库中搜索对应id的文本
doc = self.docstore.search(_id)
# 如果不需要拆分块内容,或者文档的元数据中没有context_expand字段,或者context_expand字段的值为false,则执行以下代码
if (not self.chunk_conent) or ("context_expand" in doc.metadata and not doc.metadata["context_expand"]):
# 匹配出的文本如果不需要扩展上下文则执行如下代码
# 如果搜索到的文本不是Document类型,则抛出异常
if not isinstance(doc, Document):
raise ValueError(f"Could not find document for id {_id}, got {doc}")
# 在文本的元数据中添加score字段,其值为找到的分数
doc.metadata["score"] = int(scores[0][j])
# 将文本添加到docs列表中
docs.append(doc)
continue
# 将文本id添加到id_set集合中
id_set.add(i)
# 获取文本的长度
docs_len = len(doc.page_content)
# 遍历范围在1到i和store_len - i之间的数字k
for k in range(1, max(i, store_len - i)):
# 初始化一个布尔变量,表示是否需要跳出循环
break_flag = False
# 如果文本的元数据中有context_expand_method字段,并且其值为"forward",则扩展范围设置为[i + k]
if "context_expand_method" in doc.metadata and doc.metadata["context_expand_method"] == "forward":
expand_range = [i + k]
# 如果文本的元数据中有context_expand_method字段,并且其值为"backward",则扩展范围设置为[i - k]
elif "context_expand_method" in doc.metadata and doc.metadata["context_expand_method"] == "backward":
expand_range = [i - k]
# 如果文本的元数据中没有context_expand_method字段,或者context_expand_method字段的值不是"forward"也不是"backward",则扩展范围设置为[i + k, i - k]
else:
expand_range = [i + k, i - k]
# 遍历扩展范围
for l in expand_range:
# 如果l不在id_set集合中,并且l在0到len(self.index_to_docstore_id)之间,则执行以下代码
if l not in id_set and 0 <= l < len(self.index_to_docstore_id):
# 获取l对应的文本id
_id0 = self.index_to_docstore_id[l]
# 从文本库中搜索对应id的文本
doc0 = self.docstore.search(_id0)
# 如果文本长度加上新文档的长度大于块大小,或者新文本的源不等于当前文本的源,则设置break_flag为true,跳出循环
if docs_len + len(doc0.page_content) > self.chunk_size or doc0.metadata["source"] != \
doc.metadata["source"]:
break_flag = True
break
# 如果新文本的源等于当前文本的源,则将新文本的长度添加到文本长度上,将l添加到id_set集合中,设置rearrange_id_list为true
elif doc0.metadata["source"] == doc.metadata["source"]:
docs_len += len(doc0.page_content)
id_set.add(l)
rearrange_id_list = True
# 如果break_flag为true,则跳出循环
if break_flag:
break
# 如果不需要拆分块内容,或者不需要重新排列id列表,则返回docs列表
if (not self.chunk_conent) or (not rearrange_id_list):
return docs
# 如果id_set集合的长度为0,并且分数阈值大于0,则返回空列表
if len(id_set) == 0 and self.score_threshold > 0:
return []
# 对id_set集合中的元素进行排序,并转换为列表
id_list = sorted(list(id_set))
# 调用seperate_list函数,将id_list分解成多个子列表
id_lists = self.seperate_list(id_list)
# 遍历id_lists中的每一个id序列
for id_seq in id_lists:
# 遍历id序列中的每一个id
for id in id_seq:
# 如果id等于id序列的第一个元素,则从文档库中搜索对应id的文本,并深度拷贝这个文本
if id == id_seq[0]:
_id = self.index_to_docstore_id[id]
# doc = self.docstore.search(_id)
doc = copy.deepcopy(self.docstore.search(_id))
# 如果id不等于id序列的第一个元素,则从文本库中搜索对应id的文档,将新文本的内容添加到当前文本的内容后面
else:
_id0 = self.index_to_docstore_id[id]
doc0 = self.docstore.search(_id0)
doc.page_content += " " + doc0.page_content
# 如果搜索到的文本不是Document类型,则抛出异常
if not isinstance(doc, Document):
raise ValueError(f"Could not find document for id {_id}, got {doc}")
# 计算文本的分数,分数等于id序列中的每一个id在分数列表中对应的分数的最小值
doc_score = min([scores[0][id] for id in [indices[0].tolist().index(i) for i in id_seq if i in indices[0]]])
# 在文本的元数据中添加score字段,其值为文档的分数
doc.metadata["score"] = int(doc_score)
# 将文本添加到docs列表中
docs.append(doc)
# 返回docs列表
return docs
3.8.4 delete_doc メソッド: テキスト ライブラリ内の指定されたソースからテキストを削除します。
#定义了一个名为 delete_doc 的方法,这个方法用于删除文本库中指定来源的文本
def delete_doc(self, source: str or List[str]):
# 使用 try-except 结构捕获可能出现的异常
try:
# 如果 source 是字符串类型
if isinstance(source, str):
# 找出文本库中所有来源等于 source 的文本的id
ids = [k for k, v in self.docstore._dict.items() if v.metadata["source"] == source]
# 获取向量存储的路径
vs_path = os.path.join(os.path.split(os.path.split(source)[0])[0], "vector_store")
# 如果 source 是列表类型
else:
# 找出文本库中所有来源在 source 列表中的文本的id
ids = [k for k, v in self.docstore._dict.items() if v.metadata["source"] in source]
# 获取向量存储的路径
vs_path = os.path.join(os.path.split(os.path.split(source[0])[0])[0], "vector_store")
# 如果没有找到要删除的文本,返回失败信息
if len(ids) == 0:
return f"docs delete fail"
# 如果找到了要删除的文本
else:
# 遍历所有要删除的文本id
for id in ids:
# 获取该id在索引中的位置
index = list(self.index_to_docstore_id.keys())[list(self.index_to_docstore_id.values()).index(id)]
# 从索引中删除该id
self.index_to_docstore_id.pop(index)
# 从文本库中删除该id对应的文本
self.docstore._dict.pop(id)
# TODO: 从 self.index 中删除对应id,这是一个未完成的任务
# self.index.reset()
# 保存当前状态到本地
self.save_local(vs_path)
# 返回删除成功的信息
return f"docs delete success"
# 捕获异常
except Exception as e:
# 打印异常信息
print(e)
# 返回删除失败的信息
return f"docs delete fail"
3.8.5 update_doc と lists_doc
# 定义了一个名为 update_doc 的方法,这个方法用于更新文档库中的文档
def update_doc(self, source, new_docs):
# 使用 try-except 结构捕获可能出现的异常
try:
# 删除旧的文档
delete_len = self.delete_doc(source)
# 添加新的文档
ls = self.add_documents(new_docs)
# 返回更新成功的信息
return f"docs update success"
# 捕获异常
except Exception as e:
# 打印异常信息
print(e)
# 返回更新失败的信息
return f"docs update fail"
# 定义了一个名为 list_docs 的方法,这个方法用于列出文档库中所有文档的来源
def list_docs(self):
# 遍历文档库中的所有文档,取出每个文档的来源,转换为集合,再转换为列表,最后返回这个列表
return list(set(v.metadata["source"] for v in self.docstore._dict.values()))
他のクラスでお会いしましょう: LLM と langchain/ナレッジ グラフ/データベースの間の実践的な戦闘 (7 月) [問題解決、実用性が重要]
その4: 9月23日のLangchain-Chachatアップグレード版のソースコード分析
9 月 23 日、元のプロジェクト LangChain + ChatGLM-6B がアップグレードされ、現在のLangchain-Chachatプロジェクトになりました。
主な更新内容は、サーバー フォルダーの追加です。
- チャット ( __init__.py
を含む)
chat.pyKnowledge_base_chat.py
openai_chat.py search_engine_chat.py utils.py - db (
モデルを含む)
__init__.py
base.pyKnowledge_base_model.py
(つまり、後述のセクション 4.2.1 で説明する KnowledgeBaseModel の実装)
Knowledge_file_model .py (つまり、以下のセクション 4.2.2 で説明する KnowledgeFile の実装)
リポジトリ
__init__.pyKnowledge_base_repository.py
(下記のセクション 4.3.1 で説明する add_kb_to_db の実装に関係します)
Knowledge_file_repository.py (以下のセクション 4.3.2 で説明する add_flie_to_db/add_docs_to_db の実装に関係します)
kb_cacheを含むKnowledge_base
Base.py (以下のセクション 4.1.2 で説明する KBServiceFactory の実装を含む)
faiss_cache.py
kb_service
__init__.py
Base .py
default_kb_service.py
faiss_kb_service.py
milvus_kb_service.py
pg_kb_service.py
__init__.py
kb_api.py
kb_doc_api.py (これは、以下のセクション 4.1.1 で説明するように、マルチドキュメント Q&A のコア ロジックを実装します
)- モデルワーカー
- 静的
均等に分割されたフォルダー
4.1 サーバー/knowledge_base: バッチ ドキュメントに基づくエンタープライズ ナレッジ ベース Q&A
プロジェクトの最新バージョンでは、次のようなバッチドキュメントベースの質問と回答が実装されています。
# 开始遍历自定义的文档集合(docs)
for file_name, v in docs.items():
try:
# 对于v中的每个条目,检查它是否已经是Document类型
# 如果不是,那么将其转换为Document对象
v = [x if isinstance(x, Document) else Document(**x) for x in v]
# 根据文件名和知识库名称创建KnowledgeFile对象
kb_file = KnowledgeFile(filename=file_name, knowledge_base_name=knowledge_base_name)
# 在知识库中更新该文件的文档
kb.update_doc(kb_file, docs=v, not_refresh_vs_cache=True)
# ...
4.1.1 ナレッジベース /kb_doc_api.py
以下は、プロジェクト ファイルの 1 行ごとの分析です: Langchain-Chachat/server/knowledge_base /kb_doc_api.py
-
インポートモジュール:
os
およびなどのいくつかの標準ライブラリは、それぞれオペレーティング システムの操作と URL 解析のためにインポートされますurllib
。fastapi
API を構築するための最新の高速 Web フレームワークである関連モジュールをインポートします。- ナレッジベースのモデル構成やツール機能などのカスタムモジュールと構成をインポートします。
-
DocumentWithScore クラス:
Document
クラスを継承してフィールドを追加するサブクラスですscore
。これはおそらく、ドキュメントの検索時に検索関連性スコアを返すデータ構造です。
-
search_docs 関数:
- この機能は、ナレッジ ベース内のドキュメントを検索するために使用されます。
- クエリ文字列、ナレッジベース名、返されるドキュメントの最大数、およびスコアしきい値をパラメータとして受け入れます。
KBServiceFactory
対応するナレッジ ベース サービスを取得し、ナレッジ ベース内のドキュメントを検索するために使用します。- クエリに関連する検索されたドキュメントのリストを、それぞれに関連性スコアを付けて返します。
# 定义一个用于搜索文档的函数 def search_docs(query: str = Body(..., description="用户输入", examples=["你好"]), knowledge_base_name: str = Body(..., description="知识库名称", examples=["samples"]), top_k: int = Body(VECTOR_SEARCH_TOP_K, description="匹配向量数"), score_threshold: float = Body(SCORE_THRESHOLD, description="知识库匹配相关度阈值,取值范围在0-1之间,SCORE越小,相关度越高,取到1相当于不筛选,建议设置在0.5左右", ge=0, le=1), ) -> List[DocumentWithScore]: # 根据知识库名称获取相应的知识库服务实例 kb = KBServiceFactory.get_service_by_name(knowledge_base_name) # 如果没有找到对应的知识库服务实例,返回空列表 if kb is None: return [] # 调用知识库服务的search_docs方法来搜索与查询字符串匹配的文档 docs = kb.search_docs(query, top_k, score_threshold) # 将搜索到的文档转换为DocumentWithScore对象,包括文档内容和匹配分数 data = [DocumentWithScore(**x[0].dict(), score=x[1]) for x in docs] # 返回带分数的匹配文档列表 return data
要約すると、この search_docs 関数は、指定されたナレッジ ベースでクエリ文字列に一致するドキュメントを検索し、最も関連性の高い上位 k 個のドキュメントとその一致スコアを返します。
-
list_files 関数:
- 指定されたナレッジ ベース内のすべてのファイルを一覧表示するために使用されます。
- まずナレッジ ベース名を確認し、それを使用して
KBServiceFactory
対応するナレッジ ベース サービスを取得します。 - ナレッジ ベースが存在する場合は、ナレッジ ベース内のすべてのドキュメント名が返されます。
-
_save_files_in_thread 関数:
- これは、アップロードされたファイルをバックグラウンド スレッドの指定されたナレッジ ベースに保存するために使用されるプライベート関数です。
save_file
単一のファイルを保存するための関数を内部で定義します。- ファイルが既に存在し、ユーザーが上書きを選択しなかった場合は、ファイル サイズがチェックされ、ファイルが既に存在することを示すメッセージが返されます。
- それ以外の場合は、ファイルの内容を書き込みます。
- この関数の最後に、
run_in_thread_pool
メソッドを使用してマルチスレッド環境でファイル保存操作を実行し、各ファイルの保存結果を返します。
-
upload_docs
関数- 目的: ドキュメントをアップロードし、選択的なベクトル化を実行します。
- 入力:
- 一連のファイル、ナレッジベース名、ファイル処理パラメータなど。
- 主な機能:
- ナレッジベース名の有効性を確認してください。
- アップロードされたファイルをディスクに保存します。
- 保存したファイルをベクトル化します。
- 出力: 操作が成功したかどうかを示す基本的な応答と、失敗したファイルのリスト
。upload_docs 関数も以下で定義されています。違いは、ここでの Upload_docs はクラス KnowledgeBase で定義されており、
クラス内で self.kb_service.upload_docs(docs) を呼び出すことです。これは、ある程度まで、アップロード タスクを kb_service に渡して完了させるプロキシ メソッドであることを意味します。これは、
KnowledgeBase クラスのコンテキストで簡素化されたアップロード インターフェイスを提供することと同等です。これはおそらく、何らかのコンテキストまたは構成で kb_service を使用してドキュメントをアップロードするためのものであり
、以下の 2 番目の Upload_docs 関数はおそらくドキュメントのアップロードとベクトル化タスクを実際に処理する関数です。実際のアプリケーション シナリオでは、KnowledgeBase クラスのメソッドが以下の 2 番目の Upload_docs 関数を呼び出して、実際のアップロード操作を実行することがあります。
-
delete_docs
関数- 目的: 指定されたファイルをナレッジ ベースから削除します。
- 入力:
- ナレッジベース名、削除するファイル名のリスト、およびその他の処理パラメータ。
- 主な機能:
- ナレッジベース名の有効性を確認してください。
- 指定されたファイルをナレッジ ベースから削除します。
- 出力: 操作が成功したかどうかを示す基本応答と、失敗したファイルのリスト。
-
update_docs
関数- 目的: ナレッジ ベース内のドキュメントを更新します。
- 入力:
- ナレッジベース名、更新されるファイル名のリスト、およびその他の処理パラメータ。
- 主な機能:
- ファイルからドキュメントを生成し、ベクトル化する
- ナレッジ ベース内の指定されたファイルを更新します。つまり、ファイルをナレッジ ベースのファイル リストに追加します。
- 出力: 操作が成功したかどうかを示す基本応答と失敗したファイルのリスト
KBServiceFactory と KnowledgeFile については、それぞれ「4.1.2 KBServiceFactory の実装:Knowledge_base/kb_service/base.py」と「4.2.2 KnowledgeFile の実装」のセクションで紹介します。def update_docs( knowledge_base_name: str = Body(..., description="知识库名称", examples=["samples"]), file_names: List[str] = Body(..., description="文件名称,支持多文件", examples=["file_name"]), chunk_size: int = Body(CHUNK_SIZE, description="知识库中单段文本最大长度"), chunk_overlap: int = Body(OVERLAP_SIZE, description="知识库中相邻文本重合长度"), zh_title_enhance: bool = Body(ZH_TITLE_ENHANCE, description="是否开启中文标题加强"), override_custom_docs: bool = Body(False, description="是否覆盖之前自定义的docs"), docs: Json = Body({}, description="自定义的docs", examples=[{"test.txt": [Document(page_content="custom doc")]}]), not_refresh_vs_cache: bool = Body(False, description="暂不保存向量库(用于FAISS)") ) -> BaseResponse: ''' 更新知识库文档 ''' # 验证知识库名称 if not validate_kb_name(knowledge_base_name): return BaseResponse(code=403, msg="Don't attack me") # 获取知识库服务 kb = KBServiceFactory.get_service_by_name(knowledge_base_name) if kb is None: return BaseResponse(code=404, msg=f"未找到知识库 {knowledge_base_name}") failed_files = {} kb_files = [] # 生成需要加载docs的文件列表 for file_name in file_names: # 获取文件详情 file_detail = get_file_detail(kb_name=knowledge_base_name, filename=file_name) # 如果该文件之前使用了自定义docs,则根据参数决定略过或覆盖 if file_detail.get("custom_docs") and not override_custom_docs: continue if file_name not in docs: try: # 将文件名和知识库名组合为一个KnowledgeFile对象,并添加到kb_files列表中 kb_files.append(KnowledgeFile(filename=file_name, knowledge_base_name=knowledge_base_name)) except Exception as e: # 记录失败的文件和错误信息 msg = f"加载文档 {file_name} 时出错:{e}" logger.error(f'{e.__class__.__name__}: {msg}', exc_info=e if log_verbose else None) failed_files[file_name] = msg # 从文件生成docs,并进行向量化 for status, result in files2docs_in_thread(kb_files, chunk_size=chunk_size, chunk_overlap=chunk_overlap, zh_title_enhance=zh_title_enhance): if status: # 成功处理文件后,更新知识库中的文档 kb_name, file_name, new_docs = result kb_file = KnowledgeFile(filename=file_name, knowledge_base_name=knowledge_base_name) kb_file.splited_docs = new_docs kb.update_doc(kb_file, not_refresh_vs_cache=True) else: # 记录失败的文件和错误信息 kb_name, file_name, error = result failed_files[file_name] = error # 将自定义的docs进行向量化 for file_name, v in docs.items(): try: # 对于v中的每个条目,检查它是否已经是Document类型 # 如果不是,那么将其转换为Document对象 v = [x if isinstance(x, Document) else Document(**x) for x in v] # 根据文件名和知识库名称创建KnowledgeFile对象 kb_file = KnowledgeFile(filename=file_name, knowledge_base_name=knowledge_base_name) # 在知识库中更新该文件的文档 kb.update_doc(kb_file, docs=v, not_refresh_vs_cache=True) except Exception as e: # 当遇到异常时,构建一个错误消息并使用logger进行记录 msg = f"为 {file_name} 添加自定义docs时出错:{e}" logger.error(f'{e.__class__.__name__}: {msg}', exc_info=e if log_verbose else None) failed_files[file_name] = msg # 如果需要刷新向量库,则进行保存 if not not_refresh_vs_cache: kb.save_vector_store() # 返回响应,包括失败文件列表 return BaseResponse(code=200, msg=f"更新文档完成", data={"failed_files": failed_files})
-
download_doc
関数- 目的: ナレッジ ベースから指定されたドキュメントをダウンロードします。
- 入力:
- ナレッジベース名、ダウンロードするファイル名、その他のパラメーター。
- 主な機能:
- ファイルのプレビューまたはダウンロード機能を提供します。
- 出力: ユーザーがファイルをダウンロードまたはプレビューできるようにするファイル応答。
-
recreate_vector_store
関数- 目的: コンテンツからベクター ストレージを再構築します。
- 入力:
- ナレッジベース名、ベクトルストレージタイプ、およびその他の処理パラメータ。
- 主な機能:
- 既存のベクター ストレージをクリアします。
- フォルダー内のファイルを一覧表示します。
- ドキュメントはファイルごとに生成され、ナレッジ ベースに追加されます。
- 出力: 操作の進行状況に関するリアルタイム更新のストリーミング応答
全体として、このコードは主に、ナレッジ ベース ドキュメントの CRUD 操作 (作成、読み取り、更新、削除) と関連するベクトル化処理を提供します。
4.1.2 KBServiceFactoryの実装: Knowledge_base/kb_service/base.py
-
インポートモジュール:
- OS、オペレーターなどの基本的な Python ライブラリ。
- numpy、sklearn などのデータ操作とベクトル化のためのライブラリ。
- langchain.embeddings.base、langchain.docstore.document などのプロジェクト内のモジュール。
-
サポートされるVSTypeクラス:
- これは、サポートされるベクトル記憶域タイプを定義する単純なクラスです。例: FAISS、MILVUS など。
-
KBService クラス:
- これは、ナレッジ ベース サービスの基本的な機能と動作を定義する抽象基本クラスです。
- 初期化関数: ナレッジ ベース名と組み込みモデル名を指定して初期化します。
create_kb (ナレッジベースの作成)などの一連のメソッドを提供します。
add_kb_to_db は、以下の「4.3.1Knowledge_base_repository.py: add_kb_to_db の実装」で分析されます# 创建知识库方法 def create_kb(self): # 检查doc_path路径是否存在 if not os.path.exists(self.doc_path): # 如果不存在,创建该目录 os.makedirs(self.doc_path) # 调用子类中定义的do_create_kb方法来执行具体的知识库创建过程 self.do_create_kb() # 将新的知识库添加到数据库中,并返回操作状态 status = add_kb_to_db(self.kb_name, self.vs_type(), self.embed_model) # 返回操作状态 return status
。
このうち、add_file_to_db については、後述の「4.3.2Knowledge_file_repository.py: add_file_to_db/add_docs_to_db の実装」で解析します。# 向知识库添加文件方法 def add_doc(self, kb_file: KnowledgeFile, docs: List[Document] = [], **kwargs): # 判断docs列表是否有内容 if docs: # 设置一个标志,表示这是自定义文档列表 custom_docs = True # 遍历传入的文档列表 for doc in docs: # 为每个文档的metadata设置默认的"source"属性,值为文件的路径 doc.metadata.setdefault("source", kb_file.filepath) else: # 如果没有提供docs,从kb_file中读取文档内容 docs = kb_file.file2text() # 设置一个标志,表示这不是自定义文档列表 custom_docs = False # 如果docs列表有内容 if docs: # 删除与kb_file相关联的现有文档 self.delete_doc(kb_file) # 调用子类中定义的do_add_doc方法来执行具体的文档添加过程,并返回文档信息 doc_infos = self.do_add_doc(docs, **kwargs) # 将新的文档信息添加到数据库中,并返回操作状态 status = add_file_to_db(kb_file, custom_docs=custom_docs, docs_count=len(docs), doc_infos=doc_infos) else: # 如果docs列表为空,则设置操作状态为False status = False # 返回操作状态 return status
- do_create_kb、do_search などの抽象メソッドもいくつかあります。サブクラスはこれらのメソッドを実装する必要があります。
-
KBServiceFactory クラス:
- これは、提供されたベクトル ストレージ タイプに基づいて、対応するナレッジ ベース サービス インスタンスを返すファクトリ クラスです。
- 静的メソッドを使用してさまざまなナレッジ ベース サービス インスタンスを取得する
# 知识库服务工厂类 class KBServiceFactory: # 根据向量存储类型返回相应的知识库服务实例 @staticmethod def get_instance(vs_type: str, knowledge_base_name: str) -> KBService: if vs_type == SupportedVSType.FAISS: from server.knowledge_base.kb_faiss import KBServiceFaiss return KBServiceFaiss(knowledge_base_name) elif vs_type == SupportedVSType.MILVUS: from server.knowledge_base.kb_milvus import KBServiceMilvus return KBServiceMilvus(knowledge_base_name) else: raise ValueError(f"Unsupported VS type: {vs_type}")
-
get_kb_details 関数:
- カタログとデータベース内のすべてのナレッジ ベースの詳細を取得し、それらを結合します
-
get_kb_file_details 関数:
- 指定されたナレッジ ベースのディレクトリおよびデータベース内のすべてのファイルの詳細を取得し、情報をマージします。
-
EmbeddingsFunAdapter クラス:
- これは、Embeddings クラスに追加機能を追加するために使用されるアダプター クラスです。
- embed_documents メソッドと embed_query メソッドは、入力テキストを埋め込み、結果を正規化します。
- aembed_documents と aembed_query はそれらの非同期バージョンです。
-
スコア_しきい値_プロセス関数:
- これは、スコアが特定のしきい値を下回る場合にドキュメントをフィルタリングし、上位 k 個のドキュメントを返す単純な関数です。
4.2 server/db/models フォルダーの更新
4.2.1 KnowledgeBaseModelの実装
server/db/models/knowledge_base_model.py に実装
from sqlalchemy import Column, Integer, String, DateTime, func
from server.db.base import Base
class KnowledgeBaseModel(Base):
"""
知识库模型
"""
__tablename__ = 'knowledge_base'
id = Column(Integer, primary_key=True, autoincrement=True, comment='知识库ID')
kb_name = Column(String(50), comment='知识库名称')
vs_type = Column(String(50), comment='向量库类型')
embed_model = Column(String(50), comment='嵌入模型名称')
file_count = Column(Integer, default=0, comment='文件数量')
create_time = Column(DateTime, default=func.now(), comment='创建时间')
def __repr__(self):
return f"<KnowledgeBase(id='{self.id}', kb_name='{self.kb_name}', vs_type='{self.vs_type}', embed_model='{self.embed_model}', file_count='{self.file_count}', create_time='{self.create_time}')>"
4.2.2 KnowledgeFileの実装
慎重に検索した結果、 KnowledgeFile は、server/db/models/knowledge_file_model.pyプロジェクト ファイルに実装されていることがわかりました。
# 导入sqlalchemy所需的模块和函数
from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, JSON, func
# 从server.db.base导入Base类,这通常用于ORM的基础模型
from server.db.base import Base
# 定义KnowledgeFileModel类,用于映射“知识文件”数据模型
class KnowledgeFileModel(Base):
"""
知识文件模型
"""
__tablename__ = 'knowledge_file'
id = Column(Integer, primary_key=True, autoincrement=True, comment='知识文件ID')
file_name = Column(String(255), comment='文件名')
file_ext = Column(String(10), comment='文件扩展名')
kb_name = Column(String(50), comment='所属知识库名称')
document_loader_name = Column(String(50), comment='文档加载器名称')
text_splitter_name = Column(String(50), comment='文本分割器名称')
file_version = Column(Integer, default=1, comment='文件版本')
file_mtime = Column(Float, default=0.0, comment="文件修改时间")
file_size = Column(Integer, default=0, comment="文件大小")
custom_docs = Column(Boolean, default=False, comment="是否自定义docs")
docs_count = Column(Integer, default=0, comment="切分文档数量")
create_time = Column(DateTime, default=func.now(), comment='创建时间')
# 定义对象的字符串表示形式
def __repr__(self):
return f"<KnowledgeFile(id='{self.id}', file_name='{self.file_name}',
file_ext='{self.file_ext}', kb_name='{self.kb_name}',
document_loader_name='{self.document_loader_name}',
text_splitter_name='{self.text_splitter_name}',
file_version='{self.file_version}', create_time='{self.create_time}')>"
# 定义FileDocModel类,用于映射“文件-向量库文档”数据模型
class FileDocModel(Base):
"""
文件-向量库文档模型
"""
# 定义表名为'file_doc'
__tablename__ = 'file_doc'
# 定义id字段为主键,并设置自动递增,并且附加注释
id = Column(Integer, primary_key=True, autoincrement=True, comment='ID')
# 定义知识库名称字段,并附加注释
kb_name = Column(String(50), comment='知识库名称')
# 定义文件名称字段,并附加注释
file_name = Column(String(255), comment='文件名称')
# 定义向量库文档ID字段,并附加注释
doc_id = Column(String(50), comment="向量库文档ID")
# 定义元数据字段,默认为一个空字典
meta_data = Column(JSON, default={})
# 定义对象的字符串表示形式
def __repr__(self):
return f"<FileDoc(id='{self.id}', kb_name='{self.kb_name}', file_name='{self.file_name}', doc_id='{self.doc_id}', metadata='{self.metadata}')>"
4.3 サーバー/データベース/リポジトリ
4.3.1Knowledge_base_repository.py: add_kb_to_db を実装する
def add_kb_to_db(session, kb_name, vs_type, embed_model):
# 查询指定名称的知识库是否存在于数据库中
kb = session.query(KnowledgeBaseModel).filter_by(kb_name=kb_name).first()
# 如果指定的知识库不存在,则创建一个新的知识库实例
if not kb:
kb = KnowledgeBaseModel(kb_name=kb_name, vs_type=vs_type, embed_model=embed_model)
# 将新的知识库实例添加到session,这样可以在之后提交到数据库
session.add(kb)
else: # 如果知识库已经存在,则更新它的vs_type和embed_model
kb.vs_type = vs_type
kb.embed_model = embed_model
# 返回True,表示操作成功完成
return True
KnowledgeBaseModelメソッドについては、上記「4.2.1 KnowledgeBaseModelの実装」で解析しました。
4.3.2Knowledge_file_repository.py:实现add_file_to_db/add_docs_to_db
- add_file_to_db はデータベースにファイルを追加し、最後に add_docs_to_db を呼び出します。
- add_docs_to_db、データベースにドキュメントを追加します
# 定义向数据库添加文件的函数
def add_file_to_db(session, # 数据库会话对象
kb_file: KnowledgeFile, # 知识文件对象
docs_count: int = 0, # 文档数量,默认为0
custom_docs: bool = False, # 是否为自定义文档,默认为False
doc_infos: List[str] = [], # 文档信息列表,默认为空。形式为:[{"id": str, "metadata": dict}, ...]
):
# 从数据库中查询与知识库名相匹配的知识库记录
kb = session.query(KnowledgeBaseModel).filter_by(kb_name=kb_file.kb_name).first()
# 如果该知识库存在
if kb:
# 查询与文件名和知识库名相匹配的文件记录
existing_file: KnowledgeFileModel = (session.query(KnowledgeFileModel)
.filter_by(file_name=kb_file.filename,
kb_name=kb_file.kb_name)
.first())
# 获取文件的修改时间
mtime = kb_file.get_mtime()
# 获取文件的大小
size = kb_file.get_size()
# 如果该文件已存在
if existing_file:
# 更新文件的修改时间
existing_file.file_mtime = mtime
# 更新文件的大小
existing_file.file_size = size
# 更新文档数量
existing_file.docs_count = docs_count
# 更新自定义文档标志
existing_file.custom_docs = custom_docs
# 文件版本号自增
existing_file.file_version += 1
# 如果文件不存在
else:
# 创建一个新的文件记录对象
new_file = KnowledgeFileModel(
file_name=kb_file.filename,
file_ext=kb_file.ext,
kb_name=kb_file.kb_name,
document_loader_name=kb_file.document_loader_name,
text_splitter_name=kb_file.text_splitter_name or "SpacyTextSplitter",
file_mtime=mtime,
file_size=size,
docs_count = docs_count,
custom_docs=custom_docs,
)
# 知识库的文件计数增加
kb.file_count += 1
# 将新文件添加到数据库会话中
session.add(new_file)
# 添加文档到数据库
add_docs_to_db(kb_name=kb_file.kb_name, file_name=kb_file.filename, doc_infos=doc_infos)
# 返回True表示操作成功
return True
上記のコードの最後から 2 行目を見ると、add_file_to_db が最後に add_docs_to_db を呼び出してデータベースにドキュメントを追加していることがわかります。
def add_docs_to_db(session,
kb_name: str,
file_name: str,
doc_infos: List[Dict]):
'''
将某知识库某文件对应的所有Document信息添加到数据库
doc_infos形式:[{"id": str, "metadata": dict}, ...]
'''
for d in doc_infos:
obj = FileDocModel(
kb_name=kb_name,
file_name=file_name,
doc_id=d["id"],
meta_data=d["metadata"],
)
session.add(obj)
return True
// 更新予定..
参考文献と推奨書籍
- langchain 公式サイト: LangChain、https://python.langchain.com/、API リスト: https://api.python.langchain.com/en/latest/api_reference.html
langchain 中国語サイト(翻訳はまだうまくありません) - ラングチェーン パノラマ
- 1 つの記事で langchain を理解する (この記事だけを読むだけでは十分ではないため、このタイトルは無視してください)
- LangChain を使用して 10 分でスマート チャットボットを構築する方法
- FAISS に関するいくつかのチュートリアル: Faiss の導入とアプリケーションの体験記録、
- QLoRA: 4 ビット レベルの定量化 + LoRA メソッド、3090 を使用して DB-GPT 上の 33B LLM に基づいて個人の知識ベースを作成
- LangChain+LLM に基づいて強化された QA を構築する、LangChain を使用して大規模な言語モデル アプリケーションを構築する、LangChain とは
- 7月にLLMとlangchain/ナレッジグラフ/データベースの実践的な戦い [問題解決、実用性が重要]
追記
この記事は 3 つの段階を経ました
- langchainには多くのコンポーネントがあります
。完全に理解したい場合は、段階的に理解する必要があります。最初に
このライブラリを見始めたとき、私は本当に混乱して始められませんでした。10 日後には、ファイルを 1 つずつ直接クリックすることもできます。つまり
、すべてがプロセスです。 - langchain-ChatGLM プロジェクトのソースコードの解釈
正直、プロジェクトファイルがたくさんあって最初はかなり混乱しましたが、幸いなことに 1 週間ほどで整理できました。 - 新しいものを書き始めます: パート 4: 9 月 23 日の Langchain-Chachat のアップグレード バージョンのソース コード分析
レコードの作成、変更、最適化
- 7月5日から7月9日まで毎日1部ずつ書いていきます
- 7.10、langchain とは何かについての導入の最初の部分を改善
- 7.11、langchain-ChatGLM プロジェクトの最新アップデートに従って、書かれた内容を整理
- 7.12 最初の 3.8 セクションを書き、プロジェクトの進行に合わせて各フォルダーの解釈順序を調整するなど、
ほぼ 1 週間に相当する時間をかけて、ようやく「langchain-ChatGLM の全体的なコード構造」を整理しました。 - 7.15 は、langchain アーキテクチャに関連する内容を補足するもので、理解を容易にするために、langchain ライブラリ全体が 3 つの主要な層 (基本層、機能層、およびアプリケーション層) に分割されています。
- 7.17、セクション 4.2: ナレッジ グラフを使用して LLM の事前トレーニング、推論、および解釈可能性を強化することに焦点を当てた 4 番目の部分の作成を開始します。
- 7.26、4 番目の部分を継続し、5 番目の部分の更新を開始します: LLM とデータベースの組み合わせ
- 9.15 に、元の 4 番目と 5 番目の部分が新しい記事に抽出されました:ナレッジ グラフの人気入門: KG とは何かから LLM と KG/DB の実際的な組み合わせまで
- 9.16、新しいものを書き始めます: パート 4: 9 月 23 日の Langchain-Chachat のアップグレード バージョンのソース コード分析