Spring AI 接入指南:Ollama 本地模型、API 代理与混合路由

本篇为补充专题 02b,深入讲解三类本地/低成本接入方案的完整配置与代码实战。

1. 为什么需要本地模型接入?

在生产环境中,调用 OpenAI / Claude API 存在三大痛点:

痛点 影响
数据隐私 用户数据不能离开企业内网,合规审计严格
网络延迟 国内访问海外 API 往返 >300ms,体验差
成本压力 GPT-4o 约 $2.5/1M tokens,中小项目难以承受

Spring AI 提供了三大接入路径:本地 Ollama、DashScope API(国内直连低价)、API 代理(如 OneAPI)统一管理多模型,本篇逐一拆解。
在这里插入图片描述


2. Ollama:本地大模型零成本运行

2.1 安装与模型下载

macOS / Linux 一行安装:

# 一键安装
curl -fsSL https://ollama.com/install.sh | sh

# Windows 直接下载安装包:https://ollama.com/download

拉取常用模型(根据显存选型):

# 通义千问 7B(推荐入门,int4 量化约 4GB 显存)
ollama pull qwen2.5:7b

# Llama 3.1 8B(通用能力强)
ollama pull llama3.1:8b

# Mistral 7B(指令遵循优秀)
ollama pull mistral

# Phi-3.5(微软小模型,CPU 可跑)
ollama pull phi3.5:latest

# bge-m3 嵌入模型(知识库必备)
ollama pull nomic-embed-text

显存参考表:

模型 参数量级 FP16 显存 int4 量化显存 CPU 可跑
Phi-3.5 3.8B ~8GB ~2GB
Qwen2.5 7B 7B ~14GB ~4GB ⚠️ 慢
Llama 3.1 8B 8B ~16GB ~5GB
Qwen2.5 14B 14B ~28GB ~8GB
Qwen2.5 32B 32B ~64GB ~18GB

验证安装:

ollama list
# NAME              ID           SIZE      MODIFIED
# qwen2.5:7b        a3........   4.7GB     5 minutes ago
# nomic-embed-text  0a........   274MB     2 hours ago

ollama run qwen2.5:7b "用一句话解释什么是 RAG"
# RAG(检索增强生成)是通过检索外部知识库来增强大语言模型生成能力的技术

2.2 Modelfile:自定义模型行为

Ollama 支持用 Modelfile 配置文件深度定制模型行为,无需重新训练。

场景一:固定系统提示词,打造专属客服人格

# 创建文件:./modelfiles/rag-assistant
FROM qwen2.5:7b
PARAMETER temperature 0.3
PARAMETER top_p 0.85
PARAMETER num_ctx 8192

TEMPLATE """
{% for message in messages %}
{{ '[BOT]' if message['role'] == 'assistant' else '[USER]' }}
{{ message['content'] }}
{% endfor %}
[BOT]
"""

SYSTEM """
你是一个专业的企业知识库助手。回答必须:
1. 基于提供的上下文信息回答,不要编造
2. 如果上下文没有相关信息,诚实告知用户
3. 回答简洁有条理,优先使用列表格式
"""

场景二:流式输出配置(工程化必须)

# ./modelfiles/fast-response
FROM qwen2.5:7b
PARAMETER temperature 0.7
PARAMETER num_predict 512        # 最大生成长度
PARAMETER stop "[DONE]"          # 停止词
PARAMETER num_ctx 4096           # 上下文窗口

创建并测试自定义模型:

ollama create rag-assistant -f ./modelfiles/rag-assistant
ollama run rag-assistant "Spring Boot 如何集成 Redis?"

2.3 Spring AI + Ollama 集成

Maven 依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>

application.yml 配置:

spring:
  application:
    name: spring-ai-ollama-demo

  ai:
    ollama:
      base-url: http://localhost:11434
      chat:
        options:
          model: qwen2.5:7b
          temperature: 0.7
          numCtx: 8192
          keepAlive: 5m

# 嵌入模型单独配置(知识库用)
  rag:
    embedding:
      model: nomic-embed-text

基础对话代码:

@RestController
@RequestMapping("/ai")
@RequiredArgsConstructor
public class OllamaChatController {

    private final ChatClient chatClient;

    @GetMapping("/chat")
    public String chat(@RequestParam(defaultValue = "你好,介绍一下你自己") String question) {
        return chatClient.prompt()
                .user(question)
                .call()
                .content();
    }

    // 流式响应:适合前端 SSE 推送
    @GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> chatStream(@RequestParam String question) {
        return chatClient.prompt()
                .user(question)
                .stream()
                .content();
    }
}

知识库嵌入配置:

@Configuration
public class OllamaEmbeddingConfig {

    @Bean
    public OllamaEmbeddingModel embeddingModel(OllamaApi api) {
        return new OllamaEmbeddingModel(api);
    }

    @Bean
    public VectorStore vectorStore(OllamaEmbeddingModel embeddingModel) {
        // 使用 SimpleVectorStore(轻量级,生产推荐 PGVector/Chroma)
        return new SimpleVectorStore(embeddingModel);
    }
}

导入文档到向量库:

@Service
@RequiredArgsConstructor
public class DocumentIngestionService {

    private final OllamaEmbeddingModel embeddingModel;
    private final VectorStore vectorStore;

    public void ingestDocument(Path pdfPath) throws Exception {
        // PDF 解析
        PdfDocumentReader pdfReader = new PdfDocumentReader(pdfPath.toUri().toString());
        var document = pdfReader.get();

        // 文本分割(控制每段 token 数量)
        var splitter = new TokenTextSplitter(800, 200, 10, 5000, true);
        var chunks = splitter.apply(List.of(document));

        // 写入向量库
        vectorStore.add(chunks);

        System.out.println("✅ 已导入 " + chunks.size() + " 个文本块到向量库");
    }

    public List<Document> search(String query, int topK) {
        return vectorStore.similaritySearch(
            SearchRequest.query(query).withTopK(topK)
        );
    }
}

3. DashScope API:国内低价直连通义千问

3.1 为什么选 DashScope?

对比项 OpenAI API DashScope
网络 需代理,延迟高 国内直连 <50ms
价格 GPT-4o $2.5/1M Qwen-Turbo ¥0.002/1K tokens
政策 存在封号风险 阿里云合规保障
开源模型 ✅ 支持开源模型 API

获取 API Key:

  1. 阿里云百炼平台 → 开通服务 → 创建 API Key
  2. 或阿里云 DashScope 控制台

3.2 Spring AI DashScope 配置

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-alibaba-spring-boot-starter</artifactId>
</dependency>
spring:
  ai:
    dashscope:
      api-key: ${DASHSCOPE_API_KEY}    # 推荐从环境变量读取
      base-url: https://dashscope.aliyuncs.com
      chat:
        options:
          model: qwen-turbo
          # qwen-plus / qwen-max 也可选
          temperature: 0.8
          max-tokens: 2000
@RestController
@RequestMapping("/ai")
public class DashScopeController {

    private final ChatClient chatClient;

    public DashScopeController(ChatClient.Builder builder) {
        this.chatClient = builder
            .defaultSystem("你是一个 Java 技术专家,用简洁专业的语言回答问题")
            .build();
    }

    @PostMapping("/ask")
    public Map<String, Object> ask(@RequestBody Map<String, String> request) {
        String question = request.get("question");
        String answer = chatClient.prompt()
            .user(question)
            .call()
            .content();

        return Map.of("question", question, "answer", answer);
    }
}

3.3 DashScope 嵌入模型(知识库必备)

spring:
  ai:
    dashscope:
      embedding:
        options:
          model: text-embedding-v3   # 阿里自研嵌入模型,1536 维
// 配合 RAG 使用
@Service
public class RAGService {

    private final VectorStore vectorStore;
    private final ChatClient chatClient;

    public String ragQuery(String question) {
        // Step 1: 检索相关上下文
        List<Document> contextDocs = vectorStore.similaritySearch(
            SearchRequest.query(question).withTopK(5)
        );

        String context = contextDocs.stream()
            .map(Document::getText)
            .collect(Collectors.joining("\n\n"));

        // Step 2: 构造有上下文的 Prompt
        String prompt = String.format("""
            基于以下参考资料回答用户问题。如果资料中没有相关信息,请如实说明。

            参考资料:
            %s

            用户问题:%s
            """, context, question);

        // Step 3: 调用通义千问
        return chatClient.prompt()
            .user(prompt)
            .call()
            .content();
    }
}

4. API 代理:OneAPI 统一管理多模型

4.1 为什么需要 API 代理层?

在企业场景中,通常需要同时接入多个 AI 供应商:

业务代码
    ↓ 统一调用
OneAPI(本地代理)
    ├── OpenAI(主力)
    ├── Claude(备选)
    ├── DashScope(国内备选)
    └── Ollama(内网补充)

核心价值:

  • 统一接口:业务代码只对接一个端点,换模型改配置不碰代码
  • 负载均衡:多渠道 token 轮询,降低单渠道限流风险
  • 用量统计:一个面板看所有模型的调用量和费用
  • 密钥管理:API Key 集中存储,不分散在各个服务中

4.2 OneAPI 快速部署

# Docker 一键部署
docker run -d \
  --name oneapi \
  -p 3000:3000 \
  -v ~/oneapi/data:/data \
  --restart always \
  ghcr.io/songquanpeng/one-api:latest

# 访问 http://localhost:3000
# 默认账号:root   默认密码:123456

配置渠道(以 OpenAI 为例):

  1. 登录管理后台 → 渠道管理 → 添加渠道
  2. 类型选 OpenAI,填入 API Key 和 base-url(国内需代理)
  3. 模型列表填入:gpt-4o,gpt-4o-mini,gpt-3.5-turbo,gpt-4-turbo
  4. 额度设置合理阈值

配置渠道(以 DashScope 为例):

  • 类型选 自定义,base-url 填:https://dashscope.aliyuncs.com/compatible-mode/v1
  • API Key 填阿里云 DashScope Key
  • 模型列表:qwen-turbo,qwen-plus,qwen-max,text-embedding-v3

4.3 Spring AI 通过 OneAPI 接入

spring:
  ai:
    openai:
      base-url: http://localhost:3000/v1   # OneAPI 代理地址
      api-key: ${ONEAPI_API_KEY}           # OneAPI 的 API Key(管理后台→个人API密钥)
      chat:
        options:
          model: gpt-4o                    # 可动态切换:gpt-4o / qwen-turbo / claude-3-5-sonnet

效果:业务代码完全不用改,只需改配置中的模型名称!

// 同一个 ChatClient,换模型只需改 yml
@RestController
public class ChatController {

    private final ChatClient chatClient;

    public ChatController(ChatClient.Builder builder) {
        // 配置统一从 yml 读取,这里零改动
        this.chatClient = builder.build();
    }
}

4.4 模型切换策略实战

@Service
public class SmartChatService {

    private final OpenAiApi openAiApi;

    // 简单路由:根据输入长度选择模型
    public String chatWithRouting(String input) {
        String model;
        if (input.length() < 200) {
            // 简单问题用小模型,省钱
            model = "gpt-4o-mini";
        } else if (input.length() < 2000) {
            // 中等复杂度用主力模型
            model = "gpt-4o";
        } else {
            // 复杂任务用最强模型
            model = "gpt-4-turbo";
        }

        return ChatClient.builder(new OpenAiChatModel(openAiApi, OpenAiChatOptions.builder()
                .withModel(model)
                .withTemperature(0.7)
                .build()))
            .build()
            .prompt()
            .user(input)
            .call()
            .content();
    }

    // 熔断降级:主模型失败自动切换备选
    public String chatWithFallback(String input) {
        try {
            return primaryChat(input);
        } catch (Exception e) {
            System.err.println("主模型调用失败,切换备选:" + e.getMessage());
            return fallbackChat(input);
        }
    }

    private String primaryChat(String input) {
        return ChatClient.builder(new DashScopeChatModel(DashScopeApi.builder()
                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                .build(),
                DashScopeChatOptions.builder()
                    .withModel("qwen-plus")
                    .build()))
            .build()
            .prompt()
            .user(input)
            .call()
            .content();
    }

    private String fallbackChat(String input) {
        return ChatClient.builder(new OpenAiApi("http://localhost:3000/v1",
                System.getenv("ONEAPI_API_KEY")))
            .build()
            .prompt()
            .user(input)
            .call()
            .content();
    }
}

5. 混合路由:本地 + 云端智能调度

在实际生产中,最优方案是分层使用

用户请求
    │
    ▼
┌─────────────────────────────┐
│  路由层(Spring AI Router)   │
└─────────────────────────────┘
    │
    ├── 简单/隐私数据 ──→ Ollama 本地(零成本,极速)
    │
    ├── 复杂推理 ──→ DashScope(国内低价,高性能)
    │
    └── 最高质量要求 ──→ OpenAI / Claude(最强能力)

5.1 路由策略设计

@Component
public class ModelRouter {

    private final OllamaChatModel localModel;
    private final DashScopeChatModel dashscopeModel;
    private final OpenAiApi openaiApi;

    /**
     * 根据请求特征选择最优模型
     * @param input 用户输入
     * @param mode  业务场景:SIMPLE / COMPLEX / PREMIUM
     */
    public String route(String input, String mode) {
        return switch (mode) {
            case "SIMPLE" -> callLocal(input);
            case "COMPLEX" -> callDashScope(input);
            case "PREMIUM" -> callOpenAI(input);
            default -> callDashScope(input); // 默认用 DashScope
        };
    }

    private String callLocal(String input) {
        return ChatClient.builder(localModel)
            .build()
            .prompt().user(input).call().content();
    }

    private String callDashScope(String input) {
        return ChatClient.builder(dashscopeModel)
            .build()
            .prompt().user(input).call().content();
    }

    private String callOpenAI(String input) {
        return ChatClient.builder(new OpenAiChatModel(openaiApi,
                OpenAiChatOptions.builder().withModel("gpt-4o").build()))
            .build()
            .prompt().user(input).call().content();
    }
}

5.2 根据隐私程度自动路由

public String privacyAwareRoute(String input, boolean containsUserData) {
    if (containsUserData) {
        // 敏感数据强制走本地,避免合规风险
        return "🚫 包含用户隐私数据,已路由至本地模型处理:" + callLocal(input);
    } else {
        // 非敏感数据走云端,享受最强能力
        return "📤 已路由至云端高性能模型:" + callDashScope(input);
    }
}

6. 性能对比实测

测试环境:MacBook M2 Pro(32GB),网络为上海家宽

方案 模型 首次响应 成本 适合场景
Ollama Qwen2.5 7B int4 ~1.2s ¥0 内网/隐私/开发
Ollama Llama 3.1 8B int4 ~1.8s ¥0 通用对话
DashScope Qwen-Turbo ~0.8s ¥0.002/1K 国内生产主力
DashScope Qwen-Plus ~1.2s ¥0.02/1K 高质量需求
OneAPI→OpenAI GPT-4o-mini ~1.5s ~$0.15/1M 复杂推理

7. 选型决策树

输入类型是什么?
│
├─ 包含敏感/隐私数据?
│   └─ ✅ 是 ──→ Ollama 本地(qwen2.5:7b)
│
├─ 国内生产环境,低成本优先?
│   └─ ✅ 是 ──→ DashScope Qwen-Turbo/Plus
│
├─ 需要最强推理能力(代码/复杂分析)?
│   └─ ✅ 是 ──→ OpenAI GPT-4o / Claude 3.5
│
└─ 多模型混合使用?
    └─ ✅ 是 ──→ OneAPI + 智能路由层

8. 常见问题

Q1: Ollama 模型更新后需要重启吗?

不需要。ollama pull 会自动更新。正在运行的模型不受影响,下次调用时自动使用新版本。

Q2: OneAPI 和直接调 API 的性能差异?

OneAPI 多一跳 HTTP 转发,延迟增加约 5-20ms。对于 LLM 的 token 生成时间(通常数百毫秒~数秒),这个开销可以忽略。

Q3: Ollama 显存不够怎么办?

  • 换用更小的量化版本(如 qwen2.5:3b)
  • 使用 Phi-3.5(3.8B,CPU 可跑)
  • 调低 num_ctx 参数减少 KV Cache 显存占用

Q4: DashScope 和 OpenAI API 语法兼容吗?

不完全兼容。Spring AI 的 spring-ai-alibaba 提供了独立适配器,需要单独引入依赖并配置,不能和 spring-ai-openai 混用同一个 ChatClient。

Q5: 如何监控各模型的调用量和费用?

OneAPI 自带统计面板,可以按渠道、模型查看调用次数和预估费用。也可接入 Prometheus + Grafana 展示自定义指标。

Logo

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

更多推荐