D05.从0到1实现一个微信红包封面设计Agent

一灰灰blogSpringAISpringSpringAI约 3862 字大约 13 分钟

我用SpringAI实现了个「微信红包封面设计Agent」

年底收到了微信给公众号免费发的一波红包封面兑换卡,正好上次在学习SpringAI的时候,看到了一个有趣的机制——大模型的响应前问询,这不是就是一个绝佳的应用场景嘛~

让AI来扮演一个设计师,通过与我的对话来敲定我想要的红包封面,然后基于这个设计方案来生成对应的红包封面,来个一站式的微信红包封面生成Agent

一、效果体验

线上体验地址: https://api.ppai.top/open in new window

默认进入之后长这样(前端页面由Kimi生成),通过点击右小角的对话按钮唤出设计框

接下来我们通过对话来演示一下具体的效果

公众号查看: 我用SpringAI实现了个「微信红包封面设计Agent」 | 一灰灰blogopen in new window

二、智能红包封面设计

2.1 整体介绍

这是一个基于Spring AI开发的完整应用,它能让AI像专业设计师一样,通过多轮对话理解你的需求,然后直接生成符合要求的红包封面。整个过程就像和一个懂设计的朋友聊天,你说想法,它来实现。

系统的核心亮点是"会提问的AI"。当你说"帮我做个红包封面"时,它不会盲目猜测,而是主动询问:"你想要什么风格?传统国风还是现代简约?""主色调偏向红色还是金色?"通过这些问题,AI能准确把握你的需求。

2.2 架构设计

项目采用了三层架构设计:

  • 对话层:使用AskUserQuestionTool工具,让AI具备主动提问能力。就像设计师会通过提问了解客户需求一样,我们的AI也会一步步澄清设计要求。
  • 生成层:集成阿里云千问的文生图API,将文字描述转换为视觉图像。特别针对微信红包封面的3x4比例(微信红包封面尺寸=957×1278)进行了优化。
  • 交互层:通过SSE(服务器发送事件)技术实现实时对话,用户能看到AI思考和回应的完整过程,体验非常流畅。
graph TD
    %% 统一样式定义(保留原有美化体系,微调细节)
    classDef uiLayer fill:#E1F5FE,color:#2D3748,stroke:#0288D1,stroke-width:2px,rx:8,ry:8,font-weight:500
    classDef chatLayer fill:#F3E5F5,color:#2D3748,stroke:#8E24AA,stroke-width:2px,rx:8,ry:8
    classDef prodLayer fill:#E8F5E8,color:#2D3748,stroke:#48BB78,stroke-width:2px,rx:8,ry:8
    classDef interLayer fill:#FFF3E0,color:#2D3748,stroke:#F57C00,stroke-width:2px,rx:8,ry:8
    classDef subNode fill:#FFFFFF,color:#4A5568,stroke:#CBD5E0,stroke-width:1px,rx:6,ry:6
    classDef linkNode fill:#FAFAFA,color:#2D3748,stroke:#718096,stroke-width:1px,rx:6,ry:6,dashed:true

    %% 核心层级节点(顶部入口,居中)
    A["🖥️ 用户界面层<br/>(用户交互入口)"]:::uiLayer
    


    %% 第一层子图:对话层(垂直布局第一个,子图内布局规整)
    subgraph SC["💬 对话层 - 需求交互模块"]
        style SC fill:#F3E5F520,stroke:#8E24AA,stroke-width:1px,rx:8,ry:8
        B["💬 对话层<br/>(需求交互核心)"]:::chatLayer
        %% 子节点垂直排列在顶节点下方,统一缩进
        B1["❓ AskUserQuestionTool<br/>(用户提问工具)"]:::subNode
        B2["🔄 多轮对话管理<br/>(上下文维护)"]:::subNode
        B3["📝 需求澄清机制<br/>(意图识别)"]:::subNode
        
        %% 子图内部垂直连线(规整对齐)
        B --> B1
        B1 --> B2
        B2 --> B3
    end

    %% 关联节点:固定在对话层右侧,避免布局漂移
    E["💬 ChatClient<br/>(对话客户端)"]:::linkNode

    %% 视觉分隔线(区分不同子图)

    %% 第二层子图:生产层(垂直布局第二个,与对话层布局一致)
    subgraph SA["🎨 生产层 - 图像生成模块"]
        style SA fill:#E8F5E820,stroke:#48BB78,stroke-width:1px,rx:8,ry:8,margin:10px
        %% 子图顶节点
        C["🎨 生产层<br/>(图像生成核心)"]:::prodLayer
        %% 子节点垂直排列
        C1["📡 文生图API调用<br/>(接口封装)"]:::subNode
        C2["☁️ 阿里云千问集成<br/>(模型对接)"]:::subNode
        C3["⚙️ 图像生成引擎<br/>(核心生成逻辑)"]:::subNode
        C4["✅ 质量控制模块<br/>(效果校验)"]:::subNode
        
        %% 子图内部垂直连线
        C --> C1
        C1 --> C2
        C2 --> C3
        C3 --> C4
    end

    %% 关联节点:固定在生产层右侧
    F["🎨 QwenImgGen<br/>(千问图像生成器)"]:::linkNode

    %% 视觉分隔线

    %% 第三层子图:交互层(垂直布局第三个,布局统一)
    subgraph SB["🔌 交互层 - 实时通信模块"]
        style SB fill:#FFF3E020,stroke:#F57C00,stroke-width:1px,rx:8,ry:8,margin:10px
        %% 子图顶节点
        D["🔌 交互层<br/>(实时通信支撑)"]:::interLayer
        %% 子节点垂直排列
        D1["📢 SSE实时通信<br/>(服务端推送)"]:::subNode
        D2["📊 流式响应处理<br/>(分段解析)"]:::subNode
        D3["🔄 前端状态同步<br/>(数据更新)"]:::subNode
        D4["✨ 用户体验优化<br/>(加载/反馈)"]:::subNode
        
        %% 子图内部垂直连线
        D --> D1
        D1 --> D2
        D2 --> D3
        D3 --> D4
    end

    %% 关联节点:固定在交互层右侧
    G["🔌 WebSocket/SSE<br/>(通信协议)"]:::linkNode

    %% 核心层级垂直连线(顶层→各子图层级核心,视觉引导清晰)
    A --> B
    B --> C
    C --> D

    %% 虚线关联(子图层级核心→对应关联节点,连线更短更规整)
    B -.-> E
    C -.-> F
    D -.-> G

三、核心实现

整体的实现思路基本上和上一篇非常相似,区别是改了交互像是,从控制台模式改成了web方式,具体细节请查看 Spring AI中的多轮对话艺术:让大模型主动提问获取明确需求open in new window 接下来将主要介绍一些核心的技术点

3.1 多轮问询机制

如何识别用户的意图?这好像是每个Agent开发必须解决的问题,这里可以说是给出了一个经典的解决方案,那就是主动问询,让用户主动进行澄清,这里的主要实现原理如下图

这一套设计哲学遵循问答式工作流程:

  1. AI生成问题
    • 智能体判断需要输入并构建问题(每个问题包含问题文本、标题、2-4个选项和多选标志),然后调用AskUserQuestionTool函数
  2. 用户提供答案
    • 由业务代码实现接收这些问题,然后通过合适的形式(控制台/web页面等)展示给用户,然后收集用户的回答,并将答案返回给LLM。
  3. 提出更多问题
    • 如有必要,重复步骤1、2,以收集更多用户反馈
  4. 人工智能持续关注上下文
    • LLM利用这些答案来提供量身定制的解决方案

需要说明的是,每个问题的回答并不是写死的:

  • 支持单选或多选: 选择一个选项或组合多个选项
  • 支持选项外的文本输入: 用户可以随时提供超出预定义选项范围的自定义文本
  • 丰富的上下文: 每个选项都包含一个描述,解释其含义和权衡

3.2 WEB交互方案

对于web层的用户交互,我们采用SSE的交互方式,具体流程是

用户开启会话,输入第一次设计要求
	⬇️
后端建立SSE链接,并通过异步方式将设计要求发送给LLM,
	⬇️
大模型返回问询内容
	⬇️
后端逐条将问询内容通过SSE发送给用户
	⬇️
用户回答内容,通过send/{chatId}发送给后端
	⬇️
问询结束,后端组装回答发送给LLM
	⬇️
LLM返回完整设计方案

以时序图的视角来看一下完整的交互方案

在这套方案的具体实现中,我们通过上下文来持有 会话与SSE 之间的关系,这样才可以将用户的回答内容与之前的大模型会话进行绑定

public class ReqContextHolder {

    private static final ThreadLocal<ReqInfo> reqId = new InheritableThreadLocal<>();

    public static void setReqId(ReqInfo reqId) {
        ReqContextHolder.reqId.set(reqId);
    }

    public static ReqInfo getReqId() {
        return reqId.get();
    }

    public static void clear() {
        reqId.remove();
    }

    public record ReqInfo(String chatId, SseEmitter sse) {
    }
}

通过一个临时的Map来存储用户的问询结果,这里借助BlockingQueue来实现一个简易的消息驱动模式

private Map<String, BlockingQueue<String>> chatHistory = new ConcurrentHashMap<>();

基于上面这两个中间存储管道,所以我们核心的问询回调WebQuestionHandler的具体实现如下 (下面的实现给了完整的注释,应该不难理解😊)

public class WebQuestionHandler implements AskUserQuestionTool.QuestionHandler {
        @Override
        /**
         * 处理用户问题列表,向用户发送问题并等待用户回答
         *
         * @param questions 用户问题列表
         * @return 包含问题和对应答案的映射
         */
        public Map<String, String> handle(List<AskUserQuestionTool.Question> questions) {
            // 创建用于存储问题和答案的映射
            Map<String, String> answers = new HashMap<>();
            // 获取当前请求的上下文信息
            ReqContextHolder.ReqInfo req = ReqContextHolder.getReqId();
            // 获取SSE发射器用于向客户端发送消息
            SseEmitter sseEmitter = req.sse();

            // 遍历所有需要询问用户的问题
            for (AskUserQuestionTool.Question q : questions) {
                // 向用户发送问题标题和内容
                sendMsg(sseEmitter, "\n" + q.header() + ": " + q.question());

                // 获取问题的选项列表
                List<AskUserQuestionTool.Question.Option> options = q.options();
                // 遍历选项并发送给用户
                for (int i = 0; i < options.size(); i++) {
                    AskUserQuestionTool.Question.Option opt = options.get(i);
                    sendMsg(sseEmitter, String.format("  %d. %s - %s%n", i + 1, opt.label(), opt.description()));
                }

                // 根据是否支持多选发送不同的提示信息
                if (q.multiSelect()) {
                    sendMsg(sseEmitter, "  (Enter numbers separated by commas, or type custom text)");
                } else {
                    sendMsg(sseEmitter, "  (Enter a number, or type custom text)");
                }

                // 阻塞等待用户输入
                BlockingQueue<String> queue = chatHistory.get(req.chatId());
                // 如果队列不存在,则创建新的队列
                if (queue == null) {
                    queue = new LinkedBlockingQueue<>();
                    chatHistory.put(req.chatId(), queue);
                }
                
                String response = null;
                try {
                    // 等待最多5分钟获取用户响应,超时则返回空字符串
                    response = queue.poll(5, TimeUnit.MINUTES);
                    if (response == null) {
                        response = ""; // 超时情况下的默认响应
                    }
                } catch (InterruptedException e) {
                    // 线程被中断时设置中断状态并返回空字符串
                    Thread.currentThread().interrupt();
                    response = "";
                }

                // 解析用户响应并存入答案映射
                answers.put(q.question(), parseResponse(response, options));
            }

            // 返回包含所有问题和答案的映射
            return answers;
        }

        private void sendMsg(SseEmitter sseEmitter, String msg) {
            try {
                sseEmitter.send(msg);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
}

3.3 图片生成

图文生成我们这里给出了两种,一个是基于智谱的一个是基于千问的(因为智谱的免费模型有点拉跨);

文生图的具体实现没有太多好说的,这里单独指出来是想提醒,这里实际上还可以继续扩展一下,除了生成静态图片之外,还可以考虑支持生成动态的视频;除了生成封面图之外,还可以考虑扩展生成挂件

@GetMapping(path = "/genImg")
public String genImg(@RequestParam String msg) throws IOException {
    if ("true".equals(environment.getProperty("spring.ai.dashboard.enable"))) {
        return QwenImgGen.call(environment.getProperty("spring.ai.dashboard.api-key"), msg);
    } else {
        // 这里使用的是智谱的文生图模型,效果较差
        ImageResponse response = imgModel.call(new ImagePrompt(msg,
                ImageOptionsBuilder.builder()
                        .height(1344)
                        .width(768)
                        .model("CogView-3-Flash")
                        // 返回图片类型
                        .responseFormat("png")
                        // 图像风格,如 vivid 生动风格, natural 自然风格
                        .style("natural")
                        .build())
        );
        Image img = response.getResult().getOutput();
        BufferedImage image = ImageIO.read(new URL(img.getUrl()));

        // 将图片转换为Base64编码
        java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream();
        ImageIO.write(image, "png", baos);
        byte[] imageBytes = baos.toByteArray();
        return java.util.Base64.getEncoder().encodeToString(imageBytes);
    }
}

四、小结

这一篇内容可以说是上一篇SpringAI智能体设计中问询机制的具体使用场景,整个系统展现了AI应用开发的新思路:不是让人适应机器,而是让机器理解人的表达方式。通过多轮对话获取准确需求,再通过AI生成能力直接产生成果,真正实现了从想法到作品的转换。当然整体的实现还比较初级,还有不少的挖掘空间,比如一次生成完整的微信红包方案(包括封面简称、封面图、挂件、气泡挂件、封面故事等)欢迎有兴趣的小伙伴进行补全

项目源码:

零基础入门:


实战

Loading...