SpringAI-ollama

Source

SpringAi主要依赖

在这里插入图片描述
System Prompt :设置提示词 用来预设角色
ConversationMemory: 对话集
RAG: 检索增强生成 将业务数据存储在向量数据库中(做相似性检索)通过RAG进行链接
Function Calling 用来调用自己的api

 <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>1.0.0-M6</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
 <dependencies>

        <!--Springai依赖-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
        </dependency>

        <!--pgVector向量数据库-->
       <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-pgvector-store-spring-boot-starter</artifactId>
        </dependency>

        <!--doc解析器-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-tika-document-reader</artifactId>
        </dependency>
        <!--分词器-->
        <dependency>
            <groupId>com.knuddels</groupId>
            <artifactId>jtokkit</artifactId>
            <version>1.1.0</version>
        </dependency>
    </dependencies>

yml配置

  ai:
    ollama:
      language: zh
      response-language: zh
      chat:
        options:
          model: qwen2:7b
      embedding:
        enabled: true
        model: bge-m3
      base-url: http://172.21.198.208:11434/
    vectorstore:
      pgvector:
        index-type: HNSW
        distance-type: COSINE_DISTANCE
        dimension: 1024
        batching-strategy: TOKEN_COUNT
        max-document-batch-size: 10000

chat.options.model: qwen2:7b 这个是默认的模型(但是后期不需要了我们从数据库中拿)
embedding.model: bge-m3 这个模型是用来进行向量化的模型
base-url: http://172.21.198.208:11434/ 这个是ollama模型访问地址
我这里用的是pgvector来作为向量库 vectorstore.pgvector:
dimension: 1024 这个至关重要 这里写多少创建向量库的时候就要写多少 如果两边不一致的话向向量库向量会失败

创建向量库

-- 增加扩展
create extension if not exists vector;
create extension if not exists hstore;
create extension if not exists "uuid-ossp";

create table if not exists vector_store
(
    id        uuid primary key default uuid_generate_v4(),
    content   text,
    metadata  json,
    embedding vector(1024)  //这个的参数要和配置文件的一样,要不插入不进去 这个就是向量数据库的维度
);

create index on vector_store using hnsw(embedding vector_cosine_ops);

ollama对话模型

/**
 * Ollama模型配置类
 * 负责配置和管理不同的Ollama模型实例
 */
@Slf4j
@Configuration
public class OllamaModelConfig {

    /**
     * 创建OllamaChatModel的通用方法
     * @param baseUrl Ollama服务地址
     * @param modelName 模型名称
     * @param temperature 温度参数
     * @return 配置好的OllamaChatModel实例
     */
    public OllamaChatModel createOllamaModel(String baseUrl, String modelName, double temperature) {
        Assert.hasText(baseUrl, "Ollama服务地址不能为空");
        Assert.hasText(modelName, "模型名称不能为空");
        Assert.isTrue(temperature >= 0 && temperature <= 1, "温度参数必须在0-1之间");
        
        try {
            log.debug("开始创建Ollama模型: {}, 温度参数: {}, 服务地址: {}", modelName, temperature, baseUrl);
            
            OllamaApi ollamaApi = new OllamaApi(baseUrl);
            
            OllamaOptions options = OllamaOptions.builder()
                    .model(modelName)
                    .temperature(temperature)
                    .build();

            OllamaChatModel model = OllamaChatModel.builder()
                    .ollamaApi(ollamaApi)
                    .defaultOptions(options)
                    .observationRegistry(ObservationRegistry.NOOP)
                    .build();
            
            log.debug("Ollama模型创建成功: {}", modelName);
            return model;
        } catch (Exception e) {
            log.error("创建Ollama模型失败: {}", modelName, e);
            throw new RuntimeException("创建Ollama模型失败: " + modelName, e);
        }
    }
}

temperature温度的值越小回答的越准确

调用实例

 	    // 使用模型配置创建OllamaChatModel
        OllamaChatModel chatModel = ollamaModelConfig.createOllamaModel(
                aiModel.getAimodelAccessAddress(),
                aiModel.getAimodelName(),
                aiModel.getAimodelTemperature());
 		ChatClient chatClient = ChatClient.builder(chatModel).build();

		// 使用响应式流处理对话
        return chatClient.prompt()
                    .user(message)
                    .stream()
                    .content()
                    .onErrorResume(e -> {
                        log.error("聊天过程发生错误: ", e);
                        return Flux.just("抱歉,处理您的请求时出现了错误,请稍后重试。");
                    });

接下来介绍上面提到的主要的四个属性

1.System Prompt :设置提示词 用来预设角色

 systemPrompt="你是一位专业的室内设计顾问,精通各种装修风格、材料选择和空间布局。请基于提供的参考资料,为用户提供专业、详细且实用的建议。在回答时,请注意:\n" +
                "1. 准确理解用户的具体需求\n" +
                "2. 结合参考资料中的实际案例\n" +
                "3. 提供专业的设计理念和原理解释\n" +
                "4. 考虑实用性、美观性和成本效益\n" +
                "5. 如有需要,可以提供替代方案";
 // 创建聊天客户端
 ChatClient chatClient = ChatClient.builder(ollamaChatModel)
                .defaultSystem(systemPrompt)
                .build();

将预设词放入defaultSystem中之后ai就会根据我们的提示词进行工作

2.ConversationMemory: 对话集

package com.system.ai.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;

//持久化对话
@Slf4j
@Configuration
public class AiChatMemory implements ChatMemory {

    //这个是存储在内存中
    Map<String, List<Message>> conversationHistory = new ConcurrentHashMap<>();
    
    //新增持久化 (现在没有做持久化所以都存在了内存中 重启后会丢失对话记忆)
    @Override
    public void add(String conversationId, List<Message> messages) {
        log.error("对话id: {}", conversationId);
        this.conversationHistory.putIfAbsent(conversationId, new ArrayList<>());
        this.conversationHistory.get(conversationId).addAll(messages);
    }

    //获取
    @Override
    public List<Message> get(String conversationId, int lastN) {
        List<Message> all = this.conversationHistory.get(conversationId);
        return all != null ? all.stream().skip(Math.max(0, all.size() - lastN)).toList() : List.of();
    }

    //清除
    @Override
    public void clear(String conversationId) {
        this.conversationHistory.remove(conversationId);
    }
}

这个是我们创建的自己的AiChatMemory他继承了ChatMemory类,可以看到他其实就是将消息存储到List内 保存到内存中 当我们程序重启之后就会从内存删除

使用我们自己的AiChatMemory

 	@Autowired
    private AiChatMemory aiChatMemory ;

   ChatClient chatClient = ChatClient.builder(ollamaChatModel)
                .defaultAdvisors(new PromptChatMemoryAdvisor(aiChatMemory))//设置上下文              
                .build();
                
   Flux<String> content = chatClient.prompt()
                .user(message)
                .advisors(advisorSpec -> //设置持久化对话的查询的联通上下文
                                //设置聊天记忆检索的大小为100
                                advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
                                         // 设置对话ID 在聊天应用中,不同用户或不同会话的对话历史应该是相互独立的。
                                         // 通过设置对话ID,可以让聊天记忆顾问根据不同的对话ID来管理和检索相应的对话历史记录。
                                         // 这样,当用户发起新的对话时,系统可以根据对话ID准确地获取该用户之前的对话内容,从而提供更连贯和个性化的服务。
                                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, "测试固定id")
                )
                .stream()
                .content();
                //聊天交互的场景中, content 流会不断地发出聊天响应的文本片段,
                // 而 [complete] 这个字符串可以作为一个特殊的标识,用来表示聊天响应已经结束。
                // 当客户端接收到这个 "[complete]" 标识时,就可以知道整个聊天响应已经全部接收完毕,
                // 从而可以进行相应的处理,例如关闭连接、更新 UI 状态等。

设置上下文有两个点
第一个点就是创建ChatClient的时候的:

.defaultAdvisors(new PromptChatMemoryAdvisor(aiChatMemory))//设置上下文 

这里指定的是在哪里取上下文

                .advisors(advisorSpec -> //设置持久化对话的查询的联通上下文
                                //设置聊天记忆检索的大小为100
                                advisorSpec.param(AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100)
                                         // 设置对话ID 在聊天应用中,不同用户或不同会话的对话历史应该是相互独立的。
                                         // 通过设置对话ID,可以让聊天记忆顾问根据不同的对话ID来管理和检索相应的对话历史记录。
                                         // 这样,当用户发起新的对话时,系统可以根据对话ID准确地获取该用户之前的对话内容,从而提供更连贯和个性化的服务。
                                        .param(AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY, "测试固定id")
                )

这里指定的是会话的key和取出的上下文条数
AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY= 取出的上下文条数
AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY = 会话id

也可以通过向量库存储对话上下文上下文

  	 //对话历史向量库
      AbstractChatMemoryAdvisor chatMemoryAdvisor = VectorStoreChatMemoryAdvisor.builder(历史对话向量库)
                    .protectFromBlocking(true)
                    .chatMemoryRetrieveSize(取出多少条)
                    .conversationId(会话id)
                    .build();
      ChatClient chatClient = ChatClient.builder(chatModel)
                    .defaultAdvisors(chatMemoryAdvisor) //向量库版的历史对话
                    .build();

3.RAG: 检索增强生成 将业务数据存储在向量数据库中(做相似性检索)通过RAG进行链接

在这里插入图片描述

对文件进行向量化
    @Autowired
    @Qualifier("secondaryJdbcTemplate")
    private JdbcTemplate jdbcTemplate;

    //引入嵌入模型
    @Autowired
    @Qualifier("ollamaEmbeddingModel")
    private EmbeddingModel embeddingModel;
    
    /**
     * 创建向量存储的通用方法
     */
    private PgVectorStore.Builder createVectorStoreBuilder(JdbcTemplate jdbcTemplate, 
                                                        EmbeddingModel embeddingModel,
                                                        String tableName) {
        Assert.notNull(jdbcTemplate, "JdbcTemplate 不能为空");
        Assert.notNull(embeddingModel, "EmbeddingModel 不能为空");
        Assert.hasText(tableName, "表名不能为空");
        
        return PgVectorStore.builder(jdbcTemplate, embeddingModel)
                .dimensions(dimensions)
                .vectorTableName(tableName);
    }

	public Result saveVectorStoreByType(String filePath, String dfileFid) {
        try {
            // 检查文件是否存在
            File file = new File(filePath);
            if (!file.exists()) {
                return Result.error("文件不存在: " + filePath);
            }
            Resource resource = new FileSystemResource(filePath);
            TikaDocumentReader reader = new TikaDocumentReader(resource);
            List<Document> documentList = reader.read();

            // 创建文本分割器并配置智能参数
            TextSplitter textSplitter = TokenTextSplitter.builder()
                    .withChunkSize(300)           // 适中的块大小
                    .withMinChunkSizeChars(100)   // 最小块字符数
                    .withKeepSeparator(true)      // 保留分隔符
                    .withMaxNumChunks(1000)       // 最大块数量限制
                    .build();

            // 应用智能分块
            List<Document> splitDocuments = textSplitter.apply(documentList);

            // 对分块结果进行后处理,确保质量
            List<Document> processedDocuments = new ArrayList<>();
            for (Document doc : splitDocuments) {
                String text = doc.getText().trim();
                
                // 跳过过短的块
                /*if (text.length() < 50) {
                    continue;
                }*/
                
                // 处理过长的块
                if (text.length() > 1000) {
                    // 在适当的位置进行二次分割
                    List<String> subChunks = splitLongText(text);
                    for (String subChunk : subChunks) {
                        if (subChunk.trim().length() >= 50) {
                            Map<String, Object> metadata = new HashMap<>(doc.getMetadata());
                            metadata.put("knowledgeId", dfileFid);//知识库id
                            processedDocuments.add(new Document(subChunk.trim(), metadata));
                        }
                    }
                } else {
                    // 添加元数据
                    Map<String, Object> metadata = new HashMap<>(doc.getMetadata());
                    metadata.put("knowledgeId", dfileFid);//知识库id
                    processedDocuments.add(new Document(text, metadata));
                }
            }

            VectorStore vectorStore = vectorStoreFactory.createVectorStoreByTableName(jdbcTemplate, embeddingModel, "vector_store");
            vectorStore.add(processedDocuments);

            log.info("成功将文件加载到向量数据库,共 {} 个文档片段", processedDocuments.size());
            return Result.ok("成功将文件 " + file.getName() + " 加载到向量数据库,共 " + processedDocuments.size() + " 个文档片段");
        } catch (Exception e) {
            log.error("保存向量存储失败", e);
            throw new RuntimeException("保存向量存储失败: " + e.getMessage());
        }
    }

ollamaEmbeddingModel 注入的是 ai.ollama.embedding.model: bge-m3 模型

当将数据向量化后,创建获取

   /**
     * 创建向量存储的通用方法
     */
    private PgVectorStore.Builder createVectorStoreBuilder(JdbcTemplate jdbcTemplate, 
                                                        EmbeddingModel embeddingModel,
                                                        String tableName) {
        Assert.notNull(jdbcTemplate, "JdbcTemplate 不能为空");
        Assert.notNull(embeddingModel, "EmbeddingModel 不能为空");
        Assert.hasText(tableName, "表名不能为空");`在这里插入代码片`
        
        return PgVectorStore.builder(jdbcTemplate, embeddingModel)
                .dimensions(dimensions)
                .vectorTableName(tableName);
    }
  	
  	@Bean
    @Qualifier("VectorStore ")
    public VectorStore enterpriseVectorStore(@Qualifier("secondaryJdbcTemplate") JdbcTemplate jdbcTemplate,
                                      		 @Qualifier("ollamaEmbeddingModel") EmbeddingModel embeddingModel) {
        try {
            return createVectorStoreBuilder(jdbcTemplate, embeddingModel, "vector_store")
                    .build();
        } catch (Exception e) {
            throw new RuntimeException("创建向量库失败", e);
        }
    }
  1. 从向量库中查询相关信息 转为提示词给ai
    // 从向量库中查询相关信息
    private String getVectorStoreStr(String message) {
        List<String> relevantInfo = new ArrayList<>();
        try {
            List<Document> similarDocs = enterpriseVectorStore.similaritySearch(message);
            for (Document doc : similarDocs) {
                relevantInfo.add(doc.getText());
            }
            log.error("从向量库获取到{}条相关信息", relevantInfo.size());
        } catch (Exception e) {
            log.error("向量库查询失败,将继续使用原始对话: {}", e.getMessage());
        }
        // 如果找到相关信息,添加到系统提示中
        StringBuilder context = new StringBuilder();
        String systemPrompt="";
        if (!relevantInfo.isEmpty()) {
            context.append("以下是一些相关的背景信息,请参考这些信息来回答问题:\n\n");
            for (String info : relevantInfo) {
                context.append("- ").append(info).append("\n");
            }
            context.append("\n请基于以上信息,结合自己的知识来回答用户的问题。如果上述信息不足以完整回答问题,可以使用自己的知识进行补充。");

            systemPrompt=context.toString();
        }
        return systemPrompt;
    }

	//放入预设角色中
 	ChatClient chatClient = ChatClient.builder(ollamaChatModel)
                .defaultSystem(getVectorStoreStr(message))
                .build();



  1. 创建检索增强顾问
    private Advisor getSearchEnhancementConsultant(VectorStore vectorStore,List<String> knowledge) {
        // 1. 构建复杂的文档过滤条件
        var b = new FilterExpressionBuilder();
        Object[] array = knowledge.toArray();
        // 使用in条件,匹配指定的知识库ID列表
        var filterExpression = b.in("knowledgeId", array);

        // 2. 配置文档检索器
        // 设置相似度阈值和返回文档数量,同时应用过滤条件
        DocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
                .vectorStore(vectorStore)
                .similarityThreshold(0.5)    // 设置相似度阈值,大于0.5的文档才会被返回
                .topK(3)                     // 返回相似度最高的前3个文档
                .filterExpression(filterExpression.build())
                .build();

        // 3. 创建并配置检索增强顾问
        // allowEmptyContext设置为true,确保在没有找到相关文档时也能回答问题
        Advisor advisor = RetrievalAugmentationAdvisor.builder()
                .queryAugmenter(ContextualQueryAugmenter.builder()
                        .allowEmptyContext(true)        // 允许空上下文,当没有找到相关文档时也会回答
                        .build())
                .documentRetriever(retriever)
                .build();

        return advisor;
    }

    chatClient.prompt()
              .user(message)
              .advisors(advisor)  // 只有在开启向量库时才添加检索增强顾问  // 添加检索增强顾问     
              .stream()
              .content()
              .onErrorResume(e -> {
                   log.error("聊天过程发生错误: ", e);
                   return Flux.just("抱歉,处理您的请求时出现了错误,请稍后重试。");
               });
  1. 使用ai重写查询后再去向量库查询
    private Advisor getSearchEnhancementConsultant2(ChatClient chatClient, VectorStore vectorStore) {
        Advisor retrievalAdvisor = RetrievalAugmentationAdvisor.builder()
                .queryTransformers(
                        RewriteQueryTransformer.builder()
                                .chatClientBuilder(chatClient.mutate())//使用ai优化查询
                                .build()
                )
                .documentRetriever(
                        VectorStoreDocumentRetriever.builder()
                                .similarityThreshold(0.50)
                                .vectorStore(vectorStore)
                                .build()
                )
                .build();
        return retrievalAdvisor;
    }

    chatClient.prompt()
              .user(message)
              .advisors(advisor)  // 只有在开启向量库时才添加检索增强顾问  // 添加检索增强顾问     
              .stream()
              .content()
              .onErrorResume(e -> {
                   log.error("聊天过程发生错误: ", e);
                   return Flux.just("抱歉,处理您的请求时出现了错误,请稍后重试。");
               });

4.Function Calling 用来调用自己的api (这个官网上淘汰了)

    @Bean
    @Description("处理机票退订")//告诉ai什么时候调用这个方法
    public Function<CancelBookingRequest, String> cancelBooking() {
        return cancelBookingRequest -> {
            //在这调用方法
            flightBookingService.cancelBooking(cancelBookingRequest.bookingId(),cancelBookingRequest.name() );
            return "退订成功";
        };
    }
	
	ChatClient chatClient = ChatClient.builder(chatModel)
                    .defaultFunctions("cancelBooking") //这指定的是调用那些方法的bean的名称 多个用逗号分割
                    .build();

5.Tools (这个有个问题调用工具后返回就不是流式的了)

	public class ToolsFactory {
    	@Tool(description = "获取用户的数量")
    	void getUserCount() {
        	System.out.println("ai获取用户的数量");
    	}
	}

	chatClient.prompt()
          	  .user(message)
              .tools(new ToolsFactory()) 加上这个工具后 流式返回就失效了
              .stream()
              .content()
              .onErrorResume(e -> {
                  log.error("聊天过程发生错误: ", e);
                  return Flux.just("抱歉,处理您的请求时出现了错误,请稍后重试。");
              });