From 6d16b8775bebf1ebc34281c7590b4424ad7d0155 Mon Sep 17 00:00:00 2001 From: aries_ckt <916701291@qq.com> Date: Fri, 15 May 2026 18:15:56 +0800 Subject: [PATCH] fix:budget bug --- .../docs/agents/modules/context-management.md | 11 ++++------- .../agents/modules/context-management.md | 7 ++----- packages/dbgpt-app/src/dbgpt_app/config.py | 9 +++++++-- .../openapi/api_v1/agentic_data_api.py | 12 +++--------- .../src/dbgpt/agent/core/context/budget.py | 13 +++++++++++-- .../src/dbgpt/agent/core/context/manager.py | 6 ++++++ .../agent/core/context/tests/test_budget.py | 8 ++++++++ .../agent/core/context/tests/test_manager.py | 19 +++++++++++++++++-- web/hooks/use-chat.ts | 7 ++++++- web/pages/index.tsx | 7 ++++++- web/utils/react-sse-parser.ts | 4 ++++ 11 files changed, 74 insertions(+), 29 deletions(-) diff --git a/docs/docs/agents/modules/context-management.md b/docs/docs/agents/modules/context-management.md index cfac781b5..fc934ed9c 100644 --- a/docs/docs/agents/modules/context-management.md +++ b/docs/docs/agents/modules/context-management.md @@ -276,8 +276,8 @@ Agent context management can be configured in the application TOML file: ```toml [service.web.agent_context] -# Set to 0 to auto-detect from the selected model metadata. -max_context_tokens = 0 +# Non-positive values fall back to the default context budget. +max_context_tokens = 120000 reserved_tokens = 4096 warning_threshold = 0.70 error_threshold = 0.90 @@ -289,11 +289,8 @@ min_keep_tokens = 10000 max_compact_failures = 3 ``` -When `max_context_tokens` is `0`, DB-GPT tries to read the selected model's -`context_length` from `llm_client.get_model_metadata(model_name)`. If the model -metadata is unavailable, it falls back to the default budget. - -For stable behavior, set `context_length` on each LLM deployment: +For stable behavior, set `context_length` on each LLM deployment when you want +the model metadata to reflect the real provider window: ```toml [[models.llms]] diff --git a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/agents/modules/context-management.md b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/agents/modules/context-management.md index 0318e9f82..b3389da5e 100644 --- a/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/agents/modules/context-management.md +++ b/docs/i18n/zh-CN/docusaurus-plugin-content-docs/current/agents/modules/context-management.md @@ -243,8 +243,7 @@ Observation: ```toml [service.web.agent_context] -# 设置为 0 时,会从当前模型 metadata 自动读取上下文窗口。 -max_context_tokens = 0 +max_context_tokens = 120000 reserved_tokens = 4096 warning_threshold = 0.70 error_threshold = 0.90 @@ -256,9 +255,7 @@ min_keep_tokens = 10000 max_compact_failures = 3 ``` -当 `max_context_tokens` 为 `0` 时,DB-GPT 会尝试通过 `llm_client.get_model_metadata(model_name)` 读取所选模型的 `context_length`。如果模型 metadata 不可用,则回退到默认预算。 - -为了让行为稳定,建议在每个 LLM 部署配置中显式设置 `context_length`: +为了让行为稳定,建议在每个 LLM 部署配置中显式设置 `context_length`,这样模型 metadata 也会反映真实的 provider 上下文窗口: ```toml [[models.llms]] diff --git a/packages/dbgpt-app/src/dbgpt_app/config.py b/packages/dbgpt-app/src/dbgpt_app/config.py index 96da958c7..c7afda253 100644 --- a/packages/dbgpt-app/src/dbgpt_app/config.py +++ b/packages/dbgpt-app/src/dbgpt_app/config.py @@ -1,6 +1,7 @@ from dataclasses import dataclass, field from typing import List, Optional +from dbgpt.agent.core.context.budget import DEFAULT_MAX_CONTEXT_TOKENS from dbgpt.datasource.parameter import BaseDatasourceParameters from dbgpt.model.parameter import ( ModelsDeployParameters, @@ -210,11 +211,11 @@ class AgentContextParameters(BaseParameters): __cfg_type__ = "service" max_context_tokens: Optional[int] = field( - default=0, + default=DEFAULT_MAX_CONTEXT_TOKENS, metadata={ "help": _( "Maximum context-window tokens for agent calls. " - "Set to 0 to auto-detect from model metadata." + "Non-positive values fall back to the default context budget." ) }, ) @@ -255,6 +256,10 @@ class AgentContextParameters(BaseParameters): metadata={"help": _("Consecutive compaction failures before circuit break")}, ) + def __post_init__(self): + if self.max_context_tokens is None or self.max_context_tokens <= 0: + self.max_context_tokens = DEFAULT_MAX_CONTEXT_TOKENS + @dataclass class ServiceWebParameters(BaseParameters): diff --git a/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py b/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py index 3ec68e2f6..b5bf1b1a7 100644 --- a/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py +++ b/packages/dbgpt-app/src/dbgpt_app/openapi/api_v1/agentic_data_api.py @@ -81,12 +81,9 @@ async def _load_context_budget_config( app_config = CFG.SYSTEM_APP.config.configs.get("app_config") web_config = getattr(getattr(app_config, "service", None), "web", None) agent_context = getattr(web_config, "agent_context", None) - max_context_tokens = _value(agent_context, "max_context_tokens", 0) - if max_context_tokens <= 0: - max_context_tokens = ( - await _resolve_model_context_tokens(llm_client, model_name) - or defaults.max_context_tokens - ) + max_context_tokens = _value( + agent_context, "max_context_tokens", defaults.max_context_tokens + ) return ContextBudgetConfig( max_context_tokens=max_context_tokens, warning_threshold=_value( @@ -133,9 +130,6 @@ async def _load_context_budget_config( logger.debug( "Failed to load agent context config; using defaults", exc_info=True ) - metadata_tokens = await _resolve_model_context_tokens(llm_client, model_name) - if metadata_tokens: - return ContextBudgetConfig(max_context_tokens=metadata_tokens) return defaults diff --git a/packages/dbgpt-core/src/dbgpt/agent/core/context/budget.py b/packages/dbgpt-core/src/dbgpt/agent/core/context/budget.py index 10f729724..6cb6ed100 100644 --- a/packages/dbgpt-core/src/dbgpt/agent/core/context/budget.py +++ b/packages/dbgpt-core/src/dbgpt/agent/core/context/budget.py @@ -10,6 +10,8 @@ from dbgpt.model.utils.token_utils import ProxyTokenizerWrapper logger = logging.getLogger(__name__) +DEFAULT_MAX_CONTEXT_TOKENS = 120000 + class TokenState(Enum): """Context budget state levels.""" @@ -42,7 +44,7 @@ class ContextBudgetConfig: def __init__( self, - max_context_tokens: int = 120000, + max_context_tokens: int = DEFAULT_MAX_CONTEXT_TOKENS, warning_threshold: float = 0.70, error_threshold: float = 0.90, critical_threshold: float = 0.95, @@ -53,7 +55,14 @@ class ContextBudgetConfig: truncated_observation_max_chars: int = 200, min_keep_tokens: int = 10000, ): - self.max_context_tokens = max_context_tokens + # `max_context_tokens <= 0` means "auto-detect from model metadata". + # If the caller could not resolve metadata, keep the context window usable + # by falling back to the system default budget instead of leaving it at 0. + self.max_context_tokens = ( + max_context_tokens + if max_context_tokens and max_context_tokens > 0 + else DEFAULT_MAX_CONTEXT_TOKENS + ) self.warning_threshold = warning_threshold self.error_threshold = error_threshold self.critical_threshold = critical_threshold diff --git a/packages/dbgpt-core/src/dbgpt/agent/core/context/manager.py b/packages/dbgpt-core/src/dbgpt/agent/core/context/manager.py index 13ddda19d..48b37af05 100644 --- a/packages/dbgpt-core/src/dbgpt/agent/core/context/manager.py +++ b/packages/dbgpt-core/src/dbgpt/agent/core/context/manager.py @@ -59,6 +59,12 @@ class ContextManager: ) -> None: """Push a context.status event to the registered callback (if any).""" budget = self.tracker.config.effective_budget + if budget <= 0: + logger.debug( + "Skip context status emit because effective budget is non-positive: %d", + budget, + ) + return ratio = round(token_count / budget, 4) if budget > 0 else 1.0 logger.info( "Context status: tokens=%d, budget=%d, ratio=%.4f, state=%s, layer=%s", diff --git a/packages/dbgpt-core/src/dbgpt/agent/core/context/tests/test_budget.py b/packages/dbgpt-core/src/dbgpt/agent/core/context/tests/test_budget.py index cbd628071..25d58c4cc 100644 --- a/packages/dbgpt-core/src/dbgpt/agent/core/context/tests/test_budget.py +++ b/packages/dbgpt-core/src/dbgpt/agent/core/context/tests/test_budget.py @@ -1,6 +1,7 @@ """Tests for context budget tracking.""" from dbgpt.agent.core.context.budget import ( + DEFAULT_MAX_CONTEXT_TOKENS, ContextBudgetConfig, ContextBudgetTracker, TokenState, @@ -42,6 +43,13 @@ class TestContextBudgetConfig: assert cfg.error_threshold == 0.80 assert cfg.critical_threshold == 0.90 + def test_non_positive_max_context_tokens_falls_back_to_default(self): + cfg = ContextBudgetConfig(max_context_tokens=0) + assert cfg.max_context_tokens == DEFAULT_MAX_CONTEXT_TOKENS + + cfg = ContextBudgetConfig(max_context_tokens=-1) + assert cfg.max_context_tokens == DEFAULT_MAX_CONTEXT_TOKENS + class _FakeMsg: """Lightweight message stub for testing.""" diff --git a/packages/dbgpt-core/src/dbgpt/agent/core/context/tests/test_manager.py b/packages/dbgpt-core/src/dbgpt/agent/core/context/tests/test_manager.py index 4640074bc..cf305d5cf 100644 --- a/packages/dbgpt-core/src/dbgpt/agent/core/context/tests/test_manager.py +++ b/packages/dbgpt-core/src/dbgpt/agent/core/context/tests/test_manager.py @@ -94,12 +94,13 @@ class TestContextManagerCompaction: class TestReactiveCompact: - def test_reactive_compact_reduces_messages(self): + @pytest.mark.asyncio + async def test_reactive_compact_reduces_messages(self): cfg = ContextBudgetConfig() mgr = ContextManager(config=cfg) msgs = _build_long_conversation(num_rounds=10) - result = mgr.reactive_compact(msgs) + result = await mgr.reactive_compact(msgs) # Should keep system + last 2 rounds only assert len(result) == 1 + 2 * 3 # system + 2 rounds × 3 msgs @@ -114,3 +115,17 @@ class TestContextManagerRecordsHistory: await mgr.manage_context(msgs, current_round=0) assert len(mgr.tracker.token_history) == 1 assert mgr.tracker.token_history[0] > 0 + + @pytest.mark.asyncio + async def test_skip_status_emit_when_effective_budget_invalid(self): + received = [] + + async def _on_status(status): + received.append(status) + + cfg = ContextBudgetConfig(max_context_tokens=1, reserved_tokens=10) + mgr = ContextManager(config=cfg, on_status_event=_on_status) + + msgs = [_sys("system"), _ai("T"), _human("A"), _obs("data")] + await mgr.manage_context(msgs, current_round=0) + assert received == [] diff --git a/web/hooks/use-chat.ts b/web/hooks/use-chat.ts index 72c16b55a..20979f334 100644 --- a/web/hooks/use-chat.ts +++ b/web/hooks/use-chat.ts @@ -118,12 +118,17 @@ const useChat = ({ queryAgentURL = '/api/v1/chat/completions', app_code }: Props // React-agent format: {"type": "context.status", "used": ..., "budget": ..., ...} const cs = parsedData.context_status ?? (parsedData.type === 'context.status' ? parsedData : null); if (cs) { + const budget = Number(cs.budget ?? 0); + if (!Number.isFinite(budget) || budget <= 0) { + setContextStatus(null); + return; + } // Only show banner when Layer 3 (LLM compression) is active if (cs.compact_layer === 'layer3') { setContextStatus({ state: mapContextState(cs.state || 'normal'), used_tokens: cs.used ?? 0, - max_tokens: cs.budget ?? 0, + max_tokens: budget, usage_percent: (cs.ratio ?? 0) * 100, layer: cs.compact_layer, message: cs.message, diff --git a/web/pages/index.tsx b/web/pages/index.tsx index 401d41327..46138c0b1 100644 --- a/web/pages/index.tsx +++ b/web/pages/index.tsx @@ -1562,6 +1562,11 @@ const Playground: NextPage = () => { return; } if (payload.type === 'context.status') { + const budget = Number(payload.budget ?? 0); + if (!Number.isFinite(budget) || budget <= 0) { + setContextStatus(null); + return; + } const stateMap: Record = { normal: 'OK', warning: 'WARNING', @@ -1572,7 +1577,7 @@ const Playground: NextPage = () => { setContextStatus({ state: stateMap[payload.state] || 'OK', used_tokens: payload.used ?? 0, - max_tokens: payload.budget ?? 0, + max_tokens: budget, usage_percent: (payload.ratio ?? 0) * 100, layer: payload.compact_layer ?? null, }); diff --git a/web/utils/react-sse-parser.ts b/web/utils/react-sse-parser.ts index 9cf6450c4..43b5df0c6 100644 --- a/web/utils/react-sse-parser.ts +++ b/web/utils/react-sse-parser.ts @@ -201,6 +201,10 @@ export class ReActSSEState { } private handleContextStatus(event: SSEContextStatusEvent): void { + if (!Number.isFinite(event.budget) || event.budget <= 0) { + this._contextStatus = null; + return; + } // Map backend state values (lowercase: "normal", "warning", "error", // "critical", "overflow") to frontend display states ("OK", "WARNING", "ERROR"). const stateMap: Record = {