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 类), 单据类型, 带角色的实体, 缺失信息}。 * *
只做**一件窄任务**,用受约束 JSON 解码({@link LlmJsonClient})。提示只给**类定义** * (含有界的行业常识,如 定制品问价=新建报价),不写逐案纠错规则——错误种类无穷, * 按案例打补丁写不完;观察到的错例进回归基准(bench50)而不是提示。 * 具体写动作(update/invalid/examine…)由路由层从原话推出,不在门里细分。 * *
失败(模型不可达/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)");
out.failed = true; // 门失败 ≠ 真实闲聊:下游护栏不能因此关闭
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