AST还是原始代码?AI Agent代码上下文的工程化选择
2026/9/21 20:28:22 网站建设 项目流程

最近在 Hacker News 上有一个讨论值得所有做 AI 编程工具、Agent 应用或者代码分析系统的工程师关注:当 AI Agent 需要理解代码的时候,我们到底应该喂给它 AST,还是喂给它原始源代码?

这个问题的表面答案是“都行”“看场景”,但真正深入下去,你会发现它牵扯到 token 成本、上下文窗口利用率、语义保真度、工具链设计、甚至是 Agent 的推理稳定性。尤其是当你用过 Claude Code、Codex 这类编程 Agent 之后,你会明显感觉到:这些工具处理“大代码库”的能力上限,很大程度上不是模型决定的,而是上下文构造方式决定的。

这篇文章想把这个话题拆开聊透。我会先讲清楚 AST 和原始代码作为上下文的本质区别,再分析各自的优缺点和适用场景,最后给出一套我认为更符合工程实践的混合方案,并附上可运行的代码示例。如果你正在设计 Agent 工具链、写代码索引系统,或者单纯想知道为什么 Agent 经常“看不到”代码里的关键依赖关系,这篇文章值得读完。

1. 先把问题定义清楚:Agent 需要“代码上下文”到底是在解决什么

在讨论 AST 还是 Code 之前,先想清楚一个更基础的问题:AI Agent 在读取代码上下文时,它真正需要完成的任务是什么?

典型任务包括:

  • 代码检索与问答:用户问“这个项目的支付回调在哪里处理?”Agent 需要先定位到文件,再理解函数调用关系。
  • 跨文件修改:用户让 Agent 修改接口字段,Agent 需要找到接口定义、调用方、序列化层和测试用例,然后同步修改。
  • Bug 定位:用户报了一个运行时异常,Agent 需要根据堆栈信息找到对应代码,并推断出可能的根因。
  • 重构与迁移:Agent 需要理解模块边界、依赖关系、导出符号,然后执行大规模修改。

这些任务有一个共同点:Agent 必须在一个受限的上下文窗口内,最大化地理解“代码世界的结构”

很多人在第一次接触 Agent 编程时,会误以为模型的能力是瓶颈。但实际用下来你会发现,真正经常卡住的是:模型拿着 10 万行代码的检索结果,却不知道这些文件之间是怎么组织起来的。它看到了函数的定义,却看不到这个函数被谁调用;它看到了一个类的字段,却看不到这个类在依赖注入容器里是怎么注册的。

这时候就引出了核心矛盾:源代码是信息的全集,但也是噪音最多的集合;AST 是结构化的骨架,但丢掉了太多血肉。

这就像给一个分析师两份材料:一份是几百页的原始合同扫描件,一份是合同条款的结构化摘要。前者信息全但阅读成本高,后者清晰但可能遗漏细节。Agent 的上下文窗口就是分析师的工作记忆,你给它什么,它就基于什么推理。

2. AST 与 Code:概念边界和本质差异

在深入比较之前,先把概念说清楚。

2.1 什么是 AST

AST(Abstract Syntax Tree,抽象语法树)是源代码的语法结构表示。它把代码解析成一棵树,节点是语法单元,比如函数声明、变量声明、表达式、调用表达式。

举个例子,下面这段 JavaScript 代码:

function add(a, b) { return a + b; }

它的 AST(简化 JSON 形式)长这样:

{ "type": "FunctionDeclaration", "name": "add", "params": [ { "type": "Identifier", "name": "a" }, { "type": "Identifier", "name": "b" } ], "body": { "type": "BlockStatement", "body": [ { "type": "ReturnStatement", "argument": { "type": "BinaryExpression", "operator": "+", "left": { "type": "Identifier", "name": "a" }, "right": { "type": "Identifier", "name": "b" } } } ] } }

这里的关键是:AST 表达的是“代码的意图结构”,而不是“代码的书写形式”。注释、空行、格式、甚至某种程度上的命名,都被丢弃了。

2.2 什么是原始代码

原始代码就是你在编辑器里看到的文本。它包含一切:注释、字符串、格式、命名风格,甚至代码中隐含的书写顺序。

当 Agent 读取原始代码片段时,它看到的是一种“连续文本流”。模型通过注意力机制在这些文本流里寻找模式。它可以从注释里理解业务背景,从命名里猜测意图,从代码顺序里推断执行逻辑。

2.3 核心差异对比表

维度AST原始代码 Code
信息类型结构化语法信息文本与人可读信息
上下文体积小,通常为原始代码的 30%-60%大,尤其是含注释时
语义保真保留语法语义,丢失注释和格式完全保真
跨文件关系需要通过符号解析补充需要模型自行推理
适合任务静态分析、模式识别、结构检索代码修改、生成、解释
获取成本需要解析器,对部分语言支持有限零成本
人类可读性差,几乎不可读完全可读
对模型推理的友好度结构清晰但割裂上下文连续但冗长

这个对比表其实已经给出了答案的轮廓:AST 不是原始代码的替代品,而是补充品。它们的适用场景并不完全重叠。

3. 为什么 AST 是 Agent 的“低 token 高结构”武器

先讲讲为什么很多人推崇 AST。尤其在做代码检索和全局分析时,AST 的优势非常明显。

3.1 Token 效率:同样的信息量,成本差数倍

对于 GPT-4 级别的大模型,token 直接对应成本和时间。以 1M 上下文窗口的模型为例,看起来很大,但一个中型项目动辄几万行代码,每行代码平均 10-20 个 token,几万行就是几十万 token。如果把整个项目的原始文本都塞进去,很快就把窗口耗尽——这也正是热词里频繁出现的 “Context window” 和 “Context automatically compacting” 这类问题的根源。

AST 表达同样信息时体积更小。一个函数定义,源码可能是 60 个 token,AST 表示可能只需要 20-30 个 token。这省下来的空间可以容纳更多文件、更多关联代码,也可以让 Agent 在同一上下文窗口内看到更完整的调用链。

3.2 结构性:模型更容易理解“关系”

原始代码是线性的,但代码本质上是图结构。函数调用、模块依赖、类继承,这些都是“关系”。

当 Agent 读原始代码时,它需要通过文本推断这些关系。关系复杂时,模型容易漏掉隐式依赖。而 AST 天然把关系显式化:函数定义包含参数列表,函数体包含调用表达式,类声明包含方法列表。

这意味着,如果你把一个项目所有文件的 AST 组装成一个“结构摘要”,Agent 可以像查地图一样,先看到全貌,再决定深入哪一块。

3.3 噪音过滤:注释和格式反而成了干扰

原始代码里,注释占了大量 token。注释有价值,但多数注释在 Agent 做全局分析时不是核心信息。更麻烦的是,不同开发者的注释风格差异巨大,有些注释写得像散文,有些则是过期的垃圾信息。

AST 天然把这些噪音过滤掉了。Agent 拿到的是纯语法骨架,不容易被无关信息带偏。

3.4 AST 的实际应用案例

在真实工程中,AST 最常见的用法是做代码知识索引。很多代码库索引工具就是先用 AST 解析器生成结构信息(符号表、依赖图),再把结构存入向量数据库或图数据库,供 Agent 查询。

这段代码可以用 Python 和一个简单的 AST 解析库(tree-sitter)来实现:

from tree_sitter import Language, Parser # 以 JavaScript 为例,实际使用时可替换为项目对应语言的 .so 动态库 # 假设已经通过 tree_sitter 编译好 language 库 # LANGUAGE = Language('build/my-languages.so', 'javascript') # parser = Parser() # parser.set_language(LANGUAGE) def extract_functions_with_tree_sitter(source_code_bytes): """ 提取代码中的所有函数名、参数和起始行号, 生成一个结构化摘要,用于 Agent 上下文。 """ tree = parser.parse(source_code_bytes) root = tree.root_node functions = [] def walk(node): if node.type == 'function_declaration': # 在 tree-sitter 里,function_declaration 是 JS 的具名函数节点 name_node = node.child_by_field_name('name') params_node = node.child_by_field_name('parameters') start_row = node.start_point[0] + 1 # 转成人类可读行号 func_info = { 'name': source_code_bytes[name_node.start:name_node.end].decode('utf-8'), 'params': source_code_bytes[params_node.start:params_node.end].decode('utf-8'), 'start_line': start_row } functions.append(func_info) for child in node.children: walk(child) walk(root) return functions # 使用示例(伪代码,假设已有 source_bytes) # source = open('payment.js', 'rb').read() # print(extract_functions_with_tree_sitter(source))

这段代码的价值在于:它可以把一个几千行的 JavaScript 文件,压缩成一个函数清单。这个清单放进 Agent 上下文时,Agent 不需要阅读完整文件就能知道项目提供了哪些函数、每个函数的入参是什么。

4. 原始代码为什么不可替代:信息密度之外的“语义层”

如果 AST 这么好,为什么不干脆全用 AST?因为 AST 有几个致命的问题,导致它无法单独承担 Agent 上下文的全部职责。

4.1 注释和字符串里的信息是 AST 看不见的

很多业务规则,不会显式写在代码逻辑里,而是写在注释里。比如这段代码:

// 注意:这里的 discount 字段不能发给前端,只用于内部计算。 function calculatePrice(cart) { const discount = getDiscount(cart.userId); return cart.total - discount; }

如果 Agent 只读 AST,它会看到calculatePrice函数、getDiscount调用表达式、cart.totaldiscount的二元表达式。但它看不到“discount 不能发给前端”这个约束——这个信息隐藏在注释里,而注释在 AST 中默认被丢弃。

我在实际项目里经常遇到这类情况:Agent 重构代码时把内部字段暴露到了 API 响应里,就是因为它没有读注释和字符串常量。

4.2 AST 丢失了“书写顺序”背后的意图

源代码的顺序是有意义的。一个类里先定义哪些字段、后定义哪些方法,往往反映了作者的思考顺序。在 AST 中,字段和方法声明都是节点的属性,顺序仍然保留,但优先级不再那么明显。

更重要的是,代码块之间的空行、分组、注释块,在 AST 中没有任何体现。这些格式信息虽然不影响编译,但对人类阅读很重要,对模型理解模块边界也很重要。

4.3 跨语言和宏 / 预处理场景下 AST 不完整

如果你在写 C/C++ 项目,AST 在某些预处理宏展开前是无法准确生成的。Go 语言有 build tags,Java 有注解处理器,这些都会在编译期改变代码结构。AST 在解析阶段看到的,和真正编译执行的代码可能不完全一样。

另一个常见的坑是动态语言。Python 的 AST 虽然能提取出defclass,但装饰器背后的逻辑、动态生成的属性、通过getattr调用方法的模式,AST 都只能看到表面。

4.4 原始代码是模型对齐的天然格式

大模型的训练语料绝大多数是原始代码文本,而不是 AST JSON。这意味着模型对“原始代码的文本模式”有极强的理解力。你在 prompt 里放一段 Python 代码,模型可以轻松推断出意图;但如果你放一段 AST JSON,模型需要先在内部做一次“反解析”,反而增加了推理负担。

这不是说模型看不懂 AST JSON,而是说它的“原生语言”是代码文本,不是语法树结构。用原始代码给模型,就像和母语者交流;用 AST JSON 给模型,就像和对方说一种不常用的方言——能沟通,但效率未必更高。

5. 关键场景对比:什么时候该用 AST,什么时候该用 Code

这一步很重要。因为很多讨论没有落到场景里,只停留在抽象的优劣上。下面我按真实开发任务做了一组对比。

场景推荐上下文原因
全项目结构概览AST 摘要体积小,结构清晰,快速建立项目地图
检索函数定义与调用关系AST + 符号表关系显式,模型可以直接理解依赖
修改特定函数的逻辑原始代码片段需要看到注释、上下文变量、返回值语义
定位 bug 根因原始代码 + 堆栈信息bug 往往藏在注释与边界条件里
跨文件重构AST 先导 + 原始代码执行先用 AST 建立影响面,再针对具体文件读取原始代码
代码生成原始代码风格样例模型需要模仿现有代码风格和约定
数据库查询语句调优原始 SQL + schema 信息SQL 的语义和注释一样,是核心信息
依赖分析AST 依赖图图关系比文本关系更直接

从这张表能看到一个明显的趋势:Agent 的“导航”阶段适合用 AST,“操作”阶段适合用原始代码。

所谓导航,就是 Agent 先理解项目里有什么、文件之间怎么连接——这个阶段信息量要精简、结构要清晰。所谓操作,就是 Agent 准备修改某个具体函数、类或配置——这个阶段必须看到原始代码,因为修改必须基于文本进行。

6. 混合上下文方案:一个工程化的 Agent 上下文构造示例

基于上面的分析,我认为真正值得推荐的方案不是“AST 或 Code”二选一,而是一个两阶段混合方案:

  1. 全局层(AST 摘要):用 AST 解析整个项目,生成文件清单、类/函数清单、模块依赖。
  2. 局部层(原始代码片段):当 Agent 决定对某个文件/函数做操作时,才把对应的原始代码片段加载进上下文。

这个方案在工程上怎么实现?下面给一个简化的 Python 示例,演示如何“先 AST 导航,再代码聚焦”。

6.1 第一步:生成 AST 结构索引

这里用tree-sitter解析一个示例文件,并输出结构索引。

# 文件路径:agent_context/ast_index.py """ 原理: 1. 用 tree-sitter 解析所有源码文件。 2. 提取每个文件里的类名、函数名、函数参数、起始行号。 3. 汇总成结构索引 JSON,供 Agent 上下文使用。 """ from pathlib import Path from tree_sitter import Language, Parser # 需要提前构建语言库,这里以 Python 举例 # 构建方式: # git clone https://github.com/tree-sitter/tree-sitter-python # python build.py 或者按官方文档编译 .so LANGUAGE = Language('build/python.so', 'python') parser = Parser() parser.set_language(LANGUAGE) def extract_struct_from_file(file_path: Path) -> dict: source = file_path.read_bytes() tree = parser.parse(source) root = tree.root_node file_struct = { 'path': str(file_path), 'classes': [], 'functions': [] } def walk(node, class_name=None): if node.type == 'class_definition': name_node = node.child_by_field_name('name') class_name = source[name_node.start:name_node.end].decode('utf-8') file_struct['classes'].append({ 'name': class_name, 'start_line': node.start_point[0] + 1 }) elif node.type == 'function_definition': name_node = node.child_by_field_name('name') params_node = node.child_by_field_name('parameters') func_name = source[name_node.start:name_node.end].decode('utf-8') params = source[params_node.start:params_node.end].decode('utf-8') file_struct['functions'].append({ 'name': func_name, 'params': params, 'start_line': node.start_point[0] + 1, 'class': class_name }) for child in node.children: walk(child, class_name) walk(root) return file_struct def build_project_index(root_dir: Path) -> list[dict]: index = [] for file_path in root_dir.rglob('*.py'): if 'venv' in str(file_path) or '__pycache__' in str(file_path): continue index.append(extract_struct_from_file(file_path)) return index if __name__ == '__main__': project_index = build_project_index(Path('./sample_project')) import json print(json.dumps(project_index, ensure_ascii=False, indent=2))

运行这段代码后,你会得到一个结构索引。这个索引可以格式化后作为 Agent 的全局上下文。

6.2 第二步:基于索引切分原始代码片段

有了结构索引,下一步是“按需加载”。当 Agent 决定查看某个函数时,提取函数所在的行范围,把原始代码片段放入上下文。

# 文件路径:agent_context/load_snippet.py """ 原理: 根据 AST 索引中的 start_line,配合简单的大括号匹配或者行号范围, 提取对应的源代码片段。实际项目中建议在 AST 解析阶段同时记录结束行号。 """ from pathlib import Path def load_function_snippet(file_path: Path, start_line: int, end_line: int = None) -> str: """ 从文件中加载指定行范围的代码。 如果 end_line 为空,则读取从 start_line 开始的 30 行(可根据函数大小调整)。 """ lines = file_path.read_text(encoding='utf-8').splitlines() if end_line is None: end_line = min(start_line + 30, len(lines)) snippet = '\n'.join(lines[start_line - 1:end_line]) return snippet # 使用示例:假设我们从索引里知道 payment_service.py 的第 88 行是 calculate_price 函数 snippet = load_function_snippet( file_path=Path('./sample_project/payment_service.py'), start_line=88, end_line=120 ) print(snippet)

这种设计思路的核心是:索引阶段解决“代码在哪里”,加载阶段解决“代码长什么样”。

6.3 第三步:组合成 Agent Prompt

最后,把 AST 全局摘要和代码片段组合成一个完整的上下文。

你是一个资深后端工程师,负责分析下面的项目结构。 ## 项目结构索引(AST 摘要) [ { "path": "sample_project/payment_service.py", "classes": [ {"name": "PaymentService", "start_line": 10} ], "functions": [ {"name": "calculate_price", "params": "(cart, user_id)", "start_line": 88, "class": "PaymentService"}, {"name": "apply_discount", "params": "(price, user_id)", "start_line": 120, "class": "PaymentService"} ] }, { "path": "sample_project/user_service.py", "classes": [ {"name": "UserService", "start_line": 5} ], "functions": [ {"name": "get_user", "params": "(user_id)", "start_line": 18, "class": "UserService"} ] } ] ## 需要修改的具体代码片段 文件:sample_project/payment_service.py 行号:88-120 def calculate_price(cart, user_id): # 注意:discount 字段不用于对外展示 discount = get_discount(user_id) total = cart.total - discount return total def apply_discount(price, user_id): discount = get_discount(user_id) return price - discount ## 任务 请分析:如果我想在 calculate_price 中新增一个 shipping_cost 参数, 需要同步修改哪些文件和函数?请先用项目结构索引推理影响面, 再针对影响到的代码片段给出修改方案。

这种 Prompt 结构的价值在于:

  1. Agent 先通过 AST 结构索引理解全局,不需要读完整项目。
  2. 只有当需要修改具体逻辑时,才加载原始代码片段。
  3. 注释和业务约束没有丢失,因为它们被保留在代码片段里。

这个方案在 token 效率和语义保真之间取得了平衡。

7. 从 Claude Code、Codex 等工具看上下文实践趋势

回到本文开头提到的热词:Claude Code、Codex、context window、context automatically compacting。这些搜索热词反映出当下编程 Agent 工具面临的实际问题。

我并不是在评测 Claude Code 或 Codex(这部分输入材料没有实际测试依据,我不会编造具体体验),但从这些工具的公开设计思路和用户反馈中可以得出几个趋势判断:

7.1 上下文压缩机制是标配

热词里出现 “context automatically compacting” 和 “codex ran out of room in the model's context window” 这类描述,说明主流 Agent 工具都在做同一件事:上下文窗口不够用时,自动压缩早期内容。

压缩策略通常就是:把早期读取的原始代码,替换成结构化摘要(AST 层面的摘要,或人类可读的“这段代码负责什么”的总结)。这正是 AST 上下文的一种变体。

7.2 工具链正在往“索引 + 代码片段”方向演进

从公开的工程实践看,RAG(检索增强生成)类方案在代码场景下,索引阶段普遍使用 AST 或类似的结构化解析结果,而不是直接拿原始代码做向量化。原因很现实:

  • AST 可以精确映射到代码符号(类、函数、变量)。
  • AST 天然适合做符号级检索。
  • AST 与工具链(LSP、编译器)配合更紧密。

7.3 对开发者的启示

如果你在使用这些 Agent 工具时经常遇到“上下文溢出”或“代码找不准”的问题,可以考虑:

  • 在项目里增加一个AGENTS.md或类似的项目说明文件,告诉工具哪些是核心文件、哪些是生成代码、哪些跳过。
  • 把大型文件拆分成职责单一的小文件,降低单个文件的上下文体积。
  • 善用工具的项目结构映射功能,让 Agent 先看到全貌再深入。

这些操作的本质,都是“用外部手段补偿上下文窗口的有限性”。AST 思想在工具层已经无处不在。

8. 工程最佳实践与常见误区

我在设计代码检索和 Agent 上下文的实践中总结了一些经验,同时也踩过一些坑。这里直接列出最值得注意的部分。

8.1 最佳实践清单

第一,先建立“结构感知”,再做具体操作。不要一上来就把整个项目代码塞给 Agent。用 AST 解析器生成结构索引,先建立文件地图。这就像装修房子要先看户型图,而不是先看油漆配色。

第二,必要时保留注释,尤其是业务约束注释。AST 解析阶段虽然默认丢注释,但你可以选择把“高风险注释”(包含注意不能禁止仅内部等关键词)提取出来,单独放进上下文。这是一个低成本高回报的技巧。

第三,按语言特性选择 AST 方案。不是所有语言都适合用 AST。对于 C/C++,宏和预处理会让 AST 失真;对于动态语言,装饰器和动态属性会缺失。在实际方案中,要结合语言特性做取舍。

第四,控制单次加载的代码片段大小。与其一次性给 Agent 一个 500 行的函数,不如先给它函数签名和 50 行核心逻辑,让它在需要时再请求更多。这个“渐进式披露”的策略,能显著降低上下文溢出概率。

第五,给 Agent 一个“失败后回退”路径。当 Agent 无法从 AST 推断出某个关键依赖时,允许它返回“我需要看到 X 文件的完整内容”。不要强迫它基于残缺信息做出决定。

8.2 常见误区

误区一:AST 比 Code 更“高级”,所以更好。这是一个很普遍的误解。AST 只是不同的信息表示,不是更高维度的信息。它丢失内容,同时节省体积。把它当作“更好的表示”会导致关键注释信息丢失。

误区二:给 Agent 越多上下文越好。上下文越多,噪音越多。模型在长上下文里的注意力会分散,找到关键信息的概率反而下降。这被称为“大海捞针”问题。给 Agent 信息,应该像给同事信息一样:先给结论和结构,需要细节时再补充。

误区三:AST 生成很简单,直接用现成 parse 库就行。真实项目里,AST 解析的难点不在语法分析,而在工程化:如何增量更新、如何处理语法错误、如何关联注释、如何与其他符号系统(比如 LSP)对接。这些问题远比“跑通一个 parser”复杂。

误区四:把所有代码都转换成 AST JSON 塞给模型。AST JSON 体积虽然比完整代码小,但格式化的 JSON 本身 token 消耗很高。尤其是字段名(如typenameparams)反复出现,实际节省并不像想象中那么多。更好的做法是把 AST 蒸馏成人可读的结构摘要,比如“文件 / 类 / 函数 / 调用关系”的列表。

9. 一个可以抄作业的梳理清单

这篇文章已经比较长了,最后给一个可以快速复用的梳理清单。当你需要为 AI Agent 设计代码上下文时,按这个顺序思考:

  1. Agent 的任务是什么?是全局导航还是局部修改?
  2. 全局导航阶段:用 AST 或结构解析器生成结构摘要,而不是原始代码。
  3. 局部修改阶段:把目标文件、目标函数的原始代码片段加载进来,保留注释。
  4. 如果上下文仍然溢出:先压缩早期阶段的原始代码为摘要,而不是压缩当前操作对象。
  5. 给 Agent 提供“请求更多信息”的接口:例如“查看文件 X 的第 Y 行到第 Z 行”。
  6. 在项目根目录放一个机器可读的项目说明文件,减少 Agent 自行探索的开销。

这六步不依赖任何特定平台或语言,可以适配到大多数场景。

AST 和原始代码之间本来就不该是“非此即彼”的选择。更务实的做法是:用 AST 负责地理,用 Code 负责操作,让 Agent 在结构地图和真实文本之间往返切换。上下文窗口有限,但合理的上下文工程可以让你在有限窗口里做更多事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询