
如果你手里有一套若依(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、定时任务都依赖RedisAI服务如果也要做会话缓存建议复用同一个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可以方便地创建独立环境以后模型依赖互相打架了直接删掉环境重建就行。安装Minicondawget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh bash Miniconda3-latest-Linux-x86_64.sh # 按提示安装完成后创建独立环境 conda create -n ruoyi-ai python3.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和FastAPIpip install transformers fastapi uvicorn requests sentencepiece accelerate如果你的模型是量化版本还要装bitsandbytes如果要跑Embedding做向量检索装sentence-transformers。这些都是常见的依赖不用刻意追求最新版本固定版本能减少很多兼容问题。我当时的版本组合是transformers4.40.0、torch2.3.0、fastapi0.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_codeTrue) self.model AutoModelForCausalLM.from_pretrained( self.model_path, trust_remote_codeTrue, device_mapauto, torch_dtypetorch.float16 ) print(模型加载完成) def chat(self, messages): if self.model is None: self.load() # 这里用chat template进行推理 ...注意device_mapauto。在有多张GPU的机器上它会自动分配显存。如果你只有CPU去掉这个参数加上devicecpu。3. 前后端对接让若依界面用上AI能力3.1 若依前端增加AI助手菜单若依前端的菜单管理在“系统管理 - 菜单管理”里。新增一个目录“AI助手”再增加一个菜单“智能问答”路由地址填ai/chat组件路径填ai/chat/index权限标识随便填一个比如ai:chat:list。记得给对应角色分配权限不然菜单显示不出来。这里有个小细节若依前端的动态路由是根据菜单配置从后端拉取的所以你新增菜单后不需要重新打包前端只要后端重新启动或刷新权限缓存前端登录用户重新拉取菜单就能看到。我一开始改了菜单后前端始终不显示后来发现是Redis里存了旧的菜单缓存清掉Redis重启就好了。3.2 聊天界面用SSE还是WebSocket大模型生成回复是流式的一个字一个字往外蹦。前端如果等整个回复生成完再显示用户会等得很着急。所以要实现流式输出常用方案是SSEServer-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代理时如果开启了GzipSSE流式输出会被缓冲导致前端迟迟收不到数据。所以Nginx对SSE路径要关闭缓冲location /ai/chat/sse { proxy_buffering off; proxy_cache off; proxy_set_header X-Accel-Buffering no; }这个坑我排查了很久最后发现是Nginx默认开启了proxy_bufferingSSE消息被攒在缓冲区里直到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_k3) # 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)这个模型不大几百MBCPU也能跑。将每个文本块送入模型得到384维向量存入Chromadbimport 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))], documentstexts, metadatas[{source: doc_name} for doc_name in doc_names], embeddingsembedder.encode(texts).tolist() )检索时直接results collection.query( query_embeddingsembedder.encode([query]).tolist(), n_results3 )这里有个性能优化点如果文档量不大几千个文本块直接用内存级别的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); MapString, Object body new HashMap(); body.put(user_id, userId); body.put(message, message); HttpEntityMapString, Object entity new HttpEntity(body, headers); ResponseEntityMap response restTemplate.postForEntity(aiServiceUrl, entity, Map.class); MapString, 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_DEVICES0只使用某张卡。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_4bitTrue能省下大量显存。ValueError: Tokenizer class X does not exist or is not currently imported这种一般是trust_remote_codeTrue没有设置加上即可。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字以内。结语一些大实话这套环境搭建下来最耗费时间的其实不是安装软件而是调试模型输出和编排前后端接口。当初如果只接一个付费APIDemo可能半天就能跑通但考虑到数据内网部署的要求最终还是选择了开源模型本地化。两种路线的差别就好比“点外卖”和“自己做饭”点外卖快、味道稳定但长期下来成本高而且还得看供应商的脸色自己做饭前期备菜辛苦但食材在手里怎么炒都踏实。如果你只是临时做演示直接接API完全没问题如果你要在生产环境长期跑我强烈建议至少把模型推理服务独立出来用一套标准的接口封装好以后换模型、换供应商都在这一层替换业务方无感知。最后再分享一个小技巧AI服务的配置项包括模型路径、API Key、向量库路径不要写在代码里统一放到.env文件或配置中心。我因为贪图省事把模型路径硬编码在config.py里后来迁移服务器时改一处漏一处足足花了半天才想起还有一处没改。切记配置和代码分离是工程上最便宜的省心方式。这套环境搭好之后后续无论是加新的知识库、还是对接若依的业务数据整个底座都已经成型剩下的就是不断往里面填内容了。