看客雅正 發表於 2025-12-18 17:48:00

大语言模型~Ollama本地模型和java一起体验LLM

<h1 id="语言模型">语言模型</h1>
<p>语言模型(language model,LM)通过计算单词序列的概率进行语言建模,其主要作用是基于给定的上下文,预测序列中下一个词的概率分布。随着计算能力的提升和数据量的增长,LM的发展经历了从统计语言模型(statistical language model , SLM)到神经语言模型(neural language model , NLM)的演进。</p>
<p>2018年,BERT(bidirectional encoder representations from transformers)模型的提出,标志着预训练语言模型(pre-trained language model , PLM)时代的开启。PLM是一种基于大量无标注文本数据进行深度学习的模型,旨在捕捉自然语言的语法、语义以及常识。</p>
<p>此后,一系列PLM如GPT(generative pre-trained transformer)系列、RoBERTa(Robustly optimized BERT pre-training approach)、XLNet等相继出现。</p>
<p>GPT系列模型通过自监督学习在广泛的文本数据上进行预训练,进而灵活地应用于写作助手、代码生成和自动化客户服务等多种下游任务。BERT和RoBERTa模型侧重于理解语境中的语言,如文本分类、命名实体识别和问答系统等。这些模型利用Transformer架构,并依赖自注意力(self-attention)机制捕捉输入数据中的复杂依赖关系,从而显著提高自然语言处理任务的准确性。当PLM的有效参数规模达到数百亿级别时,便称之为LLM。</p>
<h1 id="大规模语言">大规模语言</h1>
<p>LLM也称大规模语言模型,是由包含数百亿以上参数的深度神经网络构建的语言模型,通过自监督学习方法利用大量未标注文本进行训练。其核心思想是通过大规模的无监督训练学习自然语言的模式和结构,在一定程度上模拟人类的语言认知和生成过程。</p>
<p>相比传统的NLP模型,LLM能够更好地理解和生成自然文本,同时表现出一定的逻辑思维和推理能力。</p>
<p>LLM在多种应用场景下表现出色,不仅能执行拼写检查和语法修正等简单的语言任务,还能处理文本摘要、机器翻译、情感分析、对话生成和内容推荐等复杂任务。</p>
<p>在医学领域,LLM能够处理和分析海量的医学文献、病历数据等医学信息,为医学人工智能的应用提供了更加智能和高效的解决方案。</p>
<h1 id="测试说明">测试说明</h1>
<ul>
<li>用户输入多个“信息”</li>
<li>大语言模型将“信息”进行处理,转成数组;(一维张量,向量)</li>
<li>通过余弦相似度等相关算法,计算两个向量是否相似</li>
</ul>
<h1 id="ollama接口步骤">Ollama接口步骤</h1>
<ol>
<li>安装 Ollama: https://ollama.ai/</li>
<li>下载模型: ollama pull nomic-embed-text</li>
<li>Ollama 默认运行在 http://localhost:11434</li>
</ol>
<h1 id="推荐的嵌入模型">推荐的嵌入模型:</h1>
<ul>
<li>nomic-embed-text: 768维,效果好,速度快</li>
<li>mxbai-embed-large: 1024维,效果更好</li>
<li>bge-m3: 多语言支持</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/118538/202512/118538-20251218174455999-570302237.png" alt="图片" loading="lazy"></p>
<h1 id="springboot中调用本地模型">springboot中调用本地模型</h1>
<pre><code class="language-java">    @Test
        @Disabled("需要本地运行 Ollama 服务")
        public void testOllamaEmbedding() {
                // Ollama API 地址
                String apiUrl = "http://localhost:11434/api/embeddings";
                String apiKey = ""; // Ollama 本地不需要 key
                String model = "nomic-embed-text"; // 或 mxbai-embed-large

                EmbeddingClient client = new EmbeddingClientImpl(apiUrl, apiKey);

                // 水果库
                List&lt;Fruit&gt; fruits = Arrays.asList(new Fruit("红富士苹果", "红色 甜 脆 苹果 新鲜"), new Fruit("青苹果", "绿色 酸 脆 苹果 清爽"),
                                new Fruit("金帅苹果", "黄色 甜 软 苹果"), new Fruit("香蕉", "黄色 甜 软 香蕉 热带水果"), new Fruit("草莓", "红色 甜 小 草莓 多汁 浆果"),
                                new Fruit("西瓜", "绿色外皮 红色果肉 甜 大 西瓜 多汁 夏天"), new Fruit("葡萄", "紫色 甜 小 葡萄 多汁 成串"));

                // 为每个水果生成嵌入向量
                for (Fruit fruit : fruits) {
                        fruit.embedding = client.getEmbeddingVector(model, fruit.description);
                }

                // 用户搜索
                String query = "红色的甜水果";
                double[] queryVector = client.getEmbeddingVector(model, query);

                System.out.println("搜索: \"" + query + "\"");
                System.out.println("向量维度: " + queryVector.length);
                System.out.println();

                // 按相似度排序
                fruits.sort(Comparator.comparingDouble(f -&gt; -cosineSimilarity(queryVector, f.embedding)));

                // 输出结果
                System.out.println("搜索结果(按相似度排序):");
                for (Fruit f : fruits) {
                        double sim = cosineSimilarity(queryVector, f.embedding);
                        System.out.printf("%s (%.4f): %s%n", f.name, sim, f.description);
                }
        }

    /**
       * 计算两个向量的余弦相似度
       */
        public static double cosineSimilarity(double[] vectorA, double[] vectorB) {
                if (vectorA.length != vectorB.length) {
                        throw new IllegalArgumentException("向量维度必须相同");
                }

                double dotProduct = 0;
                double normA = 0;
                double normB = 0;

                for (int i = 0; i &lt; vectorA.length; i++) {
                        dotProduct += vectorA * vectorB;
                        normA += vectorA * vectorA;
                        normB += vectorB * vectorB;
                }

                if (normA == 0 || normB == 0) {
                        return 0;
                }

                return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
        }
</code></pre>
<h1 id="核心方法">核心方法</h1>
<pre><code class="language-java">@Slf4j
public class EmbeddingClientImpl implements EmbeddingClient {

        private final RestTemplate restTemplate;

        private final String address;

        private final String key;

        public EmbeddingClientImpl(String address, String key) {
                PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
                connectionManager.setMaxTotal(100);
                connectionManager.setDefaultMaxPerRoute(20);

                // 设置请求配置
                RequestConfig requestConfig = RequestConfig.custom()
                                .setConnectionRequestTimeout(Timeout.ofSeconds(30))
                                .setResponseTimeout(Timeout.ofSeconds(300)) // 5分钟响应超时
                                .build();

                // 使用 HttpClientBuilder 来构建 HttpClient
                HttpClient httpClient = HttpClientBuilder.create()
                                .setConnectionManager(connectionManager)
                                .setDefaultRequestConfig(requestConfig)
                                .build();

                // 创建 HttpComponentsClientHttpRequestFactory
                HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
                requestFactory.setConnectTimeout(30000); // 30秒连接超时
                requestFactory.setConnectionRequestTimeout(30000);

                // 创建 RestTemplate,只使用 StringHttpMessageConverter 避免 Jackson 依赖问题
                this.restTemplate = new RestTemplate(requestFactory);
                // 清除默认的消息转换器,只保留字符串转换器
                this.restTemplate.setMessageConverters(
                                Collections.singletonList(new StringHttpMessageConverter(StandardCharsets.UTF_8)));

                this.address = address;
                this.key = key;
        }

        @Override
        public String embedding(String model, String input) {
                long start = System.currentTimeMillis();
                String url = address;

                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                headers.setAcceptCharset(Collections.singletonList(StandardCharsets.UTF_8));
                if (key != null &amp;&amp; !key.isEmpty()) {
                        headers.add("Authorization", "Bearer " + key);
                }

                // 将 request 转化为 body 字符串
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("model", model);

                // Ollama API 使用 "prompt" 作为输入参数
                // OpenAI API 使用 "input" 作为输入参数
                if (isOllamaApi(url)) {
                        jsonObject.put("prompt", input);
                }
                else {
                        jsonObject.put("input", input);
                }

                String body = jsonObject.toString();
                log.info("Embedding Request URL: {}, Body: {}", url, body);

                // 请求
                HttpEntity&lt;String&gt; req = new HttpEntity&lt;&gt;(body, headers);

                ResponseEntity&lt;String&gt; result = restTemplate.postForEntity(url, req, String.class);

                if (!result.getStatusCode().equals(HttpStatus.OK)) {
                        throw new RuntimeException("embeddings error, request: " + body + ", response: " + result.getBody());
                }
                log.info("Embedding Response: {}", result.getBody());
                log.info("embedding cost {} ms", System.currentTimeMillis() - start);
                return result.getBody();
        }

        /**
       * 判断是否为 Ollama API
       */
        private boolean isOllamaApi(String url) {
                return url != null &amp;&amp; (url.contains("localhost:11434") || url.contains("127.0.0.1:11434")
                                || url.contains("/api/embeddings") || url.contains("ollama"));
        }

        /**
       * 获取文本嵌入向量
       * &lt;p&gt;
       * 解析 OpenAI 格式的响应,提取 embedding 向量
       *
       * 响应格式示例: &lt;pre&gt;
       * {
       *   "object": "list",
       *   "data": [{
       *   "object": "embedding",
       *   "index": 0,
       *   "embedding":
       *   }],
       *   "model": "text-embedding-ada-002",
       *   "usage": {"prompt_tokens": 8, "total_tokens": 8}
       * }
       * &lt;/pre&gt;
       * @param model 模型名称
       * @param input 输入文本
       * @return 嵌入向量
       */
        @Override
        public double[] getEmbeddingVector(String model, String input) {
                String response = embedding(model, input);
                return parseEmbeddingVector(response);
        }

        /**
       * 解析嵌入向量响应
       * @param response JSON响应字符串
       * @return 向量数组
       */
        private double[] parseEmbeddingVector(String response) {
                try {
                        log.debug("Parsing embedding response: {}", response);
                        JSONObject jsonResponse = JSONObject.parseObject(response);

                        if (jsonResponse == null) {
                                throw new RuntimeException("响应为空或不是有效的 JSON");
                        }

                        // Ollama 格式 (直接返回 embedding 数组)
                        // 响应格式: {"embedding": }
                        if (jsonResponse.containsKey("embedding")) {
                                JSONArray embeddingArray = jsonResponse.getJSONArray("embedding");
                                if (embeddingArray != null &amp;&amp; !embeddingArray.isEmpty()) {
                                        log.info("Ollama 格式解析成功,向量维度: {}", embeddingArray.size());
                                        return jsonArrayToDoubleArray(embeddingArray);
                                }
                                else {
                                        log.warn("Ollama embedding 数组为空,检查模型是否正确加载");
                                }
                        }

                        // OpenAI 格式
                        // 响应格式: {"data": [{"embedding": }]}
                        if (jsonResponse.containsKey("data")) {
                                JSONArray dataArray = jsonResponse.getJSONArray("data");
                                if (dataArray != null &amp;&amp; !dataArray.isEmpty()) {
                                        JSONObject firstData = dataArray.getJSONObject(0);
                                        JSONArray embeddingArray = firstData.getJSONArray("embedding");
                                        if (embeddingArray != null &amp;&amp; !embeddingArray.isEmpty()) {
                                                log.info("OpenAI 格式解析成功,向量维度: {}", embeddingArray.size());
                                                return jsonArrayToDoubleArray(embeddingArray);
                                        }
                                }
                        }

                        // 阿里通义格式
                        // 响应格式: {"output": {"embeddings": [{"embedding": }]}}
                        if (jsonResponse.containsKey("output")) {
                                JSONObject output = jsonResponse.getJSONObject("output");
                                if (output != null &amp;&amp; output.containsKey("embeddings")) {
                                        JSONArray embeddings = output.getJSONArray("embeddings");
                                        if (embeddings != null &amp;&amp; !embeddings.isEmpty()) {
                                                JSONObject firstEmbedding = embeddings.getJSONObject(0);
                                                JSONArray embeddingArray = firstEmbedding.getJSONArray("embedding");
                                                if (embeddingArray != null &amp;&amp; !embeddingArray.isEmpty()) {
                                                        log.info("阿里通义格式解析成功,向量维度: {}", embeddingArray.size());
                                                        return jsonArrayToDoubleArray(embeddingArray);
                                                }
                                        }
                                }
                        }

                        // 如果有错误信息,打印出来
                        if (jsonResponse.containsKey("error")) {
                                String error = jsonResponse.getString("error");
                                throw new RuntimeException("API 返回错误: " + error);
                        }

                        throw new RuntimeException("无法解析嵌入向量响应,未知格式。响应内容: " + response);
                }
                catch (Exception e) {
                        log.error("解析嵌入向量失败: {}", response, e);
                        throw new RuntimeException("解析嵌入向量失败: " + e.getMessage(), e);
                }
        }

        /**
       * 将 JSONArray 转换为 double 数组
       */
        private double[] jsonArrayToDoubleArray(JSONArray jsonArray) {
                if (jsonArray == null || jsonArray.isEmpty()) {
                        log.warn("JSONArray 为空,返回空数组");
                        return new double;
                }
                double[] result = new double;
                for (int i = 0; i &lt; jsonArray.size(); i++) {
                        result = jsonArray.getDoubleValue(i);
                }
                return result;
        }

}

</code></pre>
<p>运行结果:</p>
<p><img src="https://img2024.cnblogs.com/blog/118538/202512/118538-20251219085913726-267294562.png" alt="图片" loading="lazy"></p>


</div>
<div id="MySignature" role="contentinfo">
    <p></p>
<div class="navgood">
<p>作者:仓储大叔,张占岭,<br>
荣誉:微软MVP<br>QQ:853066980</p>

<p><strong>支付宝扫一扫,为大叔打赏!</strong>
<br><img src="https://images.cnblogs.com/cnblogs_com/lori/237884/o_IMG_7144.JPG"></p>
</div><br><br>
来源:https://www.cnblogs.com/lori/p/19368280
頁: [1]
查看完整版本: 大语言模型~Ollama本地模型和java一起体验LLM