用依赖接口骨架精简 AI 编程上下文
文章对比向编程智能体提供完整依赖源码与 Tree-sitter 提取的接口骨架,探讨外围实现细节对任务准确率的影响。实验采用固定模型配置和隐藏测试集,但所给摘要未展示具体成绩。
AI 深度解读
给 AI 编程助手补上下文,整份依赖源码未必比一页接口说明更有用。作者在一个三模块结算任务中发现:完整依赖模式跑了 3 轮,只通过 17/20 个测试;只提供接口骨架的模式,首轮就通过了 20/20,累计输入 token 还减少了 94.9%。
这组结果值得看,但不能直接推导出「源码给得越少越准」。真正有用的线索是:模型需要看清当前任务的约束,而源码里的旧逻辑、内部细节和过时注释,可能正在争夺它的注意力。
两种上下文,差别不只是长短
这次实验编号为 E21,候选编号为 C017,在作者的 41 种 Agent harness 模式中排名第 2。这里的 harness,可以理解为围绕模型搭建的执行框架,负责准备上下文、调用工具、接收修改和运行检查。
任务涉及三个 Python 模块:validator.py 做输入校验,settlement.py 做结算,statement.py 生成账单。作者使用 gemini-3.8-flash,参数为 thinkingLevel: LOW、temperature: 0.0。
两种模式的区别在于怎么提供周边信息:
FullDependencyContextRunner提供完整依赖实现和原始诊断信息,包含 12 个内部辅助模块的代码。TreeSitterSkeletonContextRunner提供接口骨架,保留公开函数签名、类型和契约文档,把函数实现折叠成...。
实验运行在 Proxmox 隔离虚拟机 VM225,也叫 ai-e21-225。环境没有网卡,通过 virtio-serial QGA relay 通信,并用 systemd 区分执行身份。独立评分器 labjudge21 运行 20 个隐藏单元测试与端到端测试。
这些安排有助于限制外部访问、隔离执行和评分。不过,隔离环境本身并不能证明两种模式只差一个变量,后面还要看它们具体拿到了什么信息。
省下的 token,为什么可能换来更高正确率
完整依赖模式累计产生 78,719 个字符的工具输出,执行了 3 轮,输入 token 分别为 1,643、14,712、28,915,合计 45,270,最终通过率为 85.0%。
接口骨架模式在首轮增加了约 673 tokens 的骨架内容,总输入为 2,316 tokens,输出为 1,611 tokens,一轮通过全部测试。
按作者报告,接口骨架模式让累计输入 token 减少 94.9%,约缩小到原来的 1/19.55;API 成本从 $0.0446 降至 $0.0078,下降 82.6%;执行时间减少 57.2%,约快 2.34 倍。原文没有给出绝对耗时。
这里比较的是两次任务的累计消耗。完整依赖模式多跑了两轮,历史消息继续进入后续请求,也会推高输入量。因此,94.9% 不能直接理解成「单次请求删掉函数体就能省这么多」。
更关键的是,完整依赖中存在会干扰判断的信息:旧版 v1 注释使用 sorted(keys)、>= 300 bps 和旧分隔符 :,当前公开契约却要求保留首次出现顺序、仅在 chargeback_bps > 300 时提取准备金,并使用 # 分隔字段。
模型看到的并非单纯的冗长代码,而是夹杂着过时规则的代码。
接口骨架则把边界直接摆出来:unit_cents >= 0,允许零金额;chargeback_bps 的合法区间是 0 到 1000,包含 1000;折扣门槛 subtotal >= 15000 包含等号;抵扣上限使用 min(fee_rebate, after_discount)。账单中的折扣和抵扣即使为零,也必须写成 -0。
上下文的价值,取决于它能否帮助模型分清当前规则和历史实现。
作者用「注意力稀释」解释结果,即无关内容过多,模型遗漏了真正决定验收结果的细节。这是合理解释,但这次实验没有单独验证它。
三个失败测试,实际落在两个边界上
完整依赖模式最终并没有把整个结算流程写错。作者展示的失败集中在输入校验:一个错误条件和一次过宽的对象复制,导致了 3 个测试失败。
第一处是把 cents <= 0 当成非法输入。契约允许 unit_cents == 0,用于零金额卡片授权探测,因此应该拒绝的是负数。
第二处是用 {**item, ...} 构造返回字典。这虽然创建了新对象,却也把输入里的额外字段复制进了输出,例如未经校验的 "extra": 99。契约要求返回严格包含 5 个字段的新字典。
下面是接口骨架模式提交的对应实现,它同时处理了零金额和输出字段范围:
if cnt <= 0 or cents < 0 or not (0 <= bps <= 1000):
raise ValueError("numeric bounds violated")
out.append({
"merchant_id": mid.strip().upper(),
"currency": cur.strip().upper(),
"tx_count": cnt,
"unit_cents": cents,
"chargeback_bps": bps,
})
重点看 cents < 0,以及后面逐项列出的 5 个字段。创建新对象只解决了不修改调用方数据的问题,并不自动保证输出符合字段白名单。
这种错误很适合说明契约上下文的用途。函数名和类型签名无法告诉模型「零值合法」「额外字段必须丢弃」,这些约束必须有明确载体。
作者还提供了一个 Python 提取器,用 ast 保留签名和文档字符串,折叠函数体:
import ast
def extract_interface_skeleton(source_code: str) -> str:
"""Fold function and method bodies into '...' while preserving signatures, types, and docstrings."""
tree = ast.parse(source_code)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
# 保留已有文档字符串,把其余函数体替换为 Ellipsis (...)
has_doc = (
node.body
and isinstance(node.body[0], ast.Expr)
and isinstance(node.body[0].value, ast.Constant)
and isinstance(node.body[0].value.value, str)
)
ellipsis_stmt = ast.Expr(value=ast.Constant(value=...))
node.body = [node.body[0], ellipsis_stmt] if has_doc else [ellipsis_stmt]
return ast.unparse(tree)
要看清 has_doc 的作用:它只能保留已有文档,不会从代码中推导契约。这段实现也只替换同步、异步函数的函数体,模块级代码仍会留下;它并不是完整的公开 API 筛选器。原文建议多语言仓库使用 Tree-sitter,但没有给出对应实现。
我的判断
我认可「按任务筛选上下文」这个方向,尤其是模型只需要调用邻近模块时。相比整份源码,清楚的输入输出约定更容易帮助模型完成集成,也更容易审查。
但这次实验的证据不足以支撑「永远不要提供完整依赖源码」。
首先,骨架里写的不只是签名,还有精确到比较符号、字段数量、输出负号的业务规则。原文没有说明这些契约是仓库原有内容,还是为实验整理的。若需要额外编写,整理和维护成本也应该计入方案。
其次,完整依赖模式被加入了大量旧逻辑与冲突提示。实验结果支持「明确契约优于这组噪声上下文」,还不能证明普通团队的真实依赖源码普遍有害。两种模式连工具流程也不同,无法把收益全部归因于 AST 压缩。
最后,原文未报告重复运行次数、其他模型结果或统计波动。它是一组具体实验结果,没有给出足够对照来证明普遍规律。文末「消除 Lost-in-the-Middle 错误」的说法也偏满,单次全通过支撑不了「消除」。
与完全隐藏实现相比,我更倾向于先给接口与契约,再按问题补充实现片段。涉及副作用、异常传播或性能问题时,函数体可能正是必要证据。
能拿来做什么
- 让 Agent 修改调用方之前,先整理邻近模块的签名、参数限制、返回字段和异常约定。对 Node 或全栈项目,也可以沿用这个组织方式,但提取工具需要适配语言。
- 把零值、包含等号的阈值、顺序要求和输出格式写进文档与测试。只提供类型声明,覆盖不了这些业务边界。
- 检查上下文构建脚本是否整包输出依赖、历史注释和诊断日志。先提供相关接口,出现具体疑问后再补对应实现。
- 在自己的任务上比较完整源码、接口骨架和按需读取三种方式,同时记录通过率、轮数、累计 token 与成本,再决定是否采用。