课程摘要
本节围绕Foam-Agent协作与知识检索,学习如何将CFD任务拆分为规划、网格、配置、执行、审核与可视化职责。通过边界名称不一致和Courant数偏高两个案例,掌握版本筛选、证据记录、配置依赖与有限纠错。配套Python练习可离线运行,展示八种场景的决策及时间步建议,帮助学习者区分"提出修正""执行通过"与"物理可信",为下一节CFD基准测试奠定基础。
一、从"按顺序运行",到"有依据地协作"
上一节,我们建立了受控仿真工作流:
网格生成、网格检查、求解、后处理依次执行;前一步失败,后面的步骤不能假装完成。
但执行器只能忠实执行命令。它不能仅凭返回码判断:
- 入口边界是否写错;
- 材料参数是否缺失;
- 检索到的配置是否适用于当前软件版本;
- 某次修改究竟修复了错误,还是改变了原来的问题。
因此,本节要把"执行流程"向前推进一步:
谁负责理解问题,谁负责寻找依据,谁负责提出配置,谁有权修改,以及什么时候必须停止?
完成本节后,你应能够设计一套简单、可追溯的协作流程,而不仅是让多个角色轮流生成文字。
建议学习安排:阅读约35分钟,运行与核对约20分钟,练习约15分钟。本节不需要安装OpenFOAM,也不需要大模型API密钥。
二、一个入口名称,为什么能让协作失败?
继续考虑前几节使用的平行板流动问题。
网格工具输出的入口名称是:
inlet
配置生成工具却在速度场文件中写成:
inlet_main
这两个名称分别看起来都合理,组合起来却可能不一致。
假设这是一个仅使用精确边界名称、不使用分组或通配匹配的教学算例:
网格边界:
inlet, outlet, walls, frontAndBack
速度场配置:
inlet_main, outlet, walls, frontAndBack
进行集合比较,可以发现:
未覆盖的网格边界:inlet
无对应网格边界的配置:inlet_main
以上是教学一致性检查,不是OpenFOAM原生日志。
这个问题的关键不是"再增加一个智能体",而是:
所有角色是否引用了同一份经过确认的边界清单?
可以把它想成多人合作做菜。有人写采购单,有人准备食材,有人负责烹饪。如果采购单和厨房标签不一致,即使每个人都认真工作,合作仍然可能失败。
仿真协作同样需要一份"共同事实",至少包括:
- 软件发行分支与版本;
- 几何尺寸与单位;
- 材料参数与物理模型;
- 边界集合与驱动方式;
- 计算终点与验收标准。
后续角色可以提出修改,但不能悄悄改写这些事实。
三、Foam-Agent的角色:交接的是可检查的产物
本节借鉴Foam-Agent 2.0中的六类角色:规划、网格、输入文件生成、执行、审核和可视化。知识检索是为这些角色提供依据的能力,不一定需要单独设置一个智能体。Foam-Agent 2.0论文,第3节
下面是面向课堂实践的职责约定,不是对论文代码接口的逐项复刻。
| 角色 | 应交接的产物 | 不应自行决定的事情 |
|---|---|---|
| 规划角色 Architect | 问题规格、缺失信息、任务依赖 | 猜测未提供的材料参数 |
| 网格角色 Meshing | 网格、边界清单、检查结果 | 为方便划分而改变关键几何 |
| 配置角色 Input Writer | 配置建议、引用依据、修改差异 | 混用不同版本的设置 |
| 执行角色 Runner | 命令、日志、返回码、超时记录 | 无审核地执行任意命令 |
| 审核角色 Reviewer | 错误证据、修正建议、停止条件 | 为了通过而降低验收标准 |
| 可视化角色 Visualization | 带单位、来源和状态说明的图表 | 把示意图当作计算结果 |
插图一:协作与有限纠错的整体关系

图中展示的是教学职责,不是本节程序的实际调用轨迹。
实际工程系统可以用普通函数、固定规则与少量智能体共同实现这些职责,不必每个框都启动一个大模型。
这里最重要的一条原则是:
下一角色应当能够检查上一步交来的事实,而不只是接受上一步的结论。
例如,Reviewer认为"Courant数偏高",就应给出观测值、对应时间步、日志位置和适用条件,而不是只写一句"建议减小时间步"。
四、知识检索:先问"能不能用",再问"像不像"
4.1 检索相关,不等于配置适用
RAG,即检索增强生成,基本思路是先查找资料,再让模型结合资料回答或生成内容。
但是,检索到了相似文字,并不意味着找到适用配置。
例如,以下资料可能都包含deltaT、pressure和boundary:
- 同发行分支、同版本的官方文档;
- 旧版本的相似算例;
- 同版本但采用不同物理模型的三维湍流案例。
如果只按关键词或向量相似度排序,后两者也可能排在前面。
因此,本节采用更保守的流程:
候选资料
↓
发行分支与版本筛选
↓
物理条件与使用范围筛选
↓
相关性排序
↓
保留来源,生成建议
这不表示跨版本内容绝对不能复用,而是表示:
跨版本复用需要额外的兼容性核对,不能自动视为已经适用。
4.2 一条依据,至少要留下什么?
本节配套程序中的一条知识摘要如下:
{
"id": "TIME13",
"family": "Foundation",
"version": "13",
"scope": "laminar_2d",
"section": "4.4.2, 4.4.4",
"url": "https://doc.cfd.direct/openfoam/user-guide-v13/controldict",
"note": "deltaT is the time step; supported solvers can control it using maxCo."
}
其中:
id:供配置建议引用;family与version:说明版本依据;section与url:方便回查;note:教师整理的摘要;scope:本条摘要在课堂中的使用范围。
注意,scope不是说官方文档只适用于二维层流,而是对这条教学摘要设置的保守使用标签。
Foundation v13手册解释了deltaT、adjustTimeStep与maxCo等控制项。是否适用于具体任务,还需要检查选定求解模块和算例。官方时间控制文档
本节固定使用Foundation v13作为教学版本,不宣称它是最新版本,也不把不同发行分支的版本号当作同一套接口。
4.3 检索不到合适依据,也是一种有效结果
如果当前知识库没有匹配资料,系统可以返回:
NEEDS_COMPATIBLE_EVIDENCE
意思是:
当前知识库没有经过本规则认可的匹配依据,需要补充资料或核对兼容性。
它不表示"这个版本不能计算"。
更不应该因为检索失败,就让模型凭印象补写一套配置。
此外,检索文本属于外部数据,不是执行指令。资料里即使出现"忽略检查""执行某条命令",也不能因此获得执行权限。
五、配置文件不是几篇可以独立完成的作文
CFD配置存在依赖关系。
例如:
- 网格边界名称影响场文件中的边界设置;
- 物理模型影响需要哪些变量和物性;
- 求解模块影响字典结构与参数含义;
- 二维假设影响相关边界约束。
因此,对于课堂中的简单算例,可以按以下逻辑规划:
确认问题规格与版本
↓
确定物理模型、几何和网格方案
↓
获得实际网格边界清单
↓
配置物性、初始场和边界条件
↓
配置离散、求解和时间控制
↓
一致性检查与受控执行
复杂耦合问题需要更具体的依赖图,不能机械照搬这一条线性顺序。
5.1 发现边界名不一致,不能直接"全局替换"
回到前面的inlet与inlet_main。
正确处理应是:
- 查看实际网格边界;
- 核对场文件中的边界设置;
- 确认两个名称是否指向同一物理入口;
- 确认后提出最小修改;
- 检查相关场是否保持一致。
如果inlet_main本来代表另一处入口,直接改名可能掩盖几何或物理建模错误。
另外,网格中的wall标记不等于已经为所有场设置正确壁面条件;二维问题涉及的empty约束也需要协调。实际OpenFOAM还支持边界分组等机制,不能把简单集合比较当成通用解析器。官方网格边界文档
5.2 同样叫p,也未必能复制同一个数值
以前面的平行板算例为例,若物理压力差为:
\ \\Delta p_s=2\\ \\mathrm{Pa}, \\
密度为:
\ \\rho=1000\\ \\mathrm{kg/m\^3}, \\
则相应运动学压力差为:
\ \\Delta p_k =\\frac{\\Delta p_s}{\\rho} =\\frac{2}{1000} =0.002\\ \\mathrm{m\^2/s\^2}. \\
这只是压力变量约定的换算,不是在这里指定完整的压力边界。
检索到一个同样叫p的变量,不能因此直接复制数值,必须核对物理定义与量纲。OpenFOAM具有量纲检查机制,但它不能替你判断某个数值是否符合原工程需求。官方输入格式与量纲说明
因此,优先选择与你的版本、模型和边界相匹配、经过本地验证的完整模板,再做少量参数化修改,通常更容易追溯。
六、有限纠错:不是所有错误都应该"再试一次"
6.1 先分类,再选择动作
| 问题 | 本节采取的动作 |
|---|---|
| Courant数偏高 | 在依据和预算充分时,提出较小时间步建议 |
| 缺少黏度等材料参数 | 请求补充,不猜数值 |
| 边界名称缺失或不一致 | 核对边界映射,不擅自改变几何 |
| 没有匹配版本依据 | 补充资料或核对兼容性 |
| 无有效观测值 | 先补齐或核查数据 |
| 已用完提案预算 | 停止并保留记录 |
| 无法识别的错误 | 停止,交由人工诊断 |
"缺少黏度"不能靠增加迭代次数解决。
"边界名不存在"也不能靠减小时间步解决。
纠错的第一步,是确认措施是否对准了错误类型。
6.2 具体计算:从Co=1.2得到时间步建议
假设一个待诊断的瞬态任务给出:
\ \\Delta t_{\\mathrm{old}}=0.002\\ \\mathrm{s}, \\\ \\mathrm{Co}_{\\mathrm{obs}}=1.2, \\
教学目标为:
\ \\mathrm{Co}_{\\mathrm{target}}=0.5. \\
这些是本节人为设置的诊断输入,不是从前几节真实OpenFOAM运行中读取的结果。0.5也不是所有CFD任务通用的阈值。
当网格和速度场近似不变时,可以按局部比例估计:
\ \\Delta t_{\\mathrm{proposal}} = \\Delta t_{\\mathrm{old}} \\frac{\\mathrm{Co}_{\\mathrm{target}}} {\\mathrm{Co}_{\\mathrm{obs}}}. \\
代入数值:
\ \\Delta t_{\\mathrm{proposal}} = 0.002\\times\\frac{0.5}{1.2} \\approx0.000833333\\ \\mathrm{s}. \\
这个结果表示:
可以提出将时间步从0.002秒减小到约0.000833333秒的建议。
它不保证重跑后的Co恰好达到0.5,更不保证求解收敛。
速度变化、网格变化和其他稳定性限制都可能影响结果。
6.3 什么可以改,什么不能偷偷改?
本次建议只针对deltaT。
原来的物性、几何、边界驱动、物理终点与验收要求应保持不变。
不能为了让任务"通过"而:
- 提前结束计算;
- 改变载荷或流量;
- 放宽残差要求;
- 删除失败记录;
- 将未完成的步骤标记为成功。
本节设置一次提案预算:
attempt = 0:尚未消耗提案预算
attempt = 1:预算已经用完,停止继续提案
实际系统应由可信执行层保存次数,不能让模型自行把attempt重置为0。
最重要的是区分:
\ \\text{提出修正} \\neq \\text{已应用修改} \\neq \\text{已重新求解} \\neq \\text{结果物理可信}. \\
七、离线实战:运行八种场景
7.1 下载与运行
完整程序已经独立整理,无需前几节的代码文件,另存以下代码为:lesson58_collaboration.py:
"""Offline teaching model: curated-note filtering and bounded proposals, NOT Foam-Agent."""
import argparse
import copy
import json
import math
from pathlib import Path
# These are instructor-written summaries, not downloaded document chunks.
# scope describes this note's teaching use, not all uses of its source document.
KNOWLEDGE = [
dict(id="TIME13", family="Foundation", version="13", scope="laminar_2d",
tags=["time", "courant"], section="4.4.2, 4.4.4",
url="https://doc.cfd.direct/openfoam/user-guide-v13/controldict",
note="deltaT is the time step; supported solvers can control it using maxCo."),
dict(id="PATCH13", family="Foundation", version="13", scope="laminar_2d",
tags=["boundary"], section="5.3, 5.3.2",
url="https://doc.cfd.direct/openfoam/user-guide-v13/boundaries",
note="Mesh patches and field boundary conditions must be consistent; review empty faces for 2D."),
dict(id="OLD12", family="Foundation", version="12", scope="laminar_2d",
tags=["time", "courant", "boundary"], section="TEACHING_DISTRACTOR",
url=None, note="Synthetic old-version candidate: deliberately not usable evidence."),
dict(id="OTHER3D", family="Foundation", version="13", scope="turbulent_3d",
tags=["time", "courant", "boundary"], section="TEACHING_DISTRACTOR",
url=None, note="Synthetic other-physics candidate: deliberately not usable evidence."),
]
SPEC = dict(family="Foundation", version="13", scope="laminar_2d",
H_m=0.01, L_m=1.0, rho_kg_m3=1000.0, mu_Pa_s=0.001,
deltaT_s=0.002, endTime_s=1.0, target_Co=0.5,
pressure="kinematic", patches=["inlet", "outlet", "walls", "frontAndBack"])
QUERY = {"time", "courant", "boundary"}
def positive(value):
return (type(value) in (int, float) and math.isfinite(value) and value > 0)
def retrieve(spec):
"""Exact metadata filter BEFORE transparent tag-overlap ranking; not semantic RAG."""
accepted, rejected = [], []
for item in KNOWLEDGE:
if any(item[k] != spec.get(k) for k in ("family", "version", "scope")):
rejected.append(item["id"])
elif QUERY.intersection(item["tags"]):
accepted.append(dict(item, score=len(QUERY.intersection(item["tags"]))))
accepted.sort(key=lambda x: (-x["score"], x["id"]))
return accepted, rejected
def decide(spec, kind, attempt=0, observed_Co=None):
if type(attempt) is not int or attempt < 0:
raise ValueError("attempt must be a non-negative integer")
result = dict(status="NEEDS_INPUT", proposal=None, evidence=[], rejected=[],
source="OFFLINE_TEACHING", attempt=attempt,
OpenFOAM_run=False, LLM_used=False, physical_verified=False)
fields = ("H_m", "L_m", "rho_kg_m3", "mu_Pa_s", "deltaT_s", "endTime_s", "target_Co")
if (kind == "missing_material" or any(not positive(spec.get(k)) for k in fields)
or spec.get("pressure") != "kinematic" or not spec.get("patches")):
return result
evidence, rejected = retrieve(spec)
result.update(evidence=evidence, rejected=rejected)
if {e["id"] for e in evidence} != {"TIME13", "PATCH13"}:
result["status"] = "NEEDS_COMPATIBLE_EVIDENCE"
elif attempt >= 1:
result["status"] = "STOP_BUDGET"
elif kind == "missing_patch":
result["status"] = "REVIEW_PATCH_MAPPING"
elif kind == "high_courant":
if not positive(observed_Co):
result["status"] = "STOP_INVALID_OBSERVATION"
elif observed_Co <= spec["target_Co"]:
result["status"] = "NO_CHANGE_REQUIRED"
else:
new_dt = spec["deltaT_s"] * (spec["target_Co"] / observed_Co)
if not positive(new_dt):
result["status"] = "STOP_INVALID_OBSERVATION"
else:
result["status"] = "PROPOSE_SMALLER_DT"
result["proposal"] = dict(field="deltaT", units="s",
before=spec["deltaT_s"], after=new_dt, observed_Co=observed_Co,
target_Co=spec["target_Co"], approval="PENDING_REVIEW",
evidence_ids=["TIME13"], applied=False)
else:
result["status"] = "STOP_UNKNOWN"
return result
def run_demo():
rows = []
scenarios = [
("high_courant/0", "high_courant", 0, 1.2),
("high_courant/1", "high_courant", 1, 1.2),
("missing_material/0", "missing_material", 0, None),
("missing_patch/0", "missing_patch", 0, None),
("unknown/0", "unknown", 0, None),
("version14/0", "high_courant", 0, 1.2),
("invalid_Co/0", "high_courant", 0, None),
("low_Co/0", "high_courant", 0, 0.2),
]
for name, kind, attempt, co in scenarios:
spec = copy.deepcopy(SPEC)
if name.startswith("version14"):
spec["version"] = "14"
if kind == "missing_material":
spec.pop("mu_Pa_s")
before = copy.deepcopy(spec)
record = decide(spec, kind, attempt, co)
assert spec == before, "The proposal must not mutate the problem specification"
rows.append(dict(case=name, request=before, **record))
print(f"{name}: {record['status']}")
p = rows[0]["proposal"]
print(f"deltaT: {p['before']:.9f} -> {p['after']:.9f} s (proposal only)")
print("MODE=OFFLINE_TEACHING; LLM_used=False; OpenFOAM_run=False")
return rows
def self_test():
expected = ["PROPOSE_SMALLER_DT", "STOP_BUDGET", "NEEDS_INPUT", "REVIEW_PATCH_MAPPING",
"STOP_UNKNOWN", "NEEDS_COMPATIBLE_EVIDENCE", "STOP_INVALID_OBSERVATION", "NO_CHANGE_REQUIRED"]
rows = run_demo()
assert [r["status"] for r in rows] == expected
a, rejected = retrieve(SPEC)
assert [x["id"] for x in a] == ["TIME13", "PATCH13"]
assert rejected == ["OLD12", "OTHER3D"]
assert math.isclose(rows[0]["proposal"]["after"], 1 / 1200)
for bad in (float("nan"), float("inf"), 0, -1, True):
assert decide(SPEC, "high_courant", observed_Co=bad)["status"] == "STOP_INVALID_OBSERVATION"
for bad in (-1, True, 1.5):
try:
decide(SPEC, "high_courant", bad, 1.2)
except ValueError:
pass
else:
raise AssertionError("Invalid attempt accepted")
assert all(not r["OpenFOAM_run"] and not r["physical_verified"] for r in rows)
assert rows[0]["proposal"]["applied"] is False
print("SELF_TEST=PASS (8 scenarios + filtering, numerical and input guards)")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--out", type=Path, default=Path("lesson58_run"))
parser.add_argument("--self-test", action="store_true")
args = parser.parse_args()
if args.self_test:
self_test()
return
if args.out.exists():
parser.error("Output path already exists; choose a new --out directory")
rows = run_demo()
args.out.mkdir(parents=True, exist_ok=False)
report = dict(mode="OFFLINE_TEACHING", candidate_count=len(KNOWLEDGE),
knowledge_notes=KNOWLEDGE, records=rows)
(args.out / "report.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2, allow_nan=False), encoding="utf-8")
print(f"SAVED={args.out / 'report.json'}")
if __name__ == "__main__":
main()
将文件放到自己的课程文件夹,在该文件夹中打开终端,运行:
python lesson58_collaboration.py --self-test
核心练习只使用Python标准库,不需要安装pandas、LangGraph或FAISS。完整代码也已放在下载讲义的附录中,可以直接复制保存。
7.2 程序实际上完成了什么?
程序做两件事:
- 从4条本地教学条目中,先筛选元数据,再按标签重合数排序;
- 对8种预设场景输出决策记录。
其中,TIME13与PATCH13是教师整理的官方文档摘要。
OLD12与OTHER3D是专门设置的干扰条目,并未伪装成已经读取的官方资料。
本程序没有联网检索,没有向量数据库,没有调用LLM,也没有启动OpenFOAM。错误类别由测试场景提供,不是模型自动从真实日志识别出来的。
这是协作策略的透明教学模型,不是Foam-Agent完整复现。
7.3 实际运行输出
high_courant/0: PROPOSE_SMALLER_DT
high_courant/1: STOP_BUDGET
missing_material/0: NEEDS_INPUT
missing_patch/0: REVIEW_PATCH_MAPPING
unknown/0: STOP_UNKNOWN
version14/0: NEEDS_COMPATIBLE_EVIDENCE
invalid_Co/0: STOP_INVALID_OBSERVATION
low_Co/0: NO_CHANGE_REQUIRED
deltaT: 0.002000000 -> 0.000833333 s (proposal only)
MODE=OFFLINE_TEACHING; LLM_used=False; OpenFOAM_run=False
SELF_TEST=PASS (8 scenarios + filtering, numerical and input guards)
逐项理解:
PROPOSE_SMALLER_DT:提出较小时间步建议;STOP_BUDGET:已用完提案预算;NEEDS_INPUT:问题信息不完整;REVIEW_PATCH_MAPPING:需要核对边界对应关系;STOP_UNKNOWN:错误类别无法处理;NEEDS_COMPATIBLE_EVIDENCE:本地库没有匹配依据;STOP_INVALID_OBSERVATION:观测值无效或缺失;NO_CHANGE_REQUIRED:无需执行本条"因Co过高而减小时间步"的策略。
最后一项不表示任务已经成功,只表示这条策略不需要触发。
同样,SELF_TEST=PASS只表示教学程序通过测试,不表示八个CFD任务求解成功。
7.4 保存结构化记录
运行:
python lesson58_collaboration.py --out lesson58_run
程序会输出相同的场景结果,并将记录保存为:
SAVED=lesson58_run\report.json
程序拒绝覆盖已有输出目录。再次运行时,可换一个目录名称:
python lesson58_collaboration.py --out lesson58_run_02
插图二:实际决策结果与时间步建议

此图直接读取本次程序生成的report.json绘制。
左侧展示原时间步与建议值,右侧展示8种场景的实际返回状态。它不是CFD求解结果、残差曲线或预测精度比较。
7.5 为什么看起来最相关的条目反而被拒绝?
查询标签包含:
time, courant, boundary
两个干扰条目都包含这三个标签。如果只按标签数排序,它们可能非常"相关"。
但:
OLD12:版本不匹配
OTHER3D:教学使用范围不匹配
所以它们先被排除。
保留下来的条目为:
TIME13: score=2
PATCH13: score=1
这里的分数只是标签重合数量,不是概率、置信度或正确率。
真实RAG可以在同样的元数据闸门之后,再使用语义检索与重排序。
7.6 怎样读懂修正提案?
第一条记录中的关键内容为:
{
"field": "deltaT",
"units": "s",
"before": 0.002,
"after": 0.0008333333333333334,
"observed_Co": 1.2,
"target_Co": 0.5,
"approval": "PENDING_REVIEW",
"evidence_ids": ["TIME13"],
"applied": false
}
其中:
PENDING_REVIEW表示待审核,applied=false表示没有修改任何OpenFOAM字典。
同时,记录中的:
OpenFOAM_run=False
physical_verified=False
明确说明没有执行或验证真实CFD。
还应注意:官方文档解释控制项,比例估算则依赖课堂假设。两者需要分别说明,不能把自己的推导包装成官方文档原话。
八、如何理解论文中的"成功率"?
本节固定引用Foam-Agent 2.0的arXiv:2509.18178v2版本,日期为2025年9月30日。该版本摘要报告:使用Claude 3.5 Sonnet,在110项任务上的成功率为88.2%。因此,不能沿用原始目录中"110项任务100%成功"的表述。指定版本论文摘要
更不能根据论文中的数字,保证自己的软件版本、模型和任务也能达到相同水平。
课堂实践应分别记录:
- 配置是否有效;
- 执行是否完成;
- 数值指标是否通过;
- 物理基准是否通过;
- 使用了多少次尝试;
- 是否发生人工介入。
不要把这些结果压缩成一句含义不清的"成功了"。
九、本节练习与参考答案
练习一:缺少黏度,可以先填一个"常见值"吗?
参考答案:不能默认为用户要求。
黏度会影响雷诺数与流动响应。应请求补充;如果用户明确授权使用假设值,也必须在规格和报告中标记。
练习二:入口名称不一致,可以直接批量替换吗?
参考答案:先确认两个名称是否指向同一物理入口。
确认后提出最小修改,并检查相关场的一致性。不能通过删除入口或改成壁面来消除报错。
练习三:Co为0.2、目标为0.5,是否必须增大时间步?
参考答案:不必。
本节策略只处理"Co过高时减小时间步",不负责自动增大时间步。NO_CHANGE_REQUIRED也不是整项任务通过。
练习四:同版本的三维湍流案例,可以直接用于二维层流吗?
参考答案:不能直接整套移植。
版本一致只消除一类风险。维度、湍流模型、壁面处理和边界驱动仍需要核对。
练习五:批准减小时间步后,怎样证明修正有效?
参考答案:在新目录保存修改差异,重新运行到相同物理终点,再检查Co、日志、守恒与关键物理量,并进行独立基准比较。
只有修正提案,或者一条较低的残差,都不足以证明结果可信。
十、小结与下一节安排
本节的核心不是让系统"多试几次",而是让每一次建议都有依据、有边界、有记录。
请记住四句话:
- 角色协作靠共同规格与明确交接,不靠角色数量。
- 检索先检查兼容性,再比较相关性。
- 修正应针对错误,不能改变问题来换取通过。
- 提案、执行、数值通过和物理可信是不同状态。
