【第36篇】Translate Example
项目路径:
spring-ai-alibaba-usecase-example/spring-ai-alibaba-translate-example
涵盖版本:Spring Boot 3.5.7 / Spring AI 1.1.0 / Spring AI Alibaba 1.1.2.1
1. 概述
这是一个示例项目,展示如何用 Spring AI + Spring AI Alibaba 将大模型翻译能力集成到 Spring Boot 应用中。它同时演示了两种模型接入方式:
| 后端 | 提供方 | 启动要求 | 适用场景 |
|---|---|---|---|
| Ollama | 本地/自托管 | 运行 Ollama 服务 | 离线、数据不出本地 |
| DashScope | 阿里云通义千问 | API Key | 高质量、低延迟、线上服务 |
项目露出 5 个 REST 端点,涵盖文件翻译、基础翻译、流式翻译、自定义参数以及 Markdown 文件翻译。
阅读完本指南后,你不仅能顺利在本地把服务跑起来,还能理解它背后的抽象设计,并得到一套清晰可执行的优化方案——包括代码修复、架构重构、缓存策略等。
2. 快速本地部署与验证
环境要求:JDK 17+、Maven 3.8+、curl。
项目路径(你的本地):
/home/tht/examples-main/spring-ai-alibaba-usecase-example/spring-ai-alibaba-translate-example
2.1 构建项目
# 进入父模块所在的目录
cd /home/tht/examples-main/spring-ai-alibaba-usecase-example
# 构建 translate 子模块,并一同编译依赖的父 POM
mvn clean install -pl spring-ai-alibaba-translate-example -am -DskipTests
若出现父 POM 版本解析失败,可显式指定
-Drevision=1.0.0。
如果依赖已全部缓存到本地,可追加-o离线构建。
2.2 选择模型后端
路线 A(推荐快速体验):DashScope
- 访问 DashScope 控制台 创建 API Key。
- 设置环境变量:
export AI_DASHSCOPE_API_KEY=sk-你的key
路线 B:Ollama
# 安装 Ollama
curl -fsSL https://ollama.com/install.sh | sh
# 下载模型(推荐 qwen2.5:7b)
ollama pull qwen2.5:7b
# 配置环境变量
export OLLAMA_BASE_URL=http://localhost:11434
export OLLAMA_MODEL=qwen2.5:7b
2.3 启动与验证
cd /home/tht/examples-main/spring-ai-alibaba-usecase-example/spring-ai-alibaba-translate-example
mvn spring-boot:run
看到 Started TranslateApplication 后,用 curl 进行冒烟测试:
# 基础翻译
curl "http://localhost:8080/api/dashscope/translate/simple?text=你好世界&sourceLanguage=中文&targetLanguage=英文"
# 期望:{"translatedText":"Hello World"}
# 流式翻译(SSE 打字效果)
curl -N "http://localhost:8080/api/dashscope/translate/stream?text=床前明月光&sourceLanguage=中文&targetLanguage=英文"
注意:Markdown 翻译端点在原项目中存在严重缺陷——只返回
"success"不返回译文。修复方法见 第8章。
3. 架构与核心原理
3.1 Spring AI 的抽象层次
Spring AI 的设计理念是统一抽象,多厂商实现,类似 JDBC。项目中有两种使用层次:
| Bean | 类型 | 抽象层级 | 使用示例 |
|---|---|---|---|
ChatClient |
高层 API | 提供链式调用、Advisor 拦截 | chatClient.prompt("...").call().content() |
DashScopeChatModel / OllamaChatModel |
底层厂商实现 | 直接 call(Prompt) / stream(Prompt) |
chatModel.call(new Prompt(...)) |
下面的 mermaid 图展示了从 Controller 到最终 LLM 的调用链:
3.2 流式翻译 (SSE) 原理
当 Controller 方法返回 Flux<String> 时,Spring MVC 会自动将响应切换为 text/event-stream 格式,实现 Server-Sent Events。DashScope 流式 API 每产生一个 token 就推送一次,前端可以像打字机一样逐字显示。
4. 配置层解读与修正
4.1 现有 application.yml
spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 20MB
ai:
ollama:
base-url: ${OLLAMA_BASE_URL}
chat:
options:
model: ${OLLAMA_MODEL}
dashscope:
api-key: ${AI_DASHSCOPE_API_KEY:your_api_key_here}
server:
port: 8080
解读与纠正:
- 环境变量
AI_DASHSCOPE_API_KEY→ 赋值给spring.ai.dashscope.api-key完全正确,因为启动类里手动构建DashScopeChatModel时通过@Value("${spring.ai.dashscope.api-key}")注入。无需修改。 - DashScope 的默认模型
qwen-plus没有出现在配置中,而是在代码里硬编码。这带来了维护问题,后续优化会将模型名提取到配置中。
4.2 可改进的配置
建议在 application.yml 中补充:
spring:
ai:
dashscope:
chat:
options:
model: ${DASHSCOPE_MODEL:qwen-plus}
temperature: 0.3
运行时可通过环境变量 DASHSCOPE_MODEL 自由切换 qwen-turbo / qwen-max 等。
5. 启动类深度解析
@SpringBootApplication
public class TranslateApplication {
// Bean 1:高层 ChatClient(用于 Ollama)
@Bean
public ChatClient chatClient(OllamaChatModel ollamaChatModel) {
return ChatClient.builder(ollamaChatModel)
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
// Bean 2:底层 DashScopeChatModel(用于 DashScope 调用)
@Bean
public DashScopeChatModel dashScopeChatModel(
@Value("${spring.ai.dashscope.api-key:#{null}}") String apiKey) {
DashScopeChatOptions options = DashScopeChatOptions.builder()
.withModel(DashScopeModel.ChatModel.QWEN_PLUS.getValue())
.build();
return DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder().apiKey(apiKey).build())
.defaultOptions(options)
.build();
}
}
5.1 两个 Bean 的抽象差异
ChatClient是推荐的高层 API,内置了模板渲染、Advisor 拦截(如日志)、重试等功能。Ollama 由于 Spring Boot 自动配置会创建OllamaChatModel,只需包裹成ChatClient即可享用这些能力。DashScopeChatModel目前是手工构建的底层模型对象,直接注入给 Controller。这导致 DashScope 相关的端点没有享受到ChatClient的便利。后续架构优化建议统一为 ChatClient。
5.2 SimpleLoggerAdvisor
它会在每次调用时自动打印 Prompt 和 Response 日志,对开发调试非常友好。但在生产环境建议通过配置文件控制开启/关闭。
6. Controller 层逐端点分析
6.1 TranslateController(Ollama 文件翻译)
@PostMapping("/file")
public ResponseEntity<TranslateResponse> translateFile(
@RequestPart("file") MultipartFile file,
@RequestPart("targetLang") String targetLang) { ... }
存在的问题:
targetLang使用@RequestPart语义不当,应改为@RequestParam。- 缺少源语言参数,完全靠模型猜测。
- Prompt 是字符串拼接,且无类型校验。
6.2 DashScopeTranslateController —— 四个端点
/simple 基础翻译
- 模型硬编码
QWEN_PLUS。 ChatResponse→ChatResult→Generation→text调用链过长,可以用response.getResult().getOutput().getContent()或直接利用ChatClient的.content()简化。
/stream 流式翻译
- 没有过滤
null值。某些响应块.getText()可能为null,会导致 Flux 中混入null。修复:应在map后加.filter(Objects::nonNull)。
/custom 自定义参数
- 虽然名字叫 custom,但
temperature、topP等参数都硬编码在代码里,用户根本无法自定义。真正的自定义端点应允许请求参数动态传入这些值,或至少提供一个 DTO 接收。
/markdown-file Markdown 文件翻译 (严重缺陷)
- 返回
new TranslateResponse("success")而不是译文内容,导致功能形同虚设。 - Service 层将结果写入了
~/Desktop/translated/,而客户端完全不知道。
6.3 响应模型 TranslateResponse
public record TranslateResponse(String translatedText) { }
简单清晰,但应扩充字段以支持 token 消耗、错误消息等,例如:
public record TranslateResponse(String translatedText, Integer tokensUsed, String error) {}
7. Service 层与 Prompt 工程
7.1 MarkdownTranslationService 职责混乱
当前 Service 既负责翻译又负责文件保存,违反单一职责。更好的做法是 Service 只返回译文内容,由 Controller 决定如何响应(返回给客户端 / 保存 / 缓存)。
7.2 多余的 String.valueOf 转换
Prompt prompt = new Prompt(
String.valueOf(promptTemplate.create(params)),
buildTranslationOptions()
);
promptTemplate.create(params) 已经返回 Prompt,再 String.valueOf 转成字符串后又构建新的 Prompt,完全多余。正确写法:
Prompt prompt = promptTemplate.create(params, buildTranslationOptions());
7.3 Prompt 模板分析
原模板已具备结构化雏形,但缺少输出格式约束、Few-shot 示例以及对内联代码的保护。改进后的模板(见第8章 Prompt 优化)可大幅提高翻译一致性。
8. 已发现的问题与修复方案
我们按严重程度列出所有发现的问题,并提供可直接应用的修复代码。
🔴 P0 严重:Markdown 翻译未返回内容
位置:DashScopeTranslateController.translateMarkdownFile()
修复(同时调整 Service 返回内容):
// MarkdownTranslationService
public String translateMarkdown(String content, String sourceLang, String targetLang) throws IOException {
PromptTemplate promptTemplate = new PromptTemplate(markdownPromptResource);
Prompt prompt = promptTemplate.create(
Map.of("sourceLanguage", sourceLang, "targetLanguage", targetLang, "markdownContent", content),
buildTranslationOptions()
);
return dashScopeChatModel.call(prompt)
.getResult().getOutput().getContent();
}
// Controller
@PostMapping("/markdown-file")
public ResponseEntity<TranslateResponse> translateMarkdownFile(
@RequestParam("file") MultipartFile file,
@RequestParam(defaultValue = "英文") String sourceLanguage,
@RequestParam(defaultValue = "中文") String targetLanguage) throws IOException {
String originalContent = new String(file.getBytes(), StandardCharsets.UTF_8);
String translated = markdownTranslationService.translateMarkdown(
originalContent, sourceLanguage, targetLanguage);
return ResponseEntity.ok(new TranslateResponse(translated));
}
🔴 P0 严重:Service 返回文件路径而非内容
通过上述重构解决,Service 不再触碰文件系统,Controller 直接处理 MultipartFile。
🟡 P1 中等:模型名硬编码
在 application.yml 配置:
spring.ai.dashscope.chat.options.model: ${DASHSCOPE_MODEL:qwen-plus}
启动类中读取配置:
@Value("${spring.ai.dashscope.chat.options.model}")
private String modelName;
// 构建 options
DashScopeChatOptions options = DashScopeChatOptions.builder()
.withModel(modelName)
.build();
🟡 P1 中等:流式翻译 null 风险
return stream
.map(resp -> resp.getResult().getOutput().getContent())
.filter(Objects::nonNull);
🟡 P1 中等:Prompt 缺少输出约束与示例
改进版 Prompt 模板(markdown-translation-prompt.st):
你是一名专业的 Markdown 翻译引擎。
【角色】将 Markdown 文档从 {sourceLanguage} 翻译为 {targetLanguage}。
仅输出翻译后的 Markdown 文本,不要添加任何解释。
【规则】
1. 保持所有 Markdown 语法不变。
2. 不翻译代码块 ```...```及内联代码 `...`。
3. 不翻译 URL、路径、版本号、环境变量。
4. 技术术语保留英文。
5. 只返回译文,不加前缀后缀。
【示例】
原文: ## Hello World\n\nThis is a **bold** `word`.
译文: ## 你好世界\n\n这是一个**粗体** `word`。
【内容】
{markdownContent}
🟢 P2 较低:参数注解不当 & 缺少源语言
- 将
@RequestPart("targetLang")改为@RequestParam("targetLang")。 - Ollama 端点增加
sourceLanguage参数。
🟢 P3 较低:文件名后缀硬编码
原代码 originalFilename_zh.md 永远用 _zh,修正为根据目标语言动态选择后缀。
9. 替代方案与优化设计
这一章从架构层面提出多条清晰可落地的改进路线,解决当前项目的扩展性、一致性和生产就绪问题。
9.1 替代 A:统一为 ChatClient 调用(消除 API 不一致)
目前 DashScope 端直接用 DashScopeChatModel,而 Ollama 端用了 ChatClient。这导致两套调用风格、日志、重试等能力不统一。
优化后:
@Bean
public ChatClient dashScopeChatClient(DashScopeChatModel model) {
return ChatClient.builder(model)
.defaultAdvisors(new SimpleLoggerAdvisor())
.build();
}
Controller 中统一注入 ChatClient:
chatClient.prompt()
.user(userText)
.advisors(...)
.call()
.content();
带来的好处:
- 一套 API 打天下,切换模型只需换 Bean。
- 自然享有 ChatClient 内置的 Advisor 链、重试、观测等特性。
9.2 替代 B:抽象 TranslationService 接口(依赖倒置)
public interface TranslationService {
String translate(String text, String source, String target);
Flux<String> translateStream(String text, String source, String target);
}
不同模型实现为 DashScopeTranslationService、OllamaTranslationService,通过 @ConditionalOnProperty 决定启用哪个。控制器仅依赖接口,切换后端无需改代码。
9.3 替代 C:增加缓存层(减少重复翻译成本)
对于批量翻译、文档翻译场景,同样的句子可能被多次请求,引入缓存可以大幅降低成本。
@Service
public class CachedTranslationService implements TranslationService {
private final TranslationService delegate;
private final StringRedisTemplate redis;
@Override
public String translate(String text, String source, String target) {
String key = "tl:" + Hashing.md5().hashString(text+source+target, UTF_8);
String cached = redis.opsForValue().get(key);
if (cached != null) return cached;
String translated = delegate.translate(text, source, target);
redis.opsForValue().set(key, translated, Duration.ofHours(24));
return translated;
}
}
9.4 替代 D:统一异常处理与响应格式
当前异常响应不统一,有的返回 TranslateResponse("success") 之类的信息。增加全局异常处理:
@RestControllerAdvice
public class TranslationExceptionHandler {
@ExceptionHandler(Exception.class)
public ResponseEntity<TranslateResponse> handle(Exception e) {
return ResponseEntity.status(500)
.body(new TranslateResponse(null, "Translation failed: " + e.getMessage()));
}
}
同时扩展 TranslateResponse 增加 error 字段,区分正常结果和错误。
9.5 生产级 Translation API 架构建议
综合以上替代方案,一个更健壮的翻译服务可以采用如下分层:
- 术语库:大模型 + 自定义术语表,保证专业名词翻译准确。
- 缓存:Redis 减少重复调用。
- ChatClient 统一:享受 Spring AI 所有高层能力。
10. 综合总结与改进清单
10.1 项目价值
- 完整演示了 Spring AI 的
ChatModel/ChatClient/PromptTemplate/ Advisor。 - 双后端模型对比清晰。
- 流式、非流式、Markdown 场景俱全。
10.2 已知问题速查
| 优先级 | 问题 | 修复思路 |
|---|---|---|
| 🔴 P0 | Markdown 翻译未返回内容 | Service 直接返回内容,Controller 返回 TranslateResponse |
| 🔴 P0 | Service 混淆文件路径与内容 | 重构为纯服务,无 IO 依赖 |
| 🟡 P1 | 模型硬编码 | 引入配置项,一处变更全局生效 |
| 🟡 P1 | 流式 null | 添加 .filter(Objects::nonNull) |
| 🟡 P1 | Prompt 不完整 | 增加输出约束、Few-shot |
| 🟢 P2 | 参数注解不当 | @RequestParam 统一 |
| 🟢 P3 | 文件名后缀固定 | 根据 targetLanguage 动态生成 |
10.3 下一步行动建议
- 立即修复 P0 问题,使 Markdown 端点可用。
- 短期优化:引入
ChatClient统一调用、抽象TranslationService。 - 中长期演进:增加缓存、术语库、可观测性,并考虑混合翻译 API(大模型 + 专业翻译 API)的设计。
11. 附录:常用命令与速查表
| 操作 | 命令 |
|---|---|
| 构建 | mvn clean install -pl spring-ai-alibaba-translate-example -am -DskipTests |
| 启动 | mvn spring-boot:run |
| 中文→英文 | curl "localhost:8080/api/dashscope/translate/simple?text=你好&sourceLanguage=中文&targetLanguage=英文" |
| 流式翻译 | curl -N "localhost:8080/api/dashscope/translate/stream?text=长文本&sourceLanguage=中文&targetLanguage=英文" |
| 检查 Ollama 模型 | ollama list |
| 切换端口 | mvn spring-boot:run -Dspring-boot.run.arguments="--server.port=9090" |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)