@@ -3,6 +3,8 @@ import 'dart:convert';
33import '../llm/llm_client.dart' ;
44import '../agent/agent_hook.dart' ;
55import '../agent/message_pipeline.dart' ;
6+ import '../agent/transformers/context_compactor.dart' ;
7+ import '../isolate/compute_service.dart' ;
68import '../logger/app_logger.dart' ;
79import 'executor.dart' ;
810
@@ -150,6 +152,10 @@ class AgentLoop {
150152 /// 真正卡死的场景通常在几轮内就会重复同一模式,50只是防止真的失控。
151153 final int maxToolCallRounds;
152154
155+ /// 模型的上下文窗口大小(token 数)。用于对话中压缩的阈值计算。
156+ /// 0 表示未知,会使用保守默认值 128K。
157+ final int contextWindow;
158+
153159 AgentLoop ({
154160 required this .llm,
155161 required this .executor,
@@ -158,6 +164,7 @@ class AgentLoop {
158164 this .messagePipeline = const MessagePipeline (),
159165 this .hooks = const [],
160166 this .maxToolCallRounds = 50 ,
167+ this .contextWindow = 0 ,
161168 });
162169
163170 /// 执行一轮对话 (用户输入 → 多轮 tool_call → 最终回复)
@@ -316,7 +323,96 @@ class AgentLoop {
316323 });
317324 }
318325
326+ // ✅ 对话中压缩检查:每 5 轮检查一次,防止单轮对话内上下文溢出
327+ if (round % 5 == 0 ) {
328+ await _checkAndCompressIfNeeded (round, messages);
329+ }
330+
319331 // 循环 → 把工具结果交给 LLM 继续处理
320332 }
321333 }
334+
335+ /// 对话中压缩检查:估算当前 token 数,超过阈值则触发压缩
336+ Future <void > _checkAndCompressIfNeeded (
337+ int round,
338+ List <Map <String , dynamic >> messages,
339+ ) async {
340+ final tokens = _estimateTokens (messages);
341+ final threshold = _getEffectiveThreshold ();
342+
343+ // 未超过阈值,不压缩
344+ if (tokens <= threshold) {
345+ return ;
346+ }
347+
348+ AppLogger .instance.log (
349+ '[AgentLoop] 工具循环第 $round 轮触发对话中压缩: $tokens tokens > $threshold tokens (${(tokens / (contextWindow > 0 ? contextWindow : 128000 ) * 100 ).toStringAsFixed (1 )}%)' ,
350+ );
351+
352+ final processed = await messagePipeline.process (messages);
353+
354+ // 检查是否真的发生了压缩
355+ if (processed.length < messages.length) {
356+ final beforeTokens = tokens;
357+ messages.clear ();
358+ messages.addAll (processed);
359+
360+ // 重置压缩标记,允许下次再压缩
361+ for (final transformer in messagePipeline.transformers) {
362+ if (transformer is ContextCompactor ) {
363+ transformer.resetRound ();
364+ }
365+ }
366+
367+ final afterTokens = _estimateTokens (messages);
368+ final reduction = ((1 - afterTokens / beforeTokens) * 100 )
369+ .toStringAsFixed (1 );
370+ AppLogger .instance.log (
371+ '[AgentLoop] 压缩完成: ${messages .length } 条消息, $afterTokens tokens (压缩比: $reduction %)' ,
372+ );
373+ } else {
374+ AppLogger .instance.log ('[AgentLoop] 压缩跳过: 消息数量未减少 (可能已是最小保留量)' );
375+ }
376+ }
377+
378+ /// 估算消息列表的 token 数
379+ int _estimateTokens (List <Map <String , dynamic >> messages) {
380+ int total = 0 ;
381+ for (final msg in messages) {
382+ final content = msg['content' ];
383+ if (content is String ) {
384+ total += ComputeService .estimateTokens (content);
385+ } else if (content is List ) {
386+ // Multimodal content parts
387+ for (final part in content) {
388+ if (part is Map && part['type' ] == 'text' ) {
389+ total += ComputeService .estimateTokens (
390+ part['text' ] as String ? ?? '' ,
391+ );
392+ }
393+ }
394+ }
395+ // tool_calls 参数也算 token
396+ if (msg['tool_calls' ] is List ) {
397+ for (final tc in msg['tool_calls' ] as List ) {
398+ final args = tc['function' ]? ['arguments' ] as String ? ?? '' ;
399+ total += ComputeService .estimateTokens (args);
400+ }
401+ }
402+ total += 4 ; // 每条消息的元数据开销
403+ }
404+ return total;
405+ }
406+
407+ /// 获取当前的压缩阈值
408+ int _getEffectiveThreshold () {
409+ for (final transformer in messagePipeline.transformers) {
410+ if (transformer is ContextCompactor ) {
411+ return transformer.effectiveThreshold;
412+ }
413+ }
414+ // 默认:128K * 0.60 = 76.8K
415+ final ctx = contextWindow > 0 ? contextWindow : 128000 ;
416+ return (ctx * 0.60 ).toInt ();
417+ }
322418}
0 commit comments