LangChain

LangChain 是一个以 LLM(大语言模型)模型为核心的开发框架,LangChain 的主要特性:

围绕以上设计原则,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 语言模型:

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 提供了许多内置的文档转换器,可以轻松地拆分、组合、过滤和以其他方式操作文档。

其中:

一个简单文本分割示例:

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 中提供了非常多的向量存储方案,以下指南可帮助您为您的用例选择正确的向量存储:

示例:读取本地文档,创建 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 还提供了他类型的检索器,比如:

针对不同的需求场景,可能需要对应的合适的检索器。以下是一个根据通过计算相似度分值检索的示例:

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:

其中:

以下是一个从【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

作者:京东科技 牛志伟

来源:京东云开发者社区 转载请注明来源


↙↙↙阅读原文可查看相关链接,并与作者交流