问题描述
在 v4.28.1 上,一个长期对话在上下文变长后,机器人开始持续报错,最终连纯文本消息也无法处理:
LLM 响应错误: All chat models failed: MemoryError:
随后同一会话中,用户发送任意纯文本(如「好多问题」)都会失败,且错误信息为空:
[2026-09-15 10:29:56.833] [Core][ERRO][v4.28.1] [agent_sub_stages.internal:541]: Error occurred while processing agent:
对用户可见的两条报错文本分别是:
LLM 响应错误: All chat models failed: MemoryError:
Error occurred while processing agent request:
MemoryError 由 AstrBot 自身进程抛出(不是服务商返回的),说明进程内存被耗尽。而发送纯文本也会失败这一事实说明问题不在当前消息,而在每轮都要重建的会话历史。
复核 v4.28.1 代码后,确认历史中的 base64 图片会持续累积,且现有的两道"安全网"都拦不住它。
1. 图片体积对 token 计数完全不可见
astrbot/core/agent/context/token_counter.py:34:
IMAGE_TOKEN_ESTIMATE = 765
EstimateTokenCounter.count_tokens() 对每个 ImageURLPart 一律累加该常量:
elif isinstance(part, ImageURLPart):
total += IMAGE_TOKEN_ESTIMATE
一张 3 KB 的图与一张 30 MB 的图,token 计数完全相同。上下文压缩的触发条件依赖该计数,因此图片再大也不会触发压缩。
2. 轮数裁剪默认关闭
astrbot/core/config/agent_runner.py:114:
max_turns = compression_config.get("max_turns", -1)
astrbot/core/agent/context/manager.py:60:
# 1. 基于轮次的截断 (Enforce max turns)
if self.config.enforce_max_turns != -1:
result = self.truncator.truncate_by_turns(...)
默认值为 -1(不限制),该整段被跳过。于是两道限制同时失效:轮数不裁剪,token 压缩又看不见图片体积。
3. base64 确实会进入持久化历史
astrbot/core/provider/entities.py:192 的 ProviderRequest.assemble_context() 把图片引用内联为 base64 data URI:
# 3. Read image references without resizing or transcoding.
...
image_data = await resolve_image_ref_to_base64_data(image_url)
content_blocks.append(
{"type": "image_url", "image_url": {"url": image_data.to_data_url()}},
)
同一函数中的注释明确说明这些 data URI 也会进入持久化历史:
# Capture bytes before event cleanup. Extra image paths must also
# reach providers and persisted history as portable data URIs.
astrbot/core/agent/runners/tool_loop_agent_runner.py:339/344/351 调用该方法生成运行期消息,随后由 dump_messages_with_checkpoints(all_messages) 写入会话历史;下一轮又由 astrbot/core/astr_main_agent.py:1358 原样读回:
req.contexts = json.loads(req.conversation.history)
4. 放大效应
每个请求路径上,同一份历史会被反复复制:SQLite 读出字符串 → json.loads 构建对象图 → 序列化为 HTTP 请求体 → provider SDK 再包一层。一份数十 MB 的 base64 历史,峰值内存可达数倍。
5. 加重问题的几处实现
-
内存耗尽被当作模型故障重试。 tool_loop_agent_runner.py:625 使用 except Exception 捕获异常后 continue,会对同一份超大 payload 依次尝试全部 fallback provider,越重试越吃内存;最终在 :642 抛出 All chat models failed: {type}: {e}。
-
错误信息丢失异常类型。 agent_sub_stages/internal.py:541:
logger.error(f"Error occurred while processing agent: {e}")
以及紧随其后的 error_text 同样只拼 {e}。MemoryError() 无参数、str() 为空串,用户因此只看到 Error occurred while processing agent request:,完全无法判断是内存问题。
-
WebUI 预览会原样返回整份历史。 astrbot/dashboard/services/conversation_service.py:143 的 get_conversation_detail() 直接返回 conversation.history,无分页与体积控制;超大会话会触发 astrbot/dashboard/api/app.py:178 的全局兜底,表现为 Internal server error。
-
Dashboard 与机器人在同一进程。 astrbot/core/core_lifecycle.py:358 用 asyncio.gather(*self.curr_tasks) 同时运行包括 Hypercorn 在内的所有任务,因此在 WebUI 打开超大会话可以把机器人本体一起拖垮。
补充一点:is_recoverable_image_error() 的设计是正确的——它明确把资源耗尽排除在"可跳过"之外(OSError + errno.ENOMEM 时返回 False),但上层又用 except Exception 把 MemoryError 捞了回来。
如何复现
- 使用内置 Agent,保持
agent_runner.config.compression 的默认值(max_turns 未设置)。
- 在同一会话中多轮发送图片(尤其大图),穿插纯文本消息。
- 观察
req.contexts:每轮都会携带此前全部轮次的图片 data URI,且长度持续增长。
- 当累计体积超过可用内存时,任意消息(包括纯文本)都会抛出
MemoryError。
本机观测:data/data_v4.db 体积 141 MB,文件修改时间为 2026-09-15 09:18:29,而首次 MemoryError 出现在 09:19:51。单会话的体积明细仍在统计中。
期望行为
- 图片占用应纳入上下文体积核算,例如按 data URI 实际长度参与 token 估算,或提供独立的字节预算,使压缩/裁剪能够及时触发。
max_turns 建议有非无限的安全默认值,或至少在启用图片的场景下生效。
- 资源耗尽不应进入 fallback 重试:
except Exception 应排除 MemoryError 等资源类异常,直接向上抛出。
- 错误信息应保留异常类型:
{e} 对无参异常会得到空串,建议改为 {type(e).__name__}: {e} 或等价形式。
- WebUI 会话预览应做分页或体积限制,避免单个超大会话影响服务端。
相关 issue
环境
- AstrBot 版本:v4.28.1
- 操作系统:Windows
- 部署方式:Windows 源码部署(uv / venv)
- 消息平台适配器:Onebot v11(NapCat)
错误日志
LLM 响应错误: All chat models failed: MemoryError:
[2026-09-15 10:29:56.787] [Core] [DBUG] [agent_sub_stages.internal:193]: ready to request llm provider
[2026-09-15 10:29:56.787] [Core] [DBUG] [agent_sub_stages.internal:221]: acquired session lock for llm request
[2026-09-15 10:29:56.833] [Core][ERRO][v4.28.1] [agent_sub_stages.internal:541]: Error occurred while processing agent:
检查清单
问题描述
在 v4.28.1 上,一个长期对话在上下文变长后,机器人开始持续报错,最终连纯文本消息也无法处理:
随后同一会话中,用户发送任意纯文本(如「好多问题」)都会失败,且错误信息为空:
对用户可见的两条报错文本分别是:
MemoryError由 AstrBot 自身进程抛出(不是服务商返回的),说明进程内存被耗尽。而发送纯文本也会失败这一事实说明问题不在当前消息,而在每轮都要重建的会话历史。复核 v4.28.1 代码后,确认历史中的 base64 图片会持续累积,且现有的两道"安全网"都拦不住它。
1. 图片体积对 token 计数完全不可见
astrbot/core/agent/context/token_counter.py:34:EstimateTokenCounter.count_tokens()对每个ImageURLPart一律累加该常量:一张 3 KB 的图与一张 30 MB 的图,token 计数完全相同。上下文压缩的触发条件依赖该计数,因此图片再大也不会触发压缩。
2. 轮数裁剪默认关闭
astrbot/core/config/agent_runner.py:114:astrbot/core/agent/context/manager.py:60:默认值为
-1(不限制),该整段被跳过。于是两道限制同时失效:轮数不裁剪,token 压缩又看不见图片体积。3. base64 确实会进入持久化历史
astrbot/core/provider/entities.py:192的ProviderRequest.assemble_context()把图片引用内联为 base64 data URI:同一函数中的注释明确说明这些 data URI 也会进入持久化历史:
astrbot/core/agent/runners/tool_loop_agent_runner.py:339/344/351调用该方法生成运行期消息,随后由dump_messages_with_checkpoints(all_messages)写入会话历史;下一轮又由astrbot/core/astr_main_agent.py:1358原样读回:4. 放大效应
每个请求路径上,同一份历史会被反复复制:SQLite 读出字符串 →
json.loads构建对象图 → 序列化为 HTTP 请求体 → provider SDK 再包一层。一份数十 MB 的 base64 历史,峰值内存可达数倍。5. 加重问题的几处实现
内存耗尽被当作模型故障重试。
tool_loop_agent_runner.py:625使用except Exception捕获异常后continue,会对同一份超大 payload 依次尝试全部 fallback provider,越重试越吃内存;最终在:642抛出All chat models failed: {type}: {e}。错误信息丢失异常类型。
agent_sub_stages/internal.py:541:以及紧随其后的
error_text同样只拼{e}。MemoryError()无参数、str()为空串,用户因此只看到Error occurred while processing agent request:,完全无法判断是内存问题。WebUI 预览会原样返回整份历史。
astrbot/dashboard/services/conversation_service.py:143的get_conversation_detail()直接返回conversation.history,无分页与体积控制;超大会话会触发astrbot/dashboard/api/app.py:178的全局兜底,表现为Internal server error。Dashboard 与机器人在同一进程。
astrbot/core/core_lifecycle.py:358用asyncio.gather(*self.curr_tasks)同时运行包括 Hypercorn 在内的所有任务,因此在 WebUI 打开超大会话可以把机器人本体一起拖垮。补充一点:
is_recoverable_image_error()的设计是正确的——它明确把资源耗尽排除在"可跳过"之外(OSError+errno.ENOMEM时返回False),但上层又用except Exception把MemoryError捞了回来。如何复现
agent_runner.config.compression的默认值(max_turns未设置)。req.contexts:每轮都会携带此前全部轮次的图片 data URI,且长度持续增长。MemoryError。本机观测:
data/data_v4.db体积 141 MB,文件修改时间为2026-09-15 09:18:29,而首次MemoryError出现在09:19:51。单会话的体积明细仍在统计中。期望行为
max_turns建议有非无限的安全默认值,或至少在启用图片的场景下生效。except Exception应排除MemoryError等资源类异常,直接向上抛出。{e}对无参异常会得到空串,建议改为{type(e).__name__}: {e}或等价形式。相关 issue
Internal server error与进程级内存耗尽。环境
错误日志
检查清单