组件系统设计:契约、兼容与动态表单
学习目标
- 理解 Component 基类的输入/输出契约
- 掌握契约冻结清单:哪些属性一旦发布不可修改
- 学会使用
legacy=True+replacement安全替换旧组件 - 了解组件自动发现注册机制
前置知识
- 三层包架构与依赖边界 — 组件位于 lfx 层
项目实践
1. Component 基类
Component(src/lfx/src/lfx/custom/custom_component/component.py,2015 行)是所有组件的基类:
核心属性:
| 属性 | 含义 | 是否冻结 |
|---|---|---|
name = "..." | Flow JSON 中的组件标识符 | 冻结 |
| 类名 | Python 类标识符 | 冻结 |
inputs | 输入字段列表 | 冻结(不可移除或重命名) |
outputs | 输出字段列表及顺序 | 冻结(不可重排序) |
display_name | UI 显示名称 | 可变(不影响 Flow JSON) |
description | 组件描述 | 可变 |
icon | 组件图标 | 可变 |
default | 输入默认值 | 视为冻结(改变默认影响已有 Flow) |
2. 契约冻结清单
Langflow 的 CONTRACTS.md 定义了严格的用户契约。以下是看似无害但实际是回归的操作:
| 操作 | 为什么是回归 |
|---|---|
重命名 input_value → text | 100% 的保存 Flow 中引用该输入的连线丢失 |
重排 outputs = [a, b] → [b, a] | 使用默认选择的旧 Flow 现在路由到错误的输出 |
收紧 Output(types=["Message", "Data"]) → ["Message"] | 解析为 Data 的边在加载时失效 |
改变 default="gpt-4o-mini" → default="gpt-5" | 静默更改用户计费 |
| 移动组件文件路径 | 用户 DB 中存储的 from langflow.components... 加载失败 |
3. 向后兼容:legacy=True 模式
需要修改组件时,不要就地修改,而是添加新组件 alongside:
# 旧组件(保留)class OldCalculatorComponent(LCToolComponent): name = "Calculator" legacy = True replacement = ["helpers.CalculatorComponent"] # ... 原有代码不动
# 新组件(添加)class CalculatorComponent(LCToolComponent): name = "Calculator" display_name = "Calculator" # ... 新的实现UI 行为:
- 旧组件在组件面板中标记为 “Legacy”,引导用户使用新版本
- 画布上已存在的旧组件正常工作,不受影响
- 用户看到 “Updates Available” 提示,可迁移到新组件
4. 组件自动发现
组件按类别存放在 src/lfx/src/lfx/components/<category>/ 中:
components/├── openai/ # OpenAI 集成├── anthropic/ # Anthropic 集成├── vectorstores/ # 向量存储├── tools/ # 工具组件├── input_output/ # IO 组件├── helpers/ # 辅助组件└── __init__.py # 按字母序导入所有子目录注册机制:
- 每个 category 的
__init__.py按字母序导入所有组件类 - 启动时,
DirectoryReader扫描所有组件目录 - 每个组件类通过反射提取
display_name、description、icon、输入输出 schema - 生成
component_index.json供前端组件面板使用
放置规则:
- Vendor 特定代码 →
openai/,anthropic/,datastax/等 vendor 目录 - 通用辅助组件 →
helpers/,processing/,logic/ - IO 原语 →
input_output/ - 禁止自创新 category(UI sidebar 可见,属于产品面)
5. 动态表单:update_build_config
def update_build_config( self, build_config: dict, # 当前表单配置 field_value: Any, # 变更后的字段值 field_name: str, # 变更的字段名) -> dict: """根据字段值变化动态修改表单配置。""" if field_name == "model_name": if field_value.startswith("o"): build_config["temperature"]["hidden"] = True else: build_config["temperature"]["hidden"] = False return build_config问题与规避
| 陷阱 | 现象 | 对策 |
|---|---|---|
忘记设置 name | 类名变化导致 Flow JSON 无法匹配 | 始终显式设置 name = "...",不依赖 __class__.__name__ |
replacement 格式错误 | UI 不显示替换提示 | 使用 ["category.NewClassName"] 格式(带 category 前缀) |
| 新组件忘记继承旧行为 | 旧 Flow 加载后行为异常 | 新组件必须保持与旧组件兼容的输入输出行为 |
组件文件未更新 file_names_mapping | 测试通过但用户 DB 加载失败 | 组件文件路径变更时更新所有测试版本的 file_names_mapping |
设计取舍
就地修改 vs legacy 替换:
| 维度 | 就地修改 | legacy 替换 |
|---|---|---|
| 维护成本 | 低 | 高(需要维护两套代码) |
| 用户影响 | 已有 Flow 静默破坏 | 已有 Flow 正常,用户自行迁移 |
| 回滚难度 | 高(需要 DB 迁移) | 低(删除新组件即可) |
| 用户感知 | 突然出错,无提示 | ”Updates Available” 引导迁移 |
Langflow 选择 legacy 替换的核心原因是其十元原则之一:「Flow 是用户产物,不是实现细节」。Flow JSON 运行在生产环境中,向后兼容不可谈判。
参考来源
- 源码:
src/lfx/src/lfx/custom/custom_component/component.py - 组件开发指南:
docs/agents/COMPONENTS.md - 用户契约:
docs/agents/CONTRACTS.md