从零搭建企业级 ERP AI 智能体:Spring Boot 3 + Spring AI + Ollama 全链路落地实践(踩坑全集)
一、项目全景:我们在做什么
这是一个ERP + AI 智能体系统。简单来说,就是把一个传统的 ERP(物料管理、库存查询、出入库单据、质检管理)接入大模型,让用户用自然语言就能完成业务操作。
比如用户说"查询物料 ZL001 的库存",AI 会自动调用 queryStock 工具查数据库,返回真实库存数据——不是编的。
再比如用户在知识库里上传了公司采购流程文档,之后问"公司的采购流程是什么",AI 会做 RAG 检索,基于文档内容给出准确回答,还能多轮追问"那第二步呢"“输出简单流程”。
系统架构一张图
<span leaf="">┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────────┐</span>
二、技术栈与版本清单
后端依赖(pom.xml 核心部分)
| 依赖
|
版本
|
用途
|
踩坑指数
Spring Boot
|
3.5.16
|
Web + Security + Redis + Validation
|
⭐⭐⭐⭐
| |
Spring AI
|
1.0.9
|
Ollama 对话 / pgvector 向量存储 / Tika 文档解析
|
⭐⭐⭐⭐⭐
| |
MyBatis-Plus
|
3.5.8 (spring-boot3-starter)
|
ORM + 分页 + 审计字段自动填充
|
⭐⭐⭐⭐
| |
PostgreSQL JDBC
|
42.7.13
|
数据库驱动
|
⭐
| |
pgvector
|
v0.8.5 (PG17 编译版)
|
向量相似度检索
|
⭐⭐⭐
| |
JWT (jjwt)
|
0.12.6
|
Token 生成/验证/刷新
|
⭐⭐⭐⭐
| |
Knife4j (OpenAPI3)
|
4.5.0
|
接口文档
|
⭐⭐⭐
| |
Hutool
|
5.8.46
|
工具类
|
⭐
| |
Lombok
|
1.18.44
|
实体简化
|
⭐⭐
| |
JDK
|
17(编译用 21)
|
运行环境
|
⭐⭐⭐
|
前端依赖(package.json)
| 依赖
|
版本
|
用途
Vue
|
^3.5.13
|
框架
| |
Vue Router
|
^4.5.0
|
路由
| |
Pinia
|
^2.3.0
|
状态管理
| |
Axios
|
^1.7.9
|
HTTP 请求
| |
Element Plus
|
^2.9.1
|
UI 组件库
| |
markdown-it
|
^14.1.0
|
AI 回复 Markdown 渲染
| |
Vite
|
^6.2.4
|
构建工具
|
基础设施
| 组件
|
版本
|
说明
PostgreSQL
|
17.10
|
业务库 + 向量库
| |
pgvector
|
v0.8.5-pg17
|
向量扩展
| |
Redis
|
7.x
|
Token 缓存 + 角色缓存
| |
Ollama
|
最新版
|
本地大模型推理引擎
| |
Nginx
|
1.30.4
|
反向代理 + 静态资源
| |
Nacos
|
-
|
配置中心(预留,未启用)
|
三、环境搭建:从空机器到能跑
3.1 PostgreSQL 17 + pgvector 安装
安装 PostgreSQL 17,这个没什么好说的,下一步下一步。
安装 pgvector 扩展——这里是第一个坑。
<span leaf="">现象:执行 <span>CREATE</span> EXTENSION IF <span>NOT</span> <span>EXISTS</span> vector;</span>
根因:手动安装 pgvector 时只复制了 share/extension/ 下的 SQL 和 .control 文件,漏掉了lib/vector.dll共享库。
解决:pgvector 安装需要三部分缺一不可:
| 文件
|
目标路径
|
作用
vector.dll |
PostgreSQL17/lib/ |
共享库(缺了就报 58P01)
|
| vector.control
+ vector--*.sql
| PostgreSQL17/share/extension/ |
SQL 定义
| |
头文件(可选)
| PostgreSQL17/include/server/extension/vector/ |
编译用
|
复制完后重新执行 CREATE EXTENSION IF NOT EXISTS vector; 即可。
3.2 Ollama 安装与模型下载
安装 Ollama:从官网下载 Windows 安装包,一路下一步,默认端口 11434。
下载模型(这是整个项目最关键的决策之一):
<span leaf=""><span># 主智能体模型:负责 Function Calling、ERP 业务操作</span></span>
| 模型
|
选型过程
|
踩坑经历
qwen2.5:7b-instruct-q4_K_M
|
最初用 hermes3:latest(8B),但发现字符串参数绑定时好时坏——问"查 ZL001 库存"经常 param=null 反问"请输入物料编码"。换成 Qwen2.5 后 5 次测试 0 次 param=null,工具调用参数绑定稳定性显著提升
|
Hermes3 的 Function Calling 对字符串参数填充不可靠
| |
qwen2.5:7b
|
RAG 和文档解析对工具调用没要求,用 instruct 版浪费
|
一开始两个客户端共用一个模型,后来分开
| |
bge-m3
|
一开始没配 embedding 模型,Spring AI 默认调 mxbai-embed-large,本地没装 → 上传文档向量化直接 404。bge-m3 中文效果最好,1024 维,约 1.2GB
|
不配 embedding 模型 = 知识库功能直接废掉
|
<span leaf=""><span>spring</span>:</span>
Java Config 里的关键参数(OllamaChatClientConfig.java):
<span leaf="">OllamaOptions<span>.builder</span>()</span>
关于
keepAlive的坑:不能用"-1"(Ollama 的永久驻留值)。Spring AI 的keepAlive是字符串类型,Ollama 按 Go duration 解析"-1"会报time: missing unit in duration "-1"(HTTP 400)。必须用带单位的超长时长,如"876000h"。
四、踩坑实录:30+ 个问题的根因与解决方案
下面按问题分类整理,每类给出根因分析和修复方案。这些是真实开发中遇到的问题,不是教科书示例。
4.1 Spring Boot 依赖冲突(5 个问题)
<span leaf="">现象:启动报 BeanDefinitionStoreException... factoryBeanObjectType: java.lang.String</span>
经验法则:springdoc 版本对应关系——
2.x → Spring Boot 3.x,3.0.x → Spring Boot 4.x。别看到新版本就升。
<span leaf="">现象:<span>factoryBeanObjectType</span>: java.<span>lang</span>.<span>String</span></span>
<span leaf="">现象:PSQLException: Unterminated dollar quote started at position <span>65</span></span>
<span leaf="">现象:<span>No</span> qualifying bean... expected single matching bean but found <span>2</span></span>
4.2 Spring Security 6 认证(4 个致命问题)
这一组问题是整个项目最花时间的部分,涉及全站 401、SSE 异步丢认证等诡异 bug。
<span leaf="">现象:登录成功,但后续所有 /api/** 和 /ai/** 请求都返回 401</span>
过滤器顺序正确链路:
<span leaf=""><span>SecurityContextHolderFilter</span> ← 负责加载/保存 SecurityContext</span>
这是最隐蔽的坑:
addFilterBefore和addFilterAfter一词之差,全站 401。
<span leaf="">现象:SSE 首个数据块正常,后续数据块在异步调度时抛 AccessDeniedException</span>
原理:SecurityContextHolder 是 ThreadLocal,SSE 异步线程默认无法继承。必须用
RequestAttributeSecurityContextRepository把 Context 存到 request 属性,且必须新建 Context 对象让 Filter 能检测到变更。
<span leaf="">现象:Token <span>30</span> 分钟过期,开发环境频繁被踢出</span>
<span leaf="">现象:Token 过期后直接跳转登录页,无法自动续期</span>
4.3 Nginx 反向代理(4 个问题)
<span leaf="">现象:通过 http://localhost/ai/chat(80 端口)访问,登录后请求 401</span>
<span leaf="">现象:AI 对话流式输出卡住,token 攒批下发,体感”假死”</span>
排查技巧:Vite dev server (5173) 的 /ai 走 http-proxy 默认流式透传不受影响,故"慢"多发于经 Nginx(80)/dist 访问的链路。通过对比 dev 和 Nginx 链路可快速定位。
<span leaf="">现象:Nginx 透传已生效,但请求头里的 token 是旧 token</span>
<span leaf="">现象:登录后刷新 /ai/knowledge 页面出现 401 JSON</span>
4.4 SSE 流式对话(5 个问题)
<span leaf="">现象:前端收到 application/json,无法流式解析</span>
<span leaf="">现象:前端收到 data:data:xxx</span>
关键认知:Spring Boot 3.2 的 WebFlux SSE 模式下,框架自动加
data:前缀和\n\n结尾,手动包装会导致双重前缀。
<span leaf="">现象:AI 对话流式接口抛 IllegalStateException: No StreamAdvisors available to execute</span>
<span leaf="">现象:No primary or single unique <span>constructor</span> found <span>for</span> <span>interface</span> <span>ServerHttpResponse</span></span>
<span leaf="">现象:接口有返回但页面不展示</span>
4.5 AI 模型对接与工具调用(5 个问题)
<span leaf="">现象:AI 不调用工具,直接编造回答</span>
<span leaf="">现象:问”查询物料 <span>ZL001</span> 的库存”,<span>AI</span> 反问”请输入物料编码”</span>
<span leaf="">现象:首次对话首 token 等待约 57 秒,用户以为系统卡死</span>
CPU 推理特殊说明:本机为 CPU 推理(size_vram=0),冷加载 8B 模型约 57s 是物理瓶颈,无 GPU 加速无法绕过。keepAlive 只能消除"第二次及以后"的冷加载。
<span leaf="">现象:queryStock 原参数为 <span>Long</span> materialId,用户输入物料编码 MAT-<span>001</span> 无法匹配</span>
<span leaf="">现象:AI 回复内容随机出现加粗、标题样式</span>
4.6 知识库 RAG(6 个问题)
<span leaf="">现象:上传文档时向量化 <span>404</span></span>
<span leaf="">现象:vector_store 表从未创建,日志显示 ”<span>Skipping</span> the schema initialization”</span>
<span leaf="">现象:大文档上传 <span>413</span> Request Entity Too <span>Large</span></span>
<span leaf="">根因(三层):</span>
<span leaf="">现象:前一轮已问到采购流程并返回结果,追问”输出简单流程”时提示”未检索到相关内容”</span>
<span leaf="">现象:改了 chunkSize/chunkOverlap 后检索结果没变化</span>
4.7 并发安全与兜底机制(4 个问题)
<span leaf="">现象:同一会话快速连续发送多条消息,AI 回复内容串流</span>
<span leaf="">现象:Ollama 服务挂了,AI 对话请求一直卡到 300s 超时才返回错误</span>
<span leaf="">现象:理论上无限个 SSE 连接可耗尽线程池</span>
这个坑极难发现:Spring MVC SSE 的异步订阅机制让 Semaphore 的执行时机难以预测。Flux.defer 的 lambda 是在订阅时执行,而 Spring MVC 返回 Flux 后 Tomcat 线程就释放了,订阅时机不确定。必须把并发控制在方法体里同步执行。
<span leaf="">现象:高并发下工具调用事件丢失或串流</span>
五、核心架构设计要点
5.1 双 ChatClient 分工
| Bean 名称
|
模型
|
职责
|
工具
hermesChatClient |
qwen2.5:7b-instruct-q4_K_M
|
ERP 智能体对话 + Function Calling
|
8 个 Tool 类、19 个 @Tool 方法
|
| qwenChatClient |
qwen2.5:7b
|
RAG 知识库问答 + 文档解析 + 查询改写
|
无工具
|
5.2 三层配置热加载
<span leaf="">优先级:Bean 兜底默认值 < application.yml (erp.ai.*) < 页面配置 (ai_config 表)</span>
-
AiConfigServiceImpl:@PostConstruct 从 DB 加载到 volatile 内存缓存
-
saveConfig()写 DB + 刷新缓存——热加载,无需重启
-
敏感凭证(数据库/Redis 密码、Ollama 地址)只在 yml,永不上配置页面
-
配置变更审计:
ai_config_log表记录谁/何时/改了什么/旧值→新值/IP
5.3 混合检索 RAG
<span leaf="">用户问题</span>
5.4 19 个业务工具清单
| 工具类
|
@Tool 方法
|
功能
MaterialQueryTool
|
queryMaterial / getMaterialByCode / checkLowStock
|
物料查询/低库存预警
| |
MaterialManageTool
|
createMaterial / updateMaterial / deleteMaterial / listEnabledMaterials
|
物料 CRUD
| |
StockQueryTool
|
queryStock
|
库存查询(支持编码和数字 ID)
| |
InOrderCreateTool
|
createInOrder
|
创建入库单
| |
InOrderManageTool
|
getInOrderByNo / listInOrdersByStatus / confirmInOrder / cancelInOrder / completeInOrder
|
入库单管理
| |
OutOrderCreateTool
|
createOutOrder
|
创建出库单
| |
OutOrderManageTool
|
getOutOrderByNo / listOutOrdersByStatus / confirmOutOrder / cancelOutOrder / completeOutOrder
|
出库单管理
| |
KnowledgeSearchTool
|
searchKnowledge
|
知识库检索
|
六、排查方法论
6.1 401 问题分层排查法
<span leaf="">① curl 直连后端端口(绕过 Nginx<span>/</span>前端)</span>
6.2 SSE 问题排查清单
| 现象
|
检查项
Content-Type 不是 text/event-stream
|
Controller 是否加了 produces
| |
data:data: 双重前缀
|
是否手动拼了 data: 前缀
| |
首个 chunk 后报 AccessDeniedException
|
SecurityContext 是否新建对象 + RequestAttributeSecurityContextRepository
| |
No StreamAdvisors available
|
Flux 是否被多次订阅(用 share/publish.autoConnect)
| |
流式假死/卡顿
|
Nginx 是否 proxy_buffering off + X-Accel-Buffering: no
| |
首字 57 秒
|
Ollama keepAlive 是否过期 → 模型冷加载
|
6.3 模型工具调用排查
<span leaf="">① 直接调 Ollama API(绕过 Spring AI)验证模型是否返回 tool_calls</span>
七、写在最后
这些坑为什么会出现?
-
Spring AI 1.0 还是里程碑版本:文档不完善,很多行为靠踩坑才知道(如 Flux 双订阅、keepAlive 字符串限制)
-
Spring Security 6 的变化:过滤器顺序、SecurityContext 新建 vs 修改,这些在 Spring Security 5 时代不是问题
-
8B 模型的能力边界:Function Calling 参数绑定稳定性、中文理解能力,跟在线大模型有差距
-
SSE + 异步 + 认证的组合:三个复杂机制叠加,任何一个出问题都会导致整个链路崩溃
给后来者的建议
-
先把 Ollama 模型选对:qwen2.5 比 hermes3 在中文工具调用上稳定得多
-
Spring Security 6 过滤器顺序:JWT 过滤器必须在
SecurityContextHolderFilter之后 -
SSE 流别订阅两次:用
share()或publish().autoConnect(2) -
Nginx 一定要
proxy_buffering off:否则 SSE 体验极差 -
embedding 模型一定要配:不配 = 知识库功能直接废
-
并发控制放控制器方法体里:别放
Flux.defer里,订阅时机不确定 -
keepAlive 用带单位的超长时长:别用
"-1",会 400
技术选型反思
| 选对了的
|
选错了/后悔的
Spring AI + Ollama 本地部署(数据不出企业)
|
hermes3 作为主智能体(应一开始就用 qwen2.5)
| |
pgvector(PostgreSQL 原生扩展,无需额外向量库)
|
springdoc 3.0.3(应直接用 2.5.0)
| |
MyBatis-Plus(审计字段自动填充替代触发器)
|
前端 SPA 路由 /ai/ 与后端 API /ai/ 前缀重叠
| |
SSE 流式对话(用户体验好)
|
最初没配 embedding 模型(导致知识库调试很久)
| |
双 ChatClient 分工(智能体 vs RAG)
|
-
|
本文涵盖了从环境搭建到上线全过程的 30+ 个实战问题,每个都附根因分析和解决方案。如果你也在做 Spring AI + Ollama 的企业级落地,这篇能帮你少走至少 2 周弯路。
觉得有用的话,点个赞支持一下,后面还会分享更多实战经验。你们在做AI项目时踩过什么离谱的坑?评论区聊聊 👇
- 原文作者:知识铺
- 原文链接:https://index.zshipu.com/ai002/post/20260822/%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA%E4%BC%81%E4%B8%9A%E7%BA%A7-ERP-AI-%E6%99%BA%E8%83%BD%E4%BD%93Spring-Boot-3-Spring-AI-Ollama-%E5%85%A8%E9%93%BE%E8%B7%AF%E8%90%BD%E5%9C%B0%E5%AE%9E%E8%B7%B5%E8%B8%A9%E5%9D%91%E5%85%A8%E9%9B%86/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。
- 免责声明:本页面内容均来源于站内编辑发布,部分信息来源互联网,并不意味着本站赞同其观点或者证实其内容的真实性,如涉及版权等问题,请立即联系客服进行更改或删除,保证您的合法权益。转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。也可以邮件至 sblig@126.com