环境搭好了,这一讲把模型接进来。核心内容:为什么一个 ChatClient 不够用、keepAlive 怎么配才能让模型常驻显存、以及一套改参数不用重启服务的三层配置热加载设计。

本讲复现环境与版本

项

版本/说明

操作系统

Windows 11 开发机

Ollama

Windows 版,端口 11434

模型

qwen2.5:7b-instruct-q4_K_M(主智能体)/ qwen2.5:7b(RAG)/ bge-m3(Embedding)

Spring AI

1.0.9(OllamaOptions / ChatClient)

Spring Boot

3.5.16

JDK

17

本讲问题均按「版本号 → 复现环境 → 真实报错 → 项目实际现象」四要素记录。

先说一个让我怀疑人生的数字:57 秒

系统刚跑通那天,我兴冲冲发起第一次对话,然后盯着屏幕等了 57 秒才看到第一个字。

📋 问题档案

  • 版本:hermes3:latest(8B)+ keepAlive=10m · Spring AI 1.0.9
  • 复现环境:配 keepAlive("10m"),停止对话 10 分钟后再发一条消息,必现
  • 真实报错:无异常——这才是坑。请求正常返回,只是慢
  • 项目实际现象:系统跑通当天第一次对话,首 token 等了 57 秒;/api/ps 返回 size_vram: 0 证实模型已不在显存

用户的体感就是"系统卡死了"。查 Ollama 的 /api/ps 接口:

返回(关键字段,原文节选):

size_vram: 0——模型根本没在显存里。也就是说每次对话都要把 4.7GB 的权重从磁盘加载进内存,这就是冷加载的代价。

根因是我配置的 keepAlive: 10m:10 分钟没有请求,Ollama 就把模型从内存里赶出去,下次请求重新加载。

解法简单粗暴:

改完之后验证:首次冷加载 57s,之后再次调用首 token 1.1 秒。/api/ps 里 expires_at 显示约 +100 年。

两个配套改动:

  1. timeout: 90s → 300s——即使配了永久驻留,也要防极端情况下的冷加载把超时打爆
  1. 不能用 -1——Spring AI 的 OllamaOptions 对 keepAlive 传 -1 会报参数格式错误,必须带时间单位,比如 "876000h"

📋 问题档案(keepAlive = -1)

  • 版本:Spring AI 1.0.9 OllamaOptions + Ollama Windows 版
  • 复现环境:.keepAlive("-1") 后发起任意对话
  • 真实报错:Ollama 拒绝该请求(keepAlive 参数格式不合法),对话接口直接失败
  • 项目实际现象:想当然地用 -1 表示"永不过期"(很多系统的惯例),结果一条对话都发不出去。Ollama 的 keepAlive 要的是带时间单位的时长字符串,876000h 才是正确写法

修复后的验证数据(真实测量值):

敲黑板:keepAlive 是 Ollama 本地部署最容易忽略的性能开关。显存够(qwen2.5 7B 量化版约 5GB),就大胆永久驻留。


为什么需要两个 ChatClient

架构上我定义了两个 ChatClient Bean:

Bean

模型

职责

工具

hermesChatClient

qwen2.5:7b-instruct-q4_K_M

ERP 智能体对话 + Function Calling

8 个 Tool 类、19 个 @Tool 方法

qwenChatClient

qwen2.5:7b

RAG 知识库问答 + 文档解析 + 查询改写

无工具

为什么分开?

一开始我是共用的,后来发现三个问题:

  1. 职责污染:智能体客户端挂了 19 个工具,RAG 问答时模型会"手痒"去调工具——明明该基于检索片段回答,它却去查库存表
  1. 参数不匹配:智能体要低温度(0.2)保证工具调用确定性,RAG 生成稍微高点(0.6)回答更自然
  1. 系统提示词不同:一个是"你是 ERP 业务助手,必须调用工具获取真实数据",一个是"你是知识库问答助手,必须基于提供的上下文回答"

分开之后各自干净。


OllamaChatClientConfig 完整实现

几个参数的取值逻辑解释一下:

  • temperature: 0.2 —— 工具调用场景要的是"听到指令就执行",不是发挥创造力。温度高了模型容易"发挥"
  • numCtx: 8192 —— 不是越大越好。上下文窗口越大,KV 缓存占用越高,推理越慢。8K 对仓储业务对话绰绰有余
  • numPredict: 2048 —— 单次回复上限,防止模型抽风无限输出

系统提示词的坑

这里有个隐蔽的坑,当时查了很久:AI 突然不调用工具了,直接编造回答。

📋 问题档案

  • 版本:Spring AI 1.0.9(ChatClient API)
  • 复现环境:某次重构后在 Controller 的调用链里加了 .system(systemRules),再问任何库存问题
  • 真实报错:无。日志里没有任何异常,工具调用相关的日志一行都没有
  • 项目实际现象:问「查询 ZL001 的库存」,AI 不再调 queryStock,而是直接编一个库存数字回答。静默失效比抛异常可怕得多——用户拿到的编造数据看起来跟真的一样

日志翻了个遍,最后发现是我某次重构时在 Controller 里加了 .system(systemRules),把 defaultSystem 里定义的智能体角色和工具调用说明整个覆盖了。

Spring AI 的 .system() 是替换不是追加。

解法:把所有系统提示词合并进 defaultSystem(),Controller 层绝不调用 .system()。这个约定写进了代码注释,防止后人再踩。


三层配置热加载

最后一个话题:参数怎么管理。

需求很现实:temperature 调到 0.4 试试效果,总不能每次都重启服务吧——重启一次冷加载又是 57 秒。

我的方案是三层配置,优先级从低到高:

第 1 层:Bean 默认值

代码里永远有值可用,NPE 不存在的。

第 2 层:yml 环境基线

不同环境(开发/测试)可以有不同基线。

第 3 层:页面配置 + 热加载

数据库一张 ai_config 单行表(id=1),管理员在"AI 大模型 & RAG 参数配置"页面改参数,保存即生效:

两个设计细节:

  1. volatile + 整体替换:不用锁。写侧直接替换引用,读侧要么读到旧对象要么读到新对象,都是完整一致的
  1. 变更审计:每次保存记录 oldValue / newValue / 操作人 进 ai_config_log 表,出问题可以追责到人

敏感凭证(数据库密码、JWT secret)永远只在 yml,不进页面配置——页面可改的东西越少,被改坏的概率越小。

一个必须知道的限制

热加载只覆盖运行时读取的参数(temperature、topK、阈值这类)。模型名、keepAlive 这些建立在 ChatClient Bean 上的参数,改了要重启。这是 Spring Bean 生命周期决定的,别强求。


本讲踩坑清单

#

坑

涉及版本

根因

解法

1

首 token 57 秒

hermes3:latest · keepAlive=10m

模型被赶出内存,冷加载

keepAlive("876000h") 永久驻留

2

keepAlive 传 -1 报错

Spring AI 1.0.9 OllamaOptions

要求带时间单位的时长字符串

用 "876000h" 这种格式

3

AI 不调工具开始编答案

Spring AI 1.0.9

Controller 里 .system() 覆盖了 defaultSystem

系统提示词只写在 defaultSystem,Controller 禁用 .system()

4

RAG 问答时模型去调工具

单 ChatClient 挂满工具

两个场景共用一个客户端

拆成双 ChatClient

5

调参要重启服务

参数固化在 Bean 里

无热加载通道

三层配置 + volatile 缓存热加载


写在最后

这一讲的核心就三句话:keepAlive 决定响应速度,双 ChatClient 决定职责边界,三层配置决定运维体验。

模型接进来了,但现在的接口是裸奔的——任何人 curl 一下就能调。下一讲讲认证:Spring Security 6 的过滤器顺序有一个天坑(一词之差全站 401),SSE 异步场景下认证上下文丢失也是出了名的诡异,我都会把踩坑过程完整还原。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐