@@ -1,5 +1,6 @@
package com.superbiz.agent.tool ;
import com.fasterxml.jackson.databind.ObjectMapper ;
import com.superbiz.agent.domain.entity.ToolInvocation ;
import com.superbiz.agent.dto.* ;
import com.superbiz.agent.repository.ToolInvocationRepository ;
@@ -9,6 +10,7 @@ import com.superbiz.agent.util.SessionContextHolder;
import lombok.extern.slf4j.Slf4j ;
import org.springframework.ai.tool.annotation.Tool ;
import org.springframework.beans.factory.annotation.Autowired ;
import org.springframework.beans.factory.annotation.Value ;
import org.springframework.stereotype.Component ;
import java.util.List ;
@@ -17,11 +19,29 @@ import java.util.stream.Collectors;
/**
* 知识库查询工具
* 提供给 Agent 的混合检索工具(L0 + L1)
* 内置归一化层:将 L0 匹配数 + L1 L2 距离归一化为统一质量等级
*/
@Slf4j
@Component
public class LookupKnowledgeTool {
private static final String LEVEL_PRECISE = " PRECISE " ;
private static final String LEVEL_HIGHLY_RELEVANT = " HIGHLY_RELEVANT " ;
private static final String LEVEL_REFERENCE = " REFERENCE " ;
private static final String HINT_PRECISE = " 知识库中不存在比上述结果更精准的文档 " ;
private static final String HINT_HIGHLY_RELEVANT = " 当前结果已高度相关,继续检索不太可能找到更精准的文档 " ;
private static final String HINT_REFERENCE = " 当前结果为相关参考,如需更精准信息请明确缺少的具体维度 " ;
@Value ( " ${retrieval.normalization.max-l2-distance:2.0} " )
private double maxL2Distance ;
@Value ( " ${retrieval.normalization.highly-relevant-threshold:0.75} " )
private double highlyRelevantThreshold ;
@Value ( " ${retrieval.normalization.reference-threshold:0.5} " )
private double referenceThreshold ;
@Autowired
private KnowledgeIndexService knowledgeIndexService ;
@@ -34,6 +54,9 @@ public class LookupKnowledgeTool {
@Autowired
private RetrievedDocTracker retrievedDocTracker ;
@Autowired
private ObjectMapper objectMapper ;
/**
* 查询知识库文档
*
@@ -50,7 +73,6 @@ public class LookupKnowledgeTool {
" 4) 配置说明 - 查询系统配置、中间件参数,例如 'HikariCP'、'Redis 集群配置'。 " +
" 参数 query: 查询关键词或描述 " )
public LookupResult lookupKnowledge ( String query ) {
// 生成请求ID用于追踪
String requestId = java . util . UUID . randomUUID ( ) . toString ( ) . substring ( 0 , 8 ) ;
long startTime = System . currentTimeMillis ( ) ;
@@ -69,7 +91,7 @@ public class LookupKnowledgeTool {
log . info ( " [L0 精确匹配] 找到文档: " ) ;
for ( int i = 0 ; i < Math . min ( 3 , l0Matches . size ( ) ) ; i + + ) {
KnowledgeEntry entry = l0Matches . get ( i ) ;
log . info ( " - [{}] 标题: {}, 路径: {} " , i + 1 , entry . getTitle ( ) , entry . getFilePath ( ) ) ;
log . info ( " - [{}] 标题: {}, 路径: {}, 域: {} " , i + 1 , entry . getTitle ( ) , entry . getFilePath ( ) , entry . getCategory ( ) ) ;
}
}
@@ -91,90 +113,201 @@ public class LookupKnowledgeTool {
log . info ( " [L1 语义检索] 找到文档: " ) ;
for ( int i = 0 ; i < Math . min ( 3 , l1Results . size ( ) ) ; i + + ) {
VectorSearchService . SearchResult result = l1Results . get ( i ) ;
log . info ( " - [{}] 文档ID: {}, 相似度得分 : {} " , i + 1 , result . getId ( ) , result . getScore ( ) ) ;
log . info ( " - [{}] 文档ID: {}, L2距离 : {} " , i + 1 , result . getId ( ) , String . format ( " %.4f " , result . getScore ( ) ) ) ;
}
}
} else {
log . info ( " [L1 语义检索] L0唯一匹配,跳过L1检索 " ) ;
}
// Step 4: 组装结果
LookupResult result = buildResult ( l0Matches , l1Results , highConfidence ) ;
// Step 4: 归一化质量等级判定
float l1TopScore = ( l1Results ! = null & & ! l1Results . isEmpty ( ) ) ? l1Results . get ( 0 ) . getScore ( ) : Float . MAX_VALUE ;
RelevanceAssessment assessment = computeRelevance ( l0Matches . size ( ) , l1TopScore ) ;
log . info ( " [归一化] relevanceLevel={}, completenessHint={} " , assessment . level , assessment . hint ) ;
if ( l1TopScore ! = Float . MAX_VALUE ) {
double similarity = normalizeL2 ( l1TopScore ) ;
log . info ( " [归一化] L2距离={}, similarity={} " , String . format ( " %.4f " , l1TopScore ) , String . format ( " %.4f " , similarity ) ) ;
}
// Step 5: session 级去重过滤
// Step 5: 组装结果
LookupResult result = buildResult ( l0Matches , l1Results , highConfidence ) ;
result . setRelevanceLevel ( assessment . level ) ;
result . setCompletenessHint ( assessment . hint ) ;
// Step 6: session 级去重过滤 + 域级行动记忆
String sessionId = SessionContextHolder . getSessionId ( ) ;
String domain = extractDomain ( l0Matches , l1Results ) ;
if ( sessionId ! = null & & result . isFound ( ) ) {
String docKey = extractDocKey ( result ) ;
if ( docKey ! = null & & retrievedDocTracker . isAlreadyRetrieved ( sessionId , docKey ) ) {
log . info ( " [去重] 文档已在本会话中检索过,跳过: {} " , docKey ) ;
saveToolInvocation ( query , l0Matches , l1Results , highConfidence , startTime , result ) ;
List < String > retrievedDomains = retrievedDocTracker . getRetrievedDomains ( sessionId ) ;
saveToolInvocation ( query , l0Matches , l1Results , highConfidence , startTime , result , domain , " doc_retrieved " ) ;
return LookupResult . builder ( )
. found ( false )
. message ( " 文档已在本会话中检索过,无需重复召回: " + docKey )
. relevanceLevel ( assessment . level )
. completenessHint ( assessment . hint )
. retrievedDomainsThisSession ( retrievedDomains )
. build ( ) ;
}
if ( docKey ! = null ) {
retrievedDocTracker . markRetrieved ( sessionId , docKey ) ;
retrievedDocTracker . markRetrieved ( sessionId , domain , docKey ) ;
}
}
// 记录结构化结果摘要(替代原始 MD 内容预览)
// 附加行动记忆
if ( sessionId ! = null ) {
result . setRetrievedDomainsThisSession ( retrievedDocTracker . getRetrievedDomains ( sessionId ) ) ;
}
// 记录结构化结果摘要
long totalTime = System . currentTimeMillis ( ) - startTime ;
log . info ( " ---------------------------------------- " ) ;
log . info ( " <<< [工具返回] lookup_knowledge " ) ;
log . info ( " <<< 结果: found={}, 耗时: {}ms (L0={}ms, L1= {}ms) " ,
result . isFound ( ) , totalTime , l0Time ,
l1Results ! = null ? System . currentTimeMillis ( ) - startTime - l0Time : 0 ) ;
log . info ( " <<< 结果: found={}, relevanceLevel={}, 耗时: {}ms " ,
result . isFound ( ) , result . getRelevanceLevel ( ) , totalTime ) ;
log . info ( " <<< 行动记忆: retrievedDomainsThisSession={} " , result . getRetrievedDomainsThisSession ( ) ) ;
// L0 精确匹配摘要
if ( ! l0Matches . isEmpty ( ) ) {
KnowledgeEntry top = l0Matches . get ( 0 ) ;
log . info ( " <<< [L0 主结果] 标题: {} " , top . getTitle ( ) ) ;
log . info ( " <<< [L0 主结果] 来源: {} " , top . getFilePath ( ) ) ;
log . info ( " <<< [L0 主结果] 域: {} " , top . getCategory ( ) ) ;
if ( top . getSummary ( ) ! = null ) {
log . info ( " <<< [L0 主结果] 摘要: {} " , top . getSummary ( ) ) ;
}
if ( top . getKeywords ( ) ! = null & & ! top . getKeywords ( ) . isEmpty ( ) ) {
log . info ( " <<< [L0 主结果] 关键词: {} " , String . join ( " , " , top . getKeywords ( ) ) ) ;
}
// 内容概况:长度 + 章节数
String content = result . getPrimary ( ) ! = null ? result . getPrimary ( ) . getContent ( ) : null ;
if ( content ! = null ) {
int headingCount = countMdHeadings ( content ) ;
log . info ( " <<< [L0 主结果] 内容: {} 字符, {} 个章节 " ,
content . length ( ) , headingCount ) ;
log . info ( " <<< [L0 主结果] 内容: {} 字符, {} 个章节 " , content . length ( ) , headingCount ) ;
}
}
// L1 语义检索摘要
if ( l1Results ! = null & & ! l1Results . isEmpty ( ) ) {
VectorSearchService . SearchResult topL1 = l1Results . get ( 0 ) ;
log . info ( " <<< [L1 补充] 来源: {} " , topL1 . getMetadata ( ) ! = null ? topL1 . getMetadata ( ) : topL1 . getId ( ) ) ;
log . info ( " <<< [L1 补充] 相似度: {} " , String . format ( " %.4f " , topL1 . getScore ( ) ) ) ;
if ( topL1 . getContent ( ) ! = null ) {
String snippet = extractFirstMeaningfulLine ( topL1 . getContent ( ) , 120 ) ;
log . info ( " <<< [L1 补充] 内容片段: {} " , snippet ) ;
log . info ( " <<< [L1 补充] 片段长度: {} 字符 " , topL1 . getContent ( ) . length ( ) ) ;
}
log . info ( " <<< [L1 补充] L2距离: {}, similarity: {} " ,
String . format ( " %.4f " , topL1 . getScore ( ) ) ,
String . format ( " %.4f " , normalizeL2 ( topL1 . getScore ( ) ) ) ) ;
}
log . info ( " ======================================== " ) ;
// 记录 tool_invocation(持久化检索明细)
saveToolInvocation ( query , l0Matches , l1Results , highConfidence , startTime , result ) ;
// 记录 tool_invocation
saveToolInvocation ( query , l0Matches , l1Results , highConfidence , startTime , result , domain , null ) ;
return result ;
}
// ==================== 归一化层 ====================
/**
* L2 距离 Min-Max 归一化到 [0,1] similarity
* BGE-M3 输出 L2 归一化单位向量,L2 距离硬上界 = 2.0
* similarity = 1 - min(score, maxL2Distance) / maxL2Distance
* score=0 → 1.0(完全相同),score=2.0 → 0.0(完全相反)
*/
double normalizeL2 ( float l2Score ) {
double clamped = Math . min ( l2Score , maxL2Distance ) ;
return 1 . 0 - clamped / maxL2Distance ;
}
/**
* 归一化质量等级判定
*
* @param l0MatchCount L0 匹配数
* @param l1TopScore L1 最高分(L2 距离),无 L1 结果时传 Float.MAX_VALUE
* @return RelevanceAssessment( level + hint)
*/
RelevanceAssessment computeRelevance ( int l0MatchCount , float l1TopScore ) {
double l1Similarity = ( l1TopScore ! = Float . MAX_VALUE ) ? normalizeL2 ( l1TopScore ) : 0 . 0 ;
// L0 唯一匹配 → PRECISE
if ( l0MatchCount = = 1 ) {
return new RelevanceAssessment ( LEVEL_PRECISE , HINT_PRECISE ) ;
}
// L0 命中 + L1 高分 → HIGHLY_RELEVANT
if ( l0MatchCount > 1 & & l1Similarity > = highlyRelevantThreshold ) {
return new RelevanceAssessment ( LEVEL_HIGHLY_RELEVANT , HINT_HIGHLY_RELEVANT ) ;
}
// 仅 L1 高分 → HIGHLY_RELEVANT
if ( l0MatchCount = = 0 & & l1Similarity > = highlyRelevantThreshold ) {
return new RelevanceAssessment ( LEVEL_HIGHLY_RELEVANT , HINT_HIGHLY_RELEVANT ) ;
}
// L0 多匹配 + L1 中分 → REFERENCE
if ( l0MatchCount > 1 & & l1Similarity > = referenceThreshold ) {
return new RelevanceAssessment ( LEVEL_REFERENCE , HINT_REFERENCE ) ;
}
// 仅 L1 中分 → REFERENCE
if ( l0MatchCount = = 0 & & l1Similarity > = referenceThreshold ) {
return new RelevanceAssessment ( LEVEL_REFERENCE , HINT_REFERENCE ) ;
}
// L0 多匹配 + 无 L1 / L1 低分 → REFERENCE( L0 命中本身有价值)
if ( l0MatchCount > 1 ) {
return new RelevanceAssessment ( LEVEL_REFERENCE , HINT_REFERENCE ) ;
}
// 无有效结果
return new RelevanceAssessment ( null , null ) ;
}
/**
* 归一化评估结果
*/
record RelevanceAssessment ( String level , String hint ) { }
// ==================== 域提取 ====================
/**
* 从检索结果中提取域信息
* 优先使用 L0 的 category,兜底从 L1 metadata 解析
*/
private String extractDomain ( List < KnowledgeEntry > l0Matches , List < VectorSearchService . SearchResult > l1Results ) {
// 优先 L0
if ( l0Matches ! = null & & ! l0Matches . isEmpty ( ) ) {
String category = l0Matches . get ( 0 ) . getCategory ( ) ;
if ( category ! = null & & ! category . isBlank ( ) ) {
return category ;
}
}
// 兜底 L1:从 metadata JSON 中解析 category
if ( l1Results ! = null & & ! l1Results . isEmpty ( ) ) {
try {
String metadata = l1Results . get ( 0 ) . getMetadata ( ) ;
if ( metadata ! = null & & metadata . contains ( " category " ) ) {
var node = objectMapper . readTree ( metadata ) ;
if ( node . has ( " category " ) ) {
return node . get ( " category " ) . asText ( ) ;
}
}
} catch ( Exception e ) {
log . debug ( " L1 metadata 解析 category 失败: {} " , e . getMessage ( ) ) ;
}
}
return null ;
}
// ==================== 入库 ====================
/**
* 保存工具调用明细到 tool_invocation 表
*/
private void saveToolInvocation ( String query , List < KnowledgeEntry > l0Matches ,
List < VectorSearchService . SearchResult > l1Results ,
boolean highConfidence , long startTime , LookupResult result ) {
boolean highConfidence , long startTime ,
LookupResult result , String domain , String dedupReason ) {
try {
String sessionId = SessionContextHolder . getSessionId ( ) ;
if ( sessionId = = null ) return ; // 非会话上下文不记录
if ( sessionId = = null ) return ;
boolean hasL0 = l0Matches ! = null & & ! l0Matches . isEmpty ( ) ;
boolean hasL1 = l1Results ! = null & & ! l1Results . isEmpty ( ) ;
@@ -201,7 +334,7 @@ public class LookupKnowledgeTool {
layer = null ;
}
// 拼接 output_preview(前500字符)
// output_preview
if ( result ! = null & & result . getPrimary ( ) ! = null & & result . getPrimary ( ) . getContent ( ) ! = null ) {
String content = result . getPrimary ( ) . getContent ( ) ;
outputLength = content . length ( ) ;
@@ -222,24 +355,48 @@ public class LookupKnowledgeTool {
}
}
// 构建检索明细 JSON
// L1 top score + similarity
float l1TopScore = ( hasL1 ) ? l1Results . get ( 0 ) . getScore ( ) : - 1 ;
double l1TopSimilarity = ( hasL1 ) ? normalizeL2 ( l1TopScore ) : - 1 ;
// 构建检索明细 JSON(扩展版)
StringBuilder details = new StringBuilder ( " { " ) ;
if ( hasL0 ) {
details . append ( " \" l0_match_count \" : " ) . append ( l0Count ) . append ( " , " ) ;
details . append ( " \" l0_titles \" :[ " ) ;
for ( int i = 0 ; i < Math . min ( 3 , l0Matches . size ( ) ) ; i + + ) {
if ( i > 0 ) details . append ( " , " ) ;
details . append ( " \" " ) . append ( escapeJson ( l0Matches . get ( i ) . getTitle ( ) ) ) . append ( " \" " ) ;
}
details . append ( " ] " ) ;
details . append ( " ], " ) ;
}
if ( hasL1 ) {
if ( hasL0 ) details . append ( " , " ) ;
details . append ( " \" l1_top_score \" : " ) . append ( String . format ( " %.4f " , l1TopScore ) ) . append ( " , " ) ;
details . append ( " \" l1_top_similarity \" : " ) . append ( String . format ( " %.4f " , l1TopSimilarity ) ) . append ( " , " ) ;
details . append ( " \" l1_match_count \" : " ) . append ( l1Count ) . append ( " , " ) ;
details . append ( " \" l1_scores \" :[ " ) ;
for ( int i = 0 ; i < Math . min ( 3 , l1Results . size ( ) ) ; i + + ) {
if ( i > 0 ) details . append ( " , " ) ;
details . append ( l1Results . get ( i ) . getScore ( ) ) ;
details . append ( String . format ( " %.4f " , l1Results . get ( i ) . getScore ( ) ) ) ;
}
details . append ( " ] " ) ;
details . append ( " ], " ) ;
}
// 归一化信息
if ( result ! = null & & result . getRelevanceLevel ( ) ! = null ) {
details . append ( " \" relevance_level \" : \" " ) . append ( result . getRelevanceLevel ( ) ) . append ( " \" , " ) ;
details . append ( " \" completeness_hint \" : \" " ) . append ( escapeJson ( result . getCompletenessHint ( ) ) ) . append ( " \" , " ) ;
}
// 域信息
if ( domain ! = null ) {
details . append ( " \" retrieved_domains \" :[ \" " ) . append ( escapeJson ( domain ) ) . append ( " \" ], " ) ;
}
// 去重原因
if ( dedupReason ! = null ) {
details . append ( " \" dedup_reason \" : \" " ) . append ( dedupReason ) . append ( " \" , " ) ;
}
// 移除末尾逗号
if ( details . charAt ( details . length ( ) - 1 ) = = ',' ) {
details . setLength ( details . length ( ) - 1 ) ;
}
details . append ( " } " ) ;
@@ -254,12 +411,15 @@ public class LookupKnowledgeTool {
. l1MatchCount ( hasL1 ? l1Count : null )
. isTruncated ( truncated )
. retrievalDetails ( details . toString ( ) )
. relevanceLevel ( result ! = null ? result . getRelevanceLevel ( ) : null )
. dedupReason ( dedupReason )
. durationMs ( ( int ) duration )
. success ( true )
. build ( ) ;
toolInvocationRepository . save ( inv ) ;
log . debug ( " tool_invocation 已保存: sessionId={}, layer={}, duration={}ms " , sessionId , layer , duration ) ;
log . debug ( " tool_invocation 已保存: sessionId={}, layer={}, relevanceLevel={}, duration={}ms " ,
sessionId , layer , result ! = null ? result . getRelevanceLevel ( ) : null , duration ) ;
} catch ( Exception e ) {
log . error ( " 保存 tool_invocation 失败 " , e ) ;
}
@@ -274,14 +434,8 @@ public class LookupKnowledgeTool {
. replace ( " \ t " , " \\ t " ) ;
}
/**
* 组装查询结果
*
* @param l0Matches L0 匹配结果
* @param l1Results L1 检索结果
* @param highConfidence 是否高置信度
* @return 组装后的结果
*/
// ==================== 结果组装 ====================
private LookupResult buildResult (
List < KnowledgeEntry > l0Matches ,
List < VectorSearchService . SearchResult > l1Results ,
@@ -294,8 +448,6 @@ public class LookupKnowledgeTool {
if ( l0Matches ! = null & & ! l0Matches . isEmpty ( ) ) {
KnowledgeEntry first = l0Matches . get ( 0 ) ;
boolean hasL1 = l1Results ! = null & & ! l1Results . isEmpty ( ) ;
// 场景决策:唯一匹配或 L1 无结果 → LLM 需要正文内容;多匹配且有 L1 → 只需元数据
boolean needFullContent = highConfidence | | ! hasL1 ;
String content = needFullContent
? buildCompactSummary ( first )
@@ -307,7 +459,7 @@ public class LookupKnowledgeTool {
. source ( first . getFilePath ( ) )
. matchType ( " exact_L0 " )
. confidence ( highConfidence ? " high " : " low " )
. availableSections ( null ) // MVP 返回 null
. availableSections ( null )
. build ( ) ;
log . debug ( " L0结果已构建: source={}, contentLength={} " , first . getFilePath ( ) , content . length ( ) ) ;
} else {
@@ -330,16 +482,12 @@ public class LookupKnowledgeTool {
}
builder . supplement ( supplement ) ;
// 判断是否找到结果(primary 或 supplement 至少有一个)
boolean found = ( primary ! = null ) | | ( supplement ! = null ) ;
builder . found ( found ) ;
return builder . build ( ) ;
}
/**
* 统计 MD 文档中的章节数(二级标题 ## 数量)
*/
private int countMdHeadings ( String content ) {
if ( content = = null ) return 0 ;
return ( int ) content . lines ( )
@@ -347,15 +495,10 @@ public class LookupKnowledgeTool {
. count ( ) ;
}
/**
* 构建紧凑文档摘要(替代原始 MD 全文,节省上下文窗口)
* 组合:title/summary + 章节结构 + 正文片段(~500 字符)
*/
private String buildCompactSummary ( KnowledgeEntry entry ) {
String rawContent = knowledgeIndexService . readDocument ( entry . getFilePath ( ) , 2000 ) ;
if ( rawContent = = null ) return null ;
// 跳过 YAML frontmatter 得到正文
String body = rawContent ;
if ( body . startsWith ( " --- " ) ) {
int end = body . indexOf ( " --- " , 3 ) ;
@@ -365,14 +508,11 @@ public class LookupKnowledgeTool {
}
StringBuilder sb = new StringBuilder ( ) ;
// 1. 元数据头(始终包含)
sb . append ( " 文档: " ) . append ( entry . getTitle ( ) ) . append ( " \ n " ) ;
if ( entry . getSummary ( ) ! = null ) {
sb . append ( " 摘要: " ) . append ( entry . getSummary ( ) ) . append ( " \ n " ) ;
}
// 2. 章节结构(## 标题列表)
String headings = body . lines ( )
. filter ( l - > l . trim ( ) . startsWith ( " ## " ) )
. map ( l - > " - " + l . trim ( ) . replaceAll ( " ^#+ \\ s* " , " " ) )
@@ -382,13 +522,11 @@ public class LookupKnowledgeTool {
}
sb . append ( " --- \ n " ) ;
// 3. 正文片段(去标题行、去空行,智能截断)
String textContent = body . lines ( )
. filter ( l - > ! l . trim ( ) . startsWith ( " # " ) & & ! l . trim ( ) . isEmpty ( ) )
. collect ( Collectors . joining ( " \ n " ) )
. trim ( ) ;
// 短文档保留更多内容,长文档节省上下文
int maxBodyChars = body . length ( ) < 500 ? 800 : 500 ;
if ( textContent . length ( ) > maxBodyChars ) {
sb . append ( textContent , 0 , maxBodyChars ) . append ( " ... " ) ;
@@ -399,10 +537,6 @@ public class LookupKnowledgeTool {
return sb . toString ( ) ;
}
/**
* 构建纯元数据摘要(不读文件,仅用内存索引信息)
* 多匹配且有 L1 补充时使用,L0 只需告知 LLM 命中了哪些文档
*/
private String buildMetadataOnlySummary ( KnowledgeEntry entry ) {
StringBuilder sb = new StringBuilder ( ) ;
sb . append ( " 文档: " ) . append ( entry . getTitle ( ) ) . append ( " \ n " ) ;
@@ -416,14 +550,10 @@ public class LookupKnowledgeTool {
return sb . toString ( ) ;
}
/**
* 提取 MD 内容中第一个有意义的文本行(跳过 frontmatter 和标题行)
*/
private String extractFirstMeaningfulLine ( String content , int maxLen ) {
if ( content = = null | | content . isBlank ( ) ) return " (空) " ;
String text = content . trim ( ) ;
// 跳过 YAML frontmatter (--- ... ---)
if ( text . startsWith ( " --- " ) ) {
int end = text . indexOf ( " --- " , 3 ) ;
if ( end ! = - 1 ) {
@@ -431,7 +561,6 @@ public class LookupKnowledgeTool {
}
}
// 查找第一个非空、非标题行
String [ ] lines = text . split ( " \ n " ) ;
for ( String line : lines ) {
String tl = line . trim ( ) ;
@@ -440,7 +569,6 @@ public class LookupKnowledgeTool {
}
}
// 兜底:第一行非空行
for ( String line : lines ) {
if ( ! line . trim ( ) . isEmpty ( ) ) {
String tl = line . trim ( ) ;