一、变量传递是工作流的骨架,也是塌方点
扣子工作流本质是数据在节点间流动。开始节点定义输入 → 代码节点加工 → 插件节点消费 → 大模型节点生成 → 结束节点输出。这条链路上,变量每经过一个节点就可能变形一次。
[开始] → 变量{topic:"工作流"} → [代码节点] → 变量{result:["..."]} → [插件] → ???
新手最常见的困惑:
- 「变量明明在代码节点里打印没问题,为什么大模型节点取不到?」
- 「循环第三次就报错,前两次好好的」
- 「节点改了个名字,整个工作流全红了」
本质都是变量传递出了问题。 下面按根因类型拆成 6 个坑,每个都附真实错误现象和修复方案。
二、类型系统:扣子不是 JavaScript
坑 #1:大模型输出是字符串,你当数组用
错误现象:
代码节点打印 typeof llm_output 显示 string,但明明 Prompt 让大模型输出 JSON 数组。
// ❌ 错误写法
function main({ llm_output }) {
const data = llm_output; // 以为是数组
return { first: data[0] }; // 取到的是字符串第一个字符 "{" 而不是对象
}
根因: 大模型节点的 output 变量永远是 String 类型,即使 Prompt 让它返回 ...,它也只是一个「长得像数组」的字符串。
解法:
// ✅ 正确写法
function main({ llm_output }) {
// 先清理可能的 markdown 代码块包裹
let cleaned = llm_output
.replace(/```json\s*/gi, '')
.replace(/```\s*/g, '')
.trim();
const data = JSON.parse(cleaned);
return { first: data[0], count: data.length };
}
🔑 记住:大模型输出永远是 string,用之前必须 JSON.parse。
为什么大模型节点不直接输出对象?因为扣子的大模型节点本质是 HTTP 调用------模型返回的就是纯文本流。即使 Prompt 里要求输出 JSON,它也只是一个「长得像 JSON」的字符串,需要手动解析。这个设计决定了后续所有消费大模型输出的节点,第一行代码必定是 JSON.parse。
坑 #2:数字变量被悄悄转成字符串
错误现象:
function main({ count }) {
// count 从开始节点传入,类型设的是 Number
const doubled = count * 2;
return { doubled };
}
控制台输出 doubled = NaN。
根因: 当你用 {{开始.count}} 引用数字变量时,扣子的模板引擎会把值嵌入为字符串。如果代码节点的输入映射是模板方式(如 {{开始.count}})而非直接绑定变量,进来的就是 "5" 而不是 5。
解法 1(推荐): 在节点配置里使用直接变量绑定而非模板字符串引用。
解法 2(兜底): 代码里显式转换:
如果你的工作流已经大量使用模板引用且不方便逐一改为绑定(比如已经连着十几个节点了),那就别纠结了------在代码入口统一做一次类型转换,一劳永逸。
function main({ count }) {
const num = Number(count) || 0; // 兜底
return { doubled: num * 2 };
}

无论选哪种方案,核心原则都是一个:别假设上游给你的类型是对的。 把每个代码节点当成独立微服务,数据进门先校验,出门给默认值。这个习惯能帮你避开 80% 的类型相关 bug。
三、循环节点的变量地狱
坑 #3:循环内外变量名一样,结果混乱
错误现象:
工作流有一个循环节点「搜索」,循环变量绑定了 query1, query2, query3。在循环内部的代码节点和循环外部的大模型节点都引用了 {{搜索.body}},但外部取到的是最后一次循环的值。
根因: 扣子循环节点的输出变量分两种引用方式:
| 引用位置 | 正确写法 | 含义 |
|---|---|---|
| 循环内部 | {{搜索.body}} | 当前轮次的值 |
| 循环外部 | {{搜索_循环.body}} | 全部轮次的数组 |
写错了后缀,外部取到的只是最后一轮(或干脆 undefined)。
解法:
// 循环内部代码节点:正常引用
function main({ body }) {
// body 就是当前轮次的搜索结果,直接用
return { snippet: body.snippet?.slice(0, 200) };
}
// 循环外部代码节点:必须用 _循环 后缀取所有结果
function main({ allResults }) {
// allResults = {{搜索_循环.body}} → 是一个数组
const combined = allResults
.map((r, i) => `[${i + 1}] ${r.snippet}`)
.join('\n');
return { combined };
}

这个 _循环 后缀是扣子的内部约定,文档里写得隐晦,却是最常被问的问题。建议你在工作流里加一个注释卡片,把循环内外引用的写法贴在边上------毕竟三个月后你自己也可能忘。
坑 #4:循环数组长度不一致导致越界
错误现象:
配置了三个搜索词循环,但索引硬编码:
function main({ searchResults }) {
// 循环跑了 3 轮,但有时候只有 2 条返回
const r0 = searchResults[0]; // OK
const r1 = searchResults[1]; // OK
const r2 = searchResults[2]; // ❌ undefined → 下游报错
return { result: r2.snippet };
}
根因: 搜索 API 可能返回空结果、超时、或被限流,导致某些轮次没有数据。但下游代码假设了数组长度。
解法:
function main({ searchResults }) {
const valid = (searchResults || [])
.filter(r => r && r.snippet) // 过滤空结果
.slice(0, 10); // 安全截断
if (valid.length === 0) {
return { combined: '(搜索未返回有效结果)', count: 0 };
}
const combined = valid
.map((r, i) => `[${i + 1}] ${r.snippet?.slice(0, 200)}`)
.join('\n');
return { combined, count: valid.length };
}
🔑 永远不要假设循环数组长度。每次都用 filter + slice 兜底。
这个坑之所以隐蔽,是因为它在本地调试时几乎不出现------你的搜索词总能返回结果。但一旦上线运行,某次搜索恰好遇到空结果、超时、或者 API 限流,工作流就直接崩了。生产环境最怕这种「小概率但一旦触发就全挂」的 bug,防御性编程是唯一的解法。
四、跨节点引用:改名即断链
坑 #5:节点改名导致所有引用变红
错误现象:
工作流跑通后,觉得「代码1」「代码2」命名不优雅,把「代码1」改成「搜索词生成器」。然后整个工作流爆红,十几个变量引用全部失效。
根因: 扣子的变量引用路径是 {{节点名.变量名}},节点名是引用路径的一部分。改名后,老的 {{代码1.query1}} 不会自动更新为 {{搜索词生成器.query1}}。
解法:
- 防御性做法:建工作流时先把所有节点名定好再连线
- 补救性做法:改名后,逐个节点点开,重新绑定变量(扣子目前没有批量重命名功能)
- 命名规范建议:
✅ 好命名:搜索词生成器、结果清洗器、文章润色 ❌ 坏命名:代码1、代码2、节点3、新建节点(5)

坑 #6:可选变量传了空值,下游静默失败
错误现象:
开始节点有个「可选」的 style 变量。用户不填时,代码节点收到 undefined,拼字符串不出错但结果很奇怪:
function main({ topic, style }) {
// style 为 undefined
const prompt = `写一篇关于${topic}的文章,风格:${style}`;
// 结果:"写一篇关于扣子的文章,风格:undefined"
return { prompt };
}
根因: 可选变量没填时值为 undefined(不是空字符串)。JavaScript 里 "风格:" + undefined = "风格:undefined"。
解法:
function main({ topic, style }) {
// 给每个可选变量设默认值
const safeStyle = style || '教程体';
const prompt = `写一篇关于${topic}的文章,风格:${safeStyle}`;
return { prompt };
}
// 更完善:用对象默认值
function main({ topic, style = '教程体', maxWords = 1500 }) {
// 现在 style 和 maxWords 都有兜底值
}
可选变量的坑还有一个变体:插件节点返回的字段本身就是可选的。比如必应搜索的 snippet 字段某些结果里不存在,你不能假设它一定有值。养成习惯:任何来自外部的数据(用户输入、插件返回、大模型输出),都用可选链 ?. 或默认值兜底。
五、变量传递调试三板斧
排查变量问题时,按这个顺序走:
① 加「调试输出」节点
在怀疑出问题的节点后面加一个临时代码节点,只做一件事:打印收到的变量。
function main(data) {
console.log('=== DEBUG ===');
console.log('type:', typeof data.xxx);
console.log('value:', JSON.stringify(data.xxx).slice(0, 200));
console.log('keys:', Object.keys(data));
return {};
}
点「调试运行」看控制台输出。
② 检查变量绑定方式
打开节点配置,看变量是「模板引用」({{节点.变量}})还是「直接绑定」。前者有类型转换风险,后者保持原类型。
③ 单节点测试
把上游节点全部断连,只保留当前节点 + 开始节点,手动输入测试数据跑一遍。缩小排查范围。
六、总结:一张表记住变量传递规则
| 场景 | 规则 | 坑位 |
|---|---|---|
| 大模型输出 | 永远是 String,用前 JSON.parse | #1 |
| 数字传递 | 模板引用会转字符串,用直接绑定或 Number() | #2 |
| 循环内引用 | {{节点.变量}} | #3 |
| 循环外引用 | {{节点_循环.变量}} | #3 |
| 循环结果处理 | filter + slice 兜底,不假设长度 | #4 |
| 节点命名 | 先定名再连线,别中途改名 | #5 |
| 可选变量 | 每个都设默认值 || 'default' | #6 |
核心心法:扣子不是写代码------数据在每个节点边界都可能变型。把每个节点当成一个不信任上游的微服务,进数据先 validate,出数据给默认值。
如果你经常遇到更复杂的变量传递场景------比如大模型返回非标准 JSON、循环内嵌套分支条件------这些本质上都可以用本文的规则组合解决。
关于作者 :专注扣子工作流踩坑与自动化实战,更多模板和进阶教程可搜索「米核AI易山」或访问 miheaii.com。
本文部分内容由 AI 辅助完成。