【零基础学智能仿真-53】多智能体协作——有限元自动化实战从工程需求到可审核的仿真报告

本节摘要

本节承接上一节的仿真质量检查,将需求分析、几何建模、网格划分、求解、后处理和报告组织成职责明确的协作流程。通过一个真实求解的一维杆有限元案例,学习结构化任务交接、状态管理和独立质量检查,并通过缺失参数、求解失败与结果篡改测试,理解系统何时应继续、暂停或拒绝发布结果。本节采用Python函数模拟协作角色,不调用大模型或Abaqus,为后续接入真实智能体与CAE求解器建立可验证的基础。


一、从"一个程序完成所有工作",到"每一步都有明确责任"

上一节,我们为有限元结果增加了质量检查。

但当自动化流程越来越长,新的问题会出现:

一次结果检查失败,究竟应该让谁修改什么?

例如:

  • 材料参数缺失,需要补充需求,而不是重新划网格。
  • 单元连接关系错误,需要修正模型,而不是放宽求解容差。
  • 求解已经完成,但读取了错误的分析帧,需要修正后处理,而不是重新建模。
  • 位移与理论值不符,需要先定位原因,而不是让报告程序把结论写得更肯定。

如果所有工作都塞进一个没有清晰边界的"大助手",错误可能在多次修改后越来越难追踪。

本节采用另一种组织方式:

每个角色只处理明确的任务,交接明确的数据,由程序控制下一步是否允许执行。

这里的"协作"不是让几个智能体轮流发表意见,而是建立可以检查的工作关系。

本节需要掌握的四件事

  1. 如何拆分有限元自动化任务。
  2. 不同角色之间应该传递哪些数据。
  3. 谁负责决定流程继续还是停止。
  4. 如何用可运行的小案例验证整个协作设计。

二、六个工作角色,加一道独立检查

本节采用六个工作角色。

角色 主要职责 必须交出的结果 不应该擅自做的事
需求分析 确认单位、参数、边界条件与目标 结构化问题规格 猜测缺失的材料参数
几何建模 根据规格建立几何与材料描述 模型数据 改变载荷或分析目的
网格划分 建立节点和单元连接关系 网格数据 为了通过检查而改变物理问题
求解执行 组装方程、调用求解器 原始求解结果与状态 把失败状态改成成功
后处理 提取位移、应力和反力 含单位的结果记录 修改原始结果以迎合预期
报告生成 汇总结果、检查与限制 成功报告或停止报告 隐藏失败项

此外,在后处理与报告之间增加一道质量检查。

它不依赖某个角色说"我认为正确",而是执行事先定义的数值规则。

整体关系如下:

复制代码
工程需求
   │
   ▼
需求分析 ── 信息不完整 ──→ 等待补充
   │
   ▼
几何建模
   │
   ▼
网格划分
   │
   ▼
求解执行 ── 执行失败 ───→ 停止报告
   │
   ▼
后处理
   │
   ▼
质量检查 ── 检查不通过 ─→ 拒绝发布合格结论
   │
   ▼
通过检查的结果报告

需要注意:独立检查不等于必须增加一个大模型。

力平衡、数值残差、单位检查等任务,通常可以直接交给确定性的程序。大模型可以解释检查结果,但不应获得随意修改检查结论的权限。


三、本节实现到什么程度?

为了让协作机制可以在普通电脑上直接运行,本节分清三个层次。

层次 本节是否实现
真实的一维杆有限元组装与求解 是
六个角色之间的数据交接、检查和停止机制 是
大模型自主推理、自然语言解析、Abaqus调用 否

本节程序是"多智能体工作流的确定性演练",不是已经接入大模型的多智能体系统。

六个角色暂时由六个 Python 函数承担。这样做是为了先确认:

  • 输入是否明确;
  • 交接是否完整;
  • 求解结果是否正确;
  • 失败时能否停止;
  • 报告是否如实反映状态。

之后接入大模型,替换的是部分角色的决策方式,而不是取消这些检查。

本节也不是对某个特定 AutoFEA 项目的代码复现,而是建立一个可以迁移到类似系统中的教学原型。


四、从自然语言需求开始:先明确,再计算

假设用户提出:

请计算一根长1000毫米、截面积100平方毫米的均匀直杆。弹性模量为210000兆帕,左端固定,右端施加10000牛轴向拉力,采用两个等长杆单元,输出节点位移、单元应力和支承反力。

这段需求包含了计算必需的信息。

需求分析角色需要将其整理成结构化记录:

复制代码
REQUEST = {
    "units": "N-mm-MPa",
    "L_mm": 1000.0,
    "A_mm2": 100.0,
    "E_MPa": 210000.0,
    "F_N": 10000.0,
    "elements": 2,
    "support": "left_fixed",
    "load": "right_axial_tension",
}

本节代码直接使用这份人工确认的结构化记录,不会自动解析上述自然语言。

将来接入大模型后,可以让需求分析角色提出这份记录,再由程序校验,必要时请用户确认。

4.1 为什么不能只传递一句"这是钢杆"?

"钢杆"并没有完整指定:

  • 弹性模量;
  • 是否考虑塑性;
  • 材料适用温度;
  • 分析采用的单位。

如果用户只说"计算钢杆的伸长",需求角色应提出补充问题,而不是悄悄填入一个弹性模量。

4.2 为什么要限制适用范围?

本节只支持:

  • 均匀截面直杆;
  • 线弹性、小变形;
  • 轴向拉伸;
  • 左端固定、右端集中力;
  • 两个等长一维杆单元;
  • N--mm--MPa 单位体系。

如果用户提出弯曲、接触或塑性问题,正确行为是明确说明本工具不支持,而不是继续调用这个求解器。

知道自己不能计算什么,也是可靠自动化的一部分。


五、真实有限元计算:回到我们熟悉的杆单元

选择一维杆,不是为了降低本节难度,而是为了让协作流程中的每一个数值都能手算核对。

5.1 几何与网格

三个节点的位置为:

\ x_1=0,\\qquad x_2=500,\\qquad x_3=1000\\ \\mathrm{mm} \\

两个单元分别连接:

\ e_1:(1,2),\\qquad e_2:(2,3) \\

每个单元长度:

\ L_e=500\\ \\mathrm{mm} \\

程序采用从零开始的索引,因此对应的连接关系写成:

复制代码
{
    "x_mm": [0.0, 500.0, 1000.0],
    "connectivity": [[0, 1], [1, 2]]
}

这里的 0、1、2 是程序索引,与教材中的节点 1、2、3 一一对应。

5.2 单元刚度矩阵

\ \\boldsymbol{k}\^{(e)} = \\frac{EA}{L_e} \\begin{bmatrix} 1 \& -1\\\\ -1 \& 1 \\end{bmatrix} \\

代入:

\ \\frac{EA}{L_e} = \\frac{210000\\times100}{500} = 42000\\ \\mathrm{N/mm} \\

组装得到:

\ \\boldsymbol{K} = 42000 \\begin{bmatrix} 1 \& -1 \& 0\\\\ -1 \& 2 \& -1\\\\ 0 \& -1 \& 1 \\end{bmatrix} \\

载荷向量:

\ \\boldsymbol{f} = \\begin{bmatrix} 0\\\\ 0\\\\ 10000 \\end{bmatrix} \\ \\mathrm{N} \\

施加:

\ u_1=0 \\

约束后求解两个自由位移,得到:

\ \\boldsymbol{u} = \\begin{bmatrix} 0\\\\ 0.238095\\\\ 0.476190 \\end{bmatrix} \\ \\mathrm{mm} \\

本节程序确实组装并求解这个方程,不是直接把理论位移公式当作求解器返回值。

5.3 应力与反力

单元应力:

\ \\sigma\^{(e)} = E\\frac{u_j-u_i}{L_e} \\

两个单元均得到:

\ \\sigma\^{(1)}=\\sigma\^{(2)}=100\\ \\mathrm{MPa} \\

利用未施加约束前的整体刚度矩阵恢复支承反力:

\ \\boldsymbol{r} = \\boldsymbol{K}\\boldsymbol{u}-\\boldsymbol{f} \\

左端支承反力为:

\ R_1=-10000\\ \\mathrm{N} \\

这些数值为后续协作流程提供了可核对的依据。


六、角色之间如何交接,才能减少误解?

在一次聊天中,角色可能这样回复:

网格已经划分完成,看起来没有问题。

这句话不足以驱动下一步求解。

求解角色真正需要的是:

复制代码
节点坐标
单元连接关系
材料与截面参数
边界条件
载荷

因此,本节采用结构化数据交接:

复制代码
需求规格 spec
    ↓
模型数据 model
    ↓
网格数据 mesh
    ↓
原始求解记录 raw
    ↓
后处理结果 result
    ↓
检查结果 checks

其中,raw 与 result 分开保存。

这一区分很重要:

  • raw 保存求解器返回的原始位移、刚度矩阵和载荷。
  • result 保存准备向用户展示的位移、应力和反力。

如果后处理出了问题,原始记录仍可以用于追查。

在本节代码中,后处理位移列表通过复制获得:

复制代码
"u_mm": list(u)

因此,后处理数据被修改时,不会顺带改掉原始位移列表。

这不是完整的安全隔离机制,但可以避免一个常见的共享可变数据错误。


七、谁决定流程继续?不是"大家都说通过"

本节由一个明确的调度函数控制执行顺序。

其核心逻辑如下。下面是阅读用节选,完整程序见下一部分。

复制代码
spec = requirement_role(request)

model = geometry_role(spec)

mesh = mesh_role(model)

raw = solver_role(spec, model, mesh)

result = postprocess_role(model, mesh, raw)

checks = quality_gate(spec, raw, result)

status = (
    "VERIFIED"
    if all(checks.values())
    else "REJECTED"
)

这里包含两个关键原则。

7.1 依赖尚未完成,下一步不能启动

没有确认材料参数,就不能求解。

没有求解结果,就不能声称完成后处理。

不同任务之间存在物理与数据依赖,并不是角色越多、同时运行越多就越好。

7.2 工作流状态与报告措辞必须一致

本节定义四种最终状态:

状态 含义 允许输出什么
NEEDS_INPUT 需求缺失或不受支持 补充信息提示、停止报告
FAILED 执行过程中发生错误 错误位置与停止报告
REJECTED 得到结果,但检查未通过 带失败项的诊断报告
VERIFIED 本节配置的检查全部通过 限定范围内的验证报告

这里的 VERIFIED 只表示本节检查通过,不是工程认证,也不是对模型所有方面的保证。

程序输出中的:

复制代码
requirement: APPROVED

仅表示需求字段通过本节程序校验,不表示已经获得工程师签字批准。


八、完整代码与正常运行结果

8.1 代码

复制代码
"""Six-role workflow rehearsal with a real two-element axial FEM solver.

No LLM, Abaqus, network access or automatic retries. N-mm-MPa only.
"""
import argparse
import copy
import json
import math
from pathlib import Path


REQUEST = {"units": "N-mm-MPa", "L_mm": 1000.0, "A_mm2": 100.0,
           "E_MPa": 210000.0, "F_N": 10000.0, "elements": 2,
           "support": "left_fixed", "load": "right_axial_tension"}


def requirement_role(request):
    if request.get("units") != "N-mm-MPa":
        raise ValueError("confirm units: only N-mm-MPa is supported")
    for key in ("L_mm", "A_mm2", "E_MPa", "F_N"):
        value = request.get(key)
        if (isinstance(value, bool) or not isinstance(value, (int, float))
                or not math.isfinite(value) or not 1e-6 <= value <= 1e9):
            raise ValueError(f"confirm {key}: finite value in [1e-6, 1e9] required")
    if (type(request.get("elements")) is not int or request["elements"] != 2
            or request.get("support") != "left_fixed"
            or request.get("load") != "right_axial_tension"):
        raise ValueError("only two axial elements, left fixed, right tension are supported")
    return copy.deepcopy(request)


def geometry_role(spec):
    return {"start_mm": 0.0, "end_mm": spec["L_mm"],
            "A_mm2": spec["A_mm2"], "E_MPa": spec["E_MPa"]}


def mesh_role(model):
    return {"x_mm": [0.0, model["end_mm"] / 2, model["end_mm"]],
            "connectivity": [[0, 1], [1, 2]]}


def solver_role(spec, model, mesh):
    # ponytail: exactly two 1D elements; use a tested sparse solver for larger models.
    K = [[0.0] * 3 for _ in range(3)]
    for i, j in mesh["connectivity"]:
        le = mesh["x_mm"][j] - mesh["x_mm"][i]
        k = model["E_MPa"] * model["A_mm2"] / le
        K[i][i] += k
        K[i][j] -= k
        K[j][i] -= k
        K[j][j] += k
    f = [0.0, 0.0, spec["F_N"]]
    # u[0] = 0. Solve the remaining 2 x 2 system, not the analytical rod formula.
    a, b, c, d = K[1][1], K[1][2], K[2][1], K[2][2]
    det = a * d - b * c
    if not math.isfinite(det) or det <= 0:
        raise RuntimeError("invalid reduced stiffness")
    u = [0.0, (d * f[1] - b * f[2]) / det,
         (a * f[2] - c * f[1]) / det]
    return {"source": "PYTHON_FEM", "K_N_per_mm": K, "f_N": f, "u_mm": u}


def postprocess_role(model, mesh, raw):
    u, K, f = raw["u_mm"], raw["K_N_per_mm"], raw["f_N"]
    stresses = [model["E_MPa"] * (u[j] - u[i]) /
                (mesh["x_mm"][j] - mesh["x_mm"][i])
                for i, j in mesh["connectivity"]]
    reaction = sum(K[0][j] * u[j] for j in range(3)) - f[0]
    return {"u_mm": list(u), "sigma_MPa": stresses, "reaction_N": reaction}


def quality_gate(spec, raw, result):
    u, stress, reaction = result["u_mm"], result["sigma_MPa"], result["reaction_N"]
    if len(u) != 3 or len(stress) != 2 or not all(
            math.isfinite(v) for v in u + stress + [reaction]):
        raise ValueError("invalid result shape/value")
    F, L, E, A = (spec[k] for k in ("F_N", "L_mm", "E_MPa", "A_mm2"))
    K, f = raw["K_N_per_mm"], raw["f_N"]
    residual = max(abs(sum(K[i][j] * u[j] for j in range(3)) - f[i])
                   for i in (1, 2)) / F
    expected_tip = F * L / (E * A)
    close = lambda actual, expected: math.isclose(actual, expected, rel_tol=1e-9, abs_tol=1e-12)
    return {"fixed_end": close(u[0], 0.0),
            "force_balance": abs(reaction + F) / F <= 1e-9,
            "free_dof_residual": residual <= 1e-9,
            "tip_analytical": close(u[-1], expected_tip),
            "stress_analytical": all(close(s, F / A) for s in stress)}


def report_role(state):
    lines = ["# Lesson 53 workflow report", "", f"Status: {state['status']}",
             "", "Mode: deterministic roles; real 1D Python FEM; no LLM or Abaqus."]
    if "error" in state:
        lines += ["", f"Stopped: {state['error']}"]
    if "result" in state:
        lines += ["", "Results (not approved if status is not VERIFIED):",
                  "```json", json.dumps(state["result"], indent=2), "```"]
    if "checks" in state:
        lines += ["", "| Check | Result |", "|---|---|"]
        lines += [f"| {name} | {'PASS' if ok else 'FAIL'} |"
                  for name, ok in state["checks"].items()]
    return "\n".join(lines) + "\n"


def run(request, scenario="pass"):
    state = {"status": "NEW", "request": copy.deepcopy(request), "trace": []}
    stage = "requirement"
    try:
        spec = requirement_role(request)
        state["spec"] = spec
        state["trace"].append("requirement: APPROVED")
        stage = "geometry"
        model = geometry_role(spec)
        state["model"] = model
        state["trace"].append("geometry: BUILT")
        stage = "mesh"
        mesh = mesh_role(model)
        state["mesh"] = mesh
        state["trace"].append("mesh: 3 nodes / 2 elements")
        stage = "solver"
        if scenario == "solver_error":
            raise RuntimeError("injected solver failure (test only)")
        raw = solver_role(spec, model, mesh)
        state["raw"] = raw
        state["trace"].append("solver: SOLVED")
        stage = "postprocess"
        result = postprocess_role(model, mesh, raw)
        if scenario == "corrupt":
            result["u_mm"][-1] *= 1.2  # Inject a postprocessing error; raw stays intact.
        state["result"] = result
        state["trace"].append("postprocess: EXTRACTED")
        stage = "quality_gate"
        state["checks"] = quality_gate(spec, raw, result)
        state["status"] = "VERIFIED" if all(state["checks"].values()) else "REJECTED"
        state["trace"].append("quality_gate: " + state["status"])
    except (ValueError, RuntimeError, KeyError, TypeError, ArithmeticError) as exc:
        state["status"] = "NEEDS_INPUT" if stage == "requirement" else "FAILED"
        state["error"] = f"{stage}: {exc}"
        state["trace"].append(f"{stage}: {state['status']}")
    state["trace"].append("report: " + ("VERIFIED_REPORT" if state["status"] == "VERIFIED" else "STOP_REPORT"))
    return state, report_role(state)


def self_test():
    for force in (5000.0, 10000.0, 15000.0):
        state, _ = run(dict(REQUEST, F_N=force))
        assert state["status"] == "VERIFIED"
        assert math.isclose(state["result"]["u_mm"][-1], force / 21000)
    for scenario, status in (("corrupt", "REJECTED"), ("solver_error", "FAILED")):
        state, _ = run(REQUEST, scenario)
        assert state["status"] == status and state["trace"][-1] == "report: STOP_REPORT"
    for bad in (dict(REQUEST, units="unknown"), dict(REQUEST, E_MPa=None),
                dict(REQUEST, F_N=float("nan")), dict(REQUEST, A_mm2=-1)):
        state, _ = run(bad)
        assert state["status"] == "NEEDS_INPUT" and "raw" not in state
    assert run(REQUEST, "corrupt")[0]["raw"]["u_mm"][-1] != run(REQUEST, "corrupt")[0]["result"]["u_mm"][-1]
    print("SELF_TEST_OK")


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--scenario", choices=("pass", "units", "missing", "corrupt", "solver_error"), default="pass")
    parser.add_argument("--force", type=float, default=10000.0)
    parser.add_argument("--out", type=Path, help="New output directory; existing directory is rejected")
    parser.add_argument("--self-test", action="store_true")
    args = parser.parse_args()
    if args.self_test:
        self_test()
        return 0
    request = dict(REQUEST, F_N=args.force)
    if args.scenario == "units":
        request["units"] = "unknown"
    if args.scenario == "missing":
        del request["E_MPa"]
    state, report = run(request, args.scenario)
    for entry in state["trace"]:
        print(entry)
    if "result" in state:
        print(f"u_tip_mm={state['result']['u_mm'][-1]:.6f}")
        print("sigma_MPa=" + ", ".join(f"{s:.6f}" for s in state["result"]["sigma_MPa"]))
        print(f"reaction_N={state['result']['reaction_N']:.6f}")
    for name, ok in state.get("checks", {}).items():
        print(f"[{'PASS' if ok else 'FAIL'}] {name}")
    if "error" in state:
        print(state["error"])
    if args.out:
        try:
            audit = json.dumps(state, indent=2, allow_nan=False)
            args.out.mkdir(parents=True, exist_ok=False)
            (args.out / "audit.json").write_text(audit, encoding="utf-8")
            (args.out / "report.md").write_text(report, encoding="utf-8")
        except (OSError, ValueError) as exc:
            print(f"OUTPUT_ERROR: {exc}")
            return 2
    print("FINAL=" + state["status"])
    return 0 if state["status"] == "VERIFIED" else 1


if __name__ == "__main__":
    raise SystemExit(main())

仅使用 Python 标准库:

复制代码
argparse、copy、json、math、pathlib

不需要安装大模型框架,也不需要安装 Abaqus。

将文件保存到练习文件夹,并在该文件夹打开终端。

8.2 先执行自检

复制代码
python lesson53_workflow.py --self-test

预期输出:

复制代码
SELF_TEST_OK

自检覆盖正常载荷变化、后处理错误、求解失败及无效输入等情况。

8.3 运行完整正常流程

复制代码
python lesson53_workflow.py --scenario pass

实际核对过的输出如下:

复制代码
requirement: APPROVED
geometry: BUILT
mesh: 3 nodes / 2 elements
solver: SOLVED
postprocess: EXTRACTED
quality_gate: VERIFIED
report: VERIFIED_REPORT
u_tip_mm=0.476190
sigma_MPa=100.000000, 100.000000
reaction_N=-10000.000000
[PASS] fixed_end
[PASS] force_balance
[PASS] free_dof_residual
[PASS] tip_analytical
[PASS] stress_analytical
FINAL=VERIFIED

五项检查分别对应:

检查项 检查内容
fixed_end 固定端位移是否为零
force_balance 支承反力与外力是否平衡
free_dof_residual 自由节点的离散平衡方程是否满足
tip_analytical 末端位移是否符合解析解
stress_analytical 单元应力是否符合解析解

本节简单线弹性问题采用很严格的数值容差。不能把这些阈值直接迁移到接触、非线性或其他复杂模型中。

8.4 改变载荷,检查线性响应

例如:

复制代码
python lesson53_workflow.py --force 5000

主要结果应为:

复制代码
u_tip_mm=0.238095
sigma_MPa=50.000000, 50.000000
reaction_N=-5000.000000
FINAL=VERIFIED

三个载荷工况对应:

拉力 / N 末端位移 / mm 单元应力 / MPa
5000 0.238095 50
10000 0.476190 100
15000 0.714286 150

这符合线弹性均匀杆的关系:

\ u_{\\mathrm{tip}}=\\frac{FL}{EA} \\\ \\sigma=\\frac{F}{A} \\

这些工况也包含在程序自检中。


九、最有价值的实验:让错误结果无法冒充成功

9.1 故意制造后处理错误

运行:

复制代码
python lesson53_workflow.py --scenario corrupt

程序先正常求解,再故意把后处理中的末端位移放大20%:

复制代码
result["u_mm"][-1] *= 1.2

原始求解位移保持不变。

输出如下:

复制代码
requirement: APPROVED
geometry: BUILT
mesh: 3 nodes / 2 elements
solver: SOLVED
postprocess: EXTRACTED
quality_gate: REJECTED
report: STOP_REPORT
u_tip_mm=0.571429
sigma_MPa=100.000000, 100.000000
reaction_N=-10000.000000
[PASS] fixed_end
[PASS] force_balance
[FAIL] free_dof_residual
[FAIL] tip_analytical
[PASS] stress_analytical
FINAL=REJECTED

这份输出很值得仔细读。

反力仍然正确,应力仍然正确,但准备展示的位移已经错误。

因此:

几项结果看起来合理,不能替代对整份结果一致性的检查。

9.2 为什么自由节点残差能够发现问题?

离散平衡残差定义为:

\ \\boldsymbol{r}=\\boldsymbol{K}\\boldsymbol{u}-\\boldsymbol{f} \\

对于自由节点,正常情况下应满足:

\ r_i\\approx0 \\

本节采用归一化指标:

\ e_r= \\frac{\\max_{i\\in\\mathcal{F}}\|r_i\|}{\|F\|} \\

其中,\(\mathcal{F}\) 是自由节点集合。

末端位移被改为约 \(0.571429\ \mathrm{mm}\) 后,用这个错误位移重新代入方程,自由节点残差约为:

\ \\begin{bmatrix} -4000\\\\ 4000 \\end{bmatrix} \\ \\mathrm{N} \\

因此:

\ e_r=\\frac{4000}{10000}=0.4 \\

明显不满足离散平衡。

注意,固定节点处的残差对应支承反力,不能也要求它为零。因此程序只对自由节点检查这一项。


十、信息缺失和求解失败时,系统应该怎么做?

10.1 单位不明确:停止在需求阶段

运行:

复制代码
python lesson53_workflow.py --scenario units

预期输出:

复制代码
requirement: NEEDS_INPUT
report: STOP_REPORT
requirement: confirm units: only N-mm-MPa is supported
FINAL=NEEDS_INPUT

此时没有执行网格划分或求解。

这比"先随便算一下,再在报告里提醒单位可能不对"更符合工程流程。

10.2 材料参数缺失:不能擅自补齐

运行:

复制代码
python lesson53_workflow.py --scenario missing

预期输出:

复制代码
requirement: NEEDS_INPUT
report: STOP_REPORT
requirement: confirm E_MPa: finite value in [1e-6, 1e9] required
FINAL=NEEDS_INPUT

这里的数值范围是教学程序的输入保护范围,不是材料合理性的通用判据。

10.3 求解发生错误:不得继续假装后处理成功

运行:

复制代码
python lesson53_workflow.py --scenario solver_error

预期输出:

复制代码
requirement: APPROVED
geometry: BUILT
mesh: 3 nodes / 2 elements
solver: FAILED
report: STOP_REPORT
solver: injected solver failure (test only)
FINAL=FAILED

这是一项人为注入的测试错误,不是真实 Abaqus 故障。

输出中没有:

复制代码
postprocess: EXTRACTED

说明程序没有在缺少求解结果的情况下继续执行后处理。


十一、保存报告与过程记录

运行:

复制代码
python lesson53_workflow.py --scenario pass --out run53_pass

程序会创建一个新的输出文件夹,其中包含:

复制代码
run53_pass/
├── audit.json
└── report.md

两份文件用途不同:

  • audit.json:保存需求、模型、网格、原始结果、后处理结果、检查与执行顺序。
  • report.md:供学习者阅读的简要报告。

输出目录必须是新目录;程序拒绝覆盖已有目录,避免混淆不同运行的记录。

失败报告仍可以展示诊断用数值,但必须明确标记没有通过检查,不能作为已验收结果发布。

本节程序不生成数据图,因此没有额外的计算图需要插入;主要学习材料是流程、数值结果和检查记录。


十二、以后怎样接入真正的大模型角色?

接入大模型时,可以保留已经验证的工具,把部分角色替换成"能够提出结构化请求的智能体"。

例如,需求分析角色的任务说明可以这样写:

复制代码
你负责整理有限元分析需求,不执行求解。

必须确认:
1. 单位体系;
2. 几何尺寸;
3. 材料模型与参数;
4. 载荷;
5. 边界条件;
6. 需要输出的结果。

缺少必需信息时,返回 NEEDS_INPUT 和具体问题。
信息完整时,返回结构化规格供程序校验。
不得猜测材料参数,不得声称已经完成计算。

但有一点必须强调:

提示词是角色说明,不是安全边界。

真正控制行为的应当是:

  • 工具允许接收哪些参数;
  • 参数是否通过校验;
  • 哪些文件允许读写;
  • 谁能提交求解任务;
  • 谁能修改基准与检查阈值;
  • 谁能发布最终结果。

例如,报告角色即使在文本中写出"全部通过",也不能改变程序记录的 REJECTED 状态。

12.1 不必为每个角色都配置大模型

本节六个角色不意味着需要六次模型调用。

一个可行的分配是:

  • 需求澄清:大模型辅助。
  • 已知模板建模:确定性脚本。
  • 网格生成:专门工具。
  • 求解:数值求解器。
  • 指标提取与质量检查:确定性程序。
  • 报告解释:大模型辅助,但引用受控结果。

只有确实需要语言理解、规划或解释的地方,才使用大模型。

12.2 必须设置停止条件

真实智能体可能不断尝试修改与重试,因此需要限制消息数、时间、费用和工具调用次数。AutoGen 的官方文档提供了消息数量、超时、外部停止等终止机制。AutoGen 终止条件文档

但"对话停止"与"力学验收通过"是两件事。

即使智能体输出了:

复制代码
DONE

也仍需检查程序状态与数值结果。

本节没有实现自动重试。对于错误,默认保留证据并停止。后续增加重试时,应限制次数,并明确哪些修正不改变用户的物理问题。


十三、接入 Abaqus 时,需要替换哪些部分?

迁移到 Abaqus 后,角色边界可以保留:

本节实现 Abaqus接入后的对应工作
一维几何字典 经验证的参数化建模脚本
三节点连接关系 网格种子、单元类型与网格质量检查
Python刚度矩阵求解 Abaqus作业提交与状态检查
列表中的位移、应力、反力 指定ODB、分析步、帧和区域的数据提取
一维杆解析检查 对应问题的物理、数值与回归检查
Markdown报告 带来源信息和检查结论的仿真报告

求解角色需要特别注意:

等待作业结束,不等于确认作业成功。

Abaqus 官方文档明确指出,在 waitForCompletion() 返回后,仍应检查作业状态,再决定是否继续处理结果。Abaqus 作业等待与状态检查

此外,真实接入还需要处理软件许可、超时、文件路径、作业隔离和版本差异。本节没有执行这些操作,也没有提交真实 Abaqus 作业。

对于大模型生成的脚本,建议先进行审查或使用经过验证的参数化模板,不能把任意生成代码直接当作可信工具执行。


十四、本节练习

练习一:为什么不能只检查力平衡?

运行:

复制代码
python lesson53_workflow.py --scenario corrupt

回答:

为什么支承反力与外力仍然平衡,但系统必须拒绝发布合格报告?

参考思路:反力没有被修改,但后处理位移已不满足原来的离散方程。单项正确不代表整份结果一致。

练习二:为需求角色写一个澄清问题

用户只说:

请帮我计算一根钢杆受到拉力后的伸长。

请列出需要补充的信息,并写出一段简洁的询问。

不要直接假设弹性模量、杆长、截面积或载荷。

练习三:识别合适的修复位置

如果原始结果中的末端位移是:

\ 0.476190\\ \\mathrm{mm} \\

而报告显示:

\ 0.571429\\ \\mathrm{mm} \\

应优先检查哪个环节?

参考答案:先检查后处理与报告转换过程,比较原始记录和展示数据;没有必要立即重建几何或调整材料参数。

练习四:理解简单案例的验证边界

为什么本节两个杆单元就能得到准确的节点位移,并不意味着复杂结构也只需要很少的单元?

参考思路:均匀杆受端部轴向力时,位移沿长度线性变化,正好可以由线性杆单元表示;复杂几何、弯曲、应力集中和非线性问题不具备相同条件。


下一节可以在这套流程上开展参数化研究:++将单个任务扩展为多个载荷、尺寸或材料工况,并为每个工况分别保留输入、结果和检查状态。这样,后续生成机器学习训练数据时,才能清楚区分有效样本、失败样本与尚待核查的样本。++

相关推荐
AI Coder 码农王3 小时前
AgentScope Java v2 第3课,从 ReActAgent 到 HarnessAgent
人工智能
格物致胜工业数智技术3 小时前
明治EOS系列传感器技术解析:精密制造高反光金属件在位检测
人工智能·制造
甲维斯3 小时前
DeepSeek 新版还是难评!
人工智能
晨间晓4 小时前
图片细节丰富不丰富?用华为云码道开发 OpenCV 纹理复杂度分析工具
人工智能·opencv·华为云
硅谷秋水4 小时前
面向机器人的上下文学习:方法与应用
深度学习·机器学习·语言模型·机器人
2603_965148114 小时前
电商选品调研:通过关键词搜索API获取市场热销商品,结合商品详情API分析竞品卖点与定价
开发语言·数据库·人工智能·python·信息可视化
镜象科技4 小时前
什么是好的校园AI心理健康解决方案服务商?从中外校园心理服务的一天说起
人工智能
Geeys4 小时前
拼多多单品推广完整方案(可直接落地)
大数据·人工智能
泡椒鸡脚4 小时前
AI室内设计风格融合:把布局、材质和光影参考分开写
人工智能·材质