From e8fa26d79f06e273e9fb2bba2e7873db1c609869 Mon Sep 17 00:00:00 2001 From: qing <1776177662@qq.com> Date: Sat, 1 Aug 2026 03:19:58 +0800 Subject: [PATCH] fix(aibox): fix prompt ordering, reduce per-turn token waste, and improve defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt system bugs and improvements: 1. **SKILL_PREFIX bug fixed**: SKILL_PREFIX = "1_" only matched files starting with "1_", causing 2_ through 10_ skill prompt files to be misclassified as "other" files and sorted in wrong order (e.g., 10_task_handling.md sorted before 2_memory_save.md due to lexicographic ordering). Replaced with regex _SKILL_FILE_RE = re.compile(r'^(\d+)_') and _skill_sort_key() that extracts the numeric prefix for proper numeric sorting. 2. **_init_system_prompts used unsorted list**: get_prompts_list() returned prompts in os.listdir() lexicographic order, which put 100_STATUS.md before 1_memory_search.md and 10_task_handling.md before 1_memory_search.md. Now uses get_sorted_prompts_list() for correct ordering: identity → skill prompts (1-10 by number) → soul → status. 3. **Verbose pre_prompt injected every turn (~30 lines)**: The pre_prompt contained detailed workflow instructions (search_skills first, memory search, ask_user, etc.) that duplicated what system skill prompts already say, wasting ~300+ tokens per user message. Worse, the same verbose prompt was injected on recursive think_and_respond calls after tool execution, confusing the model with repeated "第一步搜索技能" instructions when it was already mid-conversation with tool results. - Reduced pre_prompt to 5 concise bullet points as a reminder - Added is_continuation parameter: recursive calls after tool execution skip pre_prompt entirely and just pass "请基于以上工具执行结果继续回答。" - Removed post_prompt (save_memory/update_soul instructions already in 2_memory_save.md and 8_identity_management.md) 4. **Unreasonable large-file thresholds**: 3_file_operations.md default said >50KB or >200 lines is "too large" — too conservative for code files. Raised to 500KB/2000 lines; added tail=true and binary file guidance. 5. **Overly rigid memory search instruction**: 1_memory_search.md said "每次都要做!" (must do every time), causing unnecessary memory searches for simple greetings/acknowledgments. Softened to "根据话题相关性判断", with explicit examples of when to skip (greetings, confirmations). New/changed methods: - PromptManager._skill_sort_key(cls, filename) - extract numeric prefix - PromptManager._prompt_sort_key(self, p) - tuple-based sort key - PromptManager.get_sorted_prompts_list() - returns correctly ordered list - PromptManager.get_skill_prompts() - uses regex instead of startswith("1_") - DeepSeekChat.think_and_respond() - added is_continuation parameter --- aibox/aibox.py | 171 ++++++++++++++++++++++--------------------------- 1 file changed, 77 insertions(+), 94 deletions(-) diff --git a/aibox/aibox.py b/aibox/aibox.py index 09c92e7..5169026 100644 --- a/aibox/aibox.py +++ b/aibox/aibox.py @@ -436,7 +436,7 @@ class PromptManager: 文件命名规范: - 00_IDENTITY.md: 用户身份信息 - - 1_*.md/txt: 基础技能(必须从1_开头) + - N_*.md/txt: 基础技能提示词(N 为数字,如 1_memory_search.md、10_task_handling.md),按数字升序加载 - 99_SOUL.md: 智能体灵魂文件(角色定位、身份定义、性格等) - 100_STATUS.md: 智能体状态记录 """ @@ -450,8 +450,8 @@ class PromptManager: STATUS_FILE = "100_STATUS.md" SOUL_FILE = "99_SOUL.md" # 智能体灵魂文件 - 所有角色定位内容 - # 基础技能前缀 - SKILL_PREFIX = "1_" # 基础技能文件必须以这个开头 + # 基础技能文件名模式:N_xxx.md(N 为数字,如 1_memory_search.md、10_task_handling.md) + _SKILL_FILE_RE = re.compile(r'^(\d+)_') def __new__(cls): if cls._instance is None: @@ -642,14 +642,17 @@ class PromptManager: print(f"⚠️ 创建默认灵魂文件失败: {e}") def _ensure_skill_files(self): - """确保基础技能文件存在 - 以1_开头的文件""" + """确保基础技能文件存在 - N_ 开头的文件(N 为数字,按数字顺序加载)""" skill_files = [ { 'filename': '1_memory_search.md', 'content': """# 记忆搜索习惯 -## 重要习惯 -在每次回答用户问题前,**必须**先使用 `memory search` 工具搜索记忆库中与当前话题相关的记忆。 +## 核心习惯 +在回答用户问题前,**应根据话题相关性判断是否需要搜索记忆**: +- 涉及过往讨论、用户偏好、之前做过的事 → 必须搜索 +- 全新话题、简单问候("你好")、确认回应("好的""明白了")→ 无需搜索 +- 不确定时,搜索一下也无妨 ## 为什么要这样做? - 保持对话的连贯性 @@ -665,11 +668,9 @@ memory search keywords=["关键词1", "关键词2"] ## 示例 如果用户说:"上次我们讨论的那个项目怎么样了?" 你应该: -1. 先搜索 "项目" 相关的记忆 +1. 搜索 "项目" 相关的记忆 2. 根据搜索结果回答 3. 如果找不到相关记忆,诚实告知用户 - -记住:这是你的核心习惯,每次都要做! """ }, { @@ -711,19 +712,22 @@ save_memory_and_end_conversation( - **list_files**: 列出目录 ## 大文件处理规则 -当遇到以下情况时,**必须先告知用户文件过大**: -1. 文件大小超过 50KB -2. 文件行数超过 200 行 +当遇到以下情况时,**必须分页读取**,不要一次性读取整个文件: +1. 文件大小超过 500KB +2. 文件行数超过 2000 行 对于大文件,应该: -- 使用 head/tail 参数只读取开头或结尾 +- 先用 head=true 读取前50行了解文件结构 - 使用 start_line/end_line 参数指定行范围 -- 建议用户提供更具体的查询条件 +- 使用 tail=true 读取文件末尾 +- 对于二进制文件或特殊格式文件,使用 run_command 配合 head/strings 等命令查看 +- 建议用户提供更具体的查询条件以缩小范围 ## 示例 ``` read_file filepath="/path/to/file.txt" head=true max_lines=50 -read_file filepath="/path/to/file.txt" start_line=10 end_line=20 +read_file filepath="/path/to/file.txt" start_line=100 end_line=200 +read_file filepath="/path/to/file.txt" tail=true max_lines=50 ``` """ }, @@ -1047,42 +1051,55 @@ update_soul content="我是...(新的自我定义)" """创建默认提示词(已由各 _ensure_* 方法处理)""" pass + @classmethod + def _skill_sort_key(cls, filename: str): + """技能文件按前缀数字排序,非技能文件返回 None。""" + m = cls._SKILL_FILE_RE.match(filename) + return int(m.group(1)) if m else None + + def _prompt_sort_key(self, p): + """排序规则:00_IDENTITY → 技能文件(按数字升序) → 99_SOUL → 100_STATUS → 其他""" + file = p['file'] + if file == self.IDENTITY_FILE: + return (0,) + skill_num = self._skill_sort_key(file) + if skill_num is not None: + return (1, skill_num) + if file == self.SOUL_FILE: + return (2,) + if file == self.STATUS_FILE: + return (4,) + return (3, file) # 其他文件按名字排序 + def get_prompts(self, join_with: str = "\n\n") -> str: - """获取所有提示词,按顺序拼接""" + """获取所有提示词,按顺序拼接(identity → 技能文件 → soul → status)""" if not self._prompts: return "" - - # 排序规则:先基础技能(1_开头),再其他,最后灵魂文件 - def sort_key(p): - file = p['file'] - if file == self.SOUL_FILE: - return 3 # 灵魂文件最后加载 - elif file.startswith(self.SKILL_PREFIX): - return 1 # 基础技能 - elif file == self.IDENTITY_FILE: - return 0 # 身份文件最先 - elif file == self.STATUS_FILE: - return 4 # 状态文件最后 - else: - return 2 - - sorted_prompts = sorted(self._prompts, key=sort_key) + sorted_prompts = sorted(self._prompts, key=self._prompt_sort_key) contents = [p['content'] for p in sorted_prompts] return join_with.join(contents) - + + def get_sorted_prompts_list(self) -> List[Dict]: + """获取按正确顺序排列的提示词列表(用于 system 消息构建)。""" + return sorted(self._prompts, key=self._prompt_sort_key) + def get_prompts_list(self) -> List[Dict]: - """获取所有提示词列表""" + """获取所有提示词列表(原始加载顺序,已弃用,请用 get_sorted_prompts_list)。""" return self._prompts.copy() - + + def get_skill_prompts(self) -> List[Dict]: + """获取所有基础技能提示词(N_ 开头的文件)。""" + return [p for p in self._prompts if self._skill_sort_key(p['file']) is not None] + def reload(self): """重新加载所有提示词""" self._prompts = [] self._load_prompts() return len(self._prompts) - + def get_prompt_count(self) -> int: return len(self._prompts) - + def get_prompts_dir_path(self) -> str: return self._prompts_dir @@ -6536,19 +6553,19 @@ class DeepSeekChat: self._register_tools(exit_callback, command_executor) def _init_system_prompts(self, system_prompt: str = None): - """初始化系统提示词 - 从 PromptManager 加载""" + """初始化系统提示词 - 从 PromptManager 加载(按 identity → 技能 → soul → status 顺序)""" if system_prompt: # 如果明确指定了 system_prompt,使用它 self._add_message("system", system_prompt) self.logger.info("使用指定的系统提示词") else: - # 从 PromptManager 加载所有提示词 - prompts = self.prompt_manager.get_prompts_list() + # 从 PromptManager 加载所有提示词,按正确顺序排列 + prompts = self.prompt_manager.get_sorted_prompts_list() if prompts: for prompt in prompts: if prompt['content'].strip(): self._add_message("system", prompt['content'].strip()) - self.logger.info(f"已加载 {len(prompts)} 个系统提示词") + self.logger.info(f"已加载 {len(prompts)} 个系统提示词(顺序: identity → 技能 → soul → status)") else: # 如果没有任何提示词,添加一个默认的 default_prompt = "你是一个智能AI助手。" @@ -6847,8 +6864,15 @@ class DeepSeekChat: # 默认情况下,如果记忆库存在,搜索一下也无妨 return True - def think_and_respond(self, input_text: str, output_callback: Callable = None) -> Optional[str]: - """思考并回应 - 支持实时流式输出,支持思考模式""" + def think_and_respond(self, input_text: str, output_callback: Callable = None, is_continuation: bool = False) -> Optional[str]: + """思考并回应 - 支持实时流式输出,支持思考模式。 + + Args: + input_text: 用户输入或继续提示 + output_callback: 流式输出回调 + is_continuation: 是否为工具调用后的递归继续;为 True 时不注入冗长的工作流指令, + 因为 system prompt 中已有完整行为规范,继续时只需传递工具结果 + """ if not self.conversation_active: self.logger.info("对话已结束,重置状态") self.conversation_active = True @@ -6880,54 +6904,14 @@ class DeepSeekChat: self.stats["api_calls"] += 1 - # 前置提示词 - pre_prompt = """【系统指令 - 核心工作流程】 - - 在开始任何任务之前,你必须严格按照以下优先级执行: - - ## 第一步:搜索可用技能(最高优先级)⭐ - 当用户提出任何任务、需求或问题时,**必须先使用 `search_skills` 工具**搜索相关技能: - - 使用 `search_skills keyword="[任务关键词]"` 搜索是否有现成的扩展技能可以完成该任务 - - 例如:用户说"帮我查天气",你应该先执行 `search_skills keyword="天气"` - - 例如:用户说"帮我计算BMI",你应该先执行 `search_skills keyword="BMI"` - - 如果搜索结果中有匹配的技能,优先使用该技能完成任务 - - 只有在没有合适技能的情况下,才考虑使用其他工具或方法 - - ## 第二步:搜索相关记忆 - 在每次回答用户问题前,**必须**使用 `memory search` 工具搜索记忆库中与当前话题相关的记忆,确保回答的连贯性和准确性。 - - ## 第三步:积极向用户提问 - 如果你有任何无法确定的事情,那么你需要用户提供额外信息, - 请务必使用 `ask_user` 工具向用户提问,以便获得更准确的指引。 - - ## 第四步:执行任务或回答问题 - 根据搜索结果,选择合适的工具或直接回答。 - - 【重要提醒】 - - 技能优先原则:有技能先用技能,不要自己硬编码实现 - - 如果没有合适的技能,可以使用其他工具或直接回答 - - 对话结束时,必须使用 `save_memory_and_end_conversation` 工具保存本次对话的重要信息并更新智能体状态 - - 当了解到用户的身份信息时,使用 `update_identity` 工具更新 00_IDENTITY.md - - 当你的角色、性格、能力边界需要调整时,使用 `update_soul` 工具更新 99_SOUL.md 文件来重新定义自己 - - 绝对不要直接打开大文件来读,应该使用额外的压缩工具 - - 现在请处理用户的输入:""" - - # 后置提示词 - post_prompt = """ - - 【注意】 - 当本次对话的话题讨论完成时,请使用 `save_memory_and_end_conversation` 工具结束对话并保存重要信息到记忆库,同时更新你的状态到 100_STATUS.md。 - - 【自我认知更新】 - 在对话过程中,如果你发现: - - 自己的角色定位需要调整 - - 性格特点需要改变 - - 能力边界需要重新定义 - - 行为准则需要优化 - 请使用 `update_soul` 工具更新 99_SOUL.md 文件,这有助于你更好地理解自己并为用户提供更精准的服务。""" - - enhanced_input = pre_prompt + "\n\n" + input_text + post_prompt + if is_continuation: + # 递归继续(工具调用后):直接使用输入文本,不重复注入工作流指令 + # system prompt 中已有完整行为规范,此处只需传递工具结果的上下文 + enhanced_input = input_text + else: + # 首轮回话:附加简洁的工作流程提醒(不重复 system prompt 中已有的详细说明) + pre_prompt = "【工作流程提醒】\n1. 如果用户提出新任务,先用 search_skills 搜索是否有现成技能可用\n2. 根据话题相关性决定是否 memory search(简单问候/确认无需搜索)\n3. 信息不足时用 ask_user 提问,不要猜测\n4. 遇到大文件用分页读取(head/tail/start_line/end_line),不要直接打开大文件\n5. 话题完成时用 save_memory_and_end_conversation 保存记忆并结束" + enhanced_input = pre_prompt + "\n\n" + input_text # 记录本轮开始前的消息长度,用于 API 失败时回滚,防止坏消息污染对话历史 msgs_before_turn = len(self.messages) @@ -7146,9 +7130,8 @@ class DeepSeekChat: if output_callback: output_callback("line", i18n.get('analyzing_tool_results') + "\n") - # 【关键修复4】递归调用,让AI基于工具结果继续回答 - # 使用一个新的输入提示,避免重复 - return self.think_and_respond("请基于以上工具执行结果继续回答用户的问题。", output_callback) + # 递归调用,让AI基于工具结果继续回答;is_continuation=True 避免重复注入工作流指令 + return self.think_and_respond("请基于以上工具执行结果继续回答。", output_callback, is_continuation=True) elif full_response: # 正常添加助手消息,包含 reasoning_content