From 6359b3bad522993470a79e5d65e0dfd90260a042 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 20 Oct 2026 13:23:16 +0800 Subject: [PATCH 1/2] fix(agent): propagate context compression failures to runner --- astrbot/core/agent/context/compressor.py | 46 +++---- astrbot/core/agent/context/manager.py | 26 +++- .../agent/runners/tool_loop_agent_runner.py | 29 ++++- astrbot/core/exceptions.py | 24 ++++ tests/agent/test_context_manager.py | 80 +++++++----- tests/test_tool_loop_agent_runner.py | 122 ++++++++++++++++++ 6 files changed, 260 insertions(+), 67 deletions(-) diff --git a/astrbot/core/agent/context/compressor.py b/astrbot/core/agent/context/compressor.py index 32ece15414f..d11174d14e5 100644 --- a/astrbot/core/agent/context/compressor.py +++ b/astrbot/core/agent/context/compressor.py @@ -7,16 +7,6 @@ from ..message import Message from .token_counter import EstimateTokenCounter, TokenCounter -if TYPE_CHECKING: - from astrbot import logger -else: - try: - from astrbot import logger - except ImportError: - import logging - - logger = logging.getLogger("astrbot") - if TYPE_CHECKING: from astrbot.core.provider.provider import Provider @@ -207,8 +197,16 @@ async def __call__(self, messages: list[Message]) -> list[Message]: """Use LLM to generate a summary of the conversation history. Uses round-based splitting to preserve user-assistant turn boundaries. - On LLM failure, returns the original messages unchanged (caller should - fall back to truncation). + + Args: + messages: The original message list. + + Returns: + System messages, the generated summary, and recent exact context. + + Raises: + RuntimeError: If the provider returns an error or an empty summary. + Exception: If the summary request fails. """ from .round_utils import split_into_rounds @@ -271,22 +269,18 @@ async def __call__(self, messages: list[Message]) -> list[Message]: ) log_context_sanitize_stats(sanitize_stats) - # Generate summary. A single attempt is enough: on failure the caller - # falls back to truncation, so retrying (e.g. on an exhausted quota) - # would only hold up the reply. - try: - response = await self.provider.text_chat( - contexts=sanitized_summary_contexts, - request_max_retries=1, + # Try once and let the runner decide whether the uncompressed request fits. + response = await self.provider.text_chat( + contexts=sanitized_summary_contexts, + request_max_retries=1, + ) + if response.role == "err": + raise RuntimeError( + f"LLM context compression failed: {response.completion_text}" ) - summary_content = (response.completion_text or "").strip() - except Exception as e: - logger.error(f"Failed to generate summary: {e}") - return messages - + summary_content = (response.completion_text or "").strip() if not summary_content: - logger.warning("LLM context compression returned an empty summary.") - return messages + raise RuntimeError("LLM context compression returned an empty summary.") # Build result: system messages + summary pair + recent rounds result = _extract_system_messages(messages) diff --git a/astrbot/core/agent/context/manager.py b/astrbot/core/agent/context/manager.py index 84c08cc2375..cb964d214be 100644 --- a/astrbot/core/agent/context/manager.py +++ b/astrbot/core/agent/context/manager.py @@ -1,4 +1,5 @@ from astrbot import logger +from astrbot.core.exceptions import ContextCompressionError from ..message import Message from .compressor import LLMSummaryCompressor, TruncateByTurnsCompressor @@ -50,9 +51,13 @@ async def process( Args: messages: The original message list. + trusted_token_usage: Provider-reported usage for the request context. Returns: The processed message list. + + Raises: + ContextCompressionError: If the compressor fails. """ try: result = messages @@ -77,6 +82,8 @@ async def process( result = await self._run_compression(result, total_tokens) return result + except ContextCompressionError: + raise except Exception as e: logger.error(f"Error during context processing: {e}", exc_info=True) return messages @@ -87,10 +94,9 @@ async def _run_compression( """ Compress/truncate the messages. - If the context still exceeds the limit after compression (for example - because the summary request failed), whole rounds are dropped, oldest - first, until it fits. The latest round is always kept, even when it - alone exceeds the limit. + If successful compression leaves the context above the threshold, drop + whole rounds, oldest first. Always keep the latest round. Compression + failures propagate so the runner can check the model's hard limit. Args: messages: The original message list. @@ -98,10 +104,20 @@ async def _run_compression( Returns: The compressed/truncated message list. + + Raises: + ContextCompressionError: If the compressor fails. """ logger.debug("Compress triggered, starting compression...") - messages = await self.compressor(messages) + try: + messages = await self.compressor(messages) + except Exception as e: + raise ContextCompressionError( + f"Context compression failed: {e}", + messages=messages, + token_count=prev_tokens, + ) from e # double check tokens_after_summary = self.token_counter.count_tokens(messages) diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index f8418fe4205..7bba7c365bf 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -28,7 +28,7 @@ from astrbot.core.agent.message import ImageURLPart, TextPart, ThinkPart from astrbot.core.agent.tool import FunctionTool, ToolSet from astrbot.core.agent.tool_image_cache import tool_image_cache -from astrbot.core.exceptions import EmptyModelOutputError +from astrbot.core.exceptions import ContextCompressionError, EmptyModelOutputError from astrbot.core.message.components import Json from astrbot.core.message.message_event_result import ( MessageChain, @@ -827,12 +827,29 @@ async def step(self): # Process request-time context before sending it to the provider. token_usage = self.req.conversation.token_usage if self.req.conversation else 0 self._simple_print_message_role("[BefCompact]", self.run_context.messages) - processed_messages = await self._await_or_stop( - self.request_context_manager.process( - self.run_context.messages, - trusted_token_usage=token_usage, + try: + processed_messages = await self._await_or_stop( + self.request_context_manager.process( + self.run_context.messages, + trusted_token_usage=token_usage, + ) ) - ) + except ContextCompressionError as e: + max_tokens = self.provider.provider_config.get("max_context_tokens", 0) + # Keep the count from before compression; a fresh local estimate may + # undercount the unchanged history and conceal a context overflow. + if max_tokens > 0 and e.token_count > max_tokens: + self._transition_state(AgentState.ERROR) + self.stats.end_time = time.time() + raise RuntimeError( + f"Context compression failed and the context exceeds the model's " + f"maximum context length ({e.token_count} > {max_tokens} tokens): {e}" + ) from e + logger.warning( + f"{e}. Attempting the uncompressed request " + f"({e.token_count} tokens, context limit: {max_tokens})." + ) + processed_messages = e.messages if processed_messages is None: yield await self._finalize_aborted_step() return diff --git a/astrbot/core/exceptions.py b/astrbot/core/exceptions.py index 76c71af573f..eeb90a2d91b 100644 --- a/astrbot/core/exceptions.py +++ b/astrbot/core/exceptions.py @@ -1,5 +1,10 @@ from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from astrbot.core.agent.message import Message + class AstrBotError(Exception): """Base exception for all AstrBot errors.""" @@ -13,6 +18,25 @@ class EmptyModelOutputError(AstrBotError): """Raised when the model response contains no usable assistant output.""" +class ContextCompressionError(AstrBotError): + """A compression failure with the context and count used to trigger it.""" + + def __init__( + self, message: str, *, messages: list[Message], token_count: int + ) -> None: + """Preserve the request context for the runner's context-limit check. + + Args: + message: Description of the compression failure. + messages: Request messages before the failed compression attempt. + token_count: Token count that triggered compression, including any + provider-reported usage. + """ + super().__init__(message) + self.messages = messages + self.token_count = token_count + + class KnowledgeBaseUploadError(AstrBotError): """Raised when knowledge base upload fails with a user-facing message.""" diff --git a/tests/agent/test_context_manager.py b/tests/agent/test_context_manager.py index 970166f125e..d100eecee0e 100644 --- a/tests/agent/test_context_manager.py +++ b/tests/agent/test_context_manager.py @@ -19,6 +19,7 @@ TextPart, ToolCall, ) +from astrbot.core.exceptions import ContextCompressionError from astrbot.core.provider.entities import LLMResponse from astrbot.core.provider.sources.request_retry import retry_provider_request @@ -81,9 +82,11 @@ async def request(): class CharTokenCounter: - """Counts one token per character of text content.""" + """Use reported usage, or count one token per character of text content.""" def count_tokens(self, messages, trusted_token_usage: int = 0) -> int: + if trusted_token_usage > 0: + return trusted_token_usage return sum(len(m.content) for m in messages if isinstance(m.content, str)) @@ -140,7 +143,7 @@ def test_init_with_truncate_compressor(self): assert isinstance(manager.compressor, TruncateByTurnsCompressor) @pytest.mark.asyncio - async def test_llm_compressor_keeps_history_when_summary_is_empty(self): + async def test_llm_compressor_raises_when_summary_is_empty(self): from astrbot.core.agent.context.compressor import LLMSummaryCompressor provider = MockProvider() @@ -150,13 +153,21 @@ async def test_llm_compressor_keeps_history_when_summary_is_empty(self): compressor = LLMSummaryCompressor(provider=provider, keep_recent_ratio=0.15) # type: ignore[arg-type] messages = self.create_messages(6) - with patch("astrbot.core.agent.context.compressor.logger") as mock_logger: - result = await compressor(messages) + with pytest.raises(RuntimeError, match="returned an empty summary"): + await compressor(messages) - assert result == messages - mock_logger.warning.assert_called_once_with( - "LLM context compression returned an empty summary." + @pytest.mark.asyncio + async def test_llm_compressor_raises_when_provider_returns_error(self): + from astrbot.core.agent.context.compressor import LLMSummaryCompressor + + provider = MockProvider() + provider.text_chat = AsyncMock( + return_value=LLMResponse(role="err", completion_text="summary unavailable") ) + compressor = LLMSummaryCompressor(provider=provider) # type: ignore[arg-type] + + with pytest.raises(RuntimeError, match="summary unavailable"): + await compressor(self.create_messages(6)) @pytest.mark.asyncio async def test_llm_compressor_handles_textpart_content(self): @@ -648,8 +659,8 @@ def _rounds_with_tool_chain(self, latest_request: str) -> list[Message]: ] @pytest.mark.asyncio - async def test_failed_summary_drops_oldest_rounds_until_within_limit(self): - """When the summary fails, whole rounds are dropped until the context fits.""" + async def test_failed_summary_preserves_context_and_usage_for_runner(self): + """A failed summary must not discard history or lower the known count.""" provider = FailingProvider() config = ContextConfig( max_context_tokens=1000, @@ -658,43 +669,52 @@ async def test_failed_summary_drops_oldest_rounds_until_within_limit(self): ) manager = ContextManager(config) messages = self._rounds_with_tool_chain("f" * 20) + original_messages = list(messages) - result = await manager.process(messages) + with patch("astrbot.core.agent.context.manager.logger") as mock_logger: + with pytest.raises(ContextCompressionError) as error: + await manager.process(messages, trusted_token_usage=1_043_200) assert provider.last_text_chat_kwargs["request_max_retries"] == 1 - # Dropping the first round (600 tokens) is enough to get under 820, so - # the round with the tool chain stays intact. - assert result == [messages[0], *messages[3:]] + assert error.value.messages is messages + assert error.value.token_count == 1_043_200 + assert "summary model unavailable" in str(error.value) + assert messages == original_messages + mock_logger.info.assert_not_called() @pytest.mark.asyncio - async def test_fallback_keeps_latest_round_when_it_alone_exceeds_limit( - self, caplog + async def test_successful_summary_keeps_latest_round_when_it_alone_exceeds_limit( + self, ): config = ContextConfig( max_context_tokens=1000, - llm_compress_provider=FailingProvider(), # type: ignore[arg-type] + llm_compress_provider=MockProvider(), # type: ignore[arg-type] custom_token_counter=CharTokenCounter(), ) manager = ContextManager(config) messages = self._rounds_with_tool_chain("f" * 900) - result = await manager.process(messages) + with patch("astrbot.core.agent.context.manager.logger") as mock_logger: + result = await manager.process(messages) assert result == [messages[0], messages[-1]] - assert "only the latest round left" in caplog.text + mock_logger.warning.assert_called_once_with( + "Context still exceeds max tokens with only the latest round left; " + "sending it as is." + ) @pytest.mark.asyncio async def test_llm_summary_is_requested_once(self): - """A failed summary falls back to truncation instead of being retried.""" + """A failed summary raises instead of retrying or returning the history.""" from astrbot.core.agent.context.compressor import LLMSummaryCompressor provider = RateLimitedProvider() compressor = LLMSummaryCompressor(provider=provider, keep_recent_ratio=0.15) # type: ignore[arg-type] messages = self._rounds_with_tool_chain("f" * 20) - result = await compressor(messages) + with pytest.raises(QuotaExhaustedError): + await compressor(messages) - assert result is messages assert provider.attempts == 1 # ==================== Combined Truncation and Compression Tests ==================== @@ -744,9 +764,9 @@ async def test_error_handling_returns_original_messages(self): messages = self.create_messages(5) - # Make compressor raise an exception + # Preserve the existing fallback for failures outside the compressor. with patch.object( - manager.compressor, "__call__", side_effect=Exception("Test error") + manager.token_counter, "count_tokens", side_effect=Exception("Test error") ): result = await manager.process(messages) @@ -754,8 +774,8 @@ async def test_error_handling_returns_original_messages(self): assert result == messages @pytest.mark.asyncio - async def test_error_handling_logs_exception(self): - """Test that errors are logged.""" + async def test_custom_compressor_error_propagates_to_runner(self): + """Custom compressor errors carry the pre-compression count as well.""" config = ContextConfig(max_context_tokens=100) manager = ContextManager(config) @@ -769,12 +789,12 @@ async def test_error_handling_logs_exception(self): manager.compressor = mock_compressor with patch("astrbot.core.agent.context.manager.logger") as mock_logger: - result = await manager.process(messages) + with pytest.raises(ContextCompressionError, match="Test error") as error: + await manager.process(messages) - # Logger error method should be called - assert mock_logger.error.called - # Should return original messages on error - assert result == messages + assert error.value.messages is messages + assert error.value.token_count == 90 + mock_logger.error.assert_not_called() # ==================== Multi-modal Content Tests ==================== diff --git a/tests/test_tool_loop_agent_runner.py b/tests/test_tool_loop_agent_runner.py index ef8e818bb5c..22116ab91f0 100644 --- a/tests/test_tool_loop_agent_runner.py +++ b/tests/test_tool_loop_agent_runner.py @@ -16,6 +16,7 @@ from astrbot.core.agent.hooks import BaseAgentRunHooks from astrbot.core.agent.message import ImageURLPart, Message, TextPart from astrbot.core.agent.run_context import ContextWrapper +from astrbot.core.agent.runners.base import AgentState from astrbot.core.agent.runners.tool_loop_agent_runner import ToolLoopAgentRunner from astrbot.core.agent.tool import FunctionTool, ToolSet from astrbot.core.astr_agent_run_util import run_agent @@ -619,6 +620,127 @@ async def snapshot_context_manager(messages, trusted_token_usage=0): assert "工具执行结果" in tool_messages[0].content +@pytest.mark.asyncio +@pytest.mark.parametrize("streaming", [False, True]) +@pytest.mark.parametrize( + ("reported_tokens", "prompt_size", "context_limit", "exceeds_limit"), + [ + pytest.param(1_048_577, 300, 1_048_576, True, id="reported-over-limit"), + pytest.param(1_043_200, 300, 1_048_576, False, id="issue-10551-under-limit"), + pytest.param(100, 300, 100, False, id="reported-at-limit"), + pytest.param(0, 300, 100, False, id="estimated-under-limit"), + pytest.param(0, 400, 100, True, id="estimated-over-limit"), + ], +) +async def test_failed_compression_only_blocks_requests_over_model_context_limit( + runner, + mock_tool_executor, + mock_hooks, + streaming, + reported_tokens, + prompt_size, + context_limit, + exceeds_limit, +): + """Compression failure preserves history and checks 100%, rather than 82%.""" + provider = CapturingProvider(["text"]) + provider.provider_config["max_context_tokens"] = context_limit + summary_provider = MockFailingProvider() + request = ProviderRequest( + prompt="x" * prompt_size, + contexts=[ + {"role": "user", "content": "Old question"}, + {"role": "assistant", "content": "Old answer"}, + ], + conversation=SimpleNamespace( + cid="compression-test", token_usage=reported_tokens + ), + ) + await runner.reset( + provider=provider, + request=request, + run_context=ContextWrapper(context=None), + tool_executor=mock_tool_executor, + agent_hooks=mock_hooks, + llm_compress_provider=summary_provider, + streaming=streaming, + ) + original_messages = list(runner.run_context.messages) + + if exceeds_limit: + with pytest.raises(RuntimeError, match="exceeds the model's maximum context"): + async for _ in runner.step(): + pass + assert provider.call_count == 0 + assert runner.state == AgentState.ERROR + assert runner.run_context.messages == original_messages + else: + async for _ in runner.step(): + pass + assert provider.call_count == 1 + assert provider.received_contexts[0] == [ + *request.contexts, + {"role": "user", "content": [{"type": "text", "text": request.prompt}]}, + ] + assert runner.run_context.messages[:-1] == original_messages + assert runner.state == AgentState.DONE + + assert summary_provider.call_count == 1 + + +@pytest.mark.asyncio +@pytest.mark.parametrize("streaming", [False, True]) +@pytest.mark.parametrize("custom_error", [None, "Please start a new conversation."]) +async def test_run_agent_handles_compression_overflow_error( + runner, mock_tool_executor, mock_hooks, streaming, custom_error +): + """Deliver a real compression overflow exception through the chat bridge.""" + event = MagicMock() + event.is_stopped.return_value = False + event.get_extra.side_effect = lambda key: ( + custom_error if key == "persona_custom_error_message" else None + ) + provider = CapturingProvider(["text"]) + provider.provider_config["max_context_tokens"] = 100 + summary_provider = MockFailingProvider() + await runner.reset( + provider=provider, + request=ProviderRequest( + prompt="Current question", + contexts=[ + {"role": "user", "content": "Old question"}, + {"role": "assistant", "content": "Old answer"}, + ], + conversation=SimpleNamespace(cid="compression-test", token_usage=101), + ), + run_context=ContextWrapper(context=SimpleNamespace(event=event)), + tool_executor=mock_tool_executor, + agent_hooks=mock_hooks, + llm_compress_provider=summary_provider, + streaming=streaming, + ) + + chains = [chain async for chain in run_agent(runner)] + + if streaming: + assert len(chains) == 1 + error_text = chains[0].get_plain_text() + event.set_result.assert_not_called() + else: + assert chains == [] + event.set_result.assert_called_once() + error_text = event.set_result.call_args.args[0].get_plain_text() + if custom_error: + assert error_text == custom_error + else: + assert "Error Type: RuntimeError" in error_text + assert "maximum context length (101 > 100 tokens)" in error_text + assert runner.state == AgentState.ERROR + assert mock_hooks.agent_done_called + assert provider.call_count == 0 + assert summary_provider.call_count == 1 + + @pytest.mark.asyncio async def test_normal_completion_without_max_step( runner, mock_provider, provider_request, mock_tool_executor, mock_hooks From a14f23a253a130c063656c5f6397ade789d602b8 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Wed, 21 Oct 2026 20:11:23 +0800 Subject: [PATCH 2/2] docs: explain context compression failure troubleshooting --- docs/en/use/context-compress.md | 10 ++++++++-- docs/zh/use/context-compress.md | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/docs/en/use/context-compress.md b/docs/en/use/context-compress.md index 0021bbf1b77..70419076d30 100644 --- a/docs/en/use/context-compress.md +++ b/docs/en/use/context-compress.md @@ -36,7 +36,7 @@ LLM compression adds a model request, which takes time and may incur a charge. S | Recent Context Token Ratio to Keep | Defaults to `0.15`: a budget of 15% of the current context's tokens is used to retain recent content unchanged. The range is `0–0.3`. AstrBot keeps whole turns rather than cutting at an exact token boundary; a positive ratio keeps at least the latest turn. | | Context Compression Instruction | Leave empty for the default prompt. A custom prompt can ask the summary to preserve the task goal, completed steps, key conclusions, file paths, and next actions. | -The default summary focuses on the user's goal, task progress, conclusions, useful tool results, and materials already read so work can continue. If the selected compression model is unavailable, AstrBot tries the current chat model. If it cannot generate a summary, it falls back to removing older turns. +The default summary focuses on the user's goal, task progress, conclusions, useful tool results, and materials already read so work can continue. If the selected compression model is unavailable, AstrBot tries the current chat model. If summary generation fails, AstrBot checks the uncompressed context against the chat model's configured context limit. It attempts the uncompressed request if the context is within that limit, or stops with an error if it exceeds the limit. ## When Context Is Processed @@ -44,7 +44,7 @@ The default summary focuses on the user's goal, task progress, conclusions, usef AstrBot checks context **before every model request**, including follow-up requests after an Agent calls tools. The selected strategy is triggered when context usage exceeds approximately **82%** of the model's context window. -AstrBot checks again after processing. If the context is still too long, it removes complete turns from the oldest first while keeping the latest turn. Tool calls and their results are handled together to avoid leaving incomplete call records. +AstrBot checks again after successful compression. If the context is still too long, it removes complete turns from the oldest first while keeping the latest turn. Tool calls and their results are handled together to avoid leaving incomplete call records. If the latest turn alone is too large, automatic compression may still be unable to fit the request into the model's window. Reduce the size of the input or tool output, or choose a model with a larger context window. @@ -82,6 +82,12 @@ Check that you edited the profile used by the current session and saved it. Toke Check the window sizes of both the chat model and the compression model. One large message, file, or tool result can make the latest turn exceed the limit on its own. Compression does not discard the current task indefinitely; reduce the input size and try again. +### What If Compression Fails and the Context Exceeds the Limit? + +An error containing `Context compression failed and the context exceeds the model's maximum context length` means summary generation failed and the uncompressed context exceeds the chat model's configured context limit. AstrBot therefore stops the current request. + +Check the logs for the compression failure's cause; the underlying exception is also included at the end of the error message. Check the compression model's service availability, quota, and context window. Try selecting an available model with a large enough context window under **Model Provider ID for Context Compression**, save the configuration, and retry. + ### How Do I Start Fresh? Send `/reset` to clear the current conversation context or `/new` to create a new conversation. See [Built-in Commands](./command.md) for commands and wake-prefix settings. diff --git a/docs/zh/use/context-compress.md b/docs/zh/use/context-compress.md index 00cd809253d..3c62c7ee6d2 100644 --- a/docs/zh/use/context-compress.md +++ b/docs/zh/use/context-compress.md @@ -36,7 +36,7 @@ LLM 压缩会额外调用模型,因此会增加一次请求的时间和费用 | 压缩时保留最近上下文比例 | 默认 `0.15`,表示以当前上下文 Token 数的 15% 为预算保留最近的原始内容,范围为 `0–0.3`。按完整对话轮次保留,不会精确切到某个 Token;比例大于 0 时至少保留最后一轮。 | | 上下文压缩提示词 | 留空使用默认提示词。需要定制摘要时,可以要求保留任务目标、已完成的步骤、关键结论、文件路径和下一步计划。 | -默认摘要会关注用户目标、任务进度、关键结论、工具结果以及读过的资料,方便继续后续工作。如果压缩模型不可用,AstrBot 会尝试使用当前聊天模型;如果无法生成摘要,则回退为截断旧对话。 +默认摘要会关注用户目标、任务进度、关键结论、工具结果以及读过的资料,方便继续后续工作。如果压缩模型不可用,AstrBot 会尝试使用当前聊天模型;如果摘要生成失败,AstrBot 会检查压缩前的上下文是否超过聊天模型配置的窗口上限,未超过时尝试使用未压缩的上下文继续请求,超过时停止并报错。 ## 什么情况下会处理上下文 @@ -44,7 +44,7 @@ LLM 压缩会额外调用模型,因此会增加一次请求的时间和费用 AstrBot 会在**每次向模型发送请求之前**检查上下文,包括 Agent 调用工具后再次请求模型的步骤。当上下文 Token 用量超过模型窗口的约 **82%** 时,会触发所选处理方式。 -处理后会再次检查。如果仍然过长,AstrBot 会从最旧的完整对话轮次开始移除内容,并保留最后一轮。工具调用与对应结果会一起处理,避免只剩下一半的调用记录。 +压缩成功后会再次检查。如果仍然过长,AstrBot 会从最旧的完整对话轮次开始移除内容,并保留最后一轮。工具调用与对应结果会一起处理,避免只剩下一半的调用记录。 如果最新一轮本身就过大,自动压缩仍可能无法使请求符合模型限制。此时需要减少单次输入或工具返回内容,或者使用上下文窗口更大的模型。 @@ -82,6 +82,12 @@ AstrBot 会在**每次向模型发送请求之前**检查上下文,包括 Agen 检查聊天模型和压缩模型的窗口大小是否准确。单次长消息、大文件或工具返回的内容可能使最新一轮独自超限,自动压缩不会无限删去当前任务。减少本次输入规模后重试。 +### 出现上下文压缩失败且超限的报错怎么办? + +如果报错包含 `Context compression failed and the context exceeds the model's maximum context length`,表示摘要压缩失败,且压缩前的上下文已超过聊天模型配置的窗口上限,AstrBot 因此停止本次请求。 + +请先检查日志中的压缩失败原因,具体异常也会附在报错末尾。确认压缩模型的服务可用性、额度和上下文窗口是否满足要求。可以在「用于上下文压缩的模型提供商 ID」中更换为可用且窗口足够大的模型,保存配置后重试。 + ### 想从头开始一段对话怎么办? 在聊天中使用 `/reset` 清空当前对话上下文,或使用 `/new` 创建新对话。指令及唤醒前缀说明见[内置指令](./command.md)。