LangChain4j 的使用
流式生成:通过实现大模型逐 token 吐代码,直接适配 SSE 推送;配置核心:YAML 配好大模型地址 / 密钥,工厂类注入流式模型,AI 服务就能流式输出;解析核心:用正则提取 ``` 包裹的纯代码块,把 “自然语言 + 代码” 的混合文本变成结构化代码;和你项目的结合:你的智能体run()方法可以调用,返回的 Flux<String>推给前端,等流式结束后(doOnComplete)调用
1.@SystemMessage注解:
这个注解的作用不小直接看代码:
package com.yupi.yuaicodemother.ai;
import com.yupi.yuaicodemother.model.enums.CodeGenTypeEnum;
import dev.langchain4j.service.SystemMessage;
/**
* AI代码生成类型智能路由服务
* 使用结构化输出直接返回枚举类型
*
* @author yupi
*/
public interface AiCodeGenTypeRoutingService {
/**
* 根据用户需求智能选择代码生成类型
*
* @param userPrompt 用户输入的需求描述
* @return 推荐的代码生成类型
*/
@SystemMessage(fromResource = "prompt/codegen-routing-system-prompt.txt")
CodeGenTypeEnum routeCodeGenType(String userPrompt);
}
这就是当调用这个方法的时候,这个地址里面的内容会一同跟随者userprompt一起传给后面需要这些参数的方法或者是大模型,这个是LangChain4j 自带的。
2.为了确保能够正常输出Json格式的文件需要这样配置:

再加一个:
![]()
防止因输出的Json太长导致中断。
3.Reactor
大部分人会有疑问这个玩意是干什么的?
下面来看我的解释:
因为 LangChain 只负责 “吐出数据”,但它管不了 “怎么把数据实时推给前端”,而 Reactor 就是专门干这件事的:把流式数据变成 SSE 能识别的流。
为什么要用 Reactor?
- LangChain 的流,前端收不到
- SSE 只认 Reactor 的 Flux
- 普通 Java 流会阻塞,高并发扛不住
- Reactor 是 Spring 官方异步流式方案
下面来看怎么用:
因为sse是spring mvc自带的所以不用单独引入依赖:
下面来看引入的代码:
<!-- 1. 刚需:支撑 SSE + Flux 响应式 Web(必须有) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<!-- 用你项目的 Spring Boot 版本,无需指定也会继承父工程 -->
</dependency>
<!-- 2. LangChain4j 核心(必须有) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
<version>0.34.0</version> <!-- 稳定版,兼容 reactor 适配包 -->
</dependency>
<!-- 3. 关键:LangChain4j 转 Reactor Flux(核心适配包,你要的) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>0.34.0</version> <!-- 与 core 版本保持一致 -->
</dependency>
<!-- 4. 必加:大模型依赖(以 OpenAI 为例,你用啥模型就换啥) -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>0.34.0</version>
</dependency>
spring:
web:
flux:
timeout: 30m # SSE 长连接超时
下面来看业务代码:
@GetMapping(value = "/ai/chat", produces = "text/event-stream;charset=UTF-8")
public Flux<String> aiChat(String question) {
// 直接调用 LangChain Agent,返回 Flux
// Spring WebFlux 自动推 SSE
return codeAgent.run(question)
.doOnNext(token -> System.out.println("推送给前端:" + token));
}
produces = "text/event-stream;charset=UTF-8")这段代码相当于开启了SSE模式。

这些就是钩子下面来看一个例子应该就能看懂了:
return codeAgent.run(question)
// 每推一段给前端 → 就打印日志
.doOnNext(token -> System.out.println("推了:" + token))
// 报错了 → 就打印错误
.doOnError(e -> System.out.println("崩了:" + e.getMessage()))
// 推完了 → 就打印结束
.doOnComplete(() -> System.out.println("完事了"));
就是加一个这样的方法每当执行完这个相应的时机就会在推送一句这个括号里面的话。
4.开发实现
langchain4j:
open-ai:
streaming-chat-model:
base-url: https://api.deepseek.com # DeepSeek的接口地址
api-key: <Your API Key> # 你的DeepSeek密钥(鉴权用)
model-name: deepseek-chat # 要调用的模型名
max-tokens: 8192 # 大模型最多生成多少个token(控制长度)
log-requests: true # 打印请求日志(调试用,看发了什么给大模型)
log-responses: true # 打印响应日志(调试用,看大模型返回了什么)
作用:
给 LangChain4j 配置「连接大模型的参数」,让它能连上 DeepSeek 的流式接口(Token Stream 就是从这来的),相当于给智能体配好 “说话的嘴”。
第二步:创建 AI Service 工厂(注入流式模型):
@Configuration
public class AiCodeGeneratorServiceFactory {
// 注入普通模型(一次性返回完整结果)
@Resource private ChatModel chatModel;
// 注入流式模型(逐token返回,核心!)
@Resource private StreamingChatModel streamingChatModel;
@Bean
public AiCodeGeneratorService aiCodeGeneratorService() {
return AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel) // 支持同步调用
.streamingChatModel(streamingChatModel) // 支持流式调用
.build();
}
}
作用:
把第一步配置好的「流式模型」注入到 LangChain4j 的 AI 服务中,让你的 AiCodeGeneratorService 既能:
- 同步调用(一次性返回完整代码)
- 流式调用(逐 token 吐代码,就是你要的 Token Stream)
这一步是智能体能流式输出的核心,对应你之前的 CodeAgent.run() 底层依赖。
第三步:定义流式代码生成接口(返回 Flux<String>):
package com.yupi.yuaicodemother.ai;
import com.yupi.yuaicodemother.ai.model.HtmlCodeResult;
import com.yupi.yuaicodemother.ai.model.MultiFileCodeResult;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.UserMessage;
import reactor.core.publisher.Flux;
public interface AiCodeGeneratorService {
/**
* 生成 HTML 代码
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
HtmlCodeResult generateHtmlCode(String userMessage);
/**
* 生成多文件代码
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-multi-file-system-prompt.txt")
MultiFileCodeResult generateMultiFileCode(String userMessage);
/**
* 生成 HTML 代码
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
Flux<String> generateHtmlCodeStream(String userMessage);
/**
* 生成多文件代码
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-multi-file-system-prompt.txt")
Flux<String> generateMultiFileCodeStream(String userMessage);
/**
* 生成 Vue 项目代码(流式)
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-vue-project-system-prompt.txt")
TokenStream generateVueProjectCodeStream(@MemoryId long appId, @UserMessage String userMessage);
}
作用:
给 AI 服务定义「流式生成代码的方法」:
@SystemMessage:指定大模型的 “角色”(比如 “你是 HTML 代码生成助手”),提示词写在 txt 文件里,更易维护;- 返回
Flux<String>:这就是你之前问的 Token Stream(大模型逐段吐的代码片段); - 你直接在控制器里 return 这个 Flux,Spring 就会自动通过 SSE 推给前端(和你之前的
/ai/chat接口逻辑完全一致)。
第四步:代码解析逻辑(CodeParser):
为什么需要这一步?
大模型流式返回的 Token Stream 拼接后,是这样的混合文本:
随便写一段描述, html 格式 ```html <!DOCTYPE html> <html> <h1>Hello World!</h1> </html>
作用:
把大模型返回的「带描述的混合文本」,变成「纯 HTML/CSS/JS 代码」,封装成结构化对象(HtmlCodeResult/MultiFileCodeResult),方便你后续保存文件、展示代码等业务操作。
第五步:测试用例(验证解析逻辑):
@Test
void parseHtmlCode() {
// 模拟大模型返回的混合文本
String codeContent = "描述...<h1>Hello World!</h1>描述...";
// 调用解析方法
HtmlCodeResult result = CodeParser.parseHtmlCode(codeContent);
// 验证是否解析出了纯HTML代码
assertNotNull(result.getHtmlCode());
}
总结(核心关键点)
- 流式生成:通过
StreamingChatModel + Flux<String>实现大模型逐 token 吐代码,直接适配 SSE 推送; - 配置核心:YAML 配好大模型地址 / 密钥,工厂类注入流式模型,AI 服务就能流式输出;
- 解析核心:用正则提取 ``` 包裹的纯代码块,把 “自然语言 + 代码” 的混合文本变成结构化代码;
- 和你项目的结合:你的智能体
run()方法可以调用generateHtmlCodeStream(),返回的 Flux<String>推给前端,等流式结束后(doOnComplete)调用 CodeParser 解析出纯代码,完成 “生成→推送→解析” 全流程。
还有最后一点下面是此代码实现ai对话的业务代码:
@Autowired
private AiCodeGeneratorService aiService;
// 调用它 = 你现在要跟AI聊天了!
Flux<String> aiResponse = aiService.generateHtmlCodeStream("帮我写个登录页面");
此时会有疑问那个generateHtmlCodeStream不是接口吗为什么能直接调用,是因为这个lg框架自动给它生成了实现类,而且那一行注解@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")代替了:
-
这个 接口
AiCodeGeneratorService你不用写 implements 实现类LangChain4j 自动在底层给你生成了实现类 -
你加的
@SystemMessage("你是前端专家...")作用就是:把「系统提示词 + 你传进去的 userMessage」打包一起发给大模型
-
你 直接调接口方法:
-
aiService.generateHtmlCodeStream("写个登录页");
-
就等于调用了 LangChain 自动生成的实现类。
-
它到底帮你省略了哪些代码?
下面这些代码,你本来必须手写几千行,现在 一行都不用写,全被
接口 + 注解省略了: -
① 省略了:组装请求参数(Prompt 拼接)
-
// 这些 LangChain 自动帮你拼了 String systemPrompt = "你是一个前端代码生成专家..."; String userInput = "帮我写个登录页面"; String fullPrompt = systemPrompt + "\n用户:" + userInput;② 省略了:调用大模型 HTTP 接口
-
// 发 POST 请求给 DeepSeek HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() .uri(URI.create("https://api.deepseek.com")) .header("Authorization", "Bearer xxx") .POST(...) .build(); client.send(request, ...);③ 省略了:处理流式返回(Token Stream)
-
// 解析 SSE 流 // 一段一段读 token // 一段一段返回 InputStream stream = response.body(); BufferedReader reader = new BufferedReader(...); String line; while ((line = reader.readLine()) != null) { String token = parseToken(line); sink.next(token); // 手动塞进 Flux }④ 省略了:异常、重试、日志、超时
-
try { ... } catch (ApiException e) { ... } catch (IOException e) { ... } log.info("请求AI...");⑤ 省略了:把结果包装成 Flux
-
return Flux.create(sink -> { // 上面那一大坨全写在这里面 });@SystemMessage = 给 AI 的角色LangChain4j = 帮你把所有底层调用全写了你只需要:调方法 → 拿结果
5.在ACodeGeneratorFacde中添加流式调用的方法,也就是流式调用,即用户发送问题然后ai回复的流程
下面直接先来看代码,代码是controller层到service层然后service层里面又有工厂返回对象,然后另一个接口里面也有一个工厂返回对象然后调用相应的方法,这句话现在说有点早下面直接看代码。(用户请求过来就是点击一次是访问的两个接口)
controller层:
@RestController
@RequestMapping("/app")
public class AppController {
@Resource
private AppService appService;
/**
* 1. 创建应用(第一次进来调用)
*/
@PostMapping("/add")
public BaseResponse<Long> addApp(@RequestBody AppAddRequest request,
HttpServletRequest httpRequest) {
User loginUser = userService.getLoginUser(httpRequest);
Long appId = appService.createApp(request, loginUser);
return ResultUtils.success(appId);
}
/**
* 2. 对话生成代码(流式输出,用户真正聊天用)
*/
@GetMapping(value = "/chat", produces = "text/event-stream")
public Flux<String> chat(
@RequestParam Long appId,
@RequestParam String message,
HttpServletRequest httpRequest) {
User loginUser = userService.getLoginUser(httpRequest);
// 核心!就是调用你刚才那个 chatToGenCode
return appService.chatToGenCode(appId, message, loginUser);
}
}
用户点击一次先访问第一个接口再访问第二个接口。
第一个接口关键一行: Long appId = appService.createApp(request, loginUser);
第二个接口关键一行: return appService.chatToGenCode(appId, message, loginUser);
下面直接来看sevice层:
@Service
public class AppServiceImpl implements AppService {
@Resource
private AiCodeGeneratorFacade aiCodeGeneratorFacade;
@Resource
private AiCodeGenTypeRoutingServiceFactory aiCodeGenTypeRoutingServiceFactory;
/**
* 1. 创建应用:解析类型,存数据库
*/
@Override
public Long createApp(AppAddRequest appAddRequest, User loginUser) {
// 参数校验
String initPrompt = appAddRequest.getInitPrompt();
ThrowUtils.throwIf(StrUtil.isBlank(initPrompt), ErrorCode.PARAMS_ERROR, "初始化 prompt 不能为空");
// 构造入库对象
App app = new App();
BeanUtil.copyProperties(appAddRequest, app);
app.setUserId(loginUser.getId());
// 应用名称暂时为 initPrompt 前 12 位
app.setAppName(initPrompt.substring(0, Math.min(initPrompt.length(), 12)));
// 使用 AI 智能选择代码生成类型(多例模式)
AiCodeGenTypeRoutingService aiCodeGenTypeRoutingService = aiCodeGenTypeRoutingServiceFactory.createAiCodeGenTypeRoutingService();
CodeGenTypeEnum selectedCodeGenType = aiCodeGenTypeRoutingService.routeCodeGenType(initPrompt);
app.setCodeGenType(selectedCodeGenType.getValue());
// 插入数据库
boolean result = this.save(app);
ThrowUtils.throwIf(!result, ErrorCode.OPERATION_ERROR);
log.info("应用创建成功,ID: {}, 类型: {}", app.getId(), selectedCodeGenType.getValue());
return app.getId();
}
/**
* 2. 对话生成:真正调用 generateAndSaveCodeStream
*/
@Override
public Flux<String> chatToGenCode(Long appId, String message, User loginUser) {
// 1. 查数据库,拿到已经解析好的类型
App app = appMapper.selectById(appId);
String codeGenTypeValue = app.getCodeGenType();
CodeGenTypeEnum codeGenTypeEnum = CodeGenTypeEnum.getEnumByValue(codeGenTypeValue);
// 2. 权限判断
if (!app.getUserId().equals(loginUser.getId())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
}
// 3. 核心!调用你那个流式方法
return aiCodeGeneratorFacade.generateAndSaveCodeStream(
message,
codeGenTypeEnum,
appId
);
}
}
第一个方法和第二个方法都用到了工厂方法。
这里逻辑是这样的就是第一个接口先存数据就是大模型解析出来codeGenTypeValue CodeGenTypeEnum然后存入数据库然后第二个接口去取然后就是第二个接口里面的方法去取然后返回相应的实例,这里就有一个疑问是怎么解析的的?下面先来解决这个疑惑
来看这行代码: AiCodeGenTypeRoutingService routingService =
aiCodeGenTypeRoutingServiceFactory.createAiCodeGenTypeRoutingService();//从工厂获取对象
CodeGenTypeEnum codeGenType = routingService.routeCodeGenType(userPrompt);//调用一个方法解析。
就以上这两步现在来看这个对应的工厂和这个方法。
package com.yupi.yuaicodemother.ai;
import com.yupi.yuaicodemother.utils.SpringContextUtil;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.service.AiServices;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* AI代码生成类型路由服务工厂
*
* @author yupi
*/
@Slf4j
@Configuration
public class AiCodeGenTypeRoutingServiceFactory {
/**
* 创建AI代码生成类型路由服务实例
*/
public AiCodeGenTypeRoutingService createAiCodeGenTypeRoutingService() {
ChatModel chatModel = SpringContextUtil.getBean("routingChatModelPrototype", ChatModel.class);
return AiServices.builder(AiCodeGenTypeRoutingService.class)
.chatModel(chatModel)
.build();
}
/**
* 默认提供一个 Bean
*/
@Bean
public AiCodeGenTypeRoutingService aiCodeGenTypeRoutingService() {
return createAiCodeGenTypeRoutingService();
}
}
就是直接从工厂获取这样的一个对象,因为接口自带实现类所以这个的意思就是把接口对应的那个实现类以及大模型封装成了一个新的对象。其实对这个的解释可以直接来看源码AiService的源码因为返回的是它这个对象嘛。
public static <T> AiServices<T> builder(Class<T> aiService) {
AiServiceContext context = new AiServiceContext(aiService);
Iterator var2 = ServiceHelper.loadFactories(AiServicesFactory.class).iterator();
if (var2.hasNext()) {
AiServicesFactory factory = (AiServicesFactory)var2.next();
return factory.create(context);
} else {
return new DefaultAiServices(context);
}
}
public AiServices<T> chatModel(ChatModel chatModel) {
this.context.chatModel = chatModel;
return this;
}
这不就是赋值吗对吧,把一个实现类赋值进去再把一个大模型赋值进去,这一步是必须的,因为你那个接口也可以称为实现类加了@SystemMessage本质上就是大模型去读取这个注解里面地址相应的内容,如果你不给它赋值大模型那不就没用了吗对吧,所以当加了这个注解的时候要记得给这个接口(实现类)进行一层封装把大模型也穿进去,一般这种写在配置类就可以。
不知道此时大家有没有一个这样的疑惑为什么AiServices的类型也是AiCodeGenTypeRoutingService下面我来解释:
AiServices 是 LangChain4j 提供的Builder 模式工具类(类似 StringBuilder/Spring 的 BeanBuilder),它的核心作用是:
接收「目标业务接口」+「大模型配置(ChatModel)」,生成「该接口的动态代理实例」
它本身不是业务接口 / 实现类,因此不可能把返回值变成 AiServices 类型—— 就像你用 new ArrayList.Builder().add("a").build() 时,返回的是 List<String> 而非 ArrayList.Builder;你用 Spring 的 ProxyFactory 生成代理时,返回的是目标接口类型而非 ProxyFactory 类型。
为什么返回值必须是 AiCodeGenTypeRoutingService:
动态代理的 “硬约束”:JDK 动态代理只能生成 “实现目标接口” 的代理类
LangChain4j 的 AiServices.build() 底层基于 JDK 动态代理,而 JDK 动态代理有个核心规则:
动态生成的代理类,必须「实现你传入的接口」(即
AiCodeGenTypeRoutingService.class),因此代理实例可以直接赋值给该接口类型。
换句话说:
- 你传给
AiServices.builder()的是AiCodeGenTypeRoutingService.class(目标接口); - 动态代理生成的类是:
class com.sun.proxy.$ProxyXXX implements AiCodeGenTypeRoutingService; - 这个代理实例的 “类型身份” 是
AiCodeGenTypeRoutingService(因为实现了该接口),而非AiServices。
当然就按业务来说你本来就该返回这样的类要不然没办法调用方法呀。
是因为本来就没有返回值的类型就是因为传入了这个实现类才有了返回值类型
所以这一行:
AiCodeGenTypeRoutingService aiCodeGenTypeRoutingService = aiCodeGenTypeRoutingServiceFactory.createAiCodeGenTypeRoutingService();
就是获取对象。来接着往下看:
CodeGenTypeEnum selectedCodeGenType = aiCodeGenTypeRoutingService.routeCodeGenType(initPrompt);
拿这个对象调用它的方法。
package com.yupi.yuaicodemother.ai;
import com.yupi.yuaicodemother.model.enums.CodeGenTypeEnum;
import dev.langchain4j.service.SystemMessage;
/**
* AI代码生成类型智能路由服务
* 使用结构化输出直接返回枚举类型
*
* @author yupi
*/
public interface AiCodeGenTypeRoutingService {
/**
* 根据用户需求智能选择代码生成类型
*
* @param userPrompt 用户输入的需求描述
* @return 推荐的代码生成类型
*/
@SystemMessage(fromResource = "prompt/codegen-routing-system-prompt.txt")
CodeGenTypeEnum routeCodeGenType(String userPrompt);
}
这个方法在这里,为什么直接调接口就可以为什么这个接口能当实现类,上面有说。
这个方法的返回值类型是codeGenTypeEnum。
所以这个方法就是解析出来枚举。
这是枚举的代码:
package com.yupi.yuaicodemother.model.enums;
import cn.hutool.core.util.ObjUtil;
import lombok.Getter;
/**
* 代码生成类型枚举
*/
@Getter
public enum CodeGenTypeEnum {
HTML("原生 HTML 模式", "html"),
MULTI_FILE("原生多文件模式", "multi_file"),
VUE_PROJECT("Vue 工程模式", "vue_project");
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;
}
}
"原生 HTML 模式""原生多文件模式""Vue 工程模式"根据这三种返回相应的“html”什么什么的
然后:
CodeGenTypeEnum selectedCodeGenType = aiCodeGenTypeRoutingService.routeCodeGenType(initPrompt); app.setCodeGenType(selectedCodeGenType.getValue());
这个就是获取到类型然后存入数据库。
然后就该第二个接口的事啦;
第二个接口不是
chatToGenCode这个方法吗现在来看这个方法。
@Override
public Flux<String> chatToGenCode(Long appId, String message, User loginUser) {
// 1. 参数校验
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 ID 错误");
ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "提示词不能为空");
// 2. 查询应用信息
App app = this.getById(appId);
ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR, "应用不存在");
// 3. 权限校验,仅本人可以和自己的应用对话
if (!app.getUserId().equals(loginUser.getId())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限访问该应用");
}
// 4. 获取应用的代码生成类型
String codeGenType = app.getCodeGenType();
CodeGenTypeEnum codeGenTypeEnum = CodeGenTypeEnum.getEnumByValue(codeGenType);
if (codeGenTypeEnum == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "应用代码生成类型错误");
}
// 5. 在调用 AI 前,先保存用户消息到数据库中
chatHistoryService.addChatMessage(appId, message, ChatHistoryMessageTypeEnum.USER.getValue(), loginUser.getId());
// 6. 设置监控上下文(用户 ID 和应用 ID)
MonitorContextHolder.setContext(
MonitorContext.builder()
.userId(loginUser.getId().toString())
.appId(appId.toString())
.build()
);
// 7. 调用 AI 生成代码(流式)
Flux<String> codeStream = aiCodeGeneratorFacade.generateAndSaveCodeStream(message, codeGenTypeEnum, appId);
// 8. 收集 AI 响应的内容,并且在完成后保存记录到对话历史
return streamHandlerExecutor.doExecute(codeStream, chatHistoryService, appId, loginUser, codeGenTypeEnum)
.doFinally(signalType -> {
// 流结束时清理(无论成功/失败/取消)
MonitorContextHolder.clearContext();
});
}
它是一个流式输出。
这里有一个亮点:
if (!app.getUserId().equals(loginUser.getId())) { throw new BusinessException("无权限访问"); } 线程之间不是线程隔离的吗为什么还要这样判断呢。
下面来看就是因为这种情况:
B 登录了自己的账号
浏览器里存的是 B 的 token / 登录信息
他手动在地址栏 / 接口里改成 A 的 appId
这时候当前登录用户依然是 B,不会变成 A
那这时候后端拿到的是什么?
loginUser = B(从 token 里取的,真实登录人)
appId = A 的应用 ID(B 手动改的)
后端去查数据库:
这个 appId 对应的应用是 A 创建的
但现在登录的是 B
→ 不是同一个人!
所以那行代码直接拦住
然后接着往下看:
String codeGenType = app.getCodeGenType();
获取类型比如说“html”或“vue”等等。
CodeGenTypeEnum codeGenTypeEnum = CodeGenTypeEnum.getEnumByValue(codeGenType);
获取枚举下面会用到。
// 5. 在调用 AI 前,先保存用户消息到数据库中 chatHistoryService.addChatMessage(appId, message, ChatHistoryMessageTypeEnum.USER.getValue(), loginUser.getId());
这个方法代码:
@Override
public boolean addChatMessage(Long appId, String message, String messageType, Long userId) {
// 基础校验
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID不能为空");
ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "消息内容不能为空");
ThrowUtils.throwIf(StrUtil.isBlank(messageType), ErrorCode.PARAMS_ERROR, "消息类型不能为空");
ThrowUtils.throwIf(userId == null || userId <= 0, ErrorCode.PARAMS_ERROR, "用户ID不能为空");
// 验证消息类型是否有效
ChatHistoryMessageTypeEnum messageTypeEnum = ChatHistoryMessageTypeEnum.getEnumByValue(messageType);
ThrowUtils.throwIf(messageTypeEnum == null, ErrorCode.PARAMS_ERROR, "不支持的消息类型");
// 插入数据库
ChatHistory chatHistory = ChatHistory.builder()
.appId(appId)
.message(message)
.messageType(messageType)
.userId(userId)
.build();
return this.save(chatHistory);
}
存入数据库中接着往下看。
Flux<String> codeStream = aiCodeGeneratorFacade.generateAndSaveCodeStream(message, codeGenTypeEnum, appId)
这段代码是关键看这个aiCodeGeneratorFacade
package com.yupi.yuaicodemother.core;
import cn.hutool.json.JSONUtil;
import com.yupi.yuaicodemother.ai.AiCodeGeneratorService;
import com.yupi.yuaicodemother.ai.AiCodeGeneratorServiceFactory;
import com.yupi.yuaicodemother.ai.model.HtmlCodeResult;
import com.yupi.yuaicodemother.ai.model.MultiFileCodeResult;
import com.yupi.yuaicodemother.ai.model.message.AiResponseMessage;
import com.yupi.yuaicodemother.ai.model.message.ToolExecutedMessage;
import com.yupi.yuaicodemother.ai.model.message.ToolRequestMessage;
import com.yupi.yuaicodemother.constant.AppConstant;
import com.yupi.yuaicodemother.core.builder.VueProjectBuilder;
import com.yupi.yuaicodemother.core.parser.CodeParserExecutor;
import com.yupi.yuaicodemother.core.saver.CodeFileSaverExecutor;
import com.yupi.yuaicodemother.exception.BusinessException;
import com.yupi.yuaicodemother.exception.ErrorCode;
import com.yupi.yuaicodemother.model.enums.CodeGenTypeEnum;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.tool.ToolExecution;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import java.io.File;
/**
* AI 代码生成门面类,组合代码生成和保存功能
*/
@Service
@Slf4j
public class AiCodeGeneratorFacade {
@Resource
private AiCodeGeneratorServiceFactory aiCodeGeneratorServiceFactory;
@Resource
private VueProjectBuilder vueProjectBuilder;
/**
* 统一入口:根据类型生成并保存代码
*
* @param userMessage 用户提示词
* @param codeGenTypeEnum 生成类型
* @param appId 应用 ID
* @return 保存的目录
*/
public File generateAndSaveCode(String userMessage, CodeGenTypeEnum codeGenTypeEnum, Long appId) {
if (codeGenTypeEnum == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成类型不能为空");
}
// 根据 appId 获取相应的 AI 服务实例
AiCodeGeneratorService aiCodeGeneratorService = aiCodeGeneratorServiceFactory.getAiCodeGeneratorService(appId, codeGenTypeEnum);
return switch (codeGenTypeEnum) {
case HTML -> {
HtmlCodeResult result = aiCodeGeneratorService.generateHtmlCode(userMessage);
yield CodeFileSaverExecutor.executeSaver(result, CodeGenTypeEnum.HTML, appId);
}
case MULTI_FILE -> {
MultiFileCodeResult result = aiCodeGeneratorService.generateMultiFileCode(userMessage);
yield CodeFileSaverExecutor.executeSaver(result, CodeGenTypeEnum.MULTI_FILE, appId);
}
default -> {
String errorMessage = "不支持的生成类型:" + codeGenTypeEnum.getValue();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, errorMessage);
}
};
}
/**
* 统一入口:根据类型生成并保存代码(流式)
*
* @param userMessage 用户提示词
* @param codeGenTypeEnum 生成类型
* @param appId 应用 ID
* @return 保存的目录
*/
public Flux<String> generateAndSaveCodeStream(String userMessage, CodeGenTypeEnum codeGenTypeEnum, Long appId) {
if (codeGenTypeEnum == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成类型不能为空");
}
// 根据 appId 获取相应的 AI 服务实例
AiCodeGeneratorService aiCodeGeneratorService = aiCodeGeneratorServiceFactory.getAiCodeGeneratorService(appId, codeGenTypeEnum);
return switch (codeGenTypeEnum) {
case HTML -> {
Flux<String> codeStream = aiCodeGeneratorService.generateHtmlCodeStream(userMessage);
yield processCodeStream(codeStream, CodeGenTypeEnum.HTML, appId);
}
case MULTI_FILE -> {
Flux<String> codeStream = aiCodeGeneratorService.generateMultiFileCodeStream(userMessage);
yield processCodeStream(codeStream, CodeGenTypeEnum.MULTI_FILE, appId);
}
case VUE_PROJECT -> {
TokenStream tokenStream = aiCodeGeneratorService.generateVueProjectCodeStream(appId, userMessage);
yield processTokenStream(tokenStream, appId);
}
default -> {
String errorMessage = "不支持的生成类型:" + codeGenTypeEnum.getValue();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, errorMessage);
}
};
}
/**
* 将 TokenStream 转换为 Flux<String>,并传递工具调用信息
*
* @param tokenStream TokenStream 对象
* @param appId 应用 ID
* @return Flux<String> 流式响应
*/
private Flux<String> processTokenStream(TokenStream tokenStream, Long appId) {
return Flux.create(sink -> {
tokenStream.onPartialResponse((String partialResponse) -> {
AiResponseMessage aiResponseMessage = new AiResponseMessage(partialResponse);
sink.next(JSONUtil.toJsonStr(aiResponseMessage));
})
.onPartialToolExecutionRequest((index, toolExecutionRequest) -> {
ToolRequestMessage toolRequestMessage = new ToolRequestMessage(toolExecutionRequest);
sink.next(JSONUtil.toJsonStr(toolRequestMessage));
})
.onToolExecuted((ToolExecution toolExecution) -> {
ToolExecutedMessage toolExecutedMessage = new ToolExecutedMessage(toolExecution);
sink.next(JSONUtil.toJsonStr(toolExecutedMessage));
})
.onCompleteResponse((ChatResponse response) -> {
// 执行 Vue 项目构建(同步执行,确保预览时项目已就绪)
String projectPath = AppConstant.CODE_OUTPUT_ROOT_DIR + "/vue_project_" + appId;
vueProjectBuilder.buildProject(projectPath);
sink.complete();
})
.onError((Throwable error) -> {
error.printStackTrace();
sink.error(error);
})
.start();
});
}
/**
* 通用流式代码处理方法
*
* @param codeStream 代码流
* @param codeGenType 代码生成类型
* @param appId 应用 ID
* @return 流式响应
*/
private Flux<String> processCodeStream(Flux<String> codeStream, CodeGenTypeEnum codeGenType, Long appId) {
// 字符串拼接器,用于当流式返回所有的代码之后,再保存代码
StringBuilder codeBuilder = new StringBuilder();
return codeStream.doOnNext(chunk -> {
// 实时收集代码片段
codeBuilder.append(chunk);
}).doOnComplete(() -> {
// 流式返回完成后,保存代码
try {
String completeCode = codeBuilder.toString();
// 使用执行器解析代码
Object parsedResult = CodeParserExecutor.executeParser(completeCode, codeGenType);
// 使用执行器保存代码
File saveDir = CodeFileSaverExecutor.executeSaver(parsedResult, codeGenType, appId);
log.info("保存成功,目录为:{}", saveDir.getAbsolutePath());
} catch (Exception e) {
log.error("保存失败: {}", e.getMessage());
}
});
}
}
就是这个方法:
public Flux<String> generateAndSaveCodeStream(String userMessage, CodeGenTypeEnum codeGenTypeEnum, Long appId) {
if (codeGenTypeEnum == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR, "生成类型不能为空");
}
// 根据 appId 获取相应的 AI 服务实例
AiCodeGeneratorService aiCodeGeneratorService = aiCodeGeneratorServiceFactory.getAiCodeGeneratorService(appId, codeGenTypeEnum);
return switch (codeGenTypeEnum) {
case HTML -> {
Flux<String> codeStream = aiCodeGeneratorService.generateHtmlCodeStream(userMessage);
yield processCodeStream(codeStream, CodeGenTypeEnum.HTML, appId);
}
case MULTI_FILE -> {
Flux<String> codeStream = aiCodeGeneratorService.generateMultiFileCodeStream(userMessage);
yield processCodeStream(codeStream, CodeGenTypeEnum.MULTI_FILE, appId);
}
case VUE_PROJECT -> {
TokenStream tokenStream = aiCodeGeneratorService.generateVueProjectCodeStream(appId, userMessage);
yield processTokenStream(tokenStream, appId);
}
default -> {
String errorMessage = "不支持的生成类型:" + codeGenTypeEnum.getValue();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, errorMessage);
}
};
下面直接看这个方法:
// 根据 appId 获取相应的 AI 服务实例 AiCodeGeneratorService aiCodeGeneratorService = aiCodeGeneratorServiceFactory.getAiCodeGeneratorService(appId, codeGenTypeEnum);
看这里枚举开始生效啦。
看Factory里面的代码:
package com.yupi.yuaicodemother.ai;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.yupi.yuaicodemother.ai.guardrail.PromptSafetyInputGuardrail;
import com.yupi.yuaicodemother.ai.guardrail.RetryOutputGuardrail;
import com.yupi.yuaicodemother.ai.tools.*;
import com.yupi.yuaicodemother.exception.BusinessException;
import com.yupi.yuaicodemother.exception.ErrorCode;
import com.yupi.yuaicodemother.model.enums.CodeGenTypeEnum;
import com.yupi.yuaicodemother.service.ChatHistoryService;
import com.yupi.yuaicodemother.utils.SpringContextUtil;
import dev.langchain4j.community.store.memory.chat.redis.RedisChatMemoryStore;
import dev.langchain4j.data.message.ToolExecutionResultMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.service.AiServices;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
/**
* AI 服务创建工厂
*/
@Configuration
@Slf4j
public class AiCodeGeneratorServiceFactory {
@Resource(name = "openAiChatModel")
private ChatModel chatModel;
@Resource
private RedisChatMemoryStore redisChatMemoryStore;
@Resource
private ChatHistoryService chatHistoryService;
@Resource
private ToolManager toolManager;
/**
* AI 服务实例缓存
* 缓存策略:
* - 最大缓存 1000 个实例
* - 写入后 30 分钟过期
* - 访问后 10 分钟过期
*/
private final Cache<String, AiCodeGeneratorService> serviceCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(30))
.expireAfterAccess(Duration.ofMinutes(10))
.removalListener((key, value, cause) -> {
log.debug("AI 服务实例被移除,缓存键: {}, 原因: {}", key, cause);
})
.build();
/**
* 根据 appId 获取服务(为了兼容老逻辑)
*
* @param appId
* @return
*/
public AiCodeGeneratorService getAiCodeGeneratorService(long appId) {
return getAiCodeGeneratorService(appId, CodeGenTypeEnum.HTML);
}
/**
* 根据 appId 获取服务
*
* @param appId 应用 id
* @param codeGenType 生成类型
* @return
*/
public AiCodeGeneratorService getAiCodeGeneratorService(long appId, CodeGenTypeEnum codeGenType) {
String cacheKey = buildCacheKey(appId, codeGenType);
return serviceCache.get(cacheKey, key -> createAiCodeGeneratorService(appId, codeGenType));
}
/**
* 创建新的 AI 服务实例
*
* @param appId 应用 id
* @param codeGenType 生成类型
* @return
*/
private AiCodeGeneratorService createAiCodeGeneratorService(long appId, CodeGenTypeEnum codeGenType) {
log.info("为 appId: {} 创建新的 AI 服务实例", appId);
// 根据 appId 构建独立的对话记忆
MessageWindowChatMemory chatMemory = MessageWindowChatMemory
.builder()
.id(appId)
.chatMemoryStore(redisChatMemoryStore)
.maxMessages(20)
.build();
// 从数据库中加载对话历史到记忆中
chatHistoryService.loadChatHistoryToMemory(appId, chatMemory, 20);
return switch (codeGenType) {
// Vue 项目生成,使用工具调用和推理模型
case VUE_PROJECT -> {
// 使用多例模式的 StreamingChatModel 解决并发问题
StreamingChatModel reasoningStreamingChatModel = SpringContextUtil.getBean("reasoningStreamingChatModelPrototype", StreamingChatModel.class);
yield AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel)
.streamingChatModel(reasoningStreamingChatModel)
.chatMemoryProvider(memoryId -> chatMemory)
.tools(toolManager.getAllTools())
// 处理工具调用幻觉问题
.hallucinatedToolNameStrategy(toolExecutionRequest ->
ToolExecutionResultMessage.from(toolExecutionRequest,
"Error: there is no tool called " + toolExecutionRequest.name())
)
.maxSequentialToolsInvocations(20) // 最多连续调用 20 次工具
.inputGuardrails(new PromptSafetyInputGuardrail()) // 添加输入护轨
// .outputGuardrails(new RetryOutputGuardrail()) // 添加输出护轨,为了流式输出,这里不使用
.build();
}
// HTML 和 多文件生成,使用流式对话模型
case HTML, MULTI_FILE -> {
// 使用多例模式的 StreamingChatModel 解决并发问题
StreamingChatModel openAiStreamingChatModel = SpringContextUtil.getBean("streamingChatModelPrototype", StreamingChatModel.class);
yield AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel)
.streamingChatModel(openAiStreamingChatModel)
.chatMemory(chatMemory)
.inputGuardrails(new PromptSafetyInputGuardrail()) // 添加输入护轨
// .outputGuardrails(new RetryOutputGuardrail()) // 添加输出护轨,为了流式输出,这里不使用
.build();
}
default ->
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的代码生成类型: " + codeGenType.getValue());
};
}
/**
* 创建 AI 代码生成器服务
*
* @return
*/
@Bean
public AiCodeGeneratorService aiCodeGeneratorService() {
return getAiCodeGeneratorService(0);
}
/**
* 构造缓存键
*
* @param appId
* @param codeGenType
* @return
*/
private String buildCacheKey(long appId, CodeGenTypeEnum codeGenType) {
return appId + "_" + codeGenType.getValue();
}
}
看这个方法getAiCodeGeneratorService:
好像有好多这个方法哈哈哈,那说明这里用了重载看传了几个参数吧,(appId, codeGenTypeEnum)为两个所以是这个代码
*/
public AiCodeGeneratorService getAiCodeGeneratorService(long appId, CodeGenTypeEnum codeGenType) {
String cacheKey = buildCacheKey(appId, codeGenType);
return serviceCache.get(cacheKey, key -> createAiCodeGeneratorService(appId, codeGenType));
}
再看这个代码里面的
createAiCodeGeneratorService(appId, codeGenType)这个方法
private AiCodeGeneratorService createAiCodeGeneratorService(long appId, CodeGenTypeEnum codeGenType) {
log.info("为 appId: {} 创建新的 AI 服务实例", appId);
// 根据 appId 构建独立的对话记忆
MessageWindowChatMemory chatMemory = MessageWindowChatMemory
.builder()
.id(appId)
.chatMemoryStore(redisChatMemoryStore)
.maxMessages(20)
.build();
// 从数据库中加载对话历史到记忆中
chatHistoryService.loadChatHistoryToMemory(appId, chatMemory, 20);
return switch (codeGenType) {
// Vue 项目生成,使用工具调用和推理模型
case VUE_PROJECT -> {
// 使用多例模式的 StreamingChatModel 解决并发问题
StreamingChatModel reasoningStreamingChatModel = SpringContextUtil.getBean("reasoningStreamingChatModelPrototype", StreamingChatModel.class);
yield AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel)
.streamingChatModel(reasoningStreamingChatModel)
.chatMemoryProvider(memoryId -> chatMemory)
.tools(toolManager.getAllTools())
// 处理工具调用幻觉问题
.hallucinatedToolNameStrategy(toolExecutionRequest ->
ToolExecutionResultMessage.from(toolExecutionRequest,
"Error: there is no tool called " + toolExecutionRequest.name())
)
.maxSequentialToolsInvocations(20) // 最多连续调用 20 次工具
.inputGuardrails(new PromptSafetyInputGuardrail()) // 添加输入护轨
// .outputGuardrails(new RetryOutputGuardrail()) // 添加输出护轨,为了流式输出,这里不使用
.build();
}
// HTML 和 多文件生成,使用流式对话模型
case HTML, MULTI_FILE -> {
// 使用多例模式的 StreamingChatModel 解决并发问题
StreamingChatModel openAiStreamingChatModel = SpringContextUtil.getBean("streamingChatModelPrototype", StreamingChatModel.class);
yield AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel)
.streamingChatModel(openAiStreamingChatModel)
.chatMemory(chatMemory)
.inputGuardrails(new PromptSafetyInputGuardrail()) // 添加输入护轨
// .outputGuardrails(new RetryOutputGuardrail()) // 添加输出护轨,为了流式输出,这里不使用
.build();
}
default ->
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的代码生成类型: " + codeGenType.getValue());
};
}
来看: chatHistoryService.loadChatHistoryToMemory(appId, chatMemory, 20);这个方法在下面
package com.yupi.yuaicodemother.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import com.mybatisflex.spring.service.impl.ServiceImpl;
import com.yupi.yuaicodemother.constant.UserConstant;
import com.yupi.yuaicodemother.exception.ErrorCode;
import com.yupi.yuaicodemother.exception.ThrowUtils;
import com.yupi.yuaicodemother.model.dto.chathistory.ChatHistoryQueryRequest;
import com.yupi.yuaicodemother.model.entity.App;
import com.yupi.yuaicodemother.model.entity.ChatHistory;
import com.yupi.yuaicodemother.mapper.ChatHistoryMapper;
import com.yupi.yuaicodemother.model.entity.User;
import com.yupi.yuaicodemother.model.enums.ChatHistoryMessageTypeEnum;
import com.yupi.yuaicodemother.service.AppService;
import com.yupi.yuaicodemother.service.ChatHistoryService;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
/**
* 对话历史 服务层实现。
*
* @author <a href="https://github.com/liyupi">程序员鱼皮</a>
*/
@Service
@Slf4j
public class ChatHistoryServiceImpl extends ServiceImpl<ChatHistoryMapper, ChatHistory> implements ChatHistoryService {
@Resource
@Lazy
private AppService appService;
@Override
public boolean addChatMessage(Long appId, String message, String messageType, Long userId) {
// 基础校验
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID不能为空");
ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "消息内容不能为空");
ThrowUtils.throwIf(StrUtil.isBlank(messageType), ErrorCode.PARAMS_ERROR, "消息类型不能为空");
ThrowUtils.throwIf(userId == null || userId <= 0, ErrorCode.PARAMS_ERROR, "用户ID不能为空");
// 验证消息类型是否有效
ChatHistoryMessageTypeEnum messageTypeEnum = ChatHistoryMessageTypeEnum.getEnumByValue(messageType);
ThrowUtils.throwIf(messageTypeEnum == null, ErrorCode.PARAMS_ERROR, "不支持的消息类型");
// 插入数据库
ChatHistory chatHistory = ChatHistory.builder()
.appId(appId)
.message(message)
.messageType(messageType)
.userId(userId)
.build();
return this.save(chatHistory);
}
@Override
public boolean deleteByAppId(Long appId) {
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID不能为空");
QueryWrapper queryWrapper = QueryWrapper.create()
.eq("appId", appId);
return this.remove(queryWrapper);
}
@Override
public Page<ChatHistory> listAppChatHistoryByPage(Long appId, int pageSize,
LocalDateTime lastCreateTime,
User loginUser) {
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID不能为空");
ThrowUtils.throwIf(pageSize <= 0 || pageSize > 50, ErrorCode.PARAMS_ERROR, "页面大小必须在1-50之间");
ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR);
// 验证权限:只有应用创建者和管理员可以查看
App app = appService.getById(appId);
ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR, "应用不存在");
boolean isAdmin = UserConstant.ADMIN_ROLE.equals(loginUser.getUserRole());
boolean isCreator = app.getUserId().equals(loginUser.getId());
ThrowUtils.throwIf(!isAdmin && !isCreator, ErrorCode.NO_AUTH_ERROR, "无权查看该应用的对话历史");
// 构建查询条件
ChatHistoryQueryRequest queryRequest = new ChatHistoryQueryRequest();
queryRequest.setAppId(appId);
queryRequest.setLastCreateTime(lastCreateTime);
QueryWrapper queryWrapper = this.getQueryWrapper(queryRequest);
// 查询数据
return this.page(Page.of(1, pageSize), queryWrapper);
}
@Override
public int loadChatHistoryToMemory(Long appId, MessageWindowChatMemory chatMemory, int maxCount) {
try {
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(ChatHistory::getAppId, appId)
.orderBy(ChatHistory::getCreateTime, false)
.limit(1, maxCount);
List<ChatHistory> historyList = this.list(queryWrapper);
if (CollUtil.isEmpty(historyList)) {
return 0;
}
// 反转列表,确保按照时间正序(老的在前,新的在后)
historyList = historyList.reversed();
// 按照时间顺序将消息添加到记忆中
int loadedCount = 0;
// 先清理历史缓存,防止重复加载
chatMemory.clear();
for (ChatHistory history : historyList) {
if (ChatHistoryMessageTypeEnum.USER.getValue().equals(history.getMessageType())) {
chatMemory.add(UserMessage.from(history.getMessage()));
} else if (ChatHistoryMessageTypeEnum.AI.getValue().equals(history.getMessageType())) {
chatMemory.add(AiMessage.from(history.getMessage()));
}
loadedCount++;
}
log.info("成功为 appId: {} 加载 {} 条历史消息", appId, loadedCount);
return loadedCount;
} catch (Exception e) {
log.error("加载历史对话失败,appId: {}, error: {}", appId, e.getMessage(), e);
// 加载失败不影响系统运行,只是没有历史上下文
return 0;
}
}
/**
* 获取查询包装类
*
* @param chatHistoryQueryRequest
* @return
*/
@Override
public QueryWrapper getQueryWrapper(ChatHistoryQueryRequest chatHistoryQueryRequest) {
QueryWrapper queryWrapper = QueryWrapper.create();
if (chatHistoryQueryRequest == null) {
return queryWrapper;
}
Long id = chatHistoryQueryRequest.getId();
String message = chatHistoryQueryRequest.getMessage();
String messageType = chatHistoryQueryRequest.getMessageType();
Long appId = chatHistoryQueryRequest.getAppId();
Long userId = chatHistoryQueryRequest.getUserId();
LocalDateTime lastCreateTime = chatHistoryQueryRequest.getLastCreateTime();
String sortField = chatHistoryQueryRequest.getSortField();
String sortOrder = chatHistoryQueryRequest.getSortOrder();
// 拼接查询条件
queryWrapper.eq("id", id)
.like("message", message)
.eq("messageType", messageType)
.eq("appId", appId)
.eq("userId", userId);
// 游标查询逻辑 - 只使用 createTime 作为游标
if (lastCreateTime != null) {
queryWrapper.lt("createTime", lastCreateTime);
}
// 排序
if (StrUtil.isNotBlank(sortField)) {
queryWrapper.orderBy(sortField, "ascend".equals(sortOrder));
} else {
// 默认按创建时间降序排列
queryWrapper.orderBy("createTime", false);
}
return queryWrapper;
}
}
来接着看:
// 使用多例模式的 StreamingChatModel 解决并发问题
StreamingChatModel reasoningStreamingChatModel = SpringContextUtil.getBean("reasoningStreamingChatModelPrototype", StreamingChatModel.class);
// 处理工具调用幻觉问题
.hallucinatedToolNameStrategy(toolExecutionRequest ->
ToolExecutionResultMessage.from(toolExecutionRequest,
"Error: there is no tool called " + toolExecutionRequest.name())
)
// HTML 和 多文件生成,使用流式对话模型
case HTML, MULTI_FILE -> {
// 使用多例模式的 StreamingChatModel 解决并发问题
StreamingChatModel openAiStreamingChatModel = SpringContextUtil.getBean("streamingChatModelPrototype", StreamingChatModel.class);
yield AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel)
.streamingChatModel(openAiStreamingChatModel)
.chatMemory(chatMemory)
.inputGuardrails(new PromptSafetyInputGuardrail()) // 添加输入护轨
// .outputGuardrails(new RetryOutputGuardrail()) // 添加输出护轨,为了流式输出,这里不使用
.build();
}
default ->
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的代码生成类型: " + codeGenType.getValue());
这就是根据type返回相应的实例。
那么这段代码为什么可以生成新的实例呢也没有new呀,
StreamingChatModel reasoningStreamingChatModel = SpringContextUtil.getBean("reasoningStreamingChatModelPrototype", StreamingChatModel.class);l来看这一行。
看这个StreamingChatModel这个类,
package dev.langchain4j.model.chat;
import dev.langchain4j.agent.tool.ToolExecutionRequest;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.ModelProvider;
import dev.langchain4j.model.chat.listener.ChatModelListener;
import dev.langchain4j.model.chat.request.ChatRequest;
import dev.langchain4j.model.chat.request.ChatRequestParameters;
import dev.langchain4j.model.chat.request.DefaultChatRequestParameters;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import static dev.langchain4j.model.ModelProvider.OTHER;
import static dev.langchain4j.model.chat.ChatModelListenerUtils.onRequest;
import static dev.langchain4j.model.chat.ChatModelListenerUtils.onResponse;
/**
* Represents a language model that has a chat API and can stream a response one token at a time.
*
* @see ChatModel
*/
public interface StreamingChatModel {
/**
* This is the main API to interact with the chat model.
*
* @param chatRequest a {@link ChatRequest}, containing all the inputs to the LLM
* @param handler a {@link StreamingChatResponseHandler} that will handle streaming response from the LLM
*/
default void chat(ChatRequest chatRequest, StreamingChatResponseHandler handler) {
ChatRequest finalChatRequest = ChatRequest.builder()
.messages(chatRequest.messages())
.parameters(defaultRequestParameters().overrideWith(chatRequest.parameters()))
.build();
List<ChatModelListener> listeners = listeners();
Map<Object, Object> attributes = new ConcurrentHashMap<>();
StreamingChatResponseHandler observingHandler = new StreamingChatResponseHandler() {
@Override
public void onPartialResponse(String partialResponse) {
handler.onPartialResponse(partialResponse);
}
@Override
public void onPartialToolExecutionRequest(int index, ToolExecutionRequest partialToolExecutionRequest) {
handler.onPartialToolExecutionRequest(index, partialToolExecutionRequest);
}
@Override
public void onCompleteToolExecutionRequest(int index, ToolExecutionRequest completeToolExecutionRequest) {
handler.onCompleteToolExecutionRequest(index, completeToolExecutionRequest);
}
@Override
public void onCompleteResponse(ChatResponse completeResponse) {
onResponse(completeResponse, finalChatRequest, provider(), attributes, listeners);
handler.onCompleteResponse(completeResponse);
}
@Override
public void onError(Throwable error) {
ChatModelListenerUtils.onError(error, finalChatRequest, provider(), attributes, listeners);
handler.onError(error);
}
};
onRequest(finalChatRequest, provider(), attributes, listeners);
doChat(finalChatRequest, observingHandler);
}
default void doChat(ChatRequest chatRequest, StreamingChatResponseHandler handler) {
throw new RuntimeException("Not implemented");
}
default ChatRequestParameters defaultRequestParameters() {
return DefaultChatRequestParameters.EMPTY;
}
default List<ChatModelListener> listeners() {
return List.of();
}
default ModelProvider provider() {
return OTHER;
}
default void chat(String userMessage, StreamingChatResponseHandler handler) {
ChatRequest chatRequest = ChatRequest.builder()
.messages(UserMessage.from(userMessage))
.build();
chat(chatRequest, handler);
}
default void chat(List<ChatMessage> messages, StreamingChatResponseHandler handler) {
ChatRequest chatRequest = ChatRequest.builder()
.messages(messages)
.build();
chat(chatRequest, handler);
}
default Set<Capability> supportedCapabilities() {
return Set.of();
}
}
再看cofing:
// 你提供的代码里没贴原型Bean的配置类,但标准写法如下(对应工厂里的getBean名字):
// 示例:配置类中的原型Bean定义(补充代码,你项目中一定有这个配置)
@Configuration
public class AiModelConfig {
@Value("${openai.api-key}")
private String apiKey;
// 1. 定义名字为"streamingChatModelPrototype"的原型Bean
@Bean(name = "streamingChatModelPrototype")
@Scope("prototype") // 多例:每次getBean都new
public StreamingChatModel streamingChatModelPrototype() {
// Spring帮你执行new(OpenAiStreamingChatModel.builder()本质是创建新实例)
return OpenAiStreamingChatModel.builder()
.apiKey(apiKey)
.modelName("gpt-3.5-turbo")
.build();
}
// 2. 定义名字为"reasoningStreamingChatModelPrototype"的原型Bean
@Bean(name = "reasoningStreamingChatModelPrototype")
@Scope("prototype")
public StreamingChatModel reasoningStreamingChatModelPrototype() {
return OpenAiStreamingChatModel.builder()
.apiKey(apiKey)
.modelName("gpt-4") // 推理模型用GPT4
.build();
}
// 3. 定义名字为"routingChatModelPrototype"的原型Bean
@Bean(name = "routingChatModelPrototype")
@Scope("prototype")
public ChatModel routingChatModelPrototype() {
return OpenAiChatModel.builder()
.apiKey(apiKey)
.modelName("gpt-3.5-turbo")
.build();
}
}
@Bean注解告诉 Spring:“这个方法返回的实例要交给容器管理”;@Scope("prototype")告诉 Spring:“每次调用 getBean 获取这个 Bean 时,都执行一次方法,创建新实例”;- 你在工厂里调用
SpringContextUtil.getBean("streamingChatModelPrototype"),本质是 Spring 执行了streamingChatModelPrototype()方法,帮你 new 了一个StreamingChatModel实例 —— 所以你没看到 new,但 Spring 帮你做了。
现在再回到generateAndSaveCodeStream
代码
AiCodeGeneratorService aiCodeGeneratorService = aiCodeGeneratorServiceFactory.getAiCodeGeneratorService(appId, codeGenTypeEnum);
return switch (codeGenTypeEnum) {
case HTML -> {
Flux<String> codeStream = aiCodeGeneratorService.generateHtmlCodeStream(userMessage);
yield processCodeStream(codeStream, CodeGenTypeEnum.HTML, appId);
}
case MULTI_FILE -> {
Flux<String> codeStream = aiCodeGeneratorService.generateMultiFileCodeStream(userMessage);
yield processCodeStream(codeStream, CodeGenTypeEnum.MULTI_FILE, appId);
}
case VUE_PROJECT -> {
TokenStream tokenStream = aiCodeGeneratorService.generateVueProjectCodeStream(appId, userMessage);
yield processTokenStream(tokenStream, appId);
}
default -> {
String errorMessage = "不支持的生成类型:" + codeGenTypeEnum.getValue();
throw new BusinessException(ErrorCode.SYSTEM_ERROR, errorMessage);
}
aiCodeGeneratorService里面有所有的方法根据这个枚举类型选择 这些方法如下:
package com.yupi.yuaicodemother.ai;
import com.yupi.yuaicodemother.ai.model.HtmlCodeResult;
import com.yupi.yuaicodemother.ai.model.MultiFileCodeResult;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.UserMessage;
import reactor.core.publisher.Flux;
public interface AiCodeGeneratorService {
/**
* 生成 HTML 代码
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
HtmlCodeResult generateHtmlCode(String userMessage);
/**
* 生成多文件代码
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-multi-file-system-prompt.txt")
MultiFileCodeResult generateMultiFileCode(String userMessage);
/**
* 生成 HTML 代码
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-html-system-prompt.txt")
Flux<String> generateHtmlCodeStream(String userMessage);
/**
* 生成多文件代码
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-multi-file-system-prompt.txt")
Flux<String> generateMultiFileCodeStream(String userMessage);
/**
* 生成 Vue 项目代码(流式)
*
* @param userMessage 用户提示词
* @return AI 的输出结果
*/
@SystemMessage(fromResource = "prompt/codegen-vue-project-system-prompt.txt")
TokenStream generateVueProjectCodeStream(@MemoryId long appId, @UserMessage String userMessage);
}
然后:
Flux<String> codeStream = aiCodeGeneratorService.generateMultiFileCodeStream(userMessage);
yield processCodeStream(codeStream, CodeGenTypeEnum.MULTI_FILE, appId);
以这一种为例直接返回流式输出的内容。
然后回到:
ServiceImpl
看Flux<String> codeStream = aiCodeGeneratorFacade.generateAndSaveCodeStream(message, codeGenTypeEnum, appId);
// 8. 收集 AI 响应的内容,并且在完成后保存记录到对话历史
return streamHandlerExecutor.doExecute(codeStream, chatHistoryService, appId, loginUser, codeGenTypeEnum)
.doFinally(signalType -> {
// 流结束时清理(无论成功/失败/取消)
MonitorContextHolder.clearContext();
});
至此完成闭环。
return streamHandlerExecutor.doExecute(codeStream, chatHistoryService, appId, loginUser, codeGenTypeEnum) .doFinally(signalType -> { // 流结束时清理(无论成功/失败/取消) MonitorContextHolder.clearContext(); });这个方法还有如下
package com.yupi.yuaicodemother.core.handler;
import com.yupi.yuaicodemother.model.entity.User;
import com.yupi.yuaicodemother.model.enums.CodeGenTypeEnum;
import com.yupi.yuaicodemother.service.ChatHistoryService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
/**
* 流处理器执行器
* 根据代码生成类型创建合适的流处理器:
* 1. 传统的 Flux<String> 流(HTML、MULTI_FILE) -> SimpleTextStreamHandler
* 2. TokenStream 格式的复杂流(VUE_PROJECT) -> JsonMessageStreamHandler
*/
@Slf4j
@Component
public class StreamHandlerExecutor {
@Resource
private JsonMessageStreamHandler jsonMessageStreamHandler;
/**
* 创建流处理器并处理聊天历史记录
*
* @param originFlux 原始流
* @param chatHistoryService 聊天历史服务
* @param appId 应用ID
* @param loginUser 登录用户
* @param codeGenType 代码生成类型
* @return 处理后的流
*/
public Flux<String> doExecute(Flux<String> originFlux,
ChatHistoryService chatHistoryService,
long appId, User loginUser, CodeGenTypeEnum codeGenType) {
return switch (codeGenType) {
case VUE_PROJECT -> // 使用注入的组件实例
jsonMessageStreamHandler.handle(originFlux, chatHistoryService, appId, loginUser);
case HTML, MULTI_FILE -> // 简单文本处理器不需要依赖注入
new SimpleTextStreamHandler().handle(originFlux, chatHistoryService, appId, loginUser);
};
}
}
至此完整流程,这就是给ai对话生成代码的步骤流程是完整的。
这只能支持生成单个种类代码的用户语句。
更多推荐



所有评论(0)