Vibe Coding 时代,MATLAB 科研编程该如何进阶?
目录
Vibe Coding 时代,MATLAB 科研编程该如何进阶?
一、AI 改变了代码的编写方式,也改变了我们需要关注的问题
二、科研计算的难点,往往隐藏在代码之外
三、怎样让 AI 真正提高 MATLAB 科研工作的效率?
参考资料
如今,使用 MATLAB 编写一段程序,最耗费时间的环节未必还是代码本身。
过去,想要实现一个稍微复杂的数值计算任务,我们通常需要查阅相关函数的文档,寻找参考代码,再根据自己的问题修改程序。对于刚接触 MATLAB 的学生而言,有时候仅仅是弄清楚某个函数的参数含义,或者排查一处数组维度错误,就需要花费不少时间。随着大语言模型逐渐具备代码生成能力,这种工作方式正在发生变化。现在,我们可以直接用自然语言描述数学模型、计算要求和预期结果,让 AI 帮助编写程序,甚至协助分析报错、修改代码和解释算法。
2025 年,Andrej Karpathy 提出了Vibe Coding这一说法,用来描述一种高度依赖 AI 生成代码、较少关注具体实现细节的编程方式。虽然这一概念最初带有较为鲜明的个人色彩,但它很快引发了人们对 AI 辅助编程的广泛讨论。需要说明的是,严格意义上的 Vibe Coding 与经过认真审查和验证的 AI 辅助开发存在区别,尤其在科研计算中,直接接受未经检查的生成结果往往具有一定风险。
MATLAB 也在适应这一变化。MathWorks 从 R2025a 开始提供 MATLAB Copilot,支持代码生成、解释和调试辅助等功能。与此同时,通用大语言模型也能够完成不少 MATLAB 编程任务。对于主要使用 MATLAB 开展科学计算的研究人员来说,这些工具确实减少了重复编写代码和查阅文档的工作。
但科研编程还有一个容易被忽视的特点:程序能够正常运行,并不意味着它给出的数值结果一定可信。
这一点在初学阶段不容易察觉。很多时候,我们习惯通过程序是否报错、图像是否符合预期来判断代码是否正确。面对 AI 生成的程序,这种习惯也很容易延续下来。如果代码结构完整、运行过程没有异常,生成的图像看起来又比较合理,我们往往就会倾向于接受结果。
问题在于,MATLAB 的许多应用涉及数学建模和数值计算,程序中的错误未必表现为语法错误。某个数组维度的变化、一种不合适的数值方法,甚至实验数据本身的局限,都可能在程序正常运行的情况下影响最终结论。
因此,我更关心的是另一个问题:当生成 MATLAB 代码越来越容易时,我们应该怎样调整自己的学习重点,才能真正利用 AI 提高科研工作的质量和效率?
一、AI 改变了代码的编写方式,也改变了我们需要关注的问题
对于 MATLAB 初学者,AI 最直观的价值是降低了程序实现的门槛。以前需要反复查阅文档才能完成的操作,现在可以通过自然语言快速获得参考实现。常见的数据读取、矩阵处理、绘图排版和函数封装,都可以让 AI 先生成基本框架,再根据实际需求进行修改。
不过,程序生成速度的提高,也可能让一些原本需要在编程过程中仔细考虑的问题被忽略。
举一个简单的例子。假设我们需要比较一组实验观测数据与模型预测结果,并计算均方根误差(RMSE)。下面使用 MATLAB 构造一组带有随机噪声的观测数据:
rng(2026, 'twister'); t = (0:0.01:10)'; % 模型预测值 y_model = sin(t); % 模拟实验数据 y_obs = (y_model + 0.01*randn(size(t))).'; % 计算残差与 RMSE residual = y_obs - y_model; rmse = sqrt(mean(residual(:).^2)); fprintf('RMSE = %.6f\n', rmse); disp(size(residual));从计算公式来看,这段程序似乎没有明显问题。模型采用正弦函数,观测数据在模型预测值的基础上添加了标准差为 0.01 的随机噪声,残差和 RMSE 的计算方式也符合常规定义。
但如果检查residual的尺寸,就会发现一个问题:它并不是预期的 1001×1 向量,而是一个 1001×1001 的矩阵。
原因在于,y_model是列向量,而y_obs在构造过程中被转置成了行向量。自 MATLAB R2016b 起,兼容尺寸的数组可以通过隐式扩展(Implicit Expansion)完成运算。当行向量与列向量相减时,MATLAB 会自动扩展数组,得到所有元素之间的两两差值。
这与我们原本希望计算的对应采样点之间的残差显然不同。
更隐蔽的是,后面的residual(:)又将这个矩阵转换成列向量,使得 RMSE 的计算能够继续执行。因此,程序既不会报错,也不会输出NaN,而是给出一个具有确定数值的错误结果。
修正代码并不困难:
% 将观测数据统一为列向量 y_obs = y_obs(:); % 检查数据尺寸 assert(isequal(size(y_obs), size(y_model)), ... '数据尺寸不一致'); % 正确计算 RMSE residual = y_obs - y_model; rmse = sqrt(mean(residual.^2)); fprintf('修正后的 RMSE = %.6f\n', rmse);由于添加的噪声标准差为 0.01,正确计算得到的 RMSE 通常也会接近 0.01。而原先错误的计算结果可能接近 1,两者相差近两个数量级。
这个问题其实并不复杂,甚至只需要一行size命令就能够发现。但前提是,我们知道应该检查数组的尺寸,也理解 MATLAB 会怎样处理不同形状的数据。
类似的细节还有很多。例如,MATLAB 中的'表示共轭转置,.'才表示普通转置。处理实数数组时,两者往往没有明显区别;一旦涉及复数信号,混淆这两个运算符就可能改变计算结果。再比如,矩阵乘法*与逐元素乘法.*虽然只有一个符号之差,却对应不同的数学运算。
这些基础知识并不会因为 AI 能够生成代码而失去价值。相反,当我们越来越多地使用 AI 完成程序编写时,能否准确理解一段代码的实际含义,可能比熟练记忆每一个函数的调用方式更加值得关注。
但仅仅具备阅读和检查代码的能力,对于科研计算仍然不够。
前面的例子属于程序实现层面的问题,通过检查变量类型、尺寸和运算方式,通常可以找到错误。如果代码在语法和逻辑上都没有问题,而计算结果仍然不可靠,情况就会复杂得多。
二、科研计算的难点,往往隐藏在代码之外
MATLAB 广泛应用于数值模拟、矩阵计算和信号处理。这些任务都有成熟的函数和算法可供使用,AI 也能够根据需求给出相应的程序。不过,算法能够正确执行,并不意味着它在当前问题中一定具有合适的数值性质。
以一个简单的无阻尼弹簧振子为例,其运动方程为:
其中,$x$ 为位移,$\omega$ 为固有角频率。取初始条件:
对应的解析解为:
如果要求 AI 使用显式欧拉法进行数值模拟,得到的程序可能如下:
omega = 1; h = 0.02; t = 0:h:40; N = numel(t); x = zeros(1, N); v = zeros(1, N); x(1) = 1; v(1) = 0; % 显式欧拉法 for k = 1:N-1 x(k+1) = x(k) + h*v(k); v(k+1) = v(k) - h*omega^2*x(k); end % 与解析解比较 x_exact = cos(omega*t); figure; plot(t, x_exact, 'k-', 'LineWidth', 1.5); hold on; plot(t, x, 'r--', 'LineWidth', 1.2); xlabel('Time'); ylabel('Displacement'); legend('解析解', '显式欧拉法'); grid on;这段代码在算法实现上没有问题。显式欧拉法本来就是按照当前状态及其导数,估计下一个时间步的状态。
但运行程序后可以观察到,数值解的振幅会随着时间增加而逐渐偏离解析解。要理解这一现象,仅仅检查循环中的赋值语句是不够的,需要进一步分析数值方法本身。
对于单位质量的无阻尼振子,系统总能量为:
真实系统不存在阻尼和外部能量输入,因此总能量应当保持不变。
显式欧拉法的离散格式为:
将其代入离散能量表达式并展开:
只要且
,便有:
也就是说,这种数值方法会在每一步计算中引入额外的能量增长。
对于前面设置的参数,、
,仿真到
共进行 2000 次迭代,最终能量与初始能量之比为:
数值系统的能量最终增加到了初始值的约 2.225 倍,而真实振子的能量始终保持不变。
为了进一步比较,可以在原程序后添加:
% 显式欧拉法的能量 E_euler = 0.5*(v.^2 + omega^2*x.^2); % 使用 ode45 求解 odefun = @(tt,z) [z(2); -omega^2*z(1)]; opts = odeset('RelTol', 1e-9, ... 'AbsTol', 1e-11); [t45, z45] = ode45(odefun, t, [1;0], opts); % ode45 对应的能量 E_ode45 = 0.5*(z45(:,2).^2 + ... omega^2*z45(:,1).^2); figure; plot(t, E_euler/E_euler(1), ... 'r-', 'LineWidth', 1.5); hold on; plot(t45, E_ode45/E_ode45(1), ... 'b--', 'LineWidth', 1.5); yline(1, 'k:'); xlabel('Time'); ylabel('Normalized Energy'); legend('显式欧拉法', 'ode45', '理论值'); grid on;对于这个简单的非刚性问题,设置较严格误差容限的ode45通常能够得到明显更准确的结果,数值能量也更加接近理论值。
但这里还需要注意,ode45不是严格保持能量守恒的辛积分方法。对于长时间的哈密顿系统仿真,或者对几何结构保持有特殊要求的问题,还需要考虑其他合适的数值算法。
这个例子涉及一个很实际的科研问题:如果我们只要求 AI 使用某种算法完成仿真,那么即使它准确实现了该算法,也不能保证最终结果满足研究要求。
研究者需要对算法的适用条件有所了解,至少知道如何判断误差是否可以接受。存在解析解时,可以直接比较计算误差;系统具有守恒性质时,可以检查相应的守恒量;对于更加复杂的问题,还可能需要分析时间步长、空间网格或求解器参数对结果的影响。
类似的情况也出现在矩阵计算中。
例如,我们需要求解线性方程组:
对于线性方程组,MATLAB 可以通过A\b直接求得数值解。然而,求解器能够给出结果,并不意味着这个结果具有足够的精度。当系数矩阵的条件数较大时,输入数据中的微小扰动就可能导致数值解发生显著变化。这种现象在数值线性代数中被称为病态性,可以通过下面的 Hilbert 矩阵实验加以观察。
考虑下面的 Hilbert 矩阵实验:
n = 12; % 构造 Hilbert 矩阵 A = hilb(n); % 设置参考解 x_true = ones(n,1); % 构造右端项 b = A*x_true; % 数值求解 x_num = A\b; % 计算条件数、残差和解误差 kappa = cond(A); residual = norm(A*x_num-b)/norm(b); error = norm(x_num-x_true)/norm(x_true); fprintf('条件数:%.3e\n', kappa); fprintf('相对残差:%.3e\n', residual); fprintf('相对解误差:%.3e\n', error);这里人为构造了一个参考解,因此能够同时检查残差与解误差。
残差描述的是数值解代回原方程后,对方程的满足程度;解误差则反映数值解与参考解之间的实际偏差。对于条件良好的线性系统,小残差通常能够提供有用的信息,但对于病态系统,二者之间可能存在很大差异。
12 阶 Hilbert 矩阵的条件数已经达到约的量级,数值结果对输入扰动和浮点舍入误差十分敏感。因此,即使计算残差很小,也不意味着得到的每一个解分量都具有很高的精度。
可以继续给右端项施加一个很小的扰动:
b2 = b; b2(end) = b2(end) + 1e-12; x2 = A\b2; db = norm(b2-b)/norm(b); dx = norm(x2-x_num)/norm(x_num); fprintf('输入相对变化:%.3e\n', db); fprintf('解的相对变化:%.3e\n', dx);由于矩阵具有严重的病态性,右端项很小的变化就可能引起数值解较大的变化。
类似的问题也可能出现在实际科研计算中。对于病态线性系统,即使使用 MATLAB 内置的求解器,也难以避免输入误差对结果的影响。此时需要进一步分析问题本身的数值性质,而不能仅凭程序正常运行或计算残差较小,就认为所得结果足够准确。
如果不具备相关数值线性代数知识,很容易把程序输出的“小残差”直接理解成“高精度”。
前面两个例子都说明,科研人员需要理解代码背后的数值方法。但实际研究还有另一类问题:程序和算法本身都可能没有错误,输入数据却已经限制了我们能够得到的信息。
以信号处理为例,假设使用 MATLAB 的 FFT 分析一段频率为 70 Hz 的正弦信号,但采样频率只有 100 Hz:
fs = 100; t = 0:1/fs:1-1/fs; % 原始信号频率为 70 Hz x = sin(2*pi*70*t); N = length(x); Y = fft(x); % 构造单边幅度谱 P2 = abs(Y/N); P1 = P2(1:N/2+1); P1(2:end-1) = 2*P1(2:end-1); f = fs*(0:N/2)/N; [~, idx] = max(P1); fprintf('频谱峰值:%.1f Hz\n', f(idx)); figure; stem(f, P1, 'filled'); xlabel('Frequency (Hz)'); ylabel('Amplitude'); grid on;运行后会发现,频谱峰值出现在 30 Hz,而原始信号的频率明明是 70 Hz。
原因在于采样混叠。
对于最高频率为的带限信号,避免混叠通常要求采样频率满足:
本例的采样频率为 100 Hz,对应的奈奎斯特频率为 50 Hz,低于原始信号的 70 Hz。
对于这些均匀采样点,有:
也就是说,70 Hz 的正弦信号与一个相位相反的 30 Hz 正弦信号,会产生相同的离散采样值。
因此,FFT 将频谱峰值显示在 30 Hz 并不意味着算法出现错误。仅根据这组离散采样数据,我们无法判断原始连续信号究竟属于哪一种情况。
采样混叠造成的信息损失通常无法仅凭已有的离散数据消除。因此,在实际信号测量中,需要根据待测信号的频率范围合理设置采样频率,并在必要时使用抗混叠滤波器,以减少高频成分混叠到目标频段的影响。
这与前面的数值积分和线性方程组问题存在共同之处:科研代码的可靠性,最终还取决于所研究的问题、采用的数学方法以及输入数据是否满足相应条件。
因此,当 AI 已经能够完成大量程序实现工作时,科研人员需要投入更多精力的地方,也就逐渐清晰起来了。
三、怎样让 AI 真正提高 MATLAB 科研工作的效率?
理解了前面的几个问题,再讨论 AI 辅助编程的实际使用方法,就比较容易找到方向。
我认为,利用 AI 提升 MATLAB 科研效率,关键在于尽可能明确地描述计算任务,并在程序生成之后建立相应的验证过程。
仍以前面的弹簧振子为例。
如果只是要求 AI“使用 MATLAB 编写一个弹簧振子的仿真程序,并绘制位移曲线”,得到的程序可能已经能够满足一般的课堂演示需求。但对于数值方法研究,这样的任务描述显然不够充分。
我们可以将问题进一步写成:
考虑无阻尼线性振子,其运动方程为
,取
,初始条件为
、
。
请分别使用显式欧拉法和 MATLAB
ode45求解,在相同的时间区间内与解析解比较。计算位移的最大绝对误差,并绘制系统能量随时间变化的曲线。请进一步分析显式欧拉法产生能量漂移的原因,说明该方法在当前问题中的局限,并提供能够独立运行的 MATLAB 代码。
相比简单的功能描述,这里明确了数学模型、计算条件、数值方法和验证要求。AI 生成的代码因此更容易围绕实际计算目标展开。
不过,详细的提示词只能提高生成内容满足要求的可能性,并不能代替对结果的独立检查。特别是涉及数值推导、理论假设或算法适用范围时,仍然需要根据数学知识和可靠资料进行验证。
在实际科研中,我更倾向于按照问题本身的特点安排 AI 的工作。
对于尚未熟悉的 MATLAB 函数,可以让 AI 解释参数和给出基本示例,同时对照 MathWorks 官方文档检查具体用法。对于已经明确数学模型的计算任务,可以让 AI 协助实现程序框架、拆分函数和组织数据处理流程。对于复杂的数值实验,则应在生成程序之前就考虑验证方案。
当计算结果得到基本确认后,AI 还可以用于程序优化。
例如,MATLAB 中常见的数组预分配与向量化操作,往往能够改善某些程序的运行效率。但究竟是否更快,还需要实际测试。
下面通过一个简单计算比较预分配循环与向量化实现:
N = 50000; % 测量运行时间 t_loop = timeit(@() calcLoop(N)); t_vec = timeit(@() calcVector(N)); % 检查结果 y1 = calcLoop(N); y2 = calcVector(N); err = max(abs(y1-y2)); fprintf('循环耗时:%.6f s\n', t_loop); fprintf('向量化耗时:%.6f s\n', t_vec); fprintf('最大差异:%.3e\n', err); % 以下局部函数放在脚本末尾 function y = calcLoop(N) y = zeros(1,N); for k = 1:N z = k/N; y(k) = sin(z) + z^2; end end function y = calcVector(N) z = (1:N)/N; y = sin(z) + z.^2; end这里使用timeit而不是只运行一次tic和toc,是为了减少单次计时波动的影响(上述代码需要在支持脚本局部函数的 MATLAB 版本中运行,即 R2016b 及以后版本)。这段程序分别采用循环和向量化方式完成相同的计算,并通过timeit比较运行时间。实验结果可以反映两种实现方式在当前 MATLAB 版本和计算规模下的性能差异,为后续的代码优化提供参考。
所以说,具体的性能提升应该通过测试确定,不能仅凭代码形式判断。
对于规模较大的科研程序,还可以利用 MATLAB Profiler 分析运行时间主要消耗在哪些函数或代码段。如果真正的瓶颈是重复进行大型矩阵分解,或者频繁读写文件,那么优化普通循环带来的收益可能十分有限。
这一点同样适用于 AI 提出的优化建议。无论程序修改得多么简洁,都应当比较修改前后的数值结果,并在相同条件下测量运行性能。涉及浮点运算时,还需要考虑不同计算顺序可能带来的舍入误差,不能简单要求所有结果逐位完全一致。
除了程序实现与性能优化,科研代码的组织和复现也值得重视。
例如,使用 MATLAB 进行随机数值实验时,可以明确记录随机数发生器状态、实验参数及软件版本:
rng(2026, 'twister'); % 保存初始随机数状态 initialState = rng; % 构造模拟数据 x = linspace(0,1,100)'; noise = 0.05*randn(size(x)); y = sin(2*pi*x) + noise; % 记录软件版本 matlabVersion = version; % 保存实验信息 save('experiment_record.mat', ... 'x', 'y', 'noise', ... 'initialState', 'matlabVersion');这样的记录有助于后续检查和重复实验,但完整的科研复现还需要关注外部数据、工具箱依赖、代码版本以及计算环境等因素。对于较大的 MATLAB 项目,可以结合 Git 和 MATLAB Projects 管理程序与实验配置。
如果 AI 参与了代码生成或修改,也有必要保留关键的修改记录,尤其是涉及数学模型、算法选择和计算参数的部分。对于尚未公开的科研数据或者具有保密要求的代码,还应根据相关规定和工具的数据处理政策决定是否能够提交给在线 AI 服务。
从这些实际工作可以看出,AI 的价值并不局限于帮助我们减少代码输入。它能够参与程序开发的许多环节,但能否形成持续的效率提升,还取决于我们是否清楚计算任务的要求,以及能否有效检查和管理生成结果。
对于刚开始学习 MATLAB 的学生,我仍然建议先掌握基本的数组操作、函数编写、程序调试和数据处理方法。目的并不是要求独立记住所有语法,而是具备理解和修改程序的能力。否则,即使 AI 生成了一份完整代码,我们也很难发现其中的问题,更难根据实际需求进行调整。
当具备基本编程能力后,学习重点可以逐渐向具体研究领域的数学方法延伸。例如,求解微分方程需要了解数值稳定性与误差控制,进行矩阵计算需要理解条件数和误差传播,开展信号分析则需要掌握采样理论与频谱分析的基本条件。
这些知识与使用哪一种 AI 工具关系不大,却直接决定了我们能够在多大程度上判断计算结果是否可靠。
当然,科研人员没有必要掌握所有领域的数值算法,也没有必要坚持手写每一个程序模块。成熟的 MATLAB 函数和经过验证的算法实现,本来就是为了减少重复开发。合理使用 AI,可以让我们更快地了解工具、尝试算法并完成一些重复性工作,把时间留给具体的研究问题。
回到最初关于 Vibe Coding 的讨论,我认为科研编程能力的提升,并不能简单地用能够编写多少行代码,或者能够在多短时间内生成一个程序来衡量。
前面的几个实验已经表明,即使程序顺利运行,计算过程仍然可能受到数据维度、数值稳定性、矩阵条件数和采样条件等因素的影响。对于科研人员来说,发现这些问题、理解它们产生的原因,并选择合适的方法加以处理,往往比写出程序本身更加困难。
AI 能够显著缩短从问题描述到代码实现之间的距离,但这并不会自动缩短从计算结果到可靠科研结论之间的距离。
随着 AI 辅助编程工具逐渐成熟,我们可以减少一些重复性的程序编写工作,也可以尝试更加高效的科研开发方式。与此同时,数学建模、数值分析、结果验证和程序管理仍然需要认真对待。
对于 MATLAB 科研用户而言,真正值得提高的,是理解计算问题、判断结果可靠性,并利用合适的工具完成研究任务的能力。
参考资料
MathWorks 官方文档:
MATLAB Copilot
Compatible Array Sizes for Basic Operations
ode45
inv
fft
timeit
Vectorization
Vibe Coding 概念相关资料:
Simon Willison:Andrej Karpathy on Vibe Coding(2025)
注:程序运行时间及部分浮点计算结果可能随 MATLAB 版本和运行环境而变化。