Kiln Core SDK 代码审计解读:`libs/core/kiln_ai` 的全局状态、隐式配置与重构路线图

原创2026-10-13 13:55:061,767 阅读
文章标签:人工智能AI 应用AI 评测RAG微调提示工程开发工具桌面应用MLOps

Kiln Core SDK 代码审计解读:libs/core/kiln_ai 的全局状态、隐式配置与重构路线图

Kiln/libs/core/kiln_ai 是 Kiln 项目的 Python SDK 核心,承载数据模型(datamodel)、模型适配(adapters)、评估(eval)、微调(fine-tune)、沙箱执行、MCP 工具与合成数据生成等能力。本文以仓库内 specs/projects/code_conventions/research/audit/kiln_core.md 这份代码审计报告为骨架,结合当前仓库源码逐一核验其结论,为你梳理这份 SDK 的架构现状、13 条编码规则与 11 项重构建议,帮助你在阅读或演进这份代码时快速定位风险点与改进路径。

审计概览:一份面向约 5.3 万行非测试代码的体检报告

审计范围覆盖 libs/core/kiln_ai 下约 5.3 万行非测试代码(排除了 ml_model_list.py 中静态模型数据本身,仅关注其加载与变更方式)。报告以 K/ 作为 Kiln/libs/core/kiln_ai/ 的简写前缀,全文路径均以仓库根为基准。整份报告的结论可以浓缩为五个主题:

  1. Config.shared() 单例是 SDK 隐藏的依赖注入系统:61 处非测试调用点分布在 17 个文件中,凭据、base URL、项目注册表、MCP secrets、自动保存开关全部从 ~/.kiln_ai/settings.yaml 深处读取,调用方没有任何注入通道。
  2. Config 存在真实正确性缺陷:布尔环境变量解析、文件与环境变量的优先级、读写原子性、文件权限等问题。
  3. 进程级可变模块状态遍布各层:built_in_models、finetune_cache、向量库 _adapter_cache、代码信任集合、事件循环绑定对象等,对多租户宿主不友好,且让测试依赖重置钩子。
  4. 导入/安装库本身会改变宿主进程:csv.field_size_limit、atexit、pytest11 入口点、litellm.callbacks、os.environ、sys.modules["__main__"] 等。
  5. 应用层关注点混入 SDK:桌面端项目注册表、调用桌面 HTTP 服务的工具、RAG 索引存储位置、代码信任状态、文件日志、git-sync 协议等。

下文按「分类发现 → 编码规则 → 重构建议」的顺序展开,并在每类发现中标注当前仓库源码的核验结果——其中部分问题已在当前代码中得到改进,我们会一并指出。

一、分类发现:九大问题的证据链

1. 冗余注释:少量但集中的「复述代码」注释

报告称冗余注释总体稀少(约 29 处),集中在少数文件。当前仓库仍可见的典型示例:

  • libs/core/kiln_ai/tools/kiln_task_tool.py 第 119、124 行的 # Load the project first / # Load the task from the project 直接复述下一行代码;
  • libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py 第 146、177 行 # Build completion kwargs for tool calls 出现在 build_completion_kwargs 之前;
  • libs/core/kiln_ai/utils/logging.py 第 137、155、159、165 行逐句叙述 setup_litellm_logging 的每一步("Create a new logger for model calls"、"Tell litellm to use our custom logger"),第 79 行甚至出现 # No op 后的 pass;
  • libs/core/kiln_ai/tools/base_tool.py 第 72-87 行的方法 docstring 只是重复方法名(如 "Return the tool name (function name) of this tool.")。

值得注意的「误导性注释」:libs/core/kiln_ai/adapters/ollama_tools.py 第 17-22 行 docstring 声称"using environment variable if set",但实际代码读取的是 Config(环境变量只是兜底),见 ollama_tools.py。

2. Zigzag / 依赖 diff 的注释:指向仓库中不存在的规划文档

这是报告中唯一标记为 high(模式化) 的注释问题:共有 26 处引用「functional spec §X」「architecture §7」「Phase 4/5」等规划文档编号,但 libs/core/specs/projects/ 目录下只有 .gitkeep,读者无法解析这些引用。分布如下:

  • libs/core/kiln_ai/cli/commands/migrate_eval_runs.py:第 5-11、283、330、356、414、434、483、507-509、542、570、607、647 行,出现 "functional spec §2"、"architecture §7"、"Phase 4's fallback"、"since Phase 4, an eval-generated run can't be deleted" 等;
  • libs/core/kiln_ai/adapters/eval/eval_runner.py:第 79、187、281、694、721-722、746 行,"Phase 5 migrates old calibration skips";
  • libs/core/kiln_ai/adapters/eval/trace_index.py、libs/core/kiln_ai/datamodel/code_file_storage.py、libs/core/kiln_ai/datamodel/eval.py 也有零散出现。

其他经典 diff 遗留注释包括:

  • libs/core/kiln_ai/tools/tool_registry.py 第 85-87 行:mcp_server_and_tool_name_from_id(tool_id) # Fixed function name,是纯粹的 diff 残留;
  • libs/core/kiln_ai/datamodel/external_tool_server.py 第 446 行 # Always call update_settings to maintain consistency with the old behavior;
  • libs/core/kiln_ai/datamodel/task_run.py 第 261 行:# Avoid circular import at module load. 位于局部导入 from kiln_ai.datamodel.datamodel_enums import TurnMode 之前,而 datamodel_enums.py 只依赖 enum/typing,并不存在循环——注释和局部导入均已过时;
  • libs/core/kiln_ai/adapters/remote_config.py 第 33 行:"V2 explained: Kiln v0.18 was the first release with remote config, but had bugs. We no longer publish v1 URL…",属于应进 changelog 的历史;
  • libs/core/kiln_ai/adapters/remote_config.py 第 222-223 行 docstring 声称函数非线程安全,但函数已接受 refresh_lock 参数,且唯一调用方在线程中运行——docstring 早于锁的实现。

报告同时明确标注合法的例外:datamodel/* 和 provider_tools.py 中大量 "Legacy …" 注释解释磁盘数据向后兼容迁移,属于持久事实,不应清理。

3. 模块化不良:提供者知识散落 6 处,加上多个「神类」

提供者知识碎片化(high)——新增一个模型提供者需要同时改动至少 6 个位置:

  1. libs/core/kiln_ai/utils/config.py 第 50-184 行:每个提供者一个 key/环境变量对(如 OPENAI_API_KEY、GROQ_API_KEY、OLLAMA_BASE_URL、AWS_ACCESS_KEY_ID、OPENROUTER_API_KEY、FIREWORKS_API_KEY、ANTHROPIC_API_KEY、GEMINI_API_KEY、TOGETHERAI_API_KEY、SILICONFLOW_CN_API_KEY、CEREBRAS_API_KEY、FEATHERLESS_AI_API_KEY、TYPESAFE_API_KEY 等,见 config.py);
  2. libs/core/kiln_ai/adapters/provider_tools.py 第 575-649 行:provider_warnings;
  3. 同文件第 519-571 行:provider_name_from_id;
  4. 同文件第 652-844 行:lite_llm_core_config_for_provider(约 20 处 Config.shared().<provider>_api_key 读取);
  5. libs/core/kiln_ai/utils/litellm.py 第 21 行起:get_litellm_provider_info;
  6. libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py 第 561 行起:build_extra_body,硬编码了 OpenRouter 的 provider 顺序表以及 R1/DeepInfra 特例。代码自身在第 562 行承认:"Don't love having this logic here…",注释直言"Should figure out how I want to isolate this"。

provider_tools.py 是 844 行的「杂物袋」(high):混合了提供者启用检查(含对 Ollama/Docker 的网络调用)、用户模型注册表解析、旧版自定义模型、通过全局项目注册表进行的 fine-tune 查找(第 282-308 行)、解析器选择、LiteLLM 凭据组装与显示名。

datamodel/eval.py 1655 行(high):混入约 30 个类——属性 schema、评分器文件名常量、评分校验、迁移、模板编译和递归守卫全局量。其中 Eval.upgrade_old_reference_answer_eval_config(第 1521-1563 行)在 pydantic model_validator 内部从磁盘加载子文件;EvalConfig.validate_v2_templates_and_expressions(第 1222-1250 行)在 validator 内导入 jinja2.meta 和 utils/jinja_engine.py 的私有 _template_env。

其他神类(med):datamodel/basemodel.py(1041 行,文件持久化引擎、缓存、parent/child 元编程 __init_subclass__ 方法注入、附件处理、名称校验同处一模块)、litellm_adapter.py(1023)、base_adapter.py 第 222-367 行 _run_returning_run_output(约 145 行覆盖校验、输入变换、格式化、模型调用、解析、schema 校验、reasoning 检查、构建 run、通过全局量决定是否自动保存、错误包装)、eval_runner.py(963)、rag_runners.py(893)、package_project.py(1121)、migrate_eval_runs.py(870,一次性迁移却永久留在 SDK CLI 中)、synthetic_user/runner.py(704)。

复制粘贴(med/low):remote_config.py 第 99-195 行有三段几乎相同的「validate-providers-then-model」循环(models / embedding models / rerankers);fireworks_finetune.py 第 82-88、169-172、259-262 行将「读 Config key 和 account id,缺则抛错」的块复制了三次,尽管第 367-372 行已有 api_key_and_account_id();utils/name_generator.py(2880 行)是放在 .py 里的静态词表,更适合做成数据文件。

4. 全局可变状态:进程级单例的十一种形态

  • high libs/core/kiln_ai/utils/config.py 第 38、250-254 行:Config._shared_instance 懒加载单例,创建时非线程安全,测试通过直接戳 _shared_instance 来重置(根 conftest.py 第 67-72 行的 reset_config fixture,以及 libs/core/kiln_ai/tools/test_mcp_session_manager.py 第 134 行)。17 个模块使用它。
  • high libs/core/kiln_ai/adapters/eval/v2_eval_code_eval.py 第 27-51 行:_trusted_projects: set[str] 是进程级「代码信任」(允许执行用户代码的权限),按项目路径作 key。这是安全策略却放在 SDK adapter 的模块状态里,多租户服务器下会跨租户泄漏。它带有一个仅测试用的 _reset_add_code_trust。
  • high libs/core/kiln_ai/adapters/remote_config.py 第 229-232 行:built_in_models<a href="https://link.gitcode.com/i/84064c130edca4d2d9b5ea91b46cdfaf" target="_blank">:] = …(以及 embedding 和 reranker 列表)原地改写模块级列表;约 15 个模块按名字导入并不加锁地迭代,例如 [provider_tools.py 和 ml_model_list.py 第 10589-10604 行。写者运行在守护线程(refresh_model_list_background,第 235-255 行)里,读者可能看到半替换状态,且无法按调用方隔离。
  • med provider_tools.py 第 282-308 行:finetune_cache: dict[str, Finetune] 无界且从不失效——被删除或编辑过的 fine-tune 会一直留在缓存里,且只按 ID 作 key,没有租户或项目根信息。
  • med libs/core/kiln_ai/adapters/vector_store/vector_store_registry.py 第 16-19 行:_adapter_cache 从不淘汰,为每个碰过的 RAG config 永久持有 LanceDB 句柄,只按 rag_config.id 作 key。
  • med libs/core/kiln_ai/tools/sandbox_bridge.py 第 46-47、93-102 行:进程级 asyncio.Semaphore,懒创建并绑定到第一个使用它的事件循环;第二个循环(remote-config 线程里的 asyncio.run、其他 asyncio.run 调用者或测试)会报 "bound to a different event loop"。_bridge_executor(第 63-90 行)是刻意为之的单例并有文档说明。
  • med libs/core/kiln_ai/tools/mcp_session_manager.py 第 40-58 行:MCPSessionManager 单例持有 asyncio.Lock() 和存活的 ClientSession,有同样的循环亲和问题;测试通过 _shared_instance = None 重置。
  • med libs/core/kiln_ai/datamodel/eval.py 第 54-57 行:_migration_lock / _currently_migrating_eval_ids 模块级集合,仅用于阻止「在 validator 里做磁盘 I/O」引发的递归。
  • med libs/core/kiln_ai/datamodel/model_cache.py 第 29-46 行:ModelCache 单例无界,每次文件读取都让内存增长;_check_timestamp_granularity(第 107-129 行)探测的是包自身 __file__ 所在文件系统而非项目目录,导致缓存可能在 mtime 粒度粗糙的项目卷上被错误启用。
  • low libs/core/kiln_ai/utils/filesystem_cache.py 第 57-72 行:TemporaryFilesystemCache 单例首次使用即建临时目录且从不清理。
  • low libs/core/kiln_ai/datamodel/strict_mode.py 第 9-24 行:全局 _strict_mode 标志由应用切换,虽有文档,但它是进程级校验行为。
  • low libs/core/kiln_ai/utils/pdf_utils.py 第 16-24 行:懒创建的 ProcessPoolExecutor 全局无锁;注释"singleton so dev-server reloading doesn't recreate the executor"存疑,因为模块 reload 会重置它。
  • low libs/core/kiln_ai/tool_testing/plugin.py 第 28 行:插件导入时创建进程级 FakeToolBridge()。

5. 自初始化模块与导入期副作用

  • high pytest11 入口点:libs/core/pyproject.toml 第 54-55 行声明 [project.entry-points.pytest11],配合 libs/core/kiln_ai/tool_testing/plugin.py 第 31-38 行的 pytest_configure,任何安装了 kiln-ai 的环境(包括第三方项目和 kiln_server)的每个 pytest 会话都会自动加载它,然后向 sys.modules["kiln"]、["kiln.tools"]、["kiln.async_tools"] 写入合成模块(来自 libs/core/kiln_ai/sandbox/tools_surface.py 第 154-156 行),遮蔽任何真实名为 kiln 的包。这在测试工具作者时需要 from kiln import tools 在收集期可解析,但对宿主进程是侵入性的。
  • high csv.field_size_limit:审计原文指出导入 utils/dataset_import.py 会把进程级 csv.field_size_limit 抬到 100 MiB。当前仓库已改进:libs/core/kiln_ai/utils/dataset_import.py 第 32 行定义 _CSV_FIELD_SIZE_LIMIT_BYTES = 100 * 1024 * 1024,第 35-67 行的 _RaisedCSVFieldSizeLimit 是带引用计数的上下文管理器,进入时抬高、退出时恢复宿主原值(注释明确说明 100 MiB 远超任何真实 prompt/response,且能放进 32 位有符号 C long,兼容 64 位 Windows)。test_dataset_import.py 有对应测试验证「恢复宿主原值」「嵌套引用计数」等行为。审计建议 4 中的"restore it afterwards"在当前仓库已落地。
  • high setup_litellm_logging:libs/core/kiln_ai/utils/logging.py 第 136-166 行整体覆写 litellm.callbacks = <a href="https://link.gitcode.com/i/e40710fddeefe3584226113dbb5210b1" target="_blank">CustomLiteLLMLogger(...)],丢弃宿主注册的任何回调(Langfuse、PostHog 等);强制 "LiteLLM" logger 为 ERROR、给非命名空间化的 "ModelCalls" logger 挂 propagate=False 的轮转文件 handler、创建 ~/.kiln_ai/logs。其唯一调用方是桌面入口点 app/desktop/desktop_server.py 第 133 行和根 [conftest.py,审计认为它属于应用代码。模块顶层还导入了 litellm。
  • med atexit:libs/core/kiln_ai/utils/pdf_utils.py 第 93 行 atexit.register(_shutdown_pdf_conversion_executor) 在导入时注册,与是否真的用 PDF 无关。
  • med temporary_env:libs/core/kiln_ai/utils/env.py 第 5-15 行被 libs/core/kiln_ai/adapters/vector_store/lancedb_adapter.py 第 89 行用作 temporary_env("OPENAI_API_KEY", "fake-api-key"),会为整个进程改写 os.environ——窗口期内任何并发线程(FastAPI 线程池、remote-config 线程)读 OPENAI_API_KEY 都会看到假 key。它绕开的是 llama_index 的默认行为,本可显式传入 mock embed 模型。
  • med sys.modules["main"] 交换:libs/core/kiln_ai/sandbox/spawn.py 第 48-62 行 start_process_with_light_main 在 p.start() 期间用轻量 stub 交换 sys.modules["__main__"],防止 spawn 子进程重导入父进程的重型 __main__(如会拉入 litellm、google.cloud.aiplatform 的 dev_server.py)。它由模块锁串行化、窗口亚毫秒级,文档充分,但仍是库代码对宿主的可见变异(用于规避 PyInstaller 并发 spawn 的 bug #7410)。
  • low remote_config 守护线程:libs/core/kiln_ai/adapters/remote_config.py 第 234-255 行 refresh_model_list_background 启动守护线程,线程内运行自己的 asyncio.run 并改写上述全局列表;开关是库内读环境变量(第 39-41 行 should_skip_remote_model_list,即 KILN_SKIP_REMOTE_MODEL_LIST),根 conftest 必须为每个测试设置它(conftest.py)。审计同时给出好评:libs/core/kiln_ai/adapters/__init__.py 是 PEP 562 的懒加载,且库内没有 logging.basicConfig 调用。

6. 不当模块:分层倒置与导入环

  • med 分层倒置:libs/core/kiln_ai/utils/litellm.py 第 3-7 行从 kiln_ai.adapters.*(ml_model_list、ollama_tools、reranker_list)导入,utils/project_utils.py 导入 datamodel.project——utils 并非叶子层。libs/core/kiln_ai/utils/test_import_layering.py 的存在正是因为过去出现过环。
  • med 约 12 处规避环的函数内局部导入:tools/tool_registry.py 第 61-67、137-161 行;tools/built_in_tools/llm_tools.py 第 80-88、282-283 行(注释直言 "avoid the tools -> adapter -> tool_registry cycle");datamodel/dataset_split.py 第 172-173 行;datamodel/eval.py 第 721、1230-1240 行。tools 与 adapters 双向依赖。
  • med datamodel/eval.py 第 1240 行从 utils/jinja_engine.py 第 94 行导入私有 _template_env,校验需要一个公开的 "referenced variables" 辅助函数。
  • med 应用层模块混入 SDK utils:utils/git_sync_protocols.py(桌面 git-sync 保存上下文,穿过 eval_runner、rag_runners、extractor_runner、synthetic_user/runner)、utils/project_utils.py + datamodel/registry.py(桌面项目列表)、utils/logging.py(桌面文件日志)。
  • low 测试支持代码作为可导入包模块:pytest_mock_files.py、pytest_test_output.py、adapters/pytest_embedding_fanout.py、adapters/pytest_prerelease_whitelist.py、adapters/eval/_heavy_main_bench.py 都随包发布,所有 test_*.py 与源码同目录,libs/core/pyproject.toml 没有 hatch exclude。
  • low datamodel/__init__.py 是巨大的扁平 re-export,import kiln_ai.datamodel 会连带拉入 jinja、jsonschema 等。

7. Config 陷阱:懒加载单例、环境变量读取、库内读配置

  • high 布尔解析:审计原文指出 property_config.type(value) 对 bool 意味着 bool("false") is True。当前仓库已修复:libs/core/kiln_ai/utils/config.py 第 293-308 行 _convert 对 bool 走 _parse_bool(第 420-429 行,识别 {"true","1","yes","on"} 与 {"false","0","no","off"},无法识别时告警并回退默认值)。不过审计的深层关切依然成立:这处解析曾以 bool(str) 实现,且 enable_demo_tools、autosave_runs 这类 bool 配置历史上无法用环境变量关闭(KILN_AUTOSAVE_RUNS=false 曾得到 True)。修复历史再次提醒:bool 配置必须配"设置为 false 值"的测试。
  • high 优先级倒置:libs/core/kiln_ai/utils/config.py 第 266-291 行 __getattr__ 的查找顺序是 settings.yaml → 环境变量 → 默认值。一份残留的 ~/.kiln_ai/settings.yaml 会覆盖 OPENAI_API_KEY 等环境变量——与 12-factor 及 SDK 用户预期相反;在服务器上意味着凭据来自运行进程用户的主目录。
  • high 凭据/端点无注入点:provider_tools.py 第 665-805 行约 20 处 Config.shared().<provider>_api_key 读取,另有 os.getenv("OPENROUTER_BASE_URL") / os.getenv("SILICONFLOW_BASE_URL")(第 665、677 行);model_adapters/jev_adapter.py 第 221 行;fine_tune/fireworks_finetune.py 第 83-84、169-170、187-189、259-260、368-369 行,together_finetune.py 第 43、140-141 行,vertex_finetune.py 第 217-218 行;adapters/ollama_tools.py 第 23 行(ollama_base_url 从 Config.shared() 读取,ollama_tools.py);adapters/docker_model_runner_tools.py 第 19 行。adapter_for_task(libs/core/kiln_ai/adapters/adapter_registry.py 第 156 行)不接受任何凭据或配置对象。
  • high created_by 默认值:libs/core/kiln_ai/datamodel/basemodel.py 第 339-342 行 created_by: str = Field(default_factory=lambda: Config.shared().user_id)——构造任何 datamodel 对象都会实例化 Config 单例并读 ~/.kiln_ai/settings.yaml,失败时回退 getpass.getuser()。多租户服务器下每条记录都会盖上 OS 用户名(除非显式覆盖)。
  • high 自动保存双来源:adapters/model_adapters/base_adapter.py 第 340、521 行与 adapters/model_adapters/mcp_adapter.py 第 188 行检查 Config.shared().autosave_runs,而 AdapterConfig.allow_saving 本就为此存在——策略有两个来源。
  • med 读缓存 + 非原子写:config.py 第 232、263-265 行,_settings 在 __init__ 只加载一次,永不重载,另一进程(桌面应用或 CLI)的修改在本进程写盘前不可见;写入(第 367-377 行)是「fresh load → read-modify-write」且无文件锁(threading.Lock 是实例级的),open("w") 非原子,崩溃于 dump 中途会截断文件。注释 "Fresh load to avoid clobbering changes from other instances"(第 398 行)只解决了一半。
  • med setattr 副作用:config.py 第 310-320 行把赋值变成磁盘 I/O——Config.shared().foo = x 对非 in_memory key 会静默重写 settings.yaml,对 in_memory key 只更新进程内存。同一语法两种截然不同的行为。
  • med 全局项目注册表:utils/project_utils.py 第 7-19 行 project_from_id 扫描 Config.projects,被 provider_tools.py 第 293 行(解析 fine-tune)和 tools/kiln_task_tool.py 第 118-120 行(子任务工具)使用;datamodel/registry.py 第 5-16 行同理。按路径加载项目的库用户或 kiln_server 无法使用 fine-tune 模型或任务工具,除非项目出现在主目录 yaml 中。
  • med CALL_KILN_API 的死代码:tools/tool_registry.py 第 53-58 行读 Config.shared().kiln_local_api_base_url()(内存值,"server must set before starting"),if not api_base_url 守卫是死代码——f-string 永不为空,总是默认 http://127.0.0.1:8757(该默认值由 config.py 的 kiln_local_api_host=127.0.0.1、kiln_local_api_port=8757 组合而来)。
  • med ~/.kiln_ai 隐藏存储根:adapters/vector_store/lancedb_adapter.py 第 321-325 行 RAG 索引存在项目之外、按 rag_config.id 作 key;tools/mcp_session_manager.py 第 285 行 MCP cwd;utils/logging.py 第 24 行。settings_dir(create=True) 在读取路径上就建目录,测试必须 patch Config.settings_dir(见 conftest.py 相关注释)。
  • med 双存储写入:datamodel/external_tool_server.py 第 376-447 行,datamodel 的 save_to_file() 同时把 MCP secrets 写进全局 settings.yaml(_save_secrets)——一次持久化调用写两个存储,其中一个按用户隔离。
  • low tools/mcp_session_manager.py 第 465 行 custom_mcp_path 从 Config 读取;get_shell_path 跑登录 shell($SHELL -l -c 'echo $PATH')并把结果缓存在单例上。
  • low adapters/remote_config.py 第 39-41 行 should_skip_remote_model_list() 读环境变量而非接受参数。
  • 测试证据:根 conftest.py 有三个 autouse fixture——reset_config(戳 Config._shared_instance)、use_temp_settings_dir(patch Config.settings_path 到 tmp_path)、skip_remote_model_list(设置环境变量)。核心测试中约有 26 处 patch("kiln_ai.utils.config.Config.shared")、14 处 patch("kiln_ai.utils.project_utils.Config.shared")、17 处 patch("kiln_ai.adapters.provider_tools.Config…")、14 处 patch.object(Config, …),跨 95 个引用 Config 的测试文件。

8. 其他重要的可维护性标记

  • med async 代码中的阻塞 HTTP:libs/core/kiln_ai/adapters/ollama_tools.py 第 29-40 行 async def ollama_online() 调用无超时的同步 httpx.get;第 157 行 get_ollama_connection(async)调用 requests.get;第 178-190 行 resolve_ollama_model_variant 每次解析 Ollama 模型 ID 都做 requests.get(timeout=5)(被 utils/litellm.py 第 100-104 行每次解析 provider 时调用)。requests 并未声明在 libs/core/pyproject.toml 依赖中。
  • med ModelName 枚举与远程模型冲突:ml_model_list.py 第 10607-10649 行与 remote_config.py:default_structured_output_mode_for_model_provider、default_thinking_level_for_model_provider 调用 ModelName(model_name);仅通过远程配置添加、名字不在编译期枚举里的模型会静默得到默认模式。get_model_by_name 类型标注为 ModelName,但 KilnModel.name 是 str。
  • med 隐藏磁盘 I/O:datamodel/basemodel.py 第 561-564、569-593 行 KilnParentedModel.__getattribute__ 使 obj.parent 触发磁盘加载——I/O 藏在属性访问里,也是 eval.py 用全局量守卫的 validator 递归的根源。
  • med 敏感文件权限:config.py 第 405 行 open(self.settings_path(), "w") 以默认 umask(通常 0644)写 settings.yaml——该文件持有 API key、PAT/OAuth token 与 MCP secrets,且无 chmod 600。
  • med 运行时依赖含开发工具:libs/core/pyproject.toml 第 20-49 行运行时依赖包含 coverage、pytest-cov、pytest-benchmark、pdoc,以及重型可选栈 google-cloud-aiplatform、vertexai、lancedb、llama-index 和来自个人 git fork 的 together——每个 SDK 用户都要为它们买单。
  • low 日志泄露数据面:utils/logging.py 第 36-76 行的自定义 LiteLLM logger 以 INFO 把完整请求 curl 命令、请求头和每条消息(用户数据)写进 ~/.kiln_ai/logs/model_calls.log,依赖 litellm 的 curl 辅助函数屏蔽认证头。

二、十三条候选规则:新代码的行为边界

审计提出的规则是对上述问题的直接回应,适用于在 libs/core 中新增代码:

  1. 不在应用边界之外调用 Config.shared()——cli/ 或一个独立的 config_from_settings() 适配器除外。凭据、base URL、自动保存策略、用户 ID 应作为构造器/函数参数或 ProviderCredentials/KilnRuntimeConfig 对象传入;现有调用点是技术债,不要再增加。
  2. SDK 不得变异宿主进程全局——不改 litellm.callbacks/litellm.*、logging.getLogger(...).setLevel/addHandler、csv.field_size_limit、os.environ、sys.modules、atexit。需要宿主级设置时,暴露由应用入口点(app/desktop/desktop_server.py)调用的 setup_*() 函数,且保持追加式:向 litellm.callbacks append,绝不整体替换。
  3. 导入期零工作——模块顶层只允许定义。无 I/O、线程、executor、环境变量读取、注册或全局库调整。懒创建资源放在显式、可注入的 owner 后面,而非模块 global。
  4. 不新增模块级可变缓存或注册表——确实需要缓存时,必须有 owner 对象、上限/淘汰策略、失效路径,且不得在可多租户运行的代码里只按 ID 作 key。权限或信任状态(如 _trusted_projects)绝不放入模块全局。
  5. 不在模块全局或进程单例中创建 asyncio.Lock/Semaphore/Event——它们绑定第一个事件循环,应按循环或 owner 创建。
  6. 用真实解析器解析配置值——环境变量布尔值必须走显式 "1"/"true"/"yes" 解析器,绝不调用 bool(str);新增 ConfigProperty 时配一个「把环境变量设为 false 值」的测试。
  7. async def 中不做阻塞 I/O——适配器与工具用带超时的 httpx.AsyncClient;核心代码禁用 requests(非声明依赖)。
  8. datamodel validator 必须纯净——无磁盘读、无 self.configs()/.parent 遍历、validator 内不导入其他模块的私有辅助。迁移放进显式加载钩子或迁移 CLI。
  9. 新增提供者的清单:单个 provider 描述符(一旦存在)之外,当前需检查 Config 属性、provider_warnings、provider_name_from_id、lite_llm_core_config_for_provider、utils/litellm.get_litellm_provider_info 及所有 build_extra_body 分支;用 grep ModelProviderName.<x> 找全。
  10. 不通过 ModelName 枚举做模型查找——远程配置可能交付不在枚举中的名字,应对照 KilnModel.name 字符串;除 remote_config.refresh_model_list 外把 built_in_models 视为只读。
  11. 注释必须在没有 diff 和规划文档时仍然成立——不用 "Phase N"、"functional spec §x"、"architecture §y"、"now"、"no longer"、"fixed"、"old behavior"、"prior bound";直接陈述不变量或理由,历史放进 PR 或 changelog。
  12. utils/ 不得导入 adapters/、tools/、datamodel/——需要这些的辅助函数放回所属层。
  13. 测试不新增 patch("...Config.shared")——优先传值;记住根 conftest.py 已重置单例并重定向 settings_path。

三、十一项重构建议:按规模与风险分级

审计以「规模 × 风险」为每个候选标注了工作量(S/M/L)与风险(low/med/high),这里摘出可直接执行的关键方案:

  1. 向适配器注入运行时配置(L,风险 med):引入由应用从 Config 构建一次的 ProviderCredentials(或 KilnRuntimeConfig)值对象,贯穿 adapter_for_task → litellm_core_provider_config → lite_llm_core_config_for_provider,并覆盖 fine-tune 适配器与 JevAdapter 客户端;为向后兼容保留回退 Config.shared() 的默认值。范围:adapters/provider_tools.py、adapters/adapter_registry.py、adapters/fine_tune/*、model_adapters/jev_adapter.py、ollama_tools.py、docker_model_runner_tools.py。
  2. 修复并加固 Config(M,风险 med):用真正的 bool 解析器(当前仓库的 _parse_bool 已落地一半);决定并文档化优先级(环境变量应优先于文件);按 mtime 变化重载;原子写(tmp + os.replace)、0600 权限、跨进程文件锁;把 __setattr__ 磁盘写替换为显式 save_setting;让 shared() 线程安全。因优先级变化会改变用户可见行为,需随版本发布说明。
  3. 把应用层关注点移出 SDK(M,风险 low-med):setup_litellm_logging/get_log_file_path、project_utils/datamodel/registry.py 项目注册表、代码信任集合、git_sync_protocols 上下文移到 app/desktop 或变成可注入钩子;project_from_id 的调用者(finetune_from_id、KilnTaskTool)改为接受项目解析器。
  4. 移除导入期/安装期副作用(S-M,风险 low):csv.field_size_limit 移入导入函数内(当前仓库已用上下文管理器实现并恢复);pdf_utils 的导入期 atexit 换成 owner 对象;pytest11 插件改为 opt-in(-p kiln_ai.tool_testing.plugin)或仅当测试请求 fixture 时才安装 kiln shim;LanceDB 的 temporary_env 换成显式 MockEmbedding。注意插件改动会影响工具作者。
  5. 单一提供者注册表(L,风险 med):构建一张 ProviderSpec 表(配置 key、显示名、警告消息、LiteLLM provider 名与 base URL、extra-body 钩子),由它生成 provider_warnings、provider_name_from_id、lite_llm_core_config_for_provider 和 get_litellm_provider_info,把 OpenRouter/Anthropic/R1 分支从 LiteLlmAdapter.build_extra_body 移进各 provider 钩子。现有测试覆盖良好。
  6. 模型列表存储对象(M,风险 low-med):把模块全局 built_in_models[:] 换成带原子快照交换的 ModelCatalog——读者拿到不可变 tuple,查找按字符串名而非 ModelName 枚举;顺带去重 remote_config.py 三段复制粘贴的反序列化循环。
  7. 收编进程级缓存(M,风险 med):给 finetune_cache、向量库 _adapter_cache、ModelCache、MCPSessionManager 和沙箱信号量配 owner(上限 + 失效),asyncio 原语做成 loop-aware。这些缓存为 LanceDB 和性能而存在,改动需谨慎。
  8. 拆分 datamodel/eval.py 与 provider_tools.py(M-L,风险 low-med):前者拆成属性 schema、运行记录、config、eval 与 splits,并把加载期迁移和 jinja 校验移出 validator;后者拆成用户模型、fine-tune 解析、LiteLLM config、provider 显示名。datamodel 导入顺序脆弱,参考 utils/test_import_layering.py。
  9. Ollama 探测改异步(S,风险 low):ollama_tools.py 用带超时的 httpx.AsyncClient,并按连接缓存变体解析,避免每次模型 ID 解析都发一次 HTTP。
  10. 打包清理(S-M,风险 low-med):coverage、pytest-*、pdoc 移入 dev 依赖;从 wheel 排除 test_*.py、pytest_*.py、_heavy_main_bench.py;vertex、lancedb、llama-index 考虑做成 extras。风险在于部分下游用户可能依赖传递依赖。
  11. 注释清扫(S,风险 none):把第 2 类中 26 处 spec/phase 引用和 zigzag 注释改写成自洽解释,删除 task_run.py:261 过时的 "circular import" 注释与局部导入。

四、给读者的实践路径

如果你要在当前仓库中落地这些规则,最直接的起点是:

  • 验证当前状态:阅读 config.py 的 shared()/__getattr__/__setattr__,对比 conftest.py 的三个 autouse fixture,理解测试为何依赖重置钩子;
  • 复现布尔解析问题:历史上 KILN_AUTOSAVE_RUNS=false 会得到 True;当前 _parse_bool(config.py)已能正确解析 false/0/no/off,但配置的优先级顺序(文件优先于环境变量)仍是 12-factor 反模式,值得在重构时一并处理;
  • 排查新增提供者的成本:grep -rn "ModelProviderName\." libs/core/kiln_ai,即可看到提供者知识分布在多少处;
  • 跟踪全局突变:built_in_models、finetune_cache、_adapter_cache、_trusted_projects、MCPSessionManager._shared_instance 是五个最高优先级的收编对象。

这份审计的价值不在于「批评现状」,而在于为 libs/core/kiln_ai 的演进提供了一份可按优先级执行的技术债清单:先修 Config(正确性 + 安全权限),再移除导入期副作用(宿主友好性),随后逐步用注入与描述符取代全局状态——每一条都有明确的文件坐标、规模估算和风险评级,可直接转化为 PR 任务。

登录后查看全文
Kiln