【WMS 仓储系统集成 AI Agent 实战】第 2 讲:双 ChatClient 架构——首 token 从 57 秒到 1 秒,keepAlive 是关键
环境搭好了,这一讲把模型接进来。核心内容:为什么一个 ChatClient 不够用、keepAlive 怎么配才能让模型常驻显存、以及一套改参数不用重启服务的三层配置热加载设计。
本讲复现环境与版本
|
项 |
版本/说明 |
|
操作系统 |
Windows 11 开发机 |
|
Ollama |
Windows 版,端口 11434 |
|
模型 |
|
|
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 年。
两个配套改动:
timeout: 90s → 300s——即使配了永久驻留,也要防极端情况下的冷加载把超时打爆
- 不能用
-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 |
模型 |
职责 |
工具 |
|
|
qwen2.5:7b-instruct-q4_K_M |
ERP 智能体对话 + Function Calling |
8 个 Tool 类、19 个 @Tool 方法 |
|
|
qwen2.5:7b |
RAG 知识库问答 + 文档解析 + 查询改写 |
无工具 |
为什么分开?
一开始我是共用的,后来发现三个问题:
- 职责污染:智能体客户端挂了 19 个工具,RAG 问答时模型会"手痒"去调工具——明明该基于检索片段回答,它却去查库存表
- 参数不匹配:智能体要低温度(0.2)保证工具调用确定性,RAG 生成稍微高点(0.6)回答更自然
- 系统提示词不同:一个是"你是 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 参数配置"页面改参数,保存即生效:
两个设计细节:
- volatile + 整体替换:不用锁。写侧直接替换引用,读侧要么读到旧对象要么读到新对象,都是完整一致的
- 变更审计:每次保存记录
oldValue / newValue / 操作人进ai_config_log表,出问题可以追责到人
敏感凭证(数据库密码、JWT secret)永远只在 yml,不进页面配置——页面可改的东西越少,被改坏的概率越小。
一个必须知道的限制
热加载只覆盖运行时读取的参数(temperature、topK、阈值这类)。模型名、keepAlive 这些建立在 ChatClient Bean 上的参数,改了要重启。这是 Spring Bean 生命周期决定的,别强求。
本讲踩坑清单
|
# |
坑 |
涉及版本 |
根因 |
解法 |
|
1 |
首 token 57 秒 |
hermes3:latest · keepAlive=10m |
模型被赶出内存,冷加载 |
|
|
2 |
keepAlive 传 -1 报错 |
Spring AI 1.0.9 OllamaOptions |
要求带时间单位的时长字符串 |
用 |
|
3 |
AI 不调工具开始编答案 |
Spring AI 1.0.9 |
Controller 里 |
系统提示词只写在 defaultSystem,Controller 禁用 .system() |
|
4 |
RAG 问答时模型去调工具 |
单 ChatClient 挂满工具 |
两个场景共用一个客户端 |
拆成双 ChatClient |
|
5 |
调参要重启服务 |
参数固化在 Bean 里 |
无热加载通道 |
三层配置 + volatile 缓存热加载 |
写在最后
这一讲的核心就三句话:keepAlive 决定响应速度,双 ChatClient 决定职责边界,三层配置决定运维体验。
模型接进来了,但现在的接口是裸奔的——任何人 curl 一下就能调。下一讲讲认证:Spring Security 6 的过滤器顺序有一个天坑(一词之差全站 401),SSE 异步场景下认证上下文丢失也是出了名的诡异,我都会把踩坑过程完整还原。
更多推荐

所有评论(0)