● 火山引擎

通过火山引擎的api去调用ai模型
流程:

  • 创建key拿到key和对应模型名称
  • 调用官方文档的教程调用目标模型的api
  • 拿到响应结果
  • 如果要做上下文处理则可以用redis缓存把用户的对话记录历史存到redis,下一次用户再对话就可以把历史记录最近的两三条以及ai的答复一起发给ai,这样就会增大tokens数,这就是为什么上下文越多花钱越多的原因

【火山引擎官方文档】
SDK依赖:

<dependency>
  <groupId>com.volcengine</groupId>
  <artifactId>volcengine-java-sdk-ark-runtime</artifactId>
  <version>LATEST</version>
</dependency>

标准使用:

	@Resource
    private AiApiProperties aiApiProperties;
    
/**
     * 调用ai的api模型返回响应结果
     */
    @PostMapping
    public AjaxResult<Object> useAI(@RequestBody AIRequestDTO aiRequestDTO) {
        log.info("AI运行中, 消息:{}", aiRequestDTO.getMessage());
        // 创建ArkService实例
        ArkService arkService = ArkService.builder().apiKey(aiApiProperties.getArkApiKey()).baseUrl("https://ark.cn-beijing.volces.com/api/v3").build();
        CreateResponsesRequest request = CreateResponsesRequest.builder()
                .model(aiApiProperties.getModel())
                .input(ResponsesInput.builder().addListItem(
                        ItemEasyMessage.builder().role(ResponsesConstants.MESSAGE_ROLE_USER).content(
                                MessageContent.builder()
                                        .addListItem(InputContentItemText.builder().text(aiRequestDTO.getMessage()).build())
                                        .build()
                        ).build()
                ).build())
                .build();
        ResponseObject resp = arkService.createResponse(request);
        arkService.shutdownExecutor();
        log.info("AI结果:{}", resp);
        return AjaxResult.success(resp);
    }

● 上下文处理

上下文处理的原理就是存历史记录,比如一个用户第一次访问ai的接口就记录他这一次的对话存入缓存,下一次就带着这缓存+新的对话发送给ai

超级简化版:如果在精进就是弄一个上下文管理器类管理或者是把历史记录缓存交给redis

/**
     * 存放用户对话历史记录
     */
    private final ConcurrentHashMap<Long, List<HistoryChat>> history = new ConcurrentHashMap<>();

    /**
     * 调用ai的api模型返回响应结果
     */
    @PostMapping
    public AjaxResult<Object> useAI(@RequestBody AIRequestDTO aiRequestDTO) {
        log.info("AI运行中, 消息:{}", aiRequestDTO.getMessage());
        LoginBodyDetails user = (LoginBodyDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        String historyMsg = "";

        //获取历史记录
        if (!history.isEmpty()) {
            HistoryChat historyChat = history.get(user.getUserId()).stream().max(Comparator.comparing(HistoryChat::getTime)).orElse(null);
            historyMsg = Objects.requireNonNull(historyChat).getMessage();
        }

        // 创建ArkService实例
        ArkService arkService = ArkService.builder().apiKey(aiApiProperties.getArkApiKey()).baseUrl("https://ark.cn-beijing.volces.com/api/v3").build();
        CreateResponsesRequest request = CreateResponsesRequest.builder()
                .model(aiApiProperties.getModel())
                .input(ResponsesInput.builder().addListItem(
                        ItemEasyMessage.builder().role(ResponsesConstants.MESSAGE_ROLE_USER).content(
                                MessageContent.builder()
                                        //放入历史记录和新对话
                                        .addListItem(InputContentItemText.builder().text(historyMsg + aiRequestDTO.getMessage()).build())
                                        .build()
                        ).build()
                ).build())
                .build();
        ResponseObject resp = arkService.createResponse(request);
        arkService.shutdownExecutor();
        log.info("AI结果:{}", resp);

        //存入
        if (!history.containsKey(user.getUserId())) {
            history.put(user.getUserId(), new ArrayList<>());
        }

        //存放历史记录
        List<HistoryChat> historyChats = history.get(user.getUserId());

        //如果满了就清理最旧的
        if (historyChats.size() == 3) {
            HistoryChat historyChat = historyChats.stream().min(Comparator.comparing(HistoryChat::getTime)).orElse(null);
            historyChats.remove(historyChat);
        }

        historyChats.add(HistoryChat.builder().message(aiRequestDTO.getMessage()).time(new Date()).build());
        return AjaxResult.success(resp);
    }

● 限流处理[令牌桶算法机制]

令牌桶机制:
接口限流,当多个用户访问一个接口时要做限流控制,防止访问量太多导致崩溃
设置一个RateLimiter以固定速率生成令牌,比如一秒10个令牌,每个用户访问时先看令牌有没有,获取令牌,如果令牌存在则消耗掉令牌执行业务,如果没有则等待或报错

RateLimiter底层线程安全

 /**
     * 基于接口的限流令牌桶
     */
    RateLimiter productDetailLimiter = RateLimiter.create(10.0); // 每秒生成10个令牌

/**
     * 调用ai的api模型返回响应结果
     */
    @PostMapping
    public AjaxResult<Object> useAI(@RequestBody AIRequestDTO aiRequestDTO) {
        // 尝试获取令牌
        if (!productDetailLimiter.tryAcquire()) {
            throw new RuntimeException("当前访问人数过多,请稍后重试");
        }

● Spring AI

SpringAI就相当于一个把以上步骤封装起来的框架,类似于把JDBC的操作封装成Mybatis一样,他就是一个简化开发的框架

阿里百炼的依赖导入与SpringAI的配置:

<spring-ai.version>1.0.0-M6</spring-ai.version>

<!--Spring AI-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-bom</artifactId>
    <version>${spring-ai.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

<!--Spring AI适配阿里百炼-->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>

yml配置:

  #Spring AI
spring:
   ai:
   openai:
     api-key: sk-XXXXXXXXXXXXXXXXX
     base-url: https://dashscope.aliyuncs.com/compatible-mode
     chat:
       options:
         model: deepseek-v3  # 指定使用DeepSeek-V3.2模型

配置类:提供一个全局单例的ChatCilent配置类:

@Configuration
public class SpringAIConfig {

    @Bean
    public ChatClient getChatClient(OpenAiChatModel chatModel) {
        return ChatClient
                .builder(chatModel)
                .build();
    }
}

简单实现:

@RestController
@RequestMapping("/ai/chat")
public class APIController {

    @Resource
    private ChatClient chatClient;

    @GetMapping
    public AjaxResult<String> chat() {
        String result = chatClient.prompt()
        		.defaultSystem("提示词") //提示词工程 会将这个提示词封装为一个Message一起发给目标ai
                .user("你好 你是谁")
                .call()
                .content();
        return AjaxResult.success(result);
    }
}

○ SpringAI的环绕增强器

Spring AI内部内置了一个环绕增强AOP,它的作用就是拦截用户请求目标ai模型时的请求数据,以及ai模型处理完数据之后返回到后端之前的数据。

流程:

  • 后端服务器接收前端用户的prompt信息,建立ChatClient会话准备发给目标AI模型
  • 拦截器Advisor介入后将这个请求在未到达目标模型之前拦截
  • 对这个会话进行预处理、数据转换、记忆检索等功能
  • 随后把这个请求发给目标AI
  • AI模型处理完之后返回数据结果
  • 这时候Advisor拦截器再次拦截返回的数据进行后续处理
    在这里插入图片描述
    Advisor接口有三个内置的实现类:
    会话日志管理:

△ SimpleLoggerAdvisor简单日志功能

记录与AI对话时的日志,底层是实现了CallAroundAdvisor接口,主要是使用SLF4J日志框架打印日志信息

@Bean
    public ChatClient getChatClient(OpenAiChatModel chatModel) {
        return chatModel
        		.builder()
                .defaultAdvisors(new SimpleLoggerAdvisor()) //会话日志打印
                .build();
    }

yml配置日志:

logging:
  level:
    org.springframework.ai.chat.client.advisor: debug
    com.note.controller.ChatController: debug

△ MessageChatMemoryAdvisor会话记忆功能

会话记忆就是记录用户每次的聊天会话存储,先记录使用Spring AI内置的一个InMemoryChatMemory实现类,它实现了ChatMemory接口,他底层使用Map集合把会话记忆存储到内存中,还可以使用自己实现ChatMemory接口让会话记忆存储到redis等数据库中
流程是:创建ChatMemory的Bean然后配置类中配置,在接口中接收对话窗口id,根据对话窗口id进行存储用户会话记忆到InMemoryChatMemory的map集合中
创建ChatMemory的Bean:
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))这句代码的作用就是告诉springAI框架把这个chatId当做key存储到会话记忆的上下文中,以及把用户对话信息以及一些额外信息当做value
下次用户进行对话时,advisor拦截器会从上下文中查找这个chat为key的map数据然后把这些数据拼合用户对话一起发给ai模型,这就是上下文的功能

@Bean
    public ChatClient getChatClient(OpenAiChatModel chatModel, ChatMemory chatMemory) {
        return chatModel.builder()
                .defaultAdvisors(new MessageChatMemoryAdvisor(chatMemory)) //会话记忆功能
                .defaultAdvisors(new SimpleLoggerAdvisor()) //会话日志打印
                .build();
    }

    @Bean
    public ChatMemory getChatMemory() {
        return new InMemoryChatMemory();
    }

编写接口:

@Resource
    private ChatClient chatClient;

    /**
     * AI聊天功能
     *
     * @param prompt 用户信息
     * @return 响应式结果
     */
    @PostMapping(value = "/chat", produces = "text/html;charset=utf-8")
    public Flux<String> chat(@RequestParam(value = "prompt") String prompt, @RequestParam(value = "chatId") String chatId) {
        System.out.println(chatId);
        return chatClient.prompt()
                .user(prompt)
                .advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId))
                .stream()
                .content();
    }

因为用户进行对话之后,再次刷新页面,这个会话的窗口内的对话内容也会被刷新,这时候就需要重新获取会话历史,可以从InMemoryChatMemory内获取对应的chatId然后获取它对应的Message

ChatMemory接口:
当一次用户对话时,这个Advisor拦截器会自动调用ChatMemory的add方法把chatId为key,把用户的对话为value,这个对话会封装成一个Message接口的实现类对象,他会把用户的对话user(MessageType)存到这个对象、系统回复Assistant(MessageType)存到另一个对象,形成一个List<Message>的形式,这个Message对象主要存会话类型(ConversationId)、会话内容(text)

Message的会话类型(MessageType):
public enum MessageType {
    USER("user"),
    ASSISTANT("assistant"),
    SYSTEM("system"),
    TOOL("tool");
public interface ChatMemory {
    default void add(String conversationId, Message message) {
        this.add(conversationId, List.of(message));
    }

    void add(String conversationId, List<Message> messages);

    List<Message> get(String conversationId, int lastN);

    void clear(String conversationId);
}

△ 会话记忆存储流程:

  • 用户输入信息发给接口,准备调用chatClient.prompt()方法发给目标ai模型
  • prompt()链式调用user()和system(),生成Message集合,一个user一个system
  • 被Advisor的AOP环绕拦截该请求
  • 从Prompt对象的Message集合中拿到user以及他的text,system以及他的text,存到InMemoryChatMemory的底层map集合中key是chatId
  • 当这个请求经过ai之后返回响应时
  • 这个请求再次被Advisor拦截,从中找到他的Message集合的Assistant以及他的text存到InMemoryChatMemory中
  • 当用户切换页面再次回到本次对话窗口中时,会从ChatMemory中也就是其实现类InMemoryChatMemory中根据ChatId获取Message集合,这个InMemoryChatMemory底层的value是按照时间排序的也就是FIFO先进先出原则,所以再次拿到这个集合时就会有序的返回
  • 将Message集合做一次封装进行返回
/**
     * 保存会话id根据会话type
     * 从ChatMemory根据chatId获取会话Message集合
     * 将Message集合封装给前端
     *
     * @param chatId 会话id
     * @param type   会话type
     */
    @GetMapping("/{type}/{chatId}")
    public List<MessageVo> saveHistory(@PathVariable("chatId") String chatId, @PathVariable("type") String type) {
        List<Message> messages = chatMemory.get(chatId, Integer.MAX_VALUE);
        if (messages == null) {
            return new ArrayList<>();
        }
        return messages.stream().map(MessageVo::new).toList();
    }

用户输入信息

发送到后端接口

调用chatClient.prompt方法

链式调用user和system方法

生成Message集合
包含user和system消息

被Advisor AOP拦截

从Prompt对象提取消息

获取user和system的text内容

存储到InMemoryChatMemory

Map结构: key=chatId, value=Message集合

发送到AI模型处理

AI返回响应

再次被Advisor拦截

提取Assistant消息

存储Assistant消息到InMemoryChatMemory

返回响应给前端

用户切换页面后返回

根据chatId从ChatMemory获取消息

InMemoryChatMemory底层FIFO排序

有序返回Message集合

前端封装并展示

○ 工具调用(Function Calling / Tool Calling)

SpringAI的Calling就相当于他自己可以操作外部系统,比如数据库、调用一些外部api等,这样就可以根据提示词让ai收集用户信息到数据库进行CRUD,只需要写一个tools类加个@Tool注解,写一下description描述一下这个方法就可以让ai框架调用

流程:

  • 编写提示词,用对话形式收集用户信息
  • 注册Tools:.defaultTools(courseTools)
  • 编写Tools类,使用@Tool注解到需要被调用的业务方法
  • 如果方法参数是对象虽然springAI可以自动处理,也可以加一下@ToolParam(description = “”)来描述这个类的属性让ai理解更准确
  • 这样通过收集的用户信息springAI框架就会调用Tools类的方法到数据库进行CRUD

底层:

  • SpringAI会自动扫描带有@Tool注解的方法
  • 把这些方法翻译成AI框架能读懂的Json Schema格式,然后当用户跟ai对话时,springAI框架会将这些Schema格式的json数据拼接到请求体中发送给目标ai模型
  • 模型决策阶段:大模型接收到这些数据后,他会根据用户的语义来判断需不需要调用这些业务方法,如果需要调用他并不会直接去调用而是会返回一个新的Json Schema格式的数据(tool call响应)
  • spring框架解析tool call响应信息,解析arguments字符串调用指定的方法
  • AI生成最终答复

比如:

@Tool(description = "根据条件查询课程")
    public List<Course> queryCourse(@ToolParam(required = false, description = "课程查询条件") CourseQuery query) {
        QueryChainWrapper<Course> wrapper = courseService.query();
        wrapper
                .eq(query.getType() != null, "type", query.getType())
                .le(query.getEdu() != null, "edu", query.getEdu());
        if(query.getSorts() != null) {
            for (CourseQuery.Sort sort : query.getSorts()) {
                wrapper.orderBy(true, sort.getAsc(), sort.getField());
            }
        }
        return wrapper.list();
    }

翻译:

{
  "type": "function",
  "function": {
    "name": "queryCourse",
    "description": "根据条件查询课程",
    "parameters": {
      "type": "object",
      "properties": {
        "query": {
          "type": "object",
          "description": "课程查询条件",
          "properties": {
            "type": {
              "type": "string",
              "description": "课程类型:编程、设计、自媒体、其它"
            },
            "edu": {
              "type": "integer",
              "description": "学历要求:0-无、1-初中、2-高中、3-大专、4-本科及本科以上"
            },
            "sorts": {
              "type": "array",
              "description": "排序方式",
              "items": {
                "type": "object",
                "properties": {
                  "field": {
                    "type": "string",
                    "description": "排序字段: price或duration"
                  },
                  "asc": {
                    "type": "boolean",
                    "description": "是否是升序: true/false"
                  }
                }
              }
            }
          },
          "required": []
        }
      },
      "required": []
    }
  }
}

tool call响应数据格式:

{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1677652288,
  "model": "deepseek-v3.2",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_abc123",
            "type": "function",
            "function": {
              "name": "queryCourse",
              "arguments": "{\"query\": {\"type\": \"编程\", \"sorts\": [{\"field\": \"price\", \"asc\": true}]}}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls" //表示ai需要调用工具如果是stop则表示不需要
    }
  ]
}


"tool_calls": [
  {
    "id": "call_abc123",           // 工具调用的唯一ID
    "type": "function",            // 固定为 "function"
    "function": {
      "name": "queryCourse",       // 要调用的工具名
      "arguments": "..."           // JSON 字符串格式的参数
    }
  }
]

后端服务器调用工具之后将数据返回给AI模型:

应用程序发送给 AI{
  "role": "tool",
  "content": "查询到3门编程课程:1. Java基础(价格:1000元) 2. Python入门(价格:1200元) 3. Web开发(价格:1500元)",
  "tool_call_id": "call_123"
}

○ 检索知识库向量(RAG)

RAG向量模型就是根据一些文档或者网站信息资源的信息查询来增强自己的知识库,回答用户的问题更加准确,这个向量模型就是把这些文档或者网站的字数都切割成部分,然后把这些部分转为N维向量,用户问题也转换为N维向量,将用户的向量与知识库的向量进行相似度匹配从而找到文档的部分片段来进行知识增强

如果没有RAG向量模型,只根据文档或者网站来检索和匹配用户的问题,那么文档的数据数量会非常膨胀从而压迫性能

流程:

  • 读取文档
  • 切割文档成片段部分
  • Embedding模型将片段化为指定的N维向量,存入向量数据库(redis等)
  • 用户提问,将用户问题转换为N维向量
  • 知识库匹配,根据用户问题向量与知识库的向量做相似度匹配(余弦相似度等)
  • 根据检索出的向量找到具体的文档片段,拼接成上下位(此时都在Advisor拦截器进行,请求还没发给目标AI)
  • 最后将请求发给AI

使用流程(基于SimpleVectorStore内存向量数据库):
配置向量数据库:

@Bean
    public VectorStore getVectorStore(OpenAiEmbeddingModel openAiEmbeddingModel) {
        return SimpleVectorStore.builder(openAiEmbeddingModel).build();
    }

导入读取PDF文件的依赖:

 <!--SpringAI读取pdf文件-->
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-pdf-document-reader</artifactId>
        </dependency>

△ QuestionAnswerAdvisor检索增强生成(RAG实现)

暂略

[SpringAI笔记项目]

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐