EmbeddingBridgeClient.java 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. package com.agent.management.service.impl;
  2. import com.agent.management.common.LogRedactor;
  3. import com.agent.management.common.exception.BusinessException;
  4. import com.agent.management.config.EmbeddingBridgeProperties;
  5. import com.fasterxml.jackson.core.type.TypeReference;
  6. import com.fasterxml.jackson.databind.ObjectMapper;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.slf4j.Logger;
  9. import org.slf4j.LoggerFactory;
  10. import org.springframework.http.*;
  11. import org.springframework.http.client.SimpleClientHttpRequestFactory;
  12. import org.springframework.stereotype.Component;
  13. import org.springframework.web.client.RestClientResponseException;
  14. import org.springframework.web.client.RestTemplate;
  15. import java.util.List;
  16. import java.util.Map;
  17. /**
  18. * 本地 Embedding Bridge HTTP 客户端
  19. */
  20. @Slf4j
  21. @Component
  22. public class EmbeddingBridgeClient {
  23. /**
  24. * LLM/智能体调用专用 logger:
  25. * 由 logback-spring.xml 中 LLM_HTTP logger 配置为 DEBUG 级别,仅写入独立日志文件,不输出到控制台。
  26. */
  27. private static final Logger LLM_HTTP = LoggerFactory.getLogger("LLM_HTTP");
  28. private final EmbeddingBridgeProperties props;
  29. private final RestTemplate restTemplate;
  30. private final ObjectMapper objectMapper;
  31. public EmbeddingBridgeClient(EmbeddingBridgeProperties props) {
  32. this.props = props;
  33. SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
  34. int timeout = Math.max(props.getHttpTimeout(), 60000);
  35. factory.setConnectTimeout(5000);
  36. factory.setReadTimeout(timeout);
  37. this.restTemplate = new RestTemplate(factory);
  38. this.objectMapper = new ObjectMapper();
  39. }
  40. private String baseUrl() {
  41. return "http://" + props.getHost() + ":" + props.getPort();
  42. }
  43. private HttpHeaders headers() {
  44. HttpHeaders h = new HttpHeaders();
  45. h.setContentType(MediaType.APPLICATION_JSON);
  46. if (props.getAuthToken() != null && !props.getAuthToken().isBlank()) {
  47. h.set("X-Bridge-Token", props.getAuthToken());
  48. }
  49. return h;
  50. }
  51. /**
  52. * 健康检查
  53. */
  54. public boolean health() {
  55. try {
  56. ResponseEntity<String> resp = restTemplate.exchange(
  57. baseUrl() + "/health", HttpMethod.GET, new HttpEntity<>(headers()), String.class);
  58. return resp.getStatusCode().is2xxSuccessful();
  59. } catch (Exception e) {
  60. return false;
  61. }
  62. }
  63. /**
  64. * 对文档全文执行三级分块 + 向量化
  65. */
  66. @SuppressWarnings("unchecked")
  67. public Map<String, Object> indexDocument(long documentId, String text, String filename,
  68. String fileType, String filePath, Long categoryId) {
  69. Map<String, Object> body = Map.of(
  70. "document_id", documentId,
  71. "text", text,
  72. "filename", filename,
  73. "file_type", fileType,
  74. "file_path", filePath == null ? "" : filePath,
  75. "page_number", 0,
  76. "chunk_size", props.getChunkSize(),
  77. "chunk_overlap", props.getChunkOverlap(),
  78. "category_id", categoryId == null ? 0L : categoryId
  79. );
  80. return postJson("/index", body, new TypeReference<>() {});
  81. }
  82. /**
  83. * 批量向量化指定 chunk(仅用于补向量)
  84. */
  85. @SuppressWarnings("unchecked")
  86. public List<String> vectorizeChunks(long documentId, String filename, String fileType,
  87. String filePath, List<Map<String, Object>> chunks) {
  88. Map<String, Object> body = Map.of(
  89. "document_id", documentId,
  90. "filename", filename,
  91. "file_type", fileType,
  92. "file_path", filePath,
  93. "chunks", chunks
  94. );
  95. Map<String, Object> resp = postJson("/vectorize", body, new TypeReference<>() {});
  96. Object ids = resp.get("vector_ids");
  97. if (ids instanceof List<?> list) {
  98. return list.stream().map(String::valueOf).toList();
  99. }
  100. return List.of();
  101. }
  102. /**
  103. * 按文档 ID 删除 Milvus 向量
  104. */
  105. public int deleteByDocument(long documentId) {
  106. Map<String, Object> body = Map.of("document_id", documentId);
  107. Map<String, Object> resp = postJson("/delete_by_document", body, new TypeReference<>() {});
  108. Object deleted = resp.get("deleted");
  109. return deleted instanceof Number n ? n.intValue() : 0;
  110. }
  111. /**
  112. * 按 vector_id 删除 Milvus 向量
  113. */
  114. public int deleteByVectorIds(List<String> vectorIds) {
  115. Map<String, Object> body = Map.of("vector_ids", vectorIds);
  116. Map<String, Object> resp = postJson("/delete_by_vector_ids", body, new TypeReference<>() {});
  117. Object deleted = resp.get("deleted");
  118. return deleted instanceof Number n ? n.intValue() : 0;
  119. }
  120. public Map<String, Object> retrieve(String query, int topK, String mode, String filterExpr) {
  121. Map<String, Object> body = Map.of(
  122. "query", query, "top_k", topK,
  123. "mode", mode == null || mode.isBlank() ? "hybrid" : mode,
  124. "filter_expr", filterExpr == null ? "" : filterExpr);
  125. return postJson("/retrieve", body, new TypeReference<>() {});
  126. }
  127. @SuppressWarnings("unchecked")
  128. public List<List<Double>> embed(List<String> texts) {
  129. Map<String, Object> response = postJson("/embed", Map.of("texts", texts), new TypeReference<>() {});
  130. Object vectors = response.get("vectors");
  131. if (!(vectors instanceof List<?> rows)) return List.of();
  132. return rows.stream().map(row -> ((List<?>) row).stream()
  133. .map(value -> ((Number) value).doubleValue()).toList()).toList();
  134. }
  135. private <T> T postJson(String path, Object body, TypeReference<T> typeRef) {
  136. try {
  137. String jsonBody = objectMapper.writeValueAsString(body);
  138. HttpEntity<String> entity = new HttpEntity<>(jsonBody, headers());
  139. String fullUrl = baseUrl() + path;
  140. // === 请求 DEBUG 日志(仅写入 LLM 日志文件,不输出到控制台) ===
  141. LLM_HTTP.debug("[Embed-REQ] ========== 请求开始 ==========");
  142. LLM_HTTP.debug("[Embed-REQ] POST {}", fullUrl);
  143. LLM_HTTP.debug("[Embed-REQ] Body ({} bytes): {}", jsonBody.length(), jsonBody);
  144. long startTime = System.currentTimeMillis();
  145. ResponseEntity<String> resp = restTemplate.exchange(
  146. fullUrl, HttpMethod.POST, entity, String.class);
  147. long elapsed = System.currentTimeMillis() - startTime;
  148. String respBody = resp.getBody();
  149. // === 响应 DEBUG 日志 ===
  150. LLM_HTTP.debug("[Embed-RESP] HTTP {} ({}ms), body 长度={}",
  151. resp.getStatusCode(), elapsed, respBody == null ? 0 : respBody.length());
  152. LLM_HTTP.debug("[Embed-RESP] Body: {}", respBody == null ? null : LogRedactor.redactVectors(respBody));
  153. LLM_HTTP.debug("[Embed-RESP] ========== 请求结束 ==========");
  154. if (!resp.getStatusCode().is2xxSuccessful() || respBody == null) {
  155. throw new BusinessException("Embedding Bridge 调用失败: HTTP " + resp.getStatusCode());
  156. }
  157. return objectMapper.readValue(respBody, typeRef);
  158. } catch (RestClientResponseException e) {
  159. log.error("Embedding Bridge 调用失败: {}", e.getResponseBodyAsString(), e);
  160. throw new BusinessException("Embedding Bridge 调用失败: " + e.getResponseBodyAsString());
  161. } catch (Exception e) {
  162. log.error("Embedding Bridge 调用失败: {}", e.getMessage(), e);
  163. throw new BusinessException("Embedding Bridge 调用失败: " + e.getMessage());
  164. }
  165. }
  166. }