Commit ac642a35612e5bf48a5024fef39dff6d6b2cfcc1
1 parent
1057c7df
fix(quote): don't guess customer; collectForm-first + prefill known values
Bug: '我要报价纸盒' -> agent put product '纸盒' into 客户名称, FK-lookup failed, then asked to CREATE a customer named 纸盒. - System prompt rule 6: 新增报价 ALWAYS collectForm first; never map product/spec/盒型 to 客户名称; unknown customer -> let user pick from dropdown, never invent or offer to create. - collectForm(entityKeyword, knownFieldsJson?): pre-fills the form from what the user already said (产品/数量/尺寸); customer/product render as dropdowns to pick real records. - FK-not-found message no longer implies creating (was '请确认是否已存在' -> LLM read as 'create it?'); now '不是系统里已有的X,请从下拉选真实存在的X,不要新建'. Verified: '我要报价纸盒,1000个,长和宽50cm,高5cm' -> collectForm with 产品名称=纸盒(prefilled), 长/宽=50/高=5/数量=1000(prefilled), 客户名称=empty dropdown (no more guessing/create-customer).
Showing
3 changed files
with
57 additions
and
7 deletions
src/main/java/com/xly/service/SystemPromptService.java
| ... | ... | @@ -48,9 +48,12 @@ public class SystemPromptService { |
| 48 | 48 | - queryData(question):用**只读 SQL** 回答没有现成表单能直接答的临时统计 / 分析(跨表汇总、计数、排名、分组);\ |
| 49 | 49 | 仅当 readFormData / lookupRecord 都答不了时才用。 |
| 50 | 50 | - askUser(question, options?):意图不明或需二选一时,向用户提一个带选项的小问题;问完即等用户回答。 |
| 51 | - - collectForm(entityKeyword):新增字段较多的单据(如报价)时,弹一张表单让用户一次填齐;提交后你再用 proposeCreate。 | |
| 51 | + - collectForm(entityKeyword, knownFieldsJson?):新增字段较多的单据(尤其**报价**)时,弹一张表单让用户一次填齐。\ | |
| 52 | + 把用户已说的信息作为 knownFieldsJson 预填(字段中文名->值,如 {"产品名称":"纸盒","数量":"1000","长(L)":"50"}),\ | |
| 53 | + 客户/产品/物料等会渲染成下拉让用户从真实数据里选。**新增报价一律先用它**,提交后你再用 proposeCreate。 | |
| 52 | 54 | - proposeCreate(entityKeyword, fieldsJson):**提议新增**一条记录(写操作,只提议、暂不执行;用户确认后才新增)。\ |
| 53 | - fieldsJson 是已知字段的 JSON(字段中文名->值),如 {"客户名称":"常州测试公司"};主键与必填项会自动补齐。 | |
| 55 | + fieldsJson 是已知字段的 JSON(字段中文名->值),如 {"客户名称":"常州测试公司"};主键与必填项会自动补齐。\ | |
| 56 | + **仅当字段(尤其客户/产品)都由用户明确给出、无需猜测时才直接用它**;否则先 collectForm。 | |
| 54 | 57 | - proposeUpdate(entityKeyword, recordKeyword, fieldChinese, newValue):**提议**修改某条记录的某个字段(写操作,只提议、暂不执行)。 |
| 55 | 58 | - proposeDelete(entityKeyword, recordKeyword):**提议删除**某条记录(写操作,只提议、暂不执行)。删除不可恢复,慎用。 |
| 56 | 59 | - proposeExamine(entityKeyword, recordKeyword):**提议审核/过账**某张单据(写操作,只提议、暂不执行)。审核有业务后果,慎用。 |
| ... | ... | @@ -67,6 +70,9 @@ public class SystemPromptService { |
| 67 | 70 | 它们都**只生成待确认提议、不立即执行**;真正的写入要用户在对话内点【确认】才发生,你**绝不能声称已经完成**。 |
| 68 | 71 | 5. 用户问某类数据的数量 / 概况 / 某条记录时,**直接用工具读取并如实汇报**,不要无谓反问;\ |
| 69 | 72 | 只有确实缺少关键参数(如不知道要查哪张单据)时才用 askUser 提问。 |
| 73 | + 6. **新增报价**("报价X"/"给X报价"/"新增报价")**一律先 collectForm**,把用户说的产品/数量/尺寸作为 knownFieldsJson 预填,\ | |
| 74 | + 客户/产品让用户在下拉里选真实数据。**绝不要把产品名、盒型、规格猜成客户名称**——"报价纸盒"里"纸盒"是产品不是客户;\ | |
| 75 | + 客户未知就交给表单让用户选,不要自己编、也不要因为找不到就提议新建客户。 | |
| 70 | 76 | """.formatted(renderDomainMap(), renderSkills()); |
| 71 | 77 | } |
| 72 | 78 | ... | ... |
src/main/java/com/xly/tool/FormCollectTool.java
| ... | ... | @@ -9,6 +9,8 @@ import dev.langchain4j.agent.tool.Tool; |
| 9 | 9 | import org.springframework.jdbc.core.JdbcTemplate; |
| 10 | 10 | |
| 11 | 11 | import java.util.ArrayList; |
| 12 | +import java.util.HashMap; | |
| 13 | +import java.util.Iterator; | |
| 12 | 14 | import java.util.LinkedHashMap; |
| 13 | 15 | import java.util.List; |
| 14 | 16 | import java.util.Map; |
| ... | ... | @@ -42,12 +44,18 @@ public class FormCollectTool { |
| 42 | 44 | this.mapper = mapper; |
| 43 | 45 | } |
| 44 | 46 | |
| 45 | - @Tool("在对话里弹出一张 ERP 表单让用户一次性填齐多个字段(而不是逐个追问)。用于字段较多的新建/录入场景" | |
| 46 | - + "(如新建报价、新建客户)。入参 = 实体/单据类型(如 报价 / 客户)。用户填完提交后,你再用 proposeCreate 生成待确认的新增。") | |
| 47 | - public String collectForm(@P("要新建的实体/单据类型,如 报价 / 客户 / 物料") String entityKeyword) { | |
| 47 | + @Tool("在对话里弹出一张 ERP 表单让用户一次性填齐多个字段(而不是逐个追问、更不是自己猜)。用于字段较多的新建/录入场景" | |
| 48 | + + "(尤其**新建报价**)。entityKeyword = 单据类型(如 报价 / 客户);knownFieldsJson = 用户已说的字段(中文名->值)用于预填" | |
| 49 | + + "(如 {\"产品名称\":\"纸盒\",\"数量\":\"1000\",\"长(L)\":\"50\"})。客户/产品/物料会渲染成下拉让用户从真实数据里选。" | |
| 50 | + + "用户填完提交后,你再用 proposeCreate 生成待确认的新增。") | |
| 51 | + public String collectForm( | |
| 52 | + @P("要新建的实体/单据类型,如 报价 / 客户 / 物料") String entityKeyword, | |
| 53 | + @P(value = "可选:用户已说的字段 JSON(中文名->值),用于预填表单,如 {\"产品名称\":\"纸盒\",\"数量\":\"1000\"}", | |
| 54 | + required = false) String knownFieldsJson) { | |
| 48 | 55 | if (entityKeyword == null || entityKeyword.isBlank()) { |
| 49 | 56 | return err("缺少实体类型。"); |
| 50 | 57 | } |
| 58 | + Map<String, String> known = parseKnown(knownFieldsJson); | |
| 51 | 59 | Map<String, Object> form = resolver.resolveMasterForm(entityKeyword.trim()); |
| 52 | 60 | if (form == null) { |
| 53 | 61 | return err("找不到「" + entityKeyword + "」对应的可新建表单。"); |
| ... | ... | @@ -93,6 +101,10 @@ public class FormCollectTool { |
| 93 | 101 | if (hint != null && !String.valueOf(hint).isBlank()) { |
| 94 | 102 | f.put("hint", String.valueOf(hint)); |
| 95 | 103 | } |
| 104 | + String pre = known.get(normLabel(String.valueOf(f.get("label")))); | |
| 105 | + if (pre != null && !pre.isBlank()) { | |
| 106 | + f.put("default", pre); // 预填用户已说的值 | |
| 107 | + } | |
| 96 | 108 | fields.add(f); |
| 97 | 109 | } |
| 98 | 110 | |
| ... | ... | @@ -204,6 +216,34 @@ public class FormCollectTool { |
| 204 | 216 | return o == null ? null : o.toString(); |
| 205 | 217 | } |
| 206 | 218 | |
| 219 | + /** 解析预填 JSON(中文名->值),键归一化(去括号/空白)。 */ | |
| 220 | + private Map<String, String> parseKnown(String json) { | |
| 221 | + Map<String, String> m = new HashMap<>(); | |
| 222 | + if (json == null || json.isBlank()) { | |
| 223 | + return m; | |
| 224 | + } | |
| 225 | + try { | |
| 226 | + com.fasterxml.jackson.databind.JsonNode n = mapper.readTree(json.trim()); | |
| 227 | + Iterator<Map.Entry<String, com.fasterxml.jackson.databind.JsonNode>> it = n.fields(); | |
| 228 | + while (it.hasNext()) { | |
| 229 | + Map.Entry<String, com.fasterxml.jackson.databind.JsonNode> e = it.next(); | |
| 230 | + String v = e.getValue().asText(""); | |
| 231 | + if (v != null && !v.isBlank()) { | |
| 232 | + m.put(normLabel(e.getKey()), v); | |
| 233 | + } | |
| 234 | + } | |
| 235 | + } catch (Exception ignore) { | |
| 236 | + } | |
| 237 | + return m; | |
| 238 | + } | |
| 239 | + | |
| 240 | + private static String normLabel(String s) { | |
| 241 | + if (s == null) { | |
| 242 | + return ""; | |
| 243 | + } | |
| 244 | + return s.replaceAll("[((].*?[))]", "").replaceAll("\\s+", "").trim(); | |
| 245 | + } | |
| 246 | + | |
| 207 | 247 | private String err(String msg) { |
| 208 | 248 | Map<String, Object> m = new LinkedHashMap<>(); |
| 209 | 249 | m.put("error", msg); | ... | ... |
src/main/java/com/xly/tool/ProposeWriteTool.java
| ... | ... | @@ -252,7 +252,9 @@ public class ProposeWriteTool { |
| 252 | 252 | if (fk != null && !fk.isBlank()) { |
| 253 | 253 | String id = resolver.resolveFk(fk, v); // 外键:名称 -> id |
| 254 | 254 | if (id == null) { |
| 255 | - return err("找不到名为「" + v + "」的" + zh + ",请确认该" + zh + "是否已存在。"); | |
| 255 | + String ent = zh.replace("名称", ""); | |
| 256 | + return err("「" + v + "」不是系统里已有的" + ent + "。请让用户从下拉里选真实存在的" | |
| 257 | + + ent + ";**不要**因此新建" + ent + "。"); | |
| 256 | 258 | } |
| 257 | 259 | col.put(colName, id); |
| 258 | 260 | } else { |
| ... | ... | @@ -421,7 +423,9 @@ public class ProposeWriteTool { |
| 421 | 423 | if (fk != null && !fk.isBlank()) { |
| 422 | 424 | String id = resolver.resolveFk(fk, v); |
| 423 | 425 | if (id == null) { |
| 424 | - return err("找不到名为「" + v + "」的" + zh + ",请确认是否已存在。"); | |
| 426 | + String ent = zh.replace("名称", ""); | |
| 427 | + return err("「" + v + "」不是系统里已有的" + ent + "。请用 collectForm 让用户从下拉里选真实存在的" | |
| 428 | + + ent + ";**不要**因此新建" + ent + ",也不要把产品/规格当成客户。"); | |
| 425 | 429 | } |
| 426 | 430 | value = id; |
| 427 | 431 | if ("sCustomerId".equals(colName)) { | ... | ... |