当前位置:首页 > 技术 > 正文内容

基于 Spring AI 与 DeepSeek 的儿童故事生成服务构建

访客 技术 2026年10月25日 1

项目概述与核心架构

本方案旨在构建一个后端服务,利用大语言模型能力实现基于主题关键词的儿童故事自动生成。系统核心涉及数据持久化模型设计、大模型 API 集成以及流式响应处理。技术栈选用 Java 17 配合 Spring AI 框架,对接 DeepSeek 开放平台。

环境配置与依赖管理

首先需要在项目中引入 Spring AI 的相关依赖。由于 DeepSeek 兼容 OpenAI 协议,可使用 spring-ai-starter-model-openai 进行集成。以下是 pom.xml 的关键配置片段:

<properties>
    <java.version>17</java.version>
    <spring-ai.version>1.0.1</spring-ai.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-bom</artifactId>
            <version>${spring-ai.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

应用配置文件 application.yml 需设置数据库连接及 AI 服务凭证。注意 DeepSeek 需要有效的 API Key 并保证账户余额充足:

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/story_db
    username: root
    password: your_password
  ai:
    openai:
      api-key: sk-your-deepseek-key
      base-url: https://api.deepseek.com
      chat:
        options:
          model: deepseek-chat

大模型客户端配置

创建配置类以初始化 ChatClient,预设系统提示词以规范 AI 的输出风格,并启用内存组件以支持多轮对话上下文。

package com.storylab.config;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class LLMProviderConfig {

    @Bean
    public ChatMemory chatMemory() {
        // 配置上下文窗口大小,保留最近 20 条消息
        return MessageWindowChatMemory.builder()
                .chatMemoryRepository(new InMemoryChatMemoryRepository())
                .maxMessages(20)
                .build();
    }

    @Bean
    public ChatClient chatClient(ChatClient.Builder builder, ChatMemory memory) {
        return builder
                .defaultSystem("你是一位擅长讲述童话的作家。请根据用户提供的主题,创作一个温馨、有趣且富有教育意义的儿童故事。语言需生动活泼,适合 5-10 岁儿童阅读,体现友谊、勇气等积极价值观。")
                .defaultAdvisors(new MessageChatMemoryAdvisor(memory))
                .build();
    }
}

数据模型设计

故事实体类 Narrative 需包含主题、标题、摘要、正文内容及时间戳字段,对应数据库表结构以便持久化生成结果。

控制器与服务层实现

控制层提供流式接口,允许前端逐字接收生成内容。服务层负责调用模型、解析响应结构并异步存储数据。

控制器代码:

package com.storylab.controller;

import com.storylab.service.NarrativeService;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/v1/narratives")
public class NarrativeController {

    private final NarrativeService narrativeService;

    public NarrativeController(NarrativeService narrativeService) {
        this.narrativeService = narrativeService;
    }

    @GetMapping(value = "/stream", produces = "text/plain;charset=UTF-8")
    public Flux<String> streamGenerate(@RequestParam String themes, @RequestParam String sessionId) {
        if (themes == null || themes.isBlank()) {
            return Flux.just("错误:主题不能为空");
        }
        return narrativeService.streamGenerate(themes, sessionId);
    }
}

服务接口定义:

package com.storylab.service;

import reactor.core.publisher.Flux;

public interface NarrativeService {
    Flux<String> streamGenerate(String themes, String sessionId);
}

服务实现逻辑:

实现类中通过构建提示词引导 AI 按特定 Markdown 格式输出,利用流式操作收集内容,并在传输完成后异步解析并保存至数据库。

package com.storylab.service.impl;

import com.storylab.entity.Narrative;
import com.storylab.mapper.NarrativeMapper;
import com.storylab.service.NarrativeService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.scheduler.Schedulers;

import java.time.LocalDateTime;

@Service
public class NarrativeServiceImpl implements NarrativeService {

    private final NarrativeMapper narrativeMapper;
    private final ChatClient chatClient;

    public NarrativeServiceImpl(NarrativeMapper narrativeMapper, ChatClient chatClient) {
        this.narrativeMapper = narrativeMapper;
        this.chatClient = chatClient;
    }

    @Override
    public Flux<String> streamGenerate(String themes, String sessionId) {
        String prompt = "请创作一个儿童短故事,包含主题:%s。\n" +
                "请严格遵循以下 Markdown 格式返回:\n" +
                "### 标题\n" +
                "故事标题\n" +
                "### 摘要\n" +
                "100 字以内的概述\n" +
                "### 正文\n" +
                "500-1000 字的故事内容";
        
        String formattedPrompt = String.format(prompt, themes);
        StringBuilder contentBuffer = new StringBuilder();

        return chatClient.prompt()
                .user(formattedPrompt)
                .advisors(a -> a.param("conversation_id", sessionId))
                .stream()
                .content()
                .doOnNext(chunk -> contentBuffer.append(chunk))
                .doOnComplete(() -> persistNarrative(contentBuffer.toString(), themes));
    }

    private void persistNarrative(String fullText, String themes) {
        Schedulers.boundedElastic().schedule(() -> {
            try {
                Narrative story = parseContent(fullText, themes);
                story.setCreatedAt(LocalDateTime.now());
                story.setUpdatedAt(LocalDateTime.now());
                narrativeMapper.insert(story);
            } catch (Exception e) {
                System.err.println("故事保存失败:" + e.getMessage());
            }
        });
    }

    private Narrative parseContent(String response, String themes) {
        Narrative story = new Narrative();
        story.setThemes(themes);

        // 基于 Markdown 标记解析内容
        String[] sections = response.split("###");
        if (sections.length > 1) {
            // 简单处理,实际生产环境建议增强解析逻辑
            for (String section : sections) {
                if (section.contains("标题")) {
                    story.setTitle(section.split("\n")[1].trim());
                } else if (section.contains("摘要")) {
                    story.setSummary(section.split("\n")[1].trim());
                } else if (section.contains("正文")) {
                    story.setContent(section.split("\n", 2)[1].trim());
                }
            }
        }
        
        // 兜底处理
        if (story.getTitle() == null) story.setTitle("主题故事:" + themes);
        if (story.getContent() == null) story.setContent(response);
        
        return story;
    }
}

通过上述架构,系统能够接收用户输入的主题,实时流式返回生成的故事内容,并在后台自动完成数据归档,实现了 AI 能力与传统业务逻辑的有效融合。

标签: spring-ai

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

linux screen 用法详情 (nohup 的替代方案)

一、screen 是什么?能干嘛?screen 是一个终端复用器,可以:在一个 SSH 会话中开多个“虚拟终端”SSH 断线后,程序仍然在后台运行随时重新连接到原来的会话特别适合:nohup 的替代方案跑脚本 / 爬虫 / 训练模型运维、远程开发二、安装 screen# CentOS / Rocky / Almayum install -y screen# Debian / Ubuntuapt i...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。