2026/10/14 2:08:48

Kiln「Code as Files」深度解析:代码工具与代码裁判从 JSON 内嵌走向可导入的 tool.py / scorer.py 与 pytest 测试链路

Kiln「Code as Files」深度解析:代码工具与代码裁判从 JSON 内嵌走向可导入的 tool.py / scorer.py 与 pytest 测试链路 人工智能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点击查看免费下载导读本文基于 Kiln 仓库中specs/projects/code_as_files/的完整设计文档functional_spec.md 与 project_overview.md系统讲解 Kiln 如何把**代码工具code tool与代码裁判code judge**中用户编写的 Python 源码从.kilnJSON 内嵌字符串迁移为存放在构件目录中、可直接导入的tool.py/scorer.py文件并随kiln_ai发布一个 pytest 测试 shim让作者在沙箱之外用标准pytest工具对代码工具与代码裁判做单元测试。读完本文你将掌握这一设计的磁盘布局契约、加载/保存语义、验证规则、kiln测试 shim 的完整行为契约以及仓库中每一处关键实现所在的源码位置。适用前提该设计作用于桌面应用app/desktopapp/web_ui与核心库libs/core/kiln_ai由于对应功能尚未正式发布nothing shipped不涉及任何迁移。规格未覆盖之处code_tools与 code-evals 的既有行为保持不变。1. 设计动机修复Python 源码被囚禁在 JSON 字符串里的设计错误在本次改动之前两个功能的用户代码都以内嵌str形式存放在构件 JSON 中代码工具CodeTool.code见 code_tool.py序列化进code_tool.kiln代码裁判CodeEvalProperties.code见 eval.py序列化进eval_config.kiln。被 JSON 转义后的 Python 字符串有三个致命短板pytest发现不了它、无法被import、也得不到编辑器 / LSP / lint / 类型检查支持——代码实际上无法用标准工具测试。这直接击穿了 code-tools 路线图中的关键一环工具编写 Agent 通过 API 来创作这些代码决策 11而 Agent 靠写测试、跑测试来保证代码正确它无法对一段 JSON 字符串做这件事。此次改动还逆转了code_tools项目两个过早锁定的旧决策决策 8代码内嵌于.kilnJSON自包含、可 diff被推翻Kiln 构件本来就是文件夹形态{id} - {name}/附件attachment早已采用同目录兄弟文件的方式共存所以自包含应当落在文件夹层级而非文件层级并且真实的.py在 git 中比转义后的 JSON 字符串 diff 得更好。旧决策声称的收益在拆分后依然成立而可测试性的代价却不再成立。决策 10 / 3.xv1 不带 Python 测试文件被部分放开Kiln 仍不管理测试构件但通过代码即文件让测试变得可能。1.1 六个锁定的设计决策2026-07-18存储固定文件名约定文件而非附件模型。代码持久化在.kiln旁边的固定可导入文件名下代码工具 →tool.py测试写from tool import run代码裁判 →scorer.py测试写from scorer import score。设计文档明确否决了KilnAttachmentModel它把文件命名为{uuid}.py会迫使测试走importlib路径加载而不是干净的from tool import run。实现上复用的是附件的序列化上下文模式save_attachments/dest_path而非附件模型本身。内存 API 不变。code在模型上仍是str加载时从兄弟文件读入保存时写入兄弟文件。验证器、执行引擎、瞬态测试端点看到的仍是与今天完全相同的str——这次改的是存储与创作层不是运行时。范围代码工具与代码裁判对称处理两个功能不分化。测试仅限代码即文件。Kiln 把代码变成真实可导入文件并随kiln_ai发布一个可导入的kiln测试 shimpytest 插件让from kiln import tools在测试中可解析、作者可以 stub 工具响应。Kiln不存储、不展示、也不运行测试——没有测试构件、没有 run-tests 端点、没有测试 UI。测试闭环发生在安装有kiln_ai的普通 Python 环境中。执行引擎原封不动。沙箱 worker、IPC/嵌套桥、超时、信号量、信任门禁接收的都是内存代码字符串无论其来源。改动不得修改 sandbox 目录 的执行行为或adapters/eval/sandbox_worker.py。沙箱把代码当字符串读取并exec()绝不 import 兄弟文件、绝不把构件目录加入sys.path硬性不变量见 §3.3。运行时与测试共享同一个kiln.tools表面。合成模块表面代理行为、list_tools、异常类从sandbox/tools_api.py中抽出放进一个仅依赖 stdlib 的共享模块沙箱桥与测试 shim 共同 import——这是一次行为保持的抽取由既有沙箱测试套件原样通过来守护防止运行时与测试行为漂移。2. 磁盘布局一份.kiln 一个固定名的.py兄弟文件2.1 代码工具Code tool{project}/code_tools/{id} - {name}/ ├── code_tool.kiln # 元数据 功能字段但不含 code └── tool.py # Python 源码模块级 runtool.py是固定文件名内容就是作者写的源码本身——无包裹、无头注释、无重写。它是实际运行的那份代码与测试导入的那份代码的逐字节同一份。code_tool.kiln不再含code字段其余字段全部保留tool_function_name、tool_description、parameters_schema、timeout_seconds、tool_allowlist以及元数据。在源码中固定文件名被定义为模块常量TOOL_CODE_FILENAME tool.pycode_tool.py。2.2 代码裁判Code judge{task}/.../eval_configs/{id} - {name}/ ├── eval_config.kiln # EvalConfig其 CodeEvalProperties 无 code 字段 └── scorer.py # Python 源码模块级 scorescorer.py是固定名紧邻eval_config.kiln。只有 code-eval 配置会写出这个兄弟文件其他 eval 配置类型LlmJudgeProperties等完全不受影响不写任何兄弟文件。源码中的常量是SCORER_CODE_FILENAME scorer.pyeval.py。2.3 不变量Invariants每个代码构件恰好一个代码文件位于固定名、构件自身文件夹内。保存时写入、加载时读取。文件永不通过绝对路径引用也永不出现在构件文件夹之外与附件相同的包含性保证。.kiln与其代码文件是一个整体正常保存的代码构件总是两者齐备且这是 Kiln 唯一写出的配对形态。加载是宽松的只对一种例外宽容——dict 里已自带内联code手写/遗留.kiln或任何内存 dict时不读兄弟文件即可构造并在下次保存时迁移为文件见 §5.2 与 §8。这是优雅构造的回退不是第二种受支持的磁盘形态。沙箱绝不 import 该文件硬性不变量。运行时数据模型在加载时把文件读入内存code字符串沙箱子进程收到的是这个字符串参数并在全新命名空间中exec()它——与今天完全一致。沙箱不得把构件文件夹加入sys.path不得importtool.py/scorer.py或任何兄弟文件。这样做的两个保护后果用户代码内的 import 只会解析到 stdlib Kiln 锁定依赖环境bundle以及文件夹里散落的杂项文件——作者自己的test_*.py、conftest.py、其他任何东西——在运行时永不可导入、不可执行。import tool/from scorer import score只发生在外部 pytest harness§6绝不发生在应用内。源码侧印证沙箱子进程入口 worker.py 中先install_tools_modules(requests, responses)安装合成kiln包随后namespace: dict[str, Any] {}、exec(compile(code, code_tool, exec), namespace)——命名空间从空 dict 开始没有任何对sys.path的追加也没有任何对tool.py的 import代码完全以字符串形式进入执行。3. 持久化语义保存写文件、加载读文件、内存仍是字符串3.1 保存Save保存一个代码构件时代码写入兄弟文件并从.kilnJSON 中省略。代码工具save_to_file()把tool.py写进构件文件夹code不出现在code_tool.kiln中。代码裁判保存父级EvalConfig时对CodeEvalProperties写入scorer.pycode不出现在序列化的properties中。字节级保真代码文件从内存code字符串原样写出UTF-8不做 universal-newline 转换存储辅助用二进制读写不用read_text/write_text因此 CRLF 或混合换行在 load/save 往返中保持不变。保存是幂等的对未改动的构件重复保存产生字节完全一致的文件。源码印证共享存储辅助 code_file_storage.py 的write_code_to_sibling_file在序列化上下文save_attachmentsdest_path由 basemodel.py 的save_to_file()注入见其第 482-512 行存在时执行(dest / filename).write_bytes(code.encode(utf-8))然后从序列化 dict 中剔除code键。同时模型侧通过model_serializer(modewrap)把该辅助接入保存链路code_tool.py 与 eval.py。无保存上下文时普通model_dump/ API 响应code原样留在输出中且不写文件——API 契约不变。测试套件 test_code_file_storage.py 用a\r\nb\r\n、混合 CRLF/LF/bare CR、无末尾换行等参数逐一验证了保存写出精确字节、加载重建同一字符串、重存字节幂等。3.2 加载Load加载时在验证器运行之前把代码读回内存code字符串于是既有的验证三件套§5与今天一样作用于加载后的代码加载器把构件的源码文件夹提供给模型验证今天不提供——见 architecture.md §3。优先级规则如果解析出的 dict已自带内联code加载器直接使用它且不读兄弟文件有意宽松见 §8该值会在下次保存时迁移到文件。这是唯一一种代码构件不带兄弟文件也能加载的情形。否则正常的文件支撑情形无内联codecode字段从兄弟文件填充兄弟文件缺失/不可读 → 加载失败并给出指明预期文件的清晰错误例如code_tool.kiln at path is missing its tool.py。这镜像了悬挂附件失败的呈现方式与今天损坏构件暴露的方式一个正常保存的代码构件若缺了代码文件是无法加载的。源码印证read_code_from_sibling_file在ctx[loading_from_file]为真、data是 dict 且不含code键时从Path(src) / filename执行code_path.read_bytes().decode(utf-8)二进制读 显式解码避免read_text折叠 CRLF再把code注入 dict 副本Copy-on-write不改调用方输入。OSError 会包装成指明kiln_filename、src与完整预期路径的ValueError。加载上下文loading_from_file: True, source_dir: path.parent由 basemodel.py 的类方法注入。对应的单元测试覆盖了文件缺失报错命名文件与路径、code已存在时忽略兄弟文件、非加载上下文不读文件、空上下文原样通过、Copy-on-write 不改调用方 dict 等test_code_file_storage.py。另外eval.py 针对 code_eval 增加了一层加载时急切解析_eager_parse_code_eval_on_load文件加载时若properties.type code_eval先把 properties dict 显式经过CodeEvalProperties.model_validate(props, contextctx)解析使scorer.py缺失或score()函数错误等真实原因直接暴露而不是被外层V2EvalConfigProperties | dict | None联合类型的 dict 回退分支掩盖成笼统的 V2 config requires typed properties。3.3 不可变性、克隆、归档、删除——契约不变不可变功能内容依然成立code现在是tool.py创建后即冻结修改它意味着clone。克隆会读取源构件的代码文件并在新构件文件夹下写出一份全新的。归档/取消归档仅元数据文件不动。删除删除整个构件文件夹包括其代码文件与作者放进去的任何测试文件沿用既有delete()的rmtree行为见 basemodel.py。编辑仍只触碰name/description/is_archived三者都不会移动或重写代码文件。3.4 瞬态执行测试端点——不涉及文件test_code_tool与test_v2_eval端点从请求构建未保存的模型、执行、不持久化任何东西。因为它们从不调用save_to_file()也就从不写代码文件——它们让内存code字符串直接穿过沙箱执行与今天完全一致。这正是执行引擎保持未触碰的保证所在无论代码来自哪里引擎收到的始终是内存字符串。4. 创作与 UI 行为交互不变存储下沉创作 UX 完全不变变化的只是它底下的存储创建向导代码工具同样的两屏流程define → codetest同样的 CodeMirror 编辑器。保存时编辑器内容成为tool.py。瞬态测试面板不受影响§3.4。详情页展示从tool.py读出的代码与今天一样只读、元数据编辑、clone、archive。代码裁判表单同样的编辑器保存时内容成为scorer.py。无测试 UIKiln 不展示测试文件、没有 run tests 控件、没有测试结果决策 4。详情页可以展示一段简短、静态的 How to test this tool 片段链接到文档§6——纯展示P2不执行。5. 验证规则不变来源更新保存时的验证三件套行为不变只是改为针对从文件加载的 / 即将写入文件的代码运行代码工具code_tool.py 的validate_code≤ 64 KB UTF-8len(code_bytes) 64 * 1024报错语法compile(self.code, code_tool, exec)捕获SyntaxErrorAST 扫描模块级run函数def或async defast.FunctionDef/ast.AsyncFunctionDef必须存在再加上 schema 与 allowlist 验证validate_parameters_schema要求 JSON Schema 根为type: object并拒绝保留字参数名validate_allowlist校验工具白名单。代码裁判eval.py 的CodeEvalProperties.validate_code≤ 64 KB UTF-8compile(self.code, code_eval, exec)AST 扫描模块级score同步或异步均可异步协程由sandbox_worker.execute_scorer_bridged透明 await。兄弟文件未通过三件套的代码构件创建保存时无效加载时损坏或被手改的文件也会浮现同样的验证器错误。6. 可测试性本项目的核心交付这是整个项目的意义所在。改动之后身处装有kiln_ai的 Python 环境中的作者人或 Agent可以6.1 代码工具cd进工具所在文件夹或让pytest指向它。在tool.py旁边写test_tool.pyimport json import tool # the artifacts tool.py — imports cleanly now def test_happy_path(kiln_tools): kiln_tools.set(get_user, {id: 1234, name: Alice}) # static reply kiln_tools.set(list_jobs, lambda **kw: json.dumps([a, b])) # or a callable out tool.run(job_ids[a, b]) assert json.loads(out)[name] Alice assert kiln_tools.calls[0].name get_user # call assertions def test_unknown_tool_raises(kiln_tools): from kiln.tools import ToolNotAllowed # a name that was never registered behaves like a non-allowlisted tool ...运行pytest它像任何 Python 测试一样通过/失败。其支撑机制是一个随kiln_ai发布的kiln测试 shimpytest 插件§7它安装与沙箱相同的合成kiln/kiln.tools/kiln.async_tools表面使tool.py顶部的from kiln import tools在测试收集期就能解析kiln_toolsfixture 让作者逐个 stub 工具的响应并检查调用记录。6.2 代码裁判无需 shim。作者直接写test_scorer.pyfrom scorer import score def test_scores_exact_match(): assert score(reference_data{answer: 42}, output42) 1.0score()的输入是普通参数它只依赖 stdlib kiln_ai两者在普通环境中都可导入。7.kiln测试 shim 的行为契约一个随kiln_ai发布、自动发现的 pytest 插件不向单个构件写任何文件。导入保真呈现与沙箱完全一致的模块表面——tools.name(**kwargs) - str、async_tools.name可 await、list_tools()以及完全相同的异常类ToolNotAllowed/ToolTimeout/ToolCallError都带.tool/.message/.raw。一个能捕获kiln.tools.ToolCallError的测试捕获的正是运行时抛出的同一行为。安装时机模块在插件加载时就装入sys.modules先于测试模块导入——因此tool.py顶层的from kiln import tools在收集期即可解析。单靠 fixture 太迟这正是必须做成插件的原因。kiln_toolsfixture函数作用域测试间自动重置kiln_tools.set(name, reply)——为某工具函数名注册回复。reply可以是str原样返回符合返回字符串契约或可调用对象(**kwargs) - str。kiln_tools.set_error(name, exc)——让某个名字抛出给定的ToolNotAllowed/ToolTimeout/ToolCallError。未注册的名字 → 抛ToolNotAllowed与运行时的 allowlist 未命中一致。kiln_tools.calls——按序记录的{name, arguments}调用日志供断言使用。list_tools()返回已注册工具的生命name 作者提供的内容。异步路径async_tools由同一注册表支撑await async_tools.x()在asyncio.gather下可用。该插件只是测试支撑面它从不派生子进程、从不调用真实工具、也绝不在沙箱内加载沙箱安装自己的真实桥。7.1 源码级实现对照共享表面tools_surface.py 定义三个异常类与ToolBridge协议build_tools_modules()构造kiln/kiln.tools_SyncToolsModule__getattr__返回lambda *args, **kw: bridge.call(name, args, kw)/kiln.async_tools_AsyncToolsModule经asyncio.to_thread转发三个模块对象install_tools_modules_for_bridge()将其装入sys.modules。异常类同时挂到tools与async_tools上。运行时桥tools_api.py 的install_tools_modules()创建真实 IPC 桥ToolCallBridge并调用同一安装函数_resolve把not_allowed/timeout/call_error三类错误消息分别映射为三个异常类。测试插件plugin.py 中pytest_configure在收集前调用install_tools_modules_for_bridge(_bridge)kiln_toolsfixture 在每测试前后_bridge.reset()。注册入口在 pyproject.toml[project.entry-points.pytest11] kiln_tools kiln_ai.tool_testing.plugin。进程级假桥fake_bridge.py 的FakeToolBridge实现同一ToolBridge协议set()校验 reply 必须是 str 或可调用否则TypeErrorset_error()校验是异常实例call()按运行时同序分派先查 not-allowed → 再拒位置参数工具只接受关键字参数违者ToolCallError→ 再抛注册错误 → 最后返回静态或可调用回复reset()清空注册与调用日志每次调用先记入calls。行为测试test_tool_testing.py 以作者视角驱动该插件验证模块在收集前已安装、静态回复原样返回、可调用回复接收 kwargs、calls带参数记录、set拒绝非 str 非可调用值等。8. 边界情况创建后手改tool.py超出不可变契约。冷未缓存加载时验证三件套作用于被改过的文件语法错误或缺run会以正常验证器消息使加载失败。但只读模型缓存以.kiln的 mtime为键不是.py的 mtime见 architecture.md §9实现在 model_cache.py_is_cache_valid用path.stat().st_mtime_ns对比因此只改.py不会使已缓存模型失效——已加载的代码会一直提供到.kilnmtime 变化或缓存转冷为止。这种陈旧是预期行为而非 bugcode按契约不可变Kiln 不为失效而跟踪.py手改不受支持。零参数工具 / 空 schema不变tool.py仍定义run()。两个不同文件夹中同名的tool.py没问题——目录不同。但单个pytest运行横跨多个工具文件夹时可能撞上模块名冲突tool从两处导入文档化的模式是一次只测一个工具文件夹或使用rootdir/importmodeimportlib。这在文档§6中说明不由 Kiln 解决。打包版桌面应用没有pytest符合预期。测试是创作环境的活动装有kiln_ai的开发环境 / Agent发布的桌面应用运行工具不测试工具。这与决策 4Kiln 不运行测试一致。.kilnJSON 中内联存在code加载器有意宽松architecture.md §3.1。若解析出的 dict 已带code键——手写或遗留的.kiln或任何传给model_validate的内存 dict——该值按原样使用且不读兄弟tool.py下次save_to_file()时迁移到tool.py并从 JSON 中移除。实践中不存在这类文件未发布过任何东西因此这是优雅构造属性也让已带code的 dict 可以在不触碰磁盘的情况下构造不是受支持的迁移路径。有意不做有内联code就严格拒绝的检查。9. 范围之外与 project_overview.md 一致Kiln 管理的测试测试构件 / run-tests 端点 / 测试 UI、任何执行引擎改动、迁移以及code_tools项目本就排除的一切工具编写 Agent、服务端执行、依赖管理、密钥、类型 stub 等。10. 深入阅读指引规格文档functional_spec.md、project_overview.md、architecture.md、implementation_plan.md以及specs/projects/code_as_files/phase_plans/下的分阶段计划。数据模型实现code_tool.py、eval.pyCodeEvalProperties与加载时急切解析、code_file_storage.py共享的兄弟文件读写辅助与裸文件名包含性守卫、basemodel.py加载/保存上下文注入、delete()的rmtree、model_cache.pymtime 缓存失效。沙箱执行worker.pyexec字符串、不碰sys.path、tools_surface.py共享合成模块表面、tools_api.py运行时 IPC 桥与异常映射。测试 shimplugin.py、fake_bridge.py、test_tool_testing.py。配套测试test_code_file_storage.py字节保真、裸文件名守卫、宽松加载、libs/core/kiln_ai/datamodel/test_code_tool.py与test_eval_model.py模型级端到端行为。赞分享人工智能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点击查看免费下载相关推荐FoundationDB 内部开发工具指南Code Probes 代码探针与 TestHarness 测试框架深度解析FoundationDB 内部开发工具指南Code Probes 代码探针与 TestHarness 测试框架深度解析 本文聚焦 FoundationDB 面分布式数据库KV存储数据库后端深入解析 shikijs/rehype从测试夹具 a.md 看 Markdown 代码块高亮的完整链路深入解析 shikijs/rehype从测试夹具 a.md 看 Markdown 代码块高亮的完整链路 本篇文章以 shikijs/rehype 插件的核前端开发工具深入 Rome xtask 工具链代码生成、覆盖测试与基准测试的 Rust 脚本体系深入 Rome xtask 工具链代码生成、覆盖测试与基准测试的 Rust 脚本体系 本篇技术指南围绕 xtask/README.md https://lin开发工具Lint格式化前端构建编译器代码质量编辑器上一篇Android列表动画终极指南ListViewAnimations在电商、社交、新闻类App中的实战应用下一篇24x24像素的视觉革命Simple Icons 网格系统终极设计指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考