作为一名长期从事自动化工具开发的工程师,我最近在将机器学习模型集成到业务流程时遇到了一个挑战:如何让非技术同事也能轻松调用AI能力?n8n这个开源工作流工具完美解决了这个问题,但需要开发自定义节点来连接我们的内部系统。经过三个月的实践,我总结出这套完整的n8n节点开发方法论。
n8n的独特优势在于其"代码可视化"的设计理念。与Zapier等SaaS产品不同,n8n允许我们:
特别是在AI应用场景中,n8n可以成为连接大模型API与业务系统的"胶水层"。比如我们开发的智能客服系统,通过n8n将ChatGPT与CRM、工单系统无缝对接,客服响应效率提升了40%。
bash复制# 推荐使用nvm管理Node版本
nvm install 18.16.0
nvm use 18.16.0
# 初始化项目
mkdir n8n-custom-node && cd n8n-custom-node
npm init -y
# 安装核心依赖
npm install n8n-workflow n8n-core --save-dev
npm install typescript @types/node --save-dev
code复制n8n-custom-node/
├── src/
│ ├── nodes/
│ │ ├── MyCustomNode/
│ │ │ ├── MyCustomNode.node.ts # 节点实现
│ │ │ └── index.ts # 节点导出
│ │ └── index.ts # 节点注册
├── test/ # 测试用例
├── .n8n-node-modules.json # n8n模块配置
└── tsconfig.json # TypeScript配置
提示:使用monorepo结构管理多个节点时,建议采用lerna或yarn workspace
一个n8n自定义节点的完整生命周期包含四个阶段:
typescript复制interface INodeType {
description: INodeTypeDescription; // 节点元数据
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
methods?: { [key: string]: Function }; // 可选方法
}
其中INodeTypeDescription定义了节点的"外观":
typescript复制{
displayName: 'AI文本生成', // 显示名称
name: 'aiTextGenerator', // 唯一标识
icon: 'fa:robot', // FontAwesome图标
group: ['transform'], // 节点分组
version: 1, // 版本控制
description: '调用大模型生成文本内容',
defaults: { name: 'AI文本生成' },
inputs: ['main'], // 输入连接点
outputs: ['main'], // 输出连接点
properties: [ /* 参数配置 */ ]
}
typescript复制import {
IExecuteFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
NodeOperationError
} from 'n8n-workflow';
export class AITextGenerator implements INodeType {
description: INodeTypeDescription = {
displayName: 'AI文本生成',
name: 'aiTextGenerator',
icon: 'fa:robot',
group: ['transform'],
version: 1,
description: '调用大语言模型生成文本',
defaults: { name: 'AI文本生成' },
inputs: ['main'],
outputs: ['main'],
properties: [
{
displayName: '模型',
name: 'model',
type: 'options',
options: [
{ name: 'GPT-3.5', value: 'gpt-3.5-turbo' },
{ name: 'GPT-4', value: 'gpt-4' }
],
default: 'gpt-3.5-turbo'
},
{
displayName: '提示词',
name: 'prompt',
type: 'string',
typeOptions: { rows: 4 },
default: '',
placeholder: '请输入提示词...'
}
]
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
const prompt = this.getNodeParameter('prompt', i) as string;
// 模拟AI调用
const result = `AI生成内容:${prompt}`;
returnData.push({
json: { result },
pairedItem: { item: i }
});
}
return [returnData];
}
}
实际项目中需要连接真实AI服务:
typescript复制// 在execute方法中添加:
const model = this.getNodeParameter('model', i) as string;
const apiKey = await this.getCredentials('openaiApiKey');
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
model,
messages: [{ role: 'user', content: prompt }]
})
});
if (!response.ok) {
throw new NodeOperationError(this.getNode(),
`API请求失败: ${response.statusText}`);
}
const data = await response.json();
const result = data.choices[0]?.message?.content;
typescript复制properties: [
{
displayName: '温度值',
name: 'temperature',
type: 'number',
typeOptions: {
minValue: 0,
maxValue: 2,
numberPrecision: 1
},
default: 0.7,
description: '控制生成随机性 (0=确定性最高)'
}
]
typescript复制async loadOptions() {
// 从API获取可选模型列表
const response = await fetch('https://api.example.com/models');
const models = await response.json();
return models.map(model => ({
name: model.displayName,
value: model.id
}));
}
typescript复制try {
// 业务逻辑
} catch (error) {
if (this.continueOnFail()) {
returnData.push({
json: {
error: error.message,
input: items[i].json
},
pairedItem: { item: i }
});
continue;
}
throw error;
}
typescript复制import { AITextGenerator } from '../src/nodes/AITextGenerator/AITextGenerator.node';
import { NodeOperationError } from 'n8n-workflow';
describe('AI文本生成节点', () => {
const node = new AITextGenerator();
test('空提示词应报错', async () => {
const mockFunctions = {
getNodeParameter: jest.fn().mockReturnValue(''),
continueOnFail: jest.fn().mockReturnValue(false),
getNode: jest.fn()
};
await expect(node.execute.call(mockFunctions))
.rejects.toThrow(NodeOperationError);
});
});
本地开发模式:
bash复制n8n start --custom-extensions=./dist
Docker部署:
dockerfile复制FROM n8nio/n8n:latest
COPY ./dist /custom-nodes
ENV N8N_CUSTOM_EXTENSIONS=/custom-nodes
typescript复制const batchSize = 10; // 每批处理10条
const batches = [];
for (let i = 0; i < items.length; i += batchSize) {
batches.push(items.slice(i, i + batchSize));
}
const results = await Promise.all(
batches.map(batch => processBatch(batch))
);
typescript复制const cache = new Map();
async function getCachedResponse(prompt: string) {
if (cache.has(prompt)) {
return cache.get(prompt);
}
const response = await callAI(prompt);
cache.set(prompt, response);
// 1小时后自动清除
setTimeout(() => cache.delete(prompt), 3600000);
return response;
}
我们为电商平台开发的客服自动化系统架构:
code复制用户咨询 → Webhook节点 → 意图识别节点 → 知识库查询 →
→ 命中? → AI生成回答 → 满意度评价
→ 未命中 → 转人工客服
关键指标:
问题1:节点在n8n中不显示
.n8n-node-modules.json配置问题2:API调用超时
fetch(url, { signal: AbortSignal.timeout(5000) })问题3:内存泄漏
--inspect参数启动n8n进行内存分析经过多个项目的实践验证,这套开发方法能显著提升n8n与AI系统的集成效率。最关键的体会是:好的自定义节点应该像乐高积木一样,既保持简单可靠的接口,又能灵活组合出复杂功能。