11from app .core .llm .llm import LLM , simple_chat
22from app .utils .log_util import logger
3+ from icecream import ic
4+
5+ # TODO: Memory 的管理
6+ # TODO: 评估任务完成情况,rethinking
37
48
59class Agent :
@@ -8,7 +12,7 @@ def __init__(
812 task_id : str ,
913 model : LLM ,
1014 max_chat_turns : int = 30 , # 单个agent最大对话轮次
11- max_memory : int = 25 , # 最大记忆轮次
15+ max_memory : int = 12 , # 最大记忆轮次
1216 ) -> None :
1317 self .task_id = task_id
1418 self .model = model
@@ -51,14 +55,26 @@ async def run(self, prompt: str, system_prompt: str, sub_title: str) -> str:
5155 return error_msg
5256
5357 async def append_chat_history (self , msg : dict ) -> None :
54- await self . clear_memory ( )
58+ ic ( f"添加消息: role= { msg . get ( 'role' ) } , 当前历史长度= { len ( self . chat_history ) } " )
5559 self .chat_history .append (msg )
60+ ic (f"添加后历史长度={ len (self .chat_history )} " )
61+
62+ # 只有在添加非tool消息时才进行内存清理,避免在工具调用期间破坏消息结构
63+ if msg .get ("role" ) != "tool" :
64+ ic ("触发内存清理" )
65+ await self .clear_memory ()
66+ else :
67+ ic ("跳过内存清理(tool消息)" )
5668
5769 async def clear_memory (self ):
5870 """当聊天历史超过最大记忆轮次时,使用 simple_chat 进行总结压缩"""
71+ ic (f"检查内存清理: 当前={ len (self .chat_history )} , 最大={ self .max_memory } " )
72+
5973 if len (self .chat_history ) <= self .max_memory :
74+ ic ("无需清理内存" )
6075 return
6176
77+ ic ("开始内存清理" )
6278 logger .info (
6379 f"{ self .__class__ .__name__ } :开始清除记忆,当前记录数:{ len (self .chat_history )} "
6480 )
@@ -71,16 +87,21 @@ async def clear_memory(self):
7187 else None
7288 )
7389
74- # 构造总结提示
75- summarize_history = []
76- if system_msg :
77- summarize_history .append (system_msg )
90+ # 查找需要保留的消息范围 - 保留最后几条完整的对话和工具调用
91+ preserve_start_idx = self ._find_safe_preserve_point ()
92+ ic (f"保留起始索引: { preserve_start_idx } " )
7893
79- # 添加要总结的对话内容(跳过第一条系统消息和最后5条消息)
94+ # 确定需要总结的消息范围
8095 start_idx = 1 if system_msg else 0
81- end_idx = len (self .chat_history ) - 5
96+ end_idx = preserve_start_idx
97+ ic (f"总结范围: { start_idx } -> { end_idx } " )
8298
8399 if end_idx > start_idx :
100+ # 构造总结提示
101+ summarize_history = []
102+ if system_msg :
103+ summarize_history .append (system_msg )
104+
84105 summarize_history .append (
85106 {
86107 "role" : "user" ,
@@ -91,7 +112,7 @@ async def clear_memory(self):
91112 # 调用 simple_chat 进行总结
92113 summary = await simple_chat (self .model , summarize_history )
93114
94- # 重构聊天历史:系统消息 + 总结 + 最后5条消息
115+ # 重构聊天历史:系统消息 + 总结 + 保留的消息
95116 new_history = []
96117 if system_msg :
97118 new_history .append (system_msg )
@@ -100,10 +121,11 @@ async def clear_memory(self):
100121 {"role" : "assistant" , "content" : f"[历史对话总结] { summary } " }
101122 )
102123
103- # 添加最后5条消息
104- new_history .extend (self .chat_history [- 5 :])
124+ # 添加需要保留的消息(最后几条完整对话)
125+ new_history .extend (self .chat_history [preserve_start_idx :])
105126
106127 self .chat_history = new_history
128+ ic (f"内存清理完成,新历史长度: { len (self .chat_history )} " )
107129 logger .info (
108130 f"{ self .__class__ .__name__ } :记忆清除完成,压缩至:{ len (self .chat_history )} 条记录"
109131 )
@@ -112,8 +134,142 @@ async def clear_memory(self):
112134
113135 except Exception as e :
114136 logger .error (f"记忆清除失败,使用简单切片策略: { str (e )} " )
115- # 如果总结失败,回退到简单的切片策略
116- self .chat_history = self .chat_history [:2 ] + self .chat_history [- 5 :]
137+ # 如果总结失败,回退到安全的策略:保留系统消息和最后几条消息,确保工具调用完整性
138+ safe_history = self ._get_safe_fallback_history ()
139+ self .chat_history = safe_history
140+
141+ def _find_safe_preserve_point (self ) -> int :
142+ """找到安全的保留起始点,确保不会破坏工具调用序列"""
143+ # 最少保留最后3条消息,确保基本对话完整性
144+ min_preserve = min (3 , len (self .chat_history ))
145+ preserve_start = len (self .chat_history ) - min_preserve
146+ ic (
147+ f"寻找安全保留点: 历史长度={ len (self .chat_history )} , 最少保留={ min_preserve } , 开始位置={ preserve_start } "
148+ )
149+
150+ # 从后往前查找,确保不会在工具调用序列中间切断
151+ for i in range (preserve_start , - 1 , - 1 ):
152+ if i >= len (self .chat_history ):
153+ continue
154+
155+ # 检查从这个位置开始是否是安全的(没有孤立的tool消息)
156+ is_safe = self ._is_safe_cut_point (i )
157+ ic (f"检查位置 { i } : 安全={ is_safe } " )
158+ if is_safe :
159+ ic (f"找到安全保留点: { i } " )
160+ return i
161+
162+ # 如果找不到安全点,至少保留最后1条消息
163+ fallback = len (self .chat_history ) - 1
164+ ic (f"未找到安全点,使用备用位置: { fallback } " )
165+ return fallback
166+
167+ def _is_safe_cut_point (self , start_idx : int ) -> bool :
168+ """检查从指定位置开始切割是否安全(不会产生孤立的tool消息)"""
169+ if start_idx >= len (self .chat_history ):
170+ ic (f"切割点 { start_idx } >= 历史长度,安全" )
171+ return True
172+
173+ # 检查切割后的消息序列是否有孤立的tool消息
174+ tool_messages = []
175+ for i in range (start_idx , len (self .chat_history )):
176+ msg = self .chat_history [i ]
177+ if isinstance (msg , dict ) and msg .get ("role" ) == "tool" :
178+ tool_call_id = msg .get ("tool_call_id" )
179+ tool_messages .append ((i , tool_call_id ))
180+ ic (f"发现tool消息在位置 { i } , tool_call_id={ tool_call_id } " )
181+
182+ # 向前查找对应的tool_calls消息
183+ if tool_call_id :
184+ found_tool_call = False
185+ for j in range (start_idx , i ):
186+ prev_msg = self .chat_history [j ]
187+ if (
188+ isinstance (prev_msg , dict )
189+ and "tool_calls" in prev_msg
190+ and prev_msg ["tool_calls" ]
191+ ):
192+ for tool_call in prev_msg ["tool_calls" ]:
193+ if tool_call .get ("id" ) == tool_call_id :
194+ found_tool_call = True
195+ ic (f"找到对应的tool_call在位置 { j } " )
196+ break
197+ if found_tool_call :
198+ break
199+
200+ if not found_tool_call :
201+ ic (
202+ f"❌ tool消息 { tool_call_id } 没有找到对应的tool_call,切割点不安全"
203+ )
204+ return False
205+
206+ ic (f"切割点 { start_idx } 安全,检查了 { len (tool_messages )} 个tool消息" )
207+ return True
208+
209+ def _get_safe_fallback_history (self ) -> list :
210+ """获取安全的后备历史记录,确保不会有孤立的tool消息"""
211+ if not self .chat_history :
212+ return []
213+
214+ # 保留系统消息
215+ safe_history = []
216+ if self .chat_history and self .chat_history [0 ]["role" ] == "system" :
217+ safe_history .append (self .chat_history [0 ])
218+
219+ # 从后往前查找安全的消息序列
220+ for preserve_count in range (1 , min (4 , len (self .chat_history )) + 1 ):
221+ start_idx = len (self .chat_history ) - preserve_count
222+ if self ._is_safe_cut_point (start_idx ):
223+ safe_history .extend (self .chat_history [start_idx :])
224+ return safe_history
225+
226+ # 如果都不安全,只保留最后一条非tool消息
227+ for i in range (len (self .chat_history ) - 1 , - 1 , - 1 ):
228+ msg = self .chat_history [i ]
229+ if isinstance (msg , dict ) and msg .get ("role" ) != "tool" :
230+ safe_history .append (msg )
231+ break
232+
233+ return safe_history
234+
235+ def _find_last_unmatched_tool_call (self ) -> int | None :
236+ """查找最后一个未匹配的tool call的索引"""
237+ ic ("开始查找未匹配的tool_call" )
238+
239+ # 从后往前查找,寻找没有对应tool response的tool call
240+ for i in range (len (self .chat_history ) - 1 , - 1 , - 1 ):
241+ msg = self .chat_history [i ]
242+
243+ # 检查是否是包含tool_calls的消息
244+ if isinstance (msg , dict ) and "tool_calls" in msg and msg ["tool_calls" ]:
245+ ic (f"在位置 { i } 发现tool_calls消息" )
246+
247+ # 检查每个tool call是否都有对应的response
248+ for tool_call in msg ["tool_calls" ]:
249+ tool_call_id = tool_call .get ("id" )
250+ ic (f"检查tool_call_id: { tool_call_id } " )
251+
252+ if tool_call_id :
253+ # 在后续消息中查找对应的tool response
254+ response_found = False
255+ for j in range (i + 1 , len (self .chat_history )):
256+ response_msg = self .chat_history [j ]
257+ if (
258+ isinstance (response_msg , dict )
259+ and response_msg .get ("role" ) == "tool"
260+ and response_msg .get ("tool_call_id" ) == tool_call_id
261+ ):
262+ ic (f"找到匹配的tool响应在位置 { j } " )
263+ response_found = True
264+ break
265+
266+ if not response_found :
267+ # 找到未匹配的tool call
268+ ic (f"❌ 发现未匹配的tool_call在位置 { i } , id={ tool_call_id } " )
269+ return i
270+
271+ ic ("没有发现未匹配的tool_call" )
272+ return None
117273
118274 def _format_history_for_summary (self , history : list [dict ]) -> str :
119275 """格式化历史记录用于总结"""
0 commit comments