34098 字
约 113 分钟
25
第6章 Agent的记忆系统

第6章 Agent的记忆系统

来源:https://ai-agent-guide.xiaofuge.cn/chapters/ch05-memory.html
所属:第二篇-Agent的大脑


让 Agent 拥有短期的专注和长期的经验
📝 先看一个最小记忆例子

如果用户第一轮说"我喜欢简洁风格",第二轮说"帮我写周报",没有记忆的 Agent 会重新追问风格;有记忆的 Agent 会直接按简洁风格输出。

所以你可以把本章先简单理解成一句话:记忆不是为了存很多内容,而是为了让 Agent 在下一轮做出更一致、更省心的决定。

6.1 没有记忆的 Agent 会怎样?

想象一个失忆的助手:你上午告诉他你喜欢简洁的报告,下午他又问你"你需要什么风格?";你刚给了他一份资料,下一秒他就忘了。

这就是没有记忆的 Agent。它只能处理单轮、即时的请求,无法积累经验、无法保持一致性、无法处理需要跨轮引用的复杂任务。
💡 记忆的本质

记忆让 Agent 从"活在当下"变成"有过去、有经验"。它影响 Agent 的连贯性(同一对话内的一致性)、个性化(记住用户偏好)和学习进化(从历史中优化)。

6.2 三层记忆架构

人类有感觉记忆、短期记忆、长期记忆三层结构。Agent 也有类似的设计: | 类型 | 类比 | 实现 | 容量 | 持久性 | | --- | --- | --- | --- | --- | | 短期记忆 | 工作台上的文件 | LLM 上下文窗口 | 4K~200K token | 对话期间 | | 工作记忆 | 便签纸 | Scratchpad / 变量 | 任务相关 | 任务期间 | | 长期记忆 | 图书馆/档案室 | 向量数据库 | 理论无限 | 永久 | ## 6.3 短期记忆的管理策略

短期记忆 = LLM 上下文窗口。对话越长,上下文越大。但窗口有上限,必须管理:

四种管理策略

1

滑窗截断(Sliding Window)

保留最近 N 轮对话,丢弃最旧的消息。简单粗暴,但可能丢失关键早期信息。

←
1 / 4
→
重播

6.4 长期记忆:向量数据库

长期记忆让 Agent 能回忆起几天前甚至几个月前的交互。核心技术是向量数据库 + 语义检索。

主流向量数据库对比 | 数据库 | 类型 | 特点 | 适用场景 | | --- | --- | --- | --- | | Chroma | 嵌入式 | 轻量、Python原生、零配置 | 原型开发、小规模 | | Pinecone | 云服务 | 全托管、高性能、易扩展 | 生产环境、大规模 | | Weaviate | 自部署 | 支持混合搜索、GraphQL | 需要复杂查询 | | Milvus | 分布式 | 高可用、支持十亿级向量 | 超大规模企业 | | pgvector | PostgreSQL扩展 | SQL生态、事务支持 | 已有PG基础设施 | ## 6.5 上下文压缩实战

在长任务中,Agent 的上下文窗口会不断膨胀--每次工具调用返回结果、每次推理步骤、每次对话轮次都在累积 token。这种现象称为上下文膨胀(Context Bloat),也叫上下文腐烂(Context Rot)。
⚠️ Context Rot 的典型症状

  • 行为漂移:Agent 早期遵循的规则(如"用中文回答"),在 50 轮对话后可能被遗忘
  • 注意力稀释:上下文中有太多无关历史,LLM 的注意力被分散,输出质量下降
  • 成本飙升:每次调用都要处理全部历史 token,费用随对话长度线性增长
  • 窗口溢出:超过模型最大上下文长度后,被迫截断或报错

五种压缩策略详解

1

截断(Truncation)

最简单的方案:超过窗口上限时,直接丢弃最早的消息。实现成本为零,但会导致行为不一致--Agent 可能忘记用户在第一轮设定的约束条件。

←
1 / 5
→
重播

压缩前后 Token 对比

# ===== 模拟一次 30 轮对话的上下文膨胀 =====

# 原始对话(无压缩)
raw_tokens_per_turn = 150          # 每轮平均 150 token
turns = 30
total_raw = raw_tokens_per_turn * turns + 500  # +500 为 System Prompt
print(f"原始上下文: {total_raw} tokens")  # 输出: 5000 tokens

# ----- 策略 1: 截断 -----
# 保留最近 10 轮,丢弃其余
truncated = raw_tokens_per_turn * 10 + 500
print(f"截断后:     {truncated} tokens")      # 输出: 2000 tokens
# 损失: 20 轮对话完全丢失,包含用户初始约束条件

# ----- 策略 2: 摘要 -----
# 旧 20 轮压缩为一段摘要(约 200 token)
summarized = 200 + raw_tokens_per_turn * 10 + 500
print(f"摘要后:     {summarized} tokens")      # 输出: 2200 tokens
# 损失: 细节丢失,但保留了关键决策和偏好

# ----- 策略 3: 滑动窗口 -----
# 等同于截断策略(按时间维度)
sliding = truncated
print(f"滑动窗口后: {sliding} tokens")         # 输出: 2000 tokens

# ----- 策略 4: 选择性保留 -----
# 从 30 轮中标记 8 轮为"重要",其余丢弃
selected = raw_tokens_per_turn * 8 + 500 + 150  # +150 为类型标签开销
print(f"选择性保留后: {selected} tokens")       # 输出: 1850 tokens
# 信息密度最高,但可能遗漏看似不重要实则关键的上下文

# ----- 策略 5: 语义保护型压缩 -----
# System Prompt 全量 + 6 个关键行动-结果对 + 12 轮推理压缩为 300 token
semantic = 500 + raw_tokens_per_turn * 6 + 300 + 100  # +100 为因果链标注
print(f"语义保护型后: {semantic} tokens")       # 输出: 1800 tokens
# 最低压缩率 + 最高信息完整性

# ===== 效果总结 =====
print("\n--- 压缩效果对比 ---")
strategies = {
    "原始":      total_raw,
    "截断":      truncated,
    "摘要":      summarized,
    "滑动窗口":  sliding,
    "选择性保留": selected,
    "语义保护型": semantic
}
for name, tokens in strategies.items():
    ratio = tokens / total_raw
    print(f"{name:10s}: {tokens} tokens (压缩率 {ratio:.1%})")

# 输出:
# 原始      : 5000 tokens (压缩率 100.0%)
# 截断      : 2000 tokens (压缩率 40.0%)
# 摘要      : 2200 tokens (压缩率 44.0%)
# 滑动窗口  : 2000 tokens (压缩率 40.0%)
# 选择性保留: 1850 tokens (压缩率 37.0%)
# 语义保护型: 1800 tokens (压缩率 36.0%)
// ===== 模拟一次 30 轮对话的上下文膨胀 =====

// 原始对话(无压缩)
const rawTokensPerTurn = 150;       // 每轮平均 150 token
const turns = 30;
const totalRaw = rawTokensPerTurn * turns + 500; // +500 为 System Prompt
console.log(`原始上下文: ${totalRaw} tokens`);  // 输出: 5000 tokens

// ----- 策略 1: 截断 -----
const truncated = rawTokensPerTurn * 10 + 500;
console.log(`截断后:     ${truncated} tokens`);   // 输出: 2000 tokens

// ----- 策略 2: 摘要 -----
const summarized = 200 + rawTokensPerTurn * 10 + 500;
console.log(`摘要后:     ${summarized} tokens`);   // 输出: 2200 tokens

// ----- 策略 3: 滑动窗口 -----
const sliding = truncated;
console.log(`滑动窗口后: ${sliding} tokens`);      // 输出: 2000 tokens

// ----- 策略 4: 选择性保留 -----
const selected = rawTokensPerTurn * 8 + 500 + 150;
console.log(`选择性保留后: ${selected} tokens`);    // 输出: 1850 tokens

// ----- 策略 5: 语义保护型压缩 -----
const semantic = 500 + rawTokensPerTurn * 6 + 300 + 100;
console.log(`语义保护型后: ${semantic} tokens`);    // 输出: 1800 tokens

// ===== 效果总结 =====
console.log("\n--- 压缩效果对比 ---");
const strategies: Record = {
    "原始":      totalRaw,
    "截断":      truncated,
    "摘要":      summarized,
    "滑动窗口":  sliding,
    "选择性保留": selected,
    "语义保护型": semantic
};
for (const [name, tokens] of Object.entries(strategies)) {
    const ratio = tokens / totalRaw;
    console.log(`${name.padEnd(10)}: ${tokens} tokens (压缩率 ${(ratio * 100).toFixed(1)}%)`);
}

// 输出:
// 原始      : 5000 tokens (压缩率 100.0%)
// 截断      : 2000 tokens (压缩率 40.0%)
// 摘要      : 2200 tokens (压缩率 44.0%)
// 滑动窗口  : 2000 tokens (压缩率 40.0%)
// 选择性保留: 1850 tokens (压缩率 37.0%)
// 语义保护型: 1800 tokens (压缩率 36.0%)
package main

import "fmt"

func main() {
    // ===== 模拟一次 30 轮对话的上下文膨胀 =====

    // 原始对话(无压缩)
    rawTokensPerTurn := 150 // 每轮平均 150 token
    turns := 30
    totalRaw := rawTokensPerTurn*turns + 500 // +500 为 System Prompt
    fmt.Printf("原始上下文: %d tokens\n", totalRaw) // 输出: 5000 tokens

    // ----- 策略 1: 截断 -----
    truncated := rawTokensPerTurn*10 + 500
    fmt.Printf("截断后:     %d tokens\n", truncated) // 输出: 2000 tokens

    // ----- 策略 2: 摘要 -----
    summarized := 200 + rawTokensPerTurn*10 + 500
    fmt.Printf("摘要后:     %d tokens\n", summarized) // 输出: 2200 tokens

    // ----- 策略 3: 滑动窗口 -----
    sliding := truncated
    fmt.Printf("滑动窗口后: %d tokens\n", sliding) // 输出: 2000 tokens

    // ----- 策略 4: 选择性保留 -----
    selected := rawTokensPerTurn*8 + 500 + 150
    fmt.Printf("选择性保留后: %d tokens\n", selected) // 输出: 1850 tokens

    // ----- 策略 5: 语义保护型压缩 -----
    semantic := 500 + rawTokensPerTurn*6 + 300 + 100
    fmt.Printf("语义保护型后: %d tokens\n", semantic) // 输出: 1800 tokens

    // ===== 效果总结 =====
    fmt.Println("\n--- 压缩效果对比 ---")
    strategies := []struct {
        name   string
        tokens int
    }{
        {"原始", totalRaw},
        {"截断", truncated},
        {"摘要", summarized},
        {"滑动窗口", sliding},
        {"选择性保留", selected},
        {"语义保护型", semantic},
    }
    for _, s := range strategies {
        ratio := float64(s.tokens) / float64(totalRaw)
        fmt.Printf("%-10s: %d tokens (压缩率 %.1f%%)\n", s.name, s.tokens, ratio*100)
    }

    // 输出:
    // 原始      : 5000 tokens (压缩率 100.0%)
    // 截断      : 2000 tokens (压缩率 40.0%)
    // 摘要      : 2200 tokens (压缩率 44.0%)
    // 滑动窗口  : 2000 tokens (压缩率 40.0%)
    // 选择性保留: 1850 tokens (压缩率 37.0%)
    // 语义保护型: 1800 tokens (压缩率 36.0%)
}
import java.util.LinkedHashMap;
import java.util.Map;

public class TokenCompressionComparison {
    public static void main(String[] args) {
        // ===== 模拟一次 30 轮对话的上下文膨胀 =====

        // 原始对话(无压缩)
        int rawTokensPerTurn = 150;  // 每轮平均 150 token
        int turns = 30;
        int totalRaw = rawTokensPerTurn * turns + 500; // +500 为 System Prompt
        System.out.printf("原始上下文: %d tokens%n", totalRaw); // 输出: 5000 tokens

        // ----- 策略 1: 截断 -----
        int truncated = rawTokensPerTurn * 10 + 500;
        System.out.printf("截断后:     %d tokens%n", truncated); // 输出: 2000 tokens

        // ----- 策略 2: 摘要 -----
        int summarized = 200 + rawTokensPerTurn * 10 + 500;
        System.out.printf("摘要后:     %d tokens%n", summarized); // 输出: 2200 tokens

        // ----- 策略 3: 滑动窗口 -----
        int sliding = truncated;
        System.out.printf("滑动窗口后: %d tokens%n", sliding); // 输出: 2000 tokens

        // ----- 策略 4: 选择性保留 -----
        int selected = rawTokensPerTurn * 8 + 500 + 150;
        System.out.printf("选择性保留后: %d tokens%n", selected); // 输出: 1850 tokens

        // ----- 策略 5: 语义保护型压缩 -----
        int semantic = 500 + rawTokensPerTurn * 6 + 300 + 100;
        System.out.printf("语义保护型后: %d tokens%n", semantic); // 输出: 1800 tokens

        // ===== 效果总结 =====
        System.out.println("\n--- 压缩效果对比 ---");
        Map strategies = new LinkedHashMap<>();
        strategies.put("原始", totalRaw);
        strategies.put("截断", truncated);
        strategies.put("摘要", summarized);
        strategies.put("滑动窗口", sliding);
        strategies.put("选择性保留", selected);
        strategies.put("语义保护型", semantic);

        for (Map.Entry entry : strategies.entrySet()) {
            double ratio = (double) entry.getValue() / totalRaw;
            System.out.printf("%-10s: %d tokens (压缩率 %.1f%%)%n",
                entry.getKey(), entry.getValue(), ratio * 100);
        }

        // 输出:
        // 原始      : 5000 tokens (压缩率 100.0%)
        // 截断      : 2000 tokens (压缩率 40.0%)
        // 摘要      : 2200 tokens (压缩率 44.0%)
        // 滑动窗口  : 2000 tokens (压缩率 40.0%)
        // 选择性保留: 1850 tokens (压缩率 37.0%)
        // 语义保护型: 1800 tokens (压缩率 36.0%)
    }
}

Solon AI 框架的 SummarizationInterceptor

Solon AI 是一个国产的 Java AI 框架,它提供了 SummarizationInterceptor 作为上下文压缩的内置方案。其核心思路是:当对话轮次超过阈值时,自动触发摘要压缩。

// Solon AI 框架 - 上下文压缩拦截器配置

@Configuration
public class AiConfig {

    @Bean
    public ChatService chatService() {
        return new ChatService.builder()
            .model(openAiModel)                    // LLM 模型
            .interceptor(summarizationInterceptor()) // 添加压缩拦截器
            .build();
    }

    @Bean
    public SummarizationInterceptor summarizationInterceptor() {
        return new SummarizationInterceptor()
            .threshold(10)              // 超过 10 轮对话时触发压缩
            .summaryModel(openAiModel)  // 用哪个模型做摘要(可以用更便宜的模型)
            .preserveSystemPrompt(true) // 保护 System Prompt 不被压缩
            .preserveLastN(3)           // 保留最近 3 轮原始对话
            .summaryPrompt("请将以下对话历史压缩为简洁摘要," +
                           "保留所有关键决策、用户偏好和事实信息。" +
                           "丢弃闲聊和重复内容。");
    }
}

// ===== 工作原理 =====
// 1. 用户每轮对话后,Interceptor 检查当前对话轮次
// 2. 轮次  threshold:
//    - System Prompt 全量保留(preserveSystemPrompt=true)
//    - 最近 3 轮完整保留(preserveLastN=3)
//    - 其余轮次调用 summaryModel 生成摘要
//    - 摘要替代原始对话,拼接到上下文中

// ===== 压缩前后对比 =====
// 压缩前: [System] + [15轮原始对话] = ~3000 tokens
// 压缩后: [System] + [摘要~200tokens] + [最近3轮] = ~1150 tokens
// 压缩率: 38.3%,且核心指令和近期上下文无损保留
// // Solon AI 框架 - 上下文压缩拦截器配置

  // @Configuration
  // public class AiConfig {

  // @Bean
  // public ChatService chatService() {
  return new ChatService.builder();
  // .model(openAiModel)                    // LLM 模型
  // .interceptor(summarizationInterceptor()) // 添加压缩拦截器
  // .build();
  // }

  // @Bean
  // public SummarizationInterceptor summarizationInterceptor() {
  return new SummarizationInterceptor();
  // .threshold(10)              // 超过 10 轮对话时触发压缩
  // .summaryModel(openAiModel)  // 用哪个模型做摘要(可以用更便宜的模型)
  // .preserveSystemPrompt(true) // 保护 System Prompt 不被压缩
  // .preserveLastN(3)           // 保留最近 3 轮原始对话
  // .summaryPrompt("请将以下对话历史压缩为简洁摘要," +
  // "保留所有关键决策、用户偏好和事实信息。" +
  // "丢弃闲聊和重复内容。");
  // }
  // }

  // // ===== 工作原理 =====
  // // 1. 用户每轮对话后,Interceptor 检查当前对话轮次
  // // 2. 轮次  threshold:
  // //    - System Prompt 全量保留(preserveSystemPrompt=true)
  // //    - 最近 3 轮完整保留(preserveLastN=3)
  // //    - 其余轮次调用 summaryModel 生成摘要
  // //    - 摘要替代原始对话,拼接到上下文中

  // // ===== 压缩前后对比 =====
  // // 压缩前: [System] + [15轮原始对话] = ~3000 tokens
  // // 压缩后: [System] + [摘要~200tokens] + [最近3轮] = ~1150 tokens
  // // 压缩率: 38.3%,且核心指令和近期上下文无损保留
package main

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
)

	// Python: // Solon AI 框架 - 上下文压缩拦截器配置

	// Python: public class AiConfig {

	// Python: public ChatService chatService() {
	return new ChatService.builder()
	// Python: .model(openAiModel)                    // LLM 模型
	// Python: .interceptor(summarizationInterceptor()) // 添加压缩拦截器
	// Python: .build();
	// Python: }

	// Python: public SummarizationInterceptor summarizationInterceptor() {
	return new SummarizationInterceptor()
	// Python: .threshold(10)              // 超过 10 轮对话时触发压缩
	// Python: .summaryModel(openAiModel)  // 用哪个模型做摘要(可以用更便宜的模型)
	// Python: .preserveSystemPrompt(true) // 保护 System Prompt 不被压缩
	// Python: .preserveLastN(3)           // 保留最近 3 轮原始对话
	// Python: .summaryPrompt("请将以下对话历史压缩为简洁摘要," +
	// Python: "保留所有关键决策、用户偏好和事实信息。" +
	// Python: "丢弃闲聊和重复内容。");
	// Python: }
	// Python: }

	// Python: // ===== 工作原理 =====
	// Python: // 1. 用户每轮对话后,Interceptor 检查当前对话轮次
	// Python: // 2. 轮次  threshold:
	// Python: //    - System Prompt 全量保留(preserveSystemPrompt=true)
	// Python: //    - 最近 3 轮完整保留(preserveLastN=3)
	// Python: //    - 其余轮次调用 summaryModel 生成摘要
	// Python: //    - 摘要替代原始对话,拼接到上下文中

	// Python: // ===== 压缩前后对比 =====
	// Python: // 压缩前: [System] + [15轮原始对话] = ~3000 tokens
	// Python: // 压缩后: [System] + [摘要~200tokens] + [最近3轮] = ~1150 tokens
	// Python: // 压缩率: 38.3%,且核心指令和近期上下文无损保留
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;

        // Python: // Solon AI 框架 - 上下文压缩拦截器配置

        // Python: public class AiConfig {

        // Python: public ChatService chatService() {
        return new ChatService.builder();
        // Python: .model(openAiModel)                    // LLM 模型
        // Python: .interceptor(summarizationInterceptor()) // 添加压缩拦截器
        // Python: .build();
        // Python: }

        // Python: public SummarizationInterceptor summarizationInterceptor() {
        return new SummarizationInterceptor();
        // Python: .threshold(10)              // 超过 10 轮对话时触发压缩
        // Python: .summaryModel(openAiModel)  // 用哪个模型做摘要(可以用更便宜的模型)
        // Python: .preserveSystemPrompt(true) // 保护 System Prompt 不被压缩
        // Python: .preserveLastN(3)           // 保留最近 3 轮原始对话
        // Python: .summaryPrompt("请将以下对话历史压缩为简洁摘要," +
        // Python: "保留所有关键决策、用户偏好和事实信息。" +
        // Python: "丢弃闲聊和重复内容。");
        // Python: }
        // Python: }

        // Python: // ===== 工作原理 =====
        // Python: // 1. 用户每轮对话后,Interceptor 检查当前对话轮次
        // Python: // 2. 轮次  threshold:
        // Python: //    - System Prompt 全量保留(preserveSystemPrompt=true)
        // Python: //    - 最近 3 轮完整保留(preserveLastN=3)
        // Python: //    - 其余轮次调用 summaryModel 生成摘要
        // Python: //    - 摘要替代原始对话,拼接到上下文中

        // Python: // ===== 压缩前后对比 =====
        // Python: // 压缩前: [System] + [15轮原始对话] = ~3000 tokens
        // Python: // 压缩后: [System] + [摘要~200tokens] + [最近3轮] = ~1150 tokens
        // Python: // 压缩率: 38.3%,且核心指令和近期上下文无损保留
}

压缩不是删减,而是蒸馏。好的压缩策略就像化学蒸馏--把混合物中的精华提取出来,丢弃的是杂质而非有效成分。

6.6 记忆架构实战

选择合适的向量数据库和 Embedding 模型是构建记忆系统的关键决策。本节提供完整的选型对比和架构实现。

向量数据库深度对比 | 数据库 | 部署方式 | 最大向量数 | 混合搜索 | 过滤查询 | 事务支持 | 成本 | 适用场景 | | --- | --- | --- | --- | --- | --- | --- | --- | | Chroma | 嵌入式/内存 | ~百万级 | ❌ | 基础 | ❌ | 免费 | 原型/小规模 | | Pinecone | 全托管云服务 | 亿级 | ✅(稀疏+稠密) | 丰富 | ❌ | 按量计费 | 生产/大规模 | | Weaviate | 自部署/Docker | 千万级 | ✅(BM25+向量) | GraphQL | ❌ | 免费/云付费 | 复杂查询场景 | | Milvus | 分布式集群 | 十亿级 | ✅ | 丰富 | ❌ | 免费/运维成本高 | 超大规模企业 | | pgvector | PG扩展/Docker | 百万级 | ✅(全文+向量) | SQL全能力 | ✅ | 免费/低成本 | 已有PG基础设施 | 🎯 选型决策树

  • 快速原型验证 → Chroma(零配置,pip install 即用)
  • 不想运维 → Pinecone(全托管,按量付费,注册即用)
  • 需要混合搜索(关键词+语义) → Weaviate(BM25+向量双路召回)
  • 已有 PostgreSQL 且数据量不大 → pgvector(不用引入新组件,SQL+向量一体化)
  • 十亿级向量 + 高可用 → Milvus(分布式架构,专门为超大规模设计)

完整记忆系统架构设计

短期/长期/工作记忆协同代码实现

import chromadb
from openai import OpenAI

class MemorySystem:
    """三层记忆协同系统"""

    def __init__(self, embedding_model="text-embedding-3-small"):
        self.client = OpenAI()
        self.chroma = chromadb.PersistentClient(path="./memory_db")
        self.collection = self.chroma.get_or_create_collection(
            name="agent_memory",
            metadata={"hnsw:space": "cosine"}
        )
        self.short_term = []     # 短期记忆:对话历史列表
        self.working = {}        # 工作记忆:当前任务的中间变量
        self.compress_threshold = 15  # 超过 15 轮触发压缩

    # ===== 短期记忆管理 =====
    def add_to_short_term(self, role, content):
        """添加一条消息到短期记忆"""
        self.short_term.append({"role": role, "content": content})

        # 检查是否需要压缩
        if len(self.short_term) > self.compress_threshold:
            self._compress_short_term()

    def _compress_short_term(self):
        """压缩短期记忆:摘要 + 转存长期记忆"""
        # 保留最近 3 轮和 System Prompt
        system_msgs = [m for m in self.short_term if m["role"] == "system"]
        recent_msgs = self.short_term[-6:]  # 最近 3 轮(每轮2条)

        # 其余消息交给 LLM 生成摘要
        old_msgs = self.short_term[len(system_msgs):-6]
        if old_msgs:
            summary = self._summarize(old_msgs)

            # 摘要存入长期记忆(向量数据库)
            self._save_to_long_term(
                text=summary,
                metadata={"type": "summary", "turn_range":
                          f"turns {len(system_msgs)}-{len(self.short_term)-6}"}
            )

            # 用摘要替代原始对话
            self.short_term = system_msgs + [
                {"role": "system", "content": f"[历史摘要]: {summary}"}
            ] + recent_msgs

    def _summarize(self, messages):
        """用 LLM 对旧对话生成摘要"""
        text = "\n".join([f"{m['role']}: {m['content']}" for m in messages])
        response = self.client.chat.completions.create(
            model="gpt-4o-mini",  # 用便宜模型做摘要
            messages=[
                {"role": "system", "content":
                 "将以下对话压缩为简洁摘要。保留所有关键决策、"
                 "用户偏好、事实信息和约束条件。丢弃闲聊和重复内容。"},
                {"role": "user", "content": text}
            ],
            max_tokens=300
        )
        return response.choices[0].message.content

    # ===== 长期记忆管理 =====
    def _save_to_long_term(self, text, metadata=None):
        """将内容存入长期记忆(向量数据库)"""
        vector = self._get_embedding(text)
        doc_id = f"mem_{len(self.short_term)}_{hash(text) % 10000}"
        self.collection.upsert(
            ids=[doc_id],
            embeddings=[vector],
            documents=[text],
            metadatas=[metadata or {}]
        )

    def retrieve_from_long_term(self, query, top_k=5):
        """从长期记忆中检索相关内容"""
        results = self.collection.query(
            query_texts=[query],
            n_results=top_k,
            include=["documents", "metadatas", "distances"]
        )
        memories = []
        for doc, meta, dist in zip(
            results["documents"][0],
            results["metadatas"][0],
            results["distances"][0]
        ):
            if dist  this.compressThreshold) {
      this.compressShortTerm();
    }
  }

  private async compressShortTerm(): Promise {
    const systemMsgs = this.shortTerm.filter(m => m.role === 'system');
    const recentMsgs = this.shortTerm.slice(-6);
    const oldMsgs = this.shortTerm.slice(systemMsgs.length, -6);

    if (oldMsgs.length > 0) {
      const summary = await this.summarize(oldMsgs);
      this.saveToLongTerm(summary, {
        type: 'summary',
        turnRange: `turns ${systemMsgs.length}-${this.shortTerm.length - 6}`
      });
      this.shortTerm = [
        ...systemMsgs,
        { role: 'system', content: `[历史摘要]: ${summary}` },
        ...recentMsgs
      ];
    }
  }

  private async summarize(messages: Message[]): Promise {
    const text = messages.map(m => `${m.role}: ${m.content}`).join('\n');
    const response = await this.client.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [
        { role: 'system', content:
          '将以下对话压缩为简洁摘要。保留所有关键决策、用户偏好、事实信息和约束条件。丢弃闲聊和重复内容。' },
        { role: 'user', content: text }
      ],
      max_tokens: 300
    });
    return response.choices[0].message.content || '';
  }

  // ===== 长期记忆管理 =====
  async saveToLongTerm(text: string, metadata?: Record): Promise {
    const vector = await this.getEmbedding(text);
    const docId = `mem_${this.shortTerm.length}_${text.length % 10000}`;
    this.collection.push({ id: docId, embedding: vector, document: text, metadata: metadata || {} });
  }

  async retrieveFromLongTerm(query: string, topK = 5): Promise {
    const queryVector = await this.getEmbedding(query);
    // Simplified: in production use proper vector similarity search
    const results = this.collection.slice(0, topK);
    return results.filter(r => true).map(r => ({
      text: r.document,
      metadata: r.metadata,
      similarity: 0.85
    }));
  }

  // ===== 工作记忆管理 =====
  updateWorking(key: string, value: any): void { this.working[key] = value; }
  getWorking(key: string): any { return this.working[key]; }
  clearWorking(): void { this.working = {}; }

  // ===== Embedding =====
  private async getEmbedding(text: string): Promise {
    const response = await this.client.embeddings.create({
      model: 'text-embedding-3-small',
      input: text
    });
    return response.data[0].embedding;
  }

  // ===== 完整响应流程 =====
  async respond(userInput: string): Promise {
    const longMemories = await this.retrieveFromLongTerm(userInput, 3);
    const memoryContext = longMemories.map(m => m.text).join('\n');
    const workingContext = Object.entries(this.working)
      .map(([k, v]) => `${k}: ${v}`).join('\n');

    const messages: Message[] = [
      ...this.shortTerm,
      { role: 'system', content:
        `[相关历史记忆]\n${memoryContext}\n\n[当前任务状态]\n${workingContext}` },
      { role: 'user', content: userInput }
    ];

    const response = await this.client.chat.completions.create({
      model: 'gpt-4o',
      messages: messages as any
    });
    const answer = response.choices[0].message.content || '';

    this.addToShortTerm('user', userInput);
    this.addToShortTerm('assistant', answer);
    await this.saveToLongTerm(
      `用户: ${userInput}\n助手: ${answer}`,
      { type: 'interaction' }
    );

    return answer;
  }
}

// ===== 使用示例 =====
const memory = new MemorySystem();
const answer = await memory.respond("帮我分析一下最近的销售数据趋势");
console.log(answer);
package main

import (
    "context"
    "fmt"
    "log"

    openai "github.com/sashabaranov/go-openai"
)

// MemorySystem 三层记忆协同系统
type MemorySystem struct {
    client           *openai.Client
    collection       []MemoryEntry // 简化的向量存储
    shortTerm        []Message
    working          map[string]string
    compressThreshold int
}

type Message struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type MemoryEntry struct {
    ID        string
    Embedding []float32
    Document  string
    Metadata  map[string]string
}

type Memory struct {
    Text       string
    Metadata   map[string]string
    Similarity float64
}

func NewMemorySystem() *MemorySystem {
    return &MemorySystem{
        client:            openai.NewClient(),
        collection:        []MemoryEntry{},
        shortTerm:         []Message{},
        working:           make(map[string]string),
        compressThreshold: 15,
    }
}

// ===== 短期记忆管理 =====
func (m *MemorySystem) AddToShortTerm(role, content string) {
    m.shortTerm = append(m.shortTerm, Message{Role: role, Content: content})
    if len(m.shortTerm) > m.compressThreshold {
        m.compressShortTerm()
    }
}

func (m *MemorySystem) compressShortTerm() {
    var systemMsgs, recentMsgs, oldMsgs []Message
    for _, msg := range m.shortTerm {
        if msg.Role == "system" {
            systemMsgs = append(systemMsgs, msg)
        }
    }
    if len(m.shortTerm) >= 6 {
        recentMsgs = m.shortTerm[len(m.shortTerm)-6:]
        oldMsgs = m.shortTerm[len(systemMsgs) : len(m.shortTerm)-6]
    }

    if len(oldMsgs) > 0 {
        summary := m.summarize(oldMsgs)
        m.saveToLongTerm(summary, map[string]string{
            "type":       "summary",
            "turn_range": fmt.Sprintf("turns %d-%d", len(systemMsgs), len(m.shortTerm)-6),
        })
        m.shortTerm = append(systemMsgs,
            Message{Role: "system", Content: fmt.Sprintf("[历史摘要]: %s", summary)})
        m.shortTerm = append(m.shortTerm, recentMsgs...)
    }
}

func (m *MemorySystem) summarize(messages []Message) string {
    var text string
    for _, msg := range messages {
        text += fmt.Sprintf("%s: %s\n", msg.Role, msg.Content)
    }
    resp, err := m.client.CreateChatCompletion(context.Background(),
        openai.ChatCompletionRequest{
            Model: openai.GPT4oMini,
            Messages: []openai.ChatCompletionMessage{
                {Role: "system", Content: "将以下对话压缩为简洁摘要。保留所有关键决策、用户偏好、事实信息和约束条件。丢弃闲聊和重复内容。"},
                {Role: "user", Content: text},
            },
            MaxTokens: 300,
        })
    if err != nil {
        log.Printf("summarize error: %v", err)
        return ""
    }
    return resp.Choices[0].Message.Content
}

// ===== 长期记忆管理 =====
func (m *MemorySystem) saveToLongTerm(text string, metadata map[string]string) {
    vector := m.getEmbedding(text)
    docID := fmt.Sprintf("mem_%d_%d", len(m.shortTerm), len(text)%10000)
    m.collection = append(m.collection, MemoryEntry{
        ID: docID, Embedding: vector, Document: text, Metadata: metadata,
    })
}

func (m *MemorySystem) retrieveFromLongTerm(query string, topK int) []Memory {
    _ = m.getEmbedding(query) // 简化:实际应做向量相似度搜索
    var memories []Memory
    for i, entry := range m.collection {
        if i >= topK {
            break
        }
        memories = append(memories, Memory{
            Text: entry.Document, Metadata: entry.Metadata, Similarity: 0.85,
        })
    }
    return memories
}

// ===== 工作记忆管理 =====
func (m *MemorySystem) updateWorking(key, value string) { m.working[key] = value }
func (m *MemorySystem) getWorking(key string) string    { return m.working[key] }
func (m *MemorySystem) clearWorking()                   { m.working = make(map[string]string) }

// ===== Embedding =====
func (m *MemorySystem) getEmbedding(text string) []float32 {
    resp, err := m.client.CreateEmbeddings(context.Background(),
        openai.EmbeddingRequest{
            Model: openai.SmallEmbedding3,
            Input: []string{text},
        })
    if err != nil {
        log.Printf("embedding error: %v", err)
        return nil
    }
    return resp.Data[0].Embedding
}

// ===== 完整响应流程 =====
func (m *MemorySystem) respond(userInput string) string {
    longMemories := m.retrieveFromLongTerm(userInput, 3)
    var memoryContext string
    for _, mem := range longMemories {
        memoryContext += mem.Text + "\n"
    }
    var workingContext string
    for k, v := range m.working {
        workingContext += fmt.Sprintf("%s: %s\n", k, v)
    }

    messages := append(m.shortTerm,
        Message{Role: "system", Content: fmt.Sprintf("[相关历史记忆]\n%s\n\n[当前任务状态]\n%s", memoryContext, workingContext)},
        Message{Role: "user", Content: userInput},
    )

    resp, err := m.client.CreateChatCompletion(context.Background(),
        openai.ChatCompletionRequest{
            Model:    openai.GPT4o,
            Messages: toOpenAIMessages(messages),
        })
    if err != nil {
        log.Printf("respond error: %v", err)
        return ""
    }
    answer := resp.Choices[0].Message.Content

    m.AddToShortTerm("user", userInput)
    m.AddToShortTerm("assistant", answer)
    m.saveToLongTerm(fmt.Sprintf("用户: %s\n助手: %s", userInput, answer),
        map[string]string{"type": "interaction"})

    return answer
}

func toOpenAIMessages(msgs []Message) []openai.ChatCompletionMessage {
    result := make([]openai.ChatCompletionMessage, len(msgs))
    for i, m := range msgs {
        result[i] = openai.ChatCompletionMessage{Role: m.Role, Content: m.Content}
    }
    return result
}

// ===== 使用示例 =====
func main() {
    memory := NewMemorySystem()
    answer := memory.respond("帮我分析一下最近的销售数据趋势")
    fmt.Println(answer)
}
import com.openai.client.OpenAIClient;
import com.openai.models.*;
import java.util.*;

public class MemorySystem {
    private OpenAIClient client;
    private List collection = new ArrayList<>();
    private List shortTerm = new ArrayList<>();
    private Map working = new HashMap<>();
    private int compressThreshold = 15;

    static class Message {
        String role;
        String content;
        Message(String role, String content) { this.role = role; this.content = content; }
    }

    static class MemoryEntry {
        String id;
        String document;
        Map metadata;
    }

    static class Memory {
        String text;
        Map metadata;
        double similarity;
    }

    public MemorySystem() {
        this.client = new OpenAIClient();
    }

    // ===== 短期记忆管理 =====
    public void addToShortTerm(String role, String content) {
        shortTerm.add(new Message(role, content));
        if (shortTerm.size() > compressThreshold) {
            compressShortTerm();
        }
    }

    private void compressShortTerm() {
        List systemMsgs = new ArrayList<>();
        for (Message m : shortTerm) {
            if ("system".equals(m.role)) systemMsgs.add(m);
        }
        List recentMsgs = shortTerm.subList(
            Math.max(shortTerm.size() - 6, 0), shortTerm.size());
        List oldMsgs = shortTerm.subList(
            systemMsgs.size(), Math.max(shortTerm.size() - 6, systemMsgs.size()));

        if (!oldMsgs.isEmpty()) {
            String summary = summarize(oldMsgs);
            saveToLongTerm(summary, Map.of("type", "summary"));
            shortTerm.clear();
            shortTerm.addAll(systemMsgs);
            shortTerm.add(new Message("system", "[历史摘要]: " + summary));
            shortTerm.addAll(recentMsgs);
        }
    }

    private String summarize(List messages) {
        StringBuilder text = new StringBuilder();
        for (Message m : messages) {
            text.append(m.role).append(": ").append(m.content).append("\n");
        }
        ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
            .model("gpt-4o-mini")
            .addMessage(ChatCompletionMessageParam.builder()
                .role(ChatCompletionMessageParam.Role.SYSTEM)
                .content("将以下对话压缩为简洁摘要。保留所有关键决策、用户偏好、事实信息和约束条件。")
                .build())
            .addMessage(ChatCompletionMessageParam.builder()
                .role(ChatCompletionMessageParam.Role.USER)
                .content(text.toString())
                .build())
            .maxCompletionTokens(300)
            .build();
        ChatCompletion resp = client.chat().completions().create(params);
        return resp.choices().get(0).message().content().orElse("");
    }

    // ===== 长期记忆管理 =====
    public void saveToLongTerm(String text, Map metadata) {
        MemoryEntry entry = new MemoryEntry();
        entry.id = "mem_" + shortTerm.size() + "_" + (text.hashCode() % 10000);
        entry.document = text;
        entry.metadata = metadata != null ? metadata : new HashMap<>();
        collection.add(entry);
    }

    public List retrieveFromLongTerm(String query, int topK) {
        List memories = new ArrayList<>();
        for (int i = 0; i (shortTerm);
        messages.add(new Message("system",
            "[相关历史记忆]\n" + memoryContext + "\n[当前任务状态]\n" + workingContext));
        messages.add(new Message("user", userInput));

        // 调用 LLM 生成响应
        String answer = callLLM(messages);

        addToShortTerm("user", userInput);
        addToShortTerm("assistant", answer);
        saveToLongTerm("用户: " + userInput + "\n助手: " + answer,
            Map.of("type", "interaction"));

        return answer;
    }

    private String callLLM(List messages) {
        // Simplified - build params and call OpenAI
        return "Response from LLM";
    }

    // ===== 使用示例 =====
    public static void main(String[] args) {
        MemorySystem memory = new MemorySystem();
        String answer = memory.respond("帮我分析一下最近的销售数据趋势");
        System.out.println(answer);
    }
}

Embedding 模型选择 | 模型 | 提供商 | 维度 | 质量 | 成本 | 延迟 | 适用场景 | | --- | --- | --- | --- | --- | --- | --- | | text-embedding-3-large | OpenAI | 3072 | ⭐⭐⭐⭐⭐ | $0.13/1M | ~100ms | 高���度检索 | | text-embedding-3-small | OpenAI | 1536 | ⭐⭐⭐⭐ | $0.02/1M | ~50ms | 通用场景 | | bge-large-zh-v1.5 | BAAI(本地) | 1024 | ⭐⭐⭐⭐ | 免费 | ~20ms | 中文场景最佳 | | all-MiniLM-L6-v2 | SentenceTransformers | 384 | ⭐⭐⭐ | 免费 | ~5ms | 快速原型/轻量 | 💡 Embedding 选型建议

  • 中文为主 → BGE 系列(BAAI 北京智源),中文语义理解显著优于 OpenAI 模型
  • 成本敏感 → 本地模型(bge / MiniLM),零 API 费用,但需要 GPU 或 CPU 推理
  • 追求最高质量 → OpenAI text-embedding-3-large,MTEB 排行榜领先
  • 平衡成本与质量 → OpenAI text-embedding-3-small,性价比最优

6.7 反思记忆与情景记忆

前面介绍的三层记忆架构解决了存储和检索问题,但记忆系统还有更高阶的能力:从经验中学习(反思记忆)和记录完整事件(情景记忆)。

🪞 反思记忆(Reflective Memory)

从执行结果中提炼经验教训。不是存储原始对话,而是存储"我学到了什么"。

例:任务失败后反思 → "查询天气 API 时需要加超时处理,否则会卡死整个流程"

📖 情景记忆(Episodic Memory)

记录具体事件的完整上下文:谁、什么、何时、何地、为什么、结果如何。

例:记录完整事件 → "2024-03-15,用户请求分析报告,用了 Python 分析工具,耗时 3 分钟,输出 500 字摘要,用户满意"

与三层记忆的关系

反思记忆和情景记忆都存储在长期记忆(向量数据库)中,但它们的元数据标签不同,检索时可以按类型过滤: | 记忆类型 | 存储内容 | 元数据标签 | 检索场景 | | --- | --- | --- | --- | | 短期记忆 | 原始对话轮次 | 无(直接拼接) | 当前对话上下文 | | 反思记忆 | 提炼的经验教训 | {type: "reflection", task: "xxx"} | 遇到相似任务时参考经验 | | 情景记忆 | 完整事件的5W1H | {type: "episodic", timestamp: "xxx"} | 需要历史上下文时参考事件 | | 工作记忆 | 当前任务中间变量 | 无(Scratchpad) | 多步推理过程中的状态 | ### Reflexion 框架的反思循环

Reflexion 是一个经典的反思型 Agent 框架,其核心创新在于引入反思循环:Agent 执行任务后,如果结果不理想,会进行自我反思,将反思结果存入记忆,下次执行时参考这些经验。

class ReflexionAgent:
    """基于 Reflexion 框架的反思型 Agent"""

    def __init__(self, llm, memory_system):
        self.llm = llm
        self.memory = memory_system
        self.max_retries = 3  # 最大反思重试次数

    def execute_task(self, task_description):
        """执行任务,失败时触发反思循环"""

        for attempt in range(self.max_retries):
            # 1. 检索相关反思经验
            past_reflections = self.memory.retrieve_from_long_term(
                query=task_description,
                top_k=3
            )
            reflection_context = "\n".join([
                f"[过往经验 #{i+1}]: {r['text']}"
                for i, r in enumerate(past_reflections)
                if r.get('metadata', {}).get('type') == 'reflection'
            ])

            # 2. 构建带反思经验的 prompt
            prompt = f"""
            任务: {task_description}

            {f'过往反思经验:\n{reflection_context}' if reflection_context else '这是首次尝试此任务。'}

            请执行任务,注意避免过去犯过的错误。
            """

            # 3. 执行 ReAct 循环
            result = self._react_loop(prompt)

            # 4. 评估结果
            evaluation = self._evaluate(task_description, result)

            if evaluation["success"]:
                # 成功 → 记录为情景记忆
                self.memory._save_to_long_term(
                    text=f"任务成功: {task_description}\n方法: {result['approach']}\n结果: {result['output']}",
                    metadata={"type": "episodic", "outcome": "success"}
                )
                return result

            # 5. 失败 → 触发反思
            reflection = self._reflect(task_description, result, evaluation)

            # 6. 将反思存入记忆
            self.memory._save_to_long_term(
                text=reflection,
                metadata={
                    "type": "reflection",
                    "task": task_description,
                    "attempt": attempt + 1,
                    "outcome": "failure"
                }
            )

            print(f"第 {attempt+1} 次尝试失败,反思已存入记忆")

        return {"output": "任务失败,已达最大重试次数", "success": False}

    def _reflect(self, task, result, evaluation):
        """对失败结果进行反思"""
        reflect_prompt = f"""
        任务: {task}
        我的行动: {result['actions_taken']}
        得到的结果: {result['output']}
        评估反馈: {evaluation['feedback']}

        请反思以下问题:
        1. 我哪里做错了?具体是哪个步骤出了问题?
        2. 为什么会犯这个错误?根本原因是什么?
        3. 下次应该怎么避免?给出具体的改进策略。

        用简洁的要点总结你的反思。
        """
        response = self.llm.generate(reflect_prompt)
        return response

    def _evaluate(self, task, result):
        """用 LLM 评估任务结果"""
        eval_prompt = f"""
        任务: {task}
        结果: {result['output']}

        这个结果是否成功完成了任务?请判断:
- 成功: 返回 JSON {"success": true}
- 失败: 返回 JSON {"success": false, "feedback": "具体问题描述"}
        """
        response = self.llm.generate(eval_prompt)
        # 解析评估结果...
        return {"success": False, "feedback": "结果不完整"}  # 简化示例
class ReflexionAgent {
  private llm: { generate: (prompt: string) => Promise };
  private memory: MemorySystem;
  private maxRetries = 3;

  constructor(llm: any, memorySystem: MemorySystem) {
    this.llm = llm;
    this.memory = memorySystem;
  }

  async executeTask(taskDescription: string): Promise {
    for (let attempt = 0; attempt  r.metadata?.type === 'reflection')
        .map((r, i) => `[过往经验 #${i + 1}]: ${r.text}`)
        .join('\n');

      // 2. 构建带反思经验的 prompt
      const prompt = `任务: ${taskDescription}\n\n` +
        (reflectionContext ? `过往反思经验:\n${reflectionContext}` : '这是首次尝试此任务。') +
        '\n\n请执行任务,注意避免过去犯过的错误。';

      // 3. 执行 ReAct 循环
      const result = await this.reactLoop(prompt);

      // 4. 评估结果
      const evaluation = await this.evaluate(taskDescription, result);

      if (evaluation.success) {
        await this.memory.saveToLongTerm(
          `任务成功: ${taskDescription}\n方法: ${result.approach}\n结果: ${result.output}`,
          { type: 'episodic', outcome: 'success' }
        );
        return result;
      }

      // 5. 失败 → 触发反思
      const reflection = await this.reflect(taskDescription, result, evaluation);

      // 6. 将反思存入记忆
      await this.memory.saveToLongTerm(reflection, {
        type: 'reflection', task: taskDescription, attempt: attempt + 1, outcome: 'failure'
      });

      console.log(`第 ${attempt + 1} 次尝试失败,反思已存入记忆`);
    }
    return { output: '任务失败,已达最大重试次数', success: false };
  }

  private async reflect(task: string, result: any, evaluation: any): Promise {
    const prompt = `任务: ${task}\n我的行动: ${result.actions_taken}\n` +
      `得到的结果: ${result.output}\n评估反馈: ${evaluation.feedback}\n\n` +
      '请反思:\n1. 哪里做错了?\n2. 为什么会犯这个错误?\n3. 下次怎么避免?';
    return await this.llm.generate(prompt);
  }

  private async evaluate(task: string, result: any): Promise {
    const prompt = `任务: ${task}\n结果: ${result.output}\n这个结果是否成功完成任务?`;
    const response = await this.llm.generate(prompt);
    return { success: false, feedback: '结果不完整' };
  }

  private async reactLoop(prompt: string): Promise {
    const response = await this.llm.generate(prompt);
    return { output: response, approach: 'ReAct', actions_taken: 'execute' };
  }
}
package main

import "fmt"

// ReflexionAgent 基于Reflexion框架的反思型Agent
type ReflexionAgent struct {
    llm        LLMInterface
    memory     *MemorySystem
    maxRetries int
}

type LLMInterface interface {
    Generate(prompt string) string
}

type Evaluation struct {
    Success  bool
    Feedback string
}

type TaskResult struct {
    Output       string
    Approach     string
    ActionsTaken string
    Success      bool
}

func NewReflexionAgent(llm LLMInterface, memory *MemorySystem) *ReflexionAgent {
    return &ReflexionAgent{llm: llm, memory: memory, maxRetries: 3}
}

func (a *ReflexionAgent) executeTask(taskDescription string) *TaskResult {
    for attempt := 0; attempt ←
    1 / 5
    →
    重播

### 记忆治理策略代码实现

import time
from datetime import datetime, timedelta

class MemoryGovernor:
"""记忆生命周期治理引擎"""

def init(self, memory_system):
self.memory = memory_system
self.config = {
"max_short_term_turns": 15, # 短期记忆最大轮次
"compress_threshold": 12, # 触发压缩的轮次
"preserve_last_n": 3, # 压缩时保留最近 N 轮
"preserve_system_prompt": True, # 保护 System Prompt
"eviction_age_days": 90, # 超过 90 天的低重要性记忆淘汰
"eviction_max_count": 10000, # 总记忆条数上限
"retrieval_similarity_threshold": 0.7, # 检索相似度阈值
"retrieval_top_k": 5, # 检索返回 Top-K
"jit_mode": True # JIT 上下文模式
}
self.access_stats = {} # 记忆访问统计(用于淘汰决策)

===== 写入阶段 =====

def write(self, content, metadata=None):
"""写入记忆,自动评估重要性"""
importance = self._assess_importance(content)

enriched_meta = {
**(metadata or {}),
"importance": importance,
"created_at": datetime.now().isoformat(),
"access_count": 0,
"last_accessed": None
}

self.memory._save_to_long_term(content, enriched_meta)
self.memory.add_to_short_term(
enriched_meta.get("role", "user"), content
)

def _assess_importance(self, content):
"""用 LLM 快速评估内容重要性(1-5分)"""

简化版:基于规则的启发式评估

score = 1
if any(kw in content for kw in ["决策", "偏好", "规则", "约束"]):
score += 2
if any(kw in content for kw in ["失败", "错误", "反思"]):
score += 2
if len(content) > 200: # 长内容通常更重要
score += 1
return min(score, 5)

===== 索引阶段 =====

def index(self):
"""确保记忆索引完整且高效"""

Chroma/Milvus 等向量数据库自动维护向量索引

额外维护:时间索引、类型索引、访问频率索引

pass # 由向量数据库自动处理

===== 检索阶段(JIT 模式) =====

def retrieve_jit(self, current_step_description):
"""Just-in-Time 检索:只取当前步骤需要的信息"""
if not self.config["jit_mode"]:

非 JIT 模式:一次性加载所有相关记忆

return self.memory.retrieve_from_long_term(
current_step_description,
top_k=self.config["retrieval_top_k"]
)

JIT 模式:分步检索,按需注入

results = self.memory.retrieve_from_long_term(
current_step_description,
top_k=3 # JIT 模式下减少初始检索量
)

更新访问统计(用于淘汰决策)

for r in results:
doc_id = r.get("metadata", {}).get("doc_id", "")
if doc_id:
self.access_stats[doc_id] =
self.access_stats.get(doc_id, 0) + 1

相似度过滤

filtered = [
r for r in results
if r["similarity"] >= self.config["retrieval_similarity_threshold"]
]

return filtered

===== 压缩阶段 =====

def compress_if_needed(self):
"""检查并执行上下文压缩"""
if len(self.memory.short_term) > self.config["compress_threshold"]:
self._semantic_preserving_compress()

def _semantic_preserving_compress(self):
"""语义保护型压缩"""
short = self.memory.short_term

分层提取

system_msgs = [m for m in short if m["role"] == "system"]
recent = short[-(self.config["preserve_last_n"] * 2):]
middle = short[len(system_msgs):-(self.config["preserve_last_n"] * 2)]

识别行动-结果对(工具调用 + 返回值)

action_pairs = []
i = 0
while i content.includes(kw))) score += 2;
if (['失败', '错误', '反思'].some(kw => content.includes(kw))) score += 2;
if (content.length > 200) score += 1;
return Math.min(score, 5);
}

// ===== 检索阶段(JIT模式) =====
retrieveJit(currentStepDescription: string): any[] {
if (!this.config.jitMode) {
return this.memory.retrieveFromLongTerm(currentStepDescription, this.config.retrievalTopK);
}
const results = this.memory.retrieveFromLongTerm(currentStepDescription, 3);
for (const r of results) {
const docId = r.metadata?.docId || '';
if (docId) this.accessStats[docId] = (this.accessStats[docId] || 0) + 1;
}
return results.filter(r => r.similarity >= this.config.retrievalSimilarityThreshold);
}

// ===== 压缩阶段 =====
compressIfNeeded(): void {
if (this.memory.shortTerm.length > this.config.compressThreshold) {
this.semanticPreservingCompress();
}
}

private semanticPreservingCompress(): void {
// 简化版:保留system和最近N轮,其余摘要
const short = this.memory.shortTerm;
const systemMsgs = short.filter(m => m.role === 'system');
const recent = short.slice(-(this.config.preserveLastN * 2));
// 中间内容做摘要...
this.memory.shortTerm = [...systemMsgs, ...recent];
}

// ===== 淘汰阶段 =====
evict(): void {
const cutoff = new Date(Date.now() - this.config.evictionAgeDays * 86400000);
// 时效淘汰 + 容量淘汰 + 质量淘汰
const leastAccessed = Object.entries(this.accessStats)
.sort((a, b) => a[1] - b[1]).slice(0, 100);
// 删除最少访问的记忆...
}
}

// ===== 使用示例 =====
const governor = new MemoryGovernor(new MemorySystem());
governor.write("用户偏好简洁的中文回答", { type: 'preference' });
const context = governor.retrieveJit("分析销售数据趋势");
governor.compressIfNeeded();
governor.evict();


package main

import (
"fmt"
"time"
)

// MemoryGovernor 记忆生命周期治理引擎
type MemoryGovernor struct {
memory *MemorySystem
config GovernorConfig
accessStats map[string]int
}

type GovernorConfig struct {
MaxShortTermTurns int
CompressThreshold int
PreserveLastN int
PreserveSystemPrompt bool
EvictionAgeDays int
EvictionMaxCount int
RetrievalSimilarityThreshold float64
RetrievalTopK int
JITMode bool
}

func NewMemoryGovernor(mem *MemorySystem) *MemoryGovernor {
return &MemoryGovernor{
memory: mem,
config: GovernorConfig{
MaxShortTermTurns: 15, CompressThreshold: 12,
PreserveLastN: 3, PreserveSystemPrompt: true,
EvictionAgeDays: 90, EvictionMaxCount: 10000,
RetrievalSimilarityThreshold: 0.7, RetrievalTopK: 5,
JITMode: true,
},
accessStats: make(map[string]int),
}
}

// ===== 写入阶段 =====
func (g *MemoryGovernor) write(content string, metadata map[string]string) {
importance := g.assessImportance(content)
enriched := map[string]string{
"importance": fmt.Sprintf("%d", importance),
"created_at": time.Now().Format(time.RFC3339),
}
for k, v := range metadata {
enriched[k] = v
}
g.memory.saveToLongTerm(content, enriched)
role := "user"
if r, ok := metadata["role"]; ok { role = r }
g.memory.addToShortTerm(role, content)
}

func (g *MemoryGovernor) assessImportance(content string) int {
score := 1
keywords := []string{"决策", "偏好", "规则", "约束"}
for _, kw := range keywords {
if contains(content, kw) { score += 2; break }
}
failKeywords := []string{"失败", "错误", "反思"}
for _, kw := range failKeywords {
if contains(content, kw) { score += 2; break }
}
if len(content) > 200 { score += 1 }
if score > 5 { score = 5 }
return score
}

func contains(s, sub string) bool { return len(s) >= len(sub) && (s == sub || len(sub) == 0 || findSubstring(s, sub)) }
func findSubstring(s, sub string) bool {
for i := 0; i = g.config.RetrievalSimilarityThreshold {
filtered = append(filtered, r)
}
}
return filtered
}

// ===== 压缩阶段 =====
func (g *MemoryGovernor) compressIfNeeded() {
if len(g.memory.shortTerm) > g.config.CompressThreshold {
g.semanticPreservingCompress()
}
}

func (g MemoryGovernor) semanticPreservingCompress() {
short := g.memory.shortTerm
var systemMsgs, recent []Message
for _, m := range short {
if m.Role == "system" { systemMsgs = append(systemMsgs, m) }
}
if len(short) >= g.config.PreserveLastN
2 {
recent = short[len(short)-g.config.PreserveLastN*2:]
}
g.memory.shortTerm = append(systemMsgs, recent...)
}

// ===== 淘汰阶段 =====
func (g *MemoryGovernor) evict() {
cutoff := time.Now().AddDate(0, 0, -g.config.EvictionAgeDays)
_ = cutoff // 时效淘汰
// 容量淘汰 + 质量淘汰
}

// ===== 使用示例 =====
func main() {
governor := NewMemoryGovernor(NewMemorySystem())
governor.write("用户偏好简洁的中文回答", map[string]string{"type": "preference"})
_ = governor.retrieveJIT("分析销售数据趋势")
governor.compressIfNeeded()
governor.evict()
}


import java.util.*;

public class MemoryGovernor {
private MemorySystem memory;
private Map config = new HashMap<>();
private Map accessStats = new HashMap<>();

public MemoryGovernor(MemorySystem memorySystem) {
this.memory = memorySystem;
config.put("maxShortTermTurns", 15);
config.put("compressThreshold", 12);
config.put("preserveLastN", 3);
config.put("evictionAgeDays", 90);
config.put("evictionMaxCount", 10000);
config.put("retrievalSimilarityThreshold", 0.7);
config.put("retrievalTopK", 5);
config.put("jitMode", true);
}

// ===== 写入阶段 =====
public void write(String content, Map metadata) {
int importance = assessImportance(content);
Map enriched = new HashMap<>(metadata != null ? metadata : Map.of());
enriched.put("importance", String.valueOf(importance));
enriched.put("createdAt", java.time.Instant.now().toString());
memory.saveToLongTerm(content, enriched);
String role = enriched.getOrDefault("role", "user");
memory.addToShortTerm(role, content);
}

private int assessImportance(String content) {
int score = 1;
if (content.contains("决策") || content.contains("偏好") || content.contains("规则")) score += 2;
if (content.contains("失败") || content.contains("错误") || content.contains("反思")) score += 2;
if (content.length() > 200) score += 1;
return Math.min(score, 5);
}

// ===== 检索阶段(JIT模式) =====
@SuppressWarnings("unchecked")
public List retrieveJit(String stepDesc) {
boolean jitMode = (Boolean) config.get("jitMode");
int topK = (Integer) config.get("retrievalTopK");
if (!jitMode) return memory.retrieveFromLongTerm(stepDesc, topK);
List results = memory.retrieveFromLongTerm(stepDesc, 3);
for (Memory r : results) {
String docId = r.metadata.getOrDefault("docId", "");
if (!docId.isEmpty()) accessStats.merge(docId, 1, Integer::sum);
}
double threshold = (Double) config.get("retrievalSimilarityThreshold");
List filtered = new ArrayList<>();
for (Memory r : results) {
if (r.similarity >= threshold) filtered.add(r);
}
return filtered;
}

// ===== 压缩阶段 =====
public void compressIfNeeded() {
int threshold = (Integer) config.get("compressThreshold");
if (memory.shortTermSize() > threshold) {
semanticPreservingCompress();
}
}

private void semanticPreservingCompress() {
// 保留system和最近N轮,其余摘要压缩
int preserveN = (Integer) config.get("preserveLastN");
memory.compressShortTerm(preserveN);
}

// ===== 淘汰阶段 =====
public void evict() {
int maxCount = (Integer) config.get("evictionMaxCount");
// 时效淘汰 + 容量淘汰 + 质量淘汰
if (accessStats.size() > maxCount) {
// 删除最少访问的
}
}

public static void main(String[] args) {
MemoryGovernor governor = new MemoryGovernor(new MemorySystem());
governor.write("用户偏好简洁的中文回答", Map.of("type", "preference"));
List context = governor.retrieveJit("分析销售数据趋势");
governor.compressIfNeeded();
governor.evict();
}
}

**✅ 记忆治理的黄金法则**
- **写入要慷慨**:宁可多存,后续靠检索和淘汰来筛选
- **检索要精准**:JIT 原则,只在需要时取需要的量
- **压缩要保守**:保护核心指令和行动-结果对,只压缩中间推理
- **淘汰要渐进**:先降优先级再删除,给"冷记忆"一个回暖的机会
- **指标要量化**:追踪命中率、压缩率、检索延迟,用数据驱动治理策略

## 6.9 长期记忆的静态与动态分类

长期记忆不是铁板一块。仔细观察就会发现,有些记忆**几乎不变**,有些记忆**持续更新**。把长期记忆分为**静态长期记忆**和**动态长期记忆**,是面试中的高频考点,也是工程落地时的关键设计决策。

### 为什么要做这个分类?

如果所有长期记忆都用同一种策略管理,会出现两类问题:
**❌ 不分类的后果**
- **存储浪费**:用户偏好这种不变的信息,如果存入向量数据库每次做语义检索,检索开销大且没有必要--它永远都该被注入
- **信息过时**:历史对话摘要这种高频更新的信息,如果像用户偏好一样"写入就不管了",很快就会变得过时和冗余
- **召回混乱**:不同性质的记忆混在一起检索,模型很难区分"这是永远有效的规则"还是"这是上次的经验"

所以,分类的核心目的是**让不同性质的记忆用不同的管理策略**,从而提升存储效率、召回精准度和上下文利用率。

### 静态长期记忆:几乎不变的核心知识

静态长期记忆的特点是**变化频率极低**,写入后几乎不需要更新。它像一本"个人手册",定义了 Agent 与用户交互的基本规则。
**📖 静态长期记忆的典型内容**
- **用户偏好**:"用户偏好简洁的中文回答"、"用户不喜欢表格输出"--这些偏好一旦确定,几乎不变
- **系统规则**:"不要生成代码中的硬编码密码"、"回答必须附带引用来源"--规则是确定性的约束
- **领域知识**:"公司 API 文档的 Base URL 是 https://api.example.com/v2"--事实性知识,除非系统变更才更新

### 动态长期记忆:持续更新的经验积累

动态长期记忆的特点是**变化频率高**,需要持续更新、压缩和淘汰。它像一本"工作日记",记录了 Agent 的成长轨迹。
**📝 动态长期记忆的典型内容**
- **历史对话摘要**:之前多轮对话的压缩摘要--每次新对话都可能产生新的摘要,旧的摘要需要定期合并
- **任务执行经验**:"上次用 pandas 读 CSV 时遇到了编码问题,解决方案是指定 encoding='utf-8'"--经验会不断积累
- **用户行为模式**:"用户通常在下午 3 点后提问"、"用户习惯先问背景再问具体方案"--模式需要持续观察和修正

### 分类的意义:不同策略适配不同性质

分类带来三方面的差异化策略:
- **不同的存储策略**:静态记忆存本地配置文件(YAML/JSON),读写简单、确定性高;动态记忆存向量数据库,支持语义检索和大规模管理
- **不同的召回策略**:静态记忆直接注入 System Prompt,每次对话都有,不需要检索;动态记忆按相关性从向量库检索,只在需要时注入
- **不同的压缩策略**:静态记忆本身已经是精炼的表达(一条偏好就是一句话),不需要压缩;动态记忆需要定期压缩摘要、淘汰过时内容

### 对比表格 | 维度 | 静态长期记忆 | 动态长期记忆 | | --- | --- | --- | | 典型内容 | 用户偏好、系统规则、领域知识 | 对话摘要、执行经验、行为模式 | | 变化频率 | 极低,几乎不变 | 高,持续更新 | | 存储方式 | 本地配置文件(YAML/JSON) | 向量数据库(Chroma/Milvus) | | 召回方式 | 直接注入 System Prompt | 按相关性语义检索 | | 压缩策略 | 不压缩(本身已精炼) | 定期压缩摘要 + 淘汰过时内容 | | 更新触发 | 用户明确修改偏好时 | 每轮有价值交互后 | **✅ 面试要点**

    面试中回答"长期记忆的管理策略"时,**先分类再谈策略**。不要一上来就说"存向量数据库",而要先区分:"静态记忆存配置文件、直接注入 Prompt;动态记忆存向量库、按需检索、定期压缩"。这体现了你对记忆系统不同性质的深刻理解。

    关联面试题 → Q10

## 6.10 记忆存储触发链路

一个常见误解是:**"每轮对话都触发长期记忆写入"**。如果真的每轮都写,长期记忆库会变成完整对话日志的备份--噪音爆炸,检索失效。

  记忆写入不是"录一切",而是"记值得记的"。就像人类不会记住每顿饭吃了什么,但会记住第一次吃到惊艳味道的那顿。

### 不是每轮都触发!有筛选机制

记忆写入有明确的**触发条件**,只有满足条件的信息才进入长期记忆:
**🎯 记忆写入触发条件**
- **用户表达了新偏好** → 写入**静态长期记忆**(如"我不喜欢表格输出")
- **完成了一个有价值的任务** → 写入**动态长期记忆**(执行经验,如"CSV 编码问题的解决方案")
- **发现了新的上下文信息** → 写入**动态长期记忆**(知识片段,如"公司的 API 版本升级到 v2 了")
- **简单闲聊** → **不触发长期记忆写入**(如"你好"、"今天天气不错")

### 判断标准:什么信息"值得记住"?

判断一条信息是否值得写入长期记忆,核心标准是两个问题:

❓ 问题一:是否有长期参考价值?

      如果这条信息**在未来的对话中还可能被用到**,就值得记住。用户的偏好、问题的解决方案、关键的事实信息--这些都可能在下次交互中再次需要。

      反例:"今天中午吃什么"只在当天有价值,不值得写入长期记忆。

❓ 问题二:是否包含决策/偏好/知识点?

      如果这条信息包含**明确的决策、稳定的偏好或可复用的知识**,就值得记住。

      反例:"嗯嗯好的"这种确认性回复不包含任何决策或知识,不值得写入。

### 链路设计:对话→信息提取→价值评估→分类→写入

记忆写入是一个**五步链路**,每一步都有明确的职责:

### 记忆写入触发引擎代码示例

class MemoryWriteTrigger:
"""记忆写入触发引擎:判断什么信息值得写入长期记忆"""

def init(self, llm_client, static_store, dynamic_store):
self.llm = llm_client
self.static_store = static_store # 本地配置文件存储
self.dynamic_store = dynamic_store # 向量数据库存储

===== 第一步:信息提取 =====

def extract_key_info(self, conversation_turn):
"""从对话中提取关键信息片段"""
prompt = f"""从以下对话中提取关键信息。只提取可能具有长期参考价值的内容:

对话内容:
{conversation_turn}

提取格式(JSON):

  • type: preference / decision / knowledge / experience / none
  • content: 提取的关键内容(精炼为一句话)
  • confidence: 置信度(0-1)

如果对话中没有值得长期记住的信息,返回 type=none."""

result = self.llm.generate(prompt)
return self._parse_extraction(result)

===== 第二步:价值评估 =====

def evaluate_value(self, extracted_info):
"""评估信息是否值得写入长期记忆"""
if extracted_info["type"] == "none":
return False, "无关键信息"

规则+LLM 双重评估

规则层:简单闲聊模式直接跳过

trivial_patterns = ["你好", "谢谢", "好的", "嗯嗯", "再见"]
if any(p in extracted_info["content"] for p in trivial_patterns):
return False, "简单闲聊,无长期价值"

LLM 层:评估长期参考价值

prompt = f"""评估以下信息是否具有长期参考价值(未来对话中可能再次需要):

信息:{extracted_info["content"]}
类型:{extracted_info["type"]}

回答 YES 或 NO,并简要说明理由."""

evaluation = self.llm.generate(prompt)
is_valuable = "YES" in evaluation.upper()
reason = evaluation.strip()

return is_valuable, reason

===== 第三步:分类 =====

def classify_memory(self, extracted_info):
"""判断信息属于静态记忆还是动态记忆"""
type_mapping = {
"preference": "static", # 用户偏好 → 静态记忆
"decision": "static", # 决策规则 → 静态记忆
"knowledge": "dynamic", # 知识片段 → 动态记忆
"experience": "dynamic", # 执行经验 → 动态记忆
}
return type_mapping.get(extracted_info["type"], "dynamic")

===== 第四步:写入 =====

def write_memory(self, extracted_info, memory_type):
"""写入对应类型的记忆存储"""
if memory_type == "static":

静态记忆:写入本地配置文件

self.static_store.save(
key=extracted_info["type"],
value=extracted_info["content"],
metadata={"updated_at": datetime.now().isoformat()}
)
else:

动态记忆:Embedding → 向量数据库

vector = self.llm.embed(extracted_info["content"])
self.dynamic_store.save(
vector=vector,
content=extracted_info["content"],
metadata={
"type": extracted_info["type"],
"created_at": datetime.now().isoformat(),
"confidence": extracted_info.get("confidence", 0.8)
}
)

===== 完整链路 =====

def process_turn(self, conversation_turn):
"""处理一轮对话的完整记忆写入链路"""

Step 1: 信息提取

extracted = self.extract_key_info(conversation_turn)

Step 2: 价值评估

is_valuable, reason = self.evaluate_value(extracted)
if not is_valuable:
return {"action": "skip", "reason": reason}

Step 3: 分类

memory_type = self.classify_memory(extracted)

Step 4: 写入

self.write_memory(extracted, memory_type)

return {
"action": "write",
"type": memory_type,
"content": extracted["content"],
"reason": reason
}

===== 使用示例 =====

trigger = MemoryWriteTrigger(llm, static_config, vector_db)

场景 1:用户表达偏好 → 写入静态记忆

result = trigger.process_turn("以后回答都用简洁的中文,不要用英文")

→ {"action": "write", "type": "static", "content": "用户偏好简洁中文回答"}

场景 2:任务经验 → 写入动态记忆

result = trigger.process_turn("用 pandas 读 CSV 遇到编码问题,指定 encoding='utf-8' 解决了")

→ {"action": "write", "type": "dynamic", "content": "CSV编码问题解决方案..."}

场景 3:简单闲聊 → 不触发写入

result = trigger.process_turn("你好,今天天气不错")

→ {"action": "skip", "reason": "简单闲聊,无长期价值"}


class MemoryWriteTrigger {
private llm: any;
private memory: MemorySystem;
private static readonly TRIGGER_KEYWORDS = ['偏好', '喜欢', '讨厌', '规则', '记住', '别再'];
private static readonly VALUE_KEYWORDS = ['决策', '偏好', '规则', '约束', '失败', '错误', '经验'];

constructor(llm: any, memory: MemorySystem) {
this.llm = llm;
this.memory = memory;
}

// 判断是否触发记忆写入
shouldWrite(userInput: string, agentResponse: string): boolean {
// 1. 用户明确要求记住
if (MemoryWriteTrigger.TRIGGER_KEYWORDS.some(kw => userInput.includes(kw))) return true;
// 2. 包含决策/偏好/知识点
const combined = userInput + agentResponse;
if (MemoryWriteTrigger.VALUE_KEYWORDS.some(kw => combined.includes(kw))) return true;
// 3. 任务完成(简化判断)
if (agentResponse.includes('完成') || agentResponse.includes('成功')) return true;
return false;
}

// 提取值得记住的信息
async extract(userInput: string, agentResponse: string): Promise {
const prompt = 从以下对话中提取值得长期记住的信息,返回JSON数组:\n +
用户: ${userInput}\n助手: ${agentResponse}\n +
提取规则:1.用户偏好 2.关键决策 3.事实信息 4.经验教训;
const result = await this.llm.generate(prompt);
try { return JSON.parse(result); } catch { return []; }
}

// 完整写入流程
async process(userInput: string, agentResponse: string): Promise {
if (!this.shouldWrite(userInput, agentResponse)) return;
const memories = await this.extract(userInput, agentResponse);
for (const mem of memories) {
const type = this.classify(mem.content);
if (type === 'static') {
this.memory.saveToLongTerm(mem.content, { type: 'preference', storage: 'config' });
} else {
this.memory.saveToLongTerm(mem.content, { type: 'experience', storage: 'vector' });
}
}
}

private classify(content: string): 'static' | 'dynamic' {
if (['偏好', '规则', '约束'].some(kw => content.includes(kw))) return 'static';
return 'dynamic';
}
}


package main

import (
"fmt"
"strings"
)

// MemoryWriteTrigger 记忆写入触发引擎
type MemoryWriteTrigger struct {
llm LLMInterface
memory *MemorySystem
}

var triggerKeywords = []string{"偏好", "喜欢", "讨厌", "规则", "记住", "别再"}
var valueKeywords = []string{"决策", "偏好", "规则", "约束", "失败", "错误", "经验"}

func NewMemoryWriteTrigger(llm LLMInterface, mem *MemorySystem) *MemoryWriteTrigger {
return &MemoryWriteTrigger{llm: llm, memory: mem}
}

// 判断是否触发记忆写入
func (t *MemoryWriteTrigger) shouldWrite(userInput, agentResponse string) bool {
for _, kw := range triggerKeywords {
if strings.Contains(userInput, kw) { return true }
}
combined := userInput + agentResponse
for _, kw := range valueKeywords {
if strings.Contains(combined, kw) { return true }
}
if strings.Contains(agentResponse, "完成") || strings.Contains(agentResponse, "成功") {
return true
}
return false
}

// 完整写入流程
func (t *MemoryWriteTrigger) process(userInput, agentResponse string) {
if !t.shouldWrite(userInput, agentResponse) { return }
// 提取值得记住的信息
prompt := fmt.Sprintf("从以下对话中提取值得长期记住的信息:\n用户: %s\n助手: %s", userInput, agentResponse)
result := t.llm.Generate(prompt)
// 分类并写入
memType := t.classify(result)
metadata := map[string]string{"type": memType}
t.memory.saveToLongTerm(result, metadata)
}

func (t *MemoryWriteTrigger) classify(content string) string {
for _, kw := range []string{"偏好", "规则", "约束"} {
if strings.Contains(content, kw) { return "static" }
}
return "dynamic"
}


import java.util.*;

public class MemoryWriteTrigger {
private LLMInterface llm;
private MemorySystem memory;
private static final String[] TRIGGER_KEYWORDS = {"偏好", "喜欢", "讨厌", "规则", "记住", "别再"};
private static final String[] VALUE_KEYWORDS = {"决策", "偏好", "规则", "约束", "失败", "错误", "经验"};

public MemoryWriteTrigger(LLMInterface llm, MemorySystem memory) {
this.llm = llm;
this.memory = memory;
}

public boolean shouldWrite(String userInput, String agentResponse) {
for (String kw : TRIGGER_KEYWORDS) {
if (userInput.contains(kw)) return true;
}
String combined = userInput + agentResponse;
for (String kw : VALUE_KEYWORDS) {
if (combined.contains(kw)) return true;
}
if (agentResponse.contains("完成") || agentResponse.contains("成功")) return true;
return false;
}

public void process(String userInput, String agentResponse) {
if (!shouldWrite(userInput, agentResponse)) return;
String prompt = "从以下对话中提取值得长期记住的信息:\n用户: " + userInput +
"\n助手: " + agentResponse;
String result = llm.generate(prompt);
String type = classify(result);
memory.saveToLongTerm(result, Map.of("type", type));
}

private String classify(String content) {
String[] staticKws = {"偏好", "规则", "约束"};
for (String kw : staticKws) {
if (content.contains(kw)) return "static";
}
return "dynamic";
}
}

**✅ 面试要点**

    面试中回答"记忆写入机制"时,**重点强调筛选而非全量**。链路是:对话→信息提取→价值评估→分类→写入。不是每轮都触发,只有"有长期参考价值"的信息才写入。简单闲聊不触发,偏好/规则写入静态记忆,经验/知识写入动态记忆。

    关联面试题 → Q11

## 6.11 记忆召回决策与上下文污染防控

记忆召回的核心问题不是"能不能找到",而是**"该不该注入"**。召回太多无关记忆,比没有记忆更危险--这就是**上下文污染**。

  上下文污染就像往一杯清水里倒泥浆--信息越多,水质越差。精准的召回不是"把所有相关的都找出来",而是"只把最必要的放进去"。

### 召回决策机制:如何判断哪些记忆需要召回?

大模型判断哪些长期记忆需要召回,依赖三个核心维度的综合评分:
**🎯 意图匹配**

      当前对话意图与记忆标签的**语义相似度**。

      如用户问"怎么处理 CSV 编码问题",与记忆标签"CSV编码"的相似度高,应召回。

⏰ 时间衰减

      **越近期的记忆权重越高**。

      时间衰减函数:weight × e^(-λ × Δt)

      λ 是衰减系数,Δt 是距今时间。3天前的经验比3个月前的更有参考价值。
**📊 频率加权**

      **多次被召回验证有效的记忆权重更高**。

      如果一条经验被召回了5次且每次都有帮助,说明它是高价值的。从未被召回的记忆权重低。

综合评分公式:

recall_score = intent_similarity × w1 + time_decay_weight × w2 + frequency_weight × w3

其中:
intent_similarity = cosine_similarity(query_embedding, memory_embedding)
time_decay_weight = e^(-λ × days_since_creation)
frequency_weight = log(1 + successful_recall_count)
w1 + w2 + w3 = 1(权重归一化)


### 召回数量限制:不是越多越好

很多开发者认为"多召回一些总比漏掉好",但这恰恰是上下文污染的根源。
**❌ 召回过多的三大危害**
- **注意力稀释**:模型关注无关内容,忽略核心问题。就像人在嘈杂环境中听不清关键信息
- **指令冲突**:多条记忆互相矛盾。如一条说"偏好简洁回答",另一条说"上次用户要求详细分析"--模型该听谁的?
- **Token浪费**:无关记忆消耗宝贵的上下文窗口,挤占真正有用的信息空间

### 上下文污染防控策略

防控上下文污染需要四道防线:

1

数量上限

每次召回不超过 5 条记忆。宁可少召回一条有用信息,也不要多召回三条噪音。上限可以动态调整:简单问题 ≤3 条,复杂问题 ≤5 条。

    ←
    1 / 4
    →
    重播

### 智能召回引擎代码示例

class SmartRecallEngine:
"""智能召回引擎:精准召回 + 上下文污染防控"""

def init(self, vector_db, llm_client, config=None):
self.db = vector_db
self.llm = llm_client
self.config = config or {
"max_recall_count": 5, # 召回数量上限
"similarity_threshold": 0.7, # 相关性阈值
"time_decay_lambda": 0.05, # 时间衰减系数
"weights": { # 评分权重
"intent": 0.5,
"time": 0.3,
"frequency": 0.2
},
"priority_order": ["core", "recent_experience", "supplementary"]
}
self.recall_stats = {} # 记忆召回统计

===== 第一步:多维度评分 =====

def compute_recall_scores(self, query_embedding, candidate_memories):
"""为每条候选记忆计算综合召回评分"""
scores = []

for memory in candidate_memories:

意图匹配:语义相似度

intent_score = cosine_similarity(
query_embedding, memory["embedding"]
)

时间衰减:越近期权重越高

days_since = (datetime.now() - memory["created_at"]).days
time_score = math.exp(-self.config["time_decay_lambda"] * days_since)

频率加权:多次验证有效的记忆权重更高

mem_id = memory["id"]
freq_score = math.log(1 + self.recall_stats.get(mem_id, {}).get("success_count", 0))

综合评分

w = self.config["weights"]
total_score = (
intent_score * w["intent"] +
time_score * w["time"] +
freq_score * w["frequency"]
)

scores.append({
"memory": memory,
"total_score": total_score,
"intent_score": intent_score,
"time_score": time_score,
"freq_score": freq_score
})

return scores

===== 第二步:污染防控过滤 =====

def pollution_defense(self, scored_memories):
"""四道防线过滤,防止上下文污染"""

防线 1:相关性阈值过滤

filtered = [
s for s in scored_memories
if s["intent_score"] >= self.config["similarity_threshold"]
]

防线 2:数量上限截断

filtered.sort(key=lambda x: x["total_score"], reverse=True)
filtered = filtered[:self.config["max_recall_count"]]

防线 3:冲突检测

filtered = self._resolve_conflicts(filtered)

防线 4:优先级排序

filtered = self._priority_sort(filtered)

return filtered

def _resolve_conflicts(self, scored_memories):
"""冲突检测:语义矛盾的记忆取最新的"""
result = []
used_indices = set()

for i, m1 in enumerate(scored_memories):
if i in used_indices:
continue
conflict_found = False

for j, m2 in enumerate(scored_memories):
if j in used_indices or j == i:
continue

检测语义矛盾(简化:同类别但内容相反)

if m1["memory"].get("category") == m2["memory"].get("category"):

同类记忆可能冲突,取时间更新的

if m2["memory"]["created_at"] > m1["memory"]["created_at"]:
result.append(m2)
used_indices.add(j)
conflict_found = True
break

if not conflict_found:
result.append(m1)
used_indices.add(i)

return result

def _priority_sort(self, scored_memories):
"""优先级排序:核心指令 > 最近经验 > 补充信息"""
priority_map = {"core": 0, "recent_experience": 1, "supplementary": 2}

for m in scored_memories:
cat = m["memory"].get("priority_category", "supplementary")
m["priority_rank"] = priority_map.get(cat, 2)

return sorted(scored_memories, key=lambda x: x["priority_rank"])

===== 完整召回流程 =====

def recall(self, query, task_complexity="normal"):
"""智能召回完整流程"""

Step 1: 向量检索候选记忆

query_embedding = self.llm.embed(query)
candidates = self.db.search(query_embedding, top_k=20)

Step 2: 多维度评分

scored = self.compute_recall_scores(query_embedding, candidates)

Step 3: 动态调整召回上限

max_count = self.config["max_recall_count"]
if task_complexity == "simple":
max_count = 3 # 简单问题召回更少
elif task_complexity == "complex":
max_count = 5 # 复杂问题允许更多

self.config["max_recall_count"] = max_count

Step 4: 污染防控过滤

final_memories = self.pollution_defense(scored)

Step 5: 更新召回统计

for m in final_memories:
mem_id = m["memory"]["id"]
if mem_id not in self.recall_stats:
self.recall_stats[mem_id] = {"success_count": 0, "total_count": 0}
self.recall_stats[mem_id]["total_count"] += 1

return final_memories

===== 使用示例 =====

engine = SmartRecallEngine(chroma_db, llm)

简单问题:召回 ≤3 条

results = engine.recall("今天有什么日程?", task_complexity="simple")

→ 只返回最相关的 2-3 条近期日程记忆

复杂问题:召回 ≤5 条

results = engine.recall("分析上个季度的销售趋势并给出优化建议", task_complexity="complex")

→ 返回最多 5 条:1条核心指令 + 2条最近经验 + 2条补充知识


class SmartRecallEngine {
private memory: MemorySystem;
private llm: any;
private maxRecall = 5;
private similarityThreshold = 0.7;

constructor(memory: MemorySystem, llm: any) {
this.memory = memory;
this.llm = llm;
}

// 三维度评分:意图匹配 × 时间衰减 × 频率加权
async recall(userInput: string, intent?: string): Promise {
// 1. 向量检索候选记忆
const candidates = await this.memory.retrieveFromLongTerm(
intent || userInput, this.maxRecall * 2
);

// 2. 三维度评分
const scored = candidates.map(mem => ({
...mem,
score: this.score(mem, intent || userInput),
}));

// 3. 排序+截断(≤5条)
scored.sort((a, b) => b.score - a.score);
let recalled = scored.slice(0, this.maxRecall);

// 4. 冲突检测:矛盾记忆取最新
recalled = this.resolveConflicts(recalled);

// 5. 相关性阈值过滤
recalled = recalled.filter(r => r.similarity >= this.similarityThreshold);

return recalled;
}

private score(mem: any, query: string): number {
const intentScore = mem.similarity;
const timeDecay = this.timeDecay(mem.metadata?.createdAt);
const frequency = Math.log10((mem.metadata?.accessCount || 1) + 1);
return intentScore * 0.6 + timeDecay * 0.3 + frequency * 0.1;
}

private timeDecay(createdAt?: string): number {
if (!createdAt) return 0.5;
const days = (Date.now() - new Date(createdAt).getTime()) / 86400000;
return Math.exp(-days / 30); // 30天衰减常数
}

private resolveConflicts(memories: any[]): any[] {
// 简化:按内容去重,保留最新的
const seen = new Map();
for (const m of memories) {
const key = m.text.substring(0, 50);
const existing = seen.get(key);
if (!existing || (m.metadata?.createdAt > existing.metadata?.createdAt)) {
seen.set(key, m);
}
}
return Array.from(seen.values());
}
}

interface RecalledMemory {
text: string;
metadata: Record;
similarity: number;
score: number;
}


package main

import (
"math"
"sort"
"time"
)

// SmartRecallEngine 智能召回引擎
type SmartRecallEngine struct {
memory *MemorySystem
maxRecall int
similarityThreshold float64
}

func NewSmartRecallEngine(mem *MemorySystem) *SmartRecallEngine {
return &SmartRecallEngine{
memory: mem, maxRecall: 5, similarityThreshold: 0.7,
}
}

// 三维度评分召回
func (e SmartRecallEngine) recall(userInput string) []ScoredMemory {
// 1. 向量检索候选
candidates := e.memory.retrieveFromLongTerm(userInput, e.maxRecall
2)

// 2. 评分
var scored []ScoredMemory
for _, mem := range candidates {
scored = append(scored, ScoredMemory{
Memory: mem,
Score: e.score(mem, userInput),
})
}

// 3. 排序+截断
sort.Slice(scored, func(i, j int) bool { return scored[i].Score > scored[j].Score })
if len(scored) > e.maxRecall { scored = scored[:e.maxRecall] }

// 4. 相关性阈值过滤
var result []ScoredMemory
for _, s := range scored {
if s.Similarity >= e.similarityThreshold {
result = append(result, s)
}
}
return result
}

func (e SmartRecallEngine) score(mem Memory, query string) float64 {
intentScore := mem.Similarity
timeDecay := e.timeDecay(mem.Metadata["created_at"])
frequency := 0.5 // 简化
return intentScore
0.6 + timeDecay0.3 + frequency0.1
}

func (e *SmartRecallEngine) timeDecay(createdAt string) float64 {
if createdAt == "" { return 0.5 }
t, err := time.Parse(time.RFC3339, createdAt)
if err != nil { return 0.5 }
days := time.Since(t).Hours() / 24
return math.Exp(-days / 30)
}

type ScoredMemory struct {
Memory
Score float64
}


import java.util.*;
import java.util.stream.Collectors;

public class SmartRecallEngine {
private MemorySystem memory;
private int maxRecall = 5;
private double similarityThreshold = 0.7;

public SmartRecallEngine(MemorySystem memory) {
this.memory = memory;
}

// 三维度评分召回
public List recall(String userInput) {
// 1. 向量检索候选
List candidates = memory.retrieveFromLongTerm(userInput, maxRecall * 2);

// 2. 评分
List scored = candidates.stream()
.map(mem -> new ScoredMemory(mem, score(mem, userInput)))
.collect(Collectors.toList());

// 3. 排序+截断
scored.sort((a, b) -> Double.compare(b.score, a.score));
if (scored.size() > maxRecall) scored = scored.subList(0, maxRecall);

// 4. 相关性阈值过滤
return scored.stream()
.filter(s -> s.memory.similarity >= similarityThreshold)
.collect(Collectors.toList());
}

private double score(Memory mem, String query) {
double intentScore = mem.similarity;
double timeDecay = timeDecay(mem.metadata.get("created_at"));
double frequency = Math.log10(1 + 1);
return intentScore * 0.6 + timeDecay * 0.3 + frequency * 0.1;
}

private double timeDecay(String createdAt) {
if (createdAt == null) return 0.5;
long days = (System.currentTimeMillis() - java.time.Instant.parse(createdAt).toEpochMilli()) / 86400000;
return Math.exp(-days / 30.0);
}

static class ScoredMemory {
Memory memory;
double score;
ScoredMemory(Memory m, double s) { memory = m; score = s; }
}
}

**✅ 面试要点**

    面试中回答"记忆召回策略"时,**先谈召回决策再谈污染防控**。召回决策靠三维度评分(意图匹配×时间衰减×频率加权),污染防控靠四道防线(数量上限≤5、相关性阈值≥0.7、冲突检测取最新、优先级排序核心>经验>补充)。不要只说"用向量搜索找相关记忆"--那只是第一步,真正的难点是防控上下文污染。

    关联面试题 → Q12

## 6.11a Prompt 工程:静态框架与动态组装

面试追问:**"动态 Prompt 和静态 Prompt 有什么区别?Agent 每轮对话开始时,上下文是如何动态组装的?"**上一节讲了记忆的召回决策,这一节讲召回的结果怎么**组装进 Prompt**--以及哪些部分永远不动(静态),哪些每轮都变(动态)。

  把 Prompt 想象成一栋建筑:**静态部分是地基和框架**(角色定义、安全规则、工具列表)--不会随对话变化;**动态部分是家具和装饰**(记忆片段、对话历史、当前任务)--每轮都换。地基不稳,房子会塌;家具不换,住着不舒服。

### 静态 Prompt:不随对话变化的"固定框架"

静态 Prompt 包含**在所有对话中都相同**的内容,是 Agent 的"宪法":

🔒 静态 Prompt 的四个组成部分
- **角色定义**:"你是一个专业的代码审查专家"--定义 Agent 身份
- **安全规则**:"不要泄露用户隐私、不要执行危险命令"--行为边界
- **输出格式约束**:"回答格式:风险等级→问题→修复建议"--控制输出
- **工具/Skill 描述**:Function Calling Schema + Skill 触发词列表--Agent 的"能力菜单"
**📌 静态 Prompt 的三个特点**
- **不变性**:同一个会话内,内容完全不变(除非用户明确修改偏好)
- **最高优先级**:System Prompt 具有最高权重,模型始终遵循
- **缓存友好**:因为不变,放在 Prompt 前缀处可100%命中 Prompt Cache

在 OpenClaw 等框架中,静态 Prompt 就是从 `SOUL.md`(角色)、`AGENTS.md`(规则)和 ``(Skill列表)读取的内容,注入为 System Prompt 的一部分。这部分**每次请求都一样**,所以是最容易命中缓存的部分。

### 动态 Prompt:每轮对话变化的"活的内容"

动态 Prompt 包含**根据当前对话上下文实时变化**的内容:
**🔄 动态 Prompt 的三个来源**
- **长期记忆召回**:根据当前意图检索的5条相关记忆
- **对话历史**:累积的多轮交互记录(每轮增加)
- **当前任务上下文**:用户刚输入的指令 + 工具返回结果
**⚡ 动态内容的变化频率**
- **慢变化**:Skill完整指令(只在触发时加载)、静态记忆偏好(很少更新)
- **中变化**:长期记忆召回片段(每几轮可能换一批)
- **快变化**:对话历史和工具结果(每轮必变)
**⚠️ 动态内容的缓存特征**
- **慢变化**:多轮后可能命中缓存
- **中变化**:难命中,但可控制注入量
- **快变化**:完全不命中缓存

### 每轮对话的完整 Prompt 组装流程

每轮对话开始时,Agent 需要把静态和动态内容**按顺序组装**成完整 Prompt。顺序很重要--因为缓存是前缀匹配,���定部分必须放最前面:

class PromptAssembler:
"""每轮对话的 Prompt 动态组装引擎"""

def init(self, system_prompt, tool_schemas, skill_triggers,
memory_engine, chat_history_manager):

Layer 1: 静态层(整个会话不变)

self.static_prefix = f"{system_prompt}\n\n{tool_schemas}\n\n{skill_triggers}"
self.memory_engine = memory_engine
self.history_manager = chat_history_manager

def assemble(self, user_input, intent=None):
"""组装当前轮次的完整 Prompt"""
messages = []

===== Layer 1: 静态框架 =====

System Prompt + 工具Schema + Skill触发词

每次相同 → 100%命中缓存

messages.append({"role": "system", "content": self.static_prefix})

===== Layer 2: 记忆注入 =====

根据当前意图召回长期记忆

只注入与当前任务相关的 ≤5 条记忆

if intent:
recalled = self.memory_engine.recall(
query=intent, top_k=5, threshold=0.7
)
if recalled:
memory_block = "# Relevant Memory\n" + "\n".join(recalled)
messages.append({"role": "system", "content": memory_block})

===== Layer 3: 对话历史 =====

如果历史过长,先压缩再注入

history = self.history_manager.get_history(max_tokens=50000)
for msg in history:
messages.append(msg)

===== Layer 4: 当前请求 =====

messages.append({"role": "user", "content": user_input})

return messages

使用示例

assembler = PromptAssembler(
system_prompt="你是代码审查专家...",
tool_schemas=json.dumps(tool_definitions),
skill_triggers="...",
memory_engine=vector_db,
chat_history_manager=history_mgr
)

每轮调用

messages = assembler.assemble("帮我审查这段代码", intent="代码审查")

→ [system(fixed), system(memory), ...history..., user(current)]


class PromptAssembler {
private staticPrefix: string;
private memoryEngine: any;
private historyManager: any;

constructor(opts: {
systemPrompt: string;
toolSchemas: string;
skillTriggers: string;
memoryEngine: any;
chatHistoryManager: any;
}) {
// Layer 1: 静态层(整个会话不变)
this.staticPrefix = ${opts.systemPrompt}\n\n${opts.toolSchemas}\n\n${opts.skillTriggers};
this.memoryEngine = opts.memoryEngine;
this.historyManager = opts.chatHistoryManager;
}

async assemble(userInput: string, intent?: string): Promise {
const messages: Message[] = [];

// ===== Layer 1: 静态框架 =====
messages.push({ role: 'system', content: this.staticPrefix });

// ===== Layer 2: 记忆注入 =====
if (intent) {
const recalled = await this.memoryEngine.recall(intent, 5, 0.7);
if (recalled.length > 0) {
const memoryBlock = '# Relevant Memory\n' + recalled.join('\n');
messages.push({ role: 'system', content: memoryBlock });
}
}

// ===== Layer 3: 对话历史 =====
const history = this.historyManager.getHistory(50000);
messages.push(...history);

// ===== Layer 4: 当前请求 =====
messages.push({ role: 'user', content: userInput });

return messages;
}
}

interface Message { role: string; content: string; }

// 使用示例
const assembler = new PromptAssembler({
systemPrompt: '你是代码审查专家...',
toolSchemas: JSON.stringify(toolDefinitions),
skillTriggers: '...',
memoryEngine: vectorDb,
chatHistoryManager: historyMgr,
});

const messages = await assembler.assemble('帮我审查这段代码', '代码审查');


package main

import "encoding/json"

// PromptAssembler 每轮对话的Prompt动态组装引擎
type PromptAssembler struct {
staticPrefix string
memoryEngine *MemorySystem
historyManager *HistoryManager
}

type ChatMessage struct {
Role string json:"role"
Content string json:"content"
}

func NewPromptAssembler(systemPrompt, toolSchemas, skillTriggers string,
memEngine *MemorySystem, histMgr *HistoryManager) *PromptAssembler {
return &PromptAssembler{
staticPrefix: systemPrompt + "\n\n" + toolSchemas + "\n\n" + skillTriggers,
memoryEngine: memEngine,
historyManager: histMgr,
}
}

func (a *PromptAssembler) assemble(userInput, intent string) []ChatMessage {
var messages []ChatMessage

// ===== Layer 1: 静态框架 =====
messages = append(messages, ChatMessage{Role: "system", Content: a.staticPrefix})

// ===== Layer 2: 记忆注入 =====
if intent != "" {
recalled := a.memoryEngine.retrieveFromLongTerm(intent, 5)
if len(recalled) > 0 {
memoryBlock := "# Relevant Memory\n"
for _, r := range recalled {
memoryBlock += r.Text + "\n"
}
messages = append(messages, ChatMessage{Role: "system", Content: memoryBlock})
}
}

// ===== Layer 3: 对话历史 =====
history := a.historyManager.getHistory(50000)
messages = append(messages, history...)

// ===== Layer 4: 当前请求 =====
messages = append(messages, ChatMessage{Role: "user", Content: userInput})

return messages
}

// 使用示例
func main() {
schemas, _ := json.Marshal(toolDefinitions)
assembler := NewPromptAssembler(
"你是代码审查专家...", string(schemas), "...",
NewMemorySystem(), NewHistoryManager(),
)
messages := assembler.assemble("帮我审查这段代码", "代码审查")
_ = messages
}


import java.util.*;

public class PromptAssembler {
private String staticPrefix;
private MemorySystem memoryEngine;
private HistoryManager historyManager;

public PromptAssembler(String systemPrompt, String toolSchemas,
String skillTriggers, MemorySystem memEngine, HistoryManager histMgr) {
// Layer 1: 静态层(整个会话不变)
this.staticPrefix = systemPrompt + "\n\n" + toolSchemas + "\n\n" + skillTriggers;
this.memoryEngine = memEngine;
this.historyManager = histMgr;
}

public List assemble(String userInput, String intent) {
List messages = new ArrayList<>();

// ===== Layer 1: 静态框架 =====
messages.add(new ChatMessage("system", staticPrefix));

// ===== Layer 2: 记忆注入 =====
if (intent != null && !intent.isEmpty()) {
List recalled = memoryEngine.retrieveFromLongTerm(intent, 5);
if (!recalled.isEmpty()) {
StringBuilder memoryBlock = new StringBuilder("# Relevant Memory\n");
for (Memory r : recalled) {
memoryBlock.append(r.text).append("\n");
}
messages.add(new ChatMessage("system", memoryBlock.toString()));
}
}

// ===== Layer 3: 对话历史 =====
List history = historyManager.getHistory(50000);
messages.addAll(history);

// ===== Layer 4: 当前请求 =====
messages.add(new ChatMessage("user", userInput));

return messages;
}

static class ChatMessage {
String role; String content;
ChatMessage(String r, String c) { role = r; content = c; }
}
}

**✅ 面试要点**

    回答"动态Prompt和静态Prompt的区别"时,**先定义再对比**:

    **静态Prompt** = System Prompt(角色+规则+工具Schema+Skill触发词),整个会话不变,放Prompt最前面,100%命中缓存。

    **动态Prompt** = 记忆召回片段 + Skill完整指令 + 对话历史 + 用户当前输入,每轮都变,放Prompt后面,按变化频率分层。

    **组装策略**:固定→半固定→动态→当前请求,四层叠放。核心原则:**不变的放前面最大化缓存,变化的放后面最小化缓存失效**。

    关联面试题 → Q11(动态vs静态Prompt)、Q12(缓存命中设计)

    关联章节 → 1.1.6(上下文窗口Token经济学)、4.13(Token Budget)

## 6.11b 多轮对话状态爆炸与完整解决方案

面试追问:**"多轮对话Agent,怎么解决状态爆炸、上下文溢出?"**前面5.3~5.11分别讲了压缩、治理、召回防控,但面试需要的是**一个完整的系统级解决方案**--怎么把这些策略串起来?这一节给你一个全景框架。

  状态爆炸不是某一个环节的问题,而是**整个系统的压力累积**。就像水管爆裂--不是某一个接头坏了,而是整条管路压力超标。解决方案不是"堵一个洞",而是"设计一套泄压系统"。

### 什么是"状态爆炸"?为什么多轮对话会导致这个问题?

单轮对话很简单:用户问→模型答→结束。但多轮对话Agent中,**状态会从线性增长变为指数增长**:

💥 三种状态爆炸模式
- **对话历史膨胀**:每轮新增 ~1000-3000 Token,10轮就是 10K-30K,50轮可能50K-150K--线性增长但速度很快
- **工具调用结果嵌套**:一个工具返回3K Token,模型基于结果再调2个工具,每个又返回2K--嵌套增长呈**树状展开**
- **推理路径分叉**:ToT/Reflexion模式中,每条路径各自累积上下文,多条路径并行时Token消耗是**倍数增长**

📉 状态爆炸的四大危害
- **上下文溢出**:总Token超过窗口128K,直接截断丢失关键信息
- **注意力稀释**:模型在100K上下文中"迷失",重要信息被淹没(Lost in the Middle)
- **成本爆炸**:每轮都发50K+上下文给API,20轮就是$1+的账单
- **行为漂移**:长上下文导致模型偏离原始指令(Context Rot)

### 完整解决方案:四层泄压体系

解决状态爆炸不是单一策略,而是**四层递进的泄压体系**--从预防到压缩到淘汰到兜底:
**🛡️ Layer 1: 预防--不让压力累积**
- **Token Budget**:设定全局上限(如20万Token),实时累加,超预算立即停止(详见4.13节)
- **JIT召回原则**:只检索当前步骤需要的记忆,不提前加载全量(详见6.8节)
- **输出长度约束**:要求模型"只输出关键结论"而非长篇大论,减少输出Token
- **工具结果精简**:工具返回后先做格式化过滤(只保留关键行),不原样塞入上下文
**📦 Layer 2: 压缩--在信息完整的前提下缩减体积**
- **语义保护型压缩**:锁定System Prompt+原始问题,保护行动-结果对完整性,只压缩中间推理(详见6.5节)
- **三层压缩触发**:主动压缩(80%阈值)→后台压缩(60%)→反应式压缩(413状态码触发紧急压缩)
- **增量压缩**:只压缩"上次压缩后新增"的部分,避免全量重复压缩(详见6.12节)
- **对话历史摘要**:旧对话压缩为摘要存入长期记忆,上下文中只保留摘要和最近3轮原文

🗑️ Layer 3: 淘汰--扔掉不再需要的
- **记忆淘汰策略**:时效淘汰(过期记忆清理)+容量淘汰(超上限淘汰最低价值)+质量淘汰(从未被召回的记忆降优先级)(详见6.8节)
- **召回数量硬限制**:每次召回≤5条,简单问题≤3条,宁可少召回也不多注入噪音
- **工具结果裁剪**:长输出只保留Top-K关键行,命令输出只保留最后20行
- **淘汰前归档**:淘汰的记忆做最终摘要存入"归档层",彻底删除前保留摘要

🚨 Layer 4: 兜底--当所有策略都不够时
- **���急压缩(413反应式)**:上下文超限导致API返回413状态码时,立即触发最激进压缩--只保留System Prompt + 最近1轮 + 任务摘要
- **降级回答**:压缩后仍然超限,告诉用户"当前对话过长,建议开启新会话"
- **自动会话切分**:超过阈值(如50轮)后自动将当前对话摘要存入长期记忆,开启新会话继续
- **人工介入**:预算超限+压缩失败+降级回答后,通知用户人工决策

class StateOverflowGuard:
"""多轮对话状态爆炸的完整解决方案:四层泄压体系"""

def init(self, token_budget=200000, context_window=128000):
self.token_budget = token_budget # 全局Token预算
self.context_window = context_window # 上下文窗口大小
self.current_tokens = 0 # 当前累计Token
self.compressor = SemanticCompressor() # 语义保护型压缩器
self.memory_governor = MemoryGovernor() # 记忆治理引擎

===== Layer 1: 预防 =====

def check_budget(self, new_tokens):
"""预算检查:预防状态累积"""
self.current_tokens += new_tokens
if self.current_tokens > self.token_budget:
raise BudgetExceededError(
f"Token预算耗尽: {self.current_tokens}/{self.token_budget}"+
"建议开启新会话或人工介入"
)

JIT召回:只检索当前需要的记忆

输出约束:限制模型输出长度

工具精简:过滤工具返回结果

return True

===== Layer 2: 压缩 =====

def compress_if_needed(self, messages):
"""压缩检查:超80%阈值触发语义保护型压缩"""
total = count_tokens(messages)
threshold = self.context_window * 0.8 # 80%阈值

if total > threshold:

语义保护型压缩:

锁定层(System Prompt+原始问题) → 不压缩

压缩层(中间推理步骤) → 摘要化

保留层(最近3轮) → 原样保留

compressed = self.compressor.compress(messages)
return compressed
return messages

===== Layer 3: 淘汰 =====

def evict_if_needed(self, messages):
"""淘汰检查:压缩后仍超阈值时淘汰低价值内容"""
total = count_tokens(messages)
threshold = self.context_window * 0.6 # 60%阈值

if total > threshold:

淘汰策略:时效淘汰 + 容量淘汰 + 质量淘汰

召回限制:每次≤5条记忆

工具裁剪:只保留关键行

淘汰前归档:做最终摘要存入长期记忆

evicted = self.memory_governor.evict(messages)
return evicted
return messages

===== Layer 4: 兜底 =====

def emergency_handler(self, error):
"""紧急兜底:413状态码或预算超限"""
if isinstance(error, ContextOverflowError):

紧急压缩:只保留System Prompt + 最近1轮 + 摘要

return self.compressor.emergency_compress()
elif isinstance(error, BudgetExceededError):

降级回答 + 自动会话切分 + 人工介入

return {
"action": "degrade",
"message": "对话过长,建议开启新会话",
"summary": self.memory_governor.archive_session()
}

===== 完整流程 =====

def process_turn(self, messages, new_tokens):
"""每轮对话的状态管理完整流程"""
try:

Layer 1: 预防(预算检查)

self.check_budget(new_tokens)

Layer 2: 压缩(超80%触发)

messages = self.compress_if_needed(messages)

Layer 3: 淘汰(超60%触发)

messages = self.evict_if_needed(messages)

return messages
except (ContextOverflowError, BudgetExceededError) as e:

Layer 4: 兜底(紧急处理)

return self.emergency_handler(e)


class StateOverflowGuard {
private tokenBudget: number;
private contextWindow: number;
private currentTokens = 0;
private compressor: SemanticCompressor;
private memoryGovernor: MemoryGovernor;

constructor(tokenBudget = 200000, contextWindow = 128000) {
this.tokenBudget = tokenBudget;
this.contextWindow = contextWindow;
this.compressor = new SemanticCompressor();
this.memoryGovernor = new MemoryGovernor();
}

// ===== Layer 1: 预防 =====
checkBudget(newTokens: number): boolean {
this.currentTokens += newTokens;
if (this.currentTokens > this.tokenBudget) {
throw new BudgetExceededError(
Token预算耗尽: ${this.currentTokens}/${this.tokenBudget}建议开启新会话
);
}
return true;
}

// ===== Layer 2: 压缩 =====
compressIfNeeded(messages: Message[]): Message[] {
const total = countTokens(messages);
const threshold = this.contextWindow * 0.8;
if (total > threshold) {
return this.compressor.compress(messages);
}
return messages;
}

// ===== Layer 3: 淘汰 =====
evictIfNeeded(messages: Message[]): Message[] {
const total = countTokens(messages);
const threshold = this.contextWindow * 0.6;
if (total > threshold) {
return this.memoryGovernor.evict(messages);
}
return messages;
}

// ===== Layer 4: 兜底 =====
emergencyHandler(error: Error): any {
if (error instanceof ContextOverflowError) {
return this.compressor.emergencyCompress();
} else if (error instanceof BudgetExceededError) {
return {
action: 'degrade',
message: '对话过长,建议开启新会话',
summary: this.memoryGovernor.archiveSession(),
};
}
}

// ===== 完整流程 =====
processTurn(messages: Message[], newTokens: number): Message[] | any {
try {
this.checkBudget(newTokens);
messages = this.compressIfNeeded(messages);
messages = this.evictIfNeeded(messages);
return messages;
} catch (e) {
return this.emergencyHandler(e as Error);
}
}
}

function countTokens(messages: Message[]): number {
return messages.reduce((sum, m) => sum + m.content.length / 4, 0);
}


package main

import (
"fmt"
)

// StateOverflowGuard 多轮对话状态爆炸的完整解决方案
type StateOverflowGuard struct {
tokenBudget int
contextWindow int
currentTokens int
compressor *SemanticCompressor
memoryGovernor *MemoryGovernor
}

func NewStateOverflowGuard(tokenBudget, contextWindow int) *StateOverflowGuard {
return &StateOverflowGuard{
tokenBudget: tokenBudget, contextWindow: contextWindow,
compressor: NewSemanticCompressor(),
memoryGovernor: NewMemoryGovernor(NewMemorySystem()),
}
}

// ===== Layer 1: 预防 =====
func (g *StateOverflowGuard) checkBudget(newTokens int) error {
g.currentTokens += newTokens
if g.currentTokens > g.tokenBudget {
return fmt.Errorf("Token预算耗尽: %d/%d建议开启新会话", g.currentTokens, g.tokenBudget)
}
return nil
}

// ===== Layer 2: 压缩 =====
func (g *StateOverflowGuard) compressIfNeeded(messages []Message) []Message {
total := countTokens(messages)
threshold := int(float64(g.contextWindow) * 0.8)
if total > threshold {
return g.compressor.compress(messages)
}
return messages
}

// ===== Layer 3: 淘汰 =====
func (g *StateOverflowGuard) evictIfNeeded(messages []Message) []Message {
total := countTokens(messages)
threshold := int(float64(g.contextWindow) * 0.6)
if total > threshold {
return g.memoryGovernor.evictMessages(messages)
}
return messages
}

// ===== Layer 4: 兜底 =====
func (g *StateOverflowGuard) emergencyHandler(err error) interface{} {
if isContextOverflow(err) {
return g.compressor.emergencyCompress()
}
if isBudgetExceeded(err) {
return map[string]interface{}{
"action": "degrade",
"message": "对话过长,建议开启新会话",
"summary": g.memoryGovernor.archiveSession(),
}
}
return nil
}

// ===== 完整流程 =====
func (g *StateOverflowGuard) processTurn(messages []Message, newTokens int) interface{} {
err := g.checkBudget(newTokens)
if err != nil {
return g.emergencyHandler(err)
}
messages = g.compressIfNeeded(messages)
messages = g.evictIfNeeded(messages)
return messages
}

func countTokens(messages []Message) int {
total := 0
for _, m := range messages {
total += len(m.Content) / 4
}
return total
}

func isContextOverflow(err error) bool { return false }
func isBudgetExceeded(err error) bool { return false }


import java.util.*;

public class StateOverflowGuard {
private int tokenBudget;
private int contextWindow;
private int currentTokens = 0;
private SemanticCompressor compressor;
private MemoryGovernor memoryGovernor;

public StateOverflowGuard(int tokenBudget, int contextWindow) {
this.tokenBudget = tokenBudget;
this.contextWindow = contextWindow;
this.compressor = new SemanticCompressor();
this.memoryGovernor = new MemoryGovernor(new MemorySystem());
}

// ===== Layer 1: 预防 =====
public boolean checkBudget(int newTokens) throws BudgetExceededException {
currentTokens += newTokens;
if (currentTokens > tokenBudget) {
throw new BudgetExceededException(
"Token预算耗尽: " + currentTokens + "/" + tokenBudget + "建议开启新会话");
}
return true;
}

// ===== Layer 2: 压缩 =====
public List compressIfNeeded(List messages) {
int total = countTokens(messages);
int threshold = (int) (contextWindow * 0.8);
if (total > threshold) {
return compressor.compress(messages);
}
return messages;
}

// ===== Layer 3: 淘汰 =====
public List evictIfNeeded(List messages) {
int total = countTokens(messages);
int threshold = (int) (contextWindow * 0.6);
if (total > threshold) {
return memoryGovernor.evictMessages(messages);
}
return messages;
}

// ===== Layer 4: 兜底 =====
public Object emergencyHandler(Exception error) {
if (error instanceof ContextOverflowException) {
return compressor.emergencyCompress();
} else if (error instanceof BudgetExceededException) {
Map result = new HashMap<>();
result.put("action", "degrade");
result.put("message", "对话过长,建议开启新会话");
result.put("summary", memoryGovernor.archiveSession());
return result;
}
return null;
}

// ===== 完整流程 =====
public Object processTurn(List messages, int newTokens) {
try {
checkBudget(newTokens);
messages = compressIfNeeded(messages);
messages = evictIfNeeded(messages);
return messages;
} catch (Exception e) {
return emergencyHandler(e);
}
}

private int countTokens(List messages) {
int total = 0;
for (Message m : messages) total += m.content.length() / 4;
return total;
}
}

**✅ 面试要点:四层泄压体系答题模板**

    面试回答"多轮对话状态爆炸"时,**不要只说"压缩上下文"**,要给系统级方案:

    **1 先定义问题**--多轮对话中状态从线性增长变为树状展开(工具嵌套)和倍数增长(推理分叉),导致上下文溢出、注意力稀释、成本爆炸和行为漂移。

    **2 再给方案框架**--四层泄压体系递进式处理:

    Layer 1 预防:Token Budget全局预算 + JIT召回原则 + 输出长度约束 + 工具结果精简

    Layer 2 压缩:语义保护型压缩(锁定System Prompt和行动-结果对)+ 三层触发机制

    Layer 3 淘汰:记忆治理策略 + 召回硬限制≤5条 + 工具结果裁剪 + 淘汰前归档

    Layer 4 兜底:紧急压缩(413)+ 降级回答 + 自动会话切分 + 人工介入

    **3 最后强调**--不是某一层单独起作用,而是四层递进。预防失败→压缩兜住,压缩不够→淘汰兜住,淘汰也不够→紧急兜底。

    关联面试题 → Q23(状态爆炸)

    关联章节 → 4.13(Token Budget)、5.3(短期记忆管理)、5.5(上下文压缩)、5.8(记忆治理)、5.11(召回防控)、5.12(生产级压缩系统)

## 6.12 工程深度:生产级上下文压缩系统

4.5 节介绍了上下文压缩的概念,但生产级系统远比概念复杂:什么时候压缩?用什么策略压缩?压缩后如何避免频繁重复压缩?压缩失败怎么办?本节基于 WaLiCode 项目的 contextCompressor.ts 和 ChatContextManager.ts 真实实现,讲解生产级上下文压缩系统的六大设计。

### 6.12.1 轮次分割算法

压缩的第一步是理解对话的结构。对话不是扁平的消息列表,而是由多个"轮次"组成--每个轮次以 user message 开始,包含后续的所有 assistant/tool 消息。WaLiCode 的 splitIntoRounds 算法实现了这个分割:

interface ConversationRound {
startIndex: number;
endIndex: number;
messages: ClaudeMessage[];
tokenCount: number;
}

function splitIntoRounds(messages: ClaudeMessage[]): ConversationRound[] {
const rounds: ConversationRound[] = [];
let currentRound: ConversationRound | null = null;

for (let i = 0; i c.type === 'tool_result');

if (!isInlineToolResult && currentRound) {
// 结束上一轮
rounds.push(currentRound);
currentRound = null;
}

if (!currentRound) {
currentRound = {
startIndex: i, endIndex: i,
messages: [msg],
tokenCount: estimateMessageTokens(msg),
};
continue;
}
}

// 继续当前轮次
if (currentRound) {
currentRound.endIndex = i;
currentRound.messages.push(msg);
currentRound.tokenCount += estimateMessageTokens(msg);
} else {
// 开头的 system/assistant 消息
currentRound = {
startIndex: i, endIndex: i,
messages: [msg],
tokenCount: estimateMessageTokens(msg),
};
}
}

if (currentRound) rounds.push(currentRound);
return rounds;
}


关键细节:**inline tool_result 不开新轮次**。Anthropic 格式中,工具结果以 `{ role: 'user', content: [{ type: 'tool_result', ... }] }` 形式出现。如果把它当新轮次,会把一个完整的"AI 调工具→获取结果→继续推理"轮次错误地拆成两半。

### 6.12.2 三种压缩触发模式

什么时候该压缩?WaLiCode 设计了三种触发模式,形成三层防线: | 模式 | 触发条件 | 执行方式 | 阻塞对话 | 压缩力度 | | --- | --- | --- | --- | --- | | **主动压缩** | token 占比 > 80% | 同步执行 | 是(短暂) | 中等(保留近期 3-5 轮) | | **后台压缩** | token 占比 > 60% | 异步执行 | 否 | 中等 | | **反应式压缩** | API 返回 413 | 同步紧急执行 | 是 | 激进(只保留 1-2 轮) | ### 6.12.3 AI 摘要 vs 规则摘要

压缩的核心是生成摘要。WaLiCode 支持两种摘要方式:

**AI 摘要(generateAiSummary)**:用专门的压缩 Prompt 让 AI 生成 500 字以内的摘要,保留:用户意图、文件操作、错误信息与解决方案、关键决策、当前进度。质量高但消耗 token。

**规则摘要(generateRuleSummary)**:纯规则提取关键信息,不调 AI。质量低但零成本、零延迟。

// AI 摘要的 System Prompt
const COMPRESSION_SYSTEM_PROMPT = `你是一个对话压缩器。将长对话压缩为 500 字以内的摘要。
必须保留:

  1. 用户的原始意图和需求
  2. 已执行的关键文件操作(创建/修改/删除)
  3. 遇到的错误和解决方案
  4. 做出的重要决策
  5. 当前进度和下一步计划
    可以省略:
  • 工具调用的详细参数
  • AI 的推理过程
  • 重复的失败尝试`;

async function generateAiSummary(messages: ClaudeMessage[]): Promise {
const conversationText = messages.map(msg => {
const content = typeof msg.content === 'string'
? msg.content : JSON.stringify(msg.content);
const label = msg.role === 'user' ? '👤 用户'
: msg.role === 'assistant' ? '🤖 助手'
: msg.role === 'tool' ? '🔧 工具' : '📋 系统';
// 截断过长内容(避免压缩本身消耗太多 token)
const truncated = content.length > 2000
? content.slice(0, 2000) + '\n... (已截断)' : content;
return ${label}:\n${truncated};
}).join('\n\n---\n\n');

try {
let fullResponse = '';
for await (const event of ai.stream([
{ role: 'user', content: COMPRESSION_SYSTEM_PROMPT },
{ role: 'user', content: 请压缩以下对话历史:\n\n${conversationText} },
], undefined, undefined, undefined, { disableTools: true })) {
if (event.type === 'text') fullResponse += event.content || '';
}
return fullResponse || generateRuleSummary(messages);
} catch (err) {
// 降级为规则摘要
return generateRuleSummary(messages);
}
}


// // AI 摘要的 System Prompt
// const COMPRESSION_SYSTEM_PROMPT = 你是一个对话压缩器。将长对话压缩为 500 字以内的摘要。 // 必须保留: // 1. 用户的原始意图和需求 // 2. 已执行的关键文件操作(创建/修改/删除) // 3. 遇到的错误和解决方案 // 4. 做出的重要决策 // 5. 当前进度和下一步计划 // 可以省略: // - 工具调用的详细参数 // - AI 的推理过程 // - 重复的失败尝试;

// async function generateAiSummary(messages: ClaudeMessage[]): Promise {
// const conversationText = messages.map(msg => {
// const content = typeof msg.content === 'string'
// ? msg.content : JSON.stringify(msg.content);
// const label = msg.role === 'user' ? '👤 用户'
// : msg.role === 'assistant' ? '🤖 助手'
// : msg.role === 'tool' ? '🔧 工具' : '📋 系统';
// // 截断过长内容(避免压缩本身消耗太多 token)
// const truncated = content.length > 2000
// ? content.slice(0, 2000) + '\n... (已截断)' : content;
return ${label}:\n${truncated};;
// }).join('\n\n---\n\n');

// try {
// let fullResponse = '';
// for await (const event of ai.stream([
// { role: 'user', content: COMPRESSION_SYSTEM_PROMPT },
// { role: 'user', content: 请压缩以下对话历史:\n\n${conversationText} },
// ], undefined, undefined, undefined, { disableTools: true })) {
// if (event.type === 'text') fullResponse += event.content || '';
// }
return fullResponse || generateRuleSummary(messages);;
// } catch (err) {
// // 降级为规则摘要
return generateRuleSummary(messages);;
// }
// }


package main

import (
"fmt"
"os"
"os/exec"
"strings"
)

// Python: // AI 摘要的 System Prompt
// Python: const COMPRESSION_SYSTEM_PROMPT = `你是一个对话压缩器。将长对话压缩为 500 字以内的摘要。
// Python: 必须保留:
// Python: 1. 用户的原始意图和需求
// Python: 2. 已执行的关键文件操作(创建/修改/删除)
// Python: 3. 遇到的错误和解决方案
// Python: 4. 做出的重要决策
// Python: 5. 当前进度和下一步计划
// Python: 可以省略:
// Python: - 工具调用的详细参数
// Python: - AI 的推理过程
// Python: - 重复的失败尝试`;

// Python: async function generateAiSummary(messages: ClaudeMessage[]): Promise {
// Python: const conversationText = messages.map(msg => {
// Python: const content = typeof msg.content === 'string'
// Python: ? msg.content : JSON.stringify(msg.content);
// Python: const label = msg.role === 'user' ? '👤 用户'
// Python: : msg.role === 'assistant' ? '🤖 助手'
// Python: : msg.role === 'tool' ? '🔧 工具' : '📋 系统';
// Python: // 截断过长内容(避免压缩本身消耗太多 token)
// Python: const truncated = content.length > 2000
// Python: ? content.slice(0, 2000) + '\n... (已截断)' : content;
return `${label}:\n${truncated}`;
// Python: }).join('\n\n---\n\n');

// Python: try {
// Python: let fullResponse = '';
// Python: for await (const event of ai.stream([
// Python: { role: 'user', content: COMPRESSION_SYSTEM_PROMPT },
// Python: { role: 'user', content: `请压缩以下对话历史:\n\n${conversationText}` },
// Python: ], undefined, undefined, undefined, { disableTools: true })) {
// Python: if (event.type === 'text') fullResponse += event.content || '';
// Python: }
return fullResponse || generateRuleSummary(messages);
// Python: } catch (err) {
// Python: // 降级为规则摘要
return generateRuleSummary(messages);
// Python: }
// Python: }

import java.util.;
import java.util.concurrent.
;
import java.util.regex.;
import java.io.
;

// Python: // AI 摘要的 System Prompt
// Python: const COMPRESSION_SYSTEM_PROMPT = 你是一个对话压缩器。将长对话压缩为 500 字以内的摘要。 // Python: 必须保留: // Python: 1. 用户的原始意图和需求 // Python: 2. 已执行的关键文件操作(创建/修改/删除) // Python: 3. 遇到的错误和解决方案 // Python: 4. 做出的重要决策 // Python: 5. 当前进度和下一步计划 // Python: 可以省略: // Python: - 工具调用的详细参数 // Python: - AI 的推理过程 // Python: - 重复的失败尝试;

// Python: async function generateAiSummary(messages: ClaudeMessage[]): Promise {
// Python: const conversationText = messages.map(msg => {
// Python: const content = typeof msg.content === 'string'
// Python: ? msg.content : JSON.stringify(msg.content);
// Python: const label = msg.role === 'user' ? '👤 用户'
// Python: : msg.role === 'assistant' ? '🤖 助手'
// Python: : msg.role === 'tool' ? '🔧 工具' : '📋 系统';
// Python: // 截断过长内容(避免压缩本身消耗太多 token)
// Python: const truncated = content.length > 2000
// Python: ? content.slice(0, 2000) + '\n... (已截断)' : content;
return ${label}:\n${truncated};;
// Python: }).join('\n\n---\n\n');

// Python: try {
// Python: let fullResponse = '';
// Python: for await (const event of ai.stream([
// Python: { role: 'user', content: COMPRESSION_SYSTEM_PROMPT },
// Python: { role: 'user', content: 请压缩以下对话历史:\n\n${conversationText} },
// Python: ], undefined, undefined, undefined, { disableTools: true })) {
// Python: if (event.type === 'text') fullResponse += event.content || '';
// Python: }
return fullResponse || generateRuleSummary(messages);;
// Python: } catch (err) {
// Python: // 降级为规则摘要
return generateRuleSummary(messages);;
// Python: }
// Python: }
}


**降级策略**:AI 摘要失败(API 错误、超时、返回空)时自动降级为规则摘要。为什么不直接报错?因为压缩是基础设施--如果压缩本身都能失败,那对话就没法继续了。规则摘要虽然质量低,但至少保留了基本信息,让对话能继续。

### 6.12.4 增量压缩

问题:如果每次 token 超过 80% 就压缩,压缩后降到 50%,用户继续对话又涨到 80%,又要压缩......这会导致频繁压缩,每次压缩都消耗 token(AI 摘要本身也要花 token)。

WaLiCode 的解决方案:**增量压缩阈值**--只有当 token 再次上升到上次压缩水平的 80% 时才再次压缩。

class ContextCompressor {
private lastCompressTokenLevel: number = 0; // 上次压缩时的 token 水平

shouldCompress(currentTokens: number, maxTokens: number): boolean {
const ratio = currentTokens / maxTokens;

// 首次压缩:80% 阈值
if (this.lastCompressTokenLevel === 0) {
return ratio >= 0.8;
}

// 后续压缩:必须超过上次压缩水平的 80%
// 例如:上次在 80000 token 时压缩,这次必须到 80000 * 0.8 = 64000 才再压缩
// 但由于压缩后降到了 ~50000,用户需要再聊 14000 token 才触发
const threshold = this.lastCompressTokenLevel * 0.8;
return currentTokens >= threshold;
}

async compress(messages: ClaudeMessage[], maxTokens: number) {
const summary = await generateAiSummary(messages);
this.lastCompressTokenLevel = countTokens(messages);
return this.applySummary(messages, summary);
}
}


// class ContextCompressor {
// private lastCompressTokenLevel: number = 0; // 上次压缩时的 token 水平

// shouldCompress(currentTokens: number, maxTokens: number): boolean {
// const ratio = currentTokens / maxTokens;

// // 首次压缩:80% 阈值
// if (this.lastCompressTokenLevel === 0) {
return ratio >= 0.8;;
// }

// // 后续压缩:必须超过上次压缩水平的 80%
// // 例如:上次在 80000 token 时压缩,这次必须到 80000 * 0.8 = 64000 才再压缩
// // 但由于压缩后降到了 ~50000,用户需要再聊 14000 token 才触发
// const threshold = this.lastCompressTokenLevel * 0.8;
return currentTokens >= threshold;;
// }

// async compress(messages: ClaudeMessage[], maxTokens: number) {
// const summary = await generateAiSummary(messages);
// this.lastCompressTokenLevel = countTokens(messages);
return this.applySummary(messages, summary);;
// }
// }


package main

import (
"fmt"
"os"
"os/exec"
"strings"
)

// ContextCompressor - CLI Agent class
type ContextCompressor struct {
// Python: private lastCompressTokenLevel: number = 0; // 上次压缩时的 token 水平

// Python: shouldCompress(currentTokens: number, maxTokens: number): boolean {
// Python: const ratio = currentTokens / maxTokens;

// Python: // 首次压缩:80% 阈值
// Python: if (this.lastCompressTokenLevel === 0) {
return ratio >= 0.8;
// Python: }

// Python: // 后续压缩:必须超过上次压缩水平的 80%
// Python: // 例如:上次在 80000 token 时压缩,这次必须到 80000 * 0.8 = 64000 才再压缩
// Python: // 但由于压缩后降到了 ~50000,用户需要再聊 14000 token 才触发
// Python: const threshold = this.lastCompressTokenLevel * 0.8;
return currentTokens >= threshold;
// Python: }

// Python: async compress(messages: ClaudeMessage[], maxTokens: number) {
// Python: const summary = await generateAiSummary(messages);
// Python: this.lastCompressTokenLevel = countTokens(messages);
return this.applySummary(messages, summary);
// Python: }
// Python: }

}


import java.util.;
import java.util.concurrent.
;
import java.util.regex.;
import java.io.
;

public class ContextCompressor {
// Python: private lastCompressTokenLevel: number = 0; // 上次压缩时的 token 水平

// Python: shouldCompress(currentTokens: number, maxTokens: number): boolean {
// Python: const ratio = currentTokens / maxTokens;

// Python: // 首次压缩:80% 阈值
// Python: if (this.lastCompressTokenLevel === 0) {
return ratio >= 0.8;;
// Python: }

// Python: // 后续压缩:必须超过上次压缩水平的 80%
// Python: // 例如:上次在 80000 token 时压缩,这次必须到 80000 * 0.8 = 64000 才再压缩
// Python: // 但由于压缩后降到了 ~50000,用户需要再聊 14000 token 才触发
// Python: const threshold = this.lastCompressTokenLevel * 0.8;
return currentTokens >= threshold;;
// Python: }

// Python: async compress(messages: ClaudeMessage[], maxTokens: number) {
// Python: const summary = await generateAiSummary(messages);
// Python: this.lastCompressTokenLevel = countTokens(messages);
return this.applySummary(messages, summary);;
// Python: }
// Python: }
}
}


### 6.12.5 Provider/Reducer 可插拔架构

WaLiCode 的上下文管理采用**Provider/Reducer 可插拔架构**,这是生产级系统与 Demo 级系统的核心区别:

**Provider 模式**:可插拔的上下文来源,每个 Provider 负责一种类型的上下文。

interface ContextProvider {
name: string;
provide(): MessageContext[];
}

// 文件上下文:当前打开的文件、最近修改的文件
class FileProvider implements ContextProvider {
name = 'FileProvider';
provide(): MessageContext[] {
return openFiles.map(f => ({
role: 'system',
content: 当前文件: ${f.path}\n${f.content.slice(0, 2000)},
priority: MessagePriority.High,
}));
}
}

// 任务上下文:当前任务的目标和进度
class TaskProvider implements ContextProvider {
name = 'TaskProvider';
provide(): MessageContext[] {
return [{
role: 'system',
content: 当前任务: ${currentTask},
priority: MessagePriority.Critical,
}];
}
}

// 里程碑上下文:关键决策记录
class MilestoneProvider implements ContextProvider {
name = 'MilestoneProvider';
provide(): MessageContext[] {
return milestones.map(m => ({
role: 'system',
content: [${m.type}] ${m.summary},
priority: MessagePriority.High,
}));
}
}


// interface ContextProvider {
// name: string;
// provide(): MessageContext[];
// }

// // 文件上下文:当前打开的文件、最近修改的文件
// class FileProvider implements ContextProvider {
const name = 'FileProvider';;
// provide(): MessageContext[] {
return openFiles.map(f => ({;
// role: 'system',
// content: 当前文件: ${f.path}\n${f.content.slice(0, 2000)},
// priority: MessagePriority.High,
// }));
// }
// }

// // 任务上下文:当前任务的目标和进度
// class TaskProvider implements ContextProvider {
const name = 'TaskProvider';;
// provide(): MessageContext[] {
return [{;
// role: 'system',
// content: 当前任务: ${currentTask},
// priority: MessagePriority.Critical,
// }];
// }
// }

// // 里程碑上下文:关键决策记录
// class MilestoneProvider implements ContextProvider {
const name = 'MilestoneProvider';;
// provide(): MessageContext[] {
return milestones.map(m => ({;
// role: 'system',
// content: [${m.type}] ${m.summary},
// priority: MessagePriority.High,
// }));
// }
// }


package main

import (
"fmt"
"os"
"os/exec"
"strings"
)

// Python: interface ContextProvider {
// Python: name: string;
// Python: provide(): MessageContext[];
// Python: }

// Python: // 文件上下文:当前打开的文件、最近修改的文件

// FileProvider - CLI Agent class
type FileProvider struct {
// Python: name = 'FileProvider';
// Python: provide(): MessageContext[] {
return openFiles.map(f => ({
// Python: role: 'system',
// Python: content: 当前文件: ${f.path}\n${f.content.slice(0, 2000)},
// Python: priority: MessagePriority.High,
// Python: }));
// Python: }
// Python: }

// Python: // 任务上下文:当前任务的目标和进度

// TaskProvider - CLI Agent class
type TaskProvider struct {
// Python: name = 'TaskProvider';
// Python: provide(): MessageContext[] {
return [{
// Python: role: 'system',
// Python: content: 当前任务: ${currentTask},
// Python: priority: MessagePriority.Critical,
// Python: }];
// Python: }
// Python: }

// Python: // 里程碑上下文:关键决策记录

// MilestoneProvider - CLI Agent class
type MilestoneProvider struct {
// Python: name = 'MilestoneProvider';
// Python: provide(): MessageContext[] {
return milestones.map(m => ({
// Python: role: 'system',
// Python: content: [${m.type}] ${m.summary},
// Python: priority: MessagePriority.High,
// Python: }));
// Python: }
// Python: }
}


import java.util.;
import java.util.concurrent.
;
import java.util.regex.;
import java.io.
;

// Python: interface ContextProvider {
// Python: name: string;
// Python: provide(): MessageContext[];
// Python: }

// Python: // 文件上下文:当前打开的文件、最近修改的文件
public class FileProvider {
// Python: name = 'FileProvider';
// Python: provide(): MessageContext[] {
return openFiles.map(f => ({;
// Python: role: 'system',
// Python: content: 当前文件: ${f.path}\n${f.content.slice(0, 2000)},
// Python: priority: MessagePriority.High,
// Python: }));
// Python: }
// Python: }

// Python: // 任务上下文:当前任务的目标和进度
public class TaskProvider {
// Python: name = 'TaskProvider';
// Python: provide(): MessageContext[] {
return [{;
// Python: role: 'system',
// Python: content: 当前任务: ${currentTask},
// Python: priority: MessagePriority.Critical,
// Python: }];
// Python: }
// Python: }

// Python: // 里程碑上下文:关键决策记录
public class MilestoneProvider {
// Python: name = 'MilestoneProvider';
// Python: provide(): MessageContext[] {
return milestones.map(m => ({;
// Python: role: 'system',
// Python: content: [${m.type}] ${m.summary},
// Python: priority: MessagePriority.High,
// Python: }));
// Python: }
// Python: }
}
}


**Reducer 模式**:可插拔的裁剪策略,按不同策略裁剪消息。

interface MessageReducer {
name: string;
reduce(messages: MessageContext[], maxTokens: number): MessageContext[];
}

class PriorityReducer implements MessageReducer {
name = 'PriorityReducer';

reduce(messages: MessageContext[], maxTokens: number): MessageContext[] {
// 第一轮:Critical 和 High 无条件保留
// 第二轮:按优先级从高到低,在预算内逐条追加
// 第三轮:按时间排序输出
const result: MessageContext[] = [];
let usedChars = 0;
const maxChars = maxTokens * 4; // ~4 chars/token

// Critical + High 无条件保留
for (const msg of messages) {
if (msg.priority === MessagePriority.Critical || msg.priority === MessagePriority.High) {
result.push(msg);
usedChars += msg.content.length;
}
}

// 按优先级填充剩余预算
for (const priority of [MessagePriority.Medium, MessagePriority.Low]) {
for (const msg of messages) {
if (msg.priority !== priority || result.includes(msg)) continue;
if (usedChars + msg.content.length > maxChars) continue;
result.push(msg);
usedChars += msg.content.length;
}
}

// 按时间排序
return result.sort((a, b) => a.timestamp - b.timestamp);
}
}


// interface MessageReducer {
// name: string;
// reduce(messages: MessageContext[], maxTokens: number): MessageContext[];
// }

// class PriorityReducer implements MessageReducer {
const name = 'PriorityReducer';;

// reduce(messages: MessageContext[], maxTokens: number): MessageContext[] {
// // 第一轮:Critical 和 High 无条件保留
// // 第二轮:按优先级从高到低,在预算内逐条追加
// // 第三轮:按时间排序输出
// const result: MessageContext[] = [];
// let usedChars = 0;
// const maxChars = maxTokens * 4; // ~4 chars/token

// // Critical + High 无条件保留
// for (const msg of messages) {
// if (msg.priority === MessagePriority.Critical
// || msg.priority === MessagePriority.High) {
// result.push(msg);
// usedChars += msg.content.length;
// }
// }

// // 按优先级填充剩余预算
// for (const priority of [MessagePriority.Medium, MessagePriority.Low]) {
// for (const msg of messages) {
// if (msg.priority !== priority || result.includes(msg)) continue;
// if (usedChars + msg.content.length > maxChars) continue;
// result.push(msg);
// usedChars += msg.content.length;
// }
// }

// // 按时间排序
return result.sort((a, b) => a.timestamp - b.timestamp);;
// }
// }


package main

import (
"fmt"
"os"
"os/exec"
"strings"
)

// Python: interface MessageReducer {
// Python: name: string;
// Python: reduce(messages: MessageContext[], maxTokens: number): MessageContext[];
// Python: }

// PriorityReducer - CLI Agent class
type PriorityReducer struct {
// Python: name = 'PriorityReducer';

// Python: reduce(messages: MessageContext[], maxTokens: number): MessageContext[] {
// Python: // 第一轮:Critical 和 High 无条件保留
// Python: // 第二轮:按优先级从高到低,在预算内逐条追加
// Python: // 第三轮:按时间排序输出
// Python: const result: MessageContext[] = [];
// Python: let usedChars = 0;
// Python: const maxChars = maxTokens * 4; // ~4 chars/token

// Python: // Critical + High 无条件保留
// Python: for (const msg of messages) {
// Python: if (msg.priority === MessagePriority.Critical
// Python: || msg.priority === MessagePriority.High) {
// Python: result.push(msg);
// Python: usedChars += msg.content.length;
// Python: }
// Python: }

// Python: // 按优先级填充剩余预算
// Python: for (const priority of [MessagePriority.Medium, MessagePriority.Low]) {
// Python: for (const msg of messages) {
// Python: if (msg.priority !== priority || result.includes(msg)) continue;
// Python: if (usedChars + msg.content.length > maxChars) continue;
// Python: result.push(msg);
// Python: usedChars += msg.content.length;
// Python: }
// Python: }

// Python: // 按时间排序
return result.sort((a, b) => a.timestamp - b.timestamp);
// Python: }
// Python: }

}


import java.util.;
import java.util.concurrent.
;
import java.util.regex.;
import java.io.
;

// Python: interface MessageReducer {
// Python: name: string;
// Python: reduce(messages: MessageContext[], maxTokens: number): MessageContext[];
// Python: }

public class PriorityReducer {
// Python: name = 'PriorityReducer';

// Python: reduce(messages: MessageContext[], maxTokens: number): MessageContext[] {
// Python: // 第一轮:Critical 和 High 无条件保留
// Python: // 第二轮:按优先级从高到低,在预算内逐条追加
// Python: // 第三轮:按时间排序输出
// Python: const result: MessageContext[] = [];
// Python: let usedChars = 0;
// Python: const maxChars = maxTokens * 4; // ~4 chars/token

// Python: // Critical + High 无条件保留
// Python: for (const msg of messages) {
// Python: if (msg.priority === MessagePriority.Critical
// Python: || msg.priority === MessagePriority.High) {
// Python: result.push(msg);
// Python: usedChars += msg.content.length;
// Python: }
// Python: }

// Python: // 按优先级填充剩余预算
// Python: for (const priority of [MessagePriority.Medium, MessagePriority.Low]) {
// Python: for (const msg of messages) {
// Python: if (msg.priority !== priority || result.includes(msg)) continue;
// Python: if (usedChars + msg.content.length > maxChars) continue;
// Python: result.push(msg);
// Python: usedChars += msg.content.length;
// Python: }
// Python: }

// Python: // 按时间排序
return result.sort((a, b) => a.timestamp - b.timestamp);;
// Python: }
// Python: }
}
}


消息优先级推断规则:
- **Critical**:错误消息、系统指令、当前任务
- **High**:文件操作、用户配置、里程碑事件
- **Medium**:工具调用结果、AI 推理过程
- **Low**:AI 长文本解释、闲聊内容

为什么用可插拔设计?因为不同场景需要不同的 Provider 和 Reducer 组合。编码场景需要 FileProvider + TaskProvider + PriorityReducer;聊天场景可能只需要 MemoryProvider + SlidingWindowReducer。写死在代码里意味着每次改需求都要改核心代码,可插拔意味着只需注册新组件。

### 6.12.6 双作用域记忆系统

WaLiCode 的记忆系统分为两个作用域: | 维度 | User Scope(用户级) | Project Scope(项目级) | | --- | --- | --- | | 作用范围 | 跨项目共享 | 仅当前项目 | | 存储内容 | 用户偏好、通用知识、历史决策 | 架构决策、模块依赖、编码规范 | | 容量限制 | LRU 淘汰,最多 20 条 | LRU 淘汰,最多 20 条 | | 示例 | "用户喜欢用 TypeScript" | "本项目用 pnpm,不要用 npm" | ```
class MemoryService {
  private userMemory: Map = new Map();
  private projectMemory: Map = new Map();
  private readonly MAX_ENTRIES = 20;

  set(scope: 'user' | 'project', key: string, value: string) {
    const store = scope === 'user' ? this.userMemory : this.projectMemory;

    // LRU 淘汰:超过 20 条时删除最久未访问的
    if (store.size >= this.MAX_ENTRIES && !store.has(key)) {
      const oldest = [...store.entries()]
        .sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)[0];
      store.delete(oldest[0]);
    }

    store.set(key, { value, lastAccessed: Date.now() });
  }

  get(scope: 'user' | 'project', key: string): string | undefined {
    const store = scope === 'user' ? this.userMemory : this.projectMemory;
    const entry = store.get(key);
    if (entry) entry.lastAccessed = Date.now(); // LRU 更新
    return entry?.value;
  }

  // 生成记忆 Prompt 注入
  toPrompt(scope: 'user' | 'project' | 'both'): string {
    const entries: MemoryEntry[] = [];
    if (scope === 'user' || scope === 'both') {
      entries.push(...this.userMemory.values());
    }
    if (scope === 'project' || scope === 'both') {
      entries.push(...this.projectMemory.values());
    }
    return entries.map(e => `- ${e.value}`).join('\n');
  }
}
// class MemoryService {
  // private userMemory: Map = new Map();
  // private projectMemory: Map = new Map();
  // private readonly MAX_ENTRIES = 20;

  // set(scope: 'user' | 'project', key: string, value: string) {
  // const store = scope === 'user' ? this.userMemory : this.projectMemory;

  // // LRU 淘汰:超过 20 条时删除最久未访问的
  // if (store.size >= this.MAX_ENTRIES && !store.has(key)) {
  // const oldest = [...store.entries()]
  // .sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)[0];
  // store.delete(oldest[0]);
  // }

  // store.set(key, { value, lastAccessed: Date.now() });
  // }

  // get(scope: 'user' | 'project', key: string): string | undefined {
  // const store = scope === 'user' ? this.userMemory : this.projectMemory;
  // const entry = store.get(key);
  // if (entry) entry.lastAccessed = Date.now(); // LRU 更新
  return entry?.value;;
  // }

  // // 生成记忆 Prompt 注入
  // toPrompt(scope: 'user' | 'project' | 'both'): string {
  // const entries: MemoryEntry[] = [];
  // if (scope === 'user' || scope === 'both') {
  // entries.push(...this.userMemory.values());
  // }
  // if (scope === 'project' || scope === 'both') {
  // entries.push(...this.projectMemory.values());
  // }
  return entries.map(e => `- ${e.value}`).join('\n');;
  // }
  // }
package main

import (
	"fmt"
	"os"
	"os/exec"
	"strings"
)

// MemoryService - CLI Agent class
type MemoryService struct {
	// Python: private userMemory: Map = new Map();
	// Python: private projectMemory: Map = new Map();
	// Python: private readonly MAX_ENTRIES = 20;

	// Python: set(scope: 'user' | 'project', key: string, value: string) {
	// Python: const store = scope === 'user' ? this.userMemory : this.projectMemory;

	// Python: // LRU 淘汰:超过 20 条时删除最久未访问的
	// Python: if (store.size >= this.MAX_ENTRIES && !store.has(key)) {
	// Python: const oldest = [...store.entries()]
	// Python: .sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)[0];
	// Python: store.delete(oldest[0]);
	// Python: }

	// Python: store.set(key, { value, lastAccessed: Date.now() });
	// Python: }

	// Python: get(scope: 'user' | 'project', key: string): string | undefined {
	// Python: const store = scope === 'user' ? this.userMemory : this.projectMemory;
	// Python: const entry = store.get(key);
	// Python: if (entry) entry.lastAccessed = Date.now(); // LRU 更新
	return entry?.value;
	// Python: }

	// Python: // 生成记忆 Prompt 注入
	// Python: toPrompt(scope: 'user' | 'project' | 'both'): string {
	// Python: const entries: MemoryEntry[] = [];
	// Python: if (scope === 'user' || scope === 'both') {
	// Python: entries.push(...this.userMemory.values());
	// Python: }
	// Python: if (scope === 'project' || scope === 'both') {
	// Python: entries.push(...this.projectMemory.values());
	// Python: }
	return entries.map(e => `- ${e.value}`).join('\n');
	// Python: }
	// Python: }
}
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.*;
import java.io.*;

public class MemoryService {
        // Python: private userMemory: Map = new Map();
        // Python: private projectMemory: Map = new Map();
        // Python: private readonly MAX_ENTRIES = 20;

        // Python: set(scope: 'user' | 'project', key: string, value: string) {
        // Python: const store = scope === 'user' ? this.userMemory : this.projectMemory;

        // Python: // LRU 淘汰:超过 20 条时删除最久未访问的
        // Python: if (store.size >= this.MAX_ENTRIES && !store.has(key)) {
        // Python: const oldest = [...store.entries()]
        // Python: .sort((a, b) => a[1].lastAccessed - b[1].lastAccessed)[0];
        // Python: store.delete(oldest[0]);
        // Python: }

        // Python: store.set(key, { value, lastAccessed: Date.now() });
        // Python: }

        // Python: get(scope: 'user' | 'project', key: string): string | undefined {
        // Python: const store = scope === 'user' ? this.userMemory : this.projectMemory;
        // Python: const entry = store.get(key);
        // Python: if (entry) entry.lastAccessed = Date.now(); // LRU 更新
        return entry?.value;;
        // Python: }

        // Python: // 生成记忆 Prompt 注入
        // Python: toPrompt(scope: 'user' | 'project' | 'both'): string {
        // Python: const entries: MemoryEntry[] = [];
        // Python: if (scope === 'user' || scope === 'both') {
        // Python: entries.push(...this.userMemory.values());
        // Python: }
        // Python: if (scope === 'project' || scope === 'both') {
        // Python: entries.push(...this.projectMemory.values());
        // Python: }
        return entries.map(e => `- ${e.value}`).join('\n');;
        // Python: }
        // Python: }
    }
}

6.12.6 压缩可靠性保障

上下文压缩是 Agent 对话稳定性的最后一道防线,但如果压缩本身不可靠,反而会成为新的问题源。生产环境中必须解决三个可靠性挑战:压缩失败回退、语义丢失检测、压缩对对话连贯性的影响。

挑战 1:压缩失败回退

AI 摘要依赖 LLM 生成,而 LLM 可能超时、限流或返回乱码。WaLiCode 的做法是三层降级链: | 层级 | 触发条件 | 策略 | 代价 | | --- | --- | --- | --- | | L1 AI 摘要 | 正常流程 | 调 LLM 生成 500 字以内摘要 | 消耗 ~2000 token | | L2 规则摘要 | AI 超时/限流/JSON 解析失败 | 正则提取关键信息(用户意图/文件操作/错误信息/决策) | 质量较低,零 token | | L3 硬截断 | 规则摘要也失败(极端情况) | 只保留最近 2 轮对话 + system prompt | 丢失全部历史 | ```
class CompressionFallbackChain:
"""三层降级压缩链"""

def init(self, llm_client):
self.llm = llm_client
self.max_ai_attempts = 2
self.max_ai_tokens = 2000

def compress(self, messages: list, target_ratio: float = 0.3) -> dict:
"""压缩消息列表,返回 {success, strategy, compressed, error}"""

L1: 尝试 AI 摘要

for attempt in range(self.max_ai_attempts):
try:
result = self._ai_compress(messages, target_ratio)
if result and self._validate(result):
return {"success": True, "strategy": "ai", "compressed": result}
except (TimeoutError, RateLimitError) as e:
print(f"AI 摘要第{attempt+1}次失败: {e}")
continue
except Exception as e:
print(f"AI 摘要异常: {e}")
break

L2: 降级为规则摘要

try:
result = self._rule_compress(messages)
if result:
return {"success": True, "strategy": "rule", "compressed": result}
except Exception as e:
print(f"规则摘要失败: {e}")

L3: 硬截断

return {
"success": True,
"strategy": "truncate",
"compressed": messages[:4], # system + 最近2轮
"warning": "硬截断,历史已丢失"
}

def _ai_compress(self, messages, ratio):
"""AI 摘要"""
prompt = f"将以下对话压缩为500字以内的摘要,保留:用户意图/文件操作/错误与修复/关键决策/当前进度\n\n{messages}"
return self.llm.chat(prompt, max_tokens=self.max_ai_tokens)

def _rule_compress(self, messages):
"""规则摘要:正则提取关键信息"""
import re
summary_parts = []
for msg in messages:
content = msg.get("content", "")

提取用户意图

if msg["role"] == "user":
summary_parts.append(f"[用户] {content[:100]}")

提取错误信息

elif "error" in content.lower() or "报错" in content:
error_match = re.search(r'(error|报错|异常)[:\s]*([^\n]+)', content, re.I)
if error_match:
summary_parts.append(f"[错误] {error_match.group(2)[:80]}")

提取文件操作

file_match = re.search(r'(创建|修改|删除|读取)[文件了]*(.+?.[a-z]+)', content)
if file_match:
summary_parts.append(f"[文件] {file_match.group(0)[:80]}")
return "\n".join(summary_parts) if summary_parts else None

def _validate(self, result):
"""校验摘要质量"""
if not result or len(result) 2000:
return False
return True


interface CompressionResult {
success: boolean;
strategy: 'ai' | 'rule' | 'truncate';
compressed: Message[];
warning?: string;
}

class CompressionFallbackChain {
private llm: LLMClient;
private maxAiAttempts = 2;

constructor(llm: LLMClient) {
this.llm = llm;
}

async compress(messages: Message[], targetRatio = 0.3): Promise {
// L1: AI 摘要
for (let i = 0; i {
const prompt = 将以下对话压缩为500字以内的摘要...\n\n${JSON.stringify(messages)};
const result = await this.llm.chat(prompt, { maxTokens: 2000 });
return [{ role: 'system', content: [历史摘要] ${result} }];
}

private ruleCompress(messages: Message[]): Message[] | null {
const parts: string[] = [];
for (const msg of messages) {
if (msg.role === 'user') parts.push([用户] ${msg.content.slice(0, 100)});
if (/error|报错|异常/i.test(msg.content)) {
const m = msg.content.match(/(error|报错|异常)[:\s]*([^\n]+)/i);
if (m) parts.push([错误] ${m[2].slice(0, 80)});
}
}
return parts.length ? [{ role: 'system', content: [历史摘要] ${parts.join('\n')} }] : null;
}

private validate(result: Message[]): boolean {
const content = result[0]?.content ?? '';
return content.length >= 20 && content.length 2 {
parts = append(parts, "[错误] "+truncate(m[2], 80))
}
}
}
if len(parts) == 0 {
return nil, errors.New("无关键信息")
}
return []Message{{Role: "system", Content: "[历史摘要] " + strings.Join(parts, "\n")}}, nil
}

func (c *CompressionFallbackChain) validate(result []Message) bool {
if len(result) == 0 {
return false
}
content := result[0].Content
return len(content) >= 20 && len(content) > compressed;
String warning;
}

class CompressionFallbackChain {
private final LLMClient llm;
private final int maxAiAttempts = 2;

public CompressionFallbackChain(LLMClient llm) {
this.llm = llm;
}

public CompressionResult compress(List> messages, double targetRatio) {
// L1: AI 摘要
for (int i = 0; i > result = aiCompress(messages, targetRatio);
if (result != null && validate(result)) {
CompressionResult r = new CompressionResult();
r.success = true;
r.strategy = "ai";
r.compressed = result;
return r;
}
} catch (Exception e) {
System.err.println("AI 摘要第" + (i + 1) + "次失败: " + e.getMessage());
}
}

// L2: 规则摘要
try {
List> result = ruleCompress(messages);
if (result != null) {
CompressionResult r = new CompressionResult();
r.success = true;
r.strategy = "rule";
r.compressed = result;
return r;
}
} catch (Exception e) {
System.err.println("规则摘要失败: " + e.getMessage());
}

// L3: 硬截断
CompressionResult r = new CompressionResult();
r.success = true;
r.strategy = "truncate";
int endIdx = Math.min(4, messages.size());
r.compressed = new ArrayList<>(messages.subList(0, endIdx));
r.warning = "硬截断,历史已丢失";
return r;
}

private List> aiCompress(List> messages, double ratio) throws Exception {
String prompt = "将以下对话压缩为500字以内的摘要...\n\n" + messages.toString();
String result = llm.chat(prompt, 2000);
if (result == null || result.length() 2000) {
return null;
}
return List.of(Map.of("role", "system", "content", "[历史摘要] " + result));
}

private List> ruleCompress(List> messages) {
List parts = new ArrayList<>();
Pattern errorPattern = Pattern.compile("(?i)(error|报错|异常)[:\\s]*([^\\n]+)");
for (Map msg : messages) {
String content = msg.getOrDefault("content", "");
if ("user".equals(msg.get("role"))) {
parts.add("[用户] " + content.substring(0, Math.min(100, content.length())));
}
Matcher m = errorPattern.matcher(content);
if (m.find()) {
parts.add("[错误] " + m.group(2).substring(0, Math.min(80, m.group(2).length())));
}
}
if (parts.isEmpty()) return null;
return List.of(Map.of("role", "system", "content", "[历史摘要] " + String.join("\n", parts)));
}

private boolean validate(List> result) {
if (result == null || result.isEmpty()) return false;
String content = result.get(0).getOrDefault("content", "");
return content.length() >= 20 && content.length() !constraintsAfter.includes(c));

if (lost.length > 0) {
// 将丢失的约束重新注入摘要
summary = [重要约束] ${lost.join('; ')}\n + summary;
}

实现要点:约束提取用正则匹配常见模式("不要..."/"必须..."/"用..."/"目标..."),不依赖 LLM。这是一个低成本高收益的保障措施——零额外 API 调用,只需几十行正则代码,就能避免最常见的压缩事故。

挑战 3:压缩对对话连贯性的影响

压缩会改变上下文结构,可能导致 LLM 的回答出现断层——比如用户问"刚才那个文件改好了吗?",但压缩后历史中已经没有文件操作的记录了。三种应对策略: | 策略 | 做法 | 适用场景 | | --- | --- | --- | | 压缩标记注入 | 在压缩位置插入 [已压缩 N 轮对话] 标记,让 LLM 知道有历史被省略 | 大多数场景 | | 关键实体保留 | 压缩时强制保留最近提及的文件名、函数名、变量名,即使轮次被压缩 | 代码 Agent / IDE Agent | | 按需展开 | 用户追问历史细节时,从向量库检索被压缩的原始对话片段,临时注入上下文 | 长对话 > 50 轮 | 🔗 5.12 核心要点

轮次分割:按 user message 分割对话为多个轮次,inline tool_result 不开新轮次。

三层压缩触发:主动(80%)→ 后台(60%)→ 反应式(413),层层递进。

AI 摘要 vs 规则摘要:AI 质量高但贵,规则零成本但糙,AI 失败降级为规则。

增量压缩:上次压缩水平的 80% 才再压缩,避免频繁压缩浪费 token。

Provider/Reducer 架构:可插拔设计,按场景组合不同的上下文来源和裁剪策略。

双作用域记忆:User Scope 跨项目,Project Scope 仅当前项目,LRU 淘汰。

6.13 主流记忆框架对比(2026更新)

前面几节讲了记忆系统的原理和工程实践。2024-2025 年,一批专门的记忆框架崛起,它们把记忆管理从 Agent 主逻辑中解耦出来,提供开箱即用的记忆层。面试中经常被问到:"你了解哪些记忆框架?它们有什么区别?"

Mem0:三层记忆架构(Episodic + Semantic + Procedural)

Mem0 是 2024 年开源的记忆框架,核心理念是把记忆按性质分为三层,而非简单堆在向量库里:
💡 Mem0 的三层记忆

  • Episodic(情景记忆):存储具体的交互片段,如"用户上周问了X问题,Agent回答了Y"。按时间线组织,支持回溯。
  • Semantic(语义记忆):从交互中提取的结构化知识,如"用户偏好简洁风格""用户是Java开发者"。去重后存储,支持增量更新。
  • Procedural(过程记忆):从历史交互中总结的"怎么做"类知识,如"处理JSON解析错误时,先检查编码再检查格式"。类似人类的技能记忆。

Mem0 的工作流程:对话输入 → LLM 提取记忆 → 分类(episodic/semantic/procedural)→ 去重/合并 → 存储。检索时按当前意图从三层记忆分别召回,合并后注入上下文。面试要点:Mem0 的三层不是物理分区,而是逻辑分类,底层可以共用同一个向量库。

Letta(原 MemGPT):自编辑记忆 + 记忆分页

Letta 是 MemGPT 团队的商业化产品,核心创新是把操作系统的虚拟内存概念引入 Agent 记忆:
💡 Letta 的两大核心机制

  • 自编辑记忆(Self-Editing Memory):Agent 可以主动修改自己的记忆。当发现旧记忆过时或矛盾时,LLM 自行调用 memory_edit 工具更新记忆内容,而非简单追加。类似人类"纠正记忆"的过程。
  • 记忆分页(Memory Paging):把记忆分为Core Memory(常驻上下文,类似RAM)和Archival Memory(外部存储,类似磁盘)。Core Memory 满了时,自动把低优先级记忆"换出"到 Archival,需要时再"换入"。这和操作系统的页面置换机制几乎一模一样。

面试要点:Letta 的记忆分页解决的是有限上下文窗口 vs 无限记忆需求的矛盾。Core Memory 放最关键的用户信息和当前任务状态(通常 < 2000 token),Archival Memory 放历史交互。LLM 通过 search_archival 和 edit_core_memory 两个工具自主管理记忆流转。

Zep:时序知识图谱 + 自动摘要

Zep 走了一条完全不同的路线——用知识图谱组织记忆,并引入时间维度:
💡 Zep 的两个核心设计

  • 时序知识图谱(Temporal Knowledge Graph):把对话中的实体、关系、事件抽取为三元组,并附带时间戳。当用户说"我上个月换了一台Mac"时,Zep 不会只存原始文本,而是建一条 (用户, 换设备, Mac, 2025-06) 的图谱边。后续查询时可以回答"用户什么时候换的电脑"这类时序问题。
  • 自动摘要(Auto-Summarization):每轮对话后自动生成摘要,摘要本身也会被图谱化。旧摘要会随时间衰减,新摘要权重更高。这解决了"半年前的摘要和昨天的摘要权重一样"的问题。

Zep 的优势在于结构化检索。向量检索只能回答"什么和X相关",Zep 的图谱检索还能回答"X是什么时候发生的""X和Y是什么关系""X最近有变化吗"。代价是抽取质量依赖 NLP 能力,复杂场景下图谱构建成本较高。

三者对比 | 维度 | Mem0 | Letta (MemGPT) | Zep | | --- | --- | --- | --- | | 记忆组织 | 三层分类(episodic/semantic/procedural) | 两级分页(core/archival) | 时序知识图谱 | | 记忆更新 | LLM 提取 + 去重合并 | Agent 自编辑(工具调用) | 图谱增量 + 自动摘要 | | 检索方式 | 向量相似度(分层召回) | Core 直接注入 + Archival 语义搜索 | 图谱查询 + 向量检索 + 时序过滤 | | 核心优势 | 分类清晰,开箱即用 | Agent 自主管理记忆,最像OS | 支持时序查询,结构化最强 | | 适用场景 | 通用对话 Agent、客服 | 长期伴随型 Agent、个人助手 | 需要时序推理、实体关系追踪 | | 开源协议 | Apache 2.0 | Apache 2.0 | Apache 2.0 | 🔗 5.13 核心要点

Mem0:三层记忆(episodic/semantic/procedural)按性质分类,LLM 自动提取+去重,适合通用场景。

Letta:OS 式记忆分页(core/archival),Agent 自编辑记忆,最接近"Agent 自我管理"的理念。

Zep:时序知识图谱 + 自动摘要,支持"什么时候""什么关系"类时序查询,结构化最强。

选型建议:通用对话选 Mem0,长期伴随选 Letta,需要时序推理选 Zep。也可组合使用——Zep 做图谱存储 + Mem0 做分层检索。
📋 八股总结 - 面试高频考点

Q1: Agent 的记忆系统分为哪几层?各自的特点和实现方式?

  **短期记忆**:= LLM 上下文窗口,存当前对话历史。容量 4K~200K token,速度最快,对话结束即消失。

  **工作记忆**:= Scratchpad/变量,存当前任务的中间状态。任务结束清除,辅助多步推理。

  **长期记忆**:= 向量数据库,存历史交互经验。理论无限容量,需语义检索,永久持久化。

  三者关系:短期→工作(任务相关提取),短期→长期(压缩存储),长期→工作(检索回忆)。

Q2: 短期记忆的压缩策略有哪些?各有什么优缺点?

  **1 滑窗截断**:保留最近 N 轮,丢弃旧的。简单但可能丢失关键信息。

  **2 摘要压缩**:用 LLM 对旧对话生成摘要替代原文。保留关键信息但消耗额外 token。

  **3 选择性保留**:用 LLM 判断消息重要性,只保留重要的。信息密度高但判断可能不准。

  **4 分层压缩**:结合以上策略,近期完整保留、中期摘要、远期存入长期记忆。效果最好但实现最复杂。

Q3: 向量数据库在 Agent 记忆中的作用?检索流程是什么?

  **作用**:作为长期记忆的存储引擎,支持语义相似度检索,让 Agent 能"回忆"与当前查询相关的历史交互。

  **写入流程**:交互文本 → Embedding 模型 → 向量 → 存入向量数据库(附带元数据如时间、用户ID)。

  **检索流程**:用户查询 → Embedding → 在向量库中做相似度搜索(如余弦相似度)→ 返回 Top-K 最相关记忆 → 注入 prompt。

Q4: 反思记忆、实体记忆、情景记忆分别是什么?

  **反思记忆**:定期对历史交互进行反思,提取规律和偏好(如"用户偏好简洁回答"),存储提炼后的知识而非原始对话。

  **实体记忆**:以实体为中心组织记忆,维护实体知识图谱(如"小明"的属性和关系),实现实体级别的快速回忆。

  **情景记忆**:按时间线存储情节,带时间戳和上下文标签,适合"上次聊到哪了"这类时间敏感场景。

Q5: 为什么不用 SQL 数据库而用向量数据库存 Agent 记忆?

  SQL 数据库做精确查询(WHERE name='小明'),但记忆检索是**语义相似度**查询--"和用户当前问题意思相近的历史交互"。这需要把文本转为向量,在向量空间中计算距离。

  SQL 无法高效做语义相似度搜索(只能用 LIKE 做模糊匹配,效果差)。向量数据库专门优化了高维向量的相似度搜索,能在毫秒级从百万条记忆中找到最相关的几条。

  实践中也可用 pgvector(PostgreSQL 扩展)兼顾两者。

Q6: 上下文膨胀(Context Rot)是什么?五种压缩策略的优先级如何排序?

  **Context Rot**:长任务中上下文不断膨胀,导致行为漂移、注意力稀释、成本飙升和窗口溢出。

  五种压缩策略优先级(从最优到最差):

  **1 语义保护型**:锁定核心指令 + 保护行动-结果对完整性,只压缩中间推理。压缩率和信息完整性最优。

  **2 选择性保留**:按消息类型分别设定保留策略,信息密度高。

  **3 摘要压缩**:有损压缩,保留关键信息但可能丢失细节。

  **4 滑动窗口**:按时间丢弃,不考虑重要性。

  **5 截断**:最简单但最粗暴,可能导致行为不一致。

Q7: Context Engineering 和 Prompt Engineering 的区别是什么?

  **Prompt Engineering**:关注单次请求的措辞优化,是静态的、一次性的。如:优化 System Prompt、设计 Few-shot 示例。

  **Context Engineering**:关注整个上下文的动态编排,是持续性的、动态的。如:记忆的检索/压缩/淘汰策略、工具结果的格式化过滤、多源信息的优先级排序。

  类比:Prompt Engineering 是写一首诗(关注措辞),Context Engineering 是管理一个图书馆(关注信息流治理)。

Q8: 记忆生命周期包含哪些阶段?各阶段的治理要点是什么?

  五个阶段:

  **1 写入**:记录交互 + 附带元数据,默认慷慨写入,后续靠淘汰筛选。

  **2 索引**:向量索引 + 关键词索引 + 时间索引 + 类型索引,多路召回提高命中率。

  **3 检索**:JIT 模式按需取信息,相似度阈值过滤 + 元数据过滤 + Top-K 截断。

  **4 压缩**:语义保护型优先,锁定 System Prompt 和行动-结果对。

  **5 淘汰**:时效淘汰 + 容量淘汰 + 质量淘汰,淘汰前可做最终摘要归档。

Q9: Reflexion 框架的反思循环是如何工作的?

  Reflexion 的核心流程:

  **1 接收任务** → 检索相关反思经验(如有)

  **2 执行行动** → 用 ReAct 循环完成任务

  **3 评估结果** → 判断是否成功

  **4 成功** → 记录为情景记忆(完整事件)

  **5 失败** → 触发反思(分析失败原因、提炼改进策略)

  **6 存入反思记忆** → 下次重试时参考经验教训

  关键创新:不是简单重试,而是每次失败都提炼经验存入记忆,使 Agent 从"重复犯错的傻瓜"进化为"从失败中学习的聪明人"。

Q10: 长期记忆为什么要分为静态和动态?各自的存储、召回、压缩策略有何不同?

  不分类会导致三类问题:**存储浪费**(偏好存向量库每次检索没必要)、**信息过时**(摘要不更新会冗余)、**召回混乱**(规则和经验混在一起模型无法区分)。

  **静态长期记忆**(偏好/规则/知识):变化频率低 → 存本地配置文件 → 直接注入 System Prompt → 不压缩(本身已精炼)。

  **动态长期记忆**(摘要/经验/模式):变化频率高 → 存向量数据库 → 按相关性语义检索 → 定期压缩摘要+淘汰过时内容。

  面试要点:先分类再谈策略,不要一上来就说"存向量数据库"。

Q11: 为什么不是每轮对话都触发长期记忆写入?什么信息"值得记住"?

  **不是每轮都触发**,否则长期记忆库会变成完整对话日志的备份,噪音爆炸,检索失效。

  记忆写入触发条件:用户表达新偏好→写入静态记忆;完成有价值任务→写入动态记忆(执行经验);发现新上下文信息→写入动态记忆(知识片段);简单闲聊→不触发写入。

  判断"值得记住"的两个标准:**1 是否有长期参考价值**(未来对话中可能再次需要);**2 是否包含决策/偏好/知识点**(明确的、可复用的信息)。

  链路:对话→信息提取→价值评估→分类→写入。重点强调筛选而非全量。

Q12: 召回太多长期记忆会导致什么问题?如何防控上下文污染?

  **三大危害**:

  **1 注意力稀释**:模型关注无关内容,忽略核心问题

  **2 指令冲突**:多条记忆互相矛盾(如"偏好简洁"vs"要求详细")

  **3 Token浪费**:无关记忆消耗宝贵上下文窗口

  **四道防线**:

  **1 数量上限**:每次召回 ≤5 条,简单问题 ≤3 条

  **2 相关性阈值**:相似度  最近经验 > 补充信息

  召回决策靠三维度评分:意图匹配×时间衰减×频率加权。面试要点:先谈召回决策再谈污染防控,不要只说"用向量搜索"。

Q13: 多轮对话 Agent 怎么解决状态爆炸 / 上下文溢出?生产级的三板斧方案是什么?

  这是落地项目一定会遇到的痛点,面试官深挖项目必问。我主要靠三套方案组合解决:

  **1 规范状态结构**:用 LangGraph 的 TypedDict 定义固定的状态模板,只留存核心业务变量,去除无效冗余信息,防止状态字段无限膨胀。

  **2 智能截断**:不做简单粗暴的字符删减,而是按优先级保留上下文--**系统提示词 > 最近对话记录 > 当前核心任务目标**。低优先级的早期内容优先被截断。

  **3 对话摘要**:把老旧的历史对话压缩成简短摘要,放在上下文头部。既保留完整语境,又大幅减少上下文占用。类似人类的"回忆"而非"逐字回放"。

  面试要点:不要只说"用向量数据库",要展示"规范状态→智能截断→摘要压缩"三板斧组合思维,体现生产级落地经验而非纸上谈兵。

Q14: 动态 Prompt 和静态 Prompt 有什么区别?Agent 每轮对话开始时,上下文是如何动态组装的?

  **静态Prompt** = System Prompt(角色定义+安全规则+输出格式约束) + 工具Schema + Skill触发词。整个会话内不变,放在Prompt最前面,100%命中缓存,是Agent的"宪法"。

  **动态Prompt** = 记忆召回片段(按意图检索≤5条) + Skill完整指令(触发时加载) + 对话历史(逐轮增长) + 用户当前输入。每轮都变,放Prompt后面,按变化频率分层。

  **组装策略**:四层叠放,固定→半固定→动态→当前请求:

  Layer 1 静态框架:System Prompt + 工具Schema + Skill触发词(100%缓存命中)

  Layer 2 记忆注入:长期记忆召回片段(同会话内部分命中缓存)

  Layer 3 对话历史:多轮交互记录+工具返回结果(旧部分可缓存)

  Layer 4 当前请求:用户输入(完全不命中缓存)

  核心原则:**不变的放前面最大化缓存,变化的放后面最小化缓存失效**。面试要点:先定义静态和动态,再说组装顺序和缓存策略,不要只说"每次都重新生成Prompt"。

Q15: 多轮对话Agent的状态爆炸,除了三板斧还有什么更完整的系统级解决方案?

  Q13的三板斧(规范状态+智能截断+对话摘要)是入门级回答。**完整生产级方案是四层递进的泄压体系**:

  **Layer 1 预防**——不让压力累积:

  ① Token Budget全局预算控制(超预算立即停止)

  ② JIT召回原则(只检索当前步骤需要的记忆)

  ③ 输出长度约束(要求模型只输出关键结论)

  ④ 工具结果精简(只保留关键行而非原样塞入)

  **Layer 2 压缩**——在信息完整前提下缩减体积:

  ① 语义保护型压缩(锁定System Prompt和行动-结果对,只压缩中间推理)

  ② 三层压缩触发(主动80%→后台60%→反应式413)

  ③ 增量压缩(只压缩上次压缩后新增部分)

  **Layer 3 淘汰**——扔掉不再需要的:

  ① 记忆治理策略(时效+容量+质量三维度淘汰)

  ② 召回数量硬限制(≤5条)

  ③ 工具结果裁剪(只保留Top-K关键行)

  ④ 淘汰前归档(做最终摘要存入长期记忆)

  **Layer 4 兜底**——当所有策略都不够时:

  ① 紧急压缩(413状态码触发最激进压缩)

  ② 降级回答(告诉用户建议开新会话)

  ③ 自动会话切分(超50轮自动摘要+开新会话)

  ④ 人工介入(预算超限+压缩失败后通知用户)

  面试要点:不是某一层单独起作用,而是四层递进。预防失败→压缩兜住,压缩不够→淘汰兜住,淘汰也不够→紧急兜底。体现"从预防到兜底的全链路思维"。

Q16: Mem0 的三层记忆(Episodic / Semantic / Procedural)分别是什麽?如何协同工作?

  **Episodic(情景记忆)**:存储具体交互片段,按时间线组织,支持回溯。如"用户上周问了X,Agent回答了Y"。

  **Semantic(语义记忆)**:从交互中提取的结构化知识,去重后增量更新。如"用户偏好简洁风格""用户是Java开发者"。

  **Procedural(过程记忆)**:从历史中总结的"怎么做"类技能知识。如"处理JSON解析错误时,先检查编码再检查格式"。

  **协同流程**:对话输入 → LLM 提取记忆 → 分类(episodic/semantic/procedural)→ 去重/合并 → 存储。检索时按当前意图从三层分别召回,合并后注入上下文。三层是**逻辑分类**而非物理分区,底层可共用同一个向量库。

  面试要点:先说三层分别是什么,再说提取→分类→去重→存储的写入流程,最后说分层召回的检索策略。不要把三层说成三个数据库。

Q17: Letta(MemGPT)如何实现记忆分页?Zep 的时序知识图谱有什么优势?

  **Letta 记忆分页**:借鉴 OS 虚拟内存机制,把记忆分为 Core Memory(常驻上下文,类似 RAM,放最关键的用户信息和任务状态,通常 < 2000 token)和 Archival Memory(外部存储,类似磁盘,放历史交互)。Core 满了自动把低优先级记忆"换出"到 Archival,需要时通过 search_archival 工具"换入"。Agent 还能通过 edit_core_memory 工具**自编辑**记忆,纠正过时信息。

  **Zep 时序知识图谱**:把对话中的实体、关系、事件抽取为带时间戳的三元组,如 (用户, 换设备, Mac, 2025-06)。优势:①支持时序查询("用户什么时候换的电脑"),向量检索做不到;②支持关系查询("X和Y什么关系");③自动摘要随时间衰减,新摘要权重更高,解决旧摘要和新摘要同等权重的问题。代价是图谱构建质量依赖 NLP 抽取能力。

  面试要点:Letta 重点说"OS 式分页 + 自编辑",Zep 重点说"时序 + 结构化"。对比时强调:Letta 解决的是有限窗口 vs 无限记忆的矛盾,Zep 解决的是向量检索无法回答时序和关系查询的矛盾。

🧠 L3 开放题(面试高频)

深度思考 · 无标准答案

开放题

为什么不是每轮对话都触发长期记忆写入?什么信息"值得记住"?请描述记忆写入的完整链路。

    **参考思路:**

    不是每轮都触发,否则记忆库变成对话日志备份,噪音爆炸检索失效。

    **触发条件**:用户表达新偏好→静态记忆;完成有价值任务→动态记忆(经验);发现新上下文→动态记忆(知识);简单闲聊→不触发。

    **判断标准**:1是否有长期参考价值(未来可能再次需要)2是否包含决策/偏好/知识点(明确的、可复用的信息)。

    **完整链路**:对话→信息提取→价值评估→分类(静态/动态)→写入(配置文件/向量数据库)。

    关键词:筛选机制、价值评估、分类写入、不是全量记录。

    关联章节:4.10 | 面试题:Q11

开放题

如果召回太多长期记忆,会导致什么问题?如何防控上下文污染?请描述召回决策机制和四道防线。

    **参考思路:**

    **三大危害**:1注意力稀释(模型关注无关内容忽略核心问题)2指令冲突(多条记忆互相矛盾)3Token浪费(无关记忆消耗上下文窗口)。

    **召回决策机制**(三维度评分):意图匹配(语义相似度)× 时间衰减(越近期权重越高)× 频率加权(多次验证有效的记忆权重更高)。综合评分决定是否召回。

    **四道防线**:1数量上限(≤5条,简单≤3)2相关性阈值(相似度最近经验>补充信息)。

    关键词:上下文污染、注意力稀释、指令冲突、四道防线、召回评分公式。

    关联章节:4.11 | 面试题:Q12

开放题

动态Prompt和静态Prompt有什么区别?Agent每轮对话开始时,上下文是如何动态组装的?为什么固定前缀放最前面能最大化缓存命中率?

    **参考思路:**

    **静态Prompt** = System Prompt(角色+规则) + 工具Schema + Skill触发词,整个会话不变,是Agent的"宪法"。

    **动态Prompt** = 记忆召回片段 + Skill完整指令 + 对话历史 + 用户当前输入,每轮都变。

    **组装策略**:四层叠放,固定→半固定→动态→当前请求。

    Layer 1 静态框架(100%缓存命中)

    Layer 2 记忆注入(同会话内部分命中缓存)

    Layer 3 对话历史(旧部分可缓存)

    Layer 4 当前请求(完全不命中缓存)

    **为什么固定前缀放最前面**:Prompt Caching是前缀缓存,只有Prompt开头的不变部分才能命中缓存。System Prompt+工具Schema放在最前面确保每轮前缀一致,100%命中缓存,节省50%+输入成本。合并固定层为一个system消息增加前缀连续性,更容易达到OpenAI缓存最低5个消息触发阈值。

    关键词:静态框架、动态组装、四层叠放、缓存命中率、前缀缓存。

    关联章节:5.11a | 面试题:Q14

开放题

多轮对话Agent的状态爆炸怎么解决?请描述四层泄压体系的完整框架,以及每层递进的关系。

    **参考思路:**

    **问题定义**:多轮对话中状态从线性增长变为树状展开(工具嵌套)和倍数增长(推理分叉),导致上下文溢出、注意力稀释、成本爆炸和行为漂移。

    **四层泄压体系**:

    **Layer 1 预防**——不让压力累积:Token Budget全局预算控制、JIT召回原则、输出长度约束、工具结果精简。

    **Layer 2 压缩**——在信息完整前提下缩减体积:语义保护型压缩(锁定System Prompt和行动-结果对)、三层压缩触发(主动80%→后台60%→反应式413)、增量压缩。

    **Layer 3 淘汰**——扔掉不再需要的:记忆治理策略(时效+容量+质量三维度淘汰)、召回数量硬限制≤5条、工具结果裁剪、淘汰前归档。

    **Layer 4 兜底**——当所有策略都不够时:紧急压缩(413触发最激进压缩)、降级回答、自动会话切分、人工介入。

    **递进关系**:预防失败→压缩兜住,压缩不够→淘汰兜住,淘汰也不够→紧急兜底。不是某一层单独起作用,而是四层递进式泄压。

    关键词:状态爆炸、四层泄压、预算控制、语义保护型压缩、记忆治理、紧急兜底。

    关联章节:5.11b | 面试题:Q15	
第6章 Agent的记忆系统
https://clxhxhhr.top/posts/709/
作者
clxstart
发布于
2025-10-02
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。
☰ 文章目录
目录