1. 先聊一个扎心场景:Agent 又“失忆”了
做 Agent 开发的朋友大概率都撞过这堵墙:对话轮次一多,模型突然开始胡说八道,或者干脆报错,提示说超出了 token 上限,回答被截断。更难受的是,明明用户十几轮前提过的关键需求,Agent 转头就忘,像个金鱼一样。这个“记忆只存在一小会儿”的问题,是所有做 AI Agent 项目的人绕不过去的坎,也是今天我重点要聊的内容。
刚上手做 Agent 的时候,我也天真的以为,把用户的输入和模型的输出原封不动地往上下文里塞,对话越长,Agent 越“懂”用户。结果测了一周就发现,这条路根本走不通。上下文窗口是个物理限制,超了就截断,截断要么丢头、要么丢尾、要么丢中间,丢哪块都在丢用户的核心诉求。后来我在项目里逐渐摸索出一套相对可行的记忆管理方案:用“总结压缩”处理短时记忆,用“向量检索”加 Milvus 做长期记忆。这套组合拳不能说包治百病,但至少让我手里的几个 Agent 项目从“对话超过二十轮就崩”进步到了“能稳定完成跨天、跨会话的任务”。
这篇东西不是教科书式的原理科普,就是我自己在项目里摸爬滚打总结出来的实操笔记。你会看到为什么截断策略救不了 Agent,总结压缩应该怎么写、什么时候触发,Milvus 怎么装、怎么建集合、怎么做向量检索,以及最常见的几个坑在哪里、怎么绕过去。适合正在做 Agent 开发,被上下文长度和记忆问题折磨得头疼的同行,也适合刚入门想给知识库或记忆模块选型的朋友。
2. 截断为什么救不了 Agent,记忆为什么要分层
2.1 上下文窗口是硬约束,不是软建议
理解 Agent 记忆问题,先得清楚它的工作原理。在典型的 ReAct 模式里,一个大语言模型每做一步推理,都需要把系统提示词、历史对话、工具返回结果、当前用户输入拼在一起,作为输入送给模型。这些内容全部占用上下文窗口的 token 额度。GPT-4 时代的 8K、16K 窗口,放到真实 Agent 场景里其实非常局促:一次工具调用的 JSON 返回可能就吃掉几千 token,三四轮对话下来窗口就见底了。
我见过很多项目的初版代码就是简单粗暴的“只保留最近 N 轮对话”,或者用一个全局计数器,超过某个阈值就把历史记录断掉。这种做法有它的存在理由:实现简单,几行代码就能做完,而且在小 demo 里跑起来没问题。但一旦进入真实业务,问题就会暴露得很彻底。
2.2 常见截断策略的四个致命伤
第一,丢失关键信息,用户早就说过自己的约束条件、偏好或目标,截断之后 Agent 全然不知,后面所有回答都可能在错误方向上狂奔。第二,破坏逻辑连贯性,很多任务是分步完成的,前一步的结果是后一步的前提,截断会把这条推理链直接砍断。第三,浪费可用空间,截断通常是按“轮”切的,但每一轮的 token 消耗差异巨大,按轮切往往留下一大堆用不上的日志,却把真正重要的指令挤掉了。第四,没有长久记忆,就算这次对话没出问题,新开一个会话,Agent 立刻变回陌生人,用户需要重复交代所有背景。
我自己踩过最惨的一次坑,是在做一个客服工单处理 Agent。用户在第一轮就说明了会员等级和售后诉求,后面几轮一直在补充订单信息。等到第五六轮,窗口满了,我当时的处理方式是砍掉最早几轮。结果 Agent 突然开始用普通用户的权益来处理问题,给出的赔偿方案完全不符合规则。那时候我才意识到,信息不是等价的,在对话里,越早出现的约束往往越重要,无脑截断就是在自断一臂。
2.3 记忆分层的正确姿势:工作记忆、短期记忆、长期记忆
后来我去参考了不少成熟的 Agent 框架设计,发现好的记忆方案基本都遵循分层理念。工作记忆,也就是当前上下文窗口里的内容,直接参与本轮推理,速度最快但容量最小。短期记忆经过总结压缩后控制在几百到一两千 token 以内,保留当前任务的核心背景和最近几轮的关键进展。长期记忆则用向量数据库存储历史要点,按需检索相关片段回填进上下文,容量几乎无限但需要主动触发查询。
这三层各有分工,一套完整方案应该把它们串成流水线:最新对话进入工作记忆,在工作记忆逼近上限时,触发总结压缩生成新的短期记忆摘要,同时把值得长期保留的信息写入向量库。这样既保证短期任务不断片,又让历史经验在未来被重新调用。下面两部分,我分别讲清楚总结压缩和 Milvus 向量检索具体怎么做。
3. 总结压缩:用“翻译”替代“搬运”
3.1 总结压缩到底在压什么
很多开发者第一次听到“总结压缩”时,会以为只是把对话历史丢给模型,说一句“请总结一下”。这么做有效果,但非常粗糙。真正的总结压缩,目标不是在原文基础上写一段摘要,而是把“对后续推理有用的信息”提取出来,丢掉“对后续推理没用的信息”,用尽量少的 token 保住尽量多的推理要素。
这个区别非常重要。举个例子,用户和 Agent 讨论了三个候选方案,最后确定选 B,同时补充了一条要求:预算要控制在五千以内。有用的信息是“最终选择 B、预算上限五千”,没用的信息是讨论过程中 A、C 方案各自的细节点以及用户和 Agent 来回确认的口水话。粗粒度摘要可能把 A、C 的优点也抄进去,浪费 token;更糟的情况是,摘要里漏了预算约束,后续推荐方案直接跑偏。
3.2 触发时机:别等窗口满才开始抢救
我踩过的教训是,不要等上下文窗口快满了才做压缩。模型在生成答案时本来就需要足够的上下文空间,如果可用 token 只剩几百甚至几十,即使摘要生成成功,也没有空间把它放回去,于是只能继续截断,形成恶性循环。
比较稳妥的做法是设定一个水位线。以总窗口为基准,当历史对话 token 数达到总容量的某个比例时,就主动触发一轮压缩。我的习惯是设为 50% 到 60%。比如窗口是 8000 token,当历史累积到 4500 左右就开始做总结,把摘要压到 800 token 以内。这个触发比例要结合任务的复杂度和工具返回的大小来调:如果 Agent 频繁调用工具,工具返回动不动几千 token,触发点就得更早,否则一次工具调用就直接把窗口挤爆了。
实现上可以用 tiktoken 做 token 计数,每轮对话结束后统计当前的累积值,超过阈值就调用总结逻辑。可以粗略估算,中等复杂度的对话任务,把历史压到原来的 15% 到 25% 是比较健康的比例,太少会丢信息,太多省不出空间。
3.3 增量式 vs 滚动式摘要,怎么选
做总结压缩时有两种路线。滚动式摘要,每次都对全部历史做总结,信息完整但 token 消耗呈二次增长,窗口越大开销越离谱,不推荐。增量式摘要,每次只把新增对话合并到已有的 summary 里,让模型在旧摘要基础上更新生成新摘要。这样做每次只消耗新增内容加旧摘要的 token,开销可控,也是我实际采用的方式。
增量式摘要的代码骨架大概是这样,用 Python 写的话:
import openai def summarize_incremental(summary: str, new_messages: list[dict], prompt: str) -> str: content = f""" 已有摘要: {summary if summary else "(无)"} 新增对话: {format_messages(new_messages)} 请结合已有摘要和新增对话,更新生成一份新的对话摘要。 需要保留:用户的明确要求、已确定的决策、任务当前进展、需要后续跟进的事项。 不要保留:寒暄客套、重复讨论、与任务无关的闲聊。 """ resp = openai.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": prompt}, {"role": "user", "content": content}, ], temperature=0, max_tokens=800, ) return resp.choices[0].message.content给 summary 更新单独设计一条 prompt 时,我会在 system 里明确强调:这次生成的不是聊天记录,而是面向任务执行的上下文摘要。同时规定摘要应包含哪些字段,比如用户目标、已确认约束、当前备份。通过这个结构化的输出约束,可以让摘要内容更加可控,后续 Agent 也能更好地从里面提取信息。
3.4 压缩链路的隐藏风险,别让摘要“二传手”失真
总结压缩一定会丢失信息,这是它作为有损压缩的本质,关键在于丢得聪明还是丢得愚蠢。最容易出现的问题有三个。
一是把约束条件弄丢了。用户说“不要用第三方的包”“要兼容低版本浏览器”,这类否定式约束在摘要里很容易被忽略。我的解决方案是在 prompt 里单独加一句,要求摘要必须把所有否定式要求原样保留。二是把数值和名称改坏了。对话里出现过订单号、商品名、日期,摘要一概括就变成了“这个商品”“那天”,后续检索回来根本对不上。所以 prompt 里要明确要求保留专有名词和数值完整准确。三是多轮压缩之后信息漂移。不断拿旧摘要加新对话,第一次漏掉的信息,后面再也补不回来。所以定期,比如每积累十轮对话,做一次基于原始精选片段的校准回看,还是有必要的。
如果你在做的 Agent 对事实精度要求极高,比如财务或法律场景,我建议在压缩之外再补一些日志存储,让摘要承担“快速定位”职能,关键的原始记录可以落到本地或数据库,需要交叉验证时再捞出来用。
4. 向量检索与 Milvus:长期记忆的基建
4.1 为什么不用 Redis 硬扛,而要上 Milvus
短期记忆靠摘要,长期记忆就得换一套引擎。最早我做 RAG 相关项目时,图省事直接用 Redis 把文本和 embedding 数组塞进去,再在代码里算余弦相似度。项目小的时候问题不大,因为数据量撑死几千条,暴力遍历完全算得过来。但 Agent 的记忆是会线性增长的:每个用户每天产生几十条记忆,三个月后就是几千上万条,再加上向量维度动辄 768 甚至 1536,纯 Python 遍历的耗时就会从毫秒级升到秒级,直接卡在 Agent 推理链路上。
Milvus 这种专门的向量数据库解决的核心问题,就是在大数据量下做高效相似度检索。它用索引结构替代暴力遍历,再加上标量过滤,也就是类似 SQL 的 where 条件,能在“我和谁聊过”这类精确过滤基础上再做向量召回。为什么我最终选了 Milvus 而不是 FAISS 或者 Chroma:FAISS 是个库,索引在内存里,项目重启索引就没了,要做持久化得自己写不少代码;Chroma 轻量适合原型验证,但数据量大以后性能和运维能力都跟不上。Milvus 有完整的服务端、数据持久化、多集合管理和成熟的客户端生态,长期发展也更有保障。
4.2 Docker 部署单机版,一步步说清楚
Milvus 的安装分 Standalone 和 Cluster 两种,开发测试阶段完全不需要上集群,单机模式就够了。最顺手的安装方式是 Docker Compose。社区维护了一套 standalone 的 compose 文件,里面包含三个组件,Milvus 本体负责读写和索引,etcd 负责元数据存储,MinIO 负责对象存储,也就是存放原始数据和索引文件的地方。
实际操作时,先确认环境里有 Docker 和 Docker Compose,然后拉取官方 compos 文件并启动:
wget https://github.com/milvus-io/milvus/releases/download/v2.4.13/milvus-standalone-docker-compose.yml -O docker-compose.yml docker compose up -d启动后可以检查一下容器状态:
docker compose ps看到 milvus、etcd、minio 三个容器都在正常运行,就说明装好了。默认访问地址是localhost:19530。要强调一点,Milvus 的版本迭代很快,不同版本的 API 略有差异,比如 2.3 和 2.4 的 pymilvus 在某些参数名上就有变化,建议锁版本使用,别随手装个 latest 就跑。我在 2.3 升级 2.4 的时候,就踩到过索引参数不兼容的坑,后面排了半天才发现是版本问题。
4.3 集合设计:给记忆留足查询维度
Milvus 里的 collection 类似传统数据库的表,设计集合结构时,除了向量字段本身,标量字段也值得仔细规划。我会给 Agent 记忆建这样的集合结构:
| 字段名 | 类型 | 用途 |
|---|---|---|
| id | INT64 主键 | 自增主键,方便单独管理 |
| user_id | VARCHAR | 用户 ID,做记忆归属过滤 |
| session_id | VARCHAR | 会话 ID,查单次对话的记录 |
| content | VARCHAR | 原始文本内容,方便溯源查看 |
| embedding | FLOAT_VECTOR | 文本向量,用于相似度检索 |
| created_at | INT64 | 时间戳,支持按时间范围过滤 |
这里特别注意 user_id 和 session_id 这两个标量字段。如果没有它们,检索出来的向量可能来自另一个用户的对话,隐私和准确性都出问题。Milvus 提供了标量过滤加向量检索的组合查询能力,这个能力在实际业务里几乎是刚需。提前把这条过滤维度设计好,后面能省下大量该表迁移的麻烦。
写入前,embedding 必须提前算好。文本向量化可以用 OpenAI 的 embedding 接口,也可以用开源模型,比如 BGE、M3E、GTE 这一批。我自己的经验是,做中文场景优先用 bge-large-zh 或 m3e,这两类模型在中文语义上的表现不比 OpenAI 的 text-embedding-3-small 差,而且可以本地部署,不依赖外部服务。但要注意,embedding 模型必须固定一个,不能今天用这个模型生成向量,明天换另一个,因为不同模型输出的向量空间不一致,互相检索出来的相似度没有任何意义。
用 pymilvus 创建集合的代码像这样:
from pymilvus import MilvusClient, DataType client = MilvusClient(uri="http://localhost:19530") client.create_collection( collection_name="agent_memory", dimension=1024, primary_field_name="id", vector_field_name="embedding", id_type="int", metric_type="COSINE", max_length=512, enable_dynamic_field=True, )注意把 dimension 参数配成你的 embedding 模型输出的向量维度。用 openai text-embedding-3-small 是 1536,用 bge-large-zh 是 1024,用 m3e-base 是 768。配错维度,写入时会直接报错或无法建索引。metric_type 选的是相似度度量方式,我用 COSINE 比较多,因为它对文本向量更稳定,对向量模长不敏感。
4.4 写入记忆:要有信息密度,别做成日志
有了集合之后,写入逻辑的关键不是“把每轮对话都存进去”这么简单。真实对话里有大量无效信息,全存进去只会污染长期记忆,检索时总召回一堆无关内容,拉低后续推理质量。
我的做法是,在总结压缩生成摘要的同时,顺手把摘要里的关键信息抽出来,抽取规则可以在同样的 prompt 里一并处理。比如把用户明确表达的偏好、任务决策、事实性的业务要求作为“记忆条目”单独抽取,每条控制在 50 字以内,然后逐条向量化写入 Milvus。
def write_memory(content: str, user_id: str, session_id: str, embedding_model) -> None: vec = embedding_model.encode(content).tolist() client.insert( collection_name="agent_memory", data=[{ "user_id": user_id, "session_id": session_id, "content": content, "embedding": vec, "created_at": int(time.time()), }], )写入的批量控制也值得一提,一次性塞几千条进去偶尔会超时,但分批次比如每 100 条一批,基本都很稳。另外,删除或修改记忆要单独处理。Agent 会话里用户可能中途改主意,五分钟前说“选方案 A”,五分钟后改口“不选 A 了”,如果旧记忆不清除,检索时就会同时召回两条矛盾信息。最简单的做法是,发现同主题冲突记忆时,给旧的做逻辑删除,也就是加一个 invalid 标记,检索时过滤掉。
4.5 检索记忆:过滤、相似度、时机
检索是整套长期记忆方案的门面,召回质量直接决定 Agent 对历史信息的利用水平。基础写法是先用 user_id 过滤当前用户的数据,再算向量相似度,取 TopK 条。
def search_memory(query: str, user_id: str, top_k: int = 5, embedding_model=None) -> list[str]: query_vec = embedding_model.encode(query).tolist() results = client.search( collection_name="agent_memory", data=[query_vec], limit=top_k, filter=f'user_id == "{user_id}"', output_fields=["content", "created_at"], ) return [r["entity"]["content"] for r in results[0]]核心点在于 query 怎么写。不要把用户最新的那句话原封不动拿来做向量查询,向量检索的准确度高度依赖 query 是否清晰、信息是否完整。我会先用模型把“当前任务需要知道的背景”转换成一段检索式描述,再拿它去查库。比如用户说“那个绿色的方案还行”,检索 query 可以是对应完整上下文的概括。
检索时机也不能做成每一轮都查。长期记忆的作用是提供背景信息,不是填满上下文。如果每轮都把 Top5 记忆塞进去,反而会稀释模型对当前任务状态的注意力。我习惯用主动判断策略,把 Agent 流程设计成先判断当前问题是否涉及历史,涉及才触发检索。判断依据可以是:用户提到“上次”“之前”“你记得吗”一类指代性词汇,或者当前任务本身就需要多轮跨会话跟踪。
检索结果拼进上下文时,我会用单独的记忆块标注来源,让模型知道哪些是历史记忆、哪些是当前对话:
以下是该用户的历史记忆,可能和本次任务相关: - 用户要求所有回复控制在 300 字以内(来源时间:2024-06-01) - 用户偏好简洁的方案陈述,不接受长篇分析(来源时间:2024-06-01)4.6 混合检索:让精确条件先圈定范围
纯向量检索的问题在于,它擅长语义相似,但不擅长精确匹配。比如“查一下 2024 年 6 月 1 号之后产生的记忆”,向量检索很难精确理解“之后”这种时间约束。Milvus 支持的混合检索就是在做相似度召回的同时,利用标量字段施加过滤条件,我前面代码里的 user_id 过滤已经是一种混合检索了。实际业务中可以组合多个条件:
filter_str = 'user_id == "1001" and created_at >= 1717171200 and session_id == "abc123"'设计长期记忆的查询策略时,可以把语义相似度当成“大海捞针”的捞法,把标量过滤当成“先圈定一片海域”的围栏,两者结合才能既快又准。通过合理设计标量字段,对避免跨用户记忆混淆、框定时间窗口这类问题非常有帮助。
5. 端到端架构:总结压缩和 Milvus 怎么拧成一股绳
5.1 一条完整的流水线
单独看总结压缩,或者单独看向量检索,都只是孤立的能力,真正的价值在于把它们整合进 Agent 的运行时。我在项目里最终形成了一条相对稳定的流水线,拆解如下。
第一,每轮对话结束后,检查工作记忆的 token 占用量。推荐用 tiktoken 或直接按字符估算。超过 50% 到 60% 的水位线就触发下一步。第二,取当前摘要加最近若干轮新增对话,交给模型做增量式总结,生成新摘要替换旧摘要。第三,同时让模型从新增对话中抽取出值得长期保留的记忆条目,包括用户偏好、明确决策、事实性信息。第四,将记忆条目向量化写入 Milvus,并存入 user_id、session_id、时间戳等标量字段。
第五,下一轮对话开始前,根据当前用户输入生成检索 query,查询 Milvus,召回 Top5 相关记忆。第六,将召回的记忆块和新摘要一起拼入上下文,再让 Agent 执行正常推理。这套流程跑通后,Agent 的可用对话长度就不再被窗口容量限制死了,窗口只管最近任务,旧知识从数据库里按需捞。
5.2 围绕一致性的现场控制
整合方案最容易翻车的地方其实是历史记忆和当前事实打架。用户改主意了,但旧记忆还在库里,Agent 被检索出来的信息带回了错误方向。我会在检索结果拼接之前,先做一次基于当前对话的“覆盖检查”:如果本轮对话里出现了对同一主题的新表述,那么涉及该主题的旧记忆直接丢弃,不移入模型上下文。
还有一种更轻量的做法是在写入记忆条目前做去重,用相似度阈值判断是否已存在含义相近的旧条。比如新记忆和旧记忆向量相似度超过 0.9,就只保留新记录或更新旧记录,而不是无限叠加。Milvus client 的 query 接口可以按条件先把旧记录捞出来,再在 Python 里比对相似度,虽然多一步开销,但对长期记忆的干净度很有帮助。
一致性问题的另一个来源是摘要本身。增量式摘要经过多轮叠代后,旧细节有被覆盖的风险。我的做法是在每个对话会话结束时,把最终的摘要和当时抽取的关键记忆条目做一次对账,如果摘要里明显缺少某个本应保留的信息点,就在下一次压缩时当作修正项喂回去。定期做这种校准,能明显缓解信息漂移。
5.3 还能往哪些方向扩展
这套架构搭完之后,后续可以做的扩展方向其实不少。比如在多用户场景里,基于 user_id 过滤所有记忆访问,这就是记忆权限的最小实现。如果要做跨会话长期任务跟踪,可以把“任务状态”单独设计成一个集合,每次检索时把“任务进展”和“用户偏好”分开召回,效果会比混在一起好。厂商无关的 embedding 替换也不难,只要保证维度和模型固定,模型怎么换是内部实现的事。
对于依赖较多工具的 Agent,建议把“工具使用经验”也做成记忆类型。比如某个工具在特定情况下会返回固定格式错误,这种经验以前只能靠写死逻辑,现在可以写成一条记忆,通过检索被模型自动发现。这相当于让 Agent 从自己的历史里积累“业务经验”,后续面对相似情况时不再从零推理。
6. 常见问题与排查技巧实录
6.1 问题速查表
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 检索到的记忆和当前问题完全不相关 | query 生成太粗糙 | 改用模型先生成检索式描述,再加入用户 ID 等精确信息 |
| 写入 Milvus 时报维度不匹配 | embedding 模型维度配错 | 用describe_collection检查维度,和模型输出对齐 |
| 多轮后摘要丢失关键约束 | 增量式摘要叠代漂移 | 在校准环节定期基于原始关键片段回看,强制保留否定式约束 |
| 同一条用户偏好被重复写入 | 缺少相似度去重 | 写入前用向量相似度检查旧记录,接近阈值则只更新不新增 |
| 容器日志显示 etcd 连接超时 | Milvus 依赖组件未就绪 | 检查 etcd 容器状态,确保三个容器都在运行后再连客户端 |
| 检索变慢但数据量不大 | 没建索引或索引类型不合适 | 数据量超过千条后务必创建向量索引,并定期查看索引状态 |
6.2 关于 Milvus 安装和使用的几个坑
Milvus 安装环节常见的坑,集中在版本不匹配上。docker compose 文件里的镜像版本和客户端库版本如果差太多,可能出现连接后Collection not found或参数报错的情况。我现在的习惯是,compose 文件和 pymilvus 的版本都锁定在同一个大版本里。另一个坑是 macOS 或 Windows 上 Docker 资源限制不够,Milvus 启动后经常 crash,把 Docker Desktop 的内存阈值调到 4GB 以上会好很多。
索引类型的选型也是一个容易含糊的点。Milvus 默认的 FLAT 索引在小数据量下没问题,但数据过万之后,检索延迟上升明显。建议切到 HNSW 或 IVF_FLAT,HNSW 的召回质量和速度综合表现最佳,就是在建索引时占用的内存高一些。刚起步时可以把 index 参数配成HNSW,M 和 efConstruction 用默认值就够了。
6.3 我做记忆模块时踩过的低水平错误
有几个错误我每次回想起来都觉得蠢,但确实是一步一步踩出来的。第一是 embedding 模型中途更换,导致所有新写入的向量和旧向量语义空间不一致,检索结果惨不忍睹,最后只能全库重建。第二是没有给记忆条目设置来源和时间,检索出来都不知道是哪次对话里产生的,排错时完全无从下手。第三是检索 TopK 设置过大,把八条十条历史全塞进上下文,结果模型被无关信息干扰,回答质量反而下降。TopK 取 3 到 5 是相对平衡的区间。
还有一点值得单独提醒:Agent 的记忆不是攒得越多越好。长期记忆要有取舍,只记忆那些“之后还会用到的信息”。每个项目都需要根据自己的业务场景定义哪些信息是值得记的。如果能在 prompt 里把“什么该记、什么不该记”定义清楚,比后面做多少清洗都有效。
7. 结尾想说的几点实际体会
最后再分享几个我在实际项目里沉淀下来的判断。关于总结压缩,不要图省事用整段原文替换,要设计成增量式更新加定期校准的双轨结构,前者省 token,后者保精度。关于 Milvus 部署,不要迷信最新版本,稳定才是第一位的,锁版本、做备份、监控好容器状态比追新功能重要得多。关于整套记忆架构,不要一步到位全上最复杂的方案,先把短期记忆和截断检测做扎实,再用 Milvus 接长期记忆,逐步迭代。
另外,做这类系统时,我会刻意从“评估”的视角来审视效果,而不是只看 demo 跑得通。给记忆模块设计一套简单评价维度:检索准确性、摘要信息保留率、跨会话任务成功率。每次改动后,用同一组会话数据做回归对比,用数据判断方向和效果。
这套方案用下来,我自己的 Agent 项目已经能在会话长度、跨天任务续接和个性化偏好理解上达到稳定可用的状态。如果你正被困在“对话一长就崩、换会话就失忆”的泥潭里,希望这篇记录能帮你找到绕出去的路线。