概述
Embedding服务负责将文本转换为向量表示,是RAG(检索增强生成)的基础。LangChat Pro支持多种Embedding模型,可以将文本、文档等转换为高维向量用于相似度检索。核心组件
LcEmbeddingService
接口路径:langchat-core/src/main/java/cn/langchat/core/service/LcEmbeddingService.java
实现路径: langchat-core/src/main/java/cn/langchat/core/service/impl/LcEmbeddingServiceImpl.java
职责: 提供文本向量化服务
核心方法:
Copy
public interface LcEmbeddingService {
/**
* 文本向量化
*
* @param modelId Embedding模型ID
* @param text 要向量化的文本
* @return 向量表示
*/
List<Float> embed(String modelId, String text);
/**
* 批量向量化
*
* @param modelId Embedding模型ID
* @param texts 文本列表
* @return 向量列表
*/
List<List<Float>> embedBatch(String modelId, List<String> texts);
/**
* 获取Embedding模型
*
* @param modelId Embedding模型ID
* @return EmbeddingModel实例
*/
EmbeddingModel getEmbeddingModel(String modelId);
/**
* 计算文本Token数
*
* @param modelId Embedding模型ID
* @param text 文本
* @return Token数量
*/
int countTokens(String modelId, String text);
}
EmbeddingModelFactory
职责
动态创建和管理Embedding模型实例实现
Copy
@Component
public class EmbeddingModelFactory {
@Resource
private AigcModelService aigcModelService;
@Resource
private CacheUtils cacheUtils;
/**
* 本地实例缓存
*/
private Cache<String, EmbeddingModel> embeddingModelCache;
/**
* 获取Embedding模型
*/
public EmbeddingModel getEmbeddingModel(String modelId) {
AigcModel model = aigcModelService.getById(modelId);
if (model == null) {
throw new IllegalArgumentException("模型不存在: " + modelId);
}
String cacheKey = "embedding:" + modelId;
return getCache().get(cacheKey, k -> createEmbeddingModel(model));
}
/**
* 创建Embedding模型
*/
private EmbeddingModel createEmbeddingModel(AigcModel model) {
log.info("创建Embedding模型: modelId={}, provider={}",
model.getId(), model.getProvider());
return switch (model.getProvider().toLowerCase()) {
case ProviderConst.openai -> createOpenAiEmbeddingModel(model);
case ProviderConst.ollama -> createOllamaEmbeddingModel(model);
case ProviderConst.dashscope -> createDashscopeEmbeddingModel(model);
default -> throw new IllegalArgumentException("不支持的Embedding供应商: " + model.getProvider());
};
}
/**
* 创建OpenAI Embedding模型
*/
private EmbeddingModel createOpenAiEmbeddingModel(AigcModel model) {
return OpenAiEmbeddingModel.builder()
.apiKey(model.getApiKey())
.baseUrl(model.getBaseUrl())
.modelName(model.getModel())
.timeout(Duration.ofMinutes(model.getTimeout()))
.build();
}
/**
* 创建Ollama Embedding模型
*/
private EmbeddingModel createOllamaEmbeddingModel(AigcModel model) {
return OllamaEmbeddingModel.builder()
.baseUrl(model.getBaseUrl())
.modelName(model.getModel())
.timeout(Duration.ofMinutes(model.getTimeout()))
.build();
}
/**
* 创建Qwen Embedding模型
*/
private EmbeddingModel createDashscopeEmbeddingModel(AigcModel model) {
return QwenEmbeddingModel.builder()
.apiKey(model.getApiKey())
.baseUrl(model.getBaseUrl())
.modelName(model.getModel())
.build();
}
}
服务实现
LcEmbeddingServiceImpl
Copy
@Service
public class LcEmbeddingServiceImpl implements LcEmbeddingService {
@Resource
private EmbeddingModelFactory embeddingModelFactory;
@Override
public List<Float> embed(String modelId, String text) {
log.debug("文本向量化: modelId={}, textLength={}", modelId, text.length());
EmbeddingModel model = embeddingModelFactory.getEmbeddingModel(modelId);
Response response = model.embed(text);
return response.content().vector();
}
@Override
public List<List<Float>> embedBatch(String modelId, List<String> texts) {
log.debug("批量向量化: modelId={}, count={}", modelId, texts.size());
EmbeddingModel model = embeddingModelFactory.getEmbeddingModel(modelId);
Response<List<Embedding>> response = model.embedAll(texts);
return response.content().stream()
.map(Embedding::vector)
.collect(Collectors.toList());
}
@Override
public EmbeddingModel getEmbeddingModel(String modelId) {
return embeddingModelFactory.getEmbeddingModel(modelId);
}
@Override
public int countTokens(String modelId, String text) {
// 简单估算:中文字符+英文单词数
int chineseCount = (int) text.chars()
.filter(c -> Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN)
.count();
int englishCount = text.split("\\s+").length;
return chineseCount + englishCount;
}
}
支持的Embedding模型
1. OpenAI Embedding
| 模型 | 向量维度 | 说明 |
|---|---|---|
| text-embedding-ada-002 | 1536 | 通用Embedding模型 |
| text-embedding-3-small | 1536 | 更准确,速度更快 |
| text-embedding-3-large | 3072 | 最准确,向量大 |
2. Ollama Embedding
| 模型 | 向量维度 | 说明 |
|---|---|---|
| nomic-embed-text | 768 | 轻量级模型 |
| llama2 | 4096 | 通用模型 |
| mxbai-embed-large | 1024 | 高质量模型 |
3. Qwen Embedding
| 模型 | 向量维度 | 说明 |
|---|---|---|
| text-embedding-v1 | 1536 | 通用中文Embedding |
| text-embedding-v2 | 1024 | 优化版本 |
向量化流程
1. 单文本向量化
Copy
文本输入
↓
获取Embedding模型
↓
调用模型API
↓
返回向量
↓
存储到向量数据库
2. 批量向量化
Copy
文本列表
↓
批量处理(分批)
↓
每批调用模型API
↓
收集所有向量
↓
批量存储到向量数据库
知识库向量化
文档分块策略
1. 固定大小分块
Copy
public List<String> splitBySize(String text, int chunkSize) {
List<String> chunks = new ArrayList<>();
for (int i = 0; i < text.length(); i += chunkSize) {
int end = Math.min(i + chunkSize, text.length());
chunks.add(text.substring(i, end));
}
return chunks;
}
2. 按段落分块
Copy
public List<String> splitByParagraph(String text) {
return Arrays.stream(text.split("\\n\\s*\\n"))
.filter(StrUtil::isNotBlank)
.collect(Collectors.toList());
}
3. 语义分块(推荐)
Copy
public List<String> splitBySemantic(String text, int windowSize, int overlap) {
List<String> chunks = new ArrayList<>();
// 先按段落分割
String[] paragraphs = text.split("\\n\\s*\\n");
// 合并段落形成滑动窗口
StringBuilder current = new StringBuilder();
for (int i = 0; i < paragraphs.length; i++) {
current.append(paragraphs[i]).append("\n\n");
// 每隔windowSize个段落创建一个chunk
if ((i + 1) % windowSize == 0 || i == paragraphs.length - 1) {
chunks.add(current.toString().trim());
current = new StringBuilder();
// 重叠部分段落
for (int j = Math.max(0, i - overlap + 1); j <= i && j < paragraphs.length; j++) {
current.append(paragraphs[j]).append("\n\n");
}
}
}
return chunks;
}
向量化流程
Copy
文档
↓
文档解析
↓
文本提取
↓
文本分块
↓
批量向量化
↓
存储到向量库
Token计算
计算策略
Copy
public int countTokens(String modelId, String text) {
// 方法1:简单估算
int chineseCount = (int) text.chars()
.filter(c -> Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN)
.count();
int englishCount = text.split("\\s+").length;
return chineseCount + englishCount;
// 方法2:使用模型API(更准确)
// return embeddingModel.countTokens(text);
// 方法3:使用Tokenizer(最准确)
// return tokenizer.encode(text).size();
}
配置说明
AigcModel 配置(Embedding模型)
Copy
public class AigcModel {
private String id;
private String provider; // 供应商
private String model; // 模型名称
private String baseUrl; // API地址
private String apiKey; // API密钥
private String type; // 类型: embedding
private Integer timeout; // 超时时间(分钟)
}
配置示例
Copy
{
"id": "embedding-001",
"provider": "openai",
"model": "text-embedding-3-small",
"baseUrl": "https://api.openai.com/v1",
"apiKey": "sk-...",
"type": "embedding",
"timeout": 5
}
性能优化
1. 批量处理
Copy
@Override
public List<List<Float>> embedBatch(String modelId, List<String> texts) {
// 分批处理,避免单次请求过多
int batchSize = 100;
List<List<Float>> results = new ArrayList<>();
for (int i = 0; i < texts.size(); i += batchSize) {
int end = Math.min(i + batchSize, texts.size());
List<String> batch = texts.subList(i, end);
results.addAll(embedBatchInternal(modelId, batch));
}
return results;
}
2. 模型缓存
Copy
private Cache<String, EmbeddingModel> embeddingModelCache;
public EmbeddingModel getEmbeddingModel(String modelId) {
String cacheKey = "embedding:" + modelId;
return embeddingModelCache.get(cacheKey, k -> createEmbeddingModel(modelId));
}
3. 结果缓存
Copy
@Cacheable(value = "embedding", key = "#modelId + ':' + #text.hashCode()")
public List<Float> embed(String modelId, String text) {
// 向量化逻辑
}
4. 并行处理
Copy
public List<List<Float>> embedBatchParallel(String modelId, List<String> texts) {
return texts.parallelStream()
.map(text -> embed(modelId, text))
.collect(Collectors.toList());
}
最佳实践
1. 模型选择
| 场景 | 推荐模型 | 原因 |
|---|---|---|
| 通用场景 | text-embedding-3-small | 性价比高 |
| 高精度要求 | text-embedding-3-large | 准确性最高 |
| 中文场景 | text-embedding-v1 | 中文优化 |
| 本地部署 | nomic-embed-text | 轻量级 |
2. 分块策略
- 文档长度: 根据文档选择分块策略
- 语义完整: 保持语义完整性
- 重叠处理: 适当重叠避免信息丢失
- 典型大小: 500-1000字符
3. 性能优化
- 批量处理提高吞吐量
- 合理设置超时时间
- 使用缓存减少重复计算
- 并行处理提高速度
4. 成本控制
- 估算Token数量
- 批量处理减少请求次数
- 缓存向量化结果
- 选择性价比高的模型
扩展示例
自定义分块策略
Copy
@Component
public class CustomSplitter {
public List<String> split(String text, SplitConfig config) {
// 自定义分块逻辑
List<String> chunks = new ArrayList<>();
// 按配置分块
switch (config.getStrategy()) {
case "fixed_size" -> chunks.addAll(splitBySize(text, config.getChunkSize()));
case "paragraph" -> chunks.addAll(splitByParagraph(text));
case "semantic" -> chunks.addAll(splitBySemantic(text, config));
default -> throw new IllegalArgumentException("不支持的策略");
}
return chunks;
}
}
自定义Token计算
Copy
@Component
public class CustomTokenizer {
public int countTokens(String text, String modelName) {
// 使用模型特定的Tokenizer
try {
Tokenizer tokenizer = Tokenizer.new(modelName);
return tokenizer.encode(text).size();
} catch (Exception e) {
// 降级到估算
return estimateTokens(text);
}
}
private int estimateTokens(String text) {
// 中文字符+英文单词数
int chineseCount = (int) text.chars()
.filter(c -> Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN)
.count();
int englishCount = text.split("\\s+").length;
return chineseCount + englishCount;
}
}

