@@ -147,12 +147,35 @@ async def filter_empty_message(
147147 async def inject_memory_context (
148148 self , event : AstrMessageEvent , req : ProviderRequest
149149 ) -> None :
150- """在每次 LLM 请求前将三层记忆注入 system_prompt。"""
150+ """在每次 LLM 请求前将三层记忆注入 system_prompt,并在上下文过大时自动截断。"""
151+ import json as _json
152+
153+ # 1. 注入记忆上下文
151154 block = _build_context_block ()
152155 if block :
153156 req .system_prompt = (req .system_prompt or "" ) + block
154157 logger .debug ("[MemorySystem] 已注入记忆上下文(%d chars)" , len (block ))
155158
159+ # 2. 预防性历史截断:如果 history 消息总 chars 超过阈值,自动截断旧消息
160+ AUTO_COMPACT_CHARS = 200_000 # 约 50k tokens 时自动截断(保守阈值)
161+ KEEP_RECENT_TURNS = 20
162+ try :
163+ if req .contexts :
164+ contexts_str = _json .dumps (req .contexts , ensure_ascii = False )
165+ if len (contexts_str ) > AUTO_COMPACT_CHARS :
166+ from astrbot .core .agent .context .truncator import ContextTruncator
167+ from astrbot .core .agent .message import Message as _Msg
168+ msgs = [_Msg .model_validate (m ) if isinstance (m , dict ) else m for m in req .contexts ]
169+ _truncator = ContextTruncator ()
170+ truncated = _truncator .truncate_by_turns (msgs , keep_most_recent_turns = KEEP_RECENT_TURNS )
171+ req .contexts = [m .model_dump () if hasattr (m , "model_dump" ) else m for m in truncated ]
172+ logger .warning (
173+ "[MemorySystem] 自动截断历史上下文:%d -> %d 条消息(原始 %d chars 超过阈值 %d)" ,
174+ len (msgs ), len (truncated ), len (contexts_str ), AUTO_COMPACT_CHARS ,
175+ )
176+ except Exception as _e :
177+ logger .warning ("[MemorySystem] 预防性截断失败(非致命): %s" , _e )
178+
156179 # ════════════════════════════════════════
157180 # LLM 工具:写日记
158181 # ════════════════════════════════════════
@@ -381,16 +404,40 @@ async def cmd_write_diary(
381404 @filter .command ("compact" )
382405 async def cmd_compact (self , event : AstrMessageEvent ) -> None :
383406 """
384- 手动触发 LLM Summary 上下文压缩,将当前会话历史压缩为摘要。
385- 用法:/compact
407+ 手动触发上下文压缩。
408+ 用法:
409+ /compact — LLM 摘要压缩,保留最近 6 轮
410+ /compact 10 — LLM 摘要压缩,保留最近 10 轮
411+ /compact hard — 强制截断,只保留最近 20 轮(不消耗 LLM)
412+ /compact clear — 清空全部历史(谨慎!)
386413 """
387414 import json
388415 from astrbot .core .agent .context .compressor import LLMSummaryCompressor
416+ from astrbot .core .agent .context .truncator import ContextTruncator
389417 from astrbot .core .agent .message import Message
390418
391419 umo = event .unified_msg_origin
392420 conv_mgr = self .context .conversation_manager
393421
422+ # 解析参数
423+ raw_msg = event .message_str .strip ()
424+ args = raw_msg .split ()[1 :] # 去掉 /compact
425+ mode = "llm"
426+ keep_recent = 6
427+ if args :
428+ arg0 = args [0 ].lower ()
429+ if arg0 == "hard" :
430+ mode = "hard"
431+ keep_recent = 20
432+ elif arg0 == "clear" :
433+ mode = "clear"
434+ else :
435+ try :
436+ keep_recent = max (2 , int (arg0 ))
437+ except ValueError :
438+ yield event .plain_result ("⚠️ 参数无效,请用 /compact [数字|hard|clear]" )
439+ return
440+
394441 # 1. 获取当前 conversation
395442 cid = await conv_mgr .get_curr_conversation_id (umo )
396443 if not cid :
@@ -403,42 +450,72 @@ async def cmd_compact(self, event: AstrMessageEvent) -> None:
403450 return
404451
405452 history : list [dict ] = json .loads (conv .history ) if isinstance (conv .history , str ) else (conv .history or [])
406- # 过滤掉 system 消息,只保留 user/assistant 轮次
407453 non_system = [m for m in history if m .get ("role" ) != "system" ]
408- if len (non_system ) < 4 :
409- yield event .plain_result ("📭 当前对话内容太少,不需要压缩(少于 4 条非系统消息)~" )
454+
455+ if mode == "clear" :
456+ await conv_mgr .update_conversation (
457+ unified_msg_origin = umo ,
458+ conversation_id = cid ,
459+ history = [],
460+ )
461+ yield event .plain_result (f"🗑️ 已清空全部历史记录(原有 { len (non_system )} 条消息)" )
462+ return
463+
464+ if len (non_system ) < 2 :
465+ yield event .plain_result ("📭 当前对话内容太少,不需要压缩~" )
466+ return
467+
468+ if mode == "hard" :
469+ # 仅截断,不使用 LLM
470+ msgs = [Message .model_validate (m ) if isinstance (m , dict ) else m for m in history ]
471+ _truncator = ContextTruncator ()
472+ truncated = _truncator .truncate_by_turns (msgs , keep_most_recent_turns = keep_recent )
473+ result_dicts = [m .model_dump () if hasattr (m , "model_dump" ) else dict (m ) for m in truncated ]
474+ await conv_mgr .update_conversation (
475+ unified_msg_origin = umo ,
476+ conversation_id = cid ,
477+ history = result_dicts ,
478+ )
479+ after_cnt = len ([m for m in result_dicts if m .get ("role" ) != "system" ])
480+ yield event .plain_result (
481+ f"✂️ 强制截断完成!\n "
482+ f"🔹 截断前:{ len (non_system )} 条消息\n "
483+ f"🔹 截断后:{ after_cnt } 条消息(保留最近 { keep_recent } 轮)"
484+ )
410485 return
411486
412- # 2. 获取当前 provider
487+ # LLM 摘要压缩
413488 provider = self .context .get_using_provider (umo )
414489 if not provider :
415- yield event .plain_result ("⚠️ 当前没有可用的 LLM Provider,无法进行 LLM 摘要压缩~ " )
490+ yield event .plain_result ("⚠️ 当前没有可用的 LLM Provider,尝试用 /compact hard 代替 " )
416491 return
417492
418- yield event .plain_result (f"🗜️ 正在压缩上下文(共 { len (non_system )} 条消息),请稍等~" )
493+ chars_before = len (json .dumps (history , ensure_ascii = False ))
494+ yield event .plain_result (
495+ f"🗜️ 正在 LLM 摘要压缩({ len (non_system )} 条消息,约 { chars_before // 4 :,} tokens),保留最近 { keep_recent } 轮,请稍等~"
496+ )
419497
420- # 3. 构建 LLMSummaryCompressor 并执行压缩
421498 compressor = LLMSummaryCompressor (
422499 provider = provider ,
423- keep_recent = 4 ,
500+ keep_recent = keep_recent ,
424501 )
425- messages = [Message ( ** m ) if not isinstance (m , Message ) else m for m in history ]
502+ messages = [Message . model_validate ( m ) if isinstance (m , dict ) else m for m in history ]
426503 compressed = await compressor (messages )
427504
428- # 4. 写回 conversation
429505 compressed_dicts = [m .model_dump () if hasattr (m , "model_dump" ) else dict (m ) for m in compressed ]
430506 await conv_mgr .update_conversation (
431507 unified_msg_origin = umo ,
432508 conversation_id = cid ,
433509 history = compressed_dicts ,
434510 )
435511
436- before_cnt = len (non_system )
512+ chars_after = len (json . dumps ( compressed_dicts , ensure_ascii = False ) )
437513 after_cnt = len ([m for m in compressed_dicts if m .get ("role" ) != "system" ])
438514 yield event .plain_result (
439- f"✅ 上下文压缩完成!\n "
440- f"🔹 压缩前:{ before_cnt } 条消息\n "
441- f"🔹 压缩后:{ after_cnt } 条消息(保留了最近 4 轮 + 摘要)"
515+ f"✅ LLM 摘要压缩完成!\n "
516+ f"🔹 压缩前:{ len (non_system )} 条消息 / ~{ chars_before // 4 :,} tokens\n "
517+ f"🔹 压缩后:{ after_cnt } 条消息 / ~{ chars_after // 4 :,} tokens\n "
518+ f"🔹 压缩率:{ (1 - chars_after / max (chars_before ,1 ))* 100 :.1f} %(保留最近 { keep_recent } 轮 + 摘要)"
442519 )
443520
444521 async def terminate (self ) -> None :
0 commit comments