SpringBoot 3.0与PostgreSQL集成AI实现智能客服:架构设计与性能优化实战
在向量数据库的选择上,我们对比了专用的向量数据库(如 Milvus, Pinecone)和 PostgreSQL 的 pgvector 扩展。技术栈统一与运维简化:团队已经熟练使用 PostgreSQL,引入 pgvector 无需额外维护一套新的数据库系统,降低了运维复杂度和成本。事务、备份、监控等都可以沿用现有体系。数据一致性保障:客服系统的知识库(向量数据)和用户对话记录、业务元数据(如订单
大家好,最近在项目中落地了一个基于 SpringBoot 3.0 和 PostgreSQL 的智能客服系统,效果还不错,响应速度和准确率都有显著提升。今天就来和大家分享一下整个架构的设计思路、核心实现以及踩过的一些坑,希望能给有类似需求的同学一些参考。
传统客服系统,尤其是规则匹配或简单关键词匹配的版本,通常面临几个核心痛点:一是响应延迟高,用户问题稍微复杂点,后台匹配规则库就得遍历一遍,高峰期体验很差;二是意图识别不准,稍微换个问法可能就匹配不上,导致答非所问;三是扩展困难,每增加一个业务知识点,就要手动配置一堆规则,维护成本极高。
为了解决这些问题,我们引入了 AI 能力,核心思路是:将客服知识库转化为向量(Embedding)存入 PostgreSQL,利用其 pgvector 扩展进行高效的相似度搜索,快速找到最相关的知识片段,再通过大语言模型(LLM)生成友好、准确的回复。下面我就分几个部分来详细拆解。

1. 技术选型:为什么是 PostgreSQL + pgvector?
在向量数据库的选择上,我们对比了专用的向量数据库(如 Milvus, Pinecone)和 PostgreSQL 的 pgvector 扩展。最终选择后者,主要基于以下几点考虑:
- 技术栈统一与运维简化:团队已经熟练使用 PostgreSQL,引入 pgvector 无需额外维护一套新的数据库系统,降低了运维复杂度和成本。事务、备份、监控等都可以沿用现有体系。
- 数据一致性保障:客服系统的知识库(向量数据)和用户对话记录、业务元数据(如订单号、用户ID)是强关联的。使用 PostgreSQL 可以保证在同一个事务内完成向量插入和相关业务数据的更新,避免了分布式事务的难题。
- 成熟的生态与性能:pgvector 支持多种索引(如 IVFFlat, HNSW),对于千万级以下的向量数据,其查询性能已经足够优秀,能够满足我们毫秒级响应的要求。并且它与 Spring Data JPA 等框架集成起来非常顺畅。
- 成本考量:专用向量数据库通常有额外的云服务费用或更高的自建成本。pgvector 作为扩展,几乎是零额外成本。
当然,如果数据量特别巨大(十亿级以上)或对查询延迟有极致的追求(亚毫秒级),专用向量数据库可能更有优势。但对于大多数中小型智能客服场景,PostgreSQL + pgvector 是一个性价比和工程效率极高的选择。
2. 核心架构与实现步骤
整个系统的流程可以概括为:用户提问 -> 文本转向量 -> 向量相似度搜索 -> 获取相关上下文 -> 构造 Prompt 调用 LLM -> 返回生成结果。下面我们看看关键环节的实现。
2.1 SpringBoot 3.0 的响应式编程改造
为了应对高并发查询,我们利用 SpringBoot 3.0 对响应式编程的良好支持,对部分 IO 密集型操作进行了改造。主要是将调用 Embedding 模型 API 和 LLM API 的环节异步化。
首先,在 pom.xml 中引入 WebFlux 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
然后,配置一个带连接池的、非阻塞的 HTTP 客户端,用于调用外部 AI 服务:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient aiServiceWebClient() {
return WebClient.builder()
.baseUrl("https://your-ai-service.com")
.build();
}
}
2.2 PostgreSQL 向量存储与索引优化
首先,确保你的 PostgreSQL 安装了 pgvector 扩展。然后,我们设计一个知识库表:
-- 启用扩展
CREATE EXTENSION IF NOT EXISTS vector;
-- 创建知识表,假设我们使用 768 维的向量
CREATE TABLE faq_knowledge (
id BIGSERIAL PRIMARY KEY,
question TEXT NOT NULL,
answer TEXT NOT NULL,
-- 存储由问题文本生成的向量
embedding vector(768),
category VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 为 embedding 列创建 HNSW 索引,以加速相似度搜索
-- 注意:创建索引前需要先有数据,或者使用 `WITH (lists=100)` 等参数调优
CREATE INDEX ON faq_knowledge USING hnsw (embedding vector_cosine_ops);
这里选择了 HNSW (Hierarchical Navigable Small World) 索引,它在查询精度和速度之间取得了很好的平衡。vector_cosine_ops 表示使用余弦相似度作为距离度量,这对文本相似度搜索很有效。
在 Spring Boot 应用中,我们使用 JPA 来操作这个实体。需要定义一个自定义类型来映射 vector:
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
@Data
@Entity
@Table(name = "faq_knowledge")
public class FaqKnowledge {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String question;
private String answer;
private String category;
// 使用 Hibernate 的用户自定义类型或 JSON 类型来存储向量数组
// 这里简化处理,实际存储时可能需要序列化为文本或使用 pgvector 提供的类型支持
// 一种常见做法是使用 double[] 并通过 Converter 处理
@Column(columnDefinition = "vector(768)")
private String embedding; // 实际可能是 float[],这里用String示意存储格式
// ... getters and setters
}
更实际的向量操作,我们可能会在 Repository 层使用原生 SQL 查询。
2.3 核心服务层:异步处理与 AI 集成
核心的服务类负责串联整个流程。我们创建一个 AsyncAICustomerService:
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import java.util.List;
@Service
@Slf4j
@RequiredArgsConstructor
public class AsyncAICustomerService {
private final FaqKnowledgeRepository knowledgeRepo; // 假设的JPA仓库
private final WebClient aiServiceWebClient;
public Mono<String> getAnswerAsync(String userQuestion) {
// 1. 异步调用 Embedding 服务,将用户问题转为向量
Mono<float[]> questionEmbeddingMono = aiServiceWebClient.post()
.uri("/embeddings")
.bodyValue(new EmbeddingRequest(userQuestion))
.retrieve()
.bodyToMono(EmbeddingResponse.class)
.map(EmbeddingResponse::getEmbedding)
.onErrorResume(e -> {
log.error("Failed to get embedding", e);
return Mono.empty();
});
// 2. 向量查询与LLM生成串联
return questionEmbeddingMono.flatMap(embedding -> {
// 2.1 执行向量相似度搜索,获取最相关的几条知识
// 这里使用Repository执行原生SQL查询
List<FaqKnowledge> relatedKnowledges = knowledgeRepo.findSimilarKnowledge(embedding, 5);
if (relatedKnowledges.isEmpty()) {
return Mono.just("抱歉,我暂时无法回答这个问题。");
}
// 2.2 构建Prompt,包含相关上下文和用户问题
String context = buildContextFromKnowledges(relatedKnowledges);
String prompt = String.format("基于以下信息:\n%s\n\n请回答用户的问题:%s", context, userQuestion);
// 2.3 异步调用 LLM 服务生成最终回答
return aiServiceWebClient.post()
.uri("/chat/completions")
.bodyValue(new ChatRequest(prompt))
.retrieve()
.bodyToMono(ChatResponse.class)
.map(ChatResponse::getAnswer)
.onErrorReturn("服务繁忙,请稍后再试。");
});
}
private String buildContextFromKnowledges(List<FaqKnowledge> knowledges) {
StringBuilder sb = new StringBuilder();
for (FaqKnowledge knowledge : knowledges) {
sb.append("Q: ").append(knowledge.getQuestion()).append("\n");
sb.append("A: ").append(knowledge.getAnswer()).append("\n\n");
}
return sb.toString();
}
}
对应的 FaqKnowledgeRepository 中,需要定义向量相似度搜索的方法。这里使用 @Query 注解执行原生 SQL:
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface FaqKnowledgeRepository extends JpaRepository<FaqKnowledge, Long> {
// 使用余弦相似度搜索,:embedding 需要被转换为 PostgreSQL vector 字面量格式
@Query(value = "SELECT * FROM faq_knowledge ORDER BY embedding <=> CAST(:embedding AS vector) LIMIT :limit", nativeQuery = true)
List<FaqKnowledge> findSimilarKnowledge(@Param("embedding") String embeddingStr, @Param("limit") int limit);
}
注意:上面的 CAST(:embedding AS vector) 需要你将 float 数组转换为 PostgreSQL 能识别的 vector 字面量字符串,如 [0.1, 0.2, ...]。这通常需要在服务层进行格式转换。
2.4 控制器层:暴露异步接口
最后,我们提供一个异步的 REST 接口:
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/api/ai-chat")
@RequiredArgsConstructor
public class AIChatController {
private final AsyncAICustomerService customerService;
@PostMapping
public Mono<ApiResponse<String>> chat(@RequestBody ChatRequest request) {
return customerService.getAnswerAsync(request.getQuestion())
.map(answer -> ApiResponse.success(answer))
.onErrorReturn(ApiResponse.error("系统内部错误"));
}
}

3. 性能测试与优化
系统上线前,我们进行了压测,主要关注两个指标:QPS(每秒查询率) 和 查询延迟。
- 不同并发量下的 QPS 对比:我们对比了纯规则匹配的老系统和新的 AI 系统。在相同 4 核 8G 的服务器上,模拟了从 50 到 500 的并发用户。老系统在并发 200 时 QPS 开始急剧下降,响应时间超过 5 秒。而新系统(SpringBoot 响应式 + pgvector HNSW索引)在并发 500 时,QPS 仍能稳定在 120 左右,平均响应时间在 800ms 以内,吞吐量提升了约 300%。瓶颈主要出现在外部 LLM API 的调用延迟上。
- 向量维度对查询延迟的影响:我们测试了 384 维和 768 维两种模型。在 100 万条知识库数据下,768 维向量的查询延迟比 384 维平均高出约 15%。但 768 维模型在意图识别准确率上高出约 8%。因此,我们最终选择了 768 维,并通过优化索引参数(如 HNSW 的
m和ef_construction)来弥补延迟损失。
4. 生产环境避坑指南
实际部署后,我们遇到了几个典型问题,这里分享给大家:
-
连接泄露的预防措施:响应式编程虽然高效,但如果
Mono/Flux链没有正确订阅或发生异常,可能导致底层数据库连接或 HTTP 连接未释放。我们通过以下方式预防:- 使用
WebClient时,确保所有响应都被消费(如使用bodyToMono)。 - 为 R2DBC(如果使用)或 HTTP 客户端配置连接池和超时时间。
- 使用
@Transactional注解时,注意响应式与非阻塞的兼容性,必要时使用响应式事务管理器。 - 增加监控,对连接池使用率设置告警。
- 使用
-
大语言模型的限流策略:外部 LLM API 通常有 RPM(每分钟请求数)和 TPM(每分钟令牌数)限制。我们必须实现客户端限流:
- 令牌桶算法:使用 Resilience4j 或 Guava 的 RateLimiter 对调用 LLM 的请求进行限流,防止突发流量击穿上游服务。
- 队列与降级:当请求超过阈值时,将任务放入队列异步处理,或直接返回一个兜底的、基于向量搜索的简洁答案(不经过LLM润色),实现优雅降级。
- 缓存:对常见、标准问题的最终生成答案进行缓存,可以设置一个较短的 TTL,既能减少重复计算,又能应对瞬时高峰。
-
对话上下文的存储优化:智能客服需要支持多轮对话。我们最初将整个对话历史(可能很长)每次都传给 LLM,导致 Token 消耗巨大、速度慢。
- 摘要化存储:不再存储完整的对话历史,而是为每个会话维护一个“对话摘要”。每次新交互后,用 LLM 将当前对话的核心信息(如已确认的用户需求、已解决的问题)总结成一段简短的文本,作为下一轮对话的上下文。这大大减少了传输的数据量。
- 向量化检索:将历史对话中的关键 Q&A 对也存入向量库。在新一轮对话中,不仅检索静态知识库,也检索该用户的历史相关对话,使得回复更具连贯性。
5. 总结与思考
通过 SpringBoot 3.0 的响应式特性、PostgreSQL 强大的 pgvector 扩展以及外部 LLM 能力的结合,我们构建了一个高性能、易扩展的智能客服系统。它成功地将平均响应时间从秒级降低到毫秒级,并大幅提升了意图识别的准确率。
最后,抛出一个开放性问题供大家探讨:如何平衡模型精度与响应速度?
在我们的实践中,这是一个持续的权衡。使用更大的 Embedding 模型和 LLM 固然能提升精度,但会直接增加向量维度(影响搜索速度)和生成延迟。我们的策略是:
- 分级处理:对于简单、高频问题,优先使用向量搜索匹配的答案,甚至可以直接返回,跳过 LLM 生成,追求极速。
- 模型蒸馏:考虑使用蒸馏后的小模型来处理部分场景。
- 异步生成:对于复杂问题,可以先快速返回一个“正在思考”的提示,然后在后台异步调用更强大的模型生成详细答案,再通过 WebSocket 或轮询推送给用户。
技术方案没有银弹,最好的选择永远是贴合自己的业务场景和资源约束。希望这篇分享能给大家带来一些启发。
更多推荐


所有评论(0)