公司动态
AI Agent 代码上下文:AST 与原始源码对比及最佳实践
一次社区讨论里有个问题值得所有做 AI Agent 的人停下来想一下我们给 Agent 的代码上下文到底应该喂原始源码还是先解析成 AST 再喂这个问题看似抽象实际上直接决定代码审查、重构、补测试这套流程的成功率和 token 成本。现在 Claude Code、Codex、Kimi Code 这类工具越来越常用很多人直接把整个仓库塞进上下文结果要么 token 爆炸要么模型被注释和格式干扰反而抓不住结构关系。这次我们就把 AST 和原始代码这两种上下文策略放在一起对比看它们在 AI Agents 场景下各自的优势、坑和最佳实践。先说核心观点AST 不是用来替代源代码的而是用来给 Agent 提供“结构注意力”。源代码适合给模型看细节AST 适合给模型看关系。如果你正在做一个代码生成、代码重构、仓库级分析的 Agent或者你在调 Claude Code / Codex 的 prompt这篇文章会给出可直接落地的对比方法和测试模板。文章里会讲清楚如何用 Python 自带模块和 tree-sitter 生成 AST如何把 AST 序列化成 Agent 可读的上下文如何设计一组对比实验验证优劣还会给出一套批量评估脚本和常见问题排查清单。1. 核心能力速览能力项AST 上下文原始代码上下文信息组织方式树形结构突出语法层级关系线性文本保留原始格式和注释对 token 的利用效率高可剔除注释、空行、格式噪音低大量 token 花在格式和注释上对结构关系的表达能力强函数、类、调用关系一目了然弱模型需要自己推断括号和嵌套对细节和字面量的还原弱常量、字符串、注释可能丢失强所有内容原样保留适用任务重构、静态分析、跨文件依赖分析、批量代码生成精确修改、bug 修复、理解业务逻辑、生成测试用例对模型的要求需要模型理解树形文本格式对模型更友好符合训练语料习惯预处理成本需要维护解析器和序列化逻辑无直接读文件可逆性难AST 转回代码通常需要格式化生成器容易本身就是代码典型工具tree-sitter、Python ast、esprimaClaude Code、Codex、VS Code 插件、本地 IDE需要注意这个表格给的是常见情况具体效果要结合模型版本、任务类型和提示词模板来测。后面会给一套测试方法别凭感觉选。2. 适用场景与使用边界AI Agent 不是人它没有“看代码”的能力它只有一组 token。你给它的上下文格式决定了它在有限上下文窗口里能看到什么、忽略什么。2.1 AST 上下文适合什么跨文件重构比如重命名一个被多处引用的函数Agent 需要知道函数定义和被调用位置的关系AST 可以直接把调用图、继承关系列出来。静态分析类任务找出未使用的变量、循环依赖、死代码、类型不一致AST 天然适合因为这些信息就是结构信息。批量生成脚手架代码比如根据接口定义生成实现AST 能让模型只关注签名和结构不被实现细节带偏。长文件或大仓库分析如果文件超过上下文窗口AST 可以压缩掉注释和空行保留关键结构让更多内容塞进上下文。2.2 原始代码上下文适合什么精确修改一段代码改一个判断条件、调一个函数参数模型需要看到周围的上下文和注释原始代码最直接。理解业务逻辑命名、注释、字面量、SQL 语句这些信息往往隐藏在代码细节里AST 会丢掉大量这类信息。生成测试用例测试需要理解输入输出的具体值AST 难以表达运行时的数据流原始代码更靠谱。调试和 bug 定位报错信息对应的是行号和具体代码原始代码更直观。2.3 使用边界与合规提醒如果你把代码发给云端模型比如 Claude Code、Codex 或 OpenAI 接口一定要确认这些代码是否可以离开你的内部环境。涉及客户数据、密钥、未公开算法、人事信息、支付逻辑的代码要优先走本地部署模型或脱敏后再处理。再强调一次AST 不会自动脱敏字符串常量、硬编码密钥一样会被序列化出来打包前要检查。另外AST 并不能解决幻觉问题。模型仍然可能编造不存在的节点或错误的调用关系。任何 Agent 生成的代码合入仓库前都要过一遍编译、测试和人审。不能因为用了 AST 就觉得“更严谨”AST 只是上下文格式不是正确性保证。3. 环境准备与前置条件要把 AST 和 Code 作为上下文做对比不需要特别重的环境。这里给一套通用方案适用于 Python 生态如果你用 JavaScript/TypeScript可以把解析器换成 tree-sitter 或 Esprima。3.1 操作系统与语言版本操作系统Windows / macOS / Linux 均可。Python 3.9建议 3.10 以上。Node.js 16如果用 tree-sitter 的 Node 绑定。3.2 需要安装的依赖依赖用途安装命令Python 标准库ast解析 Python 源码无需安装内置tree-sitter解析多语言代码pip install tree-sittertree-sitter-pythonPython 语法树pip install tree-sitter-pythonrequests调 API 做批量测试pip install requestsopenai可选调 OpenAI 兼容接口pip install openai如果你不想折腾多语言解析先拿 Python 自带的ast模块做验证就够了。JS/TS 项目可以装babel/parser或tree-sitter-javascript。3.3 测试模型选择云端Claude Code、Codex、Kimi Code 这类 CLI 工具或 OpenAI 兼容 API。本地用 Ollama、vLLM 跑 Qwen2.5-Coder、DeepSeek-Coder 等模型适合敏感代码。建议先选一个模型跑完对比再换模型看差异。AST 上下文好不好不同模型表现可能完全不同。3.4 磁盘与网络磁盘空间本地解析不需要太多空间几十 MB 足够。本地模型另算通常需要 5GB 到 30GB。网络如果需要调云端 API确保网络畅通。本地模型不需要外网。4. 两种上下文打包方式实践这是核心部分。先看如何把源码和 AST 分别打包成 Agent 能消费的上下文文本。4.1 原始代码上下文最简单直接读文件最多加一个文件路径和语言标记。下面是一个模板from pathlib import Path def build_code_context(file_path: Path, max_chars: int 60000) - str: code file_path.read_text(encodingutf-8) if len(code) max_chars: code code[:max_chars] \n# ... [TRUNCATED] return f## File: {file_path}\npython\n{code}\n\n实际使用中可以改成把多个文件拼在一起def build_multi_code_context(file_paths: list[Path]) - str: blocks [] for fp in file_paths: blocks.append(build_code_context(fp)) return \n\n.join(blocks)这种方式实现简单模型也最熟悉。缺点是文件一大token 消耗很快而且模型容易把注意力分散到注释、空行和格式上。4.2 AST 上下文AST 的序列化方式直接影响效果。最常见的有三种ast.dump(include_attributesFalse)输出 Python 内置格式紧凑但不一定可读。自定义遍历提取Type,Name,FunctionDef,Call等关键节点删掉Load,Store这种噪音节点。用 tree-sitter 的 S-expression 输出适合多语言但噪音更多。先用 Python 标准库做一版import ast import json from pathlib import Path def simplified_ast(file_path: Path) - dict: source file_path.read_text(encodingutf-8) tree ast.parse(source) def node_to_dict(node: ast.AST) - dict: result {type: type(node).__name__} if isinstance(node, ast.Constant): result[value] node.value elif isinstance(node, ast.Name): result[id] node.id elif isinstance(node, ast.FunctionDef): result[name] node.name args [a.arg for a in node.args.args] result[args] args elif isinstance(node, ast.ClassDef): result[name] node.name result[bases] [base_to_str(b) for b in node.bases] for field, child in ast.iter_fields(node): if isinstance(child, ast.AST): result[field] node_to_dict(child) elif isinstance(child, list): result[field] [node_to_dict(x) for x in child if isinstance(x, ast.AST)] return result return node_to_dict(tree)上面这个函数只保留类型和关键字段会丢掉大部分字面量和表达式细节。用json.dumps输出模型能直接读 JSON 树。这一步之后AST 上下文变成类似下面的内容## File: example.py AST {type: Module, body: [{type: FunctionDef, name: process_order, args: [user_id, items], body: [{type: Call, func: {type: Name, id: calculate_total}}]}]}如果出现递归太深、JSON 太长的问题可以限制深度def node_to_dict_limited(node: ast.AST, depth: int 0, max_depth: int 5) - dict: if depth max_depth: return {type: type(node).__name__, truncated: True} # ... 剩余逻辑与上面一致但递归时 depth14.3 用 tree-sitter 生成多语言 AST如果项目是 JavaScript 或 TypeScript用 tree-sitter 更合适。示例from pathlib import Path from tree_sitter import Language, Parser # 需要先编译 language.so或使用预编译包 from tree_sitter_languages import get_parser parser get_parser(javascript) source Path(example.js).read_text(encodingutf-8) tree parser.parse(bytes(source, utf-8)) def walk(node, depth0): fields {} if node.is_named: for child in node.children: sub walk(child, depth 1) if sub: fields[child.type] sub return {node.type: fields} if fields else {node.type: {}} else: return {type: node.type, text: source[node.start_byte:node.end_byte]} print(walk(tree.root_node))tree-sitter 的 S-expressiontree.root_node本身也能直接字符串化但前面几层会包含大量,、.、identifier节点token 效率不高。建议做一层精简。5. 功能测试与效果验证这一步是关键。别光看 AST“看起来结构很好”要设计一组可控的实验用数据决定要不要在正式 Agent 流程里用 AST。5.1 测试任务设计选取三类典型任务任务编号任务类型示例T1重构任务把函数process_order重命名为handle_order并同步调用点T2Bug 定位任务找出divide函数可能除零的问题给出修复T3测试生成任务为calculate_total写 3 个单元测试每个任务准备 3 个不同代码文件避免偶然性。5.2 对比条件条件 A只给原始代码上下文。条件 B只给 AST 上下文。条件 C给 AST 定位到的目标函数源码混合模式后文会细讲。同一个模型同一个 prompt 前缀只改上下文部分。温度统一设置为 0输出 token 上限设置合理比如 2000。5.3 评分标准人工判分太慢可以先做自动化评价维度自动化判断方式语法正确性生成代码能否通过ast.parse或 eslint目标覆盖检查输出中是否包含指定函数名/标识符测试通过率如果生成了测试代码直接跑测试命令token 消耗统计 prompt tokens completion tokens修改准确性与正确 diff 对比或用 grep 验证旧名字是否被新名字替换5.4 批量测试脚本示例下面是一段用 Python 调 OpenAI 兼容接口做批量对比的示例。如果你用的是 Claude Code CLI也可以改成调claude -p命令但 API 方式更容易统计 token。import ast import json import time import requests from pathlib import Path API_URL http://127.0.0.1:11434/v1/chat/completions API_KEY ollama MODEL qwen2.5-coder:14b def ast_context(file_path: Path) - str: source file_path.read_text(encodingutf-8) tree ast.parse(source) # 这里可以直接用你 4.2 节定义的简化函数此处为示例 return json.dumps({file: str(file_path), ast: simplified_ast(file_path)}) def code_context(file_path: Path) - str: return f## File: {file_path}\npython\n{file_path.read_text(encodingutf-8)}\n def run_task(model, context, task_prompt): messages [ {role: system, content: You are a senior Python engineer. Follow instructions precisely.}, {role: user, content: f{context}\n\nTask: {task_prompt}\n\nOutput only the code, no explanation.} ] resp requests.post(API_URL, json{ model: model, messages: messages, temperature: 0, max_tokens: 2000 }, timeout180) data resp.json() content data[choices][0][message][content] usage data.get(usage, {}) return { output: content, prompt_tokens: usage.get(prompt_tokens), completion_tokens: usage.get(completion_tokens) } def eval_output(output: str) - dict: # 尝试解析代码判断语法是否正确 try: ast.parse(output) syntax_ok True except SyntaxError: syntax_ok False return {syntax_ok: syntax_ok} if __name__ __main__: test_file Path(sample.py) results [] for context_name, context_func in [(code, code_context), (ast, ast_context)]: ctx context_func(test_file) result run_task(MODEL, ctx, Find the bug in process_order and fix it.) result[context] context_name result.update(eval_output(result[output])) results.append(result) for r in results: print(r)运行后你会得到一个表格化的对比数据。重点看三点用 AST 上下文时模型有没有“看不懂”而开始胡编用原始代码时prompt token 是不是明显更高修复正确率有没有被 AST 丢失细节影响这个脚本是模板实际项目里要替换 API 地址、模型名、任务 prompt 和评估逻辑。如果你用 Claude Code可以在 CLI 里加--output-format json获取 token 用量。5.5 判断成功的标准如果条件 BAST在重构类任务上比条件 ACode成功率高说明结构信息确实有用。如果条件 A 在 bug 定位任务上更好说明细节信息不可丢。如果两者差距不大优先保留原始代码省掉解析复杂度。真正最优的方案通常是混合先 AST 全局再 Code 局部。6. 接口 API 与批量任务接入如果你想让 Agent 在真实项目里自动跑不能只靠一次 prompt。你需要把 AST 上下文整合到一个可批量执行的流程里。6.1 批量任务设计思路输入一个代码仓库 步骤 1. 收集改动文件列表或目标文件列表 2. 为每个文件生成两种上下文AST 索引 关键函数源码 3. 调用模型按模板生成修改建议或代码 4. 自动执行静态检查编译、lint、单测 5. 失败任务进入重试队列重试上限 2 次 6. 输出结构化结果到 JSON 文件AST 在这里更适合做“索引层”源码细节做“检索层”。比如 Agent 先通过 AST 找到process_order的定义位置再拉取该函数前后 50 行的原始代码作为填充上下文。6.2 通用 API 调用示例下面给一个封装好的函数负责把 AST 和代码片段拼进 promptdef build_context(file_ast: str, relevant_code: str) - str: return f AST INDEX {file_ast} RELEVANT SOURCE {relevant_code} .strip()然后调用模型curl -X POST http://127.0.0.1:11434/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen2.5-coder:14b, messages: [{role: user, content: Refactor process_order to handle_order. Context: [AST source]}], temperature: 0, max_tokens: 1500 }实际开发中用 Python 的requests更好方便处理批量任务和重试逻辑。下面的代码演示了一个带重试的批量评估器import time from concurrent.futures import ThreadPoolExecutor def call_model_with_retry(model, messages, max_retries2): for attempt in range(max_retries 1): try: resp requests.post(API_URL, json{ model: model, messages: messages, temperature: 0, max_tokens: 2000 }, timeout120) data resp.json() if choices in data: return data except Exception as e: if attempt max_retries: raise e time.sleep(5 * (attempt 1)) def process_files(file_paths): tasks [] for fp in file_paths: ast_str simplified_ast(fp) code_str fp.read_text(encodingutf-8) ctx build_context(ast_str, code_str) tasks.append(ctx) with ThreadPoolExecutor(max_workers4) as executor: futures [] for ctx in tasks: messages [{role: user, content: f{ctx}\n\nGenerate unit tests for this file.}] futures.append(executor.submit(call_model_with_retry, MODEL, messages)) results [f.result() for f in futures] return results6.3 批量任务中的失败重试与日志批量任务最容易出现两类问题单次请求超时、输出不符合 JSON 格式。建议在每条任务里增加def safe_json_parse(text: str): start text.find({) end text.rfind(}) if start -1 or end -1: return None try: return json.loads(text[start:end1]) except json.JSONDecodeError: return None日志要记录任务 ID、文件路径、上下文类型、token 数、耗时、输出前 200 字符。这样后面分析为什么某个任务失败时才不会变成黑盒。7. 资源占用与性能观察很多 AI Agent 项目遇到的问题是“速度慢”或“token 超限”这跟上下文格式直接相关。7.1 token 消耗对比同一份 1000 行 Python 源码原始代码上下文大概 1.5 万到 2.5 万个 token取决于注释密度和字符串长度。精简 AST 上下文可以压到 3000 到 6000 个 token因为它去掉了注释、空行、括号、缩进和大部分字面量。用 API 的usage.prompt_tokens字段可以精确统计。本地模型则看每次请求的输入长度。7.2 预处理时间解析一个 1000 行文件Pythonast.parse通常耗时在几十毫秒到几百毫秒之间。tree-sitter 也类似。这部分开销比模型推理时间小得多基本可以忽略。但如果仓库有上万个文件建议用缓存只在文件变更时重新生成 AST。7.3 显存和内存如果你用本地模型上下文长度越长KV cache 占用越高。AST 压缩上下文后同等显存可以处理更大的仓库。但显存占用不是由 AST 直接决定的而是由模型参数量和上下文 token 数决定。一个 7B 模型在 4096 上下文下大约需要 6GB 到 8GB 显存14B 量化模型通常需要 10GB 到 16GB具体看量化格式。如果你调云端 API则不需要关心显存但 token 费用和上下文窗口限制照样要关注。7.4 如何观察性能写一个小脚本在每次请求前后打印def log_performance(task_name, start_time, usage): elapsed time.time() - start_time print(f[{task_name}] elapsed{elapsed:.2f}s prompt_tokens{usage.get(prompt_tokens)} completion_tokens{usage.get(completion_tokens)})在一批任务结束后汇总平均值和最大值比较 Code 上下文和 AST 上下文的差距。注意模型输出长度也受任务复杂度影响不要只比较总耗时要看“有效 token / 正确结果”的比值。8. 常见问题与排查方法问题现象可能原因排查方式解决方案AST 解析报错源码语法不完整或解析器不支持某些版本特性打印报错行号检查源码能否通过编译器对不完整文件做容错处理截取可解析片段或用ast.parse(..., type_commentsTrue)适配类型注释AST 上下文过长递归没做深度限制节点太多检查 JSON 字符串长度限制深度、过滤叶子节点、只保留函数/类/调用关系模型输出“这段 AST 看不懂”模型没有见过 compact JSON 树格式换一种序列化格式或在上下文里加一级示例提供 1 到 2 行 AST 小例子让模型模仿丢失注释和字符串后任务失败任务本身依赖字面量或文档字符串检查任务类型是否确实是“结构型”任务改用混合上下文AST 目标函数源码AST 转回代码后格式错误AST 本身不含原始格式信息或生成器输出不规范对比生成代码和原代码 diff让模型只输出修改 diff而不是整段重写批量任务请求超时单次 prompt 太长或模型并发太多查看 API 日志逐个请求测试缩短上下文降低并发数加超时重试token 超限文件太大上下文塞太多打印实际 token 数先按 AST 索引定位文件再按函数片段拉取源码云端 API 返回 401 / 403API Key 缺失或没有权限检查环境变量API_KEY、账号配额重新配置密钥确认所在区域是否支持服务必要时切换到本地模型如果 API 返回错误包含unsupported_country_region_territory之类信息说明当前网络区域不受支持不要尝试绕过直接换成本地模型或合规的云端服务。9. 最佳实践与使用建议从我的经验看AST 上下文直接替代源码的收益有限但把它当“导航器”很划算。建议使用下面这套混合策略9.1 两层上下文策略第一层是 AST 索引覆盖整个仓库或目标文件夹让 Agent 知道有哪些文件、类、函数、依赖关系。第二层是“按需源码”当 Agent 要修改某个函数时再取该函数前后若干行源码拼接进上下文。第一层全局 - 目录结构 - 每个文件的 AST 摘要函数名、参数、返回类型、类名、调用关系 第二层局部 - 目标函数完整源码 - 目标函数直接调用的其他函数签名 - 相关测试文件片段这种做法的好处是Agent 先看地图再走进具体街道。不会因为全量文件太大而超限也不会因为只看 AST 而丢失实现细节。9.2 提示词模板建议给一个经过整理的可复用模板You are helping modify a Python repository. Repository structure: {file_tree} AST index: {ast_index} The function we want to modify is: {target_function_code} It is called from: {call_sites} Now perform the following task: {task_instruction} Keep the output as a unified diff.注意把{ast_index}控制在一个合理长度内比如 2000 token 以内。目标函数源码控制在 1500 token 以内整体 prompt 不超过模型上下文的一半留出足够空间给输出和重试。9.3 工程化建议第一次先小参数测试。不要一上来就喂整个仓库先用 3 到 5 个文件跑通。保留一套最小可运行配置。把解析、上下文打包、模型调用、评估写成一个 Python 脚本所有路径用参数化。模型文件、输入素材、输出结果分目录管理。AST 缓存单独放.cache/生成结果放generated/。批量任务要加日志和失败重试。没有日志的批量任务失败后没法复盘。接口服务要限制访问范围。如果封装成 HTTP 服务只绑定127.0.0.1不要直接暴露到公网。涉及人脸、声音、版权素材时必须确认授权。虽然这和 AST 关系不大但只要你的 Agent 处理的是他人代码或素材就得确认授权。代码的许可证、数据隔离、敏感信息脱敏都要过一遍。发布或商用前要做效果复核。AST 生成的代码可能看起来逻辑完整但在边界条件上出错。跑单测、做 code review 再合入。9.4 多语言处理AST 上下文并不仅限于 Python。JavaScript/TypeScript 用 tree-sitterJava 可以用 JavaParserGo 有go/ast标准库。关键点是统一序列化成 JSON 或 S-expression不要给每种语言设计不同的 prompt 格式这样会增加维护成本。建议把一个文件抽象成“AST 摘要 头部注释 函数签名列表”这样跨语言时模型更容易理解。10. 总结与下一步AST 和原始代码不是二选一而是上下文中“结构”和“细节”的两种表达。对于 AI Agents 场景AST 更适合做索引、导航、依赖分析和重构任务原始代码更适合做精确修改、bug 修复和测试生成。真正稳妥的方案是两层混合先 AST 看清全局再源码聚焦局部。如果你正在用 Claude Code、Codex 或自建 Agent 处理代码建议先做这件事写一个脚本把目标仓库的关键文件生成 AST 摘要然后把你最常见的任务分别跑一遍 Code 上下文和 AST 上下文记录成功率和 token 消耗。这个实验成本不高但对后续 Agent 的效果提升很有帮助。最容易踩的坑是“只给 AST 不源码”导致模型在细节上翻车反过来“全量源码”又容易超 token。掌握了混合上下文你的 Agent 才能在真实仓库里稳定工作。建议收藏备用下次调代码 Agent 时直接套用这套对比流程。