fix: 修复知识库索引并发与向量化边界

- 复用 Lucene 和 Elasticsearch 客户端并支持有界批量写入与删除

- 记录脱敏后的 Embedding 失败请求与完整响应

- 为 BGE-M3 分块统一增加上下文硬上限
This commit is contained in:
2026-08-07 12:38:09 +08:00
parent bdb69a2250
commit f13e24751a
12 changed files with 1472 additions and 263 deletions

View File

@@ -11,13 +11,19 @@ import com.easyagents.core.store.VectorData;
import com.easyagents.core.util.JSONUtil;
import com.easyagents.core.util.Maps;
import com.easyagents.core.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
public class OpenAIEmbeddingModel extends BaseEmbeddingModel<OpenAIEmbeddingConfig> {
private static final Logger LOG = LoggerFactory.getLogger(OpenAIEmbeddingModel.class);
private static final String REDACTED_HEADER_VALUE = "[REDACTED]";
private HttpClient httpClient = new HttpClient();
public OpenAIEmbeddingModel(OpenAIEmbeddingConfig config) {
@@ -40,27 +46,45 @@ public class OpenAIEmbeddingModel extends BaseEmbeddingModel<OpenAIEmbeddingConf
String payload = promptToEmbeddingsPayload(document, options, config);
String endpoint = config.getEndpoint();
String requestUrl = endpoint + config.getRequestPath();
// https://platform.openai.com/docs/api-reference/embeddings/create
String response = httpClient.post(endpoint + config.getRequestPath(), headers, payload);
String response = httpClient.post(requestUrl, headers, payload);
if (StringUtil.noText(response)) {
logResponseParsingFailure(
"response is null or empty",
requestUrl,
headers,
payload,
response
);
throw new ModelException("response is null or empty.");
}
JSONObject jsonObject = JSON.parseObject(response);
String errorMessage = JSONUtil.detectErrorMessage(jsonObject);
if (errorMessage != null) {
throw new ModelException(errorMessage);
}
try {
JSONObject jsonObject = JSON.parseObject(response);
String errorMessage = JSONUtil.detectErrorMessage(jsonObject);
if (errorMessage != null) {
logResponseParsingFailure(errorMessage, requestUrl, headers, payload, response);
throw new ModelException(errorMessage);
}
VectorData vectorData = new VectorData();
double[] embedding = JSONUtil.readDoubleArray(jsonObject, "$.data[0].embedding");
if (embedding == null || embedding.length == 0) {
throw new ModelException(buildMissingEmbeddingMessage());
}
vectorData.setVector(embedding);
VectorData vectorData = new VectorData();
double[] embedding = JSONUtil.readDoubleArray(jsonObject, "$.data[0].embedding");
if (embedding == null || embedding.length == 0) {
String missingEmbeddingMessage = buildMissingEmbeddingMessage();
logResponseParsingFailure(missingEmbeddingMessage, requestUrl, headers, payload, response);
throw new ModelException(missingEmbeddingMessage);
}
vectorData.setVector(embedding);
return vectorData;
return vectorData;
} catch (ModelException e) {
throw e;
} catch (RuntimeException e) {
logResponseParsingFailure(e.getMessage(), requestUrl, headers, payload, response);
throw new ModelException("Failed to parse embedding response.", e);
}
}
@@ -114,4 +138,35 @@ public class OpenAIEmbeddingModel extends BaseEmbeddingModel<OpenAIEmbeddingConf
+ ", endpoint=" + config.getEndpoint()
+ ", requestPath=" + config.getRequestPath();
}
private void logResponseParsingFailure(String reason,
String requestUrl,
Map<String, String> requestHeaders,
String requestBody,
String responseBody) {
LOG.error(
"Embedding response parsing failed: reason={}\n"
+ "requestUrl={}\n"
+ "requestHeaders={}\n"
+ "requestBody={}\n"
+ "responseBody={}",
reason,
requestUrl,
sanitizeHeadersForLogging(requestHeaders),
requestBody,
responseBody
);
}
static Map<String, String> sanitizeHeadersForLogging(Map<String, String> headers) {
Map<String, String> sanitizedHeaders = new LinkedHashMap<>();
if (headers == null) {
return sanitizedHeaders;
}
headers.forEach((name, value) -> sanitizedHeaders.put(
name,
"Authorization".equalsIgnoreCase(name) ? REDACTED_HEADER_VALUE : value
));
return sanitizedHeaders;
}
}

View File

@@ -7,6 +7,7 @@ import com.easyagents.core.model.exception.ModelException;
import org.junit.Assert;
import org.junit.Test;
import java.util.LinkedHashMap;
import java.util.Map;
/**
@@ -68,4 +69,46 @@ public class OpenAIEmbeddingModelTest {
Assert.assertTrue(exception.getMessage().contains("data[0].embedding"));
}
/**
* Verifies that invalid JSON responses are reported as embedding response parsing failures.
*/
@Test
public void shouldThrowModelExceptionWhenResponseIsInvalidJson() {
OpenAIEmbeddingConfig config = new OpenAIEmbeddingConfig();
config.setProvider("test-provider");
config.setModel("BAAI/bge-m3");
config.setApiKey("test-key");
OpenAIEmbeddingModel model = new OpenAIEmbeddingModel(config);
model.setHttpClient(new HttpClient() {
@Override
public String post(String url, Map<String, String> headers, String payload) {
return "not-json";
}
});
ModelException exception = Assert.assertThrows(
ModelException.class,
() -> model.embed(Document.of("hello"))
);
Assert.assertEquals("Failed to parse embedding response.", exception.getMessage());
Assert.assertNotNull(exception.getCause());
}
/**
* Verifies that diagnostic headers retain ordinary values without exposing credentials.
*/
@Test
public void shouldRedactAuthorizationHeaderForFailureLogging() {
Map<String, String> headers = new LinkedHashMap<>();
headers.put("Content-Type", "application/json");
headers.put("Authorization", "Bearer test-key");
Map<String, String> sanitizedHeaders = OpenAIEmbeddingModel.sanitizeHeadersForLogging(headers);
Assert.assertEquals("application/json", sanitizedHeaders.get("Content-Type"));
Assert.assertEquals("[REDACTED]", sanitizedHeaders.get("Authorization"));
Assert.assertEquals("Bearer test-key", headers.get("Authorization"));
}
}