Agentic Workflow 驱动 GAMESS 双电子积分代码现代化改造
在遗留 HPC High Performance Computing系统中最棘手的问题往往不是单个算法的性能而是那些运行了几十年、由 Fortran 写成、被无数研究者修改过的核心代码。GAMESS 这类量子化学计算软件中的双电子积分Two-Electron-Integral模块正是这类代码的典型代表它承担了电子结构计算中绝大部分浮点运算量同时承载着极其复杂的索引变换、对称性分支和精度要求。近年来利用 Agentic Workflow 对这类代码进行现代化改造正在成为一种值得关注的新思路。所谓 Agentic Workflow并不是简单的“让大模型重写代码”而是把“分析、规划、生成、编译、测试、修复”这一系列工程动作交给可自主决策的智能体闭环完成。与传统的静态代码翻译工具相比它更像一个能自己读报错日志、能自己补测试用例、能根据编译器和运行时反馈持续修改代码的开发者。本文会从双电子积分核心的转换难点出发拆解一个可落地的 Agentic Workflow 设计并给出环境准备、关键代码、数值验证和排错路径。如果你正在处理旧版 Fortran 科学计算程序或者想把大模型真正用进生产级代码迁移这篇文章会提供一个从思路到实现的最小闭环。1. 先理解为什么双电子积分是 HPC 现代化的试金石1.1 双电子积分在量子化学计算中的角色双电子积分描述的是两个电子的库仑相互作用在量子化学的 Hartree-Fock 方法、密度泛函理论和更高阶后自洽场方法中都会反复出现。它的数学形式通常可以写成下面的表达式[ (\mu\nu|\lambda\sigma) \int \int \chi_\mu(r_1)\chi_\nu(r_1)\frac{1}{r_{12}}\chi_\lambda(r_2)\chi_\sigma(r_2) dr_1 dr_2 ]其中 (\chi_\mu) 是基函数(r_{12}) 是两个电子之间的距离。实际计算中这个式子会被展开成大量径向和角向部分的组合再根据分子对称性、基函数壳层类型和积分精度要求进行分支处理。GAMESS 的双电子积分核心就是在超大规模的循环中反复计算这类表达式并按照对称性、密度矩阵和 Fock 矩阵更新规则对结果进行归约。由于积分计算量庞大这段代码的性能直接决定整个自洽场迭代的速度。遗留 HPC 项目里这段代码往往经过了几十年的手工优化有循环分块、临时的公共块缓存、平台相关的汇编级优化、甚至带有历史遗留的 GOTO 跳转。现代化改造想把这段代码迁移到更易维护、更适合 GPU 或新向量化指令集的实现上难度非常高。1.2 遗留 Fortran 代码转换为什么难第一类困难来自语言层面。旧式 Fortran 77 代码大量使用隐式变量类型、COMMON 块、EQUIVALENCE 语句和固定格式缩进。默认情况下变量名的首字母就能决定整型或浮点型这种代码在迁移到现代 Fortran 或 C 时非常容易产生类型不匹配。第二类困难来自数据依赖。双电子积分代码往往依赖全局数组、索引映射表和临时缓存不同子程序之间通过 COMMON 块交换数据。Agent 在转换时如果只看到某一个子函数就会产生“局部正确、整体错误”的假象。第三个问题在于验证成本。双电子积分的输出不是简单的布尔值而是浮点数数组。转换后的代码必须在同一输入下产生与原实现逐位接近的结果。但编译器优化、浮点运算顺序、向量化指令都可能带来微小差异。因此代码转换工作流不能只做“编译通过”验证还必须建立数值一致性测试和性能回归测试。这里要注意一个常见误区用大模型做代码转换如果只给一个子程序片段得到的代码可能语法正确但缺少全局上下文。正确做法是在工作流中加入“上下文收集”阶段让 Agent 先读取变量声明、公共块定义、调用关系再开始生成代码。2. 设计 Agentic Workflow从任务分解到自动验证2.1 Agentic Workflow 与传统代码翻译工具的区别传统方案比如f2c是把 Fortran 代码翻译成 C 语言的自动化工具。这类工具遵循固定规则擅长处理语法层面的转换但不理解“为什么这段代码要这样写”。遇到 GOTO 跳转和全局变量时它会机械地保留甚至放大原有结构导致输出代码几乎不可读。Agentic Workflow 则把代码转换看作一个需要多轮决策的工程任务。智能体可以拆解任务、搜索相关代码位置、编写转换计划、调用编译器、读取报错信息、修改代码、运行测试、根据数值误差调整优化策略。它不再是“一次性翻译”而是一个“计划-执行-验证-修复”的循环。举个例子传统工具遇到一个 Fortran 子程序可能直接把它转换为一个 C 函数。Agent 则可能先识别出该子程序只修改 COMMON 块中的两个数组于是把函数签名收窄为只传这两个数组同时把被其他子程序冲掉的中间变量提升为局部变量。这种决策没有出现在原代码中而是 Agent 根据上下文推理出来的。2.2 工作流的五个核心阶段一个可落地的转换工作流通常包含五个阶段阶段目标关键产出验证方式上下文收集理解原代码的输入、输出、全局依赖变量清单、调用关系图、公共块映射静态分析通过任务规划把大模块拆成可验证的小任务转换顺序、测试策略任务覆盖所有入口点代码生成按规划生成目标语言实现新代码文件、构建脚本编译器通过构建修复解决编译错误和链接错误可执行目标无未解析符号数值验证验证与原实现的一致性比对报告、误差统计误差在阈值内阶段之间要形成反馈闭环。代码生成失败后Agent 需要读取编译器输出定位是类型问题、宏缺失还是调用约定不匹配。数值验证失败后Agent 需要判断是浮点顺序问题还是算法逻辑错误。如果只做线性流程Agent 很难完成复杂内核的迁移。2.3 为 Agent 设计合适的任务边界一个常见错误是让 Agent 在一个请求里完成整个双电子积分模块的转换。上下文会超过模型窗口报错信息难以归因数值验证也无法定位问题。更好的做法是按“数据流”而不是按“文件”切分任务。推荐切分方式先转换不依赖全局状态的计算核函数例如某个基组函数的径向部分。再转换需要访问公共块的积分组合函数。最后转换外层循环和调度逻辑。每一步转换后都要保证代码可以编译并且可以用一个小测试用例验证输出。这样 Agent 不需要在某个瞬间掌握整个系统而是通过多轮迭代慢慢逼近正确结果。这种渐进式任务设计也更容易加入人工审查。3. 环境准备与最小可复现骨架3.1 软件依赖和验证工具要让这个工作流跑起来需要准备以下几类工具LLM API一个支持对话补全的模型接口可以是 OpenAI 兼容服务也可以是本地部署的开源模型。编译器工具链转换目标如果是 C需要g或clang如果目标仍是现代 Fortran需要gfortran。脚本环境Python 3.9 以上版本用于编排 Agent 调用。构建工具CMake或简单的 Makefile用于把生成的代码编译成可执行文件。比对工具用于数值一致性验证的脚本比如基于numpy比较两个数组的最大绝对误差和最大相对误差。在项目初期建议在一个固定版本的容器中运行所有步骤避免因编译器版本差异导致 Agent 反复修复非实质性问题。下面的表格给出一个基础环境版本示例实际使用时要根据项目情况确认组件示例版本说明Python3.10运行 Agent 脚本gfortran10编译原始 Fortran 参考实现g11编译转换后的 C 代码CMake3.20管理和构建目标代码LLM 服务任意通过 HTTPS 接口调用3.2 定义目标代码结构和测试用例在让 Agent 开始工作之前要先定义一个可编译的骨架目录。假设原始代码中有一个 Fortran 子程序twocenter_repulsion我们需要把它转换成 C 版本。最小骨架如下project/ reference/ integrals.f90 run_reference.f90 generated/ CMakeLists.txt src/ two_center_integral.cpp two_center_integral.hpp workflow/ agent.py tools/ compiler.py numeric_check.py tests/ case_001/ input.json output_reference.npyreference/目录保留原有 Fortran 代码用来生成参考输出。generated/目录存放 Agent 生成的新代码。workflow/目录存放驱动 Agent 的脚本和工具函数。tests/目录用于保存不同测试输入和预期输出。如果原始材料没有给出 GAMESS 的真实函数名和接口这里要强调以上函数名只是示例实际项目必须先确认原代码的调用约定。不要凭空假设函数签名。3.3 准备一个用于代理调用的工具接口Agent 不能直接执行编译器命令所以工作流中需要提供一个“工具注册表”。每个工具是一个 Python 函数Agent 通过调用这些函数完成读取文件、编译、运行测试等操作。下面是一个最小工具接口示例# workflow/tools.py import subprocess import json import numpy as np def read_file(path: str) - str: with open(path, r, encodingutf-8) as f: return f.read() def write_file(path: str, content: str) - str: with open(path, w, encodingutf-8) as f: f.write(content) return fwrite {path} success def compile_it(generated_dir: str) - str: result subprocess.run( [cmake, --build, generated_dir], capture_outputTrue, textTrue ) if result.returncode ! 0: return fCOMPILE_ERROR:\n{result.stdout}\n{result.stderr} return COMPILE_OK def run_reference(input_json: str) - str: 运行参考 Fortran 实现生成 reference output.npy # 实际实现需要调用编译好的 Fortran 可执行文件 return REFERENCE_RUN_OK def run_generated(input_json: str) - str: 运行转换后的 C 实现生成 generated output.npy return GENERATED_RUN_OK def compare_numeric(threshold: float 1e-8) - str: ref np.load(tests/case_001/output_reference.npy) gen np.load(tests/case_001/output_generated.npy) max_abs np.max(np.abs(ref - gen)) max_rel np.max(np.abs(ref - gen) / (np.abs(ref) 1e-12)) if max_abs threshold and max_rel threshold: return fNUMERIC_OK max_abs{max_abs:.3e} max_rel{max_rel:.3e} return fNUMERIC_FAIL max_abs{max_abs:.3e} max_rel{max_rel:.3e}这里的关键是要让 Agent 看到所有工具的执行结果。编译器报错、数值比对结果和文件内容都要被编码成文本回传给模型。不要只返回布尔值否则 Agent 无法根据日志修复问题。4. 实现转换工作流的关键代码4.1 定义输入 Fortran 片段为了说明工作流下面给出一个非常简化的双电子积分相关子程序。它不是真实 GAMESS 代码只为演示 Agent 需要处理的类型和上下文问题C SIMPLIFIED TWO-CENTER REPULSION KERNEL SUBROUTINE TWCENT(ISH, JSH, A, B, RESULT) IMPLICIT DOUBLE PRECISION (A-H,O-Z) DIMENSION A(3), B(3) DOUBLE PRECISION RESULT(*) COMMON /BASDAT/ ALPHA(100), COEF(100) CX A(1) - B(1) CY A(2) - B(2) CZ A(3) - B(3) DIST2 CX*CX CY*CY CZ*CZ GAM ALPHA(ISH) ALPHA(JSH) PREF (4.0D0 * ALPHA(ISH) * ALPHA(JSH) / GAM) PREF DSQRT(PREF) RESULT(1) PREF * DEXP(-COEF(ISH) * COEF(JSH) * DIST2 / GAM) RESULT(2) GAM RETURN END这个片段的难点有三个IMPLICIT DOUBLE PRECISION (A-H,O-Z)定义了整段代码的隐式类型所有以 A 到 H、O 到 Z 开头的变量都是双精度浮点。COMMON /BASDAT/表示它依赖外部公共块中的ALPHA和COEF。RESULT(*)是假定大小数组实际长度由调用方约定。这些信息如果不传递给 Agent生成的新代码几乎必然有问题。4.2 用 Agent 完成代码生成与编译修复工作流脚本按下面的方式组织 Agent 循环# workflow/agent.py import json from openai import OpenAI client OpenAI(base_urlhttp://localhost:8000/v1, api_keyEMPTY) system_prompt 你是一个 HPC 代码迁移工程师。你的任务是把 Fortran 77 子程序迁移到现代 C。 你只能使用环境提供的工具读取文件、编译和运行测试。 每次行动必须遵循以下格式 THOUGHT: 分析当前情况 ACTION: read_file(generated/src/two_center_integral.cpp) --- 工具结果 --- 如果你认为任务完成请输出: FINAL_ANSWER: 完成描述 def run_agent(objective: str) - str: messages [ {role: system, content: system_prompt}, {role: user, content: objective}, ] for step in range(10): response client.chat.completions.create( modellocal-model, messagesmessages, ) output response.choices[0].message.content print(fStep {step}: {output[:200]}) action parse_action(output) if action is None: break tool_name, arg action result execute_tool(tool_name, arg) messages.append({role: assistant, content: output}) messages.append({role: user, content: f工具结果:\n{result}}) return output实际使用中模型输出解析要兼容没有 ACTION 的情况。如果 Agent 连续多轮没有调用工具工作流应该主动中断避免空转浪费资源。4.3 两个数值一致性测试的对比方式为了验证数值一致性需要为同一个输入准备参考输出和新输出。测试输入应该覆盖典型参数、边界参数和异常参数。下面是一个测试输入 JSON 示例{ ish: 1, jsh: 2, a: [0.0, 0.0, 0.0], b: [1.5, 0.0, 0.0] }参考 Fortran 程序读取这个 JSON 后把结果写成output_reference.npy。Agent 生成的 C 程序读取同样的输入把结果写成output_generated.npy。最后调用compare_numeric工具判断是否通过。这里要注意直接比较数组元素还不够。双电子积分代码中经常有数组顺序被打乱、符号翻转或索引从 0 起步等差异。建议除逐元素比较外同时比较数组的范数、最大值、最小值和排序后的数组。这样即使索引顺序不同也能识别出数值等价性。5. 运行验证编译、数值对齐与性能回归5.1 转换后的代码长什么样一个可能的 C 转换结果如下。它只代表一个可接受形态不一定是 Agent 的最终输出#include two_center_integral.hpp #include cmath #include array constexpr int MAX_BAS 100; extern double global_alpha[MAX_BAS]; extern double global_coef[MAX_BAS]; void twocent(int ish, int jsh, const std::arraydouble, 3 a, const std::arraydouble, 3 b, std::vectordouble result) { double cx a[0] - b[0]; double cy a[1] - b[1]; double cz a[2] - b[2]; double dist2 cx*cx cy*cy cz*cz; double gam global_alpha[ish] global_alpha[jsh]; double pref 4.0 * global_alpha[ish] * global_alpha[jsh] / gam; pref std::sqrt(pref); result[0] pref * std::exp(-global_coef[ish] * global_coef[jsh] * dist2 / gam); result[1] gam; }这个示例把 Fortran 的 COMMON 块映射成了全局数组。对于学习项目这个方式足够。在生产级转换中更推荐把global_alpha和global_coef设计成上下文对象作为参数传入函数。Agent 是否选择这种方式取决于提示词中对架构目标的描述。5.2 验证三个层次编译、数值与性能验证不能只有一步至少应该分三个层次第一层编译验证。Agent 每生成一段代码工作流就应该尝试编译。编译失败时要记录错误类型和位置方便下一轮修复。第二层数值验证。将参考输出和新输出导入同一套比较工具统计最大绝对误差和最大相对误差。第三层性能验证。对于遗留 HPC 代码性能回归是核心目标。如果转换后的代码比原实现慢一个数量级那么即使数值正确现代化改造也算失败。性能验证需要在固定优化选项下进行。建议分别编译 Fortran 参考版和 C 新版使用相同的优化级别比如-O2然后在同一台机器上运行多次取中位数耗时。下表是一个简单的性能对比记录模板数据集参考耗时 (ms)转换后耗时 (ms)相对误差结论小分子 STO-3G12.314.11.2e-10可接受中分子 6-31G*210.5185.22.3e-10初步达标大分子 6-311G**3050.83010.44.1e-10待优化如果性能不达标Agent 需要进入优化循环。优化循环中要特别注意不能让 Agent 通过牺牲精度来提升性能。每次性能修改后都要重新运行数值测试。5.3 通过日志理解 Agent 的决策过程Agentic Workflow 的优势在于可以回放决策日志。每轮交互应该记录用户目标和当前任务Agent 的分析文本调用的工具和参数工具返回结果Agent 的下一步计划这些日志可以用 JSON 格式保存便于事后分析。示例日志结构{ step: 3, thought: 编译失败错误是变量 dist2 未声明, action: read_file(generated/src/two_center_integral.cpp), tool_result: 文件内容 ..., plan: 在函数开头补充 double dist2; 变量声明 }日志不仅能用来排查 Agent 行为也能作为转换工作的文档。当项目结束后整个迁移过程本身就变成了一份可审查的记录。6. 常见问题与排查路径6.1 Agent 生成的代码在编译时找不到符号现象编译器报undefined reference to global_alpha或twocent was not declared。常见原因Agent 只修改了.cpp文件却没有在头文件中声明外部变量或者原始 Fortran 的 COMMON 块名称没有正确映射到全局符号。排查方式检查头文件是否声明了extern double global_alpha[];。检查新代码的函数名大小写与调用方是否一致。搜索生成目录中的符号表确认链接阶段是否包含目标文件。解决方案在提示词里增加“必须修改 CMakeLists.txt 和头文件”的约束或者为 Agent 增加一个工具自动列出头文件中的公开符号。6.2 数值误差超出阈值现象最大绝对误差在1e-4级别但对于双精度计算来说不可接受。常见原因Fortran 使用DSQRT、DEXPC 使用std::sqrt、std::exp在绝大多数平台上精度一致。真正的问题通常来自中间表达式的计算顺序不同。例如 Fortran 编译器可能将a*b/c改成a*(b/c)而 C 编译器可能按从左到右计算。检查方式对比参考代码和新代码在相同输入下的中间变量。测试浮点舍入模式是否一致。检查是否使用了float代替double。解决方案在数值验证工具中对每个输出元素做误差统计如果误差稳定出现在某个元素把该元素对应源码行展示给 Agent要求目标代码使用与 Fortran 一致的表达式括号。6.3 转换后的代码被外部调用时行为不一致现象单元测试通过但集成到整个 GAMESS 流程后结果错误。常见原因Agent 只转换了核心子程序但没有处理调用它的外层代码对新函数签名的适配或者原始代码通过 COMMON 块隐式传递状态而新代码把状态变成了局部变量。排查路径检查新函数是否仍然保证对全局状态的读写。检查调用方传入的数组大小是否足够。用原程序的随机输入集分别调用参考实现和新实现比较输出分布。解决方案在 Agent 工作流中加入“调用点分析”阶段提前告诉 Agent 哪些代码会调用转换后的函数。6.4 代理在循环中空转现象Agent 反复修改文件但每次都产生相同的错误或者反复调用同一工具。常见原因上下文窗口被过长的编译日志淹没Agent 丢失了真正的错误位置或者模型没有能力修改复杂的构建脚本。排查方式查看日志中的工具调用序列如果连续三轮没有调用新工具或没有改变任何文件就终止本轮让 Agent 输出当前卡点。解决方案在工作流中设置最大轮数比如 10 轮。如果达到上限切换到一个只做“问题定位”的 Agent而不是继续让同一个 Agent 盲目修改代码。7. 生产落地的最佳实践7.1 不要一次性转换整个内核双电子积分核心往往有数万行代码。把整个模块一次性交给 Agent不仅输入超长编译验证和数值定位也会变得困难。推荐的粒度是函数级或基本块级一次转换一个能独立测试的子程序。子程序之间通过临时适配层相互调用保证项目随时可以编译。7.2 用测试金字塔约束 Agent 行为Agent 的工作离不开测试。在转换过程中至少要有三个层次的测试单元测试针对单个计算核函数输入输出都可以人工构造。集成测试把多个转换后的函数组合起来和原始 Fortran 结果对比。回归测试使用真实 GAMESS 的典型分子输入验证整体能量或梯度没有漂移。回归测试在 CI 中自动运行。只要任一层次失败Agent 就必须回到对应阶段修复不能通过降低测试阈值来绕过问题。7.3 保留人工审查节点和回滚策略Agentic Workflow 并不能完全替代人工审查尤其是涉及数值算法和全局数据依赖的代码。建议在三个阶段加入人工确认任务规划完成后、每个大模块生成结束后、最终合入主分支前。同时每一轮 Agent 修改前都要生成文件快照方便回滚到上一个可编译状态。回滚机制可以用git实现。Agent 每次修改完成并通过测试后创建一次提交。如果后续修改引入问题可以直接回退到最新的绿色提交。7.4 可复用检查清单下面是一个检查清单适合在每次代码转换任务开始前和完成后使用检查项是否完成说明确认原函数入口和返回方式是/否避免遗漏 COMMON 块参数确认隐式变量类型是/否检查 Fortran 首字母规则建立参考输出是/否使用固定输入文件生成 npy编译目标代码是/否使用与参考代码相同的优化级别数值一致性测试是/否设定最大相对误差阈值性能回归测试是/否与参考实现进行多次耗时对比代码审查是/否检查 Agent 是否引入了未使用变量提交到版本库是/否保留可回滚快照把这个清单放入工作流的每个阶段Agent 就能在完成代码生成后主动检查自己是否遗漏了关键步骤。对于新手团队这份清单也能帮助理解遗留 HPC 现代化改造的核心风险。从 GAMESS 双电子积分核心这个案例可以看出Agentic Workflow 的价值不在于一次性产出高质量代码而在于把“高效试错”变成工程过程。它可以在编译器和数值测试的反馈下不断修正输出并留下完整的决策日志。对于很多还没有进入生产 HPC 系统的软件团队来说先用一个小规模积分核函数跑通这套流程再逐步扩大转换范围是成本最低也最稳妥的起点。