mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-16 04:18:17 +08:00
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.
This commit is contained in:
parent
88be1f748a
commit
85ceafa055
@ -284,8 +284,25 @@ public class ReasoningNode implements NodeAction {
|
|||||||
+ "5. **内容忠实**:必须准确反映证据内容,不得歪曲、编造或过度推断。\n\n"
|
+ "5. **内容忠实**:必须准确反映证据内容,不得歪曲、编造或过度推断。\n\n"
|
||||||
+ "**违规后果**:未按规则引用来源或使用未验证的信息将导致回答被拒绝。\n";
|
+ "**违规后果**:未按规则引用来源或使用未验证的信息将导致回答被拒绝。\n";
|
||||||
|
|
||||||
private static String buildGroundedSystemPrompt(String basePrompt) {
|
private static String buildGroundedSystemPrompt(String basePrompt, boolean groundingEnforced) {
|
||||||
return basePrompt + TOOL_USE_ENFORCEMENT + GROUNDED_CONTRACT;
|
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;
|
private final ChatModel chatModel;
|
||||||
@ -542,14 +559,16 @@ public class ReasoningNode implements NodeAction {
|
|||||||
|
|
||||||
// ======= 构建 Prompt =======
|
// ======= 构建 Prompt =======
|
||||||
String systemPrompt = accessor.systemPrompt();
|
String systemPrompt = accessor.systemPrompt();
|
||||||
// Append tool-use enforcement and grounded contract to every ReasoningNode call.
|
// Tool-use enforcement is always appended: without it some models tend to
|
||||||
// Without tool-use enforcement, some models tend to "narrate" instead of calling tools.
|
// "narrate" instead of calling tools. The grounded contract (cite-or-refuse)
|
||||||
// Grounded contract ensures answers are based only on evidence from tool results.
|
// 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
|
// Appended at runtime rather than woven into the AgentEntity-stored
|
||||||
// prompt so it stays out of the user-editable agent UI but is still
|
// prompt so it stays out of the user-editable agent UI but is still
|
||||||
// always-on for the runtime LLM.
|
// always-on for the runtime LLM.
|
||||||
systemPrompt = buildGroundedSystemPrompt(systemPrompt);
|
systemPrompt = buildGroundedSystemPrompt(systemPrompt, hasWikiTool());
|
||||||
List<Message> messages = accessor.messages();
|
List<Message> messages = accessor.messages();
|
||||||
|
|
||||||
// Per-loop budget: bound the working message list a single Reasoning
|
// Per-loop budget: bound the working message list a single Reasoning
|
||||||
|
|||||||
@ -63,7 +63,14 @@ public record SourceEvidenceLedger(
|
|||||||
recordReadFile(data, builder);
|
recordReadFile(data, builder);
|
||||||
} else {
|
} else {
|
||||||
recordPlainTextEvidence(data, builder);
|
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();
|
return builder.build();
|
||||||
@ -275,6 +282,13 @@ public record SourceEvidenceLedger(
|
|||||||
return normalized.equals("read_file");
|
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) {
|
private static void recordReadFile(String data, Builder builder) {
|
||||||
try {
|
try {
|
||||||
JsonNode root = MAPPER.readTree(data);
|
JsonNode root = MAPPER.readTree(data);
|
||||||
|
|||||||
@ -234,4 +234,19 @@ class SourceEvidenceLedgerTest {
|
|||||||
assertTrue(rendered.contains("[1] Install Guide - Linux - page 12"));
|
assertTrue(rendered.contains("[1] Install Guide - Linux - page 12"));
|
||||||
assertTrue(ledger.validateAnswer(rendered).valid());
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user