feat: 全新智能体功能

- 基于先进智能体框架,增加智能体编排功能
- 增加智能体聊天,并对接持久化
This commit is contained in:
2026-05-25 11:42:48 +08:00
parent 6c3d98eaac
commit 72df00f25b
168 changed files with 22045 additions and 400 deletions

View File

@@ -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;

View File

@@ -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) + "...";
}
}