
本节摘要
本节承接上一节的仿真质量检查,将需求分析、几何建模、网格划分、求解、后处理和报告组织成职责明确的协作流程。通过一个真实求解的一维杆有限元案例,学习结构化任务交接、状态管理和独立质量检查,并通过缺失参数、求解失败与结果篡改测试,理解系统何时应继续、暂停或拒绝发布结果。本节采用Python函数模拟协作角色,不调用大模型或Abaqus,为后续接入真实智能体与CAE求解器建立可验证的基础。
一、从"一个程序完成所有工作",到"每一步都有明确责任"
上一节,我们为有限元结果增加了质量检查。
但当自动化流程越来越长,新的问题会出现:
一次结果检查失败,究竟应该让谁修改什么?
例如:
- 材料参数缺失,需要补充需求,而不是重新划网格。
- 单元连接关系错误,需要修正模型,而不是放宽求解容差。
- 求解已经完成,但读取了错误的分析帧,需要修正后处理,而不是重新建模。
- 位移与理论值不符,需要先定位原因,而不是让报告程序把结论写得更肯定。
如果所有工作都塞进一个没有清晰边界的"大助手",错误可能在多次修改后越来越难追踪。
本节采用另一种组织方式:
每个角色只处理明确的任务,交接明确的数据,由程序控制下一步是否允许执行。
这里的"协作"不是让几个智能体轮流发表意见,而是建立可以检查的工作关系。
本节需要掌握的四件事
- 如何拆分有限元自动化任务。
- 不同角色之间应该传递哪些数据。
- 谁负责决定流程继续还是停止。
- 如何用可运行的小案例验证整个协作设计。
二、六个工作角色,加一道独立检查
本节采用六个工作角色。
| 角色 | 主要职责 | 必须交出的结果 | 不应该擅自做的事 |
|---|---|---|---|
| 需求分析 | 确认单位、参数、边界条件与目标 | 结构化问题规格 | 猜测缺失的材料参数 |
| 几何建模 | 根据规格建立几何与材料描述 | 模型数据 | 改变载荷或分析目的 |
| 网格划分 | 建立节点和单元连接关系 | 网格数据 | 为了通过检查而改变物理问题 |
| 求解执行 | 组装方程、调用求解器 | 原始求解结果与状态 | 把失败状态改成成功 |
| 后处理 | 提取位移、应力和反力 | 含单位的结果记录 | 修改原始结果以迎合预期 |
| 报告生成 | 汇总结果、检查与限制 | 成功报告或停止报告 | 隐藏失败项 |
此外,在后处理与报告之间增加一道质量检查。
它不依赖某个角色说"我认为正确",而是执行事先定义的数值规则。
整体关系如下:
工程需求
│
▼
需求分析 ── 信息不完整 ──→ 等待补充
│
▼
几何建模
│
▼
网格划分
│
▼
求解执行 ── 执行失败 ───→ 停止报告
│
▼
后处理
│
▼
质量检查 ── 检查不通过 ─→ 拒绝发布合格结论
│
▼
通过检查的结果报告
需要注意:独立检查不等于必须增加一个大模型。
力平衡、数值残差、单位检查等任务,通常可以直接交给确定性的程序。大模型可以解释检查结果,但不应获得随意修改检查结论的权限。
三、本节实现到什么程度?
为了让协作机制可以在普通电脑上直接运行,本节分清三个层次。
| 层次 | 本节是否实现 |
|---|---|
| 真实的一维杆有限元组装与求解 | 是 |
| 六个角色之间的数据交接、检查和停止机制 | 是 |
| 大模型自主推理、自然语言解析、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} \\
应优先检查哪个环节?
参考答案:先检查后处理与报告转换过程,比较原始记录和展示数据;没有必要立即重建几何或调整材料参数。
练习四:理解简单案例的验证边界
为什么本节两个杆单元就能得到准确的节点位移,并不意味着复杂结构也只需要很少的单元?
参考思路:均匀杆受端部轴向力时,位移沿长度线性变化,正好可以由线性杆单元表示;复杂几何、弯曲、应力集中和非线性问题不具备相同条件。
下一节可以在这套流程上开展参数化研究:++将单个任务扩展为多个载荷、尺寸或材料工况,并为每个工况分别保留输入、结果和检查状态。这样,后续生成机器学习训练数据时,才能清楚区分有效样本、失败样本与尚待核查的样本。++