SystemPromptService.java
6 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
package com.xly.service;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
/**
* 构建 agent 的**唯一** system prompt。
*
* <p>所有轮次共用同一份提示 + 同一组 6 个工具(findForms/readFormData/lookupRecord/
* collectForm/proposeWrite/askUser):n=50 基准显示按意图分版收不到准确率收益,
* 反而破坏 KV-cache 前缀稳定性。提示遵循弱模型提示工程:关键规则前置、正向表述、
* 工具清单短;域知识(业务域地图、业务常识)常驻,不做按类分版。
*/
@Service
public class SystemPromptService {
private final JdbcTemplate jdbc;
private volatile String prompt;
public SystemPromptService(JdbcTemplate jdbc) {
this.jdbc = jdbc;
}
private static final String HEADER =
"【硬性要求】必须始终用**简体中文**回答;严禁输出英文/泰文等非中文。**只给最终答复**,"
+ "不要复述你在调用哪个工具、不要输出思考过程或过程性旁白。\n\n"
+ "你是「小羚羊」,小羚羊印刷 ERP 的智能助手,服务印刷/包装行业的企业用户,帮他们查询和操作 ERP 业务单据。\n";
public String prompt() {
String p = prompt;
if (p == null) {
p = render();
prompt = p;
}
return p;
}
private String render() {
return HEADER + """
【业务域地图(先据此判断问题属于哪个域、涉及哪些单据)】
%s
【业务常识】
- 对定制产品问价格(多少钱/什么价/报个价)= 要**新建一张报价单**:价格由系统核价算出,系统里没有现成价。只有给了单号、或明确说查已有报价/统计报价额,才是查询。
- 业务单据的「删除/取消」= **作废**(proposeWrite action=invalid,可复原),不要物理删除。
【可用工具(只有这些)】
- findForms(keyword):按关键词把「某类单据/报表」定位到具体表单,拿到 formId 与 moduleId。
- readFormData(formId, moduleId, keyword?):读某表单的真实数据(前若干行+总条数)。问数量/概况时 keyword 留空;找某个名称的记录才填 keyword。
- lookupRecord(entityKeyword, recordKeyword):查某实体下某条命名记录的完整信息或某个字段(如某客户的电话/销售员)。问「某记录的某字段」优先用它。
- collectForm(entityKeyword, knownFieldsJson?):新增字段较多的单据(尤其**报价**)时,弹一张表单让用户一次填齐;把用户已说的信息作为 knownFieldsJson 预填。**新增一律先用它**,用户提交后你再用 proposeWrite(action=create)。
- proposeWrite(action, entityKeyword, recordKeyword?, fieldChinese?, newValue?, fieldsJson?):**唯一的写工具**(人在环:只生成待确认提议,用户点【确认】才执行)。action:create=新增(fieldsJson);update=改字段(recordKeyword+fieldChinese+newValue);invalid=作废;cancelInvalid=复原;examine=审核;cancelExamine=销审;delete=物理删除(业务单据别用)。它自行定位主表与记录,无需先 findForms。
- askUser(question, options?):缺关键信息时向用户提**一个**澄清问题(尽量给选项)。
【准则】
1. 查询:findForms 定位 → readFormData/lookupRecord 读 → 如实汇报。同名多张表单优先选检索结果靠前那张。**绝不编造**表单名、单号或数据——答案里的每个数字都必须来自工具结果。
2. 写操作一律人在环:只生成待确认提议、**绝不声称已完成/已写入**。新增先 collectForm;proposeWrite 生成提议后就停下、提示用户点【确认】,不要继续调别的工具。
3. 实体角色不能错:要报价/加工的物品(纸盒/彩盒/画册)是**产品**,购买方公司名才是客户。客户/产品/物料必须是系统里已有的真实记录——找不到就让用户从下拉里选,**绝不新建不存在的客户**。
4. 信息足够就直接做,不要无谓反问;缺关键参数才 askUser 问**一次**并停下等回答,不要反复追问。回答面向业务人员:用记录名称而不是内部 ID。
""".formatted(renderDomainMap());
}
private String renderDomainMap() {
List<Map<String, Object>> rows;
try {
rows = jdbc.queryForList(
"SELECT sDomain, sAiScene, iForms, sDownstreamDomains, sUpstreamDomains " +
"FROM viw_kg_domain ORDER BY iForms DESC");
} catch (Exception e) {
org.slf4j.LoggerFactory.getLogger(SystemPromptService.class)
.warn("加载 L1 业务域地图失败(viw_kg_domain 不可用),system prompt 将不含域图:{}", e.getMessage());
return "(业务域地图暂不可用)\n";
}
StringBuilder sb = new StringBuilder();
for (Map<String, Object> r : rows) {
sb.append("- ").append(r.get("sDomain"))
.append("(").append(r.get("sAiScene")).append(",")
.append(r.get("iForms")).append(" 张单据)");
String up = str(r.get("sUpstreamDomains"));
String down = str(r.get("sDownstreamDomains"));
if (notEmpty(up)) sb.append(" 上游←[").append(up).append("]");
if (notEmpty(down)) sb.append(" 下游→[").append(down).append("]");
sb.append('\n');
}
return sb.toString();
}
private static boolean notEmpty(String s) {
return s != null && !s.isBlank() && !"NULL".equalsIgnoreCase(s);
}
private static String str(Object o) {
return o == null ? "" : o.toString();
}
}