启程

Spring AI中文文档

Spring AI 学习环境

开发工具:IntelliJ IDEA Ultimate 或 VSCode
Java 环境:JDK 17 或更高版本
构建工具:Maven 3.8+ 或 Gradle 7.0+
容器环境:Docker Desktop 和 Kubernetes
AI 服务:OpenAI、Azure OpenAI 或其他 AI 服务账号
📖 参考资料
官方文档:Spring AI 官方文档和 API 参考
技术博客:Spring 官方博客和技术文章
开源项目:GitHub 上的 Spring AI 示例项目
技术社区:Stack Overflow、Reddit、Discord 等技术社区
学习平台:Coursera、Udemy 等在线学习平台的 AI 课程

AI的主要分支

  1. 机器学习(Machine Learning):使计算机能够从数据中学习,而无需明确编程。
  2. 深度学习(Deep Learning):基于人工神经网络的机器学习子集,能处理更复杂的数据模式。
  3. 自然语言处理(NLP):使计算机能够理解、解释和生成人类语言。
  4. 计算机视觉(Computer Vision):使计算机能够”看到”并理解视觉信息。
  5. 机器人学(Robotics):研究设计、构建和操作机器人的学科。

大语言模型(LLM)

大语言模型(Large Language Models,简称LLM)基于深度学习的模型,通过分析大量文本数据学习语言模式和知识。
LLM的基本原理

  1. 预训练-微调范式:
    ·预训练:模型在大量通用文本上进行训练,学习语言的基本结构和知识。
    ·微调:在特定任务的数据集上进一步训练,使模型适应特定领域或任务。
  2. Transformer架构:
    ·大多数现代LLM基于Transformer架构,这是一种特别适合处理序列数据的神经网络结构。
    ·Transformer的核心是自注意力机制,使模型能够捕捉句子中不同单词之间的关系。
  3. 大规模参数:
    ·现代LM通常包含数十亿甚至数千亿参数。
    ·参数越多,模型的能力通常越强,但训练和运行成本也越高。
  4. 自监督学习:
    ·LLM主要通过预测下一个单词或填补缺失单词等任务进行训练。
    ·这种方法不需要人工标注的数据,能够利用互联网上海量的文本资源。

AI 术语

  • 提示词(Prompt)
    提示词是用户向AI模型提供的输入文本,用于指导模型生成期望的输出。
    一个好的提示词可以显著提高模型输出的质量和相关性。
1
2
String prompt="请为剪切助手应用编写一个简短的产品描述,强调其跨设备同步功能。"
String response aiModel.generateText(prompt);
  • 模型(Model)
    AI模型是经过训练的算法,能够执行特定任务。(大型语言模型)

  • 标记(Token)
    标记是LLM处理文本的基本单位。一个标记可能是一个单词、单词的一部分或标点符号。
    理解标记对于管理模型输入限制和成本计算很重要。

  • 嵌入(Embedding)
    嵌入是将文本、图像等数据转换为密集的数值向量,使A!系统能够处理和理解这些数据。
    文本嵌入在搜索、推荐系统和文本分类中特别有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 使用嵌入的简化示例
public class TextEmbedding {
private EmbeddingModel model;

public TextEmbedding(EmbeddingModel model) {
this.model = model;
}

public double[] getEmbedding(String text) {
// 将文本转换为嵌入向量
return model.embed(text);
}

public double calculateSimilarity(String text1, String text2) {
double[] embedding1 = getEmbedding(text1);
double[] embedding2 = getEmbedding(text2);

// 计算余弦相似度
return cosineSimilarity(embedding1, embedding2);
}

private double cosineSimilarity(double[] v1, double[] v2) {
// 实现余弦相似度计算
// ...
return 0.87; // 示例返回值
}
}
  • 温度(Temperature)
    温度是控制AI生成文本随机性的参数。
    较低的温度(接近0)会产生更确定性、保守的输出,而较高的温度(接近1或更高)会产生更多样化、创造性的输出。

  • 微调(Fine-tuning)
    微调是指在通用预训练模型的基础上,使用特定领域的数据进一步训川练,使模型更适合特定任务或领域的过程。

  • 上下文窗口(Context Window)
    上下文窗口是指模型一次能处理的最大标记数。它限制了用户可以提供的输入长度和模型可以生成的输出长度的总和。

  • 推理(Inference)
    推理是指模型使用其学到的模式处理新输入并生成输出的过程。对于LLM,这通常指的是生成文本的过程。

  • RAG(检索增强生成)
    RAG是一种将检索系统与文本生成模型结合的技术,使模型能够访问和利用外部知识源,提高生成内容的准确性和相关性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// RAG 系统的简化示例
public class RAGSystem {
private DocumentStore documentStore;
private EmbeddingModel embeddingModel;
private LLMClient llmClient;

// 构造函数和其他方法...

public String answerQuestion(String question) {
// 1. 将问题转换为嵌入向量
double[] questionEmbedding = embeddingModel.embed(question);

// 2. 检索相关文档
List<Document> relevantDocs = documentStore.searchSimilar(questionEmbedding, 3);

// 3. 构建包含检索到的信息的提示词
StringBuilder contextBuilder = new StringBuilder();
for (Document doc : relevantDocs) {
contextBuilder.append(doc.getContent()).append("\n\n");
}

String prompt = "基于以下信息回答问题:\n\n" +
contextBuilder.toString() +
"\n\n问题:" + question;

// 4. 使用 LLM 生成答案
return llmClient.generateText(prompt);
}
}

Spring 框架

Spring核心框架是整个Spring生态系统的基础.
提供了依赖注入(DI)、面向切面编程(AOP)、事件机制、资源管理等核心功能。

SpringBoot
SpringBoot简化了Spring应用的初始搭建和开发过程。

Spring Data
Spring Data简化了数据访问层的开发,提供了统一的API来访问不同类型的数据存储。

Spring Security
Spring Security提供了全面的安全解决方案,包括认证、授权、防护攻击等功能,
可以保护Web应用、RESTful API以及微服务。

Spring Cloud
Spring Cloud提供了一套工具,用于在分布式系统中快速构建微服务应用。
它包含了服务发现、配置管理、断路器、智能路由、负载均衡等组件。

Spring Integration Spring Batch
Spring Integration提供了企业集成模式的实现,用于连接不同系统和应用。
Spring Batch则专注于批处理操作,如数据迁移、ETL任务等。

框架与库(Library)的区别在于控制权的反转:
·使用库时,开发者的代码调用库中的代码
·使用框架时,框架的代码调用开发者的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 使用库的示例:开发者代码调用库函数
public void libraryExample() {
// 开发者决定何时调用库中的方法
String text = "编程导航欢迎你";
System.out.println(StringUtils.capitalize(text)); // 使用 Apache Commons 库
}

// 使用框架的示例:框架调用开发者代码
@Component
public class FrameworkExample {
// 框架决定何时调用此方法
@PostConstruct
public void init() {
System.out.println("面试鸭系统初始化完成");
}
}

RESTful API基础

RESTful API(Representational State Transfer API)
是一种设计风格,用于构建分布式系统,特别是Web服务。

REST的核心原则

  1. 资源(Resources):系统的核心是资源,每个资源都有一个唯一的标识符(URI)。
  2. 表示(Representations):资源可以有多种表示形式,如JSON、XML、HTML等。
  3. 状态转移(State Transfer):通过HTTP方法(GET、POST、PUT、DELETE等)对资源执行操作。
  4. 无状态(Stateless):服务器不保存客户端状态,每个请求都包含了服务器处理该请求所需的全部信息。
  5. 统一接口(Uniform Interface):所有资源都遵循相同的接口约束。

RESTful API设计最佳实践

  1. 使用HTTP方法表示操作:
    ·GET:获取资源
    ·POST:创建新资源
    ·PUT:更新资源(整体替换)
    ·PATCH:部分更新资源
    ·DELETE:删除资源
  2. 使用HTTP状态码表示结果:
    ·2xx:成功(如200OK、201 Created)
    ·3Xx:重定向
    ·4xx:客户端错误(如400 Bad Request、.404 Not Found)
    ·5xx:服务器错误(如500 Internal Server Error)
  3. 使用名词复数表示资源集合:
    users(而不是user)
    /articles
    /products
  4. 使用子资源表示关系:
    /users/fid)/orders
    /articles/fid)/comments
  5. 使用查询参数进行过滤、排序和分页:
    /products?category=electronics
    /users?sort=name
    /articles?page=2&size=10

Spring Boot中实现RESTful API
Spring Boot提供了强大的支持来构建RESTful API。以下是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@RestController
@RequestMapping("/api/articles")
public class ArticleController {

private final ArticleService articleService;

public ArticleController(ArticleService articleService) {
this.articleService = articleService;
}

// 获取所有文章
@GetMapping
public List<Article> getAllArticles() {
return articleService.findAll();
}

// 获取特定文章
@GetMapping("/{id}")
public ResponseEntity<Article> getArticleById(@PathVariable Long id) {
return articleService.findById(id)
.map(article -> ResponseEntity.ok(article))
.orElse(ResponseEntity.notFound().build());
}

// 创建新文章
@PostMapping
public ResponseEntity<Article> createArticle(@RequestBody Article article) {
Article savedArticle = articleService.save(article);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedArticle.getId())
.toUri();
return ResponseEntity.created(location).body(savedArticle);
}

// 更新文章
@PutMapping("/{id}")
public ResponseEntity<Article> updateArticle(@PathVariable Long id, @RequestBody Article article) {
if (!articleService.existsById(id)) {
return ResponseEntity.notFound().build();
}
article.setId(id);
Article updatedArticle = articleService.save(article);
return ResponseEntity.ok(updatedArticle);
}

// 删除文章
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteArticle(@PathVariable Long id) {
if (!articleService.existsById(id)) {
return ResponseEntity.notFound().build();
}
articleService.deleteById(id);
return ResponseEntity.noContent().build();
}
}

请求和响应格式
在RESTful API中,JSON是最常用的数据交换格式。
Spring Boot通过Jackson库自动处理JSON序列化和反序列化。
请求示例:

1
2
3
4
5
6
7
8
9
POST /api/articles
Content-Type: application/json

{
"title": "Spring Boot 实战",
"content": "Spring Boot 让 Java 开发变得更简单...",
"author": "程序员鱼皮",
"tags": ["Spring", "Java", "教程"]
}

响应示例:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 201 Created
Location: /api/articles/123
Content-Type: application/json

{
"id": 123,
"title": "Spring Boot 实战",
"content": "Spring Boot 让 Java 开发变得更简单...",
"author": "程序员鱼皮",
"tags": ["Spring", "Java", "教程"],
"createdAt": "2023-05-20T10:30:00Z"
}

Spring AI

简化人工智能和机器学习在Spring应用中的集成,使开发者能更容易地利用AI技术构建智能应用。
Spring Al框架主要解决了以下问题:

  1. 复杂性封装:隐藏与A1服务交互的复杂性,提供简洁的API。
  2. 统一接口:为不同的Al提供商(如OpenAl、Anthropic、.本地模型等)提供统一的接口。
  3. 技术整合:将Al功能与Spring生态系统无缝集成。
  4. 降低门槛:使企业级开发者能够轻松采用A!技术。

模型抽象层

Spring Al定义了统一的接口来访问不同的Al模型:

  • ChatClient:用于与聊天模型交互,如GPT、Claude等。
  • EmbeddingClient:用于生成文本嵌入向量。
  • ImageClient:用于图像生成和处理。

提示词管理

Spring Al提供了强大的提示词管理功能:

  • PromptTemplate:用于创建动态提示词模板。
  • SystemPromptTemplate:专门用于系统消息的模板。
  • UserPromptTemplate:用于用户消息的模板。

AI模型提供商

  1. OpenAI
  2. Anthropic
  3. Ollama
    Spring Al还支持或计划支持其他多种提供商:
    ·Azure OpenAl:微软的企业级OpenAI服务
    ·Hugging Face:开源模型社区
    ·Google Vertex Al:Google的AI云服务
    ·本地Transformers模型:直接在应用中运行小型模型
    ·AWS Bedrock:亚马逊的A!基础模型服务

Spring AI的工作流程

  1. 配置AI客户端:设置必要的认证信息和选项。
  2. 准备提示词:构建用于AI模型的提示词。
  3. 调用AI服务:发送提示词并获取响应。
  4. 处理响应:解析和使用A!生成的内容。
    完整示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RestController
@RequestMapping("/api/assistant")
public class AssistantController {
private final ChatClient chatClient;

public AssistantController(ChatClient chatClient) {
this.chatClient = chatClient;
}

@PostMapping("/chat")
public ResponseEntity<Map<String, String>> chat(@RequestBody Map<String, String> request) {
// 1. 提取用户输入
String userMessage = request.get("message");

// 2. 调用 AI 服务并处理响应
String answer = chatClient.prompt()
.system("你是编程导航的专业助手,专门帮助用户解决编程相关问题。请提供准确、实用的建议。")
.user(String.format("请回答这个编程问题:%s", userMessage))
.call()
.content();

// 3. 返回结果
Map<String, String> responseBody = new HashMap<>();
responseBody.put("response", answer);
return ResponseEntity.ok(responseBody);
}
}

大型语言模型(LLM)
嵌入模型
多模态模型

Spring AI相关配置

  1. API密钥:
    OpenAl: https://platform.openai.com/
    Anthropic:https://www.anthropic.com/product
    AzureOpenAl:通过Azure门户

  2. 开发时注意事项:
    不要将API密钥硬编码在代码中
    使用环境变量或配置服务存储密钥
    设置适当的请求速率限制,避免超出配额

1
2
3
4
5
6
7
8
9
10
// 正确做法
@Bean
public ChatClient chatClient(OpenAiChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}

@Bean
public OpenAiChatModel openAiChatModel(@Value("${openai.api-key}") String apiKey) {
return OpenAiChatModel.builder().apiKey(apiKey).build();
}

本地开发工具选择

  1. Ollama(用于本地运行开源模型):
    安装简单:brew instal1ol1ama(macOS)
    ·支持多种模型:Llama、Mistral、Falcon等
    ·适合开发和测试阶段使用
1
2
3
4
5
6
7
# 安装 Ollama 并下载模型
# macOS/Linux
curl -fsSL https://ollama.com/install.sh | sh
ollama pull llama3

# 运行服务
ollama serve

使用Spring Initializr创建Spring AI项目

  1. 使用Spring Initializr创建项目
  2. alt text
  3. 添加Spring AI依赖
1
2
3
4
5
6
7
8
9
<dependencies>
<!-- 现有依赖 -->
<!-- Spring AI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
  1. 创建AI服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class AIAssistantService {

private final ChatClient chatClient;

@Autowired
public AIAssistantService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String askQuestion(String question) {
return chatClient.prompt()
.system("你是编程导航的助手,擅长回答编程相关问题。")
.user(question)
.call()
.content();
}
}

开发环境搭建

Maven依赖配置

  1. 添加Spring AI BOM(Bill of Materials)
1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>1.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
  1. 添加Spring AI基础依赖
1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI Core -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-core</artifactId>
</dependency>
</dependencies>
  1. 添加特定提供商的依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!-- OpenAI -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
<version>1.0.0</version>
</dependency>

<!-- 或者使用 Anthropic -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-anthropic</artifactId>
<version>1.0.0</version>
</dependency>

<!-- 或者使用 Ollama (本地模型) -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
<version>1.0.0</version>
</dependency>
  1. 添加向量存储支持(如果需要RAG功能)
1
2
3
4
5
6
7
8
9
10
<!-- PostgreSQL 向量存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store</artifactId>
</dependency>
<!-- 或者使用 Redis 向量存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-redis-store</artifactId>
</dependency>

获取密钥

本地部署

  1. 下载ollama
  2. 速度慢,复制下载链接到任意网盘软件下载
  3. ollama安装路径更改代码.\download_OllamaSetup.exe /DIR=E:\Ollama
  4. 系统环境变量设置:OLLAMA_MODELS = D:\ollamaimagers
  5. 下载模型:魔搭平台
  6. 选择DeepSeek-R1-Distill-Qwen-7B-GGUF(必须是GGUF,否则需要转换)
  7. DeepSeek-R1-Distill-Qwen-7B-F16.gguf
  8. ModelFile(dsr1-7b.txt)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
FROM ./DeepSeek-R1-Distill-Qwen-7B-F16.gguf
PARAMETER temperature 0.7
PARAMETER top_p 0.95
PARAMETER top_k 40
PARAMETER repeat_penalty 1.1
PARAMETER min_p 0.05
PARAMETER num_ctx 1024
PARAMETER num_thread 4
PARAMETER num_gpu 8

# 设置对话终止符
PARAMETER stop "<|begin▁of▁sentence|>"
PARAMETER stop "<|end▁of▁sentence|>"
PARAMETER stop "<|User|>"
PARAMETER stop "<|Assistant|>"

SYSTEM """
"""

TEMPLATE """{{- if .System }}{{ .System }}{{ end }}
{{- range $i, $_ := .Messages }}
{{- $last := eq (len (slice $.Messages $i)) 1}}
{{- if eq .Role "user" }}<|User|>{{ .Content }}
{{- else if eq .Role "assistant" }}<|Assistant|>{{ .Content }}{{- if not $last }}<|end▁of▁sentence|>{{- end }}
{{- end }}
{{- if and $last (ne .Role "assistant") }}<|Assistant|>{{- end }}
{{- end }}"""
  1. 创建模型:ollama create wgai-r1:7b -f ./dsr1-7b.txt
    模型列表:ollama list
    运行模型:ollama run wgai-r1:7b
  2. 启动服务:ollama serve

API密钥安全管理最佳实践

  1. 使用环境变量:
1
2
# .env 文件示例
OPENAI_API_KEY=sk-your-key-here
1
2
- 通过环境变量传递 API 密钥
- 在本地开发中使用 `.env` 文件(记得将其添加到 .gitignore)
  1. 在Spring Boot中使用配置:
1
2
# application.properties 或 application.yml
spring.ai.openai.api-key=${OPENAI_API_KEY}
  1. 使用加密配置:
    生产环境中考虑使用Spring Cloud ConfigVault等服务
  2. 应用适当的速率限制:
    避免超出API使用限制
    实现错误重试机制
1
2
3
4
5
6
7
8
9
10
11
12
13
// 在 Spring Boot 应用中加载 API 密钥的示例
@Configuration
public class AIConfig {

@Value("${spring.ai.openai.api-key}")
private String apiKey;

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

第一次实践

添加依赖:

1
2
3
4
5
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
<version>1.0.0</version>
</dependency>

set WGAI-API-KEY=密钥
配置文件:application.properties

1
2
3
4
5
6
7
8
9
spring.application.name=Spring-ai-demo
# OpenAI
#server.port=8080
#spring.ai.api-key=${WGAI-API-KEY}
#spring.ai.chat.model=wgai-r1:1.5b

# Ollama
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.model=wgai-r1:1.5b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.Spring_ai_demo;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class TestController {
private final ChatClient chatClient;

@Autowired
public TestController(ChatClient chatClient) {
this.chatClient = chatClient;
}

@GetMapping("/test")
public String testAI(@RequestParam(defaultValue = "你好,Spring AI!") String message) {
return chatClient.prompt().user(message).call().content();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.Spring_ai_demo;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AiConfig {

@Bean
public ChatClient chatClient(ChatClient.Builder builder) {
return builder.build();
}
}

启动测试:
http://localhost:8080/test

练习

练习1:实现API密钥轮换机制
创建一个配置类,支持多个API密钥轮换使用,避免单个密钥超出使用限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class AIClientConfig {

@Value("${api.keys}")
private List<String> apiKeys;
// 原子整形,自动变换
private AtomicInteger currentKeyIndex = new AtomicInteger(0);

@Bean
public ChatClient chatClient() {
// 实现一个简单的轮换机制
String currentKey = getNextApiKey();
return new OpenAiChatClient(currentKey);
}

private String getNextApiKey() {
int index = currentKeyIndex.getAndUpdate(i -> (i + 1) % apiKeys.size());
return apiKeys.get(index);
}
}

练习2:创建开发/生产环境配置分离
实现开发环境使用本地Ollama模型,生产环境使用OpenAl模型的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class AIClientConfiguration {

@Bean
@Profile("dev")
public ChatClient devChatClient(OllamaChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}

@Bean
@Profile("prod")
public ChatClient prodChatClient(OpenAiChatModel chatModel) {
return ChatClient.builder(chatModel).build();
}
}

Spring AI 模型

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
my-spring-ai-app/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/myapp/
│ │ │ ├── MySpringAiApplication.java # 主应用类
│ │ │ ├── config/ # 配置类
│ │ │ │ └── AiConfig.java # AI 相关配置
│ │ │ ├── controller/ # 控制器
│ │ │ │ └── ChatController.java # 处理聊天请求
│ │ │ ├── service/ # 服务层
│ │ │ │ └── AiService.java # AI 服务
│ │ │ ├── model/ # 数据模型
│ │ │ │ ├── ChatRequest.java # 请求模型
│ │ │ │ └── ChatResponse.java # 响应模型
│ │ │ └── exception/ # 异常处理
│ │ │ └── AiServiceException.java # AI 服务异常
│ │ └── resources/
│ │ ├── application.properties # 应用配置
│ │ └── static/ # 静态资源
│ └── test/ # 测试代码
└── pom.xml # Maven 配置

AI配置类:配置AI客户端和相关设置。

1
2
3
4
5
6
7
8
@Configuration
public class AiConfig {

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

AI服务类:封装AI功能逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class AiService {

private final ChatClient chatClient;

public AiService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String generateResponse(String prompt) {
return chatClient.prompt().user(prompt).call().content();
}
}

控制器:提供HTTP接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/api/chat")
public class ChatController {

private final AiService aiService;

public ChatController(AiService aiService) {
this.aiService = aiService;
}

@PostMapping
public ChatResponse chat(@RequestBody ChatRequest request) {
String response = aiService.generateResponse(request.getMessage());
return new ChatResponse(response);
}
}

配置

配置文件(application.properties)设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 应用基本配置
server.port=8080
spring.application.name=spring-ai-demo

# OpenAI 配置
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.options.model = gpt-3.5-turbo
spring.ai.openai.chat.options.temperature =0.7
spring.ai.openai.chat.options.top-p=1.0
spring.ai.openai.chat.options.max-tokens=2000

# Ollama
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.model=wgai-r1:1.5b
spring.ai.ollama.chat.options.model=wgai-r1:7b

# 超时设置
spring.ai.ollama.init.timeout = 60000
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server:
port: 8080

spring:
application:
name: spring-ai-demo

ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
model: gpt-3.5-turbo
temperature: 0.7
top-p: 1.0
max-tokens: 2000

timeout:
connect: 60000
read: 60000

多环境配置

1
2
3
4
5
6
7
8
9
# 开发环境配置 (application-dev.properties)
spring.ai.ollama.base-url=http://localhost:11434
spring.ai.ollama.chat.options.model=wgai-r1:1.5b
logging.level.org.springframework.ai.chat.client.advisor=DEBUG

# 生产环境配置 (application-prod.properties)
spring.ai.openai.chat.options.model=gpt-4
# 查看日志 = DEBUG
logging.level.org.springframework.ai.chat.client.advisor=false

然后在主配置文件中指定当前激活的配置文件:

1
spring.profiles.active=dev

创建控制器处理HTTP请求

在Web应用中,我们需要创建控制器来处理前端或其他服务发送的HTTP请求。
以下是一个完整的AI聊天控制器示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@RestController
@RequestMapping("/api/chat")
public class ChatController {

private final AiService aiService;

public ChatController(AiService aiService) {
this.aiService = aiService;
}

@PostMapping
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) {
try {
String response = aiService.generateResponse(request.getMessage());
ChatResponse chatResponse = new ChatResponse(response);
return ResponseEntity.ok(chatResponse);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ChatResponse("处理请求时出错:" + e.getMessage()));
}
}

@PostMapping("/stream")
public SseEmitter streamChat(@RequestBody ChatRequest request) {
SseEmitter emitter = new SseEmitter(300000L); // 5分钟超时

try {
aiService.streamResponse(request.getMessage(), emitter);
} catch (Exception e) {
emitter.completeWithError(e);
}

return emitter;
}
}

// 请求和响应模型
public class ChatRequest {
private String message;

// getters, setters, constructors
}

public class ChatResponse {
private String content;
private LocalDateTime timestamp;

public ChatResponse(String content) {
this.content = content;
this.timestamp = LocalDateTime.now();
}

// getters, setters
}

流式响应处理
对于流式响应,服务层需要相应的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Service
public class AiService {

private final ChatClient chatClient;

// ... 其他代码 ...

public void streamResponse(String prompt, SseEmitter emitter) {
Prompt chatPrompt = new Prompt(new UserMessage(prompt));

chatClient.stream(chatPrompt)
.subscribe(
// 处理每个响应块
chunk -> {
try {
String content = chunk.getResult().getOutput().getContent();
if (content != null && !content.isEmpty()) {
emitter.send(SseEmitter.event().data(content));
}
} catch (IOException e) {
emitter.completeWithError(e);
}
},
// 处理错误
error -> emitter.completeWithError(error),
// 处理完成
() -> emitter.complete()
);
}
}

实现简单的文本生成功能

  1. 内容摘要生成器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class SummaryService {

private final ChatClient chatClient;

public SummaryService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String summarizeText(String text, int maxWords) {
String prompt = String.format(
"请将以下文本总结为不超过 %d 个字的摘要:\n\n%s",
maxWords, text
);

return chatClient.prompt().user(prompt).call().content();
}
}
  1. 代码解释器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class CodeExplainerService {

private final ChatClient chatClient;

public CodeExplainerService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String explainCode(String code, String language) {
String systemPrompt = "你是代码小抄的代码解释专家,请详细解释以下代码的功能和关键部分";

return chatClient.prompt()
.system(systemPrompt)
.user(String.format("```%s\n%s\n```", language, code))
.call()
.content();
}
}
  1. 个性化邮件生成器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class EmailGeneratorService {

private final ChatClient chatClient;

public EmailGeneratorService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String generateEmail(String recipient, String subject, String keyPoints, String tone) {
String prompt = String.format(
"请生成一封发给%s的邮件,主题是"%s"。\n" +
"需要包含的要点:%s\n" +
"语气风格:%s",
recipient, subject, keyPoints, tone
);

return chatClient.prompt().user(prompt).call().content();
}
}

错误处理与异常管理

定义自定义异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 基础 AI 服务异常
public class AiServiceException extends RuntimeException {

public AiServiceException(String message) {
super(message);
}

public AiServiceException(String message, Throwable cause) {
super(message, cause);
}
}

// 具体异常类型
public class AiTokenLimitExceededException extends AiServiceException {

public AiTokenLimitExceededException(String message) {
super(message);
}
}

public class AiTimeoutException extends AiServiceException {

public AiTimeoutException(String message, Throwable cause) {
super(message, cause);
}
}

异常处理服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Service
public class AiService {

private final ChatClient chatClient;
private final int maxRetries;

public AiService(ChatClient chatClient, @Value("${ai.max-retries:3}") int maxRetries) {
this.chatClient = chatClient;
this.maxRetries = maxRetries;
}

public String generateResponseWithRetry(String prompt) {
int attempts = 0;

while (attempts < maxRetries) {
try {
return chatClient.prompt().user(prompt).call().content();
} catch (Exception e) {
attempts++;

// 如果是最后一次尝试,抛出异常
if (attempts >= maxRetries) {
if (e instanceof ResourceAccessException) {
throw new AiTimeoutException("AI 服务请求超时", e);
} else {
throw new AiServiceException("调用 AI 服务失败", e);
}
}

// 否则等待后重试
try {
Thread.sleep(1000 * attempts); // 指数退避
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new AiServiceException("重试过程被中断", ie);
}
}
}

throw new AiServiceException("超过最大重试次数");
}
}

全局异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@RestControllerAdvice
public class GlobalExceptionHandler {

private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);

@ExceptionHandler(AiServiceException.class)
public ResponseEntity<ErrorResponse> handleAiServiceException(AiServiceException e) {
logger.error("AI 服务异常", e);

ErrorResponse errorResponse = new ErrorResponse(
"AI_SERVICE_ERROR",
e.getMessage()
);

return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body(errorResponse);
}

@ExceptionHandler(AiTokenLimitExceededException.class)
public ResponseEntity<ErrorResponse> handleTokenLimitException(AiTokenLimitExceededException e) {
logger.error("令牌限制异常", e);

ErrorResponse errorResponse = new ErrorResponse(
"TOKEN_LIMIT_EXCEEDED",
"输入内容过长,请缩短后重试"
);

return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception e) {
logger.error("未处理的异常", e);

ErrorResponse errorResponse = new ErrorResponse(
"INTERNAL_SERVER_ERROR",
"服务器内部错误"
);

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
}
}

// 错误响应模型
public class ErrorResponse {
private String code;
private String message;
private LocalDateTime timestamp = LocalDateTime.now();

public ErrorResponse(String code, String message) {
this.code = code;
this.message = message;
}

// getters, setters
}

第二次实践

  1. 构建一个AI问答应用
    包含以下依赖:
  • Spring Web
  • Spring AI
  • Lombok
  1. 配置应用
1
2
3
4
5
6
7
8
9
10
11
server.port=8080
spring.application.name=面试鸭-AI助手

# OpenAI 配置
spring.ai.openai.api-key=${OPENAI_API_KEY}
spring.ai.openai.chat.model=gpt-3.5-turbo
spring.ai.openai.chat.temperature=0.7
spring.ai.openai.chat.max-tokens=1500

# 会话设置
app.chat.history-size=10
  1. 创建数据模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Message {
private String role; // "user" 或 "assistant"
private String content;
private LocalDateTime timestamp = LocalDateTime.now();
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatRequest {
private String message;
private String sessionId;
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatResponse {
private String message;
private List<Message> history;
private LocalDateTime timestamp = LocalDateTime.now();
}
  1. 实现会话管理服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Service
public class ChatSessionService {

@Value("${app.chat.history-size:10}")
private int historySize;

// 使用内存存储会话(生产环境应使用持久化存储)
private final Map<String, List<Message>> chatSessions = new ConcurrentHashMap<>();

public List<Message> getSessionHistory(String sessionId) {
return chatSessions.getOrDefault(sessionId, new ArrayList<>());
}

public void addMessage(String sessionId, Message message) {
List<Message> history = chatSessions.computeIfAbsent(
sessionId, k -> new ArrayList<>()
);

history.add(message);

// 限制历史记录大小
if (history.size() > historySize * 2) { // 保留用户和助手消息对
history = history.subList(history.size() - historySize * 2, history.size());
chatSessions.put(sessionId, history);
}
}

public String createNewSession() {
String sessionId = UUID.randomUUID().toString();
chatSessions.put(sessionId, new ArrayList<>());
return sessionId;
}
}
  1. 实现AI服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
@Service
public class MianshiyaAiService {

private final ChatClient chatClient;
private final ChatSessionService sessionService;

public MianshiyaAiService(ChatClient chatClient, ChatSessionService sessionService) {
this.chatClient = chatClient;
this.sessionService = sessionService;
}

public ChatResponse processChat(ChatRequest request) {
// 获取会话 ID,如果为空则创建新会话
String sessionId = request.getSessionId();
if (sessionId == null || sessionId.isEmpty()) {
sessionId = sessionService.createNewSession();
}

// 保存用户消息
Message userMessage = new Message("user", request.getMessage());
sessionService.addMessage(sessionId, userMessage);

// 构建提示,包含历史记录
List<Message> history = sessionService.getSessionHistory(sessionId);

// 调用 AI 获取回答
String aiResponse = chatClient.prompt()
.system("你是面试鸭平台的智能助手,专长于回答编程和求职相关的问题。给出准确、简洁且有帮助的回答,必要时提供代码示例。")
.user(request.getMessage())
.call()
.content();

// 保存 AI 回答
Message assistantMessage = new Message("assistant", aiResponse);
sessionService.addMessage(sessionId, assistantMessage);

// 构建并返回响应
return new ChatResponse(
aiResponse,
sessionService.getSessionHistory(sessionId),
LocalDateTime.now()
);
}

private List<AbstractMessage> buildMessages(String sessionId) {
List<Message> history = sessionService.getSessionHistory(sessionId);
List<AbstractMessage> messages = new ArrayList<>();

// 添加系统消息
messages.add(new SystemMessage(
"你是面试平台的智能助手,专长于回答编程和求职相关的问题。" +
"给出准确、简洁且有帮助的回答,必要时提供代码示例。"
));

// 添加历史消息(最近的几条)
int startIndex = Math.max(0, history.size() - 6); // 最多保留3轮对话
for (int i = startIndex; i < history.size(); i++) {
Message msg = history.get(i);
if ("user".equals(msg.getRole())) {
messages.add(new UserMessage(msg.getContent()));
} else if ("assistant".equals(msg.getRole())) {
messages.add(new AssistantMessage(msg.getContent()));
}
}

return messages;
}
}
  1. 实现接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/api/chat")
public class ChatController {

private final MianshiyaAiService aiService;

public ChatController(MianshiyaAiService aiService) {
this.aiService = aiService;
}

@PostMapping
public ResponseEntity<ChatResponse> chat(@RequestBody ChatRequest request) {
ChatResponse response = aiService.processChat(request);
return ResponseEntity.ok(response);
}
}
  1. 创建简单的前端页面(可选)
    在src/main/resources/static下创建index.html:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>微光zc AI 助手</title>

<style>
body {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.chat-container {
border: 1px solid #ddd;
border-radius: 8px;
min-height: 400px;
padding: 10px;
margin-bottom: 20px;
overflow-y: auto;
}
.message {
margin-bottom: 10px;
padding: 8px 12px;
border-radius: 18px;
max-width: 70%;
}
.user-message {
background-color: #e3f2fd;
margin-left: auto;
}
.assistant-message {
background-color: #f1f1f1;
margin-right: auto;
}
.input-container {
display: flex;
gap: 10px;
}
#messageInput {
flex: 1;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
}
button {
padding: 10px 20px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>

</head>

<body>
<h1>微光zc AI 助手</h1>

<div id="chatContainer" class="chat-container"></div>

<div class="input-container">
<input type="text" id="messageInput" placeholder="输入你的问题...">
<button id="sendButton">发送</button>

</div>

<script>
let sessionId = '';

document.getElementById('sendButton').addEventListener('click', sendMessage);
document.getElementById('messageInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});

function sendMessage() {
const messageInput = document.getElementById('messageInput');
const message = messageInput.value.trim();

if (message) {
// 显示用户消息
appendMessage('user', message);
messageInput.value = '';

// 发送到服务器
fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
message: message,
sessionId: sessionId
})
})
.then(response => response.json())
.then(data => {
// 显示 AI 回复
appendMessage('assistant', data.message);
// 保存会话 ID
if (data.sessionId) {
sessionId = data.sessionId;
}
})
.catch(error => {
console.error('Error:', error);
appendMessage('assistant', '抱歉,发生了错误,请稍后重试。');
});
}
}

function appendMessage(role, content) {
const chatContainer = document.getElementById('chatContainer');
const messageDiv = document.createElement('div');
messageDiv.className = `message ${role}-message`;
messageDiv.textContent = content;
chatContainer.appendChild(messageDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
</script>

</body>

</html>
  1. 启动应用并测试
    确保设置了OPENAI API KEY环境变量
    运行应用:mvn spring-boot:run
    访问http:/localhost:8080进行测试

提示词工程基础

使用PromptTemplate创建动态提示

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Service
public class ArticleGeneratorService {

private final ChatClient chatClient;

public ArticleGeneratorService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String generateArticle(String topic, String targetAudience, int wordCount) {
String templateString = """
请以编程导航博主的身份,撰写一篇关于{topic}的技术文章。
目标读者:{audience}
文章风格:专业、清晰、易懂
字数要求:约{wordCount}字
文章结构应包含:引言、主要内容(2-3个核心部分)、总结
""";

PromptTemplate template = new PromptTemplate(templateString);
Map<String, Object> params = new HashMap<>();
params.put("topic", topic);
params.put("audience", targetAudience);
params.put("wordCount", wordCount);

String prompt = template.render(params);
return chatClient.prompt().user(prompt).call().content();
}
}

条件逻辑和高级模板
PromptTemplate支持条件逻辑,让你可以根据参数动态调整提示内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@Service
public class CodeReviewService {

private final ChatClient chatClient;

public CodeReviewService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String reviewCode(String code, String language, String level) {
String templateString = """
作为代码小抄的高级{language}开发专家,请对以下代码进行代码评审:
``` {language}
{code}
{# 根据不同级别调整评审深度 }
{% if level == '初级' %}
‍ ⁡ 请重点关注基本⁠代码规范和常见错误‍。
{% elseif level == '中级' %}
‍ ⁡ 请重点关注代码⁠结构、性能优化和边界情‍况处理。
{% else %}
‍ ⁡ 请进行全面深度⁠评审,包括架构设计‍、性能优化、安全性‌、可扩展性和最佳实践。
{% endif %}
/```
请按以下格式提供评审结果:
1. 总体评价
2. 主要问题(按严重性排序)
3. 改进建议
4. 优化后的代码示例
""";
PromptTemplate template = new PromptTemplate(templateString);
Map<String, Object> params = new HashMap<>();
params.put("code", code);
params.put("language", language);
params.put("level", level);

String prompt = template.render(params);
return chatClient.prompt().user(prompt).call().content();
}
}

提示模板的复用和组合
对干复杂应用我们可以创建可复用的模板库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Configuration
public class PromptTemplateConfig {

@Bean
public PromptTemplate codeExplanationTemplate() {
return new PromptTemplate("""
请解释以下{language}代码的功能:
```{language}
{code}
解释应该面向{audience}级别的开发者。
""");
}


@Bean
public PromptTemplate bugFixingTemplate() {
returnnew Prom⁠ptTemplat‍e("""
以下{language}代码存在问题:
```{language}
{code}
/```
错误信息:{error}

请:
1. 分析问题根源
2. 提供修复后的完整代码
3. 解释修复的原理
""");
}
}

系统消息与用户消息的区别

在使用LLM时,消息类型对模型行为有很大的影响。
Spring AI支持不同类型的消息,主要包括系统消息和用户消息。

系统消息(SystemMessage)
用于设置AI助手的行为指南、角色定义或全局指令。不是对话的一部分,而是对AI行为的”元指导”。

1
2
3
4
5
SystemMessage s‍ystemMessage = new SystemMessa⁡ge(
"你是算法导航的专业算法讲师,擅长将复杂的算⁠法概念解释得通俗易懂。" +
"回答时应包含示例代码‍,并解释算法的时间和空间复杂度。" +
"风格应保持‌专业但友好,适合计算机科学本科生理解。"
);

用户消息(UserMessage)
用户消息代表用户的实际输入,是对话中用户向AI提问或发出指令的部分。

1
2
3
UserMes‍sage userMessa⁡ge = new UserM⁠essage(
"请‍解释快速排序算法的工作原理,‌并给出 Java 实现。"
);

正确组合使用
系统消息和用户消息结合使用,可以获得更精确的AI响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class AlgorithmTutorService {

private final ChatClient chatClient;

public AlgorithmTutorService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String explainAlgorithm(String algorithm) {
return chatClient.prompt()
.syste‍m("你是算法导航的专业算法讲师,擅长将复杂的算法概念解释得通俗易懂。"⁡ +
"回答时应包含示例代码,并⁠解释算法的时间和空间复杂度。" +
"风格应保持专业但友好,适合计算机科学本科生理解          ‌                      ")
.user("请解释" + algorithm + "算法的工作原理,并给出 Java 实现。")
.call()
.content();
}
}

上下文管理与对话历史

Al对话的质量很大程度上取决于上下文管理。
Spring AI提供了灵活的方式来处理对话历史。

保持对话上下文
通过将之前的消息包含在提示中,可以让AI了解对话历史,从而给出更连贯的回答。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Service
public class ConversationService {

private final ChatClient chatClient;
private final Map<String, List<AbstractMessage>> conversationHistory = new HashMap<>();

public ConversationService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public String continueConversation(String userId, String newMessage) {
// 获取或创建用户的对话历史
List<AbstractMessage> history = conversationHistory.computeIfAbsent(
userId, k -> new ArrayList<>(Arrays.asList(
new SystemMessage("你是面试鸭平台的面试教练,帮助用户准备技术面试。")
))
);

// 添加新的用户消息
UserMessage userMessage = new UserMessage(newMessage);
history.add(userMessage);

// 准备包含历史记录的提示
Prompt prompt = new Prompt(history);

// 获取 AI 响应
ChatResponse response = chatClient.call(prompt);
String responseContent = response.getResult().getOutput().getContent();

// 将 AI 响应添加到历史记录
history.add(new AssistantMessage(responseContent));

// 如果历史记录过长,可以裁剪
if (history.size() > 10) {
// 保留系统消息和最近的对话
List<AbstractMessage> trimmedHistory = new ArrayList<>();
trimmedHistory.add(history.get(0)); // 系统消息
trimmedHistory.addAll(history.subList(history.size() - 9, history.size()));
conversationHistory.put(userId, trimmedHistory);
}

return responseContent;
}
}

上下文窗口限制处理
语言模型有上下文窗口限制,需要适当管理历史记录长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
@Service
public class TokenAwareConversationService {

private final ChatClient chatClient;
private final Map<String, List<AbstractMessage>> conversationHistory = new HashMap<>();
private final int maxTokens = 4000; // 根据模型限制设置

// 简化的令牌计算逻辑(实际中应使用更准确的算法)
private int estimateTokenCount(String text) {
return text.split("\\s+").length;
}

public String continueConversation(String userId, String newMessage) {
// 获取或创建对话历史
List<AbstractMessage> history = conversationHistory.computeIfAbsent(
userId, k -> new ArrayList<>(Arrays.asList(
new SystemMessage("你是编程导航的助教。")
))
);

// 添加新消息
history.add(new UserMessage(newMessage));

// 检查并裁剪历史记录,确保不超过令牌限制
int totalTokens = 0;
List<AbstractMessage> effectiveHistory = new ArrayList<>();

// 总是保留系统消息
effectiveHistory.add(history.get(0));
totalTokens += estimateTokenCount(((SystemMessage)history.get(0)).getContent());

// 从最近的消息向前添加,直到接近令牌限制
for (int i = history.size() - 1; i > 0; i--) {
AbstractMessage message = history.get(i);
String content = "";

if (message instanceof UserMessage) {
content = ((UserMessage)message).getContent();
} else if (message instanceof AssistantMessage) {
content = ((AssistantMessage)message).getContent();
}

int messageTokens = estimateTokenCount(content);
if (totalTokens + messageTokens < maxTokens) {
effectiveHistory.add(0, message); // 添加到列表开头
totalTokens += messageTokens;
} else {
break;
}
}

// 调用 AI
Prompt prompt = new Prompt(effectiveHistory);
String responseContent = chatClient.call(prompt)
.getResult().getOutput().getContent();

// 更新历史
history.add(new AssistantMessage(responseContent));

return responseContent;
}
}

常见提示词技巧与模式

思维链提示(Chain of Thought)
引导AI逐步思考问题,提高推理能力

1
2
3
4
5
6
7
8
9
10
11
12
13
String ‍chainOfThough⁡tPrompt = """⁠
请分析以下算法问题并找出‍最优解:
给定一个整数数组‌,找出其中和最大的连续子数组。

请按照以下步骤思考:
1. 首先,分析问题的基本特点和边界条件
2. 考虑可能的解法(暴力法、分治法、动态规划等)
3. 比较不同方法的时间和空间复杂度
4. 确定最优解法
5. 用 Java 实现该算法
6. 分析算法的复杂度
7. 验证算法在示例用例上的正确性
""";

零样本、单样本和多样本提示
根据任务复杂度,提供不同数量的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 零样本提示
String zeroShotPrompt = "将以下句子从中文翻译成英文:'编程导航是一个帮助程序员学习的平台。'";

// 单样本提示
String oneS‍hotPrompt = """
请根据示例⁡,提供类似的转换:
输入:'Java 是一⁠种广泛使用的编程语言。'
输出:'Java‍ is a widely used pro‌gramming language.'

现在,请转换:'编程导航是一个帮助程序员学习的平台。'
""";

// 多样本提示
String multi‍ShotPrompt = """
请根据以下示⁡例,进行类似的转换:
示例 1:
输入:'Ja⁠va 是一种广泛使用的编程语言。'
输出:'J‍ava is a widely used pr‌ogramming language.'

示例 2:
输入:‍'面试鸭提供编程面试题和答案。'
⁡输出:'Mianshiya pro⁠vides programming‍ interview questi‌ons and answers.'

示例 3:
输入:‍'老鱼简历帮助求职者制作专业简⁡历。'
输出:'Laoyu Resum⁠e helps job seeke‍rs create profess‌ional resumes.'

现在,请转换:'编程导航是一个帮助程序员学习的平台。'
""";

结果限制技巧
明确限制输出格式和长度。

1
2
3
4
5
6
7
8
9
10
String‍ constraine⁡dPrompt = "⁠""
请提供 5 个提‍高 Java 编程技‌能的建议。
每个建议必须:
1. 不超过 20 字
2. 以动词开头
3. 可以立即执行
4. 具体明确

回答格式必‍须是带序号的简洁列表⁡,不包含任何额外⁠解释。         ‍          ‌             
""";

第三次实践

创建自定义角色的AI助手
我们要创建一个”算法教练”AI助手,它能够:
解释算法概念
·提供算法习题
·评估用户的解法
·给出针对性的学习建议

  1. 定义系统提示模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class AlgorithmCoachPrompts {

public PromptTemplate getSystemPromptTemplate() {
returnnew Prom⁠ptTemplat‍e("""
你是算法导航的资深算法教练,名为"算法导航教练",专注于帮助{level}开发者提升算法能力。

你的特点:
1. 深入浅出:能将复杂算法概念解释得通俗易懂
2. 循序渐进:根据学习者水平调整难度
3. 鼓励思考:不直接给出完整答案,而是引导用户思考
4. 实用导向:强调算法在实际工作中的应用场景

回答风格:
- 使用比喻和类比解释抽象概念
- 提供可视化描述帮助理解
- 代码示例使用 Java 语言,包含详细注释
- 友好专业,语气积极鼓励

你不是:
- 不是搜索引擎,不提供与算法无关的信息
- 不是代码生成工具,不解决完整项目问题
- 不是竞赛教练,不过度关注算法竞赛技巧
""");
}
}
  1. 实现服务层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@Service
public class AlgorithmCoachService {

private final ChatClient chatClient;
private final AlgorithmCoachPrompts prompts;
private final Map<String, List<AbstractMessage>> sessionHistories = new ConcurrentHashMap<>();

public AlgorithmCoachService(ChatClient chatClient, AlgorithmCoachPrompts prompts) {
this.chatClient = chatClient;
this.prompts = prompts;
}

public String startSession(String userId, String level) {
// 创建系统提示
Map<String, Object> params = Map.of("level", level);
String systemPrompt = prompts.getSystemPromptTemplate().render(params);

List<AbstractMessage> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPrompt));
messages.add(new UserMessage("你好,我想学习算法。"));

// 保存会话历史
sessionHistories.put(userId, messages);

// 调用 AI
Prompt prompt = new Prompt(messages);
ChatResponse response = chatClient.call(prompt);
String content = response.getResult().getOutput().getContent();

// 更新历史
messages.add(new AssistantMessage(content));

return content;
}

public String continueConversation(String userId, String message) {
// 获取历史记录
List<AbstractMessage> history = sessionHistories.get(userId);
if (history == null) {
// 如果没有历史,创建新会话
return startSession(userId, "中级");
}

// 添加新消息
history.add(new UserMessage(message));

// 调用 AI
Prompt prompt = new Prompt(history);
ChatResponse response = chatClient.call(prompt);
String content = response.getResult().getOutput().getContent();

// 更新历史
history.add(new AssistantMessage(content));

// 如果历史太长,裁剪
if (history.size() > 15) {
List<AbstractMessage> trimmedHistory = new ArrayList<>();
trimmedHistory.add(history.get(0)); // 系统消息
trimmedHistory.addAll(history.subList(history.size() - 10, history.size()));
sessionHistories.put(userId, trimmedHistory);
}

return content;
}

public String getAlgorithmExplanation(String userId, String algorithm) {
String message = "请解释" + algorithm + "算法的工作原理、时间复杂度和适用场景。";
return continueConversation(userId, message);
}

public String getPracticeQuestion(String userId, String difficulty) {
String message = "请给我一道" + difficulty + "难度的算法题,包含题目描述、示例输入输出和提示。";
return continueConversation(userId, message);
}

public String evaluateSolution(String userId, String problem, String solution) {
‍ String ⁡message = ⁠"我的解法如下,请评‍估这个解法并给出改进‌建议:\n\n问题:" +
problem + "\n\n我的解法:\n```java\n" + solution + "\n```";
return continueConversation(userId, message);
}
}

新版本Prompt改了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package com.example.Spring_ai_6.service;

import com.example.Spring_ai_6.model.AlgorithmCoachPrompts;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class AlgorithmCoachService {

private final ChatClient chatClient;
private final AlgorithmCoachPrompts prompts;
private final Map<String, List<Message>> sessionHistories = new ConcurrentHashMap<>();

public AlgorithmCoachService(ChatClient chatClient, AlgorithmCoachPrompts prompts) {
this.chatClient = chatClient;
this.prompts = prompts;
}

public String startSession(String userId, String level) {
Map<String, Object> params = Map.of("level", level);
String systemPrompt = prompts.getSystemPromptTemplate().render(params);

List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPrompt));
messages.add(new UserMessage("你好,我想学习算法。"));

// 调用 AI(新 API)
String content = chatClient.prompt()
.messages(messages)
.call()
.content();

messages.add(new AssistantMessage(content));
sessionHistories.put(userId, messages);

return content;
}

public String continueConversation(String userId, String message) {
List<Message> history = sessionHistories.get(userId);
if (history == null) {
return startSession(userId, "中级");
}

// 添加用户新消息
List<Message> currentMessages = new ArrayList<>(history);
currentMessages.add(new UserMessage(message));

// 调用 AI
String content = chatClient.prompt()
.messages(currentMessages)
.call()
.content();

// 更新历史(线程安全考虑:避免直接修改共享列表)
List<Message> updatedHistory = new ArrayList<>(currentMessages);
updatedHistory.add(new AssistantMessage(content));

// 裁剪历史
if (updatedHistory.size() > 15) {
List<Message> trimmed = new ArrayList<>();
trimmed.add(updatedHistory.get(0)); // 系统消息
trimmed.addAll(updatedHistory.subList(updatedHistory.size() - 10, updatedHistory.size()));
sessionHistories.put(userId, trimmed);
} else {
sessionHistories.put(userId, updatedHistory);
}

return content;
}

public String getAlgorithmExplanation(String userId, String algorithm) {
String message = "请解释" + algorithm + "算法的工作原理、时间复杂度和适用场景。";
return continueConversation(userId, message);
}

public String getPracticeQuestion(String userId, String difficulty) {
String message = "请给我一道" + difficulty + "难度的算法题,包含题目描述、示例输入输出和提示。";
return continueConversation(userId, message);
}

public String evaluateSolution(String userId, String problem, String solution) {
String message = "我的解法如下,请评估这个解法并给出改进建议:\n\n问题:" +
problem + "\n\n我的解法:\n```java\n" + solution + "\n```";
return continueConversation(userId, message);
}
}
  1. 创建控制器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@RestController
@RequestMapping("/api/algorithm-coach")
public class AlgorithmCoachController {

private final AlgorithmCoachService coachService;

public AlgorithmCoachController(AlgorithmCoachService coachService) {
this.coachService = coachService;
}

@PostMapping("/start")
public ResponseEntity<Map<String, String>> startSession(
@RequestBody Map<String, String> request) {
String userId = request.get("userId");
String level = request.getOrDefault("level", "中级");

String response = coachService.startSession(userId, level);
return ResponseEntity.ok(Map.of("response", response));
}

@PostMapping("/chat")
public ResponseEntity<Map<String, String>> chat(
@RequestBody Map<String, String> request) {
String userId = request.get("userId");
String message = request.get("message");

String response = coachService.continueConversation(userId, message);
return ResponseEntity.ok(Map.of("response", response));
}

@PostMapping("/explain")
public ResponseEntity<Map<String, String>> explainAlgorithm(
@RequestBody Map<String, String> request) {
String userId = request.get("userId");
String algorithm = request.get("algorithm");

String response = coachService.getAlgorithmExplanation(userId, algorithm);
return ResponseEntity.ok(Map.of("response", response));
}

@PostMapping("/practice")
public ResponseEntity<Map<String, String>> getPracticeQuestion(
@RequestBody Map<String, String> request) {
String userId = request.get("userId");
String difficulty = request.getOrDefault("difficulty", "中等");

String response = coachService.getPracticeQuestion(userId, difficulty);
return ResponseEntity.ok(Map.of("response", response));
}

@PostMapping("/evaluate")
public ResponseEntity<Map<String, String>> evaluateSolution(
@RequestBody Map<String, String> request) {
String userId = request.get("userId");
String problem = request.get("problem");
String solution = request.get("solution");

String response = coachService.evaluateSolution(userId, problem, solution);
return ResponseEntity.ok(Map.of("response", response));
}
}

结构化输出处理

在AI应用中,模型的输出可以大致分为两类:

  1. 非结构化响应(自然语言)
  2. 结构化响应(编程语言)

结构化输出的应用场景

  1. 数据提取和内容分类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 从简历文本中提取结构化信息
public class ResumeParser {
private final ChatClient chatClient;

public ResumeParser(ChatClient chatClient) {
this.chatClient = chatClient;
}

public ResumeData parseResume(String resumeText) {
String prompt = "分析以下简历文本,提取关键信息:\n\n" + resumeText;

return chatClient.call(
prompt,
new ResponseParser<>(ResumeData.class)
);
}
}

// 结构化数据模型
public class ResumeData {
private PersonalInfo personalInfo;
private List<Education> education;
private List<WorkExperience> workExperience;
private List<String> skills;
// getters and setters
}
  1. 内容转换与格式化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 将博客文章转换为社交媒体摘要
public class ContentConverter {
private final ChatClient chatClient;

public ContentConverter(ChatClient chatClient) {
this.chatClient = chatClient;
}

public List<SocialMediaPost> convertBlogToSocialMedia(String blogContent) {
String prompt = "将以下博客文章转换为3种不同的社交媒体平台的帖子:\n\n" + blogContent;

return chatClient.call(
prompt,
new ResponseParser<>(new TypeReference<List<SocialMediaPost>>() {})
);
}
}

// 社交媒体帖子结构
public class SocialMediaPost {
private String platform; // "Twitter", "LinkedIn", "Facebook"
private String content;
private List<String> hashtags;
private int estimatedReadTime;
// getters and setters
}
  1. 数据增强与丰富
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 为产品描述增加 SEO 相关信息
public class SeoEnhancer {
private final ChatClient chatClient;

public SeoEnhancer(ChatClient chatClient) {
this.chatClient = chatClient;
}

public SeoData enhanceProductDescription(Product product) {
String prompt = String.format(
"为以下产品生成 SEO 优化信息:\n名称:%s\n描述:%s\n类别:%s",
product.getName(), product.getDescription(), product.getCategory()
);

return chatClient.call(
prompt,
new ResponseParser<>(SeoData.class)
);
}
}

// SEO 数据结构
public class SeoData {
private String title; // SEO 优化的标题
private String metaDescription;
private List<String> keywords;
private Map<String, String> structuredData; // JSON-LD 格式
// getters and setters
}
  1. 智能表单填充
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 从用户描述中填充表单
public class FormFiller {
private final ChatClient chatClient;

public FormFiller(ChatClient chatClient) {
this.chatClient = chatClient;
}

public BugReport extractBugReport(String userDescription) {
return chatClient
.prompt()
.user("从以下用户描述中提取bug报告所需的结构化信息:\n\n" + userDescription)
.call()
.entity(BugReport.class);
}
}

// Bug 报告结构
public class BugReport {
private String title;
private String description;
private String browser;
private String operatingSystem;
private String reproducibilitySteps;
private String severity;
private String priority;
// getters and setters
}

使用ChatClient获取结构化响应

Spring AI提供了简洁的方法来获取结构化JSON响应。以下是几种常用的方式:

  1. 明确指导AI生成JSON
    为了增加AI生成有效JSON的概率,我们可以在提示中提供明确的格式指导和示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Product getProductWithFormatGuidance(String productType) {
String prompt = String.format("""
生成一个%s产品的详细信息,必须严格按照以下JSON格式返回:

{
"name": "产品名称",
"price": 数字类型的价格,
"description": "产品描述",
"features": ["特点1", "特点2", "特点3"],
"specifications": {
"weight": "重量",
"dimensions": "尺寸",
"material": "材质"
}
}

不要添加任何额外的文本、解释或Markdown格式,只返回有效的JSON对象。
""", productType);

return chatClient.prompt()
.user(prompt)
.call()
.entity(Product.class);
}
  1. 使用系统消息引导输出格式
    系统消息是引导AI行为的有效方式,可用于确保结构化输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Product getProductWithSystemMessage(String productType) {
SystemMessage systemMessage = new SystemMessage(
"你是一个产品信息生成API。你只能返回有效的JSON格式数据,不能包含任何其他文本。"
);

UserMessage userMessage = new UserMessage(
String.format("生成一个%s产品的详细信息", productType)
);

return chatClient.prompt()
.system("你是一个产品信息生成API。你只能返回有效的JSON格式数据,不能包含任何其他文字。")
.user(String.format("生成一个%s产品的详细信息", productType))
.call()
.entity(Product.class);
}

Java对象映射技术

Spring AI利用Jackson库进行JSON和Java对象之间的映射。
了解一些常用的Jackson注解可以帮助你更好地控制映射过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Product {
// 指定 JSON 属性名
@JsonProperty("product_name")
private String name;

// 格式化数值
@JsonFormat(shape = JsonFormat.Shape.NUMBER_FLOAT, pattern = "#.##")
private BigDecimal price;

// 忽略某个字段
@JsonIgnore
private String internalId;

// 指定日期格式
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private LocalDate releaseDate;

// 控制序列化和反序列化行为
@JsonInclude(JsonInclude.Include.NON_NULL)
private String optionalDescription;

// getters and setters
}

处理不匹配字段
有时AI生成的JSON可能包含我们模型中不存在的字段,或者缺少某些必要字段。
可以使用以下注解来处理这些情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 忽略未知属性,防止反序列化失败
@JsonIgnoreProperties(ignoreUnknown = true)
public class ProductReview {
private String reviewer;
private int rating;
private String comment;

// 设置默认值,处理缺失字段
@JsonSetter(nulls = Nulls.SKIP)
private List<String> pros = new ArrayList<>();

@JsonSetter(nulls = Nulls.SKIP)
private List<String> cons = new ArrayList<>();

// getters and setters
}

处理多态类型
对于需要根据类型字段动态确定具体类的情况,可以使用多态注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = PhysicalProduct.class, name = "physical"),
@JsonSubTypes.Type(value = DigitalProduct.class, name = "digital"),
@JsonSubTypes.Type(value = SubscriptionProduct.class, name = "subscription")
})
public abstract class BaseProduct {
private String id;
private String name;
private BigDecimal price;
// common properties and methods
}

public class PhysicalProduct extends BaseProduct {
private String dimensions;
private double weight;
private String shippingMethod;
// physical-specific properties
}

public class DigitalProduct extends BaseProduct {
private String downloadUrl;
private String fileFormat;
private long fileSizeInBytes;
// digital-specific properties
}

public class SubscriptionProduct extends BaseProduct {
private String billingCycle;
private boolean autoRenew;
private int trialPeriodDays;
// subscription-specific properties
}

使用@JsonClassDescription注解

可以为AI提供关于如何生成与Java类匹配的JSON结构的指导。
这个注解特别重要,因为它帮助AI模型理解你期望的输出格式和内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@JsonClassDescription("表示一个产品的详细信息,包含名称、价格、描述和分类")
public class Product {

@JsonPropertyDescription("产品的完整名称,应当简洁明了且具有描述性")
private String name;

@JsonPropertyDescription("产品价格,以元为单位的数值,不含货币符号")
private BigDecimal price;

@JsonPropertyDescription("产品的详细描述,突出产品的主要特点和优势")
private String description;

@JsonPropertyDescription("产品的主要分类,如'电子产品'、'家居用品'等")
private String category;

// getters and setters
}

嵌套对象描述
对于包含嵌套对象的复杂类,每个嵌套类也可以添加描述注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@JsonClassDescription("电子商务系统中的完整产品信息")
public class DetailedProduct {

@JsonPropertyDescription("产品的基本信息")
private ProductBasic basicInfo;

@JsonPropertyDescription("产品的技术规格列表")
private List<Specification> specifications;

@JsonPropertyDescription("产品的价格信息,包括折扣")
private PriceInfo priceInfo;

// getters and setters
}

@JsonClassDescription("产品的基本信息")
public class ProductBasic {
// fields with descriptions
}

@JsonClassDescription("产品的技术规格")
public class Specification {
// fields with descriptions
}

@JsonClassDescription("产品的价格信息")
public class PriceInfo {
// fields with descriptions
}

在提示中使用类描述
使用注解后,Spring AI会自动将这些描述信息纳入提示,指导AI生成符合预期的JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class ProductGenerationService {

private final ChatClient chatClient;

public ProductGenerationService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public DetailedProduct generateDetailedProduct(String productType) {
String prompt = "为" + productType + "生成一个详细的产品信息。";

return chatClient.prompt()
.user(prompt)
.call()
.entity(DetailedProduct.class);
}
}

处理复杂的嵌套数据结构

实际应用中,我们可能需要处理具有多层嵌套的复杂数据结构。
而SpringAI提供了可处理这类嵌套数据结构情况。
嵌套对象和集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@JsonClassDescription("代表一个电子商务网站的完整产品目录")
public class ProductCatalog {

@JsonPropertyDescription("目录的名称")
private String name;

@JsonPropertyDescription("目录的最后更新时间,格式为ISO-8601")
private LocalDateTime lastUpdated;

@JsonPropertyDescription("产品类别列表,按照类别组织产品")
private List<ProductCategory> categories;

// getters and setters
}

@JsonClassDescription("产品类别,包含该类别下的所有产品")
public class ProductCategory {

@JsonPropertyDescription("类别的唯一标识符")
private String id;

@JsonPropertyDescription("类别的显示名称")
private String name;

@JsonPropertyDescription("该类别的简短描述")
private String description;

@JsonPropertyDescription("该类别下的所有产品")
private List<DetailedProduct> products;

@JsonPropertyDescription("子类别列表")
private List<ProductCategory> subCategories;

// getters and setters
}

处理动态属性
有时我们需要处理不确定的属性名,例如存储产品的自定义属性:

1
2
3
4
5
6
7
8
9
10
11
public class CustomProduct {

private String name;
private BigDecimal price;

// 使用 Map 处理动态属性
@JsonPropertyDescription("产品的自定义属性,键为属性名,值为属性值")
private Map<String, Object> customAttributes;

// getters and setters
}

处理复杂的响应结构
对于非常复杂的结构,可以使用组合设计模式和抽象类/接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = TextBlock.class, name = "text"),
@JsonSubTypes.Type(value = ImageBlock.class, name = "image"),
@JsonSubTypes.Type(value = TableBlock.class, name = "table"),
@JsonSubTypes.Type(value = ListBlock.class, name = "list")
})
public abstract class ContentBlock {
private String id;
private String type;
private Map<String, Object> metadata;

// common methods
}

// 不同类型的内容块实现...
public class TextBlock extends ContentBlock {
private String text;
private String formatting;
// text-specific methods
}

public class ImageBlock extends ContentBlock {
private String url;
private String altText;
private Dimensions dimensions;
// image-specific methods
}

public class TableBlock extends ContentBlock {
private List<List<String>> cells;
private List<String> headers;
// table-specific methods
}

public class ListBlock extends ContentBlock {
private List<String> items;
private String listType; // bullet, numbered, etc.
// list-specific methods
}

第四次实践

开发AI产品推荐系统
该系统将根据用户的偏好和历史行为,生成个性化的产品推荐。

  1. 定义数据模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@JsonClassDescription("用户信息")
public class User {
private String id;

@JsonPropertyDescription("用户的完整姓名")
private String name;

@JsonPropertyDescription("用户的年龄")
private int age;

@JsonPropertyDescription("用户的性别,如'男'、'女'或'其他'")
private String gender;

@JsonPropertyDescription("用户的兴趣爱好列表")
private List<String> interests;

// getters and setters
}

@JsonClassDescription("产品推荐请求")
public class RecommendationRequest {

@JsonPropertyDescription("用户信息")
private User user;

@JsonPropertyDescription("要推荐的产品类别")
private String category;

@JsonPropertyDescription("推荐产品的数量")
private int count;

@JsonPropertyDescription("用户的历史购买记录")
private List<String> purchaseHistory;

// getters and setters
}

@JsonClassDescription("产品推荐结果")
public class RecommendationResponse {

@JsonPropertyDescription("推荐的产品列表")
private List<RecommendedProduct> recommendations;

@JsonPropertyDescription("推荐的依据或原因")
private String reasonForRecommendations;

// getters and setters
}

@JsonClassDescription("推荐的产品信息")
public class RecommendedProduct {

@JsonPropertyDescription("产品的唯一标识符")
private String id;

@JsonPropertyDescription("产品名称")
private String name;

@JsonPropertyDescription("产品描述")
private String description;

@JsonPropertyDescription("产品价格")
private BigDecimal price;

@JsonPropertyDescription("产品推荐理由")
private String recommendationReason;

@JsonPropertyDescription("与用户兴趣的匹配度,1-5分")
private int matchScore;

// getters and setters
}
  1. 实现推荐服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Service
public class ProductRecommendationService {

private final ChatClient chatClient;

public ProductRecommendationService(ChatClient chatClient) {
this.chatClient = chatClient;
}

public RecommendationResponse getRecommendations(RecommendationRequest request) {
// 构建系统消息
SystemMessage systemMessage = new SystemMessage(
"你是代码小抄的产品推荐专家。根据用户信息和历史购买记录提供个性化产品推荐。" +
"确保推荐理由合理且符合用户特征。返回的所有推荐都必须是JSON格式。"
);

// 构建用户消息
String userMessageContent = String.format(
"请为以下用户推荐%d个%s类别的产品:\n" +
"用户名: %s\n" +
"年龄: %d\n" +
"性别: %s\n" +
"兴趣爱好: %s\n" +
"历史购买: %s",
request.getCount(),
request.getCategory(),
request.getUser().getName(),
request.getUser().getAge(),
request.getUser().getGender(),
String.join(", ", request.getUser().getInterests()),
String.join(", ", request.getPurchaseHistory())
);
UserMessage userMessage = new UserMessage(userMessageContent);

// 构建提示并调用 AI
return chatClient.prompt()
.system("你是代码小抄的产品推荐专家。根据用户信息和历史购买记录提供个性化产品推荐。" +
"确保推荐理由合理且符合用户特征。返回的所有推荐都必须是JSON格式。")
.user(userMessageContent)
.call()
.entity(RecommendationResponse.class);
}
}
  1. 创建控制器(REST控制器暴露API)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@RestController
@RequestMapping("/api/recommendations")
public class RecommendationController {

private final ProductRecommendationService recommendationService;

public RecommendationController(ProductRecommendationService recommendationService) {
this.recommendationService = recommendationService;
}

@PostMapping
public ResponseEntity<RecommendationResponse> getRecommendations(@RequestBody RecommendationRequest request) {
try {
RecommendationResponse response = recommendationService.getRecommendations(request);
return ResponseEntity.ok(response);
} catch (Exception e) {
// 错误处理
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

@GetMapping("/sample")
public ResponseEntity<RecommendationRequest> getSampleRequest() {
// 创建一个样例请求,用于示例
User user = new User();
user.setName("微光zc");
user.setAge(22);
user.setGender("男");
user.setInterests(Arrays.asList("编程", "技术博客", "开源项目", "算法"));

RecommendationRequest request = new RecommendationRequest();
request.setUser(user);
request.setCategory("编程工具");
request.setCount(3);
request.setPurchaseHistory(Arrays.asList("Java编程指南", "算法导论", "设计模式"));

return ResponseEntity.ok(request);
}
}
  1. 测试系统
    向/api/recommendations端点发送POST请求:
1
2
3
4
5
6
7
8
9
10
11
{
"user": {
"name": "微光zc",
"age": 22,
"gender": "男",
"interests": ["编程", "技术博客", "开源项目", "算法"]
},
"category": "编程工具",
"count": 3,
"purchaseHistory": ["Java编程指南", "算法导论", "设计模式"]
}

使用python或postman发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import requests

url = "http://localhost:8080/api/recommendations"
data = {
"user": {
"name": "微光zc",
"age": 22,
"gender": "男",
"interests": ["编程", "技术博客", "开源项目", "算法"]
},
"category": "编程工具",
"count": 3,
"purchaseHistory": ["Java编程指南", "算法导论", "设计模式"]
}

response = requests.post(url, json=data)
print(response.status_code)
print(response.json())

系统将返回类似以下的结构化响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"recommendations": [
{
"id": "tool-001",
"name": "IntelliJ IDEA 旗舰版",
"description": "强大的 Java 集成开发环境,提供智能代码补全、重构工具和深度静态分析。",
"price": 1999.00,
"recommendationReason": "作为一名关注编程和开源项目的程序员,高效的 IDE 能显著提升编码效率。历史购买显示对 Java 有浓厚兴趣。",
"matchScore": 5
},
{
"id": "tool-002",
"name": "算法导航 Pro 会员",
"description": "一个专注于算法可视化和学习的平台,包含大量互动练习和竞赛题目。",
"price": 299.00,
"recommendationReason": "根据用户对算法的兴趣和购买历史中的《算法导论》,这个工具可以帮助用户深入理解和实践算法。",
"matchScore": 5
},
{
"id": "tool-003",
"name": "代码小抄 Premium",
"description": "代码片段管理和分享工具,支持多种编程语言的语法高亮和版本控制。",
"price": 199.00,
"recommendationReason": "作为技术博主和开源项目爱好者,这个工具可以帮助整理和分享代码片段,提高工作效率。",
"matchScore": 4
}
],
"reasonForRecommendations": "基于用户的编程背景、算法兴趣和Java学习方向,推荐了提升开发效率的IDE、深化算法学习的平台和代码管理工具。"
}

向量数据库

嵌入(Embedding)的概念解析

嵌入(Embedding)是AI领域的核心概念(向量数据库的基础)
是将复杂数据(如文本、图像、音频等)转换为密集的数值向量的过程。
这些向量能够捕获原始数据的语义信息,使得语义相似的内容在向量空间中彼此靠近。

嵌入的工作原理
嵌入模型通过深度学习训练如何将输入数据映射到一个高维向量空间。
在这个空间中:

  • 相似的概念拥有相似的向量表示
  • 向量间的距离(如欧几里得距离、余弦距离)反映了原始数据的语义差异
  • 向量可以用于数学运算,支持语义组合和比较

不同类型的嵌入

  1. 文本嵌入:将单词、句子或文档转换为向量(如Word2Vec、GloVe、BERT、GPT)。
  2. 图像嵌入:将图像转换为向量(如ResNet、VGG、CLIP)。
  3. 多模态嵌入:将不同类型的数据映射到同一向量空间。

常见向量数据库介绍

  • PostgreSQL + pgvector
  • Redis + RediSearch
  • Milvus(开源专用)
  • Pinecone
  • Elasticsearch + KNN

Spring AI中的向量存储抽象

Spring AI提供了一个统一的抽象层以一致的方式使用不同的向量数据库。

VectorStore接口
VectorStore是Spring AI中向量存储的核心接口,继承自DocumentWriter接口,定义了向量数据的基本操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public interface VectorStore extends DocumentWriter {

// 获取向量存储的名称
default String getName() {
return this.getClass().getSimpleName();
}

// 添加文档到向量存储
void add(List<Document> documents);

// 实现DocumentWriter接口
default void accept(List<Document> documents) {
this.add(documents);
}

// 根据ID列表删除向量
void delete(List<String> idList);

// 使用过滤表达式删除向量
void delete(Filter.Expression filterExpression);

// 使用字符串过滤表达式删除向量
default void delete(String filterExpression) {
SearchRequest searchRequest = SearchRequest.builder().filterExpression(filterExpression).build();
Filter.Expression textExpression = searchRequest.getFilterExpression();
Assert.notNull(textExpression, "Filter expression must not be null");
this.delete(textExpression);
}

// 使用搜索请求进行相似度搜索
@Nullable
List<Document> similaritySearch(SearchRequest request);

// 使用查询字符串进行相似度搜索
@Nullable
default List<Document> similaritySearch(String query) {
return this.similaritySearch(SearchRequest.builder().query(query).build());
}

// 获取原生客户端
default <T> Optional<T> getNativeClient() {
return Optional.empty();
}

// 构建器接口
public interface Builder<T extends Builder<T>> {
T observationRegistry(ObservationRegistry observationRegistry);

T customObservationConvention(VectorStoreObservationConvention convention);

T batchingStrategy(BatchingStrategy batchingStrategy);

VectorStore build();
}
}

搜索请求构建
Spring AI提供了SearchRequest类,用于构建相似度搜索请求:

1
2
3
4
5
6
7
8
SearchRequest request = SearchRequest.builder()
.query("什么是程序员鱼皮的编程导航学习网 codefather.cn?")
.topK(5) // 返回最相似的5个结果
.similarityThreshold(0.7) // 相似度阈值,0.0-1.0之间
.filterExpression("category == 'web' AND date > '2025-05-03'") // 过滤表达式
.build();

List<Document> results = vectorStore.similaritySearch(request);

SearchRequest提供了多种配置选项:

  1. query:搜索的查询文本
  2. topK:返回的最大结果数,默认为4
  3. similarityThreshold:相似度阈值,低于此值的结果会被过滤掉
  4. filterExpression:基于文档元数据的过滤表达式

Spring AI支持的向量存储实现

  1. 内存向量存储:适用于开发和测试,不需要外部依赖。
1
2
3
4
@Bean
public VectorStore inMemoryVectorStore(EmbeddingModel embeddingModel) {
return SimpleVectorStore.builder(embeddingModel).build();
}
  1. Redis向量存储:使用Redis存储向量数据。
1
2
3
4
5
6
7
8
@Bean
public VectorStore redisVectorStore(
EmbeddingModel embeddingModel,
RedisConnectionFactory redisConnectionFactory) {
return RedisVectorStore.builder(redisConnectionFactory, embeddingModel)
.namespace("ai:embeddings")
.build();
}
  1. PostgreSQL向量存储:基于PostgreSQL和pgvector扩展。
1
2
3
4
5
6
@Bean
public VectorStore pgVectorStore(
EmbeddingClient embeddingClient,
DataSource dataSource) {
return new PgVectorStore(embeddingClient, dataSource);
}

配置Spring AI的向量存储
以PostgreSQL向量存储为例,配置包括以下步骤:

  1. 添加依赖:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- Spring AI PGVector 依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pgvector-store</artifactId>
<version>${spring-ai.version}</version>
</dependency>

<!-- PostgreSQL 驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>

  1. 数据库配置:
1
2
3
4
5
6
7
8
# application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/vectordb
spring.datasource.username=postgres
spring.datasource.password=password
spring.datasource.driver-class-name=org.postgresql.Driver

# pgvector 表名配置
spring.ai.vectorstore.pgvector.table-name=document_embeddings
  1. 初始化pgvector:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 在 PostgreSQL 中启用 pgvector 扩展
CREATE EXTENSION IF NOT EXISTS vector;

-- 创建表存储文档和嵌入
CREATE TABLE IF NOT EXISTS document_embeddings (
id text PRIMARY KEY,
content text,
metadata jsonb,
embedding vector(1536)
);

-- 创建向量索引(HNSW)
CREATE INDEX IF NOT EXISTS document_embeddings_embedding_idx ON document_embeddings
USING hnsw (embedding vector_cosine_ops);

基础向量搜索实现

使用Spring AI的向量存储来实现基础的向量搜索功能。

  1. 文档索引服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service
public class DocumentIndexService {

private final VectorStore vectorStore;
private final EmbeddingClient embeddingClient;

public DocumentIndexService(VectorStore vectorStore, EmbeddingClient embeddingClient) {
this.vectorStore = vectorStore;
this.embeddingClient = embeddingClient;
}

public void indexDocument(String id, String content, Map<String, Object> metadata) {
// 生成内容的嵌入
Embedding embedding = embeddingClient.embed(content);

// 存储文档及其嵌入
vectorStore.add(id, embedding, metadata);
}

public void batchIndexDocuments(List<Document> documents) {
vectorStore.add(documents);
}

public void deleteDocument(String id) {
vectorStore.delete(Collections.singletonList(id));
}
}
  1. 文档搜索服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
public class DocumentSearchService {

private final VectorStore vectorStore;

public DocumentSearchService(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}

public List<Document> searchSimilarDocuments(String query, int maxResults) {
return vectorStore.similaritySearch(query, maxResults);
}

public List<Document> searchWithFilter(String query, int maxResults, String category) {
List<String> filter = Collections.singletonList("metadata.category == '" + category + "'");
return vectorStore.similaritySearch(query, maxResults, filter);
}
}
  1. 文档处理工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Component
public class DocumentProcessor {

// 将文本分割成较小的块,便于向量化
public List<Document> splitTextIntoDocuments(String text, String metadata) {
List<Document> documents = new ArrayList<>();
// 假设我们按段落分割
String[] paragraphs = text.split("\n\n");

for (int i = 0; i < paragraphs.length; i++) {
if (paragraphs[i].trim().length() > 10) { // 忽略太短的段落
Map<String, Object> meta = new HashMap<>();
meta.put("index", i);
meta.put("source", metadata);

documents.add(new Document(
UUID.randomUUID().toString(),
paragraphs[i],
meta
));
}
}

return documents;
}

// 从文件创建文档
public List<Document> createDocumentsFromFile(File file) throws IOException {
String content = new String(Files.readAllBytes(file.toPath()));
return splitTextIntoDocuments(content, file.getName());
}
}

相似度计算与匹配原理

向量搜索的核心是相似度计算,决定如何度量两个向量之间的”距离”或”相似程度“。
常用的相似度计算方法

  1. 余弦相似度(Cosine Similarity):
    计算两个向量夹角的余弦值
    范围为-1到1,1表示完全相同,0表示正交,-1表示方向相反
    不考虑向量的大小,只考虑方向
    公式:cosine(A,B)=(AB)/(‖A‖·‖B‖)
1
2
3
4
5
6
7
8
9
10
11
12
13
public double cosineSimilarity(List<Double> v1, List<Double> v2) {
double dotProduct = 0.0;
double norm1 = 0.0;
double norm2 = 0.0;

for (int i = 0; i < v1.size(); i++) {
dotProduct += v1.get(i) * v2.get(i);
norm1 += Math.pow(v1.get(i), 2);
norm2 += Math.pow(v2.get(i), 2);
}

return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
}
  1. 欧几里得距离(Euclidean Distance):
    计算两点在欧几里得空间中的直线距离
    值越小表示越相似
    受向量大小影响
    公式:distance(A,B)=sqrt(sum((A_i-B_i)^2))
1
2
3
4
5
6
7
8
9
10
public double euclideanDistance(List<Double> v1, List<Double> v2) {
double sum = 0.0;

for (int i = 0; i < v1.size(); i++) {
double diff = v1.get(i) - v2.get(i);
sum += diff * diff;
}

return Math.sqrt(sum);
}
  1. 点积(Dot Product):
    两个向量对应元素相乘然后求和
    假设向量已归一化,值越大表示越相似
    公式:dotProduct(A,B)=sum(A_i*B_i)
1
2
3
4
5
6
7
8
9
public double dotProduct(List<Double> v1, List<Double> v2) {
double result = 0.0;

for (int i = 0; i < v1.size(); i++) {
result += v1.get(i) * v2.get(i);
}

return result;
}
  1. 近似最近邻算法
    向量数据库通常使用近似最近邻(ANN)算法来加速相似向量的检索,常见的算法包括:
  • HNSW (Hierarchical Navigable Small World):
    ·构建多层图结构,较低层连接密集,较高层连接稀疏
    ·搜索时从顶层开始,逐层向下查找相似点
    ·查询速度非常快,但构建索引需要较多内存
  • IVF (Inverted File Index):
    ·将向量空间分割成多个髌
    ·查询时先找到最近的簇,然后在簇内搜索
    ·内存占用较小,但查询准确性可能略低
  • IVFPQ (IVF Product Quantization):
    ·VF的扩展,使用乘积量化压缩向量
    ·可以大幅减小内存占用
    ·适合非常大的向量集合

检索增强生成(RAG)入门

RAG的基本概念

检索增强生成(Retrieval–Augmented Generation,RAG)是一种混合AI架构
它将检索系统与生成式AI模型结合起来。
工作流程如下:

  1. 将知识库文档转换为向量表示并存储
  2. 当收到用户查询时,检索最相关的文档片段
  3. 将检索到的文档作为上下文,连同用户查询一起发送给AI模型
  4. AI模型基于检索到的信息生成回答
    RAG的核心优势在于它结合了两种技术的优点:检索系统的准确性和生成模型的灵活性。

文档处理与ETL管道

实现RAG第一步需要处理和准备文档,将其转换为适合检索的格式。
Spring AI提供了一套完整的ETL(提取-转换-加载)组件来简化这一过程。
ETL管道的组成部分
一个完整的RAG ETL管道通常包括以下步骤:

  1. 提取(Extract):从各种来源加载文档
  2. 转换(Transform):清理、分块和向量化文档
  3. 加载(Load):将向量化的文档存入向量数据库

DocumentReader:文档提取
Spring AI提供了多种DocumentReader实现,可以从不同来源和格式读取文档:

  • JsonReader:解析JSON数据
  • TextReader:读取纯文本文件
  • MarkdownReader:读取Markdown文件
  • PDFReader:读取PDF文件
  • HtmlReader:解析HTML内容
  • TikaDocumentReader:使用Apache Tika读取多种格式
    示例代码:使用TextReader读取文本文件:
1
2
3
4
5
6
7
public List<Document> loadDocuments(String directoryPath) {
TextReader reader = new TextReader();
reader.setRecursive(true);

Resource resource = new FileSystemResource(directoryPath);
return reader.get(resource);
}

DocumentTransformer:文档转换
DocumentTransformer负责处理和转换文档,主要包括以下几类:

  1. 文本分块器(TextSplitter)
    分块器负责将长文本切分为适合嵌入的片段:
1
2
3
4
5
6
7
8
// 创建基于Token的分块器
TextSplitter splitter = new TokenTextSplitter();
splitter.setChunkSize(512); // 设置块大小
splitter.setChunkOverlap(50); // 设置重叠大小

// 处理文档
List<Document> documents = loadDocuments("./data");
List<Document> splitDocuments = splitter.apply(documents);
  1. 元数据增强器(MetadataEnricher)
    元数据增强器可以为文档添加额外的元数据信息:
1
2
3
4
5
6
7
8
9
10
11
// 创建关键字元数据增强器
KeywordMetadataEnricher keywordEnricher = new KeywordMetadataEnricher();
keywordEnricher.setMetadataKey("keywords");

// 创建摘要元数据增强器
SummaryMetadataEnricher summaryEnricher = new SummaryMetadataEnricher(chatClient);
summaryEnricher.setMetadataKey("summary");

// 处理文档
List<Document> enrichedDocuments = keywordEnricher.apply(splitDocuments);
enrichedDocuments = summaryEnricher.apply(enrichedDocuments);
  1. 内容格式化器(ContentFormatter)
    ContentFormatter可以重新格式化文档内容,例如移除多余的空白或HTML标签:
1
2
3
4
ContentFormatter formatter = new ContentFormatter();
formatter.setFormatType(ContentFormatter.FormatType.MARKDOWN);

List<Document> formattedDocuments = formatter.apply(enrichedDocuments);

DocumentWriter:文档存储
DocumentWriter负责将处理后的文档存储到目标位置:

1
2
3
4
5
6
7
// 存储到文件
FileDocumentWriter fileWriter = new FileDocumentWriter("./processed-documents");
fileWriter.accept(formattedDocuments);

// 存储到向量数据库
VectorStoreWriter vectorStoreWriter = new VectorStoreWriter(vectorStore);
vectorStoreWriter.accept(formattedDocuments);

向量存储与检索

VectorStore接口
Spring Al提供了统一的VectorStore接口,它扩展了DocumentWriter接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public interface VectorStore extends DocumentWriter {
// 获取向量存储的名称
default String getName() {
return this.getClass().getSimpleName();
}
// 添加文档到向量存储
void add(List<Document> documents);
// 实现DocumentWriter接口
default void accept(List<Document> documents) {
this.add(documents);
}
// 根据ID列表删除向量
void delete(List<String> idList);
// 使用过滤表达式删除向量
void delete(Filter.Expression filterExpression);
// 使用字符串过滤表达式删除向量
default void delete(String filterExpression) {
SearchRequest searchRequest = SearchRequest.builder().filterExpression(filterExpression).build();
Filter.Expression textExpression = searchRequest.getFilterExpression();
Assert.notNull(textExpression, "Filter expression must not be null");
this.delete(textExpression);
}
// 使用搜索请求进行相似度搜索
@Nullable
List<Document> similaritySearch(SearchRequest request);
// 使用查询字符串进行相似度搜索
@Nullable
default List<Document> similaritySearch(String query) {
return this.similaritySearch(SearchRequest.builder().query(query).build());
}
// 获取原生客户端
default <T> Optional<T> getNativeClient() {
return Optional.empty();
}
// 构建器接口
public interface Builder<T extends Builder<T>> {
T observationRegistry(ObservationRegistry observationRegistry);
T customObservationConvention(VectorStoreObservationConvention convention);
T batchingStrategy(BatchingStrategy batchingStrategy);
VectorStore build();
}
}

SearchRequest
SearchRequest是一个构建器类,用于创建相似性搜索请求:

1
2
3
4
5
6
7
8
SearchRequest request = SearchRequest.builder()
.query("Spring框架如何处理依赖注入?")
.topK(5) // 返回前5个结果
.similarityThreshold(0.7f) // 设置相似度阈值
.filterExpression("metadata.category == '技术文档'") // 设置过滤条件
.build();

List<Document> results = vectorStore.similaritySearch(request);

支持的向量数据库
Spring AI支持多种向量数据库,包括:

  1. 内存向量库:用于测试和小型应用
1
2
3
4
5
// 创建内存向量存储
EmbeddingModel embeddingModel = new OpenAiEmbeddingModel("text-embedding-3-small");
InMemoryVectorStore vectorStore = InMemoryVectorStore.builder()
.embeddingModel(embeddingModel)
.build();
  1. Redis向量库:利用Redis的高性能特性
1
2
3
4
5
6
// 创建 Redis 向量存储
RedisVectorStore vectorStore = RedisVectorStore.builder()
.embeddingModel(embeddingModel)
.redisTemplate(redisTemplate)
.indexName("document-index")
.build();
  1. PostgreSQL向量库:使用PGVector扩展
1
2
3
4
5
// 创建 PostgreSQL 向量存储
PgVectorStore vectorStore = PgVectorStore.builder()
.embeddingModel(embeddingModel)
.jdbcTemplate(jdbcTemplate)
.build();
  1. 其他支持的向量数据库:Milvus、Elasticsearch、Chroma、Pinecone、Weaviate等。

实现RAG系统

查询处理与优化
Spring AI提供了一系列组件,用于优化用户查询,提高检索质量:

  1. 查询转换器(QueryTransformer)
    查询转换器可以重写或增强用户查询:
1
2
3
4
5
6
7
8
9
10
11
// 查询重写转换器
RewriteQueryTransformer rewriteTransformer = new RewriteQueryTransformer(chatClient);
String enhancedQuery = rewriteTransformer.transform("怎么学Java?");

// 翻译查询转换器
TranslationQueryTransformer translationTransformer = new TranslationQueryTransformer(chatClient, Locale.ENGLISH);
String translatedQuery = translationTransformer.transform("如何实现单例模式?");

// 压缩查询转换器
CompressionQueryTransformer compressionTransformer = new CompressionQueryTransformer(chatClient);
String compressedQuery = compressionTransformer.transform("请详细解释Spring框架中的依赖注入机制...");
  1. 查询扩展器(QueryExpander)
    查询扩展器可以生成多个查询变体,提高检索命中率:
1
2
MultiQueryExpander multiQueryExpander = new MultiQueryExpander(chatClient);
List<String> queryVariants = multiQueryExpander.expand("如何优化MySQL查询?");

文档检索

  1. DocumentRetriever接口
    DocumentRetriever是检索文档的核心接口:
1
2
3
public interface DocumentRetriever {
List<Document> retrieve(String query);
}

主要实现是VectorStoreDocumentRetriever

1
2
3
4
5
6
7
// 创建向量存储文档检索器
VectorStoreDocumentRetriever retriever = VectorStoreDocumentRetriever.builder()
.vectorStore(vectorStore)
.k(5) // 检索前5个结果
.build();

List<Document> documents = retriever.retrieve("Spring Boot自动配置原理");
  1. DocumentJoiner
    负责合并来自多个来源的文档:
1
2
ConcatenationDocumentJoiner joiner = new ConcatenationDocumentJoiner();
Document combinedDocument = joiner.join(documents);

RAG Advisors

Spring AI提供了高级的RAG Advisor组件,简化RAG系统实现:

  1. QuestionAnswerAdvisor
    QuestionAnswerAdvisor提供了检索文档并构建AI提示的基本功能:
1
2
3
4
5
6
7
8
9
10
11
12
// 创建问答顾问
QuestionAnswerAdvisor advisor = QuestionAnswerAdvisor.builder()
.chatClient(chatClient)
.retriever(retriever)
.build();

// 回答问题
QaResponse response = advisor.answer("Spring Boot的核心特性有哪些?");
System.out.println("回答: " + response.getText());

// 获取来源文档
List<RetrievalResult> retrievalResults = response.getRetrievalResults();
  1. RetrievalAugmentationAdvisor
    提供了更灵活的RAG实现,可以自定义查询转换器和文档检索器:
1
2
3
4
5
6
7
8
9
10
11
// 创建检索增强顾问
RetrievalAugmentationAdvisor ragAdvisor = RetrievalAugmentationAdvisor.builder()
.chatClient(chatClient)
.queryTransformer(rewriteTransformer)
.retriever(retriever)
.build();

// 回答问题
String answer = ragAdvisor.retrieve("什么是微服务架构?")
.thenApply(ragAdvisor::generateAnswer)
.getText();
  1. ContextualQueryAugmenter
    可以在上下文为空时处理边缘情况,并允许自定义错误消息:
1
2
3
4
5
6
7
// 创建上下文查询增强器
ContextualQueryAugmenter augmenter = ContextualQueryAugmenter.builder()
.emptyContextPromptTemplate("找不到相关信息,请告诉用户:{query}")
.contextPromptTemplate("基于以下信息回答问题:\n\n{context}\n\n问题:{query}")
.build();

String prompt = augmenter.augment("Spring WebFlux是什么?", retrievalResults);

工具调用功能

工具定义模式

Spring Al提供了两种定义工具的模式:

  1. 基于Methods方法(推荐)
  2. 基于Functions函数式编程

定义工具的方式
Spring AI提供了两种定义工具的实现方法:

  1. 注解式:只需在普通Java方法上添加@Tool注解即可定义工具,简单又直观。
    为每个工具添加详细清晰的描述非常重要,这能帮助AI准确判断何时应调用该工具。
    同时,可以使用@Too1Param注解为工具参数提供说明。
1
2
3
4
5
6
7
class WeatherTools {
@Tool(description = "获取指定城市的当前天气情况")
String getWeather(@ToolParam(description = "城市名称") String city) {
// 获取天气的实现逻辑
return "北京今天晴朗,气温25°C";
}
}
  1. 编程式:如果需要在运行时动态创建工具,可以选择更灵活的编程式方法。
    首先定义工具类:
1
2
3
4
5
6
class WeatherTools {
String getWeather(String city) {
// 获取天气的实现逻辑
return "北京今天晴朗,气温25°C";
}
}

然后将其转换为ToolCallback工具定义:

1
2
3
4
5
6
7
8
Method method = ReflectionUtils.findMethod(WeatherTools.class, "getWeather", String.class);
ToolCallback toolCallback = MethodToolCallback.builder()
.toolDefinition(ToolDefinition.builder(method)
.description("获取指定城市的当前天气情况")
.build())
.toolMethod(method)
.toolObject(new WeatherTools())
.build();

在定义工具时,需要注意选择合适的参数和返回值类型。
Spring AI支持绝大多数常见Java类型作为参数和返回值,包括基本类型、复杂对象和各类集合。
返回值必须可序列化,因为它们需要传送给AI大模型。
以下类型目前不支持作为工具方法的参数或返回类型:

  • Optional类型
  • 异步类型(如CompletableFuture,Future)
  • 响应式类型(如Flow,Mono,Flux)
  • 函数式类型(如Function,Supplier,Consumer)

使用工具

定义好工具后,Spring AI提供了多种灵活方式将工具提供给ChatClient,让AI能在适当时机调用这些工具。

  1. 按需使用:这是最简洁的方式,直接在构建ChatClient请求时通过tools()方法附加所需工具。
1
2
3
4
5
String response = ChatClient.create(chatModel)
.prompt("北京今天天气怎么样?")
.tools(new WeatherTools()) // 在当前对话中提供天气工具
.call()
.content();
  1. 全局使用:如果某些工具需要在所有对话中都可用,可以在构建ChatClient时注册为默认工具。
1
2
3
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new WeatherTools(), new TimeTools()) // 注册默认可用的工具
.build();
  1. 更底层的使用方式:除了给ChatClient绑定工具,还可以直接为底层ChatModel绑定工具。
1
2
3
4
5
6
7
8
9
// 先获取工具对象
ToolCallback[] weatherTools = ToolCallbacks.from(new WeatherTools());
// 绑定工具到对话
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(weatherTools)
.build();
// 构造 Prompt 时指定对话选项
Prompt prompt = new Prompt("北京今天天气怎么样?", chatOptions);
chatModel.call(prompt);

在使用工具时,Spring AI为我们自动处理了整个工具调用流程:
从AI模型判断需要调用工具–>执行相应的工具方法–>将结果返回给模型–>最后模型基于工具执行结果生成最终回答。
这整个过程对开发者来说是完全透明的,我们只需专注于实现工具的核心业务逻辑即可。

使用@Tool注解创建可调用函数

@Tool注解是Spring AI工具调用功能的核心,可用于标记那些可以被Al模型调用的方法。
@Too1注解主要有以下核心属性:

  • description:详细说明工具的功能和用途,帮助AI准确判断何时应该使用该工具
  • name:工具的唯一标识符(可选参数,默认会使用方法名)

首先,在项目根包下创建too1s包,用于集中管理所有工具类;
同时,为了使工具结果含义更加明确,我们尽量让工具返回String类型的值。

  1. 文件操作工具
    主要提供两个核心功能:文件保存和文件读取。
    由于文件操作会影响系统资源,需要将所有文件统一存放在一个隔离的目录中,先在constant包下创建文件常量类:
1
2
3
4
5
6
public interface FileConstant {
/**
* 文件保存目录
*/
String FILE_SAVE_DIR = System.getProperty("user.dir") + "/tmp";
}

然后编写文件操作工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class FileOperationTool {

private final String FILE_DIR = FileConstant.FILE_SAVE_DIR + "/file";

@Tool(description = "读取指定文件的内容")
public String readFile(@ToolParam(description = "要读取的文件名称") String fileName) {
String filePath = FILE_DIR + "/" + fileName;
try {
return FileUtil.readUtf8String(filePath);
} catch (Exception e) {
return "读取文件出错: " + e.getMessage();
}
}

@Tool(description = "将内容写入到指定文件")
public String writeFile(
@ToolParam(description = "要写入的文件名称") String fileName,
@ToolParam(description = "要写入的文件内容") String content) {
String filePath = FILE_DIR + "/" + fileName;
try {
// 确保目录存在
FileUtil.mkdir(FILE_DIR);
FileUtil.writeUtf8String(content, filePath);
return "文件成功写入至: " + filePath;
} catch (Exception e) {
return "写入文件出错: " + e.getMessage();
}
}
}
  1. 联网搜索工具
    根据用户提供的关键词搜索相关网页信息。
    可以利用专业的网页搜索API(如Search API)来实现多渠道的内容搜索:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class WebSearchTool {

// SearchAPI 搜索接口地址
private static final String SEARCH_API_URL = "https://www.searchapi.io/api/v1/search";

private final String apiKey;

public WebSearchTool(String apiKey) {
this.apiKey = apiKey;
}

@Tool(description = "从百度搜索引擎搜索信息")
public String searchWeb(
@ToolParam(description = "搜索关键词") String query) {
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("q", query);
paramMap.put("api_key", apiKey);
paramMap.put("engine", "baidu");
try {
String response = HttpUtil.get(SEARCH_API_URL, paramMap);
// 取出返回结果的前 5 条
JSONObject jsonObject = JSONUtil.parseObj(response);
// 提取 organic_results 部分
JSONArray organicResults = jsonObject.getJSONArray("organic_results");
List<Object> objects = organicResults.subList(0, 5);
// 拼接搜索结果为字符串
String result = objects.stream().map(obj -> {
JSONObject tmpJSONObject = (JSONObject) obj;
return tmpJSONObject.toString();
}).collect(Collectors.joining(","));
return result;
} catch (Exception e) {
return "搜索出错: " + e.getMessage();
}
}
}
  1. 网页抓取工具
    用于获取和解析指定网址的页面内容。
    借助jsoup库实现网页内容的抓取和解析:
1
2
3
4
5
6
7
8
9
10
11
12
public class WebScrapingTool {

@Tool(description = "从指定URL抓取网页内容")
public String scrapeWebPage(@ToolParam(description = "要抓取的网页URL") String url) {
try {
Document doc = Jsoup.connect(url).get();
return doc.html();
} catch (IOException e) {
return "抓取网页出错: " + e.getMessage();
}
}
}
  1. 终端操作工具
    允许执行系统命令,例如运行Python脚本或执行其他命令行操作。
    通过Java的Process API,我们可以安全地执行终端命令:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class TerminalOperationTool {

@Tool(description = "在终端执行命令")
public String executeTerminalCommand(@ToolParam(description = "要执行的命令") String command) {
StringBuilder output = new StringBuilder();
try {
ProcessBuilder builder = new ProcessBuilder("cmd.exe", "/c", command);
Process process = builder.start();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
String line;
while ((line = reader.readLine()) != null) {
output.append(line).append("\n");
}
}
int exitCode = process.waitFor();
if (exitCode != 0) {
output.append("命令执行失败,退出码: ").append(exitCode);
}
} catch (IOException | InterruptedException e) {
output.append("执行命令出错: ").append(e.getMessage());
}
return output.toString();
}
}
  1. 资源下载工具
    从指定链接获取文件并保存到本地存储。
    利用Hutool的HttpUti1.down1 oadFile方法,我们可以简化文件下载过程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ResourceDownloadTool {

@Tool(description = "从指定URL下载资源到本地")
public String downloadResource(@ToolParam(description = "要下载的资源URL") String url, @ToolParam(description = "保存到本地的文件名") String fileName) {
String fileDir = FileConstant.FILE_SAVE_DIR + "/download";
String filePath = fileDir + "/" + fileName;
try {
// 确保目录存在
FileUtil.mkdir(fileDir);
// 使用 Hutool 的 downloadFile 方法下载资源
HttpUtil.downloadFile(url, new File(filePath));
return "资源成功下载至: " + filePath;
} catch (Exception e) {
return "下载资源出错: " + e.getMessage();
}
}
}
  1. PDF生成工具
    根据提供的内容创建格式化的PDF文档。
    使用itext库实现PDF文档的生成:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class PDFGenerationTool {

@Tool(description = "根据内容生成PDF文件")
public String generatePDF(
@ToolParam(description = "要保存的PDF文件名") String fileName,
@ToolParam(description = "PDF文件的内容") String content) {
String fileDir = FileConstant.FILE_SAVE_DIR + "/pdf";
String filePath = fileDir + "/" + fileName;
try {
// 确保目录存在
FileUtil.mkdir(fileDir);
// 创建 PdfWriter 和 PdfDocument 对象
try (PdfWriter writer = new PdfWriter(filePath);
PdfDocument pdf = new PdfDocument(writer);
Document document = new Document(pdf)) {
// 使用内置中文字体
PdfFont font = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H");
document.setFont(font);
// 创建段落
Paragraph paragraph = new Paragraph(content);
// 添加段落并关闭文档
document.add(paragraph);
}
return "PDF文件成功生成至: " + filePath;
} catch (IOException e) {
return "生成PDF出错: " + e.getMessage();
}
}
}

集中注册工具
开发完成这些工具类后,根据应用需求,我们可以一次性向AI提供所有工具,让其自主决定何时调用哪个工具。
为此,创建一个工具注册类可以更方便地统一管理和绑定所有工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration
public class ToolRegistration {

@Value("${search-api.api-key}")
private String searchApiKey;

@Bean
public ToolCallback[] allTools() {
FileOperationTool fileOperationTool = new FileOperationTool();
WebSearchTool webSearchTool = new WebSearchTool(searchApiKey);
WebScrapingTool webScrapingTool = new WebScrapingTool();
ResourceDownloadTool resourceDownloadTool = new ResourceDownloadTool();
TerminalOperationTool terminalOperationTool = new TerminalOperationTool();
PDFGenerationTool pdfGenerationTool = new PDFGenerationTool();
return ToolCallbacks.from(
fileOperationTool,
webSearchTool,
webScrapingTool,
resourceDownloadTool,
terminalOperationTool,
pdfGenerationTool
);
}
}

MCP 协议

MCP(Model Context Protocol,模型上下文协议)
是一种开放标准,它让AI能够与外部系统交互。可以将MCP想象成AI应用的标准接口,就像USB让电脑可以连接各种设备一样,MCP让AI模型能够连接不同的数据源和工具
MCP协议的三大作用:

  1. 轻松增强AI的能力:通过接入MCP服务,AI可以搜索网页、查询数据库、调用第三方API、执行计算等
  2. 统一标准,降低使用成本:开发者不必为每个项目重复开发相同功能
  3. 打造服务生态:形成MCP服务市场,让开发者能共享和使用彼此的服务
    MCP的核心架构是”客户端-服务器”模式,客户端主机可以连接到多个服务器。
    在开发层面,MCP SDK分为三层:
  4. 客户端/服务器层:处理各自的协议操作
  5. 会话层:管理通信模式和状态
  6. 传输层:处理消息序列化和传输

MCP支持多种核心概念,其中最实用的是Tools(工具),让服务端可以向客户端提供可调用的函数,使AI模型能执行计算、查询信息或与外部系统交互。

云平台使用 MCP
以阿里云百炼为例,平台提供了多种预置的MCP服务,比如高德地图MCP服务。
使用非常简单,只需进入智能体应用,在左侧添加MCP服务,然后选择想用的服务即可。
例如,我们可以测试高德地图MCP:
提示:我的另一半居住在上海静安区,请帮我找到 5 公里内合适的约会地点。
AI会自动调用MCP提供的工具,比如将地点转为坐标、查找附近地点,然后生成回复。

软件客户端使用MCP
以Cursor为例,步骤如下:

  1. 安装必要工具,如Node.js和NPX(大多数MCP服务都支持NPX运行)
  2. 申请所需的API Key(如高德地图API Key)
  3. 在Cursor设置中添加MCP Server,并粘贴配置
  4. 测试使用,同样可以询问附近约会地点
1
2
3
4
5
6
7
8
9
// MCP 配置示例
{
"name": "amap-maps",
"command": "npx",
"args": ["-y", "@amap/amap-maps-mcp-server"],
"env": {
"AMAP_MAPS_API_KEY": "你的API密钥"
}
}

程序中使用MCP
通过Spring Al框架,我们可以在Java程序中使用MCP。

  1. 引入依赖:
1
2
3
4
5
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
  1. 配置MCP服务:
1
2
3
4
5
6
7
8
9
10
11
12
// mcp-servers.json
{
"mcpServers": {
"amap-maps": {
"command": "npx",
"args": ["-y", "@amap/amap-maps-mcp-server"],
"env": {
"AMAP_MAPS_API_KEY": "你的API密钥"
}
}
}
}
  1. 修改Spring配置:
1
2
3
4
5
6
spring:
ai:
mcp:
client:
stdio:
servers-configuration: classpath:mcp-servers.json
  1. 编写代码使用MCP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Resource
private ToolCallbackProvider toolCallbackProvider;

public String doChatWithMcp(String message, String chatId) {
ChatResponse response = chatClient
.prompt()
.user(message)
.advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))
.advisors(new MyLoggerAdvisor())
.tools(toolCallbackProvider)
.call()
.chatResponse();
String content = response.getResult().getOutput().getText();
log.info("content: {}", content);
return content;
}

Spring AI MCP开发模式
MCP客户端开发
Spring AI 提供了与 Spring Boot整合的MCP 客户端 SDK,支持同步和异步调用模式。

  1. 引入依赖:
1
2
3
4
5
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-mcp-client-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
  1. 配置连接:支持直接在配置文件中设置或引用JSON文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 方式一:直接配置
spring:
ai:
mcp:
client:
enabled: true
name: 编程导航-mcp-client
version: 1.0.0
request-timeout: 30s
type: SYNC
sse:
connections:
server1:
url: http://localhost:8080

# 方式二:引用JSON文件
spring:
ai:
mcp:
client:
stdio:
servers-configuration: classpath:mcp-servers.json
  1. 使用服务:通过自动注入的Bean来使用MCP服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用方式一:直接控制 MCP 客户端
@Autowired
private List<McpSyncClient> mcpSyncClients;

// 使用方式二:整合 Spring AI 工具调用
@Autowired
private SyncMcpToolCallbackProvider toolCallbackProvider;

// 在 AI 对话中使用 MCP 工具
ChatResponse response = chatClient
.prompt()
.user("请查询上海静安区附近的餐厅")
.tools(toolCallbackProvider)
.call()
.chatResponse();

MCP服务端开发
SpringAI还提供了MCP服务端开发的支持,让我们可以开发自己的MCP服务。

  1. 引入依赖:
1
2
3
4
5
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server</artifactId>
<version>1.0.0</version>
</dependency>
  1. 配置服务
1
2
3
4
5
6
7
8
spring:
ai:
mcp:
server:
name: 编程导航-mcp-server
version: 1.0.0
stdio: true
type: SYNC
  1. 开发服务:使用@Too1注解标记服务类中的方法
1
2
3
4
5
6
7
8
9
@Service
public class WeatherService {
@Tool(description = "获取指定城市的天气信息")
public String getWeather(
@ToolParam(description = "城市名称,如北京、上海") String cityName) {
// 实现天气查询逻辑
return "城市" + cityName + "的天气是晴天,温度22°C";
}
}
  1. 注册工具
1
2
3
4
5
6
7
8
9
@SpringBootApplication
public class McpServerApplication {
@Bean
public ToolCallbackProvider weatherTools(WeatherService weatherService) {
return MethodToolCallbackProvider.builder()
.toolObjects(weatherService)
.build();
}
}

第五次实践

MCP开发实战:图片搜索服务
服务端开发

  1. 创建项目并引入依赖:
1
2
3
4
5
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
<version>1.0.0</version>
</dependency>
  1. 配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
application:
name: yu-image-search-mcp-server
profiles:
active: stdio
ai:
mcp:
server:
name: yu-image-search-mcp-server
version: 0.0.1
type: SYNC
stdio: true
server:
port: 8127
  1. 开发图片搜索工具:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Service
public class ImageSearchTool {

private static final String API_KEY = "你的Pexels API密钥";
private static final String API_URL = "https://api.pexels.com/v1/search";

@Tool(description = "search image from web")
public String searchImage(@ToolParam(description = "Search query keyword") String query) {
try {
return String.join(",", searchMediumImages(query));
} catch (Exception e) {
return "Error search image: " + e.getMessage();
}
}

public List<String> searchMediumImages(String query) {
// 设置请求头
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", API_KEY);

// 设置请求参数
Map<String, Object> params = new HashMap<>();
params.put("query", query);

// 发送GET请求
String response = HttpUtil.createGet(API_URL)
.addHeaders(headers)
.form(params)
.execute()
.body();

// 解析响应JSON
return JSONUtil.parseObj(response)
.getJSONArray("photos")
.stream()
.map(photoObj -> (JSONObject) photoObj)
.map(photoObj -> photoObj.getJSONObject("src"))
.map(photo -> photo.getStr("medium"))
.filter(StrUtil::isNotBlank)
.collect(Collectors.toList());
}
}
  1. 在主类中注册工具:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
public class YuImageSearchMcpServerApplication {

public static void main(String[] args) {
SpringApplication.run(YuImageSearchMcpServerApplication.class, args);
}

@Bean
public ToolCallbackProvider imageSearchTools(ImageSearchTool imageSearchTool) {
return MethodToolCallbackProvider.builder()
.toolObjects(imageSearchTool)
.build();
}
}

客户端调用

  1. 配置客户端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// mcp-servers.json
{
"mcpServers": {
"yu-image-search-mcp-server": {
"command": "java",
"args": [
"-Dspring.ai.mcp.server.stdio=true",
"-Dspring.main.web-application-type=none",
"-Dlogging.pattern.console=",
"-jar",
"yu-image-search-mcp-server/target/yu-image-search-mcp-server-0.0.1-SNAPSHOT.jar"
],
"env": {}
}
}
}
  1. 测试
1
2
3
4
5
6
7
@Test
void doChatWithMcp() {
// 测试图片搜索 MCP
String message = "帮我搜索一些编程导航相关的图片";
String answer = loveApp.doChatWithMcp(message, chatId);
Assertions.assertNotNull(answer);
}

MCP部署方案
MCP服务的部署可分为本地部署和远程部署两种方式:

  1. 本地部署:适用于 stdio 传输方式,将 MCP服务打包后放在客户端可访问的路径下
  2. 远程部署:适用于 SSE 传输方式,部署为独立的Web 服务
  3. Serverless部署:使用云函数等Serverless平台部署MCP服务,按需付费
    还可以将开发好的 MCP 服务提交到 MCP 服务市场,供更多开发者使用,比如 MCP.so、阿里云百炼 MCP 服务市场等。

练习

练习1:MCP客户端配置
请编写一个使用SpringAI通过stdio模式连接图片搜索MCP服务的配置文件。

1
2
3
4
5
6
spring:
ai:
mcp:
client:
stdio:
servers-configuration: classpath:mcp-servers.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// mcp-servers.json
{
"mcpServers": {
"image-search": {
"command": "java",
"args": [
"-jar",
"path/to/image-search-mcp-server.jar"
],
"env": {
"API_KEY": "your-api-key-here"
}
}
}
}

练习2:创建简单的MCP工具
开发一个计算器MCP服务,提供加法和乘法工具。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Service
public class CalculatorService {

@Tool(description = "计算两个数字的和")
public double add(
@ToolParam(description = "第一个数字") double a,
@ToolParam(description = "第二个数字") double b) {
return a + b;
}

@Tool(description = "计算两个数字的乘积")
public double multiply(
@ToolParam(description = "第一个数字") double a,
@ToolParam(description = "第二个数字") double b) {
return a * b;
}
}

@SpringBootApplication
public class CalculatorMcpApplication {

public static void main(String[] args) {
SpringApplication.run(CalculatorMcpApplication.class, args);
}

@Bean
public ToolCallbackProvider calculatorTools(CalculatorService calculatorService) {
return MethodToolCallbackProvider.builder()
.toolObjects(calculatorService)
.build();
}
}

AI应用测试与部署

单元测试与集成测试实现

AI组件的单元测试
对于AI应用,单元测试主要关注应用的非AI部分和与AI服务的集成接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerIntegrationTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private ChatClient chatClient;

@Test
public void testGenerateProductDescription() throws Exception {
// 模拟 AI 响应
when(chatClient.call(any())).thenReturn(
"这是一个AI生成的产品描述。"
);

// 执行请求并验证
mockMvc.perform(post("/api/products/describe")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"代码小抄\",\"category\":\"开发工具\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.description").exists())
.andExpect(jsonPath("$.description").isString());
}
}

AI应用的集成测试
集成测试验证整个系统的功能,可以使用真实的AI服务或模拟服务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@SpringBootTest
@AutoConfigureMockMvc
public class ProductControllerIntegrationTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private ChatClient chatClient;

@Test
public void testGenerateProductDescription() throws Exception {
// 模拟 AI 响应
when(chatClient.call(any())).thenReturn(
"这是一个AI生成的产品描述。"
);

// 执行请求并验证
mockMvc.perform(post("/api/products/describe")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"代码小抄\",\"category\":\"开发工具\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.description").exists())
.andExpect(jsonPath("$.description").isString());
}
}

使用固定提示和响应集
为了处理AI响应的非确定性,可以使用固定的测试数据集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testProductRecommendations() {
// 从测试资源中加载固定的提示和期望响应对
List<PromptResponsePair> testCases = loadTestCases("product_recommendations_test_cases.json");

for (PromptResponsePair testCase : testCases) {
// 模拟固定响应
when(chatClient.call(testCase.getPrompt())).thenReturn(testCase.getResponse());

// 验证系统行为
ProductRecommendation recommendation =
recommendationService.getRecommendation(testCase.getInput());

// 断言期望结果
assertThat(recommendation).isNotNull();
assertThat(recommendation.getProducts().size()).isGreaterThan(0);
// 其他断言...
}
}

使用Mock对象进行测试

Mock对象对于 AI 应用测试尤为重要,因为可以:

  • 消除对实际AI服务的依赖
  • 创建可重复的测试环境
  • 避免API调用费用
  • 模拟特定的响应场景
    创建ChatClient的Mock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Configuration
public class TestConfig {

@Bean
@Primary
@Profile("test")
public ChatClient mockChatClient() {
// 创建一个预设响应的 Mock ChatClient
Map<String, String> promptResponseMap = new HashMap<>();
promptResponseMap.put(
"总结这篇文章",
"这是一篇关于Spring AI的文章。"
);
promptResponseMap.put(
"分析这段代码",
"这段代码实现了一个简单的排序算法。"
);

return new MockChatClient(promptResponseMap);
}
}

public class MockChatClient implements ChatClient {

private final Map<String, String> promptResponseMap;

public MockChatClient(Map<String, String> promptResponseMap) {
this.promptResponseMap = promptResponseMap;
}

@Override
public String call(String prompt) {
// 尝试精确匹配
if (promptResponseMap.containsKey(prompt)) {
return promptResponseMap.get(prompt);
}

// 尝试模糊匹配
for (Map.Entry<String, String> entry : promptResponseMap.entrySet()) {
if (prompt.contains(entry.getKey())) {
return entry.getValue();
}
}

// 默认响应
return "这是一个模拟响应。";
}

@Override
public ChatResponse call(Prompt prompt) {
// 实现从 Prompt 对象提取文本并返回响应的逻辑
// ...
}

// 实现其他必要的方法...
}

模拟特定场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testErrorHandling() {
// 创建会抛出异常的 mock
ChatClient errorChatClient = mock(ChatClient.class);
when(errorChatClient.call(anyString())).thenThrow(new RuntimeException("API 错误"));

// 注入 mock
ReflectionTestUtils.setField(aiService, "chatClient", errorChatClient);

// 验证错误处理
assertThatThrownException(() ->
aiService.generateContent("任何提示词")
).isInstanceOf(AIServiceException.class)
.hasMessageContaining("调用 AI 服务失败");
}

评估AI响应质量的方法

AI评估

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Service
public class ResponseEvaluationService {

private final ChatClient evaluationChatClient;

public ResponseEvaluationService(ChatClient evaluationChatClient) {
this.evaluationChatClient = evaluationChatClient;
}

public EvaluationResult evaluateResponse(String query, String response) {
String evaluationPrompt = String.format("""
请评估以下AI回答的质量。评分标准:
1. 相关性(1-10):回答与问题的相关程度
2. 准确性(1-10):回答中事实的准确性
3. 完整性(1-10):回答是否完整地解决了问题
4. 清晰度(1-10):回答的逻辑和表述是否清晰

问题:%s
AI回答:%s

请提供四个评分和总体评价,格式为JSON:
{"relevance": 分数, "accuracy": 分数, "completeness": 分数, "clarity": 分数, "comments": "评价"}
""", query, response);

String evaluationJson = evaluationChatClient.call(evaluationPrompt);

// 解析JSON响应
ObjectMapper mapper = new ObjectMapper();
try {
return mapper.readValue(evaluationJson, EvaluationResult.class);
} catch (Exception e) {
throw new RuntimeException("解析评估结果失败", e);
}
}

public record EvaluationResult(
int relevance,
int accuracy,
int completeness,
int clarity,
String comments
) {}
}

人工评估

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Test
@Tag("HumanReview")
public void testContentGenerationQuality() {
// 生成内容
String prompt = "为编程导航网站写一个首页介绍";
String generatedContent = aiService.generateContent(prompt);

// 保存到评估文件
String filename = "content_evaluation_" + System.currentTimeMillis() + ".txt";
try {
Path filePath = Paths.get("target/human-review/" + filename);
Files.createDirectories(filePath.getParent());
Files.write(filePath, (prompt + "\n\n" + generatedContent).getBytes());

// 记录待评估的内容
System.out.println("需要人工评估的内容已保存到: " + filePath);
// 在 CI 系统中,可以通过特定机制通知评审人员
} catch (IOException e) {
fail("无法保存评估内容", e);
}
}

SpringBoot应用的部署选项

Spring AI应用本质上是SpringBoot应用,可以采用多种部署方式:

  1. 传统服务器部署
1
2
3
4
5
# 构建可执行 JAR
mvn clean package

# 运行应用
java -jar target/spring-ai-app-1.0.0.jar
1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Spring AI Application
After=network.target

[Service]
User=appuser
WorkingDirectory=/opt/spring-ai-app
ExecStart=/usr/bin/java -jar spring-ai-app-1.0.0.jar
EnvironmentFile=/opt/spring-ai-app/config/env
Restart=always

[Install]
WantedBy=multi-user.target
  1. Docker容器化部署
    创建 Dockerfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM eclipse-temurin:17-jdk

WORKDIR /app

COPY target/spring-ai-app-1.0.0.jar app.jar

# 设置健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/actuator/health || exit 1

# 设置环境变量
ENV SPRING_PROFILES_ACTIVE=prod
ENV SERVER_PORT=8080

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
1
2
3
4
5
6
7
8
# 构建镜像
docker build -t spring-ai-app:1.0.0 .

# 运行容器
docker run -d -p 8080:8080 \
-e OPENAI_API_KEY=your-api-key \
--name spring-ai-app \
spring-ai-app:1.0.0
  1. Kubernetes部署
    创建Kubernetes部署配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-ai-app
spec:
replicas: 3
selector:
matchLabels:
app: spring-ai-app
template:
metadata:
labels:
app: spring-ai-app
spec:
containers:
- name: spring-ai-app
image: spring-ai-app:1.0.0
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: OPENAI_API_KEY
valueFrom:
secretKeyRef:
name: ai-secrets
key: openai-api-key
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 30
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10

创建服务

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Service
metadata:
name: spring-ai-app
spec:
selector:
app: spring-ai-app
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
  1. 云平台部署
    针对不同云平台的特定部署方式也可以应用于SpringAl应用,例如:
    AWSElasticBeanstalk
    Azure App Service
    Google Cloud Run
    Heroku
    阿里云EDAS
    腾讯云TKE

监控与日志管理

Spring Boot Actuator集成
添加 Actuator 依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

配置Actuator:

1
2
3
4
5
6
7
8
9
# 启用所有端点
management.endpoints.web.exposure.include=*

# 显示详细健康信息
management.endpoint.health.show-details=always

# 启用Prometheus指标
management.endpoint.metrics.enabled=true
management.endpoint.prometheus.enabled=true

自定义AI特定指标
创建AI服务监控指标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Component
public class AiServiceMetrics {

private final MeterRegistry registry;

public AiServiceMetrics(MeterRegistry registry) {
this.registry = registry;
}

// 记录请求计数
public void recordRequest(String modelName, String operationType) {
registry.counter("ai.request.count",
"model", modelName,
"operation", operationType
).increment();
}

// 记录响应时间
public Timer.Sample startTimer() {
return Timer.start(registry);
}

public void stopTimer(Timer.Sample sample, String modelName, String operationType) {
sample.stop(registry.timer("ai.response.time",
"model", modelName,
"operation", operationType
));
}

// 记录令牌使用量
public void recordTokenUsage(String modelName, int promptTokens, int completionTokens) {
registry.counter("ai.tokens.prompt", "model", modelName).increment(promptTokens);
registry.counter("ai.tokens.completion", "model", modelName).increment(completionTokens);
registry.counter("ai.tokens.total", "model", modelName).increment(promptTokens + completionTokens);
}

// 记录错误
public void recordError(String modelName, String errorType) {
registry.counter("ai.error.count",
"model", modelName,
"error", errorType
).increment();
}
}

使用AOP实现自动指标收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Aspect
@Component
public class AiServiceMonitoringAspect {

private final AiServiceMetrics metrics;

public AiServiceMonitoringAspect(AiServiceMetrics metrics) {
this.metrics = metrics;
}

@Around("execution(* org.springframework.ai.chat.ChatClient.call(..)) || " +
"execution(* org.springframework.ai.image.ImageClient.generateImage(..))")
public Object monitorAiCalls(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String modelName = determineModelName(joinPoint);

// 记录请求
metrics.recordRequest(modelName, methodName);

// 开始计时
Timer.Sample timer = metrics.startTimer();

try {
// 执行方法
Object result = joinPoint.proceed();

// 记录令牌使用量(如果有此信息)
if (result instanceof ChatResponse response) {
Usage usage = response.getUsage();
if (usage != null) {
metrics.recordTokenUsage(
modelName,
usage.getPromptTokens(),
usage.getCompletionTokens()
);
}
}

return result;
} catch (Exception e) {
// 记录错误
metrics.recordError(modelName, e.getClass().getSimpleName());
throw e;
} finally {
// 停止计时
metrics.stopTimer(timer, modelName, methodName);
}
}

private String determineModelName(ProceedingJoinPoint joinPoint) {
// 根据调用参数或目标对象确定模型名称
// ...
return "default";
}
}

结构化日志记录
使用结构化日志记录AI交互:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Slf4j
@Service
public class LoggingAiClient implements ChatClient {

private final ChatClient delegate;

public LoggingAiClient(ChatClient delegate) {
this.delegate = delegate;
}

@Override
public String call(String prompt) {
String requestId = UUID.randomUUID().toString();

// 记录请求
log.info("AI请求开始 [requestId={}] prompt='{}'", requestId, truncate(prompt, 100));

long startTime = System.currentTimeMillis();
String response = null;
try {
// 调用实际服务
response = delegate.call(prompt);

// 记录成功响应
log.info("AI请求成功 [requestId={}, duration={}ms] response='{}'",
requestId,
System.currentTimeMillis() - startTime,
truncate(response, 100));

return response;
} catch (Exception e) {
// 记录错误
log.error("AI请求失败 [requestId={}, duration={}ms] error='{}'",
requestId,
System.currentTimeMillis() - startTime,
e.getMessage(), e);

throw e;
}
}

private String truncate(String text, int maxLength) {
if (text != null && text.length() > maxLength) {
return text.substring(0, maxLength) + "...";
}
return text;
}

// 实现其他必要方法...
}

AI应用性能优化

AI应用性能的关键指标

响应时间是最直观的指标,包括首字节时间(TTFB)和完整响应时间。
吞吐量反映了系统的处理能力,资源利用率则关系到成本控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Component
public class PerformanceMonitor {

private final MeterRegistry meterRegistry;

public PerformanceMonitor(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}

// 记录 AI 请求响应时间
public void recordAiRequestTime(String modelType, long duration) {
Timer.Sample sample = Timer.start(meterRegistry);
sample.stop(Timer.builder("ai.request.duration")
.tag("model", modelType)
.register(meterRegistry));
}

// 记录 Token 使用量
public void recordTokenUsage(String operation, int tokenCount) {
Counter.builder("ai.tokens.used")
.tag("operation", operation)
.register(meterRegistry)
.increment(tokenCount);
}

// 记录缓存命中率
public void recordCacheHit(String cacheType, boolean hit) {
Counter.builder("ai.cache.requests")
.tag("type", cacheType)
.tag("result", hit ? "hit" : "miss")
.register(meterRegistry)
.increment();
}
}

缓存策略与实现

缓存可以很大程度的提升AI应用性能。
由于相同或相似的问题可能被重复询问,因此,设计合理的缓存策略能有效减少AI模型调用次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Service
public class AiCacheService {

private final RedisTemplate<String, String> redisTemplate;
private final ChatClient chatClient;

public AiCacheService(RedisTemplate<String, String> redisTemplate,
ChatClient chatClient) {
this.redisTemplate = redisTemplate;
this.chatClient = chatClient;
}

// 基于问题哈希的缓存策略
public String getCachedResponse(String question) {
String cacheKey = "ai:response:" + DigestUtils.md5DigestAsHex(question.getBytes());

// 尝试从缓存获取
String cachedResponse = redisTemplate.opsForValue().get(cacheKey);
if (cachedResponse != null) {
return cachedResponse;
}

// 缓存未命中,调用 AI 模型
String response = chatClient.prompt().user(question).call().content();

// 存储到缓存,设置过期时间
redisTemplate.opsForValue().set(cacheKey, response, Duration.ofHours(24));

return response;
}
}

批处理与并行请求

批处理能够提高系统吞吐量,而并行处理则能缩短总体响应时间。
Spring AI支持多种并行处理模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Service
public class BatchProcessingService {

private final ChatClient chatClient;
private final ExecutorService executorService;

public BatchProcessingService(ChatClient chatClient) {
this.chatClient = chatClient;
this.executorService = Executors.newFixedThreadPool(10);
}

// 批量处理多个问题
public List<String> processBatch(List<String> questions) {
List<CompletableFuture<String>> futures = questions.stream()
.map(question -> CompletableFuture.supplyAsync(() ->
chatClient.prompt().user(question).call().content(), executorService))
.collect(Collectors.toList());

return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList());
}

// 分块处理大量数据
public void processLargeDataset(List<String> data, int batchSize) {
for (int i = 0; i < data.size(); i += batchSize) {
List<String> batch = data.subList(i,
Math.min(i + batchSize, data.size()));

processBatch(batch);

// 添加延迟以避免 API 限流
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}

响应流处理技术

流式响应能够提供更好的用户体验,让用户能够实时看到AI的回答过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@RestController
public class StreamingController {

private final ChatClient chatClient;

public StreamingController(ChatClient chatClient) {
this.chatClient = chatClient;
}

@GetMapping(value = "/stream-chat", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> streamChat(@RequestParam String question) {
return chatClient.prompt().user(question).stream().content()
.map(response -> "data: " + response + "\n\n")
.onErrorReturn("data: [ERROR] 处理请求时发生错误\n\n");
}

// 服务器发送事件(SSE)实现
@GetMapping("/sse-chat")
public SseEmitter sseChat(@RequestParam String question) {
SseEmitter emitter = new SseEmitter(30000L);

CompletableFuture.runAsync(() -> {
try {
chatClient.prompt().user(question).stream().content()
.subscribe(
chunk -> {
try {
emitter.send(SseEmitter.event()
.name("message")
.data(chunk));
} catch (IOException e) {
emitter.completeWithError(e);
}
},
emitter::completeWithError,
emitter::complete
);
} catch (Exception e) {
emitter.completeWithError(e);
}
});

return emitter;
}
}

资源使用优化

合理的资源管理能够提高系统稳定性并降低成本。
连接池管理、内存优化和线程池配置都是重要的优化点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
@Configuration
public class AiOptimizationConfig {

// 配置 HTTP 客户端连接池
@Bean
public RestTemplate restTemplate() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();

CloseableHttpClient httpClient = HttpClients.custom()
.setMaxConnTotal(100)
.setMaxConnPerRoute(20)
.setConnectionTimeToLive(30, TimeUnit.SECONDS)
.build();

factory.setHttpClient(httpClient);
factory.setConnectTimeout(5000);
factory.setReadTimeout(30000);

return new RestTemplate(factory);
}

// 配置任务执行器
@Bean
public TaskExecutor aiTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("ai-task-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

@Service
public class ResourceOptimizedAiService {

// 使用对象池减少对象创建开销
private final ObjectPool<StringBuilder> stringBuilderPool;

public ResourceOptimizedAiService() {
this.stringBuilderPool = new GenericObjectPool<>(
new BasePooledObjectFactory<StringBuilder>() {
@Override
public StringBuilder create() {
return new StringBuilder();
}

@Override
public PooledObject<StringBuilder> wrap(StringBuilder obj) {
return new DefaultPooledObject<>(obj);
}

@Override
public void passivateObject(PooledObject<StringBuilder> p) {
p.getObject().setLength(0); // 清空内容
}
}
);
}

public String processWithOptimization(String input) {
StringBuilder sb = null;
try {
sb = stringBuilderPool.borrowObject();

// 使用 StringBuilder 进行字符串操作
sb.append("处理编程导航用户请求: ").append(input);

return sb.toString();
} catch (Exception e) {
throw new RuntimeException("资源获取失败", e);
} finally {
if (sb != null) {
try {
stringBuilderPool.returnObject(sb);
} catch (Exception e) {
// 记录日志但不抛出异常
}
}
}
}
}

第六次实践

优化高流量AI应用性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@RestController
@RequestMapping("/api/optimized")
public class OptimizedAiController {

private final AiCacheService cacheService;
private final CostControlService costControlService;
private final PerformanceMonitor performanceMonitor;
private final ChatClient chatClient;

@PostMapping("/chat")
public ResponseEntity<?> optimizedChat(@RequestBody ChatRequest request) {
long startTime = System.currentTimeMillis();

try {
// 1. 成本控制检查
if (!costControlService.checkTokenLimit(request.getUserId(),
estimateTokens(request.getQuestion()))) {
return ResponseEntity.status(429)
.body("今日 Token 使用量已达上限,请明天再试或升级为面试鸭 VIP");
}

// 2. 尝试从缓存获取
String cachedResponse = cacheService.getCachedResponse(request.getQuestion());
if (cachedResponse != null) {
performanceMonitor.recordCacheHit("semantic", true);
return ResponseEntity.ok(new ChatResponse(cachedResponse, true));
}

// 3. 选择最优模型
String selectedModel = costControlService.selectOptimalModel(
request.getQuestion(), request.getUserId());

// 4. 调用 AI 服务
String response = chatClient.prompt().user(request.getQuestion()).call().content();

// 5. 记录性能指标
long duration = System.currentTimeMillis() - startTime;
performanceMonitor.recordAiRequestTime(selectedModel, duration);
performanceMonitor.recordCacheHit("semantic", false);

return ResponseEntity.ok(new ChatResponse(response, false));

} catch (Exception e) {
return ResponseEntity.status(500)
.body("处理请求时发生错误: " + e.getMessage());
}
}

private int estimateTokens(String text) {
// 简单的 Token 估算:英文约 4 字符/Token,中文约 1.5 字符/Token
return (int) Math.ceil(text.length() / 3.0);
}
}

// 请求和响应模型
class ChatRequest {
private String question;
private String userId;

// getters and setters
public String getQuestion() { return question; }
public void setQuestion(String question) { this.question = question; }
public String getUserId() { return userId; }
public void setUserId(String userId) { this.userId = userId; }
}

class ChatResponse {
private String answer;
private boolean fromCache;

public ChatResponse(String answer, boolean fromCache) {
this.answer = answer;
this.fromCache = fromCache;
}

// getters and setters
public String getAnswer() { return answer; }
public boolean isFromCache() { return fromCache; }
}