用Agentic Workflow啃Legacy HPC代码:以GAMESS双电子积分为例

用Agentic Workflow啃Legacy HPC代码:以GAMESS双电子积分为例 如果你的工作涉及维护量子化学计算程序很可能面对过这样的场景某个核心模块还是 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-6SCF 迭代可能直接不收敛。第三层是验证环境。很多遗留 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(nameagent_output, contentresponse) 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我的建议很简单先圈定一个边界清晰、可验证、有实际意义的核心单元比如双电子积分中的某一条完整计算路径然后设计转换契约跑通“分析-转换-验证-人工审查”的最小闭环。不要一上来就追求整库自动重写不要跳过数值对比更不要相信一次生成的代码可以直接进主干。把每一次转换都当成一次带有验收标准的工程交付这套方法论才能真正在性能、可维护性和风险控制之间找到平衡。