告别LangChain!用纯Java代码玩转Ollama和Qdrant,构建轻量级DeepSeek智能助手

如果你是一位Java开发者,厌倦了Python生态中那些臃肿的AI框架,渴望用更简洁、更可控的方式构建智能应用,那么这篇文章正是为你准备的。我们将带你用纯Java代码,仅依赖SpringBoot、OkHttp和Jackson这三个轻量级库,实现从文本嵌入到向量检索再到大模型问答的全流程开发。

1. 为什么选择轻量化Java方案?

在AI应用开发领域,LangChain等框架确实提供了便利,但它们也带来了不少问题:

  • 过度抽象:框架隐藏了太多底层细节,当需要定制时往往无从下手
  • 依赖复杂:引入大量间接依赖,增加项目维护成本
  • 性能损耗:多层抽象带来的额外开销不容忽视
  • 学习曲线:需要掌握框架特有的概念和API

相比之下,我们的轻量化方案具有以下优势:

对比维度 LangChain方案 纯Java方案
依赖数量 10+个直接依赖 3个核心依赖
代码透明度 低(框架封装) 高(直接API调用)
定制灵活性 受限 完全可控
启动时间 较长 极快
内存占用 较高 优化可控

核心思路:通过直接调用Ollama和Qdrant的HTTP API,我们既能保持代码简洁,又能完全掌控每一个技术细节。

2. 环境准备与基础配置

2.1 服务端组件安装

首先确保已安装以下服务组件:

  1. Ollama服务

    curl -fsSL https://ollama.com/install.sh | sh
    ollama pull deepseek-r1:8b
    ollama pull nomic-embed-text:latest
    
  2. Qdrant向量数据库

    docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
    

2.2 SpringBoot项目初始化

创建标准的SpringBoot项目,添加以下核心依赖:

<dependencies>
    <!-- HTTP客户端 -->
    <dependency>
        <groupId>com.squareup.okhttp3</groupId>
        <artifactId>okhttp</artifactId>
        <version>4.12.0</version>
    </dependency>
    
    <!-- JSON处理 -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.15.2</version>
    </dependency>
    
    <!-- SpringBoot基础 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

提示:OkHttp版本建议使用4.10.0+以获得更好的HTTP/2支持,这对频繁的API调用场景很重要。

3. 核心服务层实现

3.1 文本嵌入服务

我们先实现文本向量化服务,这是RAG(检索增强生成)流程的第一步:

@Service
public class TextEmbeddingService {
    private static final MediaType JSON = MediaType.get("application/json");
    private final OkHttpClient httpClient;
    private final ObjectMapper objectMapper;
    private final String embeddingModel;
    private final String ollamaUrl;

    public TextEmbeddingService(
            @Value("${ollama.url}") String ollamaUrl,
            @Value("${ollama.embedding-model}") String embeddingModel) {
        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(120, TimeUnit.SECONDS)
                .build();
        this.objectMapper = new ObjectMapper();
        this.ollamaUrl = ollamaUrl;
        this.embeddingModel = embeddingModel;
    }

    public float[] embed(String text) throws IOException {
        Map<String, Object> request = Map.of(
                "model", embeddingModel,
                "prompt", text
        );

        RequestBody body = RequestBody.create(
                objectMapper.writeValueAsString(request), 
                JSON
        );

        Request httpRequest = new Request.Builder()
                .url(ollamaUrl + "/api/embeddings")
                .post(body)
                .build();

        try (Response response = httpClient.newCall(httpRequest).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Embedding failed: " + response.body().string());
            }
            
            JsonNode root = objectMapper.readTree(response.body().byteStream());
            return objectMapper.convertValue(
                    root.get("embedding"),
                    float[].class
            );
        }
    }
}

关键优化点:

  • 使用float[]而非List减少内存占用
  • 配置合理的超时时间(嵌入操作通常较快)
  • 直接解析JSON流避免中间字符串转换

3.2 Qdrant向量存储服务

接下来实现向量数据库操作服务:

@Service
public class VectorStoreService {
    private final OkHttpClient httpClient;
    private final ObjectMapper objectMapper;
    private final String qdrantUrl;
    private final String collectionName;

    public VectorStoreService(
            @Value("${qdrant.host}") String host,
            @Value("${qdrant.port}") int port,
            @Value("${qdrant.collection}") String collection) {
        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .build();
        this.objectMapper = new ObjectMapper();
        this.qdrantUrl = "http://" + host + ":" + port;
        this.collectionName = collection;
    }

    public void createCollection(int dimensions) throws IOException {
        Map<String, Object> config = Map.of(
                "vectors", Map.of(
                        "size", dimensions,
                        "distance", "Cosine"
                )
        );

        Request request = new Request.Builder()
                .url(qdrantUrl + "/collections/" + collectionName)
                .put(RequestBody.create(
                        objectMapper.writeValueAsString(config),
                        JSON
                ))
                .build();

        try (Response response = httpClient.newCall(request).execute()) {
            if (!response.isSuccessful() && response.code() != 400) {
                throw new IOException("Create collection failed: " + response.body().string());
            }
        }
    }

    public List<String> searchSimilar(float[] vector, int limit) throws IOException {
        Map<String, Object> query = Map.of(
                "vector", vector,
                "limit", limit,
                "with_payload", true
        );

        Request request = new Request.Builder()
                .url(qdrantUrl + "/collections/" + collectionName + "/points/search")
                .post(RequestBody.create(
                        objectMapper.writeValueAsString(query),
                        JSON
                ))
                .build();

        try (Response response = httpClient.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Search failed: " + response.body().string());
            }

            JsonNode root = objectMapper.readTree(response.body().byteStream());
            return StreamSupport.stream(root.get("result").spliterator(), false)
                    .map(point -> point.get("payload").get("text").asText())
                    .collect(Collectors.toList());
        }
    }
}

性能优化技巧:

  • 使用批量操作减少HTTP请求次数
  • 合理设置分片和复制因子提升查询性能
  • 对高频查询结果添加本地缓存

4. 智能问答服务集成

4.1 大模型交互服务

实现与DeepSeek模型的交互:

@Service
public class AIChatService {
    private static final String PROMPT_TEMPLATE = """
            基于以下上下文回答问题:
            %s
            
            问题:%s
            
            请用中文给出专业、准确的回答。如果无法从上下文中得到答案,请明确说明。""";

    private final OkHttpClient httpClient;
    private final ObjectMapper objectMapper;
    private final String ollamaUrl;
    private final String chatModel;

    public AIChatService(
            @Value("${ollama.url}") String ollamaUrl,
            @Value("${ollama.chat-model}") String chatModel) {
        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(300, TimeUnit.SECONDS) // 大模型响应可能需要较长时间
                .build();
        this.objectMapper = new ObjectMapper();
        this.ollamaUrl = ollamaUrl;
        this.chatModel = chatModel;
    }

    public String generateResponse(String prompt, List<String> contexts) throws IOException {
        String context = String.join("\n---\n", contexts);
        String fullPrompt = String.format(PROMPT_TEMPLATE, context, prompt);

        Map<String, Object> request = Map.of(
                "model", chatModel,
                "prompt", fullPrompt,
                "stream", false
        );

        Request httpRequest = new Request.Builder()
                .url(ollamaUrl + "/api/generate")
                .post(RequestBody.create(
                        objectMapper.writeValueAsString(request),
                        JSON
                ))
                .build();

        try (Response response = httpClient.newCall(httpRequest).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("Generation failed: " + response.body().string());
            }

            JsonNode root = objectMapper.readTree(response.body().byteStream());
            return root.get("response").asText();
        }
    }
}

提示工程技巧:

  • 使用明确的指令引导模型行为
  • 添加上下文分隔标记提高可读性
  • 设置合理的超时时间应对长文本生成

4.2 RAG流程控制器

最后实现完整的检索增强生成流程:

@RestController
@RequestMapping("/api/ai")
public class AIController {
    private final TextEmbeddingService embeddingService;
    private final VectorStoreService vectorStoreService;
    private final AIChatService chatService;

    @PostMapping("/ask")
    public ResponseEntity<String> askQuestion(@RequestBody String question) {
        try {
            // 1. 生成问题向量
            float[] queryVector = embeddingService.embed(question);
            
            // 2. 检索相关上下文
            List<String> contexts = vectorStoreService.searchSimilar(queryVector, 3);
            
            // 3. 生成回答
            String answer = chatService.generateResponse(question, contexts);
            
            return ResponseEntity.ok(answer);
        } catch (Exception e) {
            return ResponseEntity.status(500)
                    .body("处理请求时出错: " + e.getMessage());
        }
    }
}

错误处理建议:

  • 为不同服务设置不同的重试策略
  • 添加熔断机制防止级联故障
  • 记录详细的请求日志便于调试

5. 高级优化与实践技巧

5.1 性能调优策略

在实际应用中,我们还需要考虑以下优化点:

  1. 异步处理

    @Async
    public CompletableFuture<float[]> embedAsync(String text) {
        return CompletableFuture.completedFuture(embed(text));
    }
    
  2. 批量操作

    public void batchUpsert(List<Document> docs) {
        // 实现批量插入逻辑
    }
    
  3. 缓存策略

    @Cacheable("embeddings")
    public float[] getCachedEmbedding(String text) {
        return embed(text);
    }
    

5.2 生产环境注意事项

  • 配置管理:使用Spring Cloud Config集中管理各服务地址
  • 监控指标:暴露Prometheus指标监控API调用情况
  • 限流保护:使用Resilience4j实现API限流
@Bean
public CircuitBreakerConfigCustomizer circuitBreakerConfig() {
    return CircuitBreakerConfigCustomizer
            .of("ollama", builder -> builder
                    .failureRateThreshold(50)
                    .waitDurationInOpenState(Duration.ofSeconds(30))
                    .slidingWindowSize(10));
}

这套轻量级Java方案不仅运行高效,而且便于集成到现有Java生态系统中。相比使用LangChain等框架,我们的代码更透明、更可控,能够根据业务需求灵活调整每一个技术细节。

更多推荐