1. 简介
在本教程中,我们将研究LangChain的细节,这是一个由语言模型驱动的应用程序开发框架。我们将首先收集有关语言模型的基本概念,这将有助于本教程。
尽管LangChain主要提供Python和JavaScript/TypeScript版本,但也可以选择在Java中使用LangChain。我们将讨论LangChain作为框架的构建块,然后继续在Java中尝试它们。
2. 背景
在深入探讨为什么我们需要一个由语言模型驱动的应用程序构建框架之前,我们必须首先了解什么是语言模型。我们还将介绍使用语言模型时遇到的一些典型复杂问题。
2.1 大语言模型
语言模型是自然语言的概率模型,可以生成一系列单词的概率。大语言模型(LLM)是一种以规模大为特征的语言模型,它们是人工神经网络,可能具有数十亿个参数。
LLM通常使用自监督和半监督学习技术对大量未标记数据进行预训练。然后,使用微调和快速工程等各种技术将预训练模型调整为特定任务:
这些LLM能够执行多种自然语言处理任务,如语言翻译和内容摘要,它们还能够执行内容创建等生成任务。因此,它们在回答问题等应用中非常有价值。
几乎所有主要的云服务提供商都已将大型语言模型纳入其服务产品中。例如,Microsoft Azure提供Llama 2和OpenAI GPT-4等LLM。Amazon Bedrock提供AI21 Labs、Anthropic、Cohere、Meta和Stability AI的模型。
2.2 提示工程
LLM是基于大量文本数据进行训练的基础模型,因此,它们可以捕捉人类语言固有的语法和语义。但是,它们 必须经过调整才能执行我们希望它们执行的特定任务。
提示工程是适应LLM的最快方法之一。这是一个构建文本的过程,可供LLM解释和理解。在这里,我们使用自然语言文本来描述我们期望LLM执行的任务:
我们创建的提示可帮助LLM进行情境学习,这是暂时的。我们可以使用提示工程来促进LLM的安全使用,并构建新功能,例如使用领域知识和外部工具增强LLM。
这是一个活跃的研究领域,新技术不断涌现。然而,像思路提示这样的技术已经变得相当流行,这里的理念是让LLM在给出最终答案之前,通过一系列中间步骤来解决问题。
2.3 词嵌入
正如我们所见,LLM能够处理大量自然语言文本。如果我们将自然语言中的单词表示为词向量,LLM的性能将大大提高。这是一个能够编码单词含义的实值向量。
通常,词向量是使用Tomáš Mikolov的Word2vec或斯坦福大学的GloVe等算法生成的。GloVe是一种无监督学习算法,它基于语料库中汇总的全局单词共现统计数据进行训练:
在提示工程中,我们将提示转换为其词向量,使模型能够更好地理解和响应提示。此外,它还非常有助于增强我们为模型提供的上下文,从而使模型能够提供更多上下文答案。
例如,我们可以从现有数据集生成词向量并将其存储在向量数据库中。此外,我们可以使用用户提供的输入对该向量数据库进行语义搜索。然后,我们可以将搜索结果用作模型的附加上下文。
3. LangChain的LLM技术栈
正如我们已经看到的,创建有效的提示是成功利用任何应用程序中的LLM功能的关键要素。这包括使与语言模型的交互具有上下文感知能力,并能够依赖语言模型进行推理。
为此,我们需要执行多项任务,例如创建提示模板、调用语言模型以及向语言模型提供来自多个来源的用户特定数据。为了简化这些任务,我们需要一个像LangChain这样的框架作为我们LLM技术栈的一部分:
该框架还有助于开发需要链接多个语言模型并能够回忆过去与语言模型交互的信息的应用程序。然后,还有更复杂的用例,涉及使用语言模型作为推理引擎。
最后,我们可以执行日志记录、监控、流式传输和其他维护和故障排除的基本任务。LLM技术栈正在快速发展,以解决其中许多问题。然而,LangChain正迅速成为LLM技术栈中一个有价值的组成部分。
4. Java版LangChain
LangChain于2022年作为开源项目推出,并很快通过社区支持获得了发展势头。它最初由Harrison Chase用Python开发,很快就成为AI领域增长最快的初创公司之一。
2023年初,在Python版本之后,出现了一个JavaScript/TypeScript版本的LangChain。它很快就变得非常流行,并开始支持多种JavaScript环境,如Node.js、Web浏览器、CloudFlare工作器、Vercel/Next.js、Deno和Supabase Edge函数。
不幸的是,没有适用于Java/Spring应用程序的官方Java版LangChain。不过,有一个Java版的LangChain社区版本,名为LangChain4j。它适用于Java 8或更高版本,并支持Spring Boot 2和3。
LangChain的各种依赖可在Maven Central中找到。根据我们使用的功能,我们可能需要在应用程序中添加一个或多个依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>0.23.0</version>
</dependency>
例如,在本教程的后续部分中,我们还需要支持与OpenAI模型集成的依赖、提供对嵌入的支持以及类似all-MiniLM-L6-v2的句子转换器模型。
LangChain4j的设计目标与LangChain类似,它提供了一个简单而连贯的抽象层以及众多实现。它已经支持OpenAI等多家语言模型提供商和Pinecone等嵌入存储提供商。
但是,由于LangChain和LangChain4j都在快速发展,Python或JS/TS版本中可能支持Java版本中尚不支持的功能。不过,基本概念、总体结构和词汇大致相同。
5. LangChain的构建模块
LangChain为我们的应用程序提供了多种价值主张,可用作模块组件,模块化组件提供有用的抽象以及用于处理语言模型的实现集合。让我们用Java中的示例来讨论其中一些模块。
5.1 模型I/O
在使用任何语言模型时,我们都需要与之交互的能力。LangChain提供了必要的构建块,例如模板化提示以及动态选择和管理模型输入的能力。此外,我们可以使用输出解析器从模型输出中提取信息:
提示模板是用于生成语言模型提示的预定义方案,可能包括说明、少量示例和特定上下文:
PromptTemplate promptTemplate = PromptTemplate
.from("Tell me a joke about <section class="jumbotron geopattern" data-pattern-id="Finagle简介">
<div class="container">
<div id="jumbotron-meta-info">
<h1>Finagle简介</h1>
<span class="meta-info">
<span class="octicon octicon-calendar"></span> 2025/03/28
</span>
</div>
</div>
</section>
<script>
$(document).ready(function(){
$('.geopattern').each(function(){
$(this).geopattern($(this).data('pattern-id'));
});
});
</script>
<article class="post container " itemscope itemtype="http://schema.org/BlogPosting">
<div class="row">
<div class="col-md-9 markdown-body">
<h2 id="1-概述">1. 概述</h2>
<p>在本教程中,我们将快速介绍Twitter的RPC库Finagle。</p>
<p>我们将使用它来构建一个简单的客户端和服务器。</p>
<h2 id="2-构建块">2. 构建块</h2>
<p>在深入实现之前,我们需要了解构建应用程序所需的基本概念。这些概念广为人知,但在Finagle的世界中含义略有不同。</p>
<h3 id="21-service">2.1 Service</h3>
<p>服务是由类表示的功能,它接收请求并返回包含操作最终结果或有关失败的信息的Future。</p>
<h3 id="22-filter">2.2 Filter</h3>
<p>过滤器也是函数,它们接收请求和服务,对请求执行一些操作,将其传递给服务,对生成的Future执行一些操作,最后返回最终的Future。我们可以将它们视为<a href="https://www.baeldung.com/spring-aop">切面</a>,因为它们可以实现围绕函数执行发生的逻辑并改变其输入和输出。</p>
<h3 id="23-future">2.3 Future</h3>
<p>Future表示异步操作的最终结果,它们可能处于以下三种状态之一:待处理、成功或失败。</p>
<h2 id="3-服务">3. 服务</h2>
<p>首先,我们将实现一个简单的HTTP问候服务,它将从请求中获取name参数并响应,并添加惯常的“Hello”消息。</p>
<p>为此,我们需要创建一个类,它将扩展Finagle库中的抽象Service类,<strong>并实现其apply方法</strong>。</p>
<p>我们所做的看起来类似于实现一个<a href="https://www.baeldung.com/java-8-functional-interfaces">函数式接口</a>。但有趣的是,我们实际上不能使用这个特定的功能,因为Finagle是用Scala编写的,而我们利用的是Java-Scala互操作性:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">GreetingService</span> <span class="kd">extends</span> <span class="nc">Service</span><span class="o"><</span><span class="nc">Request</span><span class="o">,</span> <span class="nc">Response</span><span class="o">></span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">Future</span><span class="o"><</span><span class="nc">Response</span><span class="o">></span> <span class="nf">apply</span><span class="o">(</span><span class="nc">Request</span> <span class="n">request</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">String</span> <span class="n">greeting</span> <span class="o">=</span> <span class="s">"Hello "</span> <span class="o">+</span> <span class="n">request</span><span class="o">.</span><span class="na">getParam</span><span class="o">(</span><span class="s">"name"</span><span class="o">);</span>
<span class="nc">Reader</span><span class="o"><</span><span class="nc">Buf</span><span class="o">></span> <span class="n">reader</span> <span class="o">=</span> <span class="nc">Reader</span><span class="o">.</span><span class="na">fromBuf</span><span class="o">(</span><span class="k">new</span> <span class="nc">Buf</span><span class="o">.</span><span class="na">ByteArray</span><span class="o">(</span><span class="n">greeting</span><span class="o">.</span><span class="na">getBytes</span><span class="o">(),</span> <span class="mi">0</span><span class="o">,</span> <span class="n">greeting</span><span class="o">.</span><span class="na">length</span><span class="o">()));</span>
<span class="k">return</span> <span class="nc">Future</span><span class="o">.</span><span class="na">value</span><span class="o">(</span><span class="nc">Response</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="n">request</span><span class="o">.</span><span class="na">version</span><span class="o">(),</span> <span class="nc">Status</span><span class="o">.</span><span class="na">Ok</span><span class="o">(),</span> <span class="n">reader</span><span class="o">));</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="4-过滤器">4. 过滤器</h2>
<p>接下来,我们将编写一个过滤器,将有关请求的一些数据记录到控制台。与Service类似,我们需要实现Filter的apply方法,该方法将接收请求并返回Future响应,但这次它还将接收Service作为第二个参数。</p>
<p>基本的<a href="https://twitter.github.io/finagle/docs/com/twitter/finagle/Filter.html">Filter</a>类有4种类型参数,但很多时候我们不需要改变过滤器内部的请求和响应的类型。</p>
<p>为此,我们将使用将4个类型参数合并为2个的<a href="https://twitter.github.io/finagle/docs/com/twitter/finagle/SimpleFilter.html">SimpleFilter</a>,我们将从请求中打印一些信息,然后简单地从提供的Service中调用apply方法:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">LogFilter</span> <span class="kd">extends</span> <span class="nc">SimpleFilter</span><span class="o"><</span><span class="nc">Request</span><span class="o">,</span> <span class="nc">Response</span><span class="o">></span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="nc">Future</span> <span class="nf">apply</span><span class="o">(</span><span class="nc">Request</span> <span class="n">request</span><span class="o">,</span> <span class="nc">Service</span><span class="o"><</span><span class="nc">Request</span><span class="o">,</span> <span class="nc">Response</span><span class="o">></span> <span class="n">service</span><span class="o">)</span> <span class="o">{</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Request host:"</span> <span class="o">+</span> <span class="n">request</span><span class="o">.</span><span class="na">host</span><span class="o">().</span><span class="na">getOrElse</span><span class="o">(()</span> <span class="o">-></span> <span class="s">""</span><span class="o">));</span>
<span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"Request params:"</span><span class="o">);</span>
<span class="n">request</span><span class="o">.</span><span class="na">getParams</span><span class="o">().</span><span class="na">forEach</span><span class="o">(</span><span class="n">entry</span> <span class="o">-></span> <span class="n">logger</span><span class="o">.</span><span class="na">info</span><span class="o">(</span><span class="s">"\t"</span> <span class="o">+</span> <span class="n">entry</span><span class="o">.</span><span class="na">getKey</span><span class="o">()</span> <span class="o">+</span> <span class="s">" : "</span> <span class="o">+</span> <span class="n">entry</span><span class="o">.</span><span class="na">getValue</span><span class="o">()));</span>
<span class="k">return</span> <span class="n">service</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h2 id="5-服务器">5. 服务器</h2>
<p>现在我们可以使用服务和过滤器来构建一个实际监听请求并处理请求的服务器。</p>
<p>我们将为该服务器提供一项服务,该服务包含通过andThen方法链接在一起的过滤器和服务:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Service</span> <span class="n">serverService</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LogFilter</span><span class="o">().</span><span class="na">andThen</span><span class="o">(</span><span class="k">new</span> <span class="nc">GreetingService</span><span class="o">());</span>
<span class="nc">Http</span><span class="o">.</span><span class="na">serve</span><span class="o">(</span><span class="s">":8080"</span><span class="o">,</span> <span class="n">serverService</span><span class="o">);</span>
</code></pre></div></div>
<h2 id="6-客户端">6. 客户端</h2>
<p>最后,我们需要一个客户端向我们的服务器发送请求。</p>
<p>为此,我们将使用Finagle的<a href="https://twitter.github.io/finagle/docs/com/twitter/finagle/Http$.html">Http</a>类中便捷的newService方法创建一个HTTP服务,它将直接负责发送请求。</p>
<p>此外,我们将使用之前实现的相同日志过滤器并将其与HTTP服务链接起来。然后,我们只需调用apply方法即可。</p>
<p><strong>最后一个操作是异步的,其最终结果存储在Future实例中</strong>。我们可以等待这个Future成功或失败,但这将是一个阻塞操作,我们可能希望避免它。相反,我们可以实现一个在Future成功时触发的回调:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Service</span><span class="o"><</span><span class="nc">Request</span><span class="o">,</span> <span class="nc">Response</span><span class="o">></span> <span class="n">clientService</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">LogFilter</span><span class="o">().</span><span class="na">andThen</span><span class="o">(</span><span class="nc">Http</span><span class="o">.</span><span class="na">newService</span><span class="o">(</span><span class="s">":8080"</span><span class="o">));</span>
<span class="nc">Request</span> <span class="n">request</span> <span class="o">=</span> <span class="nc">Request</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="nc">Method</span><span class="o">.</span><span class="na">Get</span><span class="o">(),</span> <span class="s">"/?name=John"</span><span class="o">);</span>
<span class="n">request</span><span class="o">.</span><span class="na">host</span><span class="o">(</span><span class="s">"localhost"</span><span class="o">);</span>
<span class="nc">Future</span><span class="o"><</span><span class="nc">Response</span><span class="o">></span> <span class="n">response</span> <span class="o">=</span> <span class="n">clientService</span><span class="o">.</span><span class="na">apply</span><span class="o">(</span><span class="n">request</span><span class="o">);</span>
<span class="nc">Await</span><span class="o">.</span><span class="na">result</span><span class="o">(</span><span class="n">response</span>
<span class="o">.</span><span class="na">onSuccess</span><span class="o">(</span><span class="n">r</span> <span class="o">-></span> <span class="o">{</span>
<span class="n">assertEquals</span><span class="o">(</span><span class="s">"Hello John"</span><span class="o">,</span> <span class="n">r</span><span class="o">.</span><span class="na">getContentString</span><span class="o">());</span>
<span class="k">return</span> <span class="nc">BoxedUnit</span><span class="o">.</span><span class="na">UNIT</span><span class="o">;</span>
<span class="o">})</span>
<span class="o">.</span><span class="na">onFailure</span><span class="o">(</span><span class="n">r</span> <span class="o">-></span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="n">r</span><span class="o">);</span>
<span class="o">})</span>
<span class="o">);</span>
</code></pre></div></div>
<p>请注意,我们返回的是BoxedUnit.UNIT。返回<a href="https://www.scala-lang.org/api/current/scala/Unit.html">Unit</a>是Scala处理void方法的方式,因此我们在这里这样做是为了保持互操作性。</p>
<h2 id="7-总结">7. 总结</h2>
<p>在本教程中,我们学习了如何使用Finagle构建一个简单的HTTP服务器和一个客户端,以及如何在它们之间建立通信并交换消息。</p>
<div>
</div>
<!-- Comments -->
<div class="comment">
<a href="#" class="show_disqus_comment" onclick="return false;">Show Disqus Comments</a>
<div id="disqus_thread"></div>
<script>
var disqus_config = function () {
this.page.url = 'http://tuyucheng777.github.io/libraries/2025/03/28/java-finagle.html';
this.page.identifier = '/libraries/2025/03/28/java-finagle.html';
this.page.title = 'Finagle简介';
};
var disqus_loaded = false;
$(function() {
$('.show_disqus_comment').on('click', function() { // DON'T EDIT BELOW THIS LINE
$(this).html('加载中...');
var that = this;
if (!disqus_loaded) {
var d = document, s = d.createElement('script');
s.type = 'text/javascript';
s.async = true;
var shortname = 'tuyucheng777';
s.src = '//' + shortname + '.disqus.com/embed.js';
s.setAttribute('data-timestamp', +new Date());
(d.head || d.body).appendChild(s);
disqus_loaded = true;
}
$(that).remove();
})
})
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript>
<div id="gitalk-container"></div>
<script src="/assets/js/gitalk.min.js"></script>
<script>
var gitalk = new Gitalk({
id: '/libraries/2025/03/28/java-finagle.html',
clientID: 'b5981caa1bd346619214',
clientSecret: '04549c149e3e6f9dbcb3563d3605eca1e08be343',
repo: 'blog-comments',
owner: 'tuyucheng777',
admin: ['tuyucheng777'],
labels: ['gitment'],
perPage: 50,
})
gitalk.render('gitalk-container')
</script>
</div>
</div>
<div class="col-md-3">
<h3>Post Directory</h3>
<div id="post-directory-module">
<section class="post-directory">
<!-- Links that trigger the jumping -->
<!-- Added by javascript below -->
<dl></dl>
</section>
</div>
<script type="text/javascript">
$(document).ready(function(){
$( "article h2" ).each(function( index ) {
$(".post-directory dl").append("<dt><a class=\"jumper\" hre=#" +
$(this).attr("id")
+ ">"
+ $(this).text()
+ "</a></dt>");
var children = $(this).nextUntil("h2", "h3")
children.each(function( index ) {
$(".post-directory dl").append("<dd><a class=\"jumper\" hre=#" +
$(this).attr("id")
+ ">"
+ " - " + $(this).text()
+ "</a></dd>");
});
});
var fixmeTop = $('#post-directory-module').offset().top - 100; // get initial position of the element
$(window).scroll(function() { // assign scroll event listener
var currentScroll = $(window).scrollTop(); // get current position
if (currentScroll >= fixmeTop) { // apply position: fixed if you
$('#post-directory-module').css({ // scroll to that element or below it
top: '100px',
position: 'fixed',
width: 'inherit'
});
} else { // apply position: static
$('#post-directory-module').css({ // if you scroll above it
position: 'inherit',
width: 'inherit'
});
}
});
$("a.jumper").on("click", function( e ) {
e.preventDefault();
$("body, html").animate({
scrollTop: ($( $(this).attr('hre') ).offset().top - 100)
}, 600);
});
});
</script>
</div>
</div>
<div class="asb-post-01">
<div class="mask"></div>
<div class="info">
<div>扫码关注公众号:<span style="color: #E9405A; font-weight: bold;">Taketoday</span></div>
<div>
<span>发送 </span><span class="token" style="color: #e9415a; font-weight: bold; font-size: 17px; margin-bottom: 45px;">290992</span>
<div>
即可<span style="color: #e9415a; font-weight: bold;">立即永久</span>解锁本站全部文章
</div>
<img class="code-img" style="width: 300px;display:unset" src="/assets/images/keeppuresmile.jpg">
</div>
</div>
</article>
..");
Map<String, Object> variables = new HashMap<>();
variables.put("adjective", "funny");
variables.put("content", "computers");
Prompt prompt = promptTemplate.apply(variables);
这里,我们创建一个能够接受多个变量的提示模板。变量是我们从用户输入中接收并提供给提示模板的内容。
LangChain支持集成两种类型的模型,即语言模型和聊天模型。聊天模型也由语言模型支持,但提供聊天功能:
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey(<OPENAI_API_KEY>)
.modelName(GPT_3_5_TURBO)
.temperature(0.3)
.build();
String response = model.generate(prompt.text());
这里我们用特定的OpenAI模型和相关的API Key创建一个聊天模型,我们可以通过免费注册从OpenAI获取API Key,参数temperature用于控制模型输出的随机性。
最后,语言模型的输出可能不够结构化,无法呈现。LangChain提供输出解析器,帮助我们构建语言模型响应-例如,从输出中提取信息作为Java中的POJO。
5.2 记忆
通常,利用LLM的应用程序具有对话界面。任何对话的一个重要方面是能够引用对话中先前介绍的信息,存储有关过去交互的信息的能力称为记忆:
LangChain提供了向应用程序添加内存的关键推动因素。例如,我们需要能够从内存中读取以增强用户输入。然后,我们需要能够将当前运行的输入和输出写入内存:
ChatMemory chatMemory = TokenWindowChatMemory
.withMaxTokens(300, new OpenAiTokenizer(GPT_3_5_TURBO));
chatMemory.add(userMessage("Hello, my name is Kumar"));
AiMessage answer = model.generate(chatMemory.messages()).content();
System.out.println(answer.text()); // Hello Kumar! How can I assist you today?
chatMemory.add(answer);
chatMemory.add(userMessage("What is my name?"));
AiMessage answerWithName = model.generate(chatMemory.messages()).content();
System.out.println(answer.text()); // Your name is Kumar.
chatMemory.add(answerWithName);
在这里,我们使用TokenWindowChatMemory实现了固定窗口聊天内存,它允许我们读取和写入与语言模型交换的聊天消息。
LangChain还提供更复杂的数据结构和算法,以便从内存中返回选定的消息,而不是返回所有内容。例如,它支持返回过去几条消息的摘要,或者仅返回与当前运行相关的消息。
5.3 检索
大型语言模型通常是在大量文本语料库上进行训练的。因此,它们在一般任务中非常高效,但在特定领域的任务中可能不太有用。为此,我们需要检索相关的外部数据并在生成步骤中将其传递给语言模型。
此过程称为检索增强生成(RAG),它有助于将模型建立在相关且准确的信息上,并让我们深入了解模型的生成过程。LangChain提供了创建RAG应用程序所需的构建块:
首先,LangChain提供了文档加载器,用于从存储位置检索文档。然后,有转换器可用于准备文档以供进一步处理。例如,我们可以让它将大文档拆分成较小的块:
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?";
//The assumption here is that the answer to this question is contained in the document we processed earlier.
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 链
通常,一个应用程序需要按特定顺序调用多个组件。这就是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: \n\nBase your answer on the following information:\n"))
.build();
这里我们使用了预先构建的链ConversationalRetrievalChain,它允许我们将聊天模型与检索器以及内存和提示模板一起使用。现在,我们可以简单地使用该链来执行用户查询:
String answer = chain.execute("Who is Simpson?");
该链带有默认的内存和提示模板,我们可以覆盖这些模板。创建自定义链也相当容易,创建链的能力使得实现复杂应用程序的模块化实现变得更加容易。
6.2 代理
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.prtintln(answer); // The sum of the numbers of letters in the words "language" and "model" is 13.
当我们运行此代码时,我们会发现语言模型现在能够执行计算。
值得注意的是,语言模型在执行某些需要时间和空间概念或执行复杂算术程序的任务时会遇到困难。但是,我们始终可以为模型补充必要的工具来解决这个问题。
7. 总结
在本教程中,我们介绍了创建由大型语言模型驱动的应用程序的一些基本要素。此外,我们还讨论了将LangChain等框架作为开发此类应用程序的技术堆栈的一部分的价值。
Post Directory
