SystemPromptService.java 3.57 KB
package com.xly.service;

import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

/**
 * 构建 agent 的**唯一** system prompt:模板 {@code prompts/system.txt} + 业务域地图(DB,缓存一次)
 * + 技能索引({@link SkillService},目录模式下热更)。
 *
 * <p>所有轮次共用同一份提示 + 同一组固定工具(useSkill/findForms/readFormData/lookupRecord/
 * collectForm/proposeWrite/askUser):稳定前缀利于 KV-cache;流程知识在技能文本里,
 * 由模型按需 useSkill 载入,不再压进 prompt。基准脚本(bench/bench_ext.py --arch new)
 * 读取同一模板与技能文件,避免复刻漂移。
 */
@Service
public class SystemPromptService {

    private final JdbcTemplate jdbc;
    private final SkillService skills;

    private volatile String template;
    private volatile String domainMap;

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

    public String prompt() {
        String t = template;
        if (t == null) {
            t = loadTemplate();
            template = t;
        }
        String d = domainMap;
        if (d == null) {
            d = renderDomainMap();
            domainMap = d;
        }
        String index = skills.indexLines();
        if (index.isBlank()) { // 零技能优雅降级:索引空渲染,裸工具 ReAct 正常聊
            index = "(当前未配置任何技能,直接用下方工具完成任务。)\n";
        }
        return t.replace("{DOMAIN_MAP}", d).replace("{SKILL_INDEX}", index);
    }

    private static String loadTemplate() {
        try {
            return new String(new ClassPathResource("prompts/system.txt")
                    .getInputStream().readAllBytes(), StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new IllegalStateException("system prompt 模板缺失(prompts/system.txt)", e);
        }
    }

    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();
    }
}