Ai代码小助手,使用Spring Boot3 + LangChain + Vue实现调用本地Ollama大模型的编程小助手
背景:
mac os 15.7
IDEA最新版
其他组件均为最新稳定版本
嗨嗨嗨!!最近ai很火,ai流行的框架也越来越多了。目前流行的有Spring团队原生支持的SpringAI和阿里旗下的Langchain4j框架
SpringAI适用于SpringBoot项目,而LangChain4j则是一种兼容的框架。它们的核心组件都是差不多的,可以根据个人的需要自由选择
前置条件需要本地部署大模型,如果还不是很了解的可以看看我的这篇文章:
https://bytedance.larkoffice.com/share/base/form/shrcn9BK1yGzHT4OIEvIK6R7ONb
补充:本次项目使用的deepseek-r1:14b是不支持tools工具调用的!!!这也可能是我配置失败的原因之一,如果大家对mcp开发有兴趣,一定要先确认自己选择的大模型是否支持工具调用!
——————————————————————项目搭建——————————————————————

最新版的IDEA是自带了SpringBoot的初始创建的,如果是老版本的IDEA社区版则需要手动创建项目结构引入POM文件,或者使用Spring插件也可以完成。如果是商业版则完全没有问题是一样的操作的
至于配置文件格式则是根据个人的习惯来就行,我个人比较习惯yaml的配置文件格式
这里的javaSDK版本选择21,因为LangChain4j框架的最低要求是java17,在这个基础上选一个高一点的稳定版本21就够了
完成以后选择下一步,开始选择依赖:

这里先只用选择基础的web框架和Flux流式响应的web框架
进阶一点的可以选上lombok
这个可以简化代码的书写,有的时候还是比较方便的,推荐选上
最重要的是Spring版本!!!建议选择Spring3+的稳定版本!!!!选择4版本不稳定,容易有各种bug,反正个人目前来说用过最好用的是2加版本哈哈哈啊哈💦
然后创建项目即可
创建完项目首先来到pom文件,将langchain4j的一些依赖补上去即可:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.11</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.wc</groupId>
<artifactId>LangChain4j</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>LangChain4j</name>
<description>LangChain4j</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
<spring-ai.version>1.1.2</spring-ai.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.11.0-beta19</version>
</dependency>
<!--这是自动配置yaml依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
<version>1.11.0-beta19</version>
</dependency>
<!-- LangChain本地集成ollama-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.11.0</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>1.1.0-beta7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.1.0-beta7</version>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.20.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
一次性将本项目所有需要用的依赖都放上来了 ,可以直接复制。也可以根据个人项目进度选择依赖放入:
这是langchain4j适配ollama接口的依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>1.11.0</version>
</dependency>
langchain4j基础依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.11.0-beta19</version>
</dependency>
langchain4j和Ollama集成Springboot:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama-spring-boot-starter</artifactId>
<version>1.11.0-beta19</version>
</dependency>
langchain4j基础依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.11.0</version>
</dependency>
langchain4j的实现mcp服务接口的依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-mcp</artifactId>
<version>1.1.0-beta7</version>
</dependency>
测试单元:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
langchain4j流式响应:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.1.0-beta7</version>
</dependency>
langchian with tools工具依赖:
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.20.1</version>
</dependency>
之后就差不多了
然后是yaml文件配置:
spring:
application:
name: LangChain4j
langchain4j:
ollama:
chat-model:
base-url: http://localhost:11434
model-name: deepseek-r1:14b
log-requests: true # 注意这里是 log-requests 不是 log-request
log-responses: true # 注意这里是 log-responses 不是 log-response
temperature: 0.7
timeout: 60s
max-retries: 3
embedding-model:
base-url: http://localhost:11434
model-name: Qwen3-Embedding:4b
timeout: 60s
streaming-chat-model:
base-url: http://localhost:11434
model-name: deepseek-r1:14b
temperature: 0.7
log-requests: true
log-responses: true
server:
port: 8081
servlet:
context-path: /api
# MCP 配置
mcp:
enabled: true
log-events: true
search:
engine: baidu
allowed-engines: baidu,bing,csdn,juejin
proxy:
enabled: false
这里主要是来配置各种大模型的,如果没有本地配置也可以使用ai厂商提供的api服务进行调用,只是需要购买token再进行配置
langchain4j:
community:
dashscope:
chat-model:
model-name: qwen-max
api-key: <You API Key here>
至于模型的名称是你使用的模型的名字
当然更加灵活一点还可以使用手动@Configuration + @Bean手动配置chatModel
ChatModel qwenModel = QwenChatModel.builder()
.apiKey("You API key here")
.modelName("qwen-max")
.enableSearch(true)
.temperature(0.7)
.maxTokens(4096)
.stops(List.of("Hello"))
.build();
————————————————前置工作完成,小试牛刀!——————————————————
可以先不管项目的层级结构
先创建一个AiCodeHelper:
package com.wc.langchain4j.service;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class AiCodeHelper {
@Resource
private ChatModel deepseekChatModel;
public String chat(String message){
UserMessage userMessage = UserMessage.from(message);
ChatResponse chatResponse = deepseekChatModel.chat(userMessage);
AiMessage aiMessage = chatResponse.aiMessage();
log.info("AI输出" + aiMessage.toString());
return aiMessage.toString();
}
}
在这个服务中注入我们的ChatModel,然后调用这个Model的各种方法
然后再写一个测试类:来看看我们的模型基本的回答能力


这里选择Debug运行

像这样测试通过就算是项目已经成功了一半了!!!哈哈哈哈哈
java也可以集成AI了,难道不是非常兴奋的一件事吗!!!
只是跑本地ai非常吃性能,比如我这里使用了本地配置的Deepseek-r1:14b,当时mac直接功耗直接拉满了 💦
直接是70多w的功耗💦,而且等出结果还需要等一会
不过当时我跑出来结果还是感觉很高兴的,嗨嗨嗨!!
好了闲话少叙,接下来可以开始Ai接口编写了
————————————————————AI Service-——————————————————————
像刚刚的AiCodeHelper的简单的小调用,可以看到内部的实现还是比较麻烦的
需要手动自定义UserMessage然后调用方法
langchain4j给出了一个更加方便的使用形式——AI Service,高度抽象的接口实现,使得业务代码的实现更加简单了
和传统后端业务实现差不多,也是通过Service接口 + 一个实现类完成的,只是这里使用一个工厂代替了实现类的完成
Service定义业务能力,工厂负责内部的细节实现:
从这里就可以开始规范项目的结构了,比如经典的Service层,controller层之类:

这里就是项目的整个结构
清晰的项目结构可以让编程的思路更加清晰,通常service接口类都是在service目录下,然后之类的实现类则是在工厂目录下保存的
Service接口:
package com.wc.langchain4j.service;
import com.wc.langchain4j.model.Report;
import com.wc.langchain4j.model.SafeInputGuardrail;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.guardrail.InputGuardrails;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
//这是AI Service的业务开发模式,可以使用工厂类来实现这个业务接口
//或者直接使用@AiService注解来自动创建实例,注意这种方式就需要注释掉工厂类的@Configuration的注解了,否则会有依赖冲突的风险
//@AiService
@InputGuardrails({SafeInputGuardrail.class})
public interface AiCodeHelperService {
@SystemMessage("你是一位编程小助手")
String chat(@UserMessage String userMessage);
@SystemMessage(fromResource = "system-prompt.txt")
Report chatForReport(@UserMessage String userMessage);
@SystemMessage("你是一位编程小助手")
String chatWithMcp(@UserMessage String userMessage);
@SystemMessage("你是一位编程小助手")
dev.langchain4j.service.Result<String> chatWithRag(@UserMessage String userMessage);
Flux<String> chatStream(@UserMessage String userMessage);
}
这里的@System注解是给ai调用加入系统提示词,可以专业化ai的人格,让回答更加专业
短的提示词可以直接写在注解里,如果提示词长了写在注解里非常不优雅,所以可以在资源文件目录中专门保存一个系统提示词,然后注解再调用它们

system-prompt.txt
只返回纯粹的 JSON 格式,不要添加任何解释,不要包含任何Markdown代码块标记比如(```json或者```)
你是一个专业的信息提取助手。请从给定文本中提取人员信息,
并严格按照以下 JSON 格式返回结果:
{
"name": "人员姓名",
"age": 年龄数字,
"height": 身高(米),
"married": true/false,
"occupation": "职业"
}
重要规则:
1. 只返回纯粹的 JSON 格式,不要添加任何解释,不要包含任何Markdown代码块标记比如(```json或者```)
2. 如果信息不明确,使用 null
3. age 必须是数字,不是字符串
4. married 必须是布尔值
我们先来实现接口中的chat方法,其他方法稍后会介绍的,至于@InputGuardrails则是后面的拦截器的注解,这里可以先忽略
@SystemMessage("你是一位编程小助手")
String chat(@UserMessage String userMessage);
然后创建一个工厂类来实现这个接口:也就是填充一个bean
package com.wc.langchain4j.factory;
import com.wc.langchain4j.service.AiCodeHelperService;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.chat.StreamingChatModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.service.AiServices;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AiCodeHelperServiceFactory {
@Resource
private ChatModel deepseekChatModel;
@Resource
private ContentRetriever contentRetriever;
@Autowired(required = false)
private McpToolProvider mcpToolProvider;
@Resource
private StreamingChatModel deepseekStreamingChatModel;
@Bean
public AiCodeHelperService aiCodeHelperService() {
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
// return AiServices.create(AiCodeHelperService.class, deepseekChatModel);
//这个工厂类用于实现service接口,原理是反射出一个代理对象进行结果转换,这里使用create或者builder方法都可以
return AiServices.builder(AiCodeHelperService.class)
.chatModel(deepseekChatModel)
.streamingChatModel(deepseekStreamingChatModel)
.chatMemory(chatMemory)
.contentRetriever(contentRetriever) //在构建AI Service时绑定内容检索器,RAG检索增强生成
.toolProvider(mcpToolProvider)//调用mcp工具
.build();
}
}
目前来说@Resource的bean引入只需要一个ChatModel即可,除此之外的都是额外的配置:
我们只需要关心这些就行了:
return AiServices.builder(AiCodeHelperService.class)
.chatModel(deepseekChatModel)
.build();
这里也可以使用crate方法进行装配,但是build会更加灵活,方便以后将其他额外配置进行构建
create:
@Configuration
public class AiCodeHelperServiceFactory {
@Resource
private ChatModel qwenChatModel;
@Bean
public AiCodeHelperService aiCodeHelperService() {
return AiServices.create(AiCodeHelperService.class, qwenChatModel);
}
}
比如这样引入千问的一个大模型
注意这里的@Configuration,这是手动装配bean 的核心注解,没有的话会出现经典的invoke null错误哈哈哈哈哈啊哈💦,当年学Spring 的时候没少吃过这个亏
这里create的底层原理是java的代理对象机制,我们开发的时候不用关系类型转换的种种细节,只需要安心编写接口和工厂类就行了,嗨嗨嗨!!
这是手动装配,比较麻烦一点,不过也比较灵活。当然还是可以使用自动装配的注解@AiService
注意这个时候需要用到之前的基础langchain4j集成springboot的依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.1.0-beta7</version>
</dependency>
然后直接给接口类加上注解即可自动装配好接口service的实现类:
@AiService
public interface AiCodeHelperService {
@SystemMessage(fromResource = "system-prompt.txt")
String chat(String userMessage);
}
注意两种方式只能选取一种方式进行,也就是要么使用@Configuration注解,要么使用@AiService注解。如果都使用的话会出现bean注入冲突的问题
然后进行单元测试:
@SpringBootTest
public class AiCodeHelperServiceTest {
@Autowired
private AiCodeHelperService aiCodeHelperService;
@Test
void chat(){
if (aiCodeHelperService == null) {
System.err.println("AiCodeHelperService is null!");
return;
}
String result = aiCodeHelperService.chat("你好我是小明");
System.out.println(result);
}
看到这个提前bean的空值判断了吗,这个就是之前真的在这了卡了很久,从接口定义到工厂类全部检查了几遍的,项目重新来了两次的那种,都没有检查出问题,然后又怀疑是不是测试类的问题,结果全都没有问题哈哈哈💦
于是本来我说要不先做后面的功能算了,但是最后又试了一下,换了一个依赖版本,居然可以了!!!!!!!!!!!!!!!!!牛逼11!!!!
总之还真是依赖的问题!!!当时反复怀疑create和builder方法到底有没有把接口实现出来哈哈哈,又换了@AiService注解还是有问题。总之还是长经验了
还是一样的debug运行

这样就是测试通过了,(我当时测试通过的时候真的非常惊喜!!!nb!!!!!)
—————————————————————Ai对话记忆————————————————————
维持ai的对话记忆,最传统的方式是使用一个自己维护的消息队列,但是非常麻烦
获取用户历史记录 -》添加用户新消息 -〉 构建完整历史上下文 -》 调用AI API -〉 保存AI回复到历史
现在langchain4j框架直接提供了现成的API,直接调用即可。
回到工厂类中将这个额外配置添加到build的构建过程即可:
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
非常简单,定义记住的最大消息数量为10。
添加到build构建:
return AiServices.builder(AiCodeHelperService.class)
.chatModel(deepseekChatModel)
.streamingChatModel(deepseekStreamingChatModel)
.chatMemory(chatMemory)
.build();
像这里就算是配置好了
这里我们也可以看出手动写一个工厂类进行配置是比@AiService注解来得更加灵活的
@Configuration
public class AiCodeHelperServiceFactory {
@Resource
private ChatModel qwenChatModel;
@Bean
public AiCodeHelperService aiCodeHelperService() {
// 会话记忆
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class)
.chatModel(qwenChatModel)
.chatMemory(chatMemory)
.build();
return aiCodeHelperService;
}
}
然后写一个测试类进行测试即可:
@Test
void chatMemory() {
String result = aiCodeHelperService.chat("你好我是小明");
System.out.println(result);
result = aiCodeHelperService.chat("你好我是谁来着");
System.out.println(result);
}
debug运行:

像这样就是测试成功了
不过默认这种方式是将会话内容存储在内存中的,进阶用法是定义ChatMemoryStore接口来将数据存储在数据库中
不过本次项目是一个本地部署的项目,因此没有使用这种进阶的用法了,感兴趣的小伙伴可以看看鱼皮大佬的ai项目,里面详细讲述了这种进阶用法(不像我这种菜鸡哈哈哈哈💦)
————————————————————结构化输出———————————————————————
结构化输出,意思就是ai可以按照用户自定义的内容结构输出结构化的内容
比如从一段文本中提取出信息,并按照json格式输出
{
"name": "人员姓名",
"age": 年龄数字,
"height": 身高(米),
"married": true/false,
"occupation": "职业"
}
因此还需要实现定义专门的系统提示词:
你是一个专业的信息提取助手。请从给定文本中提取人员信息,
并严格按照以下 JSON 格式返回结果:
{
"name": "人员姓名",
"age": 年龄数字,
"height": 身高(米),
"married": true/false,
"occupation": "职业"
}
重要规则:
1. 只返回 JSON 格式,不要添加任何解释
2. 如果信息不明确,使用 null
3. age 必须是数字,不是字符串
4. married 必须是布尔值
我们再写一个结构化输出的单元测试:
@Test
void chatForReport() {
String userMessage = "你好我是小坤,我是练习时长两年半的个人练习生,请帮我定制学习报告";
Report report = aiCodeHelperService.chatForReport(userMessage);
System.out.println(report);
}
当然我们还要修改对话的返回值,让框架帮我们自实现结构化输出
添加一个model,用于定义一个输出结构:

package com.wc.langchain4j.model;
import java.util.List;
public record Report(String name, List<String> suggestions) {
}
编写测试单元:
@Test
void chatForReport() {
String userMessage = "你好我是小坤,我是练习时长两年半的个人练习生,请帮我定制学习报告";
Report report = aiCodeHelperService.chatForReport(userMessage);
System.out.println(report);
}
debug:

哈哈哈,信息的准确提取还是得看模型本身的实力才行,我这里部署的是deepseek-r1的孙子版本哈哈哈哈哈哈💦,因此这个功能不是很完美实现得。这里使用通用模型比专门的推理模型要更好
——————————————————RAG检索增强————————————————————————
RAG!!!这是最近很流行的一种说法
人工投喂信息给embedding模型,embdding模型会将信息转换为llm模型的知识再给ai,也就是增加了ai的信息来源,相当于ai通过RAG可以学习更多知识了,增加了知识储备
RAG使得ai回答信息的时候会查一查我们投喂的特定知识库,减少出现幻觉的情况
这里我们使用标准版的LangChain4j的RAG配置,虽然也有开箱即用的快速配置,但是实际开发还是使用标准版更加灵活:
-
加载 Markdown 文档并按需切割
-
Markdown 文档补充文件名信息
-
自定义 Embedding 模型
-
自定义内容检索器
RAG需要两个模型,一个是embedding模型,将投喂的文章拆解,然后建立检索,转换为ai可以识别的向量模型
然后再是我们熟悉的llm大语言模型直接调用这些向量模型,从而达到扩充知识的目的
在yaml中新增我们的embedding模型:
langchain4j:
ollama:
chat-model:
base-url: http://localhost:11434
model-name: deepseek-r1:14b
log-requests: true # 注意这里是 log-requests 不是 log-request
log-responses: true # 注意这里是 log-responses 不是 log-response
temperature: 0.7
timeout: 60s
max-retries: 3
embedding-model:
base-url: http://localhost:11434
model-name: Qwen3-Embedding:4b
timeout: 60s
这里需要重新在ollama中部署:我一般喜欢从终端中输入命令:
ollama pull Qwen3-Embedding:4b
embedding模型就没有llm模型那么吃性能,这里可以选择较好的模型进行部署
使用标准模式的RAG,需要我们手动书写一个config来执行RAG流程和自定义内容检索器
config.RagConfig.class:
package com.wc.langchain4j.config;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;
import java.util.List;
@Configuration
/*
检索增强,信息检索和和AI内容生成的混合架构,也就是给AI额外的信息来源,防止AI出现幻觉和知识空缺,确保回答基于真实的资料而不是想象
*/
public class RagConfig {
@Resource
private EmbeddingModel qwenEmbeddingModel;
@Resource
private EmbeddingStore<TextSegment> embeddingStore;
@Bean
public ContentRetriever contentRetriever() {
//1.从资源文件目录中加载文档
List<Document> documents = FileSystemDocumentLoader.loadDocuments("src/main/resources/docs");
//2.定义一个文档切割:将文档按照段落进行切割,最大1000字符,每次最多重叠200个字符
DocumentByParagraphSplitter paragraphSplitter = new DocumentByParagraphSplitter(1000,200);
//3.自定义文档加载器
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder().documentSplitter(paragraphSplitter)
//给每个textsegment添加文档名称
.textSegmentTransformer(textsegment -> TextSegment.from(
textsegment.metadata().getString("file_name") + "\n" + textsegment.text(),
textsegment.metadata()
//使用指定的向量模型
)).embeddingModel(qwenEmbeddingModel).embeddingStore(embeddingStore).build();
ingestor.ingest(documents);
//自定义内容查询器
return EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.embeddingModel(qwenEmbeddingModel)
.maxResults(5)
.minScore(0.75)
.build();
}
}
注意这里的文档加载路径:

在resources中创建docs,当然也可以自定义文档的加载路径,只要确保文档确实在这个路径即可。
然后在里面放一些markdown的文档,实际上直接从word文档中复制到markdown也没有什么问题
有了这个配置,再到工厂类中添加到build的构建过程就可以正常使用了:
@Bean
public AiCodeHelperService aiCodeHelperService() {
ChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(10);
// return AiServices.create(AiCodeHelperService.class, deepseekChatModel);
//这个工厂类用于实现service接口,原理是反射出一个代理对象进行结果转换,这里使用create或者builder方法都可以
return AiServices.builder(AiCodeHelperService.class)
.chatModel(deepseekChatModel)
.chatMemory(chatMemory)
.contentRetriever(contentRetriever) //在构建AI Service时绑定内容检索器,RAG检索增强生成
.build();
非常灵活,不是吗哈哈哈
一般这样就可以了,如果想增加可信度,比如展示回答的时候点名知识来源,可以在Ai Service接口的返回类型String上封装一个Result,本次项目采取这样的方式:
@SystemMessage("你是一位编程小助手")
dev.langchain4j.service.Result<String> chatWithRag(@UserMessage String userMessage);
然后再来一个测试类,测试ai是否消化了刚刚投喂的文档:
@Test
void chatWithRag(){
//我靠,真的牛逼,可以根据你给出的markdown文档进行知识库的积累!!!相当于AI也会学习总结东西嘛!RAG!!!
//因此不管什么内容多么冗杂,都可以丢给AI让它帮你总结!!!nb!!!!!!!!
Result<String> result = aiCodeHelperService.chatWithRag(" 线程崩溃了,进程也会崩溃吗?");
System.out.println(result.content());
System.out.println(result.sources());
}
看看我们的测试结果:

可以看到,ai输出了回答和引用来源(
System.out.println(result.sources());
)
还是非常有意思的,像是ai也会学习了哈哈哈,可以根据你投喂的文档总结内容
————————————————MCP模型上下文——————————————————————
MCP就是扩展Ai的接口,Ai可以通过mcp完成联网搜索等各种功能
但是目前Langchain4j框架对mcp的开发适配程度还不是很好,而且使用java开发也有点麻烦,💦
我自己尝试了过mcp的开发的联网搜索功能,想用本地化不需要额外购买WebSearch服务的方式实现
我是用的npx本地运行open-websearch服务,但是一直连接超时
更换搜索引擎,各种排查错误也不知怎么回事
感兴趣的小伙伴可以自己尝试一下:
首先是yaml配置mcp服务:
server:
port: 8081
servlet:
context-path: /api
# MCP 配置
mcp:
enabled: true
log-events: true
search:
engine: baidu
allowed-engines: baidu,bing,csdn,juejin
proxy:
enabled: false
用不用代理我都试了一下,但是都连接超时💦
然后新建一个配置类:
package com.wc.langchain4j.config;
import dev.langchain4j.mcp.McpToolProvider;
import dev.langchain4j.mcp.client.DefaultMcpClient;
import dev.langchain4j.mcp.client.McpClient;
import dev.langchain4j.mcp.client.transport.stdio.StdioMcpTransport;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.List;
import java.util.Map;
@Configuration
public class McpConfig {
@Value("${open-websearch.search-engine:baidu}")
private String searchEngine;
@Value("${open-websearch.use-proxy:false}")
private boolean useProxy;
@Value("${open-websearch.proxy-url:}")
private String proxyUrl;
@Value("${mcp.log-events:true}")
private boolean logEvents;
@Bean
public McpToolProvider mcpToolProvider() {
// 构建环境变量
Map<String, String> env = new java.util.HashMap<>();
env.put("DEFAULT_SEARCH_ENGINE", searchEngine);
env.put("MODE", "stdio"); // 强制使用stdio模式
if (useProxy && proxyUrl != null && !proxyUrl.isEmpty()) {
env.put("USE_PROXY", "true");
env.put("PROXY_URL", proxyUrl);
}
// 配置传输层
StdioMcpTransport transport = new StdioMcpTransport.Builder()
.command(List.of("bing-cn-mcp"))
.environment(env)
.logEvents(logEvents)
.build();
McpClient mcpClient = new DefaultMcpClient.Builder()
.transport(transport)
.pingTimeout(Duration.ofSeconds(60))
.build();
return McpToolProvider.builder()
.mcpClients(List.of(mcpClient))
.build();
}
}
注意如果后面测试失败的话还是最好注释掉这个config,不然项目已开始就会因为nullBean报错,从而根本没办法跑通业务
然后是修改工厂类,新添加一个配置对象:
@Autowired(required = false)
private McpToolProvider mcpToolProvider;//mcp服务适配器
但是由于是配置失败的,因此这里使用自动装配的时候加上一个false注解值,允许注入一个空的null,不然测试直接不同过的
然后将mcp添加到AIservice的构建过程:
return AiServices.builder(AiCodeHelperService.class)
.chatModel(deepseekChatModel)
.streamingChatModel(deepseekStreamingChatModel)
.chatMemory(chatMemory)
.contentRetriever(contentRetriever) //在构建AI Service时绑定内容检索器,RAG检索增强生成
.toolProvider(mcpToolProvider)//调用mcp工具
.build();
但是实际上如果是null的话没有什么作用的
然后是新增接口方法:
@SystemMessage("你是一位编程小助手")
String chatWithMcp(@UserMessage String userMessage);
再进行测试:
/*
目前mcp开发在java上的生态还不成熟,因此这里为了先把业务跑通,只能先允许注入一个空的null的mcp服务提供
*/
@Test
void chatWithMcp(){
String result = aiCodeHelperService.chatWithMcp("什么是十万个为什么?");
System.out.println(result);
}
测试是可以正常通过,但是不会联网进行搜索了,只会利用已知的知识进行搜索
————————————————————拦截器————————————————————————
使用拦截器可以在用户的请求到达ai之前就检查一遍信息,防止有敏感内容
首先实现一个拦截器的接口:

拦截器:
package com.wc.langchain4j.model;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.guardrail.InputGuardrail;
import dev.langchain4j.guardrail.InputGuardrailResult;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
public class SafeInputGuardrail implements InputGuardrail {
/*
自定义的一个拦截器:在用户的请求到达ai服务之前先筛选掉含有敏感词的请求
*/
private static final Set<String> sensitiveWords = Set.of("kill","evil");//敏感词
@Override
public InputGuardrailResult validate(UserMessage userMessage) {
String inputText = userMessage.singleText().toLowerCase();//将用户的输入全部转换为小写
String[] words = inputText.split("\\W+");//将输入的文本切割为单词
for(String word : words){//遍历所有的单词,检查是否存在敏感词
if(sensitiveWords.contains(word)){
return fatal("Sensitive word detected :" + word);
}
}
return success();
}
}
然后再回到我们的Aiservice接口了,这个时候就可以接入我们的拦截器了:
@InputGuardrails({SafeInputGuardrail.class})
AiService:
package com.wc.langchain4j.service;
import com.wc.langchain4j.model.Report;
import com.wc.langchain4j.model.SafeInputGuardrail;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.guardrail.InputGuardrails;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
//这是AI Service的业务开发模式,可以使用工厂类来实现这个业务接口
//或者直接使用@AiService注解来自动创建实例,注意这种方式就需要注释掉工厂类的@Configuration的注解了,否则会有依赖冲突的风险
//@AiService
@InputGuardrails({SafeInputGuardrail.class})
public interface AiCodeHelperService {
@SystemMessage("你是一位编程小助手")
String chat(@UserMessage String userMessage);
@SystemMessage(fromResource = "system-prompt.txt")
Report chatForReport(@UserMessage String userMessage);
@SystemMessage("你是一位编程小助手")
String chatWithMcp(@UserMessage String userMessage);
@SystemMessage("你是一位编程小助手")
dev.langchain4j.service.Result<String> chatWithRag(@UserMessage String userMessage);
Flux<String> chatStream(@UserMessage String userMessage);
}
然后进行测试:
@Test
void chatWithGuardrail(){
String result = aiCodeHelperService.chat("kill the game");
System.out.println(result);
}
测试结果不通过才是对的,说明拦截器生效了:

————————————————————添加日志功能————————————————————
有些ChatModel是支持日志实现的,直接在yaml配置中配置一下即可:
log-requests: true # 注意这里是 log-requests 不是 log-request
log-responses: true # 注意这里是 log-responses 不是 log-response
如果不支持日志的话得手动进行配置listener了:具体参考鱼皮大佬的code:
新建 listener.ChatModelListenerConfig,输出请求、响应、错误信息:
@Configuration
@Slf4j
public class ChatModelListenerConfig {
@Bean
ChatModelListener chatModelListener() {
return new ChatModelListener() {
@Override
public void onRequest(ChatModelRequestContext requestContext) {
log.info("onRequest(): {}", requestContext.chatRequest());
}
@Override
public void onResponse(ChatModelResponseContext responseContext) {
log.info("onResponse(): {}", responseContext.chatResponse());
}
@Override
public void onError(ChatModelErrorContext errorContext) {
log.info("onError(): {}", errorContext.error().getMessage());
}
};
}
}
然后构造的时候添加给build:
新建 model.QwenChatModelConfig,构造 ChatModel 对象并绑定 Listener:
@Configuration
@ConfigurationProperties(prefix = "langchain4j.community.dashscope.chat-model")
@Data
public class QwenChatModelConfig {
private String modelName;
private String apiKey;
@Resource
private ChatModelListener chatModelListener;
@Bean
public ChatModel myQwenChatModel() {
return QwenChatModel.builder()
.apiKey(apiKey)
.modelName(modelName)
.listeners(List.of(chatModelListener))
.build();
}
}
————————————————SSE流式接口的开发————————————————————
到这里差不多AI的功能已经开发的差不多,完整的项目要加上前端,那么得写一个接口给前端使用:
对于AI应用来说使用流式响应比同步响应体验更好,这里使用flux进行接口的编写:
首先引入流式接口的依赖:
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.1.0-beta7</version>
</dependency>
然后给AI接口新增流式对话的方法:
Flux<String> chatStream(@UserMessage String userMessage);
再在yaml配置中新增流式模型:
streaming-chat-model:
base-url: http://localhost:11434
model-name: deepseek-r1:14b
temperature: 0.7
log-requests: true
log-responses: true
然后直接在AIService的工厂类中添加到build的过程:
@Resource
private StreamingChatModel deepseekStreamingChatModel;//流式对话模型,也是Chat模型
return AiServices.builder(AiCodeHelperService.class)
.chatModel(deepseekChatModel)
.streamingChatModel(deepseekStreamingChatModel)
.chatMemory(chatMemory)
.contentRetriever(contentRetriever) //在构建AI Service时绑定内容检索器,RAG检索增强生成
.toolProvider(mcpToolProvider)//调用mcp工具
.build();
然后新增一个服务器yaml配置,准备实地测试了:
server:
port: 8081
servlet:
context-path: /api
再启动项目进行测试:
curl -G 'http://localhost:8081/api/ai/chat' \
--data-urlencode 'message=你好'
测试结果:

这样看到类似于打字机一样实时输出结果,就说明流式接口是ok的
————————————————跨域支持——————————————————————————
最后让后端的代码可以成功和前端沟通上,需要自定义一个跨域支持的配置类:
package com.wc.langchain4j.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**")
.allowCredentials(true)
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("*");
}
}
至此后端部分就大功告成了!!!!!!!哈哈哈哈💦
(实际上我真是修了各种bug才走到这里的哈哈哈💦)
——————————————————AI前端————————————————————————
对于后端的同学,前端部分就使用AI来实现就好了:这里使用字节的Trae进行前端页面生成
对话的时候加入提示词,让AI运行更加专业:这里使用国内版的Trae
然后前端框架选的是vue

你是一位专业的前端开发,请帮我根据下列信息来生成对应的前端项目代码。
## 需求
应用为《AI 编程小助手》,帮助用户解答编程学习和求职面试相关的问题,并给出建议。
只有一个页面,就是主页:页面风格为聊天室,上方是聊天记录(用户信息在右边,AI 信息在左边),下方是输入框,进入页面后自动生成一个聊天室 id,用于区分不同的会话。通过 SSE 的方式调用 chat 接口,实时显示对话内容。
## 技术选型
1. Vue3 项目
2. Axios 请求库
## 后端接口信息
接口地址前缀:http://localhost:8081/api
## SpringBoot 后端接口代码
@RestController
@RequestMapping("/ai")
public class AiController {
@GetMapping("/chat")
public Flux<ServerSentEvent<String>> chat(int memoryId, String message) {
return aiCodeHelperService.chatStream(memoryId, message)
.map(chunk -> ServerSentEvent.<String>builder()
.data(chunk)
.build());
}
}
然后让AI生成就行了!!
如果中途有bug再去让AI自查一下就行(真是nb现在的AI)
生成完以后大概是这样

页面展示:

然后我们来试试对话怎么样:

哈哈哈哈哈,还是很好玩的
项目完全在本地运行!!!!做出来还是比较有成就感的!!!!嗨嗨嗨!!!
但是目前AI生成的前端只有最基础的记忆会话方法,其他的RAG以及流式响应页面还有待后续进一步开发~
那么本次项目到这里也差不多告一段落咯!!!
这次使用的是Langchain4j的框架,当然使用SPring AI也是可以的。
如果有任何的问题欢迎留言或私信,本人菜鸡,文章有错误还请轻喷💦哈哈哈
感谢看到这里的小伙伴,那么我们下次再见!!!
——————————————————2026年3月9日
更多推荐



所有评论(0)