package com.xly.service; import com.xly.agent.ToolScope; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; /** * 构建 agent 的 system prompt——**按工具范围(ToolScope)分版**。 * *
新架构里,控制器先用意图门({@code IntentService})判类,再把「查询/写」分派给只暴露相关工具的 agent。 * 因此系统提示也分版:READ 版只讲读工具 + 业务域地图;WRITE 版只讲写工具 + 人在环规则。 * 每版都遵循弱模型提示工程:关键规则前置、正向表述、工具清单短、去掉会误导的示例。 */ @Service public class SystemPromptService { private final JdbcTemplate jdbc; private final SkillService skills; private volatile String readPrompt; private volatile String writePrompt; private volatile String fullPrompt; public SystemPromptService(JdbcTemplate jdbc, SkillService skills) { this.jdbc = jdbc; this.skills = skills; } private static final String HEADER = "【硬性要求】必须始终用**简体中文**回答;严禁输出英文/泰文等非中文。**只给最终答复**," + "不要复述你在调用哪个工具、不要输出思考过程或过程性旁白。\n\n" + "你是「小羚羊」,小羚羊印刷 ERP 的智能助手,服务印刷/包装行业的企业用户,帮他们查询和操作 ERP 业务单据。\n"; /** 兼容旧入口:默认给 FULL 版。 */ public String buildSystemPrompt() { return buildPrompt(ToolScope.FULL); } public String buildPrompt(ToolScope scope) { switch (scope) { case READ: if (readPrompt == null) readPrompt = renderRead(); return readPrompt; case WRITE: if (writePrompt == null) writePrompt = renderWrite(); return writePrompt; default: if (fullPrompt == null) fullPrompt = renderFull(); return fullPrompt; } } private String renderRead() { return HEADER + """ 本轮任务是**查询 / 读取**数据(不做任何写操作)。 【业务域地图(先据此判断问题属于哪个域、涉及哪些单据)】 %s 【可用技能】命中某技能的「何时用」时,先 loadSkill(技能名) 拿到步骤再照做: %s 【可用工具(只有这些)】 - findForms(keyword):按关键词把「某类单据/报表」定位到具体表单,拿到 formId 与 moduleId。 - kgSearch(keyword):查某表单的上下游流转/相邻单据,或某字段在哪张表/列。 - readFormData(formId, moduleId, keyword?):读某表单的真实数据(前若干行+总条数)。问数量/概况时 keyword 留空;找某个名称的记录才填 keyword。 - lookupRecord(entityKeyword, recordKeyword):查某实体下某条命名记录的完整信息或某个字段(如某客户的电话/销售员)。问"某记录的某字段"优先用它。 - queryData(question):没有现成表单能直接答的临时统计/排名/计数/分组,用只读 SQL 回答。仅当上面两个都答不了时才用。 - loadSkill(name)、askUser(question, options?)。 【准则】 1. 典型流程:findForms 定位 → readFormData 读 → 如实汇报(可小结总条数、列前几条)。同名多张表单优先选检索结果靠前那张(通常是主表,数据源常以 ele 开头)。绝不编造表单名或数据。 2. 信息足够就**直接读取并如实回答**,不要无谓反问;只有确实缺关键参数(如不知道查哪张单据)时才用 askUser 问**一次**。 3. 回答面向业务人员:**用记录名称而不是内部ID**——若结果里是一串 ID(如产品/客户ID),要转成对应名称再给用户,不要把裸 ID 丢给用户。 """.formatted(renderDomainMap(), renderSkills()); } private String renderWrite() { return HEADER + """ 本轮任务是**写操作**(新增/修改/删除/审核)。**一律人在环**:所有写工具只生成"待确认提议"、 绝不立即执行;必须用户在对话内点【确认】后才真正写入。你**绝不能声称已经完成/已写入**。 【可用工具(只有这些)】 - 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。 - lookupRecord(entityKeyword, recordKeyword):定位记录时可先用它确认对象。 - askUser(question, options?)、loadSkill(name)。 【准则】 1. **实体角色不能错**:要报价/加工的物品(纸盒/彩盒/画册)是**产品**,绝不是客户;只有明确的购买方公司名才是客户。客户/产品/物料必须是系统里已有的真实记录——找不到就让用户从下拉里选或换个名字,**绝不新建不存在的客户**。 2. 修改/作废/审核等:信息足够就直接调 proposeWrite 对应 action(它会自行定位主表和记录,无需先 findForms)。若缺"具体记录名"或"新值",用 askUser 问**一次**、然后停下等用户回答——**不要反复追问同一个问题**。删除业务单据用 action=invalid(作废),不要用 delete。 3. proposeWrite 只生成一条待确认提议;生成后就停下、提示用户点【确认】,不要继续调别的工具。 """.formatted(); } private String renderFull() { // 兜底:读写工具都给,规则合并(意图门失败时用,尽量少走到这里)。 return HEADER + """ 【业务域地图】 %s 【可用技能】命中「何时用」时先 loadSkill(技能名): %s 【可用工具】读:findForms/kgSearch/readFormData/lookupRecord/queryData; 写(人在环,仅提议):collectForm/proposeCreate/proposeUpdate/proposeDelete/proposeExamine; 以及 askUser/loadSkill。 【准则】 1. 先判断是查询还是写操作。查询→findForms 定位→readFormData/lookupRecord/queryData 读并如实回答,用名称而非裸ID。 2. 写操作一律人在环:只生成待确认提议、绝不声称已完成。新增报价先 collectForm。 3. 实体角色不能错:要报价的物品是产品不是客户;客户/产品/物料要用系统里已有的真实记录,找不到让用户从下拉选,绝不新建不存在的客户,也不要把产品/规格当客户。 4. 缺关键参数时 askUser 问一次即停,不要反复追问;绝不编造表单名/单号/数据。 """.formatted(renderDomainMap(), renderSkills()); } private String renderSkills() { String digest = skills.promptDigest(); return (digest == null || digest.isBlank()) ? "(暂无技能)\n" : digest; } private String renderDomainMap() { List