在本教程中,我们重点介绍如何使用 Dewy 和 LangChain.js 构建问答 CLI 工具。 Dewy 是一个开源知识库,可帮助开发人员高效地组织和检索信息。 LangChain.js 是一个简化大型语言模型 (LLM) 与应用程序集成的框架。通过将 Dewy 的知识管理功能与 LangChain.js 的 LLM 集成相结合,您可以创建用精确且相关的信息回答复杂查询的工具。
本指南将引导您设置环境、将文档加载到 Dewy 中,以及通过 LangChain.js 使用 LLM 来回答基于存储数据的问题。它专为希望通过高级问答功能增强其项目的工程师而设计。
Dewy是一个 OSS 知识库,旨在简化开发人员存储、组织和检索信息的方式。它的灵活性和易用性使其成为旨在构建知识驱动应用程序的开发人员的绝佳选择。
另一方面, LangChain.js是一个功能强大的框架,使开发人员能够将 LLM 无缝集成到他们的应用程序中。通过将 Dewy 的结构化知识管理与 LangChain.js 的 LLM 功能相结合,开发人员可以创建复杂的问答系统,该系统可以理解和处理复杂的查询,提供精确且与上下文相关的答案。
我们的目标是构建一个简单但功能强大的问答 CLI 脚本。该脚本将允许用户将文档加载到 Dewy 知识库中,然后通过 LangChain.js 使用 LLM 根据 Dewy 中存储的信息回答问题。本教程将指导您完成从设置环境到实施 CLI 脚本的整个过程。
您将学习如何使用 LangChain 构建一个简单的问答应用程序,以及如何将 Dewy 集成为知识源,让您的应用程序根据您提供的特定文档来回答问题。
在深入学习本教程之前,请确保您满足以下先决条件:
如果您想继续前进,可以在 Dewy 存储库中找到此示例的最终代码。
首先,为 TypeScript CLI 项目创建一个目录并切换到该目录
mkdir dewy_qa cd dewy_qa
设置目录后,您可以安装 TypeScript 并初始化项目:
npm init -y npm i typescript --save-dev npx tsc --init
根据您的环境,您可能需要对 TypeScript 配置进行一些更改。确保您的tsconfig.json
类似于以下内容:
{ "compilerOptions": { "target": "ES6", "module": "CommonJS", "moduleResolution": "node", "declaration": true, "outDir": "./dist", "esModuleInterop": true, "strict": true, }
现在您已准备好创建 CLI 应用程序。为了防止代码变得太混乱,将其组织到几个目录中,布局如下
dewy_qa/ ├── commands/ │ └── ... ├── utils/ │ └── ... ├── index.ts ├── package.json └── tsconfig.ts
每个命令都会在commands
目录中实现,共享代码将在utils
目录中。 CLI 应用程序的入口点是文件index.ts
。
从简单的“hello world”版本的index.ts
开始 - 您将在下一节中开始填写它
#!/usr/bin/env ts-node-script console.log("hello world");
要验证环境设置是否正确,请尝试运行以下命令 - 您应该看到控制台中打印出“hello world”:
npx ts-node index.ts
让我们在package.json
中为该命令创建一个条目,而不是每次都输入这个很长的命令。这将帮助我们记住如何调用 CLI,并使其更容易作为命令安装:
{ ... "bin": { "dewy_qa": "./index.ts" } ... }
现在您可以使用npm exec dewy_qa
运行脚本或npm link
包并将其作为dewy_qa
运行
通过设置 Dewy 客户端加载文档。第一步是向项目添加一些依赖项。第一个是dewy-ts
,Dewy 的客户端库。第二个是commander
,它将帮助我们构建一个具有参数解析、子命令等功能的 CLI 应用程序。最后, chalk
使提示更加丰富多彩。
npm install dewy-ts commander chalk
接下来,实现加载命令的逻辑。您将在名为commands/load.ts
的单独文件中执行此操作。该文件实现了一个名为load
的函数,它需要一个 URL 和一些附加选项 - 这将在后面的部分中与 CLI 连接起来。
Dewy 使文档加载变得超级简单 - 只需设置客户端并使用您要加载的文件的 URL 调用addDocument
即可。 Dewy 负责提取 PDF 的内容,将它们分割成大小合适的块,以便发送给法学硕士,并对它们建立索引以进行语义搜索。
import { Dewy } from 'dewy-ts'; import { success, error } from '../utils/colors'; export async function load(url: string, options: { collection: string, dewy_endpoint: string }): Promise<void> { console.log(success(`Loading ${url} into collection: ${options.collection}`)); try { const dewy = new Dewy({ BASE: options.dewy_endpoint }) const result = await dewy.kb.addDocument({ collection: options.collection, url }); console.log(success(`File loaded successfully`)); console.log(JSON.stringify(result, null, 2)); } catch (err: any) { console.error(error(`Failed to load file: ${err.message}`)); } }
您可能已经注意到一些函数是从../utils/colors
导入的。该文件只是设置了一些用于为控制台输出着色的帮助程序 - 将其放入utils
中,以便可以在其他地方使用:
import chalk from 'chalk'; export const success = (message: string) => chalk.green(message); export const info = (message: string) => chalk.blue(message); export const error = (message: string) => chalk.red(message);
有了将文档加载到 Dewy 中的能力,是时候集成 LangChain.js 以利用 LLM 来回答问题了。此步骤涉及设置 LangChain.js 来查询 Dewy 知识库并使用 LLM 处理结果以生成答案。
首先,安装一些额外的包 - langchain
和openai
以使用 OpenAI API 作为 LLM:
npm install dewy-langchain langchain @langchain/openai openai
该命令有点长,因此我们将先介绍其中的几个部分,然后最后将它们组合起来
首先要设置的是 Dewy(和以前一样)和法学硕士。与之前的一个区别是dewy
用于构建DewyRetriever
:这是 LangChain 用于检索作为链的一部分的信息的特殊类型。您很快就会看到如何使用检索器。
const model = new ChatOpenAI({ openAIApiKey: options.openai_api_key, }); const dewy = new Dewy({ BASE: options.dewy_endpoint }) const retriever = new DewyRetriever({ dewy, collection });
这是一个字符串模板,指示 LLM 的行为方式,并带有用于创建“链”时提供的附加上下文的占位符。在这种情况下,法学硕士被指示回答问题,但仅使用其提供的信息。这减少了模型“产生幻觉”的倾向,或者提出看似合理但错误的答案。 context
和question
的值在下一步中提供:
const prompt = PromptTemplate.fromTemplate(`Answer the question based only on the following context: {context} Question: {question}`);
LangChain 的工作原理是建立控制如何查询 LLM 和其他数据源的行为“链”。本示例使用LCEL ,它比 LangChain 的一些原始接口提供了更灵活的编程体验。
使用RunnableSequence
创建 LCEL 链。该链描述了如何生成context
和question
值:上下文是使用之前创建的检索器生成的,问题是通过传递步骤的输入生成的。 Dewy 检索的结果通过管道传输到formatDocumentsAsString
函数来格式化为字符串。
该链执行以下操作:
DewyRetriever
检索文档,并将它们分配给context
,并将链的输入值分配给question
。context
和question
变量来格式化提示字符串。 const chain = RunnableSequence.from([ { context: retriever.pipe(formatDocumentsAsString), question: new RunnablePassthrough(), }, prompt, model, new StringOutputParser(), ]);
现在链已经构建完毕,执行它并将结果输出到控制台。正如您将看到的, question
是函数调用者提供的输入参数。
使用chain.streamLog()
执行链可以让您看到从 LLM 返回的每个响应块。流处理程序循环有点丑陋,但它只是过滤到适当的流结果并将它们写入STDOUT
(使用console.log
会在每个块后添加换行符)。
const stream = await chain.streamLog(question); // Write chunks of the response to STDOUT as they're received console.log("Answer:"); for await (const chunk of stream) { if (chunk.ops?.length > 0 && chunk.ops[0].op === "add") { const addOp = chunk.ops[0]; if ( addOp.path.startsWith("/logs/ChatOpenAI") && typeof addOp.value === "string" && addOp.value.length ) { process.stdout.write(addOp.value); } } }
现在您已经了解了所有内容,可以准备创建query
命令了。这应该与之前的load
命令类似,但有一些额外的导入。
import { StringOutputParser } from "@langchain/core/output_parsers"; import { PromptTemplate } from "@langchain/core/prompts"; import { formatDocumentsAsString } from "langchain/util/document"; import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables"; import { ChatOpenAI } from "@langchain/openai"; import { Dewy } from 'dewy-ts'; import { DewyRetriever } from 'dewy-langchain'; import { success, error } from '../utils/colors'; export async function query(question: string, options: { collection: string, dewy_endpoint: string, openai_api_key: string }): Promise<void> { console.log(success(`Querying ${options.collection} collection for: "${question}"`)); try { const model = new ChatOpenAI({ openAIApiKey: options.openai_api_key, }); const dewy = new Dewy({ BASE: options.dewy_endpoint }) const retriever = new DewyRetriever({ dewy, collection: options.collection }); const prompt = PromptTemplate.fromTemplate(`Answer the question based only on the following context: {context} Question: {question}`); const chain = RunnableSequence.from([ { context: retriever.pipe(formatDocumentsAsString), question: new RunnablePassthrough(), }, prompt, model, new StringOutputParser(), ]); const stream = await chain.streamLog(question); // Write chunks of the response to STDOUT as they're received console.log("Answer:"); for await (const chunk of stream) { if (chunk.ops?.length > 0 && chunk.ops[0].op === "add") { const addOp = chunk.ops[0]; if ( addOp.path.startsWith("/logs/ChatOpenAI") && typeof addOp.value === "string" && addOp.value.length ) { process.stdout.write(addOp.value); } } } } catch (err: any) { console.error(error(`Failed to query: ${err.message}`)); } }
集成 Dewy 和 LangChain.js 后,下一步是构建 CLI 界面。使用像commander
这样的库创建一个用户友好的命令行界面,该界面支持将文档加载到 Dewy 中并使用 LangChain.js 查询知识库的命令。
首先,重写index.ts
以创建子命令load
和query
。 --collection
参数确定文档应加载到哪个 Dewy 集合(Dewy 允许您将文档组织到不同的集合中,类似于文件夹)。 --dewy-endpoint
参数允许您指定如何连接到 Dewy - 默认情况下假定实例在端口8000
上本地运行。最后, --openai_api_key
参数(默认为环境变量)配置 OpenAI API:
#!/usr/bin/env ts-node-script import { Command } from 'commander'; import { load } from './commands/load'; import { query } from './commands/query'; const program = new Command(); program.name('dewy-qa').description('CLI tool for interacting with a knowledge base API').version('1.0.0'); const defaultOpenAIKey = process.env.OPENAI_API_KEY; program .command('load') .description("Load documents into Dewy from a URL") .option('--collection <collection>', 'Specify the collection name', 'main') .option('--dewy-endpoint <endpoint>', 'Specify the collection name', 'http://localhost:8000') .argument('<url>', 'URL to load into the knowledge base') .action(load); program .command('query') .description('Ask questions using an LLM and the loaded documents for answers') .option('--collection <collection>', 'Specify the collection name', 'main') .option('--dewy-endpoint <endpoint>', 'Specify the collection name', 'http://localhost:8000') .option('--openai-api-key <key>', 'Specify the collection name', defaultOpenAIKey) .argument('<question>', 'Question to ask the knowledge base') .action(query); program.parse(process.argv);
好的,一切都完成了 - 是不是很容易?您可以通过运行以下命令来尝试一下:
dewy_qa load https://arxiv.org/pdf/2009.08553.pdf
你应该看到类似的东西
Loading https://arxiv.org/pdf/2009.08553.pdf into collection: main File loaded successfully { "id": 18, "collection": "main", "extracted_text": null, "url": "https://arxiv.org/pdf/2009.08553.pdf", "ingest_state": "pending", "ingest_error": null }
提取大型 PDF 的内容可能需要一两分钟,因此当您首次加载新文档时,您经常会看到"ingest_state": "pending"
。
接下来,尝试问一些问题:
dewy_qa query "tell me about RAG
你应该看到类似的东西
Querying main collection for: "tell me about RAG" Answer: Based on the given context, RAG refers to the RAG proteins, which are involved in DNA binding and V(D)J recombination. The RAG1 and RAG2 proteins work together to bind specific DNA sequences known as RSS (recombination signal sequences) and facilitate the cutting and rearrangement of DNA segments during the process of V(D)J recombination...
通过遵循本指南,您已经了解了如何创建一个 CLI,该 CLI 使用 Dewy 来管理知识,并使用 LangChain.js 来处理问题并生成答案。该工具演示了将结构化知识库与法学硕士的分析能力相结合的实际应用,使开发人员能够构建更智能、响应更灵敏的应用程序。