鱼皮的AI(对java接入大模型进行扩展)

ChatModel 会话大模型
ChatClient 会话客户端->需要指定会话大模型,可以调用大模型进行会话

官方提供SDK方式调用大模型
package com.server.demo;// 建议dashscope SDK的版本 >= 2.12.0
import java.util.Arrays;
import java.lang.System;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.common.Message;
import com.alibaba.dashscope.common.Role;
import com.alibaba.dashscope.exception.ApiException;
import com.alibaba.dashscope.exception.InputRequiredException;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.JsonUtils;

public class SDKtoAI {
    public static GenerationResult callWithMessage() throws ApiException, NoApiKeyException, InputRequiredException {
        Generation gen = new Generation();
        Message systemMsg = Message.builder()
                .role(Role.SYSTEM.getValue())
                .content("You are a helpful assistant.")
                .build();
        Message userMsg = Message.builder()
                .role(Role.USER.getValue())
                .content("阿里巴巴创建多久了?")
                .build();
        GenerationParam param = GenerationParam.builder()
                // 若没有配置环境变量,请用百炼API Key将下行替换为:.apiKey("sk-xxx")
                .apiKey(System.getenv("OPENAI_API_KEY"))
                // 此处以qwen-plus为例,可按需更换模型名称。模型列表:https://help.aliyun.com/zh/model-studio/getting-started/models
                .model("qwen-plus")
                .messages(Arrays.asList(systemMsg, userMsg))
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .build();
        return gen.call(param);
    }
    public static void main(String[] args) {
        try {
            GenerationResult result = callWithMessage();
            System.out.println(JsonUtils.toJson(result));
        } catch (ApiException | NoApiKeyException | InputRequiredException e) {
            // 使用日志框架记录异常信息
            System.err.println("An error occurred while calling the generation service: " + e.getMessage());
        }
        System.exit(0);
    }
}

http方式调用大模型
(curl工具:可以通过命令行构造http请求,接收响应)
package com.server.demo;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONArray;
public class HttptoAI {

    private static final String API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation";
    private static final String API_KEY = System.getenv("OPENAI_API_KEY"); // 请替换为您的实际API密钥

    public static void main(String[] args) {
        // 构建请求体
        JSONObject requestBody = new JSONObject();
        requestBody.put("model", "qwen-plus");

        // 构建input对象
        JSONObject input = new JSONObject();
        JSONArray messages = new JSONArray();

        // 添加system消息
        JSONObject systemMessage = new JSONObject();
        systemMessage.put("role", "system");
        systemMessage.put("content", "You are a helpful assistant.");
        messages.add(systemMessage);

        // 添加user消息
        JSONObject userMessage = new JSONObject();
        userMessage.put("role", "user");
        userMessage.put("content", "介绍一下长城,限定50字以内");
        messages.add(userMessage);
        input.put("messages", messages);
        requestBody.put("input", input);

        // 构建parameters对象
        JSONObject parameters = new JSONObject();
        parameters.put("result_format", "message");
        requestBody.put("parameters", parameters);

        // 发送HTTP请求
        HttpResponse response = HttpRequest.post(API_URL)
                .header("Authorization", "Bearer " + API_KEY)
                .header("Content-Type", "application/json")
                .body(requestBody.toString())
                .execute();

        // 处理响应
        if (response.isOk()) {
            System.out.println("请求成功!");
            System.out.println("响应内容:" + response.body());
        } else {
            System.out.println("请求失败!");
            System.out.println("状态码:" + response.getStatus());
            System.out.println("错误信息:" + response.body());
        }
    }
}

SpringAI框架调用大模型(对tokenblog.cn上的内容进行补充)
package com.server.demo;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.boot.CommandLineRunner;

public class SpringAItoAI implements CommandLineRunner {

    @Resource
    private ChatModel dashScopeChatClient;

    public void run(String... args) throws Exception {
        AssistantMessage output = dashScopeChatClient.call(new Prompt("你是谁"))
                .getResult()
                .getOutput();
        System.out.println(output.getText());
    }
}


chatModel   大模型
chatClient  对话客户端()
chatResponse  返回结果对象

Langchain4J框架调用大模型

Prompt工程
优化提示词
减少无用的上下文节约token
样本学习:提供样本数据,让ai根据样本生成内容
提供外部资源库,增加可靠性

Advisors(顾问) 拦截器
构建时指定默认拦截器
public Love(ChatModel dashScopeChatClient) {
ChatMemory chatMemory=new InMemoryChatMemory();
chatClient=ChatClient.builder(dashScopeChatClient)
.defaultSystem(prompt)
.defaultAdvisors(
new MessageChatMemoryAdvisor(chatMemory) //创建使用对话记忆拦截器
// ,new SimpleLoggerAdvisor(0)
)
.build();
// FileBaseChatMemory FileBaseChatMemory=new FileBaseChatMemory(“D://tmp1”);
// chatClient=ChatClient.builder(dashScopeChatClient)
// .defaultSystem(prompt)
// .defaultAdvisors(
// new MessageChatMemoryAdvisor(FileBaseChatMemory)
//// ,new SimpleLoggerAdvisor(0)
// )
// .build();
}

MessageChatMemoryAdvisor(ChatMemory chatMemory)    会将对话历史作为一系列独立的消息添加到提示中
ChatMemory    存储对话历史消息的组件,对历史消息进行增删查
    InMemoryChatMemory  内存存储
自定义拦截器:
    package com.server.chatMemory;
    import com.esotericsoftware.kryo.Kryo;
    import com.esotericsoftware.kryo.io.Input;
    import com.esotericsoftware.kryo.io.Output;
    import org.objenesis.strategy.StdInstantiatorStrategy;
    import org.springframework.ai.chat.memory.ChatMemory;
    import org.springframework.ai.chat.messages.Message;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.List;

    /**
    * 基于文件持久化的对话记忆
    */
    public class FileBaseChatMemory implements ChatMemory {

        private final String BASE_DIR;
        private static final Kryo kryo = new Kryo();

        static {
            kryo.setRegistrationRequired(false);
            // 设置实例化策略
            kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        }

        // 构造对象时,指定文件保存目录
        public FileBaseChatMemory(String dir) {
            this.BASE_DIR = dir;
            File baseDir = new File(dir);
            if (!baseDir.exists()) {
                baseDir.mkdirs();
            }
        }

        @Override
        public void add(String conversationId, List<Message> messages) {
            List<Message> conversationMessages = getOrCreateConversation(conversationId);
            conversationMessages.addAll(messages);
            saveConversation(conversationId, conversationMessages);
        }

        @Override
        public List<Message> get(String conversationId, int lastN) {
            List<Message> allMessages = getOrCreateConversation(conversationId);
            return allMessages.stream()
                    .skip(Math.max(0, allMessages.size() - lastN))
                    .toList();
        }

        @Override
        public void clear(String conversationId) {
            File file = getConversationFile(conversationId);
            if (file.exists()) {
                file.delete();
            }
        }

        private List<Message> getOrCreateConversation(String conversationId) {
            File file = getConversationFile(conversationId);
            List<Message> messages = new ArrayList<>();
            if (file.exists()) {
                try (Input input = new Input(new FileInputStream(file))) {
                    messages = kryo.readObject(input, ArrayList.class);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return messages;
        }

        private void saveConversation(String conversationId, List<Message> messages) {
            File file = getConversationFile(conversationId);
            try (Output output = new Output(new FileOutputStream(file))) {
                kryo.writeObject(output, messages);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        private File getConversationFile(String conversationId) {
            return new File(BASE_DIR, conversationId + ".kryo");
        }
    }

官方拦截器
    日志拦截器(提供一些日志信息)
        SimpleLoggerAdvisor
    重复阅读拦截器(重复提示词信息)
        ReReadingAdvisor
在拦截器中可以获取该链条的其他拦截器数据,使用AdvisedResponse.adviseContext().get()    雷神ThreadLocal的使用方法

结构化输出
Sprirng提供的转换器
MapOutputConverter
BeanOutputConverter
ListOutputConverter
//结构化输出
public User doChatStruct(String message,String chatId){
User user = chatClient.prompt()
.system(“每次会话要为我生成一个标题为{用户名}的恋爱报告”)
.user(message)
.advisors(sp -> sp.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 1))
.call()
.entity(User.class); //指定接收类型,ai会将结果尽可能的转换为目标结构
log.info(user.toString());
return user;
}

.entity()   
    指定接收类型
    new ParameterizedTypeReference<Object>() {}    可以通过该抽象类实现更复杂的目标结构

对话记忆化存储
将会话持久化
将会话持久化到文件系统中
package com.server.chatMemory;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.objenesis.strategy.StdInstantiatorStrategy;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.Message;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* 基于文件持久化的对话记忆
*/
public class FileBaseChatMemory implements ChatMemory {

    private final String BASE_DIR;
    private static final Kryo kryo = new Kryo();

    static {
        kryo.setRegistrationRequired(false);
        // 设置实例化策略
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
    }

    // 构造对象时,指定文件保存目录
    public FileBaseChatMemory(String dir) {
        this.BASE_DIR = dir;
        File baseDir = new File(dir);
        if (!baseDir.exists()) {
            baseDir.mkdirs();
        }
    }

    @Override
    public void add(String conversationId, List<Message> messages) {
        List<Message> conversationMessages = getOrCreateConversation(conversationId);
        conversationMessages.addAll(messages);
        saveConversation(conversationId, conversationMessages);
    }

    @Override
    public List<Message> get(String conversationId, int lastN) {
        List<Message> allMessages = getOrCreateConversation(conversationId);
        return allMessages.stream()
                .skip(Math.max(0, allMessages.size() - lastN))
                .toList();
    }

    @Override
    public void clear(String conversationId) {
        File file = getConversationFile(conversationId);
        if (file.exists()) {
            file.delete();
        }
    }

    private List<Message> getOrCreateConversation(String conversationId) {
        File file = getConversationFile(conversationId);
        List<Message> messages = new ArrayList<>();
        if (file.exists()) {
            try (Input input = new Input(new FileInputStream(file))) {
                messages = kryo.readObject(input, ArrayList.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return messages;
    }

    private void saveConversation(String conversationId, List<Message> messages) {
        File file = getConversationFile(conversationId);
        try (Output output = new Output(new FileOutputStream(file))) {
            kryo.writeObject(output, messages);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private File getConversationFile(String conversationId) {
        return new File(BASE_DIR, conversationId + ".kryo");
    }
}
(难点:持久化存储过程中->历史记录序列化到文档中->文档中的历史记录序列化到内存中)
chatMemory    记忆化存储
自定义chatMemory
    序列化:Kryo工具

RAG(检索增强)
步骤:
1.将数据源存储到数据库(向量数据库或其他)
对原始文档进行加工处理后存储到数据库:原始文档->预处理文档->文档分片->存储到数据库
2.用户输入->AI去RAG数据库中查询(过滤条件)->查询结果进行Rank精排->最终结果作为上下文辅助AI回答问题
混合检索策略:
score阈值,相似度高于阈值的不会被检索,(为相似的设置一个门槛)
1.创建数据源文件
2.将数据源文件加载到内存中,使用SpringAI提供的ETL组件
加载数据源(例如:Markdown):
package com.server.rag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.markdown.config.MarkdownDocumentReaderConfig;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.stereotype.Component;

        import java.util.ArrayList;
        import java.util.List;

        @Component
        @Slf4j
        public class LocalRAG {


            private ResourcePatternResolver resourcePatternResolver;

            public LocalRAG(ResourcePatternResolver resourcePatternResolver) {
                this.resourcePatternResolver = resourcePatternResolver;
            }

            List<Document> getList(){
                List<Document> documents = new ArrayList<>();
                try{
                    Resource[] resources = resourcePatternResolver.getResources("classpath:*.md");
                    for (Resource resource : resources) {
                        String filename = resource.getFilename();
                        MarkdownDocumentReaderConfig config=MarkdownDocumentReaderConfig.builder()
                                .withHorizontalRuleCreateDocument(true)
                                .withIncludeCodeBlock(false)
                                .withIncludeBlockquote(false)
                                .withAdditionalMetadata("filename",filename)
                                .build();
                        MackdownDocumentReader reader=new MackdownDocumentReader(resource,config);
                        documents.addAll(reader.get());
                    }

                }catch (Exception e){
                    log.error("文档加载失败",e);
                }
                return documents;
            }
        }

    注册VectorStore并存储数据:
        package com.server.rag;
        import jakarta.annotation.Resource;
        import org.springframework.ai.document.Document;
        import org.springframework.ai.embedding.EmbeddingModel;
        import org.springframework.ai.vectorstore.SimpleVectorStore;
        import org.springframework.ai.vectorstore.VectorStore;
        import org.springframework.context.annotation.Bean;
        import org.springframework.context.annotation.Configuration;

        import java.util.List;
        @Configuration
        public class LocalRAGconfig {
            @Resource
            private LocalRAG localRAG;

            @Bean
            VectorStore Load(EmbeddingModel dachscopeEmbeddingModel) {
                VectorStore vectorStore=SimpleVectorStore.builder(dachscopeEmbeddingModel).build();
                List<Document> list = localRAG.getList();
                vectorStore.add(list);
                return vectorStore;
            }
        }

    在会话客户端局部配置问答拦截器->参数为VectorStore
        //RAG增强检索
        public User doChatRAG(String message,String chatId){
            User user = chatClient.prompt()
                    .system("每次会话要为我生成一个标题为{用户名}的恋爱报告")
                    .user(message)
                    .advisors(sp -> sp.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
                            .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 1))
                    .advisors(new QuestionAnswerAdvisor(vectorStore))   //配置问答拦截器并配置需要读取的RAG检索资源(VectorStore是一个接口,用于将向量保存到存储引擎)
                    .call()
                    .entity(User.class);
            log.info(user.toString());
            return user;
        }
    测试:
        
特殊:QuestionAnswerAdvisor   问答拦截器(在回答之前先去进行RAG增强处理,再进行问答)

RAG核心特性:
文档收集和切割ETL:
E:
SpringAI的文档是通过Document进行存储的
可以存储MetaData 元信息
可以存储Media 多媒体内容
DocumentReader 文档读取器
T:
TextSplitter 文本分割器,基类
split() 分割文本的api
TokenTextSplitter 基于token的文本分割器,实现类
MetadataEnricher 元数据增强器,为我们的Document补充更多元信息
KeywordMetadataEnricher 关键词元数据增强器,增加关键字元信息
SummaryMetadataEnricher 使用AI生成文档摘要,并添加到元信息,不仅可以为当前文档生成摘要,还可以关联相邻的文档,让摘要更完整
ContentFormatter 内容格式转换器
DefaultContentFormatter 内容格式转换器实现类,用来统一格式,操作元信息,将内容和元信息结合,排除一些元信息。
L:
DocumentWriter 文档加载(写入)
DocumentWriter实现了Consumer<List>接口,负责将处理后的文档写入到存储中
FileDocumentWriter 将文档写入到文件系统中
VectorStoreWriter 将文档写入到向量数据库中
向量转换和存储
VectorStore接口,继承DocumentWriter接口,主要实现向量的转换和存储
SpringAI提供了SearchRequest类,可以自定义相似度搜索请求(过滤规则)

特殊:批处理策略->避免一次性插入过量的数据
    SpringAI提供了BatchingStrategy接口允许基于标记计数以及分批方式处理文档

模块化的RAG架构:
    预检索:优化用户查询
        SpringAI提供了多种查询处理组件
            查询转换:
                查询重写:RewriteQueryTransformer     调用ai使问题更精确
                查询翻译:TranslationQueryTransformer    调用ai翻译问题
                查询压缩:CompressionQueryTransformer    调用ai压缩问题,根据对话历史和最新的查询,去除一些杂质
                多查询扩展:MultiQueryExpander         调用ai增加一些问题的多个语义的变种,有助于检索额外的上下文,并增加找到相关结果的机会
    检索:提供用户查询的相关性
        文档搜索:
        DocumentRetriever 这是SpringAI提供的文档检索器,可以自定义文档检索器实现类
            VectorStoreDocumentRetriever   从向量存储中检索与语义相相似的文档
                filterExpression   可以灵活指定过滤条件
                Query   也可以构造Query对象的FILTER_EXPRESSION参数动态指定过滤表达式
        文档合并:
        ConcatenationDocumentJoiner   通过连接操作实现的文档合并器
            会将多个查询和多个检索到的文档合并成一个文档,重复的部分只保留首次出现
    检索后:优化文档处理
        优化文档,减少冗余,去除杂质
    查询增强和关联
        QuestionAnswerAdvisor    问答增强拦截器
        RetrievalAugmentationAdviosr     检索增强顾问(不允许检索的上下文为空)
            可以配合RAG架构进行,检索前,检索时,检索后的操作
        ContextualQueryAugmenter   空上下文处理器

最佳实践:
优化原始文档,尽量结构化
文档的分片,推荐使用智能分片,然后人工二次核验
添加元信息
…待补充

高级知识
混合检索策略:
向量检索:根据语义检索,但关键字不够敏感
倒排索引的全文检索:在精确匹配关键字上效果出色,但不能识别语义
并行混合检索:
向量检索与全文检索同时检索,最后融合结果
级联混合检索:
一级一级检索,多次不同检索,每次使用的检索技术可以动态优化
动态混合检索:
使用分析器智能选择检索技术
大模型幻觉:
大模型没有正确的标准,可能会胡言乱语,这和大模型的训练有关
评估和错误改进:
通常有一套系统用来评估大模型的幻觉,通过打分的方式,找出表现不好的地方改进
高级RAG架构:
自纠错RAG:能够搜集信息验证给出的答案
自省式RAG:定义知识搜集策略,提高系统效率
检索树RAG:根据语义拆分问题,分别检索,最后将结果合并
多智能体RAG:将用户提问通过策略分配给擅长的智能体回答,智能体直接协同处理