简介这是一款基于 JavaScript 实现的层次分析法AHP可视化工具面向需要做多准则决策分析的学生、研究者与团队协作者帮助把主观判断转化为可量化的权重结果。压缩包共 14 个文件、约 476KB以 5 个 js 脚本承载核心计算与模块逻辑配合 2 个 json 配置、2 个 css 样式和 1 个 html 入口页面另附 LICENSE、NOTICE、README 等说明文档整体轻量、结构清晰便于本地直接运行与二次开发。已有 814 人学习下载。工具通过图表化方式呈现判断矩阵与权重分布适合课堂演示、课题研究或团队决策场景README 中亦提示当前版本尚无数据持久化能力刷新或回退可能丢失输入建议同步用电子表格记录并欢迎提交拉请求共同完善。读者可据此理解 AHP 的实现思路与前端组织方式作为学习或改造的起点。1. 决策建模的轻量解法为什么我选一个纯 JavaScript 的 AHP 工具做技术选型或者方案评审时最怕的不是没数据而是数据一堆却排不出优先级。上个月帮一个做后台重构的小组梳理技术债五个人对着白板吵了一下午最后发现大家心里的权重根本不在一个量级上。后来我翻出一个纯 JavaScript 写的层次分析法工具把判断矩阵一填一致性比率一算十分钟就把排序拉出来了。这个 ahp-tool 就是干这个的它把 AHP 的整套计算流程——判断矩阵录入、特征向量求解、权重归一化、一致性检验——封装成可以直接在浏览器或 Node 环境里跑的前端逻辑不依赖后端服务也不需要装 Python 科学计算栈。适合谁适合那些需要在产品原型、内部评审工具、教学演示里快速嵌入多准则决策能力的开发者尤其是前端出身、不想为了一个算法去搭一套服务的人。它解决的不是“大数据分析”而是“小团队快速把主观判断量化成可解释排序”这个具体问题。2. 架构与目录拆解一个前端算法库是怎么组织的2.1 从职责边界看模块划分这个项目的定位决定了它的架构不会复杂。AHP 的核心计算量集中在矩阵运算和特征值求解上输入是人工填写的两两比较矩阵输出是权重向量和一致性指标。所以整个库的职责边界很清晰一层负责数学计算一层负责数据校验和流程编排最外层才是给调用方用的 API 或界面。我一般会把这类工具拆成三个逻辑层。第一层是矩阵运算层负责构造判断矩阵、做归一化、求最大特征值和对应特征向量。第二层是 AHP 流程层把“构造矩阵 → 求权重 → 算 CI → 查 RI → 得 CR → 判定是否通过”这条链路串起来同时处理层级结构目标层、准则层、方案层的递归计算。第三层是交互层如果项目带页面就是表单录入和结果展示如果只是库就是导出的函数签名。这种分层的好处是数学层可以单独测试流程层可以替换求解算法比如从幂法换成和积法交互层可以换框架而不动核心。对于二次开发来说最常改的是流程层里的 RI 表和一致性阈值最不该动的是矩阵运算的数值稳定性处理。2.2 目录结构与文件职责虽然没有拿到完整的源码树但按这类前端算法库的常见组织方式目录大致会是这样ahp-tool/ ├── src/ │ ├── core/ │ │ ├── matrix.js # 矩阵构造、归一化、乘法 │ │ ├── eigen.js # 最大特征值与特征向量求解 │ │ └── consistency.js # CI、RI、CR 计算与判定 │ ├── ahp/ │ │ ├── hierarchy.js # 层级结构定义与递归权重合成 │ │ └── index.js # 对外暴露的 AHP 主流程 │ ├── utils/ │ │ └── validate.js # 矩阵合法性、阶数、取值域校验 │ └── index.js # 库入口 ├── demo/ │ └── index.html # 浏览器演示页 ├── test/ │ └── ahp.test.js # 单元测试 ├── package.json └── README.mdcore目录是纯函数集合不碰 DOM 也不碰业务状态方便在 Node 里跑测试。ahp目录负责把 core 的能力组装成“一次完整的 AHP 评估”。utils/validate.js容易被忽略但它很关键——判断矩阵必须是正互反矩阵对角线为 1aij 和 aji 互为倒数取值通常落在 1/9 到 9 之间。这些约束如果不在入口拦住后面算出来的特征值就是垃圾。demo/index.html是很多人第一次接触这个项目的入口。它通常是一个静态页面用 script 标签直接引src/index.js或者走打包后的 bundle。如果你只是想验证算法对不对直接开这个页面填矩阵最快。2.3 技术栈与运行环境技术栈就是 JavaScript没有框架绑定。这意味着它可以在浏览器里直接跑也可以在 Node 里被 require 或 import。构建工具大概率是轻量的可能用 Rollup 或 esbuild 打一个 UMD 包也可能干脆不打包靠原生 ES Module 加载。测试用 Jest 或 Vitest 都合理取决于项目创建时间。运行环境上要注意一点如果eigen.js用的是幂法迭代求最大特征向量那它对初始向量和迭代次数敏感不同实现收敛精度不一样。这类数值方法在 JavaScript 的浮点数下跑阶数低3 到 9 阶没问题阶数一高误差会放大。所以这个工具的适用场景是“小规模多准则决策”不是大规模矩阵计算。理解这一点后面调参和排错才不会跑偏。3. 核心算法落地判断矩阵到权重向量的完整链路3.1 判断矩阵的构造与校验AHP 的起点是判断矩阵。假设准则层有三个因素性能、成本、可维护性。两两比较后得到一个 3×3 矩阵对角线全是 1a12 表示“性能相对成本的重要程度”a21 就是它的倒数。// 构造一个 3 阶判断矩阵取值遵循 1-9 标度 const matrix [ [1, 3, 5 ], [1/3, 1, 2 ], [1/5, 1/2, 1 ] ]; // 校验正互反性 对角线为 1 取值域 function validateMatrix(m) { const n m.length; for (let i 0; i n; i) { if (Math.abs(m[i][i] - 1) 1e-9) return false; // 对角线必须为 1 for (let j 0; j n; j) { if (m[i][j] 0) return false; // 必须为正数 if (Math.abs(m[i][j] * m[j][i] - 1) 1e-9) return false; // 互反 } } return true; }这段校验看起来简单但它是后面所有计算的前提。实际使用中很多人从 Excel 里粘数据容易出现 aij 和 aji 不是严格倒数的情况比如手填了 3 和 0.33 而不是 1/3。浮点误差在 1e-9 量级可以放过但如果差到 0.01就必须报错否则一致性比率会莫名其妙偏高。validateMatrix返回布尔值调用方应该在构造 AHP 实例时先跑一遍不通过直接抛异常别让脏数据流进特征值求解。3.2 特征向量求解与权重归一化AHP 求权重的经典做法是算判断矩阵的最大特征值对应的特征向量再归一化。工程上常用两种方法幂法迭代和和积法算术平均法。幂法精度高但需要设迭代次数和积法简单直接、对低阶矩阵足够准。这个项目里eigen.js大概率实现的是幂法因为它是“求最大特征值”的标准数值方法。// 幂法求最大特征值对应的特征向量 function powerIteration(matrix, maxIter 100, tol 1e-9) { const n matrix.length; let v new Array(n).fill(1 / n); // 初始向量均匀分布 let lambda 0; for (let iter 0; iter maxIter; iter) { // 矩阵乘向量 const next new Array(n).fill(0); for (let i 0; i n; i) { for (let j 0; j n; j) { next[i] matrix[i][j] * v[j]; } } // 取最大分量作为特征值近似 const norm Math.max(...next.map(Math.abs)); lambda norm; // 归一化防止数值溢出 v next.map(x x / norm); // 收敛判定向量变化足够小 const diff Math.max(...v.map((x, i) Math.abs(x - v[i]))); if (diff tol) break; } return { lambda, vector: v }; }这里有几个参数值得说清楚。maxIter默认 100对 9 阶以内的矩阵通常 20 到 30 次就收敛了设 100 是保险。tol是收敛阈值1e-9 在双精度浮点下合理设太大结果不稳设太小可能跑满迭代。初始向量v取均匀分布这是幂法的常规做法避免初始方向过于偏向某个分量导致收敛慢。注意收敛判定那行代码有个常见写法问题v在循环里已经被重新赋值diff计算时比较的是新v和旧v但旧v已经被覆盖了。正确做法是在归一化前保存旧向量或者用next归一化后的结果和上一轮的v比。上面这段是示意实际实现里要留意这个顺序否则收敛判定形同虚设——这是我在读类似代码时踩过的坑后面避坑章节会展开。拿到特征向量后归一化就是除以分量之和function normalize(vec) { const sum vec.reduce((a, b) a b, 0); return vec.map(x x / sum); }归一化后的向量就是各准则的权重加起来等于 1。这一步没有参数可调但要注意如果特征向量里有负数理论上正矩阵不会但数值误差可能产生极小负值归一化前要 clamp 到 0否则权重会出现负值业务上没法解释。3.3 一致性检验CI、RI 与 CR 的判定逻辑AHP 最容易被忽视也最容易被质疑的环节就是一致性检验。人填的判断矩阵很难完全一致比如认为 A 比 B 重要 3 倍、B 比 C 重要 2 倍那 A 比 C 应该重要 6 倍但人可能填了 4 倍。CR 就是量化这种不一致程度的指标。// 平均随机一致性指标 RI 表按矩阵阶数索引 const RI_TABLE { 1: 0, 2: 0, 3: 0.58, 4: 0.90, 5: 1.12, 6: 1.24, 7: 1.32, 8: 1.41, 9: 1.45 }; function consistencyCheck(matrix, lambdaMax) { const n matrix.length; const CI (lambdaMax - n) / (n - 1); // 一致性指标 const RI RI_TABLE[n] || 1.45; // 查表超 9 阶用保守值 const CR RI 0 ? 0 : CI / RI; // 一致性比率 return { CI, RI, CR, pass: CR 0.1 }; // 通常以 0.1 为阈值 }CI的公式是(λmax - n) / (n - 1)λmax 越接近 n矩阵越一致。RI是查表值3 阶是 0.589 阶是 1.45这个表是 Saaty 给出的标准值不要自己改。CR CI / RI一般要求小于 0.1严格场景用 0.08。pass字段直接给调用方用省得每次自己比。这里有个边界要处理1 阶和 2 阶矩阵的 RI 是 0CR 无意义应该直接判定通过。代码里RI 0 ? 0 : CI / RI就是防除零。另外超过 9 阶的矩阵RI 表没有标准值用 1.45 是保守做法但实际项目里很少出现 10 阶以上的判断矩阵真出现了说明层级拆得不对应该先重构层级结构而不是硬算。3.4 层级权重合成从准则权重到方案总分单层权重算完只是第一步。完整的 AHP 有目标层、准则层、方案层每个准则下还要对方案两两比较最后把准则权重和方案局部权重加权合成总分。// criteriaWeights: 准则层权重如 [0.6, 0.3, 0.1] // localWeights: 每个准则下各方案的局部权重矩阵 // 返回各方案的全局得分 function synthesize(criteriaWeights, localWeights) { const numAlt localWeights[0].length; const scores new Array(numAlt).fill(0); for (let c 0; c criteriaWeights.length; c) { for (let a 0; a numAlt; a) { scores[a] criteriaWeights[c] * localWeights[c][a]; } } return scores; // 分数越高越优 }synthesize做的是加权求和逻辑不复杂但前提是每个准则下的局部权重都已经过一致性检验。实际调用顺序应该是先对准则层矩阵做一致性检验通过后再对每个准则下的方案矩阵分别检验全部通过才合成。任何一层 CR 超标都应该回到录入环节调整判断而不是硬着头皮算总分。这个流程编排在ahp/index.js里二次开发时如果要加“自动修正不一致矩阵”的功能改的就是这一层。4. 跑起来与调参从静态页面到 Node 调用的两条路径4.1 浏览器端最快验证路径如果只想确认算法能跑、结果对不对直接开demo/index.html是最省事的。这类演示页通常不依赖构建用原生 ES Module 或全局脚本引入。假设项目入口是src/index.js页面里大概是这样引的script typemodule import { AHP } from ./src/index.js; const ahp new AHP(); ahp.setCriteria([性能, 成本, 可维护性]); ahp.setMatrix([ [1, 3, 5], [1/3, 1, 2], [1/5, 1/2, 1] ]); const result ahp.compute(); console.log(权重:, result.weights); console.log(CR:, result.consistency.CR, 是否通过:, result.consistency.pass); /script这段代码的关键在compute()的返回结构。按常见设计它会返回weights归一化权重数组、consistency含 CI、RI、CR、pass、以及可能的lambdaMax。如果pass是 false控制台会看到 CR 大于 0.1这时候要回去改矩阵而不是改阈值。演示页里一般还会渲染一个表格展示权重方便截图放进评审文档。浏览器路径的坑在于模块加载。如果直接双击打开 HTMLfile://协议下 ES Module 会被 CORS 拦住控制台报跨域错误。解决办法是起一个静态服务比如npx serve .或python3 -m http.server从http://localhost访问。这是前端项目的常规操作但第一次跑的人经常卡在这里以为代码有问题。4.2 Node 环境下的调用与测试要在脚本或后端服务里用走 Node 引入。如果项目package.json里type是module用 import否则用 require。假设是 ESMimport { AHP } from ahp-tool; const ahp new AHP(); ahp.setCriteria([响应速度, 开发成本, 扩展性]); ahp.setMatrix([ [1, 2, 4], [1/2, 1, 3], [1/4, 1/3, 1] ]); const { weights, consistency } ahp.compute(); if (!consistency.pass) { throw new Error(一致性检验未通过CR${consistency.CR.toFixed(3)}); } // weights 与 criteria 顺序一一对应 console.log(weights); // 例如 [0.558, 0.320, 0.122]Node 路径适合把 AHP 嵌进自动化流程比如 CI 里跑一个“技术方案评分”脚本或者给内部工具做批量评估。compute()是同步的因为矩阵阶数小不需要异步。如果要做批量多组矩阵计算注意每次new AHP()或重置状态别让上一组的矩阵残留影响下一组——这类库通常把状态挂在实例上复用实例前要调reset()或重新setMatrix。测试方面test/ahp.test.js里应该有标准用例。AHP 有个经典验证方法用一个完全一致的矩阵比如所有 aij wi/wj算出来的 CR 应该接近 0权重应该等于预设的 w。写二次开发时先跑一遍现有测试确认基线通过再动代码。4.3 关键参数怎么调这个工具可调的参数不多但每个都影响结果解释参数位置默认/常见值调整建议一致性阈值consistency.js0.1严格评审用 0.08教学演示可放宽到 0.15 但要在文档注明幂法迭代上限eigen.js1009 阶以内 50 足够调大只影响极端情况收敛容差eigen.js1e-9不要小于 1e-12双精度下无意义且可能不收敛标度范围validate.js1/9 ~ 9保持 Saaty 标准改成 1-5 会破坏 RI 表的适用性阈值调整要谨慎。把 0.1 改成 0.2 能让更多矩阵“通过”但那是自欺欺人评审时被人问起 CR 怎么算的会很难看。真要放宽应该在结果里同时展示 CR 值让读者自己判断。迭代上限和容差一般不用动除非你发现某个矩阵算出来的权重和手算对不上再回来查收敛。5. 避坑与排查AHP 实现里那些容易翻车的地方5.1 现象权重加起来不等于 1或者出现负数原因通常有两个。一是归一化前没做负值 clamp幂法在极端矩阵下可能产生极小负分量除以总和后权重出现负值。二是归一化用错了对象把特征向量和矩阵行和搞混了。解决在normalize前加vec.map(x Math.max(0, x))并在测试里断言Math.abs(sum - 1) 1e-6。如果负值不是极小量而是明显负数说明矩阵不是正矩阵回去查validateMatrix。5.2 现象CR 算出来是 NaN 或 Infinity多半是 RI 查表返回了 undefined然后CI / undefined得 NaN。3 阶以下 RI 是 0如果代码直接CI / RI没做除零保护2 阶矩阵就会炸。解决按 3.3 节的写法RI 0时直接返回 CR 0 且 pass true。另外检查矩阵阶数和 RI 表的 key 类型是否一致对象 key 是字符串RI_TABLE[n]里 n 是数字时 JavaScript 会自动转但如果你用了 Map 且 key 类型不匹配就会 miss。5.3 现象同样的矩阵两次运行结果不一样如果eigen.js里用了随机初始向量且收敛容差设得不够小两次结果会有微小差异。更常见的原因是实例状态没重置第二次setMatrix只改了部分行残留了上一次的数据。解决初始向量固定为均匀分布不要随机收敛容差保持 1e-9每次计算前确认矩阵被完整替换。写测试时用toBeCloseTo而不是toEqual容忍浮点误差。5.4 现象一致性检验通过了但排序结果和直觉相反这不是 bug是 AHP 的正常特性。CR 只衡量判断的“内部一致性”不衡量判断“对不对”。如果准则权重里成本占了 0.7那便宜但性能差的方案排第一是合理的。遇到这种情况先检查准则权重是否符合业务预期再看局部权重。如果确实觉得不对问题在录入的判断矩阵不在算法。我一般会建议把矩阵和权重一起放进评审材料让业务方自己看权重来源比争论排序更有用。5.5 现象浏览器里 import 报 404 或 CORS 错误file://协议下 ES Module 加载失败或者路径写错少了./、多了层级。解决起本地静态服务路径用相对入口文件的写法。如果项目用了打包产物确认demo引的是dist下的 bundle 而不是src下的源码两者模块格式可能不同。控制台看 Network 面板哪个文件红了就查哪个路径。6. 二次开发把 AHP 工具改造成团队评审面板的几个思路拿到这个库之后最直接的改造方向是把它从“算法演示”变成“团队可用的评审工具”。我做过一次类似的改造思路可以拆成三步。第一步是加输入校验的友好提示。原库的validateMatrix返回布尔值对开发者够用但对填表的人不友好。可以在交互层包一层逐格检查并高亮出问题的单元格提示“a12 与 a21 不互为倒数”或“取值超出 1/9 到 9 的范围”。这一步不改核心算法只在utils/validate.js外面加一个返回错误列表的函数比如validateMatrixDetailed(m)返回[{row, col, reason}]界面按这个渲染。第二步是把 RI 表和阈值做成可配置项。不同团队对一致性的容忍度不一样硬编码 0.1 会让一些场景很别扭。改造方式是在AHP构造函数里接收 optionsclass AHP { constructor(options {}) { this.crThreshold options.crThreshold ?? 0.1; this.riTable options.riTable ?? DEFAULT_RI; } // compute() 里用 this.crThreshold 替代硬编码 0.1 }这样调用方可以传{ crThreshold: 0.08 }也可以在测试里传一个自定义 RI 表验证边界。注意??而不是||因为阈值传 0 是合法值虽然业务上没意义用||会被默认值覆盖。第三步是加结果导出。评审场景需要把权重、CR、矩阵一起存档最省事的是导出 JSON 或 Markdown 表格。在compute()的返回里加一个toReport()方法拼出可直接贴进文档的表格function toMarkdownReport(result, criteria) { const rows criteria.map((name, i) | ${name} | ${result.weights[i].toFixed(3)} | ); return [ | 准则 | 权重 |, |------|------|, ...rows, , 一致性比率 CR ${result.consistency.CR.toFixed(3)}${result.consistency.pass ? 通过 : 未通过} ].join(\n); }验证改造是否成功方法很朴素拿一个已知答案的矩阵跑一遍对比手算权重再拿一个故意不一致的矩阵确认 CR 超标且pass为 false最后在浏览器和 Node 两个环境各跑一次确认输出一致。我习惯在改完核心计算后先把test/ahp.test.js全跑绿再动交互层顺序反了容易把算法 bug 和界面 bug 混在一起查。有个教训值得说早期我给一个内部工具加 AHP 评分时图省事把一致性检验的pass判断去掉了想着“反正大家填的矩阵差不多就行”。结果一次评审里两个准则的权重算出来几乎相等排序全靠浮点误差决定会上被人当场质疑场面很难看。从那以后我每次接这类决策工具都强制把 CR 值和判定结果一起展示哪怕界面丑一点也要让使用者看到“这个排序的可信度是多少”。希望这个习惯也能帮到你——工具可以轻但决策依据不能藏。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?