Skip to main content

概述

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 职责: 提供文本向量化服务 核心方法:
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模型实例

实现

@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

@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-0021536通用Embedding模型
text-embedding-3-small1536更准确,速度更快
text-embedding-3-large3072最准确,向量大

2. Ollama Embedding

模型向量维度说明
nomic-embed-text768轻量级模型
llama24096通用模型
mxbai-embed-large1024高质量模型

3. Qwen Embedding

模型向量维度说明
text-embedding-v11536通用中文Embedding
text-embedding-v21024优化版本

向量化流程

1. 单文本向量化

文本输入

获取Embedding模型

调用模型API

返回向量

存储到向量数据库

2. 批量向量化

文本列表

批量处理(分批)

每批调用模型API

收集所有向量

批量存储到向量数据库

知识库向量化

文档分块策略

1. 固定大小分块

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. 按段落分块

public List<String> splitByParagraph(String text) {
    return Arrays.stream(text.split("\\n\\s*\\n"))
        .filter(StrUtil::isNotBlank)
        .collect(Collectors.toList());
}

3. 语义分块(推荐)

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;
}

向量化流程

文档

文档解析

文本提取

文本分块

批量向量化

存储到向量库

Token计算

计算策略

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模型)

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;        // 超时时间(分钟)
}

配置示例

{
  "id": "embedding-001",
  "provider": "openai",
  "model": "text-embedding-3-small",
  "baseUrl": "https://api.openai.com/v1",
  "apiKey": "sk-...",
  "type": "embedding",
  "timeout": 5
}

性能优化

1. 批量处理

@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. 模型缓存

private Cache<String, EmbeddingModel> embeddingModelCache;

public EmbeddingModel getEmbeddingModel(String modelId) {
    String cacheKey = "embedding:" + modelId;
    return embeddingModelCache.get(cacheKey, k -> createEmbeddingModel(modelId));
}

3. 结果缓存

@Cacheable(value = "embedding", key = "#modelId + ':' + #text.hashCode()")
public List<Float> embed(String modelId, String text) {
    // 向量化逻辑
}

4. 并行处理

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数量
  • 批量处理减少请求次数
  • 缓存向量化结果
  • 选择性价比高的模型

扩展示例

自定义分块策略

@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计算

@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;
    }
}

参考文档