mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(memory): P1 review fixes — close 4 semantic gaps in data truth layer
This commit is contained in:
parent
13a3394ffd
commit
84c8f8f9a0
@ -101,14 +101,14 @@ public class AgentService {
|
||||
memoryRecallTracker.trackRecalls(agentId, message);
|
||||
BaseAgent agent = getOrBuildAgent(agentId);
|
||||
return withLifecycleSync(agentId, message, conversationId,
|
||||
() -> agent.chat(message, conversationId));
|
||||
(msg, convId) -> agent.chat(msg, convId));
|
||||
}
|
||||
|
||||
public Flux<String> chatStream(Long agentId, String message, String conversationId) {
|
||||
memoryRecallTracker.trackRecalls(agentId, message);
|
||||
BaseAgent agent = getOrBuildAgent(agentId);
|
||||
return withLifecycleFlux(agentId, message, conversationId,
|
||||
() -> agent.chatStream(message, conversationId),
|
||||
(msg, convId) -> agent.chatStream(msg, convId),
|
||||
chunk -> chunk);
|
||||
}
|
||||
|
||||
@ -141,7 +141,7 @@ public class AgentService {
|
||||
|
||||
if (agent instanceof StructuredStreamCapable capable) {
|
||||
return withLifecycleFlux(agentId, message, conversationId,
|
||||
() -> capable.chatStructuredStream(message, conversationId,
|
||||
(msg, convId) -> capable.chatStructuredStream(msg, convId,
|
||||
requesterId != null ? requesterId : "")
|
||||
.doFinally(signal -> ThinkingLevelHolder.clear()),
|
||||
StreamDelta::content);
|
||||
@ -150,7 +150,7 @@ public class AgentService {
|
||||
// 降级:不支持结构化流的 Agent,包装为纯内容流
|
||||
ThinkingLevelHolder.clear();
|
||||
return withLifecycleFlux(agentId, message, conversationId,
|
||||
() -> agent.chatStream(message, conversationId)
|
||||
(msg, convId) -> agent.chatStream(msg, convId)
|
||||
.map(chunk -> new StreamDelta(chunk, null)),
|
||||
StreamDelta::content);
|
||||
}
|
||||
@ -159,7 +159,7 @@ public class AgentService {
|
||||
memoryRecallTracker.trackRecalls(agentId, goal);
|
||||
BaseAgent agent = getOrBuildAgent(agentId);
|
||||
return withLifecycleSync(agentId, goal, conversationId,
|
||||
() -> agent.execute(goal, conversationId));
|
||||
(msg, convId) -> agent.execute(msg, convId));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -176,7 +176,7 @@ public class AgentService {
|
||||
memoryRecallTracker.trackRecalls(agentId, userMessage);
|
||||
BaseAgent agent = getOrBuildAgent(agentId);
|
||||
return withLifecycleSync(agentId, userMessage, conversationId,
|
||||
() -> agent.chatWithReplay(userMessage, conversationId, toolCallPayload));
|
||||
(msg, convId) -> agent.chatWithReplay(msg, convId, toolCallPayload));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -192,7 +192,7 @@ public class AgentService {
|
||||
memoryRecallTracker.trackRecalls(agentId, userMessage);
|
||||
BaseAgent agent = getOrBuildAgent(agentId);
|
||||
return withLifecycleFlux(agentId, userMessage, conversationId,
|
||||
() -> agent.chatWithReplayStream(userMessage, conversationId, toolCallPayload,
|
||||
(msg, convId) -> agent.chatWithReplayStream(msg, convId, toolCallPayload,
|
||||
requesterId != null ? requesterId : ""),
|
||||
StreamDelta::content);
|
||||
}
|
||||
@ -231,15 +231,20 @@ public class AgentService {
|
||||
/**
|
||||
* Wraps a synchronous agent call with lifecycle mediator hooks.
|
||||
* When lifecycleMediatorEnabled is off, runs plainInvoke directly (Phase 0 behavior).
|
||||
*
|
||||
* P1-1 fix: prefetchAll result is now prepended to userMessage as <memory-context> block.
|
||||
* P1-4 fix: N/A for sync (no cancel/error signal issue).
|
||||
*/
|
||||
private String withLifecycleSync(Long agentId, String message, String conversationId,
|
||||
Supplier<String> plainInvoke) {
|
||||
java.util.function.BiFunction<String, String, String> invoke) {
|
||||
if (!memoryProperties.isLifecycleMediatorEnabled()) {
|
||||
return plainInvoke.get();
|
||||
return invoke.apply(message, conversationId);
|
||||
}
|
||||
TurnContext ctx = new TurnContext(agentId, conversationId, conversationId, 0, message);
|
||||
lifecycleMediator.beforeLlmCall(ctx);
|
||||
String result = plainInvoke.get();
|
||||
String memoryContext = lifecycleMediator.beforeLlmCall(ctx);
|
||||
// Inject memory context into the user message (RFC-037 §3.3)
|
||||
String enrichedMessage = injectMemoryContext(message, memoryContext);
|
||||
String result = invoke.apply(enrichedMessage, conversationId);
|
||||
lifecycleMediator.afterLlmCall(ctx, result != null ? result : "");
|
||||
return result;
|
||||
}
|
||||
@ -247,24 +252,38 @@ public class AgentService {
|
||||
/**
|
||||
* Wraps a streaming agent call with lifecycle mediator hooks.
|
||||
* When lifecycleMediatorEnabled is off, runs plainInvoke directly (Phase 0 behavior).
|
||||
*
|
||||
* P1-1 fix: prefetchAll result is now prepended to userMessage.
|
||||
* P1-4 fix: afterLlmCall only fires on COMPLETE signal, not on cancel/error.
|
||||
*/
|
||||
private <T> Flux<T> withLifecycleFlux(Long agentId, String message, String conversationId,
|
||||
Supplier<Flux<T>> plainInvoke,
|
||||
java.util.function.BiFunction<String, String, Flux<T>> invoke,
|
||||
Function<T, String> contentExtractor) {
|
||||
if (!memoryProperties.isLifecycleMediatorEnabled()) {
|
||||
return plainInvoke.get();
|
||||
return invoke.apply(message, conversationId);
|
||||
}
|
||||
TurnContext ctx = new TurnContext(agentId, conversationId, conversationId, 0, message);
|
||||
lifecycleMediator.beforeLlmCall(ctx);
|
||||
String memoryContext = lifecycleMediator.beforeLlmCall(ctx);
|
||||
String enrichedMessage = injectMemoryContext(message, memoryContext);
|
||||
StringBuilder reply = new StringBuilder();
|
||||
return plainInvoke.get()
|
||||
return invoke.apply(enrichedMessage, conversationId)
|
||||
.doOnNext(item -> {
|
||||
String text = contentExtractor.apply(item);
|
||||
if (text != null) {
|
||||
reply.append(text);
|
||||
}
|
||||
})
|
||||
.doFinally(signal -> lifecycleMediator.afterLlmCall(ctx, reply.toString()));
|
||||
.doOnComplete(() -> lifecycleMediator.afterLlmCall(ctx, reply.toString()))
|
||||
.doOnError(e -> log.debug("[Memory] Stream error, skipping afterLlmCall: {}", e.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepend memory-context block to user message if non-empty.
|
||||
* Does not pollute build-time system prompt snapshot.
|
||||
*/
|
||||
private String injectMemoryContext(String message, String memoryContext) {
|
||||
if (memoryContext == null || memoryContext.isBlank()) return message;
|
||||
return memoryContext + "\n\n" + message;
|
||||
}
|
||||
|
||||
// ==================== 内部方法 ====================
|
||||
|
||||
@ -100,17 +100,14 @@ public class FactController {
|
||||
content = content + "\n" + marker + "\n";
|
||||
}
|
||||
workspaceFileService.saveFile(agentId, filename, content);
|
||||
// Rebuild projection from the UPDATED canonical content (not stale file object)
|
||||
// Forgotten section will be skipped by PatternEntityExtractor
|
||||
projectionBuilder.rebuildOne(agentId, filename, content);
|
||||
}
|
||||
|
||||
// Trigger projection rebuild — the forgotten fact will be excluded
|
||||
// because FactProjectionBuilder skips sections with Forgotten: metadata
|
||||
projectionBuilder.rebuildOne(agentId, filename,
|
||||
file != null ? file.getContent() : "");
|
||||
|
||||
// Soft-delete the fact immediately (rebuild will also handle it)
|
||||
fact.setDeleted(1);
|
||||
fact.setUpdateTime(LocalDateTime.now());
|
||||
factMapper.updateById(fact);
|
||||
// Do NOT directly write mate_fact — let projection rebuild handle visibility.
|
||||
// The rebuild will either skip the Forgotten section (removing the fact)
|
||||
// or soft-delete it via deleteByAgentIdAndSourceRefNotIn.
|
||||
|
||||
log.info("[Fact] Forgotten fact {} for agent={} by {}", factId, agentId, userId);
|
||||
return R.ok(null);
|
||||
@ -132,24 +129,35 @@ public class FactController {
|
||||
return R.fail("Fact not found");
|
||||
}
|
||||
|
||||
// Write feedback metadata to canonical source
|
||||
// Write feedback metadata to the specific canonical section (not file tail)
|
||||
String sourceRef = fact.getSourceRef();
|
||||
String[] parts = sourceRef.split("#", 2);
|
||||
String filename = parts[0];
|
||||
String sectionKey = parts.length > 1 ? parts[1] : null;
|
||||
WorkspaceFileEntity file = workspaceFileService.getFile(agentId, filename);
|
||||
if (file != null && file.getContent() != null) {
|
||||
String marker = "> UserFeedback: " + kind + " " + LocalDate.now();
|
||||
workspaceFileService.saveFile(agentId, filename, file.getContent() + "\n" + marker + "\n");
|
||||
String content = file.getContent();
|
||||
if (sectionKey != null) {
|
||||
String sectionHeader = "## " + sectionKey;
|
||||
int idx = content.indexOf(sectionHeader);
|
||||
if (idx >= 0) {
|
||||
int nextSection = content.indexOf("\n## ", idx + sectionHeader.length());
|
||||
int insertAt = nextSection > 0 ? nextSection : content.length();
|
||||
content = content.substring(0, insertAt) + "\n" + marker + "\n" + content.substring(insertAt);
|
||||
} else {
|
||||
content = content + "\n" + marker + "\n";
|
||||
}
|
||||
} else {
|
||||
content = content + "\n" + marker + "\n";
|
||||
}
|
||||
workspaceFileService.saveFile(agentId, filename, content);
|
||||
// Rebuild projection from updated canonical — trust will be derived from metadata
|
||||
projectionBuilder.rebuildOne(agentId, filename, content);
|
||||
}
|
||||
|
||||
// Adjust trust score
|
||||
double delta = kind.equals("HELPFUL") ? 0.1 : -0.2;
|
||||
double newTrust = Math.max(0.0, Math.min(1.0, (fact.getTrust() != null ? fact.getTrust() : 0.5) + delta));
|
||||
fact.setTrust(newTrust);
|
||||
fact.setUpdateTime(LocalDateTime.now());
|
||||
factMapper.updateById(fact);
|
||||
|
||||
log.info("[Fact] Feedback {} on fact {} for agent={}, trust={}", kind, factId, agentId, newTrust);
|
||||
// Do NOT directly write mate_fact.trust — let projection rebuild derive it from canonical metadata.
|
||||
log.info("[Fact] Feedback {} on fact {} for agent={}", kind, factId, agentId);
|
||||
return R.ok(null);
|
||||
}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ package vip.mate.memory.fact.extraction;
|
||||
/**
|
||||
* A single fact extracted from canonical memory content.
|
||||
*
|
||||
* @param trust derived from UserFeedback metadata in the canonical section
|
||||
* (0.5 base + helpful*0.1 - unhelpful*0.2, clamped [0,1])
|
||||
* @author MateClaw Team
|
||||
*/
|
||||
public record ExtractedFact(
|
||||
@ -12,5 +14,6 @@ public record ExtractedFact(
|
||||
String predicate,
|
||||
String objectValue,
|
||||
double confidence,
|
||||
double trust,
|
||||
String extractedBy
|
||||
) {}
|
||||
|
||||
@ -23,6 +23,8 @@ public class PatternEntityExtractor implements EntityExtractor {
|
||||
private static final Pattern SECTION_HEADER = Pattern.compile("^## (.+)$", Pattern.MULTILINE);
|
||||
private static final Pattern KV_BULLET = Pattern.compile("^- \\*\\*(.+?)\\*\\*:\\s*(.+)$", Pattern.MULTILINE);
|
||||
private static final Pattern FORGOTTEN_MARKER = Pattern.compile("^> Forgotten:", Pattern.MULTILINE);
|
||||
private static final Pattern FEEDBACK_HELPFUL = Pattern.compile("^> UserFeedback: HELPFUL", Pattern.MULTILINE);
|
||||
private static final Pattern FEEDBACK_UNHELPFUL = Pattern.compile("^> UserFeedback: UNHELPFUL", Pattern.MULTILINE);
|
||||
|
||||
@Override
|
||||
public List<ExtractedFact> extract(Long agentId, String filename, String content) {
|
||||
@ -38,6 +40,9 @@ public class PatternEntityExtractor implements EntityExtractor {
|
||||
// Skip forgotten sections (rfc-038 L3: projection excludes Forgotten metadata)
|
||||
if (FORGOTTEN_MARKER.matcher(section).find()) continue;
|
||||
|
||||
// Derive trust from UserFeedback metadata in this section
|
||||
double trust = deriveTrust(section);
|
||||
|
||||
// Extract key-value bullets: - **key**: value
|
||||
Matcher kvMatcher = KV_BULLET.matcher(section);
|
||||
while (kvMatcher.find()) {
|
||||
@ -45,7 +50,7 @@ public class PatternEntityExtractor implements EntityExtractor {
|
||||
String value = kvMatcher.group(2).trim();
|
||||
if (value.isBlank() || value.equals(":")) continue;
|
||||
String sourceRef = filename + "#" + toSlug(key);
|
||||
facts.add(new ExtractedFact(sourceRef, category, key, "is", value, 0.9, "pattern"));
|
||||
facts.add(new ExtractedFact(sourceRef, category, key, "is", value, 0.9, trust, "pattern"));
|
||||
}
|
||||
|
||||
// Extract section heading facts from structured files
|
||||
@ -61,7 +66,7 @@ public class PatternEntityExtractor implements EntityExtractor {
|
||||
|
||||
String firstLine = body.split("\n")[0].replaceAll("^[-*>]+\\s*", "").trim();
|
||||
if (firstLine.length() >= 5) {
|
||||
facts.add(new ExtractedFact(sourceRef, category, heading, "has", firstLine, 0.8, "pattern"));
|
||||
facts.add(new ExtractedFact(sourceRef, category, heading, "has", firstLine, 0.8, trust, "pattern"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,6 +74,19 @@ public class PatternEntityExtractor implements EntityExtractor {
|
||||
return facts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive trust score from UserFeedback metadata in a section.
|
||||
* Base 0.5, HELPFUL +0.1 each, UNHELPFUL -0.2 each, clamped [0,1].
|
||||
*/
|
||||
private double deriveTrust(String section) {
|
||||
double trust = 0.5;
|
||||
Matcher helpful = FEEDBACK_HELPFUL.matcher(section);
|
||||
while (helpful.find()) trust += 0.1;
|
||||
Matcher unhelpful = FEEDBACK_UNHELPFUL.matcher(section);
|
||||
while (unhelpful.find()) trust -= 0.2;
|
||||
return Math.max(0.0, Math.min(1.0, trust));
|
||||
}
|
||||
|
||||
private String inferCategory(String filename) {
|
||||
if (filename.contains("user")) return "user_pref";
|
||||
if (filename.contains("project")) return "project";
|
||||
|
||||
@ -119,8 +119,9 @@ public class FactProjectionBuilder {
|
||||
existing.setObjectValue(fact.objectValue());
|
||||
existing.setConfidence(fact.confidence());
|
||||
existing.setExtractedBy(fact.extractedBy());
|
||||
// Apply trust time decay (half-life from config, default 60 days)
|
||||
existing.setTrust(applyTimeDecay(existing.getTrust(), existing.getUpdateTime(), now));
|
||||
// Trust derived from canonical feedback metadata, then time-decayed
|
||||
double baseTrust = fact.trust();
|
||||
existing.setTrust(applyTimeDecay(baseTrust, existing.getUpdateTime(), now));
|
||||
existing.setUpdateTime(now);
|
||||
existing.setDeleted(0); // un-delete if previously soft-deleted
|
||||
factMapper.updateById(existing);
|
||||
@ -133,7 +134,7 @@ public class FactProjectionBuilder {
|
||||
entity.setPredicate(fact.predicate());
|
||||
entity.setObjectValue(fact.objectValue());
|
||||
entity.setConfidence(fact.confidence());
|
||||
entity.setTrust(0.5);
|
||||
entity.setTrust(fact.trust());
|
||||
entity.setUseCount(0);
|
||||
entity.setExtractedBy(fact.extractedBy());
|
||||
entity.setCreateTime(now);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user