1.自定义仓库管理系统中的仓库信息查询工具:
java
@Data
public class WmsInventoryQuery {
//商品名称
@ToolParam(required = true, description = "商品名称(必填)")
private String productName;
//商品编码
@ToolParam(required = false, description = "商品编码")
private String productCode;
//货主名称
@ToolParam(required = true, description = "货主名称(必填)")
private String ownerName;
//货主编码
@ToolParam(required = false, description = "货主编码")
private String ownerCode;
//仓库名称
@ToolParam(required = true, description = "仓库名称(必填)")
private String warehouseName;
//仓库编码
@ToolParam(required = false, description = "仓库编码")
private String warehouseCode;
//储位编码
@ToolParam(required = false, description = "储位编码")
private String locationCode;
}
写一个查询仓库信息的工具类:
java
@Component
public class WmsInventoryTools {
@Resource
private IWmsInventoryService wmsInventoryService;
@Resource
private IWmsCargoOwnersService wmsCargoOwnersService;
@Resource
private IWmsWarehousesService wmsWarehousesService;
@Resource
private IWmsProductsService wmsProductsService;
/**
* 查询库存
*/
@Tool(description = "根据条件查询库存信息")
public List<WmsInventory> queryInventory(
@ToolParam(description = "库存查询条件") WmsInventoryQuery query,
@ToolParam(description = "库存查询页码") Integer pageNo) {
if (pageNo == null || pageNo <= 0) {
pageNo = 1;
}
WmsInventory wmsInventory = new WmsInventory();
BeanUtils.copyProperties(query, wmsInventory);
IPage<WmsInventory> wmsInventoryIPage = wmsInventoryService.queryList(wmsInventory, pageNo, 2);
return wmsInventoryIPage.getRecords();
}
/**
* 查询货主信息
*/
@Tool(description = "查询货主信息")
public List<WmsCargoOwners> queryOwnerInfo() {
return wmsCargoOwnersService.getBaseMapper().selectList(null);
}
/**
* 查询仓库信息
*/
@Tool(description = "查询仓库信息")
public List<WmsWarehouses> queryWarehouses() {
return wmsWarehousesService.getBaseMapper().selectList(null);
}
/**
* 查询商品信息
*/
@Tool(description = "查询商品信息")
public List<WmsProducts> queryProducts() {
return wmsProductsService.getBaseMapper().selectList(null);
}
}
@ToolParam有两种用法,一种是在方法参数上直接标注,另一种是在VO/DTO类上直接标注,本文采用第二种,required参数值必须是true才能生效。
controller类建立聊天窗口:
java
@RestController
@Slf4j
@RequestMapping("/ai")
public class ChatController {
@Resource(name = "chatClientOpenAi")
private ChatClient chatClient;
@Resource
private OpenAiEmbeddingModel openAiEmbeddingModel;
@Resource
private IAiragAppService airagAppService;
@Resource
private RedisUtil redisUtil;
@Resource
private ISysBaseAPI sysBaseApi;
@Resource
private VectorStore pgVectorStore;
@Resource
private WmsInventoryTools wmsInventoryTools;
// 辅助方法:将float数组格式化为字符串
private String arrayToString(float[] array) {
if (array == null) {
return "null";
}
if (array.length == 0) {
return "[]";
}
StringBuilder sb = new StringBuilder();
sb.append("[");
// 限制显示长度,避免输出过长的向量
// 只显示前10个元素
int limit = Math.min(array.length, 10);
for (int i = 0; i < limit; i++) {
sb.append(String.format("%.6f", array[i]));
if (i < limit - 1) {
sb.append(", ");
}
}
if (array.length > limit) {
sb.append(", ... (").append(array.length).append(" dimensions)");
}
sb.append("]");
return sb.toString();
}
private String getUsername(HttpServletRequest httpRequest) {
try {
TokenUtils.getTokenByRequest();
String token;
if (null != httpRequest) {
token = TokenUtils.getTokenByRequest(httpRequest);
} else {
token = TokenUtils.getTokenByRequest();
}
if (TokenUtils.verifyToken(token, sysBaseApi, redisUtil)) {
return JwtUtil.getUsername(token);
}
} catch (Exception e) {
return null;
}
return null;
}
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> chat(String prompt) {
Flux<String> content = chatClient.prompt(prompt).stream().content();
log.debug(String.valueOf(content));
return content;
}
private QuestionAnswerAdvisor getQuestionAnswerAdvisor(List<String> knowIds) {
StringBuilder expression = new StringBuilder();
Iterator<String> iterator = knowIds.iterator();
//通过下边迭代器拼接字符串 最终拼接字符串为:knowledgeId == 'xx' || knowledgeId == 'yy || ...'
while (iterator.hasNext()) {
String knowId = iterator.next();
expression.append("knowledgeId == '").append(knowId).append("'");
if (iterator.hasNext()) {
expression.append(" || ");
}
}
return new QuestionAnswerAdvisor(
// 向量库
pgVectorStore,
// 向量检索的请求参数
SearchRequest.builder()
// 相似度阈值
.similarityThreshold(0.5d)
.filterExpression(expression.toString())
// 返回的文档片段数量
.topK(2)
.build()
);
}
@RequestMapping(value = "/chat/send")
public SseEmitter send(@RequestBody ChatSendParams chatSendParams, HttpServletRequest httpRequest) throws IOException {
AssertUtils.assertNotEmpty("参数异常", chatSendParams);
String userMessage = chatSendParams.getContent();
AssertUtils.assertNotEmpty("至少发送一条消息", userMessage);
// 获取会话信息
String conversationId = chatSendParams.getConversationId();
String topicId = oConvertUtils.getString(chatSendParams.getTopicId(), UUIDGenerator.generate());
chatSendParams.setTopicId(topicId);
String username = getUsername(httpRequest);
chatSendParams.setUsername(username);
// 每次会话都生成一个新的,用来缓存emitter
String requestId = UUIDGenerator.generate();
SseEmitter emitter = new SseEmitter(-0L);
AiragApp app = null;
List<String> knowIds = null;
if (oConvertUtils.isNotEmpty(chatSendParams.getAppId())) {
app = airagAppService.getById(chatSendParams.getAppId());
knowIds = app.getKnowIds();
}
// QuestionAnswerAdvisor questionAnswerAdvisor = getQuestionAnswerAdvisor(knowIds);
// 发送初始思考提示消息
try {
// 发送 "> " 消息
sendMessage(emitter, conversationId, topicId, requestId, "> ", "MESSAGE");
// 发送 "\n> " 消息
sendMessage(emitter, conversationId, topicId, requestId, "\n> ", "MESSAGE");
} catch (IOException e) {
emitter.completeWithError(e);
return emitter;
}
String json = JSONObject.toJSONString(chatSendParams);
Flux<String> flux = null;
if (app == null) {
//没有挂应用
flux = chatClient
.prompt(userMessage)
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, json))
.stream()
.content();
} else {
if (knowIds != null && !knowIds.isEmpty()) {
QuestionAnswerAdvisor questionAnswerAdvisor = getQuestionAnswerAdvisor(knowIds);
flux = chatClient
.prompt(userMessage)
.user(userMessage)
// 添加系统提示词
.system(app.getPrompt())
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, json))
.advisors(questionAnswerAdvisor)
.tools(wmsInventoryTools)
.stream()
.content();
} else {
flux = chatClient
.prompt(userMessage)
.user(userMessage)
// 添加系统提示词
.tools(wmsInventoryTools)
.system(app.getPrompt())
.advisors(a -> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, json))
.stream()
.content();
}
}
/**
* 是否正在思考
*/
AtomicBoolean isThinking = new AtomicBoolean(false);
try {
sendMessage(emitter, conversationId, topicId, requestId, ">", "MESSAGE");
} catch (IOException e) {
emitter.completeWithError(e);
}
// sendMessage(emitter, conversationId, topicId, requestId, "\\n", "MESSAGE");
flux.subscribe(
data -> {
try {
// 兼容推理模型
if ("<think>".equals(data)) {
isThinking.set(true);
data = "> ";
}
if ("</think>".equals(data)) {
isThinking.set(false);
data = "\n\n";
}
if (isThinking.get()) {
if (null != data && data.contains("\n")) {
data = "\n> ";
}
}
sendMessage(emitter, conversationId, topicId, requestId, data, "MESSAGE");
} catch (IOException e) {
emitter.completeWithError(e);//将错误信息发送给客户端,并结束SSE连接。
}
},
error -> {
try {
// 错误响应
sendMessage(emitter, conversationId, topicId, requestId, error.getMessage(), "ERROR");
// 发送结束消息
sendEndMessage(emitter, conversationId, topicId, requestId);
} catch (IOException ioException) {
emitter.completeWithError(ioException);
}
},
() -> {
try {
// 发送结束消息两次
sendEndMessage(emitter, conversationId, topicId, requestId);
sendEndMessage(emitter, conversationId, topicId, requestId);
} catch (IOException e) {
emitter.completeWithError(e);
}
}
);
return emitter;
}
// 辅助方法:向客户端发送消息
private void sendMessage(SseEmitter emitter, String conversationId, String topicId,
String requestId, String message, String event) throws IOException {
Map<String, Object> response = new HashMap<>();
response.put("conversationId", conversationId);
response.put("topicId", topicId);
response.put("requestId", requestId);
response.put("event", event);
Map<String, Object> messageData = new HashMap<>();
messageData.put("message", message);
response.put("data", messageData);
String jsonData = JSONObject.toJSONString(response);
emitter.send(SseEmitter.event().data(jsonData));
}
// 辅助方法:发送结束消息
private void sendEndMessage(SseEmitter emitter, String conversationId, String topicId,
String requestId) throws IOException {
Map<String, Object> endResponse = new HashMap<>();
endResponse.put("event", "MESSAGE_END");
endResponse.put("flowId", null);
endResponse.put("requestId", requestId);
endResponse.put("conversationId", conversationId);
endResponse.put("topicId", topicId);
endResponse.put("data", null);
String endJson = JSONObject.toJSONString(endResponse);
emitter.send(SseEmitter.event().data(endJson));
}
}
QuestionAnswerAdvisor 是 Spring AI 的内置 Advisor,它在每次 LLM 调用前自动执行以下流程:
将用户问题向量化,RAG 检索(调用 LLM 前自动触发)
|---------------------|----------------------|---------------------------------|
| 参数 | 值 | 作用 |
| pgVectorStore | PostgreSQL 向量库 | 存储知识文档的向量和元数据 |
| similarityThreshold | 0.5 | 只有相似度 ≥ 0.5 的文档才会被召回,过滤低质量结果 |
| filterExpression | knowledgeId == 'xxx' | 限定只检索指定的知识库,避免跨库污染 |
| topK | 2 | 每次最多返回 2 个最相关的文档片段,控制 Prompt 长度 |
在调用大模型链里面的作用:
ChatClient
.prompt(userMessage) ← 用户输入信息
.system(app.getPrompt()) ← 系统提示词
.advisors(questionAnswerAdvisor) ← RAG 检索(调用 LLM 前自动触发)
.tools(wmsInventoryTools) ← 工具注册
.advisors(memory) ← 会话记忆(自动,ChatConfiguration 注入)
.stream()