IntentService.java
7.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
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):把一句用户话分类为 {意图, 单据类型, 带角色的实体, 缺失信息}。
*
* <p>只做**一件窄任务**,用受约束 JSON 解码({@link OllamaJsonClient})。实测在此形态下
* qwen3:14b 对包括「报价纸盒→纸盒是产品而非客户」「给苏州华为报价彩盒→客户+产品分离」
* 「有多少个客户→查询」在内的样本 8/8 正确、~2-3s/次。相较之下,让同一个模型在 12 个工具的
* 单次 ReAct 里同时判意图/选工具/编参数则频繁出错(把查询错当新增、更新流程死循环等)。
*
* <p>失败(模型不可达/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<String, Object> writeSchema() {
Map<String, Object> 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<String, Object> 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<String, Object> schema() {
Map<String, Object> entityProps = new LinkedHashMap<>();
entityProps.put("value", Map.of("type", "string"));
entityProps.put("role", Map.of("type", "string",
"enum", List.of("客户", "产品", "物料", "供应商", "数量", "尺寸", "日期", "金额", "其他", "未知")));
Map<String, Object> entityItem = new LinkedHashMap<>();
entityItem.put("type", "object");
entityItem.put("properties", entityProps);
entityItem.put("required", List.of("value", "role"));
Map<String, Object> 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<String, Object> schema = new LinkedHashMap<>();
schema.put("type", "object");
schema.put("properties", props);
schema.put("required", List.of("intent", "danju", "entities"));
return schema;
}
}