如果你手里有一套若依(RuoYi-Vue)后台,老板突然丢过来一个需求:要给内部管理系统加一个AI问答助手,让用户能直接用自然语言查数据、问流程、甚至让AI帮忙写周报——要求一周内看到Demo。这种需求现在越来越常见,但大部分人第一反应是“把ChatGPT接口接进去不就行了”,真动手才发现,AI能力接进来容易,能稳定跑在若依这个Java生态里却是另一回事。
这篇文章把我最近一次在RuoYi项目里从零搭建AI环境的完整过程整理出来。涉及的内容包括:整体架构怎么设计、后端Java和Python模型服务之间怎么通信、PyTorch环境怎么装、前端聊天窗口怎么对接、流式输出怎么做、权限和密钥怎么管,以及我实际踩过的坑和排查思路。无论你是刚接触若依的新手,还是已经在用若依的老手,只要想给项目加AI能力,这篇都能当作一份可以直接照着做的参考。
先说结论:RuoYi + AI最舒服的落地方式,不是把大模型硬塞进Spring Boot里,而是拆成“若依业务系统 + 独立AI服务”的两层结构。若依负责权限、菜单、API网关、用户管理,AI服务专心处理模型推理和智能体逻辑,中间用HTTP或消息队列通信。这样互不干扰,模型迭代、换模型、调参数都和业务系统解耦。下面我把每一步拆开讲,包含具体的命令、配置和代码片段,以及为什么这么选的思考过程。
1. 整体设计与技术选型
1.1 先搞清楚你的AI能力要解决什么问题
很多人一上来就装PyTorch、拉模型,环境搭了一整天,最后发现业务根本用不上。动手之前,先问自己三个问题:
- 这个AI助手是给谁用的?内部员工还是外部客户?
- 它需要访问若依系统里的业务数据吗?比如订单、工单、用户信息。
- 交互方式是单轮问答、多轮对话,还是需要像Agent一样能调工具、执行动作?
这三个问题直接决定技术选型的重量级。如果只是做一个“公司规章制度问答机器人”,不需要接入业务数据,那一个纯Python服务加个向量库就够了,甚至用现成的Embedding模型也行。但如果要让AI帮用户在系统里查待办、生成报表、发起审批流程,这就不是单纯的“聊天”了,而是需要一套Agent机制,让模型能够调用若依后端暴露的接口,甚至在必要时获得用户的授权。
我在这次项目里选的是折中方案:先做内部知识问答,再预留工具调用的接口。也就是说,AI服务本身有独立的数据库存文档向量和会话记录,但它也预留了一个“调用若依OpenAPI”的通道,后续要做业务查询时,只要在AI服务里注册一个工具函数,就能通过JWT令牌调用若依的接口。这样做的好处是,初期Demo不用触碰复杂的业务权限,但架构上已经为日后扩展留了口子。
1.2 技术栈选型:为什么是“若依 + 独立AI服务”而不是纯Java方案
若依本身是Java生态,Spring Boot + Vue + MyBatis,非常成熟。而当前主流的AI开发生态在Python侧:PyTorch、Transformers、LangChain、FastAPI、向量数据库,几乎都是Python的天下。硬要在Java里复刻一套,不是不行,但维护成本很高。比如本地跑一个开源模型,Java侧的ONNX Runtime能加载部分模型,但生态和文档远不如Python方便;再比如Agent编排、Embedding、向量检索这些,Java也有Spring AI和LangChain4j,但成熟度和社区热度仍然跟Python差一截。
所以我的选择是:若依系统保持Java不动,AI能力作为独立服务用Python写。具体技术栈如下:
- 若依后端:Spring Boot 2.7,保持原有框架版本,不升级、不引入AI依赖。
- AI服务:Python 3.10 + FastAPI + PyTorch + Transformers。
- 模型:先接OpenAI兼容API做验证,后续可以切换到本地模型(比如ChatGLM、Qwen),本地模型用Hugging Face Transformers加载。
- 通信方式:若依后端通过HTTP调用AI服务,AI服务暴露
/api/chat、/api/embedding、/api/agent等接口。请求超时时间设置为60秒以上,因为大模型推理不是一个瞬时的过程。 - 前端:若依Vue3版,新增一个“AI助手”菜单页,聊天界面用WebSocket或SSE实现流式输出。
选这套组合的核心理由是“低侵入”。若依框架的代码尽量少改,AI相关逻辑全部放在独立的服务和独立的前端页面里,不污染原有的业务模块。升级若依版本时,AI功能不受影响。
2. 后端环境准备:Java侧和Python侧
2.1 若依后端环境检查
在动手之前,先确认你的若依项目能跑起来。我用的是RuoYi-Vue版本(Spring Boot 2.7 + MyBatis + Redis + Nacos可选),JDK要求1.8以上,Maven 3.6以上,MySQL 5.7或8.0。这些基本条件没问题,我们才开始。
需要特别注意的是Redis。若依的验证码、登录token、定时任务都依赖Redis,AI服务如果也要做会话缓存,建议复用同一个Redis,但key要加前缀区分,避免和若依的业务数据冲突。我当时的场景里,AI会话记录既存在AI服务的MySQL中,也把最近几条上下文放在Redis里,方便快速取用。
另外,若依后端有统一的返回格式AjaxResult,你在写AI信息查询等接口时,要复用这个格式,这样前端才能统处理。AI相关的新接口建议放到com.xxx.ai.controller包下,不要塞进现有的system模块。
2.2 PyTorch环境搭建(踩坑重点)
如果你的AI服务需要本地跑模型,PyTorch环境几乎是绕不开的。我这次在Ubuntu 22.04上搭建,Windows上的流程也大同小异,关键点在于CUDA版本的匹配。
第一步,安装Python 3.10。这里建议用Miniconda而不是直接装系统Python,因为Conda可以方便地创建独立环境,以后模型依赖互相打架了,直接删掉环境重建就行。安装Miniconda:
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh bash Miniconda3-latest-Linux-x86_64.sh # 按提示安装完成后,创建独立环境 conda create -n ruoyi-ai python=3.10 conda activate ruoyi-ai第二步,安装PyTorch。这里千万注意:不要看到官网的pip命令就无脑复制,先看你的显卡驱动和CUDA版本。在终端执行:
nvidia-smi这个命令会显示你的GPU信息和CUDA版本,例如“CUDA Version: 12.1”。然后去PyTorch官网找到对应的安装命令。比如CUDA 12.1,安装命令是:
pip3 install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121如果你的机器没有NVIDIA显卡,那就安装CPU版本,性能虽有差距,但做功能验证足够:
pip install torch torchvision torchaudio我在这里踩过一个坑:公司服务器的显卡是A100,驱动显示CUDA 12.2,但我装了个CUDA 11.8版本的PyTorch,结果跑模型时提示torch.cuda.is_available()为False。一查才发现PyTorch 11.8的预编译包并不兼容12.x的驱动。后来重装12.1版本才正常。
第三步,安装Transformers和FastAPI:
pip install transformers fastapi uvicorn requests sentencepiece accelerate如果你的模型是量化版本,还要装bitsandbytes;如果要跑Embedding做向量检索,装sentence-transformers。这些都是常见的依赖,不用刻意追求最新版本,固定版本能减少很多兼容问题。我当时的版本组合是:transformers==4.40.0、torch==2.3.0、fastapi==0.110.0,实测很稳。
2.3 模型下载与加载方式
模型下载是一个容易忽略的网络问题。Hugging Face上的模型动辄几个G,如果网络不好,很容易下载中断。我当时用了两种办法:一是直接用huggingface_hub的断点续传功能,二是在服务器上先下载到本地目录,再拷贝到项目里。
写代码时,模型加载最好做成懒加载,也就是AI服务启动时不加载模型,等第一次请求来了再加载。这样AI服务启动速度快,而且不会因为模型加载失败导致整个服务起不来。我写了一个简单的模型管理类:
from transformers import AutoModelForCausalLM, AutoTokenizer import threading class ModelManager: def __init__(self, model_path): self.model_path = model_path self.model = None self.tokenizer = None self.lock = threading.Lock() def load(self): with self.lock: if self.model is None: print("开始加载模型...") self.tokenizer = AutoTokenizer.from_pretrained(self.model_path, trust_remote_code=True) self.model = AutoModelForCausalLM.from_pretrained( self.model_path, trust_remote_code=True, device_map="auto", torch_dtype=torch.float16 ) print("模型加载完成") def chat(self, messages): if self.model is None: self.load() # 这里用chat template进行推理 ...注意device_map="auto"。在有多张GPU的机器上,它会自动分配显存。如果你只有CPU,去掉这个参数,加上device="cpu"。
3. 前后端对接:让若依界面用上AI能力
3.1 若依前端增加AI助手菜单
若依前端的菜单管理在“系统管理 -> 菜单管理”里。新增一个目录“AI助手”,再增加一个菜单“智能问答”,路由地址填ai/chat,组件路径填ai/chat/index,权限标识随便填一个比如ai:chat:list。记得给对应角色分配权限,不然菜单显示不出来。
这里有个小细节:若依前端的动态路由是根据菜单配置从后端拉取的,所以你新增菜单后不需要重新打包前端,只要后端重新启动(或刷新权限缓存),前端登录用户重新拉取菜单就能看到。我一开始改了菜单后前端始终不显示,后来发现是Redis里存了旧的菜单缓存,清掉Redis重启就好了。
3.2 聊天界面用SSE还是WebSocket
大模型生成回复是流式的,一个字一个字往外蹦。前端如果等整个回复生成完再显示,用户会等得很着急。所以要实现流式输出,常用方案是SSE(Server-Sent Events)或WebSocket。
我的建议是:优先用SSE。因为SSE是基于HTTP的,若依后端的网关和过滤器都能通用,不需要额外维护WebSocket长连接的状态;而且大模型回复是单向流式的,从服务器流向客户端,正好符合SSE的模型。实现思路有两种:
- 思路一:前端直接连接AI服务,绕过若依后端。这样最快,但会产生跨域和鉴权问题,而且AI服务的地址暴露给了前端。如果AI服务只在内网使用,可以接受;如果对外,不建议。
- 思路二:前端请求若依后端的
/ai/chat/sse接口,若依后端再转发到AI服务,AI服务的流式输出通过若依后端透传给前端。这样能复用若依的登录鉴权和统一出口。缺点是多一层中转,但可维护性高。
我用的思路二。若依后端写一个接口:
@GetMapping("/ai/chat/sse") public SseEmitter streamChat(@RequestParam("message") String message) { SseEmitter emitter = new SseEmitter(0L); // 0L表示不超时 // 异步调用AI服务,拿到流式响应后通过emitter.send()发送 return emitter; }前端用EventSource或者fetch的ReadableStream接收。需要注意的是,Nginx代理时如果开启了Gzip,SSE流式输出会被缓冲,导致前端迟迟收不到数据。所以Nginx对SSE路径要关闭缓冲:
location /ai/chat/sse { proxy_buffering off; proxy_cache off; proxy_set_header X-Accel-Buffering no; }这个坑我排查了很久,最后发现是Nginx默认开启了proxy_buffering,SSE消息被攒在缓冲区里,直到AI服务输出完了才一次性发给前端。把缓冲关掉后,立刻恢复流式效果。
3.3 AI密钥与用户权限管理
AI服务会消耗token,如果你接的是付费API,必须管好密钥。不要在前端代码里直接写API Key,甚至不要在后端配置文件里硬编码。我的做法是:
- API Key存放在若依后端的
application-druid.yml之外的独立配置文件ai-config.yml中,并加入.gitignore。 - 若依后端调用AI服务时,从配置中心或环境变量读取Key,再在HTTP请求头中加上,AI服务收到后校验来源IP或统一Token,防止被外部直接调用。
- 前端用户不感知Key,只需要有若依的登录态。
若依有现成的用户体系,AI助手的提问可以考虑按用户隔离。我的实现是在AI服务的会话接口里多传一个userId字段,AI服务在Redis中按userId存历史记录,这样不同用户之间的对话互不串扰。如果后续要做数据权限控制,AI服务调用若依接口时,需要把用户token传过去,让若依自己去鉴权。
4. 实操案例:做一个文档问答助手
4.1 场景定义和技术路径
为了让整个流程更具体,我以一个“公司内部制度文档问答助手”为例,从头到尾演示一遍。功能很简单:用户上传或导入一些制度文档(比如请假制度、报销流程),AI助手能根据文档内容回答员工的问题。不需要实时访问业务数据,只需要在文档库范围内做检索增强生成(RAG)。
技术路径是:
- 把文档切成文本块。
- 用Embedding模型把每个文本块转成向量,存入向量数据库(这里用Chromadb,轻量,无需单独部署)。
- 用户提问时,AI服务先检索最相关的文本块。
- 将文档块拼接到Prompt中,让大模型基于这些内容回答。
大模型我选了一个开源的中文模型,因为项目要求数据不出内网。为了降低资源占用,我使用的是量化版本(4-bit),在一张16G显存的GPU上能跑。如果你在内网没有GPU,可以先用CPU跑一个小模型验证流程。
4.2 后端Python服务代码结构
AI服务的目录结构如下:
ai-service/ ├── main.py # FastAPI入口 ├── config.py # 配置项 ├── models/ │ └── model_manager.py # 模型管理器 ├── rag/ │ ├── loader.py # 文档加载 │ ├── splitter.py # 文本切分 │ └── retriever.py # 向量检索 ├── data/ │ └── docs/ # 原始文档 └── requirements.txtmain.py核心部分:
from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import Optional app = FastAPI() class ChatRequest(BaseModel): user_id: str message: str history: Optional[list] = None @app.post("/api/chat") async def chat(req: ChatRequest): # 1. 检索相关文档 docs = retriever.search(req.message, top_k=3) # 2. 构造prompt prompt = build_prompt(docs, req.message) # 3. 调用模型生成 answer = model_manager.generate(prompt) return {"answer": answer, "sources": [doc["source"] for doc in docs]}这里我遇到了一个值得注意的问题:Pydantic的模型字段如果对不上,FastAPI会直接报422错误,很多初学者在联调时会莫名其妙看到这个状态码。我的建议是,前端和若依后端传递参数时,字段名要跟AI服务定义完全一致,或者用alias兼容。
4.3 文档切分与向量化细节
文档切分是RAG效果好坏的关键。一开始我图省事,直接把整个Word文档转成文本,按固定长度400字切分。结果发现很多切分点正好在句子中间,导致上下文不连续,检索出来的文档块答非所问。
后来我改用按语义边界切分:优先按段落切,再把过长段落按句子拆,同时相邻文本块之间保留20字的重叠,保证跨块上下文不断裂。
切分完成后,用sentence-transformers加载一个中文Embedding模型:
from sentence_transformers import SentenceTransformer embedder = SentenceTransformer('shibing624/text2vec-base-chinese')这个模型不大,几百MB,CPU也能跑。将每个文本块送入模型得到384维向量,存入Chromadb:
import chromadb client = chromadb.PersistentClient(path="./data/chroma") collection = client.get_or_create_collection("documents") collection.add( ids=[str(i) for i in range(len(texts))], documents=texts, metadatas=[{"source": doc_name} for doc_name in doc_names], embeddings=embedder.encode(texts).tolist() )检索时直接:
results = collection.query( query_embeddings=embedder.encode([query]).tolist(), n_results=3 )这里有个性能优化点:如果文档量不大(几千个文本块),直接用内存级别的Chromadb完全够用,检索耗时不到10毫秒。但如果文档量达到几十万级别,就需要换用Milvus或Qdrant,并且把Embedding结果持久化。我们内部系统文档量不大,所以Chromadb是性价比最高的选择。
4.4 若依后端对接AI服务的代码
若依后端这边,我写了一个AiService供业务调用,同时保持对外的接口是若依风格的。核心代码如下:
@Service public class AiChatService { private final RestTemplate restTemplate; public AiChatService(RestTemplate restTemplate) { this.restTemplate = restTemplate; } public String chat(String userId, String message) { String aiServiceUrl = "http://127.0.0.1:8000/api/chat"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); headers.set("X-Auth-Token", aiServiceToken); Map<String, Object> body = new HashMap<>(); body.put("user_id", userId); body.put("message", message); HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers); ResponseEntity<Map> response = restTemplate.postForEntity(aiServiceUrl, entity, Map.class); Map<String, Object> result = response.getBody(); return result.get("answer").toString(); } }这里RestTemplate需要手动配置连接超时和读取超时。因为大模型生成时间可能很长,默认的5秒超时肯定不够。我当时设置了60秒:
@Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); factory.setReadTimeout(60000); return new RestTemplate(factory); }如果你用的是流式输出,则不能用RestTemplate直接拿到结果,需要改用WebClient或OkHttp的流式接口,把字节流逐行转发给SseEmitter。这部分在文章前面已经提过,实现时需要注意线程池隔离,不要让大模型的慢请求阻塞若依的主线程。
4.5 配置管理与启动顺序
我整理了一份启动清单,方便自己以后复用:
- 启动MySQL和Redis,确保若依后端可以登录。
- 启动AI服务:
uvicorn main:app --host 0.0.0.0 --port 8000。 - 启动若依后端:
mvn spring-boot:run。 - 启动若依前端:
npm run dev。
AI服务如果使用GPU,启动的时候会打印显存占用。我习惯在启动前用nvidia-smi看一眼显存是否够用,如果被其他进程占了,就用kill -9清理掉旧进程,或者设置CUDA_VISIBLE_DEVICES=0只使用某张卡。
5. 常见问题与排查实录
5.1 AI服务调不通:网络、端口、跨域
最常见的问题是若依后端调AI服务时连接超时或连接拒绝。先用curl从服务器上直接测试AI服务是否正常:
curl -X POST http://127.0.0.1:8000/api/chat -H "Content-Type: application/json" -d '{"user_id":"1","message":"你好"}'如果curl通了但若依后端调用不通,检查两点:一是若依后端和AI服务是否在同一台机器,如果不是,确认防火墙和安全组是否放行8000端口;二是若依后端代码中的URL是否写错,尤其是多了斜杠或用了https却给AI服务加上SSL证书验证,这会导致握手失败。
如果前端直接调用AI服务跨域报错,我推荐在后端添加CORS中间件,而不是在前端使用代理改指纹。FastAPI加CORS很简单:
from fastapi.middleware.cors import CORSMiddleware app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])不过生产环境不要用*,指定若依前端的域名。
5.2 PyTorch相关报错
ModuleNotFoundError: No module named 'torch':检查当前激活的conda环境,可能在基础环境里跑了,切换到ruoyi-ai环境。OSError: [Errno 22] Invalid argument:一般是模型路径有问题,或者Hugging Face下载时网络中断导致缓存文件损坏。删除~/.cache/huggingface下对应模型文件,重新下载。CUDA out of memory:显存不够。降低max_length,或开启量化加载,或换更小的模型,或者直接把batch_size降到1。我建议在模型加载时加一个参数load_in_4bit=True,能省下大量显存。ValueError: Tokenizer class X does not exist or is not currently imported:这种一般是trust_remote_code=True没有设置,加上即可。
5.3 若依前端菜单不显示
新增菜单后前端看不到,先F12看网络请求。若依前端登录后,会根据返回的menus动态生成路由。如果返回中没有新菜单,大概率是权限缓存或者Redis缓存。到若依后台“系统管理 -> 菜单管理”看看新菜单的显示状态是否开启、权限字符是否和角色的权限匹配。清空Redis后重新登录试试。
还有一个容易忽略的地方:若依的后端有数据权限拦截,菜单表里的status字段如果是1(停用),前端也不会显示。我那次就是新增菜单时忘了把状态改正常,导致白白排查了半天。
5.4 安全合规注意事项
给若依加AI能力,最容易忽略的是内容安全。特别是面向内部员工的知识问答,如果AI给出了错误或不合适的内容,轻则误导用户,重则产生合规风险。我的工程上有几个建议,也都是实际总结出来的:
- 在AI服务的Prompt里加一层系统约束,告诉模型“只能基于给定的文档内容回答,如果文档中没有答案,请直接说明不知道”。
- 在输出前端做二次过滤,不允许AI回答涉及政治敏感、暴力、歧视等内容的请求。这个可以在AI服务后端接入一个简单的敏感词过滤库,或者对接云安全接口。我们考虑到内网环境,用的本地敏感词过滤,开源方案有不少。
- 日志留存。AI问答的全量请求和响应都要记录日志,包含用户ID、时间、提问内容、AI回答内容。一旦出现问题,可以追溯。
- 内部的API Key定期轮换,AI服务本身不要暴露到公网,尽量只允许若依后端的服务器IP访问。
5.5 模型效果不好的调优思路
如果你发现AI回答的内容跟文档完全不相关,先在检索环节排查。最简单的测试方式是,把用户问题直接拿去查询向量库,看返回的几条文档块是不是语义上跟问题相关。如果不相关,可能是Embedding模型选得不好,或者文档切分粒度太粗。
如果检索结果相关,但模型回答不正确,问题出在Prompt构造上。我当时把Prompt模板围成这样的结构:
你是公司制度助手,请根据以下资料回答问题。 资料: <文档块1> <文档块2> 请只根据资料回答,不要编造。 用户问题:<问题>注意这里资料和问题之间要有明确的标记。同时,把“不知道”作为允许的输出。模型在缺乏信息时如果没有退路,就容易胡编。
还有一个容易翻车的点:如果一次检索的文档块数量太多,超出大模型的上下文长度,会直接报错或者截断。要控制top_k在3-5之间。我们用的是4K上下文的模型,所以单次文档块总字数控制在1500字以内。
结语:一些大实话
这套环境搭建下来,最耗费时间的其实不是安装软件,而是调试模型输出和编排前后端接口。当初如果只接一个付费API,Demo可能半天就能跑通;但考虑到数据内网部署的要求,最终还是选择了开源模型本地化。两种路线的差别,就好比“点外卖”和“自己做饭”:点外卖快、味道稳定,但长期下来成本高,而且还得看供应商的脸色;自己做饭前期备菜辛苦,但食材在手里,怎么炒都踏实。如果你只是临时做演示,直接接API完全没问题;如果你要在生产环境长期跑,我强烈建议至少把模型推理服务独立出来,用一套标准的接口封装好,以后换模型、换供应商都在这一层替换,业务方无感知。
最后再分享一个小技巧:AI服务的配置项,包括模型路径、API Key、向量库路径,不要写在代码里,统一放到.env文件或配置中心。我因为贪图省事,把模型路径硬编码在config.py里,后来迁移服务器时改一处漏一处,足足花了半天才想起还有一处没改。切记,配置和代码分离,是工程上最便宜的省心方式。这套环境搭好之后,后续无论是加新的知识库、还是对接若依的业务数据,整个底座都已经成型,剩下的就是不断往里面填内容了。