智能体的自定义工具

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()

相关推荐
Darkwanderor22 分钟前
对Linux的进程控制的研究
linux·运维·c++
我命由我1234529 分钟前
执行 Gradle 指令报错,无法将“grep”项识别为 cmdlet、函数、脚本文件或可运行程序的名称
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
考虑考虑43 分钟前
Sentinel安装
java·后端·微服务
_瑞43 分钟前
深入理解 iOS 渲染原理
前端·ios
IT_陈寒1 小时前
SpringBoot自动配置失灵?你可能忘了这个关键注解
前端·人工智能·后端
iCOD3R1 小时前
Skill - kill-ai-slop 解决“AI 味”样式
前端·css·ai编程
皮音2 小时前
Skill 在项目中的实践 —— 从 Agent 困境到 Skill 工程化
前端
大龄秃头程序员2 小时前
RN 0.86 新架构实战:TurboModule + Fabric 组件 + iOS 原生容器,完整代码开源
前端·react native
这是个栗子2 小时前
前端开发中的常用工具函数(八)
开发语言·前端·javascript
Hilaku2 小时前
Vue 和 React 真正的差距,不在语法,而在团队犯错成本
前端·javascript·程序员