news 2026/8/31 12:26:57

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用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-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.txt

compare_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,我的建议很简单:先圈定一个边界清晰、可验证、有实际意义的核心单元,比如双电子积分中的某一条完整计算路径,然后设计转换契约,跑通“分析-转换-验证-人工审查”的最小闭环。不要一上来就追求整库自动重写,不要跳过数值对比,更不要相信一次生成的代码可以直接进主干。把每一次转换都当成一次带有验收标准的工程交付,这套方法论才能真正在性能、可维护性和风险控制之间找到平衡。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 12:25:53

Ubuntu更改最大化最小化按钮大小,sudo与su

/etc/apt/sources.list#编辑源信息 脚本中使用apt-get,平常在交互式shell中就使用apt apt更新(2014年发布),apt-get(1998年发布) apt解决了apt-get的一些设计错误,但是apt-get是向后兼容的,因此在脚本中使用apt-get#最常用的包管理命令分散在apt-get,apt-cache,apt-config中 #a…

作者头像 李华
网站建设 2026/8/31 12:25:33

渲染实现某个PAGE能对特定组查看

想要实现某个page能对特定组可见&#xff0c;能通过渲染实现 代码如下 加一句visibleWhen“owning_groupDBA” //渲染文档内容 <?xml version"1.0" encoding"UTF-8"?> <!--Copyright 2012. Siemens Product Lifecycle Management Software Inc.…

作者头像 李华
网站建设 2026/8/31 12:21:33

360春招Windows开发笔试深度解析:核心考点与底层机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/8/31 12:21:31

IDM与鼓打贝斯声音设计:从Breakbeat到混音的完整技术解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/8/31 12:21:14

本地模型驱动的自动编程:从零实现最小 dev harness

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华