第2310篇:FLARE动态检索——基于预测不确定性的主动检索策略
2026/4/30大约 7 分钟
第2310篇:FLARE动态检索——基于预测不确定性的主动检索策略
适读人群:RAG系统工程师、NLP应用开发者 | 阅读时长:约16分钟 | 核心价值:掌握FLARE的核心机制,构建在生成过程中动态识别不确定性并主动触发检索的智能系统
有一次,我在看一篇关于某个开源框架的技术文档时发现,我们的RAG系统对这个框架相关的问题,回答质量特别参差不齐。
详细分析后发现了一个规律:问的是框架整体架构,回答还不错(基础知识在训练数据里);但问到某个具体的配置参数、某个特定版本的行为变化,回答就开始出错——有时候LLM会用旧版本的参数名,有时候会把两个框架的配置混淆。
问题在于:我们的RAG在回答开始前检索一次,但LLM在生成的过程中如果"走到一个它不确定的地方",没有机会再触发检索。
FLARE(Forward-Looking Active Retrieval Augmented Generation)正是为了解决这个问题而生的。
FLARE的工作原理
FLARE的核心洞察:生成是个逐词预测的过程,当模型对即将生成的内容不确定时,那正是需要检索的时刻。
传统RAG:
- 用户问问题 → 检索一次 → 生成完整回答
FLARE:
- 用户问问题 → 生成部分内容 → 检测到低置信度token → 暂停生成 → 触发针对性检索 → 基于新检索继续生成 → 重复直到生成完成
核心挑战:检测生成时的不确定性
标准的Chat API调用只返回文本,不返回token概率。要实现FLARE,需要能获取token-level概率的接口。
对于OpenAI类API,可以用logprobs参数:
@Service
public class UncertaintyDetector {
private final OpenAIClient openAIClient;
/**
* 生成文本同时检测不确定片段
* 使用logprobs获取每个token的置信度
*/
public GenerationWithUncertainty generateWithConfidence(
String prompt,
float uncertaintyThreshold) {
ChatCompletionRequest request = ChatCompletionRequest.builder()
.model("gpt-4o")
.messages(List.of(new Message("user", prompt)))
.logprobs(true)
.topLogprobs(1) // 获取每个位置top 1的logprob
.maxTokens(500)
.build();
ChatCompletionResponse response = openAIClient.createChatCompletion(request);
String generatedText = response.choices().get(0).message().content();
List<TokenLogprob> logprobs = response.choices().get(0).logprobs().content();
// 识别低置信度片段
List<UncertainSpan> uncertainSpans = identifyUncertainSpans(
generatedText, logprobs, uncertaintyThreshold
);
return new GenerationWithUncertainty(generatedText, logprobs, uncertainSpans);
}
/**
* 从logprobs序列中识别低置信度的连续片段
*/
private List<UncertainSpan> identifyUncertainSpans(
String text,
List<TokenLogprob> logprobs,
float threshold) {
List<UncertainSpan> spans = new ArrayList<>();
int spanStart = -1;
StringBuilder spanText = new StringBuilder();
for (int i = 0; i < logprobs.size(); i++) {
TokenLogprob tokenLogprob = logprobs.get(i);
float probability = (float) Math.exp(tokenLogprob.logprob()); // logprob转probability
if (probability < threshold) {
// 低置信度token
if (spanStart == -1) {
spanStart = i;
}
spanText.append(tokenLogprob.token());
} else {
// 高置信度token
if (spanStart != -1 && spanText.length() > 3) {
// 结束一个不确定片段(过滤掉太短的,避免噪声)
spans.add(new UncertainSpan(spanStart, i - 1, spanText.toString()));
spanStart = -1;
spanText = new StringBuilder();
} else if (spanStart != -1) {
spanStart = -1;
spanText = new StringBuilder();
}
}
}
return spans;
}
}从不确定片段提取检索查询
识别到"我在这里不确定"之后,要把这个不确定性转化成一个好的检索查询:
@Component
public class UncertaintyToQueryConverter {
private final ChatClient chatClient;
/**
* 将不确定片段转化为检索查询
* @param originalQuestion 原始用户问题
* @param generatedSoFar 已生成的确定性内容
* @param uncertainSpan 不确定的片段
*/
public String convertToQuery(String originalQuestion,
String generatedSoFar,
UncertainSpan uncertainSpan) {
String prompt = """
原始问题:%s
已生成的回答(确定部分):
%s
当前不确定、需要验证的内容片段:
"%s"
请生成一个精准的搜索查询,用于检索能够验证或纠正这个不确定片段的信息。
要求:
1. 查询要聚焦在不确定的具体信息点上
2. 包含必要的上下文(比如产品名、版本号等)
3. 使用关键词形式,不超过20个字
只输出查询字符串,不要其他内容。
""".formatted(originalQuestion, generatedSoFar, uncertainSpan.text());
return chatClient.prompt()
.user(prompt)
.call()
.content()
.trim();
}
}完整的FLARE执行引擎
@Service
public class FLAREEngine {
private final UncertaintyDetector uncertaintyDetector;
private final UncertaintyToQueryConverter queryConverter;
private final VectorSearchService vectorSearch;
private final ChatClient regenerationClient;
private static final float DEFAULT_UNCERTAINTY_THRESHOLD = 0.1f; // 低于10%概率视为不确定
private static final int MAX_RETRIEVAL_ROUNDS = 3;
/**
* FLARE增强的文本生成
*/
public FLAREResult generate(String question) {
StringBuilder finalAnswer = new StringBuilder();
List<FLARERetrievalEvent> retrievalEvents = new ArrayList<>();
String currentPrompt = buildInitialPrompt(question, "");
for (int round = 0; round < MAX_RETRIEVAL_ROUNDS; round++) {
// 生成下一个片段,同时检测不确定性
GenerationWithUncertainty generation = uncertaintyDetector.generateWithConfidence(
currentPrompt, DEFAULT_UNCERTAINTY_THRESHOLD
);
if (generation.uncertainSpans().isEmpty()) {
// 这一段生成得很确定,直接接受
finalAnswer.append(generation.text());
// 检查是否生成完成(简单方案:如果生成了完整句子就停)
if (isGenerationComplete(generation.text())) {
break;
}
// 更新prompt,继续生成
currentPrompt = buildContinuationPrompt(question, finalAnswer.toString(), "");
} else {
// 有不确定片段,需要检索
UncertainSpan firstUncertainSpan = generation.uncertainSpans().get(0);
log.info("检测到不确定片段: '{}', 触发检索", firstUncertainSpan.text());
// 转换为检索查询
String searchQuery = queryConverter.convertToQuery(
question, finalAnswer.toString(), firstUncertainSpan
);
// 检索
List<RetrievedDocument> docs = vectorSearch.search(searchQuery, 3);
retrievalEvents.add(new FLARERetrievalEvent(
round + 1, firstUncertainSpan.text(), searchQuery, docs
));
// 基于新文档重新生成这一段
String docsContext = docs.stream()
.map(RetrievedDocument::content)
.collect(Collectors.joining("\n---\n"));
currentPrompt = buildContinuationPrompt(
question, finalAnswer.toString(), docsContext
);
// 不追加uncertain的生成内容,重新生成
}
}
return new FLAREResult(finalAnswer.toString(), retrievalEvents);
}
private String buildInitialPrompt(String question, String additionalContext) {
if (additionalContext.isBlank()) {
return """
请回答以下问题:
%s
回答时请保持准确,如果涉及具体数据或参数,请确保其准确性。
""".formatted(question);
}
return """
参考文档:
%s
请回答以下问题:
%s
""".formatted(additionalContext, question);
}
private String buildContinuationPrompt(String question,
String generatedSoFar,
String newContext) {
return """
用户问题:%s
%s
已生成的回答(请继续生成,不要重复):
%s
请继续完成回答:
""".formatted(
question,
newContext.isBlank() ? "" : "参考文档:\n" + newContext,
generatedSoFar
);
}
private boolean isGenerationComplete(String text) {
// 判断是否生成了完整的回答
String trimmed = text.trim();
return trimmed.endsWith("。") || trimmed.endsWith("!") || trimmed.endsWith("?")
|| trimmed.length() > 400; // 超过400字视为足够完整
}
}简化版:基于文本标记的不确定性检测
如果你的API不支持logprobs,或者想要更简单的实现,可以让LLM自己标记不确定内容:
@Service
public class TextMarkBasedFLARE {
private final ChatClient chatClient;
private final VectorSearchService vectorSearch;
private static final String UNCERTAINTY_MARKING_PROMPT = """
回答用户问题。对于你不确定的具体信息(如具体数值、版本、日期、名称等),
用 [UNCERTAIN: 查询关键词] 标记,括号内是你想搜索以确认的关键词。
例如:Spring Boot [UNCERTAIN: Spring Boot 3.2 默认连接池配置] 的默认连接池是HikariCP。
重要:只对真正不确定的具体信息标记,不要滥用标记。
""";
public String generateWithActivatedRetrieval(String question) {
// 第一轮:生成带不确定标记的初始回答
String markedAnswer = chatClient.prompt()
.system(UNCERTAINTY_MARKING_PROMPT)
.user(question)
.call()
.content();
// 提取所有不确定标记
List<UncertaintyMark> marks = extractUncertaintyMarks(markedAnswer);
if (marks.isEmpty()) {
// 没有不确定标记,去掉标记格式返回
return cleanMarks(markedAnswer);
}
// 检索所有标记的信息
Map<String, String> retrievedContext = new HashMap<>();
for (UncertaintyMark mark : marks) {
List<RetrievedDocument> docs = vectorSearch.search(mark.query(), 2);
if (!docs.isEmpty()) {
retrievedContext.put(mark.originalText(), docs.get(0).content());
}
}
// 第二轮:基于检索结果重新生成
String contextSummary = retrievedContext.entrySet().stream()
.map(e -> "关于「%s」:%s".formatted(e.getKey(), e.getValue()))
.collect(Collectors.joining("\n"));
return chatClient.prompt()
.system("基于以下参考信息,准确回答用户问题:\n" + contextSummary)
.user(question)
.call()
.content();
}
private List<UncertaintyMark> extractUncertaintyMarks(String text) {
List<UncertaintyMark> marks = new ArrayList<>();
Pattern pattern = Pattern.compile("\\[UNCERTAIN:\\s*([^\\]]+)\\]");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
marks.add(new UncertaintyMark(matcher.group(0), matcher.group(1).trim()));
}
return marks;
}
}与标准RAG的对比
| 场景 | 标准RAG | FLARE |
|---|---|---|
| 问题:Spring Boot连接池默认配置 | 检索一次,可能检索到旧版本文档 | 生成时发现版本号不确定,精准检索当前版本 |
| 问题:某公司最新财报 | 检索可能命中多个季度 | 在提到具体数字时触发检索 |
| 问题:什么是微服务 | 不必要地检索定义 | 基础定义不触发检索,具体细节才检索 |
FLARE最大的价值在于:检索时机更精准,检索查询更聚焦。相比"在开头检索一次",FLARE在需要的时刻、围绕具体的不确定点检索,命中率更高,引入的噪声更少。
