Kiln Core SDK 代码审计解读:`libs/core/kiln_ai` 的全局状态、隐式配置与重构路线图
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/ 的简写前缀,全文路径均以仓库根为基准。整份报告的结论可以浓缩为五个主题:
Config.shared()单例是 SDK 隐藏的依赖注入系统:61 处非测试调用点分布在 17 个文件中,凭据、base URL、项目注册表、MCP secrets、自动保存开关全部从~/.kiln_ai/settings.yaml深处读取,调用方没有任何注入通道。Config存在真实正确性缺陷:布尔环境变量解析、文件与环境变量的优先级、读写原子性、文件权限等问题。- 进程级可变模块状态遍布各层:
built_in_models、finetune_cache、向量库_adapter_cache、代码信任集合、事件循环绑定对象等,对多租户宿主不友好,且让测试依赖重置钩子。 - 导入/安装库本身会改变宿主进程:
csv.field_size_limit、atexit、pytest11入口点、litellm.callbacks、os.environ、sys.modules["__main__"]等。 - 应用层关注点混入 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 个位置:
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);libs/core/kiln_ai/adapters/provider_tools.py第 575-649 行:provider_warnings;- 同文件第 519-571 行:
provider_name_from_id; - 同文件第 652-844 行:
lite_llm_core_config_for_provider(约 20 处Config.shared().<provider>_api_key读取); libs/core/kiln_ai/utils/litellm.py第 21 行起:get_litellm_provider_info;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_configfixture,以及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没有 hatchexclude。 - 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_memorykey 会静默重写settings.yaml,对in_memorykey 只更新进程内存。同一语法两种截然不同的行为。 - 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)在读取路径上就建目录,测试必须 patchConfig.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(patchConfig.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 中新增代码:
- 不在应用边界之外调用
Config.shared()——cli/或一个独立的config_from_settings()适配器除外。凭据、base URL、自动保存策略、用户 ID 应作为构造器/函数参数或ProviderCredentials/KilnRuntimeConfig对象传入;现有调用点是技术债,不要再增加。 - SDK 不得变异宿主进程全局——不改
litellm.callbacks/litellm.*、logging.getLogger(...).setLevel/addHandler、csv.field_size_limit、os.environ、sys.modules、atexit。需要宿主级设置时,暴露由应用入口点(app/desktop/desktop_server.py)调用的setup_*()函数,且保持追加式:向litellm.callbacksappend,绝不整体替换。 - 导入期零工作——模块顶层只允许定义。无 I/O、线程、executor、环境变量读取、注册或全局库调整。懒创建资源放在显式、可注入的 owner 后面,而非模块
global。 - 不新增模块级可变缓存或注册表——确实需要缓存时,必须有 owner 对象、上限/淘汰策略、失效路径,且不得在可多租户运行的代码里只按 ID 作 key。权限或信任状态(如
_trusted_projects)绝不放入模块全局。 - 不在模块全局或进程单例中创建
asyncio.Lock/Semaphore/Event——它们绑定第一个事件循环,应按循环或 owner 创建。 - 用真实解析器解析配置值——环境变量布尔值必须走显式
"1"/"true"/"yes"解析器,绝不调用bool(str);新增ConfigProperty时配一个「把环境变量设为 false 值」的测试。 async def中不做阻塞 I/O——适配器与工具用带超时的httpx.AsyncClient;核心代码禁用requests(非声明依赖)。- datamodel validator 必须纯净——无磁盘读、无
self.configs()/.parent遍历、validator 内不导入其他模块的私有辅助。迁移放进显式加载钩子或迁移 CLI。 - 新增提供者的清单:单个 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>找全。 - 不通过
ModelName枚举做模型查找——远程配置可能交付不在枚举中的名字,应对照KilnModel.name字符串;除remote_config.refresh_model_list外把built_in_models视为只读。 - 注释必须在没有 diff 和规划文档时仍然成立——不用 "Phase N"、"functional spec §x"、"architecture §y"、"now"、"no longer"、"fixed"、"old behavior"、"prior bound";直接陈述不变量或理由,历史放进 PR 或 changelog。
utils/不得导入adapters/、tools/、datamodel/——需要这些的辅助函数放回所属层。- 测试不新增
patch("...Config.shared")——优先传值;记住根 conftest.py 已重置单例并重定向settings_path。
三、十一项重构建议:按规模与风险分级
审计以「规模 × 风险」为每个候选标注了工作量(S/M/L)与风险(low/med/high),这里摘出可直接执行的关键方案:
- 向适配器注入运行时配置(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。 - 修复并加固
Config(M,风险 med):用真正的 bool 解析器(当前仓库的_parse_bool已落地一半);决定并文档化优先级(环境变量应优先于文件);按 mtime 变化重载;原子写(tmp +os.replace)、0600权限、跨进程文件锁;把__setattr__磁盘写替换为显式save_setting;让shared()线程安全。因优先级变化会改变用户可见行为,需随版本发布说明。 - 把应用层关注点移出 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)改为接受项目解析器。 - 移除导入期/安装期副作用(S-M,风险 low):
csv.field_size_limit移入导入函数内(当前仓库已用上下文管理器实现并恢复);pdf_utils的导入期atexit换成 owner 对象;pytest11插件改为 opt-in(-p kiln_ai.tool_testing.plugin)或仅当测试请求 fixture 时才安装kilnshim;LanceDB 的temporary_env换成显式MockEmbedding。注意插件改动会影响工具作者。 - 单一提供者注册表(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 钩子。现有测试覆盖良好。 - 模型列表存储对象(M,风险 low-med):把模块全局
built_in_models[:]换成带原子快照交换的ModelCatalog——读者拿到不可变 tuple,查找按字符串名而非ModelName枚举;顺带去重remote_config.py三段复制粘贴的反序列化循环。 - 收编进程级缓存(M,风险 med):给
finetune_cache、向量库_adapter_cache、ModelCache、MCPSessionManager和沙箱信号量配 owner(上限 + 失效),asyncio 原语做成 loop-aware。这些缓存为 LanceDB 和性能而存在,改动需谨慎。 - 拆分
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。 - Ollama 探测改异步(S,风险 low):
ollama_tools.py用带超时的httpx.AsyncClient,并按连接缓存变体解析,避免每次模型 ID 解析都发一次 HTTP。 - 打包清理(S-M,风险 low-med):
coverage、pytest-*、pdoc移入 dev 依赖;从 wheel 排除test_*.py、pytest_*.py、_heavy_main_bench.py;vertex、lancedb、llama-index 考虑做成 extras。风险在于部分下游用户可能依赖传递依赖。 - 注释清扫(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 任务。