RH
RouterHub AI API 中转站导航与平台推荐
博客

ADK 文件读取为何不再吃满内存

ADK 2.14.0 为 ReadFileTool 增加分块读取路径,但自定义环境仍需覆写 read_file_stream 才能避开整文件缓冲。

muchacha 2026-11-11 02:29:22

正文

先说结论

Google ADK 2.14.0 把执行环境的文件读取从单一的 read_file 扩展为可选的 read_file_stream。对使用内置 LocalEnvironment、E2B 或 Daytona 环境的项目,ReadFileTool 现在可以按块扫描文件,只保留请求的行范围,并在输出受限时继续统计文件总行数。调用方仍然传 path、start_line 和 end_line,所以普通 Agent 配置不需要改;真正需要检查的是自定义环境是否实现了新的流式方法。

这不是“所有环境都自动变成流式”的版本升级。BaseEnvironment.read_file_stream 的默认实现只是把旧的 read_file 结果作为一个块返回,目的是保持已有环境类可以继续实例化。换句话说,升级后接口兼容性变好,但内存行为是否改善,取决于具体环境有没有覆写这个方法。要核对自己的模型入口和框架版本,可以先查看模型与平台目录;如果项目此前处理过 ADK 遥测字段,也可以对照升级 ADK 后,旧的 call_llm 看板为何失灵,两者都属于版本升级后需要同步检查实现边界的场景。

2.14.0 具体改了什么

ADK 2.14.0 的发布说明把执行环境从实验能力提升为稳定能力,并列出了 BaseEnvironment、LocalEnvironment、EnvironmentToolset、DaytonaEnvironment 和 E2BEnvironment。与 ReadFileTool 直接相关的改动有三层。

第一层是接口。BaseEnvironment 现在拥有 execute、read_file、read_file_stream 和 write_file 四个工作方法。前三个文件相关动作仍然以工作目录为边界,调用方不需要把文件对象、文件描述符或远端容器细节暴露给工具。read_file_stream 返回的是连续的字节块,而不是已经拆好的行,因此行边界可能跨越两个块,消费方必须自己处理跨块拼接。

第二层是默认行为。基类中的 read_file_stream 会调用旧的 read_file,然后只产出一个字节块。这样做解决的是升级风险,不是大文件内存问题:只实现旧四个抽象成员的自定义环境仍然能工作,但它的单次读取仍会先把整个文件放进内存。官方指南也明确把覆写流式方法作为大文件场景的实现责任,并要求缺失文件继续抛出 FileNotFoundError。

第三层是工具实现。ReadFileTool.run_async 不再直接等待整个 read_file 结果,而是通过异步上下文消费 read_file_stream。内部扫描器维护当前行号、请求范围、已保留字节数和跨块的行缓冲。它会保留 start_line 到 end_line 范围内的内容,范围之外的内容只用于继续计数,不会加入返回结果。即使目标范围很小,工具仍然可以告诉模型文件共有多少行,这也是它没有在读到 end_line 后立即停止的原因。

ReadFileTool 的行为边界

行号仍然是 1-based

start_line 和 end_line 仍按 1 起始计数。未传 start_line 时从第 1 行开始,传入 0 或负数会被归一到第 1 行;end_line=0 仍表示读到文件末尾。若 start_line 超过文件总行数,工具返回错误并附带 total_lines;若起止范围颠倒,也会返回错误,而不是静默交换两个参数。

行边界的定义也变得更明确:实现只按换行符 \n 分行,单独的回车符 \r 不会产生新行。读取 Windows 文本或混合换行文件时,不要把“显示出来像两行”当成 ADK 一定会返回两行。工具先处理字节,再以 UTF-8 解码;无法解码的字节会使用替换字符,因此调用方拿到的是可展示的文本,不是原始二进制内容。

输出限制不等于文件被截断

ReadFileTool 有 max_output_chars 参数,默认上限来自环境工具常量,当前版本是 30,000 个字符。实现按该上限的四倍估算字节预算,并额外读取一个字节来判断是否需要截断;这不是一个精确的“字符数等于字节数”模型,中文和其他多字节 UTF-8 内容会更早触发边界。

当请求结果超过输出预算时,返回内容会带有截断标记,并给出文件的总行数。这里要区分两件事:输出被截断,不代表文件只被读到截断位置。为了得到 total_lines,扫描器仍会处理后续块,但不会继续保存超出预算的内容。对大日志来说,这意味着模型可以请求一个局部范围,同时知道范围是否落在文件尾部;对自定义环境来说,这也意味着流实现必须能够稳定地产出直到文件结束。

长单行是一个容易忽略的边界。因为实现必须把一行拼完整后才能决定是否加入结果,单行本身会占用行缓冲;不过工具仍把总保留字节控制在预算附近,并对超长内容返回截断结果。不要把 start_line 和 end_line 当成任意字节区间,它们只能按换行分隔的逻辑行工作。

内置环境和自定义环境有什么不同

LocalEnvironment 是真正的分块读取

ADK 2.14.0 的 LocalEnvironment.read_file_stream 先复用工作目录路径检查,再通过线程读取文件,每次读取 64 KiB,直到文件结束,并在异步生成器关闭时关闭文件句柄。ReadFileTool 因而可以在事件循环之外执行阻塞文件读取,同时把块交给异步扫描器。

路径检查仍然只约束文件方法。read_file、read_file_stream 和 write_file 会把路径解析到工作目录内,阻止通过 .. 越出目录;但 LocalEnvironment.execute 运行的是本机用户权限下的 shell 命令,能否访问工作目录以外的文件、网络和环境变量,不会因为文件读取改成流式而改变。流式读取解决的是缓冲方式,不是沙箱问题。

E2B、Daytona 和其他环境不能混为一谈

发布说明和实现提交明确列出 E2B 与 Daytona 的流式路径。其他环境如果没有覆写 read_file_stream,就使用基类的一块式回退。提交说明特别指出,OpenSandbox、Agent Engine 和自定义环境不能仅凭“升级到了 2.14.0”就推断为流式读取;它们是否分块,需要查看各自版本的实现。

这对多环境部署很重要:本地开发时使用 LocalEnvironment,测试时换成自定义内存环境,生产时再切到远端沙箱,三者可能有不同的峰值内存行为。不要只在本地用小文件验证 ReadFileTool,应对每个实现分别确认 read_file_stream 是否被调用,并确认缺失文件、路径越界和跨块行都能得到一致结果。

自定义环境的迁移方式

如果你的环境类已经实现 read_file,最小迁移不需要修改 Agent 声明。先保持旧方法不变,再增加一个异步生成器,让它按块产出同一文件的原始字节:

from collections.abc import AsyncGenerator
from pathlib import Path

class RemoteEnvironment(BaseEnvironment):
    async def read_file_stream(
        self, path: str | Path
    ) -> AsyncGenerator[bytes, None]:
        async with self.client.open_stream(self._resolve_path(path)) as stream:
            async for chunk in stream:
                yield chunk

这个示例只表达接口契约,open_stream 和 _resolve_path 必须替换成你的远端文件系统 API。实现时需要固定四个条件。

第一,输出必须是 bytes 块,不能直接产出字符串。第二,块之间必须保持文件原始顺序,不能因为远端分片并发下载而改变顺序。第三,文件不存在时应抛出 FileNotFoundError,不要把空文件和缺失文件都返回为空。第四,生成器被提前关闭时要释放 HTTP 响应、对象存储句柄或远端会话;ReadFileTool 使用异步关闭上下文处理这个生命周期。

如果暂时无法实现真正的远端流式接口,可以先不覆写 read_file_stream,依靠基类回退保持功能正确。但这时要把它视为兼容迁移,不要把它写成“大文件已解决”。建议为自定义环境补三组测试:让一个换行符横跨两个块,确认行内容不重复也不丢失;让请求范围很小但文件尾部很长,确认 total_lines 仍准确;让流在中途抛错,确认工具返回错误且生成器已关闭。ADK 官方测试已经覆盖了这些边界,直接按照同样的断言思路扩展比只测一份小文本更可靠。

升级前后的验收清单

先把依赖固定到明确的 ADK 版本,再检查环境实现来源。不要只看 pip show 的版本号,还要确认运行时加载的 BaseEnvironment 和 ReadFileTool 来自同一套安装。若应用通过多个 Python 环境启动,最简单的核验方式是在启动日志中打印包版本和模块路径。

然后做一个最小行为测试:创建跨越多个读取块的文本,调用 ReadFileTool 请求中间几行,检查返回的行号、内容和 total_lines。再测试一个超出输出预算的文件,检查截断标记是否存在,并确认工具没有把完整文件内容塞进模型上下文。最后测试 ../../ 路径和通过 Execute 读取工作目录外文件的情况,分别验证文件方法的路径保护与本地 shell 的非沙箱边界。

如果你的服务在网关后面运行,还要把环境行为和 API 兼容性分开验收。模型能否发出 ReadFile 工具调用,只说明上游协议接受工具声明;网关是否保留参数、是否允许长时间流、是否正确转发工具错误,是另一层问题。可先在模型与平台目录确认实际接入端点,再用一次只读请求核对 path、start_line、end_line 是否原样到达,不要用普通文本请求成功来替代工具链测试。

这次升级不该被误读成什么

它不是把 Agent 变成文件搜索引擎,也不是为 Execute 增加了输出流。read_file_stream 只改变文件读取工具的输入方式,execute 仍然等命令结束后返回缓冲的标准输出和标准错误。它也没有自动为任意第三方环境增加远端分块能力;自定义实现仍要自己处理范围检查、文件句柄、异常和取消。

更准确的理解是:ADK 把“如何取得文件字节”从工具逻辑中抽成了一个可选的异步流接口,内置环境利用这个接口降低了 ReadFileTool 为了读取局部行范围而整文件缓冲的风险,同时保留了旧环境的回退路径。开发者真正要做的不是重写每个 Agent,而是检查环境实现,补跨块和大文件测试,并在需要安全隔离时使用真正的远端沙箱,而不是把流式读取当成权限边界。

参考来源