项目路径: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 选择模型后端

在线快速体验

完全本地运行

需要翻译服务

选择后端

DashScope 路线

Ollama 路线

获取阿里云 API Key 并配置

安装 Ollama 并拉取模型

路线 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 的调用链:

Client DashScope API (dashscope.aliyuncs.com) ChatModel (DashScopeChatModel) Controller Client DashScope API (dashscope.aliyuncs.com) ChatModel (DashScopeChatModel) Controller call(new Prompt(prompt, options)) POST /api/v1/services/aigc/text-generation/generation JSON response (choices[0].message.content) ChatResponse TranslateResponse JSON

3.2 流式翻译 (SSE) 原理

当 Controller 方法返回 Flux<String> 时,Spring MVC 会自动将响应切换为 text/event-stream 格式,实现 Server-Sent Events。DashScope 流式 API 每产生一个 token 就推送一次,前端可以像打字机一样逐字显示。

DashScope Controller Browser DashScope Controller Browser loop [逐个 token] GET /stream?text=Hello stream(Prompt) data: {token: "你"} data: 你 data: {token: "好"} data: 好

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
  • ChatResponseChatResultGenerationtext 调用链过长,可以用 response.getResult().getOutput().getContent() 或直接利用 ChatClient.content() 简化。
/stream 流式翻译
  • 没有过滤 null 值。某些响应块 .getText() 可能为 null,会导致 Flux 中混入 null修复:应在 map 后加 .filter(Objects::nonNull)
/custom 自定义参数
  • 虽然名字叫 custom,但 temperaturetopP 等参数都硬编码在代码里,用户根本无法自定义。真正的自定义端点应允许请求参数动态传入这些值,或至少提供一个 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);
}

不同模型实现为 DashScopeTranslationServiceOllamaTranslationService,通过 @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 架构建议

综合以上替代方案,一个更健壮的翻译服务可以采用如下分层:

TranslationService

Controller Layer

TranslationService Interface

CachingDecorator

AI Service Impl
DashScope/Ollama

Spring AI ChatClient

模型 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 下一步行动建议

  1. 立即修复 P0 问题,使 Markdown 端点可用。
  2. 短期优化:引入 ChatClient 统一调用、抽象 TranslationService
  3. 中长期演进:增加缓存、术语库、可观测性,并考虑混合翻译 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"
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐