news 2026/9/5 11:05:13

AST代码轮廓:让AI编程Agent按需读取,告别整文件硬啃

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AST代码轮廓:让AI编程Agent按需读取,告别整文件硬啃

最近我在调一个 AI 编程 Agent 的工作流时,被一个现象搞得很头疼:面对一个几千行的老文件,Agent 的第一反应永远是“把整个文件读一遍”。上下文窗口确实变大了,但模型读不完就开始自己脑补结尾,最后改出来的代码全是幻觉。后来我做了个叫 ast-outline 的小模块,思路很朴素——先让 Agent 拿到文件的语法树轮廓,再决定读哪一小段。

这篇文章把完整的实现思路、核心代码和踩坑记录拿出来聊聊。适合正在做 AI Agent、Coding Agent 工具链,或者在各类 Agent 框架里自定义代码读取能力的开发者。如果你只是好奇“为什么我的 AI 编程助手经常改错旧文件”,这篇也能帮你理解背后的代码上下文问题。

1. AI 编程 Agent 为什么不建议整文件硬啃

1.1 上下文窗口再大,也不是用来让模型从头读文件的

每次看到有人把 3000 行代码直接塞给模型,我都觉得这是在浪费推理预算。大模型上下文窗口是 200k 不假,但你把一个 5000 行文件塞进去,真正对当前任务有用的信息可能只有其中两个函数。更麻烦的是,长上下文里存在一个已经被很多人验证过的现象:模型对中间部分内容的利用率会明显下降。你的核心函数写在文件中部,它反而看不见,最后改出了一个看似合理但根本没碰到正确逻辑的补丁。

我遇到过最典型的一次:一个 3200 行的 service.py 文件,Agent 连续调了三次读取工具,先把前 800 行看完,又看 800 行,最后把整个文件都装进了上下文。Token 倒是没超限,但模型越往后越糊涂,频繁把同名局部变量当成模块级变量,甚至在某个函数里改另一个函数的内部状态。后来我定位到真正要改的逻辑只需要看 L1800 到 L1950 共 150 行代码,前面那几千行全是噪音。

这个问题的本质是:代码读取不能搞“大水漫灌”,AI Agent 需要的是按需获取。不是所有信息都值得进上下文,优质上下文应该只包含与当前任务相关的直接代码和必要的调用关系。整文件硬啃看似省事,实际上让 Agent 在开局就背负了大量无用 token,推理速度变慢,错误率反而升高。

1.2 按行切块和关键词检索,为什么会把函数切得支离破碎

也许你会说,那不用整文件,用 RAG 按块召回不就行了?现实是,很多 RAG 方案对代码文件都是按固定行数切块,比如每 200 行切一段。一个 300 行的函数被切成两半,第一段只有函数头,第二段只有函数体下半部分。向量检索可能召回其中一段,模型拿到的是残缺代码,它根本看不出这个函数从哪里开始、到哪里结束。

关键词检索也有类似问题。BM25 这类方法能帮你定位到“这个文件里有 create_order 函数”,但只返回一行命中结果远远不够,模型依然需要二次读取整个文件才能理解上下文。如果这个函数有嵌套、有装饰器、有多个分支,单靠关键词回传的片段完全无法支撑推理。

所以我们需要一种比“行号区间”更结构化的文件描述方式:告诉 Agent 这个文件里有哪些符号、每个符号从第几行到第几行、它们之间的包含关系是什么样的。这正是 AST 能提供的核心能力。

2. ast-outline 的核心设计:给 Agent 输出代码轮廓而不是全文

2.1 第一轮只给目录,不要让模型通读整个文件

ast-outline 的核心产出,是一份紧凑的“代码地图”。这份地图不用包含具体实现,只列出符号信息,大概长这样:

文件: order_service.py(共 1240 行) - def create_order(user_id, items) L18-L156 - class OrderRepository L180-L620 - async def find_by_id(id) L195-L260 - def save(order) L270-L410 - def cancel_order(order_id) L630-L900

第一轮跟 Agent 交互时,我只把这个结构给模型。它看到文件里有哪些函数、类、方法以及行号范围,就能做出判断:这次需求要改的是 create_order,那我只去读 L18-L156 这一段,其他代码一概不读。

这种设计对 Agent 工作流的改变是巨大的。原来 Agent 读一个文件需要消耗 1 万到 8 万 token,现在只需要几百到一千 token 就能完成“侦查”阶段。当它真正动手修改时,再按符号 ID 读取精确区间,整个任务累计消耗的 token 通常不会超过原来全量读取的 1/5。

有人会问:直接用编辑器里的 LSP 返回 documentSymbol 不也一样吗?思路确实类似,但 ast-outline 要解决的问题更聚焦。LSP 依赖一个完整的语言服务进程,对于临时拉下来的源码目录、跨语言混编项目,启动成本很高。而且 LSP 的定位是辅助 IDE 展示,它的返回结构不一定方便 Agent 直接作为工具调用。ast-outline 只做一件事:解析文件并输出紧凑可读的符号地图。

2.2 为什么是基于 AST 而不是正则表达式去猜边界

处理 Python 代码时,有人会尝试用正则匹配 def 关键字来提取函数名和行号。这种方案在小文件里看着能用,一旦遇到多行参数列表、函数名和括号之间换行、或者是类里嵌套函数,正则很快就会翻车。

举个例子,Python 里你可能会这样写函数:

def process( data, config=None, callback=lambda x: x, ) -> dict: ...

正则如果不做状态机,大概率把结束行号算错。更复杂的情况是函数内部有一个 if 分支,分支里又定义了一个局部函数。如果目标函数结束行被你算到局部函数结束,那读出来的代码就包含了错误的区域,Agent 拿到后完全无法理解缩进层级。AST 则会按照 Python 语法规则精确告诉你这几个节点各自的 begin 和 end,天然支持嵌套关系。

AST 还有一个隐藏优势:你可以提取装饰器、async/await 标记、函数参数名、类型注解等元信息。这些信息对 Agent 判断“这个函数能不能异步调用”“这个 API 接口暴露了哪些路由”非常关键。正则方案想提取这些很容易写成一团乱麻。

2.3 从 outline 到按需读取,核心循环只有五步

ast-outline 的完整调用流程可以归纳成五个动作:

  1. Agent 首次接触一个文件时,调用get_file_outline(path),拿到符号地图。
  2. Agent 根据地图判断当前任务最相关的符号,比如OrderRepository.find_by_id
  3. Agent 调用read_symbol(path, symbol_id),工具服务端从源码里截取该符号的完整代码区间并返回。
  4. Agent 阅读这段代码,如果发现该函数内部还有复杂分支,可以继续请求该函数内的子区块。
  5. 修改完成后,整个对话上下文里只出现过真正相关的代码片段,没有多余噪音。

这个循环的关键是第二步。Agent 必须先建立“文件里有这些符号”的认知,再决定下一步读哪里。如果第一步直接跳过,它就会回到老路上去整文件硬啃。

3. 动手实现一个最小可用的 ast-outline:代码与调用设计

3.1 Python 示例:用内置 ast 模块提取类和函数

如果你主要处理 Python 项目,完全不需要引入第三方解析器,标准库的ast模块就够了。我先写一个精简版收集器:

from typing import List, Dict, Any import ast def _base_symbol(node) -> Dict[str, Any]: return { "name": node.name, "kind": "function" if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) else "class", "start": node.lineno, "end": getattr(node, "end_lineno", node.lineno), "children": [], } def _func_detail(node) -> Dict[str, Any]: sym = _base_symbol(node) if isinstance(node, ast.AsyncFunctionDef): sym["kind"] = "async_function" try: args = [] for a in node.args.args: args.append(a.arg) if node.args.vararg: args.append("*" + node.args.vararg.arg) if node.args.kwarg: args.append("**" + node.args.kwarg.arg) sym["params"] = ", ".join(args) except Exception: sym["params"] = "" decorators = [] for dec in node.decorator_list: try: decorators.append(ast.unparse(dec)) except Exception: pass sym["decorators"] = decorators return sym def collect_symbols(source: str) -> List[Dict[str, Any]]: tree = ast.parse(source) symbols = [] for node in tree.body: if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): symbols.append(_func_detail(node)) elif isinstance(node, ast.ClassDef): class_sym = _base_symbol(node) class_sym["kind"] = "class" for stmt in node.body: if isinstance(stmt, (ast.FunctionDef, ast.AsyncFunctionDef)): class_sym["children"].append(_func_detail(stmt)) symbols.append(class_sym) return symbols

这个实现只处理了顶层函数和类内方法,但已经覆盖了绝大多数日常场景。使用方式也简单,读入源码后直接调用 collect_symbols,把结果转成 JSON 或文本格式就能喂给 Agent:

if __name__ == "__main__": source_code = open("demo.py", encoding="utf-8").read() for sym in collect_symbols(source_code): print(sym["kind"], sym["name"], sym["start"], sym["end"])

要注意end_lineno是 Python 3.8 以后才有的属性,如果你还在维护老版本环境,需要写回退逻辑。更好的做法是遍历 AST 节点的type_ignores或直接用后续子节点的起始行减一,但大部分现代项目基本都跑在 3.10+ 上,可以直接用。

3.2 TypeScript 和更多语言的接入方式:解析器选型思路

Python 项目用内置 ast 很舒服,但现实中的代码库往往是多语言混编。Node 项目里有 TS 也有 JS,前端项目还有 Vue/Svelte 单文件组件。如果每个语言都自己写一套提取逻辑,工作量会迅速失控。

TypeScript/JavaScript 项目里,我建议直接用@typescript-eslint/parser,它是 ESTree 生态里最成熟的解析器,能处理装饰器、类型注解等语法:

import { parse } from "@typescript-eslint/parser"; export function outlineTs(source: string) { const ast = parse(source, { loc: true, range: true, ecmaVersion: "latest", sourceType: "module", range: true, }); const symbols: any[] = []; // 遍历 ast.body,遇到 FunctionDeclaration / ClassDeclaration / VariableDeclaration // 就提取 name、loc.start.line、loc.end.line return symbols; }

如果你维护的是一个超大型 monorepo,需要覆盖几十种语言,我的建议是用 tree-sitter 做统一解析层。tree-sitter 提供增量解析能力,同一套 API 就能处理 C、Java、Python、Go 等几十种语言。你可以用 query 语法把每种语言的函数节点找出来:

(function_declaration name: (identifier) @name) @func

然后把 query 命中的节点转换成统一符号结构。虽然要额外学习一点语法,但对多语言项目来说,这个成本是值得的。

3.3 把 outline 包装成 Agent 工具:函数调用协议设计

拿到 outline 之后,下一步是让 Agent 能实际调用。如果你用的是 OpenAI function calling 或类似协议,可以暴露两个工具。

第一个是get_file_outline,参数只需要文件路径,返回紧凑的符号列表:

{ "name": "get_file_outline", "description": "Get the AST symbol outline of a code file without reading full content.", "parameters": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } }

第二个是read_symbol,关键参数是 symbol_id。我给每个符号生成一个唯一 ID,格式是路径 + 起始行 + 名称,比如order_service.py:18:create_order。工具函数收到 ID 后,解析出对应的起止范围,再直接从文件里把这段源码取出来:

def read_symbol(symbol_id: str) -> str: path, start, name = parse_symbol_id(symbol_id) source_lines = read_lines_from_cache(path) symbol = lookup_symbol_in_outline(path, name, start) return "\n".join(source_lines[symbol["start"] - 1: symbol["end"]])

工具返回时最好带一行元信息,告诉模型这是哪个文件、哪个符号、真实行号范围,避免模型自己脑补。最后再拼接上源码。保持返回格式统一,Agent 解析起来才不容易出错。

4. 工程化落地中的关键取舍:粒度、缓存和多语言策略

4.1 符号粒度怎么选:类要展开到方法,超长函数要二次分块

我给 Agent 的 outline 粒度,并不是越细越好。如果每个文件都输出到变量级、每条语句都给行号,outline 本身就变成了第二个大文件,模型照样读不过来。

实际操作下来,我建议的粒度是:顶层函数和类各占一个符号,类内部要展开到方法级。原因很简单,一个 800 行的 class 如果只当成一个符号,Agent 点进去还是要读 800 行,这对定位具体方法没有任何帮助。展开到方法级后,Agent 可以直接定位到find_by_id这样的具体方法上,典型的按需读取就实现了。

那如果一个函数本身就超过 300 行怎么办?这类代码是公司老项目中极常见的历史包袱。AST 虽然能给出函数完整边界,但函数体太长,单次读取还是可能超出预算。我的经验是把超长函数内部的顶层控制流语句也做成“伪符号”,比如函数里那些独立的 if 分支、for 循环、try 块,单独列出它们的行号区间。

这一步不需要专门扩展解析器,直接在 AST 遍历时拿到 function 节点的 body,然后遍历其子语句,把IfForWhileTry这类子节点加入 children 数组就行。这样 Agent 看到的是一个带目录的函数:

- def handle_migration(data) L100-L520 - if config.dry_run L130-L180 - for item in batch: L210-L330 - try: L400-L500

Agent 如果确定当前 bug 出在批量处理逻辑里,就会选择只读 L210-L330,其他部分完全不碰。

4.2 缓存和增量解析:不能每次调用都重新 parse 一遍

Agent 在修改一个文件时往往会多次读取 outline。如果你每次都重新做一次 AST 解析,虽然 Python 解析器速度不慢,但文件多起来也会浪费不少时间。

我的做法是在工具进程内维护一个字典缓存:

outline_cache: Dict[str, tuple[float, list]] = {} def get_outline(path: str) -> list: mtime = os.path.getmtime(path) cache = outline_cache.get(path) if cache and cache[0] == mtime: return cache[1] source = open(path, "r", encoding="utf-8").read() symbols = collect_symbols(source) outline_cache[path] = (mtime, symbols) return symbols

用文件修改时间做 key,比每次无脑 parse 要稳得多。修改操作让 Agent 改完文件后,下一次读取会因为 mtime 变化而自动触发重新解析。这个过程对调用方完全透明。

如果项目文件特别大或者 Agent 会在多个文件中频繁切换,还可以考虑在后台线程里异步解析目录树,先把所有文件的顶层符号索引建好。等 Agent 需要时直接查内存索引,而不是现场解析。不过大多数场景下,单文件按需 parse 已经足够。

4.3 多语言支持:不要试图用一个正则库通吃所有文件

前面提到 tree-sitter 适合做统一解析层,但它也不是银弹。不同的语言,AST 节点的结构差异非常大,比如 Java 里 Class 和 Interface 都算类型声明,Go 里的 package 级函数和方法有不同接收者,Rust 里还要考虑宏展开的情况。

如果你只处理两三种主流语言,我建议不要过早抽象,先给每种语言单独写 parser,用统一的数据结构包装。只有当你需要覆盖的语言超过五种时,才值得引入 tree-sitter 或类似方案。接口上可以统一暴露get_file_outline(path),内部根据文件后缀分发到不同的解析器实现。这样 Agent 工具层不需要关心文件是什么语言,那部分复杂度全部被隔离了。

Python 文件还有一类特殊情况:有些文件顶层存在运行时动态代码,比如 CLI 入口在函数执行前会做一堆参数校验,这些逻辑不在函数里,但在整个文件执行流程中非常重要。如果你只提取函数和类,Agent 可能会漏掉这些入口逻辑。所以源码解析时我会对顶层那些没有包在函数里的赋值语句和 if 块也做标记,统一命名为<module>的子节点,这样 Agent 至少知道文件开头有不可忽略的操作。

5. 接入 AI Agent 后的实测结果与 Prompt 调优

5.1 Prompt 侧怎么约束 Agent:不要只给工具,要给使用规范

单纯添加 get_file_outline 工具,并不能让 Agent 自动改变行为。我在初期就试过只把工具塞进函数列表,结果模型大部分时候根本不主动调用 outline,还是直接跑去读整个文件。

后来我在系统提示词里加了一段明确约束,效果立刻不一样了:

You have a tool get_file_outline. Before reading any code file, you MUST call get_file_outline first. You must NOT call read_file / read_lines to fetch an entire file unless the file has fewer than 100 lines. When you need implementation details, call read_symbol with symbol id. Read one symbol at a time, not multiple symbols in one request.

除此之外,还有个更狠的做法:直接在工具层把整文件读取能力禁掉。在 Agent 框架里,如果当前任务只需要编辑某个函数,我根本不给 Agent 暴露read_file工具,只暴露read_symbolread_line_range。这样它就算想整文件硬啃也没有入口。这个约束比 Prompt 更硬,因为它是在能力层面限制,而不是在意图层面劝导。

5.2 实际效果和 token 消耗数据

我在一个真实案例里对比过两种读取方式。目标文件是一个 Python 单文件,2860 行,大概 90KB。处理一个新增状态字段的任务,需要改动的地方集中在两个方法里。

整文件读取模式:第一轮就要把整个文件塞入上下文,按代码字符换算大约消耗 3 万多 token。加上 Agent 后续推理和修改生成的 token,单个任务全程费用很高。更麻烦的是,由于上下文太杂,Agent 在第二三次编辑时经常搞混变量作用域,同一个任务反复修改,累计成本会进一步上涨。

使用 ast-outline 后,第一轮 Agent 只拿到约 900 token 的 outline,它通过函数签名里的字段名判断出目标位置,随后只读了一个 80 行的函数和一个 40 行的方法。整个任务所有代码阅读类 token 加在一起不到 5000 token,而且上下文里全是跟当前任务强相关的代码。

我没有做严格的对照组统计,但目测了几十个任务后,至少有两个明显趋势。一是首次定位的准确率高了不少,因为模型一开始就看到了文件的全貌,不会漏掉同名方法。二是后期修改速度更快,不会出现“早期读过的代码被后续对话冲淡”的情况。

5.3 和 LSP、代码检索方案配合起来,比单用更好

ast-outline 还有一个好处,它可以作为全局符号索引的基础。单个文件内部按符号读取解决的是“局部理解”,但 Agent 还需要“跳转理解”。比如正在处理一个 bug,原因可能是 A 文件调用了 B 文件的某个方法,而 B 文件的方法签名变了。如果只做单文件 outline,Agent 很难自动跨文件追踪调用关系。

这时候可以让 LSP 或者代码索引系统提供符号定义跳转,把“谁调用了这个符号”的结果返回给 Agent。本质上是把本地符号地图升级成全局符号图谱。我的经验是,outline 是第一步切片,第二步可以用 LSP 定位相关的远端文件,然后继续用 outline 按需读取那个文件里的具体类和方法。

如果项目里已经有 RAG 代码检索,也可以把 chunk 切分规则改成基于符号边界,而不是固定行数。每个函数单独作为一个 chunk,函数太长再按内部区间二次切分。这样 embedding 的段落在语义上是完整的,模型不会检索到半个 if 块。

6. 踩坑记录:AST 解析和 Agent 行为调试实录

6.1 常见解析问题速查

现象原因处理方式
某个文件 outline 为空文件有语法错误,AST.parse 抛异常捕获异常,回退为行数读取
函数结束行一直算不对Python 3.7 以下没有 end_lineno记录 Python 版本,回退用子节点末行
装饰器没有被展示提取 outline 时只遍历了函数节点遍历 decorator_list,用 ast.unparse 生成
同名函数/方法分不清一个类里可能存在重载或同名 casesymbol_id 里带上 start line,不用 name 做唯一值
顶层常量没有收录只处理 FunctionDef/ClassDef对 Assign/AnnAssign 也要生成符号

语法错误是最容易踩的坑。Agent 编辑过程里如果产生了临时语法错误,我们依然需要给它 outline,否则它无法理解现场全貌。更好的方式是对每个顶层代码块做 try/except,某个单独块解析失败就只标出大致行号,而不是让整个文件 outline 挂掉。

6.2 Agent 就是不按 outline 走,该怎么办

这是最让人头疼的问题。工具和提示词都给了,但 Agent 还是会用其他方式读文件,比如直接读第 1 行到第 2000 行。

试过有效的办法有三个。第一,把整文件读取工具的 max 行数改小,超过 300 行的文件直接拒绝读取,强制走 outline。第二,在 read_symbol 的返回结果开头附上“当前符号只是文件的一部分”,让 Agent 知道它没有拿到全部上下文。第三,在模型选择工具时,把 get_file_outline 排在工具列表第一个,并给一个高优先级描述。

如果这些都不生效,那基本可以判断是模型本身的多步规划能力太弱。对这类模型,只能靠外围框架兜底:你自己在执行层先调用 get_file_outline,把结果插入到 Agent 的每次请求里,代替 Agent 自主决定。这虽然牺牲了灵活性,但能保证不倒退。

6.3 动态代码、装饰器和宏展开,这些边界要小心

AST 反映的是静态语法结构,不是运行期行为。Python 里你可以在运行时用 setattr 给类动态增加方法,AST 根本看不到;C/C++ 里宏定义可能展开出一整个函数体,AST 里只有一个预处理符号。所以 ast-outline 生成的符号表不能等价于“程序实际行为图”,它只是帮助 Agent 快速定位源码位置的目录。

如果遇到很依赖动态生成的代码,我建议在工具描述里明确告诉 Agent:outline 只覆盖静态源码符号,如果怀疑运行期动态注册的符号,需要结合调用栈或日志去排查。Agent 收到这个提示后,通常不会再把“AST里没找到”当成“函数不存在”。

还有一个关于装饰器的细节。Python 中带@app.route("/order")的函数,从源码逻辑上看,真正暴露给外部的是一个 HTTP 接口。如果 outline 不显示装饰器,Agent 在修改接口路由时可能找不到注册信息。所以我在 outline 里特意加入了装饰器列表,有时候它比函数体更有价值。

最后留两个我自己常用的扩展技巧

如果你已经准备把 ast-outline 接到自己的 Agent 里,我强烈建议给 outline 加一个“签名摘要”字段,把函数的第一个注释、docstring 的第一行也一起塞进去。Agent 看到 find_by_id 前面有一句"""get order by primary key""",判断是否要读取这个符号的准确率会高很多。

另一个技巧是在 read_symbol 返回代码时,把该符号对应的路径和行号一并返回。我在实际使用时发现,Agent 只要看到“这个是 order_service.py 第 18 行到第 156 行”,就不太会在后续修改中把整个文件重写一遍,而是老老实实做局部编辑。这算是人机协作里一个很微妙的心理效应。

整体做下来,ast-outline 并不复杂,严格意义上它只解决“不要让 Agent 整文件硬啃”这一件事。但就是这一件事,把我日常调试 AI Agent 的返工率降了很大一截。后续我打算把它扩展成支持更多语言版本的独立工具,让所有 Agent 框架都能直接调用这份代码地图。

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

德玛仕商用蒸饭车选购、操作与维护全攻略

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

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

混凝土缺陷检测数据集:VOC+YOLO双格式7513张实战样本

简介&#xff1a;本资源是面向计算机视觉工程师、土木工程智能化研究者及AI模型训练初学者的混凝土表面缺陷检测专用数据集&#xff0c;旨在支撑裂缝识别、病害分类等工业质检场景下的目标检测算法开发与验证。数据集包含7513张高质量现场采集图像&#xff0c;覆盖“可见裂斑”…

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

混凝土缺陷检测数据集:7513张VOC+YOLO工业级样本

简介&#xff1a;本资源是面向计算机视觉工程师、土木工程智能化研究者及AI模型训练初学者的混凝土表面缺陷检测专用数据集&#xff0c;解决基础设施巡检中自动化识别与分类难题。数据集包含7513张高质量现场采集图像&#xff0c;覆盖“可见裂斑”“分层”“风化”“缝隙”“剥…

作者头像 李华
网站建设 2026/9/5 11:01:06

DevOps实战课程解析:Docker、Kubernetes与CI/CD工具链集成

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

作者头像 李华
网站建设 2026/9/5 11:00:41

ASTC纹理压缩编码器源码解析:从ARM架构到工程落地实践

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

作者头像 李华
网站建设 2026/9/5 10:56:58

JEREMIAD项目解析:基于哀叹机制的MLP错误学习新范式

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

作者头像 李华