news 2026/10/21 1:51:35

Kiln 输入变换(Input Transform)落地指南:Jinja2 模板引擎、数据模型与适配器集成的 Phase 1 实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Kiln 输入变换(Input Transform)落地指南:Jinja2 模板引擎、数据模型与适配器集成的 Phase 1 实战
  • 人工智能
  • AI 应用
  • AI 评测
  • RAG
  • 微调
  • 提示工程
  • 开发工具
  • 桌面应用

【免费下载链接】Kiln

Build, 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
点击查看免费下载

导读

input_transform是 Kiln(项目根目录)为运行配置(RunConfig)新增的一项通用基础设施能力:在任务调用时刻,用 Jinja2 模板把原始任务输入渲染成模型实际看到的首条用户消息。本文以 Kiln 仓库中specs/projects/templates规格项目的第一阶段(Phase 1: Engine + Datamodel Foundation)为骨架,完整讲解引擎模块、Pydantic 数据模型与字段落位,并对照当前仓库中已完成的真实实现(jinja_engine.py、input_transform.py、run_config.py)逐一印证设计与代码。读完本文,你将掌握:如何为 RunConfig 配置输入变换、Jinja2 沙箱环境的双实例设计为何必要、StrictUndefined与默认Undefined两种语义的分工、保存期与运行期的错误处理边界,以及 Phase 1 对应的全部单元测试清单。

1. Phase 1 目标与交付物

Phase 1 是整个 Input Transform 项目(规格见 project_overview.md 与 functional_spec.md)的第一阶段,其定位是引擎 + 数据模型地基,目标是三份可独立测试、无需改动任何适配器的交付物:

  1. 引擎模块libs/core/kiln_ai/utils/jinja_engine.py:Jinja2 沙箱环境的两个实例与四个公开函数;
  2. 数据模型libs/core/kiln_ai/datamodel/input_transform.py:JinjaInputTransform模型与InputTransform判别联合(discriminated union);
  3. KilnAgentRunConfigProperties上的新字段input_transform: InputTransform | None(位于 run_config.py)。

项目整体仅分两个阶段:Phase 1 完成后,Phase 2(见 phase_2.md)再把变换挂入BaseAdapter的同步与流式执行管线。实施顺序与总览见 implementation_plan.md。

当前仓库中该功能已实现完毕(规格文件均标记status: complete),因此本文讲解的设计细节均可在真实源码中找到对应实现。

2. 新增依赖:jinja2

Phase 1 的第一步是把 Jinja2 加入核心库依赖。规格要求在libs/core/pyproject.toml中增加jinja2 >= 3.1.0,然后运行uv lock更新锁文件。

实际实现与规格一致,libs/core/pyproject.toml 中可以看到依赖声明:

"jinja2>=3.1.0",

实操提示:在仓库根目录执行uv lock即可完成依赖锁定;后续运行测试可用uv run pytest <路径>或项目自带的uv run ./checks.sh --agent-mode(见 implementation_plan.md)。

选择 Jinja2 3.1.x 的理由在规格中写得很明确:模板作者需要熟悉且完整的模板语法({{ }}替换、{% if %}/{% for %}块、{% set %}等),同时项目需要SandboxedEnvironment这类官方沙箱能力来承接用户编写的模板内容。

3. 引擎模块:双沙箱环境 + 四个公开函数

引擎是 Phase 1 的核心。规格要求模块级创建两个SandboxedEnvironment实例,并提供四个公开函数;真实实现(jinja_engine.py)完全遵循了这一设计。

3.1 为什么是两个 SandboxedEnvironment

undefined=这个设置是**环境级(environment-scoped)**的,无法在同一实例上按调用切换,因此必须用两个实例:

环境undefined配置服务对象缺失变量时的行为
_template_envStrictUndefinedcompile_template_or_raise、render_input_transform抛UndefinedError(硬失败)
_expression_envUndefined(默认)compile_expression_or_raise、extract返回Undefined(falsy,且与None可区分)

模板渲染必须硬失败:模板里打错变量名是作者必须立刻看到的 bug,StrictUndefined保证缺变量时报错而不是渲染成空串;而表达式求值(extract)需要软语义:调用方要能区分"键缺失"(返回Undefined)与"显式 null"(返回None),不依赖 try/except。

两个环境共享相同的安全边界与语法面(同一SandboxedEnvironment类),且都开启:

  • trim_blocks=True:去除块标签({% ... %})后的换行,输出更干净;
  • lstrip_blocks=True:去除块标签前的行首空白;
  • 不使用FileSystemLoader或任何 I/O loader,模板只从内联字符串渲染;
  • V1 不注册自定义过滤器(纯内置过滤器)。

真实实现如下:

_template_env = SandboxedEnvironment( undefined=StrictUndefined, trim_blocks=True, lstrip_blocks=True, ) _expression_env = SandboxedEnvironment( undefined=Undefined, trim_blocks=True, lstrip_blocks=True, )

3.2 四个公开函数的行为契约

真实实现(jinja_engine.py)中四个函数的行为如下:

def compile_template_or_raise(template: str) -> None: """Validate that the template compiles. Raises ValueError on syntax error.""" try: _template_env.from_string(template) except TemplateSyntaxError as e: raise ValueError( f"Invalid Jinja2 template: {e.message} (line {e.lineno})" ) from e def compile_expression_or_raise(expression: str) -> None: """Validate that a Jinja2 expression compiles. Raises ValueError on syntax error.""" try: _expression_env.compile_expression(expression) except TemplateSyntaxError as e: raise ValueError( f"Invalid Jinja2 expression: {e.message} (line {e.lineno})" ) from e def render_input_transform(transform: InputTransform, task_input: Any) -> str: """Render the transform against the task input, returning the first-user-message string.""" from kiln_ai.datamodel.input_transform import JinjaInputTransform if isinstance(transform, JinjaInputTransform): namespace = _build_namespace(task_input) return _template_env.from_string(transform.template).render(**namespace) raise ValueError(f"Unknown InputTransform variant: {type(transform).__name__}") def extract(expression: str, data: dict) -> Any: """Evaluate a Jinja2 expression against a dict; returns the Python value.""" try: compiled = _expression_env.compile_expression(expression, undefined_to_none=False) except TemplateSyntaxError as e: raise ValueError( f"Invalid Jinja2 expression: {e.message} (line {e.lineno})" ) from e try: result = compiled(**data) if isinstance(result, types.GeneratorType): result = list(result) except UndefinedError as e: raise JinjaExtractionError(str(e)) from e return result

各函数要点:

  • compile_template_or_raise/compile_expression_or_raise:只做编译校验、返回None,不返回编译后的对象。语法错误统一转成带行号的ValueError(e.message+e.lineno)。这是保存期校验的钩子:数据模型的field_validator直接调用它们。
  • render_input_transform:根据变换类型分发(V1 只有JinjaInputTransform),先由私有函数_build_namespace构造命名空间,再用_template_env.from_string(template).render(**namespace)渲染。遇到未知变换类型抛ValueError。
  • extract:用表达式环境求值。缺失键返回Undefined;显式None返回None;生成器结果自动物化为列表(map/selectattr/groupby等返回惰性生成器,只有list()之后逐项查询才会真正执行)。在真实实现中,extract还多了规格之外的一层:对缺失值做进一步操作(属性访问、索引、过滤器)时抛出的UndefinedError会被转换为自定义的JinjaExtractionError(ValueError子类),使调用方可以统一按"数据不符合表达式要求"处理,而非未预期的崩溃。

3.3_build_namespace:输入处理规则与为何保持私有

命名空间构造遵循规格 §5 的输入处理规则(真实实现在 jinja_engine.py):

  • 模板始终只看到一个变量input,不做顶层展开、不做别名,因此同一模板可以跨不同输入形状的任务移植;
  • dict(对象 schema)输入:原样暴露为input,可用{{ input.field_name }}、{{ input.items() }};
  • list(数组 schema)输入:原样暴露为input,可用{{ input[0] }}、{{ input | length }};
  • 字符串(纯文本)输入:先尝试json.loads(...),成功则用解析后的值(dict/list/标量),失败则回退为原始字符串。这一"魔法"意味着"42"会解析成 int 42、"true"解析成 bool True,而 Jinja2 都能正确处理这些类型。
def _build_namespace(task_input: Any) -> dict: if isinstance(task_input, str): try: parsed = json.loads(task_input) except (ValueError, TypeError): parsed = task_input return {"input": parsed} return {"input": task_input}

_build_namespace是私有的,原因有二(架构文档 §3.3):一是 eval v2 消费方会自行构造四字段合成 dict(final_message、trace、reference_data、task_input)直接传给render_input_transform,不需要纯文本自动解析;二是没有其他 V1 消费方,过早暴露属于投机性 API 面。

3.4 编译成本与缓存预留

render_input_transform与extract每次调用都会重新编译源码,而compile_*_or_raise返回None(不缓存编译产物)。规格明确这是有意的:Jinja2 对提示词规模的模板编译只需微秒级,V1 可接受。若未来性能剖析证明有必要,可在引擎模块内部加一个以源码字符串为键的模块级 LRU 缓存,而公开 API 无需任何改动——校验器返回None正是为这个未来敞开的。

4. 数据模型:JinjaInputTransform 与判别联合

4.1 类定义与保存期校验

真实实现(input_transform.py)与规格一致:

class JinjaInputTransform(BaseModel): type: Literal["jinja"] = "jinja" template: str = Field( description="Jinja2 template source. Validated at save time.", ) @field_validator("template") @classmethod def validate_template_compiles(cls, v: str) -> str: from kiln_ai.utils.jinja_engine import compile_template_or_raise compile_template_or_raise(v) return v def _get_input_transform_type(data: Any) -> str | None: if isinstance(data, dict): return data.get("type") return getattr(data, "type", None) InputTransform = Annotated[ Union[Annotated[JinjaInputTransform, Tag("jinja")],], Discriminator(_get_input_transform_type), ]

要点:

  • type是判别标签(字面量"jinja");
  • template字段的@field_validator调用compile_template_or_raise,因此格式错误的模板在保存期就被拒绝(Pydantic 抛ValidationError),运行时不会遇到语法错误;
  • validate_template_compiles内的延迟导入(lazy import)是刻意为之:jinja_engine的类型注解需要InputTransform,而input_transform需要compile_template_or_raise,若不延迟导入会形成datamodel <-> utils的循环导入;
  • _get_input_transform_type是判别器:从 dict 取type键、从模型实例取type属性;
  • 联合类型 V1 只有jinja一个成员,后续增加新变换变体只需在Union中追加一行并新增一个BaseModel类。

4.2 字段落位:为什么在 KilnAgentRunConfigProperties

新字段input_transform落在KilnAgentRunConfigProperties,与tools_config平级(真实实现见 run_config.py):

input_transform: InputTransform | None = Field( default=None, description=( "Optional transform applied to the task input at run time, producing " "the first user message sent to the model. Default None preserves " "the identity path." ), )

规格明确不放在其他三个位置,理由各有讲究:

  • 不放TaskRunConfig(持久化包装类,见 task.py):agent 专属配置应属于 agent 的 RunConfigProperties 子类;
  • 不放Task:同一个任务可以有多个携带不同变换的 RunConfig("同一个评测、不同 judge 提示词"的工作流依赖这一点);
  • 不放McpRunConfigProperties:MCP 工具接收原始输入,不需要变换。

input_transform默认None即"恒等路径"(identity path),保证所有既有 RunConfig 行为完全不变。规格还指出TaskRunConfig遵循"创建一次、按 ID 引用"的约定不可变惯例,input_transform自动参与该约定。

4.3 字段级向后兼容

磁盘上既有的 RunConfig JSON 没有input_transform键,加载时通过 Pydantic 默认值自动得到input_transform=None。无需任何迁移代码,也不需要upgrade_old_entries式的钩子。TaskRunConfig上已有的model_validator(mode="before") upgrade_old_entries(task.py)只回填structured_output_mode,完全不触碰input_transform,因此不受影响。

5. 输入处理与输出契约

5.1 输入规则汇总

任务输入形状预处理模板中input的值
对象 schema(dict)无dict,如{{ input.field_name }}、{{ input.items() }}
数组 schema(list)无list,如{{ input[0] }}、{{ input \| length }}
纯文本(str)先json.loads(...),成功用解析值、失败用原串解析后的 dict/list/标量,或失败时的原始字符串

5.2 输出契约

render_input_transform(...)返回str,该字符串成为发送给模型的首条用户消息正文,替代原本会传给 prompt 构建器/格式化器的输入。注意:

  • 输出内容不限——即使是恰好合法的 JSON 字符串,对模型而言仍是文本,V1 不区分"字符串 vs JSON"输出类型;
  • 空字符串允许(模型收到空首条用户消息),无特殊处理;
  • 变换不产生、不修改系统提示词(来自prompt_id)、后续多轮用户消息、助手消息或对话其他部分。

6. 运行时执行与 TaskRun.input 的持久化语义

6.1 执行顺序

在同步路径_run_returning_run_output与流式路径_prepare_stream(base_adapter.py)中,变换的运行顺序为:

  1. 输入 schema 校验(既有行为,变换不能绕过它);
  2. 若run_config.input_transform非空:构造命名空间(§5.1 规则)→ 调用render_input_transform得到字符串 → 以该字符串作为模型面对的首条用户消息;
  3. provider 专属格式化器(既有行为,此时看到的是字符串,与无变换的纯文本任务路径完全一致);
  4. 推理(既有行为)。

input_transform is None时第 2 步整体跳过,恒等路径保持原样。

6.2 关键实现细节:原输入与模型输入分离

规格特意强调一个易错点:TaskRun.input永远保存原始未变换输入。因为渲染结果是衍生值(原输入 + 变换可随时重算),变换不可逆(若只存渲染串则无法还原原始输入),而且对对象/数组 schema 任务而言渲染串几乎不可能通过任务input_json_schema校验——把它写进TaskRun.input会破坏 schema 不变量。

因此 Phase 2 的接线方式(真实实现见 base_adapter.py)是:model_input = self._apply_input_transform(input),把model_input交给格式化器,而原始input继续流向generate_run用于持久化。_apply_input_transform辅助方法(base_adapter.py)把"渲染变换"这一逻辑收敛到一处,同步与流式两条路径共用,未来的变换变体只需更新这一处:

def _apply_input_transform(self, input: InputType) -> InputType: if not isinstance(self.run_config, KilnAgentRunConfigProperties): return input transform = self.run_config.input_transform if transform is None: return input try: return render_input_transform(transform, input) except Exception as e: raise ValueError(f"Input transform failed: {e}") from e

注意isinstance(self.run_config, KilnAgentRunConfigProperties)守卫:MCP 运行配置没有input_transform字段,直接原样返回输入,MCP 执行路径零改动。

渲染后的字符串经格式化器进入既有的 trace 机制,落在TaskRun.trace[0](首条用户消息),因此无需为 TaskRun 新增任何字段即可恢复"模型实际看到了什么"。

7. 错误处理:保存期硬校验,运行期硬失败

所有变换错误都发生在推理之前。规格的错误处理表如下:

失败场景发生时机呈现方式
模板语法错误保存期PydanticValidationError(保存被拒绝)
模板语法错误(防御性,正常不会发生)运行期原样重新抛出
UndefinedError(缺失变量)运行期重新抛出;不发起推理
过滤器等渲染异常运行期重新抛出;不发起推理
沙箱SecurityError运行期重新抛出;不发起推理

适配器不捕获这些异常:它们从_run_returning_run_output/_prepare_stream中冒出。真实实现中_apply_input_transform会把渲染异常统一包装为带说明的ValueError(f"Input transform failed: {e}")。规格特别指出:由于变换运行在try包装区之前,KilnRunError包装不适用于变换错误——它们以普通 Python 异常呈现给调用方,因为变换错误属于"配置/创作错误"而非"运行执行错误",不应与模型失败混为一谈(这与既有的 schema 校验错误同待遇)。

设计哲学一句话:fail loud, fail fast, fail attributable(响亮失败、快速失败、可归因失败)。需要更软语义的消费方(如 eval v2 的 skip-with-reason)应在调用前用自己的预检查包装该 API,通用任务路径保持硬失败。

8. 安全模型:模板是 Jinja2 语法,不是 Python 代码

威胁模型:模板是用户编写的内容,来源从本地用户(低风险)到共享/社区数据集(高风险)不等。规格 §13 的安全承诺(真实实现由SandboxedEnvironment承载)非常明确:

模板能做到:

  • Jinja2 语法:{{ ... }}替换、{% if %}/{% for %}块、{% set %}等;
  • 内置过滤器与测试:| default、| length、| join、| upper、| tojson、is defined、is none等;
  • 对传入数据的属性访问与索引:{{ input.field }}、{{ input[0] }};
  • 作用域内值的算术与字符串拼接。

模板做不到:

  • 无任意 Python 执行:没有{% python %}块、没有eval(),模板语法除 Jinja2 自身暴露的过滤器/测试外无法定义或调用 Python 函数;
  • 无文件 I/O:无open(...)/read()/Path(...),模板只从内联字符串渲染,不配置FileSystemLoader;
  • 无网络 I/O:无requests/urllib/socket;
  • 无 shell 访问:无subprocess/os.system/popen;
  • 无import:模板无法导入 Python 模块;
  • 无法访问 dunder 属性(__class__、__bases__、__subclasses__、__globals__等)——SandboxedEnvironment阻断已知的 Jinja2 SSTI 攻击链遍历;
  • 无副作用过滤器:V1 不注册任何自定义过滤器。

真实测试 test_jinja_engine.py 专门覆盖了沙箱违反场景:{{ input.__class__ }}、{{ input.__class__.__bases__ }}、{{ input.__class__.__mro__[1].__subclasses__() }}、{{ ''.__class__.__mro__[2].__subclasses__() }}全部断言抛出SecurityError。

明确记录的 V1 范围外风险:渲染无 CPU/内存上限——病态模板(如对超大生成器做{% for %})可能挂起渲染调用。V1 接受该风险,待真实出现问题再处理。

源码注记:真实实现还在两个环境中注册了两个内置过滤器fromjson(把 JSON 字符串解析为 Python 对象,解析失败抛JinjaExtractionError)与format_trace(把对话 trace 渲染为带角色标签的规范转录文本,与EvalTraceFormatter保持一致)。这两个过滤器是规格之外、由消费方需求演化出的补充,分别服务于 eval v2 的表达式提取与 LLM 可读的 trace 渲染(相关测试见 test_jinja_engine.py 与 test_jinja_engine.py)。

9. 引擎层单元测试:test_jinja_engine.py

Phase 1 要求引擎测试覆盖规格 §7.1 的全部用例。真实实现 test_jinja_engine.py 分门别类组织如下(均可作为行为契约的活文档):

  • 编译校验(TestCompileTemplateOrRaise、TestCompileExpressionOrRaise):合法模板/表达式静默通过;{% if、未闭合块{% for x in items %}、foo ||| bar等语法错误抛带"line"/"Invalid Jinja2 template"/"Invalid Jinja2 expression"信息的ValueError;
  • 渲染(TestRenderInputTransform):
    • dict 输入属性访问:{{ input.foo }}+{"foo": 1}→"1";多字段{{ input.a }} and {{ input.b }};
    • list 输入索引与过滤器:{{ input[0] }}→"1"、{{ input | length }}→"3";
    • 字符串输入:{{ input }}+"hello"→"hello";
    • 纯文本 JSON 自动解析:{"foo": 1}、[10, 20]、"42"三种形状分别走{{ input.foo }}、{{ input[0] }}、{{ input }};
    • 非 JSON 回退:"not json"→"not json";
    • 缺失属性:{{ input.missing }}+{"foo": 1}抛UndefinedError;
    • 沙箱违反:dunder 访问抛SecurityError(见 §8);
    • 空模板返回空串;未知变换变体抛ValueError;
  • 表达式提取(TestExtract):返回普通值(int/str/list/dict);缺失键返回Undefined实例(而非None、而非抛异常);显式None返回None;生成器物化为列表(data | map(attribute='x') | list→[1, 2],无list过滤器时也物化);嵌套属性访问;畸形表达式抛ValueError;
  • 对缺失值操作的边界(TestExtractOperationsOnMissingData):missing | int、missing + 1、(final_message | fromjson).user.status(解析出的 JSON 无该字段)、trace[-1].tool_calls[0].function.name(越界索引)等强制求值操作抛JinjaExtractionError且保留 Jinja 原始消息;而final_message ~ typo这类~拼接把Undefined字符串化为''的静默操作被专门测试钉住;
  • 空白控制(TestTrimAndLstripBlocks):trim_blocks去掉块标签后换行、lstrip_blocks去掉块前缩进、两者组合的代表性模板输出;
  • 表达式变量提取(TestExpressionVariables):expression_variables返回表达式读取的命名空间变量集合(final_message、trace、outpt.status等),用于消费方预检查;
  • 规范表达式(TestCanonicalExpressions):eval UI 的规范示例表达式(从 JSON 取字段、截断长输出、取 trace 最后消息、大写、统计消息数、取工具调用名、JSON 数组长度)全部可运行。

10. 数据模型测试:test_input_transform.py 与 test_run_config.py 扩展

10.1 datamodel/test_input_transform.py

真实实现 test_input_transform.py 覆盖规格 §7.2:

  • JinjaInputTransform(template="{{ input }}")构造成功,type == "jinja";带块的模板也合法;
  • 畸形模板{% if、未闭合块抛 PydanticValidationError(消息含"Invalid Jinja2 template");
  • .model_dump()后model_validate重建相等;model_dump_json()后model_validate_json相等(JSON 序列化往返);
  • 判别联合:{"type": "jinja", "template": "..."}分派到JinjaInputTransform;缺type键、未知type(如"unknown")抛ValidationError;模型实例直接验证通过;经TypeAdapter往返一致。

10.2 datamodel/test_run_config.py 扩展

真实实现新增TestInputTransformField(test_run_config.py 附近),覆盖规格 §7.3:

  • 不带input_transform构造时默认None;
  • 传入JinjaInputTransform(template="{{ input.name }}")被接受且类型正确;
  • 传入{"type": "jinja", "template": "{{ input }}"}dict 时判别器正确分派;
  • 向后兼容回归:无input_transform键的旧 JSON 加载后input_transform is None;
  • 负向测试:McpRunConfigProperties没有input_transform属性(not hasattr(config, "input_transform"));
  • 带变换的配置往返(model_dump→ 重建)保持相等。

11. 完整测试清单(Phase 1 验收标准)

按规格 phase_1.md 的 Tests 小节整理,全部用例在当前仓库中均有对应实现:

引擎测试(utils/test_jinja_engine.py)

  • test_compile_template_valid/test_compile_template_syntax_error(带行号信息)
  • test_compile_expression_valid/test_compile_expression_syntax_error
  • test_render_dict_input:{{ input.foo }}
  • test_render_list_input:{{ input[0] }}/{{ input | length }}
  • test_render_string_input:{{ input }}
  • test_render_plaintext_json_dict:JSON 字符串自动解析为 dict
  • test_render_plaintext_json_fallback:非 JSON 字符串回退原串
  • test_render_undefined_error:缺失属性抛UndefinedError
  • test_render_sandbox_violation:dunder 访问抛SecurityError
  • test_extract_returns_value/test_extract_missing_key_returns_undefined/test_extract_explicit_none
  • test_extract_materializes_generators:生成器结果变 list
  • test_trim_lstrip_blocks:trim_blocks/lstrip_blocks空白行为

数据模型测试(datamodel/test_input_transform.py)

  • test_jinja_input_transform_valid/test_jinja_input_transform_invalid(ValidationError)
  • test_jinja_input_transform_roundtrip:model_dump+ 重建相等
  • test_discriminated_union_dispatch:{"type": "jinja", ...}分派到JinjaInputTransform

RunConfig 扩展(datamodel/test_run_config.py)

  • test_run_config_input_transform_default_none
  • test_run_config_input_transform_accepts_valid
  • test_run_config_input_transform_dict_dispatch
  • test_run_config_backcompat_no_input_transform:旧 JSON 加载后为None
  • test_mcp_no_input_transform:McpRunConfigProperties无该字段

此外,规格架构文档还预留了适配器集成测试清单(Phase 2,见 test_base_adapter.py 的TestInputTransformIntegration):对象 schema 任务渲染串进入TaskRun.trace首条消息且TaskRun.input保留原始 dict、纯文本 JSON 与非 JSON 两条路径、数组 schema 的{{ input[0] }}、transform=None恒等路径字节级一致、流式路径与同步路径同构、UndefinedError在推理前抛出(mock provider 断言从未被调用)、MCP 配置行为不变。

12. 边界与未来钩子

规格明确列出 V1 范围外事项,避免误用:

  • 渲染无 CPU/内存上限(V1 接受的风险);
  • V1 无编译结果缓存(引擎 API 设计已保证未来可在模块内部加 LRU 而不破坏调用方);
  • V1 无自定义 Jinja2 过滤器(仅内置过滤器;真实实现后来为消费方补充了fromjson与format_trace两个无副作用过滤器,属实现演化而非违背 V1 约束);
  • 无 eval 侧代码(由未来的 eval v2 项目消费);
  • 无变换编写 UI(未来项目);
  • 不公开暴露_build_namespace(未来按需提升)。

一句话总结 Phase 1 的验收姿态:引擎、数据模型、字段落位三者独立可测,全部测试无需触碰任何适配器即可运行;实现顺序上,先加依赖与锁文件、再写引擎与引擎测试、再写数据模型与数据模型测试、最后落字段并扩展test_run_config.py,最后跑uv run ./checks.sh --agent-mode验证全量检查。

  • 人工智能
  • AI 应用
  • AI 评测
  • RAG
  • 微调
  • 提示工程
  • 开发工具
  • 桌面应用

【免费下载链接】Kiln

Build, 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
点击查看免费下载

相关推荐

上一篇:基于 Crossplane 的 Exoscale 云资源可视化设计:Meshery 集成模型实战指南
下一篇:Android图表无障碍焦点处理:MPAndroidChart的键盘导航支持

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 1:51:14

ModuleNotFoundError: No module named ‘skimage‘ 的排查与修复指南

先说结论&#xff1a;ModuleNotFoundError: No module named skimage这个报错&#xff0c;绝大多数情况下并不是 pip install 命令本身在执行过程中弹出来的&#xff0c;而是代码运行到from skimage import ...这一句时&#xff0c;Python 解释器在已安装的模块目录里翻了个底朝…

作者头像 李华
网站建设 2026/10/18 12:04:22

AI编程API中断应对指南:安全、可观测性与弹性架构

1. 事件起点&#xff1a;一个被突然切断的API连接我第一次意识到不对劲&#xff0c;是在某个周三下午三点十七分。当时正在调试一个跨平台文档协同工具的代码补全模块&#xff0c;后端调用的是 Claude Code 的 API 接口。前一秒还在稳定返回带注释的 Python 函数片段&#xff0…

作者头像 李华
网站建设 2026/10/19 16:56:04

siamfC项目拆解:孪生网络与特征比较实战指南

很多人第一次看到“siamfC”这个项目代号时&#xff0c;第一反应都是愣一下。小写开头、中间夹个大写F、末尾再来个大写C&#xff0c;既不像是标准的驼峰命名&#xff0c;也看不出某个单词的直接缩写&#xff0c;乍一看还以为是随手敲键盘敲出来的乱码。但如果你手上的确拿到了…

作者头像 李华
网站建设 2026/10/18 4:26:54

Git源码代管理实战:从版本控制到协作操作系统

1. 这不是“又一个Git教程”&#xff0c;而是你真正能用起来的源码管理实战手册 我带过不少刚接触代码协作的新手&#xff0c;也帮某高校实验室重构过三个跨年级团队的项目交付流程。最常听到的一句话是&#xff1a;“Git命令背了一堆&#xff0c;一到实际改需求、合代码、回滚…

作者头像 李华
网站建设 2026/10/18 12:12:26

智能助手开发:Task、Session、Action 分离管理实战

1. 从一个真实翻车现场说起去年我参与过一个内部工具的开发&#xff0c;核心功能是让一个智能助手帮用户处理多步骤的文档整理任务。上线第一周就收到一条反馈&#xff1a;用户上午让助手整理了一份会议纪要&#xff0c;中途去开会了&#xff0c;下午回来接着聊&#xff0c;助手…

作者头像 李华