如何拦截潜在NameError:ATLAS用tree-sitter调用图推理构建结构性否决的完整指南
【免费下载链接】ATLASAdaptive Test-time Learning and Autonomous Specialization项目地址: https://gitcode.com/gh_mirrors/atlas112/ATLAS
ATLAS是一个自适应测试时学习(Adaptive Test-time Learning)的 AI 编程代理系统:它会为每个任务生成多个候选代码,再由沙箱执行、几何透镜评分,最后通过**结构性否决(structural veto)**筛掉"能跑但会崩"的候选。这套机制的核心内幕,在于它用tree-sitter 预计算调用图可达性,在代码执行之前就能拦截那些沙箱永远发现不了的潜在NameError。🔍
为什么沙箱抓不住 NameError?
沙箱回答的问题是"这段代码能不能跑",而结构性否决回答的是"这段代码的调用是否真的能解析"。两个问题并不等价:
- 代码里可能有
try/except ImportError兜底、惰性导入、或者根本没执行到的死分支——沙箱全部放行,但换个输入就会在运行时抛出NameError; - 真实的事故案例:某次编辑只导入了
render_template_string,却直接调用了render_template,编辑路径没发送项目上下文导致整个否决被跳过,带病代码以"已验证"身份落地(见 v3-service/pipeline.py 中的复盘注释)。
于是 ATLAS 引入了一条静态防线:在候选落盘之前,先问它每一个直接调用"你是谁、从哪来"。
第一步:tree-sitter 把源码拆成扁平事实
一切始于解析。v3-service/graph/extract.py 用 tree-sitter 对 Python(及 JavaScript)源码做单次 AST 遍历,把每个文件拆成五种扁平事实元组,统一装入语言无关的CodeGraph数据模型(v3-service/graph/types.py):
| 事实 | 含义 | 例子 |
|---|---|---|
defines | 定义了哪个函数/类/方法 | app.py 定义了 run_server (function, 第12行) |
calls | 谁调用了谁 | main -> render_page |
imports | 导入了什么 | from pkg.util import helper |
exports | 文件对外暴露的名字 | 顶层非方法定义 |
contains | 类包含哪些方法 | App -> handle_request |
这种"扁平事实"设计是刻意的:它既让调用图天然语言无关,又可以直接翻译成 Prolog 事实供可选求解器层消费(docs/reports/CALL_GRAPH_REASONING_V3.md 记录了完整的阶段化设计)。
第二步:跨文件导入解析,把 import 钉到真实文件
知道"导入了helper"还不够,要知道它来自哪个文件。v3-service/graph/resolve.py 做了三档匹配:
from pkg import mod→ 优先指向pkg/mod.py(代码真正所在处,而非包__init__.py);- 模块名精确匹配(
import pkg.mod/from pkg.mod import x); - 后缀匹配:裸
import mod只要恰好命中一个*/mod文件就解析成功。
解析不了的标准库 / 第三方导入保持None——这不是 bug,而是后面"保守降级"策略的伏笔。
第三步:预计算可达性——O(V+E) 的原生遍历
调用图建好后,v3-service/graph/analyses.py 提供了一组预计算的 O(V+E) 原生查询,全部是朴素 BFS / Tarjan SCC,不依赖任何求解器:
reachability(frm, to):从frm出发的 BFS 能否到达to;path(frm, to):最短调用链(修复阶段把"入口→故障函数"的真实路径喂给模型);impact(target):沿反向边 BFS,找出"改它谁都会坏"的传递调用方集合;cycles():迭代版 Tarjan 强连通分量,捕捉函数级循环调用。
一个细节值得一提:图索引用dict 模拟有序集合(analyses.py 的_build_index),因为 Python 的set迭代顺序受哈希种子影响会让path/impact结果不确定——对一个要进代理延迟预算的关键路径工具,确定性就是正确性。
第四步:调用名解析——四层判定顺序
拦截NameError的核心在 v3-service/graph/resolve_calls.py 的unresolved_calls()。对候选代码里的每个直接标识符调用,按四层顺序判定它是否"已解析":
- 本地绑定名:任意位置绑定过的名字都算数——函数参数、赋值目标、
for/with/海象运算符目标、推导式变量(v3-service/symbols.py 的_extract_python_bound_names专门为此设计,防止把cb()、x = lambda...误杀); - 内置名:
PY_BUILTINS从解释器dir(builtins)动态派生而非手工维护——任何手工子集缺一个内置名,就是一次对合法代码的假否决(比如exit(1)曾被拒); - 显式导入名:
import a.b.c只绑定顶层包a(你会调用a.b.c.x()而绝不会裸调c()); - 通配符导入:
from mod import *会解析到该模块的真实导出集合;若模块解析不到批内文件(标准库/第三方),则整轮判定宽松化(lenient=True,什么都不标记)。
⚠️严格模式的深化正在于此:一个名字哪怕在项目的其他文件里确实定义过,只要候选文件没有导入它,严格模式就会标记它——这正是旧版"项目符号任意放行"否决漏掉的真实NameError(测试见 tests/v3-service/test_graph_resolve_calls.py)。
第五步:结构性否决的裁决——保守、加法、永不空集
最后,裁决逻辑接在流水线沙箱之后(v3-service/pipeline.py):候选只要出现 ≥1 个未解析直接调用,就被打上vetoed_by: "structural",附带的 stderr 会写明"将在运行时抛出 NameError 的调用"。
整套机制遵守三条保守红线,宁可漏报、不可误杀:
- 属性调用永远豁免:
obj.foo()的接收者类型静态不可知,os.path.join绝不标记; - 不透明通配符 → 宽松模式:
from os import *后什么都知道不了,那就什么都不标; - fail-open:tree-sitter 缺失或非 UTF-8 时检查视为通过;且否决永远不会清空候选集——全部失败时降级到修复阶段重来,而不是让流水线卡死。
代理侧还通过/internal/call_resolve端点在edit_file/write_file落盘前做同解析器的编辑前门,对原始文件与编辑后文件做未解析名 diff,截断列表会破坏该对比因此返回全量结果(v3-service/main.py)。
一图看懂:从源码到否决的完整链路
候选代码 ──tree-sitter──▶ 扁平事实 (defines/calls/imports/exports/contains) │ 项目文件 ──导入解析────────▶ CodeGraph(按文件哈希缓存增量重建) │ ┌─────────────┼──────────────────┐ ▼ ▼ ▼ 可达性 BFS 最短路径/影响集 未解析调用检查 (repair 上下文) (修复与注入) (结构性否决:≥1 → 拒) │ ▼ 沙箱通过 + 结构解析通过 ⇒ 候选落盘📦 想深入源码的同学,核心文件都在这里:图引擎 v3-service/graph/、否决内核 v3-service/symbols.py、流水线接线 v3-service/pipeline.py、设计全记录 docs/reports/CALL_GRAPH_REASONING_V3.md。这套"tree-sitter 预计算可达性 + 保守裁决"的组合拳,让 ATLAS 在代码还没跑起来之前,就能精准拦下那些埋着NameError的候选——快、确定、且绝不误伤合法代码。
【免费下载链接】ATLASAdaptive Test-time Learning and Autonomous Specialization项目地址: https://gitcode.com/gh_mirrors/atlas112/ATLAS
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考