跳到主要内容

数据截至 (上游 commit 7a975c596eca)

组件生态与对外出口

30 秒导读: 这一章收尾,讲两件事。组件从哪来——Langflow 内置 354 个组件,如果启动时挨个 import 会慢到不能用,它的解法是把组件模板在构建期烤成一份 6MB 的 JSON 索引,运行时只读 JSON。流往哪去——一条画布上的流,可以变成 Agent 手里的 tool、变成外部 AI 客户端能连的 MCP 服务器、或者脱离数据库被一行 CLI 单独跑起来。

前五章讲的是"一条流怎么在 Langflow 里跑起来"(组件模型建图调度环与分支运行时与事件)。这一章讲边上的两条线:进来的组件出去的流


1. 先看规模:为什么这事必须认真对待

内置组件不是"几十个类"的量级。下面是这个 commit 上的真实数字:

指标数量数据来源
lfx/components/ 下的顶层集成目录109src/lfx/src/lfx/components/
组件 Python 源文件515同上,递归统计 *.py
预生成索引里的组件条目354_assets/component_index.jsonmetadata.num_components
索引里的类别数95同上,metadata.num_modules
索引文件体积6.27 MB同上

这 109 个目录里躺着 openaianthropicpineconemilvusmongodbcomposiocrewainotionfirecrawl……每一个都可能 import 一个重量级第三方 SDK。

核心矛盾一句话: 用户打开画布,前端要一次性拿到所有组件的表单定义(/api/v1/all),但服务端没法为此在启动时把 515 个模块全 import 一遍——那意味着几十秒的冷启动,而且任何一个可选依赖没装就可能炸掉整个启动。

Langflow 的答案分三层,下一节展开。


2. 顶层全景:组件从磁盘到画布的三条路

这张图从上往下读,是生产模式的降级顺序:命中即停,全都不行才走最贵的那条。

需要 all_types_dict(组件模板全集)


┌──────────────────────────┐
│ ① 预生成索引(内置 JSON) │ ← 最快,纯读文件
│ _assets/component_ │ 过 SHA256 + 版本双校验
│ index.json │
└───────────┬──────────────┘
│ 校验不过 / 文件不在

┌──────────────────────────┐
│ ② 用户缓存目录里的索引 │ ← 上次动态生成后存的
│ ~/.cache/lfx/… │ 同样过双校验
└───────────┬──────────────┘
│ 也没有

┌──────────────────────────┐
│ ③ 动态扫描 + 并发 import │ ← 最贵,走完还会把
│ pkgutil.walk_packages │ 结果写回②
└──────────────────────────┘

三条路的入口是同一个函数:import_langflow_componentssrc/lfx/src/lfx/interface/components.py:591)。它先问一句"现在是不是开发模式",再挑策略:

# src/lfx/src/lfx/interface/components.py:565-573(真实源码,节选)
dev_mode_enabled, target_modules = _parse_dev_mode()

if dev_mode_enabled and not target_modules:
modules_dict, index_source = await _load_full_dev_mode()
elif dev_mode_enabled and target_modules:
modules_dict, index_source = await _load_selective_dev_mode(settings_service, target_modules)
else:
modules_dict, index_source = await _load_production_mode(settings_service)

这段是整章第一个要记住的分叉点:开发模式动态重建、生产模式读索引


3. 核心机制一:预生成索引怎么被信任

3.1 它要解决的小问题

"读一个 JSON 就当组件定义"听起来很省事,但立刻带来两个风险:JSON 可能是坏的(下载中断、磁盘损坏),JSON 可能是旧的(升级了 lfx 但索引还是上一版烤的)。

_read_component_indexsrc/lfx/src/lfx/interface/components.py:156)就是这两个风险的守门人。

3.2 三道校验,任一不过就返回 None

读到 blob

├─ 解析失败?────────────► 返回 None(warning 日志)

├─ 没有 sha256 字段?────► 返回 None("may be tampered")

├─ 重算 sha256 对不上?──► 返回 None(warning)

└─ blob["version"] ≠ 装机 lfx 版本?──► 返回 None(debug 日志)


返回 blob

完整性校验的真实写法(src/lfx/src/lfx/interface/components.py:210-223):

tmp = dict(blob)
sha = tmp.pop("sha256", None)
if not sha:
logger.warning("Component index missing SHA256 hash - index may be tampered")
return None
calc = hashlib.sha256(orjson.dumps(tmp, option=orjson.OPT_SORT_KEYS)).hexdigest()

关键细节: 校验时把 sha256 字段先弹出去再算,而且用 orjson.OPT_SORT_KEYS 排序序列化——这和构建脚本 scripts/build_component_index.py:158 的算法必须逐字节一致,否则永远校验不过。构建脚本为此还做了两件事:_strip_dynamic_fields 剥掉 timestamp/deprecated_at 这类会随依赖更新而变的字段(scripts/build_component_index.py:58),_normalize_for_determinism 递归排序字典键(:28)。目的是让索引可复现、git diff 干净。

版本比对紧随其后(src/lfx/src/lfx/interface/components.py:226-240):拿 importlib.metadata.version("lfx") 和索引里的 version 比。有意思的是它对"查不到版本"是宽容的——Docker workspace 安装时 lfx 可 import 但没有 dist-info,这时跳过版本检查而不是拒绝。

"静默回退"的准确说法: 校验失败不抛异常、不阻断启动,只是让 _load_from_index_or_cache:347)继续往下走。日志级别有差别——SHA 不符是 warning,版本不符只是 debug,所以版本漂移在默认日志级别下确实是"悄悄"发生的。

3.3 缓存:动态生成的结果会被存起来

走到第③条路(动态扫描)以后,_load_production_mode:624)会把结果落盘:

# src/lfx/src/lfx/interface/components.py:642-648
modules_dict = await _load_components_dynamically(target_modules=None)
index_source = "dynamic"

# Save to cache for future use
if modules_dict:
await logger.adebug("Saving generated component index to cache")
_save_generated_index(modules_dict)

落盘路径由 _get_cache_path:248)算出,用 platformdirs.user_cache_dir("lfx", "langflow")——macOS 上是 ~/Library/Caches/lfx,Linux 上是 ~/.cache/lfx_save_generated_index:257)把 modules_dict 转成和内置索引一样的 entries 结构,算 SHA256,写文件。

这里曾经有一处不对称,新版已修复: 早期 _save_generated_index 写入用 version("langflow"),而 _read_component_index 读回比对 version("lfx")——两包版本一旦分叉,写出的缓存就会被自己的版本检查判死,永远回退动态扫描。新版两侧统一为 version("lfx")(写 components.py:276、读 components.py:230),隐患消除;当前两个包版本也同步在 1.12.0(pyproject.toml:3src/lfx/pyproject.toml:3)。


4. 核心机制二:真要动态扫描时怎么扛住

4.1 思路

动态扫描是最贵的路,但它必须存在(首次安装、索引损坏、开发模式)。设计目标是尽量并行 + 单个模块炸了不影响全局

_load_components_dynamicallysrc/lfx/src/lfx/interface/components.py:430)的骨架:

pkgutil.walk_packages(lfx.components)

├─ 跳过 "deactivated" 目录
├─ 跳过 Astra Cloud 禁用组件
└─ dev 模式选择性过滤(只留 target_modules)


_warm_circular_imports() ← 先单线程预热,见下


asyncio.gather(asyncio.to_thread(_process_single_module, m) for m in 模块列表)


逐个合并结果;单个模块的异常只 warning,不中断

4.2 精华:为什么要先"预热循环导入"

这是全章最不显然的一个设计。_warm_circular_imports:370)的 docstring 把死锁讲得很清楚,值得完整理解:

  • 第三方包 toolguard.runtime(包 __init__)和 toolguard.runtime.runtime(子模块)互相 import
  • 这个环在单线程首次导入时能正常解开。
  • 但 lfx 有两个组件模块从不同入口进入这个环:policies.tool_invoker 从包进,policies.guard_sync_utils 从子模块进。
  • 一旦这两个模块落在 asyncio.to_thread 扇出的两个不同工作线程上同时执行:线程 A 持包锁等子模块锁,线程 B 持子模块锁等包锁 → CPython 导入机制抛 _DeadlockError

解法很朴素——扇出之前先单线程 import 一遍,把它们塞进 sys.modules

# src/lfx/src/lfx/interface/components.py:390-394
for modname in MODULES_WITH_INTERNAL_CIRCULAR_IMPORTS:
with contextlib.suppress(ImportError):
importlib.import_module(modname)

MODULES_WITH_INTERNAL_CIRCULAR_IMPORTS 是个硬编码常量(:45),只有两项。这条经验可以带走:任何"并发 import 一大堆第三方模块"的代码,都要提前把已知的循环导入单线程烤热。

4.3 单模块处理:失败被吞掉

_process_single_module:583)对每个模块做三件事:import、找出本模块里定义的组件类、逐个实例化并 create_component_template。两处容错值得注意:

# :592-598
try:
module = importlib.import_module(modname)
except Exception as e: # noqa: BLE001
# Catch all exceptions during import to prevent component failures from crashing startup
# TODO: Surface these errors to the UI in a friendly manner
logger.error(f"Failed to import module {modname}: {e}", exc_info=True)
return None

以及实例化失败的批量汇报(:637-645):失败的类被收进 failed_count 列表,最后打一条 warning 说"跳过了 N 个组件类"。

代价是诚实的: 一个组件因为缺依赖而消失,用户在画布上看到的只是"这个组件不见了",源码里那句 TODO: Surface these errors to the UI 还挂着。

4.4 LFX_DEV 的三态

_parse_dev_mode:87)把一个环境变量解析成三种行为:

LFX_DEV 取值返回实际行为用途
未设置 / 空(False, None)生产模式,读索引默认
1 / true / yes(True, None)全量动态重建(515 个文件)改了框架层代码
0 / false / no(False, None)显式关闭覆盖上层配置
mistral,openai(True, ["mistral","openai"])读索引,动态重载这两个模块改单个集成,最常用

选择性模式的实现是"先读索引垫底、再用动态结果覆盖"(_load_selective_dev_mode:488-514):

modules_dict, _ = await _load_from_index_or_cache(settings_service)
dynamic_modules = await _load_components_dynamically(target_modules=target_modules)
for top_level, components in dynamic_modules.items():
modules_dict.setdefault(top_level, {}).update(components) # 语义等价的简化写法

这是个很实用的开发体验设计:改 openai 组件时你只付 openai 那几个文件的 import 代价,其余 350 多个组件仍然是秒读的。


5. 缓存对象与合并顺序

5.1 ComponentCache 装了什么

ComponentCachesrc/lfx/src/lfx/interface/components.py:56)是个单例(:69 component_cache = ComponentCache()),五个字段:

字段类型干什么
all_types_dictdict | None组件模板全集,/api/v1/all 的数据源
fully_loaded_componentsdict[str, bool]懒加载模式下"哪些已经补全过了"
type_to_current_hashdict[str, set[str]] | None组件类型 → 合法代码哈希集合
all_known_hashesset[str] | None所有已知代码哈希的扁平集合
code_by_hashdict[str, str] | None哈希 → 服务端可信源码

后三个是安全用的,第 7 节会用到。注释里点明了 None 的语义:None 表示"还没加载"(fail-closed),{} 表示"加载完了但没找到组件":58-60)。这个区分不是洁癖——校验代码要靠它来决定是"拒绝执行"还是"允许通过"。

5.2 合并三个来源

get_and_cache_all_types_dict:1109)是唯一的构建入口,合并顺序写死在这里:

# src/lfx/src/lfx/interface/components.py:1143-1147
component_cache.all_types_dict = {
**langflow_components["components"], # 内置
**custom_flat, # LANGFLOW_COMPONENTS_PATH 下的旧式自定义组件
**extension_components, # Extension System 的 bundle
}

后者覆盖前者:扩展 bundle 赢,注释说得明白——"a manifest-shipping bundle supersedes any same-named legacy entry"。构建完立刻调 _build_code_hash_lookups:1152)把安全用的三张表算出来。

整个构建被 if component_cache.all_types_dict is None 包着,所以一个进程只会构建一次。启动时由 langflow/preload.py:237langflow/main.py:403,407 预热。

5.3 扩展 bundle 的遮蔽仲裁

import_extension_components:883)从四个来源发现 bundle:

来源加载函数槽位典型场景
installedload_installed_extensions()@officialpip 装的、带 extension.json 的发行包
seedload_seed_extensions()@officialDocker 镜像烤进 /opt/langflow/bundles
devload_dev_extensions()@officiallfx extension dev 注册的
inlinediscover_inline_bundles(...)@extraLANGFLOW_COMPONENTS_PATH 下的子目录

四个来源可能提供同名 bundle_resolve_bundle_shadowing:773)用一个写死的优先级解决:

# src/lfx/src/lfx/interface/components.py:770
_DISCOVERY_PRECEDENCE: tuple[str, ...] = ("installed", "seed", "dev", "inline")

算法两遍:第一遍按优先级顺序 setdefault 选出每个 bundle 名的赢家;第二遍把所有输家的 components 清空并附上一条带路径的 ExtensionError 警告。

妙在哪: 注释(:930-940)解释了不做仲裁会踩什么坑——registry 按 bundle 名做键,最后写入的赢(last-wins by iteration order),于是 reload 端点会去遍历赢家的路径,而运维正在编辑另一份拷贝,症状是"reload 成功但什么都没变"的空 delta bug。仲裁把这个隐性冲突变成了一条能看见路径的显式警告。

代码里还保留了两个错误码:seed-bundle-shadowed(只用于 installed 遮蔽 seed 这一对,为了不破坏已有的 CLI 告警白名单和快照测试)和新的通用 bundle-shadowed。这是"改进不破坏既有契约"的一个小样板。


6. "懒加载"到底懒在哪(最容易误解的一点)

这是本节的重点澄清:读索引不等于"组件类没被加载",而是"组件类根本不需要在这时候被加载"。

索引里存的是每个组件的前端节点模板——显示名、输入字段、输出定义,以及最关键的一项:组件自己的源码字符串template.code.value)。有了这些,服务端就能画出画布、能做类型校验,全程不需要 import 任何一个组件模块。

真正 import 发生在执行的时候,而且方式很反直觉:

# src/lfx/src/lfx/interface/initialize/loading.py:44-53(真实源码,节选)
custom_params = get_params(vertex.params)
code = custom_params.pop("code")
class_object: type[CustomComponent | Component] = eval_custom_component_code(code)
custom_component = class_object(_user_id=user_id, _parameters=custom_params, _vertex=vertex, ...)

eval_custom_component_codesrc/lfx/src/lfx/custom/eval.py:9)只有三行:抽类名、create_class 编译执行。

所以: 一个节点跑起来时,Langflow 执行的是这个节点自己 JSON 里存的那段代码,不是 lfx.components.openai.xxx 这个模块。内置组件和用户自定义组件在执行路径上完全没有区别。这一个事实同时解释了:

  • 为什么可以只读 JSON 就把服务跑起来(模块 import 被推迟到单个节点执行时)。
  • 为什么用户改了一个组件的代码,这条流以后就一直用改过的版本(代码跟着流走)。
  • 为什么 allow_custom_components 这个开关必须存在(第 7 节)。

6.1 另一个"懒加载":lazy_load_components

配置项 lazy_load_componentssrc/lfx/src/lfx/services/settings/groups/components.py:40,默认 False)是另一回事,容易和上面混。看 _determine_loading_strategysrc/lfx/src/lfx/interface/components.py:695):

if settings_service.settings.lazy_load_components:
component_cache.all_types_dict = await aget_component_metadata(settings_service.settings.components_path)
elif settings_service.settings.components_path:
custom_paths = [p for p in settings_service.settings.components_path if p != BASE_COMPONENTS_PATH]
...

它只作用于 components_path——也就是用户自己那些目录下的组件,跟 354 个内置组件无关。开启后 aget_component_metadata:1162)只扫目录、造出显示名/类型这种骨架,并打上 lazy_loaded: True。真要用时由 ensure_component_loaded:1296)→ load_single_component:1335)补全成完整模板。

load_single_component 的异常处理写得很啰嗦(七个 except 分支分别对应 import 错误、结构错误、文件缺失、配置非法、数据结构错误、运行时错误、OS 错误),全部返回 None 而不抛——同样是"一个坏组件不能拖垮全局"的思路。


7. 核心机制三:自定义组件 = 把字符串变成类

7.1 两条路,一定要分清

用户在 UI 的代码编辑器里敲组件代码,会走到两个端点。它们做的事不一样

端点位置做什么执不执行用户代码
POST /api/v1/validate/codevalidate_code只查语法和 import 能不能找到不执行
POST /api/v1/custom_componentendpoints.py:1498真的建类、取出输入输出、返回前端节点执行
POST /api/v1/custom_component/updateendpoints.py:1577同上 + 应用参数变更、刷新 build config执行

7.2 validate_code:一个被修过的漏洞

validate_codesrc/lfx/src/lfx/custom/validate.py:32)现在只做两件事:ast.parse 拿语法错误,然后对每个函数定义只 compile 不 exec

源码里的注释把原因写得非常清楚(src/lfx/src/lfx/custom/validate.py:60-69):

# Security (GHSA-2wcq-pvw2-xh7v): this endpoint only
# *validates* code, but it previously compiled and exec()'d every function
# definition. Executing a function definition evaluates its decorators and
# default-argument expressions at definition time, so a payload such as
# def f(x=__import__("os").system("...")): ...
# achieves arbitrary code execution during "validation" — the function never
# has to be called.

这是很值得带走的一条: exec 一个 def 语句本身就是危险的——装饰器和默认参数表达式在定义时求值,函数根本不用被调用。任何"我只是校验一下"的代码路径都不能 exec

7.3 create_class:五步流水线

create_classsrc/lfx/src/lfx/custom/validate.py:194)是真正把字符串变成类的地方。

code (str)

│ 兼容性字符串替换(老 import 路径改写)+ 前置 DEFAULT_IMPORT_STRING

① ast.parse ──► module


② prepare_global_scope(module) ← 解析并真实 import 所有依赖,
│ 顺便 exec 掉类外的定义/赋值

③ extract_class_code(module, name) ← 只挑出目标 ClassDef 节点


④ compile_class_code(...) ← 把 __future__ 导入拼在前面再 compile


⑤ build_class_constructor(...) ← exec 到 exec_globals,返回类对象

②最重的一步。 prepare_global_scope:329)把模块体拆成四堆:imports__future__ 导入、import_froms、其余定义(ClassDef/FunctionDef/Assign/AnnAssign)。它对 import 做了两级兼容降级:

  • _get_module_fallbacks:311):langflow.* 找不到就试 lfx.*langchain.* 找不到就试 langchain_classic.*——只在 import 失败后才触发,所以 langchain 1.0 的新路径不会被误改。
  • _resolve_attribute:265):属性取不到就试当子模块 import,再不行对 langchain 试 classic 等价物。

然后它 exec 类外的所有定义(:421-425)——这是"要执行任意代码"这句话的第一处落点,用户在类外写的任何语句都在这里跑掉了。

③④的分工很讲究。 extract_class_code 只取 ClassDef 节点,compile_class_code:446)再把 __future__ 导入拼回去:

# src/lfx/src/lfx/custom/validate.py:456-457
body = (future_imports or []) + [class_code]
return compile(ast.Module(body=body, type_ignores=[]), "<string>", "exec")

原因是 from __future__ import annotations编译器指令,必须出现在被编译单元的开头才生效——把类单独抠出来编译会丢掉它,PEP 563 的延迟注解就失效了。

⑤是第二处执行点。 build_class_constructor:460exec 编译好的类体,然后把 exec_globals 里所有模块对象搬进当前模块的 globals(),最后返回类:

# src/lfx/src/lfx/custom/validate.py:471-483(节选)
exec_locals = dict(locals())
exec(compiled_class, exec_globals, exec_locals)
exec_globals[class_name] = exec_locals[class_name]

7.4 _MissingModulePlaceholder:缺依赖时不炸,留占位

有些组件依赖的 C 扩展包在某些平台没有 wheel(源码里举的例子是 Windows 上的 jq)。硬失败会导致整个类建不出来,连表单都渲染不了。

_MissingModulePlaceholdersrc/lfx/src/lfx/custom/validate.py:295)的做法是:塞一个假模块,任何属性访问才抛 ModuleNotFoundError

class _MissingModulePlaceholder:
def __init__(self, module_name: str) -> None:
self._module_name = module_name

def __getattr__(self, name: str):
msg = f"No module named '{self._module_name}'"
raise ModuleNotFoundError(msg)

触发条件卡得很死(:370-382):只在 sys.platform == "win32" 时插占位符,其他平台照常抛异常("the package should be installable, so raise to surface the real error")。

效果是:Windows 用户能看到并配置这个组件的表单,但一旦真跑到用 jq 的那行,就会得到一个清楚的报错,而不是一个莫名其妙的 AttributeError延迟失败到有意义的时刻——这个模式在插件系统里很好用。


8. 三道闸:谁被允许 exec

既然 Component(_code=...) 就是执行任意代码,端点前面必须有闸。custom_componentsrc/backend/base/langflow/api/v1/endpoints.py:1498)和 custom_component_update:1350)挂着同一套。

8.1 闸门判定图

请求带着 code 进来


_requires_component_hash_lookups(settings, user)? ← endpoints.py:85
│ 是(受限部署)

哈希表建好了吗?── 没有 ──► 503 "still initializing"(fail-closed)
│ 有

闸①: admin_only 且非超管 且 哈希不匹配 ──► 403 "restricted to administrators"
│ 过

闸②: not allow_custom_components 且 哈希不匹配 ──► 403 "creation is disabled"
│ 过

闸③: 用服务端可信源码替换客户端字节 ── 找不到 ──► 403(fail-closed)


Component(_code=effective_code) → 真正 exec

8.2 三个配置项各管什么

配置项默认定义位置语义
allow_custom_componentsTruesettings/groups/security.py:41False 时,只有代码哈希命中服务端已知模板的才放行
custom_component_admin_onlyFalsesettings/groups/security.py:56True 时,非超管只能刷新已知模板,不能写真正的自定义代码
allow_components_paths_overrideTruesettings/groups/security.py:60False 时,LANGFLOW_COMPONENTS_PATH 不再作为绕过 allow_custom_components 的白名单

判定这两个开关是否需要哈希表,收在一个小函数里(endpoints.py:101-105):

def _requires_component_hash_lookups(settings: object, user: CurrentActiveUser) -> bool:
requires_admin_only_hashes = (
getattr(settings, "custom_component_admin_only", False) is True and not user.is_superuser
)
return requires_admin_only_hashes or not settings.allow_custom_components

8.3 闸③是全章最精巧的一处

前两道闸靠"代码哈希在不在已知集合里"。但这个哈希是截断的——只取 SHA256 的前 12 个十六进制字符:

# src/lfx/src/lfx/utils/flow_validation.py:94-96
def _compute_code_hash(code: str) -> str:
return hashlib.sha256(code.encode("utf-8")).hexdigest()[:12]

12 个 hex ≈ 48 bit。这对"防误改"够用,对"防蓄意构造"不够——理论上可以构造第二原像撞上某个已知哈希,从而带着攻击代码通过闸②。

Langflow 的补丁不是加长哈希,而是通过闸之后不执行客户端的字节

# src/backend/base/langflow/api/v1/endpoints.py:1316-1325(节选)
effective_code = raw_code.code
if _requires_component_hash_lookups(settings, user):
effective_code = get_trusted_code_for_validation(raw_code.code)
if effective_code is None:
raise HTTPException(status_code=403, detail="Custom component creation is disabled")
component = Component(_code=effective_code)

get_trusted_code_for_validationsrc/lfx/src/lfx/utils/flow_validation.py:436)按同一个哈希去 component_cache.code_by_hash 里取服务端自己那份源码。这样即使真发生碰撞,跑起来的也只是服务端已知的组件,而不是攻击者的代码。

code_by_hash 的构建(collect_code_by_hash:170)自己也留了一手:

# src/lfx/src/lfx/utils/flow_validation.py:217-218
if _compute_code_hash(source) != code_hash:
continue

只信任"重算哈希等于它的键"的源码——防止一条畸形索引条目把任意代码挂到某个已知哈希下面。

这个模式值得单独记:哈希只用来"查表",不用来"背书"。 通过校验后执行的永远是本地可信副本,不是外来输入。

8.4 诚实说明边界在哪

把话说清楚:

  • 默认配置(allow_custom_components=True)下,任何通过认证的用户都可以在这台服务器上执行任意 Python。 没有沙箱、没有子进程、没有 seccomp——exec 直接跑在 Langflow 的进程里,拥有它全部的文件系统权限、网络权限和环境变量。
  • 官方 docstring 自己也这么说(settings/groups/security.py:54-55):"this is a beta feature. For security in a multi-tenant environment, use hardware-level isolation."
  • 边界靠的是部署形态(一人一容器),不是代码里的什么机制。上面那三道闸只有在你主动把 allow_custom_components 关掉之后才起作用,而关掉之后画布的自定义组件功能基本就没了——这是一个真实的取舍,不是"既要又要"。
  • 还有一个更宽的口子:流执行路径(instantiate_class)读的是节点里存的代码,所以导入一个别人给的流 JSON,等于同意执行里面每个节点的代码。这条路由 validate_flow_for_current_settings / check_flow_and_raiseflow_validation.py:463:363)用同一套哈希表把守,逻辑在 _get_invalid_components:225)里;那里也留着一条 CVE 注释(GHSA-mfp9-86w4-493f)——以前 type 为空的节点会被 continue 跳过,而它的代码照样会执行,现在改成直接 block。

9. 出口一:tool mode —— 任意组件变成 Agent 能调的工具

9.1 要解决的小问题

Agent 需要一组"工具"。Langflow 已经有 354 个组件,每个都是"给定输入产出输出"的函数——它们本来就是工具,缺的只是一层 LangChain StructuredTool 的包装和一份 JSON Schema。

9.2 触发条件

_handle_tool_modesrc/lfx/src/lfx/custom/custom_component/component.py:1342)在每次 _build_results 开头被调(:1298):

def _handle_tool_mode(self):
if (
hasattr(self, "outputs") and any(getattr(_input, "tool_mode", False) for _input in self.inputs)
) or self.add_tool_output:
self._append_tool_to_outputs_map()

条件是"至少一个输入声明了 tool_mode=True"。所以组件作者要做的只是在某个 Input 上打个标记——这就是"任意组件都能变工具"的入口。

_append_tool_to_outputs_map:2097)往输出表里插一条合成输出:

# src/lfx/src/lfx/custom/custom_component/component.py:2102-2103
def _build_tool_output(self) -> Output:
return Output(name=TOOL_OUTPUT_NAME, display_name=TOOL_OUTPUT_DISPLAY_NAME, method="to_toolkit", types=["Tool"])

常量在 src/lfx/src/lfx/base/tools/constants.py:3-5component_as_tool / Toolset / tools_metadata。画布上那根接到 Agent tools 口的线,接的就是这个 component_as_tool

9.3 从组件到 StructuredTool

Component(某个输入标了 tool_mode=True)

│ to_toolkit() component.py:1536

_get_tools() → ComponentToolkit.get_tools() component_tool.py:300

│ 对每个"够格的输出":
│ · args_schema ← create_input_schema(tool_mode 输入)
│ · func/coroutine ← _build_output_function(...)
│ · name ← _derive_tool_name → _format_tool_name

list[StructuredTool]

│ _filter_tools_by_status(metadata) component.py:1591

过滤后的工具列表 → 交给 Agent

args_schema 的四级降级component_tool.py:321-354):

优先级条件schema 来源
1传了 flow_mode_inputscreate_input_schema_from_dict,参数收进 flow_tweak_data
2tool_mode=True 的输入只用这些输入
3输出声明了 required_inputs用这些必填输入;若其中有非 tool_mode 的就直接抛错
4都没有退化成组件的全部输入

第 3 级那个抛错值得看(:338-350):如果一个输出的必填输入没被标成 tool_mode,工具被调用时必然缺参数,所以建工具的时候就报错,而不是等到 Agent 调用时才炸。错误信息里还列出具体是哪些输入。把运行时错误提前到构造时——一个小而有效的设计。

9.4 精华:每次调用都 deepcopy

_build_output_functioncomponent_tool.py:90)包出来的函数里有这么一段:

# src/lfx/src/lfx/base/tools/component_tool.py:106-109
# Create an isolated copy to prevent race conditions when this
# tool is invoked concurrently by an agent (GitHub issue #8791)
comp = deepcopy(component)
local_method = getattr(comp, method_name, output_method)

原因很实在:Agent 可能并发调同一个工具。组件实例是有状态的(set(**kwargs) 直接写属性),两次并发调用会互相踩。解法是每次调用深拷贝一份。

代价也很实在——每次工具调用一次 deepcopy。这是用性能换正确性的明确取舍,注释还挂着 issue 号。

同一个函数还做了三件收尾:位置参数按 tool_mode 输入名映射成关键字参数(:100-105)、异常统一包成 ToolException:127)、结果按类型归一(Messageget_text()Data.dataDataFrame 原样,其余 serialize:134-141)。

还有一层 _patch_send_message_decorator:80-85):调用期间把 component.send_message 换成 no-op,跑完再换回来——工具模式下组件不应该往聊天窗口推消息(那是 Agent 的活)。

9.5 Actions 面板:哪些工具开着

一个组件可能产出多个工具(每个够格输出一个)。用户要能在 UI 里勾选、改名、改描述。承载这个的是 tools_metadata,由 _build_tools_metadata_inputcomponent.py:1640)产出一个 ToolsInput

它的合并逻辑(:1664-1695)比看上去讲究:

  • 先比 old_tagsnew_tagscheck_for_tool_tag_change:1584,用 set 比较)。
  • 标签变了 → 代码结构变了,整份重置。
  • 标签没变 → 逐条合并:status(开/关)和 name 从旧的保留,args 从新的取;description 只在"用户真的改过"时保留——判据是 old_desc != old_display_description:1690-1694)。

这是个很干净的"用户编辑 vs 代码派生"冲突解法: 通过比较"当前值"和"派生值"来判断字段是否被人动过,动过才保留。

过滤在 _filter_tools_by_status:1591):有 metadata 就按 status 字段过;没有就退到 enabled_tools 白名单(按工具名或 tag 匹配)。


10. 出口二:MCP —— 两个方向,别搞混

Langflow 的 MCP 支持是双向的,代码分散在四个文件里,第一次读很容易混。先用一张图定位:

┌────────────── 出站(Langflow 当服务器)──────────────┐
│ │
Claude/Cursor ──MCP──► /api/v1/mcp/project/{id}/streamable
等 MCP 客户端 │

项目里 mcp_enabled 的 flow
每条 = 一个 MCP tool

┌────────────── 入站(Langflow 当被操作对象)─────────┐
│ │
某个 AI agent ──MCP──► lfx-mcp (FastMCP) ──REST──► Langflow 服务器
create_flow / add_component /
connect_components / run_flow

10.1 出站:项目的 flow 变成 MCP 工具

三层实现:

文件职责
全局 serverapi/v1/mcp.py:27一个 Server("langflow-mcp-server"),暴露当前用户所有 flow
项目 serverapi/v1/mcp_projects.py:1286 ProjectMCPServer每个项目一个 server 实例,只暴露该项目里 mcp_enabled 的 flow
共用 handlerapi/v1/mcp_utils.pyhandle_list_tools / handle_call_tool 两个函数被两层共用

列工具mcp_utils.py:434 handle_list_tools):查数据库里的 flow,一条 flow 造一个 types.Tool。三个字段的来源:

# src/backend/base/langflow/api/v1/mcp_utils.py:438-444(节选)
base_name = sanitize_mcp_name(flow.action_name) if flow.action_name else sanitize_mcp_name(flow.name)
name = get_unique_name(base_name, MAX_MCP_TOOL_NAME_LENGTH, existing_names)
description = flow.action_description or (flow.description if flow.description else f"Tool generated from flow: {name}")

inputSchemajson_schema_from_flowlangflow/helpers/flow.py:540)从 flow 的输入节点反推:遍历 graph.verticesis_input 的顶点,把 show=Trueadvanced=False 的模板字段转成 JSON Schema 属性,顺便做 Python→JSON 类型映射(strstringintinteger……)。

安全上有个修过的洞(mcp_utils.py:443-444):

# SECURITY: tools returned from the global server previously included every
# user's flows (PVR0754098). Always scope to the authenticated caller.

现在全局 server 强制按 current_user.id 过滤,项目 server 再叠一层 folder_id + user_id 双过滤(注释标为 defense-in-depth,:404-407)。

调工具mcp_utils.py:295 handle_call_tool):按名字找 flow → 校验属于该项目 → 组装 SimplifiedAPIRequest → 调 simple_run_flow → 把所有输出去重后转成 TextContent 返回。中间还起了一个后台任务每秒推一次进度通知(send_progress_updates:302-316),进度封顶 0.9,结束时补一个 1.0——因为 flow 执行时长不可预测,这是个"心跳式"的假进度。

传输层两套并存: 老的 SSE(mcp.py:85 SseServerTransport)和新的 Streamable HTTP(mcp.py:161 StreamableHTTP 类)。后者的实现有个细节值得看——session manager 的 run() 上下文必须在同一个 task 里进出,否则 asyncio 会抛 RuntimeError,所以它专门起了一个 task 拿着 _mgr_ready / _mgr_close 两个 Event 来控制生命周期(mcp.py:173-204)。项目级 server 面临同样问题,用的是 AnyIO TaskGroup 版本(mcp_projects.py:1376 ProjectTaskGroup),docstring 里把原因写明了。

认证mcp_projects.py:87 verify_project_auth)分几档:OAuth 项目 + 有效 composer token → 返回项目所有者;需要 API key 的 → 校验 x-api-key 并确认用户拥有该项目;其余 → _superuser_fallback:164,会打 AUTO_LOGIN_WARNING)。

源码里有一段很坦率的注释(:120-124)解释为什么 OAuth 项目仍然要 API key:网络层信任(loopback / 同主机代理)不安全,因为它没法区分本地的 MCP Composer 子进程和反向代理后面的另一个 loopback 对端。凭据转发还没做,所以先要 key。这是"知道不完美并写下来"的好例子。

10.2 入站:lfx-mcp 让 agent 自己造流

方向反过来:把 Langflow 的编辑能力暴露给一个外部 agent。入口是 lfx-mcp 这个 console script(src/lfx/pyproject.toml:64),实现是 src/lfx/src/lfx/mcp/server.py:98FastMCP("langflow-mcp")

它注册的工具是编辑器动作,不是业务动作:

分组代表工具位置
authloginserver.py:226
flowcreate_flowcreate_flow_from_speclist_flowsduplicate_flowexport_flow:258:349:442:499:1277
componentsearch_component_typesdescribe_component_typeadd_componentconfigure_component:873:891:569:677
connectionconnect_componentsdisconnect_components:938:979
executionrun_flowbuild_flowget_build_resultsvalidate_flow:1015:1070:1085:1179
batchbatch:1540

设计取舍写在文件头(server.py:1-8): "Flow data is never cached — every mutating tool does GET -> modify -> PATCH. The component registry is cached on first access." 流数据不缓存(避免和人在 UI 上的编辑打架),组件目录缓存(它在一次会话里不变)。

组件目录来自 registry.py:15 load_registry——它调 Langflow 的 /all 接口,也就是第 2-5 节那份索引的产物。这一圈闭合得很漂亮:构建期烤的索引,最终变成一个外部 agent 能搜索的工具目录。

describe_componentregistry.py:77)里有一段专门伺候 agent 的逻辑(:87-107):如果模板里任何输入字段带 tool_mode,就合成一条名叫 component_as_tool 的虚拟输出挂上去,描述写成 "Connect to an Agent's 'tools' input."。注释明说这是在镜像运行时的权威 Component._handle_tool_mode——也就是第 9 节那个函数。同一个概念在两处表达,代码里用注释把它们钉在一起。

search_registryregistry.py:35)默认隐藏 legacy 组件,但保留 beta:注释说明理由是"agent 的发现路径不该冒出废弃节点",而 legacy 仍可用精确名字 describe 到。

会话状态用 contextvar + 模块级单例的双层结构(server.py:74-77):stdio 单 agent 用共享单例,SSE 多 agent 并发时用 contextvar 覆盖。换 client 时会主动作废共享 registry(_set_client:129-136),因为旧 registry 是对旧服务器加载的。


11. 出口三:lfx CLI —— 脱开数据库跑一条流

11.1 它解决什么

Langflow 完整跑起来需要数据库、认证、前端。但很多场景不需要这些:CI 里跑一条流做回归、把一条流塞进 Lambda、在容器里当个单一用途的 HTTP 服务。

lfx 就是为这个存在的——src/lfx/ 整个包不依赖 langflow-base 的服务层。

11.2 lfx run:一次性执行

# 四种输入方式
lfx run flow.json "你好"
lfx run graph.py "你好"
lfx run --flow-json '{"data": ...}' "你好"
cat flow.json | lfx run --stdin "你好"

命令定义在 src/lfx/src/lfx/cli/run.py:52,实际逻辑委托给 lfx.run.base.run_flow。输出格式四选一(json / text / message / result:61-66),默认 json不缩进——docstring 说明是为容器和 serverless 场景优化的(:117-118)。

失败时不抛栈,而是吐一个结构化 JSON 再 exit 1:163-174):

error_response = {"success": False, "type": "error"}
if e.original_exception:
error_response["exception_type"] = type(e.original_exception).__name__

还有一处很贴心的诊断:_check_langchain_version_compatibility:17)专门识别 langchain_core.memory 缺失这个错误,然后打印一段带具体版本约束的修复指令——因为这个错误的真实原因(langchain-openai >= 1.0langchain-core 顶到 1.x)从原始报错里完全看不出来。

.py 脚本这条路script_loader.py 处理:

函数位置做什么
_load_module_from_scriptscript_loader.py:36importlib.util 加载文件为模块,用 temporary_sys_path 临时把脚本目录加进 sys.path
load_graph_from_script:89优先找 get_graph()(支持 async),退回模块级 graph 变量
_validate_graph_instance:61校验是 Graph 实例、必须含 Chat Input 和 Chat Output、跑一遍 validate_flow_for_current_settings
find_graph_variable:208纯 AST 解析,不执行脚本,用来做静态诊断

注意 _validate_graph_instance 那两个硬性要求(:76-82)——脚本路径下图里没有 Chat Input / Chat Output 就直接报错。这是个不写在文档里就会绊人的约束。

11.3 lfx serve:把流变成 HTTP API

lfx serve flows/ # 目录里所有 .json
lfx serve flow.json --port 8000
lfx serve flows/ --workers 4 --flow-dir /tmp/lfx-flows

命令定义在 src/lfx/src/lfx/cli/commands.py:219,默认 127.0.0.1:8000、1 个 worker。

核心数据结构是 FlowRegistrysrc/lfx/src/lfx/cli/serve_app.py:149)——内存 dict 是每 worker 的缓存,背后可插一个 FlowStore

Store行为场景
NullFlowStore(默认)纯内存单 worker
FilesystemFlowStore("/tmp/lfx-flows")同 pod 内多 worker 共享多 worker
FilesystemFlowStore("/mnt/...")PVC 挂载,跨 podK8s

跨 worker 删除怎么传播(docstring :149-163):每次 get() 都对 store 做一次 read() 存在性检查。删除的 worker 删文件;其他 worker 的下一个请求 get() 拿到 None,逐出缓存,返回 404。docstring 同时诚实提醒:网络卷(NFS/PVC)上这个 per-request 检查每次都要过网络,有实测代价,建议用本地 tmpfs 并接受"最终一致"。

HTTP 面create_multi_serve_app:542):

路由位置说明
GET /flows:573列出已注册流
GET /health:577唯一不要 API key 的
POST /flows/upload/:592运行时注册新流;注册在 /{flow_id} 之前避免路径遮蔽
DELETE /flows/{id}:658移除
POST /flows/{id}/run:686同步执行
POST /flows/{id}/stream:742SSE 流式执行

鉴权是单一静态 key(verify_api_key:61),从 x-api-key 头或查询参数取。

执行路径的三步固定动作:689-693):

validate_flow_for_current_settings(graph)
graph_copy = deepcopy(graph)
registry.stamp(graph_copy) # deepcopy 会丢掉 graph.context,重新盖章
apply_global_vars_to_graph(graph_copy, request.global_vars)

第二三步的注释点出一个真实的坑:Graph.__deepcopy__ 不带 context,所以 no_env_fallback 这类策略必须在拷贝后重新写一遍。no_env_fallback 的作用是禁止凭据解析回落到 os.environFlowRegistry.__init__ docstring,:165-167)——多租户下一个必要的隔离开关。

global_vars 则是走 graph.context['request_variables'] 的按请求变量注入(RunRequest 字段说明,:98-102),用来传凭据而不碰进程环境。

多 worker 的启动难点:uvicorn 的 worker 进程继承不到父进程内存里的 app 对象。解法是 ASGI app 工厂 create_serve_app:787)——父进程先把 LFX_SERVE_FLOW_DIR 等环境变量设好,每个 worker 调工厂自己重建 registry。里面还有一个 asyncio 的坑及其解法(:818-823):工厂被 uvicorn 调用时事件循环已经在跑asyncio.run() 会抛 RuntimeError,所以它把协程扔进一个 ThreadPoolExecutor(max_workers=1) 去拿干净的循环。


12. 巧妙之处(可以带走的几条)

  1. 构建期烤索引,运行期只读文件。 354 个组件的模板在 CI 里生成一次(Makefile:446 build_component_index),运行时不 import 任何集成模块。可复现性靠 _normalize_for_determinism + OPT_SORT_KEYS 双保险(scripts/build_component_index.py:35:154)。

  2. 哈希只用来查表,不用来背书。 截断哈希通过后执行的是服务端可信副本,而非客户端字节(endpoints.py:1543-1550 + flow_validation.py:436)。碰撞攻击的收益被压成零。

  3. 并发 import 前先单线程预热已知循环导入。 _warm_circular_importscomponents.py:403)用 6 行代码堵掉一个只在多线程扇出下才出现的 _DeadlockError,docstring 把因果链写全了。

  4. 缺依赖插占位符,把失败延迟到有意义的时刻。 _MissingModulePlaceholdervalidate.py:295)让 Windows 用户至少能看见表单,用到才报清楚的错。

  5. "用户编辑 vs 代码派生"靠比较当前值和派生值来判定。 Actions 面板的合并逻辑(component.py:1690-1694)用 description != display_description 判断用户是否改过描述——不需要额外的脏标记字段。

  6. 并发工具调用靠 deepcopy 隔离。 _build_output_functioncomponent_tool.py:106-109)明确用性能换正确性,注释挂着 issue #8791。

  7. 来源优先级显式化,冲突变成带路径的警告。 _resolve_bundle_shadowingcomponents.py:863)把"last-wins by iteration order"这种隐性行为变成可诊断的 bundle-shadowed 错误码。

  8. exec 一个 def 也是危险的。 validate_codevalidate.py:60-69)的 GHSA 注释——装饰器和默认参数在定义时求值,函数不用被调用就能 RCE。


13. 边界与局限(诚实版)

这一节承担全组文档的边界分析,分四类。

13.1 安全边界

  • 没有沙箱。 默认配置下认证用户可在服务进程内执行任意 Python(endpoints.py:1552 Component(_code=...)loading.py:46 eval_custom_component_code)。隔离靠部署形态,不靠代码。官方 docstring 自认是 beta 并推荐硬件级隔离(security.py:54-55)。
  • 关掉自定义组件≈关掉一半功能。 allow_custom_components=False 之后,只有哈希命中服务端模板的代码能跑,画布上的代码编辑器基本失去意义。
  • 索引的 SHA256 是自证的。 component_index.json 里存着自己的哈希,能改文件的人也能改哈希。它防的是损坏和意外篡改,不是供应链攻击。真正的信任根在构建流水线,不在这个字段。
  • 哈希只有 48 bitflow_validation.py:112)。第 8.3 节的可信源替换是补丁而非根治——如果哪天有个路径漏了替换步骤,那条路径就退回到弱哈希。
  • 导入他人的流 = 同意执行他人的代码,因为代码存在节点里。validate_flow_for_current_settings 只在受限配置下才真正拦。
  • OAuth 项目仍需 API keymcp_projects.py:147-170),凭据转发未实现——这是已知缺口,源码里写明了。

13.2 组件加载的脆弱点

  • 索引是构建产物,不是源码的镜像。 改了组件源码但没重跑 make build_component_index,服务端看到的还是旧模板。开发时必须靠 LFX_DEV
  • 版本号写读不对称。 缓存写入用 version("langflow")components.py:274),校验用 version("lfx"):196)。当前两者同为 1.10.1 所以没事;一旦分叉,用户缓存永久失效。
  • 失败静默。 模块 import 失败只打 logger.error:597,带 TODO: Surface these errors to the UI),组件实例化失败只打 warning:642)。用户看到的是"组件不见了",没有解释。
  • 自定义索引路径的来源标签不准。 _load_from_index_or_cache 即使走的是 components_index_path 指定的自定义索引,返回的 index_source 仍是 "builtin":344),遥测里区分不出来。
  • LFX_DEV=1 全量模式很慢。 515 个文件、每个可能拖一个重 SDK,即便有线程扇出也是数量级的差别。日常开发应该用逗号列表形式。

13.3 tool mode 的边界

  • 每次工具调用一次 deepcopy 组件持有大对象(模型客户端、连接池)时这是真实开销。
  • 必须显式标注。 组件作者不在某个输入上写 tool_mode=True,这个组件就永远变不成工具——不是自动的。
  • 必填输入没标 tool_mode 会直接建不出工具component_tool.py:338-350)。报错清晰,但对使用者是硬门槛。
  • 工具名会被正则洗过。 _format_tool_name:201)把所有非 [a-zA-Z0-9_-] 字符替换成 -,中文显示名会被洗成一串连字符。

13.4 MCP 与 CLI 的边界

  • 全局 MCP server 的工具名靠截断 + 数字后缀去重mcp_utils.py:510-520)。流名很长且前缀相同时,名字对 LLM 的可读性会退化。
  • 进度是假的。 send_progress_updatesmcp_utils.py:349-363)每秒 +0.1 封顶 0.9,和真实执行进度无关——只是防客户端超时的心跳。
  • inputSchema 从输入节点静态反推helpers/flow.py:540),只覆盖 show=True 且非 advanced 的字段;未知类型一律降级成 string 并打 warning(:573-574)。
  • lfx serve 是单静态 API key,没有用户、没有 RBAC、没有轮转。
  • 多 worker 一致性是最终一致的,窗口是"每个 worker 的下一次请求"。上传和删除都受此影响,docstring 明写(serve_app.py:662-667)。
  • lfx run.py 脚本时强制要求 Chat Input + Chat Outputscript_loader.py:76-82);JSON 流没有这个限制。

14. 代码地图(导航索引)

按符号名 grep 比按行号更抗上游漂移。

组件发现与加载

主题文件符号
加载总入口(三策略分派)src/lfx/src/lfx/interface/components.pyimport_langflow_components
LFX_DEV 三态解析同上_parse_dev_mode
索引读取 + SHA256/版本校验同上_read_component_index
索引→缓存降级链同上_load_from_index_or_cache
用户缓存路径 / 写缓存同上_get_cache_path_save_generated_index
动态扫描 + 线程扇出同上_load_components_dynamically
循环导入预热同上_warm_circular_importsMODULES_WITH_INTERNAL_CIRCULAR_IMPORTS
三种模式的具体实现同上_load_full_dev_mode_load_selective_dev_mode_load_production_mode
单模块处理(容错点)同上_process_single_module
缓存单例同上ComponentCachecomponent_cache
三来源合并 + 哈希表构建同上get_and_cache_all_types_dict_build_code_hash_lookups
扩展 bundle 发现与遮蔽仲裁同上import_extension_components_resolve_bundle_shadowing_DISCOVERY_PRECEDENCE
bundle 热重载后刷新缓存同上refresh_bundle_cache_from_record
自定义组件懒加载补全同上aget_component_metadataensure_component_loadedload_single_component
索引构建脚本scripts/build_component_index.pybuild_component_index_normalize_for_determinism_strip_dynamic_fields

动态代码执行与安全闸

主题文件符号
只编译不执行的校验src/lfx/src/lfx/custom/validate.pyvalidate_code
字符串→类五步流水同上create_class
全局作用域准备 + import 降级同上prepare_global_scope_get_module_fallbacks_resolve_attribute
类体抽取 / 编译 / 实例化同上extract_class_codecompile_class_codebuild_class_constructor
缺依赖占位符同上_MissingModulePlaceholder
节点执行时的 exec 入口src/lfx/src/lfx/custom/eval.pyeval_custom_component_code
顶点实例化src/lfx/src/lfx/interface/initialize/loading.pyinstantiate_class
自定义组件端点src/backend/base/langflow/api/v1/endpoints.pycustom_componentcustom_component_update_requires_component_hash_lookups
代码哈希与可信源src/lfx/src/lfx/utils/flow_validation.py_compute_code_hashcode_hash_matches_any_templateget_trusted_code_for_validationcollect_code_by_hash
流级校验同上validate_flow_for_current_settingscheck_flow_and_raise_get_invalid_components
安全配置项src/lfx/src/lfx/services/settings/groups/security.pyallow_custom_componentscustom_component_admin_onlyallow_components_paths_override
组件配置项src/lfx/src/lfx/services/settings/groups/components.pylazy_load_componentscomponents_index_path

tool mode

主题文件符号
触发与合成输出src/lfx/src/lfx/custom/custom_component/component.py_handle_tool_mode_append_tool_to_outputs_map_build_tool_output
工具生成主流程同上to_toolkit_get_tools
Actions 元数据与过滤同上_build_tools_metadata_input_filter_tools_by_statuscheck_for_tool_tag_change
前端节点切换 tool mode同上run_and_validate_update_outputs
StructuredTool 构造src/lfx/src/lfx/base/tools/component_tool.pyComponentToolkit.get_tools
调用包装(deepcopy 隔离)同上_build_output_function_build_output_async_function_patch_send_message_decorator
命名规范化同上_format_tool_name_derive_tool_namebuild_description
常量src/lfx/src/lfx/base/tools/constants.pyTOOL_OUTPUT_NAMETOOLS_METADATA_INPUT_NAME

MCP

主题文件符号
全局 MCP server + 两种传输src/backend/base/langflow/api/v1/mcp.pyserverhandle_sseStreamableHTTPhandle_streamable_http
项目级 MCP serversrc/backend/base/langflow/api/v1/mcp_projects.pyProjectMCPServerProjectTaskGroupget_project_mcp_serverinit_mcp_servers
项目认证同上verify_project_authverify_project_auth_conditional_superuser_fallback
工具列表 / 配置安装同上_build_project_tools_responseupdate_project_mcp_settingsinstall_mcp_config
共用 handlersrc/backend/base/langflow/api/v1/mcp_utils.pyhandle_list_toolshandle_call_toolcurrent_user_ctx
flow → JSON Schemasrc/backend/base/langflow/helpers/flow.pyjson_schema_from_flow
反向 MCP(agent 造流)src/lfx/src/lfx/mcp/server.pymcp(FastMCP 实例)、create_flow_from_specconfigure_componentrun_flowbatch_tracked
组件目录搜索src/lfx/src/lfx/mcp/registry.pyload_registrysearch_registrydescribe_component

CLI

主题文件符号
lfx runsrc/lfx/src/lfx/cli/run.pyrun_check_langchain_version_compatibility
lfx serve 编排src/lfx/src/lfx/cli/commands.pyserve_command_build_serve_registrybuild_registry_from_directorybuild_registry_from_paths_gate_flow_for_serve
HTTP 服务与注册表src/lfx/src/lfx/cli/serve_app.pyFlowRegistrycreate_multi_serve_appcreate_serve_appverify_api_key
脚本加载src/lfx/src/lfx/cli/script_loader.pyload_graph_from_script_validate_graph_instancefind_graph_variabletemporary_sys_path
CLI 入口src/lfx/src/lfx/__main__.pyappmain