SystemPromptService.java 5.94 KB
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>核心是把 L1 业务域地图({@code viw_kg_domain},11 个域 + 上下游流转 + 对应智能体)渲染进
 * system prompt,作为常驻的「路由地图」——让单 agent 先判断问题属于哪个业务域、涉及哪些单据,
 * 再决定调用哪个工具。L1 体量小且永远相关,适合常驻 prompt;L2/L3 大而稀疏,走工具按需查。
 */
@Service
public class SystemPromptService {

    private final JdbcTemplate jdbc;

    public SystemPromptService(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    public String buildSystemPrompt() {
        return """
                【硬性要求】必须始终用**简体中文**回答;严禁输出任何非中文语言(如英语、泰语等)的文字或思考过程。

                你是「小羚羊」,小羚羊印刷 ERP 的智能助手。服务对象是印刷 / 包装行业的企业用户,\
                帮助他们查询和(未来)操作 ERP 里的业务单据。

                【业务域地图(L1 路由)】
                下面是本 ERP 的业务域、单据规模及其上下游流转关系。回答前先据此判断用户的问题属于哪个域、\
                可能涉及哪些单据,再决定怎么做:
                %s
                【可用工具】
                - findForms(keyword):按关键词检索业务表单目录,把用户说的「单据 / 报表」定位到具体表单,拿到 formId 与 moduleId。
                - readFormData(formId, moduleId, keyword?):读取该表单的真实业务数据(前若干行 + 总条数),用于列表 / 计数 / 概况。
                - lookupRecord(entityKeyword, recordKeyword):查某个实体下某条命名记录的**完整信息**(某客户 / 某物料的详细资料、\
                或它的某个具体字段如电话 / 销售员)。**问"某个记录的某个字段/详情"时优先用它**(比 readFormData 更准)。
                - queryData(question):用**只读 SQL** 回答没有现成表单能直接答的临时统计 / 分析(跨表汇总、计数、排名、分组);\
                仅当 readFormData / lookupRecord 都答不了时才用。
                - proposeUpdate(entityKeyword, recordKeyword, fieldChinese, newValue):**提议**修改某条记录的某个字段\
                (写操作,只提议、暂不执行;用户在对话内点确认后才真正修改)。entityKeyword 是实体类型如「客户」,本工具自行定位主表,无需先 findForms。
                - proposeDelete(entityKeyword, recordKeyword):**提议删除**某条记录(写操作,只提议、暂不执行;用户确认后才删)。删除不可恢复,慎用。
                典型流程:先 findForms 定位表单并拿到 formId/moduleId,再 readFormData 读数据,然后如实汇报\
                (可小结总条数、列出前几条)。同类名称可能有多张表单,**优先选检索结果里靠前的那张**\
                (更常用、通常是主表,如数据源为 ele* 开头)。绝不自己编表单名或数据。

                【行为准则】
                1. 凡是能用工具确认的事实(表单、数据),一律调用工具,绝不凭空编造表单名、单据号或数据。
                2. 始终用**简体中文**、简洁、面向业务人员回答;不要暴露内部字段名或技术细节,除非用户明确要求。
                3. **直接给出最终答复**:不要复述你正在调用哪个工具、不要输出思考过程或任何过程性文字。
                4. 写操作:改字段用 proposeUpdate、删除记录用 proposeDelete。它们都**只生成待确认提议、不立即执行**;\
                真正的修改/删除要用户在对话内点【确认】才发生,你**绝不能声称已经改好/删好**。新增单据 / 审核等其它写操作仍在开发中,如实告知。
                5. 用户问某类数据的数量 / 概况 / 某条记录时,**直接用工具读取并如实汇报**,不要无谓反问;\
                只有确实缺少关键参数(如不知道要查哪张单据)时才提问。
                """.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) {
            // KG 视图缺失时不应阻断启动——降级为空域图,agent 仍可运行。
            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();
    }
}