learn-claude-code -s09

📅 2026/7/4 20:00:23
learn-claude-code -s09
一、整体概览s09 在做什么s09 在前面的压缩管道基础上增加了一套持久化、可检索的记忆机制。它的工作流程可以概括为 4 步启动时扫描.memory/目录生成一份索引文件MEMORY.md只包含每条记忆的名称和一句话描述轻量级。每次用户提问前把索引注入SYSTEM提示让 AI 知道有哪些记忆可用。对话过程中每轮 LLM 调用前根据最近对话内容自动选择相关记忆把完整内容插入当前用户消息让 AI 参考。每轮对话结束后分析本轮对话提取新的偏好/事实写入新的记忆文件当记忆文件过多时自动合并压缩。.memory/ MEMORY.md ← 索引一行一条记忆≤200行 user-preference-tabs.md ← 具体记忆文件含 YAML 头部 project-facts.md feedback-rules.md二、关键数据结构与辅助函数1. 记忆文件格式含 YAML frontmatter每个记忆文件如user-preference.md内容如下--- name: user-preference-tabs description: 用户喜欢用空格缩进不用 Tab type: user --- 用户明确要求所有 Python 代码使用 4 个空格缩进并禁止使用 Tab 键。用户明确要求所有 Python 代码使用 4 个空格缩进并禁止使用 Tab 键。name短标识kebab-case用于索引。description一句话摘要显示在索引中约 80 字。typeuser用户偏好、feedback指导意见、project项目事实、reference外部参考。body详细内容Markdown 格式。2. 索引重建函数_rebuild_index()扫描所有.md文件除MEMORY.md外提取name、description生成索引文件text- [user-preference-tabs](user-preference-tabs.md) — 用户喜欢用空格缩进 - [project-python-version](project-python-version.md) — 项目要求 Python 3.11索引只占极少 token每次请求都随SYSTEM发送给模型。三、记忆检索Selective Recall3.1select_relevant_memories(messages, max_items5)这是记忆系统的大脑根据最近的对话内容挑选最相关的记忆文件。实现步骤从messages中提取最近 3 条用户消息的文本最多 2000 字符。构建一个“记忆目录”0: user-preference-tabs — 用户喜欢空格缩进等。首选方式调用一次 LLMclient.messages.create让它从目录中选出相关索引返回 JSON 数组[0, 3]。降级方式如果 LLM 调用失败退化为关键词匹配从最近消息中提取长度 3 的词与每个记忆的name description做子串匹配。为什么用 LLM因为语义匹配比关键词更精准。比如用户说“我不喜欢 Tab”关键词匹配可能找不到“空格”但 LLM 能理解隐含意思。3.2load_memories(messages)调用select_relevant_memories拿到文件名列表后读取每个文件的完整内容并组装成一段被relevant_memories标签包裹的文本。这个文本将在agent_loop中被插入到当前用户消息的内容之前让模型在生成回复时参考。四、记忆提取Memory Extraction4.1extract_memories(messages)在每一轮对话结束后调用分析最近的对话提取新的记忆。流程取最后 10 条消息拼成对话片段role: content。获取现有记忆列表list_memory_files()构造“已有记忆”描述用于去重。构造一个结构化提示要求 LLM 返回 JSON 数组每个元素含name、type、description、body。如果 LLM 返回非空对每个条目调用write_memory_file写入新文件并自动重建索引。巧妙点extract_memories使用的是pre_compress压缩前的消息快照而不是压缩后的消息。因为压缩可能删除细节影响提取质量。我们在agent_loop中在压缩前做了pre_compress messages.copy()。五、记忆合并Consolidation5.1consolidate_memories()当记忆文件数量达到CONSOLIDATE_THRESHOLD 10时触发。作用用一次 LLM 调用将现有记忆合并、去重、压缩保持总数不超过 30 条。过程读取所有记忆文件的完整内容拼成一个长目录。要求 LLM 返回合并后的 JSON 数组。删除所有旧文件根据 LLM 返回结果重新写入新文件。打印合并统计。这相当于一个“记忆整理日”防止记忆无限膨胀用户提问 │ ├─ 1. load_memories() │ │ │ ├─ 1.1 select_relevant_memories() │ │ ├─ 提取最近3条用户消息 │ │ ├─ 调用 LLM (或关键词) 选索引 │ │ └─ 返回文件名列表 │ │ │ ├─ 1.2 对每个文件名: read_memory_file() │ │ └─ 读取硬盘内容 │ │ │ └─ 1.3 拼接成 relevant_memories ... /relevant_memories │ ├─ 2. build_system() │ └─ 读取 MEMORY.md 索引拼进 SYSTEM │ ├─ 3. while True: │ │ │ ├─ 3.1 压缩管道 (L3→L1→L2→L4) │ │ │ ├─ 3.2 注入记忆 (memories_content 用户消息) │ │ │ ├─ 3.3 调用 API │ │ │ ├─ 3.4 若 stop_reason ! tool_use: │ │ ├─ 4. extract_memories(pre_compress) │ │ │ ├─ 取最近10条对话 │ │ │ ├─ 调用 LLM 提取 JSON │ │ │ └─ 对每个记忆: write_memory_file() │ │ │ └─ _rebuild_index() │ │ │ │ │ ├─ 5. consolidate_memories() │ │ │ ├─ 检查文件数 ≥ 10 │ │ │ ├─ 调用 LLM 合并去重 │ │ │ ├─ 删除所有旧文件 │ │ │ └─ 对每个合并结果: write_memory_file() │ │ │ │ │ └─ return │ │ │ └─ 3.5 否则: 执行工具 → 追加结果 → continue │ └─ 返回答案给用户接下来我们从loop的代码看他如何实现在哪一层调用函数MAX_REACTIVE_RETRIES 1 def agent_loop(messages: list): reactive_retries 0 # s09: inject relevant memory content into the current user turn memories_content load_memories(messages) memory_turn len(messages) - 1 if messages and isinstance(messages[-1].get(content), str) else None # s09: build system once per user turn; memory is updated after the loop returns system build_system() while True: # s09: save pre-compression snapshot for accurate memory extraction pre_compress [m if isinstance(m, dict) else {role: m.get(role,), content: str(m.get(content,))} for m in messages] # s08: compression pipeline (budget → snip → micro) messages[:] tool_result_budget(messages) messages[:] snip_compact(messages) messages[:] micro_compact(messages) if estimate_size(messages) CONTEXT_LIMIT: print([auto compact]) messages[:] compact_history(messages) try: request_messages messages if memories_content and memory_turn is not None and memory_turn len(messages): request_messages messages.copy() request_messages[memory_turn] { **messages[memory_turn], content: memories_content \n\n messages[memory_turn][content], } response client.messages.create( modelMODEL, systemsystem, messagesrequest_messages, toolsTOOLS, max_tokens8000 ) reactive_retries 0 except Exception as e: if (prompt_too_long in str(e).lower() or too many tokens in str(e).lower()) and reactive_retries MAX_REACTIVE_RETRIES: print([reactive compact]) messages[:] reactive_compact(messages) reactive_retries 1 continue raise messages.append({role: assistant, content: response.content}) if response.stop_reason ! tool_use: # s09: extract from pre-compression snapshot for full fidelity extract_memories(pre_compress) consolidate_memories() return results [] for block in response.content: if block.type ! tool_use: continue print(f\033[36m {block.name}\033[0m) handler TOOL_HANDLERS.get(block.name) output handler(**block.input) if handler else fUnknown: {block.name} print(str(output)[:200]) results.append({type: tool_result, tool_use_id: block.id, content: output}) messages.append({role: user, content: results})首先看这一章节新加的第一处代码memories_content load_memories(messages)我们看看load_memories函数def load_memories(messages: list) - str: Load relevant memory content for injection into context. selected_files select_relevant_memories(messages) if not selected_files: return parts [relevant_memories] for filename in selected_files: content read_memory_file(filename) if content: parts.append(content) parts.append(/relevant_memories) return \n\n.join(parts)行号代码解释1def load_memories(messages: list) - str:定义函数接收对话历史返回一个字符串。2selected_files select_relevant_memories(messages)调用刚才讲的“图书管理员”拿到它挑出来的文件名列表比如[user-tabs.md, project-python.md]。3-4if not selected_files: return 如果管理员说“没有相关记忆”直接返回空字符串什么都别注入。5parts [relevant_memories]准备一个“包裹”开始打包。第一个元素是开标签告诉 AI“下面开始是相关的记忆内容”。6-8for filename in selected_files:content read_memory_file(filename)if content: parts.append(content)遍历每个选中的文件名调用read_memory_file去硬盘读取对应的.md文件含 YAML 头部和正文。如果读到了内容就把这整份文件内容塞进包裹。9parts.append(/relevant_memories)加一个闭合标签告诉 AI“记忆内容到此结束”。10return \n\n.join(parts)把包裹里的所有碎片开标签、多个记忆正文、闭标签用两个换行连接成一个完整字符串返回。返回值示例relevant_memories --- name: user-preference-tabs description: 用户喜欢用空格缩进不用 Tab type: user --- 用户明确要求所有 Python 代码使用 4 个空格缩进并禁止使用 Tab 键。 --- name: project-python-version description: 项目要求 Python 3.11 type: project --- 本项目强制要求 Python 3.11 或更高版本请勿使用已弃用的语法。 /relevant_memories我们查看load_memories代码中用的select_relevant_memories函数def select_relevant_memories(messages: list, max_items: int 5) - list[str]: Select relevant memory filenames by matching recent conversation against memory names/descriptions. Uses a simple LLM call (or falls back to keyword matching on namedescription). files list_memory_files() if not files: return [] # Collect recent user text for context recent_texts [] for msg in reversed(messages): if msg.get(role) user: content msg.get(content, ) if isinstance(content, list): content .join( str(getattr(b, text, )) for b in content if getattr(b, type, None) text ) if isinstance(content, str): recent_texts.append(content) if len(recent_texts) 3: break recent .join(reversed(recent_texts))[:2000] if not recent.strip(): return [] # Build catalog of name description for LLM to choose from catalog_lines [] for i, f in enumerate(files): catalog_lines.append(f{i}: {f[name]} — {f[description]}) catalog \n.join(catalog_lines) prompt ( Given the recent conversation and the memory catalog below, select the indices of memories that are clearly relevant. Return ONLY a JSON array of integers, e.g. [0, 3]. If none are relevant, return [].\n\n fRecent conversation:\n{recent}\n\n fMemory catalog:\n{catalog} ) try: response client.messages.create( modelMODEL, messages[{role: user, content: prompt}], max_tokens200, ) text extract_text(response.content).strip() # Extract JSON array from response match re.search(r\[.*?\], text, re.DOTALL) if match: indices json.loads(match.group()) selected [] for idx in indices: if isinstance(idx, int) and 0 idx len(files): selected.append(files[idx][filename]) if len(selected) max_items: break return selected except Exception: pass # Fallback: keyword matching on name description keywords [w.lower() for w in recent.split() if len(w) 3] selected [] for f in files: text (f[name] f[description]).lower() if any(kw in text for kw in keywords): selected.append(f[filename]) if len(selected) max_items: break return selected我们先确定这个函数的整体流程输入messages对话历史max_items5最多挑 5 个记忆输出一个文件名列表比如[user-preference-tabs.md, project-python.md]流程先用 LLM最聪明挑选 → 如果失败退化为关键词匹配保守方案接下来在逐行解释代码def select_relevant_memories(messages: list, max_items: int 5) - list[str]: files list_memory_files() if not files: return []第 1 行定义函数参数为messages和max_items返回字符串列表文件名。第 2 行调用list_memory_files()它会扫描.memory/目录返回所有记忆文件的元数据列表每个元素是{filename, name, description, type, body}。第 3-4 行如果没有任何记忆文件直接返回空列表没什么可挑的。recent_texts [] for msg in reversed(messages): if msg.get(role) user: content msg.get(content, ) if isinstance(content, list): content .join( str(getattr(b, text, )) for b in content if getattr(b, type, None) text ) if isinstance(content, str): recent_texts.append(content) if len(recent_texts) 3: break recent .join(reversed(recent_texts))[:2000]第 6 行创建一个空列表recent_texts用来存放最近几条用户消息的纯文本。第 7 行从后往前遍历messagesreversed这样先处理最新的消息。第 8 行只关心role user的消息用户说的话。第 9 行取出消息的content字段。第 10-13 行如果content是列表例如包含工具结果和文本则只提取其中type text的块的text属性用空格连接得到纯文本。第 14-15 行如果得到的content是字符串就把它加入recent_texts。第 16-17 行一旦收集到3 条用户消息就停止收集break。第 18 行把收集到的文本按时间正序连接因为之前是倒序所以reversed(recent_texts)并截取前2000 个字符避免上下文太长存为recent。为什么只取 3 条最近的 3 条用户消息最能代表当前话题而且长度限制 2000 字够用了。if not recent.strip(): return []如果recent是空或只有空白字符说明没有可用的对话内容无法判断相关性返回空。catalog_lines [] for i, f in enumerate(files): catalog_lines.append(f{i}: {f[name]} — {f[description]}) catalog \n.join(catalog_lines)第 23 行准备一个列表用于生成目录文本。第 24-25 行遍历files给每个记忆编号i生成一行像0: user-preference-tabs — 用户喜欢空格缩进这样的描述。第 26 行用换行符拼接所有行得到catalog字符串。这个目录就是给 LLM 看的菜单只包含编号、名称和一句话描述不包含详细内容所以 token 消耗很小。prompt ( Given the recent conversation and the memory catalog below, select the indices of memories that are clearly relevant. Return ONLY a JSON array of integers, e.g. [0, 3]. If none are relevant, return [].\n\n fRecent conversation:\n{recent}\n\n fMemory catalog:\n{catalog} )这是一个明确指令要求 LLM 只返回一个 JSON 数组不包含其他文字。提供Recent conversation最近对话和Memory catalog记忆目录让 LLM 根据语义判断相关性。try: response client.messages.create( modelMODEL, messages[{role: user, content: prompt}], max_tokens200, ) text extract_text(response.content).strip() match re.search(r\[.*?\], text, re.DOTALL) if match: indices json.loads(match.group()) selected [] for idx in indices: if isinstance(idx, int) and 0 idx len(files): selected.append(files[idx][filename]) if len(selected) max_items: break return selected except Exception: pass第 37 行用try包裹防止 LLM 调用失败导致程序崩溃。第 38-41 行调用client.messages.create用prompt作为用户消息不带 system只请求max_tokens200很小因为只需返回短 JSON。第 42 行用extract_text提取返回的纯文本。第 43 行用正则r\[.*?\]在文本中搜索 JSON 数组支持换行re.DOTALL。第 44-51 行如果找到匹配用json.loads解析数组。遍历每个索引检查是否合法整数且在范围内然后通过files[idx][filename]获取文件名。一旦收集到max_items个立即停止。返回文件名列表。第 52 行如果任何环节出错解析失败、网络异常等except Exception: pass静默忽略进入降级方案。keywords [w.lower() for w in recent.split() if len(w) 3] selected [] for f in files: text (f[name] f[description]).lower() if any(kw in text for kw in keywords): selected.append(f[filename]) if len(selected) max_items: break return selected第 54 行从recent中切分出单词过滤掉长度 ≤3 的短词如 the, is将剩下的转为小写作为关键词列表。第 55 行准备空列表selected。第 56-60 行遍历每个记忆文件将name和description拼接成一个字符串转小写。检查是否有任何一个关键词出现在这个字符串中。如果有匹配就把文件名加入selected并检查是否已达到max_items是则break。第 61 行返回最终选中的文件名列表可能为空。特点说明LLM 优先用模型进行语义理解能捕捉隐含关联如“我不喜欢 Tab” - “空格缩进”轻量级目录只传名称和描述给 LLM而不是完整内容极大节省 token降级方案LLM 失败时退化为关键词匹配保证功能不中断限制数量最多取 5 个记忆防止注入过多上下文只取最近 3 条用户消息聚焦当前话题避免噪音长度截断限制 2000 字符控制输入大小JSON 提取要求 LLM 只返回 JSON解析稳定接下来回到loop我们看下一行代码memory_turn len(messages) - 1 if messages and isinstance(messages[-1].get(content), str) else None这一行就是“看看最新记录是不是人话。如果是记住它在第几页如果不是就不记。因为只有‘人话’那页才能往上面贴‘记忆便利贴’。”例子 1正常 本子上有 3 页索引 0、1、2。 你刚在第 3 页索引 2写了“我喜欢用空格”。 程序检查发现内容是文字于是 memory_turn 3 - 1 2。 后面程序就知道“要往第 2 页前面贴记忆”。 例子 2异常 本子上有 3 页索引 0、1、2。 第 3 页索引 2写的是一大串 AI 刚才读文件返回的数据列表格式。 程序检查发现不是普通文字于是 memory_turn None。 后面程序就知道“算了这一页不能贴跳过。”# s09: build system once per user turn; memory is updated after the loop returns system build_system()build_system()是个函数它跑去硬盘里读取.memory/MEMORY.md这个索引文件里面只有记忆的名字和一句话简介。然后它把这句简介拼成一段话比如You are a coding agent... Memories available: \n- user-preference-tabs — 用户喜欢空格缩进。最后这段长文字被赋值给变量system。pre_compress [ m if isinstance(m, dict) else {role: m.get(role,), content: str(m.get(content,))} for m in messages ]这一行代码换一种写法容易看懂pre_compress [] # 准备一个空复印本 for m in messages: # 翻看原件里的每一页 if isinstance(m, dict): # 如果这一页是标准字典格式 page m # 直接照抄这一页复印件 else: # 如果这一页不是字典比如是特殊对象 page { # 我自己手写一张新页 role: m.get(role, ), # 把角色role抄下来 content: str(m.get(content, )) # 把内容转成字符串抄下来 } pre_compress.append(page) # 把这一页放进复印本❓ 为什么要做“复印”这件事原因有两个原因 1防止“原件”被撕坏压缩会删东西紧接在这一行代码后面的就是压缩管道pythonmessages[:] tool_result_budget(messages) # 把大块内容存硬盘换成小纸条 messages[:] snip_compact(messages) # 把中间的对话直接删除如果你不复印等到对话结束你想提取新记忆时messages里已经丢了一大半对话被snip_compact删了提取记忆的 AI 会漏掉用户刚才说的“我喜欢用空格缩进”这种关键信息。复印之后你用pre_compress原件复印件去提取记忆它保留了所有细节不会漏掉任何偏好。原因 2保证“复印件”格式统一不出错messages里的消息有时候是 Python 字典{role:user,content:你好}有时候是 Anthropic API 返回的特殊对象比如Message对象长得像字典但不完全是。extract_memories这个函数提取记忆的只认识标准字典。如果给它一个特殊对象它可能会崩溃。所以这一行代码做了个格式统一如果是字典 → 直接用。如果不是字典 → 强行把它变成字典只取role和content把content转成字符串。try: request_messages messages if memories_content and memory_turn is not None and memory_turn len(messages): request_messages messages.copy() request_messages[memory_turn] { **messages[memory_turn], content: memories_content \n\n messages[memory_turn][content], } response client.messages.create( modelMODEL, systemsystem, messagesrequest_messages, toolsTOOLS, max_tokens8000 ) reactive_retries 0 except Exception as e: if (prompt_too_long in str(e).lower() or too many tokens in str(e).lower()) and reactive_retries MAX_REACTIVE_RETRIES: print([reactive compact]) messages[:] reactive_compact(messages) reactive_retries 1 continue raise第 1 步准备用原稿pythonrequest_messages messages此时request_messages和messages指向同一本档案册。改其中任何一个另一个也会变。第 2 步发现要贴记忆贴纸pythonif ...: request_messages messages.copy()copy()做了什么事它在桌上摊开一本全新的空白草稿纸把messages的内容抄写了一份过去。messages原档案册纹丝不动。request_messages草稿纸是独立的副本。第 3 步在草稿纸上贴贴纸pythonrequest_messages[memory_turn][content] 记忆内容\n\n 原始内容只有草稿纸上的那一页被改写了。原档案册messages上的那一页依然是干净的“帮我写脚本”没有记忆内容。第 4 步把草稿纸交给 AIpythonresponse client.messages.create(messagesrequest_messages)AI 看的是草稿纸带记忆。AI 回答后代码执行messages.append(答复)把答复记入原档案册。草稿纸request_messages用完就丢。结果正式档案册里只有“用户提问”和“AI 答复”没有记忆内容。为什么如果记忆内容也被写进档案册下一轮循环 AI 会看到两次记忆造成记忆膨胀。场景 2出错重试档案册被急救压缩第 1 步AI 报错“提示词太长”pythonexcept ...: messages[:] reactive_compact(messages)这里用的是messages[:] 不是。区别messages 新列表把档案册换成一本新的外面的引用会断导致循环找不到档案册。messages[:] 新列表把原档案册的每一页撕掉换成新页档案册还是那本只是内容变了。第 2 步continue跳回循环开头程序跳回while True。关键点memories_content和memory_turn是在while循环外面定义的它们没有变。所以重试时代码依然会认为“需要贴记忆贴纸”。第 3 步再次执行正常流程这次messages已经变薄了被压缩过。再次执行request_messages messages.copy()抄写一份薄档案册的副本。再次贴记忆贴纸。再次发给 AI。结果因为档案册变薄了这次成功发出去没有超限。第一轮对话假设记忆写进档案册 第 1 步你原始提问 text messages [{role:user, content:帮我写个 Python 脚本}] 第 2 步程序判断需要记忆把记忆贴进档案册假设没有 copy直接改原件 text messages [{role:user, content:记忆用户喜欢用空格缩进\n\n帮我写个 Python 脚本}] 此时档案册里这条消息已经永久变成了“记忆 提问”的混合物。 第 3 步AI 回答完了结束本轮 text messages [ {role:user, content:记忆用户喜欢用空格缩进\n\n帮我写个 Python 脚本}, {role:assistant, content:好的我用空格缩进写了脚本...} ] 第二轮对话你接着问膨胀开始 第 4 步你发新的问题 text messages [ {role:user, content:记忆用户喜欢用空格缩进\n\n帮我写个 Python 脚本}, # 这是上一轮的已经带记忆了 {role:assistant, content:好的我用空格缩进写了脚本...}, {role:user, content:再加一个函数} # 新问题 ] 第 5 步程序又要加载记忆load_memories 被再次调用 程序看最后一条用户消息再加一个函数觉得“空格缩进”这个记忆依然相关。 因为没有 copy()程序会直接在最后一条用户消息上贴新的记忆 text messages [ {role:user, content:记忆用户喜欢用空格缩进\n\n帮我写个 Python 脚本}, # 第一轮留下的记忆第一份 {role:assistant, content:好的我用空格缩进写了脚本...}, {role:user, content:记忆用户喜欢用空格缩进\n\n再加一个函数} # 第二轮贴的记忆第二份 ] 现在你数数档案册里有几份“记忆”内容 → 两份第一轮提问里有一份第二轮提问里又有一份。 第三轮对话继续膨胀 如果第三轮你又问了新问题程序会再次贴记忆变成三份。 最终结果 对话里没有任何“纯用户提问”全都是“记忆提问”的混合物。这些记忆内容越积越多每轮都在叠加上一层最后档案册里大部分内容都是重复的“记忆文本”导致整本对话记录臃肿不堪很快就超过上下文限制因为全是废话。 ✅ 有了 copy() 之后历史保持干净 第一轮结束后的档案册干净 text messages [ {role:user, content:帮我写个 Python 脚本}, # 没有记忆纯提问 {role:assistant, content:好的我用空格缩进写了脚本...} ] 第二轮开始时 复印一份草稿纸。 在草稿纸上的最新提问前面贴记忆。 AI 看到的草稿纸是“记忆 再加一个函数”。 但档案册里只记录纯提问 text messages [ {role:user, content:帮我写个 Python 脚本}, # 纯的 {role:assistant, content:好的我用空格缩进写了脚本...}, {role:user, content:再加一个函数} # 纯的没有记忆 ] 第三轮时档案册里只有两条纯用户提问绝对干净。记忆只出现在“发给 AI 时抄的那张草稿纸上”用完就扔永远不会写进档案册。 一句话解释“膨胀” 记忆被写进档案册后每一轮都会在历史上留一份副本像滚雪球一样越滚越大。 用 copy() 就是为了让记忆只存在于“临时草稿纸”上档案册里永远只存用户原始的、干干净净的话。messages.append({role: assistant, content: response.content}) if response.stop_reason ! tool_use: # s09: extract from pre-compression snapshot for full fidelity extract_memories(pre_compress) consolidate_memories() return 第一步先把 AI 的回复存进档案册pythonmessages.append({role: assistant, content: response.content})不管 AI 是要休息还是要干活程序先把 AI 的这顿“输出”原封不动写进messages档案册里留作永久记录。 岔路 1红灯亮起AI 说“我干完了任务结束”pythonif response.stop_reason ! tool_use: extract_memories(pre_compress) consolidate_memories() return什么时候走进这条路当response.stop_reason是end_turn或stop_sequence不是tool_use时。 这时候程序要干两件“收尾”的事①extract_memories(pre_compress)为什么用pre_compress不用messages因为刚才可能执行了压缩删了中间内容messages已经缺页了。而pre_compress是压缩前拍的照片所有对话细节都在。程序拿着这张“完整照片”去提取用户偏好比如“用户喜欢用空格”才不会漏掉关键信息。②consolidate_memories()检查一下硬盘上的记忆文件是不是太多了超过 10 个。如果是就调用一次 AI把所有记忆合并精简一下比如把 10 个合并成 3 个避免记忆文件夹越来越臃肿。③return彻底退出agent_loop函数回到主菜单等待用户输入下一个问题。相当于“下班了回去歇着”。