Agent外挂记忆系统实战:从原理到接入Mem0的完整指南
如果你做过一个稍微上点规模的AI客服Agent大概率遇到过这种崩溃瞬间用户问“我上次那个退款怎么回事”Agent一脸茫然地开始重复要订单号或者用户已经说清楚了“我在上海想找一个周末的线下活动”下一轮对话它又问你“请问您所在城市是哪里”。问题不在模型笨而是Agent压根不记得自己几分钟前说过什么、用户是谁。我开始认真研究给AI Agent加一个“外挂记忆系统”试了一圈方案之后最后把Mem0这类开源记忆层接进了项目里。这篇文章就是我对整个过程的原理拆解、接入实操和踩坑复盘适合正在做Agent、被上下文丢失和长期记忆问题折磨的开发者参考。1. Agent失忆困局为什么塞上下文治标不治本1.1 窗口再大也挡不住“用户画像”级别的长期信息先聊一个基本的认知大语言模型本身是不带状态的。同一个模型你喂它同样一段话它输出的结果大概率是一样的它之所以能“聊天”靠的是你在每次请求时把历史消息重新拼一遍再丢给它。这个机制用一句话说就是上下文窗口里装着什么模型就知道什么没装进去的一律不知道。于是很多人第一反应是把窗口拉长不就行了现在动辄几十万甚至上百万的上下文长度看起来塞什么都不怕。但实际运营下来你会发现窗口越长问题越隐蔽首先是成本每一轮都把完整历史重新计算一遍token消耗会随会话轮数暴涨其次是噪声用户几个月前随口提过一个偏好和今天要处理的任务根本无关你全塞进去反而干扰模型判断最要命的是很多信息根本不该出现在本轮上下文里却必须在未来某个时刻被唤醒——比如用户早期说过自己是学生、预算有限半年后他再回来咨询你需要立刻知道这件事。这种周期跨度过长、存在“休眠期”的信息靠窗口装是装不住的。还有一类信息更特殊它属于“某个用户”而不属于“某段对话”。用户前一次会话里告诉你他住城东这一次会话问的却是完全不同的另一件事但你对他的服务始终应该基于“他住城东”这个事实。这类用户级状态天然就和会话隔离必须有一个独立于对话历史的存储层来承接。1.2 对话记忆和知识库检索是两回事很多团队会把记忆问题和RAG检索增强生成混在一起解决建一个知识库把用户说的话都存进去等要用的时候再用向量检索捞出来。这个思路方向没错但粒度差得很远。RAG解决的是“模型不知道的公共知识”比如产品手册、公司制度、行业文档。它的特点是内容相对稳定、答案相对统一谁问都一样。但用户记忆完全不是这样我今天说喜欢简洁风格明天可能改口说喜欢详细报告我说“我是后端开发”这既是一个事实又是一个画像标签还影响你后续所有回答的语气和深度。知识库检索很难处理这种“同一对象、持续变化、存在新旧冲突”的语义。更本质的区别在于知识库是“用户来问我去查”记忆系统是“用户在表达我在沉淀”。它需要在对的时间提取、在对的时间更新、在对的时间想起这比简单的文档检索多了一套生命周期管理。所以我当时的结论是我们不能用一个通用的“存文本向量检索”的壳子去硬套记忆需求而是需要一个真正围绕记忆场景设计的中间层。1.3 自己写缓存能不能替代也有人说这还不简单Redis存一下消息记录每次拿回来拼进prompt不就行了我曾经也走过这条路。早期我给自己客服Agent写过一个基于消息历史的缓存层每个用户一个list满了就截断最后把最近N条拼进去。效果确实比完全没有强但用了两天就发现三个硬伤第一原始对话里大量信息是冗余甚至无用的。用户说“嗯”“好的”“那行吧”这些全部塞进上下文真正有价值的偏好和事实被淹没。第二原始消息没法做冲突处理。用户昨天说“我喜欢邮件沟通”今天说“别给我发邮件了”列表模式只会把两条都存着模型不知道该信哪条。第三没有记忆粒度。你存储的是一段对话不是一个长期的用户画像跨会话召回时完全没有相关性排序能力。这些痛点叠加在一起让我意识到记忆功能看起来像“存储检索”实际上还需要“提取更新冲突解决相关性重排”这些环节。这正是我在调研后转向Mem0这类专业记忆层的直接原因。2. Mem0的机制拆解一条记忆从产生到复用的全链路2.1 add操作原始对话怎么变成可用的记忆条目第一次看Mem0的用法时我最大的感受是它把复杂逻辑藏在了简洁的接口后面。你要记一件事只需要调用一个add方法把一段对话传进去即可。from mem0 import Memory memory Memory.from_config() # 模拟一段真实对话 messages [ {role: system, content: 你是一个购物助手。}, {role: user, content: 我叫小北预算控制在三千以内主要想买个适合通勤的双肩包。}, {role: assistant, content: 好的我会按三千以内的预算帮你筛选。}, ] # 为指定用户写入记忆 result memory.add(messages, user_idtest_user) print(result)这段代码背后的逻辑链大致是这样Mem0会把这段对话交给一个内置的LLM链路让它从对话中识别出值得长期保存的信息比如“用户名叫小北”、“预算三千以内”、“通勤双肩包”这几个候选条目然后对这些条目做去重、打分、格式化最后写入向量数据库。它不是简单地把原文剪碎存起来而是做了一次“提炼”。我一开始比较担心的是它会不会把一些无关紧要的寒暄也记进去。实际测试下来那个内置打分环节会过滤掉低价值信息不过这个阈值可以在配置里调整。后面第四章我会讲怎么调默认值在某些场景下会显得过于“敏感”。2.2 search操作需要的时候怎么把记忆捞回来记忆存进去是为了未来能用所以读取侧的search接口同样关键。它的输入是你当前的用户问题输出是一批和该用户相关的记忆条目按相关度排序。hits memory.search(这个用户买包子的预算和需求, user_idtest_user) for item in hits: print(item[memory])这段代码的实验现象很直观当我在测试环境里问“用户预算多少”返回结果里能搜出“预算控制在三千以内”当我问“用户叫什么”返回的是“我叫小北”当我问“用户喜欢什么颜色”它可能返回空或者相关度极低的条目。从机制上讲search做了两件事一是用embedding模型把当前问题向量化去向量库里做相似度召回二是会利用user_id这类元数据做范围过滤保证我只搜了test_user这个用户的记忆空间不会把其他用户的信息捞出来。这比我自己拼上下文要可靠得多因为它是“按需召回”而不是“全部堆在桌面上”。2.3 记忆更新与冲突处理旧信息不会被无脑覆盖记忆系统和普通缓存的另一大区别是更新策略。缓存通常是key相同就覆盖而记忆系统需要考虑一个非常现实的问题用户的新说法是不是真的在推翻旧说法举个例子用户第一轮说“我喜欢Java”过了两天又说“最近开始用Go写项目了”。这两句话单独看都是事实但如果只知道“新增”不知道“更新”记忆库里就会同时存在两条互相矛盾的记录后续Agent检索时可能同时带出“喜欢Java”和“在用Go”模型就困惑了。Mem0对这类场景的处理思路是新信息入库时会先通过语义相似度在已有记忆中找关联记录如果发现高度相关它会比较新旧内容判断这是“补充”还是“更新”必要时把旧条目标记为过期或直接更新为新表述。这个机制我实测下来效果还不错但也不是万能——有些细粒度冲突它依然判断不准比如“用户说喜欢安静咖啡馆但另一条记忆是他周末常去Livehouse”这种矛盾需要结合上下文去理解单靠文本相似度很难做对。这里值得单独提醒一句记忆不是越多越好矛盾记忆必须被处理否则比没有记忆更可怕。它会让模型在同一段回答里精神分裂。3. 给Agent外挂记忆从零接入的完整过程3.1 最小可跑通版本要先把环境跑起来我们需要两样东西Mem0的Python包以及一个可以存储向量的后端。安装很简单pip install mem0ai对于测试环境我建议先用Chroma这类轻量级向量库起步不用单独部署服务端。配置文件写成这样config { vector_store: { provider: chroma, config: { collection_name: agent_memory, path: ./memory_store } }, embedder: { provider: openai, config: { model: text-embedding-3-small } }, llm: { provider: openai, config: { model: gpt-4o, temperature: 0.1 } } } memory Memory.from_config(config)你如果用的是本地模型做embedding或LLM把provider换成对应的本地推理服务即可。这里我没有把参数写死因为各个版本API会有细微差别以官方文档为准。关键是理解它的结构向量库负责存、embedding负责算相似度、LLM负责提取和判断三者缺一不可。3.2 把记忆接入Agent的主循环跑通最小版本之后真正的工作是把记忆“挂”到Agent的业务主循环里。我采用的模式是“两阶段回调”每次接收用户消息之前先做一次search把相关记忆注入系统提示词每次完整对话结束之后再把这段对话交给add让新信息沉淀下来。def get_memory_context(query: str, user_id: str) - str: hits memory.search(query, user_iduser_id) if not hits: return lines [f- {item[memory]} for item in hits[:5]] return 以下是关于该用户的长期记忆\n \n.join(lines) while True: user_input input(你: ) if user_input.lower() in (exit, quit): break memory_context get_memory_context(user_input, user_idu_10086) system_prompt f 你是我的智能助手。 回答时优先参考关于用户的长期记忆如果记忆与当前问题相关请不要让用户重复说明。 长期记忆: {memory_context} .strip() # 这里拼装完整的messages调用你的LLM messages [ {role: system, content: system_prompt}, {role: user, content: user_input} ] # assistant_reply your_llm_call(messages) assistant_reply 此处模拟模型返回 # 对话结束后沉淀新记忆 dialogue [ {role: user, content: user_input}, {role: assistant, content: assistant_reply} ] memory.add(dialogue, user_idu_10086)这个结构看起来简单但它解决了此前最头疼的问题模型每次看到的不是一大坨历史记录而是“关于这个用户这条问题最相关的5条记忆”。我实际跑下来响应速度更快回答的个性化程度也明显提升。这里的核心思路是把记忆系统当作一个“长期状态插件”而不是把历史消息当作“短期上下文”。3.3 生产环境配置清单如果你要推到生产环境有几个配置项需要认真选型。我整理了一张清单每项都标注了我在实践中认为最稳妥的选择配置项推荐方案原因向量存储Qdrant或PostgreSQL加向量插件数据量大时Chroma单机不够稳Qdrant查询并发更好Embedding模型和你的LLM同一生态的embedding模型避免不同生态向量空间差异导致检索不准用户身份统一业务侧用户唯一ID映射search和add都需要严格传user_id防止串记忆记忆条数上限每个用户控制在一定数量内检索性能和上下文注入都会受益访问控制记忆接口需要鉴权记忆比知识库更隐私泄露风险更高遗忘策略提供delete接口让用户可删除合规要求也避免记忆固化错误信息4. 实战中的坑我调了几天才稳定下来的细节4.1 噪声记忆污染上下文第一次跑通之后我很快发现一个问题Agent偶尔会在回答里冒出一句莫名其妙的话比如用户问“今天天气怎么样”它来一句“根据你之前提到的喜欢蓝色我为你推荐蓝色主题”。原因很简单Mem0默认的记忆打分阈值在我这个客服场景下偏低导致一些偶然提到的细节被当成了长期偏好。解决办法有两个方向一是在配置里调高记忆提取的置信度门槛二是在注入侧做限制只取search结果里分数最高的N条。我现在生产环境用的是“top 3”效果明显比“top 10”干净。这一条经验可能是我最想告诉后来者的记忆系统上线后最先要调的往往不是模型而是“它太爱记了”这个问题。4.2 user_id混乱导致记忆串台还有一次比较严重的事故我们业务里同一个用户在App端和Web端的用户ID体系不一致导致第一天在App上留下的记忆第二天在Web端完全查不到。更麻烦的是某些匿名访客在未登录状态下用的是临时ID临时ID产生了一堆“一次性记忆”把用户正式登录后的记忆空间挤占了。这个问题的根源不在Mem0而在我对身份体系的接入方式。后来我加了一层统一身份网关把匿名临时身份和真实用户身份统一映射到一个业务侧唯一的user_id上搜索和写入都走这个统一ID问题才彻底解决。做多端Agent时务必在最早阶段就把身份归一化设计好。4.3 记忆固化和“事实漂移”另一个我踩得比较深的坑是用户已经明确改了偏好但旧记忆依然权重很高。比如用户半年前说过“喜欢邮件沟通”最近说“现在都用微信”search时两条记忆都被召回模型在回复时容易出现“既要邮件又要微信”的怪异表达。Mem0的冲突更新能力能解决一部分但它的判断还是基于文本相似度遇到“用户换了联系方式、换了城市、换了工作”这类跨主题漂移它不一定能把“当前信息”和“历史信息”合理衔接。我的做法是给记忆检索结果做二次排序搜索结果里如果出现时间字段优先采信时间更近的记忆同时设一个“最新信息覆盖旧信息”的规则。如果你用的版本不支持时间权重可以自己在记忆条目里存时间戳在注入提示词时手动调整排序。4.4 成本与隐私边界记忆系统虽然香但它不是免费的午餐。每一次add都要调用LLM做提取每一次search都要调embedding都会产生token费用用户量大之后这部分成本增长非常快不是“存一条几厘钱”那么轻描淡写。更关键的是隐私合规。用户聊天记录里往往包含手机号、地址等敏感信息把它们长期存进向量库风险不小。我的建议是入库前做一次脱敏清洗给用户提供“清除我的记忆”的入口定期清理长时间未活跃用户的记忆数据。记住记忆系统的价值在于用户信任它如果它让人觉得是个“记仇本”那就本末倒置了。5. 装上记忆层之后的实测对比和它做不了的事5.1 一次简单的对比实验我在同一个客服Agent场景里做了对照一组不接记忆层只靠当前轮次上下文回答另一组接入Mem0按上面的流程跑完整对话闭环。用了几十个真实用户场景模拟测试几个关键指标差异非常明显对比维度无记忆Agent外挂记忆Agent用户重复说明需求的次数高经常要用户重新提供背景明显减少能主动引用历史偏好跨会话连续性每轮都像初见能记得上次没处理完的事回答个性化程度通用模板感强会结合用户画像调整语气与建议上下文token消耗随轮数线性膨胀只注入最相关的少量记忆更省矛盾信息出现概率低但普遍取决于更新策略需调优这里要说明这不是一个严格意义上的A/B实验变量控制也没那么精细但趋势非常稳定。最直观的感受是接入记忆之后用户那句“我刚才不是说了吗”的出现频率大幅下降——这个细节比任何指标都让我觉得值。5.2 它做不了的几件事把记忆系统神化是另一个坑。我在用了一段时间后清楚地意识到它的边界第一它不能增加模型的知识上限。用户问一个专业领域问题如果模型本来就不会记忆再多也救不了。第二它不能保证记忆永远准确。当用户意图表达得很隐晦时提取出来的记忆可能是错的错记忆一旦固化后续所有轮次都在错误前提下回答反而比没有记忆更糟。第三删除和遗忘需要自己设计。Mem0提供了delete接口但什么时候触发遗忘、哪些记忆该被冷处理业务侧还是得自己定义。所以更准确的说法是外挂记忆系统是一个“长期状态管理器”它负责让Agent在正确的时候回忆正确的事但它既不负责思考也不负责判断对错更不负责兜底模型能力。这些边界想清楚了你在使用它时才不会对结果有不切实际的期待。我把这套方案稳定跑了两三个月最大的体会是给Agent加记忆不是“加个数据库”这么简单它本质上是在重新设计Agent对人认知的建模方式。你在add和search之间反复调优的过程其实就是把“用户是谁”这个问题一步步拆开、落地的过程。如果你也在做类似的项目建议从最小闭环开始先跑通一条记忆的“存—取—用”再去考虑冲突处理、遗忘策略和成本控制。记忆系统这东西真正用起来之后你会觉得它平平无奇但一旦拔掉它Agent立刻会退回那个“一问三不知”的陌生人状态。希望这篇记录能让你少走几天弯路。

相关新闻

Matpower 8.0安装与工程级潮流计算实战指南

Matpower 8.0安装与工程级潮流计算实战指南

简介:本资源为MATLAB电力系统仿真核心工具箱MATPOWER 8.0的官方兼容安装包,面向电气工程专业本科生、研究生及电力系统分析方向科研人员,用于支撑潮流计算、最优潮流、状态估计、可靠性评估等典型建模与算法验证任务。压缩包为ZIP格式&#x…

2026/10/13 13:24:18 阅读更多 →
Cursor 搭配 Graphify 实属强强联合了:把 Base URL 改到 TaoToken 的完整配置

Cursor 搭配 Graphify 实属强强联合了:把 Base URL 改到 TaoToken 的完整配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 13:24:21 阅读更多 →
Agent落地实践:上下文窗口、记忆分层与MCP协议全解析

Agent落地实践:上下文窗口、记忆分层与MCP协议全解析

做了大半年 Agent 落地,我几乎每天都会被同一个问题卡住:上下文窗口。最初我把精力都放在怎么把窗口用满、怎么塞更多历史,后来才发现,真正让 Agent 变聪明的并不是窗口本身,而是记忆和工具这两件事。等我把外置记忆和…

2026/10/13 13:24:25 阅读更多 →

最新新闻

ETL实操手记:稳定性、可追溯性与业务语义落地

ETL实操手记:稳定性、可追溯性与业务语义落地

1. 项目概述:这不是一份简历,而是一份ETL实操手记“ETL工作经验分享(1)”——看到这个标题,别急着划走。它不是那种罗列“熟悉Kettle、会写SQL、了解DataStage”的简历式总结,也不是教科书里抽象的“抽取-转…

2026/10/13 15:10:42 阅读更多 →
Elastic-Job分布式任务调度:分片、高可用与生产实践

Elastic-Job分布式任务调度:分片、高可用与生产实践

做过定时任务的都知道,单机版Quartz写起来很爽,但一上生产、多节点部署,问题立马就堆过来了:任务重复执行、机器挂了没人接管、数据量大了单机扛不住。今天要聊的Elastic-Job,就是专门解决这类分布式场景下任务调度痛点…

2026/10/13 15:10:42 阅读更多 →
XGBoost工程实战手册:从安装配置到可解释调参

XGBoost工程实战手册:从安装配置到可解释调参

简介:本资源是Jason Brownlee撰写的英文原版经典教程《XGBoost with Python》PDF电子书,面向具备Python与机器学习基础的开发者、数据分析师及进阶学习者,系统解决梯度提升建模中原理理解难、XGBoost实操门槛高、参数调优无从下手等核心问题。…

2026/10/13 15:10:42 阅读更多 →
数学建模竞赛实战指南:从组队分工到论文写作的全流程经验

数学建模竞赛实战指南:从组队分工到论文写作的全流程经验

我不是啥机构认证的竞赛导师,就是一个带队参赛过很多次的老学长。这些年我带过的队伍,有拿奖拿到手软的,也有三天三夜肝到崩溃最后连成功参赛奖都没混上的。你问我数学建模竞赛(MCM)到底难不难?我的答案是&…

2026/10/13 15:10:42 阅读更多 →
为什么TCP要“动态适应网络特性“?用“开车根据路况调整速度“比喻。

为什么TCP要“动态适应网络特性“?用“开车根据路况调整速度“比喻。

核心比喻: TCP发送数据,就像你开车拉货,从A地送到B地。 你的货车 发送方主机货物 TCP分段(前面的拼图碎片)公路 网络链路路上堵车、事故、限速 网络拥塞(路由器排队、丢包、延迟变大)拥塞窗…

2026/10/13 15:10:42 阅读更多 →
基于模型的测试(MBT)实战:状态机建模与用例自动生成

基于模型的测试(MBT)实战:状态机建模与用例自动生成

1. 为什么我会推荐把MBT放进测试技术栈1.1 传统用例设计的三道坎做测试这行时间长了,你会慢慢发现一个有意思的现象:真正让人头疼的不是测试执行,而是测试设计。需求评审完了,开发方案定了,接下来就是用例设计。但用例…

2026/10/13 15:09:41 阅读更多 →

日新闻

物联网系统集成避坑指南:从选型到验收的关键要点

物联网系统集成避坑指南:从选型到验收的关键要点

做了这么多年物联网系统集成的项目,我最大的感受是,大部分坑不是出在技术前沿,而是出在一些看起来很平常的地方。设备选了便宜的、协议没问清楚、现场没摸透、验收标准写得含糊——这些细枝末节,最后都会变成延期、超支和扯皮的大…

2026/10/13 0:00:57 阅读更多 →
ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查

如果你玩ESP32有一阵子了,很难不碰到舵机控制这道坎。做智能小车要转方向,做机械臂要抬关节,做云台要偏航俯仰,背后都是同一个东西在干活:舵机。而我见过太多人卡在“代码能编译、舵机就是不听话”这个阶段&#xff0c…

2026/10/13 0:00:57 阅读更多 →
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析

1. 为什么CAN总线至今仍是工业现场的“硬通货”?——从汽车底盘到智能温室的真实需求你有没有拆过一辆老款燃油车的中控台?或者调试过某高校实验室里那台跑着Modbus协议却频频丢帧的PLC控制柜?又或者,在某个农业物联网项目里&…

2026/10/13 0:00:57 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 21:05:09 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →