From 377ddd2b9b536925a8c95bb203c3948920af2c19 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Fri, 17 Apr 2026 14:19:40 +0800 Subject: [PATCH] fix(llm): unify JSON output handling across providers and fix tool+json_output compatibility - Remove redundant `response_format` injection for VOLCANO provider since it's unsupported; rely on system prompt injection instead - Extend system prompt JSON injection logic to cover VOLCANO and tool-enabled cases universally - Simplify model parameter construction by removing redundant `params["model_kwargs"] = model_kwargs` assignments - Refactor `CompatibleChatOpenAI._get_request_payload` to strip `response_format` when tools are present, avoiding strict validation errors in langchain_openai - Fix timestamp calculation order in `datetime_tool.py` to avoid integer truncation before multiplication --- api/app/core/agent/langchain_agent.py | 13 ++++++---- api/app/core/models/base.py | 28 ++++++++++----------- api/app/core/models/compatible_chat.py | 23 ++++++++++++++++- api/app/core/tools/builtin/datetime_tool.py | 4 +-- api/app/core/workflow/nodes/llm/node.py | 9 ++++--- 5 files changed, 51 insertions(+), 26 deletions(-) diff --git a/api/app/core/agent/langchain_agent.py b/api/app/core/agent/langchain_agent.py index 927eb734..a3d1d308 100644 --- a/api/app/core/agent/langchain_agent.py +++ b/api/app/core/agent/langchain_agent.py @@ -12,7 +12,7 @@ import time from typing import Any, AsyncGenerator, Dict, List, Optional, Sequence from langchain.agents import create_agent -from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, SystemMessage +from langchain_core.messages import AIMessage, BaseMessage, HumanMessage from langchain_core.tools import BaseTool from langgraph.errors import GraphRecursionError @@ -83,7 +83,12 @@ class LangChainAgent: # ChatTongyi 要求 messages 含 'json' 字样才能使用 response_format # 在 system prompt 中注入 JSON 要求 from app.models.models_model import ModelProvider - if json_output and provider.lower() == ModelProvider.DASHSCOPE and not is_omni: + if json_output and ( + (provider.lower() == ModelProvider.DASHSCOPE and not is_omni) + or provider.lower() == ModelProvider.VOLCANO + # 有工具时 response_format 会被移除,所有 provider 都需要 system prompt 注入保证 JSON 输出 + or bool(tools) + ): self.system_prompt += "\n请以JSON格式输出。" logger.debug( @@ -240,9 +245,7 @@ class LangChainAgent: Returns: List[BaseMessage]: 消息列表 """ - messages:list = [SystemMessage(content=self.system_prompt)] - - # 添加系统提示词 + messages: list = [] # 添加历史消息 if history: diff --git a/api/app/core/models/base.py b/api/app/core/models/base.py index 7b570b47..86ac5fe0 100644 --- a/api/app/core/models/base.py +++ b/api/app/core/models/base.py @@ -101,12 +101,10 @@ class RedBearModelFactory: extra_body["enable_thinking"] = True if config.thinking_budget_tokens: extra_body["thinking_budget"] = config.thinking_budget_tokens - params["extra_body"] = extra_body # JSON 输出模式 if config.json_output: model_kwargs = params.setdefault("model_kwargs", {}) model_kwargs["response_format"] = {"type": "json_object"} - params["model_kwargs"] = model_kwargs return params if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK, ModelProvider.OLLAMA, ModelProvider.VOLCANO]: @@ -148,11 +146,12 @@ class RedBearModelFactory: extra_body["enable_thinking"] = True if config.thinking_budget_tokens: extra_body["thinking_budget"] = config.thinking_budget_tokens - params["extra_body"] = extra_body # JSON 输出模式 if config.json_output: - params.setdefault("model_kwargs", {}) - params["model_kwargs"]["response_format"] = {"type": "json_object"} + model_kwargs = params.setdefault("model_kwargs", {}) + # VOLCANO 模型不支持 response_format,JSON 输出由 system prompt 注入实现 + if provider != ModelProvider.VOLCANO: + model_kwargs["response_format"] = {"type": "json_object"} return params elif provider == ModelProvider.DASHSCOPE: params = { @@ -172,11 +171,9 @@ class RedBearModelFactory: model_kwargs["incremental_output"] = True if config.thinking_budget_tokens: model_kwargs["thinking_budget"] = config.thinking_budget_tokens - params["model_kwargs"] = model_kwargs if config.json_output: model_kwargs = params.setdefault("model_kwargs", {}) model_kwargs["response_format"] = {"type": "json_object"} - params["model_kwargs"] = model_kwargs return params elif provider == ModelProvider.BEDROCK: # Bedrock 使用 AWS 凭证 @@ -225,8 +222,8 @@ class RedBearModelFactory: } # JSON 输出模式 if config.json_output: - params.setdefault("model_kwargs", {}) - params["model_kwargs"]["response_format"] = {"type": "json_object"} + model_kwargs = params.setdefault("model_kwargs", {}) + model_kwargs["response_format"] = {"type": "json_object"} return params else: raise BusinessException(f"不支持的提供商: {provider}", code=BizCode.PROVIDER_NOT_SUPPORTED) @@ -261,12 +258,13 @@ def get_provider_llm_class(config: RedBearModelConfig, type: ModelType = ModelTy if provider == ModelProvider.VOLCANO: return CompatibleChatOpenAI if provider in [ModelProvider.OPENAI, ModelProvider.XINFERENCE, ModelProvider.GPUSTACK]: - if type == ModelType.LLM: - return OpenAI - elif type == ModelType.CHAT: - return ChatOpenAI - else: - raise BusinessException(f"不支持的模型提供商及类型: {provider}-{type}", code=BizCode.PROVIDER_NOT_SUPPORTED) + return CompatibleChatOpenAI + # if type == ModelType.LLM: + # return OpenAI + # elif type == ModelType.CHAT: + # return CompatibleChatOpenAI + # else: + # raise BusinessException(f"不支持的模型提供商及类型: {provider}-{type}", code=BizCode.PROVIDER_NOT_SUPPORTED) elif provider == ModelProvider.DASHSCOPE: return ChatTongyi elif provider == ModelProvider.OLLAMA: diff --git a/api/app/core/models/compatible_chat.py b/api/app/core/models/compatible_chat.py index 114a3567..218c46e0 100644 --- a/api/app/core/models/compatible_chat.py +++ b/api/app/core/models/compatible_chat.py @@ -8,12 +8,33 @@ from __future__ import annotations from typing import Any, Optional, Union +from langchain_core.messages import BaseMessage from langchain_core.outputs import ChatGenerationChunk, ChatResult from langchain_openai import ChatOpenAI class CompatibleChatOpenAI(ChatOpenAI): - """火山和千问的omni兼容模型,支持深度思考内容(reasoning_content)的流式和非流式透传。""" + """火山和千问的omni兼容模型,支持深度思考内容(reasoning_content)的流式和非流式透传。 + + 同时修复 json_output + tools 同时使用时 langchain_openai 强制走 .parse()/.stream() + 导致 strict 校验报错的问题:有工具时从 payload 中移除 response_format, + 让父类走普通 .create()/.astream() 路径,JSON 输出由 system prompt 指令保证。 + """ + + def _get_request_payload( + self, + input_: list[BaseMessage], + *, + stop: list[str] | None = None, + **kwargs: Any, + ) -> dict: + payload = super()._get_request_payload(input_, stop=stop, **kwargs) + # 有工具时 langchain_openai 检测到 response_format 会切换到 .parse()/.stream() + # 接口,OpenAI SDK 要求此时所有工具必须 strict=True,动态生成的工具不满足。 + # 移除 response_format,让父类走普通路径,JSON 输出由 system prompt 指令保证。 + if payload.get("tools") and "response_format" in payload: + payload.pop("response_format") + return payload def _create_chat_result(self, response: Union[dict, Any], generation_info: Optional[dict] = None) -> ChatResult: result = super()._create_chat_result(response, generation_info) diff --git a/api/app/core/tools/builtin/datetime_tool.py b/api/app/core/tools/builtin/datetime_tool.py index 2fda6b8b..d37e2dcd 100644 --- a/api/app/core/tools/builtin/datetime_tool.py +++ b/api/app/core/tools/builtin/datetime_tool.py @@ -253,9 +253,9 @@ class DateTimeTool(BuiltinTool): return { "datetime": input_value, "timezone": timezone_str, - "timestamp": int(dt.timestamp()) * 1000, + "timestamp": int(dt.timestamp() * 1000), "iso_format": dt.isoformat(), - "result_data": int(dt.timestamp()) * 1000 + "result_data": int(dt.timestamp() * 1000) } def _calculate_datetime(self, kwargs) -> dict: diff --git a/api/app/core/workflow/nodes/llm/node.py b/api/app/core/workflow/nodes/llm/node.py index 664a28fa..db7f1009 100644 --- a/api/app/core/workflow/nodes/llm/node.py +++ b/api/app/core/workflow/nodes/llm/node.py @@ -226,9 +226,12 @@ class LLMNode(BaseNode): self.messages = [{"role": "user", "content": rendered}] # ChatTongyi 要求 messages 含 'json' 字样才能使用 response_format,在 system prompt 中注入 - if (self.typed_config.json_output - and model_info.provider.lower() == ModelProvider.DASHSCOPE - and not model_info.is_omni): + # VOLCANO 模型不支持 response_format,同样需要 system prompt 注入 + need_json_prompt = self.typed_config.json_output and ( + (model_info.provider.lower() == ModelProvider.DASHSCOPE and not model_info.is_omni) + or model_info.provider.lower() == ModelProvider.VOLCANO + ) + if need_json_prompt: system_msg = next((m for m in self.messages if m["role"] == "system"), None) if system_msg: system_msg["content"] += "\n请以JSON格式输出。"