人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用【免费下载链接】KilnBuild, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.项目地址https://gitcode.com/gh_mirrors/ki/Kiln点击查看免费下载Pytest 每次调用都会导入根conftest.py而 Kiln 的根 conftest 曾在模块作用域直接import litellm——这个导入要花费数秒导致即使没有任何测试触碰 litellm每次运行尤其是 Agent 最常做的“改一个测试文件、跑一次”的内循环都要白白支付这笔成本。本文基于 Kiln 云沙箱开发者体验项目Cloud Sandbox Developer Experience的 Phase 3 阶段计划完整讲清这次改动的动机、四步实施方式、安全性论证与验证手段读完你可以掌握如何用sys.modules守卫把昂贵第三方库的导入从“每次进程启动必付”降级为“按需才付”同时保证测试通过/跳过数不变、日志配置不丢。一、问题背景最常用 Agent 操作被 litellm 导入拖慢Kiln 的测试入口是仓库根目录的 conftest.py。Pytest 的加载顺序是先导入根 conftest再收集import各个测试模块。原实现的顶部有两条模块级导入import litellm from kiln_ai.datamodel.basemodel import KilnAttachmentModel问题在于import litellm的真实成本。Phase 3 文档phase_3.md将其记为“several seconds”而项目的研究阶段research_findings.md §11给出了更精确的拆解冷启动下import litellm约4.0 s该机器实测每个 pytest 进程各付一次配合-n auto并行跑测试时每个 xdist worker 都要付一次其中约 0.6 s 是导入时向raw.githubusercontent.com拉取 model cost map 的网络请求litellm 的固有行为剩下的 ~3.2 s 是纯粹的导入工作——共2,148 个模块最贵的是litellm.proxy._types0.431 s4,834 行 pydantic 模型被secret_managers/*为了几个枚举间接拖进来连带拖入 fastapi。于是出现一个很典型的“固定开销”针对单个小测试文件的定向运行墙钟时间里跑 0.14 s 的测试却要花 6.1 s 的固定成本。这正是 Agent 迭代循环研究编号 F6每一步都要交的税。二、修复思路把导入从“必然发生”变成“按需发生”Phase 3 的核心决策是不做任何 mock 或桩只把 litellm 的导入时机推迟——autouse fixture 在sys.modules里查找 litellm只有当某个测试模块已经导入过它时才执行 litellm 相关的 setup否则直接返回。这里的关键是安全性论证Phase 3 文档明确指出了该改动中“最危险的失败模式”最危险的失败模式是静默地少收集/少运行测试所以 pass 和 skip 计数才是首要断言。跳过逻辑之所以安全是因为跳过的条件与“没有需要清理的状态”严格等价缓存刷新不会被错误跳过。原 autouse fixture 的职责之一是litellm.in_memory_llm_clients_cache.flush_cache()清空 litellm 的 in-memory HTTP 客户端缓存避免测试间状态泄漏。新实现只在litellm不在sys.modules时跳过该刷新而“不在sys.modules”就意味着没有任何代码导入过 litellm自然也不存在已缓存的 HTTP 客户端可供刷新更不存在可泄漏的跨测试状态。守卫条件与跳过条件完全同构不存在“该刷没刷”的空隙。导入顺序保证守卫有效。pytest 先导入根 conftest、再收集测试模块任何使用 litellm 的测试模块都会在自己的模块作用域导入它。因此当该模块的测试真正运行时sys.modules里必然已有 litellm——守卫不会在需要时误判。代价是有意的、被显式记录的。一次从未导入 litellm 的完整运行也永远不会配置 litellm 日志LiteLLMlogger 保持默认级别ModelCallslogger 拿不到 handler。Phase 3 文档写明“Nothing depends on that today”今天没有任何东西依赖它并把这个代价写进了 fixture 的注释里当前 conftest.py 的注释完整保留了两类不受影响的未配置场景完全没导入 litellm 的运行、以及在测试体中途才导入 litellm 的测试。三、实施四步步骤 1清理模块顶部导入删除import litellm和from kiln_ai.datamodel.basemodel import KilnAttachmentModel新增import sys并把from typing import Callable换成from typing import TYPE_CHECKING, Callabledatamodel 的导入移到if TYPE_CHECKING:之下——这样类型检查器仍能解析注解运行时不再触发该导入链。当前 conftest.py 的头部正是这个形态import os import shutil import sys import uuid from pathlib import Path from typing import TYPE_CHECKING, Callable from unittest.mock import patch import pytest from dotenv import load_dotenv from kiln_ai.pytest_mock_files import MockFileFactoryMimeType from kiln_ai.pytest_test_output import make_test_output_dir from kiln_ai.utils.config import Config if TYPE_CHECKING: from kiln_ai.datamodel.basemodel import KilnAttachmentModel注意KilnAttachmentModel留在顶部注解位置是不行的kiln_ai.datamodel.basemodel的导入链会间接拉起重依赖TYPE_CHECKING守卫让它在运行时彻底消失。步骤 2重写 autouse fixture 为_litellm_per_test_setup原 fixture旧名_clear_httpx_clients只清 httpx 客户端被整体重写为 _litellm_per_test_setup。两个命名上的细节在 Phase 3 文档中被专门强调改名因为它不再只是清 httpx 客户端而是承担整个 litellm 侧的 per-test setup函数体没有yield因此“teardown”根本不属于它的行为——fixture 就是“配置一次、刷新一次”不存在清理阶段。仓库中的最终实现含说明守卫代价与顺序依赖的完整注释pytest.fixture(autouseTrue) def _litellm_per_test_setup() - None: # Importing litellm costs several seconds, and pytest imports this conftest on # every invocation. Most test modules never touch litellm, so this only runs # once something else has already imported it. Skipping it is safe: with no # litellm import there are no cached HTTP clients to flush. # ...注释还记录了日志配置的两个未覆盖场景见上文第二节 litellm sys.modules.get(litellm) if litellm is None: return from kiln_ai.utils.logging import setup_litellm_logging # Idempotent: returns immediately once a CustomLiteLLMLogger is registered. setup_litellm_logging(test_model_calls.log) litellm.in_memory_llm_clients_cache.flush_cache()setup_litellm_logging的 import 放在守卫之后函数体内这是整个方案“惰性”的关键没导入过 litellm 的运行连kiln_ai.utils.logging都不会被导入——而该模块自身在模块顶部就有import litellm见 logging.py。步骤 3删除 session 级setup_test_loggingfixture原 conftest 有一个 session 作用域的 autousesetup_test_loggingfixture 负责配置 litellm 日志它的逻辑现在已被上面的守卫路径吸收setup_litellm_logging(test_model_calls.log)逐测试调用因此整个 fixture 被删除。这样做的依据是幂等性。查看 libs/core/kiln_ai/utils/logging.py 的实现def setup_litellm_logging(filename: str model_calls.log): for callback in litellm.callbacks or []: if isinstance(callback, CustomLiteLLMLogger): return ... litellm.callbacks [CustomLiteLLMLogger(model_calls_logger)]函数开头就扫描litellm.callbacks一旦已存在CustomLiteLLMLogger立即返回。所以“从 session 级每 session 调一次”改成“function 级每测试调一次”的开销只是首个测试之后的一次短列表扫描可以忽略。完整配置内容是把默认LiteLLMlogger 压到ERROR级别为ModelCallslogger 挂一个RotatingFileHandler5 MB、保留 3 个备份、propagate False只写文件并注册CustomLiteLLMLogger回调打印请求 curl、格式化输入与模型响应。顺带说明一个由日志路径实现决定、Phase 3 文档专门验证过的点get_log_file_path 拼接的路径是Config.settings_dir()/logs/filename可被环境变量KILN_LOG_FILE覆盖而 Config.settings_dir 是直接从Path.home()推导~/.kiln_ai的——这一点在第五节的日志路径探针中会再出现。步骤 4mock_attachment_factory的注解引号化与本地导入fixture mock_attachment_factory 内部工厂函数的返回注解改为字符串形式导入移到函数体内def create_attachment( mime_type: MockFileFactoryMimeType, text: str | None None, ) - KilnAttachmentModel: from kiln_ai.datamodel.basemodel import KilnAttachmentModel if text is None: path mock_file_factory(mime_type) return KilnAttachmentModel.from_file(path) return KilnAttachmentModel.from_data(text, mime_type)引号注解配合TYPE_CHECKING块里的导入让注解对类型检查器仍然可解析而运行时KilnAttachmentModel只在真正调用create_attachment时才导入。导入顺序为什么始终成立Phase 3 文档把这条不变式单列出来pytest 在收集测试模块之前导入根 conftest而任何使用 litellm 的模块都在自己的模块作用域导入 litellm所以模块的测试执行时sys.modules里必有它。换句话说守卫的求值点每个测试的 fixture setup 时刻晚于所有模块级导入不存在“模块导入了 litellm 但守卫看不见”的时序窗口。四、验证一pass/skip 计数与运行耗时Phase 3 明确声明没有新增单元测试——这次改动的是 fixture 管道现有测试套件本身就是断言。验证分三层1. 全量套件计数不变。命令为uv run python3 -m pytest --benchmark-quiet -q -n auto .报告6369 passed, 10020 skipped, 0 errors与改动前完全一致。任一计数下降都意味着测试被静默跳过这是本改动的第一道防线。研究阶段的对照数据research_findings.md §10显示全套件墙钟从 63.3 s 降到 58.9 s-n auto4 workers收集到的测试数保持 16,389。2. 单文件内循环耗时。Phase 3 文档给出的本机数据是 libs/core/kiln_ai/utils/test_config.py42 个测试三次重复1.93 / 1.93 / 1.93 s对照临时恢复模块级import litellm后在同一台机器测得的 11.72 / 11.31 / 11.20 s。文档同时强调把它当作“单点测量”而非可引用的倍数——warm 机器上的比例会小得多因为 litellm 首次导入还包含拉取 model cost map 的网络开销。决策依据的 ~7× 来自研究机器上的 7.40/7.73/6.86 s → 0.96/0.91/2.03 s而后续跨机器实测倍数最低落到 ~2.3×项目总览 project_overview.md 决策 6 专门记录了这一点“方向与计数一致性在所有机器上复现结论不变但倍数别外推”。3. 全仓检查绿。uv run ./checks.sh --agent-mode通过。五、验证二日志路径探针与一个“记录而未修复”的顺序依赖这是 Phase 3 中技术含量最高、也最值得单独讲的一节。5.1 为什么日志路径需要专门验证改动前litellm 日志在 session 级 fixture 里配置一次日志文件路径在会话早期解析改动后setup_litellm_logging(test_model_calls.log)是在某个测试执行期间才第一次真正执行路径解析。而根 conftest 的 autouse fixture use_temp_settings_dir 会 patchConfig.settings_path指向tmp_path意图是测试不污染用户真实的 settings。担忧随之而来惰性化之后解析到的日志路径会不会跟着被 patch 进tmp_path让每个 worker 在临时目录里各写一份探针测试的结论是不会原因是这两个方法不是一回事use_temp_settings_dirpatch 的是Config.settings_path返回settings.yaml路径日志路径走 get_log_file_path它调用的是 Config.settings_dir——一个未被 patch 的 classmethod路径由Path.home()拼出。探针的实现方式是写一个导入 litellm 的测试模块检查ModelCallslogger 上 handler 的baseFilename确认仍为~/.kiln_ai/logs/test_model_calls.log与改动前一致同时断言litellm.callbacks中确实注册了CustomLiteLLMLogger证明用到 litellm 的运行里日志依然被正确配置。因此不需要额外的 session 级兜底 fixture。5.2 记录而未修复路径解析变成“先到先得”Phase 3 文档诚实地记录了一个没有修复、只做了显式注释的隐患把路径解析从 session 级挪到函数级之后它变成顺序依赖的。仓库里有两个测试模块通过自己的 autouse fixture patch 了Config.settings_dir本体到tmp_pathtest_lancedb_adapter.pypytest.fixture(autouseTrue) def patch_settings_dir(tmp_path): with patch(kiln_ai.utils.config.Config.settings_dir, return_valuetmp_path): yieldtest_vector_store_registry.py同形。由于setup_litellm_logging是幂等的“第一个跑到 litellm 测试”的模块赢下整个 worker 的日志路径。今天这是正确的因为pytest 实例化 autouse fixture 时根 conftest 的 fixture 先于模块级 fixture——根 conftest 的_litellm_per_test_setup会先用真实路径完成日志配置幂等短路此后模块级patch_settings_dir的tmp_path就再也写不进 handler 了。但文档强调这是 pytest 的文档化属性而非偶然却同时是“承重且不明显”load-bearing and unobvious的——因此它被写进了 fixture 的注释conftest.py 最后一段注释和项目架构文档 architecture.md 第 4 节警告任何后续修改 fixture 作用域的人都应重新核查这一依赖。六、要点总结设计点做法为什么成立惰性导入守卫sys.modules.get(litellm)缺失即 return未导入即无缓存客户端可刷跳过条件与“无状态可泄漏”严格等价首要断言全量套件 pass/skip 计数不变6369 / 10020 / 0 errors最危险的失败模式是静默少跑测试日志配置幂等化逐测试调用setup_litellm_logging首个之后只是短列表扫描logging.py 开头对litellm.callbacks的短路日志路径不变探针断言 handler 的baseFilename仍为~/.kiln_ai/logs/test_model_calls.log解析走未 patch 的Config.settings_dir()而非 patch 的settings_path顺序依赖不修复显式注释 架构文档记录pytest 根 conftest autouse fixture 先于模块级 fixture 实例化属文档化属性但承重这套改动的通用启示在于当一个昂贵依赖只在测试子集中被需要时用“sys.modules里有没有它”作为守卫比按测试名打标记更可靠——守卫条件由实际导入行为推导天然与“需要清理的状态是否存在”对齐而验证这类管道改动时把 pass/skip 计数当作首要断言、用探针测试锁定日志 handler 的具体状态如baseFilename、callbacks内容再显式记录那些“今天靠不变式兜底”的隐式依赖比默默依赖它要稳健得多。赞分享人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用【免费下载链接】KilnBuild, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.项目地址https://gitcode.com/gh_mirrors/ki/Kiln点击查看免费下载相关推荐Polar 前端性能优化规避 Barrel 文件导入砍掉 React/Next.js 数百毫秒模块加载与冷启动开销Polar 前端性能优化规避 Barrel 文件导入砍掉 React/Next.js 数百毫秒模块加载与冷启动开销 导读 本文基于仓库内 Vercel Re后端前端金融科技attrs 导入性能优化实战解析从惰性导入到 import attr 提速的工程实现attrs 导入性能优化实战解析从惰性导入到 import attr 提速的工程实现 导读 import attr / import attrs 是几乎所有后端psutil 测试提速实战将 pytest 启动时间从 0.42s 优化到 0.30s约 28%psutil 测试提速实战将 pytest 启动时间从 0.42s 优化到 0.30s约 28% 导读 本文以 psutil 项目在 2025 年的一次真可观测性系统编程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?