LangChain的完整JavaScript指南 — CodesCode

了解LangChain的基本组成部分-代理、模型、块和链-以及如何利用JavaScript发挥LangChain的力量

“`html

在本全面指南中,我们将深入探讨LangChain的基本组件,并演示如何在JavaScript中利用其强大功能。

LangChainJS是一个多功能的JavaScript框架,赋予开发者和研究人员创建、实验和分析语言模型和代理的能力。它提供了丰富的自然语言处理(NLP)功能,从构建自定义模型到高效操作文本数据。作为JavaScript框架,它还允许开发者轻松将其AI应用集成到Web应用程序中。

先决条件

为了跟随本文,创建一个新文件夹并安装LangChain npm包:

npm install -S langchain

创建一个新的JS模块文件,使用.mjs后缀(例如test1.mjs)。

代理

在LangChain中,代理是能够理解和生成文本的实体。这些代理可以配置具有特定行为和数据源,并经过培训以执行各种与语言相关的任务,使它们成为广泛应用的通用工具。

创建LangChain代理

代理可以配置使用“工具”来收集所需的数据并制定出一个良好的响应。请看下面的示例。它使用Serp API(一个互联网搜索API)在互联网上搜索与问题或输入相关的信息,并使用该信息进行响应。它还使用llm-math工具执行数学运算,例如单位转换或查找两个值之间的百分比变化:

import { initializeAgentExecutorWithOptions } from "langchain/agents";import { ChatOpenAI } from "langchain/chat_models/openai";import { SerpAPI } from "langchain/tools";import { Calculator } from "langchain/tools/calculator";process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"process.env["SERPAPI_API_KEY"] = "YOUR_SERPAPI_KEY"const tools = [new Calculator(), new SerpAPI()];const model = new ChatOpenAI({ modelName: "gpt-3.5-turbo", temperature: 0 });const executor = await initializeAgentExecutorWithOptions(tools, model, {  agentType: "openai-functions",  verbose: false,});const result = await executor.run("通过搜索互联网,找出Boldy James自2010年以来发布了多少张专辑,以及Nas自2010年以来发布了多少张专辑?找出更多专辑的人并显示百分比差异。");console.log(result);

通过使用modelName: "gpt-3.5-turbo"temperature: 0创建model变量后,我们创建了executor,它将所创建的model与指定的工具(SerpAPI和Calculator)组合在一起。在输入中,我要求LLM搜索互联网(使用SerpAPI)并找出自2010年以来哪位艺术家发布了更多专辑 —— Nas还是Boldy James —— 并显示百分比差异(使用Calculator)。

在这个例子中,我必须明确告诉LLM“通过搜索互联网…”,以便它使用互联网获取数据,而不是使用OpenAI默认仅限于2021年的数据。

“`

这是输出的样子:

> node test1.mjsBoldy James自2010年以来发布了4张专辑。 Nas自2010年以来已经发布了17张专辑。因此,Nas发布的专辑比Boldy James更多。专辑数量的差异是13。要计算百分比差异,我们可以使用公式:(差异/总数)* 100。在这种情况下,差异是13,总数是17。百分比差异是:(13/17)* 100 = 76.47%。因此,自2010年以来,Nas发布的专辑比Boldy James多76.47%。

模型

LangChain有三种类型的模型:LLM、聊天模型和文本嵌入模型。让我们用一些示例来探索每种类型的模型。

语言模型

LangChain提供了一种在JavaScript中使用语言模型根据文本输入生成文本输出的方法。它没有聊天模型那么复杂,并且最适合用于简单的输入-输出语言任务。这是一个使用OpenAI的示例:

import { OpenAI } from "langchain/llms/openai";const llm = new OpenAI({  openAIApiKey: "YOUR_OPENAI_KEY",  model: "gpt-3.5-turbo",  temperature: 0});const res = await llm.call("List all red berries");console.log(res);

如你所见,它使用gpt-3.5-turbo模型列出了所有红色浆果。在这个示例中,我将温度设置为0,以使LLM的结果更符合事实。输出:

1. Strawberries2. Cranberries3. Raspberries4. Redcurrants5. Red Gooseberries6. Red Elderberries7. Red Huckleberries8. Red Mulberries

聊天模型

如果你想要更复杂的答案和对话,你需要使用聊天模型。聊天模型在技术上与语言模型有什么不同?用LangChain文档的话说:

聊天模型是语言模型的一种变体。虽然聊天模型在内部使用语言模型,但它们使用的接口有所不同。它们不是使用“文本输入,文本输出”API,而是使用“聊天消息”作为输入和输出的接口。

这是一个简单(无用但有趣)的JavaScript聊天模型脚本:

import { ChatOpenAI } from "langchain/chat_models/openai";import { PromptTemplate } from "langchain/prompts";const chat = new ChatOpenAI({  openAIApiKey: "YOUR_OPENAI_KEY",  model: "gpt-3.5-turbo",  temperature: 0});const prompt = PromptTemplate.fromTemplate(`You are a poetic assistant that always answers in rhymes: {question}`);const runnable = prompt.pipe(chat);const response = await runnable.invoke({ question: "Who is better, Djokovic, Federer or Nadal?" });console.log(response);

如你所见,代码首先发送系统消息,告诉聊天机器人要成为一个总是用押韵方式回答的诗意助手,然后发送人类消息告诉聊天机器人告诉我谁是更好的网球运动员:Djokovic、Federer还是Nadal。如果你运行这个聊天机器人模型,你会看到像这样的结果:

AIMessage.content:'在网球领域,他们都闪耀夺目,' +'Djokovic、Federer和Nadal,壮丽的风光。' +'每个人都有自己独特的风格和技巧,' +'在选择最好的时候是很困惑的。' +'\n' +'Djokovic,塞尔维亚人,精确而果断,' +'灵活机敏,凭决心比赛。' +'他强大的击球和无情的进攻,' +"使他成为一个难以对抗的力量。" +'\n' +'Federer,瑞士大师,真正的艺术家,' +'他的比赛优雅而精湛。' +'他流畅的技巧和神奇的触感,' +'让观众惊叹,非常令人难以置信。' +'\n' +'Nadal,西班牙人,在红土上的战士,' +'他的坚定决心让对手望而却步。' +'凭借他无尽的力量和永不放弃的战斗,' +'他全力征服球场。' +'\n' +"那么,谁更好呢?这是个品味的问题," +"每位选手的伟大都无法抹去。" +"最终,是我们共同对游戏的热爱," +'使他们都成为了无可比拟的冠军。'

挺酷的!

嵌入模型

嵌入模型提供了一种将文本中的单词和数字转化为向量的方法,然后可以将其与其他单词或数字相关联。这听起来有点抽象,我们来看一个例子:

import { OpenAIEmbeddings } from "langchain/embeddings/openai";
process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"
const embeddings = new OpenAIEmbeddings();
const res = await embeddings.embedQuery("谁创造了互联网?");
console.log(res)

这会返回一长串浮点数:

[  0.02274114,  -0.012759142,   0.004794503,  -0.009431809,    0.01085313,  0.0019698727,  -0.013649924,   0.014933698, -0.0038185727,  -0.025400387,  0.010794181,   0.018680222,   0.020042595,   0.004303263,   0.019937797,  0.011226473,   0.009268062,   0.016125774,  0.0116391145, -0.0061765253,  -0.0073358514, 0.00021696436,   0.004896026,  0.0034026562,  -0.018365828,  ... 还有1501个项目]

这就是嵌入模型的样子。六个单词就有这么多浮点数!

然后,这个嵌入模型可以用于将输入文本与潜在答案、相关文本、名称等关联起来。

现在让我们来看一个嵌入模型的用例…

现在,这是一个脚本,将使用嵌入模型在提供的可能答案列表中找到与问题“最重的动物是什么?”匹配的正确答案:

import { OpenAIEmbeddings } from "langchain/embeddings/openai";
process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"
const embeddings = new OpenAIEmbeddings();
function cosinesim(A, B) {
    var dotproduct = 0;
    var mA = 0;
    var mB = 0;
    for(var i = 0; i < A.length; i++) {
        dotproduct += A[i] * B[i];
        mA += A[i] * A[i];
        mB += B[i] * B[i];
    }
    mA = Math.sqrt(mA);
    mB = Math.sqrt(mB);
    var similarity = dotproduct / (mA * mB);
    return similarity;
}
const res1 = await embeddings.embedQuery("蓝鲸是世界上最重的动物");
const res2 = await embeddings.embedQuery("乔治·奥威尔写了1984");
const res3 = await embeddings.embedQuery("随机东西");
const text_arr = ["蓝鲸是世界上最重的动物", "乔治·奥威尔写了1984", "随机东西"]
const res_arr = [res1, res2, res3]
const question = await embeddings.embedQuery("最重的动物是什么?");
const sims = []
for (var i=0;i<res_arr.length;i++){
    sims.push(cosinesim(question, res_arr[i]))
}
Array.prototype.max = function() {
    return Math.max.apply(null, this);
};
console.log(text_arr[sims.indexOf(sims.max())])

这段代码使用了 cosinesim(A, B) 函数来找到每个答案与问题的相关性。通过使用 Array.prototype.max 函数找到与问题相关性数组中的最大值,然后通过找到 text_arr 中与最相关答案对应的文本,代码能够找到正确答案:text_arr[sims.indexOf(sims.max())]

输出:

蓝鲸是世界上最重的动物

分块

LangChain 模型无法处理大文本并对其进行响应。这就是分块文本分割的作用。让我向你展示两种简单的方法,在将文本数据输入 LangChain 之前将其分成块。

按字符分割块

为了避免块中的突然断裂,您可以通过在每次出现换行符的位置分割段落来分割文本:

import { Document } from "langchain/document";
import { CharacterTextSplitter } from "langchain/text_splitter";
const splitter = new CharacterTextSplitter({
  separator: "\n",
  chunkSize: 7,
  chunkOverlap: 3,
});
const output = await splitter.createDocuments([your_text]);

这是一种有用的文本分割方法。不过,您可以使用任何字符作为块分隔符,而不仅仅是\n

递归分割块

如果您想严格按照一定长度的字符分割文本,可以使用RecursiveCharacterTextSplitter

import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 100,
  chunkOverlap: 15,
});
const output = await splitter.createDocuments([your_text]);

在此示例中,文本每100个字符分割一次,块之间有15个字符的重叠。

块大小和重叠

通过观察这些示例,您可能已经开始想知道块大小和重叠参数的确切含义以及对性能的影响。让我简单地用两点来解释。

  • 块大小决定了每个块中的字符数量。块大小越大,块中的数据越多,LangChain处理它并生成输出所需的时间就越长,反之亦然。

  • 块重叠是为了在块之间共享信息,以便它们分享一些上下文。块重叠越高,您的块越冗余;块重叠越低,块之间共享的上下文就越少。通常,良好的块重叠值在块大小的10%到20%之间,尽管理想的块重叠在不同的文本类型和用例之间有所不同。

基本上是多个LLM功能链接在一起,执行更复杂的任务,这些任务不能通过简单的LLM的输入-输出方式完成。让我们看一个很酷的例子:

import { ChatPromptTemplate } from "langchain/prompts";
import { LLMChain } from "langchain/chains";
import { ChatOpenAI } from "langchain/chat_models/openai";
process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"
const wiki_text = `Alexander Stanislavovich 'Sasha' Bublik (Александр Станиславович Бублик; born 17 June 1997) is a Kazakhstani professional tennis player. He has been ranked as high as world No. 25 in singles by the Association of Tennis Professionals (ATP), which he achieved in July 2023, and is the current Kazakhstani No. 1 player...Alexander Stanislavovich Bublik was born on 17 June 1997 in Gatchina, Russia and began playing tennis at the age of four. He was coached by his father, Stanislav. On the junior tour, Bublik reached a career-high ranking of No. 19 and won eleven titles (six singles and five doubles) on the International Tennis Federation (ITF) junior circuit.[4][5]...`
const chat = new ChatOpenAI({ temperature: 0 });
const chatPrompt = ChatPromptTemplate.fromMessages([
  [
    "system",
    "You are a helpful assistant that {action} the provided text",
  ],
  ["human", "{text}"],
]);
const chainB = new LLMChain({ prompt: chatPrompt, llm: chat });
const resB = await chainB.call({
  action: "lists all important numbers from",
  text: wiki_text,
});
console.log({ resB });

该代码将一个变量传入其提示中,并生成一个事实上正确的答案(temperature: 0)。在这个例子中,我要求LLM从我最喜欢的网球运动员的简短维基百科传记中列出所有重要数字。

以下是此代码的输出:

{
  resB: {
    text: 'Important numbers from the provided text:\n' +
      '\n' +
      "- Alexander Stanislavovich 'Sasha' Bublik's date of birth: 17 June 1997\n" +
      "- Bublik's highest singles ranking: world No. 25\n" +
      "- Bublik's highest doubles ranking: world No. 47\n" +
      "- Bublik's career ATP Tour singles titles: 3\n" +
      "- Bublik's career ATP Tour singles runner-up finishes: 6\n" +
      "- Bublik's height: 1.96 m (6 ft 5 in)\n" +
      "- Bublik's number of aces served in the 2021 ATP Tour season: unknown\n" +
      "- Bublik's junior tour ranking: No. 19\n" +
      "- Bublik's junior tour titles: 11 (6 singles and 5 doubles)\n" +
      "- Bublik's previous citizenship: Russia\n" +
      "- Bublik's current citizenship: Kazakhstan\n" +
      "- Bublik's role in the Levitov Chess Wizards team: reserve member"
  }
}

“`html

相当令人印象深刻,但这并不能完全展示链式操作的威力。让我们看一个更实际的例子:

import { z } from "zod";import { zodToJsonSchema } from "zod-to-json-schema";import { ChatOpenAI } from "langchain/chat_models/openai";import {  ChatPromptTemplate,  SystemMessagePromptTemplate,  HumanMessagePromptTemplate,} from "langchain/prompts";import { JsonOutputFunctionsParser } from "langchain/output_parsers";process.env["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"const zodSchema = z.object({  albums: z    .array(      z.object({        name: z.string().describe("专辑名称"),        artist: z.string().describe("制作专辑的艺术家"),        length: z.number().describe("专辑长度(分钟)"),        genre: z.string().optional().describe("专辑流派"),      })    )    .describe("文本中提到的音乐专辑的数组"),});const prompt = new ChatPromptTemplate({  promptMessages: [    SystemMessagePromptTemplate.fromTemplate(      "列出以下文本中提到的所有音乐专辑。"    ),    HumanMessagePromptTemplate.fromTemplate("{inputText}"),  ],  inputVariables: ["inputText"],});const llm = new ChatOpenAI({ modelName: "gpt-3.5-turbo", temperature: 0 });const functionCallingModel = llm.bind({  functions: [    {      name: "output_formatter",      description: "始终用于正确格式化输出",      parameters: zodToJsonSchema(zodSchema),    },  ],  function_call: { name: "output_formatter" },});const outputParser = new JsonOutputFunctionsParser();const chain = prompt.pipe(functionCallingModel).pipe(outputParser);const response = await chain.invoke({  inputText: "我最喜欢的专辑包括:2001、To Pimp a Butterfly和Led Zeppelin IV",});console.log(JSON.stringify(response, null, 2));

这段代码读取一个输入文本,识别出所有提到的音乐专辑,识别出每个专辑的名称、艺术家、长度和流派,最后将所有数据转换为JSON格式。给定输入文本“我最喜欢的专辑包括:2001、To Pimp a Butterfly和Led Zeppelin IV”,以下是输出结果:

{  "albums": [    {      "name": "2001",      "artist": "Dr. Dre",      "length": 68,      "genre": "嘻哈"    },    {      "name": "To Pimp a Butterfly",      "artist": "Kendrick Lamar",      "length": 79,      "genre": "嘻哈"    },    {      "name": "Led Zeppelin IV",      "artist": "Led Zeppelin",      "length": 42,      "genre": "摇滚"    }  ]}

这只是一个有趣的例子,但该技术可以用于为无结构的文本数据构建结构,适用于无数其他应用。

超越OpenAI

虽然我一直以OpenAI模型作为展示LangChain不同功能的示例,但它并不限于OpenAI模型。您可以将LangChain与许多其他LLMs和AI服务一起使用。您可以在其文档中找到LangChain和JavaScript可集成的LLMs的完整列表。

例如,您可以在LangChain中使用Cohere。安装Cohere后,使用npm install cohere-ai,您可以像这样使用LangChain和Cohere创建一个简单的问题→答案代码:

import { Cohere } from "langchain/llms/cohere";const model = new Cohere({  maxTokens: 50,  apiKey: "YOUR_COHERE_KEY", // 在Node.js中默认为process.env.COHERE_API_KEY});const res = await model.call(  "为Nas的新专辑想一个名字");console.log({ res });

输出:

{  res: '以下是几个可能适合Nas的新专辑的名称:\n' +    '\n' +    "- 国王的登陆\n" +    "- 上帝之子:续集\n" +    "- 街道门徒\n" +    '- 伊兹自由\n' +    '- Nas和伊尔马蒂克的节奏\n' +    '\n' +    '你喜欢哪个'}

“`

结论

在这篇指南中,你看到了使用JavaScript的LangChain的不同方面和功能。你可以使用JavaScript中的LangChain轻松开发AI驱动的Web应用程序并尝试LLMs。确保参考LangChainJS文档以获取有关特定功能的更多详细信息。

愉快编码和尝试JavaScript中的LangChain!如果你喜欢这篇文章,你可能还想阅读关于如何在Python中使用LangChain的文章。

分享本文


Leave a Reply

Your email address will not be published. Required fields are marked *