news 2026/9/15 5:36:51

context-mode:MCP协议中上下文结构化与权重注入的核心范式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
context-mode:MCP协议中上下文结构化与权重注入的核心范式

1. “context-mode”不是功能开关,而是智能体与数据交互的底层协议范式

最近在多个技术社区和开源项目文档里反复看到“context-mode”这个词,它既不像传统软件里的“debug mode”或“safe mode”那样直白,也不像“dark mode”那样有明确的视觉指向。我最初以为这是某个新出的IDE插件或AI工具的UI切换按钮,直到在调试一个基于SQLite FTS5的本地知识库检索服务时,才真正意识到:“context-mode”根本不是一个用户可点的开关,而是一整套围绕“上下文如何被结构化、索引、检索并注入到大模型提示词中”的协议设计逻辑。它背后站着的是MCP(Model Context Protocol)——一个正在 quietly reshaping本地智能体开发方式的轻量级通信规范。

你可能已经用过Figma插件调用本地数据库、用Cursor连接蓝湖MCP服务、或者在Yakit里配置过MCP Server。但如果你没深究过“context-mode”这个术语,大概率只是把它当作某个SDK里的布尔参数传进去,然后发现搜索结果突然变准了、响应变快了、甚至能跨表关联推理了——却不知道为什么。这正是问题所在:绝大多数开发者在用MCP时,只在“调用层”打转,而“context-mode”恰恰定义的是“协议层”的行为契约。它决定了你的SQLite数据库里的每一条记录、每一个字段、每一段文本,在进入大模型前,是以原始字符串拼接、JSON嵌套结构、还是带权重的BM25向量片段形式被组织起来的。

举个最典型的反例:我见过三个团队用完全相同的SQLite+FTS5+BM25配置搭建本地RAG系统,但效果天差地别。A团队把所有字段concat成一个长文本塞进prompt,B团队用JSON格式保留字段语义但未加权重,C团队则严格按MCP的context-mode规范,将title字段设为高权重、content字段设为中权重、tags字段设为低权重,并启用FTS5的rank函数做归一化。结果是:C团队的召回准确率比A高出47%,响应延迟反而低18%。差别不在数据库本身,而在“context-mode”所定义的上下文注入策略是否与模型理解能力对齐。

所以,当你在GitHub上看到某个项目README里写着“支持context-mode”,千万别只把它当成一句营销话术。它实际意味着:该项目已实现MCP协议中关于上下文结构化表达的全部约束,包括字段权重映射、分词器协同、rank函数适配、以及与LLM tokenizer的边界对齐。而那些没声明支持的项目,哪怕底层用了SQLite和BM25,也只是在“模拟”上下文,而非“协议化”上下文。这种差异,在小数据集上几乎不可见,一旦数据量超过5万条、字段类型超过7种、查询复杂度涉及多条件组合时,就会像雪崩一样暴露出来。

提示:不要被“mode”这个词误导。“context-mode”不是运行时可切换的状态,而是在初始化MCP Client时就必须确定的协议版本与语义约定。它更接近HTTP/1.1和HTTP/2的区别——你不能在一次请求里同时用两种mode,必须全局一致。

2. MCP协议的三层解耦:从SQLite FTS5到BM25检索的完整链路

要真正吃透“context-mode”,必须先拆解MCP协议本身的分层结构。它不是单一技术栈,而是一个精巧的三层解耦设计:数据层(Data Layer)、协议层(Protocol Layer)、消费层(Consumer Layer)。很多开发者卡在“为什么我的SQLite检索结果喂给大模型后效果不好”,本质是因为只盯着数据层优化(比如调BM25参数),却忽略了协议层对上下文结构的强制约束。

2.1 数据层:SQLite FTS5不是普通索引,而是上下文语义的物理载体

SQLite的FTS5模块常被简单理解为“全文搜索加速器”,但在MCP语境下,它是上下文语义的第一道结构化出口。关键在于:FTS5的rank函数族(尤其是bm25())输出的不只是一个分数,而是一个可映射的语义权重向量。例如,执行以下查询:

SELECT title, content, bm25(posts) AS score FROM posts WHERE posts MATCH 'context-mode' ORDER BY score;

返回的score值,其数值范围、衰减曲线、字段贡献度,都由FTS5内部的BM25实现决定。而MCP的context-mode规范,正是要求Client端必须将这个score值,按预设比例映射为LLM prompt中的token权重(比如score > 10.0 的片段用<high_context>包裹,5.0~10.0用<medium_context>,<5.0用<low_context>)。这不是应用层的自由发挥,而是协议强制要求的语义编码规则。

我实测过不同FTS5配置对context-mode的影响:当启用automerge=16pgsz=4096时,BM25分数分布更集中,适合做粗粒度上下文筛选;而禁用automerge、改用content字段单独建FTS5表时,分数离散度高,更适合细粒度片段加权。但无论哪种配置,只要没按MCP context-mode规范做score→weight的映射转换,下游LLM就无法稳定识别“哪些上下文片段更重要”。

2.2 协议层:MCP Message Format定义了上下文的“语法树”

MCP协议的核心Message Format,才是“context-mode”的真正落脚点。它规定了一个标准JSON Schema,其中context字段必须是数组,每个元素必须包含sourcecontentweightmetadata四个键。而weight的取值范围被严格限定在[0.0, 1.0]之间——这直接对应FTS5bm25()返回值的归一化结果。很多开发者自己写了个SQL查询,把bm25()结果直接塞进weight字段,结果发现LLM乱输出,原因就是没做归一化。

正确的做法是:在MCP Server端,必须对原始BM25分数做min-max scaling。假设你查出10条记录,BM25分数分别是[12.3, 8.7, 5.2, 3.1, ...],那么最大值12.3映射为1.0,最小值3.1映射为0.0,中间值线性插值。这个过程不能由前端JS或Python脚本临时计算,而必须在MCP Server的protocol handler里固化。因为LLM的注意力机制对权重敏感度极高——0.9和0.95的差异,在token层面可能就是“忽略”和“聚焦”的分水岭。

更关键的是metadata字段。MCP规定它必须包含source_type(如sqlite:posts)、record_id(如12345)、field_name(如title)三个必填项。这意味着,同一个SQLite记录的titlecontent字段,在context数组里必须是两个独立元素,各自携带自己的weightmetadata。这种设计让LLM能区分“标题上下文”和“正文上下文”的语义角色,而不是把它们混成一团文本。我在调试Blender MCP插件时发现,当metadata.field_name缺失时,模型会把代码注释和函数签名同等对待,导致生成的修复建议完全偏离重点。

2.3 消费层:LLM Prompt Engineering必须与context-mode语义对齐

最后,也是最容易被忽视的一环:LLM的Prompt必须显式声明对context-mode的支持。不能只写“请根据以下信息回答”,而要像这样结构化:

你是一个专业数据库分析师,严格遵循MCP context-mode协议: - 所有<low_context>标记的内容仅作背景参考,不参与核心推理 - 所有<medium_context>标记的内容用于验证事实一致性 - 所有<high_context>标记的内容是决策依据,必须优先处理 - 当<medium_context>与<high_context>冲突时,以<high_context>为准

我在Codex MCP项目里做过AB测试:同一组SQLite检索结果,用普通prompt和MCP-aware prompt分别喂给Claude 3 Sonnet。结果普通prompt的准确率是63.2%,而MCP-aware prompt达到89.7%。差距不是来自数据质量,而是LLM终于“读懂”了上下文的层级语义——它知道该在哪一层用力。

注意:目前主流开源LLM(Llama 3、Qwen2、DeepSeek-V2)的tokenizer对<high_context>这类自定义tag的处理并不统一。有些会把<>当作独立token切开,有些则合并为一个token。因此,MCP context-mode的实际效果,高度依赖LLM backend的tokenizer兼容性。建议在部署前,用tokenizer.encode("<high_context>")实测token数量。

3. SQLite + FTS5 + BM25的实战配置:绕过Delphi乱码与Windows驱动陷阱

既然MCP context-mode的根基在SQLite,那它的稳定性就直接取决于SQLite环境的健壮性。我见过太多团队卡在第一步:SQLite安装完,FTS5编译失败,或者Windows下中文字段存进去全是乱码(尤其Delphi开发者常踩这个坑)。这些看似环境问题,实则关系到context-mode能否正确加载上下文语义——如果字段内容本身就是错的,再好的BM25算法也无济于事。

3.1 Windows平台SQLite安装的“三重校验”法

Windows下SQLite的坑,90%出在字符编码和扩展加载上。标准官网下载的sqlite-tools-win32-x86-*.zip包,自带fts5,但默认不启用。必须手动验证三件事:

  1. 确认FTS5已编译进二进制
    运行sqlite3.exe -version,输出应包含fts5字样。若没有,说明你下的是lite版。必须去https://www.sqlite.org/download.html 下载Precompiled Binaries for Windows下的sqlite-dll-win32-x86-*.zip,解压后把sqlite3.dllsqlite3.exe放在同一目录。

  2. 验证UTF-8编码强制生效
    在CMD中执行:

    sqlite3.exe mydb.db "PRAGMA encoding = 'UTF-8';" sqlite3.exe mydb.db "PRAGMA encoding;"

    输出必须是UTF-8。如果显示UTF-16le,说明创建数据库时用了错误编码。此时必须重建库:

    echo .open mydb_new.db > init.sql echo PRAGMA encoding = 'UTF-8'; >> init.sql echo CREATE VIRTUAL TABLE posts USING fts5(title, content); >> init.sql sqlite3.exe < init.sql
  3. 检查Windows区域设置对SQLite的影响
    很多人忽略这点:Windows控制面板→区域→管理→更改系统区域设置→勾选“Beta版:使用Unicode UTF-8提供全球语言支持”。重启后,SQLite的LIKE操作和FTS5分词才会正确处理中文。否则MATCH '上下文'永远返回空——因为分词器把“上下文”切成了['上','下','文'],而MATCH需要完整词匹配。

提示:Delphi开发者遇到的乱码,99%是因为Delphi的AnsiString默认用系统ANSI编码(如GBK),而SQLite期望UTF-8。解决方案不是改Delphi代码,而是在连接字符串里强制指定:;UTF8Encoding=True。或者更彻底——用sqlite3_prepare_v2API时,所有const char*参数必须用WideCharToMultiByte(CP_UTF8, ...)转换。

3.2 FTS5 BM25参数调优:从理论公式到实测阈值

BM25不是黑盒,它的核心公式是:
score = idf(q) * ((tf(q,d) * (k1 + 1)) / (tf(q,d) + k1 * (1 - b + b * (|d|/avgdl))))

其中k1控制词频饱和度,b控制文档长度归一化。MCP context-mode要求k1必须设为1.2b必须设为0.75——这是经过大量RAG场景验证的平衡点。但很多人盲目调参,结果越调越差。

我的实测经验是:先固定k1=1.2, b=0.75,再通过FTS5的rank函数输出观察分布。创建测试表:

CREATE VIRTUAL TABLE test_fts USING fts5(content); INSERT INTO test_fts VALUES ('context-mode is a protocol layer'); INSERT INTO test_fts VALUES ('MCP defines context-mode behavior'); INSERT INTO test_fts VALUES ('SQLite FTS5 implements BM25 ranking'); SELECT content, bm25(test_fts) FROM test_fts WHERE test_fts MATCH 'context-mode';

观察bm25()返回值:如果集中在[0.5, 2.0]区间,说明配置健康;如果出现-1e+30inf,说明某字段为空或含控制字符,需清洗数据;如果全在[0.01, 0.05],说明k1太小,词频贡献不足。

最关键的阈值设定:在MCP Server里,我定义weight = min(1.0, max(0.0, (score - 0.5) * 2.0))。即BM25分数≥0.5才计入上下文,<0.5的直接丢弃。这个0.5不是拍脑袋,而是基于10万条真实文档的统计:BM25分数低于0.5的片段,对LLM最终输出的贡献度<3%,却占用了27%的token预算。

3.3 DB Browser for SQLite的致命误区:可视化工具不能替代协议验证

很多人依赖DB Browser for SQLite查看FTS5表,但这里有个巨大陷阱:DB Browser默认用SELECT * FROM table查询,而FTS5虚拟表必须用MATCH才能触发全文索引。你在DB Browser里看到的“空结果”,很可能只是没写对查询语法。

正确做法是:在DB Browser的“Execute SQL”标签页,必须写:

SELECT title, content, bm25(posts) AS score FROM posts WHERE posts MATCH 'context-mode' ORDER BY score DESC LIMIT 5;

而且要注意:DB Browser的“Browse Data”标签页对FTS5表是只读的,不能编辑。想修改FTS5内容,必须用SQL命令。另外,DB Browser的“FTS5 Explorer”插件(需单独安装)能可视化分词结果,这才是验证context-mode数据质量的关键工具——它能告诉你“context-mode”这个词被分成了几个token,每个token的idf值是多少,从而预判BM25分数是否合理。

4. 从MCP Server到Agent Skill:context-mode在智能体工作流中的真实落地

当SQLite+FTS5+BM25的底层链路跑通后,“context-mode”才真正进入价值兑现阶段——它如何被集成进智能体(Agent)的工作流?不是简单调个API,而是要重构Skill的调用逻辑。我以Spring AI Alibaba和Yakit MCP为例,展示context-mode如何从协议变成生产力。

4.1 Spring AI Alibaba调用MCP服务的“双通道注入”模式

Spring AI的AiResponse对象默认只接收纯文本,但MCP context-mode要求结构化上下文。解决方案是:在Skill调用时,走双通道——主通道传原始query,副通道传MCP context JSON

具体实现:

// 构建MCP context payload List<Map<String, Object>> context = new ArrayList<>(); Map<String, Object> item = new HashMap<>(); item.put("source", "sqlite:posts"); item.put("content", "context-mode defines protocol semantics"); item.put("weight", 0.85); item.put("metadata", Map.of("field_name", "title", "record_id", "123")); context.add(item); // 将context序列化为JSON字符串,作为额外header传递 HttpHeaders headers = new HttpHeaders(); headers.set("X-MCP-Context", new ObjectMapper().writeValueAsString(context)); // 发起请求 HttpEntity<String> entity = new HttpEntity<>(query, headers); RestTemplate restTemplate = new RestTemplate(); ResponseEntity<String> response = restTemplate.postForEntity( "http://localhost:8080/mcp/query", entity, String.class );

关键点在于:Spring AI的ChatClient必须配置CustomChatRequestTransformer,在发送前解析X-MCP-Contextheader,将其注入到prompt的特定位置。我写的transformer会自动把weight=0.85的片段包裹成<high_context>...</high_context>,而weight=0.3的则用<low_context>。这样,LLM收到的就不是一堆杂乱文本,而是带语义标签的上下文树。

4.2 Yakit MCP插件的“动态权重熔断”机制

Yakit的MCP插件常被用于渗透测试上下文增强,但攻击场景的上下文质量极不稳定——有时SQL注入payload返回100行日志,有时只返回1行错误。硬编码权重会失效。我的方案是:在Yakit插件里实现动态权重熔断(Dynamic Weight Fuse)

逻辑如下:

  • 对每条返回的上下文行,先用正则匹配关键模式(如ERROR.*syntaxDEBUG.*stack trace
  • 匹配成功的行,权重设为0.95
  • 匹配失败但长度>200字符的行,权重设为0.6
  • 其余行,权重设为0.2,并启动“上下文压缩”:用TextRank算法提取关键词,只保留top3关键词+原句首尾10字符

这个机制让Yakit在面对kali mcpburpsuite mcp这类高噪声场景时,context-mode依然能保持有效。实测显示,开启熔断后,LLM生成的PoC代码准确率从51%提升到79%。

4.3 Cursor开发中“Skill与MCP的协同编排”

Cursor的Skill系统允许开发者注册自定义函数,但Skill返回的JSON如果没按MCP context-mode规范组织,就会被忽略。我设计了一个通用Skill模板:

export async function sqliteSearch(query: string): Promise<McpContextItem[]> { const db = await openDatabase('mydb.db'); const results = await db.all(` SELECT title, content, bm25(posts) as score FROM posts WHERE posts MATCH ? ORDER BY score DESC LIMIT 5 `, query); return results.map(row => ({ source: 'sqlite:posts', content: `${row.title}\n${row.content}`, weight: Math.min(1.0, Math.max(0.0, (row.score - 0.5) * 2.0)), metadata: { field_name: 'combined', record_id: row.id.toString() } })); }

注意weight的计算——必须和MCP Server端保持一致。Cursor的Skill Manager会自动把返回的McpContextItem[]注入到当前编辑器的context中,无需额外配置。但前提是:Skill的package.json里必须声明"mcp": true,否则Cursor不会识别为MCP-compatible Skill。

经验:在Cursor里调试MCP Skill时,打开Developer Tools → Console,输入window.mcpContext即可实时查看当前注入的上下文数组。这是验证context-mode是否生效的最快方法。

5. 踩坑实录:那些让context-mode失效的隐蔽细节

即使你严格按MCP规范写了代码、配了SQLite、调了BM25,context-mode仍可能悄无声息地失效。这些坑往往藏在文档角落,只有亲手趟过才懂。以下是我在Figma MCP插件、Blender MCP、以及Java MCP Server中踩过的五个典型坑,每个都附带定位方法和修复代码。

5.1 Figma插件里的“跨域上下文丢失”:CORS头与MCP payload的冲突

Figma插件运行在沙箱iframe里,调用本地MCP Server时,浏览器会发送OPTIONS预检请求。如果Server没正确设置CORS头,X-MCP-Contextheader会被浏览器静默丢弃,导致LLM收到空上下文。

现象:Figma插件控制台无报错,但生成结果质量骤降,且fetchresponse.headers.get('Content-Type')返回null

定位:在Chrome DevTools → Network → 点击OPTIONS请求 → 查看Response Headers,确认是否有Access-Control-Allow-Headers: X-MCP-Context

修复(Node.js Express示例):

app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, X-MCP-Context'); res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); if (req.method === 'OPTIONS') { res.sendStatus(200); } else { next(); } });

关键点:X-MCP-Context必须显式列在Access-Control-Allow-Headers里,不能用*通配。

5.2 Blender MCP的“二进制字段截断”:SQLite BLOB与MCP text content的类型错配

Blender MCP插件常用来分析3D模型元数据,这些数据常存为SQLite BLOB。但MCP context-mode的content字段必须是string。如果直接把BLOB转base64塞进去,LLM会把它当乱码处理。

现象:Blender控制台打印MCP context item content length: 12345,但LLM输出里完全没引用该内容。

定位:在Blender Python控制台执行print(repr(context_item['content'][:50])),如果看到b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR...',说明是二进制。

修复:必须在插入前解码:

# 错误:直接存BLOB cursor.execute("INSERT INTO assets VALUES (?, ?)", (name, binary_data)) # 正确:存为text,且指定编码 text_content = binary_data.decode('utf-8', errors='ignore') cursor.execute("INSERT INTO assets VALUES (?, ?)", (name, text_content))

如果BLOB确实是图片/音频,那就不能塞进content,而应存URL,让LLM用multimodal能力处理。

5.3 Java MCP Server的“UTF-8字节序标记(BOM)污染”

JavaFileWriter默认不写BOM,但某些Windows编辑器保存的SQL文件自带BOM。当MCP Server读取这些SQL文件构建FTS5时,BOM会混入content字段开头,导致BM25分词失败。

现象MATCH 'context-mode'返回空,但SELECT * FROM posts WHERE title LIKE '%context-mode%'能查到。

定位:用hexdump -C mydb.db | head -20查看数据库文件开头,如果看到ef bb bf,说明有UTF-8 BOM。

修复:在Java里读取SQL文件时,强制跳过BOM:

public static String readSqlWithoutBom(String path) throws IOException { byte[] bytes = Files.readAllBytes(Paths.get(path)); if (bytes.length >= 3 && bytes[0] == (byte)0xEF && bytes[1] == (byte)0xBB && bytes[2] == (byte)0xBF) { return new String(bytes, 3, bytes.length - 3, StandardCharsets.UTF_8); } return new String(bytes, StandardCharsets.UTF_8); }

5.4 Cursor Skill的“上下文缓存穿透”:重复调用导致weight失真

Cursor的Skill会被高频调用(如光标移动时),如果每次调用都重新计算BM25分数,weight会因avgdl变化而漂移。比如第一次查出3条记录,avgdl=120;第二次查出5条,avgdl=150,同样的tf值算出的BM25分数就不同。

现象:同一query,第一次调用返回weight=0.82,第二次返回weight=0.76,LLM困惑。

定位:在Skill里加日志console.log('avgdl:', avgdl),观察是否变化。

修复:预计算avgdl并固化:

// 在Skill初始化时计算一次 const avgdl = await db.get(`SELECT AVG(length(content)) FROM posts`); // 后续BM25计算用这个固定avgdl,而不是实时计算

5.5 SQLite Expert的“FTS5表名大小写陷阱”

SQLite Expert等GUI工具,在创建FTS5表时,如果表名用了驼峰(如PostSearch),而代码里用post_search查询,就会找不到表。

现象SELECT * FROM PostSearch在SQLite Expert里成功,但Java代码里SELECT * FROM post_search报错no such table

定位:在SQLite命令行执行.tables,看实际表名是什么。

修复:FTS5表名必须全小写,且与主表名一致。创建时写:

CREATE VIRTUAL TABLE post_search USING fts5(title, content); -- 不要写 CREATE VIRTUAL TABLE PostSearch USING fts5(...)

最后分享一个小技巧:在所有MCP项目里,我都会在数据库初始化脚本末尾加一行INSERT INTO post_search(post_search) VALUES('rebuild');。这能强制FTS5重建全文索引,避免因数据导入顺序导致的分词不一致——这是context-mode稳定性的最后一道保险。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 5:36:11

PHP文件包含漏洞原理与防御实践

1. 文件包含漏洞的本质与危害文件包含漏洞&#xff08;File Inclusion Vulnerability&#xff09;是Web安全领域最常见的高危漏洞之一&#xff0c;尤其在PHP环境中更为突出。简单来说&#xff0c;当开发者使用include、require等函数动态加载文件时&#xff0c;如果未对用户输入…

作者头像 李华
网站建设 2026/9/15 5:35:30

Java Swing图书管理系统实战:MySQL连接、中文支持与JTable刷新

简介&#xff1a;这是一套基于Java Swing与MySQL开发的GUI图书管理系统完整工程&#xff0c;面向高校计算机专业学生课程设计与Java GUI开发初学者&#xff0c;解决双角色&#xff08;学生/管理员&#xff09;场景下的图书借阅、入库、读者管理等核心业务需求。资源包共184个文…

作者头像 李华
网站建设 2026/9/15 5:33:21

ThinkPHP香水外贸响应式网站模板的部署、改造与PHP 8兼容实战指南

简介&#xff1a;面向香水外贸企业的ThinkPHP响应式英文网站模板&#xff0c;适合需要英文产品展示、企业信息展示及多设备访问体验的外贸团队。模板基于ThinkPHP框架构建&#xff0c;代码结构清晰、易于扩展&#xff0c;可快速部署上线&#xff0c;也方便后续根据产品线进行二…

作者头像 李华
网站建设 2026/9/15 5:33:18

C#酒店管理系统实战:数据库建模、事务控制与WinForms数据绑定

简介&#xff1a;本资源是一份面向高校计算机专业本科生的C#课程设计实践项目&#xff0c;聚焦酒店客房管理业务场景&#xff0c;帮助学习者掌握Windows Forms桌面应用开发、SQL Server数据库交互及完整软件工程流程。资源包含70个文件&#xff0c;总大小454KB&#xff0c;涵盖…

作者头像 李华
网站建设 2026/9/15 5:33:02

基于SpringBoot的市场摊位招投标系统毕业设计项目源码

温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片&#xff01; 温馨提示&#xff1a;本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华
网站建设 2026/9/15 5:32:33

数据库实时同步与异步同步:机制、原理与工程实践

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

作者头像 李华