AI 应用的国际化——多语言 UI 和多语言 AI 的协同
2026/4/30大约 10 分钟
AI 应用的国际化——多语言 UI 和多语言 AI 的协同
我第一次做 AI 应用国际化的时候,以为跟普通应用差不多——i18n 框架配一配,翻译文件搞一下,完事。
上线后收到了法国用户的反馈:他用法语问了一个问题,AI 用英语回答了他。他不是不会英语,但用他自己的话说:"感觉在跟一个外国人说话,很疏离。"
这让我意识到:AI 应用的国际化,比普通应用多了整整一层复杂度。
普通应用国际化只需要处理 UI 文本的多语言。AI 应用还需要处理:
- Prompt 的多语言版本:System Prompt 用什么语言写,会影响 AI 的回答语言倾向
- 自动语言检测:用户用什么语言提问,AI 就应该用什么语言回答
- Embedding 的语言适配:向量检索里,中文问题查中文知识库,英文问题查英文知识库——或者用多语言 Embedding 模型统一处理
- 语言一致性保持:对话过程中语言不应该突然切换
今天把这几个问题系统讲清楚。
AI 应用国际化的层次模型
大多数团队在做 AI 应用国际化时,只做了第一层,然后发现问题再去补第二层,最后被第三层卡住。
System Prompt 的多语言设计
错误方式:直接翻译 System Prompt
// 英文版 System Prompt
You are a helpful customer service assistant for TechCorp.
Answer questions about our products clearly and professionally.
// 中文版 System Prompt(机翻)
您是TechCorp的有用客服助手。
清晰、专业地回答有关我们产品的问题。这种方式有两个问题:
一是机翻版本的 Prompt 质量往往不如原版,AI 在不同语言下的行为会有偏差;二是翻译版本和原版很容易出现语义偏差,"helpful" 翻译成"有用"就已经丢失了一些语义。
正确方式:用语言指令动态注入
// 通用 System Prompt(英文为基础,语言指令单独处理)
You are a helpful customer service assistant for TechCorp.
Answer questions about our products clearly and professionally.
IMPORTANT: Always respond in {{DETECTED_LANGUAGE}}.
If the user writes in Chinese, respond in Chinese.
If the user writes in French, respond in French.
Maintain the same language throughout the conversation.这种方式有几个优势:
- 只维护一个 Prompt 版本,减少翻译和同步成本
- 语言指令是动态注入的,可以根据检测结果实时更新
- AI 理解"用什么语言回答"比"你是一个说中文的助手"更精确
语言检测的实现
方案一:使用专门的语言检测库
@Component
public class LanguageDetector {
private static final Logger log = LoggerFactory.getLogger(LanguageDetector.class);
// 使用 Apache OpenNLP 或 Lingua 库进行语言检测
// 这里用 Lingua 举例,它对短文本效果较好
private final LanguageDetector detector;
public LanguageDetector() {
// Lingua 支持 75 种语言,对短文本准确率高
this.detector = LanguageDetectorBuilder
.fromLanguages(
Language.CHINESE, Language.ENGLISH, Language.JAPANESE,
Language.KOREAN, Language.FRENCH, Language.GERMAN,
Language.SPANISH, Language.ARABIC
)
.build();
}
/**
* 检测文本语言,返回 BCP-47 语言标签
*/
public DetectedLanguage detect(String text) {
if (text == null || text.trim().length() < 3) {
return DetectedLanguage.of("en", 0.5); // 太短无法判断,默认英文
}
Optional<Language> detected = detector.detectLanguageOf(text);
if (detected.isEmpty()) {
log.debug("语言检测无结果,使用默认语言: text={}",
text.substring(0, Math.min(text.length(), 20)));
return DetectedLanguage.of("en", 0.3);
}
Language lang = detected.get();
double confidence = detector.computeLanguageConfidenceValues(text)
.stream()
.filter(v -> v.getLanguage() == lang)
.mapToDouble(ConfidenceValue::getValue)
.findFirst()
.orElse(0.5);
return DetectedLanguage.of(toLocaleTag(lang), confidence);
}
private String toLocaleTag(Language lang) {
return switch (lang) {
case CHINESE -> "zh-CN";
case ENGLISH -> "en";
case JAPANESE -> "ja";
case KOREAN -> "ko";
case FRENCH -> "fr";
case GERMAN -> "de";
case SPANISH -> "es";
case ARABIC -> "ar";
default -> "en";
};
}
@Value
@With
public static class DetectedLanguage {
String languageTag;
double confidence;
public static DetectedLanguage of(String tag, double confidence) {
return new DetectedLanguage(tag, confidence);
}
public boolean isHighConfidence() {
return confidence >= 0.8;
}
}
}方案二:让 AI 来检测语言
有时候语言检测库对混合语言(比如中英夹杂)效果不好,可以让 AI 来判断:
@Component
public class AILanguageDetector {
@Autowired
private ChatModel chatModel;
private static final AnthropicChatOptions LANGUAGE_DETECTION_OPTIONS =
AnthropicChatOptions.builder()
.withModel("claude-haiku-3") // 用便宜的小模型
.withMaxTokens(50)
.withTemperature(0.0) // 确定性输出
.build();
/**
* 用 AI 检测语言(适合处理混合语言或特殊格式)
*/
public String detectWithAI(String text) {
String prompt = String.format("""
What language is this text primarily written in?
Reply with only the BCP-47 language tag (e.g., zh-CN, en, ja, fr).
Text: %s
""", text.substring(0, Math.min(text.length(), 200)));
try {
ChatResponse response = chatModel.call(
new Prompt(prompt, LANGUAGE_DETECTION_OPTIONS));
String result = response.getResult().getOutput().getContent().trim();
// 验证格式
if (result.matches("[a-z]{2}(-[A-Z]{2})?")) {
return result;
}
return "en"; // 无法解析时默认英文
} catch (Exception e) {
return "en";
}
}
}代码:自动检测语言并切换 Prompt 的实现
语言上下文管理
/**
* 管理当前会话的语言上下文
*/
@Component
@SessionScope
public class LanguageContext {
private String detectedLanguage;
private double confidence;
private String userPreferredLanguage; // 用户手动设置的语言偏好
/**
* 确定最终使用的语言
* 优先级:用户手动设置 > 高置信度检测结果 > 客户端 Accept-Language > 默认英文
*/
public String getEffectiveLanguage(HttpServletRequest request) {
// 1. 用户手动设置优先级最高
if (userPreferredLanguage != null) {
return userPreferredLanguage;
}
// 2. 高置信度的自动检测结果
if (detectedLanguage != null && confidence >= 0.8) {
return detectedLanguage;
}
// 3. HTTP Accept-Language 头
String acceptLanguage = request.getHeader("Accept-Language");
if (acceptLanguage != null && !acceptLanguage.isEmpty()) {
return parseAcceptLanguage(acceptLanguage);
}
// 4. 默认英文
return "en";
}
private String parseAcceptLanguage(String acceptLanguage) {
// 解析 "zh-CN,zh;q=0.9,en;q=0.8" 格式
try {
return acceptLanguage.split(",")[0].split(";")[0].trim();
} catch (Exception e) {
return "en";
}
}
}多语言 Prompt 构建器
@Component
public class MultilingualPromptBuilder {
@Autowired
private LanguageDetector languageDetector;
@Autowired
private PromptManager promptManager;
/**
* 构建多语言感知的对话 Prompt
*/
public Prompt buildMultilingualPrompt(String userMessage,
String sessionLanguage,
String apiVersion) {
// 1. 检测当前消息的语言
LanguageDetector.DetectedLanguage msgLanguage =
languageDetector.detect(userMessage);
// 2. 确定回复应使用的语言
String responseLanguage = determineResponseLanguage(
sessionLanguage, msgLanguage);
// 3. 获取 System Prompt 模板
String systemPromptTemplate = promptManager.getActivePrompt(
"chat.system",
apiVersion,
Map.of()
);
// 4. 注入语言指令
String systemPromptWithLanguage = injectLanguageInstruction(
systemPromptTemplate, responseLanguage, msgLanguage.getLanguageTag());
// 5. 构建消息列表
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPromptWithLanguage));
messages.add(new UserMessage(userMessage));
return new Prompt(messages);
}
/**
* 在 System Prompt 中注入语言指令
*/
private String injectLanguageInstruction(String systemPrompt,
String responseLanguage,
String detectedLanguage) {
String languageInstruction = buildLanguageInstruction(
responseLanguage, detectedLanguage);
// 如果 Prompt 有占位符,替换它
if (systemPrompt.contains("{{LANGUAGE_INSTRUCTION}}")) {
return systemPrompt.replace("{{LANGUAGE_INSTRUCTION}}", languageInstruction);
}
// 否则在末尾追加
return systemPrompt + "\n\n" + languageInstruction;
}
private String buildLanguageInstruction(String responseLanguage,
String detectedLanguage) {
Map<String, String> languageNames = Map.of(
"zh-CN", "Simplified Chinese (简体中文)",
"zh-TW", "Traditional Chinese (繁體中文)",
"en", "English",
"ja", "Japanese (日本語)",
"ko", "Korean (한국어)",
"fr", "French (Français)",
"de", "German (Deutsch)",
"es", "Spanish (Español)"
);
String langName = languageNames.getOrDefault(responseLanguage, "English");
return String.format("""
LANGUAGE INSTRUCTION:
- The user is communicating in: %s
- You MUST respond in: %s
- Maintain this language throughout the conversation unless explicitly instructed otherwise.
- If you're unsure about the language, default to the language the user wrote in.
""", languageNames.getOrDefault(detectedLanguage, "English"), langName);
}
/**
* 确定回复语言:
* 如果会话语言和当前消息语言一致,用会话语言;
* 如果消息语言置信度高且与会话语言不同,跟随消息语言(用户切换了语言)
*/
private String determineResponseLanguage(String sessionLanguage,
LanguageDetector.DetectedLanguage msgLanguage) {
if (sessionLanguage == null) {
return msgLanguage.getLanguageTag();
}
// 如果检测置信度高且用户切换了语言,跟随用户
if (msgLanguage.isHighConfidence() &&
!sessionLanguage.startsWith(msgLanguage.getLanguageTag().split("-")[0])) {
return msgLanguage.getLanguageTag();
}
return sessionLanguage;
}
}对话服务层整合
@Service
public class MultilingualChatService {
private static final Logger log = LoggerFactory.getLogger(MultilingualChatService.class);
@Autowired
private ChatModel chatModel;
@Autowired
private MultilingualPromptBuilder promptBuilder;
@Autowired
private LanguageDetector languageDetector;
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 多语言感知的对话调用
*/
public ChatResult chat(String sessionId, String userMessage, String userId) {
// 1. 获取或初始化会话语言
String sessionLanguage = getSessionLanguage(sessionId, userMessage);
// 2. 构建多语言 Prompt
Prompt prompt = promptBuilder.buildMultilingualPrompt(
userMessage, sessionLanguage, "v1");
// 3. 调用 AI
ChatResponse response = chatModel.call(prompt);
String content = response.getResult().getOutput().getContent();
// 4. 更新会话语言(如果用户切换了语言)
LanguageDetector.DetectedLanguage msgLang =
languageDetector.detect(userMessage);
if (msgLang.isHighConfidence()) {
updateSessionLanguage(sessionId, msgLang.getLanguageTag());
}
// 5. 验证输出语言(可选,成本高,生产环境按需启用)
// validateOutputLanguage(content, sessionLanguage);
return ChatResult.builder()
.content(content)
.sessionId(sessionId)
.detectedLanguage(msgLang.getLanguageTag())
.sessionLanguage(sessionLanguage)
.build();
}
/**
* 获取会话语言:优先从 Redis 缓存取,没有则从当前消息检测
*/
private String getSessionLanguage(String sessionId, String firstMessage) {
String cachedLanguage = redisTemplate.opsForValue()
.get("session:language:" + sessionId);
if (cachedLanguage != null) {
return cachedLanguage;
}
// 首条消息,检测语言并缓存
LanguageDetector.DetectedLanguage detected =
languageDetector.detect(firstMessage);
String language = detected.getLanguageTag();
updateSessionLanguage(sessionId, language);
return language;
}
private void updateSessionLanguage(String sessionId, String language) {
redisTemplate.opsForValue().set(
"session:language:" + sessionId,
language,
Duration.ofHours(24)
);
}
}Embedding 的语言适配
RAG 应用里,语言适配还多了一层:
@Service
public class MultilingualRAGService {
@Autowired
private EmbeddingModel embeddingModel; // 多语言 Embedding 模型
@Autowired
private VectorStore vectorStore;
@Autowired
private LanguageDetector languageDetector;
/**
* 多语言 RAG 检索
*/
public List<Document> retrieve(String query, String language) {
// 使用多语言 Embedding 模型,无需区分语言
// 推荐:multilingual-e5-large, paraphrase-multilingual-mpnet-base-v2
List<Double> queryEmbedding = embeddingModel.embed(query);
SearchRequest searchRequest = SearchRequest.query(query)
.withTopK(5)
.withSimilarityThreshold(0.7);
// 可以通过 filter 来限定语言(如果向量库里存了语言标签)
if (language != null && !"auto".equals(language)) {
searchRequest = searchRequest.withFilterExpression(
"language == '" + language + "' || language == 'multilingual'");
}
return vectorStore.similaritySearch(searchRequest);
}
/**
* 多语言 RAG 完整流程
*/
public String ragChat(String userQuery) {
// 1. 检测语言
LanguageDetector.DetectedLanguage lang = languageDetector.detect(userQuery);
// 2. 检索相关文档
List<Document> relevantDocs = retrieve(userQuery, lang.getLanguageTag());
// 3. 构建上下文
String context = relevantDocs.stream()
.map(Document::getContent)
.collect(Collectors.joining("\n\n---\n\n"));
// 4. 构建多语言 Prompt
String prompt = String.format("""
Based on the following context, answer the user's question.
Context:
%s
User Question: %s
IMPORTANT: Respond in the same language as the user's question (%s).
""", context, userQuery, lang.getLanguageTag());
return chatModel.call(prompt);
}
}多语言 AI 请求处理流程
i18n 框架与 AI 语言检测的结合
@RestController
@RequestMapping("/api/chat")
public class MultilingualChatController {
@Autowired
private MultilingualChatService chatService;
@Autowired
private MessageSource messageSource; // Spring i18n
@PostMapping
public ResponseEntity<?> chat(
@RequestBody ChatRequest request,
@RequestHeader(value = "Accept-Language", defaultValue = "en") String acceptLanguage,
HttpServletRequest httpRequest) {
// 确定 UI 语言(用于返回错误消息等 UI 文本)
Locale uiLocale = LocaleContextHolder.getLocale();
// 调用多语言 AI 服务
try {
ChatResult result = chatService.chat(
request.getSessionId(),
request.getMessage(),
getUserId(httpRequest)
);
return ResponseEntity.ok(result);
} catch (TokenQuotaExceededException e) {
// 错误消息用 UI 语言返回
String errorMsg = messageSource.getMessage(
"error.quota.exceeded",
null,
uiLocale
);
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(Map.of("error", errorMsg));
}
}
private String getUserId(HttpServletRequest request) {
// 从 JWT 或 Session 获取用户 ID
return "user-123"; // 简化示例
}
}i18n 资源文件:
# messages_zh_CN.properties
error.quota.exceeded=今日使用额度已满,请明天再来
error.rate.limit=请求太频繁,请稍后再试
error.ai.unavailable=AI服务暂时不可用,请稍后重试
# messages_en.properties
error.quota.exceeded=Daily quota exceeded, please try again tomorrow
error.rate.limit=Too many requests, please try again later
error.ai.unavailable=AI service is temporarily unavailable总结
AI 应用国际化的核心要点:
语言指令优于翻译 Prompt:与其维护多套语言版本的 Prompt,不如用语言检测 + 动态注入语言指令的方式,让同一个 Prompt 适配所有语言。
会话语言状态要持久化:用户在会话中使用的语言,要缓存在 Redis 里,后续消息才能保持语言一致性。
检测置信度影响决策:低置信度的检测结果不应该覆盖会话语言,避免混合语言或代码片段被误判。
Embedding 用多语言模型:RAG 场景用多语言 Embedding 模型(如 multilingual-e5),不需要为每种语言单独维护索引。
UI i18n 和 AI 语言检测是分开的:UI 语言跟随用户设置,AI 回复语言跟随用户输入——这两个通常一致,但不强耦合。
