diff --git a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java index d8718da7..f1da9bb9 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -1376,6 +1376,18 @@ public class AgentGraphBuilder { Use workspace memory tools (MEMORY.md, daily notes) for long-form narrative notes. Use structured memory tools for key-value facts the system can query efficiently. + ## Memory vs Knowledge Base Precedence + When a question is about the user themselves — who they are, their current + project, its name/codename, tech stack, goals, metrics, budget, team, or what + they are working on — your recalled memory (the block plus + structured/workspace memory) is the authoritative source. Knowledge-base / wiki + pages are reference material that may describe unrelated, example, or upstream + projects; do NOT treat a KB page's subject as the user's own project. Only read + the knowledge base for explicit reference lookups, never to decide what the + user's project is. If memory and a KB page disagree about the user's project, + trust memory. If memory has no answer, say you do not have it rather than + adopting a KB article as the user's project. + ## Session Search - `session_search(agentId, currentConversationId, mode, query, limit)` — search conversation history - mode="recent": list recent conversations (titles, times, message counts) diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java index 9070f18d..b92c6999 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/ReasoningNode.java @@ -971,7 +971,17 @@ public class ReasoningNode implements NodeAction { List prefix = new ArrayList<>(); prefix.add(new SystemMessage(systemPrompt)); prefix.add(new UserMessage(RuntimeContextInjector.buildContextMessage(workspaceBasePath, null, chatOrigin))); - if (wikiContextService != null && agentIdStr != null && !agentIdStr.isEmpty()) { + // When this turn already recalled the user's own current project from + // structured memory, skip auto-injecting knowledge-base reference context. + // Otherwise the KB pages (reference material, possibly about unrelated + // projects) compete with — and tend to override — the user's actual + // project identity. The agent can still query the wiki on demand. + boolean projectRecalled = userMsg != null + && userMsg.contains(vip.mate.memory.service.StructuredMemoryService.PROJECT_RECALLED_MARKER); + if (projectRecalled) { + log.debug("[ReasoningNode] Skipping wiki-relevant injection: user's project was recalled from memory this turn"); + } + if (!projectRecalled && wikiContextService != null && agentIdStr != null && !agentIdStr.isEmpty()) { try { Long parsedAgentId = Long.parseLong(agentIdStr); String wikiRelevant = wikiContextService.buildRelevantContext(parsedAgentId, userMsg); diff --git a/mateclaw-server/src/main/java/vip/mate/memory/service/StructuredMemoryService.java b/mateclaw-server/src/main/java/vip/mate/memory/service/StructuredMemoryService.java index 47d79d7d..9b57659f 100644 --- a/mateclaw-server/src/main/java/vip/mate/memory/service/StructuredMemoryService.java +++ b/mateclaw-server/src/main/java/vip/mate/memory/service/StructuredMemoryService.java @@ -54,6 +54,15 @@ public class StructuredMemoryService { /** Maximum number of entries injected by a single query-conditioned prefetch. */ private static final int MAX_PREFETCH_ENTRIES = 6; + /** + * Appended to the prefetch block header when a {@code project}-type entry is + * included, i.e. the user's own current project was recalled for this turn. + * Downstream prompt assembly detects this marker to avoid also injecting + * knowledge-base reference context that would compete for "what project is + * this" — personal project memory is authoritative over reference articles. + */ + public static final String PROJECT_RECALLED_MARKER = "includes the user's current project"; + /** Latin word tokens of length >= 2 used for relevance shingling. */ private static final Pattern WORD_RE = Pattern.compile("[a-z0-9]{2,}"); @@ -249,7 +258,12 @@ public class StructuredMemoryService { List scored = recallRelevant(agentId, userQuery, PREFETCH_TYPES, MAX_PREFETCH_ENTRIES); if (scored.isEmpty()) return ""; - StringBuilder sb = new StringBuilder("## Relevant Structured Memory\n"); + boolean hasProject = scored.stream().anyMatch(e -> "project".equals(e.type())); + StringBuilder sb = new StringBuilder("## Relevant Structured Memory"); + if (hasProject) { + sb.append(" (").append(PROJECT_RECALLED_MARKER).append(")"); + } + sb.append("\n"); for (ScoredEntry e : scored) { sb.append("- **").append(e.key()).append("**: ") .append(extractContentOnly(e.body())); diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java index a46d2a83..4df4700a 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiContextService.java @@ -85,8 +85,12 @@ public class WikiContextService { } StringBuilder sb = new StringBuilder("\n"); - sb.append("[Relevant wiki pages for this query. Use wiki_read_page(slug) for full content. " + - "When using information from these pages in your answer, always cite the source page title, " + + sb.append("[Relevant pages from the shared knowledge base for this query. These are " + + "reference articles and may cover topics unrelated to this user — do NOT assume " + + "they describe the user's own project, identity, or current work. For who the user " + + "is and what they are working on, rely on instead; it takes " + + "precedence over these pages. Use wiki_read_page(slug) for full content. When using " + + "information from these pages in your answer, always cite the source page title, " + "e.g. 「来源:[[页面标题]]」or「(来源:页面标题)」.]\n\n"); int totalChars = 0; int maxChars = properties.getMaxContextChars(); diff --git a/mateclaw-server/src/test/java/vip/mate/memory/service/StructuredMemoryPrefetchTest.java b/mateclaw-server/src/test/java/vip/mate/memory/service/StructuredMemoryPrefetchTest.java index 6fa26239..7992911a 100644 --- a/mateclaw-server/src/test/java/vip/mate/memory/service/StructuredMemoryPrefetchTest.java +++ b/mateclaw-server/src/test/java/vip/mate/memory/service/StructuredMemoryPrefetchTest.java @@ -99,6 +99,36 @@ class StructuredMemoryPrefetchTest { assertTrue(block.contains("updated 2026-05-29"), "recency hint should be present"); } + @Test + @DisplayName("prefetch marks the block when the user's own project is recalled (project type)") + void prefetchMarksProjectRecall() { + StructuredMemoryService svc = newService( + "## project_codename\n用户的项目代号叫\"天枢\"。\n> Source: agent | Updated: 2026-05-29", + null); + + String block = svc.buildPrefetchBlock(AGENT_ID, "我的项目代号是什么?"); + + assertTrue(block.contains(StructuredMemoryService.PROJECT_RECALLED_MARKER), + "a project-type recall should carry the marker so wiki injection can be suppressed"); + } + + @Test + @DisplayName("prefetch does NOT mark the block for reference-only recall") + void prefetchNoMarkerForReferenceOnly() { + // Only a reference-type file is present; the project file is absent. + WorkspaceFileService files = mock(WorkspaceFileService.class); + when(files.getFile(eq(AGENT_ID), anyString())).thenReturn(null); + WorkspaceFileEntity ref = new WorkspaceFileEntity(); + ref.setContent("## api_endpoint\n参考:订单查询接口 /api/orders。\n> Source: agent | Updated: 2026-05-29"); + when(files.getFile(AGENT_ID, "structured/reference.md")).thenReturn(ref); + StructuredMemoryService svc = new StructuredMemoryService(files, mock(ApplicationEventPublisher.class)); + + String block = svc.buildPrefetchBlock(AGENT_ID, "订单查询接口参考是什么?"); + + assertFalse(block.contains(StructuredMemoryService.PROJECT_RECALLED_MARKER), + "reference-only recall must not claim a project so wiki context stays available"); + } + @Test @DisplayName("prefetch returns empty for an unrelated question") void prefetchEmptyForUnrelatedQuery() {