1.LangChain4j

langchain4j 是一个针对 Java 生态的开源框架,专为构建基于大语言模型(LLM)的应用程序设计,提供了统一的 API 来集成各类大模型(如 OpenAI、GPT-4、LLaMA 等),并支持链式调用、对话记忆、工具调用(如数据库查询、API 调用)、文档加载与检索等核心功能,简化了复杂 LLM 应用的开发流程,同时兼容响应式编程(如 Reactor)和 Spring 生态,方便在 Java 项目中快速实现智能对话、内容生成、知识问答等场景。

2.大模型接入

引入依赖:

<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>1.1.0</version>
</dependency>
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
    <version>1.1.0-beta7</version>
</dependency>

需要获取Deepseek的 API Key:首次调用 API | DeepSeek API Docs

chatModel负责和AI大模型交互。

在配置文件application.yml中填写Chat Model配置,并且为了调试方便,开启详细的日志记录。

# AI
langchain4j:
  open-ai:
    chat-model:
      base-url: https://api.deepseek.com
      api-key: <Your API Key>
      model-name: deepseek-chat
      log-requests: true
      log-responses: true

为了保护敏感信息也可以创建application-local.yml,填入配置信息,并在主配置文件中激活本地环境。

3. 开发AI服务

3.1 定义接口(以生成HTML代码为例):

public interface AiCodeGeneratorService {
    /**
     * 生成HTML代码
     *
     * @param userMessage 用户提示词
     * @return 生成的代码结果
     */
    @SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
    HtmlCodeResult generateHTMLCode(String userMessage);

}

系统提示词比较长,将他们维护到资源文件中:

系统提示词示例:

你是一位资深的 Web 前端开发专家,精通 HTML、CSS 和原生 JavaScript。
你的任务是根据用户提供的网站描述,生成一个完整、独立的单页面网站。你需要一步步思考,并最终将所有代码整合到一个 HTML 文件中。

约束:
1. 技术栈: 只能使用 HTML、CSS 和原生 JavaScript。
2. 禁止外部依赖: ......
3. 独立文件: ......
4. 响应式设计: ......
5. 内容填充: ......
6. 代码质量: 代码必须结构清晰、有适当的注释,易于阅读和维护。
7. 交互性: .......
8. 输出格式: 你的最终输出必须包含 HTML 代码块,可以在代码块之外添加解释、标题或总结性文字。格式如下:

```html
... HTML 代码 ...

    3.2创建工厂类来初始化AI服务:

    @Configuration
    public class AiCodeGeneratorServiceFactory {
    
        @Resource
        private ChatModel chatModel;
    
        @Bean
        public AiCodeGeneratorService aiCodeGeneratorService() {
            return AiServices.create(AiCodeGeneratorService.class, chatModel);
        }
    }
    

    3.3 编写一个单元测试验证功能:

    @SpringBootTest
    class AiCodeGeneratorServiceTest {
    
        @Resource
        private AiCodeGeneratorService aiCodeGeneratorService;
    
        @Test
        void generateHtmlCode() {
            String result = aiCodeGeneratorService.generateHtmlCode("做个程序员的工作记录小工具");
            Assertions.assertNotNull(result);
        }
    
    }
    

    3.4 结构化输出

    在调用AI输出代码后,我们需要将AI的输出转换为结构化的对象以便于后续解析代码并保存为文件,利用LangChain4j的结构化输出特性。

    创建结构类HtmlCodeResult:

    /**
     * HTML代码生成结果
     */
    @Description("HTML代码生成结果")
    @Data
    public class HtmlCodeResult {
    
        /**
         * HTML代码
         */
        @Description("HTML代码")
        private String htmlCode;
    
        /**
         * 描述
         */
        @Description("生成代码的描述")
        private String description;
    }

    为结果类和属性添加详细的描述信息,告诉AI每个属性应该存入什么信息,@Data注解让AI可以将值写入属性。

    3.5 优化技巧

    1.设置max_tokens参数,防止JSON字符串被中途截断。

    在项目配置文件中添加:

    langchain4j:
      open-ai:
        chat-model:
          max-tokens: 8192
    

    2. JSON Schema配置

    设置 response-format 参数为json_object,可以严格确保结构化输出生效,可以在项目中使用这个配置 :

    langchain4j:
      open-ai:
        chat-model:
          strict-json-schema: true
          response-format: json_object
    

    3.在系统提示词中明确要求输出JSON格式,进一步提高成功率。

    4. 程序处理写入

    接下来将生成的代码保存到本地文件系统。

    4.1 定义生成类型枚举

    我们可以有多种生成类型,比如说HTML,多文件,还有Vue工程化等等。在调用AI服务时将生成类型传入,AI生成对应的代码。

    @Getter
    public enum CodeGenTypeEnum {
    
        HTML("原生 HTML 模式", "html"),
        MULTI_FILE("原生多文件模式", "multi_file");
    
        private final String text;
        private final String value;
    
        CodeGenTypeEnum(String text, String value) {
            this.text = text;
            this.value = value;
        }
    
        /**
         * 根据 value 获取枚举
         *
         * @param value 枚举值的value
         * @return 枚举值
         */
        public static CodeGenTypeEnum getEnumByValue(String value) {
            if (ObjUtil.isEmpty(value)) {
                return null;
            }
            for (CodeGenTypeEnum anEnum : CodeGenTypeEnum.values()) {
                if (anEnum.value.equals(value)) {
                    return anEnum;
                }
            }
            return null;
        }
    }
    

    4.2 创建文件写入工具类

    假设将文件存入目录tmp下。每次生成都对应一个目录下的文件夹,使用业务类型+雪花ID的命名方式来确保唯一性。

    public class CodeFileSaver {
    
        // 文件保存根目录
        private static final String FILE_SAVE_ROOT_DIR = System.getProperty("user.dir") + "/tmp/";
    
        /**
         * 保存 HtmlCodeResult
         */
        public static File saveHtmlCodeResult(HtmlCodeResult result) {
            String baseDirPath = buildUniqueDir(CodeGenTypeEnum.HTML.getValue());
            writeToFile(baseDirPath, "index.html", result.getHtmlCode());
            return new File(baseDirPath);
        }
    
        /**
         * 构建唯一目录路径:tmp/bizType_雪花ID
         */
        private static String buildUniqueDir(String bizType) {
            String uniqueDirName = StrUtil.format("{}_{}", bizType, IdUtil.getSnowflakeNextIdStr());
            String dirPath = FILE_SAVE_ROOT_DIR + File.separator + uniqueDirName;
            FileUtil.mkdir(dirPath);
            return dirPath;
        }
    
        /**
         * 写入单个文件
         */
        private static void writeToFile(String dirPath, String filename, String content) {
            String filePath = dirPath + File.separator + filename;
            FileUtil.writeString(content, filePath, StandardCharsets.UTF_8);
        }
    }
    

    5. SSE流式输出

    如果AI生成的速度比较慢,用户需要等待一段时间才能得到结果,体验不好。所以引入SSE(Server-Sent Events)流式输出,效果为AI生成一个字,就将其返回给前端。

    目前的流式输出不支持结构化输出,所以我们在流式返回的过程中拼接AI的返回结果,可以实时返回给前端,等全部输出完以后,再对拼接结果进行解析和保存。

    这里使用 LangChain4j + Reactor 实现。Reactor 通过 Flux 和 Sinks 将 langchain4j 的异步流式响应转换为符合 SSE 规范的持续输出流,实现服务器到客户端的高效实时数据推送。

    5.1 引入依赖

    <dependency>
      <groupId>dev.langchain4j</groupId>
      <artifactId>langchain4j-reactor</artifactId>
      <version>1.1.0-beta7</version>
    </dependency>
    

    5.2 开发实现

    1. 配置流式模型

    langchain4j:
      open-ai:
        streaming-chat-model:
          base-url: https://api.deepseek.com
          api-key: <Your API Key>
          model-name: deepseek-chat
          max-tokens: 8192
          log-requests: true
          log-responses: true
    

    2. 在AiCodeGeneratorServiceFactory工厂中注入流式模型

    @Configuration
    public class AiCodeGeneratorServiceFactory {
    
        @Resource
        private ChatModel chatModel;
    
        @Resource
        private StreamingChatModel streamingChatModel;
    
        @Bean
        public AiCodeGeneratorService aiCodeGeneratorService() {
            return AiServices.builder(AiCodeGeneratorService.class)
                    .chatModel(chatModel)
                    .streamingChatModel(streamingChatModel)
                    .build();
        }
    }
    

    3. 在AI Service中新增流式方法,返回Flux对象:

    /**
     * 生成 HTML 代码(流式)
     *
     * @param userMessage 用户消息
     * @return 生成的代码结果
     */
    @SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
    Flux<String> generateHtmlCodeStream(String userMessage);
    

    4. 编写解析逻辑

    由于模型返回的流式数据通常是碎片化、非结构化的原始内容(如单词、短句、标记片段等),而前端或下游系统需要结构化、可理解的对象(如完整 JSON、实体类等)。所以需要代码解析器将零散的流式数据实时拼接、校验、转换为目标格式的对象,同时处理可能的格式错误(如未闭合的括号、不完整的字段),确保最终输出符合预期结构,避免下游因数据不完整或格式混乱而解析失败。

    代码解析器相当于一个工具类,不用搞懂内部逻辑(也是用AI生成的)。

    /**
     * 代码解析器
     * 提供静态方法解析不同类型的代码内容
     */
    public class CodeParser {
    
        private static final Pattern HTML_CODE_PATTERN = Pattern.compile("```html\\s*\\n([\\s\\S]*?)```", Pattern.CASE_INSENSITIVE);
        private static final Pattern CSS_CODE_PATTERN = Pattern.compile("```css\\s*\\n([\\s\\S]*?)```", Pattern.CASE_INSENSITIVE);
        private static final Pattern JS_CODE_PATTERN = Pattern.compile("```(?:js|javascript)\\s*\\n([\\s\\S]*?)```", Pattern.CASE_INSENSITIVE);
    
        /**
         * 解析 HTML 单文件代码
         */
        public static HtmlCodeResult parseHtmlCode(String codeContent) {
            HtmlCodeResult result = new HtmlCodeResult();
            // 提取 HTML 代码
            String htmlCode = extractHtmlCode(codeContent);
            if (htmlCode != null && !htmlCode.trim().isEmpty()) {
                result.setHtmlCode(htmlCode.trim());
            } else {
                // 如果没有找到代码块,将整个内容作为HTML
                result.setHtmlCode(codeContent.trim());
            }
            return result;
        }
    
        /**
         * 提取HTML代码内容
         *
         * @param content 原始内容
         * @return HTML代码
         */
        private static String extractHtmlCode(String content) {
            Matcher matcher = HTML_CODE_PATTERN.matcher(content);
            if (matcher.find()) {
                return matcher.group(1);
            }
            return null;
        }
    
        /**
         * 根据正则模式提取代码
         *
         * @param content 原始内容
         * @param pattern 正则模式
         * @return 提取的代码
         */
        private static String extractCodeByPattern(String content, Pattern pattern) {
            Matcher matcher = pattern.matcher(content);
            if (matcher.find()) {
                return matcher.group(1);
            }
            return null;
        }
    }
    

    5. 在 AiCodeGeneratorFacade 中添加流式调用AI的方法。

    核心逻辑为:拼接AI实时响应的字符串,并在流式返回完成后解析字符串并保存代码文件。

    /**
     * 生成 HTML 模式的代码并保存(流式)
     *
     * @param userMessage 用户提示词
     * @return 保存的目录
     */
    private Flux<String> generateAndSaveHtmlCodeStream(String userMessage) {
        Flux<String> result = aiCodeGeneratorService.generateHtmlCodeStream(userMessage);
        // 当流式返回生成代码完成后,再保存代码
        StringBuilder codeBuilder = new StringBuilder();
        return result
                .doOnNext(chunk -> {
                    // 实时收集代码片段
                    codeBuilder.append(chunk);
                })
                .doOnComplete(() -> {
                    // 流式返回完成后保存代码
                    try {
                        String completeHtmlCode = codeBuilder.toString();
                        HtmlCodeResult htmlCodeResult = CodeParser.parseHtmlCode(completeHtmlCode);
                        // 保存代码到文件
                        File savedDir = CodeFileSaver.saveHtmlCodeResult(htmlCodeResult);
                        log.info("保存成功,路径为:" + savedDir.getAbsolutePath());
                    } catch (Exception e) {
                        log.error("保存失败: {}", e.getMessage());
                    }
                });
    }

    Logo

    有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

    更多推荐