diff --git a/src/main/java/com/xly/agent/Intent.java b/src/main/java/com/xly/agent/Intent.java
new file mode 100644
index 0000000..642ace2
--- /dev/null
+++ b/src/main/java/com/xly/agent/Intent.java
@@ -0,0 +1,69 @@
+package com.xly.agent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 第 0 阶段「意图 + 实体」抽取结果(架构 §5 的 intent gate)。
+ *
+ *
由 {@code IntentService} 用受约束 JSON 一次得出:把一句自然语言压成
+ * {意图, 单据/实体类型, 带业务角色的实体清单, 还缺什么}。下游据此做**确定性路由**,
+ * 而不是让弱模型在 12 个工具里盲选——这是治理「理解错/选错工具」的核心。
+ */
+public class Intent {
+
+ /** 操作意图。 */
+ public static final String QUERY = "查询";
+ public static final String CREATE = "新增";
+ public static final String UPDATE = "修改";
+ public static final String DELETE = "删除";
+ public static final String EXAMINE = "审核";
+ public static final String CHAT = "闲聊";
+ public static final String UNCLEAR = "不清楚";
+
+ public static class Entity {
+ public String value;
+ public String role; // 客户/产品/物料/供应商/数量/尺寸/日期/金额/其他/未知
+ public Entity() {}
+ public Entity(String value, String role) { this.value = value; this.role = role; }
+ }
+
+ /** 写操作(修改/删除/审核)的专用槽位:比通用 entities 更贴合 propose* 工具的入参。 */
+ public static class WriteSlots {
+ public String entityType = ""; // 记录所属实体:客户/供应商/物料/产品/单据…
+ public String record = ""; // 要操作的记录名(公司名/物料名/单号)
+ public String field = ""; // 要修改的字段中文名(删除/审核留空)
+ public String newValue = ""; // 修改后的新值(删除/审核留空)
+ }
+
+ public String intent = UNCLEAR;
+ public String danju = ""; // 单据/实体类型,如 报价/客户/物料/销售订单
+ public List entities = new ArrayList<>();
+ public List missing = new ArrayList<>(); // 完成该意图还缺的关键信息
+
+ public boolean isWrite() {
+ return CREATE.equals(intent) || UPDATE.equals(intent)
+ || DELETE.equals(intent) || EXAMINE.equals(intent);
+ }
+
+ /** 取某业务角色的第一个实体值(如「客户」「产品」),没有则返回 null。 */
+ public String firstValueByRole(String role) {
+ for (Entity e : entities) {
+ if (e != null && role.equals(e.role) && e.value != null && !e.value.isBlank()) {
+ return e.value.trim();
+ }
+ }
+ return null;
+ }
+
+ /** 所有实体值拼接(用于给下游 agent 做 grounding 描述)。 */
+ public String describeEntities() {
+ StringBuilder sb = new StringBuilder();
+ for (Entity e : entities) {
+ if (e == null || e.value == null || e.value.isBlank()) continue;
+ if (sb.length() > 0) sb.append(",");
+ sb.append(e.value).append("(").append(e.role == null ? "未知" : e.role).append(")");
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/com/xly/agent/ToolScope.java b/src/main/java/com/xly/agent/ToolScope.java
new file mode 100644
index 0000000..492eaf0
--- /dev/null
+++ b/src/main/java/com/xly/agent/ToolScope.java
@@ -0,0 +1,17 @@
+package com.xly.agent;
+
+/**
+ * 按意图收窄「本轮可见的工具集」(架构 §5 的 tool scoping)。
+ *
+ * 依据:单次 ReAct 的选工具准确率随工具数量上升而骤降(业界经验:50 个工具 ~94% → 200 个 ~64%)。
+ * 弱模型(qwen3:14b Q4)一次性面对 12 个语义重叠的工具时,频繁选错族(把「查询」当「新增」)。
+ * 于是先用意图门判类,再**只暴露该意图相关的 3-5 个工具**给 ReAct 执行——大幅降低选错概率。
+ */
+public enum ToolScope {
+ /** 读:findForms/kgSearch/readFormData/lookupRecord/queryData(+askUser/loadSkill)。 */
+ READ,
+ /** 写:collectForm/proposeCreate/Update/Delete/Examine(+lookupRecord 定位、askUser/loadSkill)。 */
+ WRITE,
+ /** 兜底:全部工具(意图门失败时用)。 */
+ FULL
+}
diff --git a/src/main/java/com/xly/config/AgentFactory.java b/src/main/java/com/xly/config/AgentFactory.java
index 4b5cf1e..00933dd 100644
--- a/src/main/java/com/xly/config/AgentFactory.java
+++ b/src/main/java/com/xly/config/AgentFactory.java
@@ -3,6 +3,7 @@ package com.xly.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xly.agent.AgentIdentity;
import com.xly.agent.ReActAgent;
+import com.xly.agent.ToolScope;
import com.xly.service.AuditService;
import com.xly.service.ErpClient;
import com.xly.service.FormResolverService;
@@ -54,8 +55,6 @@ public class AgentFactory {
private final SkillTool skillTool;
private final InteractionTool interactionTool;
- private volatile String cachedSystemPrompt;
-
public AgentFactory(@Qualifier("agentStreamingModel") OllamaStreamingChatModel streamingModel,
@Qualifier("sqlChatModel") OllamaChatModel sqlModel,
RedisChatMemoryStore memoryStore,
@@ -79,33 +78,43 @@ public class AgentFactory {
this.interactionTool = interactionTool;
}
- /** system prompt 是全局的(L1 域图 + Skill 摘要),构建一次后缓存。 */
- private String systemPrompt() {
- String p = cachedSystemPrompt;
- if (p == null) {
- synchronized (this) {
- if (cachedSystemPrompt == null) {
- cachedSystemPrompt = systemPromptService.buildSystemPrompt();
- }
- p = cachedSystemPrompt;
- }
- }
- return p;
+ /** 兜底:全部工具(意图门失败时用)。 */
+ public ReActAgent build(AgentIdentity identity) {
+ return build(identity, ToolScope.FULL);
}
- /** 用给定身份组装一个 ReAct agent(工具实例携带该身份的 token 与权限边界)。 */
- public ReActAgent build(AgentIdentity identity) {
- String systemPrompt = systemPrompt();
+ /**
+ * 按意图范围组装 ReAct agent:只暴露该范围相关的工具 + 对应版本的 system prompt。
+ *
+ *
收窄可见工具(3-5 个)显著提升弱模型的选工具准确率;{@code maxSequentialToolsInvocations}
+ * 作为循环护栏,杜绝「反复追问同一问题」这类失控 ReAct 循环。
+ */
+ public ReActAgent build(AgentIdentity identity, ToolScope scope) {
+ String systemPrompt = systemPromptService.buildPrompt(scope);
+
+ ErpReadTool readTool = new ErpReadTool(erp, jdbc, resolver, identity);
+ Object[] tools;
+ switch (scope) {
+ case READ:
+ tools = new Object[]{kgQueryTool, skillTool, interactionTool, readTool,
+ new QueryTool(sqlModel, jdbc, audit, identity, resolver)};
+ break;
+ case WRITE:
+ tools = new Object[]{skillTool, interactionTool, readTool,
+ new ProposeWriteTool(erp, jdbc, ops, mapper, identity, resolver),
+ new FormCollectTool(erp, jdbc, resolver, identity, mapper)};
+ break;
+ default: // FULL
+ tools = new Object[]{kgQueryTool, skillTool, interactionTool, readTool,
+ new ProposeWriteTool(erp, jdbc, ops, mapper, identity, resolver),
+ new QueryTool(sqlModel, jdbc, audit, identity, resolver),
+ new FormCollectTool(erp, jdbc, resolver, identity, mapper)};
+ }
+
return AiServices.builder(ReActAgent.class)
.streamingChatModel(streamingModel)
- .tools(
- kgQueryTool,
- skillTool,
- interactionTool,
- new ErpReadTool(erp, jdbc, resolver, identity),
- new ProposeWriteTool(erp, jdbc, ops, mapper, identity, resolver),
- new QueryTool(sqlModel, jdbc, audit, identity),
- new FormCollectTool(erp, jdbc, resolver, identity, mapper))
+ .tools(tools)
+ .maxSequentialToolsInvocations(8) // 循环护栏:防止 askUser/工具无限自我循环
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(30)
@@ -114,4 +123,14 @@ public class AgentFactory {
.systemMessageProvider(memoryId -> systemPrompt)
.build();
}
+
+ /** 供确定性「新增」路径直接构建 collectForm 表单(不经 LLM 工具选择)。 */
+ public FormCollectTool formCollectTool(AgentIdentity identity) {
+ return new FormCollectTool(erp, jdbc, resolver, identity, mapper);
+ }
+
+ /** 供确定性「修改/作废/审核…」路径直接调 proposeWrite(不经 LLM 工具选择)。 */
+ public ProposeWriteTool proposeWriteTool(AgentIdentity identity) {
+ return new ProposeWriteTool(erp, jdbc, ops, mapper, identity, resolver);
+ }
}
diff --git a/src/main/java/com/xly/service/ErpClient.java b/src/main/java/com/xly/service/ErpClient.java
index 7fb240f..502a0e8 100644
--- a/src/main/java/com/xly/service/ErpClient.java
+++ b/src/main/java/com/xly/service/ErpClient.java
@@ -194,6 +194,45 @@ public class ErpClient {
}
/**
+ * 作废 / 取消作废(复原)一条记录(ERP {@code /checkModel/updatebInvalid} → 存储过程 {@code Sp_Invalidation})。
+ * {@code cancel=false} → 作废(handleType=toVoid);{@code cancel=true} → 取消作废/复原(handleType=cancel)。
+ * 业务单据的“删除”应走这里(软作废,可复原),而非物理删。会话过期自动重登重试。
+ */
+ public JsonNode invalidForm(String authToken, String moduleId, String table, String billId, boolean cancel) {
+ JsonNode root = doInvalid(moduleId, table, billId, cancel, resolveToken(authToken));
+ if (root.path("code").asInt() == -2 && canRelogin(authToken)) {
+ login();
+ root = doInvalid(moduleId, table, billId, cancel, resolveToken(authToken));
+ }
+ return root;
+ }
+
+ private JsonNode doInvalid(String moduleId, String table, String billId, boolean cancel, String tok) {
+ try {
+ String url = baseUrl + "/checkModel/updatebInvalid?sModelsId=" + moduleId;
+ Map body = new LinkedHashMap<>();
+ body.put("sId", List.of(billId)); // 后端 sId 期望 List
+ body.put("sTableName", table);
+ body.put("handleType", cancel ? "cancel" : "toVoid");
+ body.put("sClientType", "PC");
+ body.put("sComputeName", "AI-Agent");
+ body.put("sIpAddress", "127.0.0.1");
+ body.put("sLanguage", "chinese");
+ String json = mapper.writeValueAsString(body);
+ HttpRequest req = HttpRequest.newBuilder(URI.create(url))
+ .header("Content-Type", "application/json;charset=UTF-8")
+ .header("Authorization", tok)
+ .timeout(Duration.ofSeconds(60))
+ .POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8))
+ .build();
+ HttpResponse resp = http.send(req, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
+ return mapper.readTree(resp.body());
+ } catch (Exception e) {
+ throw new RuntimeException("ERP 作废异常: " + e.getMessage(), e);
+ }
+ }
+
+ /**
* 委托 ERP 侧暂存执行器执行一条暂存写操作(架构 §10)。ERP 读共享库 {@code ai_op_queue} 行、以用户身份
* 执行并回写状态,返回 {@code {status, msg, billId}}。用户 token 必传(执行以用户身份进行)。
*/
diff --git a/src/main/java/com/xly/service/IntentService.java b/src/main/java/com/xly/service/IntentService.java
new file mode 100644
index 0000000..ae82851
--- /dev/null
+++ b/src/main/java/com/xly/service/IntentService.java
@@ -0,0 +1,159 @@
+package com.xly.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.xly.agent.Intent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 第 0 阶段意图门(intent gate):把一句用户话分类为 {意图, 单据类型, 带角色的实体, 缺失信息}。
+ *
+ * 只做**一件窄任务**,用受约束 JSON 解码({@link OllamaJsonClient})。实测在此形态下
+ * qwen3:14b 对包括「报价纸盒→纸盒是产品而非客户」「给苏州华为报价彩盒→客户+产品分离」
+ * 「有多少个客户→查询」在内的样本 8/8 正确、~2-3s/次。相较之下,让同一个模型在 12 个工具的
+ * 单次 ReAct 里同时判意图/选工具/编参数则频繁出错(把查询错当新增、更新流程死循环等)。
+ *
+ *
失败(模型不可达/JSON 异常)时返回 intent=不清楚 的空结果,调用方降级到通用 agent,绝不中断。
+ */
+@Service
+public class IntentService {
+
+ private static final Logger log = LoggerFactory.getLogger(IntentService.class);
+
+ private static final String SYSTEM =
+ "你是印刷/包装 ERP 的意图与实体抽取器。给你一句用户话,判断其操作意图"
+ + "(查询/新增/修改/删除/审核/闲聊/不清楚)、涉及的单据或实体类型,并抽取句中出现的实体及其业务角色"
+ + "(客户/产品/物料/供应商/数量/尺寸/日期/金额)。规则:"
+ + "①『报价X』『给X报价』里,若 X 是物品(如 纸盒/彩盒/画册/小册子)则 X 是**产品**,不是客户;"
+ + "只有明确出现公司/客户名(如『给苏州华为报价』)时才把它当**客户**。"
+ + "②对某个产品/规格问价——『多少钱』『什么价』『报个价』『报价』『价格是多少』——在印刷/包装行业"
+ + "**通常是要新建一张报价单**(价格由系统核价算出,系统里没有该定制活的现成价),intent=**新增**、单据=**报价**;"
+ + "只有明确查看/统计**已存在**的单据(给了单号,或说『查已有报价/列出报价单/上月报价额』)才是查询。"
+ + "③『有多少X/查一下X/X的电话是多少』这类问既有数据是**查询**,不是新增。"
+ + "④只说一个名词(如『报价』『纸盒』)而无动作时,intent=不清楚。"
+ + "⑤missing 里列出完成该意图还缺的关键信息(如 修改缺『具体记录』『新值』)。"
+ + "只输出 JSON,不要解释。";
+
+ private final OllamaJsonClient llm;
+
+ public IntentService(OllamaJsonClient llm) {
+ this.llm = llm;
+ }
+
+ /** 意图门主入口。utterance 为空或模型失败时返回不清楚。 */
+ public Intent classify(String utterance) {
+ Intent out = new Intent();
+ if (utterance == null || utterance.isBlank()) {
+ return out;
+ }
+ JsonNode n = llm.completeJson(SYSTEM, utterance.trim(), schema());
+ if (n == null) {
+ log.warn("intent classify fell back to UNCLEAR (model unavailable)");
+ return out;
+ }
+ String intent = n.path("intent").asText(Intent.UNCLEAR);
+ out.intent = normalizeIntent(intent);
+ out.danju = n.path("danju").asText("");
+ JsonNode es = n.path("entities");
+ if (es.isArray()) {
+ for (JsonNode e : es) {
+ String v = e.path("value").asText("");
+ String r = e.path("role").asText("未知");
+ if (!v.isBlank()) {
+ out.entities.add(new Intent.Entity(v, r));
+ }
+ }
+ }
+ JsonNode ms = n.path("missing");
+ if (ms.isArray()) {
+ for (JsonNode m : ms) {
+ String v = m.asText("");
+ if (!v.isBlank()) out.missing.add(v);
+ }
+ }
+ return out;
+ }
+
+ private static final String WRITE_SYSTEM =
+ "你是 ERP 写操作的槽位抽取器。从用户话中抽取要操作的记录及改动:"
+ + "record=要操作的那条记录的名称(公司名/客户名/物料名/单号等);"
+ + "entityType=该记录属于哪类实体(客户/供应商/物料/产品/单据;拿不准就填 客户);"
+ + "field=要修改的字段中文名(如 电话/地址/简称/备注),删除或审核时留空;"
+ + "newValue=改成什么新值,删除或审核时留空。只输出 JSON,不要解释。";
+
+ /** 抽取写操作槽位(修改/删除/审核用)。失败返回空槽位。 */
+ public Intent.WriteSlots extractWrite(String utterance) {
+ Intent.WriteSlots w = new Intent.WriteSlots();
+ if (utterance == null || utterance.isBlank()) {
+ return w;
+ }
+ JsonNode n = llm.completeJson(WRITE_SYSTEM, utterance.trim(), writeSchema());
+ if (n == null) {
+ return w;
+ }
+ w.entityType = n.path("entityType").asText("");
+ w.record = n.path("record").asText("");
+ w.field = n.path("field").asText("");
+ w.newValue = n.path("newValue").asText("");
+ return w;
+ }
+
+ private Map writeSchema() {
+ Map props = new LinkedHashMap<>();
+ props.put("entityType", Map.of("type", "string"));
+ props.put("record", Map.of("type", "string"));
+ props.put("field", Map.of("type", "string"));
+ props.put("newValue", Map.of("type", "string"));
+ Map schema = new LinkedHashMap<>();
+ schema.put("type", "object");
+ schema.put("properties", props);
+ schema.put("required", List.of("record"));
+ return schema;
+ }
+
+ private static String normalizeIntent(String s) {
+ if (s == null) return Intent.UNCLEAR;
+ s = s.trim();
+ switch (s) {
+ case Intent.QUERY:
+ case Intent.CREATE:
+ case Intent.UPDATE:
+ case Intent.DELETE:
+ case Intent.EXAMINE:
+ case Intent.CHAT:
+ case Intent.UNCLEAR:
+ return s;
+ default:
+ return Intent.UNCLEAR;
+ }
+ }
+
+ private Map schema() {
+ Map entityProps = new LinkedHashMap<>();
+ entityProps.put("value", Map.of("type", "string"));
+ entityProps.put("role", Map.of("type", "string",
+ "enum", List.of("客户", "产品", "物料", "供应商", "数量", "尺寸", "日期", "金额", "其他", "未知")));
+ Map entityItem = new LinkedHashMap<>();
+ entityItem.put("type", "object");
+ entityItem.put("properties", entityProps);
+ entityItem.put("required", List.of("value", "role"));
+
+ Map props = new LinkedHashMap<>();
+ props.put("intent", Map.of("type", "string",
+ "enum", List.of("查询", "新增", "修改", "删除", "审核", "闲聊", "不清楚")));
+ props.put("danju", Map.of("type", "string"));
+ props.put("entities", Map.of("type", "array", "items", entityItem));
+ props.put("missing", Map.of("type", "array", "items", Map.of("type", "string")));
+
+ Map schema = new LinkedHashMap<>();
+ schema.put("type", "object");
+ schema.put("properties", props);
+ schema.put("required", List.of("intent", "danju", "entities"));
+ return schema;
+ }
+}
diff --git a/src/main/java/com/xly/service/OllamaJsonClient.java b/src/main/java/com/xly/service/OllamaJsonClient.java
new file mode 100644
index 0000000..4b9367b
--- /dev/null
+++ b/src/main/java/com/xly/service/OllamaJsonClient.java
@@ -0,0 +1,80 @@
+package com.xly.service;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.xly.util.OkHttpUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 直连 Ollama /api/chat 的**受约束 JSON**补全(constrained decoding)。
+ *
+ * 为什么绕开 LangChain4j:Ollama 的 {@code format=} 会用 XGrammar 做语法约束解码,
+ * 保证输出**一定**是 schema 合法的 JSON(违反 schema 的 token 概率直接置 0)——这是根治「把产品名塞进客户字段」
+ * 这类**槽位/参数幻觉**的关键手段。实测 qwen3:14b 在此模式下意图/实体抽取 8/8 正确、~2-3s/次(think=false)。
+ *
+ * 用于两处「窄而稳」的推理子任务:{@link IntentService}(意图+实体分类)与
+ * {@link SlotFillService}(按表单真实字段做受约束的槽位填充)。主对话/查询仍走 LangChain4j 工具循环。
+ */
+@Service
+public class OllamaJsonClient {
+
+ private static final Logger log = LoggerFactory.getLogger(OllamaJsonClient.class);
+
+ private final ObjectMapper mapper;
+
+ @Value("${langchain4j.ollama.base-url}")
+ private String baseUrl;
+
+ @Value("${langchain4j.ollama.chat-model-name}")
+ private String model;
+
+ public OllamaJsonClient(ObjectMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ /**
+ * 受约束 JSON 补全:think=false(快且稳定,实测无精度损失)、低温度、format=schema、非流式。
+ *
+ * @param system 系统提示(角色 + 抽取规则)
+ * @param user 用户话
+ * @param schema JSON Schema(Map 结构,直接序列化进 format)
+ * @return 解析后的 JsonNode;失败返回 null(调用方须降级处理,绝不因它中断主流程)
+ */
+ public JsonNode completeJson(String system, String user, Map schema) {
+ try {
+ Map body = new LinkedHashMap<>();
+ body.put("model", model);
+ body.put("stream", false);
+ body.put("think", false);
+ body.put("format", schema);
+ body.put("messages", List.of(
+ Map.of("role", "system", "content", system),
+ Map.of("role", "user", "content", user)));
+ Map opts = new LinkedHashMap<>();
+ opts.put("temperature", 0.1);
+ opts.put("top_p", 0.9);
+ opts.put("num_ctx", 8192); // 足够容纳字段清单/历史,避免长上下文时工具/JSON 退化
+ body.put("options", opts);
+
+ String json = mapper.writeValueAsString(body);
+ String resp = OkHttpUtil.getInstance(10, 120, 30).postJson(baseUrl + "/api/chat", json);
+ JsonNode root = mapper.readTree(resp);
+ String content = root.path("message").path("content").asText("");
+ if (content.isBlank()) {
+ log.warn("ollama json completion: empty content");
+ return null;
+ }
+ return mapper.readTree(content);
+ } catch (Exception e) {
+ log.warn("ollama json completion failed: {}", e.getMessage());
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/xly/service/SlotFillService.java b/src/main/java/com/xly/service/SlotFillService.java
new file mode 100644
index 0000000..bdd9377
--- /dev/null
+++ b/src/main/java/com/xly/service/SlotFillService.java
@@ -0,0 +1,200 @@
+package com.xly.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.xly.agent.Intent;
+import org.springframework.stereotype.Service;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * 新增单据的**确定性槽位映射**:把意图门已抽好的「带角色实体」+ 正则解析的尺寸/数量,
+ * 按表单真实字段落位,产出 collectForm 的预填 JSON。
+ *
+ * 为什么不用 LLM 做这步:实测 qwen3:14b 能可靠区分「产品 vs 客户」(意图门已给出角色),
+ * 但对「长和宽50cm,高5cm」这类**尺寸拆分**始终出错(塞进备注/单价)。这类窄任务用 Java 正则 +
+ * 角色映射远比让模型受约束填槽可靠——把不确定的判断交给能确定处理的代码,是治『不智能』的关键手法之一。
+ */
+@Service
+public class SlotFillService {
+
+ private final ObjectMapper mapper;
+
+ public SlotFillService(ObjectMapper mapper) {
+ this.mapper = mapper;
+ }
+
+ // 标注式尺寸:"长和宽50"、"高5cm"、"长:50"
+ private static final Pattern DIM_LABELED =
+ Pattern.compile("([长宽高厚深](?:[和与、,,及/][长宽高厚深])*)\\s*[::是为]?\\s*(\\d+(?:\\.\\d+)?)");
+ // 位置式尺寸:"50*30*5"、"50x30x5"、"50×30×5"
+ private static final Pattern DIM_POS =
+ Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*[*xX×]\\s*(\\d+(?:\\.\\d+)?)(?:\\s*[*xX×]\\s*(\\d+(?:\\.\\d+)?))?");
+
+ /**
+ * 依据意图门的角色实体 + 正则,确定性地构建新增表单的预填字段(中文字段名 -> 值)。
+ *
+ * @param utterance 用户原话(用于解析尺寸)
+ * @param entity 单据/实体类型(如 报价 / 客户)
+ * @param fields {@code FormResolverService.businessFields}(含 label/fk/type)
+ * @param it 意图门结果(提供带角色的实体)
+ */
+ public Map buildCreateFields(String utterance, String entity,
+ List