上篇
基于LangChain4j从0到1搭建自己的的AI智能体并部署上线-1-CSDN博客

检索增强生成RAG

什么是RAG?

Retrieval-Augmented Generation 检索增强生成

将原始问题以及提示词信息发送给大语言模型之前,先通过外部知识库检索相关信息,然后将检索结果和原始问题一起发送给大模型,大模型依据外部知识库再结合自身的训练数据,组织自然语言回答问题。通过这种方式,大语言模型可以获取到特定领域的相关信息,并能够利用这些信息进行回复。

  • 优点:数据存储在外部知识库,可以实时更新,不依赖对模型自身的训练,成本更低。
  • 缺点:需要两次查询:先查询知识库,然后再查询大模型,性能不如微调大模型。
  • 应用场景:适用于知识库规模大且频繁更新的场景,如企业客服、实时新闻查询、法律和医疗领域的最新知识问答等。

RAG的过程

RAG过程分为2个不同的阶段:索引和检索

索引阶段:
在索引阶段,对知识库文档进行预处理,可实现检索阶段的高效搜索。
以下是索引阶段的简化图:
加载知识库文档==>将文档中的文本分段==>利用向量大模型将分段后的文本转换成向量==>将向量存入向量数据库

为什么要进行文本分段?

大语言模型(LLM)的上下文窗口有限,所以整个知识库可能无法全部容纳其中。

  • 你在提问中提供的信息越多,大语言模型处理并做出回应所需的时间就越长。
  • 你在提问中提供的信息越多,花费也越多。
  • 提问中的无关信息可能会干扰大语言模型,增加产生幻觉(生成错误信息)的几率。

我们可以通过将知识库分割成更小、更易于理解的片段来解决这些问题。

检索阶段:

  • 以下是检索阶段的简化图:

通过向量模型将用户查询转换成向量 -> 在向量数据库中根据用户查询进行相似度匹配 -> 将用户查询和向量数据库中匹配到的相关内容一起交给LLM处理

文档加载器

常见文档加载器

  • 来自 langchain4j 模块的文件系统文档加载器 (FileSystemDocumentLoader)
  • 来自 langchain4j 模块的类路径文档加载器 (ClassPathDocumentLoader)
  • 来自 langchain4j 模块的网址文档加载器 (UrlDocumentLoader)
  • 来自 langchain4j-document-loader-amazon-s3 模块的亚马逊 S3 文档加载器 (AmazonS3DocumentLoader)
  • 来自 langchain4j-document-loader-azure-storage-blob 模块的 Azure Blob 存储文档加载器 (AzureBlobStorageDocumentLoader)
  • 来自 langchain4j-document-loader-github 模块的 GitHub 文档加载器 (GitHubDocumentLoader)
  • 来自 langchain4j-document-loader-google-cloud-storage 模块的谷歌云存储文档加载器 (GoogleCloudStorageDocumentLoader)
  • 来自 langchain4j-document-loader-selenium 模块的 Selenium 文档加载器 (SeleniumDocumentLoader)
  • 来自 langchain4j-document-loader-tencent-cos 模块的腾讯云对象存储文档加载器 (TencentCosDocumentLoader)

测试

@SpringBootTest
public class RAGTest {
    @Test
    public void testReadDocument(){
        //使用FileSystemDocumentLoader读取指定目录下的知识库文档
        //并使用默认的文档解析器TextDocumentParser对文档进行解析
        Document document= FileSystemDocumentLoader.loadDocument("E:\\test.txt");
        System.out.println(document.text());
    }



}

文档解析器

默认可解析txt、md文件,若想解析其他文件(如pdf)则需要:

引入依赖

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
            <version>1.0.0-beta3</version>
        </dependency>

测试

    @Test
    public void testParsePdf(){

        Document document= FileSystemDocumentLoader.loadDocument("E:\\医院信息.pdf",
                new ApachePdfBoxDocumentParser());
        System.out.println(document.text());
    }

文档分割器

工作方式

  1. 实例化一个“文档分割器”(DocumentSplitter),指定所需的“文本片段”(TextSegment)大小,并且可以选择指定characters或token的重叠部分。
  2. “文档分割器”(DocumentSplitter)将给定的文档(Document)分割成更小的单元,这些单元的性质因分割器而异。例如,“按段落分割文档器”(DocumentByParagraphSplitter)将文档分割成段落(由两个或更多连续的换行符定义),而“按句子分割文档器”(DocumentBySentenceSplitter)使用OpenNLP库的句子检测器将文档分割成句子,依此类推。
  3. 然后,“文档分割器”(DocumentSplitter)将这些较小的单元(段落、句子、单词等)组合成“文本片段”(TextSegment),尝试在单个“文本片段”(TextSegment)中包含尽可能多的单元,同时不超过第一步中设置的限制。如果某些单元仍然太大,无法放入一个“文本片段”(TextSegment)中,它会调用一个子分割器。这是另一个“文档分割器”(DocumentSplitter),能够将不适合的单元分割成更细粒度的单元。会向每个文本片段添加一个唯一的数据条目“index”。第一个“文本片段”(TextSegment)将包含 index=0,第二个是 index=1,依此类推。

LangChain4j 有一个“文档分割器”(DocumentSplitter) 接口,并且提供了几种开箱即用的实现方式:

  • 按段落文档分割器 (DocumentByParagraphSplitter)
  • 按行文档分割器 (DocumentByLineSplitter)
  • 按句子文档分割器 (DocumentBySentenceSplitter)
  • 按单词文档分割器 (DocumentByWordSplitter)
  • 按字符文档分割器 (DocumentByCharacterSplitter)
  • 按正则表达式文档分割器 (DocumentByRegexSplitter)

递归分割:DocumentSplitters.recursive (...)

默认情况下每个文本片段最多不能超过300个token

简单的RAG实现

引入easyRag

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-easy-rag</artifactId>
            <version>1.0.0-beta3</version>
        </dependency>
    @Test
    public void testReadDocumentAndStore() {

        //使用FileSystemDocumentLoader读取指定目录下的知识库文档
        //并使用默认的文档解析器对文档进行解析(TextDocumentParser)
        Document document = FileSystemDocumentLoader.loadDocument("E:\\Program\\人工智能.md");

        //为了简单起见,我们暂时使用基于内存的向量存储
        InMemoryEmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>();

        //ingest
        //1、分割文档,默认使用递归分割器,将文档分割为多个文本片段,每个片段包含不超过 300个token,并且有 30个token的重叠部分保证连贯性
        // (DocumentByParagraphSplitter(DocumentByLineSplitter(DocumentBySentenceSplitter(DocumentByWordSplitter))))
        //2、文本向量化,使用一个LangChain4j内置的轻量化向量模型对每个文本片段进行向量化
        //3、将原始文本和向量存储到向量数据库中 (InMemoryEmbeddingStore)
        //查看向量数据库内容
        EmbeddingStoreIngestor.ingest(document, embeddingStore);

        System.out.println(embeddingStore);
    }

在项目中实现RAG(基于内存)

配置AI类

@Bean
    public ContentRetriever contentRetrieverXiaozhi(){
        //使用FileSystemDocumentLoader读取指定文件目录下的知识库文档
        //并使用默认的文档解析器对文档进行解析
        Document document1= FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/医院信息.md");
        Document document2= FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/科室信息.md");
        Document document3= FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/神经内科.md");
        List<Document> documents= Arrays.asList(document1,document2,document3);

        //使用内存向量存储
        InMemoryEmbeddingStore<TextSegment> embeddingStore=new InMemoryEmbeddingStore<>();
        //使用默认文档分割器
        EmbeddingStoreIngestor.ingest(documents,embeddingStore);
        //从嵌入存储里(EmbeddingStore)里检索和查询内容相关信息
        return EmbeddingStoreContentRetriever.from(embeddingStore);
    }

在智能体中添加向量检索器

@AiService(
        wiringMode = EXPLICIT,
        chatModel = "qwenChatModel",
        chatMemoryProvider = "chatMemoryProviderXiaozhi",
        tools = "appointmentTools",
        contentRetriever = "contentRetrieverXiaozhi"
)
public interface XiaozhiAgent {
    @SystemMessage(fromResource = "xiaozhi-prompt-template.txt")
    String chat(@MemoryId Long memoryId, @UserMessage String userMessage);
}

测试

向量模型和向量存储

模型配置

#向量模型
langchain4j.community.dashscope.embedding-model.api-key=${DASH_SCOPE_API_KEY}
langchain4j.community.dashscope.embedding-model.model-name=text-embedding-v3

测试

@SpringBootTest
public class EmbeddingTest {
    @Autowired
    private EmbeddingModel embeddingModel;

    @Test
    public void testEmbeddingModel(){
        Response<Embedding> embed=embeddingModel.embed("你好");
        System.out.println("向量维度"+embed.content().vector().length);
        System.out.println("向量输出"+ embed);
    }
}

向量存储

这里使用Pinecone来进行向量存储

The vector database to build knowledgeable AI | Pinecone

登录注册后保存API并创建数据库,插入几条示例数据

添加依赖

        <dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-pinecone</artifactId>
            <version>1.0.0-beta3</version>
        </dependency>

配置向量存储对象

@Configuration
public class EmbeddingStoreConfig {
    @Autowired
    private EmbeddingModel embeddingModel;
    @Bean
    public EmbeddingStore<TextSegment> embeddingStore(){
        //创建向量存储
        EmbeddingStore<TextSegment> embeddingStore= PineconeEmbeddingStore.builder()
                .apiKey(System.getenv("PINECONE_API_KEY"))
                .index("xiaozhi-index")//若不存在新建
                .nameSpace("xiaozhi-namespace")//若不存在新建
                .createIndex(PineconeServerlessIndexConfig.builder()
                        .cloud("AWS")
                        .region("us-east-1")
                        .dimension(embeddingModel.dimension())
                        .build())
                .build();
        return embeddingStore;
    }
    
}

测试

@Test
    public void testPineconeEmbeded(){
        //将文本转换成向量
        TextSegment segment1=TextSegment.from("我喜欢羽毛球");
        System.out.println(segment1);
        Embedding embedding1=embeddingModel.embed(segment1).content();

        //存入向量数据库
        embeddingStore.add(embedding1,segment1);

        TextSegment segment2=TextSegment.from("今天天气很好");
        Embedding embedding2=embeddingModel.embed(segment2).content();
        embeddingStore.add(embedding2,segment2);

    }

相似度匹配

/**
 * Pinecone-相似度匹配
 */
@Test
public void embeddingSearch() {
    // 提问,并将问题转成向量数据
    Embedding queryEmbedding = embeddingModel.embed("你最喜欢的运动是什么?").content();
    // 创建搜索请求对象
    EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
            .queryEmbedding(queryEmbedding)
            .maxResults(1) // 匹配最相似的一条记录
            .build();

    // 根据搜索请求 searchRequest 在向量存储中进行相似度搜索
    EmbeddingSearchResult<TextSegment> searchResult = embeddingStore.search(searchRequest);

    // searchResult.matches(), 获取搜索结果中的匹配项
    // .get(0): 从匹配项列表中获取第一个匹配项
    EmbeddingMatch<TextSegment> embeddingMatch = searchResult.matches().get(0);

    // 获取匹配项的相似度得分
    System.out.println(embeddingMatch.score()); // 0.8144288515898701

    // 返回文本结果
    System.out.println(embeddingMatch.embedded().text());
}

在项目中整合向量数据库

上传知识库到pinecone

    @Test
    public void testUploadKnowledgeLibrary() {
        // 使用 FileSystemDocumentLoader 读取指定目录下的知识库文档
        // 并使用默认的文档解析器对文档进行解析
        Document document1 = FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/医院信息.md");
        Document document2 = FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/科室信息.md");
        Document document3 = FileSystemDocumentLoader.loadDocument("E:/Program/knowledge/神经内科.md");
        List<Document> documents = Arrays.asList(document1, document2, document3);

        // 文本向量化并存入向量数据库, 将每个片段进行向量化, 得到一个嵌入向量
        EmbeddingStoreIngestor
                .builder()
                .embeddingStore(embeddingStore)
                .embeddingModel(embeddingModel)
                .build()
                .ingest(documents);
    }

修改配置类

    @Bean
    public ContentRetriever contentRetrieverXiaozhiPinecone() {
        // 创建一个 EmbeddingStoreContentRetriever 对象, 用于从嵌入存储中检索内容
        return EmbeddingStoreContentRetriever
                .builder()
                // 设置用于生成嵌入向量的嵌入模型
                .embeddingModel(embeddingModel)
                // 指定要使用的嵌入存储
                .embeddingStore(embeddingStore)
                // 设置最大检索结果数量, 这里表示最多返回 1 条匹配结果
                .maxResults(1)
                // 设置最小得分阈值, 只有得分大于等于 0.8 的结果才会被返回
                .minScore(0.8)
                // 构建最终的 EmbeddingStoreContentRetriever 实例
                .build();
    }
@AiService(
        wiringMode = EXPLICIT,
        chatModel = "qwenChatModel",
        chatMemoryProvider = "chatMemoryProviderXiaozhi",
        tools = "appointmentTools",
        contentRetriever = "contentRetrieverXiaozhiPinecone"
)
public interface XiaozhiAgent {
    @SystemMessage(fromResource = "xiaozhi-prompt-template.txt")
    String chat(@MemoryId Long memoryId, @UserMessage String userMessage);
}

流式输出改造

引入相关依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
        </dependency>
        <dependency>
        <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-reactor</artifactId>
            <version>1.0.0-beta3</version>
        </dependency>

配置流式输出模型

# 在application.properties中配置流式输出大模型
# 集成阿里云千问-流式输出
langchain4j.community.dashscope.streaming-chat-model.api-key=${DASH_SCOPE_API_KEY}
langchain4j.community.dashscope.streaming-chat-model.model-name=qwen-plus

修改在智能体中所要使用的模型

运行前端工程

至此项目完成,后续可以优化前端显示适配MD格式,看起来会更加美观

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐