feat: 增强 Agentic RAG 主动检索引导

- 统一组合用户、知识库与异步工具系统提示词

- 补充知识库调用策略与运行时回归测试
This commit is contained in:
2026-08-29 17:01:35 +08:00
parent 651b292d83
commit 2d26494775
4 changed files with 277 additions and 37 deletions

View File

@@ -21,6 +21,7 @@ import com.easyagents.agent.runtime.mcp.McpSkillRegistration;
import com.easyagents.agent.runtime.mcp.McpSpecValidator;
import com.easyagents.agent.runtime.mcp.McpToolkitAdapter;
import com.easyagents.agent.runtime.persistence.session.noop.NoopAgentSessionStore;
import com.easyagents.agent.runtime.prompt.SystemPromptComposer;
import com.easyagents.agent.runtime.skill.AgentSkillBinding;
import com.easyagents.agent.runtime.skill.AgentSkillRuntimeContext;
import com.easyagents.agent.runtime.tool.AgentToolInvoker;
@@ -54,18 +55,6 @@ import java.util.function.Supplier;
*/
public class AgentScopeReActRuntime implements AgentRuntime {
private static final String ASYNC_TOOL_SYSTEM_PROMPT = """
Async tool protocol:
- Async tools may expose submit, observe, result, cancel, and list sub-tools. Treat these sub-tools as one user-facing tool.
- Do not ask the user to choose submit, observe, result, cancel, or list. These are internal execution phases.
- For a normal user request to use an async tool, call its submit sub-tool first with the user-provided arguments by default.
- After submit returns task_id, immediately call observe with that task_id to check progress.
- If the task is completed and result is available, use the returned result to answer the user.
- If the task is still running after observation, tell the user that the task is running and keep task_id/next_action for later tool calls.
- Use result, list, or cancel directly only when the user explicitly asks to get a known task result, list tasks, or cancel a task.
""";
private final AgentScopeModelFactory modelFactory;
private final AgentScopeToolAdapter toolAdapter;
private final AgentScopeKnowledgeAdapter knowledgeAdapter;
@@ -1157,7 +1146,7 @@ public class AgentScopeReActRuntime implements AgentRuntime {
ReActAgent.Builder builder = ReActAgent.builder()
.name(definition.getAgentName())
.description(definition.getDescription())
.sysPrompt(systemPrompt(definition))
.sysPrompt(SystemPromptComposer.compose(definition))
.model(model)
.toolkit(toolkit)
.memory(memory)
@@ -1172,30 +1161,6 @@ public class AgentScopeReActRuntime implements AgentRuntime {
return builder.build();
}
private String systemPrompt(AgentDefinition definition) {
String prompt = definition.getSystemPrompt();
if (!hasAsyncTool(definition)) {
return prompt;
}
if (prompt == null || prompt.isBlank()) {
return ASYNC_TOOL_SYSTEM_PROMPT.strip();
}
return prompt.stripTrailing() + ASYNC_TOOL_SYSTEM_PROMPT;
}
private boolean hasAsyncTool(AgentDefinition definition) {
if (definition == null || definition.getToolSpecs() == null) {
return false;
}
for (AgentToolSpec toolSpec : definition.getToolSpecs()) {
// AsyncToolSpecExpander marks all generated sub-tools with this runtime metadata.
if (toolSpec != null && Boolean.TRUE.equals(toolSpec.getMetadata().get("asyncTool"))) {
return true;
}
}
return false;
}
/**
* 构建 AgentScope Toolkit并返回按 Skill ID 分组的工具。
*

View File

@@ -0,0 +1,105 @@
package com.easyagents.agent.runtime.prompt;
import com.easyagents.agent.runtime.AgentDefinition;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import java.util.ArrayList;
import java.util.List;
/**
* 组合智能体运行时使用的系统提示词。
*/
public final class SystemPromptComposer {
private static final String KNOWLEDGE_TOOL_PROTOCOL = """
Knowledge tool protocol:
- Knowledge tools provide grounded information for the scopes described in their tool descriptions.
- Before answering a request involving facts, policies, procedures, entitlements, prices, product or service details, or other information within a knowledge tool's scope, call the most relevant knowledge tool first.
- Do not rely only on general model knowledge for claims that fall within an available knowledge tool's scope.
- If it is uncertain whether the request falls within a knowledge tool's scope, prefer making one retrieval call.
- Build a concise, standalone query from the current request and only the necessary conversation context. Resolve pronouns and omitted subjects, and preserve relevant names, time, location, product, membership level, constraints, and user intent. Do not copy the entire conversation.
- If the returned results are empty or do not address the request, reformulate the query and retry once when a materially different query is possible. Do not repeat the same query or retrieve indefinitely.
- Base knowledge-backed claims only on information actually returned by the tools. Never imply that a knowledge base contains information that was not returned.
- If retrieval remains insufficient, follow the Agent's configured system prompt for the response strategy.
- Skip retrieval for greetings, casual conversation, pure writing or translation, and tasks that clearly do not depend on knowledge-base facts.
""".strip();
private static final String ASYNC_TOOL_PROTOCOL = """
Async tool protocol:
- Async tools may expose submit, observe, result, cancel, and list sub-tools. Treat these sub-tools as one user-facing tool.
- Do not ask the user to choose submit, observe, result, cancel, or list. These are internal execution phases.
- For a normal user request to use an async tool, call its submit sub-tool first with the user-provided arguments by default.
- After submit returns task_id, immediately call observe with that task_id to check progress.
- If the task is completed and result is available, use the returned result to answer the user.
- If the task is still running after observation, tell the user that the task is running and keep task_id/next_action for later tool calls.
- Use result, list, or cancel directly only when the user explicitly asks to get a known task result, list tasks, or cancel a task.
""".strip();
/**
* 阻止工具类被实例化。
*/
private SystemPromptComposer() {
}
/**
* 按固定顺序组合用户提示词与运行时工具协议。
*
* @param definition 智能体定义
* @return 最终系统提示词;没有任何提示词时返回原始空值
*/
public static String compose(AgentDefinition definition) {
if (definition == null) {
return null;
}
boolean knowledgeProtocolEnabled = hasEnabledKnowledgeTool(definition);
boolean asyncProtocolEnabled = hasAsyncTool(definition);
String userPrompt = definition.getSystemPrompt();
if (!knowledgeProtocolEnabled && !asyncProtocolEnabled) {
return userPrompt;
}
List<String> promptSections = new ArrayList<>(3);
if (userPrompt != null && !userPrompt.isBlank()) {
promptSections.add(userPrompt.stripTrailing());
}
if (knowledgeProtocolEnabled) {
promptSections.add(KNOWLEDGE_TOOL_PROTOCOL);
}
if (asyncProtocolEnabled) {
promptSections.add(ASYNC_TOOL_PROTOCOL);
}
return String.join("\n\n", promptSections);
}
/**
* 判断当前定义是否会注册可调用的知识库工具。
*
* @param definition 智能体定义
* @return 知识库工具可用时返回 true
*/
private static boolean hasEnabledKnowledgeTool(AgentDefinition definition) {
return definition.getExecutionOptions() != null
&& definition.getExecutionOptions().isToolCallingEnabled()
&& definition.getKnowledgeSpecs() != null
&& !definition.getKnowledgeSpecs().isEmpty();
}
/**
* 判断当前定义是否包含异步工具。
*
* @param definition 智能体定义
* @return 包含异步工具时返回 true
*/
private static boolean hasAsyncTool(AgentDefinition definition) {
if (definition.getToolSpecs() == null) {
return false;
}
for (AgentToolSpec toolSpec : definition.getToolSpecs()) {
// AsyncToolSpecExpander 会为生成的全部子工具写入该运行时元数据。
if (toolSpec != null && Boolean.TRUE.equals(toolSpec.getMetadata().get("asyncTool"))) {
return true;
}
}
return false;
}
}

View File

@@ -155,6 +155,27 @@ public class AgentScopeStatefulRuntimeTest {
Assert.assertTrue(runtime.getAgent().getSysPrompt().contains("immediately call observe"));
}
@Test
public void shouldAppendKnowledgeToolProtocolPromptWhenKnowledgeToolsExist() {
AgentInitRequest request = initRequest();
AgentKnowledgeSpec knowledgeSpec = new AgentKnowledgeSpec();
knowledgeSpec.setKnowledgeId("knowledge-faq");
knowledgeSpec.setRuntimeName("faq");
request.getAgentDefinition().setKnowledgeSpecs(List.of(knowledgeSpec));
request.setKnowledgeRegistrations(List.of(new AgentKnowledgeRegistration(
knowledgeSpec, retrievalRequest -> AgentKnowledgeRetrievalResult.of(List.of()))));
AgentScopeReActRuntime runtime = fakeRuntime();
runtime.init(request);
Assert.assertTrue(runtime.getAgent().getSysPrompt()
.startsWith("system\n\nKnowledge tool protocol:"));
Assert.assertTrue(runtime.getAgent().getSysPrompt()
.contains("call the most relevant knowledge tool first"));
Assert.assertTrue(runtime.getAgent().getSysPrompt()
.contains("reformulate the query and retry once"));
}
@Test
public void shouldEmitSideEventWithRuntimeIdentityFromBridge() throws Exception {
AgentRuntimeExecutionContext context = new AgentRuntimeExecutionContext();

View File

@@ -0,0 +1,149 @@
package com.easyagents.agent.runtime.prompt;
import com.easyagents.agent.runtime.AgentDefinition;
import com.easyagents.agent.runtime.knowledge.AgentKnowledgeSpec;
import com.easyagents.agent.runtime.tool.AgentToolSpec;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
/**
* 测试运行时系统提示词组合器。
*/
public class SystemPromptComposerTest {
@Test
public void shouldKeepUserPromptWhenNoRuntimeProtocolIsRequired() {
AgentDefinition definition = definition(" user prompt ");
Assert.assertEquals(" user prompt ", SystemPromptComposer.compose(definition));
}
@Test
public void shouldReturnKnowledgeProtocolWhenUserPromptIsBlank() {
AgentDefinition definition = definition(" ");
definition.setKnowledgeSpecs(List.of(knowledge("faq")));
String prompt = SystemPromptComposer.compose(definition);
Assert.assertTrue(prompt.startsWith("Knowledge tool protocol:"));
Assert.assertFalse(prompt.startsWith("\n"));
}
@Test
public void shouldAppendKnowledgeProtocolAfterUserPrompt() {
AgentDefinition definition = definition("user prompt");
definition.setKnowledgeSpecs(List.of(knowledge("faq")));
String prompt = SystemPromptComposer.compose(definition);
Assert.assertTrue(prompt.startsWith("user prompt\n\nKnowledge tool protocol:"));
Assert.assertTrue(prompt.contains("prefer making one retrieval call"));
Assert.assertTrue(prompt.contains("reformulate the query and retry once"));
}
@Test
public void shouldAppendKnowledgeProtocolOnlyOnceForMultipleKnowledgeTools() {
AgentDefinition definition = definition("user prompt");
definition.setKnowledgeSpecs(List.of(knowledge("faq"), knowledge("policy")));
String prompt = SystemPromptComposer.compose(definition);
Assert.assertEquals(1, occurrences(prompt, "Knowledge tool protocol:"));
}
@Test
public void shouldSkipKnowledgeProtocolWhenToolCallingIsDisabled() {
AgentDefinition definition = definition("user prompt");
definition.setKnowledgeSpecs(List.of(knowledge("faq")));
definition.getExecutionOptions().setToolCallingEnabled(false);
Assert.assertEquals("user prompt", SystemPromptComposer.compose(definition));
}
@Test
public void shouldPreserveAsyncToolProtocolBehavior() {
AgentDefinition definition = definition(null);
definition.setToolSpecs(List.of(asyncTool()));
String prompt = SystemPromptComposer.compose(definition);
Assert.assertTrue(prompt.startsWith("Async tool protocol:"));
Assert.assertTrue(prompt.contains("These are internal execution phases."));
Assert.assertTrue(prompt.contains("call its submit sub-tool first with the user-provided arguments by default"));
Assert.assertTrue(prompt.contains("immediately call observe"));
}
@Test
public void shouldComposeUserKnowledgeAndAsyncProtocolsInStableOrder() {
AgentDefinition definition = definition("user prompt");
definition.setKnowledgeSpecs(List.of(knowledge("faq")));
definition.setToolSpecs(List.of(asyncTool()));
String prompt = SystemPromptComposer.compose(definition);
int userIndex = prompt.indexOf("user prompt");
int knowledgeIndex = prompt.indexOf("Knowledge tool protocol:");
int asyncIndex = prompt.indexOf("Async tool protocol:");
Assert.assertTrue(userIndex >= 0);
Assert.assertTrue(knowledgeIndex > userIndex);
Assert.assertTrue(asyncIndex > knowledgeIndex);
Assert.assertEquals(1, occurrences(prompt, "Knowledge tool protocol:"));
Assert.assertEquals(1, occurrences(prompt, "Async tool protocol:"));
}
/**
* 创建测试智能体定义。
*
* @param systemPrompt 用户系统提示词
* @return 智能体定义
*/
private AgentDefinition definition(String systemPrompt) {
AgentDefinition definition = new AgentDefinition();
definition.setSystemPrompt(systemPrompt);
return definition;
}
/**
* 创建测试知识库定义。
*
* @param runtimeName 知识库运行名
* @return 知识库定义
*/
private AgentKnowledgeSpec knowledge(String runtimeName) {
AgentKnowledgeSpec spec = new AgentKnowledgeSpec();
spec.setKnowledgeId("knowledge-" + runtimeName);
spec.setRuntimeName(runtimeName);
return spec;
}
/**
* 创建测试异步工具定义。
*
* @return 异步工具定义
*/
private AgentToolSpec asyncTool() {
AgentToolSpec toolSpec = new AgentToolSpec();
toolSpec.setName("demo_submit");
toolSpec.getMetadata().put("asyncTool", true);
return toolSpec;
}
/**
* 统计文本片段出现次数。
*
* @param value 待检查文本
* @param fragment 目标片段
* @return 出现次数
*/
private int occurrences(String value, String fragment) {
int count = 0;
int offset = 0;
while ((offset = value.indexOf(fragment, offset)) >= 0) {
count++;
offset += fragment.length();
}
return count;
}
}