我进入大型语言模型领域稍微晚了一点,但通常不会太早地跟上炒作周期的步伐。
例如,我从未对区块链这种还在寻找问题要解决的解决方案产生兴趣,也不对微服务这种最新的IT潮流感兴趣。虽然我对大型语言模型(LLM)的了解比较晚,但我一直是大型语言模型的常规用户。我用OpenAI回答一些我知识范围之外的非争议性问题,比如语言学或法律相关的问题;我用GitHub Copilot在IDE里改进代码。
这篇文章的重点是把聊天机器人集成到我的应用程序里并看看它的能耐。
选择一个大型语言模型目前有很多大型语言模型可供选择。我提到了OpenAI,但还有很多其他的也非常引人注目:Google Gemini、Cohere、Amazon Bedrock,等等等等。每个模型都有自己的优缺点,但这与这篇文章无关。
我的主要要求是它需要本地运行。另外,我希望在LLM之上有一个抽象层来以便学习抽象,而不是具体实现。
我选择了LangChain4J和Ollama,因为它们比较出名,并且能满足这个项目的特定需求。
LangChain4J 和 Ollama 的快速入门这里就是LangChain4J用它自己的话来介绍自己的方式:
LangChain4j的目标是简化将LLM集成到Java应用程序中的流程。
具体做法如下:
1. 统一API:LLM提供商(如OpenAI或Google Vertex AI)和嵌入(向量)存储(如Pinecone或Milvus)使用专有API。LangChain4j提供统一的API,避免了学习和实现每个提供商特定API的必要性。要尝试不同的LLM或嵌入存储系统,您可以轻松在不同的LLM或嵌入存储之间切换,无需重写代码。LangChain4j目前支持超过15个流行的LLM提供商和超过20个嵌入存储系统。
2. 全面工具箱:自2023年初以来,社区一直在构建许多基于LLM的应用程序,识别出常见的抽象、模式和技术。LangChain4j已经把这些抽象、模式和技术提炼成一个可以直接使用的工具箱。我们的工具箱包括从低级提示模板、聊天记忆管理、函数调用到高级模式如AI服务和RAG的各种工具。对于每个抽象,我们都提供了接口以及基于常见技术的多个实用实现。无论您是构建聊天机器人还是开发从数据收集到检索的完整RAG流程,LangChain4j都提供了广泛的选择。
3. 多个示例:这些示例展示了如何开始创建各种LLM应用,为您提供灵感并帮助您快速开始开发。
奥拉玛的介绍短得惊人:
快速入门大语言模型。
运行Llama 3.2、Phi 3、Mistral、Gemma 2等模型。自定义并创建你自己的模型,
一个运行时支持多种模型。
先试试看我将把这一部分分成LangChain4j应用和Ollama的基础设施。
LangChain4j 应用程序LangChain4j 提供了一个 Spring Boot 集成启动器。这里是我们最小的依赖:
``
<dependencies>
<dependency>
<组Id>org.springframework.boot</组Id>
<工件Id>spring-boot-starter-web</工件Id>
</dependency>
<dependency>
<组Id>dev.langchain4j</组Id>
<工件Id>langchain4j-ollama-spring-boot-starter</工件Id>
<版本>0.35.0</版本>
</dependency>
</dependencies>
LangChain4j 提供了一个针对不同大型语言模型的抽象 API。接下来,我们将重点介绍本节中将使用的部分内容:
最基本的 API model.generate(String)
将用户的消息传递给 Ollama 实例并接收响应。我们需要创建一个端点来处理这个调用;这些细节不重要。
LangChain4J的Spring Boot启动器插件会自动从指定的依赖集(如Ollama)创建一个ChatLanguageModel
对象。同时,它提供了丰富的Spring Boot配置选项。
langchain4j.ollama.chat-model:
base-url: http://localhost:11434 #1
model-name: llama3.2 #2
- 点击正在运行的 Ollama 实例
- 使用的模型
当应用启动的时候,LangChain4j 会创建一个 ChatLanguageModel
类型的 bean,并将这个 bean 添加到上下文中。需要注意的是,具体类型取决于在类路径上找到的依赖关系。
为了使用方便,我将使用Docker,更具体地说,我将使用Docker Compose。我的Compose文件如下:
services:
langchain4j,
build:
context: .
environment:
LANGCHAIN4J_OLLAMA_CHAT_MODEL_BASE_URL: http://ollama:11434 #1 注释:ollama服务的URL地址
ports:
- "8080:8080"
depends_on:
- ollama
ollama,
image: ollama/ollama #2 注释:ollama镜像
volumes:
- ./ollama:/root/.ollama #3 注释:将本地ollama文件夹挂载到ollama容器中的/root/.ollama路径
- 覆盖 JAR 中配置的 URL 设置,以便在 Docker Compose 中使用 Docker 容器
- 使用最新版本的镜像;这只是一个测试环境
- 在主机上保留模型的副本,请参见下文
如上所述,Ollama 是一个可切换模型的运行时,默认没有预设模型。要下载模型,请通过 docker exec
进入容器并运行以下命令:
运行llama3.2命令:```
ollama run llama3.2
小心,`llama3.2` 高达 20GB 之大;因此,在每次运行 `docker compose up` 时,你不想每次都下载模型。这就是上面提到的卷映射的原因。
当然,你可以把 `llama3.2` 换成像 `tinyllama` 这样的更小的模型。
这时,我们可以使用`curl`命令来测试我们的应用,并查看结果如何。
curl localhost:8080 -d 你好,我是Nicolas,我做DevRel
# 要通过流式处理来增强
上述解决方案虽然有效,但用户体验还有提升空间。命令会卡住,几秒钟后才有回应,不像传统的OpenAI用户界面那样,会流式传输令牌给用户。
我们可以简单地将 `ChatLanguageModel` 替换为 `StreamingChatLanguageModel` 来实现这个目标。方法会有一些不同。
![](https://imgapi.imooc.com/673d38850946526012180649.jpg)
我们需要根据情况调整一下应用的设置。
services:
langchain4j: # 下面是 Langchain4j 服务的配置。
build: # 构建上下文。
context: .
environment: # 设置环境变量,指向 Ollama 模型的 URL。
LANGCHAIN4J_OLLAMA_STREAMING_CHAT_MODEL_BASE_URL: http://ollama:11434 #1
ports: # 配置端口映射。
- "8080:8080"
depends_on: # 依赖的服务。
- ollama
1. 原先的名称是 `LANGCHAIN4J_OLLAMA_CHAT_MODEL_BASE_URL`
同时,我们必须将 Spring Web MVC 迁移至 Spring Webflux。然后,我们将 LLM 结果流输出连接到应用结果流输出上,如下所示:
class AppStreamingResponseHandler(private val sink: Sinks.Many<String>) : StreamingResponseHandler<AiMessage> {
override fun onNext(token: String) { //1
sink.tryEmitNext(token) // 尝试将 token 发送到 sink
}
override fun onError(error: Throwable) { //1
sink.tryEmitError(error) // 尝试将错误发送到 sink
}
override fun onComplete(response: Response<AiMessage>) { //2
println(response.content()?.text()) // 打印响应的内容
sink.tryEmitComplete() // 尝试将完成信号发送到 sink
}
}
class PromptHandler(private val model: StreamingChatLanguageModel) {
suspend fun handle(req: ServerRequest): ServerResponse {
val prompt = req.awaitBody<String>() //3
val sink = Sinks.many().unicast().onBackpressureBuffer<String>() //4
// 使用 AppStreamingResponseHandler 处理响应
model.generate(prompt, AppStreamingResponseHandler(sink)) //5
// 返回一个包含 sink 的响应
return ServerResponse.ok().bodyAndAwait(sink.asFlux().asFlow()) //6
}
}
1. 将 token 和错误信息引向 sink
2. 该函数**不是**抽象的且不执行任何操作;因此,它不会关闭流,请记得重写它。
3. 异步获取请求体
4. 创建 sink
5. 调用模型并将 sink 作为引用传递
6. 返回 sink
我们现在可以用 `curl` 的 `-N` 标志进入流式模式。
curl -N localhost:8080 -d 你好,我是Nicolas,我做 DevRel
结果已经好多了!
# 记住历史
目前,每个聊天机器人的请求都是独立的——它们之间没有上下文关联。聊天历史是我们目前缺少的一个重要功能,而现在市面上的AI助手已经具备了这个功能。我们需要从两个方向重构应用程序:首先,保存用户和模型之间的每条消息;其次,把每个用户的聊天记录隔离开。
我最初是自己在内存中保存历史记录的。如果感兴趣,可以查看提交历史了解我是怎么做的。然而,LangChain4j 通过其 `AiServices` 类提供了一种集成的解决方案。`ChatLanguageModel` 表示与 LLM 进行基本请求响应接口,而 `AiServices` 还包括了聊天记忆、检索增强生成(RAG)和外部函数调用等功能。
![](https://imgapi.imooc.com/673d3886092439b314000980.jpg)
以下是相关代码:
data class StructuredMessage(val sessionId: String, val text: String) //1 结构化消息定义
interface ChatBot { //2 会话机器人接口
fun talk(@MemoryId sessionId: String, @UserMessage message: String): TokenStream //3-4-5 发送消息并接收TokenStream响应
}
class PromptHandler(private val chatBot: ChatBot) {
suspend fun handle(req: ServerRequest): ServerResponse {
val message = req.awaitBody<StructuredMessage>()
val sink = Sinks.many().unicast().onBackpressureBuffer<String>()
chatBot.talk(message.sessionId, message.text) //6 聊天机器人发送消息
.onNext(sink::tryEmitNext) //7 处理TokenStream的事件
.onError(sink::tryEmitError) //7 处理TokenStream的事件
.onComplete { sink.tryEmitComplete() } //7 处理TokenStream的事件
.start()
return ServerResponse.ok().bodyAndAwait(sink.asFlux().asFlow())
}
}
fun beans() = beans {
bean {
coRouter {
val chatBot = AiServices //8 创建聊天机器人的实例
.builder(ChatBot::class.java)
.streamingChatLanguageModel(ref<StreamingChatLanguageModel>())
.chatMemoryProvider { MessageWindowChatMemory.withMaxMessages(40) }
.build()
POST("/")(PromptHandler(chatBot)::handle) //POST("/") 处理POST请求并调用PromptHandler的handle方法
}
}
}
1. 我们需要一种方法来传递一个相关联的ID,以便将具有相同聊天历史的消息分组。因为我们使用的是curl而不是浏览器来发送请求,我们需要显式地传递一个ID,与用户消息一起发送。
2. 定义一个不强制层级结构的接口。函数是自由形式的,但您可以提供提示。
3. `@MemoryId` 标注关联ID
4. `@UserMessage` 标注用户发送给模型的消息
5. 可以订阅 `TokenStream`
6. LangChain4j 调用配置的模型接口
7. 将 `TokenStream` 管道传输到接收端,就像我们自定义实现那样
8. 构建 `ChatBot`: `AiServices` 将在运行时创建实现类
这是它的用法:
curl -N -H 'Content-Type: application/json' localhost:8080 -d '{ "sessionId": "1", "message": "你好,我是Nicolas,我是一名DevRel工程师" }'
curl -N -H 'Content-Type: application/json' localhost:8080 -d '{ "sessionId": "2", "message": "你好,我是Jane Doe,我是测试样本" }'
增强检索生成功能
LLM的表现取决于它们训练的数据,你可能希望你的聊天机器人使用你自己的自定义数据进行训练。RAG就是解决这个问题的好方法。具体来说,提前索引内容,然后将这些内容存储起来,在检索时加入这些索引数据。更多细节可以参考LangChain4j对[RAG的解释](https://docs.langchain4j.dev/tutorials/rag)。
我们将用我博客的数据为应用添加一些RAG的雏形。
LangChain4j 提供了一个名为 [Easy RAG](https://docs.langchain4j.dev/tutorials/rag#easy-rag) 的依赖库。它支持两种来源,即文件和网址,并内置了一个内存中的嵌入存储。在常规应用程序中,你会离线索引并将嵌入存储在常规数据库中,但在我们这里,会将它们在启动时存储在内存中。对于我们进行原型设计来说,这已经足够了。
class BlogDataLoader(private val embeddingStore: EmbeddingStore<TextSegment>) {
private val urls = arrayOf(
"https://blog.frankel.ch/speaking/",
// 更多的URL
)
@EventListener(ApplicationStartedEvent::class) // 注释1
fun onApplicationStarted() {
val parser = TextDocumentParser()
val documents = urls.map { UrlDocumentLoader.load(it, parser) }
EmbeddingStoreIngestor.ingest(documents, embeddingStore)
}
}
fun beans() = beans {
bean<EmbeddingStore<TextSegment>> {
InMemoryEmbeddingStore<TextSegment>() // 注释2
}
bean {
BlogDataLoader(ref<EmbeddingStore<TextSegment>>()) // 注释3
}
bean {
coRouter {
val chatBot = AiServices
.builder(ChatBot::class.java)
.streamingChatLanguageModel(ref<StreamingChatLanguageModel>())
.chatMemoryProvider { MessageWindowChatMemory.withMaxMessages(40) } // 设置最大消息数为40
.contentRetriever(EmbeddingStoreContentRetriever.from(ref<EmbeddingStore<TextSegment>>())) // 注释4
.build()
}
}
}
1. 在应用启动时运行这段代码
2. 定义嵌入存储。一般应用最好使用持久化数据存储:LangChain4j 支持[多种选项](https://docs.langchain4j.dev/tutorials/embedding-stores)。
3. 在加载代码中注入存储
4. 配置聊天机器人从存储中获取数据
我们可以通过向输入的文档提问来测试RAG。
在OpenAI上,我问道:“尼古拉斯·弗蘭克爾写了哪些书?”它回答说:《Vaadin 学习》(正确)、《Spring Security 实战》(可能是,但它在乱说)、以及《Java EE 开发:使用WildFly》(不可能,它又在乱说了)。
我们也在这具有RAG功能的应用上做同样的事情:
curl -N -H 'Content-Type: application/json' localhost:8080 -d '{ "sessionId": "1", "message": "What books did Nicolas Fränkel write?" }'
答案好多了。
_> 提供的信息没有提到尼古拉斯·弗兰克尔撰写的特定书籍的具体信息。这段信息只提供了他的博客的元数据,他的博客里有一个专门的“书籍”部分等等。_
这其实不太对——我实际上说过我写了那些书,但至少没有瞎编。
# 结论。
在这篇文章里,我展示了如何分几步开始你的Langchain4j之旅。我们首先将Langchain4j作为Ollama的一个简单接口使用。接着,我们将token切换为流式传输。随后,我们重构了代码,通过Langchain4j的抽象功能增加了聊天记录。最后,我们通过增加一个简单的内存存储和静态链接实现了RAG功能,完成了这个演示。
此帖子的完整源代码可以在GitHub上找到:哦。
## [GitHub - ajavageek/langchain4j-musings加入 ajavageek/langchain4j-musings 的开发过程,在 GitHub 上创建一个账户](https://github.com/ajavageek/langchain4j-musings?source=post_page-----fb5c084675cc--------------------------------)
**了解更多**:
* [LangChain4j](https://docs.langchain4j.dev)
* [Ollama](https://ollama.com/)
* 使用REST API的LangChain应用流式传输(https://chalise-arun.medium.com/streaming-with-rest-api-for-langchain-applications-f3a164a207d7)
_原发表于,_ [_A Java Geek_](https://blog.frankel.ch/langchain4j-musings/) _, 2024年11月10日,_
共同学习,写下你的评论
评论加载中...
作者其他优质文章