TS版LangChain實戰:基於檔案的增強檢索(RAG)

2023-11-28 12:06:48

LangChain

LangChain是一個以 LLM (大語言模型)模型為核心的開發框架,LangChain的主要特性:

  • 可以連線多種資料來源,比如網頁連結、本地PDF檔案、向量資料庫等
  • 允許語言模型與其環境互動
  • 封裝了Model I/O(輸入/輸出)、Retrieval(檢索器)、Memory(記憶)、Agents(決策和排程)等核心元件
  • 可以使用鏈的方式組裝這些元件,以便最好地完成特定用例。

圍繞以上設計原則,LangChain解決了現在開發人工智慧應用的一些切實痛點。以 GPT 模型為例:

  1. 資料滯後,現在訓練的資料是到 2021年9月。
  2. token數量限制,如果讓它對一個300頁的pdf進行總結,直接使用則無能為力。
  3. 不能進行聯網,獲取不到最新的內容。
  4. 不能與其他資料來源連結。

另外作為一個膠水層框架,極大地提高了開發效率,它的作用可以類比於jquery在前端開發中的角色,使得開發者可以更專注於創新和優化產品功能。

1、Model I/O

LangChain提供了與任何語言模型互動的構建塊,互動的輸入輸出主要包括:Prompts、Language models、Output parsers三部分。

1.1 Prompts

LangChain 提供了多個類和函數,使構建和使用提示詞變得容易。Prompts模組主要包含了模板化、動態選擇和管理模型輸入兩部分。其中:

1.1.1 Prompt templates

提示模版類似於ES6模板字串,可以在字串中插入變數或表示式,接收來自終端使用者的一組引數並生成提示。

一個簡單的例子:

const multipleInputPrompt = new PromptTemplate({
  inputVariables: ["adjective", "content"],
  template: "Tell me a {adjective} joke about {content}.",
});
const formattedMultipleInputPrompt = await multipleInputPrompt.format({
  adjective: "funny",
  content: "chickens",
});
console.log(formattedMultipleInputPrompt);
// "Tell me a funny joke about chickens.




同時可以通過 PipelinePrompt將多個PromptTemplate提示模版進行組合,組合的優點是可以很方便的進行復用。比如常見的系統角色提示詞,一般都遵循以下結構:{introduction} {example} {start},比如一個【名人採訪】角色的提示詞:

使用PipelinePrompt組合實現:

import { PromptTemplate, PipelinePromptTemplate } from "langchain/prompts";

const fullPrompt = PromptTemplate.fromTemplate(`{introduction}

{example}

{start}`);

const introductionPrompt = PromptTemplate.fromTemplate(
  `You are impersonating {person}.`
);

const examplePrompt =
  PromptTemplate.fromTemplate(`Here's an example of an interaction:
Q: {example_q}
A: {example_a}`);

const startPrompt = PromptTemplate.fromTemplate(`Now, do this for real!
Q: {input}
A:`);

const composedPrompt = new PipelinePromptTemplate({
  pipelinePrompts: [
    {
      name: "introduction",
      prompt: introductionPrompt,
    },
    {
      name: "example",
      prompt: examplePrompt,
    },
    {
      name: "start",
      prompt: startPrompt,
    },
  ],
  finalPrompt: fullPrompt,
});

const formattedPrompt = await composedPrompt.format({
  person: "Elon Musk",
  example_q: `What's your favorite car?`,
  example_a: "Telsa",
  input: `What's your favorite social media site?`,
});

console.log(formattedPrompt);

/*
  You are impersonating Elon Musk.

  Here's an example of an interaction:
  Q: What's your favorite car?
  A: Telsa

  Now, do this for real!
  Q: What's your favorite social media site?
  A:
*/




1.1.2 Example selectors

為了大模型能夠給出相對精準的輸出內容,通常會在prompt中提供一些範例描述,如果包含大量範例會浪費token數量,甚至可能會超過最大token限制。為此,LangChain提供了範例選擇器,可以從使用者提供的大量範例中,選擇最合適的部分作為最終的prompt。通常有2種方式:按長度選擇和按相似度選擇。

按長度選擇:對於較長的輸入,它將選擇較少的範例來;而對於較短的輸入,它將選擇更多的範例。

...
// 定義長度選擇器
const exampleSelector = await LengthBasedExampleSelector.fromExamples(
    [
      { input: "happy", output: "sad" },
      { input: "tall", output: "short" },
      { input: "energetic", output: "lethargic" },
      { input: "sunny", output: "gloomy" },
      { input: "windy", output: "calm" },
    ],
    {
      examplePrompt,
      maxLength: 25,
    }
);
...
// 最終會根據使用者的輸入長度,來選擇合適的範例

// 使用者輸入較少,選擇所有範例
console.log(await dynamicPrompt.format({ adjective: "big" })); 
/*
   Give the antonym of every input

   Input: happy
   Output: sad

   Input: tall
   Output: short

   Input: energetic
   Output: lethargic

   Input: sunny
   Output: gloomy

   Input: windy
   Output: calm

   Input: big
   Output:
   */
// 使用者輸入較多,選擇其中一個範例
const longString =
    "big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else";
console.log(await dynamicPrompt.format({ adjective: longString }));
/*
   Give the antonym of every input

   Input: happy
   Output: sad

   Input: big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else
   Output:
   */




按相似度選擇:查詢與輸入具有最大餘弦相似度的嵌入範例

...
// 定義相似度選擇器
const exampleSelector = await SemanticSimilarityExampleSelector.fromExamples(
  [
    { input: "happy", output: "sad" },
    { input: "tall", output: "short" },
    { input: "energetic", output: "lethargic" },
    { input: "sunny", output: "gloomy" },
    { input: "windy", output: "calm" },
  ],
  new OpenAIEmbeddings(),
  HNSWLib,
  { k: 1 }
);
...
// 跟天氣類相關的範例
console.log(await dynamicPrompt.format({ adjective: "rainy" }));
/*
  Give the antonym of every input

  Input: sunny
  Output: gloomy

  Input: rainy
  Output:
*/
// 跟尺寸相關的範例
console.log(await dynamicPrompt.format({ adjective: "large" }));
/*
  Give the antonym of every input

  Input: tall
  Output: short

  Input: large
  Output:
*/




1.2 Language models

LangChain支援多種常見的Language models提供商(詳見附錄一),並提供了兩種型別的模型的介面和整合:

  • LLM:採用文字字串作為輸入並返回文字字串的模型
  • Chat models:由語言模型支援的模型,但將聊天訊息列表作為輸入並返回聊天訊息

定義一個LLM語言模型:

import { OpenAI } from "langchain/llms/openai";
// 範例化一個模型
const model = new OpenAI({ 
    // OpenAI內建引數
    openAIApiKey: "YOUR_KEY_HERE",
    modelName: "text-davinci-002", //gpt-4、gpt-3.5-turbo
    maxTokens: 25, 
    temperature: 1, //發散度
    // LangChain自定義引數
    maxRetries: 10, //發生錯誤後重試次數
    maxConcurrency: 5, //最大並行請求次數
    cache: true //開啟快取
});
// 使用模型
const res = await model.predict("Tell me a joke");




取消請求和超時處理:

import { OpenAI } from "langchain/llms/openai";

const model = new OpenAI({ temperature: 1 });
const controller = new AbortController();

const res = await model.call(
  "What would be a good name for a company that makes colorful socks?",
  { 
    signal: controller.signal, //呼叫controller.abort()即可取消請求
    timeout: 1000 //超時時間設定
  }
);




流式響應:通常,當我們請求一個服務或者介面時,伺服器會將所有資料一次性返回給我們,然後我們再進行處理。但是,如果返回的資料量很大,那麼我們需要等待很長時間才能開始處理資料。

而流式響應則不同,它將資料分成多個小塊,每次只返回一部分資料給我們。我們可以在接收到這部分資料之後就開始處理,而不需要等待所有資料都到達。

import { OpenAI } from "langchain/llms/openai";

const model = new OpenAI({
  maxTokens: 25,
});

const stream = await model.stream("Tell me a joke.");

for await (const chunk of stream) {
  console.log(chunk);
}

/*

Q
:
 What
 did
 the
 fish
 say
 when
 it
 hit
 the
 wall
?

A
:
 Dam
!
*/




此外,所有的語言模型都實現了Runnable 介面,預設實現了invoke,batch,stream,map等方法, 提供了對呼叫、流式傳輸、批次處理和對映請求的基本支援

1.3 Output parsers

語言模型可以輸出文字或富文字資訊,但很多時候,我們可能想要獲得結構化資訊,比如常見的JSON結構可以和應用程式更好的結合。LangChain封裝了一下幾種輸出解析器:

名稱 中文名 解釋
BytesOutputParser 位元組輸出 轉換為二進位制資料
CombiningOutputParser 組合輸出 組合不同的解析器
CustomListOutputParser 自定義列表輸出 指定分隔符並分割為陣列格式
JsonOutputFunctionsParser JSON函數輸出 結合OpenAI回撥函數格式化輸出
OutputFixingParser 錯誤修復 解析失敗時再次呼叫LLM以修復錯誤
StringOutputParser 字串輸出 轉換為字串
StructuredOutputParser 結構化輸出 通常結合Zod格式化為JSON物件

一個自定義列表的解析器案例:

...
const parser = new CustomListOutputParser({ length: 3, separator: "\n" });

const chain = RunnableSequence.from([
  PromptTemplate.fromTemplate(
    "Provide a list of {subject}.\n{format_instructions}"
  ),
  new OpenAI({ temperature: 0 }),
  parser,
]);

/* 最終生成的prompt
Provide a list of great fiction books (book, author).
Your response should be a list of 3 items separated by "\n" (eg: `foo\n bar\n baz`)
*/
const response = await chain.invoke({
  subject: "great fiction books (book, author)",
  format_instructions: parser.getFormatInstructions(),
});

console.log(response);
/*
[
  'The Catcher in the Rye, J.D. Salinger',
  'To Kill a Mockingbird, Harper Lee',
  'The Great Gatsby, F. Scott Fitzgerald'
]
*/




一個完整的Model I/O案例:將一個國家的資訊:名稱、首都、面積、人口等資訊結構化輸出

2、Retrieval

一些LLM應用通常需要特定的使用者資料,這些資料不屬於模型訓練集的一部分。可以通過檢索增強生成(RAG)的方式,檢索外部資料,然後在執行生成步驟時將其傳遞給 LLM 。LangChain 提供了 RAG 應用程式的所有構建模組,包含以下幾個關鍵模組:

2.1 Document loaders

Document loaders可以從各種資料來源載入檔案。LangChain 提供了許多不同的檔案載入器以及與對應的第三方整合工具。下圖中,黃色顏色代表Loaders對應的npm第三方依賴庫。

返回的檔案物件格式如下:

interface Document {
  pageContent: string;
  metadata: Record<string, any>;
}




2.2 Document transformers

載入檔案後,通常需要進行資料處理,比如:將長檔案分割成更小的塊、過濾不需要的HTML標籤以及結構化處理等。LangChain提供了許多內建的檔案轉換器,可以輕鬆地拆分、組合、過濾和以其他方式操作檔案。

其中:

  • RecursiveCharacterTextSplitter除了可以按指定分隔符進行分割外,還支援根據特定於語言的語法分割文字,比如:JavaScript、Python、Solidity 和 Rust 等流行語言,以及 Latex、HTML 和 Markdown。
  • 當處理大量任意檔案集合時,簡單的文字分割可能會出現重疊文字的檔案,CharacterTextSplitter可以用後設資料標記檔案,從而解決矛盾來源的資訊等問題。
  • 當提取 HTML 檔案以供以後檢索時,我們通常只對網頁的實際內容而不是語意感興趣。HtmlToTextTransformer和MozillaReadabilityTransformer都可以從檔案中剝離HTML標籤,從而使檢索更加有效
  • MetadataTagger轉換器可以自動從檔案中提取後設資料,以便以後進行更有針對性的相似性搜尋。

一個簡單文字分割範例:

import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";

const text = `Hi.\n\nI'm Harrison.\n\nHow? Are? You?\nOkay then f f f f.
This is a weird text to write, but gotta test the splittingggg some how.\n\n
Bye!\n\n-H.`;
const splitter = new RecursiveCharacterTextSplitter({
  separators: ["\n\n", "\n", " ", ""], //預設分隔符
  chunkSize: 1000, //最終檔案的最大大小(以字元數計),預設1000
  chunkOverlap: 200, //塊之間應該有多少重疊,預設200
});

const output = await splitter.createDocuments([text]);




2.3 Text embedding models

文字嵌入模型(Text embedding models)是用於建立文字資料的數值表示的模型。它可以將文字轉換為向量表示,從而在向量空間中進行語意搜尋和查詢相似文字。LangChain嵌入模型提供了標準介面,可以與多個Language models提供商(詳見附錄一)進行整合。

一個OpenAI的嵌入範例:通常要結合檔案(Document)和向量儲存(Vector stores)一起使用。

import { OpenAIEmbeddings } from "langchain/embeddings/openai";

/* Create instance */
const embeddings = new OpenAIEmbeddings();

/* Embed queries */
const res = await embeddings.embedQuery("Hello world");
/*
[
   -0.004845875,   0.004899438,  -0.016358767,  -0.024475135, -0.017341806,
    0.012571548,  -0.019156644,   0.009036391,  -0.010227379, -0.026945334,
    0.022861943,   0.010321903,  -0.023479493, -0.0066544134,  0.007977734,
  ... 1436 more items
]
*/

/* Embed documents */
const documentRes = await embeddings.embedDocuments(["Hello world", "Bye bye"]);
/*
[
  [
    -0.0047852774,  0.0048640342,   -0.01645707,  -0.024395779, -0.017263541,
      0.012512918,  -0.019191515,   0.009053908,  -0.010213212, -0.026890801,
      0.022883644,   0.010251015,  -0.023589306,  -0.006584088,  0.007989113,
    ... 1436 more items
  ],
  [
      -0.009446913,  -0.013253193,   0.013174579,  0.0057552797,  -0.038993083,
      0.0077763423,    -0.0260478, -0.0114384955, -0.0022683728,  -0.016509168,
      0.041797023,    0.01787183,    0.00552271, -0.0049789557,   0.018146982,
    ... 1436 more items
  ]
]
*/




2.4 Vector stores

Vector stores是用於儲存和搜尋嵌入式資料的一種技術,負責儲存嵌入資料並執行向量搜尋。它通過將文字或檔案轉換為嵌入向量,並在查詢時嵌入非結構化查詢,以檢索與查詢最相似的嵌入向量來實現。LangChain中提供了非常多的向量儲存方案,以下指南可幫助您為您的用例選擇正確的向量儲存:

  • 如果您正在尋找可以在 Node.js 應用程式內執行,無需任何其他伺服器來支援,那麼請選擇HNSWLibFaissLanceDBCloseVector
  • 如果您正在尋找可以在類似瀏覽器的環境記憶體中執行,那麼請選擇MemoryVectorStoreCloseVector
  • 如果您來自 Python 並且正在尋找類似於 FAISS 的東西,請嘗試HNSWLibFaiss
  • 如果您正在尋找可以在 Docker 容器中本地執行的開源全功能向量資料庫,那麼請選擇Chroma
  • 如果您正在尋找一個提供低延遲、本地檔案嵌入並支援邊緣應用程式的開源向量資料庫,那麼請選擇Zep
  • 如果您正在尋找可以在本地Docker 容器中執行或託管在雲中的開源生產就緒向量資料庫,那麼請選擇Weaviate
  • 如果您已經在使用 Supabase,那麼請檢視Supabase向量儲存。
  • 如果您正在尋找可用於生產的向量儲存,不需要自己託管,那麼就選擇Pinecone
  • 如果您已經在使用 SingleStore,或者您發現自己需要分散式高效能資料庫,那麼您可能需要考慮SingleStore向量儲存。
  • 如果您正在尋找線上 MPP(大規模並行處理)資料倉儲服務,您可能需要考慮AnalyticDB向量儲存。
  • 如果您正在尋找一個經濟高效的向量資料庫,允許使用 SQL 執行向量搜尋,那麼MyScale就是您的最佳選擇。
  • 如果您正在尋找可以從瀏覽器端和伺服器端載入的向量資料庫,請檢視CloseVector。它是一個旨在跨平臺的向量資料庫。

範例:讀取本地檔案,建立MemoryVectorStore和檢索

import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { TextLoader } from "langchain/document_loaders/fs/text";

// Create docs with a loader
const loader = new TextLoader("src/document_loaders/example_data/example.txt");
const docs = await loader.load();

// Load the docs into the vector store
const vectorStore = await MemoryVectorStore.fromDocuments(
  docs,
  new OpenAIEmbeddings()
);

// Search for the most similar document
const resultOne = await vectorStore.similaritySearch("hello world", 1);

console.log(resultOne);

/*
  [
    Document {
      pageContent: "Hello world",
      metadata: { id: 2 }
    }
  ]
*/




2.5 Retrievers

檢索器(Retriever)是一個介面:根據非結構化查詢返回檔案。它比Vector Store更通用,建立Vector Store後,將其用作檢索器的方法非常簡單:

...
retriever = vectorStore.asRetriever()




此外,LangChain還提供了他型別的檢索器,比如:

  • ContextualCompressionRetriever:用給定查詢的上下文來壓縮它們,以便只返回相關資訊,而不是立即按原樣返回檢索到的檔案,同時還可以減少token數量。
  • MultiQueryRetriever:從不同角度為給定的使用者輸入查詢生成多個查詢。
  • ParentDocumentRetriever:在檢索過程中,它首先獲取小塊,然後查詢這些塊的父 ID,並返回那些較大的檔案。
  • SelfQueryRetriever:一種能夠查詢自身的檢索器。
  • VespaRetriever:從Vespa.ai資料儲存中檢索檔案。

針對不同的需求場景,可能需要對應的合適的檢索器。以下是一個根據通過計算相似度分值檢索的範例:

import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { ScoreThresholdRetriever } from "langchain/retrievers/score_threshold";

const vectorStore = await MemoryVectorStore.fromTexts(
  [
    "Buildings are made out of brick",
    "Buildings are made out of wood",
    "Buildings are made out of stone",
    "Buildings are made out of atoms",
    "Buildings are made out of building materials",
    "Cars are made out of metal",
    "Cars are made out of plastic",
  ],
  [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }],
  new OpenAIEmbeddings()
);

const retriever = ScoreThresholdRetriever.fromVectorStore(vectorStore, {
  minSimilarityScore: 0.9, // Finds results with at least this similarity score
  maxK: 100, // The maximum K value to use. Use it based to your chunk size to make sure you don't run out of tokens
  kIncrement: 2, // How much to increase K by each time. It'll fetch N results, then N + kIncrement, then N + kIncrement * 2, etc.
});

const result = await retriever.getRelevantDocuments(
  "What are buildings made out of?"
);

console.log(result);

/*
  [
    Document {
      pageContent: 'Buildings are made out of building materials',
      metadata: { id: 5 }
    },
    Document {
      pageContent: 'Buildings are made out of wood',
      metadata: { id: 2 }
    },
    Document {
      pageContent: 'Buildings are made out of brick',
      metadata: { id: 1 }
    },
    Document {
      pageContent: 'Buildings are made out of stone',
      metadata: { id: 3 }
    },
    Document {
      pageContent: 'Buildings are made out of atoms',
      metadata: { id: 4 }
    }
  ]
*/




一個完整的Retrieval案例:從指定URL地址(靜態網站)中載入檔案資訊,進行分割生成嵌入資訊並儲存為向量,跟據使用者的問題進行檢索。(請使用公開資訊,防止隱私資料洩漏)

3、Chains

Chains是一種將多個元件組合在一起建立單一、連貫應用程式的方法。通過使用Chains,我們可以建立一個接受使用者輸入、使用PromptTemplate格式化輸入並將格式化的響應傳遞給LLM的鏈。我們可以通過將多個鏈組合在一起或將鏈與其他元件組合來構建更復雜的鏈。LangChain中內建了很多不同型別的Chain:

其中:

  • LLMChain:最基本的鏈。它採用提示模板,根據使用者輸入對其進行格式化,然後返回LLM的響應。
  • SimpleSequentialChain和SequentialChain:一個呼叫的輸出用作另一個呼叫的輸入,進行一系列呼叫。前者每個步驟都有一個單一的輸入/輸出,後者更通用,允許多個輸入/輸出。
  • loadQAStuffChain、loadQARefineChain、loadQAMapReduceChain、loadSummarizationChain和AnalyzeDocumentChain這些是處理檔案的核心鏈。它們對於總結檔案、回答檔案問題、從檔案中提取資訊等很有用。
  • APIChain:允許使用 LLM 與 API 互動以檢索相關資訊。通過提供與所提供的 API 檔案相關的問題來構建鏈。
  • createOpenAPIChain:可以僅根據 OpenAPI 規範自動選擇和呼叫 API。它將輸入 OpenAPI 規範解析為 OpenAI 函數 API 可以處理的 JSON 模式。
  • loadSummarizationChain:摘要鏈可用於彙總多個檔案,生成摘要。
  • createExtractionChainFromZod:從輸入文字和所需資訊的模式中提取結構化資訊。
  • MultiPromptChain:基於RouterChain,從多個prompt中選擇合適的一個,比如定義多個老師的提示。
  • MultiRetrievalQAChain:基於RouterChain,從多個檢索器中動態選擇。

以下是一個從【2020年美國國情諮文】中生成摘要的範例:

import { OpenAI } from "langchain/llms/openai";
import { loadSummarizationChain, AnalyzeDocumentChain } from "langchain/chains";
import * as fs from "fs";

// In this example, we use the `AnalyzeDocumentChain` to summarize a large text document.
const text = fs.readFileSync("state_of_the_union.txt", "utf8");
const model = new OpenAI({ temperature: 0 });
const combineDocsChain = loadSummarizationChain(model);
const chain = new AnalyzeDocumentChain({
  combineDocumentsChain: combineDocsChain,
});
const res = await chain.call({
  input_document: text,
});
console.log({ res });
/*
{
  res: {
    text: ' President Biden is taking action to protect Americans from the COVID-19 pandemic and Russian aggression, providing economic relief, investing in infrastructure, creating jobs, and fighting inflation.
    He is also proposing measures to reduce the cost of prescription drugs, protect voting rights, and reform the immigration system. The speaker is advocating for increased economic security, police reform, and the Equality Act, as well as providing support for veterans and military families.
    The US is making progress in the fight against COVID-19, and the speaker is encouraging Americans to come together and work towards a brighter future.'
  }
}
*/




4、GPTs

Open AI最新發佈會,釋出了GPTs相關的功能:使用者可以用自然語言的方式,來構建自己的GPT應用:簡單的比如一個根據提示詞生成的各種系統角色;或者通過自定義Action實現一些複雜的功能:比如呼叫第三方API、讀取本地或網路檔案等。在一定程度上可以不用通過LangChain等編碼來實現增強檢索等,但是LangChain的一些思路和實現還是值得學習和借鑑的,比如LangChain中可以使用在地化部署的LLM和向量儲存等,來解決隱私資料洩漏問題。

參考文獻:

https://js.langchain.com/docs/get_started/introduction

一文入門最熱的LLM應用開發框架LangChain

作者:京東科技 牛志偉

來源:京東雲開發者社群 轉載請註明來源