简述
实现一个高精度、低延迟的文本相似性检索系统,包含两个阶段:
- 召回(Recall):使用向量数据库
pgvector快速筛选出 top-k 相似文档。 - 重排(Rerank):使用 BGE 模型对召回结果进行语义重排序,提升最终结果的相关性。
为什么选择 Java 而非 Python?
|
系统集成 |
与 Spring Boot、微服务、企业级系统无缝集成,无需跨语言调用。 |
|
部署运维 |
单 JAR 包部署,无 Python 环境依赖(如 conda、pip、CUDA 配置)。 |
|
性能与内存 |
JVM GC 优化成熟,适合长时间运行服务;模型可缓存复用。 |
|
工程化 |
更强的类型安全、模块化、依赖管理(Maven/Gradle),适合团队协作。 |
|
生产稳定性 |
在金融、电商等场景广泛使用,故障率低,监控体系完善。 |
整体架构
+------------------+ +-------------------+ +------------------+
| 用户查询 Query | --> | EmbeddingService | --> | pgvector 查询 Top-K |
+------------------+ +-------------------+ +------------------+
|
v
+------------------+
| RerankerService | --> 重排序结果
+------------------+
在构建基于向量搜索的文本相似度系统时,pgvector 作为 PostgreSQL 的开源扩展,提供高效的向量存储和相似度计算能力(如 L2 距离、内积或余弦相似度),适用于大规模数据检索。BGE(BAAI General Embedding)模型是一种高性能的嵌入生成模型,支持多语言文本向量化,常用于语义搜索和推荐场景。传统实现中,BGE 模型通常依赖 Python 的 Hugging Face Transformers 库加载,但本分析聚焦于纯 Java 环境实现,避免 Python 依赖。具体目标包括:使用 BGE 生成文本嵌入,进行初始相似度筛选(retrieval),并通过重排(reranking)优化结果排序,以提升检索精度。
挑战在于 BGE 是基于 Transformer 的模型,原生不支持 Java,但可以通过 ONNX(Open Neural Network Exchange)格式转换并利用 ONNX Runtime 的 Java 绑定实现本地推理。这确保了跨平台兼容性和性能优化,同时结合 JDBC 或 Spring AI 与 pgvector 集成,实现端到端流程。
纯 Java 实现 pgvector + BGE 的相似筛选与重排完全可行,通过 ONNX 桥接 ML 模型和 JDBC/Spring AI 集成数据库,适用于企业级应用,避免 Python 依赖带来的部署复杂性
向量存储与相似度筛选:集成 pgvector
pgvector 扩展 PostgreSQL,支持向量类型(vector)和索引(如 HNSW 用于近似最近邻搜索)。在 Java 中,通过 JDBC 或 Spring AI 抽象层操作数据库,实现嵌入插入和相似度查询。
筛选阶段使用 HNSW 索引实现高效近似搜索,适用于海量数据(百万级向量),召回率 >95% 时延迟 <50ms。
重排机制:优化检索结果
初始筛选(retrieval)后,重排通过更精细的相似度计算(如跨编码器模型)对 top-K 结果重新排序,提升精度。BGE 本身支持 reranking,但需额外模型或算法。
实现
创建表结构
CREATE TABLE triage_feedback (
id SERIAL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
select * from triage_feedback
CREATE TABLE embedding (
id SERIAL PRIMARY KEY,
triage_feedback_id INTEGER NOT NULL,
chunk_text TEXT NOT NULL, -- 原始文本片段
embedding_vector VECTOR(1536), -- 假设使用 OpenAI 的 text-embedding-ada-002 (1536维)
search_vector TSVECTOR, -- 用于中文全文搜索
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
-- 外键约束
CONSTRAINT fk_triage_feedback
FOREIGN KEY (triage_feedback_id)
REFERENCES triage_feedback(id)
ON DELETE CASCADE
);
-- 创建索引以提高搜索性能
CREATE INDEX idx_embedding_vector ON embedding USING IVFFLAT (embedding_vector vector_cosine_ops);
CREATE INDEX idx_embedding_search_vector ON embedding USING GIN (search_vector);
相关的jpa代码
Embedding.java
package com.triage.mspbotai.entity;
import jakarta.persistence.*;
import java.time.ZonedDateTime;
import lombok.*;
import org.hibernate.annotations.ColumnTransformer;
import org.hibernate.annotations.Formula;
@Entity
@Table(name = "embedding")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Embedding {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "triage_feedback_id", nullable = false)
private Integer triageFeedbackId;
@Column(name = "chunk_text", nullable = false)
private String chunkText;
@Column(name = "embedding_vector", columnDefinition = "vector")
@ColumnTransformer(
read = "embedding_vector::float4[]",
write = "?::vector"
)
private float[] embeddingVector;
@Formula("to_tsvector('english', coalesce(content, ''))")
private String searchVector;
@Column(name = "created_at")
private ZonedDateTime createdAt;
@Column(name = "content")
private String content;
}
TriageFeedback.java
package com.triage.mspbotai.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import jakarta.persistence.*;
import java.time.ZonedDateTime;
@Entity
@Table(name = "triage_feedback")
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TriageFeedback {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false)
private String title;
private String description;
@Column(name = "created_at")
private ZonedDateTime createdAt;
}
TriageFeedbackRepository.java
package com.triage.mspbotai.repository;
import com.triage.mspbotai.entity.TriageFeedback;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface TriageFeedbackRepository extends JpaRepository<TriageFeedback, Integer> {
@Query(value = """
SELECT id, triage_feedback_id, chunk_text, created_at, content,
1 - (embedding_vector <=> CAST(?1 AS vector)) AS score
FROM embedding
ORDER BY embedding_vector <=> CAST(?1 AS vector)
LIMIT ?2
""", nativeQuery = true)
List<Object[]> findSimilarEmbeddings(float[] embedding, int limit);
}
因为jpa不支持直接操作tsvector 结构,我们需要使用触发器来修改这个字段
-- 创建触发器函数
CREATE OR REPLACE FUNCTION update_search_vector()
RETURNS TRIGGER AS $$
BEGIN
NEW.search_vector := to_tsvector('english', COALESCE(NEW.content, ''));
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- 创建触发器
CREATE TRIGGER tsvector_update
BEFORE INSERT OR UPDATE ON embedding
FOR EACH ROW EXECUTE FUNCTION update_search_vector();
-- 创建 GIN 索引(提升查询性能)
CREATE INDEX idx_search_vector ON embedding USING GIN(search_vector);
bge 模型调用代码
pom.xml 使用依赖
<!-- DJL for BGE models -->
<dependency>
<groupId>ai.djl</groupId>
<artifactId>api</artifactId>
<version>0.27.0</version>
</dependency>
<dependency>
<groupId>ai.djl.pytorch</groupId>
<artifactId>pytorch-engine</artifactId>
<version>0.27.0</version>
</dependency>
<dependency>
<groupId>ai.djl.onnxruntime</groupId>
<artifactId>onnxruntime-engine</artifactId>
<version>0.25.0</version> <!-- 请使用最新版本 -->
</dependency>
<dependency>
<groupId>ai.djl.huggingface</groupId>
<artifactId>tokenizers</artifactId>
<version>0.25.0</version> <!-- 与 DJL 版本匹配 -->
</dependency>
EmbeddingService.java
package com.triage.mspbotai.service;
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.djl.inference.Predictor;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.translate.TranslateException;
import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
import com.triage.mspbotai.entity.TriageFeedback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@Service
public class EmbeddingService {
private static final Logger logger = LoggerFactory.getLogger(EmbeddingService.class);
private Predictor<String, float[]> predictor;
public EmbeddingService() {
logger.info("EmbeddingService constructor called");
}
@PostConstruct
public void init() {
try {
Path tokenizerPath = Paths.get("/opt/maxkb/model/bge-large-en-v1.5");
Path modelPath = Paths.get("/opt/maxkb/model/bge-large-en-v1.5/onnx/model.onnx");
if (!Files.exists(tokenizerPath) || !Files.exists(modelPath)) {
throw new IOException("Model or tokenizer path does not exist: " + tokenizerPath + " or " + modelPath);
}
HuggingFaceTokenizer tokenizer = HuggingFaceTokenizer.builder()
.optTokenizerPath(tokenizerPath)
.optPadding(true)
.optTruncation(true)
.optMaxLength(4096)
.build();
Criteria<String, float[]> criteria = Criteria.builder()
.setTypes(String.class, float[].class)
.optEngine("OnnxRuntime")
.optModelPath(modelPath)
.optTranslator(new BgeEmbeddingTranslator(tokenizer))
.build();
ZooModel<String, float[]> model = criteria.loadModel();
this.predictor = model.newPredictor();
logger.info("EmbeddingService initialized successfully");
} catch (Exception e) {
logger.error("Failed to initialize EmbeddingService", e);
throw new RuntimeException("EmbeddingService initialization failed", e);
}
}
public float[] generateEmbedding(String text) {
try {
return predictor.predict(text);
} catch (TranslateException e) {
logger.error("Failed to generate embedding for text: {}", text, e);
throw new RuntimeException("Embedding generation failed", e);
}
}
public float[] generateEmbedding(TriageFeedback feedback) throws Exception {
String combined = feedback.getTitle() + " " + (feedback.getDescription() != null ? feedback.getDescription() : "");
return generateEmbedding(combined);
}
private static class BgeEmbeddingTranslator implements Translator<String, float[]> {
private final HuggingFaceTokenizer tokenizer;
private final boolean normalize;
public BgeEmbeddingTranslator(HuggingFaceTokenizer tokenizer) {
this.tokenizer = tokenizer;
this.normalize = true;
}
@Override
public NDList processInput(TranslatorContext ctx, String input) throws Exception {
// Tokenize the input text
var encoding = tokenizer.encode(input);
NDManager manager = ctx.getNDManager();
// Create input tensors with shape [1, sequence_length]
long[] inputIdsArray = encoding.getIds();
long[] attentionMaskArray = encoding.getAttentionMask();
long[] tokenTypeIdsArray = encoding.getTypeIds();
// Convert to NDArray with shape [sequence_length]
NDArray inputIds = manager.create(inputIdsArray);
NDArray attentionMask = manager.create(attentionMaskArray);
NDArray tokenTypeIds = manager.create(tokenTypeIdsArray);
return new NDList(inputIds, attentionMask, tokenTypeIds);
}
@Override
public float[] processOutput(TranslatorContext ctx, NDList output) throws Exception {
NDArray lastHiddenState = output.get(0);
// Step 1: 确保是 [seq_len, hidden_size] 或 [1, seq_len, hidden_size]
if (lastHiddenState.getShape().dimension() == 3) {
// 如果是 [1, seq_len, hidden_size],去掉 batch 维度 -> [seq_len, hidden_size]
if (lastHiddenState.size(0) == 1) {
lastHiddenState = lastHiddenState.squeeze(0); // shape: [seq_len, hidden_size]
}
}
// 现在 shape 应该是 [seq_len, hidden_size]
long seqLen = lastHiddenState.size(0);
long hiddenSize = lastHiddenState.size(1);
logger.debug("last_hidden_state shape: [{} x {}]", seqLen, hiddenSize);
// Step 2: Mean Pooling over sequence length (axis=0)
NDArray sentenceEmbedding = lastHiddenState.mean(new int[]{0}); // shape: [hidden_size]
// Step 3: L2 Normalization (BGE 模型要求)
if (normalize) {
sentenceEmbedding = sentenceEmbedding.div(sentenceEmbedding.norm());
}
// Step 4: 转为 float[],长度 = hidden_size (如 1024)
return sentenceEmbedding.toFloatArray();
}
}
}
RerankerService.java
package com.triage.mspbotai.service;
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.djl.inference.Predictor;
import ai.djl.ndarray.NDArray;
import ai.djl.ndarray.NDList;
import ai.djl.ndarray.NDManager;
import ai.djl.repository.zoo.Criteria;
import ai.djl.repository.zoo.ZooModel;
import ai.djl.translate.TranslateException;
import ai.djl.translate.Translator;
import ai.djl.translate.TranslatorContext;
import com.triage.mspbotai.entity.TriageFeedback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class RerankerService {
private static final Logger logger = LoggerFactory.getLogger(RerankerService.class);
private Predictor<RerankerInput, Float> predictor;
private HuggingFaceTokenizer tokenizer;
@PostConstruct
public void init() {
try {
Path modelPath = Paths.get("/opt/maxkb/model/bge-reranker-large/onnx/model.onnx");
Path tokenizerPath = Paths.get("/opt/maxkb/model/bge-reranker-large");
// 1. 检查模型和 tokenizer 路径是否存在
if (!Files.exists(modelPath)) {
throw new IOException("Reranker model path does not exist: " + modelPath);
}
if (!Files.exists(tokenizerPath)) {
throw new IOException("Tokenizer path does not exist: " + tokenizerPath);
}
// 2. 显式加载 HuggingFace Tokenizer
this.tokenizer = HuggingFaceTokenizer.builder()
.optTokenizerPath(tokenizerPath)
.optPadding(true)
.optTruncation(true)
.optMaxLength(512)
.build();
// 3. 构建 Criteria,使用自定义 Translator
Criteria<RerankerInput, Float> criteria = Criteria.builder()
.setTypes(RerankerInput.class, Float.class)
.optEngine("OnnxRuntime")
.optModelPath(modelPath)
.optTranslator(new RerankerTranslator(tokenizer))
.build();
// 4. 加载模型
ZooModel<RerankerInput, Float> model = criteria.loadModel();
this.predictor = model.newPredictor();
logger.info("Reranker service initialized successfully with model: {}", modelPath);
} catch (Exception e) {
logger.error("Failed to initialize Reranker service", e);
throw new RuntimeException("Reranker initialization failed", e);
}
}
public List<ScoredFeedback> rerank(String query, List<TriageFeedback> candidates) throws TranslateException {
return candidates.stream()
.map(cand -> {
try {
String doc = cand.getTitle() + " " + (cand.getDescription() != null ? cand.getDescription() : "");
Float score = predictor.predict(new RerankerInput(query, doc));
return new ScoredFeedback(cand, score);
} catch (Exception e) {
return new ScoredFeedback(cand, 0.0f);
}
})
.sorted((a, b) -> Float.compare(b.score, a.score))
.limit(5)
.collect(Collectors.toList());
}
public static class RerankerInput {
private String query;
private String document;
public RerankerInput(String query, String document) {
this.query = query;
this.document = document;
}
public String getQuery() {
return query;
}
public String getDocument() {
return document;
}
}
public static class ScoredFeedback {
private TriageFeedback feedback;
private float score;
public ScoredFeedback(TriageFeedback feedback, float score) {
this.feedback = feedback;
this.score = score;
}
public TriageFeedback getFeedback() {
return feedback;
}
public float getScore() {
return score;
}
}
private static class RerankerTranslator implements Translator<RerankerInput, Float> {
private final HuggingFaceTokenizer tokenizer;
public RerankerTranslator(HuggingFaceTokenizer tokenizer) {
this.tokenizer = tokenizer;
}
@Override
public NDList processInput(TranslatorContext ctx, RerankerInput input) throws Exception {
// Combine query and document for tokenization
String text = input.getQuery() + " " + input.getDocument();
// Tokenize the input text
var encoding = tokenizer.encode(text);
NDManager manager = ctx.getNDManager();
// Convert tokens to NDArray
long[] inputIdsArray = encoding.getIds();
NDArray inputIds = manager.create(inputIdsArray);
inputIds.setName("input_ids");
// Create attention mask
long[] attentionMaskArray = encoding.getAttentionMask();
NDArray attentionMask = manager.create(attentionMaskArray);
attentionMask.setName("attention_mask");
// Return NDList with input_ids and attention_mask
return new NDList(inputIds, attentionMask);
}
@Override
public Float processOutput(TranslatorContext ctx, NDList output) throws Exception {
// The bge-reranker-large model typically outputs a single score
NDArray scoreArray = output.get(0);
// Apply sigmoid to convert logits to probability (0-1 range)
float score = (float) (1.0 / (1.0 + Math.exp(-scoreArray.getFloat())));
return score;
}
}
}
TriagePipelineService.java
package com.triage.mspbotai.service;
import com.huaban.analysis.jieba.JiebaSegmenter;
import com.huaban.analysis.jieba.SegToken;
import com.triage.mspbotai.entity.Embedding;
import com.triage.mspbotai.entity.TriageFeedback;
import com.triage.mspbotai.repository.EmbeddingRepository;
import com.triage.mspbotai.repository.TriageFeedbackRepository;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class TriagePipelineService {
@Autowired
private EmbeddingService embeddingService;
@Autowired
private TriageFeedbackRepository feedbackRepository;
@Autowired
private EmbeddingRepository embeddingRepository;
@Autowired
private RerankerService rerankerService;
@Transactional
public void insertFeedbackTriageTicket(String title, String description) {
// Step 1: Create and save new feedback
TriageFeedback newFeedback = new TriageFeedback();
newFeedback.setTitle(title);
newFeedback.setDescription(description);
newFeedback.setCreatedAt(ZonedDateTime.now());
newFeedback = feedbackRepository.save(newFeedback);
// Step 2: Generate and save embedding
String combined = newFeedback.getTitle() + " " + (newFeedback.getDescription() != null ? newFeedback.getDescription() : "");
float[] embedding = embeddingService.generateEmbedding(combined);
Embedding embeddingEntity = new Embedding();
embeddingEntity.setTriageFeedbackId(newFeedback.getId());
embeddingEntity.setChunkText(title + " " + (description != null ? description : ""));
embeddingEntity.setEmbeddingVector(embedding);
embeddingEntity.setCreatedAt(ZonedDateTime.now());
// Step 3: Generate search vector
embeddingEntity.setContent(combined);
embeddingRepository.save(embeddingEntity);
}
@Transactional
public List<RerankerService.ScoredFeedback> search(String title, String description) throws Exception {
TriageFeedback newFeedback = new TriageFeedback();
newFeedback.setTitle(title);
newFeedback.setDescription(description);
float[] embedding = embeddingService.generateEmbedding(newFeedback);
// Step 3: Vector search for top-20 similar feedbacks
List<Object[]> similarFeedbacks = feedbackRepository.findSimilarEmbeddings(embedding, 20);
// id, triage_feedback_id, chunk_text, created_at, content, embedding_vector
List<TriageFeedback> candidates = similarFeedbacks.stream().map(result -> {
TriageFeedback feedback = new TriageFeedback();
feedback.setId((Integer) result[1]);
feedback.setTitle(StringUtils.substring((String) result[2], 0,20));
feedback.setDescription((String) result[2]);
return feedback;
}).collect(Collectors.toList());
List<RerankerService.ScoredFeedback> reranked = new ArrayList<>();
String query = newFeedback.getTitle() + " " + (newFeedback.getDescription() != null ? newFeedback.getDescription() : "");
if (!candidates.isEmpty()) {
// Step 4: Rerank candidates
reranked = rerankerService.rerank(query, candidates);
}
// Step 5: return reranked
return reranked;
}
}
需要重点说明下
1. 使用 DJL + ONNX Runtime 而非 PyTorch
为什么重要? 这是整个服务的技术选型核心。
- 说明:本服务使用 Deep Java Library (DJL) 的
OnnxRuntime引擎加载bge-large-en-v1.5模型的 ONNX 格式。 - 优势:
- 避免 PyTorch 原生库下载:不依赖庞大的
libtorch.so,避免在生产环境因网络问题下载失败。 - 轻量、高性能:ONNX Runtime 是专为推理优化的引擎,启动快、内存占用低。
- 跨平台:无需为不同 OS/架构编译 PyTorch。
- 避免 PyTorch 原生库下载:不依赖庞大的
- 模型需提前从 Hugging Face 转换为 ONNX 格式(使用
transformers.onnx工具)。
HuggingFaceTokenizer 的使用与配置
为什么重要? 分词是生成正确 embedding 的第一步。
- 说明:使用 DJL 的
HuggingFaceTokenizer组件进行文本预处理,确保与 BGE 模型训练时的分词方式一致。 - 关键配置:
.optPadding(true):自动填充到最大长度,满足 ONNX 模型对固定输入 shape 的要求。.optTruncation(true):超长文本自动截断。.optMaxLength(512):BGE 模型推荐的最大序列长度,必须与 ONNX 模型导出时的配置一致。
processInput 中的张量维度处理(关键!)
为什么重要? 这是模型推理能否成功的关键。
- 说明:ONNX 模型期望输入为
[batch_size, sequence_length]的 2D 张量。
processOutput 中的 Mean Pooling 与 L2 归一化
为什么重要? 这是生成可用 sentence embedding 的后处理步骤。
- 说明:
- Mean Pooling:BGE 模型输出
last_hidden_state是[seq_len, hidden_size]的矩阵。通过对sequence_length维度(axis=0)求平均,得到[hidden_size]的句子向量。 - L2 Normalization:BGE 模型要求输出向量为单位向量(模长为1),这样才能使用 cosine 相似度进行高效检索。
sentenceEmbedding.div(sentenceEmbedding.norm())实现了这一点。
- Mean Pooling:BGE 模型输出
最后写一点spring托管相比python的优点
predictor 的线程安全性
- 说明:DJL 的
Predictor是线程安全的。 - 需说明:
EmbeddingService是一个单例(@Service),其predictor成员变量可以被多个 HTTP 请求线程并发调用,无需额外的同步机制。这使得服务可以高效处理高并发的 embedding 请求。
测试
测试feedback 向量化保存
curl --location 'http://127.0.0.1:9000/postFeedback' \
--header 'Content-Type: application/json' \
--data-raw '[
{
"title": "My problem isn'\''t listed or I have feedback",
"description": "If you couldn'\''t find your problem listed under a category or you would like to submit feedback for the support portal\r\n\r\nDetails\r\n-----------------------------------------\r\nRequesting an IT Service: Service Requests are for things that aren'\''t broken but IT need to help with, like changing mailbox access or setting up a computer. Request an IT Service here\r\nWould you like to provide feedback or report a different problem?: Different problem\r\nBriefly describe the problem that is occurring: Requesting admin access to be able to install triconvey \r\n",
"billing_codes": "Remote Urgent Support"
},
{
"title": "User offboarding",
"description": "Immediate offboarding of \nPeter Garrone",
"billing_codes": "Remote Urgent Support"
},
...
]'

测试命中
curl --location 'http://127.0.0.1:9000/test_hit' \
--header 'Content-Type: application/json' \
--data '{
"title": "New Support Ticket R",
"description": "User offboarding Immediate offboarding of \nPeter Garrone"
}'

返回中score是重拍的得分
{
"feedback": {
"id": 94,
"title": "User offboarding Imm",
"description": "User offboarding Immediate offboarding of \nPeter Garrone",
"createdAt": null
},
"score": 0.43528542
},
总结:用到的技术
|
DJL + ONNX Runtime 选型 |
解释为何不使用 Python/PyTorch,突出 Java 部署优势。 |
|
HuggingFaceTokenizer 配置 |
确保分词行为与模型训练一致,是准确性的基础。 |
|
|
关键修复点,否则模型无法运行。 |
|
Mean Pooling + L2 Normalize |
解释如何从模型输出得到最终的 sentence embedding。 |
|
模型路径与部署 |
明确运维要求,确保环境一致性。 |
|
|
说明“宁可不启动,也不返回错误结果”的设计哲学。 |
|
|
证明服务可以支持高并发,是性能保障。 |

4180

被折叠的 条评论
为什么被折叠?



