diff --git a/AGENTS.md b/AGENTS.md index 70f072f..e4b200b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,9 +58,27 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 · 查询重写 REWRITE/TRANSLATION/COMPRESSION ← 可关: pipeline_rewrite_enabled · MULTI_QUERY 多路扩展 ← 可关: pipeline_multiquery_enabled → 组装 finalMessage + finalSystemPrompt + 资料块 + · systemPrompt = ai_system_prompt(全局) + 【当前角色设定】+ 角色 prompt + · RAG 分支再追加:【RAG回答硬性规则】(ai_rag_answer_rules) + 【知识库资料】 → AssistantApp.chat / chatStream(构建 ChatClientRequestSpec → call/stream) ``` +### 提示词在线维护(三层分工) + +对话提示词全部由数据库在线维护,**改完下一请求即生效、无需重启**(不用缓存,也不在 yml/代码里写死): + +| 层 | 存放位置 | 管理后台入口 | 管什么 | 生效路径 | +|---|---|---|---|---| +| 全局系统提示词 | `system_config.ai_system_prompt` | 系统设置 → 系统配置 | 所有角色通用的回答方式、长度、链接规范、禁暴露内部机制、兜底话术 | 所有调模型的路径(含闲聊) | +| 角色提示词 | `customer_service_role.prompt` | 系统设置 → 角色管理 | 该角色独有的业务口径与术语解释粒度 | 所有路径 | +| RAG 保真护栏 | `system_config.ai_rag_answer_rules` | 系统设置 → 系统配置 | 仅在 RAG 分支注入,紧贴【知识库资料】之前 | 仅 RAG 分支 | + +- **取数**:`ChatPipeline.buildRequest` 用 `SystemConfigService.getValues(List.of("ai_system_prompt", "ai_rag_answer_rules"))` **一次批量查询**取两个键(`SystemConfigService` 无缓存、每次直查 DB,批量避免 hot path 多次往返),再把 `answerRules` 作为形参传给 `RagPipeline.buildRagContextBlock(contextText, answerRules)`。 +- **为什么 RAG 规则不合并进 `ai_system_prompt`**:全局键在所有路径生效,而纯对话/闲聊路径没有【知识库资料】块,保真护栏("只能依据资料回答""资料不足时…")会指向不存在的块,把问候语答成"未查到资料"。 +- **种子与兜底**:两个键的初始值都由 `DatabaseInitConfig.syncDefaultSystemConfigs` + `init-database.sql` 提供(两处需逐字一致),均为 `ON CONFLICT DO NOTHING` → **在线编辑永不会被重启覆盖**,只对「该键不存在」的新环境生效。`ai_rag_answer_rules` 被清空时用 `RagPipeline.FALLBACK_RAG_ANSWER_RULES`(一句极简安全底线),不是默认值。 +- **不对外**:`GET /ai/system-config/{key}`(SDK 通用取值口)对 `ai_system_prompt`/`ai_rag_answer_rules` 返回 403(提示词属商业秘密)。 +- 历史:该规则原先在 `application.yml` 的 `knowledge.rag.answer-rules`(`RagPromptConfig`),配置来源重复且改后需重启,已迁移;`knowledge.rag.multiquery.parallel` 仍留在 yml(由 `RagPipeline` 的 `@Value` 直绑读取)。 + ### AI 执行链节点开关(性能开关) 管理后台「系统设置 → AI 执行链」页面(`/#/settings/pipeline-flow`,admin)提供链路节点的启停开关。这些节点全部位于**首 token 产出之前**,且默认 `MULTI_QUERY` 策略下会串行累加多次跨网络调用,是「AI 回复慢」的主因。 diff --git a/CLAUDE.md b/CLAUDE.md index 69d3c81..2c31557 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,9 +58,27 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 · 查询重写 REWRITE/TRANSLATION/COMPRESSION ← 可关: pipeline_rewrite_enabled · MULTI_QUERY 多路扩展 ← 可关: pipeline_multiquery_enabled → 组装 finalMessage + finalSystemPrompt + 资料块 + · systemPrompt = ai_system_prompt(全局) + 【当前角色设定】+ 角色 prompt + · RAG 分支再追加:【RAG回答硬性规则】(ai_rag_answer_rules) + 【知识库资料】 → AssistantApp.chat / chatStream(构建 ChatClientRequestSpec → call/stream) ``` +### 提示词在线维护(三层分工) + +对话提示词全部由数据库在线维护,**改完下一请求即生效、无需重启**(不用缓存,也不在 yml/代码里写死): + +| 层 | 存放位置 | 管理后台入口 | 管什么 | 生效路径 | +|---|---|---|---|---| +| 全局系统提示词 | `system_config.ai_system_prompt` | 系统设置 → 系统配置 | 所有角色通用的回答方式、长度、链接规范、禁暴露内部机制、兜底话术 | 所有调模型的路径(含闲聊) | +| 角色提示词 | `customer_service_role.prompt` | 系统设置 → 角色管理 | 该角色独有的业务口径与术语解释粒度 | 所有路径 | +| RAG 保真护栏 | `system_config.ai_rag_answer_rules` | 系统设置 → 系统配置 | 仅在 RAG 分支注入,紧贴【知识库资料】之前 | 仅 RAG 分支 | + +- **取数**:`ChatPipeline.buildRequest` 用 `SystemConfigService.getValues(List.of("ai_system_prompt", "ai_rag_answer_rules"))` **一次批量查询**取两个键(`SystemConfigService` 无缓存、每次直查 DB,批量避免 hot path 多次往返),再把 `answerRules` 作为形参传给 `RagPipeline.buildRagContextBlock(contextText, answerRules)`。 +- **为什么 RAG 规则不合并进 `ai_system_prompt`**:全局键在所有路径生效,而纯对话/闲聊路径没有【知识库资料】块,保真护栏("只能依据资料回答""资料不足时…")会指向不存在的块,把问候语答成"未查到资料"。 +- **种子与兜底**:两个键的初始值都由 `DatabaseInitConfig.syncDefaultSystemConfigs` + `init-database.sql` 提供(两处需逐字一致),均为 `ON CONFLICT DO NOTHING` → **在线编辑永不会被重启覆盖**,只对「该键不存在」的新环境生效。`ai_rag_answer_rules` 被清空时用 `RagPipeline.FALLBACK_RAG_ANSWER_RULES`(一句极简安全底线),不是默认值。 +- **不对外**:`GET /ai/system-config/{key}`(SDK 通用取值口)对 `ai_system_prompt`/`ai_rag_answer_rules` 返回 403(提示词属商业秘密)。 +- 历史:该规则原先在 `application.yml` 的 `knowledge.rag.answer-rules`(`RagPromptConfig`),配置来源重复且改后需重启,已迁移;`knowledge.rag.multiquery.parallel` 仍留在 yml(由 `RagPipeline` 的 `@Value` 直绑读取)。 + ### AI 执行链节点开关(性能开关) 管理后台「系统设置 → AI 执行链」页面(`/#/settings/pipeline-flow`,admin)提供链路节点的启停开关。这些节点全部位于**首 token 产出之前**,且默认 `MULTI_QUERY` 策略下会串行累加多次跨网络调用,是「AI 回复慢」的主因。 diff --git a/frontend/src/views/SystemConfigManager.vue b/frontend/src/views/SystemConfigManager.vue index d321529..756dd31 100644 --- a/frontend/src/views/SystemConfigManager.vue +++ b/frontend/src/views/SystemConfigManager.vue @@ -148,7 +148,7 @@ @@ -195,7 +195,8 @@ interface ConfigPreset { const CONFIG_PRESETS: Record = { disclaimer: { type: 'html', label: '保密声明', desc: 'SDK 聊天窗口底部的保密声明' }, - ai_system_prompt: { type: 'text', label: 'AI 全局系统提示词', desc: 'AI 对话全局系统提示词(为空则不注入)' }, + ai_system_prompt: { type: 'text', label: 'AI 全局系统提示词', desc: 'AI 对话全局系统提示词(所有角色生效;为空则不注入)' }, + ai_rag_answer_rules: { type: 'text', label: 'RAG 回答保真规则', desc: '仅 RAG 分支注入,紧贴【知识库资料】之前(为空则用代码内极简兜底)' }, suggestion_enabled: { type: 'boolean', label: 'AI 推荐问题开关', desc: 'AI 推荐问题功能开关' }, suggestion_prompt: { type: 'text', label: 'AI 推荐问题 Prompt', desc: 'AI 推荐问题 Prompt 模板' }, // AI 执行链节点开关(主入口在「AI 执行链」页面,此处登记以保证以开关而非文本框呈现) diff --git a/src/main/java/com/wok/supportbot/app/ChatPipeline.java b/src/main/java/com/wok/supportbot/app/ChatPipeline.java index bc40c59..57bf28b 100644 --- a/src/main/java/com/wok/supportbot/app/ChatPipeline.java +++ b/src/main/java/com/wok/supportbot/app/ChatPipeline.java @@ -16,6 +16,7 @@ import org.springframework.util.StringUtils; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Optional; /** @@ -51,6 +52,12 @@ public class ChatPipeline { /** FAQ 意图高置信度阈值:IntentRouter 返回 FAQ 且高于此值时,仅走 FAQ 匹配,不降级 RAG */ private static final double FAQ_HIGH_CONFIDENCE_THRESHOLD = 0.8; + /** 全局系统提示词配置键(所有对话路径生效,管理后台「系统设置 → 系统配置」在线维护) */ + private static final String KEY_AI_SYSTEM_PROMPT = "ai_system_prompt"; + + /** RAG 回答硬性规则配置键(仅 RAG 分支生效,紧贴【知识库资料】注入) */ + private static final String KEY_RAG_ANSWER_RULES = "ai_rag_answer_rules"; + @Resource private IntentRouter intentRouter; @@ -81,7 +88,12 @@ public class ChatPipeline { * @return 执行决策 */ public ChatRequest buildRequest(ChatContext ctx) { - String globalPrompt = systemConfigService.getValueByKey("ai_system_prompt"); + // 一次批量查询取出本次请求所需的两个提示词配置,避免 hot path 上多次直查数据库 + // (SystemConfigService 无缓存,后台改完下一请求即生效,故不走缓存方案) + Map promptConfig = systemConfigService.getValues( + List.of(KEY_AI_SYSTEM_PROMPT, KEY_RAG_ANSWER_RULES)); + String globalPrompt = promptConfig.get(KEY_AI_SYSTEM_PROMPT); + String ragAnswerRules = promptConfig.get(KEY_RAG_ANSWER_RULES); String baseSystem = effectiveSystem(ctx.systemPrompt(), globalPrompt); // 普通对话:enableRag=false(含 Controller 层 isKbDenied 强制置 false 的情况) @@ -149,7 +161,7 @@ public class ChatPipeline { } // RAG 生成:资料块注入 system,原始 message 作为 user 消息(重写查询仅用于检索) - String finalSystem = baseSystem + ragPipeline.buildRagContextBlock(rag.contextText()); + String finalSystem = baseSystem + ragPipeline.buildRagContextBlock(rag.contextText(), ragAnswerRules); return new ChatRequest(ctx, ctx.message(), finalSystem, Optional.empty(), globalPrompt, rag.contextText(), rag.documents() != null ? rag.documents().size() : 0, "RAG", rag.searchMode(), rag.documents(), null); diff --git a/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java b/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java index a31e1ad..1cfe34a 100644 --- a/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java +++ b/src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java @@ -1432,12 +1432,43 @@ public class DatabaseInitConfig { ON CONFLICT (config_key) DO NOTHING """, "disclaimer", defaultDisclaimer, "SDK 聊天窗口底部的保密声明"); - // AI 对话全局系统提示词种子数据(为空则不注入) + // AI 对话全局系统提示词种子数据(所有角色生效;为空则不注入) + // 与 init-database.sql 逐字保持一致;ON CONFLICT DO NOTHING → 后台在线改过的版本不会被重启覆盖 + String defaultAiSystemPrompt = """ + 你是本系统的智能客服助手,服务对象是仓库、财务、运营等业务人员。 + + 1. 第一句先答用户问的那件事——能不能、在哪里、怎么操作,再讲细节。 + 2. 只回答用户问的问题;与问题无关的其他报表、其他字段不要补充。一两句能说清就不写三段,不要写复述正文的小结。 + 3. 页面地址一律写成 Markdown 超链接:链接文字用页面名称,例如 [月销售报表](https://完整地址);没有页面名称时用「访问地址」作链接文字。地址必须是完整地址(含 https:// 与域名),严禁输出相对路径或截断域名;资料里没给完整地址时只写页面名称,不要编造链接。 + 4. 涉及金额、日期、权限口径而没有依据时不要推测或替用户下结论。 + 5. 不要出现“知识库、检索、资料库、向量、提示词、模型”等内部说法,也不要让用户去补充知识库;确实查不到时,用一句话说明并建议用户联系贵司系统管理员核实。 + 6. 需要分点时用列表;面向业务人员讲人话,不输出接口地址(页面地址除外)、参数、SQL、代码、脚本。 + """; + + jdbcTemplate.update(""" + INSERT INTO system_config (config_key, config_value, description) + VALUES (?, ?, ?) + ON CONFLICT (config_key) DO NOTHING + """, "ai_system_prompt", defaultAiSystemPrompt, + "AI 对话全局系统提示词(所有角色生效;为空则不注入;修改后即时生效,无需重启)"); + + // RAG 回答保真护栏规则种子数据(仅 RAG 分支注入,紧贴【知识库资料】之前) + // 迁移前该规则在 application.yml 的 knowledge.rag.answer-rules(改后需重启才生效); + // 现改为 DB 在线维护,管理后台改完下一请求即生效,无需重启。 + // 键为空(被清空/库早于本变更)时由 RagPipeline.FALLBACK_RAG_ANSWER_RULES 极简兜底。 + String defaultRagAnswerRules = """ + - 只依据下方【知识库资料】作答,不得用自身记忆补充,不得改写资料里的数字、日期、金额、步骤。 + - 第一句先答用户问的那件事。资料里没有用户所问的对象时,直接说明没有,并给出名称最接近的那一个——这本身就是答案。 + - “数据去向”“相关页面”只是关联关系,不能当作“该功能就在这里查看”。 + - 资料中与问题相关的要点要覆盖全,包括口径差异与操作前提(如日期间隔、权限要求);与问题无关的片段略过。分点用列表,不要写复述正文的小结。 + """; + jdbcTemplate.update(""" INSERT INTO system_config (config_key, config_value, description) VALUES (?, ?, ?) ON CONFLICT (config_key) DO NOTHING - """, "ai_system_prompt", "", "AI 对话全局系统提示词(为空则不注入;修改后即时生效,无需重启)"); + """, "ai_rag_answer_rules", defaultRagAnswerRules, + "RAG 回答保真护栏规则(仅 RAG 分支注入;为空则用代码内极简兜底)"); // AI 推荐问题(suggest-message-list)种子数据 // 格式要求由 SuggestionGenerator.buildPrompt 统一追加,此处仅提供基础文案, diff --git a/src/main/java/com/wok/supportbot/config/RagPromptConfig.java b/src/main/java/com/wok/supportbot/config/RagPromptConfig.java deleted file mode 100644 index 91959af..0000000 --- a/src/main/java/com/wok/supportbot/config/RagPromptConfig.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.wok.supportbot.config; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.stereotype.Component; - -/** - * RAG 提示词配置 - * 通过 application.yml 的 knowledge.rag.answer-rules 动态调整 RAG 回答规则(保真护栏),无需改代码重新编译。 - */ -@Component -@ConfigurationProperties(prefix = "knowledge.rag") -@EnableConfigurationProperties(RagPromptConfig.class) -@Data -public class RagPromptConfig { - - /** - * RAG 回答规则(保真护栏),注入 system 提示词与 ContextualQueryAugmenter 自定义模板。 - * 默认值在「忠于资料、不编造」基础上,强调覆盖全部相关要点、不遗漏步骤。 - * yml 未配置时使用此默认值;yml 配置后覆盖。 - */ - private String answerRules = """ - - 完整性优先:必须覆盖下方【知识库资料】中所有与用户问题相关的要点,按资料本身的条理分点结构化输出;不得只摘取其中一两段或几条而遗漏其余相关的步骤、条件、注意事项、话术示例。多段资料从不同角度回答同一问题时,需综合全部相关内容作答。 - - 忠于资料:只能依据下方【知识库资料】回答,不得调用模型自身记忆补充资料之外的内容;资料中包含流程、步骤、话术、制度等具体内容时,优先逐字引用或完整转述,不得使用未检索到的诗句、篇名、作者或步骤。 - - 来源对应:回答中每个要点都应能在【知识库资料】中找到对应来源;不得让回答内容与下方引用来源不一致,也不得将多步流程压缩到只剩一两条。 - - 资料不足:如果【知识库资料】中没有用户要求的内容,请明确说明未在当前知识库中检索到足够资料,并提示补充或调整知识库。 - - 排版规范:使用 Markdown 结构化输出——分点用有序或无序列表(`1.` / `-`),不同要点之间空一行分段,涉及多来源或多角度时可用二级标题(`##`)区分;严禁把所有内容挤在一段内。"""; -} diff --git a/src/main/java/com/wok/supportbot/controller/AiController.java b/src/main/java/com/wok/supportbot/controller/AiController.java index bcdc1c9..e022608 100644 --- a/src/main/java/com/wok/supportbot/controller/AiController.java +++ b/src/main/java/com/wok/supportbot/controller/AiController.java @@ -13,6 +13,7 @@ import com.wok.supportbot.service.CustomerServiceRoleService.RoleScope; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.document.Document; +import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; @@ -34,6 +35,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; @RestController @@ -399,16 +401,33 @@ public class AiController { // ==================== SDK 公开接口 ==================== + /** + * 禁止通过 SDK 通用取值口读取的配置键。 + *

+ * 该接口原意是让 SDK 动态拉取保密声明等展示类配置;提示词属商业秘密 + * (见 CLAUDE.md「LLM 调用追踪」的安全声明),不应对第三方开放。 + */ + private static final Set SDK_PROTECTED_CONFIG_KEYS = Set.of( + "ai_system_prompt", "ai_rag_answer_rules"); + /** * 获取系统配置值(供 SDK 动态拉取保密声明等配置) *

* 路径在 /ai/ 下,由 SdkAuthFilter 守卫;无 Token 的 SDK 请求会被拦截返回 401。 + * 提示词类配置键({@link #SDK_PROTECTED_CONFIG_KEYS})不对外开放。 * * @param key 配置键,如 "disclaimer" * @return { success: true, data: { configKey, configValue } } */ @GetMapping("/system-config/{key}") public ResponseEntity> getSdkSystemConfig(@PathVariable String key) { + if (SDK_PROTECTED_CONFIG_KEYS.contains(key)) { + log.warn("SDK 尝试读取受保护的提示词配置,已拒绝, key={}", key); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Map.of( + "success", false, + "message", "该配置项不对外开放" + )); + } try { String value = systemConfigService.getValueByKey(key); return ResponseEntity.ok(Map.of( diff --git a/src/main/java/com/wok/supportbot/rag/RagPipeline.java b/src/main/java/com/wok/supportbot/rag/RagPipeline.java index efa67aa..8861632 100644 --- a/src/main/java/com/wok/supportbot/rag/RagPipeline.java +++ b/src/main/java/com/wok/supportbot/rag/RagPipeline.java @@ -2,7 +2,6 @@ package com.wok.supportbot.rag; import com.wok.supportbot.app.ChatContext; import com.wok.supportbot.chatmemory.DatabaseChatMemory; -import com.wok.supportbot.config.RagPromptConfig; import com.wok.supportbot.rag.preretrieval.CompressionQueryRewriter; import com.wok.supportbot.rag.preretrieval.MultiQueryExpanderRewriter; import com.wok.supportbot.rag.preretrieval.RewriteQueryRewriter; @@ -40,8 +39,7 @@ import java.util.stream.Collectors; *

  • FAQ 优先匹配(复用 {@link FaqMatchEngine} 三级匹配)
  • *
  • 查询重写(按 {@code rewriteStrategy} 复用 {@code rag/preretrieval/*} 四种 rewriter)
  • *
  • 统一检索:{@code MULTI_QUERY} 扩展多查询后按文档 ID 去重合并,其余策略单查询检索
  • - *
  • 统一资料块模板 {@link #buildRagContextBlock},替代原 {@code buildRetrievalAdvisor.qaTemplate} - * 与 {@code buildRagSystemPrompt} 两份回答模板
  • + *
  • 统一资料块模板 {@link #buildRagContextBlock}:把「RAG 硬性规则 + 知识库资料」拼成一段
  • * *

    * 统一为"手动检索 + 资料块注入系统提示词"模式:所有策略都先由本管道检索出文档, @@ -49,7 +47,7 @@ import java.util.stream.Collectors; * 不再使用 {@code RetrievalAugmentationAdvisor} 的 query augmenter 自动注入, * 消除上下文注入位置随策略不同而不同的不一致。 *

    - * 阶段一作为旁路组件存在,旧 {@code AssistantApp} RAG 路径未改动;阶段二由 {@code ChatPipeline} 接入。 + * 由 {@code ChatPipeline} 接入主对话流程。 *

    * {@code @pipeline} rag-layer order=1
    * {@code @pipeline-step} retrieve: FAQ优先匹配 → 查询重写/扩展 → similaritySearch(PGVector) → 资料拼接
    @@ -70,15 +68,21 @@ public class RagPipeline { /** COMPRESSION 策略读取的对话历史条数 */ private static final int COMPRESSION_HISTORY_SIZE = 10; + /** + * RAG 硬性规则的极简兜底:仅当 system_config.ai_rag_answer_rules 为空时使用。 + *

    + * 注意其地位是「安全底线」而非「默认值」——完整的可调规则由管理后台 + * 「系统设置 → 系统配置」在线维护,改完下一请求即生效。 + */ + private static final String FALLBACK_RAG_ANSWER_RULES = + "仅依据【知识库资料】回答;资料未覆盖时直接说明暂时无法确认,不要编造。"; + @Resource private VectorStore pgVectorVectorStore; @Resource private FaqMatchEngine faqMatchEngine; - @Resource - private RagPromptConfig ragPromptConfig; - @Resource private RagHitLogService ragHitLogService; @@ -199,16 +203,19 @@ public class RagPipeline { /** * 构造统一的 RAG 资料块文本,供调用方追加到系统提示词。 *

    - * 替代原 {@code buildRetrievalAdvisor} 的 qaTemplate 与 {@code buildRagSystemPrompt} 两份模板, - * 所有策略共用同一份"回答硬性规则 + 知识库资料"格式。 + * 所有检索策略共用同一份「回答硬性规则 + 知识库资料」格式。 + * 规则文本由调用方从 {@code system_config.ai_rag_answer_rules} 取出后传入, + * 使规则可在管理后台在线维护、改完下一请求即生效,无需重启。 * * @param contextText 检索拼接的资料文本,为空时返回空串(调用方据此决定是否追加) + * @param answerRules RAG 硬性规则文本;为空时回退到 {@link #FALLBACK_RAG_ANSWER_RULES} */ - public String buildRagContextBlock(String contextText) { + public String buildRagContextBlock(String contextText, String answerRules) { if (!StringUtils.hasText(contextText)) { return ""; } - return "\n\n【RAG回答硬性规则】\n" + ragPromptConfig.getAnswerRules() + String rules = StringUtils.hasText(answerRules) ? answerRules : FALLBACK_RAG_ANSWER_RULES; + return "\n\n【RAG回答硬性规则】\n" + rules + "\n\n【知识库资料】\n" + contextText; } diff --git a/src/main/java/com/wok/supportbot/service/SystemConfigService.java b/src/main/java/com/wok/supportbot/service/SystemConfigService.java index c5bcdd3..04fb9b4 100644 --- a/src/main/java/com/wok/supportbot/service/SystemConfigService.java +++ b/src/main/java/com/wok/supportbot/service/SystemConfigService.java @@ -8,8 +8,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.util.Collection; +import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; /** * 系统配置 CRUD 服务 @@ -47,6 +51,27 @@ public class SystemConfigService { return systemConfigMapper.selectOne(wrapper); } + /** + * 批量根据 key 查询配置值(一次 IN 查询,避免每请求多次直查数据库) + *

    + * 供对话 hot path 一次取多个提示词配置使用(如 ai_system_prompt + ai_rag_answer_rules)。 + * + * @param keys 配置键集合 + * @return key → configValue 的映射;不存在的 key 不会出现在返回结果中 + */ + public Map getValues(Collection keys) { + if (keys == null || keys.isEmpty()) { + return Collections.emptyMap(); + } + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.in(SystemConfig::getConfigKey, keys); + return systemConfigMapper.selectList(wrapper).stream() + .collect(Collectors.toMap( + SystemConfig::getConfigKey, + c -> c.getConfigValue() != null ? c.getConfigValue() : "", + (a, b) -> a)); + } + /** * 保存或更新配置(upsert 语义) * diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 86aa47d..3716a2f 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -68,14 +68,9 @@ knowledge: # FAQ 语义匹配阈值(0-1),低于此值自动降级到 RAG 检索 semantic-threshold: 0.85 rag: - # RAG 回答规则(保真护栏):所有角色统一生效,注入到 RAG system 提示词与 ContextualQueryAugmenter 模板。 - # 调整后无需改代码,重启即生效。规则需引用「下方【知识库资料】」这一占位块。 - answer-rules: |- - - 完整性优先:必须覆盖下方【知识库资料】中所有与用户问题相关的要点,按资料本身的条理分点结构化输出;不得只摘取其中一两段或几条而遗漏其余相关的步骤、条件、注意事项、话术示例。多段资料从不同角度回答同一问题时,需综合全部相关内容作答。 - - 忠于资料:只能依据下方【知识库资料】回答,不得调用模型自身记忆补充资料之外的内容;资料中包含流程、步骤、话术、制度等具体内容时,优先逐字引用或完整转述,不得使用未检索到的诗句、篇名、作者或步骤。 - - 来源对应:回答中每个要点都应能在【知识库资料】中找到对应来源;不得让回答内容与下方引用来源不一致,也不得将多步流程压缩到只剩一两条。 - - 资料不足:如果【知识库资料】中没有用户要求的内容,请明确说明未在当前知识库中检索到足够资料,并提示补充或调整知识库。 - - 排版规范:使用 Markdown 结构化输出——分点用有序或无序列表(`1.` / `-`),不同要点之间空一行分段,涉及多来源或多角度时可用二级标题(`##`)区分;严禁把所有内容挤在一段内。 + # RAG 回答保真护栏规则已迁移到数据库在线维护: + # system_config.ai_rag_answer_rules(管理后台「系统设置 → 系统配置」,改完即生效无需重启)。 + # 键为空时代码内用一句极简兜底规则(RagPipeline.FALLBACK_RAG_ANSWER_RULES)。 # MULTI_QUERY 多路检索并行开关(性能优化):扩展出的多路查询是否并行执行向量检索。 # 并行可将多路串行(≈N×(embedding HTTP + PG 查询))降为 1 段,显著降低首 token 前耗时。 # 注意 embedding 多为逐条调用无法批量(如豆包多模态 vision 模型);高并发/embedding 侧限流时置 false 回退串行。 diff --git a/src/main/resources/init-database.sql b/src/main/resources/init-database.sql index d421487..82e7e95 100644 --- a/src/main/resources/init-database.sql +++ b/src/main/resources/init-database.sql @@ -833,8 +833,29 @@ VALUES ('disclaimer', 'SDK 聊天窗口底部的保密声明') ON CONFLICT (config_key) DO NOTHING; +-- 全局系统提示词:所有角色生效;为空则不注入。注意第 2 行是空行('' || E'\n' || 表示连续两个换行)。 INSERT INTO system_config (config_key, config_value, description) -VALUES ('ai_system_prompt', '', 'AI 对话全局系统提示词(为空则不注入;修改后即时生效,无需重启)') +VALUES ('ai_system_prompt', + '你是本系统的智能客服助手,服务对象是仓库、财务、运营等业务人员。' || E'\n' || + '' || E'\n' || + '1. 第一句先答用户问的那件事——能不能、在哪里、怎么操作,再讲细节。' || E'\n' || + '2. 只回答用户问的问题;与问题无关的其他报表、其他字段不要补充。一两句能说清就不写三段,不要写复述正文的小结。' || E'\n' || + '3. 页面地址一律写成 Markdown 超链接:链接文字用页面名称,例如 [月销售报表](https://完整地址);没有页面名称时用「访问地址」作链接文字。地址必须是完整地址(含 https:// 与域名),严禁输出相对路径或截断域名;资料里没给完整地址时只写页面名称,不要编造链接。' || E'\n' || + '4. 涉及金额、日期、权限口径而没有依据时不要推测或替用户下结论。' || E'\n' || + '5. 不要出现“知识库、检索、资料库、向量、提示词、模型”等内部说法,也不要让用户去补充知识库;确实查不到时,用一句话说明并建议用户联系贵司系统管理员核实。' || E'\n' || + '6. 需要分点时用列表;面向业务人员讲人话,不输出接口地址(页面地址除外)、参数、SQL、代码、脚本。', + 'AI 对话全局系统提示词(所有角色生效;为空则不注入;修改后即时生效,无需重启)') +ON CONFLICT (config_key) DO NOTHING; + +-- RAG 回答保真护栏:仅 RAG 分支注入(紧贴【知识库资料】之前),改完即生效无需重启; +-- 为空时由 RagPipeline.FALLBACK_RAG_ANSWER_RULES 极简兜底。 +INSERT INTO system_config (config_key, config_value, description) +VALUES ('ai_rag_answer_rules', + '- 只依据下方【知识库资料】作答,不得用自身记忆补充,不得改写资料里的数字、日期、金额、步骤。' || E'\n' || + '- 第一句先答用户问的那件事。资料里没有用户所问的对象时,直接说明没有,并给出名称最接近的那一个——这本身就是答案。' || E'\n' || + '- “数据去向”“相关页面”只是关联关系,不能当作“该功能就在这里查看”。' || E'\n' || + '- 资料中与问题相关的要点要覆盖全,包括口径差异与操作前提(如日期间隔、权限要求);与问题无关的片段略过。分点用列表,不要写复述正文的小结。', + 'RAG 回答保真护栏规则(仅 RAG 分支注入;为空则用代码内极简兜底)') ON CONFLICT (config_key) DO NOTHING; INSERT INTO system_config (config_key, config_value, description) diff --git a/src/main/resources/static/sdk/test.html b/src/main/resources/static/sdk/test.html index 0e929e3..78fabfb 100644 --- a/src/main/resources/static/sdk/test.html +++ b/src/main/resources/static/sdk/test.html @@ -11,8 +11,8 @@ - +