From b1392f84dbda20ca54d61334f51f23c0b80749fa Mon Sep 17 00:00:00 2001 From: orz_zsy <11630699+orzzsy@user.noreply.gitee.com> Date: Thu, 5 Feb 2026 09:15:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=B5=81=E5=BC=8F?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E4=B8=AD=20finishReason=20=E4=B8=8D=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ChatResponseDefault 添加 lastFinishReason 字段保存 LLM 返回的原始值 - 添加 getLastFinishReasonNormalized() 方法,使用时归一化转换 - 各方言在解析 finish_reason 时保存原始值 - 兜底创建 ChatChoice 时使用归一化后的值 --- .../solon/ai/chat/ChatResponseDefault.java | 48 +++++++++++++++++++ .../dialect/claude/ClaudeResponseParser.java | 5 +- .../dashscope/DashscopeChatDialect.java | 5 +- .../dialect/gemini/GeminiResponseParser.java | 8 ++-- .../llm/dialect/ollama/OllamaChatDialect.java | 6 ++- .../llm/dialect/openai/OpenaiChatDialect.java | 5 +- .../openai/OpenaiResponsesResponseParser.java | 4 +- 7 files changed, 69 insertions(+), 12 deletions(-) diff --git a/solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponseDefault.java b/solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponseDefault.java index 58a39674..767741d6 100644 --- a/solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponseDefault.java +++ b/solon-ai-core/src/main/java/org/noear/solon/ai/chat/ChatResponseDefault.java @@ -244,6 +244,54 @@ public class ChatResponseDefault implements ChatResponse { * */ public String lastToolCallId; + /** + * 最后的 finishReason(保存 LLM 返回的原始值,使用时通过 normalizeFinishReason 归一化) + */ + public String lastFinishReason; + + /** + * 获取归一化后的 finishReason,如果没有则返回默认值 "stop" + * + * @return 归一化后的 finishReason + */ + public String getLastFinishReasonNormalized() { + String normalized = normalizeFinishReason(lastFinishReason); + return normalized != null ? normalized : "stop"; + } + + /** + * 归一化 finishReason + * + *

将各 LLM 返回的不同值映射为框架统一定义的值: + *

+ * + * @param finishReason LLM 返回的原始 finishReason + * @return 归一化后的 finishReason + */ + public static String normalizeFinishReason(String finishReason) { + if (finishReason == null || finishReason.isEmpty()) { + return finishReason; + } + + String lower = finishReason.toLowerCase(); + + // 工具调用 → "tool" + if (lower.contains("tool") || lower.contains("function")) { + return "tool"; + } + + // 正常结束 → "stop" + if (lower.contains("stop") || lower.contains("end")) { + return "stop"; + } + + // 其他保持原值 + return finishReason; + } + /** * 重置响应数据 */ diff --git a/solon-ai-llm-dialects/solon-ai-dialect-claude/src/main/java/org/noear/solon/ai/llm/dialect/claude/ClaudeResponseParser.java b/solon-ai-llm-dialects/solon-ai-dialect-claude/src/main/java/org/noear/solon/ai/llm/dialect/claude/ClaudeResponseParser.java index 3ec95850..1fffedb8 100644 --- a/solon-ai-llm-dialects/solon-ai-dialect-claude/src/main/java/org/noear/solon/ai/llm/dialect/claude/ClaudeResponseParser.java +++ b/solon-ai-llm-dialects/solon-ai-dialect-claude/src/main/java/org/noear/solon/ai/llm/dialect/claude/ClaudeResponseParser.java @@ -128,7 +128,7 @@ public class ClaudeResponseParser { } if ("[DONE]".equals(jsonData)) { if (resp.isFinished() == false) { - resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); resp.setFinished(true); } return true; @@ -306,6 +306,7 @@ public class ClaudeResponseParser { String finishReason = stopReason.get("stop_reason").getString(); if (Utils.isNotEmpty(finishReason)) { resp.setFinished(true); + resp.lastFinishReason = finishReason; } } } else if ("message_stop".equals(eventType)) { @@ -331,7 +332,7 @@ public class ClaudeResponseParser { public boolean parseNonStreamResponse(ChatResponseDefault resp, String json) { if ("[DONE]".equals(json)) { if (resp.isFinished() == false) { - resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); resp.setFinished(true); } return true; diff --git a/solon-ai-llm-dialects/solon-ai-dialect-dashscope/src/main/java/org/noear/solon/ai/llm/dialect/dashscope/DashscopeChatDialect.java b/solon-ai-llm-dialects/solon-ai-dialect-dashscope/src/main/java/org/noear/solon/ai/llm/dialect/dashscope/DashscopeChatDialect.java index a01700ea..f652506e 100644 --- a/solon-ai-llm-dialects/solon-ai-dialect-dashscope/src/main/java/org/noear/solon/ai/llm/dialect/dashscope/DashscopeChatDialect.java +++ b/solon-ai-llm-dialects/solon-ai-dialect-dashscope/src/main/java/org/noear/solon/ai/llm/dialect/dashscope/DashscopeChatDialect.java @@ -101,7 +101,7 @@ public class DashscopeChatDialect extends AbstractChatDialect { public boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String json) { if ("[DONE]".equals(json)) { //不是数据结构 if(resp.isFinished() == false) { - resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); resp.setFinished(true); } return true; @@ -132,6 +132,7 @@ public class DashscopeChatDialect extends AbstractChatDialect { if (Utils.isNotEmpty(finish_reason)) { resp.setFinished(true); + resp.lastFinishReason = finish_reason; } index++; @@ -139,7 +140,7 @@ public class DashscopeChatDialect extends AbstractChatDialect { if (resp.isFinished()) { if (resp.hasChoices() == false) { - resp.addChoice(new ChatChoice(0, created, "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, created, resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); } } diff --git a/solon-ai-llm-dialects/solon-ai-dialect-gemini/src/main/java/org/noear/solon/ai/llm/dialect/gemini/GeminiResponseParser.java b/solon-ai-llm-dialects/solon-ai-dialect-gemini/src/main/java/org/noear/solon/ai/llm/dialect/gemini/GeminiResponseParser.java index eb1845f8..761e5cdb 100644 --- a/solon-ai-llm-dialects/solon-ai-dialect-gemini/src/main/java/org/noear/solon/ai/llm/dialect/gemini/GeminiResponseParser.java +++ b/solon-ai-llm-dialects/solon-ai-dialect-gemini/src/main/java/org/noear/solon/ai/llm/dialect/gemini/GeminiResponseParser.java @@ -98,7 +98,7 @@ public class GeminiResponseParser { if ("[DONE]".equals(jsonData)) { if (resp.isFinished() == false) { - resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); resp.setFinished(true); } return true; @@ -145,6 +145,7 @@ public class GeminiResponseParser { if (Utils.isNotEmpty(finishReason)) { resp.setFinished(true); + resp.lastFinishReason = finishReason; } ONode oContent = oChoice1.get("content"); @@ -180,7 +181,7 @@ public class GeminiResponseParser { public boolean parseNonStreamResponse(ChatResponseDefault resp, String json) { if ("[DONE]".equals(json)) { if (resp.isFinished() == false) { - resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); resp.setFinished(true); } return true; @@ -233,13 +234,14 @@ public class GeminiResponseParser { if (Utils.isNotEmpty(finishReason)) { resp.setFinished(true); + resp.lastFinishReason = finishReason; } } } if (resp.isFinished()) { if (resp.hasChoices() == false) { - resp.addChoice(new ChatChoice(0, created, "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, created, resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); } } diff --git a/solon-ai-llm-dialects/solon-ai-dialect-ollama/src/main/java/org/noear/solon/ai/llm/dialect/ollama/OllamaChatDialect.java b/solon-ai-llm-dialects/solon-ai-dialect-ollama/src/main/java/org/noear/solon/ai/llm/dialect/ollama/OllamaChatDialect.java index 1e2ffbc0..e905ae0e 100644 --- a/solon-ai-llm-dialects/solon-ai-dialect-ollama/src/main/java/org/noear/solon/ai/llm/dialect/ollama/OllamaChatDialect.java +++ b/solon-ai-llm-dialects/solon-ai-dialect-ollama/src/main/java/org/noear/solon/ai/llm/dialect/ollama/OllamaChatDialect.java @@ -126,6 +126,10 @@ public class OllamaChatDialect extends AbstractChatDialect { resp.addChoice(new ChatChoice(0, created, done_reason, msg1)); } + if (Utils.isNotEmpty(done_reason)) { + resp.lastFinishReason = done_reason; + } + if (resp.isFinished()) { long promptTokens = oResp.get("prompt_eval_count").getLong(); long completionTokens = oResp.get("eval_count").getLong(); @@ -134,7 +138,7 @@ public class OllamaChatDialect extends AbstractChatDialect { resp.setUsage(new AiUsage(promptTokens, completionTokens, totalTokens, oResp)); if (resp.hasChoices() == false) { - resp.addChoice(new ChatChoice(0, created, "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, created, resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); } } } diff --git a/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiChatDialect.java b/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiChatDialect.java index fbe42293..bb2a23db 100644 --- a/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiChatDialect.java +++ b/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiChatDialect.java @@ -60,7 +60,7 @@ public class OpenaiChatDialect extends AbstractChatDialect { public boolean parseResponseJson(ChatConfig config, ChatResponseDefault resp, String json) { if ("[DONE]".equals(json)) { //不是数据结构 if(resp.isFinished() == false) { - resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); resp.setFinished(true); } return true; @@ -100,12 +100,13 @@ public class OpenaiChatDialect extends AbstractChatDialect { if (Utils.isNotEmpty(finish_reason)) { resp.setFinished(true); + resp.lastFinishReason = finish_reason; } } if (resp.isFinished()) { if (resp.hasChoices() == false) { - resp.addChoice(new ChatChoice(0, created, "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, created, resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); } } diff --git a/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiResponsesResponseParser.java b/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiResponsesResponseParser.java index 93339227..97433f08 100644 --- a/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiResponsesResponseParser.java +++ b/solon-ai-llm-dialects/solon-ai-dialect-openai/src/main/java/org/noear/solon/ai/llm/dialect/openai/OpenaiResponsesResponseParser.java @@ -95,7 +95,7 @@ public class OpenaiResponsesResponseParser { if (jsonData.isEmpty() || "[DONE]".equals(jsonData)) { if ("[DONE]".equals(jsonData)) { if (!resp.isFinished()) { - resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); resp.setFinished(true); } return true; @@ -265,7 +265,7 @@ public class OpenaiResponsesResponseParser { public boolean parseNonStreamResponse(ChatResponseDefault resp, String json) { if ("[DONE]".equals(json)) { if (!resp.isFinished()) { - resp.addChoice(new ChatChoice(0, new Date(), "stop", new AssistantMessage(""))); + resp.addChoice(new ChatChoice(0, new Date(), resp.getLastFinishReasonNormalized(), new AssistantMessage(""))); resp.setFinished(true); } return true; -- Gitee