如果你的工作涉及维护量子化学计算程序,很可能面对过这样的场景:某个核心模块还是 20 世纪 90 年代的 Fortran 77 写法,全局 COMMON 块、隐式变量类型、满屏的数字魔法常数,注释里只写了“GAMESS 积分公式见文献 [34]”。所有人都知道这个模块是性能瓶颈,但它同时也是整个 SCF 迭代里最不能出错的部分,于是十年过去了,代码还在那里,没人敢动。
这篇文章要聊的,就是如何用 Agentic Workflow(智能体工作流)去啃这类 Legacy HPC 代码中最难啃的骨头。我们以 GAMESS 的双电子积分(Two-Electron Integral)核心为例子,拆解一套可以落地的 AI 辅助现代化流程。读完你会得到三个东西:一套 Agentic Workflow 的角色与阶段设计,一个从 Fortran 到 C++ 的最小转换示例,以及一组防止 AI 改坏数值精度的验证方法。
先说结论:面对 GAMESS 这样的存量代码,Agentic Workflow 的价值不是“让 AI 一夜之间重写整个软件包”,而是把“人写方案、AI 执行、人验证”的过程结构化、流水线化。双电子积分模块是这类工作流最好的试验田,因为它计算模式密集、语义边界清晰、数值结果可验证,非常适合用最小单元验证整套方法论。
1. 为什么 Legacy HPC 代码很难“直接现代化”
Legacy HPC 代码和普通业务系统的旧代码不一样。普通 Java 系统重构,主要是梳理状态、接口和依赖;而 HPC 代码现代化要同时面对三层复杂性。
第一层是语言与风格。GAMESS 早期大量代码是 Fortran 77 甚至更早的写法,数组用固定大小声明,循环嵌套很深,过程间通过 COMMON 块共享全局状态。代码可读性低是一回事,更麻烦的是 Fortran 的列主序存储、默认按引用传递、一维数组伪装多维数组等习惯,给自动转换带来了额外负担。
第二层是数值语义。双电子积分的计算路径里充满了数值技巧:误差补偿、截断阈值、特殊函数逼近、近似替代。这些技巧往往没有注释,是前人通过数值实验慢慢调出来的。如果 LLM 在转换时“按常规逻辑优化”掉了一些看似多余的操作,数值结果可能从 1e-12 变成 1e-6,SCF 迭代可能直接不收敛。
第三层是验证环境。很多遗留 HPC 项目没有单元测试,没有持续集成,唯一的“测试”是跑基准分子、对比总能量。这样的验收方式对大型修改来说太脆弱。你很难判断一个局部改动到底是变好了还是变坏了。
所以,对 Legacy HPC 代码做现代化,第一原则不是“重写”,而是“在保持数值行为一致的前提下,逐步替换实现方式”。Agentic Workflow 在这个过程中扮演的角色不是替你做判断,而是把每个步骤的执行效率提上去,同时留下足够的验证轨迹。
2. 双电子积分模块:GAMESS 的核心与性能瓶颈
在量子化学计算里,双电子积分描述的是两个电子的库仑相互作用。用基函数展开分子轨道后,这类积分写成 (pq|rs) 的形式,其中 p、q、r、s 是原子轨道基函数的指标。之所以叫“双电子”,是因为积分核里同时包含两个电子的空间坐标。
双电子积分的计算量为 N^4 数量级,这里的 N 是基函数个数。虽然可以利用对称性缩减到约 N^4/8,但对于中等大小的分子,需要计算的积分数量仍然非常可观。在 Hartree-Fock、DFT 以及后 HF 方法中,SCF 迭代每一步都要反复使用这些积分,因此它历来是量子化学计算中最耗时的部分之一。
这也是为什么很多老牌量子化学程序包里,双电子积分模块经过了几十年手工优化:有专门针对高对称分子的加速路径,有基于阈值筛选的近似策略,还可能嵌入了硬件相关的手工向量化片段。这些优化极大提升了性能,但它们也让代码难以理解和修改。
从另一个角度看,双电子积分模块恰恰是 Agentic Workflow 的理想对象,因为它的输入输出非常明确:输入是基函数参数和原子坐标,输出是一个数值或一组数值。它不像 SCF 主循环那样有复杂的迭代状态,也不像解析梯度模块那样依赖一大片耦合公式。你完全可以把积分内核当作一个“数值函数”,让 AI Agent 在函数层面做转换,再用大量随机输入做数值对比验证。
| 对比维度 | 双电子积分模块 | SCF 主循环 | 解析梯度模块 |
|---|---|---|---|
| 计算密度 | 极高 | 中 | 高 |
| 模块边界 | 清晰 | 模糊 | 中等 |
| 数值可验证性 | 强 | 弱 | 中等 |
| LLM 理解难度 | 中 | 中高 | 高 |
| 适合 Agent 处理 | 非常适合 | 较难 | 需要拆分 |
这意味着,如果你要在 GAMESS 这样的项目里试点 Agentic 现代化,双电子积分是最容易做出成效、也最容易做安全验证的入口。
3. Agentic Workflow 的基本概念与角色设计
Agentic Workflow 这个词听起来很玄,其实落到 HPC 代码转换场景里,本质就是“用多个各司其职的 AI Agent 串成一条可追踪、可验证的生产流水线”。它和“把整个文件丢给 ChatGPT 让它改写”的区别在于,前者把任务拆成了有输入输出契约的阶段性工作,每一步都有独立产物和验收标准。
一个典型的 Agentic Workflow 通常包含以下角色:解析 Agent 负责把旧代码结构、边界条件、外部接口梳理清楚;规划 Agent 基于解析结果设计转换策略,确定代码映射关系;转换 Agent 负责生成目标代码;验证 Agent 负责编译、运行数值对比测试;审查 Agent 则模拟人工审阅,检查代码风格、异常处理和性能隐患。
实际实现时,这些 Agent 不一定需要五个独立的模型实例。更常见的做法是在一个流程编排器里,用同一个底层模型配合不同的上下文和工具权限来承担不同角色。这样做的好处是便于控制状态、记录日志和设置人工审批节点。
这里有一个关键点:Agentic Workflow 并不等于让 AI 全自动改代码。更稳妥的设计是在阶段之间留人工检查点,尤其是“转换完成”到“验证开始”之间,必须要求人类开发者确认转换契约没有被破坏。AI 写代码的能力已经很强,但它对“项目历史背景”和“数值意义”的理解依然有限,这两点恰恰是 HPC 代码改动中最重要的。
4. 转换契约:开始之前先定义“什么是成功”
在让 Agent 写第一行代码之前,你需要先定义一份“转换契约”。这份契约不是给人类看的,而是给所有 Agent 和验证工具看的统一标准。
数值一致性是最核心的部分。常见的做法是固定双精度浮点(FP64),并且约定最大绝对误差或相对误差阈值。对于双电子积分,一个比较稳妥的起步阈值是最大绝对误差 1e-10,最大相对误差 1e-10。如果目标构型中包含近似的、省略的微小积分,还需要特别说明哪些场景允许跳过或截断。
接口兼容性同样重要。GAMESS 主程序基本是 Fortran 风格,外部模块如果改成 C++,需要通过 C ABI 封装,让 Fortran 侧用 ISO_C_BINDING 来调用。转换契约里应当注明保留原有子程序名称、参数顺序和数据类型,这样上层代码不需要大面积改动。
性能目标也不能缺。某些 AI 生成的代码虽然数值正确,但循环结构或内存访问方式可能导致性能下降十倍。契约里可以约定“相同测试用例下,性能不低于原实现的 80%”这类可量化目标,让验证 Agent 有据可查。
5. 用最小示例跑通 Agentic 转换流程
下面我们用一段教学性质的示意代码来演示整个流程。它不来自真实 GAMESS 源码,但保留了很多遗留 HPC 代码的典型特征,比如隐式类型、固定格式、数组按列传递、数学常数缺乏来源说明。
5.1 旧代码段(示意性 Fortran)
! 文件:two_e_demo.f ! 示意代码:双电子积分 (ab|cd) 高斯乘积路径的部分计算 ! 注意:教学示例,不代表 GAMESS 真实源码 subroutine two_e_demo(alpha, beta, gamma, delta, & a_xyz, b_xyz, c_xyz, d_xyz, & result) implicit none double precision :: alpha, beta, gamma, delta double precision :: a_xyz(3), b_xyz(3), c_xyz(3), d_xyz(3) double precision :: result double precision :: gam1, gam2 double precision :: p_xyz(3), q_xyz(3) double precision :: dij2, dkl2, drpq2 double precision :: pref1, pref2, pref integer :: i gam1 = alpha + beta gam2 = gamma + delta pref1 = 1.0d0 / (alpha + beta) pref2 = 1.0d0 / (gamma + delta) dij2 = 0.0d0 drpq2 = 0.0d0 dkl2 = 0.0d0 do i = 1, 3 p_xyz(i) = (alpha * a_xyz(i) + beta * b_xyz(i)) * pref1 q_xyz(i) = (gamma * c_xyz(i) + delta * d_xyz(i)) * pref2 dij2 = dij2 + (a_xyz(i) - b_xyz(i)) ** 2 dkl2 = dkl2 + (c_xyz(i) - d_xyz(i)) ** 2 drpq2 = drpq2 + (p_xyz(i) - q_xyz(i)) ** 2 end do pref = exp(-alpha * beta * dij2 * pref1) & * exp(-gamma * delta * dkl2 * pref2) pref = pref * (3.14159265358979323846d0 / (gam1 * gam2)) & * exp(-gam1 * gam2 * drpq2 / (gam1 + gam2)) result = pref end这段代码计算了一个简化的双电子积分预因子,实际生产代码里还有 Boy 函数递归、角动量提升、收缩基组展开等步骤。这里拿它做示例,已经足够展示 Agent 转换时需要保留的数值语义。
5.2 给转换 Agent 的任务提示词模板
在 Agentic Workflow 中,Prompt 是一个带结构的工作指令,不是简单的“把代码翻译一下”。下面是一份可复用的提示词模板。
任务:将附件中的 Fortran 子程序转换为 C++ 实现。 约束条件: 1. 输入输出接口映射关系如下: - alpha -> double alpha_in - a_xyz -> const double a_xyz[3] - result -> double& result_out 2. 所有中间计算保持 double 精度,禁止使用 float。 3. 必须保持原代码中的数学计算顺序,禁止“优化”掉看似冗余的乘法或中间变量。 4. 原代码中存在的 `1.0d0 / (alpha + beta)` 写法,转换为 C++ 时仍保持先求倒数再参与乘法的顺序。 5. 不修改任何输入数组的内容。 6. 输出只输出 result,不要添加额外的调试信息。 请先输出转换后的 C++ 代码,并在代码注释中逐行说明与 Fortran 的对应关系。这样写的意义在于:你在把“行业默认规范”固定成 Agent 必须遵守的硬约束。尤其第四条,看起来啰嗦,实际上是为了防止 AI 自作主张把a/b/c重写成a/(b*c)之类的等价变换——数值上未必等价。
5.3 转换后的 C++ 代码
// 文件:two_e_demo.cpp // 对应 two_e_demo.f 的 C++ 转换版 #include <cmath> extern "C" void two_e_demo( double alpha_in, double beta_in, double gamma_in, double delta_in, const double a_xyz[3], const double b_xyz[3], const double c_xyz[3], const double d_xyz[3], double& result_out) { const double gam1 = alpha_in + beta_in; const double gam2 = gamma_in + delta_in; const double pref1 = 1.0 / (alpha_in + beta_in); const double pref2 = 1.0 / (gamma_in + delta_in); double p_xyz[3] = {0.0, 0.0, 0.0}; double q_xyz[3] = {0.0, 0.0, 0.0}; double dij2 = 0.0; double dkl2 = 0.0; double drpq2 = 0.0; for (int i = 0; i < 3; ++i) { p_xyz[i] = (alpha_in * a_xyz[i] + beta_in * b_xyz[i]) * pref1; q_xyz[i] = (gamma_in * c_xyz[i] + delta_in * d_xyz[i]) * pref2; const double da = a_xyz[i] - b_xyz[i]; const double dc = c_xyz[i] - d_xyz[i]; dij2 += da * da; dkl2 += dc * dc; const double dpq = p_xyz[i] - q_xyz[i]; drpq2 += dpq * dpq; } double pref = std::exp(-alpha_in * beta_in * dij2 * pref1) * std::exp(-gamma_in * delta_in * dkl2 * pref2); pref = pref * (3.14159265358979323846 / (gam1 * gam2)) * std::exp(-gam1 * gam2 * drpq2 / (gam1 + gam2)); result_out = pref; }这段代码严格保持了原 Fortran 的计算顺序,唯一的变化是用extern "C"声明,方便将来通过 C ABI 被 Fortran 代码调用。在真实项目里,你还会在函数外再包一层 Fortran 友好的桥接函数,这里我们就聚焦核心转换本身。
5.4 驱动编排脚本
编写好角色 Prompt 后,需要一套调度逻辑把“解析、转换、验证、审查”串起来。下面是一个简化版编排脚本,展示了工作流骨架:
# 文件:workflow_driver.py # 示意:Agentic Workflow 最小编排器,不绑定具体模型 API from dataclasses import dataclass from typing import List, Callable @dataclass class Artifact: name: str content: str def run_agent(prompt: str, context: List[Artifact], model_call: Callable[[str], str]) -> Artifact: """调用模型。实际项目中 model_call 可以替换为企业内部的 LLM 服务或本地量化模型。""" combined_prompt = "\n\n".join( [f"[{a.name}]\n{a.content}" for a in context] + [prompt] ) response = model_call(combined_prompt) return Artifact(name="agent_output", content=response) def run_workflow(source_fortran: str, model_call: Callable[[str], str]) -> Artifact: # 1. 解析 Agent:提取子程序签名和关键约束 analysis = run_agent( "请提取这个 Fortran 子程序的输入输出签名、临时变量和数学表达式顺序。", [Artifact("source.f", source_fortran)], model_call, ) # 2. 转换 Agent:根据约束生成 C++ 代码 generated = run_agent( "请根据上面的解析结果和转换契约,生成 C++ 代码。", [analysis], model_call, ) # 3. 在这里可以插入人工审查节点 # 人工确认后再进入验证阶段 return generated if __name__ == "__main__": # 读者需要在这里接入自己的模型调用函数 def my_model(prompt: str) -> str: # 示例:用一个固定说明代替真实模型调用 return "// generated by placeholder model" with open("two_e_demo.f", "r") as f: source = f.read() result = run_workflow(source, my_model) print(result.content)这段脚本的核心思想是:把 LLM 调用抽象成model_call,把每个阶段的产物用Artifact保存下来,方便后续审计和复现。真实项目里还会增加重试、超时、异常熔断和人工审批状态,这些是 Agentic Workflow 进入生产环境的必要条件。
6. 数值验证与性能回归
转换完成后,验证 Agent 的工作才刚刚开始。数值验证最朴素也最可靠的方法是“大规模随机输入对比”。
你可以随机生成大量基函数指数和坐标,确保覆盖不同的数量级:指数从 0.01 到 1000,坐标从 -10 到 10 埃。然后分别调用 Fortran 版本和 C++ 版本,统计结果差值的最大值、平均值、均方根误差。
# 示意命令:编译两个版本并运行对比测试 gfortran -O2 -o two_e_fortran two_e_demo.f test_driver.f90 g++ -O2 -o two_e_cpp two_e_demo.cpp test_driver.cpp ./two_e_fortran > fortran_out.txt ./two_e_cpp > cpp_out.txt python3 compare_results.py fortran_out.txt cpp_out.txtcompare_results.py里可以定义如下判据:最大绝对误差大于 1e-10 则测试失败;如果某个用例相对误差超过 1e-10,还要额外检查是否是输入参数本身接近奇异。性能回归可以用相同的输入规模分别计时,注意绑核、预热和多次取中位数,排除机器负载干扰。
从材料看,这类验证方式在数值计算领域已经很成熟,难点不在于方法,而在于能否坚持执行。很多团队在“看起来差不多”之后就跳过严格对比,结果往往要等到 SCF 迭代不收敛或者能量出现微小偏差时才回头来查,成本高得多。
7. 常见问题与排查思路
在实际落地 Agentic Workflow 时,你大概率会遇到下面这些问题。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 转换后数值误差偏大 | Agent 在转换中改变了数学表达式顺序 | 对比中间变量值,逐行对照 Fortran 与 C++ | 在 Prompt 中强制“保持原计算顺序”并检查转换日志 |
| 编译通过但链接报错 | C++ 函数名修饰与 Fortran 调用约定不匹配 | 检查是否使用extern "C",确认函数名大小写 | 用 C ABI 封装,并在 Fortran 侧使用iso_c_binding声明接口 |
| 性能明显下降 | 循环结构被改变,或引入了不必要的临时数组 | 用性能分析工具查看热点函数 | 将性能回归阈写入验证 Agent 的契约,未达标时回退并重新生成 |
| Agent 生成代码反复出现同一种错误 | Prompt 中缺少对应约束条件 | 检查转换 Agent 的上下文是否包含失败案例 | 把失败案例作为负面示例加入 Prompt,形成持续改进的上下文库 |
| 验证用例覆盖不足 | 随机输入没有覆盖极端参数区间 | 检查测试用例的覆盖率和边界值分布 | 增加指数、坐标、收缩系数的边界值用例,必要时加入真实分子参考数据 |
排查时要记得一条原则:先怀疑输入,再怀疑代码,最后怀疑工具链。在 HPC 场景里,编译器优化选项(比如-ffast-math)可能改变浮点行为,这不是 Agent 代码的问题,但会干扰验证结论。
8. 从试点到生产:Agentic Workflow 的工程建议
当你在双电子积分这个小模块上验证了整套流程之后,下一步要考虑的是如何把该方法扩展到 GAMESS 的其他部分,以及如何让工作流在团队里真正带来长期价值。
第一步是固化“转换契约”。把数值误差阈值、接口兼容规则、性能回归目标写进配置文件,让所有 Agent 和验证脚本共享同一份标准。这样即使是新成员参与,也能快速理解“改到什么程度算成功”。
第二步是建立独立的验证分支。不要在主干上直接跑 Agent 生成的代码,而是在 git 分支里完成转换和验证,确认通过后再走人工评审和合并。评审人不仅要看代码,还要看转换日志和数值对比报告。Agent 的上下文越长、越复杂,越容易在某个细节上犯迷糊,所以每个阶段都要能追溯到输入输出。
第三步是给 Agent 提供丰富但受控的上下文。双电子积分的转换,除了源码本身,往往还需要基组定义、参考文献公式、旧代码的注释说明。把这些材料整理成规范的知识库格式,能显著提高转换质量。但要控制上下文长度,避免无关内容干扰。
第四步是警惕“局部正确、全局错误”。双电子积分模块的输入输出比较独立,但放到完整 GAMESS 里,它还会被 SCF 和梯度模块调用。局部数值误差可能因为迭代放大成不可接受的结果。所以,在模块级验证后,一定要设计几个中等规模分子的端到端回归测试,对比总能量和梯度。
这些建议本质上是在强调同一个道理:Agentic Workflow 在 HPC 现代化中能提效,但前提是团队愿意为它设计验收标准、构建测试环境、维护失败案例库。缺少这些工程投入,它很快会退化成“AI 自动改代码、人类被动救火”的新型技术债。
更进一步,这项工作对 Agentic Workflow 领域的启示也很明显:双电子积分只是 GAMESS 的一个核心计算单元,其他模块比如 DFT 格点积分、SCF 收敛器、解析梯度、溶剂模型,每一项都有自己的数值语义和性能特征。只有当这些模块的转换方法论被逐个验证、沉淀成可复用的工作流模板后,Legacy HPC 现代化才真正有了规模化的可能。
如果你正在一个遗留 HPC 项目里尝试引入 Agent,我的建议很简单:先圈定一个边界清晰、可验证、有实际意义的核心单元,比如双电子积分中的某一条完整计算路径,然后设计转换契约,跑通“分析-转换-验证-人工审查”的最小闭环。不要一上来就追求整库自动重写,不要跳过数值对比,更不要相信一次生成的代码可以直接进主干。把每一次转换都当成一次带有验收标准的工程交付,这套方法论才能真正在性能、可维护性和风险控制之间找到平衡。