fix(wiki): resolve an agent's own knowledge base before shared ones

This commit is contained in:
matevip 2026-05-19 16:53:12 +08:00
parent b0d9bde664
commit c6d51f8ced
5 changed files with 99 additions and 6 deletions

View File

@ -61,12 +61,12 @@ public class WikiContextService {
return "";
}
List<WikiKnowledgeBaseEntity> kbs = kbService.listByAgentId(agentId);
if (kbs.isEmpty()) {
WikiKnowledgeBaseEntity primaryKb = kbService.resolvePrimaryKb(agentId);
if (primaryKb == null) {
return "";
}
Long kbId = kbs.get(0).getId();
Long kbId = primaryKb.getId();
List<PageSearchResult> hits = hybridRetriever.search(kbId, userMessage, "hybrid", 5);
if (hits.isEmpty()) {
return "";

View File

@ -112,6 +112,32 @@ public class WikiKnowledgeBaseService {
.orderByDesc(WikiKnowledgeBaseEntity::getUpdateTime));
}
/**
* Resolve the single knowledge base an agent's wiki tools should operate on.
* <p>
* Prefers a KB explicitly bound to the agent; a shared (agent-less) KB is
* only used as a fallback when the agent has no bound KB of its own. This
* matters because {@link #listByAgentId} also returns shared KBs, and a
* shared KB with a more recent {@code update_time} would otherwise win the
* {@code get(0)} pick over the agent's own KB. Within each tier the most
* recently updated KB wins. Returns {@code null} when the agent can reach
* no knowledge base at all.
*/
public WikiKnowledgeBaseEntity resolvePrimaryKb(Long agentId) {
List<WikiKnowledgeBaseEntity> kbs = listByAgentId(agentId);
if (kbs.isEmpty()) {
return null;
}
if (agentId != null) {
for (WikiKnowledgeBaseEntity kb : kbs) {
if (agentId.equals(kb.getAgentId())) {
return kb;
}
}
}
return kbs.get(0);
}
public WikiKnowledgeBaseEntity getById(Long id) {
return kbMapper.selectById(id);
}

View File

@ -817,8 +817,8 @@ public class WikiTool {
// ==================== Helpers ====================
private Long resolveKbId(Long agentId) {
List<WikiKnowledgeBaseEntity> kbs = kbService.listByAgentId(agentId);
return kbs.isEmpty() ? null : kbs.get(0).getId();
WikiKnowledgeBaseEntity kb = kbService.resolvePrimaryKb(agentId);
return kb == null ? null : kb.getId();
}
private JSONArray resolveSourceFiles(String sourceRawIdsJson) {

View File

@ -48,7 +48,7 @@ class WikiContextServiceTest {
WikiKnowledgeBaseEntity kb = new WikiKnowledgeBaseEntity();
kb.setId(42L);
when(kbService.listByAgentId(any())).thenReturn(List.of(kb));
when(kbService.resolvePrimaryKb(any())).thenReturn(kb);
service = new WikiContextService(kbService, pageService, hybridRetriever, properties);
}

View File

@ -0,0 +1,67 @@
package vip.mate.wiki.service;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
import vip.mate.wiki.repository.WikiKnowledgeBaseMapper;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link WikiKnowledgeBaseService#resolvePrimaryKb(Long)}.
*
* <p>{@code listByAgentId} returns both the agent's own KBs and shared
* (agent-less) KBs, ordered by {@code update_time} descending. A naive
* {@code get(0)} pick therefore hands back whichever KB was touched most
* recently which can be an unrelated shared KB. {@code resolvePrimaryKb}
* must still return the KB actually bound to the agent.
*/
class WikiKnowledgeBaseServiceTest {
private final WikiKnowledgeBaseMapper kbMapper = mock(WikiKnowledgeBaseMapper.class);
private final WikiKnowledgeBaseService service = new WikiKnowledgeBaseService(
kbMapper, null, null, null, null, null);
private static WikiKnowledgeBaseEntity kb(long id, Long agentId) {
WikiKnowledgeBaseEntity entity = new WikiKnowledgeBaseEntity();
entity.setId(id);
entity.setAgentId(agentId);
return entity;
}
@Test
@DisplayName("prefers the agent's bound KB even when a shared KB was updated more recently")
void prefersBoundKbOverNewerSharedKb() {
// listByAgentId order is update_time DESC: two shared KBs precede the bound one.
when(kbMapper.selectList(any())).thenReturn(List.of(
kb(900L, null),
kb(800L, null),
kb(100L, 7L)));
assertThat(service.resolvePrimaryKb(7L)).isNotNull();
assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(100L);
}
@Test
@DisplayName("falls back to the most recent shared KB when the agent has no bound KB")
void fallsBackToSharedKbWhenNoneBound() {
when(kbMapper.selectList(any())).thenReturn(List.of(
kb(900L, null),
kb(800L, null)));
assertThat(service.resolvePrimaryKb(7L).getId()).isEqualTo(900L);
}
@Test
@DisplayName("returns null when the agent can reach no knowledge base")
void returnsNullWhenNoKb() {
when(kbMapper.selectList(any())).thenReturn(List.of());
assertThat(service.resolvePrimaryKb(7L)).isNull();
}
}