第2384篇:Agentic RAG系统——让Agent自主决策如何检索和使用知识
大约 6 分钟
第2384篇:Agentic RAG系统——让Agent自主决策如何检索和使用知识
适读人群:想把RAG与Agent结合的AI工程师 | 阅读时长:约22分钟 | 核心价值:掌握Agentic RAG的工程实现,构建能够自主规划检索策略的智能体
普通RAG的检索逻辑很死板:用户一个问题,检索一次,生成一次,完事。
但有些复杂任务需要的远不止于此。比如用户说:"帮我整理一份关于竞争对手A公司的市场分析,包括他们的产品线、定价策略、近期动态和我们的差距分析。"
这个任务需要:
- 先搜"A公司产品线"
- 再搜"A公司定价"
- 再搜"A公司近期新闻"
- 搜"我们自己的产品线和定价"
- 综合以上信息做差距分析
每一步的检索需求,要基于前几步的结果来确定。这不是一次检索能解决的,需要的是一个能自主规划检索策略的Agent。
这就是Agentic RAG要解决的问题。
Agentic RAG的核心思路
/**
* Agentic RAG vs 普通RAG
*
* 普通RAG:
* 用户问题 → 检索一次 → 生成答案
* 检索策略是固定的、一次性的
*
* Agentic RAG:
* 用户任务 → Agent分析任务 → 决定需要哪些信息
* → 执行第一次检索 → 分析结果
* → 决定是否需要更多信息
* → 执行第二次检索(基于第一次结果)
* → ...
* → 所有信息收集完毕 → 生成综合答案
*
* 核心差异:检索策略由Agent动态决定,不是预设的
*/Tool的设计:给Agent提供检索能力
/**
* Agentic RAG的核心是把检索封装成Tool
* Agent通过调用Tool来获取信息
*/
@Service
public class RAGToolProvider {
private final VectorStore vectorStore;
private final EmbeddingModel embeddingModel;
/**
* 语义检索工具
* Agent调用这个Tool来检索知识库
*/
@Tool(
name = "search_knowledge_base",
description = "在知识库中搜索相关信息。当需要了解某个主题的详细内容时,使用此工具。"
)
public SearchResult searchKnowledgeBase(
@ToolParam(description = "搜索查询,描述你要找什么信息") String query,
@ToolParam(description = "结果数量,默认5,最多10") Integer topK
) {
int k = topK != null ? Math.min(topK, 10) : 5;
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(query).withTopK(k)
);
return SearchResult.builder()
.query(query)
.documents(docs.stream()
.map(d -> DocumentSummary.builder()
.id(d.getId())
.content(d.getContent())
.relevanceScore((Float) d.getMetadata().get("distance"))
.source((String) d.getMetadata().get("source"))
.build())
.collect(Collectors.toList()))
.found(docs.size())
.build();
}
/**
* 过滤检索工具
* 按元数据条件检索(适合精确查找)
*/
@Tool(
name = "search_by_filter",
description = "按条件检索知识库。当知道特定的分类、时间范围等条件时,使用此工具。"
)
public SearchResult searchByFilter(
@ToolParam(description = "搜索查询") String query,
@ToolParam(description = "过滤条件,如 category=产品文档") String filterCondition
) {
FilterExpression filter = parseFilterCondition(filterCondition);
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(query)
.withTopK(5)
.withFilterExpression(filter)
);
return SearchResult.of(docs);
}
/**
* 文档详情工具
* 当找到相关文档ID后,获取完整内容
*/
@Tool(
name = "get_document_detail",
description = "根据文档ID获取完整文档内容。当搜索结果摘要不够时,使用此工具获取完整内容。"
)
public String getDocumentDetail(
@ToolParam(description = "文档ID") String documentId
) {
Document doc = findDocumentById(documentId);
if (doc == null) {
return "文档不存在:" + documentId;
}
return doc.getContent();
}
}Agentic RAG的核心执行循环
@Service
public class AgenticRAGService {
private final ChatClient chatClient;
private final RAGToolProvider ragToolProvider;
/**
* Agentic RAG主流程
*
* 使用ReAct(Reasoning + Acting)框架:
* 1. Reason:分析当前任务,决定下一步
* 2. Act:执行检索Tool
* 3. Observe:分析检索结果
* 4. 重复,直到收集足够信息
* 5. 生成最终答案
*/
public AgenticRAGResult execute(String userTask, String userId) {
// 构建系统提示:定义Agent的行为和可用工具
String systemPrompt = buildAgentSystemPrompt();
// 初始化对话历史
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPrompt));
messages.add(new UserMessage(userTask));
List<ToolCallRecord> toolCallHistory = new ArrayList<>();
int maxIterations = 10;
for (int i = 0; i < maxIterations; i++) {
// 调用LLM,允许它调用工具
ChatResponse response = chatClient.prompt()
.messages(messages)
.tools(ragToolProvider) // 注册所有RAG工具
.call()
.chatResponse();
// 检查是否有工具调用
AssistantMessage assistantMessage = response.getResult().getOutput();
messages.add(assistantMessage);
List<ToolCall> toolCalls = assistantMessage.getToolCalls();
if (toolCalls == null || toolCalls.isEmpty()) {
// 没有工具调用,Agent认为信息收集完毕,这是最终答案
return AgenticRAGResult.success(
assistantMessage.getContent(),
toolCallHistory,
i + 1
);
}
// 执行工具调用
for (ToolCall toolCall : toolCalls) {
String toolResult = executeToolCall(toolCall);
// 记录工具调用历史
toolCallHistory.add(ToolCallRecord.builder()
.toolName(toolCall.name())
.input(toolCall.arguments())
.output(toolResult)
.iteration(i + 1)
.build()
);
// 把工具结果加入对话历史
messages.add(new ToolResultMessage(toolCall.id(), toolResult));
}
}
// 超过最大迭代次数
return AgenticRAGResult.maxIterations(toolCallHistory);
}
private String buildAgentSystemPrompt() {
return """
你是一个智能知识助手。你有以下工具可以使用:
1. search_knowledge_base:在知识库中语义搜索
2. search_by_filter:按条件检索
3. get_document_detail:获取文档完整内容
工作方式:
1. 先分析用户的任务,确定需要哪些信息
2. 逐步调用工具收集信息
3. 每次工具调用后,评估是否已有足够信息
4. 所有信息收集完毕后,综合生成最终答案
注意:
- 不要在信息不足时给出答案
- 每次工具调用要有明确的目的
- 避免重复调用获取相同信息的工具
- 如果搜索结果不相关,换个查询词重试
""";
}
private String executeToolCall(ToolCall toolCall) {
try {
return switch (toolCall.name()) {
case "search_knowledge_base" -> {
String query = extractParam(toolCall.arguments(), "query");
Integer topK = extractIntParam(toolCall.arguments(), "topK");
SearchResult result = ragToolProvider.searchKnowledgeBase(query, topK);
yield formatSearchResult(result);
}
case "search_by_filter" -> {
String query = extractParam(toolCall.arguments(), "query");
String filter = extractParam(toolCall.arguments(), "filterCondition");
SearchResult result = ragToolProvider.searchByFilter(query, filter);
yield formatSearchResult(result);
}
case "get_document_detail" -> {
String docId = extractParam(toolCall.arguments(), "documentId");
yield ragToolProvider.getDocumentDetail(docId);
}
default -> "Unknown tool: " + toolCall.name();
};
} catch (Exception e) {
log.error("Tool call failed: {}", toolCall.name(), e);
return "工具调用失败:" + e.getMessage();
}
}
}检索策略的智能优化
@Service
public class SmartQueryOptimizer {
/**
* 检测查询是否质量不好,给出改进建议
*
* Agent有时会生成质量很差的检索查询(太模糊、太长)
* 这个组件在Tool被调用前,先优化查询
*/
public OptimizedQuery optimize(String originalQuery) {
// 查询太长,可能效果不好
if (originalQuery.length() > 200) {
String compressed = compressQuery(originalQuery);
return OptimizedQuery.builder()
.originalQuery(originalQuery)
.optimizedQuery(compressed)
.optimizationApplied("query_compression")
.build();
}
// 查询包含过多连词,可能需要拆分
if (containsMultipleTopics(originalQuery)) {
List<String> subQueries = splitIntoSubQueries(originalQuery);
return OptimizedQuery.builder()
.originalQuery(originalQuery)
.subQueries(subQueries)
.optimizationApplied("query_splitting")
.build();
}
return OptimizedQuery.noOptimization(originalQuery);
}
private String compressQuery(String longQuery) {
String prompt = """
请将以下长查询压缩为一个简洁的检索查询(30字以内),保留核心信息:
原查询:%s
压缩后:
""".formatted(longQuery);
return chatClient.prompt(prompt).call().content().trim();
}
}Agent的自我评估:避免"我已经知道答案了"的幻觉
@Service
public class AgentSelfEvaluator {
/**
* Agent在决定结束检索前,做一次自我评估
*
* 问题:Agent可能在收集信息不足时就停下来
* 解决:在每次"准备给出最终答案"前,强制评估一次
*/
public boolean hasEnoughInformation(String originalTask,
List<ToolCallRecord> toolHistory,
String currentDraft) {
String prompt = """
请评估以下情况:
原始任务:%s
已收集的信息(工具调用记录):
%s
当前草稿答案:
%s
请判断:
1. 草稿答案是否完整回答了原始任务的所有要求?
2. 是否有明显的信息缺口?
输出JSON:
{
"has_enough_info": true/false,
"missing_info": ["缺少的信息1", "缺少的信息2"],
"confidence": 0-1的置信度
}
""".formatted(
originalTask,
formatToolHistory(toolHistory),
currentDraft
);
String response = chatClient.prompt(prompt).call().content();
SelfEvaluationResult result = parseSelfEvaluation(response);
return result.hasEnoughInfo() && result.getConfidence() > 0.7;
}
}实践经验
做Agentic RAG需要注意几个工程细节:
1. 工具调用次数的上限:必须设,否则可能陷入无限循环。通常5-10次足够。
2. 工具调用历史的记忆:Agent需要记得之前查过什么,避免重复查询。把Tool调用记录加入对话历史就能解决。
3. 费用控制:每次工具调用后,Agent需要重新推理,会消耗大量Token。要对任务类型做路由——简单问题直接走普通RAG,只有复杂任务才走Agentic RAG。
4. 可观测性:Agentic RAG的调试比普通RAG困难得多,因为推理过程是动态的。必须把每一次工具调用和Agent的思考过程记录下来,方便排查问题。
