LangChain4j实战
官方文档
引入依赖
<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>
配置yml
包含api-key、接口地址、模型名称、前置拦截日志、后置拦截日志,详细参数可以参考官网示例。
langchain4j:
open-ai:
chat-model:
api-key: xxx
base-url: https://api.deepseek.com
model-name: deepseek-chat
log-requests: true
log-responses: true
声明式AI服务
创建AI服务接口,定义读取的提示词指定位置
/**
* AI服务接口
*/
interface AiCodeGenerateService {
/**
* 从指定位置文件中获取系统提示语,生成单个HTML代码片段
* @param userMessage
* @return
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
String generateHtmlCode(String userMessage);
/**
* 从指定位置文件中获取系统提示语,生成多个HTML代码片段
* @param userMessage
* @return
*/
@SystemMessage(fromResource = "prompt/codegen-muti-html-system-prompt.txt")
String generateMutlHtmlCode(String userMessage);
}
创建工厂类,来创建AIService服务。
@Configuration
public class AiCodeGeneratorServiceFactory {
@Resource
private ChatModel chatModel;
@Bean
public AiCodeGenerateService aiCodeGeneratorService() {
return AiServices.create(AiCodeGenerateService.class, chatModel);
}
}
测试
@SpringBootTest
@ActiveProfiles("local")
@Slf4j
class FlyGeniusApplicationTests {
@Resource
private AiCodeGenerateService aiCodeGenerateService;
@Test
void contextLoads() {
String generateHtmlCode = aiCodeGenerateService.generateHtmlCode("给我生成一篇博客");
log.info(generateHtmlCode);
String generateMutlHtmlCode = aiCodeGenerateService.generateMutlHtmlCode("给我生成一篇博客");
log.info(generateMutlHtmlCode);
}
}
通过前置拦截,我们可以看到AI请求对应的格式,包含请求地址、请求体、角色等
25-08-04.14:25:34.959 [main ] INFO LoggingHttpClient - HTTP request:
- method: POST
- url: https://api.deepseek.com/chat/completions
- headers: [Authorization: Beare...33], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "deepseek-chat",
"messages" : [ {
"role" : "system",
"content" : "你是一位资深的 Web 前端开发专家,精通 HTML、CSS 和原生 JavaScript。你擅长构建响应式、美观且代码整洁的单页面网站。\r\n\r\n你的任务是根据用户提供的网站描述,生成一个完整、独立的单页面网站。你需要一步步思考,并最终将所有代码整合到一个 HTML 文件中。\r\n\r\n约束:\r\n1. 技术栈: 只能使用 HTML、CSS 和原生 JavaScript。\r\n2. 禁止外部依赖: 绝对不允许使用任何外部 CSS 框架、JS 库或字体库。所有功能必须用原生代码实现。\r\n3. 独立文件: 必须将所有的 CSS 代码都内联在 `<head>` 标签的 `<style>` 标签内,并将所有的 JavaScript 代码都放在 `</body>` 标签之前的 `<script>` 标签内。最终只输出一个 `.html` 文件,不包含任何外部文件引用。\r\n4. 响应式设计: 网站必须是响应式的,能够在桌面和移动设备上良好显示。请优先使用 Flexbox 或 Grid 进行布局。\r\n5. 内容填充: 如果用户描述中缺少具体文本或图片,请使用有意义的占位符。例如,文本可以使用 Lorem Ipsum,图片可以使用 https://picsum.photos 的服务 (例如 `<img src=\"https://picsum.photos/800/600\" alt=\"Placeholder Image\">`)。\r\n6. 代码质量: 代码必须结构清晰、有适当的注释,易于阅读和维护。\r\n7. 交互性: 如果用户描述了交互功能 (如 Tab 切换、图片轮播、表单提交提示等),请使用原生 JavaScript 来实现。\r\n8. 安全性: 不要包含任何服务器端代码或逻辑。所有功能都是纯客户端的。\r\n9. 输出格式: 你的最终输出必须包含 HTML 代码块,可以在代码块之外添加解释、标题或总结性文字。格式如下:\r\n\r\n```html\r\n... HTML 代码 ..."
}, {
"role" : "user",
"content" : "给我生成一篇博客"
} ],
"stream" : false
}

结构化输出
AI响应的格式有点乱,我们可以使用结构化输出来规范AI响应格式,返回指定JSON。示例如下:
可以通过Description描述来指定对应的字段给AI
@Description("a person")
record Person(@Description("person's first and last name, for example: John Doe") String name,
@Description("person's age, for example: 42") int age,
@Description("person's height in meters, for example: 1.78") double height,
@Description("is person married or not, for example: false") boolean married) {
}
我们可以根据需要的格式,编写实体类
@Description("生成 HTML 代码文件的结果")
@Data
public class HtmlCodeResult {
@Description("HTML代码")
private String htmlCode;
@Description("生成代码的描述")
private String description;
}
@Description("生成多个代码文件的结果")
@Data
public class MultiFileCodeResult {
@Description("HTML代码")
private String htmlCode;
@Description("CSS代码")
private String cssCode;
@Description("JS代码")
private String jsCode;
@Description("生成代码的描述")
private String description;
}
修改接口返回指定格式数据
/**
* AI服务接口
*/
public interface AiCodeGenerateService {
/**
* 从指定位置文件中获取系统提示语,生成单个HTML代码片段
* @param userMessage
* @return
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
HtmlCodeResult generateHtmlCode(String userMessage);
/**
* 从指定位置文件中获取系统提示语,生成多个HTML代码片段
* @param userMessage
* @return
*/
@SystemMessage(fromResource = "prompt/codegen-muti-html-system-prompt.txt")
MultiFileCodeResult generateMutlHtmlCode(String userMessage);
}
测试
@SpringBootTest
@ActiveProfiles("local")
@Slf4j
class FlyGeniusApplicationTests {
@Resource
private AiCodeGenerateService aiCodeGenerateService;
@Test
void contextLoads() {
HtmlCodeResult generateHtmlCode = aiCodeGenerateService.generateHtmlCode("给我生成一篇博客");
log.info("生成代码的描述:{}", generateHtmlCode.getDescription());
log.info(generateHtmlCode.getHtmlCode());
MultiFileCodeResult generatedMutlHtmlCode = aiCodeGenerateService.generateMutlHtmlCode("给我生成一篇博客");
log.info("生成代码的描述:{}", generatedMutlHtmlCode.getDescription());
log.info(generatedMutlHtmlCode.getHtmlCode());
log.info(generatedMutlHtmlCode.getCssCode());
log.info(generatedMutlHtmlCode.getJsCode());
}
输出,可以看到实际上只是将我们的实体类注释转换到预设中。
25-08-04.14:40:08.112 [main ] INFO LoggingHttpClient - HTTP request:
- method: POST
- url: https://api.deepseek.com/chat/completions
- headers: [Authorization: Beare...33], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "deepseek-chat",
"messages" : [ {
"role" : "system",
"content" : "你是一位资深的 Web 前端开发专家,精通 HTML、CSS 和原生 JavaScript。你擅长构建响应式、美观且代码整洁的单页面网站。\r\n\r\n你的任务是根据用户提供的网站描述,生成一个完整、独立的单页面网站。你需要一步步思考,并最终将所有代码整合到一个 HTML 文件中。\r\n\r\n约束:\r\n1. 技术栈: 只能使用 HTML、CSS 和原生 JavaScript。\r\n2. 禁止外部依赖: 绝对不允许使用任何外部 CSS 框架、JS 库或字体库。所有功能必须用原生代码实现。\r\n3. 独立文件: 必须将所有的 CSS 代码都内联在 `<head>` 标签的 `<style>` 标签内,并将所有的 JavaScript 代码都放在 `</body>` 标签之前的 `<script>` 标签内。最终只输出一个 `.html` 文件,不包含任何外部文件引用。\r\n4. 响应式设计: 网站必须是响应式的,能够在桌面和移动设备上良好显示。请优先使用 Flexbox 或 Grid 进行布局。\r\n5. 内容填充: 如果用户描述中缺少具体文本或图片,请使用有意义的占位符。例如,文本可以使用 Lorem Ipsum,图片可以使用 https://picsum.photos 的服务 (例如 `<img src=\"https://picsum.photos/800/600\" alt=\"Placeholder Image\">`)。\r\n6. 代码质量: 代码必须结构清晰、有适当的注释,易于阅读和维护。\r\n7. 交互性: 如果用户描述了交互功能 (如 Tab 切换、图片轮播、表单提交提示等),请使用原生 JavaScript 来实现。\r\n8. 安全性: 不要包含任何服务器端代码或逻辑。所有功能都是纯客户端的。\r\n9. 输出格式: 你的最终输出必须包含 HTML 代码块,可以在代码块之外添加解释、标题或总结性文字。格式如下:\r\n\r\n```html\r\n... HTML 代码 ..."
}, {
"role" : "user",
"content" : "给我生成一篇博客\nYou must answer strictly in the following JSON format: {\n\"htmlCode\": (HTML代码; type: string),\n\"description\": (生成代码的描述; type: string)\n}"
} ],
"stream" : false
}
门面模式-生成html代码
为了统一管理生成和保存的逻辑,决定使用门面模式 这一设计模式。
门面模式通过提供一个统一的高层接口来隐藏子系统的复杂性,让客户端只需要与这个简化的接口交互,而不用了解内部 的复杂实现细节。
编写通用枚举类用于区分单个文件和多个文件。
/**
* 代码生成类型枚举
*
* @author flycode
*/
@Getter
public enum CodeGenTypeEnum {
HTML("原生Html代码", "html"),
MULTI_FILE("多个文件代码", "multi-file");
private String text;
private String value;
CodeGenTypeEnum(String text, String value) {
this.text = text;
this.value = value;
}
public static CodeGenTypeEnum getEnumByValue(String value) {
if (value == null) {
return null;
}
for (CodeGenTypeEnum item : CodeGenTypeEnum.values()) {
if (item.value.equals(value)) {
return item;
}
}
return null;
}
}
生成和保存代码到本地
/**
* 文件根据html生成对应文件
*
* @author flycode
*/
public class CodeFileSaver {
// 保存文件的目录
public static final String FILE_SAVE_DIR = System.getProperty("user.dir") + "/tmp/ai_code_result";
/**
* 根据生成类型生成文件唯一标识,使用雪花算法
*
* @param bizType 代码类型
* @return 返回目录
*/
public static String buildUniqueFileDir(String bizType) {
String uniqueDirName = StrUtil.format("{}_{}", bizType, IdUtil.getSnowflakeNextIdStr());
String dirPath = FILE_SAVE_DIR + File.separator + uniqueDirName;
FileUtil.mkdir(dirPath);
return dirPath;
}
/**
* 生成文件
*
* @param fileName 文件名
* @param dirPath 目录
* @param content 内容
* @return 文件
*/
public static File writeContentToFile(String fileName, String dirPath, String content) {
String filePath = dirPath + File.separator + fileName;
return FileUtil.writeString(content, filePath, StandardCharsets.UTF_8);
}
/**
* 生成单个html文件
*
* @param result html代码
* @return 文件
*/
public static File saveHtmlCode(HtmlCodeResult result) {
String dirPath = buildUniqueFileDir(CodeGenTypeEnum.HTML.getValue());
writeContentToFile("index.html", dirPath, result.getHtmlCode());
return new File(dirPath);
}
/**
* 生成多个文件
*
* @param result 多个文件代码
* @return 文件
*/
public static File saveMultiFileCode(MultiFileCodeResult result) {
String dirPath = buildUniqueFileDir(CodeGenTypeEnum.MULTI_FILE.getValue());
writeContentToFile("index.html", dirPath, result.getHtmlCode());
writeContentToFile("style.css", dirPath, result.getCssCode());
writeContentToFile("script.js", dirPath, result.getJsCode());
return new File(dirPath);
}
}
门面模式生成指定代码到本地
@Service
public class AiCodeGeneratorFacade {
@Resource
private AiCodeGenerateService aiCodeGenerateService;
public File generatorAndSaveFile(String userMessage, CodeGenTypeEnum codeGenType) {
if (codeGenType == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
return switch (codeGenType) {
case HTML -> generateHtmlCode(userMessage);
case MULTI_FILE -> generateMutlHtmlCode(userMessage);
default -> throw new BusinessException(ErrorCode.PARAMS_ERROR, "不支持的类型");
};
}
/**
* 生成多个文件代码
*
* @param userMessage
* @return
*/
private File generateMutlHtmlCode(String userMessage) {
MultiFileCodeResult generatedMutlHtmlCode = aiCodeGenerateService.generateMutlHtmlCode(userMessage);
return CodeFileSaver.saveMultiFileCode(generatedMutlHtmlCode);
}
/**
* 生成 HTML 代码
*
* @param userMessage
* @return
*/
private File generateHtmlCode(String userMessage) {
HtmlCodeResult generateHtmlCode = aiCodeGenerateService.generateHtmlCode(userMessage);
return CodeFileSaver.saveHtmlCode(generateHtmlCode);
}
}

SSE
目前有两种流式输出方式,但是流式输出不支持结构化输出,但是我们可以在流式输出的过程中,拼接结果。
Langchain4j+Reactor
引入依赖
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.2.0-beta8</version>
</dependency>
interface Assistant {
Flux<String> chat(String message);
}

TokenStream
interface Assistant {
TokenStream chat(String message);
}
StreamingChatModel model = OpenAiStreamingChatModel.builder()
.apiKey(System.getenv("OPENAI_API_KEY"))
.modelName(GPT_4_O_MINI)
.build();
Assistant assistant = AiServices.create(Assistant.class, model);
TokenStream tokenStream = assistant.chat("Tell me a joke");
tokenStream
.onPartialResponse((String partialResponse) -> System.out.println(partialResponse))
.onPartialThinking((PartialThinking partialThinking) -> System.out.println(partialThinking))
.onRetrieved((List<Content> contents) -> System.out.println(contents))
.onIntermediateResponse((ChatResponse intermediateResponse) -> System.out.println(intermediateResponse))
.onToolExecuted((ToolExecution toolExecution) -> System.out.println(toolExecution))
.onCompleteResponse((ChatResponse response) -> System.out.println(response))
.onError((Throwable error) -> error.printStackTrace())
.start();
Reactor改造流式输出
配置yml
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
更改AiCodeGeneratorServiceFactory为流式输出
@Configuration
public class AiCodeGeneratorServiceFactory {
@Resource
private ChatModel chatModel;
@Resource
private StreamingChatModel streamingChatModel;
/**
* 流式输出
*
* @return
*/
@Bean
public AiCodeGenerateService aiCodeGeneratorService() {
return AiServices.builder(AiCodeGenerateService.class)
.chatModel(chatModel)
.streamingChatModel(streamingChatModel)
.build();
}
}
修改AiCodeGenerateService接口,增加流式输出
/**
* 从指定位置文件中获取系统提示语,生成单个HTML代码片段,流式输出
* @param userMessage
* @return
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
Flux<String> generateHtmlCodeStream(String userMessage);
/**
* 从指定位置文件中获取系统提示语,生成多个HTML代码片段,流式输出
* @param userMessage
* @return
*/
@SystemMessage(fromResource = "prompt/codegen-muti-html-system-prompt.txt")
Flux<String> generateMutlHtmlCodeStream(String userMessage);
解析代码,因为AI返回的是字符串数据,我们需要进行切割,将描述信息和代码片段进行分隔。直接让AI生成就行。
/**
* 代码解析器
* 提供静态方法解析不同类型的代码内容
*
* @author flycode
*/
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 + CSS + JS)
*/
public static MultiFileCodeResult parseMultiFileCode(String codeContent) {
MultiFileCodeResult result = new MultiFileCodeResult();
// 提取各类代码
String htmlCode = extractCodeByPattern(codeContent, HTML_CODE_PATTERN);
String cssCode = extractCodeByPattern(codeContent, CSS_CODE_PATTERN);
String jsCode = extractCodeByPattern(codeContent, JS_CODE_PATTERN);
// 设置HTML代码
if (htmlCode != null && !htmlCode.trim().isEmpty()) {
result.setHtmlCode(htmlCode.trim());
}
// 设置CSS代码
if (cssCode != null && !cssCode.trim().isEmpty()) {
result.setCssCode(cssCode.trim());
}
// 设置JS代码
if (jsCode != null && !jsCode.trim().isEmpty()) {
result.setJsCode(jsCode.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;
}
}
在之前的门面模式AiCodeGeneratorFacade中,修改流式输出
@Resource
private AiCodeGenerateService aiCodeGenerateService;
/**
* 生成多个文件代码,完成流式输出后,保存到本地
*
* @param userMessage
* @return
*/
private Flux<String> generateMutlHtmlCodeStream(String userMessage) {
Flux<String> result = aiCodeGenerateService.generateMutlHtmlCodeStream(userMessage);
StringBuilder codeBuilder = new StringBuilder();
return result.doOnNext(chunk -> {
codeBuilder.append(chunk);
})
.doOnComplete(() -> {
try {
String res = codeBuilder.toString();
MultiFileCodeResult multiFileCodeResult = CodeParser.parseMultiFileCode(res);
File file = CodeFileSaver.saveMultiFileCode(multiFileCodeResult);
log.info("生成代码成功:{}", file.getAbsolutePath());
} catch (Exception e) {
log.error("生成代码失败", e);
}
});
}
/**
* 生成 HTML 代码
*
* @param userMessage
* @return
*/
private Flux<String> generateHtmlCodeStream(String userMessage) {
Flux<String> result = aiCodeGenerateService.generateHtmlCodeStream(userMessage);
StringBuilder codeBuilder = new StringBuilder();
return result.doOnNext(chunk -> {
codeBuilder.append(chunk);
}).doOnComplete(() -> {
try {
String res = codeBuilder.toString();
HtmlCodeResult htmlCodeResult = CodeParser.parseHtmlCode(res);
File file = CodeFileSaver.saveHtmlCode(htmlCodeResult);
log.info("生成代码成功:{}", file.getAbsolutePath());
} catch (Exception e) {
log.error("生成代码失败", e);
}
}
);
}
/**
* 流式输出和保存文件
*
* @param userMessage
* @param codeGenType
* @return
*/
public Flux<String> generatorAndSaveFileStream(String userMessage, CodeGenTypeEnum codeGenType) {
if (codeGenType == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
return switch (codeGenType) {
case HTML -> generateHtmlCodeStream(userMessage);
case MULTI_FILE -> generateMutlHtmlCodeStream(userMessage);
default -> throw new BusinessException(ErrorCode.PARAMS_ERROR, "不支持的类型");
};
}
测试
@Test
public void testSaveFileStream() {
String userMessage = "请生成登录界面,代码简短,需要20行内";
Flux<String> fileStream = aiCodeGeneratorFacade.generatorAndSaveFileStream(userMessage, CodeGenTypeEnum.MULTI_FILE);
List<String> block = fileStream.collectList().block();
log.info("fileStream: {}", block);
}

设计模式
策略模式
定义了一系列算法或行为,并将每个算法封装起来,使它们可以相互替换。

模板设计模式
在父类中定义了操作的流程标准,具体实现步骤让子类实现。

执行器模式
提供统一的执行入口来协调处理不同的模板和策略的调用,适合处理参数不同,但是业务相同的场景。

混合模式
执行器模式:提供统一的执行入口,根据不同的类型执行不同的操作。
策略模式:每种模式对应的解析策略作为一个单独的类维护。
模板方法生成:抽象类定一个算法的骨架,具体实现在子类中

贡献者
flycodeu
版权所有
版权归属:flycodeu