mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +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"
|
||||
+ "**违规后果**:未按规则引用来源或使用未验证的信息将导致回答被拒绝。\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<Message> messages = accessor.messages();
|
||||
|
||||
// Per-loop budget: bound the working message list a single Reasoning
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user