IntentService.java
8.17 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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
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):把一句用户话分类为 {意图(4 类), 单据类型, 带角色的实体, 缺失信息}。
*
* <p>只做**一件窄任务**,用受约束 JSON 解码({@link LlmJsonClient})。提示只给**类定义**
* (含有界的行业常识,如 定制品问价=新建报价),不写逐案纠错规则——错误种类无穷,
* 按案例打补丁写不完;观察到的错例进回归基准(bench50)而不是提示。
* 具体写动作(update/invalid/examine…)由路由层从原话推出,不在门里细分。
*
* <p>失败(模型不可达/JSON 异常)时返回 intent=其他 的空结果,调用方降级到通用 agent,绝不中断。
*/
@Service
public class IntentService {
private static final Logger log = LoggerFactory.getLogger(IntentService.class);
private static final String SYSTEM =
"你是印刷/包装 ERP 的意图与实体抽取器。把一句用户话分类为四种意图之一,并抽取句中实体及其业务角色。\n"
+ "意图定义:\n"
+ "- 查询:查看、统计或查找系统里**已有**的数据/单据。\n"
+ "- 新增:要创建一张新单据或一条新记录。行业常识:对定制产品问价格(多少钱/什么价/报个价)"
+ "就是要**新建报价单**——价格由系统核价算出,没有现成价;只有给了单号或明确说查已有报价才算查询。\n"
+ "- 操作已有单据:对系统里已存在的单据/记录做修改、作废/删除、审核/反审核、复原。"
+ "只要动作属于这些,即使还没说具体哪条记录也算本类(缺的信息之后澄清)。\n"
+ "- 其他:闲聊、问候、与 ERP 业务无关,或只说名词、没有任何动作、无法判断想做什么。\n"
+ "实体角色:客户=购买方的公司/单位名;产品=要生产/加工/报价的物品(如纸盒、彩盒、画册);其余按字面。\n"
+ "danju=该意图针对的单据或实体类型:新增时=要新建的单据类型(如问价即 报价),"
+ "查询/操作时=要查或要操作的单据/实体(如 客户/销售订单);确实没有才留空。"
+ "missing=完成该意图还缺的关键信息。只输出 JSON,不要解释。";
private final LlmJsonClient llm;
public IntentService(LlmJsonClient llm) {
this.llm = llm;
}
/** 意图门主入口。utterance 为空或模型失败时返回 其他。 */
public Intent classify(String utterance) {
return classify(utterance, null);
}
/** 带会话状态摘要的分类:状态槽让「那张单子」这类跨轮指代可解。 */
public Intent classify(String utterance, String stateDigest) {
Intent out = new Intent();
if (utterance == null || utterance.isBlank()) {
return out;
}
JsonNode n = llm.completeJson(SYSTEM, withState(utterance, stateDigest), schema());
if (n == null) {
log.warn("intent classify fell back to 其他 (model unavailable)");
return out;
}
String intent = n.path("intent").asText(Intent.OTHER);
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) {
return extractWrite(utterance, null);
}
/** 带会话状态摘要的写槽位抽取(「把那张报价单作废」的 record 可从状态里补齐)。 */
public Intent.WriteSlots extractWrite(String utterance, String stateDigest) {
Intent.WriteSlots w = new Intent.WriteSlots();
if (utterance == null || utterance.isBlank()) {
return w;
}
JsonNode n = llm.completeJson(WRITE_SYSTEM, withState(utterance, stateDigest), 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 withState(String utterance, String stateDigest) {
String u = utterance.trim();
if (stateDigest == null || stateDigest.isBlank()) {
return u;
}
return "【会话状态】" + stateDigest + "\n【这句话】" + u;
}
private static String normalizeIntent(String s) {
if (s == null) return Intent.OTHER;
s = s.trim();
switch (s) {
case Intent.QUERY:
case Intent.CREATE:
case Intent.OPERATE:
case Intent.OTHER:
return s;
default:
return Intent.OTHER;
}
}
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;
}
}