fix(memory): prefer recalled personal memory over knowledge base for user/project questions

This commit is contained in:
matevip 2026-05-29 18:03:13 +08:00
parent d4ea75a806
commit 7e9f2ee54c
5 changed files with 74 additions and 4 deletions

View File

@ -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 <memory-context> 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)

View File

@ -971,7 +971,17 @@ public class ReasoningNode implements NodeAction {
List<Message> 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);

View File

@ -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<ScoredEntry> 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()));

View File

@ -85,8 +85,12 @@ public class WikiContextService {
}
StringBuilder sb = new StringBuilder("<wiki-relevant>\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 <memory-context> 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();

View File

@ -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() {