feat(wiki): instrument compile / relation / retrieval with WikiMetrics

This commit is contained in:
matevip 2026-05-02 19:04:19 +08:00
parent 4f96a46900
commit 6f0ead162e
3 changed files with 39 additions and 3 deletions

View File

@ -8,11 +8,13 @@ import vip.mate.wiki.WikiProperties;
import vip.mate.wiki.dto.PageSearchResult; import vip.mate.wiki.dto.PageSearchResult;
import vip.mate.wiki.dto.RelatedPageResult; import vip.mate.wiki.dto.RelatedPageResult;
import vip.mate.wiki.dto.WikiPageLite; import vip.mate.wiki.dto.WikiPageLite;
import vip.mate.wiki.metrics.WikiMetrics;
import vip.mate.wiki.model.WikiChunkEntity; import vip.mate.wiki.model.WikiChunkEntity;
import vip.mate.wiki.model.WikiPageEntity; import vip.mate.wiki.model.WikiPageEntity;
import vip.mate.wiki.repository.WikiPageMapper; import vip.mate.wiki.repository.WikiPageMapper;
import vip.mate.wiki.retrieval.SnippetExtractor; import vip.mate.wiki.retrieval.SnippetExtractor;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -32,6 +34,7 @@ public class HybridRetriever {
private final WikiEmbeddingService embeddingService; private final WikiEmbeddingService embeddingService;
private final WikiProperties properties; private final WikiProperties properties;
private final WikiPageMapper pageMapper; private final WikiPageMapper pageMapper;
private final WikiMetrics metrics;
@Autowired(required = false) @Autowired(required = false)
private WikiRelationService relationService; private WikiRelationService relationService;
@ -42,12 +45,14 @@ public class HybridRetriever {
WikiChunkService chunkService, WikiChunkService chunkService,
WikiEmbeddingService embeddingService, WikiEmbeddingService embeddingService,
WikiProperties properties, WikiProperties properties,
WikiPageMapper pageMapper) { WikiPageMapper pageMapper,
WikiMetrics metrics) {
this.pageService = pageService; this.pageService = pageService;
this.chunkService = chunkService; this.chunkService = chunkService;
this.embeddingService = embeddingService; this.embeddingService = embeddingService;
this.properties = properties; this.properties = properties;
this.pageMapper = pageMapper; this.pageMapper = pageMapper;
this.metrics = metrics;
} }
public enum Mode { KEYWORD, SEMANTIC, HYBRID } public enum Mode { KEYWORD, SEMANTIC, HYBRID }
@ -79,6 +84,8 @@ public class HybridRetriever {
public List<PageSearchResult> search(Long kbId, String query, String modeStr, int topK) { public List<PageSearchResult> search(Long kbId, String query, String modeStr, int topK) {
Mode mode = parseMode(modeStr); Mode mode = parseMode(modeStr);
long startNanos = System.nanoTime();
List<RankedItem> semantic = List.of(); List<RankedItem> semantic = List.of();
List<RankedItem> keyword = List.of(); List<RankedItem> keyword = List.of();
@ -107,7 +114,11 @@ public class HybridRetriever {
// Batch-fetch page info (N+1 fix) // Batch-fetch page info (N+1 fix)
List<Long> topIds = fused.stream().limit(topK).map(ri -> ri.pageId).toList(); List<Long> topIds = fused.stream().limit(topK).map(ri -> ri.pageId).toList();
if (topIds.isEmpty()) return List.of(); if (topIds.isEmpty()) {
metrics.recordRetrieval(mode.name().toLowerCase(),
Duration.ofNanos(System.nanoTime() - startNanos), 0);
return List.of();
}
Map<Long, WikiPageLite> liteMap = pageMapper.selectBatchLite(topIds) Map<Long, WikiPageLite> liteMap = pageMapper.selectBatchLite(topIds)
.stream().collect(Collectors.toMap(WikiPageLite::id, p -> p)); .stream().collect(Collectors.toMap(WikiPageLite::id, p -> p));
@ -142,6 +153,8 @@ public class HybridRetriever {
snippet != null ? snippet : lite.summary(), snippet != null ? snippet : lite.summary(),
ri.matchedBy, reason, ri.score)); ri.matchedBy, reason, ri.score));
} }
metrics.recordRetrieval(mode.name().toLowerCase(),
Duration.ofNanos(System.nanoTime() - startNanos), results.size());
return results; return results;
} }

View File

@ -14,10 +14,12 @@ import org.springframework.stereotype.Service;
import vip.mate.agent.prompt.PromptLoader; import vip.mate.agent.prompt.PromptLoader;
import vip.mate.wiki.job.WikiJobStep; import vip.mate.wiki.job.WikiJobStep;
import vip.mate.wiki.job.WikiModelRoutingService; import vip.mate.wiki.job.WikiModelRoutingService;
import vip.mate.wiki.metrics.WikiMetrics;
import vip.mate.wiki.model.WikiChunkEntity; import vip.mate.wiki.model.WikiChunkEntity;
import vip.mate.wiki.model.WikiPageEntity; import vip.mate.wiki.model.WikiPageEntity;
import vip.mate.wiki.repository.WikiChunkMapper; import vip.mate.wiki.repository.WikiChunkMapper;
import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -43,6 +45,7 @@ public class WikiCompileService {
private final WikiPageService pageService; private final WikiPageService pageService;
private final WikiCitationService citationService; private final WikiCitationService citationService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final WikiMetrics metrics;
/** /**
* Optional. When wired we use the routing chain (stepModels[CREATE_PAGE] * Optional. When wired we use the routing chain (stepModels[CREATE_PAGE]
@ -91,12 +94,16 @@ public class WikiCompileService {
int cap = (maxEvidenceChunks == null || maxEvidenceChunks <= 0) int cap = (maxEvidenceChunks == null || maxEvidenceChunks <= 0)
? 8 : Math.min(20, maxEvidenceChunks); ? 8 : Math.min(20, maxEvidenceChunks);
long startNanos = System.nanoTime();
// 1. Retrieve evidence chunks via semantic search (hybrid retriever). // 1. Retrieve evidence chunks via semantic search (hybrid retriever).
List<HybridRetriever.ChunkHit> hits = hybridRetriever.searchChunks(kbId, topic, cap); List<HybridRetriever.ChunkHit> hits = hybridRetriever.searchChunks(kbId, topic, cap);
if (hits.isEmpty()) { if (hits.isEmpty()) {
// Structured "nothing matched" result rather than throw lets the // Structured "nothing matched" result rather than throw lets the
// tool surface respond with a clean message instead of a stack trace. // tool surface respond with a clean message instead of a stack trace.
log.info("[WikiCompile] No evidence chunks for topic='{}' kbId={}", topic, kbId); log.info("[WikiCompile] No evidence chunks for topic='{}' kbId={}", topic, kbId);
metrics.recordCompileStage("no_evidence", kbId,
Duration.ofNanos(System.nanoTime() - startNanos));
return CompileResult.noEvidence(); return CompileResult.noEvidence();
} }
@ -186,6 +193,8 @@ public class WikiCompileService {
} }
if (overviewService != null) overviewService.rebuild(kbId); if (overviewService != null) overviewService.rebuild(kbId);
metrics.recordCompileStage("compile_page", kbId,
Duration.ofNanos(System.nanoTime() - startNanos));
return new CompileResult(persisted.getId(), resolvedSlug, title, evidenceChunkIds.size(), created); return new CompileResult(persisted.getId(), resolvedSlug, title, evidenceChunkIds.size(), created);
} }

View File

@ -3,11 +3,13 @@ package vip.mate.wiki.service;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import vip.mate.wiki.dto.*; import vip.mate.wiki.dto.*;
import vip.mate.wiki.metrics.WikiMetrics;
import vip.mate.wiki.model.WikiPageEntity; import vip.mate.wiki.model.WikiPageEntity;
import vip.mate.wiki.relation.RelationSignalStrategy; import vip.mate.wiki.relation.RelationSignalStrategy;
import vip.mate.wiki.repository.WikiPageCitationMapper; import vip.mate.wiki.repository.WikiPageCitationMapper;
import vip.mate.wiki.repository.WikiPageMapper; import vip.mate.wiki.repository.WikiPageMapper;
import java.time.Duration;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -23,15 +25,18 @@ public class WikiRelationService {
private final WikiPageMapper pageMapper; private final WikiPageMapper pageMapper;
private final WikiPageService pageService; private final WikiPageService pageService;
private final WikiPageCitationMapper citationMapper; private final WikiPageCitationMapper citationMapper;
private final WikiMetrics metrics;
public WikiRelationService(List<RelationSignalStrategy> signals, public WikiRelationService(List<RelationSignalStrategy> signals,
WikiPageMapper pageMapper, WikiPageMapper pageMapper,
WikiPageService pageService, WikiPageService pageService,
WikiPageCitationMapper citationMapper) { WikiPageCitationMapper citationMapper,
WikiMetrics metrics) {
this.signals = signals; this.signals = signals;
this.pageMapper = pageMapper; this.pageMapper = pageMapper;
this.pageService = pageService; this.pageService = pageService;
this.citationMapper = citationMapper; this.citationMapper = citationMapper;
this.metrics = metrics;
} }
/** /**
@ -41,6 +46,8 @@ public class WikiRelationService {
WikiPageEntity seed = pageService.getBySlug(kbId, seedSlug); WikiPageEntity seed = pageService.getBySlug(kbId, seedSlug);
if (seed == null) return List.of(); if (seed == null) return List.of();
long startNanos = System.nanoTime();
Map<Long, Double> totalScores = new HashMap<>(); Map<Long, Double> totalScores = new HashMap<>();
Map<Long, List<String>> signalHits = new HashMap<>(); Map<Long, List<String>> signalHits = new HashMap<>();
@ -62,6 +69,13 @@ public class WikiRelationService {
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.toList(); .toList();
// Reports both compute duration and the candidate-set size.
// Cache hit/miss is reported once a relation cache layer is wired up;
// until then every call is a cold compute by definition.
metrics.recordRelationCompute(kbId, totalScores.size(),
Duration.ofNanos(System.nanoTime() - startNanos));
metrics.recordRelationCacheHit(false);
if (topIds.isEmpty()) return List.of(); if (topIds.isEmpty()) return List.of();
Map<Long, WikiPageLite> liteMap = pageMapper.selectBatchLite(topIds) Map<Long, WikiPageLite> liteMap = pageMapper.selectBatchLite(topIds)
.stream().collect(Collectors.toMap(WikiPageLite::id, l -> l)); .stream().collect(Collectors.toMap(WikiPageLite::id, l -> l));