From 46f3d425e07ca88bad9ba8d10c9cc5714cd60576 Mon Sep 17 00:00:00 2001 From: matevip Date: Sun, 7 Jun 2026 19:15:14 +0800 Subject: [PATCH] feat(agent): add kill-switch for final-answer Markdown normalization Gate MarkdownNormalizer behind mate.agent.markdown-normalize-enabled (default true) so operators can disable the rewrite verbatim if a normalization edge case ever mangles a legitimate answer. --- .../vip/mate/agent/AgentGraphBuilder.java | 7 +++++- .../agent/graph/node/FinalAnswerNode.java | 22 ++++++++++++++++--- .../src/main/resources/application.yml | 4 ++++ 3 files changed, 29 insertions(+), 4 deletions(-) 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 f9e16cc2..36065c9b 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/AgentGraphBuilder.java @@ -88,6 +88,11 @@ public class AgentGraphBuilder { @org.springframework.beans.factory.annotation.Value( "${mateclaw.skill.disclosure.load-skill-tool.enabled:true}") private boolean loadSkillToolEnabled; + + /** Escape hatch: when false, the final answer is sent verbatim without Markdown normalization. */ + @org.springframework.beans.factory.annotation.Value( + "${mate.agent.markdown-normalize-enabled:true}") + private boolean markdownNormalizeEnabled; private final ConversationService conversationService; private final ModelConfigService modelConfigService; private final ModelProviderService modelProviderService; @@ -809,7 +814,7 @@ public class AgentGraphBuilder { SummarizingNode summarizingNode = new SummarizingNode(chatModel, streamingHelper, streamTracker); LimitExceededNode limitExceededNode = new LimitExceededNode( chatModel, observationProcessor, streamingHelper, i18nService, progressLedgerService); - FinalAnswerNode finalAnswerNode = new FinalAnswerNode(generatedFileCache); + FinalAnswerNode finalAnswerNode = new FinalAnswerNode(generatedFileCache, markdownNormalizeEnabled); KeyStrategyFactory keyStrategyFactory = KeyStrategy.builder() // 输入字段 diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/FinalAnswerNode.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/FinalAnswerNode.java index ea478714..0db1585d 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/node/FinalAnswerNode.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/node/FinalAnswerNode.java @@ -40,12 +40,25 @@ public class FinalAnswerNode implements NodeAction { */ private final GeneratedFileCache generatedFileCache; + /** + * Kill-switch for the deterministic Markdown cleanup applied to the answer + * body. {@code true} (default) runs {@link MarkdownNormalizer}; set to + * {@code false} to surface model output verbatim if a normalization edge + * case ever mangles a legitimate answer in production. + */ + private final boolean markdownNormalizeEnabled; + public FinalAnswerNode() { - this(null); + this(null, true); } public FinalAnswerNode(GeneratedFileCache generatedFileCache) { + this(generatedFileCache, true); + } + + public FinalAnswerNode(GeneratedFileCache generatedFileCache, boolean markdownNormalizeEnabled) { this.generatedFileCache = generatedFileCache; + this.markdownNormalizeEnabled = markdownNormalizeEnabled; } @Override @@ -176,8 +189,11 @@ public class FinalAnswerNode implements NodeAction { // unaligned table pipes) that prompt rules fail to prevent; this fixes the // mechanical defects before the answer is persisted / sent to channels. // Verbatim tool output (RETURN_DIRECT) and approval-wait paths return early - // above and are intentionally left untouched. - finalAnswer = MarkdownNormalizer.normalize(finalAnswer); + // above and are intentionally left untouched. Gated so operators can turn + // the rewrite off (mate.agent.markdown-normalize-enabled=false). + if (markdownNormalizeEnabled) { + finalAnswer = MarkdownNormalizer.normalize(finalAnswer); + } // Build the event list. Always carries the finish_reason event so // downstream consumers (memory gate, channel accumulator, message diff --git a/mateclaw-server/src/main/resources/application.yml b/mateclaw-server/src/main/resources/application.yml index 9c8b6255..d064b00c 100644 --- a/mateclaw-server/src/main/resources/application.yml +++ b/mateclaw-server/src/main/resources/application.yml @@ -222,6 +222,10 @@ mateclaw: # MateClaw Agent 配置 mate: agent: + # Deterministic Markdown cleanup of the final answer (heading spaces, glued + # ---, table pipe alignment) before persistence / channel delivery. Set to + # false to pass model output through verbatim. + markdown-normalize-enabled: true graph: observation: # 与 GraphObservationProperties.java 默认值对齐,参考 openclaw token-budget 设计