项目参考自B站程序员鱼皮,基于LangChain4j和Spring Boot实现的一个简单的对话平台。
视频: https://www.bilibili.com/video/BV1X4GGziEyr/p=7&share_source=copy_web&vd_source=678f52c7135e8109c9d58a20f6e13fbc
GitHub:https://github.com/liyupi/ai-code-helper
我的实现:toyAIBot: 基于langchain4j和SpringBoot实现的微型AI对话平台
Demo展示:ToyAiHelper
多模态
多模态是指能够同时处理、理解和生成多种不同类型数据的能力,比如文本、图像、音频、视频、PDF 等等。
系统提示词
系统提示词是设置 AI 模型行为规则和角色定位的隐藏指令,用户通常不能直接看到。系统 Prompt 相当于给 AI 设定人格和能力边界。
AI Service
LangChain4j 最重要的开发模式 —— AI Service,提供了很多高层抽象的、用起来更方便的 API,把 AI 应用当做服务来开发。
调用 AiServices.create 方法就可以创建出 AI Service 的实现类了,背后的原理是利用 Java 反射机制创建了一个实现接口的代理对象,代理对象负责输入和输出的转换,比如把 String 类型的用户消息参数转为 UserMessage 类型并调用 ChatModel,再将 AI 返回的 AiMessage 类型转换为 String 类型作为返回值。
@Configuration
public class AiCodeHelperFactory {
@Resource
private ChatModel qwenChatModel;
@Bean
public AiCodeService aiCodeService(){
// 直接调用AiServices.create 就能够创建出AiCodeService这个Service接口的实现类 背后的原理基于反射
// 用反射机制创建了一个实现接口的代理对象
return AiServices.create(AiCodeService.class, qwenChatModel);
}
}
会话记忆
会话记忆是指让 AI 能够记住用户之前的对话内容,并保持上下文连贯性,这是实现 AI 应用的核心特性。
LangChain4j 为我们提供了开箱即用的 MessageWindowChatMemory 会话记忆,最多保存 N 条消息,多余的会自动淘汰。创建会话记忆后,在构造 AI Service 设置 chatMemory
结构化输出
结构化输出是指将大模型返回的文本输出转换为结构化的数据格式,比如一段 JSON、一个对象、或者是复杂的对象列表。
结构化输出有 3 种实现方式:
- 利用大模型的 JSON schema
- 利用 Prompt + JSON Mode
- 利用 Prompt(让大模型输出Json)
实际用LangChain4j开发时,不需要定义这些方式,只需要修改对话方法的返回值,框架就回自动实现结构化输出。
RAG:检索增强生成
RAG(Retrieval-Augmented Generation,检索增强生成)是一种结合信息检索技术和 AI 内容生成的混合架构,可以解决大模型的知识时效性限制和幻觉问题。
简单来说,RAG让 AI 回答问题前先查一查特定的知识库来获取知识,确保回答是基于真实资料而不是凭空想象。很多企业也基于 RAG 搭建了自己的智能客服,可以用自己积累的领域知识回复用户。
工具调用
工具调用(Tool Calling)可以理解为让 AI 大模型 借用外部工具 来完成它自己做不到的事情。
跟人类一样,如果只凭手脚完成不了工作,那么就可以利用工具箱来完成。
工具可以是任何东西,比如网页搜索、对外部 API 的调用、访问外部数据、或执行特定的代码等。
比如用户提问 “帮我查询上海最新的天气”,AI 本身并没有这些知识,它就可以调用 “查询天气工具”,来完成任务。
需要注意的是,工具调用的本质 并不是 AI 服务器自己调用这些工具、也不是把工具的代码发送给 AI 服务器让它执行,它只能提出要求,表示 “我需要执行 XX 工具完成任务”。而真正执行工具的是我们自己的应用程序,执行后再把结果告诉 AI,让它继续工作。
MCP
MCP(Model Context Protocol,模型上下文协议)是一种开放标准,目的是增强 AI 与外部系统的交互能力。MCP 为 AI 提供了与外部工具、资源和服务交互的标准化方式,让 AI 能够访问最新数据、执行复杂操作,并与现有系统集成。
调用MCP实现全网内容搜索:
@Configuration
public class MCPConfig {
@Value("${bigmodel.api-key}")
private String apiKey;
@Bean
public McpToolProvider mcpToolProvider() {
// 和 MCP 服务通讯 通过sse
McpTransport transport = new HttpMcpTransport.Builder()
.sseUrl("https://open.bigmodel.cn/api/mcp/web_search/sse?Authorization=" + apiKey)
.logRequests(true) // 开启日志,查看更多信息
.logResponses(true)
.build();
// 创建 MCP 客户端
McpClient mcpClient = new DefaultMcpClient.Builder()
.key("yupiMcpClient")
.transport(transport)
.build();
// 从 MCP 客户端获取工具
McpToolProvider toolProvider = McpToolProvider.builder()
.mcpClients(mcpClient)
.build();
return toolProvider;
}
}
然后在service中添加工具调用即可
AiCodeService aiCodeHelperService = AiServices.builder(AiCodeService.class)
.chatModel(qwenChatModel)
.chatMemory(chatMemory) // 提供会话记忆功能
.contentRetriever(contentRetriever) // 提供内容检索器 RAG检索增强生成
.tools(new InterviewQuestionTool() ) // 工具调用
.toolProvider(mcpToolProvider) // MCP 工具调用
.build();
护轨 Guardrail
把它理解为拦截器就好了。分为输入护轨(input guardrails)和输出护轨(output guardrails),可以在请求 AI 前和接收到 AI 的响应后执行一些额外操作,比如调用 AI 前鉴权、调用 AI 后记录日志。
SSE 流式接口开发
我们平时开发的大多数接口都是同步接口,也就是等后端处理完再返回。但是对于 AI 应用,特别是响应时间较长的对话类应用,可能会让用户失去耐心等待,因此推荐使用 SSE(Server-Sent Events)技术实现实时流式输出,类似打字机效果,大幅提升用户体验。
注意,流式响应不支持结构化输出。
使用 Flux 代替 TokenStream,让 AI 对话方法返回 Flux 响应式对象即可。
service接口中实现:
@SystemMessage(fromResource = "system-prompt.txt") Flux<String> chatStream(@MemoryId int memoryId, @UserMessage String userMessage);
serviceFactory中添加:
AiCodeHelperService aiCodeHelperService = AiServices.builder(AiCodeHelperService.class) .chatModel(myQwenChatModel) .streamingChatModel(qwenStreamingChatModel)
项目难点的点
一些零碎知识
- @Service注解:
@Service注解的作用是标记一个类是“业务逻辑层”(Service层)的组件,并告诉Spring:“请创建这个类的实例并放入你的容器中,以便在需要的地方自动注入”。本质是@Component注解的特化,用于自动扫描和装配,默认情况下@Service和@Component行为一样。 - .text和.toString方法区别:
在Jsoup中,.text()方法能够提取HTML中的纯文本内容(去除掉<h1>、<p>这类标签),.toString()方法会把这些HTML源码一起获取下来。
部署过程的问题
1.RAG资源路径问题
Caused by: java.lang.IllegalArgumentException: 'src/main/resources/docs' is not a directory
代码(在 RAGConfig 类中)试图从路径 src/main/resources/docs 加载文档,但在云服务器的运行环境中,这个路径不存在或者不是一个目录。
原因是,本地开发时:src/main/resources/docs 是项目源代码里的一个目录,Maven 会将其打包进 JAR 文件,云服务器运行时:JAR 包内的文件结构是只读的,并且路径不再是文件系统路径。
解决方法:
将文档目录外置,然后用@Value("${app.rag.docs-path}")直接配置项目的RAG路径。
2.跨域
public void addCorsMappings(CorsRegistry registry){
// 覆盖所有请求
registry.addMapping("/**")
// 允许发送 Cookie
.allowCredentials(true)
// 放行哪些域名
.allowedOriginPatterns("*")
.allowedMethods("GET","POST","PUT","DELETE","OPTIONS")
.allowedHeaders("*")
.exposedHeaders("*");
}
在后端部署到服务器上,前端仍在本机上调用调试的时候,使用.allowedOriginPatterns("*")时候,有跨域问题,改一下:
.allowedOriginPatterns( "http://localhost:3000", "http://127.0.0.1:3000", "http://117.72.66.48:3000", "https://117.72.66.48:3000", "https://yuyuyuyi1.xyz:300" )
项目部署
前端:进入ai-code-helper-frontend目录,命令行中:
npm install npm run dev
后端:默认启用8081接口。
项目环境
云服务器:
- 京东云 CPU 2核 内存 2GB
- 京东云服务器是直接使用WordPress-6.0.1应用镜像创建的,其Linux系统为CentOS7
该项目需要使用JDK21。安装方式:阿里云 CentOS 7.6 / 7.9 yum 安装 Oracle JDK 21 – 简书
前端部署
前端命令行:npm run build,上传dist文件到服务器。 创建/etc/nginx/conf.d/vue-app.conf,配置nginx:
# 前端 Vue 应用服务器配置
server {
listen 3000; # 前端访问端口
server_name ; # 服务器IP
root ; # Vue dist 文件的实际路径
index index.html;
# 错误页面配置
error_page 404 /index.html;
error_page 500 502 503 504 /50x.html;
# 前端静态文件服务 - 处理 Vue Router 的路由
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
}
# 静态资源缓存配置
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# API 代理配置 - 将前端的/api请求转发到后端8081端口
location /api/ {
# 代理到后端Spring Boot
proxy_pass http://117.72.66.48:8081/api/;
# 传递必要的头信息
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# CORS 头
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header Access-Control-Allow-Headers '*' always;
# 处理 OPTIONS 预检请求
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin $http_origin;
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS';
add_header Access-Control-Allow-Headers '*';
add_header Access-Control-Max-Age 1728000;
add_header Content-Type 'text/plain; charset=utf-8';
add_header Content-Length 0;
return 204;
}
}
# 安全头
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
设置文件权限:
# 设置正确的文件权限 chown -R nginx:nginx /AIbot/dist/ chmod -R 755 /AIbot/dist/ # 检查权限 ls -la /AIbot/dist/
重启Nginx:systemctl restart nginx
后端部署
打包SpringBoot项目,打包后的结果是一个/target/下的jar包。
后台运行:nohup java -jar your-app.jar > app.log 2>&1 &