From 85ceafa055e9f8f9431f6997320996652ab6329e Mon Sep 17 00:00:00 2001 From: matevip Date: Tue, 16 Jun 2026 07:46:34 +0800 Subject: [PATCH] fix(agent): scope KB grounding to wiki-equipped agents and wiki tools The knowledge-base trust verification recorded wiki citations from every non-readFile tool response by sniffing its JSON for a top-level title / pages / chunks field. Tools like getGoalStatus return a top-level title, which falsely populated the citation set and then forced [n] citations on the final answer (otherwise flagged EVIDENCE_INSUFFICIENT). Gate citation mining on the wiki_* tool name instead. Likewise, the grounded answer contract (cite-or-refuse) was appended to every ReasoningNode call unconditionally, degrading general agents that have no knowledge base. Append it only when the agent has a wiki_* tool bound, scoping the strict regime to KB-grounded scenarios. Adds a regression test asserting a non-wiki tool with a top-level title creates no wiki citations. --- .../mate/agent/graph/node/ReasoningNode.java | 31 +++++++++++++++---- .../graph/state/SourceEvidenceLedger.java | 16 +++++++++- .../graph/state/SourceEvidenceLedgerTest.java | 15 +++++++++ 3 files changed, 55 insertions(+), 7 deletions(-) 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 eb20cd5b..0ec0da7d 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 @@ -284,8 +284,25 @@ public class ReasoningNode implements NodeAction { + "5. **内容忠实**:必须准确反映证据内容,不得歪曲、编造或过度推断。\n\n" + "**违规后果**:未按规则引用来源或使用未验证的信息将导致回答被拒绝。\n"; - private static String buildGroundedSystemPrompt(String basePrompt) { - return basePrompt + TOOL_USE_ENFORCEMENT + GROUNDED_CONTRACT; + private static String buildGroundedSystemPrompt(String basePrompt, boolean groundingEnforced) { + String prompt = basePrompt + TOOL_USE_ENFORCEMENT; + return groundingEnforced ? prompt + GROUNDED_CONTRACT : prompt; + } + + /** + * The grounded-answer contract (cite-or-refuse) only fits agents that retrieve + * from a knowledge base. Detecting a bound {@code wiki_*} tool scopes the strict + * regime to those scenarios instead of degrading every agent — a casual agent + * with no KB should not be forced to refuse or emit [n] citations. + */ + private boolean hasWikiTool() { + if (toolCallbacks == null) { + return false; + } + return toolCallbacks.stream().anyMatch(cb -> { + String name = cb.getToolDefinition().name(); + return name != null && name.toLowerCase(Locale.ROOT).replace("-", "_").startsWith("wiki_"); + }); } private final ChatModel chatModel; @@ -542,14 +559,16 @@ public class ReasoningNode implements NodeAction { // ======= 构建 Prompt ======= String systemPrompt = accessor.systemPrompt(); - // Append tool-use enforcement and grounded contract to every ReasoningNode call. - // Without tool-use enforcement, some models tend to "narrate" instead of calling tools. - // Grounded contract ensures answers are based only on evidence from tool results. + // Tool-use enforcement is always appended: without it some models tend to + // "narrate" instead of calling tools. The grounded contract (cite-or-refuse) + // is appended only when the agent has a knowledge-base (wiki_*) tool bound, + // so KB-grounded scenarios get strict source attribution while general + // agents keep their normal answering behaviour. // // Appended at runtime rather than woven into the AgentEntity-stored // prompt so it stays out of the user-editable agent UI but is still // always-on for the runtime LLM. - systemPrompt = buildGroundedSystemPrompt(systemPrompt); + systemPrompt = buildGroundedSystemPrompt(systemPrompt, hasWikiTool()); List messages = accessor.messages(); // Per-loop budget: bound the working message list a single Reasoning diff --git a/mateclaw-server/src/main/java/vip/mate/agent/graph/state/SourceEvidenceLedger.java b/mateclaw-server/src/main/java/vip/mate/agent/graph/state/SourceEvidenceLedger.java index 3ecfbea0..5884cbf8 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/graph/state/SourceEvidenceLedger.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/graph/state/SourceEvidenceLedger.java @@ -63,7 +63,14 @@ public record SourceEvidenceLedger( recordReadFile(data, builder); } else { recordPlainTextEvidence(data, builder); - recordWikiEvidence(data, builder); + // Only mine wiki citations from wiki retrieval tools. Sniffing every + // tool's JSON for a top-level title/pages/chunks field would let + // unrelated tools (e.g. getGoalStatus, which returns a top-level + // "title") populate the citation set and falsely force [n] citation + // enforcement on the final answer. + if (isWikiTool(response.name())) { + recordWikiEvidence(data, builder); + } } } return builder.build(); @@ -275,6 +282,13 @@ public record SourceEvidenceLedger( return normalized.equals("read_file"); } + private static boolean isWikiTool(String name) { + if (name == null) { + return false; + } + return name.toLowerCase(Locale.ROOT).replace("-", "_").startsWith("wiki_"); + } + private static void recordReadFile(String data, Builder builder) { try { JsonNode root = MAPPER.readTree(data); diff --git a/mateclaw-server/src/test/java/vip/mate/agent/graph/state/SourceEvidenceLedgerTest.java b/mateclaw-server/src/test/java/vip/mate/agent/graph/state/SourceEvidenceLedgerTest.java index add5ecc7..e5c77ce0 100644 --- a/mateclaw-server/src/test/java/vip/mate/agent/graph/state/SourceEvidenceLedgerTest.java +++ b/mateclaw-server/src/test/java/vip/mate/agent/graph/state/SourceEvidenceLedgerTest.java @@ -234,4 +234,19 @@ class SourceEvidenceLedgerTest { assertTrue(rendered.contains("[1] Install Guide - Linux - page 12")); assertTrue(ledger.validateAnswer(rendered).valid()); } + + @Test + @DisplayName("non-wiki tool JSON with a top-level title does not create wiki citations") + void nonWikiToolWithTitleDoesNotForceCitations() { + // getGoalStatus returns a top-level "title" field; it must not be mined as a + // wiki citation, otherwise a final answer with no [n] markers would be wrongly + // flagged EVIDENCE_INSUFFICIENT. + SourceEvidenceLedger ledger = SourceEvidenceLedger.fromToolResponses(List.of( + new ToolResponseMessage.ToolResponse("c1", "getGoalStatus", """ + {"active":true,"goalId":"42","title":"Ship the release","status":"in_progress"} + """))); + + assertFalse(ledger.hasWikiEvidence()); + assertTrue(ledger.validateAnswer("The release is on track.").valid()); + } }