mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(wiki): budget the system-prompt KB page listing to the model window
buildWikiContext enumerated an agent's bound knowledge-base pages into the system prompt capped only by maxContextChars (default 10000, sized for large cloud models). On a small-context model a large KB therefore consumed a big fixed slice of the window on every turn — the "tool token estimate fills the context" report in #521 (the growth lands in the system-prompt bucket, not the tool-schema bucket; wiki tool schemas are fixed-size and do not scale with file count). Add a budgeted buildWikiContext(agentId, budgetTokens) overload mirroring buildRelevantContext: the page enumeration also stops once the estimated token total exceeds the budget, appending the existing '... and more (use wiki_list_pages)' hint. AgentGraphBuilder passes the same prefix budget it already applies to the memory block; the legacy Integer.MAX_VALUE path keeps chars-only behavior for large models. Tests cover null-budget (all pages), token-budget truncation, and zero-budget skip.
This commit is contained in:
parent
04a9bb9e13
commit
fc3d84d6c2
@ -1743,8 +1743,13 @@ public class AgentGraphBuilder {
|
||||
""";
|
||||
}
|
||||
|
||||
// Wiki 知识库上下文注入
|
||||
String wikiContext = wikiContextService.buildWikiContext(entity.getId());
|
||||
// Wiki 知识库上下文注入。Share the same prefix budget as the memory
|
||||
// block so a large KB's page listing can't consume a fixed
|
||||
// maxContextChars-sized slice of a small model's window every turn
|
||||
// (issue #521). Integer.MAX_VALUE (the unbudgeted default path) keeps
|
||||
// the legacy chars-only cap for large cloud models.
|
||||
Integer wikiBudgetTokens = memoryBudgetTokens == Integer.MAX_VALUE ? null : memoryBudgetTokens;
|
||||
String wikiContext = wikiContextService.buildWikiContext(entity.getId(), wikiBudgetTokens);
|
||||
|
||||
return basePrompt + ABOUT_YOU_BLOCK + toolGuidance + searchGuidance + wikiContext;
|
||||
}
|
||||
|
||||
@ -151,9 +151,24 @@ public class WikiContextService {
|
||||
* Build full wiki context for agent system prompt.
|
||||
*/
|
||||
public String buildWikiContext(Long agentId) {
|
||||
return buildWikiContext(agentId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Budgeted variant of the system-prompt wiki listing. In addition to the
|
||||
* absolute {@code maxContextChars} cap (sized for large cloud models), the
|
||||
* enumerated page list may not exceed {@code budgetTokens} (estimated), so
|
||||
* a large KB cannot consume a fixed ~{@code maxContextChars}-sized slice of
|
||||
* a small local model's context window on every turn. A null budget keeps
|
||||
* the previous chars-only behavior; a non-positive budget skips injection.
|
||||
*/
|
||||
public String buildWikiContext(Long agentId, Integer budgetTokens) {
|
||||
if (!properties.isEnabled()) {
|
||||
return "";
|
||||
}
|
||||
if (budgetTokens != null && budgetTokens <= 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<WikiKnowledgeBaseEntity> kbs = kbService.listByAgentId(agentId);
|
||||
if (kbs.isEmpty()) {
|
||||
@ -166,6 +181,9 @@ public class WikiContextService {
|
||||
|
||||
int totalChars = 0;
|
||||
int maxChars = properties.getMaxContextChars();
|
||||
// Running estimate of what has been appended, so the page enumeration
|
||||
// (the part that scales with KB file count) can respect budgetTokens.
|
||||
int totalTokens = TokenEstimator.estimateTokens(sb.toString());
|
||||
|
||||
// Each KB renders as a HEADING-ONLY block (### <name>) followed by a
|
||||
// metadata line and its page list. The heading deliberately contains
|
||||
@ -180,16 +198,20 @@ public class WikiContextService {
|
||||
List<WikiPageEntity> pages = pageService.listSummaries(kb.getId());
|
||||
if (pages.isEmpty()) continue;
|
||||
|
||||
// Heading: pure KB name. This is what `kbName` expects verbatim.
|
||||
sb.append("### ").append(kb.getName()).append("\n");
|
||||
// Heading: pure KB name (what `kbName` expects verbatim) plus a
|
||||
// metadata line — built as one string so its tokens are budgeted too.
|
||||
StringBuilder heading = new StringBuilder();
|
||||
heading.append("### ").append(kb.getName()).append("\n");
|
||||
// Metadata line: page count first (easy to scan), then optional
|
||||
// description. Lives on its own line so it can't be confused for
|
||||
// part of the name.
|
||||
sb.append(pages.size()).append(" pages");
|
||||
heading.append(pages.size()).append(" pages");
|
||||
if (kb.getDescription() != null && !kb.getDescription().isBlank()) {
|
||||
sb.append(" — ").append(kb.getDescription());
|
||||
heading.append(" — ").append(kb.getDescription());
|
||||
}
|
||||
sb.append("\n\n");
|
||||
heading.append("\n\n");
|
||||
sb.append(heading);
|
||||
totalTokens += TokenEstimator.estimateTokens(heading.toString());
|
||||
|
||||
boolean compact = pages.size() > 20;
|
||||
|
||||
@ -204,12 +226,15 @@ public class WikiContextService {
|
||||
}
|
||||
line += "\n";
|
||||
}
|
||||
if (totalChars + line.length() > maxChars) {
|
||||
int lineTokens = TokenEstimator.estimateTokens(line);
|
||||
if (totalChars + line.length() > maxChars
|
||||
|| (budgetTokens != null && totalTokens + lineTokens > budgetTokens)) {
|
||||
sb.append("- ... and more (use wiki_list_pages to see all)\n");
|
||||
break;
|
||||
}
|
||||
sb.append(line);
|
||||
totalChars += line.length();
|
||||
totalTokens += lineTokens;
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import org.mockito.Mockito;
|
||||
import vip.mate.wiki.WikiProperties;
|
||||
import vip.mate.wiki.dto.PageSearchResult;
|
||||
import vip.mate.wiki.model.WikiKnowledgeBaseEntity;
|
||||
import vip.mate.wiki.model.WikiPageEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -17,12 +18,15 @@ import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
|
||||
/**
|
||||
* Tests for the token-budgeted knowledge-base relevance injection in
|
||||
* {@link WikiContextService}.
|
||||
* Tests for the token-budgeted knowledge-base injection in
|
||||
* {@link WikiContextService} — both the per-query relevance block
|
||||
* ({@code buildRelevantContext}) and the system-prompt page listing
|
||||
* ({@code buildWikiContext}).
|
||||
*/
|
||||
class WikiContextServiceBudgetTest {
|
||||
|
||||
private WikiKnowledgeBaseService kbService;
|
||||
private WikiPageService pageService;
|
||||
private HybridRetriever hybridRetriever;
|
||||
private WikiProperties properties;
|
||||
private WikiContextService service;
|
||||
@ -31,7 +35,7 @@ class WikiContextServiceBudgetTest {
|
||||
void setUp() {
|
||||
kbService = Mockito.mock(WikiKnowledgeBaseService.class);
|
||||
hybridRetriever = Mockito.mock(HybridRetriever.class);
|
||||
WikiPageService pageService = Mockito.mock(WikiPageService.class);
|
||||
pageService = Mockito.mock(WikiPageService.class);
|
||||
properties = new WikiProperties();
|
||||
service = new WikiContextService(kbService, pageService, hybridRetriever, properties);
|
||||
|
||||
@ -84,4 +88,53 @@ class WikiContextServiceBudgetTest {
|
||||
assertEquals("", result);
|
||||
Mockito.verifyNoInteractions(hybridRetriever);
|
||||
}
|
||||
|
||||
// ---- buildWikiContext (system-prompt page listing) budget (issue #521) ----
|
||||
|
||||
private void stubKbWithPages(int pageCount) {
|
||||
WikiKnowledgeBaseEntity kb = new WikiKnowledgeBaseEntity();
|
||||
kb.setId(7L);
|
||||
kb.setName("产品知识库");
|
||||
Mockito.when(kbService.listByAgentId(anyLong())).thenReturn(List.of(kb));
|
||||
|
||||
List<WikiPageEntity> pages = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
WikiPageEntity p = new WikiPageEntity();
|
||||
p.setSlug("page-" + i);
|
||||
p.setTitle("知识库页面标题" + i);
|
||||
pages.add(p);
|
||||
}
|
||||
Mockito.when(pageService.listSummaries(anyLong())).thenReturn(pages);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("null budget lists all pages (chars-only legacy behavior)")
|
||||
void wikiContextNullBudgetListsAll() {
|
||||
stubKbWithPages(50);
|
||||
String result = service.buildWikiContext(1L, null);
|
||||
assertTrue(result.contains("page-0"));
|
||||
assertTrue(result.contains("page-49"));
|
||||
assertFalse(result.contains("... and more"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("token budget truncates the page listing and appends the search hint")
|
||||
void wikiContextBudgetTruncates() {
|
||||
stubKbWithPages(50);
|
||||
// Each compact line ("- page-N: 知识库页面标题N\n") is ~10+ CJK tokens; a
|
||||
// small budget must cut the listing well before all 50 pages.
|
||||
String result = service.buildWikiContext(1L, 80);
|
||||
assertTrue(result.contains("page-0"));
|
||||
assertFalse(result.contains("page-49"));
|
||||
assertTrue(result.contains("... and more (use wiki_list_pages to see all)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("zero or negative budget skips the wiki block entirely")
|
||||
void wikiContextZeroBudgetSkips() {
|
||||
String result = service.buildWikiContext(1L, 0);
|
||||
assertEquals("", result);
|
||||
Mockito.verifyNoInteractions(kbService);
|
||||
Mockito.verifyNoInteractions(pageService);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user