1. 简介
本文将探讨LangChain,这是一个用于开发基于语言模型应用程序的框架。我们将首先了解语言模型的基础知识,这有助于理解后续内容。
虽然LangChain主要提供Python和JavaScript/TypeScript版本,但我们也可以在Java中使用它。我们会讨论LangChain框架的核心构建模块,并通过Java代码示例进行实践。
2. 背景
在深入讨论为何需要构建基于语言模型应用的框架之前,我们需要先理解什么是语言模型,以及使用它们时可能遇到的典型挑战。
2.1. 大型语言模型
语言模型是自然语言的概率模型,能够生成一系列单词的概率分布。大型语言模型则以其庞大的参数量著称,通常是包含数十亿参数的人工神经网络。
LLM通常通过在大量无标注数据上进行预训练来构建,采用自监督学习和弱监督学习技术。之后,通过微调和提示词工程等技术,将预训练模型适配到特定任务。

大型语言模型可以执行多种自然语言处理任务,如翻译、摘要等。此外,它们还具备强大的内容生成能力,因此在问答等场景中非常有用。目前,几乎所有主流云服务提供商都集成了大型语言模型服务。
2.2. 提示词工程
大型语言模型是经过海量文本训练的基础模型,能够捕捉人类语言的语法和语义。然而,要让它们执行特定任务,还需要进一步调整。
提示词工程是引导语言模型完成特定任务最快捷的方法之一。它通过结构化文本向模型描述任务目标,使其理解并执行。
提示词帮助LLM进行上下文学习,这种学习是临时性的。通过提示词工程,我们不仅能促进LLM的安全使用,还能构建新功能,例如将领域知识和外部工具整合到模型中。像链式思维提示这类技术已经变得非常流行,其核心是引导LLM在给出最终答案前,先将问题分解为一系列中间步骤。
2.3. 词向量
如前所述,LLM能高效处理自然语言。如果我们将自然语言中的单词表示为词向量,模型的性能将得到显著提升。词向量是能够编码单词语义的实值向量。
词向量通常由Word2vec或GloVe等算法生成。GloVe是一种无监督学习算法,在语料库的全局词共现统计上进行训练。
在提示词工程中,我们将提示转换为词向量,这使模型更容易理解和响应。此外,它还能有效增强我们提供给模型的上下文,从而获得更符合情境的回答。例如,我们可以从现有数据生成词向量并存入向量数据库。随后,可以使用用户输入在向量数据库中进行语义搜索,并将搜索结果作为附加上下文提供给模型,这正是构建由人工智能驱动的应用的关键步骤之一。
3. 使用 LangChain 构建 LLM 技术栈
我们已经了解,创建有效的提示词是成功利用LLM的关键。这包括使与语言模型的交互具备上下文感知能力,并依赖其进行推理。
为此,我们需要执行多项任务,例如为提示词创建模板、调用语言模型,以及从多种来源提供用户特定数据。为了简化这些任务,我们需要像LangChain这样的框架作为LLM技术栈的一部分。

该框架还能帮助开发需要链式调用多个语言模型的应用程序,并能够记住与语言模型过去的交互信息。此外,还有更复杂的用例,例如将语言模型用作推理引擎。
最后,我们可以执行日志记录、监控、流式处理以及其他重要的运维和故障排除任务。LLM技术栈正在快速发展以应对这些挑战,而LangChain正迅速成为其中的宝贵组成部分。
4. 面向 Java 的 LangChain
LangChain于2022年作为开源项目推出,在社区支持下迅速发展。它最初是由Harrison Chase开发的Python项目,后来成为AI领域增长最快的初创企业之一。
随后,JavaScript/TypeScript版本的LangChain于2023年初推出,并迅速流行起来。然而,目前并没有官方的Java版本LangChain。不过,社区开发了一个Java版本,称为LangChain4j,它支持Java 8或更高版本,并对Java和Spring Boot开发者友好。
LangChain4j的依赖可以在Maven Central上找到。根据所需功能,我们可能需要在应用中添加一个或多个依赖项。

<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.23.0</version>
</dependency>
5. LangChain 的构建模块
LangChain通过模块化组件为应用提供多种价值。这些组件不仅提供了有用的抽象,还包含了一系列操作语言模型的实现。接下来,我们将通过Java示例探讨其中一些模块。
5.1. 模型输入/输出(Models I/O)
在与语言模型交互时,我们需要具备相应的能力。LangChain提供了必要的构建模块,例如模板化提示的能力,以及动态选择和管理模型输入的能力。此外,我们还可以使用输出解析器从模型输出中提取信息。

提示模板是用于生成语言模型提示的预定义配方,可以包含指令、少量示例和特定上下文。
PromptTemplate promptTemplate = PromptTemplate
.from("Tell me a {{adjective}} joke about {{content}}..");
Map<String, Object> variables = new HashMap<>();
variables.put("adjective", "funny");
variables.put("content", "computers");
Prompt prompt = promptTemplate.apply(variables);
5.2. 内存
通常,基于LLM的应用程序会具备对话界面。对话的一个重要方面是能够引用先前交互中的信息。这种存储过去交互信息的能力称为内存。

LangChain提供了为应用程序添加内存的关键功能。例如,我们需要能够从内存中读取信息以增强用户输入,同时还需要将当前运行的输入和输出写入内存。
ChatMemory chatMemory = TokenWindowChatMemory
.withMaxTokens(300, new OpenAiTokenizer(GPT_3_5_TURBO));
chatMemory.add(userMessage("你好,我叫 Kumar"));
AiMessage answer = model.generate(chatMemory.messages()).content();
System.out.println(answer.text()); // 你好 Kumar!今天我能为您做些什么?
chatMemory.add(answer);
chatMemory.add(userMessage("我叫什么名字?"));
AiMessage answerWithName = model.generate(chatMemory.messages()).content();
System.out.println(answerWithName.text()); // 您的名字是 Kumar。
chatMemory.add(answerWithName);
这里,我们使用TokenWindowChatMemory实现了固定窗口聊天内存,它允许我们读写与语言模型交换的聊天消息。
LangChain还支持更复杂的数据结构和算法,以便从内存中返回选定的消息,例如返回过去几条消息的摘要,或仅返回与当前运行相关的消息。
5.3. 检索(Retrieval)
LLM通常在大量通用文本语料库上训练,因此在处理特定领域任务时可能效果不佳。为解决此问题,我们需要在生成阶段检索相关的外部数据,并将其传递给语言模型。
这个过程被称为检索增强生成。RAG有助于将模型的生成过程与相关且准确的信息结合,同时也让我们更深入地了解模型的生成过程。LangChain提供了构建RAG应用所需的核心组件。

首先,LangChain提供了文档加载器(如FileSystemDocumentLoader)用于从存储位置检索文档。随后,还提供了转换器用于进一步处理文档,例如将大文档分割成更小的块。
Document document = FileSystemDocumentLoader.loadDocument("simpson's_adventures.txt");
DocumentSplitter splitter = DocumentSplitters.recursive(100, 0,
new OpenAiTokenizer(GPT_3_5_TURBO));
List<TextSegment> segments = splitter.split(document);
这里,我们使用FileSystemDocumentLoader从文件系统加载文档,然后使用OpenAiTokenizer将文档分割成更小的段落。
为提高检索效率,这些文档通常被转换为嵌入向量,并存储在向量数据库中。LangChain支持多种嵌入模型,并与主流向量存储集成。
EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel();
List<Embedding> embeddings = embeddingModel.embedAll(segments).content();
EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();
embeddingStore.addAll(embeddings, segments);
这里,我们使用AllMiniLmL6V2EmbeddingModel为文档段落创建嵌入,然后将嵌入存储在内存向量存储中。
当外部数据以嵌入形式存储后,就可以从中进行检索。LangChain支持多种检索算法,从简单的语义搜索到更复杂的集成检索器。
String question = "Who is Simpson?";
// 假设该问题的答案包含在我们之前处理的文档中。
Embedding questionEmbedding = embeddingModel.embed(question).content();
int maxResults = 3;
double minScore = 0.7;
List<EmbeddingMatch<TextSegment>> relevantEmbeddings = embeddingStore
.findRelevant(questionEmbedding, maxResults, minScore);
我们为用户问题生成嵌入,然后使用该嵌入从向量存储中检索相关匹配项。检索到的内容可以作为附加上下文,添加到发送给模型的提示中。
6. LangChain 的复杂应用
我们已经了解了如何使用单个组件创建基于语言模型的应用程序。LangChain还提供了构建更复杂应用的组件,例如链和代理,可用于构建更加自适应和功能增强的应用。
6.1. 链(Chains)
通常,一个应用程序需要按特定顺序调用多个组件。在LangChain中,这被称为链。链简化了复杂应用的开发过程,并使调试、维护和改进更加容易。
链还可以组合多个子链来构建更复杂的应用程序,这些应用可能需要与多个语言模型交互。LangChain提供了创建链的便捷方式,并内置了许多预构建链。
ConversationalRetrievalChain chain = ConversationalRetrievalChain.builder()
.chatLanguageModel(chatModel)
.retriever(EmbeddingStoreRetriever.from(embeddingStore, embeddingModel))
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.promptTemplate(PromptTemplate
.from("Answer the following question to the best of your ability: {{question}}\n\nBase your answer on the following information:\n{{information}}"))
.build();
这里,我们使用了预构建的ConversationalRetrievalChain,它允许我们将聊天模型与检索器、内存和提示模板结合起来。现在,我们可以简单地使用该链执行用户查询。
String answer = chain.execute("Who is Simpson?");
该链提供了默认的内存和提示模板,我们可以根据需要进行覆盖。创建自定义链也非常容易,链的能力使我们能够更轻松地实现复杂应用的模块化。
6.2. 代理(Agents)
LangChain还提供了更强大的结构,例如代理。与链不同,代理将语言模型用作推理引擎,以确定应该执行哪些操作以及执行的顺序。我们还可以为代理提供访问合适工具的权限,以执行必要的操作。
在LangChain4j中,代理以AI服务的形式提供,用于声明式地定义复杂的AI行为。让我们看看如何通过提供一个计算器工具来增强AI服务,使语言模型能够执行计算。
首先,我们定义一个包含基本计算功能的类,并用自然语言描述每个函数,以便模型理解。
public class AIServiceWithCalculator {
static class Calculator {
@Tool("Calculates the length of a string")
int stringLength(String s) {
return s.length();
}
@Tool("Calculates the sum of two numbers")
int add(int a, int b) {
return a + b;
}
}
接下来,定义一个用于构建AI服务的接口。
interface Assistant {
String chat(String userMessage);
}
然后,使用LangChain4j的构建器工厂,通过定义的接口和工具创建AI服务。
Assistant assistant = AiServices.builder(Assistant.class)
.chatLanguageModel(OpenAiChatModel.withApiKey(<OPENAI_API_KEY>))
.tools(new Calculator())
.chatMemory(MessageWindowChatMemory.withMaxMessages(10))
.build();
现在,我们可以向这个增强后的语言模型发送包含计算任务的问题。
String question = "What is the sum of the numbers of letters in the words \"language\" and \"model\"?";
String answer = assistant.chat(question);
System.out.println(answer); // The sum of the numbers of letters in the words "language" and "model" is 13.
运行这段代码,我们会发现语言模型现在能够执行计算。需要注意的是,语言模型在执行某些任务时可能会遇到困难,例如需要时间和空间概念的任务或复杂算术。然而,我们可以通过为模型提供必要的工具来解决这些问题。
7. 总结
本文探讨了创建基于大型语言模型应用的基本要素。我们讨论了将LangChain作为技术栈的一部分,对于开发此类应用的重要价值。
这引导我们探索了LangChain的Java实现——LangChain4j的一些核心组件。随着这些库的快速发展,开发语言模型驱动的应用正变得更加成熟和高效。