diff --git a/configs/dbgpt-proxy-deepseek.toml b/configs/dbgpt-proxy-deepseek.toml index 3ffa960d0..95923f0ed 100644 --- a/configs/dbgpt-proxy-deepseek.toml +++ b/configs/dbgpt-proxy-deepseek.toml @@ -23,9 +23,12 @@ persist_path = "pilot/data" # Model Configurations [models] [[models.llms]] -name = "deepseek-reasoner" +name = "deepseek-v4-pro" +# name = "deepseek-reasoner" # name = "deepseek-chat" provider = "proxy/deepseek" +# Disable V4-Pro thinking mode so ReAct responses stay parseable. +thinking_enabled = false api_key = "your_deepseek_api_key" [[models.embeddings]] diff --git a/docs/docs/getting-started/providers/deepseek.md b/docs/docs/getting-started/providers/deepseek.md index 5fd08ec13..5a69b36aa 100644 --- a/docs/docs/getting-started/providers/deepseek.md +++ b/docs/docs/getting-started/providers/deepseek.md @@ -45,9 +45,11 @@ Edit `configs/dbgpt-proxy-deepseek.toml`: ```toml [models] [[models.llms]] -name = "deepseek-reasoner" +name = "deepseek-v4-pro" provider = "proxy/deepseek" api_key = "your-deepseek-api-key" +# Disable V4-Pro thinking mode for strict ReAct output parsing. +thinking_enabled = false [[models.embeddings]] name = "BAAI/bge-large-zh-v1.5" @@ -60,9 +62,14 @@ provider = "hf" | Model | Config name | Notes | |---|---|---| +| DeepSeek-V4-Pro | `deepseek-v4-pro` | 1M context, advanced reasoning, coding, and agent tasks | | DeepSeek-R1 | `deepseek-reasoner` | Strong reasoning, chain-of-thought | | DeepSeek-V3 | `deepseek-chat` | General purpose chat | +For ReAct agents, keep `thinking_enabled = false` with `deepseek-v4-pro`. DeepSeek +V4-Pro enables thinking mode by default, which can add reasoning blocks before the +strict `Thought/Action/Action Input` response format. + ## Start the server ```bash diff --git a/docs/docs/getting-started/troubleshooting/llm.md b/docs/docs/getting-started/troubleshooting/llm.md index 9d6ffff7a..8f364811a 100644 --- a/docs/docs/getting-started/troubleshooting/llm.md +++ b/docs/docs/getting-started/troubleshooting/llm.md @@ -44,7 +44,7 @@ export OPENAI_API_KEY="sk-your-actual-key" | Provider | Example Name | |---|---| | OpenAI | `chatgpt_proxyllm`, `gpt-4o` | -| DeepSeek | `deepseek-chat`, `deepseek-reasoner` | +| DeepSeek | `deepseek-v4-pro`, `deepseek-chat`, `deepseek-reasoner` | | Ollama | `qwen2.5:latest` (must be pulled first) | | HuggingFace | `THUDM/glm-4-9b-chat-hf` | diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/getting-started/providers/deepseek.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/getting-started/providers/deepseek.md index ac9643f4e..a9f779c33 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/getting-started/providers/deepseek.md +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/getting-started/providers/deepseek.md @@ -45,9 +45,11 @@ uv sync --all-packages \ ```toml [models] [[models.llms]] -name = "deepseek-reasoner" +name = "deepseek-v4-pro" provider = "proxy/deepseek" api_key = "your-deepseek-api-key" +# 为严格的 ReAct 输出解析关闭 V4-Pro 思考模式。 +thinking_enabled = false [[models.embeddings]] name = "BAAI/bge-large-zh-v1.5" @@ -60,9 +62,14 @@ provider = "hf" | 模型 | 配置名 | 说明 | |---|---|---| +| DeepSeek-V4-Pro | `deepseek-v4-pro` | 1M 上下文,适合高级推理、代码与 Agent 任务 | | DeepSeek-R1 | `deepseek-reasoner` | 推理能力强,适合复杂思考任务 | | DeepSeek-V3 | `deepseek-chat` | 通用聊天与问答 | +ReAct Agent 使用 `deepseek-v4-pro` 时建议保留 `thinking_enabled = false`。 +DeepSeek V4-Pro 默认开启思考模式,可能在严格的 +`Thought/Action/Action Input` 输出格式前产生推理块,导致解析失败。 + ## 启动服务 ```bash diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/getting-started/troubleshooting/llm.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/getting-started/troubleshooting/llm.md index 3d93e3321..8f876fbda 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/getting-started/troubleshooting/llm.md +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/getting-started/troubleshooting/llm.md @@ -44,7 +44,7 @@ export OPENAI_API_KEY="sk-your-actual-key" | Provider | 示例名称 | |---|---| | OpenAI | `chatgpt_proxyllm`, `gpt-4o` | -| DeepSeek | `deepseek-chat`, `deepseek-reasoner` | +| DeepSeek | `deepseek-v4-pro`, `deepseek-chat`, `deepseek-reasoner` | | Ollama | `qwen2.5:latest`(需先拉取) | | HuggingFace | `THUDM/glm-4-9b-chat-hf` | diff --git a/packages/dbgpt-core/src/dbgpt/agent/util/react_parser.py b/packages/dbgpt-core/src/dbgpt/agent/util/react_parser.py index d713f43da..d20b5e957 100644 --- a/packages/dbgpt-core/src/dbgpt/agent/util/react_parser.py +++ b/packages/dbgpt-core/src/dbgpt/agent/util/react_parser.py @@ -93,9 +93,7 @@ class ReActOutputParser: def _find_prefix_matches(self, text: str, escaped_prefix: str) -> List[re.Match]: """Find line-start ReAct prefix matches outside markdown code fences.""" - pattern = re.compile( - self._prefix_line_pattern(escaped_prefix), re.MULTILINE - ) + pattern = re.compile(self._prefix_line_pattern(escaped_prefix), re.MULTILINE) fence_spans = self._markdown_fence_spans(text) return [ match @@ -196,9 +194,7 @@ class ReActOutputParser: text = self._normalize_react_text(text).strip() # Find all line-start instances of the thought prefix outside code fences. - thought_matches = self._find_prefix_matches( - text, self.thought_prefix_escaped - ) + thought_matches = self._find_prefix_matches(text, self.thought_prefix_escaped) if not thought_matches: return [] @@ -272,12 +268,8 @@ class ReActOutputParser: self.action_reason_prefix_escaped ) action_line = self._prefix_line_pattern(self.action_prefix_escaped) - action_input_line = self._prefix_line_pattern( - self.action_input_prefix_escaped - ) - observation_line = self._prefix_line_pattern( - self.observation_prefix_escaped - ) + action_input_line = self._prefix_line_pattern(self.action_input_prefix_escaped) + observation_line = self._prefix_line_pattern(self.observation_prefix_escaped) thought_match = re.search( rf"{thought_line}(.*?)(?={phase_line}|{action_intention_line}|" diff --git a/packages/dbgpt-core/src/dbgpt/agent/util/tests/test_react_parser.py b/packages/dbgpt-core/src/dbgpt/agent/util/tests/test_react_parser.py index c4b3af10a..0c4336067 100644 --- a/packages/dbgpt-core/src/dbgpt/agent/util/tests/test_react_parser.py +++ b/packages/dbgpt-core/src/dbgpt/agent/util/tests/test_react_parser.py @@ -55,10 +55,7 @@ Action Input: {"sql": "select distinct major_type from assets"}""" assert len(steps) == 1 assert steps[0].thought == "I need to inspect the database schema." assert steps[0].action_intention == "Query the available asset categories." - assert ( - steps[0].action_reason - == "The user asked about assets by major type." - ) + assert steps[0].action_reason == "The user asked about assets by major type." assert steps[0].action == "query_db" assert steps[0].action_input == { "sql": "select distinct major_type from assets" @@ -486,9 +483,7 @@ Action Input: {"result": "网络设备资产共 30 条"}""" assert len(all_steps) == 2 assert len(current_steps) == 1 assert current_steps[0].action == "query_db" - assert current_steps[0].action_input == { - "sql": "select count(*) from assets" - } + assert current_steps[0].action_input == {"sql": "select count(*) from assets"} def test_react_markers_inside_action_input_code_fence_are_not_steps(self): """Do not split Action Input code fences that mention ReAct labels.""" diff --git a/packages/dbgpt-core/src/dbgpt/model/proxy/llms/deepseek.py b/packages/dbgpt-core/src/dbgpt/model/proxy/llms/deepseek.py index 8dc392722..cc2263ef3 100644 --- a/packages/dbgpt-core/src/dbgpt/model/proxy/llms/deepseek.py +++ b/packages/dbgpt-core/src/dbgpt/model/proxy/llms/deepseek.py @@ -1,8 +1,18 @@ import os from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Dict, Optional, Type, Union, cast +from typing import ( + TYPE_CHECKING, + Any, + AsyncIterator, + Dict, + List, + Optional, + Type, + Union, + cast, +) -from dbgpt.core import ModelMetadata +from dbgpt.core import ModelMetadata, ModelOutput from dbgpt.core.awel.flow import ( TAGS_ORDER_HIGH, ResourceCategory, @@ -57,6 +67,16 @@ class DeepSeekDeployModelParameters(OpenAICompatibleDeployModelParameters): }, ) + thinking_enabled: Optional[bool] = field( + default=None, + metadata={ + "help": _( + "Whether to enable DeepSeek thinking mode. If None, thinking is " + "disabled for deepseek-v4-pro to keep ReAct output parseable." + ), + }, + ) + async def deepseek_generate_stream( model: ProxyModel, tokenizer, params, device, context_len=2048 @@ -88,6 +108,7 @@ class DeepseekLLMClient(OpenAILLMClient): context_length: Optional[int] = None, openai_client: Optional["ClientType"] = None, openai_kwargs: Optional[Dict[str, Any]] = None, + thinking_enabled: Optional[bool] = None, **kwargs, ): api_base = ( @@ -96,7 +117,9 @@ class DeepseekLLMClient(OpenAILLMClient): api_key = api_key or os.getenv("DEEPSEEK_API_KEY") model = model or _DEFAULT_MODEL if not context_length: - if "deepseek-chat" in model: + if "deepseek-v4" in model: + context_length = 1024 * 1024 + elif "deepseek-chat" in model: context_length = 1024 * 32 elif "deepseek-coder" in model: context_length = 1024 * 16 @@ -109,6 +132,7 @@ class DeepseekLLMClient(OpenAILLMClient): "Deepseek API key is required, please set 'DEEPSEEK_API_KEY' in " "environment variable or pass it to the client." ) + self._thinking_enabled = thinking_enabled super().__init__( api_key=api_key, api_base=api_base, @@ -124,6 +148,69 @@ class DeepseekLLMClient(OpenAILLMClient): **kwargs, ) + @classmethod + def new_client( + cls, + model_params: DeepSeekDeployModelParameters, + default_executor=None, + ) -> "DeepseekLLMClient": + """Create a new client with the model parameters.""" + return cls( + api_key=model_params.api_key, + api_base=model_params.api_base, + api_type=model_params.api_type, + api_version=model_params.api_version, + model=model_params.real_provider_model_name, + proxy=model_params.http_proxy, + model_alias=model_params.real_provider_model_name, + context_length=model_params.context_length, + thinking_enabled=model_params.thinking_enabled, + ) + + def _build_request(self, request, stream: Optional[bool] = False) -> Dict[str, Any]: + payload = super()._build_request(request, stream) + model = payload.get("model") or self.default_model + extra_body = payload.get("extra_body") or {} + if "thinking" not in extra_body and self._should_set_thinking(model): + thinking_enabled = self._thinking_enabled is True + extra_body["thinking"] = { + "type": "enabled" if thinking_enabled else "disabled" + } + payload["extra_body"] = extra_body + return payload + + def _should_set_thinking(self, model: str) -> bool: + return self._thinking_enabled is not None or model == "deepseek-v4-pro" + + def _is_thinking_disabled(self, payload: Dict[str, Any]) -> bool: + thinking = (payload.get("extra_body") or {}).get("thinking") or {} + return thinking.get("type") == "disabled" + + def _drop_thinking_if_disabled( + self, output: ModelOutput, payload: Dict[str, Any] + ) -> ModelOutput: + if not self._is_thinking_disabled(payload) or not output.has_thinking: + return output + text = output.text if output.has_text else "" + return ModelOutput.build( + text=text, + usage=output.usage, + finish_reason=output.finish_reason, + metrics=output.metrics, + ) + + async def generate_v1( + self, messages: List[Dict[str, Any]], payload: Dict[str, Any] + ) -> ModelOutput: + output = await super().generate_v1(messages, payload) + return self._drop_thinking_if_disabled(output, payload) + + async def generate_stream_v1( + self, messages: List[Dict[str, Any]], payload: Dict[str, Any] + ) -> AsyncIterator[ModelOutput]: + async for output in super().generate_stream_v1(messages, payload): + yield self._drop_thinking_if_disabled(output, payload) + def check_sdk_version(self, version: str) -> None: if not version >= "1.0": raise ValueError( @@ -154,6 +241,14 @@ class DeepseekLLMClient(OpenAILLMClient): register_proxy_model_adapter( DeepseekLLMClient, supported_models=[ + ModelMetadata( + model="deepseek-v4-pro", + context_length=1024 * 1024, + max_output_length=384 * 1024, + description="DeepSeek-V4-Pro by DeepSeek", + link="https://api-docs.deepseek.com/news/news260424", + function_calling=True, + ), ModelMetadata( model="deepseek-chat", context_length=64 * 1024, diff --git a/packages/dbgpt-core/src/dbgpt/model/proxy/tests/test_deepseek.py b/packages/dbgpt-core/src/dbgpt/model/proxy/tests/test_deepseek.py new file mode 100644 index 000000000..0a3e45bfb --- /dev/null +++ b/packages/dbgpt-core/src/dbgpt/model/proxy/tests/test_deepseek.py @@ -0,0 +1,94 @@ +"""Tests for DeepSeek proxy LLM client.""" + +import os +from unittest.mock import patch + +from dbgpt.core import ModelMessage, ModelOutput, ModelRequest +from dbgpt.model.proxy.llms.deepseek import DeepseekLLMClient + + +class TestDeepSeekModelRegistration: + """DeepSeek provider/model adapter resolution.""" + + def test_supported_models_contains_v4_pro_metadata(self): + from dbgpt.model.adapter.base import get_model_adapter + + adapter = get_model_adapter("proxy/deepseek", "deepseek-v4-pro") + assert adapter is not None + + metadata = {metadata.model: metadata for metadata in adapter.supported_models()} + v4_pro = metadata["deepseek-v4-pro"] + + assert v4_pro.context_length == 1024 * 1024 + assert v4_pro.max_output_length == 384 * 1024 + assert v4_pro.function_calling is True + + +class TestDeepSeekContextLength: + """DeepSeek model context length defaults.""" + + @patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"}) + def test_v4_pro_uses_one_million_context(self): + client = DeepseekLLMClient( + model="deepseek-v4-pro", + openai_client=_FakeOpenAIClient(), + ) + + assert client.context_length == 1024 * 1024 + assert client._context_length == 1024 * 1024 + + +class TestDeepSeekThinkingMode: + """DeepSeek V4 thinking mode defaults for parse-sensitive agents.""" + + def _request(self): + return ModelRequest( + model="deepseek-v4-pro", + messages=[ModelMessage(role="user", content="hi")], + ) + + @patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"}) + def test_v4_pro_disables_thinking_by_default(self): + client = DeepseekLLMClient( + model="deepseek-v4-pro", + openai_client=_FakeOpenAIClient(), + ) + + payload = client._build_request(self._request()) + + assert payload["extra_body"]["thinking"] == {"type": "disabled"} + + @patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"}) + def test_v4_pro_can_enable_thinking_explicitly(self): + client = DeepseekLLMClient( + model="deepseek-v4-pro", + thinking_enabled=True, + openai_client=_FakeOpenAIClient(), + ) + + payload = client._build_request(self._request()) + + assert payload["extra_body"]["thinking"] == {"type": "enabled"} + + @patch.dict(os.environ, {"DEEPSEEK_API_KEY": "test-key"}) + def test_disabled_thinking_drops_reasoning_content(self): + client = DeepseekLLMClient( + model="deepseek-v4-pro", + openai_client=_FakeOpenAIClient(), + ) + output = ModelOutput.build( + text="Thought: continue\nAction: terminate\nAction Input: {}", + thinking="private reasoning", + ) + payload = client._build_request(self._request()) + + sanitized = client._drop_thinking_if_disabled(output, payload) + + assert sanitized.has_thinking is False + assert ( + sanitized.text == "Thought: continue\nAction: terminate\nAction Input: {}" + ) + + +class _FakeOpenAIClient: + default_headers = {}