{"record":{"id":"1a66aca4b30a7be3","repo":"YunaiV/yudao-cloud","slug":"tool","errorCode":null,"errorMessage":"暂不支持 tool 消息：{}","messagePattern":"暂不支持 tool 消息：(.+?)","errorType":"exception","errorClass":"UnsupportedOperationException","httpStatus":500,"severity":"error","filePath":"yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java","lineNumber":148,"sourceCode":"                return OllamaChatOptions.builder().model(model).temperature(temperature).numPredict(maxTokens)\n                        .toolCallbacks(toolCallbacks).toolContext(toolContext).build();\n            default:\n                throw new IllegalArgumentException(StrUtil.format(\"未知平台({})\", platform));\n        }\n    }\n\n    public static Message buildMessage(String type, String content) {\n        if (MessageType.USER.getValue().equals(type)) {\n            return new UserMessage(content);\n        }\n        if (MessageType.ASSISTANT.getValue().equals(type)) {\n            return new AssistantMessage(content);\n        }\n        if (MessageType.SYSTEM.getValue().equals(type)) {\n            return new SystemMessage(content);\n        }\n        if (MessageType.TOOL.getValue().equals(type)) {\n            throw new UnsupportedOperationException(\"暂不支持 tool 消息：\" + content);\n        }\n        throw new IllegalArgumentException(StrUtil.format(\"未知消息类型({})\", type));\n    }\n\n    public static Map<String, Object> buildCommonToolContext() {\n        Map<String, Object> context = new HashMap<>();\n        context.put(TOOL_CONTEXT_LOGIN_USER, SecurityFrameworkUtils.getLoginUser());\n        context.put(TOOL_CONTEXT_TENANT_ID, TenantContextHolder.getTenantId());\n        return context;\n    }\n\n    @SuppressWarnings(\"ConstantValue\")\n    public static String getChatResponseContent(ChatResponse response) {\n        if (response == null\n                || response.getResult() == null\n                || response.getResult().getOutput() == null) {\n            return null;\n        }","sourceCodeStart":130,"sourceCodeEnd":166,"githubUrl":"https://github.com/YunaiV/yudao-cloud/blob/477be9dd49ab7223a972a6abdff0684d6423dec3/yudao-module-ai/yudao-module-ai-server/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java#L130-L166","documentation":"AiUtils.buildMessage can construct UserMessage, AssistantMessage and SystemMessage for Spring AI, but throws UnsupportedOperationException for knowledge/role messages whose type is 'tool'. Tool messages carry structured call results (name, arguments, response) that cannot be faithfully rebuilt from a plain string, so the builder refuses them by design.","triggerScenarios":"Calling buildMessage with a persisted ai knowledge message row whose type column equals the TOOL enum value; re-hydrating chat history that contains tool-call results and passing it through this helper; a client sending type=tool in a message list to an AI chat/conversation endpoint that funnels into buildMessage.","commonSituations":"Conversation history containing tool interactions being replayed into a new prompt; admin UI letting users paste arbitrary message types into a role prompt; migrating data where message type was mis-tagged as tool.","solutions":["Filter out TOOL-type messages before calling buildMessage (they are not renderable prompt content)","If tool context is genuinely needed, construct a Spring AI ToolResponseMessage explicitly instead of via this generic builder","Fix the upstream data/UI so tool-interaction records are not sent as ordinary chat messages"],"exampleFix":"// before\nList<Message> messages = messages.stream()\n    .map(m -> AiUtils.buildMessage(m.getType(), m.getContent()))\n    .collect(Collectors.toList());\n\n// after\nList<Message> messages = messages.stream()\n    .filter(m -> !MessageType.TOOL.getValue().equals(m.getType())) // drop tool rows\n    .map(m -> AiUtils.buildMessage(m.getType(), m.getContent()))\n    .collect(Collectors.toList());","handlingStrategy":"validation","validationCode":"// drop non-renderable message types before building prompts\nList<Message> messages = rows.stream()\n    .filter(r -> !MessageType.TOOL.getValue().equals(r.getType()))\n    .filter(r -> isRenderable(r.getType()))\n    .map(r -> AiUtils.buildMessage(r.getType(), r.getContent()))\n    .toList();","typeGuard":"boolean isRenderable(String type) {\n    return MessageType.USER.getValue().equals(type)\n        || MessageType.ASSISTANT.getValue().equals(type)\n        || MessageType.SYSTEM.getValue().equals(type);\n}","tryCatchPattern":"try {\n    messages.add(AiUtils.buildMessage(type, content));\n} catch (UnsupportedOperationException e) { // tool rows are not prompt content\n    log.debug(\"skipping non-renderable message type={}\", type);\n}","preventionTips":["Filter persisted history to user/assistant/system before prompt assembly","Represent tool interactions in a dedicated table/field, not as generic messages","Reject type=tool at the request VO level in your controller"],"tags":["ai","spring-ai","chat","message-type","unsupported-operation"],"backgroundTag":null,"analyzedSha":"477be9dd49ab7223a972a6abdff0684d6423dec3","analyzedAt":"2026-08-14T13:35:31.121Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}