feat: 全新智能体功能
- 基于先进智能体框架,增加智能体编排功能 - 增加智能体聊天,并对接持久化
This commit is contained in:
@@ -45,7 +45,24 @@ public final class ChatToolNameHelper {
|
||||
return buildFallbackName(fallbackPrefix, resourceId);
|
||||
}
|
||||
|
||||
private static String buildFallbackName(String fallbackPrefix, BigInteger resourceId) {
|
||||
/**
|
||||
* 判断工具名称是否满足 OpenAI-compatible function.name 约束。
|
||||
*
|
||||
* @param name 待检查名称
|
||||
* @return 名称合法返回 true,否则返回 false
|
||||
*/
|
||||
public static boolean isSafeToolName(String name) {
|
||||
return StringUtils.hasText(name) && SAFE_TOOL_NAME_PATTERN.matcher(name).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建稳定的安全兜底工具名称。
|
||||
*
|
||||
* @param fallbackPrefix 安全兜底名前缀
|
||||
* @param resourceId 资源 ID
|
||||
* @return 安全兜底工具名称
|
||||
*/
|
||||
public static String buildFallbackName(String fallbackPrefix, BigInteger resourceId) {
|
||||
String prefix = StringUtils.hasText(fallbackPrefix) ? fallbackPrefix : "tool";
|
||||
String suffix = resourceId == null ? "unknown" : resourceId.toString();
|
||||
return prefix + "_" + suffix;
|
||||
|
||||
@@ -7,16 +7,7 @@ import com.easyagents.core.model.rerank.RerankModel;
|
||||
import com.easyagents.core.store.DocumentStore;
|
||||
import com.easyagents.core.store.SearchWrapper;
|
||||
import com.easyagents.core.store.StoreOptions;
|
||||
import com.easyagents.rag.retrieval.HitSource;
|
||||
import com.easyagents.rag.retrieval.KeywordRetriever;
|
||||
import com.easyagents.rag.retrieval.RagHit;
|
||||
import com.easyagents.rag.retrieval.RagQuery;
|
||||
import com.easyagents.rag.retrieval.RagRetrievalExecutor;
|
||||
import com.easyagents.rag.retrieval.RagScoreNormalizer;
|
||||
import com.easyagents.rag.retrieval.RagRetrievalResult;
|
||||
import com.easyagents.rag.retrieval.RetrievalMode;
|
||||
import com.easyagents.rag.retrieval.RrfFusionStrategy;
|
||||
import com.easyagents.rag.retrieval.VectorRetriever;
|
||||
import com.easyagents.rag.retrieval.*;
|
||||
import com.easyagents.search.engine.service.DocumentSearcher;
|
||||
import com.easyagents.search.engine.service.KeywordSearchRequest;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
@@ -26,13 +17,13 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import tech.easyflow.ai.config.SearcherFactory;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportKeys;
|
||||
import tech.easyflow.ai.entity.DocumentChunk;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.enums.DocumentProcessStatus;
|
||||
import tech.easyflow.ai.enums.PublishStatus;
|
||||
import tech.easyflow.ai.documentimport.DocumentImportKeys;
|
||||
import tech.easyflow.ai.mapper.DocumentChunkMapper;
|
||||
import tech.easyflow.ai.mapper.DocumentCollectionMapper;
|
||||
import tech.easyflow.ai.mapper.DocumentMapper;
|
||||
@@ -50,18 +41,10 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_DOC_RECALL_MAX_NUM;
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_RERANK_ENABLE;
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.KEY_SIMILARITY_THRESHOLD;
|
||||
import static tech.easyflow.ai.entity.DocumentCollection.*;
|
||||
|
||||
/**
|
||||
* 服务层实现。
|
||||
@@ -76,6 +59,7 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
private static final int MAX_FAQ_IMAGES_IN_PROMPT = 3;
|
||||
private static final int INTERNAL_RECALL_MULTIPLIER = 5;
|
||||
private static final int MAX_INTERNAL_RECALL_LIMIT = 100;
|
||||
private static final int LOG_TEXT_MAX_LENGTH = 300;
|
||||
|
||||
@Resource
|
||||
private ModelService llmService;
|
||||
@@ -122,6 +106,18 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
int docRecallMaxNum = resolveDocRecallMaxNum(request, documentCollection);
|
||||
int internalRecallLimit = resolveInternalRecallLimit(docRecallMaxNum);
|
||||
float minSimilarity = resolveMinSimilarity(request, documentCollection);
|
||||
LOG.info(
|
||||
"Knowledge retrieval started, callerType={}, callerId={}, knowledgeId={}, query={}, retrievalMode={}, limit={}, internalLimit={}, minSimilarity={}, faqCollection={}",
|
||||
request.getCallerType(),
|
||||
request.getCallerId(),
|
||||
request.getKnowledgeId(),
|
||||
keyword,
|
||||
retrievalMode,
|
||||
docRecallMaxNum,
|
||||
internalRecallLimit,
|
||||
minSimilarity,
|
||||
documentCollection.isFaqCollection()
|
||||
);
|
||||
|
||||
RagQuery ragQuery = new RagQuery();
|
||||
ragQuery.setQuery(keyword);
|
||||
@@ -136,10 +132,28 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
);
|
||||
|
||||
RagRetrievalResult retrievalResult = retrievalExecutor.retrieve(ragQuery);
|
||||
LOG.info(
|
||||
"Knowledge retrieval raw hits, callerType={}, callerId={}, knowledgeId={}, query={}, hitCount={}, hits={}",
|
||||
request.getCallerType(),
|
||||
request.getCallerId(),
|
||||
request.getKnowledgeId(),
|
||||
keyword,
|
||||
retrievalResult == null || retrievalResult.getHits() == null ? 0 : retrievalResult.getHits().size(),
|
||||
summarizeRagHits(retrievalResult == null ? null : retrievalResult.getHits())
|
||||
);
|
||||
List<Document> searchDocuments = prepareSearchDocuments(
|
||||
documentCollection,
|
||||
toDocuments(retrievalResult.getHits())
|
||||
);
|
||||
LOG.info(
|
||||
"Knowledge retrieval prepared documents, callerType={}, callerId={}, knowledgeId={}, query={}, documentCount={}, documents={}",
|
||||
request.getCallerType(),
|
||||
request.getCallerId(),
|
||||
request.getKnowledgeId(),
|
||||
keyword,
|
||||
searchDocuments.size(),
|
||||
summarizeDocuments(searchDocuments)
|
||||
);
|
||||
if (searchDocuments.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
@@ -154,6 +168,15 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
toDocuments(rerankResult.getHits())
|
||||
);
|
||||
reranked = true;
|
||||
LOG.info(
|
||||
"Knowledge retrieval reranked documents, callerType={}, callerId={}, knowledgeId={}, query={}, documentCount={}, documents={}",
|
||||
request.getCallerType(),
|
||||
request.getCallerId(),
|
||||
request.getKnowledgeId(),
|
||||
keyword,
|
||||
searchDocuments.size(),
|
||||
summarizeDocuments(searchDocuments)
|
||||
);
|
||||
} catch (RerankException e) {
|
||||
LOG.warn("Rerank failed for collectionId={}, modelId={}, fallback to retrieved results. message={}",
|
||||
documentCollection.getId(), documentCollection.getRerankModelId(), e.getMessage());
|
||||
@@ -161,7 +184,23 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
}
|
||||
|
||||
RagScoreNormalizer.normalize(searchDocuments, retrievalMode, reranked);
|
||||
return formatDocuments(searchDocuments, shouldApplyMinSimilarityFilter(retrievalMode, reranked), minSimilarity, docRecallMaxNum);
|
||||
List<Document> formattedDocuments = formatDocuments(
|
||||
searchDocuments,
|
||||
shouldApplyMinSimilarityFilter(retrievalMode, reranked),
|
||||
minSimilarity,
|
||||
docRecallMaxNum
|
||||
);
|
||||
LOG.info(
|
||||
"Knowledge retrieval completed, callerType={}, callerId={}, knowledgeId={}, query={}, reranked={}, finalCount={}, documents={}",
|
||||
request.getCallerType(),
|
||||
request.getCallerId(),
|
||||
request.getKnowledgeId(),
|
||||
keyword,
|
||||
reranked,
|
||||
formattedDocuments.size(),
|
||||
summarizeDocuments(formattedDocuments)
|
||||
);
|
||||
return formattedDocuments;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,12 +299,25 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
StoreOptions options = StoreOptions.ofCollectionName(documentCollection.getVectorStoreCollection());
|
||||
options.setIndexName(documentCollection.getVectorStoreCollection());
|
||||
List<Document> documents = documentStore.search(wrapper, options);
|
||||
return documents == null ? Collections.<Document>emptyList() : documents;
|
||||
List<Document> result = documents == null ? Collections.<Document>emptyList() : documents;
|
||||
LOG.info(
|
||||
"Knowledge vector search completed, knowledgeId={}, collectionName={}, query={}, limit={}, minSimilarity={}, hitCount={}, hits={}",
|
||||
documentCollection.getId(),
|
||||
documentCollection.getVectorStoreCollection(),
|
||||
keyword,
|
||||
docRecallMaxNum,
|
||||
minSimilarity,
|
||||
result.size(),
|
||||
summarizeDocuments(result)
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Document> searchKeywordDocuments(DocumentCollection documentCollection, String keyword, int docRecallMaxNum) {
|
||||
DocumentSearcher searcher = searcherFactory.getSearcher();
|
||||
if (searcher == null) {
|
||||
LOG.warn("Knowledge keyword search skipped because searcher is unavailable, knowledgeId={}, query={}",
|
||||
documentCollection == null ? null : documentCollection.getId(), keyword);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
KeywordSearchRequest request = KeywordSearchRequest.of(keyword, docRecallMaxNum);
|
||||
@@ -273,7 +325,16 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
? null
|
||||
: documentCollection.getId().toString());
|
||||
List<Document> documents = searcher.searchDocuments(request);
|
||||
return documents == null ? Collections.<Document>emptyList() : documents;
|
||||
List<Document> result = documents == null ? Collections.<Document>emptyList() : documents;
|
||||
LOG.info(
|
||||
"Knowledge keyword search completed, knowledgeId={}, query={}, limit={}, hitCount={}, hits={}",
|
||||
request.getKnowledgeId(),
|
||||
keyword,
|
||||
docRecallMaxNum,
|
||||
result.size(),
|
||||
summarizeDocuments(result)
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<RagHit> adaptDocuments(List<Document> documents, HitSource hitSource) {
|
||||
@@ -392,21 +453,38 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
}
|
||||
if (documentCollection.isFaqCollection()) {
|
||||
fillSearchContent(documentCollection, searchDocuments);
|
||||
LOG.info(
|
||||
"Knowledge FAQ documents prepared, knowledgeId={}, remainingCount={}, documents={}",
|
||||
documentCollection.getId(),
|
||||
searchDocuments.size(),
|
||||
summarizeDocuments(searchDocuments)
|
||||
);
|
||||
return searchDocuments;
|
||||
}
|
||||
DocumentHitSnapshot hitSnapshot = loadDocumentHitSnapshot(documentCollection, searchDocuments);
|
||||
if (hitSnapshot.isEmpty()) {
|
||||
LOG.info(
|
||||
"Knowledge document hits filtered out, knowledgeId={}, reason=no_completed_source_document, originalCount={}",
|
||||
documentCollection.getId(),
|
||||
searchDocuments.size()
|
||||
);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return searchDocuments.stream()
|
||||
List<Document> preparedDocuments = searchDocuments.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.filter(item -> {
|
||||
Object chunkId = item.getId();
|
||||
String content = hitSnapshot.findChunkContent(item.getId());
|
||||
if (!StringUtil.hasText(content)) {
|
||||
return false;
|
||||
}
|
||||
item.setContent(content);
|
||||
item.addMetadata("chunkId", chunkId);
|
||||
Object sourceDocumentId = hitSnapshot.findSourceDocumentId(item.getId());
|
||||
if (sourceDocumentId != null) {
|
||||
item.addMetadata("documentId", sourceDocumentId);
|
||||
}
|
||||
String renderMarkdown = hitSnapshot.findChunkRenderMarkdown(item.getId());
|
||||
if (StringUtil.hasText(renderMarkdown)) {
|
||||
item.addMetadata("renderMarkdown", renderMarkdown);
|
||||
@@ -418,6 +496,14 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
return true;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
LOG.info(
|
||||
"Knowledge document hits prepared, knowledgeId={}, originalCount={}, remainingCount={}, documents={}",
|
||||
documentCollection.getId(),
|
||||
searchDocuments.size(),
|
||||
preparedDocuments.size(),
|
||||
summarizeDocuments(preparedDocuments)
|
||||
);
|
||||
return preparedDocuments;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -512,6 +598,8 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
.map(image -> image.get("url"))
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
metadataMap.put("chunkId", item.getId());
|
||||
metadataMap.put("documentId", item.getId());
|
||||
metadataMap.put("imageUrls", imageUrls);
|
||||
item.setMetadataMap(metadataMap);
|
||||
});
|
||||
@@ -606,6 +694,17 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
return StringUtil.noText(documentChunk.getContent()) ? null : documentChunk.getContent();
|
||||
}
|
||||
|
||||
private Object findSourceDocumentId(Object chunkId) {
|
||||
DocumentChunk documentChunk = chunkMap.get(String.valueOf(chunkId));
|
||||
if (documentChunk == null || documentChunk.getDocumentId() == null) {
|
||||
return null;
|
||||
}
|
||||
if (!documentMap.containsKey(String.valueOf(documentChunk.getDocumentId()))) {
|
||||
return null;
|
||||
}
|
||||
return documentChunk.getDocumentId();
|
||||
}
|
||||
|
||||
private String findChunkRenderMarkdown(Object chunkId) {
|
||||
DocumentChunk documentChunk = chunkMap.get(String.valueOf(chunkId));
|
||||
if (documentChunk == null || documentChunk.getDocumentId() == null || documentChunk.getOptions() == null) {
|
||||
@@ -740,4 +839,69 @@ public class DocumentCollectionServiceImpl extends ServiceImpl<DocumentCollectio
|
||||
.setScale(4, RoundingMode.HALF_UP)
|
||||
.doubleValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 RAG 原始命中摘要,便于排查向量、关键词与融合阶段的召回情况。
|
||||
*
|
||||
* @param hits RAG 命中列表
|
||||
* @return 命中摘要
|
||||
*/
|
||||
private List<Map<String, Object>> summarizeRagHits(List<RagHit> hits) {
|
||||
if (hits == null || hits.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return hits.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(hit -> {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("id", hit.getDocumentId());
|
||||
summary.put("title", hit.getTitle());
|
||||
summary.put("source", hit.getHitSource());
|
||||
summary.put("score", hit.getScore());
|
||||
summary.put("vectorScore", hit.getVectorScore());
|
||||
summary.put("keywordScore", hit.getKeywordScore());
|
||||
summary.put("rank", hit.getRank());
|
||||
summary.put("content", truncate(hit.getContent()));
|
||||
summary.put("metadata", hit.getMetadata());
|
||||
return summary;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建文档命中摘要,避免完整知识库内容撑爆日志。
|
||||
*
|
||||
* @param documents 文档命中列表
|
||||
* @return 文档摘要
|
||||
*/
|
||||
private List<Map<String, Object>> summarizeDocuments(List<Document> documents) {
|
||||
if (documents == null || documents.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return documents.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(document -> {
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("id", document.getId());
|
||||
summary.put("title", document.getTitle());
|
||||
summary.put("score", document.getScore());
|
||||
summary.put("content", truncate(document.getContent()));
|
||||
summary.put("metadata", document.getMetadataMap());
|
||||
return summary;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 截断日志文本,保留足够排查上下文。
|
||||
*
|
||||
* @param text 原始文本
|
||||
* @return 截断文本
|
||||
*/
|
||||
private String truncate(String text) {
|
||||
if (text == null || text.length() <= LOG_TEXT_MAX_LENGTH) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, LOG_TEXT_MAX_LENGTH) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user