📖 本章学习目标
- ✅ 理解 LLM 的"失忆"问题及其对应用开发的影响
- ✅ 使用
thread_id自动管理多轮对话历史- ✅ 选择合适的状态持久化方案(MemorySaver vs PostgresSaver)
- ✅ 扩展 Agent 状态,存储自定义业务数据
- ✅ 实现基于向量检索的长期记忆系统
- ✅ 使用摘要记忆控制 Token 消耗
- ✅ 避免常见的记忆管理陷阱
一、LLM 的"失忆"问题
大语言模型是无状态的。每次 API 调用都是独立的------模型不记得上一次你说了什么,也不知道用户的历史偏好。
1、为什么 LLM 是无状态的?
这个特性在技术上是合理的:
- ✅ 便于横向扩展:每个请求可以路由到不同的服务器
- ✅ 简化架构:不需要维护会话状态
- ✅ 降低成本:无需为每个用户保持连接
但给应用开发带来了挑战。
2、没有记忆管理的后果
如果没有记忆管理,Agent 会:
| 问题 | 具体表现 | 用户体验 |
|---|---|---|
| 无法维持多轮对话 | 下一轮不知道上一轮说了什么 | "我刚才不是告诉过你了吗?" |
| 无法记住用户偏好 | 每次都要重新告诉它你喜欢什么格式 | 重复说明,效率低下 |
| 无法复现任务结果 | 历史工作结果丢失 | 需要重新执行相同操作 |
这就有点像你跟一个有严重健忘症的人聊天:
- 你说:"我叫张三"
- 他回答:"好的,张三"
- 下一秒你再问:"我叫什么名字?"
- 他回答:"我不知道"
这就是没有记忆管理的 Agent 的表现。
3、LangChain.js 的记忆方案
LangChain.js 提供了完整的记忆管理方案,分为两个层次:
Messages History"] S["自定义状态
Custom State"] end subgraph Long["长期记忆(跨会话)"] DB["外部存储
Database / Vector Store"] Summary["记忆摘要
Memory Summary"] end Agent --> Short Agent <-->|读取/写入| Long style Short fill:#e8f4fd,stroke:#1890ff,stroke-width:3px style Long fill:#f6ffed,stroke:#52c41a,stroke-width:3px
记忆类型对比:
| 类型 | 作用范围 | 存储位置 | 典型用途 |
|---|---|---|---|
| 短期记忆 | 当前会话内 | 内存或数据库 | 维持多轮对话连贯性 |
| 长期记忆 | 跨会话、跨设备 | 向量数据库 | 记住用户偏好、历史任务 |
二、短期记忆:对话历史的管理
短期记忆解决的核心问题是:如何让 Agent 在多轮对话中保持上下文连贯。
1、手动管理消息历史(基础方式)
最直观的方式是手动拼接消息历史。
示例代码
typescript
import "dotenv/config";
import { createAgent } from "langchain";
const agent = createAgent({
model: "openai:gpt-4o",
tools: []
});
// 第一轮对话
const result1 = await agent.invoke({
messages: [{ role: "user", content: "我叫张三,今年 28 岁。" }],
});
// 第二轮对话:手动带上第一轮的消息
const result2 = await agent.invoke({
messages: [
// 第一轮的用户消息
{ role: "user", content: "我叫张三,今年 28 岁。" },
// 第一轮的 AI 回复
result1.messages.at(-1)!,
// 第二轮的新问题
{ role: "user", content: "我叫什么名字,多大了?" },
],
});
console.log(result2.messages.at(-1)?.content);
// 输出:你叫张三,今年 28 岁。
代码解读:
- 第 10-12 行:第一轮对话,告知个人信息
- 第 16-25 行:第二轮对话,手动拼接历史消息
- 包含第一轮的用户消息
- 包含第一轮的 AI 回复
- 添加第二轮的新问题
- 第 28 行:获取最终回答
问题所在:
- ❌ 手动维护很繁琐
- ❌ 容易遗漏历史消息
- ❌ 代码可读性差
2、使用 thread_id 自动管理(推荐)
LangChain.js 的 Agent 支持通过 thread_id(会话 ID)自动持久化和恢复对话历史,无需手动维护消息列表。
第一步:配置 Checkpointer
typescript
import "dotenv/config";
import { createAgent } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
// 创建内存中的状态存储
// 适合开发测试,生产环境用数据库存储
const checkpointer = new MemorySaver();
const agent = createAgent({
model: "openai:gpt-4o",
tools: [],
checkpointer, // 传入状态持久化器
});
什么是 Checkpointer?
- Checkpointer 负责保存和恢复 Agent 的状态
MemorySaver是最简单的实现,把状态存在内存中- 还有其他实现:
PostgresSaver、RedisSaver等
第二步:使用 thread_id
typescript
// 配置 thread_id,同一 thread_id 下的对话会自动保存和恢复
const config = {
configurable: {
thread_id: "user-zhangsan-session-001"
}
};
// 第一次对话
await agent.invoke(
{ messages: [{ role: "user", content: "我叫张三,喜欢 TypeScript。" }] },
config // 传入 thread_id
);
// 第二次对话(历史自动从 checkpointer 恢复)
const result = await agent.invoke(
{ messages: [{ role: "user", content: "我叫什么名字,喜欢什么编程语言?" }] },
config // 同一个 thread_id
);
console.log(result.messages.at(-1)?.content);
// 输出:你叫张三,你说你喜欢 TypeScript。
执行流程:
优势:
- ✅ 自动管理历史,无需手动拼接
- ✅ 代码简洁,可读性好
- ✅ 支持多会话隔离
💡 thread_id 的设计建议
thread_id是区分不同会话的唯一标识符。在实际应用中,可以用userId + sessionId的组合:
typescript// 最佳实践:用户ID + 会话ID const config = { configurable: { thread_id: `user-${userId}-session-${sessionId}` } }; // 示例: // user-12345-session-abc123 // user-12345-session-def456 (同一用户的不同会话) // user-67890-session-ghi789 (不同用户)好处:
- 既能区分不同用户
- 也能在同一用户的不同会话间切换
- 便于调试和问题追踪
3、生产环境的状态持久化
MemorySaver 把状态保存在内存里,进程重启后数据就丢了,只适合开发测试。
方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| MemorySaver | 简单快速,零配置 | 进程重启数据丢失 | 开发测试 |
| PostgresSaver | 持久化,支持复杂查询 | 需要 PostgreSQL 数据库 | 生产环境首选 |
| RedisSaver | 高性能,支持过期策略 | 需要 Redis 服务 | 高并发场景 |
| SQLiteSaver | 轻量级,单文件 | 不支持并发写入 | 小型应用 |
使用 PostgreSQL 持久化
第一步:安装依赖
bash
pnpm add @langchain/langgraph-checkpoint-postgres pg
第二步:配置数据库连接
typescript
import { PostgresSaver } from "@langchain/langgraph-checkpoint-postgres";
import pg from "pg";
// 创建数据库连接池
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
});
// 创建 PostgresSaver 实例
const checkpointer = PostgresSaver.fromConnString(process.env.DATABASE_URL!);
// 初始化数据库表(只需运行一次)
await checkpointer.setup();
// 创建 Agent
const agent = createAgent({
model: "openai:gpt-4o",
tools: [],
checkpointer, // 使用 PostgreSQL 持久化
});
第三步:使用(与 MemorySaver 完全一样)
typescript
const config = {
configurable: {
thread_id: "user-12345-session-001"
}
};
// 对话历史会自动保存到 PostgreSQL
await agent.invoke(
{ messages: [{ role: "user", content: "你好!" }] },
config
);
数据存储位置:
PostgreSQL 中会创建以下表:
checkpoints:存储检查点数据checkpoint_blobs:存储大的状态数据checkpoint_writes:存储待写入的操作
⚠️ 重要提示:
- 生产环境必须使用持久化存储
- 定期备份数据库,防止数据丢失
- 监控数据库性能,必要时添加索引
三、自定义状态:扩展 Agent 的上下文
除了对话历史,很多时候我们需要在 Agent 的状态里保存业务数据。
1、默认状态结构
LangChain.js Agent 的默认状态只有 messages 字段:
typescript
// Agent 的默认状态结构
{
messages: BaseMessage[] // 对话历史
}
局限性:
- 只能存储消息
- 无法存储用户偏好、任务状态等业务数据
2、为什么要自定义状态?
实际场景举例:
假设你在构建一个智能客服 Agent,需要:
- 记录用户的会员等级(影响优惠策略)
- 保存当前的订单号(用于查询订单状态)
- 标记是否已经验证过用户身份
这些信息不适合放在对话历史中,应该作为独立的状态字段。
3、定义自定义状态 Schema
通过 LangGraph 的 Annotation 可以定义包含更多字段的自定义状态。
完整示例
typescript
import {
Annotation,
MessagesAnnotation
} from "@langchain/langgraph";
// 定义自定义状态
const AgentState = Annotation.Root({
// 继承默认的 messages 字段(带消息合并逻辑)
...MessagesAnnotation.spec,
// 添加自定义字段:用户偏好
userPreferences: Annotation<{
language: string;
responseStyle: "concise" | "detailed";
timezone: string;
}>({
reducer: (_, next) => next, // 简单覆盖更新
default: () => ({
language: "zh-CN",
responseStyle: "concise",
timezone: "Asia/Shanghai",
}),
}),
// 添加自定义字段:任务上下文
taskContext: Annotation<string | null>({
reducer: (_, next) => next,
default: () => null,
}),
});
代码分步解读:
-
第 9 行:继承默认状态
MessagesAnnotation.spec包含messages字段- 自带消息合并逻辑(追加新消息)
-
第 11-23 行:定义用户偏好
- 类型:包含语言、回复风格、时区
reducer:定义如何更新(这里直接覆盖)default:提供默认值
-
第 26-30 行:定义任务上下文
- 类型:字符串或 null
- 用于存储当前任务的临时信息
🔍 Annotation 和 Reducer 是什么?
LangGraph 的状态是不可变的,每次更新都产生一个新状态。
reducer定义了如何把旧值和新值合并:常见 Reducer 类型:
typescript// 1. 直接覆盖(适合普通字段) reducer: (_, next) => next // 2. 数组追加(适合消息列表) reducer: (current, next) => [...current, ...next] // 3. 对象合并(适合配置项) reducer: (current, next) => ({ ...current, ...next }) // 4. 自定义逻辑 reducer: (current, next) => { // 你的合并逻辑 return merged; }这种设计保证了状态变更的可追踪性,是 LangGraph 能够支持时间旅行调试和断点续传的基础。
4、在 Agent 中使用自定义状态
typescript
import { StateGraph } from "@langchain/langgraph";
// 创建工作流
const workflow = new StateGraph(AgentState);
// 添加节点
workflow.addNode("agent", async (state) => {
// 读取自定义状态
const preferences = state.userPreferences;
const taskContext = state.taskContext;
console.log(`用户语言:${preferences.language}`);
console.log(`回复风格:${preferences.responseStyle}`);
// ...Agent 逻辑
return {
messages: [...], // 更新消息
taskContext: "新任务", // 更新自定义字段
};
});
// 编译工作流
const app = workflow.compile();
关键点:
- 可以在节点中读写自定义状态字段
- 返回的对象会合并到当前状态
- 不同类型的字段使用不同的 reducer
四、长期记忆:跨会话的信息持久化
短期记忆只在当前进程有效。当你需要跨会话甚至跨设备保留用户信息时,需要长期记忆。
1、短期记忆 vs 长期记忆
| 维度 | 短期记忆 | 长期记忆 |
|---|---|---|
| 作用范围 | 当前会话内 | 跨会话、跨设备 |
| 存储内容 | 完整对话历史 | 精选的重要信息 |
| 存储位置 | 内存或关系数据库 | 向量数据库 |
| 检索方式 | 按 thread_id 精确匹配 | 语义相似度检索 |
| 典型用途 | 维持对话连贯性 | 记住用户偏好、历史经验 |
2、基于向量检索的长期记忆
长期记忆的挑战在于:当记忆量很大时,不可能把所有历史都塞进上下文(Token 限制)。
解决方案: 把重要信息以向量形式存储,在需要时通过语义相似度检索最相关的记忆。
让我们以构建一个能记住用户信息的个人助手为例。
第一步:初始化向量存储
typescript
import { OpenAIEmbeddings } from "@langchain/openai";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { Document } from "@langchain/core/documents";
// 初始化向量存储(作为长期记忆库)
const embeddings = new OpenAIEmbeddings();
const memoryStore = new MemoryVectorStore(embeddings);
说明:
OpenAIEmbeddings:将文本转换为向量MemoryVectorStore:内存中的向量存储(生产环境用 Pinecone、Chroma 等)
第二步:定义"保存记忆"工具
typescript
import { tool } from "@langchain/core/tools";
import { z } from "zod";
const saveMemory = tool(
async ({ content, category }) => {
// 将信息保存到向量存储
await memoryStore.addDocuments([
new Document({
pageContent: content,
metadata: {
category, // 记忆类别
timestamp: new Date().toISOString(),
},
}),
]);
return "记忆已保存";
},
{
name: "save_memory",
description: "保存重要信息到长期记忆,用于将来的对话中使用。适合保存用户偏好、重要事实、任务进度等。",
schema: z.object({
content: z.string().describe("要保存的信息内容"),
category: z.enum(["preference", "fact", "task"]).describe("记忆类别:preference=偏好,fact=事实,task=任务"),
}),
}
);
代码解读:
- 第 4-14 行:执行函数
- 创建 Document 对象
- 包含内容和元数据(类别、时间戳)
- 添加到向量存储
- 第 17-24 行:元数据
- 工具名称和描述
- 参数 Schema(内容和类别)
第三步:定义"检索记忆"工具
typescript
const recallMemory = tool(
async ({ query, k = 3 }) => {
// 语义搜索相关记忆
const results = await memoryStore.similaritySearch(query, k);
if (results.length === 0) {
return "没有找到相关记忆";
}
// 格式化返回结果
return results.map((doc, i) =>
`记忆 ${i + 1}:${doc.pageContent}(时间:${doc.metadata.timestamp})`
).join("\n");
},
{
name: "recall_memory",
description: "从长期记忆中检索与当前问题相关的信息。在回答问题前,先调用此工具查看是否有相关历史记忆。",
schema: z.object({
query: z.string().describe("检索关键词或问题描述"),
k: z.number().optional().describe("返回最多几条记忆,默认 3"),
}),
}
);
工作原理:
- 将查询文本转换为向量
- 计算与存储向量的余弦相似度
- 返回最相似的 K 条记忆
第四步:创建 Agent
typescript
import { createAgent } from "langchain";
const agent = createAgent({
model: "openai:gpt-4o",
tools: [saveMemory, recallMemory],
systemPrompt: `你是一个有长期记忆的助手。
行为准则:
1. 当用户告诉你重要信息(姓名、偏好、任务等)时,主动调用 save_memory 保存
2. 当回答问题时,先调用 recall_memory 检索相关的历史记忆,再给出回答
3. 如果检索到相关记忆,在回答中引用这些信息
记忆类别:
- preference:用户偏好(如喜欢的食物、颜色、编程语言的偏好等)
- fact:客观事实(如用户的生日、职业、所在城市等)
- task:任务相关信息(如正在进行的项目、待办事项等)`,
});
第五步:测试
typescript
// 第一轮:保存信息
const result1 = await agent.invoke({
messages: [{ role: "user", content: "我叫张三,是一名前端工程师,喜欢 React 和 TypeScript。" }]
});
// Agent 应该自动调用 save_memory 保存这些信息
// 第二轮:询问之前保存的信息
const result2 = await agent.invoke({
messages: [{ role: "user", content: "我是做什么工作的?我喜欢什么技术?" }]
});
console.log(result2.messages.at(-1)?.content);
// 输出:你是一名前端工程师,喜欢 React 和 TypeScript。
执行流程:
- 第一轮:Agent 识别出这是重要信息,调用
save_memory保存 - 第二轮:Agent 先调用
recall_memory检索相关记忆 - 基于检索到的记忆回答问题
3、对话摘要记忆
当对话历史很长时,全量传入会超出 Context Window。摘要记忆是一种策略:当历史消息超过一定数量,对早期消息做摘要,只保留摘要和最近 N 条消息。
使用 ConversationSummaryBufferMemory
typescript
import { ChatOpenAI } from "@langchain/openai";
import { ConversationSummaryBufferMemory } from "langchain/memory";
// 创建摘要记忆
const memory = new ConversationSummaryBufferMemory({
llm: new ChatOpenAI({
model: "gpt-4o-mini" // 用小模型做摘要(省钱)
}),
maxTokenLimit: 2000, // 超过 2000 Token 就开始摘要
returnMessages: true,
});
// 加载历史(包含摘要 + 最近消息)
const { history } = await memory.loadMemoryVariables({});
console.log(history);
// 输出:[
// SystemMessage { content: "之前的对话摘要:用户介绍了自己的基本信息..." },
// HumanMessage { content: "最近的一条消息" },
// AIMessage { content: "最近的回复" }
// ]
工作原理:
10000 tokens"] --> B["超过阈值?"] B -- 是 --> C["对早期消息生成摘要
500 tokens"] B -- 否 --> D["保留全部历史"] C --> E["摘要 + 最近 N 条消息
2000 tokens"] D --> F["发送给模型"] E --> F
优势:
- ✅ 控制 Token 消耗
- ✅ 保留关键信息
- ✅ 平衡成本和效果
成本对比:
| 方案 | Token 消耗 | 成本 | 信息完整性 |
|---|---|---|---|
| 保留全部历史 | 10000 tokens | $0.10 | 100% |
| 摘要 + 最近消息 | 2000 tokens | $0.02 | 80% |
| 仅最近 5 条 | 500 tokens | $0.005 | 40% |
五、常见记忆模式总结
根据不同的使用场景,选择合适的记忆方案。
1. 决策矩阵
| 场景 | 推荐方案 | 说明 | 复杂度 |
|---|---|---|---|
| 单轮或短对话 | 无需专门管理 | 直接传入 messages | ⭐ |
| 多轮对话(进程内) | MemorySaver + thread_id |
简单快速,开发首选 | ⭐⭐ |
| 多轮对话(持久化) | PostgresSaver / RedisSaver |
生产环境必选 | ⭐⭐⭐ |
| 跨会话用户记忆 | 向量存储 + recall_memory 工具 |
语义检索,扩展性好 | ⭐⭐⭐⭐ |
| 超长历史压缩 | ConversationSummaryBufferMemory |
平衡 Token 消耗和上下文保留 | ⭐⭐⭐ |
| 结构化业务数据 | 自定义 State + LangGraph | 需要在状态中存非消息数据 | ⭐⭐⭐⭐ |
2. 组合使用示例
在实际项目中,通常会组合使用多种方案:
typescript
// 短期记忆:PostgreSQL 持久化
const checkpointer = PostgresSaver.fromConnString(DATABASE_URL);
// 长期记忆:向量存储
const memoryStore = new PineconeStore(embeddings);
// 自定义状态:存储业务数据
const AgentState = Annotation.Root({
...MessagesAnnotation.spec,
userId: Annotation<string>(),
orderId: Annotation<string | null>(),
});
// Agent 配置
const agent = createAgent({
model: "openai:gpt-4o",
tools: [saveMemory, recallMemory],
checkpointer,
});
六、常见踩坑指南
记忆管理看似简单,但有很多隐藏的陷阱。
⚠️ 踩坑 1:Context Window 溢出
问题: 不加限制地累积对话历史,最终会超出模型的 Context Window。
症状:
bash
Error: This model's maximum context length is 128000 tokens.
However, your messages resulted in 150000 tokens.
解决方案:
typescript
// 方案 1:限制消息数量
const MAX_MESSAGES = 20;
if (messages.length > MAX_MESSAGES) {
messages = messages.slice(-MAX_MESSAGES); // 只保留最近 20 条
}
// 方案 2:使用摘要记忆
const memory = new ConversationSummaryBufferMemory({
maxTokenLimit: 2000, // 设置合理的上限
});
// 方案 3:定期清理不重要的历史
function cleanupHistory(messages: BaseMessage[]) {
return messages.filter(msg => {
// 保留系统消息和用户的重要消息
return msg.role === "system" || isImportantMessage(msg);
});
}
最佳实践:
- 设置合理的 Token 上限(通常为 Context Window 的 70-80%)
- 优先保留最近的消息和系统消息
- 对长期运行的 Agent,定期清理历史
⚠️ 踩坑 2:thread_id 冲突导致数据泄露
问题: 如果多个用户共用同一个 thread_id,他们的对话历史会混在一起。
❌ 危险示例:
typescript
// 所有用户共用一个 thread_id
const config = {
configurable: { thread_id: "global-session" }
};
// 用户 A 说:"我的密码是 123456"
// 用户 B 问:"上一个用户说了什么?"
// 结果:用户 B 能看到用户 A 的密码!
✅ 安全做法:
typescript
// 每个用户独立的 thread_id
const config = {
configurable: {
thread_id: `user-${req.userId}-session-${Date.now()}`
}
};
// 或者更简洁
const config = {
configurable: { thread_id: req.userId }
};
安全检查清单:
- ✅ thread_id 必须包含用户标识符
- ✅ 不同用户的 thread_id 绝对不能重复
- ✅ 在生产环境中进行权限校验
- ✅ 定期审计访问日志
⚠️ 踩坑 3:记忆粒度不当
问题: 长期记忆的粒度影响检索质量。
太精细的问题:
typescript
// 每条消息都保存
saveMemory({ content: "你好", category: "fact" });
saveMemory({ content: "今天天气不错", category: "fact" });
saveMemory({ content: "我吃了午饭", category: "fact" });
后果: 大量无关记忆混入,检索噪音多
太宽泛的问题:
typescript
// 一整天的对话保存为一条
saveMemory({
content: "今天聊了很多话题,包括工作、生活、兴趣爱好...",
category: "fact"
});
后果: 关键信息被稀释,检索不准确
✅ 推荐做法:
typescript
// 按信息类别分开存储
// 1. 用户偏好(相对稳定)
saveMemory({
content: "用户喜欢 TypeScript 和 React",
category: "preference"
});
// 2. 重要事实(偶尔变化)
saveMemory({
content: "用户是一名前端工程师,在北京工作",
category: "fact"
});
// 3. 当前任务(频繁变化)
saveMemory({
content: "正在开发一个电商网站的项目",
category: "task"
});
// 检索时带类别过滤
recallMemory({
query: "用户的职业是什么?",
category: "fact" // 只检索事实类记忆
});
⚠️ 踩坑 4:忘记清理过期记忆
问题: 长期记忆不断累积,占用存储空间,降低检索效率。
解决方案:
typescript
// 定期清理过期记忆
async function cleanupOldMemories(daysToKeep = 90) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
// 删除 90 天前的记忆
await memoryStore.delete({
filter: {
timestamp: { $lt: cutoffDate.toISOString() }
}
});
console.log("已清理过期记忆");
}
// 每天凌晨执行
setInterval(cleanupOldMemories, 24 * 60 * 60 * 1000);
七、本章小结
记忆系统让 Agent 从"一次性问答"升级为"有上下文感知的持续对话"。
📝 核心知识点回顾
| 知识点 | 关键要点 | 适用场景 |
|---|---|---|
| 短期记忆 | MemorySaver + thread_id 自动管理 |
多轮对话 |
| 持久化存储 | PostgresSaver / RedisSaver |
生产环境 |
| 自定义状态 | LangGraph Annotation 扩展字段 | 存储业务数据 |
| 长期记忆 | 向量存储 + 语义检索 | 跨会话记忆 |
| 摘要记忆 | 压缩早期历史,控制 Token | 超长对话 |
🎯 动手练习
尝试完成以下练习,巩固所学知识:
练习 1:实现多会话管理 创建一个支持多用户的聊天应用:
- 每个用户有独立的
thread_id - 使用
MemorySaver管理短期记忆 - 测试用户 A 和用户 B 的对话不会互相干扰
练习 2:持久化改造 将练习 1 的 MemorySaver 替换为 PostgresSaver:
- 安装 PostgreSQL
- 配置数据库连接
- 验证进程重启后对话历史仍然保留
练习 3:长期记忆系统 构建一个能记住用户偏好的助手:
- 实现
save_memory和recall_memory工具 - 测试保存用户信息(姓名、喜好、职业等)
- 测试在新会话中检索这些信息
练习 4:摘要记忆优化 模拟一个长对话场景(50+ 轮对话):
- 不使用摘要:观察 Token 消耗
- 使用
ConversationSummaryBufferMemory:对比效果 - 调整
maxTokenLimit,找到最佳的平衡点