领域 Agent 的确定性工具编译与延迟执行——从自然语言规格到单次 CAE 提交
📅 创建时间:2026-07-29
🏷️ 标签:#DomainAgent #DeterministicCompilation #DeferredExecution #HumanInTheLoop #Schema #Controller #CAEAgent
📚 前置知识:[[01-function-calling]] [[05-agent-workflow]] [[09-structured-output]] [[10-tools-design]] [[13-workflow-orchestration]] [[15-harness-loop-skills]]
📋 本章目标
- 理解为什么普通的“LLM 调工具 → 立即执行”循环不适合 CAE、CAD、数据库迁移和工业控制等高副作用领域
- 掌握一种新的领域 Agent 模式:Human-Gated Deterministic Tool Compilation
- 区分设计规格、Tool Call、领域中间表示、生成脚本和真实执行结果
- 理解 JSON Schema 能约束什么、不能约束什么,以及为什么 Controller 仍然不可替代
- 掌握 Controller 如何冻结目标、控制状态、路由工具、收集多轮 Tool Call、验证顺序并决定唯一执行时机
- 理解如何把 LLM 输出的 JSON 参数确定性编译为 SAM Python,而不是让 LLM自由编写生产脚本
- 理解 SSE Tool Call 分片重组、Provider 兼容、执行凭据和结果回读
- 区分“单次执行”“精确一次”“幂等”和“事务原子性”,避免给系统不存在的安全保证
- 将 SAM Agent 的经验抽象为可复用的领域 Agent 架构
第0部分:先回答核心问题——为什么普通 Function Calling 不够?
0.1 标准 Function Calling 默认“调用后立即执行”
常见 Agent 循环如下:
while True:
response = llm.chat(messages, tools=tools)
if not response.tool_calls:
return response.text
for call in response.tool_calls:
result = execute_tool(call.name, call.arguments)
messages.append(tool_result(call.id, result))它隐含了一个重要语义:
LLM 输出 Tool Call
↓
宿主程序立即执行
↓
执行结果成为下一轮 Observation这种模式非常适合搜索、读取文件、查询数据库和获取天气,因为工具结果本身就是下一步推理需要的信息。一次调用失败,Agent 可以根据错误修正参数后继续。
但对于 CAE 建模,这种立即执行会产生三个问题。
第一,很多操作本来属于同一个工程意图:
复制全部单元
→ 合并重合节点
→ 删除重复单元
→ 清理自由节点
→ 只读审计如果每一步收到 Tool Call 就立即修改 SAM,系统会在完整方案尚未形成时开始改变模型。
第二,SAM Python 具有真实副作用。脚本执行到第三步时报错,前两步可能已经改变内存中的模型。普通 ReAct 的“失败后再试一次”可能把同一个副作用重复施加。
第三,LLM 并不可靠地掌握某个具体 SAM 版本的 API 签名。让模型直接生成:
part.Element(nodes=(n1, n2, n3, n4), elemShape=QUAD)意味着它必须同时正确处理 Python 版本、模块导入、SAM 枚举、节点对象、目标 Part、调用顺序和平台差异。任何一项猜错都会让整段脚本失败。
0.2 领域 Agent 需要把“决定做什么”和“知道怎么做”分开
SAM Agent 最终形成的边界是:
LLM:理解用户意图,形成规格,填写领域参数
Controller:决定允许哪些操作、按什么顺序、何时执行
Compiler:把领域参数转换为已验证的 SAM Python
SAM:执行脚本并返回真实证据这里的 Tool Call 不再等同于一次立即执行的函数调用。它更接近一条领域操作提案,或者一段待编译的领域中间表示。
第1部分:模式定义——Human-Gated Deterministic Tool Compilation
1.1 名称与定义
本章把这种模式称为:
Human-Gated Deterministic Tool Compilation
人工确认的确定性工具编译模式
它包含三个关键约束:
- Human-Gated:自然语言意图先变成工程规格,用户确认后才能进入有副作用阶段。
- Deterministic Tool Compilation:LLM 只输出结构化领域参数,本地编译器确定性生成平台脚本。
- Deferred Execution:Tool Call 先收集和校验,不在每次模型返回后立即执行;全部收齐后再组合提交。
完整链路是:
用户请求
│
▼
只读观察 SAM 当前状态
│
▼
Design:生成中文工程规格,不发送工具
│
▼
用户确认规格
│
▼
Route:确定本次允许的模块与能力
│
▼
Collect:一轮或多轮收集 Tool Call 参数,不执行
│
▼
Validate + Compile:本地校验并生成脚本片段
│
▼
Compose:按固定阶段组合成一个 Python 脚本
│
▼
Execute:SAM 只接收一次脚本执行请求
│
▼
Readback + Verify:回读真实模型并验证证据1.2 它不是标准 ReAct
| 维度 | ReAct | 普通 Plan-and-Execute | 本模式 |
|---|---|---|---|
| 规划 | 边执行边规划 | 先规划 | 先形成可确认工程规格 |
| Tool Call 后是否立即执行 | 是 | 通常是 | 否,先收集 |
| 下一轮 LLM 是否看到工具结果 | 是 | 可能看到 | 参数收集阶段看不到执行结果 |
| 生产代码由谁生成 | 工具实现或 LLM | 工具实现或 LLM | 本地领域编译器 |
| 执行次数 | 多次 | 多次 | 一次组合脚本提交 |
| 适合场景 | 探索和诊断 | 固定业务流程 | 高副作用、强 API 约束领域 |
本模式不是要取代 ReAct。它只是在“中间状态不应随意暴露给模型反复修改”的任务上,把执行权收回到 Controller。
1.3 五个必须区分的对象
Design Spec 用户可读、可确认的工程规格
Tool Call LLM 提交的结构化操作提案
Domain IR 经过解析和规范化的领域中间表示
Generated Script 本地编译器生成的平台代码
Execution Receipt SAM 执行和回读产生的事实证据它们不能混为一谈:
- 设计写得正确,不代表 Tool Call 参数正确。
- Tool Call 符合 Schema,不代表业务语义正确。
- 脚本生成成功,不代表脚本执行成功。
- 脚本返回
PASS,也不代表目标模型确实发生了预期变化。
第2部分:总体架构——模型只占系统的一层
2.1 六层架构
┌──────────────────────────────────────────────────────────────┐
│ UI / Human Gate │
│ 选择 Part/Property/Mesh、冻结 Model/Part、展示规格、确认执行 │
└──────────────────────────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ DirectAgentController │
│ 状态机、路由、轮次控制、顺序校验、收集、编译调度、验证 │
└──────────────┬───────────────────────────────┬───────────────┘
▼ ▼
┌──────────────────────────┐ ┌────────────────────────────┐
│ Provider Adapter │ │ Tool Catalog + Schemas │
│ Prompt、HTTP、SSE 重组 │ │ 能力映射、最小工具披露 │
└──────────────┬───────────┘ └──────────────┬─────────────┘
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ LLM │
│ Design 阶段输出规格;Script 阶段输出 Tool Calls │
└──────────────────────────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ Domain Tool Compilers │
│ JSON → normalized arguments → preflight/mutation/postflight │
└──────────────────────────┬───────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ SAM Gateway + Embedded Runtime │
│ 执行一次组合脚本、写回执、回读 Model/Part 状态 │
└──────────────────────────────────────────────────────────────┘2.2 每层拥有的决定权
| 层 | 可以决定 | 不应该决定 |
|---|---|---|
| UI | 当前模式、目标对象、是否确认 | SAM API 细节 |
| LLM | 意图翻译、规格表达、工具参数 | 目标绑定、真实执行时机 |
| Schema | 字段、类型、枚举、基本结构 | 跨字段业务真值 |
| Tool Catalog | 可见工具、固定能力顺序 | 用户是否确认 |
| Controller | 状态转换、调用顺序、何时提交 | 自由猜测工程参数 |
| Compiler | 平台 API、规范化、预检、代码生成 | 擅自扩展用户意图 |
| SAM Gateway | 传输和执行 | 判断设计是否合理 |
| Readback Verifier | 判断证据是否完整 | 用预期结果冒充真实结果 |
这张表是整个架构的核心:可靠性来自明确分权,而不是来自一段越来越长的 System Prompt。
第3部分:交互是怎么被控制的——Controller 状态机
3.1 显式状态
当前实现使用以下状态:
enum class AgentPhase {
Idle,
Observing,
Designing,
Review,
GeneratingScript,
Executing,
Complete,
Failed
};状态转换如下:
Idle / Complete / Failed
│ submit
▼
Observing
│ observationReady
▼
Designing
│ designReady
▼
Review
│ confirm
▼
GeneratingScript
│ calls collected + compiled
▼
Executing
│ receipt + evidence valid
▼
Complete
任一阶段发生不可恢复错误 → Failed
工具参数或顺序错误 → 返回 Review,让用户重新确认或重试3.2 为什么状态机比 Prompt 更重要
Prompt 可以告诉模型“设计阶段不要调用工具”,但真正的保证来自请求体:Design 阶段根本不把 tools 字段发给 Provider。
Prompt 可以告诉模型“没有确认不要执行”,但真正的保证来自 Controller:只有 phase == Review 且 confirm() 被调用后,才能进入 GeneratingScript。
Prompt 可以告诉模型“不要改变目标 Part”,但真正的保证来自冻结目标:Review 阶段如果 Model、Part 或对象模式改变,Controller 会让本次运行失败,要求重新观察和重新设计。
这体现了 Harness 的基本原则:
Prompt 提供行为引导;Controller 提供不可绕过的状态边界。
3.3 submit:先观察,不直接问模型
submit() 主要完成:
- 拒绝在
Observing / Designing / GeneratingScript / Executing中重入。 - 验证 Provider、SAM Gateway、History 是否可用。
- 冻结用户请求、Model、Part 和
Part/Property/Mesh模式。 - 创建唯一
runId和运行目录。 - 保存
request.txt。 - 进入
Observing,请求 SAM 返回只读工作区状态。
为什么不直接让 LLM 规划?因为“当前 Part 是空的还是已有结构”“有哪些 Set”“节点与单元数量是多少”不能依靠对话历史猜测。
3.4 Observation:事实必须来自平台
观察回执至少承担两个作用:
- 验证目标 Model 是否真实存在;
- 为路由和设计提供当前 Part 摘要。
观察通过后才进入 Designing。这意味着 LLM 收到的是:
{
"tool_module": "Part",
"target": {
"model_name": "Model-1",
"part_name": "Part-1",
"exists": true
},
"observation": {
"summary": {
"node_count": 6,
"element_count": 2
}
}
}3.5 Design:无工具的规格阶段
Design 阶段只允许输出中文工程规格:
- 用户想创建、增加、删除、变换、修复还是审计什么;
- 使用哪些尺寸、坐标、区域和选择条件;
- 哪些是假设,哪些是当前能力边界;
- 需要哪些能力,按什么顺序。
请求体中不包含 Tool Schema,因此模型即使想调用工具也没有正式的 Tool Call 通道。
3.6 Review:人类确认的是语义,不是 JSON
用户确认的对象是可读工程规格,而不是数百行 JSON Schema。这一层解决的是:
- 尺寸是否正确;
- 坐标系是否正确;
- 是追加还是替换;
- 是全部单元还是某个区域;
- 是否需要清理和审计。
确认并不证明后续参数一定正确,它只是冻结了允许实现的意图范围。
3.7 confirm:打开执行阶段,但仍不执行 SAM
确认时 Controller:
confirmed = true
collectedToolCalls = []
providerToolRound = 1
phase = GeneratingScript
发送第一轮 Script 请求这里特别重要:confirm() 只是允许模型开始提交工具参数,并没有立即修改 SAM。
第4部分:Schema 的真实作用——限制表达空间,而不是替代控制器
4.1 Tool Schema 由三部分组成
{
"type": "function",
"function": {
"name": "build_part",
"description": "把自然语言几何映射为节点和单元……",
"parameters": {
"type": "object",
"additionalProperties": false,
"required": ["nodes", "elements"],
"properties": {}
}
}
}三部分分别承担不同职责:
name:让模型区分能力。description:告诉模型何时使用、如何把自然语言映射到字段、有哪些禁止事项。parameters:限制 JSON 的字段、类型、数组和枚举。
4.2 build_part:局部 ID,不暴露真实 SAM 标签
简化后的 Schema 是:
{
"nodes": [
{"id": "N1", "coordinates": [0, 0, 0]},
{"id": "N2", "coordinates": [60, 0, 0]},
{"id": "N3", "coordinates": [60, 60, 0]},
{"id": "N4", "coordinates": [0, 60, 0]}
],
"elements": [
{
"id": "E1",
"shape": "QUAD",
"node_ids": ["N1", "N2", "N3", "N4"]
}
]
}设计中的关键点不是字段名称,而是 ID 语义:
N1 / E1 = 本次 Tool Call 内的局部别名
SAM label = 平台运行后真实生成的正整数标签LLM 不需要也不应该猜测 SAM 将分配什么标签。Compiler 在执行时把局部关系解析成真实节点对象。
4.3 edit_part:nodes 是坐标解析表,不是强制创建表
这是实际开发中最重要的一次 Schema 语义修正。
用户要求利用两个已有坐标增加 TRI:
(0,0,500)
(1000,0,500)
(500,500,500)正确 Tool Call 仍然必须列出三个坐标:
{
"nodes": [
{"id": "n1", "coordinates": [0, 0, 500]},
{"id": "n2", "coordinates": [1000, 0, 500]},
{"id": "n3", "coordinates": [500, 500, 500]}
],
"add_elements": [
{
"id": "e1",
"shape": "TRI",
"node_ids": ["n1", "n2", "n3"]
}
]
}Compiler 的语义是:
for coordinate in requested_coordinates:
if surviving_node_exists_at(coordinate):
reuse_node()
else:
create_one_node()如果只提供新节点 n3,却让单元引用 n1/n2/n3,Schema 可能仍然是合法 JSON,但跨字段引用不成立。这个错误必须由 Compiler 的语义校验发现。
4.4 Schema 能做什么
Schema 擅长约束:
coordinates必须是三个数字;shape只能是QUAD / TRI / BEAM;- 标签必须是大于等于 1 的整数;
levels只能是 1 到 3;- 不允许未知字段;
- 必填字段不能缺失;
- 数组是否至少包含一项。
4.5 Schema 不能证明什么
仅靠 JSON Schema 很难证明:
- QUAD 恰好引用四个不同坐标节点;
node_ids都能在同一次调用的nodes中解析;- Set 引用的元素确实由同一次调用创建;
- 删除节点时依赖单元应该如何级联删除;
clear_all=true是否真的来自用户明确的“删除全部”意图;- Section 类型是否与被选元素类型匹配;
- 梁方向
n1是否与梁轴平行; - 两个工具的选择区域是否相互冲突;
- Tool Call 的顺序是否与确认规格一致。
因此需要三层验证:
JSON Schema 结构合法性
Domain Compiler 领域语义合法性
Readback Verifier 平台执行事实合法性4.6 Tool description 不是注释,而是模型操作手册
当前工具描述中不仅写 API 能力,还写了:
- “平面板”如何映射为 QUAD;
- “梁/杆”如何映射为 BEAM;
- 共享坐标必须复用;
- 哪些 ID 是局部 ID,哪些是 SAM 标签;
- 正确和错误 TRI 示例;
- 未明确要求时不得生成 Set;
clear_all只能用于显式整体替换。
这解释了为什么“只把 API 封装成 Tool”还不够。模型面对的不是 C++ 实现,而是 Tool Schema 与 description。它们共同构成模型的领域操作手册。
第5部分:工具不是一次全部发送——Tool Catalog 与渐进披露
5.1 一级路由由 UI 决定
用户先在界面选择:
Part | Property | Mesh这一选择冻结为 AgentObjectMode。LLM 不需要在所有 CAE 模块中猜测当前任务属于什么。
这是一个重要设计原则:
如果可靠的界面状态已经知道路由结果,就不要再让 LLM 重做一次分类。
5.2 二级路由由确认规格决定
当前能力集合是:
Part: build, edit, transform, repair, audit
Property: define, assign, audit
Mesh: refine, auditTool Catalog 把能力名映射为 Wire Tool Name:
build → build_part
edit → edit_part
transform → transform_part
repair → repair_part
audit → audit_part
define → define_property
assign → assign_property
refine → refine_mesh5.3 空 Part 与已有 Part 的路由不同
Part 模块结合 Observation 判断:
目标不存在或节点数=0且单元数=0 → build_part
目标已有结构 → edit/transform/repair/auditbuild_part 不能和已有 Part 的操作组合。这不是 LLM 偏好,而是 Controller/Compiler 的硬规则。
5.4 每一轮只发送剩余工具
假设确认规格需要:
transform_part → repair_part → audit_part第一轮上下文包含全部确认能力,Tool Catalog 发送剩余工具。若 Provider 只返回 transform_part,Controller 不执行它,而是记录:
collected = [transform_part]
remaining = [repair_part, audit_part]第二轮只暴露剩余工具。这样既减少 Schema Token,也降低模型重复调用已经收集工具的概率。
5.5 显式路由标记与语义回退
早期实现要求设计最后一行必须严格包含 capability line,例如:
计划 Mesh 操作类别:refine, audit严格格式曾导致合法规格被阻断:
Confirmed design must contain exactly one Mesh capability line后续 Property 和 Mesh 增加了语义回退:
- 有合法显式标记时,以标记为准;
- 标记遗漏时,从规格中的明确操作语义推断;
- 如果仍无法识别,才拒绝路由。
这里的经验是:
自然语言中的格式标记可以提高确定性,但不应该成为唯一安全边界。真正的边界应由工具白名单、参数校验和执行控制建立。
第6部分:多轮 Tool Call 是如何被收集而不执行的
6.1 Controller 保存两组状态
概念上应区分:
expectedTools 确认规格要求的完整工具序列
collectedToolCalls Provider 已提交但尚未执行的调用每轮返回后,Controller 计算:
collectedCount = collectedToolCalls.size();
remainingCount = expectedTools.size() - collectedCount;然后逐项校验:
expectedIndex = collectedCount + responseIndex;
if (call.name != expectedTools[expectedIndex])
return_to_review();
collectedToolCalls.append(call);6.2 顺序由 Controller 控制
Provider 不能把:
transform → repair → audit擅自改成:
audit → transform → repair因为后者的审计发生在修改之前,语义完全不同。Controller 对工具名称、位置和数量进行校验。
6.3 为什么允许多轮收集
一些 Provider 在一次响应中不能稳定返回多个工具,或者模型只愿意返回第一个调用。因此 Controller 接受:
本轮返回调用数 >= 1
本轮返回调用数 <= 剩余调用数只要顺序正确,就继续收集。如果还没有收齐,再发下一轮 Script 请求。
6.4 “collected”绝不能写成“completed”
当前实现已经使用 collectedToolCalls 保存真实状态,这是正确方向。但传给 Provider 的兼容字段仍名为:
"completed_part_tools": ["transform_part"]这个命名会诱导模型说:
transform_part 已经完成,现在执行 repair_part。
事实上它尚未进入 SAM。更准确的命名应该是:
"collected_tool_calls": ["transform_part"]同理,当前 ExecutedPartToolCall 在脚本提交前就被填充,它实际更接近:
CompiledToolCall / PlannedToolCall状态命名不是小事。错误命名会同时污染:
- Provider Prompt;
- UI 文案;
- 日志解释;
- 结果验证;
- 后续维护者对执行时机的理解。
6.5 收齐之前零副作用
这是本模式最重要的不变量:
collectedToolCalls.size() < expectedTools.size()
⇒ 不调用 sam.executeScript()只有全部调用收齐、JSON 解析成功、所有 Compiler 校验通过、脚本组合成功并写入历史后,Controller 才会进入 Executing。
第7部分:Provider Adapter——兼容模型差异,而不是假设 API 一致
7.1 Design 与 Script 使用不同请求体
Design 请求:
{
"model": "...",
"stream": true,
"temperature": 0.1,
"messages": ["system", "user"]
}Script 请求才增加:
{
"tools": ["当前模块、当前轮次真正需要的 Tool Schema"]
}这比在一个巨大的 System Prompt 中反复声明“现在不能调用工具”可靠。
7.2 为什么没有强制 tool_choice
实际接入 DeepSeek thinking 模式时曾出现:
Thinking mode does not support this tool_choice因此当前策略是不向 Provider 强行发送不兼容的 tool_choice,而把约束放到本地:
- 只发送允许的工具;
- Prompt 要求每个剩余工具调用一次;
- Controller 校验工具名称、顺序和数量;
- 没有调用或调用错误时返回 Review。
通用经验是:
Provider Capability ≠ Agent InvariantProvider 支持某项功能时可以利用;不支持时,Agent 的关键不变量仍应由 Harness 本地执行。
7.3 SSE Tool Call 不是一次完整 JSON
流式响应可能把一个调用拆成:
chunk 1: index=0, id="call_", name="transform_", args="{\"oper"
chunk 2: index=0, id="123", name="part", args="ations\":["
chunk 3: index=0, args="...]}"Adapter 使用 index 聚合:
QMap<int, ProviderToolCall> callsByIndex;
ProviderToolCall &call = callsByIndex[index];
call.id += chunk.id;
call.name += chunk.function.name;
call.argumentsJson += chunk.function.arguments;完成后再按 index 输出完整调用。
7.4 SSE 解析必须验证结束语义
当前解析器检查:
- 是否至少收到一个
data:事件; - 每个 Tool Call 是否有非负数值
index; - 每个 SSE data 是否是合法 JSON;
- 是否最终收到
[DONE]。
没有 [DONE] 时,即使前面看起来已有完整参数,也不能轻易当成完整响应,因为尾部可能被网络截断。
7.5 UI 重复文本与重复执行是两回事
多轮 Provider 响应会累计到 scriptResponse,界面可能显示:
TOOL transform_part
TOOL transform_part
TOOL repair_part
TOOL audit_part这可能来自累计文本重复展示,也可能来自 Provider 在下一轮复述先前调用。判断是否真正重复执行,不能看 UI 文本次数,而要看:
parsed tool call index
collectedToolCalls
compiled tool receipts
execution submission count
SAM receipt因此可观测性至少要区分五类事件:
provider_stream_chunk
provider_tool_call_reconstructed
tool_call_collected
tool_call_compiled
script_execution_submitted第8部分:Tool Call 是领域 IR,不是 Python 代码
8.1 为什么不让 LLM 写生产 Python
早期自由脚本曾出现:
TypeError: String Expected as dictionary Type
NameError: name 'QUAD' is not defined
TypeError: keyword error on dimensionality这些错误并不是用户几何意图错误,而是模型猜错了运行时边界:
- 宿主注入值的类型;
- SAM 枚举导入;
Part()的真实关键字;- SAM 与 Abaqus API 的差异。
把这些责任交给 Tool Compiler 后,LLM 只需要表达:
{
"shape": "QUAD",
"node_ids": ["N1", "N2", "N3", "N4"]
}Compiler 负责生成正确的平台代码:
part.Element(
nodes=(resolved_nodes["N1"], resolved_nodes["N2"],
resolved_nodes["N3"], resolved_nodes["N4"]),
elemShape=QUAD,
intersectNodes=True)8.2 Domain IR 的四个特征
一个好的领域 Tool Call 应当:
- 使用业务概念,而不是泄漏底层 API 参数。
- 使用局部符号或声明式选择器,而不是让模型猜运行时对象。
- 能被本地程序完全校验。
- 能确定性编译,同一规范化参数生成同一语义脚本。
8.3 编译过程
raw argumentsJson
│ JSON parse
▼
QJsonObject
│ schema + domain validation
▼
normalizedArguments
│ platform mapping
▼
ScriptFragment {
preflight,
mutation,
postflight,
evidence
}8.4 规范化的意义
规范化参数用于:
- 统一枚举大小写;
- 去重标签;
- 固定坐标精度;
- 补入安全默认值;
- 删除未被用户明确要求的字段;
- 记录真正进入编译器的参数。
例如用户没有明确要求 Set 时,Controller 会移除模型擅自生成的 element_sets。clear_all=true 还必须通过原始请求中的显式整体删除意图检查。
这说明 Controller 不只验证 JSON,它还把“用户授权范围”与“模型输出”做交叉验证。
第9部分:脚本组合——Preflight、Mutation、Postflight
9.1 每个工具编译为片段
Tool Compiler 不立即执行,而是返回:
struct ScriptFragment {
QString toolName;
QStringList preflight;
QStringList mutation;
QStringList postflight;
QJsonObject normalizedArguments;
QJsonObject evidence;
};9.2 组合器按阶段重排
PartScriptProgram::compose() 的结构是:
固定导入与目标绑定
→ 所有工具的 preflight
→ 所有工具的 mutation
→ 所有工具的 postflight
→ viewport refresh
→ completion marker这比简单拼接:
tool1 全部代码
tool2 全部代码
tool3 全部代码更安全,因为能在任何修改发生前尽可能完成全局预检。
9.3 目标绑定由宿主注入
生成脚本使用:
model_name = SAM_AGENT_TARGET['model_name']
part_name = SAM_AGENT_TARGET['part_name']Tool Schema 不允许模型传 model_name 和 part_name。这避免:
- 模型修改错误 Part;
- 用户规格与执行目标漂移;
- Tool Call 注入另一个模型名称。
9.4 Property 的额外不变量
Property 组合脚本在执行前记录节点、单元数,执行后检查:
if len(part.nodes) != before_nodes or len(part.elements) != before_elements:
raise RuntimeError('Property workflow changed Part geometry')这是一条很好的模块边界:Property 模式可以修改材料、Section 和分配,但不能改变几何拓扑。
9.5 一次执行不等于数据库事务
组合脚本只调用一次 sam.executeScript(),它具有以下价值:
- 避免工具逐个往返 SAM;
- 固定执行顺序;
- 统一保存生成脚本;
- 在修改前执行更多预检;
- 只产生一次执行提交记录。
但必须明确:
Single Submission ≠ Atomic Transaction如果 Mutation 第三步抛异常,前两步的内存修改仍可能保留。除非 SAM 提供事务、快照恢复或在副本上执行,否则不能宣称具备 rollback。
第10部分:完成的定义——不是“脚本没报错”
10.1 执行回执的第一层检查
SAM Gateway 返回 receipt。Controller 首先检查:
receipt.status == PASS失败时优先展示 traceback,其次展示 error。
10.2 Readback Evidence
执行成功后,Controller 检查目标是否仍存在,并读取:
- 节点数;
- 单元数;
- QUAD/TRI/BEAM 数量;
- Set 列表;
- Property 摘要;
- Audit 报告文件。
例如 edit_part 会根据执行前状态、删除数量、新增坐标数和新增元素数,计算合理范围,再与真实回读对比。
10.3 为什么使用范围而不是始终精确相等
删除节点可能级联删除依赖单元;重复坐标会被复用;删除和新增可能相互影响。因此某些操作的预期结果只能表达为:
minimum <= actual <= maximum这比简单要求 actual == before + added - deleted 更符合真实拓扑操作。
10.4 Audit Artifact 也是证据
如果调用了 audit_part,回执中必须能找到 part_audit.json;如果调用了 Property Audit,则必须有 property_audit.json。
这防止出现:工具名被记录为已执行,但实际脚本没有生成报告的假完成状态。
10.5 建议使用更精确的状态词汇
Proposed LLM 已提出调用
Collected Controller 已收集调用
Compiled 本地编译成功
Submitted 脚本已提交 SAM
Executed SAM 返回执行成功
Verified 回读证据符合预期只有 Verified 才适合向用户显示“完成”。
第11部分:错误如何分类,Controller 如何响应
11.1 六类错误
| 错误层 | 示例 | 推荐动作 |
|---|---|---|
| Observation | Model 不存在 | 停止,要求修正目标 |
| Design | 无法识别能力 | 回到需求/规格阶段 |
| Provider Protocol | SSE 缺 [DONE] | 失败,可安全重试 Provider |
| Tool Call | 工具顺序错误、参数非 JSON | 返回 Review,不执行 SAM |
| Compile | 未知节点 ID、类型冲突 | 返回 Review,不执行 SAM |
| Execution | SAM Python traceback | 标记失败,提示可能部分修改 |
| Verification | 执行 PASS 但回读证据缺失 | 标记失败,不宣称完成 |
11.2 参数错误为什么返回 Review
工具参数或顺序错误发生时,SAM 尚未执行,因此可以安全地:
confirmed = false
phase = Review
展示明确错误这是一个有界恢复点。系统不应该无限让模型在后台自动重试,因为多次不透明重试会消耗 Token,也会让用户不知道规格是否被改变。
11.3 运行错误不能盲目重试
执行阶段失败后,模型可能已经部分改变。正确恢复流程应是:
停止自动重试
→ 重新观察 SAM 当前真实状态
→ 生成修复规格
→ 用户确认
→ 新一轮编译与执行这叫人工确认的有界修复循环,而不是自主 ReAct 修复。
第12部分:真实失败案例如何推动架构演进
12.1 自由 Python:平台 API 猜测失败
错误包括:
String Expected as dictionary Type
QUAD is not defined
keyword error on dimensionality教训:LLM 不应承担目标绑定、模块导入、SAM 枚举和 API 签名。
改进:把自然语言翻译为 Tool Call,由本地 Compiler 生成生产 Python。
12.2 build_part 能创建,edit_part 却无法复用已有节点
早期 edit_part 把局部 ID 与 SAM 标签混淆,导致:
Element references an unknown added node id教训:局部引用必须有完整的坐标解析表;“列出节点”不应等同于“强制创建节点”。
改进:nodes 定义为 coordinate-resolution entries,已有坐标复用,缺失坐标才创建。
12.3 模型擅自生成 Set
用户只要求增加 TRI,模型却附带 element_sets,并引用已有或不存在元素。
教训:Tool description 必须解释“用户不会说 Set”并不代表应该自动创建 Set;Controller 还要对照原始用户意图移除未授权字段。
12.4 多工具计划只返回第一个 Tool Call
系统曾要求 Provider 一次返回 transform/repair/audit,Provider 只调用 transform,Controller 报错。
教训:不能把“模型必须一次返回全部工具”当成 Provider 能力保证。
改进:允许多轮参数收集,但执行仍延迟到全部收齐以后。
12.5 tool_choice 与 Provider thinking 模式冲突
教训:OpenAI 风格接口不代表所有 Provider 支持完全相同的行为参数。
改进:取消不兼容的强制 tool_choice,用最小工具披露和本地顺序校验实现同一不变量。
12.6 严格 capability line 阻断正确请求
教训:把自然语言输出格式当成唯一协议,会让形式正确性压过任务正确性。
改进:显式标记优先,明确语义回退,最终仍由 Tool 白名单约束。
12.7 refine_mesh 使用字符串而非枚举
运行时报错:
found string, expecting enum教训:Schema 中的 "QUAD" 是领域 IR 字符串,不能原样当作 SAM API 枚举。
改进:Compiler 负责 "QUAD" → QUAD 的平台映射。
第13部分:事务、幂等和部分执行风险
13.1 四个容易混淆的概念
| 概念 | 含义 | 当前实现 |
|---|---|---|
| 单次提交 | Controller 只调用一次执行网关 | 已实现 |
| 精确一次 | 同一运行不会被重复提交 | 部分实现,需要持久化幂等键加强 |
| 幂等 | 重复执行产生同一最终状态 | 不保证,部分操作天然非幂等 |
| 原子事务 | 全部成功或全部回滚 | 未实现 |
13.2 哪些操作不是幂等的
translate_copy 重复执行会多复制一份
linear_array 重复执行会继续增加元素
refine_mesh 重复执行会再次细分
createNode 无坐标复用保护时会产生重复节点因此网络超时后不能简单地“再执行一次”。必须先查询当前状态或使用运行 ID 确认上一次是否已提交。
13.3 推荐的事务增强路线
从低成本到高成本:
- 执行前全局 Preflight:尽量在 Mutation 前发现错误。
- Execution Idempotency Key:以
runId + script hash防止重复提交。 - 执行前快照:保存临时
.sam或复制目标 Part。 - 影子 Part 执行:在临时 Part 上修改,验证后替换。
- 平台事务 API:如果 SAM 将来提供 begin/commit/rollback,映射为真正事务。
在没有快照或事务的情况下,UI 必须诚实提示:运行失败可能留下部分修改。
第14部分:大型 CAE 模型的 Context Engineering
14.1 全量 Observation 不可扩展
一个几十万节点、几十万元素的模型如果把所有坐标和连接关系塞入 Prompt,会造成:
- 输入 Token 爆炸;
- Provider 请求截断;
- 设计响应被挤压;
- 模型注意力分散;
- 敏感工程数据暴露范围扩大。
14.2 分层观察模型
推荐四层:
L0 Workspace Index
模型、Part 名称和存在性
L1 Part Summary
节点/单元数、包围盒、类型计数、Set/Section 名称
L2 Region Index
分区、站位、空间盒、连通分量摘要
L3 On-Demand Detail
指定标签、Set、坐标范围内的节点与单元LLM 默认只拿 L0/L1。需要局部操作时,通过只读查询能力取得 L2/L3,而不是把全模型提前注入。
14.3 Observation 也需要 Schema
不仅 Tool Call 需要 Schema,Observation 也应该稳定:
{
"target_exists": true,
"summary": {
"node_count": 188630,
"element_count": 176022,
"bounding_box": {
"minimum": [0, -5000, 0],
"maximum": [32634, 5000, 8000]
},
"shape_counts": {
"QUAD": 170000,
"TRI": 6022,
"BEAM": 0
}
},
"sets": ["HULL", "DECK", "LEFT_END", "RIGHT_END"]
}稳定 Observation Schema 能提高 Prompt 缓存、日志对比和 Controller 验证的可靠性。
第15部分:从重复分支演进为通用模块协议
15.1 当前问题
Controller 中 Part、Property、Mesh 有大量相似逻辑:
- 解析设计能力;
- 生成 Tool 定义;
- 校验调用顺序;
- 解析参数;
- 编译 Fragment;
- 组合脚本;
- 验证回执。
如果继续增加 Assembly、Load、Boundary、Step 和 Job,分支会快速膨胀。
15.2 推荐模块协议
class DomainModule {
public:
virtual QString designPrompt() const = 0;
virtual RouteResult routeCapabilities(
const QString &design,
const QJsonObject &observation) const = 0;
virtual QJsonArray toolDefinitions(
const QStringList &remainingCapabilities) const = 0;
virtual CompileResult compileToolCalls(
const QList<ProviderToolCall> &calls,
const DirectTarget &target,
const QJsonObject &observation) const = 0;
virtual QString verifyReceipt(
const CompileReceipt &expected,
const QJsonObject &actual) const = 0;
};Controller 只保留通用流程:
observe
→ request design
→ route
→ review
→ collect
→ module.compile
→ execute
→ module.verify15.3 哪些规则仍应留在模块内
- Part:空/已有几何路由、坐标复用、拓扑删除。
- Property:材料与 Section 引用、壳/梁分配、几何不变性。
- Mesh:细分合法性、Section 继承、共享中点。
- Load:载荷类型、区域、步关联和单位。
- Boundary:自由度、参考点、耦合和冲突检查。
通用 Controller 不应该知道 QUAD 有四个节点,也不应该知道壳 SectionAssignment 的替换规则。
第16部分:可观测性——记录“发生了什么”,而不是只存最终文本
16.1 当前运行证据
一次运行目录可包含:
request.txt
workspace.json
design.txt
design_route.json
provider_tool_response.sse
provider_tool_response.2.sse
script_response.txt
tool_calls.json
generated_script.py
result.json
part_audit.json / property_audit.json / mesh_audit.json这些文件让问题可以沿链路定位:
用户是否表达错
→ 设计是否翻译错
→ 路由是否选错
→ Provider 是否填错参数
→ Compiler 是否生成错脚本
→ SAM 是否执行失败
→ Readback 是否证据不足16.2 推荐事件模型
{
"run_id": "...",
"event": "tool_call_compiled",
"module": "Mesh",
"tool": "refine_mesh",
"tool_index": 0,
"provider_round": 1,
"script_hash": "...",
"timestamp": "..."
}建议事件至少包括:
observation_completeddesign_receiveddesign_confirmedtool_call_reconstructedtool_call_collectedtool_call_rejectedtool_call_compiledscript_composedexecution_submittedexecution_completedreadback_verified
16.3 UI 文案必须对应真实状态
已收到参数 ≠ 已执行
脚本已生成 ≠ 已提交
SAM 返回成功 ≠ 已验证建议界面分别显示:
正在收集 2/3 个操作参数
正在编译已确认操作
脚本已提交 SAM
SAM 执行成功,正在回读验证
验证完成第17部分:可复用 Controller 骨架
下面的伪代码展示模式本身,而不是某个框架 API:
class DeterministicDomainController:
def submit(self, request, target, module):
self.assert_not_busy()
self.run = RunContext(
request=request,
frozen_target=target,
module=module,
phase="OBSERVING",
)
self.sam.observe(target)
def on_observation(self, observation):
self.verify_observation(observation)
self.run.observation = observation
self.run.phase = "DESIGNING"
# Design 阶段不提供 tools
self.provider.request(
prompt=self.module.design_prompt(),
context=self.design_context(),
tools=None,
)
def on_design(self, design):
route = self.module.route_capabilities(
design, self.run.observation
)
self.assert_route_valid(route)
self.run.design = design
self.run.expected_tools = route.ordered_tools
self.run.phase = "REVIEW"
def confirm(self):
self.assert_phase("REVIEW")
self.run.confirmed = True
self.run.collected_calls = []
self.run.phase = "COLLECTING"
self.request_remaining_calls()
def request_remaining_calls(self):
remaining = self.run.expected_tools[
len(self.run.collected_calls):
]
self.provider.request(
prompt=self.module.tool_prompt(),
context=self.script_context(remaining),
tools=self.module.definitions(remaining),
)
def on_tool_calls(self, calls):
self.validate_names_order_and_count(calls)
self.run.collected_calls.extend(calls)
if len(self.run.collected_calls) < len(self.run.expected_tools):
self.request_remaining_calls()
return
# 到此之前没有执行任何领域修改
compiled = self.module.compile_all(
calls=self.run.collected_calls,
target=self.run.frozen_target,
observation=self.run.observation,
original_request=self.run.request,
)
self.persist(compiled.normalized_calls)
self.persist(compiled.script)
self.run.phase = "EXECUTING"
self.sam.execute_once(
target=self.run.frozen_target,
script=compiled.script,
idempotency_key=self.run.id,
)
def on_execution_receipt(self, receipt):
self.assert_execution_passed(receipt)
self.module.verify_receipt(
expected=self.run.compiled_evidence,
actual=receipt,
before=self.run.observation,
)
self.run.phase = "COMPLETE"17.1 Controller 的核心不变量
实现时可以把以下规则写成断言或状态检查:
I1 Design 请求不得包含 tools
I2 未确认不得进入 Collecting
I3 Review 后 target/module 不得漂移
I4 只能调用本次路由允许的工具
I5 Tool Call 顺序必须与 expectedTools 一致
I6 未收齐全部调用不得执行 SAM
I7 所有调用必须先编译成功再组合
I8 同一 runId 最多提交一次执行
I9 SAM PASS 后仍必须完成 readback verification
I10 Execution 失败不得自动盲重试副作用脚本第18部分:何时应该使用这种模式
18.1 适合
- CAE/CAD 模型修改;
- 数据库 Schema 迁移;
- PLC/工业控制配置;
- 云基础设施变更;
- 财务批处理;
- 多步骤但应统一提交的文档或配置变更;
- 平台 API 复杂、模型容易猜错签名的领域系统。
共同特征是:
修改有副作用
+ 操作之间有顺序
+ 中间状态不适合交给 LLM 自由探索
+ 底层 API 应由确定性代码掌握18.2 不适合
- 搜索和信息检索;
- 需要根据每次工具结果动态决定下一步的诊断;
- 工具全部只读且失败成本很低;
- 任务路径本质未知,必须边探索边规划。
这些场景使用 ReAct 更自然。
18.3 混合模式
复杂工程系统可以外层使用本模式,局部只读分析使用 ReAct:
只读 ReAct 探索模型
↓
形成修改规格
↓
用户确认
↓
确定性编译与单次提交
↓
只读 ReAct 分析审计结果关键边界是:ReAct 可以自由探索事实,但真正修改必须重新经过确认门。
第19部分:设计检查清单
19.1 Schema
- [ ] 工具参数使用领域概念,而不是暴露底层 API 对象
- [ ]
additionalProperties=false - [ ] 枚举、数组长度、数值范围尽量明确
- [ ] 局部 ID 与平台真实 ID/标签语义分离
- [ ] Tool description 解释自然语言映射、边界和正确示例
- [ ] 跨字段引用由 Compiler 校验
19.2 Controller
- [ ] 状态机显式存在,而不是由 UI 文案隐式推断
- [ ] Design 阶段不发送工具
- [ ] 用户确认后冻结目标和模块
- [ ] expected、collected、compiled、executed、verified 状态分离
- [ ] 多轮调用只发送剩余工具
- [ ] 工具名称、顺序和数量由本地代码校验
- [ ] 收齐全部调用之前不产生副作用
19.3 Compiler
- [ ] 所有平台 API 语法由本地代码生成
- [ ] Schema 字符串枚举转换为真实平台枚举
- [ ] 全局 Preflight 尽量先于 Mutation
- [ ] 规范化参数和预期证据可持久化
- [ ] 编译器不能添加用户未授权的操作
19.4 Execution 与验证
- [ ] 同一运行具有幂等提交键
- [ ] 清楚声明是否支持 rollback
- [ ] 运行错误不自动盲重试
- [ ] PASS 后回读真实状态
- [ ] Audit 文件和目标计数被当作执行证据
- [ ] UI 不把 collected 显示为 completed
19.5 Provider
- [ ] 不假设所有 Provider 支持相同
tool_choice - [ ] SSE 按 index 重组 Tool Call
- [ ] 检查完整结束标志
- [ ] 原始响应、重组调用和执行事件分开记录
- [ ] 工具数量按模块和轮次最小化
核心总结
总结1:Tool Call 可以是提案,而不是立即执行
标准 Function Calling 把 Tool Call 解释为“请现在执行这个函数”。高副作用领域可以把它重新解释为:
请把这条结构化领域操作加入待提交计划。总结2:Schema 只负责第一道边界
Schema 保证结构
Compiler 保证领域语义与平台映射
Controller 保证授权、状态、顺序和执行时机
Readback 保证真实结果任何一层都不能完全替代其他层。
总结3:Controller 是交互的真正导演
模型并不控制完整流程。Controller 决定:
- 什么时候观察;
- 什么时候设计;
- 什么时候等待确认;
- 发送哪些工具;
- 还缺哪些调用;
- 是否允许编译;
- 是否允许执行;
- 什么时候才算完成。
总结4:确定性编译器是领域 Agent 的关键边界
LLM 擅长把“60×100 的平面板”翻译成节点和连接关系,但不应该负责记忆 SAM 某版本的枚举、对象类型和函数签名。让模型输出领域 IR,让本地编译器输出平台代码,是当前系统从不稳定走向可用的关键变化。
总结5:单次提交提高可靠性,但不自动获得事务
一次组合脚本能够减少中间往返和顺序漂移,但脚本仍可能部分执行。除非实现快照、影子对象或平台事务,否则必须诚实承认没有原子 rollback。
总结6:最可靠的 Agent 不是校验最多,而是边界最清楚
早期系统的问题并不是“校验不够多”,而是把很多形式校验放在了错误层:强制 capability line、让模型自由写 Python、混淆局部 ID 与真实标签、把已收集说成已完成。
真正有效的约束是:
UI 冻结目标
Schema 限制表达
Controller 控制流程
Compiler 掌握 API
SAM 提供事实
Readback 证明结果章节测试
测试1:模式辨析
为什么本章模式不能简单称为 ReAct?
测试2:Schema 边界
一个 Tool Call 完全符合 JSON Schema,为什么仍然可能被 Compiler 拒绝?请给出两个例子。
测试3:状态语义
collected_tool_calls、compiled_tool_calls 和 executed_tool_calls 有什么区别?为什么不能统一叫 completed tools?
测试4:多轮调用
确认工具序列为 transform → repair → audit,Provider 第一轮只返回 transform。Controller 应该立即执行 transform,还是继续收集?为什么?
测试5:Provider 兼容
Provider 不支持强制 tool_choice 时,如何仍然保证模型只能按顺序调用允许的工具?
测试6:SSE
为什么必须按 tool_call.index 重组流式调用,而不能简单把所有 argument 文本拼成一个字符串?
测试7:事务
“Controller 只调用一次 executeScript”能否证明整个修改具有原子性?为什么?
测试8:完成语义
SAM 返回 PASS 后为什么还需要 Readback Verification?
测试9:架构抽象
如果增加 Load 模块,哪些逻辑应该复用通用 Controller,哪些逻辑应该放进 LoadModule?
参考答案
测试1答案
ReAct 在每次 Tool Call 后立即执行工具,并把结果作为 Observation 交给下一轮模型。本模式先生成可确认规格,再多轮收集 Tool Call,全部收齐后才统一编译和执行。参数收集期间没有执行结果返回给 LLM,因此不是标准 Thought-Action-Observation 循环。
测试2答案
Schema 只能验证结构。例如 node_ids 可以是字符串数组,但不能保证每个 ID 都在同一次调用的 nodes 中存在;shape=QUAD 可以是合法枚举,但 Schema 未必保证它引用四个不同坐标。跨字段引用、拓扑约束和平台状态需要 Compiler 校验。
测试3答案
- collected:已从 Provider 收到并通过顺序检查,尚未编译和执行;
- compiled:已解析、规范化并生成脚本片段,尚未获得 SAM 执行事实;
- executed:SAM 已执行并返回成功;
- verified:回读证据也符合预期。
统一称 completed 会让模型、UI 和维护者误判副作用已经发生。
测试4答案
继续收集 repair 和 audit,不执行 transform。三个操作属于同一确认计划,只有全部参数收齐并共同通过编译后,才能组合为一次脚本提交。立即执行会破坏“执行前完整计划”和“零中间副作用”的不变量。
测试5答案
只向 Provider 暴露当前模块和当前轮次剩余工具;在 System Prompt 中说明固定顺序;Controller 本地校验工具名称、位置、数量;错误时拒绝并返回 Review。Provider 参数是优化手段,本地 Harness 才是最终保证。
测试6答案
一个 SSE 响应可能同时交错多个 Tool Call。只有相同 index 的 id、name 和 arguments 分片属于同一个调用。全局拼接会把两个调用的参数混在一起,或者破坏原有顺序。
测试7答案
不能。单次调用只表示一次提交。Python 脚本内部仍包含多个顺序 Mutation,后一步报错时前一步可能已经改变 SAM。真正原子性需要事务 API、快照恢复或影子对象提交。
测试8答案
PASS 只能证明运行器没有报告异常,不能证明正确 Part 被修改、数量符合预期、Section 被保留或 Audit 文件真正生成。Readback 使用平台真实状态验证目标存在性、节点单元计数、类型数量、Set、Property 摘要和报告产物。
测试9答案
通用 Controller 复用观察、Design、Review、确认、多轮收集、顺序校验、编译调度、执行和回读阶段。LoadModule 负责载荷能力路由、Load Tool Schema、载荷区域和 Step 引用校验、SAM Load API 编译以及载荷专属回读证据。
相关笔记
- [[01-function-calling]] - Tool Call 的基础协议和宿主执行语义
- [[05-agent-workflow]] - ReAct 与 Plan-and-Execute
- [[09-structured-output]] - JSON Schema 与结构化输出
- [[10-tools-design]] - Tool description、参数与粒度设计
- [[12-agent-modes]] - Plan/Edit 模式和工具渐进披露
- [[13-workflow-orchestration]] - 状态机与持久化工作流
- [[14-context-engineering]] - Observation 和上下文预算
- [[15-harness-loop-skills]] - 模型外部约束系统
- [[19-权限与门卫]] - 人工确认和高副作用操作边界
- [[22-可观测性与调试]] - Trace、日志和执行回放
下一步学习
- [ ] 将
completed_part_tools重命名为collected_tool_calls - [ ] 将
ExecutedPartToolCall拆分为 Compiled、Submitted、Executed、Verified 状态 - [ ] 为脚本提交增加
runId + script hash幂等键 - [ ] 抽象统一
DomainModule接口,减少 Part/Property/Mesh Controller 分支 - [ ] 为大型 CAE 模型实现 Summary → Region → Detail 分层 Observation
- [ ] 评估影子 Part 或执行前快照,实现失败恢复
学习状态:🟡 开始学习