From 0e85fcfe51e570638420e500002fc3f7de7c0985 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Sun, 1 Feb 2026 14:00:28 +0800 Subject: [PATCH] fix: optimize suggestion words and retries --- agent/memory/manager.py | 2 +- agent/prompt/builder.py | 84 ++++++++++--------- agent/prompt/workspace.py | 25 +----- agent/protocol/agent.py | 17 ++-- agent/protocol/agent_stream.py | 100 ++++++++++++++++++++++- agent/tools/__init__.py | 2 - agent/tools/current_time/current_time.py | 75 ----------------- bridge/agent_bridge.py | 46 ++++++++++- channel/feishu/feishu_channel.py | 13 ++- 9 files changed, 216 insertions(+), 148 deletions(-) delete mode 100644 agent/tools/current_time/current_time.py diff --git a/agent/memory/manager.py b/agent/memory/manager.py index 7f0b983..b313e82 100644 --- a/agent/memory/manager.py +++ b/agent/memory/manager.py @@ -456,7 +456,7 @@ class MemoryManager: - 当天笔记 → memory/{today_file} - 静默存储,仅在明确要求时确认 -**使用原则**: 自然使用记忆,就像你本来就知道。不要主动提起或列举记忆,除非用户明确询问。""" +**使用原则**: 自然使用记忆,就像你本来就知道。不需要生硬地提起或列举记忆,除非用户提到。""" else: guidance = f"""## Memory System diff --git a/agent/prompt/builder.py b/agent/prompt/builder.py index 3d9808a..6d8fc80 100644 --- a/agent/prompt/builder.py +++ b/agent/prompt/builder.py @@ -1,7 +1,7 @@ """ System Prompt Builder - 系统提示词构建器 -参考 clawdbot 的 system-prompt.ts,实现中文版的模块化提示词构建 +实现模块化的系统提示词构建,支持工具、技能、记忆等多个子系统 """ import os @@ -90,22 +90,21 @@ def build_agent_system_prompt( **kwargs ) -> str: """ - 构建Agent系统提示词(精简版,中文) + 构建Agent系统提示词 - 包含的sections: - 1. 基础身份 - 2. 工具说明 - 3. 技能系统 - 4. 记忆系统 - 5. 用户身份 - 6. 文档路径 - 7. 工作空间 - 8. 项目上下文文件 + 顺序说明(按重要性和逻辑关系排列): + 1. 工具系统 - 核心能力,最先介绍 + 2. 技能系统 - 紧跟工具,因为技能需要用 read 工具读取 + 3. 记忆系统 - 独立的记忆能力 + 4. 工作空间 - 工作环境说明 + 5. 用户身份 - 用户信息(可选) + 6. 项目上下文 - SOUL.md, USER.md, AGENTS.md(定义人格和身份) + 7. 运行时信息 - 元信息(时间、模型等) Args: workspace_dir: 工作空间目录 language: 语言 ("zh" 或 "en") - base_persona: 基础人格描述 + base_persona: 基础人格描述(已废弃,由SOUL.md定义) user_identity: 用户身份信息 tools: 工具列表 context_files: 上下文文件列表 @@ -120,33 +119,30 @@ def build_agent_system_prompt( """ sections = [] - # 1. 基础身份 - sections.extend(_build_identity_section(base_persona, language)) - - # 2. 工具说明 + # 1. 工具系统(最重要,放在最前面) if tools: sections.extend(_build_tooling_section(tools, language)) - # 3. 技能系统 + # 2. 技能系统(紧跟工具,因为需要用 read 工具) if skill_manager: sections.extend(_build_skills_section(skill_manager, tools, language)) - # 4. 记忆系统 + # 3. 记忆系统(独立的记忆能力) if memory_manager: sections.extend(_build_memory_section(memory_manager, tools, language)) - # 5. 用户身份 + # 4. 工作空间(工作环境说明) + sections.extend(_build_workspace_section(workspace_dir, language, is_first_conversation)) + + # 5. 用户身份(如果有) if user_identity: sections.extend(_build_user_identity_section(user_identity, language)) - # 6. 工作空间 - sections.extend(_build_workspace_section(workspace_dir, language, is_first_conversation)) - - # 7. 项目上下文文件(SOUL.md, USER.md等) + # 6. 项目上下文文件(SOUL.md, USER.md, AGENTS.md - 定义人格) if context_files: sections.extend(_build_context_files_section(context_files, language)) - # 8. 运行时信息(如果有) + # 7. 运行时信息(元信息,放在最后) if runtime_info: sections.extend(_build_runtime_section(runtime_info, language)) @@ -270,9 +266,9 @@ def _build_skills_section(skill_manager: Any, tools: Optional[List[Any]], langua "", "在回复之前:扫描下方 中的 条目。", "", - f"- 如果恰好有一个技能明确适用:使用 `{read_tool_name}` 工具读取其 路径下的 SKILL.md 文件,然后遵循它。", - "- 如果多个技能都适用:选择最具体的一个,然后读取并遵循。", - "- 如果没有明确适用的:不要读取任何 SKILL.md。", + f"- 如果恰好有一个技能明确适用:使用 `{read_tool_name}` 工具读取其 路径下的 SKILL.md 文件,然后遵循它", + "- 如果多个技能都适用:选择最具体的一个,然后读取并遵循", + "- 如果没有明确适用的:不要读取任何 SKILL.md", "", "**约束**: 永远不要一次性读取多个技能;只在选择后再读取。", "", @@ -455,7 +451,27 @@ def _build_runtime_section(runtime_info: Dict[str, Any], language: str) -> List[ if not runtime_info: return [] - # Only include if there's actual runtime info to display + lines = [ + "## 运行时信息", + "", + ] + + # Add current time if available + if runtime_info.get("current_time"): + time_str = runtime_info["current_time"] + weekday = runtime_info.get("weekday", "") + timezone = runtime_info.get("timezone", "") + + time_line = f"当前时间: {time_str}" + if weekday: + time_line += f" {weekday}" + if timezone: + time_line += f" ({timezone})" + + lines.append(time_line) + lines.append("") + + # Add other runtime info runtime_parts = [] if runtime_info.get("model"): runtime_parts.append(f"模型={runtime_info['model']}") @@ -465,14 +481,8 @@ def _build_runtime_section(runtime_info: Dict[str, Any], language: str) -> List[ if runtime_info.get("channel") and runtime_info.get("channel") != "web": runtime_parts.append(f"渠道={runtime_info['channel']}") - if not runtime_parts: - return [] - - lines = [ - "## 运行时信息", - "", - "运行时: " + " | ".join(runtime_parts), - "" - ] + if runtime_parts: + lines.append("运行时: " + " | ".join(runtime_parts)) + lines.append("") return lines diff --git a/agent/prompt/workspace.py b/agent/prompt/workspace.py index 54055b3..a096706 100644 --- a/agent/prompt/workspace.py +++ b/agent/prompt/workspace.py @@ -236,24 +236,9 @@ def _get_agents_template() -> str: 这个文件夹是你的家。好好对待它。 -## 系统自动加载 - -以下文件在每次会话启动时**已经自动加载**到系统提示词中,你无需再次读取: - -- ✅ `SOUL.md` - 你的人格设定(已加载) -- ✅ `USER.md` - 用户信息(已加载) -- ✅ `AGENTS.md` - 本文件(已加载) - -## 按需读取 - -以下文件**不会自动加载**,需要时使用相应工具读取: - -- 📝 `memory/YYYY-MM-DD.md` - 每日记忆(用 memory_search 检索) -- 🧠 `MEMORY.md` - 长期记忆(用 memory_search 检索) - ## 记忆系统 -你每次会话都是全新的。这些文件是你的连续性: +你每次会话都是全新的,记忆文件让你保持连续性: ### 📝 每日记忆:`memory/YYYY-MM-DD.md` - 原始的对话日志 @@ -296,13 +281,9 @@ def _get_agents_template() -> str: - 不要在未经询问的情况下运行破坏性命令 - 当有疑问时,先问 -## 工具使用 +## 工作空间演化 -技能提供你的工具。当你需要一个时,查看它的 `SKILL.md`。 - -## 让它成为你的 - -这只是一个起点。随着你弄清楚什么有效,添加你自己的约定、风格和规则。 +这个工作空间会随着你的使用而不断成长。当你学到新东西、发现更好的方式,或者犯错后改正时,记录下来。 """ diff --git a/agent/protocol/agent.py b/agent/protocol/agent.py index d1f2be6..5c4f994 100644 --- a/agent/protocol/agent.py +++ b/agent/protocol/agent.py @@ -95,15 +95,16 @@ class Agent: """ Get the full system prompt including skills. - :param skill_filter: Optional list of skill names to include - :return: Complete system prompt with skills appended - """ - base_prompt = self.system_prompt - skills_prompt = self.get_skills_prompt(skill_filter=skill_filter) + Note: Skills are now built into the system prompt by PromptBuilder, + so we just return the base prompt directly. This method is kept for + backward compatibility. - if skills_prompt: - return base_prompt + "\n" + skills_prompt - return base_prompt + :param skill_filter: Optional list of skill names to include (deprecated) + :return: Complete system prompt + """ + # Skills are now included in system_prompt by PromptBuilder + # No need to append them here + return self.system_prompt def refresh_skills(self): """Refresh the loaded skills.""" diff --git a/agent/protocol/agent_stream.py b/agent/protocol/agent_stream.py index f8b2df1..3ad6eb5 100644 --- a/agent/protocol/agent_stream.py +++ b/agent/protocol/agent_stream.py @@ -55,6 +55,9 @@ class AgentStreamExecutor: # Message history - use provided messages or create new list self.messages = messages if messages is not None else [] + + # Tool failure tracking for retry protection + self.tool_failure_history = [] # List of (tool_name, args_hash, success) tuples def _emit_event(self, event_type: str, data: dict = None): """Emit event""" @@ -68,6 +71,60 @@ class AgentStreamExecutor: except Exception as e: logger.error(f"Event callback error: {e}") + def _hash_args(self, args: dict) -> str: + """Generate a simple hash for tool arguments""" + import hashlib + # Sort keys for consistent hashing + args_str = json.dumps(args, sort_keys=True, ensure_ascii=False) + return hashlib.md5(args_str.encode()).hexdigest()[:8] + + def _check_consecutive_failures(self, tool_name: str, args: dict) -> tuple[bool, str]: + """ + Check if tool has failed too many times consecutively + + Returns: + (should_stop, reason) + """ + args_hash = self._hash_args(args) + + # Count consecutive failures for same tool + args + same_args_failures = 0 + for name, ahash, success in reversed(self.tool_failure_history): + if name == tool_name and ahash == args_hash: + if not success: + same_args_failures += 1 + else: + break # Stop at first success + else: + break # Different tool or args, stop counting + + if same_args_failures >= 3: + return True, f"Tool '{tool_name}' with same arguments failed {same_args_failures} times consecutively. Stopping to prevent infinite loop." + + # Count consecutive failures for same tool (any args) + same_tool_failures = 0 + for name, ahash, success in reversed(self.tool_failure_history): + if name == tool_name: + if not success: + same_tool_failures += 1 + else: + break # Stop at first success + else: + break # Different tool, stop counting + + if same_tool_failures >= 6: + return True, f"Tool '{tool_name}' failed {same_tool_failures} times consecutively (with any arguments). Stopping to prevent infinite loop." + + return False, "" + + def _record_tool_result(self, tool_name: str, args: dict, success: bool): + """Record tool execution result for failure tracking""" + args_hash = self._hash_args(args) + self.tool_failure_history.append((tool_name, args_hash, success)) + # Keep only last 50 records to avoid memory bloat + if len(self.tool_failure_history) > 50: + self.tool_failure_history = self.tool_failure_history[-50:] + def run_stream(self, user_message: str) -> str: """ Execute streaming reasoning loop @@ -413,7 +470,16 @@ class AgentStreamExecutor: arguments = json.loads(tc["arguments"]) if tc["arguments"] else {} except json.JSONDecodeError as e: logger.error(f"Failed to parse tool arguments: {tc['arguments']}") - arguments = {} + logger.error(f"JSON decode error: {e}") + # Return a clear error message to the LLM instead of empty dict + # This helps the LLM understand what went wrong + tool_calls.append({ + "id": tc["id"], + "name": tc["name"], + "arguments": {}, + "_parse_error": f"Invalid JSON in tool arguments: {tc['arguments'][:100]}... Error: {str(e)}" + }) + continue tool_calls.append({ "id": tc["id"], @@ -481,6 +547,31 @@ class AgentStreamExecutor: tool_id = tool_call["id"] arguments = tool_call["arguments"] + # Check if there was a JSON parse error + if "_parse_error" in tool_call: + parse_error = tool_call["_parse_error"] + logger.error(f"Skipping tool execution due to parse error: {parse_error}") + result = { + "status": "error", + "result": f"Failed to parse tool arguments. {parse_error}. Please ensure your tool call uses valid JSON format with all required parameters.", + "execution_time": 0 + } + self._record_tool_result(tool_name, arguments, False) + return result + + # Check for consecutive failures (retry protection) + should_stop, stop_reason = self._check_consecutive_failures(tool_name, arguments) + if should_stop: + logger.error(f"🛑 {stop_reason}") + self._record_tool_result(tool_name, arguments, False) + # 返回错误给 LLM,让它尝试其他方法 + result = { + "status": "error", + "result": f"{stop_reason}\n\nThis approach is not working. Please try a completely different method or ask the user for more information/clarification.", + "execution_time": 0 + } + return result + self._emit_event("tool_execution_start", { "tool_call_id": tool_id, "tool_name": tool_name, @@ -507,6 +598,10 @@ class AgentStreamExecutor: "execution_time": execution_time } + # Record tool result for failure tracking + success = result.status == "success" + self._record_tool_result(tool_name, arguments, success) + # Auto-refresh skills after skill creation if tool_name == "bash" and result.status == "success": command = arguments.get("command", "") @@ -530,6 +625,9 @@ class AgentStreamExecutor: "result": str(e), "execution_time": 0 } + # Record failure + self._record_tool_result(tool_name, arguments, False) + self._emit_event("tool_execution_end", { "tool_call_id": tool_id, "tool_name": tool_name, diff --git a/agent/tools/__init__.py b/agent/tools/__init__.py index a8863ce..76f7e2e 100644 --- a/agent/tools/__init__.py +++ b/agent/tools/__init__.py @@ -4,7 +4,6 @@ from agent.tools.tool_manager import ToolManager # Import basic tools (no external dependencies) from agent.tools.calculator.calculator import Calculator -from agent.tools.current_time.current_time import CurrentTime # Import file operation tools from agent.tools.read.read import Read @@ -82,7 +81,6 @@ __all__ = [ 'BaseTool', 'ToolManager', 'Calculator', - 'CurrentTime', 'Read', 'Write', 'Edit', diff --git a/agent/tools/current_time/current_time.py b/agent/tools/current_time/current_time.py deleted file mode 100644 index 5fb0f95..0000000 --- a/agent/tools/current_time/current_time.py +++ /dev/null @@ -1,75 +0,0 @@ -import datetime -import time - -from agent.tools.base_tool import BaseTool, ToolResult - - -class CurrentTime(BaseTool): - name: str = "time" - description: str = "A tool to get current date and time information." - params: dict = { - "type": "object", - "properties": { - "format": { - "type": "string", - "description": "Optional format for the time (e.g., 'iso', 'unix', 'human'). Default is 'human'." - }, - "timezone": { - "type": "string", - "description": "Optional timezone specification (e.g., 'UTC', 'local'). Default is 'local'." - } - }, - "required": [] - } - config: dict = {} - - def execute(self, args: dict) -> ToolResult: - try: - # Get the format and timezone parameters, with defaults - time_format = args.get("format", "human").lower() - timezone = args.get("timezone", "local").lower() - - # Get current time - current_time = datetime.datetime.now() - - # Handle timezone if specified - if timezone == "utc": - current_time = datetime.datetime.utcnow() - - # Format the time according to the specified format - if time_format == "iso": - # ISO 8601 format - formatted_time = current_time.isoformat() - elif time_format == "unix": - # Unix timestamp (seconds since epoch) - formatted_time = time.time() - else: - # Human-readable format - formatted_time = current_time.strftime("%Y-%m-%d %H:%M:%S") - - # Prepare additional time components for the response - year = current_time.year - month = current_time.month - day = current_time.day - hour = current_time.hour - minute = current_time.minute - second = current_time.second - weekday = current_time.strftime("%A") # Full weekday name - - result = { - "current_time": formatted_time, - "components": { - "year": year, - "month": month, - "day": day, - "hour": hour, - "minute": minute, - "second": second, - "weekday": weekday - }, - "format": time_format, - "timezone": timezone - } - return ToolResult.success(result=result) - except Exception as e: - return ToolResult.fail(result=str(e)) diff --git a/bridge/agent_bridge.py b/bridge/agent_bridge.py index 135c1a4..34535bb 100644 --- a/bridge/agent_bridge.py +++ b/bridge/agent_bridge.py @@ -473,6 +473,15 @@ class AgentBridge: # Load context files context_files = load_context_files(workspace_root) + # Initialize skill manager + skill_manager = None + try: + from agent.skills import SkillManager + skill_manager = SkillManager(workspace_dir=workspace_root) + logger.info(f"[AgentBridge] Initialized SkillManager with {len(skill_manager.skills)} skills for session {session_id}") + except Exception as e: + logger.warning(f"[AgentBridge] Failed to initialize SkillManager for session {session_id}: {e}") + # Check if this is the first conversation from agent.prompt.workspace import is_first_conversation, mark_conversation_started is_first = is_first_conversation(workspace_root) @@ -483,15 +492,49 @@ class AgentBridge: language="zh" ) + # Get current time and timezone info + import datetime + import time + + now = datetime.datetime.now() + + # Get timezone info + try: + offset = -time.timezone if not time.daylight else -time.altzone + hours = offset // 3600 + minutes = (offset % 3600) // 60 + if minutes: + timezone_name = f"UTC{hours:+03d}:{minutes:02d}" + else: + timezone_name = f"UTC{hours:+03d}" + except Exception: + timezone_name = "UTC" + + # Chinese weekday mapping + weekday_map = { + 'Monday': '星期一', + 'Tuesday': '星期二', + 'Wednesday': '星期三', + 'Thursday': '星期四', + 'Friday': '星期五', + 'Saturday': '星期六', + 'Sunday': '星期日' + } + weekday_zh = weekday_map.get(now.strftime("%A"), now.strftime("%A")) + runtime_info = { "model": conf().get("model", "unknown"), "workspace": workspace_root, - "channel": conf().get("channel_type", "unknown") + "channel": conf().get("channel_type", "unknown"), + "current_time": now.strftime("%Y-%m-%d %H:%M:%S"), + "weekday": weekday_zh, + "timezone": timezone_name } system_prompt = prompt_builder.build( tools=tools, context_files=context_files, + skill_manager=skill_manager, memory_manager=memory_manager, runtime_info=runtime_info, is_first_conversation=is_first @@ -507,6 +550,7 @@ class AgentBridge: max_steps=50, output_mode="logger", workspace_dir=workspace_root, + skill_manager=skill_manager, enable_skills=True ) diff --git a/channel/feishu/feishu_channel.py b/channel/feishu/feishu_channel.py index b42ec37..16248de 100644 --- a/channel/feishu/feishu_channel.py +++ b/channel/feishu/feishu_channel.py @@ -285,7 +285,18 @@ class FeiShuChanel(ChatChannel): context["origin_ctype"] = ctype cmsg = context["msg"] - context["session_id"] = cmsg.from_user_id + + # Set session_id based on chat type to ensure proper session isolation + if cmsg.is_group: + # Group chat: combine user_id and group_id to create unique session per user per group + # This ensures: + # - Same user in different groups have separate conversation histories + # - Same user in private chat and group chat have separate histories + context["session_id"] = f"{cmsg.from_user_id}:{cmsg.other_user_id}" + else: + # Private chat: use user_id only + context["session_id"] = cmsg.from_user_id + context["receiver"] = cmsg.other_user_id if ctype == ContextType.TEXT: