ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AST还是原始代码?AI Agent代码上下文的工程化选择

AST还是原始代码?AI Agent代码上下文的工程化选择 最近在 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 什么是 ASTASTAbstract 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 个 tokenAST 表示可能只需要 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.total和discount的二元表达式。但它看不到“discount 不能发给前端”这个约束——这个信息隐藏在注释里而注释在 AST 中默认被丢弃。我在实际项目里经常遇到这类情况Agent 重构代码时把内部字段暴露到了 API 响应里就是因为它没有读注释和字符串常量。4.2 AST 丢失了“书写顺序”背后的意图源代码的顺序是有意义的。一个类里先定义哪些字段、后定义哪些方法往往反映了作者的思考顺序。在 AST 中字段和方法声明都是节点的属性顺序仍然保留但优先级不再那么明显。更重要的是代码块之间的空行、分组、注释块在 AST 中没有任何体现。这些格式信息虽然不影响编译但对人类阅读很重要对模型理解模块边界也很重要。4.3 跨语言和宏 / 预处理场景下 AST 不完整如果你在写 C/C 项目AST 在某些预处理宏展开前是无法准确生成的。Go 语言有 build tagsJava 有注解处理器这些都会在编译期改变代码结构。AST 在解析阶段看到的和真正编译执行的代码可能不完全一样。另一个常见的坑是动态语言。Python 的 AST 虽然能提取出def和class但装饰器背后的逻辑、动态生成的属性、通过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”二选一而是一个两阶段混合方案全局层AST 摘要用 AST 解析整个项目生成文件清单、类/函数清单、模块依赖。局部层原始代码片段当 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_nameNone): 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_asciiFalse, indent2))运行这段代码后你会得到一个结构索引。这个索引可以格式化后作为 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(encodingutf-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_pathPath(./sample_project/payment_service.py), start_line88, end_line120 ) 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 结构的价值在于Agent 先通过 AST 结构索引理解全局不需要读完整项目。只有当需要修改具体逻辑时才加载原始代码片段。注释和业务约束没有丢失因为它们被保留在代码片段里。这个方案在 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 models 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 消耗很高。尤其是字段名如type、name、params反复出现实际节省并不像想象中那么多。更好的做法是把 AST 蒸馏成人可读的结构摘要比如“文件 / 类 / 函数 / 调用关系”的列表。9. 一个可以抄作业的梳理清单这篇文章已经比较长了最后给一个可以快速复用的梳理清单。当你需要为 AI Agent 设计代码上下文时按这个顺序思考Agent 的任务是什么是全局导航还是局部修改全局导航阶段用 AST 或结构解析器生成结构摘要而不是原始代码。局部修改阶段把目标文件、目标函数的原始代码片段加载进来保留注释。如果上下文仍然溢出先压缩早期阶段的原始代码为摘要而不是压缩当前操作对象。给 Agent 提供“请求更多信息”的接口例如“查看文件 X 的第 Y 行到第 Z 行”。在项目根目录放一个机器可读的项目说明文件减少 Agent 自行探索的开销。这六步不依赖任何特定平台或语言可以适配到大多数场景。AST 和原始代码之间本来就不该是“非此即彼”的选择。更务实的做法是用 AST 负责地理用 Code 负责操作让 Agent 在结构地图和真实文本之间往返切换。上下文窗口有限但合理的上下文工程可以让你在有限窗口里做更多事。
返回列表