告别LangChain!用纯Java代码玩转Ollama和Qdrant,构建轻量级DeepSeek智能助手
·
告别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 服务端组件安装
首先确保已安装以下服务组件:
-
Ollama服务:
curl -fsSL https://ollama.com/install.sh | sh ollama pull deepseek-r1:8b ollama pull nomic-embed-text:latest -
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 性能调优策略
在实际应用中,我们还需要考虑以下优化点:
-
异步处理:
@Async public CompletableFuture<float[]> embedAsync(String text) { return CompletableFuture.completedFuture(embed(text)); } -
批量操作:
public void batchUpsert(List<Document> docs) { // 实现批量插入逻辑 } -
缓存策略:
@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等框架,我们的代码更透明、更可控,能够根据业务需求灵活调整每一个技术细节。
更多推荐


所有评论(0)