第2373篇:多语言RAG系统——跨语言知识库的检索和答案生成
大约 7 分钟
第2373篇:多语言RAG系统——跨语言知识库的检索和答案生成
适读人群:需要支持多语言场景的AI工程师 | 阅读时长:约18分钟 | 核心价值:掌握多语言向量检索、跨语言问答的核心工程方案,避免常见的语言混乱陷阱
接了个出海项目,需求是东南亚用户可以用中文、英文、泰语、越南语提问,知识库里的文档是这几种语言混合的。
我第一直觉是:用一个多语言embedding模型(比如multilingual-e5),应该直接能用。
现实是:确实能用,但有几个坑让我们加班了一周。
最头疼的问题:用泰语提问,检索到的文档80%是中文文档,因为中文文档数量占大多数,向量空间里中文"更密集",导致泰语查询偏向中文结果。用户拿到的是一堆中文文档,即使最后翻译了,用户体验也很差。
多语言RAG的核心挑战
/**
* 多语言RAG面临的主要问题
*
* 问题1:语言偏向(Language Bias)
* - 某种语言文档数量多,向量空间被这种语言主导
* - 其他语言的查询结果偏向主导语言
*
* 问题2:跨语言语义鸿沟
* - "退款政策"(中文)和"refund policy"(英文)语义相同
* - 但向量距离可能不够近,导致跨语言检索失败
*
* 问题3:语言检测的准确性
* - 混合语言的输入(中英混杂)难以判断主语言
* - 语言检测错误导致后续处理全部跑偏
*
* 问题4:回答语言的一致性
* - 用泰语问,要用泰语回答
* - 但参考文档是中文的,LLM容易混着用两种语言
*/方案一:查询翻译策略
最可靠的方案:把用户查询翻译成知识库的主要语言,再检索,最后把答案翻译回用户语言。
@Service
public class MultilingualQueryService {
private final ChatClient chatClient;
private final VectorStore vectorStore;
private final LanguageDetector languageDetector;
/**
* 跨语言检索主流程
* 核心思路:统一到一个中间语言(通常是英文或中文)做检索
*/
public MultilingualResult retrieve(String query) {
// 第一步:检测查询语言
String detectedLang = languageDetector.detect(query);
// 第二步:翻译到知识库主语言(这里假设知识库主语言是中文)
String normalizedQuery = query;
if (!"zh".equals(detectedLang)) {
normalizedQuery = translateToChineseForSearch(query, detectedLang);
}
// 第三步:检索
List<Document> docs = vectorStore.similaritySearch(
SearchRequest.query(normalizedQuery).withTopK(5)
);
// 第四步:生成回答(用原始语言)
String answer = generateAnswerInLanguage(query, detectedLang, docs);
return MultilingualResult.builder()
.originalQuery(query)
.detectedLanguage(detectedLang)
.normalizedQuery(normalizedQuery)
.retrievedDocs(docs)
.answer(answer)
.build();
}
/**
* 翻译查询到中文(为检索优化,不追求文学性)
*/
private String translateToChineseForSearch(String query, String sourceLang) {
String prompt = """
请将以下%s文本翻译成中文。只翻译,不要加任何解释。
原文:%s
中文翻译:
""".formatted(getLanguageName(sourceLang), query);
return chatClient.prompt(prompt).call().content().trim();
}
/**
* 用用户的语言生成回答
* 关键:在Prompt里明确指定输出语言
*/
private String generateAnswerInLanguage(String originalQuery,
String targetLang,
List<Document> docs) {
String context = docs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n"));
String languageInstruction = getLanguageInstruction(targetLang);
String prompt = """
%s
基于以下文档内容,回答用户的问题。
文档内容:
%s
用户问题:%s
请用指定语言给出完整回答:
""".formatted(languageInstruction, context, originalQuery);
return chatClient.prompt(prompt).call().content();
}
private String getLanguageInstruction(String langCode) {
return switch (langCode) {
case "zh" -> "请用中文回答。";
case "en" -> "Please answer in English.";
case "th" -> "โปรดตอบเป็นภาษาไทย (Please answer in Thai)";
case "vi" -> "Vui lòng trả lời bằng tiếng Việt (Please answer in Vietnamese)";
case "ja" -> "日本語で回答してください。";
case "ko" -> "한국어로 답변해 주세요.";
default -> "Please answer in the same language as the question.";
};
}
}方案二:多语言并行检索
当知识库里确实有多种语言文档时,更好的方案是在每种语言的索引里分别检索,再合并结果。
@Service
public class ParallelMultilingualRetriever {
// 不同语言各自的向量索引
private final Map<String, VectorStore> languageVectorStores;
private final VectorStore universalVectorStore; // 多语言统一索引(备用)
/**
* 并行检索多个语言的索引
*/
public List<Document> retrieveAcrossLanguages(String query, String queryLang) {
List<String> searchLanguages = determineSearchLanguages(queryLang);
// 把查询翻译成各个目标语言
Map<String, String> translatedQueries = translateQueryToLanguages(query, searchLanguages);
// 并行检索
List<CompletableFuture<List<Document>>> futures = new ArrayList<>();
for (String lang : searchLanguages) {
String translatedQuery = translatedQueries.getOrDefault(lang, query);
VectorStore langStore = languageVectorStores.get(lang);
if (langStore != null) {
futures.add(CompletableFuture.supplyAsync(() ->
langStore.similaritySearch(
SearchRequest.query(translatedQuery).withTopK(3)
)
));
}
}
// 等待所有检索完成并合并结果
List<Document> allDocs = new ArrayList<>();
for (CompletableFuture<List<Document>> future : futures) {
try {
allDocs.addAll(future.get(3, TimeUnit.SECONDS));
} catch (TimeoutException e) {
log.warn("Language search timeout");
}
}
// 去重和重排序
return deduplicateAndRerank(allDocs, query);
}
/**
* 确定应该检索哪些语言的索引
*
* 策略:
* - 同语言的优先检索(置信度高)
* - 加上英文(作为通用语言)
* - 如果知识库里某语言文档稀少,不单独建索引
*/
private List<String> determineSearchLanguages(String queryLang) {
List<String> languages = new ArrayList<>();
languages.add(queryLang); // 首先是用户语言
// 英文作为通用语言总是要检索
if (!"en".equals(queryLang)) {
languages.add("en");
}
// 根据知识库配置添加其他语言
languages.addAll(getConfiguredAdditionalLanguages(queryLang));
return languages;
}
/**
* 多语言结果去重
* 同一份内容的不同语言版本,只保留一份
*/
private List<Document> deduplicateAndRerank(List<Document> docs, String query) {
// 按文档ID去重(不同语言版本的同一份文档应该有相同的baseDocId)
Map<String, Document> deduplicated = new LinkedHashMap<>();
for (Document doc : docs) {
String baseId = (String) doc.getMetadata().getOrDefault("base_doc_id", doc.getId());
if (!deduplicated.containsKey(baseId)) {
deduplicated.put(baseId, doc);
}
}
return new ArrayList<>(deduplicated.values());
}
}语言检测:不能太简单
很多项目用简单的字符集判断(有中文字符就是中文),这在实际场景里经常出问题。
@Service
public class RobustLanguageDetector {
// 使用ICU4J或语言检测库
private final LanguageDetector detector;
/**
* 多策略语言检测,提高准确性
*/
public LanguageDetectionResult detect(String text) {
if (text == null || text.trim().length() < 3) {
return LanguageDetectionResult.unknown();
}
// 策略1:基于Unicode字符集的快速判断
LanguageHint charsetHint = detectByCharset(text);
if (charsetHint.getConfidence() > 0.95) {
return LanguageDetectionResult.of(charsetHint.getLang(), charsetHint.getConfidence());
}
// 策略2:统计模型检测
LanguageHint statisticalResult = detectByStatisticalModel(text);
// 综合两种结果
if (charsetHint.getLang().equals(statisticalResult.getLang())) {
// 两种方法一致,置信度高
return LanguageDetectionResult.of(
charsetHint.getLang(),
Math.max(charsetHint.getConfidence(), statisticalResult.getConfidence())
);
}
// 不一致时,用统计模型结果(更准确)
return LanguageDetectionResult.of(
statisticalResult.getLang(),
statisticalResult.getConfidence() * 0.8 // 降低置信度
);
}
/**
* 处理混合语言文本
*
* 例:一段中英混杂的文字,主语言是什么?
*/
public String detectDominantLanguage(String text) {
// 按句子分割,检测每个句子的语言
String[] sentences = text.split("[。!?.!?\\n]");
Map<String, Integer> langCounts = new HashMap<>();
for (String sentence : sentences) {
if (sentence.trim().length() < 3) continue;
LanguageDetectionResult result = detect(sentence);
if (result.getConfidence() > 0.7) {
langCounts.merge(result.getLanguage(), 1, Integer::sum);
}
}
return langCounts.entrySet().stream()
.max(Map.Entry.comparingByValue())
.map(Map.Entry::getKey)
.orElse("en"); // 默认英文
}
}文档入库时的多语言处理
@Service
public class MultilingualDocumentIngester {
/**
* 文档入库时的处理
*
* 关键决策:是翻译成统一语言存储,还是保留原语言?
*
* 推荐:保留原语言 + 生成英文摘要,两份都存
* 原因:翻译会损失细节,保留原文检索精度更高
* 但英文摘要可以让跨语言检索更容易命中
*/
public void ingestDocument(String content, String language, String docId) {
// 1. 原文入库
Document originalDoc = Document.builder()
.id(docId + "_" + language)
.content(content)
.metadata(Map.of(
"language", language,
"base_doc_id", docId,
"is_translation", "false"
))
.build();
getVectorStore(language).add(List.of(originalDoc));
// 2. 如果不是英文,生成英文摘要也入库
if (!"en".equals(language)) {
String englishSummary = generateEnglishSummary(content);
Document summaryDoc = Document.builder()
.id(docId + "_en_summary")
.content(englishSummary)
.metadata(Map.of(
"language", "en",
"base_doc_id", docId,
"is_translation", "true",
"original_language", language
))
.build();
getVectorStore("en").add(List.of(summaryDoc));
}
}
private String generateEnglishSummary(String content) {
// 控制摘要长度,不要太长
String truncated = content.length() > 2000
? content.substring(0, 2000) + "..."
: content;
String prompt = """
Please provide a concise English summary (2-3 sentences) of the following content.
Focus on the key information that would help someone find this document.
Content: %s
English summary:
""".formatted(truncated);
return chatClient.prompt(prompt).call().content().trim();
}
}一个工程经验
做多语言RAG,最容易忽略的是测试数据的语言覆盖。
很多团队做完功能测试,测试用例全是中文或英文的。泰语、越南语的测试只有一两条。结果上线后才发现,泰语的检索命中率只有40%,因为泰语的分词和向量化有特殊问题。
建议:每种支持的语言至少准备50条测试问题,覆盖不同类型的查询(简单查找、复杂问题、跨主题等)。在CI里加语言维度的自动化评测,有问题早发现。
