- 人工智能
- AI 应用
- AI 评测
- RAG
- 微调
- 提示工程
- 开发工具
- 桌面应用
【免费下载链接】Kiln
Build, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.
导读
本文基于 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/desktop+app/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.x("v1 不带 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测试 shim(pytest 插件),让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 源码(模块级 run)tool.py是固定文件名,内容就是作者写的源码本身——无包裹、无头注释、无重写。它是"实际运行的那份代码"与"测试导入的那份代码"的逐字节同一份。code_tool.kiln不再含code字段,其余字段全部保留:tool_function_name、tool_description、parameters_schema、timeout_seconds、tool_allowlist以及元数据。
在源码中,固定文件名被定义为模块常量:TOOL_CODE_FILENAME = "tool.py"(code_tool.py)。
2.2 代码裁判(Code judge)
{task}/.../eval_configs/{id} - {name}/ ├── eval_config.kiln # EvalConfig;其 CodeEvalProperties 无 code 字段 └── scorer.py # Python 源码(模块级 score)scorer.py是固定名,紧邻eval_config.kiln。只有 code-eval 配置会写出这个兄弟文件;其他 eval 配置类型(LlmJudgeProperties等)完全不受影响,不写任何兄弟文件。- 源码中的常量是
SCORER_CODE_FILENAME = "scorer.py"(eval.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.py;code不出现在序列化的properties中。
字节级保真:代码文件从内存code字符串原样写出(UTF-8),不做 universal-newline 转换(存储辅助用二进制读写,不用read_text/write_text),因此 CRLF 或混合换行在 load/save 往返中保持不变。保存是幂等的:对未改动的构件重复保存产生字节完全一致的文件。
源码印证:共享存储辅助 code_file_storage.py 的write_code_to_sibling_file在序列化上下文(save_attachments+dest_path,由 basemodel.py 的save_to_file()注入,见其第 482-512 行)存在时执行(dest / filename).write_bytes(code.encode("utf-8")),然后从序列化 dict 中剔除code键。同时模型侧通过model_serializer(mode="wrap")把该辅助接入保存链路(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;该值会在下次保存时迁移到文件)。这是唯一一种"代码构件不带兄弟文件也能加载"的情形。 - 否则(正常的文件支撑情形,无内联
code),code字段从兄弟文件填充;兄弟文件缺失/不可读 → 加载失败,并给出指明预期文件的清晰错误(例如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, context=ctx)解析,使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 → code+test),同样的 CodeMirror 编辑器。保存时,编辑器内容成为
tool.py。瞬态测试面板不受影响(§3.4)。 - 详情页:展示从
tool.py读出的代码(与今天一样只读)、元数据编辑、clone、archive。 - 代码裁判表单:同样的编辑器;保存时内容成为
scorer.py。 - 无测试 UI:Kiln 不展示测试文件、没有 "run tests" 控件、没有测试结果(决策 4)。详情页可以展示一段简短、静态的 "How to test this tool" 片段链接到文档(§6)——纯展示,P2,不执行。
5. 验证:规则不变,来源更新
保存时的验证三件套行为不变,只是改为针对"从文件加载的 / 即将写入文件的"代码运行:
- 代码工具(code_tool.py 的
validate_code):- ≤ 64 KB UTF-8:
len(code_bytes) > 64 * 1024报错; - 语法:
compile(self.code, "<code_tool>", "exec"),捕获SyntaxError; - AST 扫描:模块级
run函数(def或async def,ast.FunctionDef/ast.AsyncFunctionDef)必须存在; - 再加上 schema 与 allowlist 验证(
validate_parameters_schema要求 JSON Schema 根为type: object并拒绝保留字参数名;validate_allowlist校验工具白名单)。
- ≤ 64 KB UTF-8:
- 代码裁判(eval.py 的
CodeEvalProperties.validate_code):- ≤ 64 KB UTF-8;
compile(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.py:
import json import tool # the artifact's 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测试 shim(pytest 插件,§7):它安装与沙箱相同的合成kiln/kiln.tools/kiln.async_tools表面,使tool.py顶部的from kiln import tools在测试收集期就能解析;kiln_toolsfixture 让作者逐个 stub 工具的响应并检查调用记录。
6.2 代码裁判
无需 shim。作者直接写test_scorer.py:
from scorer import score def test_scores_exact_match(): assert score(reference_data={"answer": "42"}, output="42") == 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 或可调用(否则TypeError);set_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 变化(或缓存转冷)为止。这种"陈旧"是预期行为而非 bug:code按契约不可变,Kiln 不为失效而跟踪.py,手改不受支持。 - 零参数工具 / 空 schema:不变;
tool.py仍定义run()。 - 两个不同文件夹中同名的
tool.py:没问题——目录不同。但单个pytest运行横跨多个工具文件夹时可能撞上模块名冲突(tool从两处导入);文档化的模式是一次只测一个工具文件夹(或使用rootdir/importmode=importlib)。这在文档(§6)中说明,不由 Kiln 解决。 - 打包版桌面应用没有
pytest:符合预期。测试是创作环境的活动(装有kiln_ai的开发环境 / Agent);发布的桌面应用运行工具,不测试工具。这与决策 4("Kiln 不运行测试")一致。 .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.py(
CodeEvalProperties与加载时急切解析)、code_file_storage.py(共享的兄弟文件读写辅助与裸文件名包含性守卫)、basemodel.py(加载/保存上下文注入、delete()的rmtree)、model_cache.py(mtime 缓存失效)。 - 沙箱执行:worker.py(
exec字符串、不碰sys.path)、tools_surface.py(共享合成模块表面)、tools_api.py(运行时 IPC 桥与异常映射)。 - 测试 shim:plugin.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
- 微调
- 提示工程
- 开发工具
- 桌面应用
【免费下载链接】Kiln
Build, Evaluate, and Optimize AI Systems. Includes evals, RAG, agents, fine-tuning, synthetic data generation, dataset management, MCP, and more.
相关推荐
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格式化前端构建编译器代码质量编辑器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考