diff --git a/pom.xml b/pom.xml
index cb0aeeeeb9cdd6ced2b7b381a75d40fd3a42704f..79cbf92324bb92d3d37eb97d4e710cd9d21cb791 100644
--- a/pom.xml
+++ b/pom.xml
@@ -34,7 +34,7 @@
2.3.1
2.3.9
5.8.25
- 1.18.20
+ 1.18.40
3.5.7
1.2.83
5.8.25
@@ -85,6 +85,13 @@
${java.version}
${java.version}
${project.build.sourceEncoding}
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
diff --git "a/sql/Mysql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql" "b/sql/Mysql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql"
index e9714c3a9bd68b1c7dcdacb680002e8f74813190..b34f531406842c020563d0df6ab5cd313549edf1 100644
--- "a/sql/Mysql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql"
+++ "b/sql/Mysql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql"
@@ -1,25 +1,47 @@
/*
O2-O8 RAG 完整测试知识路由初始化脚本
+ 当前脚本适用于 KnowledgeBase 硬边界改造后的新模型。
+
使用顺序:
- 1. 先在管理端一次性上传 document/O2-O8-RAG能力完整测试验收方案.md 第 4.1 节列出的 15 份必传样例。
- 2. 每份文档完成“确认策略方案”和“构建索引执行”,确认 parse_status=3、strategy_status=3、index_status=3。
- 3. 直接执行本脚本。脚本会按 original_file_name 自动选择每个文件最新上传的有效文档记录。
-
- 注意:
- - 本脚本只初始化测试知识域、知识主题、文档画像和主题文档关联。
- - 本脚本会更新 super_agent_document 的 knowledge_scope_code、knowledge_scope_name、business_category、document_tags。
- - 本脚本使用 INSERT ... ON DUPLICATE KEY UPDATE,可重复执行。
- - 本脚本不会修改文档解析、策略方案、索引状态、chunk、向量库、ES/BM25、KG 或 RAPTOR 数据。
- - 如需重新跑完整验收,优先重新上传/重新解析文档后重跑本脚本;不需要手动填写文档 ID,也不需要删除不同批次文档。
+ 1. 先在管理端创建 3 个测试知识库:
+ - 解析回归知识库
+ - 运营制度知识库
+ - GraphRAG图谱评测知识库
+ 2. 按 document/O2-O8-RAG能力完整测试验收方案.md 第 1 步上传清单上传 15 份必传样例。
+ 3. 等每份文档完成解析、策略确认和索引构建,确认 parse_status=3、strategy_status=3、index_status=3。
+ 4. 执行本脚本。脚本会自动按知识库名称和 original_file_name 选择最新有效文档记录。
+
+ 本脚本会创建或更新:
+ - super_agent_knowledge_scope_node
+ - super_agent_knowledge_topic_node
+ - super_agent_topic_document_relation
+ - super_agent_document_profile
+
+ 本脚本不会:
+ - 创建知识库。
+ - 修改文档所属知识库。
+ - 修改文档解析、策略方案、索引状态、chunk、向量库、ES/BM25、KG 或 RAPTOR 数据。
+ - 写入旧字段:知识域编码、知识域名称、业务分类、文档标签。
+
+ 本脚本使用固定高位 ID,可重复执行。如果你的数据库极端情况下已占用 @base_id 这一段,
+ 可以把 @base_id 改成其他未使用的大整数。
*/
+SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+
START TRANSACTION;
/* =========================================================
- 0. 自动按文件名选择最新文档 ID
+ 0. 固定参数
========================================================= */
+SET @base_id = 8800041800000000000;
+
+SET @kb_parse_name = '解析回归知识库';
+SET @kb_operation_name = '运营制度知识库';
+SET @kb_graph_name = 'GraphRAG图谱评测知识库';
+
SET @file_o2_provider_pdf = 'O2-provider-artifact验收样例.pdf';
SET @file_o2_ocr_pdf = 'O2-扫描OCR验收样例-图片型PDF.pdf';
SET @file_o2_ocr_png = 'O2-扫描OCR验收样例-文字截图.png';
@@ -38,329 +60,250 @@ SET @file_release_graph_alias = 'O6多社区排序-生产发布回滚别名B.md'
SET @file_data_graph_spec = 'O6多社区排序-客户数据访问控制规范A.md';
SET @file_data_graph_alias = 'O6多社区排序-客户数据访问控制别名B.md';
-SET @doc_o2_provider_pdf_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_o2_provider_pdf ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_o2_ocr_pdf_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_o2_ocr_pdf ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_o2_ocr_png_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_o2_ocr_png ORDER BY create_time DESC, id DESC LIMIT 1);
-
-SET @doc_xinglian_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_xinglian ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_release_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_release ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_incident_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_incident ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_data_policy_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_data_policy ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_travel_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_travel ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_onboarding_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_onboarding ORDER BY create_time DESC, id DESC LIMIT 1);
-
-SET @doc_audit_evidence_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_audit_evidence ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_audit_alias_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_audit_alias ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_release_graph_spec_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_release_graph_spec ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_release_graph_alias_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_release_graph_alias ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_data_graph_spec_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_data_graph_spec ORDER BY create_time DESC, id DESC LIMIT 1);
-SET @doc_data_graph_alias_id = (SELECT id FROM super_agent_document WHERE status = 1 AND original_file_name = @file_data_graph_alias ORDER BY create_time DESC, id DESC LIMIT 1);
-
-SELECT id, document_name, original_file_name, parse_status, strategy_status, index_status, last_index_task_id, create_time
-FROM super_agent_document
-WHERE id IN (
- @doc_o2_provider_pdf_id,
- @doc_o2_ocr_pdf_id,
- @doc_o2_ocr_png_id,
- @doc_xinglian_id,
- @doc_release_id,
- @doc_incident_id,
- @doc_data_policy_id,
- @doc_travel_id,
- @doc_onboarding_id,
- @doc_audit_evidence_id,
- @doc_audit_alias_id,
- @doc_release_graph_spec_id,
- @doc_release_graph_alias_id,
- @doc_data_graph_spec_id,
- @doc_data_graph_alias_id
-)
-ORDER BY original_file_name, create_time DESC;
-
-/* =========================================================
- 1. 固定配置编码
- ========================================================= */
-
-SET @scope_parse_code = 'rag_o_parse';
-SET @scope_parse_name = 'O2 解析固定回归';
-SET @scope_operation_code = 'rag_o_operation';
-SET @scope_operation_name = '运营制度与RAG问答评测';
-SET @scope_graph_code = 'rag_o_graph';
-SET @scope_graph_name = 'GraphRAG跨文档图谱评测';
+DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_required_kb;
+CREATE TEMPORARY TABLE tmp_o2_o8_required_kb (
+ kb_name VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL PRIMARY KEY,
+ expected_doc_count INT NOT NULL
+) ENGINE=InnoDB;
-/*
- 这些 id 只用于新插入范围、主题、关系、画像时。
- 如果你的数据库里极端情况下已经占用了这些 id,可以把 @base_id 改成其他未使用的大整数。
-*/
-SET @base_id = 8800041700000000000;
+INSERT INTO tmp_o2_o8_required_kb (kb_name, expected_doc_count)
+VALUES
+(@kb_parse_name, 3),
+(@kb_operation_name, 6),
+(@kb_graph_name, 6);
+
+DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_expected_document;
+CREATE TEMPORARY TABLE tmp_o2_o8_expected_document (
+ batch_code VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ kb_name VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ original_file_name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
+ PRIMARY KEY (kb_name, original_file_name)
+) ENGINE=InnoDB;
+
+INSERT INTO tmp_o2_o8_expected_document (batch_code, kb_name, original_file_name)
+VALUES
+('A-O2', @kb_parse_name, @file_o2_provider_pdf),
+('A-O2', @kb_parse_name, @file_o2_ocr_pdf),
+('A-O2', @kb_parse_name, @file_o2_ocr_png),
+
+('B-业务', @kb_operation_name, @file_xinglian),
+('B-业务', @kb_operation_name, @file_release),
+('B-业务', @kb_operation_name, @file_incident),
+('B-业务', @kb_operation_name, @file_data_policy),
+('B-业务', @kb_operation_name, @file_travel),
+('B-业务', @kb_operation_name, @file_onboarding),
+
+('C-O6', @kb_graph_name, @file_audit_evidence),
+('C-O6', @kb_graph_name, @file_audit_alias),
+('C-O6', @kb_graph_name, @file_release_graph_spec),
+('C-O6', @kb_graph_name, @file_release_graph_alias),
+('C-O6', @kb_graph_name, @file_data_graph_spec),
+('C-O6', @kb_graph_name, @file_data_graph_alias);
+
+DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_assert_fail;
+CREATE TEMPORARY TABLE tmp_o2_o8_assert_fail (
+ id INT NOT NULL PRIMARY KEY,
+ reason VARCHAR(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL
+) ENGINE=InnoDB;
+
+INSERT INTO tmp_o2_o8_assert_fail (id, reason)
+VALUES (1, 'sentinel');
/* =========================================================
- 1.1 自动取数和状态保护
+ 1. 自动解析知识库和文档 ID
========================================================= */
-SET @missing_doc_ids = CONCAT_WS(',',
- IF(@doc_o2_provider_pdf_id IS NULL, @file_o2_provider_pdf, NULL),
- IF(@doc_o2_ocr_pdf_id IS NULL, @file_o2_ocr_pdf, NULL),
- IF(@doc_o2_ocr_png_id IS NULL, @file_o2_ocr_png, NULL),
- IF(@doc_xinglian_id IS NULL, @file_xinglian, NULL),
- IF(@doc_release_id IS NULL, @file_release, NULL),
- IF(@doc_incident_id IS NULL, @file_incident, NULL),
- IF(@doc_data_policy_id IS NULL, @file_data_policy, NULL),
- IF(@doc_travel_id IS NULL, @file_travel, NULL),
- IF(@doc_onboarding_id IS NULL, @file_onboarding, NULL),
- IF(@doc_audit_evidence_id IS NULL, @file_audit_evidence, NULL),
- IF(@doc_audit_alias_id IS NULL, @file_audit_alias, NULL),
- IF(@doc_release_graph_spec_id IS NULL, @file_release_graph_spec, NULL),
- IF(@doc_release_graph_alias_id IS NULL, @file_release_graph_alias, NULL),
- IF(@doc_data_graph_spec_id IS NULL, @file_data_graph_spec, NULL),
- IF(@doc_data_graph_alias_id IS NULL, @file_data_graph_alias, NULL)
+DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_kb;
+CREATE TEMPORARY TABLE tmp_o2_o8_kb AS
+SELECT
+ required.kb_name,
+ required.expected_doc_count,
+ (
+ SELECT kb.id
+ FROM super_agent_knowledge_base kb
+ WHERE kb.status = 1
+ AND kb.base_name = required.kb_name
+ ORDER BY kb.id DESC
+ LIMIT 1
+ ) AS knowledge_base_id
+FROM tmp_o2_o8_required_kb required;
+
+SET @missing_kbs = (
+ SELECT GROUP_CONCAT(kb_name ORDER BY kb_name SEPARATOR ', ')
+ FROM tmp_o2_o8_kb
+ WHERE knowledge_base_id IS NULL
);
-SET @missing_doc_ids_error = IF(
- @missing_doc_ids IS NULL OR @missing_doc_ids = '',
- NULL,
- CONCAT('这些文件没有找到 status=1 的最新上传记录,请先上传后再执行脚本: ', @missing_doc_ids)
+SELECT
+ CASE
+ WHEN @missing_kbs IS NULL OR @missing_kbs = '' THEN 'OK: 3 个测试知识库均已找到'
+ ELSE CONCAT('ERROR: 以下知识库不存在或未启用,请先创建后再执行脚本: ', @missing_kbs)
+ END AS knowledge_base_check;
+
+INSERT INTO tmp_o2_o8_assert_fail (id, reason)
+SELECT 1, 'missing enabled knowledge base'
+WHERE @missing_kbs IS NOT NULL AND @missing_kbs <> '';
+
+DROP TEMPORARY TABLE IF EXISTS tmp_o2_o8_latest_document;
+CREATE TEMPORARY TABLE tmp_o2_o8_latest_document AS
+SELECT
+ expected.batch_code,
+ expected.kb_name,
+ kb.knowledge_base_id,
+ expected.original_file_name,
+ (
+ SELECT d.id
+ FROM super_agent_document d
+ WHERE d.status = 1
+ AND d.knowledge_base_id = kb.knowledge_base_id
+ AND d.original_file_name = expected.original_file_name
+ ORDER BY d.create_time DESC, d.id DESC
+ LIMIT 1
+ ) AS document_id
+FROM tmp_o2_o8_expected_document expected
+JOIN tmp_o2_o8_kb kb ON kb.kb_name = expected.kb_name;
+
+SET @missing_docs = (
+ SELECT GROUP_CONCAT(CONCAT(kb_name, '/', original_file_name) ORDER BY kb_name, original_file_name SEPARATOR '; ')
+ FROM tmp_o2_o8_latest_document
+ WHERE document_id IS NULL
);
SELECT
CASE
- WHEN @missing_doc_ids_error IS NULL THEN 'OK: 已自动找到 15 个文档 ID'
- ELSE @missing_doc_ids_error
- END AS parameter_check;
-
-SET @missing_doc_ids_sql = IF(
- @missing_doc_ids_error IS NULL,
- 'SELECT 1',
- CONCAT('SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''', @missing_doc_ids_error, '''')
-);
-PREPARE missing_doc_ids_stmt FROM @missing_doc_ids_sql;
-EXECUTE missing_doc_ids_stmt;
-DEALLOCATE PREPARE missing_doc_ids_stmt;
+ WHEN @missing_docs IS NULL OR @missing_docs = '' THEN 'OK: 已按知识库和文件名找到 15 份最新有效文档'
+ ELSE CONCAT('ERROR: 以下文档未在预期知识库下找到 status=1 记录: ', @missing_docs)
+ END AS document_id_check;
+
+INSERT INTO tmp_o2_o8_assert_fail (id, reason)
+SELECT 1, 'missing expected document'
+WHERE @missing_docs IS NOT NULL AND @missing_docs <> '';
SET @not_ready_docs = (
SELECT GROUP_CONCAT(CONCAT(
- original_file_name,
- '(parse=', IFNULL(CAST(parse_status AS CHAR), 'NULL'),
- ', strategy=', IFNULL(CAST(strategy_status AS CHAR), 'NULL'),
- ', index=', IFNULL(CAST(index_status AS CHAR), 'NULL'),
+ latest.kb_name, '/',
+ d.original_file_name,
+ '(parse=', IFNULL(CAST(d.parse_status AS CHAR), 'NULL'),
+ ', strategy=', IFNULL(CAST(d.strategy_status AS CHAR), 'NULL'),
+ ', index=', IFNULL(CAST(d.index_status AS CHAR), 'NULL'),
+ ', task=', IFNULL(CAST(d.last_index_task_id AS CHAR), 'NULL'),
')'
- ) ORDER BY original_file_name SEPARATOR '; ')
- FROM super_agent_document
- WHERE id IN (
- @doc_o2_provider_pdf_id,
- @doc_o2_ocr_pdf_id,
- @doc_o2_ocr_png_id,
- @doc_xinglian_id,
- @doc_release_id,
- @doc_incident_id,
- @doc_data_policy_id,
- @doc_travel_id,
- @doc_onboarding_id,
- @doc_audit_evidence_id,
- @doc_audit_alias_id,
- @doc_release_graph_spec_id,
- @doc_release_graph_alias_id,
- @doc_data_graph_spec_id,
- @doc_data_graph_alias_id
- )
- AND (IFNULL(parse_status, -1) <> 3 OR IFNULL(strategy_status, -1) <> 3 OR IFNULL(index_status, -1) <> 3)
-);
-
-SET @not_ready_docs_error = IF(
- @not_ready_docs IS NULL OR @not_ready_docs = '',
- NULL,
- CONCAT('以下最新文档还没有完成 parse_status=3、strategy_status=3、index_status=3,请等待完成后重跑: ', @not_ready_docs)
+ ) ORDER BY latest.kb_name, d.original_file_name SEPARATOR '; ')
+ FROM tmp_o2_o8_latest_document latest
+ JOIN super_agent_document d ON d.id = latest.document_id
+ WHERE IFNULL(d.parse_status, -1) <> 3
+ OR IFNULL(d.strategy_status, -1) <> 3
+ OR IFNULL(d.index_status, -1) <> 3
+ OR d.last_index_task_id IS NULL
);
SELECT
CASE
- WHEN @not_ready_docs_error IS NULL THEN 'OK: 15 个最新文档均已完成解析、策略确认和索引构建'
- ELSE @not_ready_docs_error
+ WHEN @not_ready_docs IS NULL OR @not_ready_docs = '' THEN 'OK: 15 份文档均已完成解析、策略确认和索引构建'
+ ELSE CONCAT('ERROR: 以下文档未完成 parse=3、strategy=3、index=3 或缺 last_index_task_id: ', @not_ready_docs)
END AS document_status_check;
-SET @not_ready_docs_sql = IF(
- @not_ready_docs_error IS NULL,
- 'SELECT 1',
- CONCAT('SIGNAL SQLSTATE ''45000'' SET MESSAGE_TEXT = ''', @not_ready_docs_error, '''')
-);
-PREPARE not_ready_docs_stmt FROM @not_ready_docs_sql;
-EXECUTE not_ready_docs_stmt;
-DEALLOCATE PREPARE not_ready_docs_stmt;
+INSERT INTO tmp_o2_o8_assert_fail (id, reason)
+SELECT 1, 'document not ready'
+WHERE @not_ready_docs IS NOT NULL AND @not_ready_docs <> '';
+
+SET @kb_parse_id = (SELECT knowledge_base_id FROM tmp_o2_o8_kb WHERE kb_name = @kb_parse_name);
+SET @kb_operation_id = (SELECT knowledge_base_id FROM tmp_o2_o8_kb WHERE kb_name = @kb_operation_name);
+SET @kb_graph_id = (SELECT knowledge_base_id FROM tmp_o2_o8_kb WHERE kb_name = @kb_graph_name);
+
+SET @doc_o2_provider_pdf_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_o2_provider_pdf);
+SET @doc_o2_ocr_pdf_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_o2_ocr_pdf);
+SET @doc_o2_ocr_png_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_o2_ocr_png);
+
+SET @doc_xinglian_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_xinglian);
+SET @doc_release_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_release);
+SET @doc_incident_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_incident);
+SET @doc_data_policy_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_data_policy);
+SET @doc_travel_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_travel);
+SET @doc_onboarding_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_onboarding);
+
+SET @doc_audit_evidence_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_audit_evidence);
+SET @doc_audit_alias_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_audit_alias);
+SET @doc_release_graph_spec_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_release_graph_spec);
+SET @doc_release_graph_alias_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_release_graph_alias);
+SET @doc_data_graph_spec_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_data_graph_spec);
+SET @doc_data_graph_alias_id = (SELECT document_id FROM tmp_o2_o8_latest_document WHERE original_file_name = @file_data_graph_alias);
+
+SELECT
+ latest.batch_code,
+ latest.kb_name,
+ latest.knowledge_base_id,
+ d.id AS document_id,
+ d.document_name,
+ d.original_file_name,
+ d.parse_status,
+ d.strategy_status,
+ d.index_status,
+ d.last_index_task_id
+FROM tmp_o2_o8_latest_document latest
+JOIN super_agent_document d ON d.id = latest.document_id
+ORDER BY latest.kb_name, latest.original_file_name;
/* =========================================================
- 2. 更新 15 份文档主表元数据
+ 2. 固定 ID
========================================================= */
-UPDATE super_agent_document
-SET document_name = 'O2-provider-artifact验收样例',
- knowledge_scope_code = @scope_parse_code,
- knowledge_scope_name = @scope_parse_name,
- business_category = 'O2解析固定样例',
- document_tags = 'O2,Document Mind,layout,表格,FIGURE,bbox,artifact,解析回归',
- edit_time = NOW()
-WHERE id = @doc_o2_provider_pdf_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = 'O2-扫描OCR验收样例-图片型PDF',
- knowledge_scope_code = @scope_parse_code,
- knowledge_scope_name = @scope_parse_name,
- business_category = 'O2解析固定样例',
- document_tags = 'O2,OCR,图片型PDF,PAGE_IMAGE,TABLE_IMAGE,bbox,解析回归',
- edit_time = NOW()
-WHERE id = @doc_o2_ocr_pdf_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = 'O2-扫描OCR验收样例-文字截图',
- knowledge_scope_code = @scope_parse_code,
- knowledge_scope_name = @scope_parse_name,
- business_category = 'O2解析固定样例',
- document_tags = 'O2,OCR,PNG,图片文本,蓝桥订单,RAG-O2-20260630,支付回调延迟',
- edit_time = NOW()
-WHERE id = @doc_o2_ocr_png_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = '星联智服全渠道客服平台上线与运营管理手册',
- knowledge_scope_code = @scope_operation_code,
- knowledge_scope_name = @scope_operation_name,
- business_category = '客服平台运营手册',
- document_tags = '星联智服,客服平台,上线运营,知识治理,机器人策略,灰度验证,上线观察,故障处理,RAG,O8基线',
- edit_time = NOW()
-WHERE id = @doc_xinglian_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = '生产环境发布与回滚操作规范',
- knowledge_scope_code = @scope_operation_code,
- knowledge_scope_name = @scope_operation_name,
- business_category = '生产发布规范',
- document_tags = '生产发布,回滚,灰度节奏,发布暂停,NovaRAG,召回成功率,强制回滚,O8路由,O7跨文档总结',
- edit_time = NOW()
-WHERE id = @doc_release_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = '核心业务系统故障应急响应预案',
- knowledge_scope_code = @scope_operation_code,
- knowledge_scope_name = @scope_operation_name,
- business_category = '故障应急预案',
- document_tags = '故障应急,NovaRAG,检索服务降级,P1,P2,人工转接,多文档路由,O3 rerank',
- edit_time = NOW()
-WHERE id = @doc_incident_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = '客户数据分级与访问控制管理制度',
- knowledge_scope_code = @scope_operation_code,
- knowledge_scope_name = @scope_operation_name,
- business_category = '数据访问制度',
- document_tags = '客户数据,L4高敏感,访问控制,审批,日志保存,DataCleanRoom,表格问答,citation',
- edit_time = NOW()
-WHERE id = @doc_data_policy_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = '差旅与费用报销管理办法',
- knowledge_scope_code = @scope_operation_code,
- knowledge_scope_name = @scope_operation_name,
- business_category = '费用报销制度',
- document_tags = '差旅,费用报销,住宿标准,审批金额阈值,表格问答',
- edit_time = NOW()
-WHERE id = @doc_travel_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = '澄星智能新员工入职培训手册',
- knowledge_scope_code = @scope_operation_code,
- knowledge_scope_name = @scope_operation_name,
- business_category = '入职培训手册',
- document_tags = '入职培训,首周日程,30天,60天,90天,表格问答',
- edit_time = NOW()
-WHERE id = @doc_onboarding_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = 'O6跨文档图谱-审计证据规范A',
- knowledge_scope_code = @scope_graph_code,
- knowledge_scope_name = @scope_graph_name,
- business_category = 'O6 GraphRAG样例',
- document_tags = 'O6,GraphRAG,AuditTrail,审计系统,权限记录,跨文档canonical,关系证据',
- edit_time = NOW()
-WHERE id = @doc_audit_evidence_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = 'O6跨文档图谱-审计系统别名说明B',
- knowledge_scope_code = @scope_graph_code,
- knowledge_scope_name = @scope_graph_name,
- business_category = 'O6 GraphRAG样例',
- document_tags = 'O6,GraphRAG,AuditTrail,审计系统,别名,系统职责,负边界',
- edit_time = NOW()
-WHERE id = @doc_audit_alias_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = 'O6多社区排序-生产发布回滚规范A',
- knowledge_scope_code = @scope_graph_code,
- knowledge_scope_name = @scope_graph_name,
- business_category = 'O6 GraphRAG样例',
- document_tags = 'O6,GraphRAG,ReleaseControl,CAB,值班SRE,生产发布,回滚演练,community',
- edit_time = NOW()
-WHERE id = @doc_release_graph_spec_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = 'O6多社区排序-生产发布回滚别名B',
- knowledge_scope_code = @scope_graph_code,
- knowledge_scope_name = @scope_graph_name,
- business_category = 'O6 GraphRAG样例',
- document_tags = 'O6,GraphRAG,ReleaseControl,生产发布控制台,变更评审委员会,别名,community',
- edit_time = NOW()
-WHERE id = @doc_release_graph_alias_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = 'O6多社区排序-客户数据访问控制规范A',
- knowledge_scope_code = @scope_graph_code,
- knowledge_scope_name = @scope_graph_name,
- business_category = 'O6 GraphRAG样例',
- document_tags = 'O6,GraphRAG,DataAccessGuard,客户数据访问控制,数据治理负责人,信息安全部,community',
- edit_time = NOW()
-WHERE id = @doc_data_graph_spec_id AND status = 1;
-
-UPDATE super_agent_document
-SET document_name = 'O6多社区排序-客户数据访问控制别名B',
- knowledge_scope_code = @scope_graph_code,
- knowledge_scope_name = @scope_graph_name,
- business_category = 'O6 GraphRAG样例',
- document_tags = 'O6,GraphRAG,DataAccessGuard,客户数据Owner,安全复核组,别名,负边界',
- edit_time = NOW()
-WHERE id = @doc_data_graph_alias_id AND status = 1;
+SET @scope_parse_id = @base_id + 1;
+SET @scope_operation_id = @base_id + 2;
+SET @scope_graph_id = @base_id + 3;
+
+SET @topic_o2_docmind_id = @base_id + 101;
+SET @topic_o2_ocr_id = @base_id + 102;
+SET @topic_o2_table_bbox_id = @base_id + 103;
+
+SET @topic_operation_xinglian_id = @base_id + 201;
+SET @topic_operation_release_id = @base_id + 202;
+SET @topic_operation_incident_id = @base_id + 203;
+SET @topic_operation_data_id = @base_id + 204;
+SET @topic_operation_travel_id = @base_id + 205;
+SET @topic_operation_onboarding_id = @base_id + 206;
+SET @topic_operation_raptor_id = @base_id + 207;
+
+SET @topic_graph_audit_id = @base_id + 301;
+SET @topic_graph_release_id = @base_id + 302;
+SET @topic_graph_data_id = @base_id + 303;
+SET @topic_graph_boundary_id = @base_id + 304;
/* =========================================================
3. 知识范围配置
========================================================= */
INSERT INTO super_agent_knowledge_scope_node (
- id, scope_code, scope_name, parent_scope_code, description, aliases, examples, sort_order,
+ id, knowledge_base_id, scope_name, parent_scope_id, description, aliases, examples, sort_order,
create_time, edit_time, status
)
VALUES
(
- @base_id + 1,
- @scope_parse_code,
- @scope_parse_name,
+ @scope_parse_id,
+ @kb_parse_id,
+ 'O2 解析固定回归',
NULL,
- '用于承接 O2 固定解析回归样例,只验证 OCR、layout、reading order、表格、bbox、artifact 和 O9 文档侧观测,不作为生产业务问答知识域。',
+ '用于承接 O2 固定解析回归样例,只验证 OCR、layout、reading order、表格、bbox、artifact 和 O9 文档侧观测。',
'O2解析,OCR回归,Document Mind回归,解析固定样例,文档侧观测',
'["O2 扫描 OCR 样例里有没有识别到关键短语","O2 固定样例中的表格有几行几列","这份 PDF 样例是否包含图示或图片区域"]',
10,
NOW(), NOW(), 1
),
(
- @base_id + 2,
- @scope_operation_code,
- @scope_operation_name,
+ @scope_operation_id,
+ @kb_operation_id,
+ '运营制度与RAG问答评测',
NULL,
- '用于承接运营制度、客服平台上线、发布回滚、故障应急、数据访问、差旅报销和入职培训类问答,重点验收 O3/O4/O7/O8。',
+ '用于承接运营制度、客服平台上线、生产发布、故障应急、数据访问、差旅报销和入职培训类问答,重点验收 O3/O4/O7/O8。',
'运营制度,RAG问答评测,客服平台,生产发布,故障应急,客户数据,差旅报销,入职培训',
'["检索命中率突然下降的可能原因都有哪些","NovaRAG 检索服务降级时按什么顺序处理","L4 高敏感信息的审批要求是什么"]',
20,
NOW(), NOW(), 1
),
(
- @base_id + 3,
- @scope_graph_code,
- @scope_graph_name,
+ @scope_graph_id,
+ @kb_graph_id,
+ 'GraphRAG跨文档图谱评测',
NULL,
'用于承接 O6 GraphRAG 跨文档别名、canonical、实体关系、community、多社区排序和负边界测试。',
'O6图谱,GraphRAG,跨文档图谱,AuditTrail,ReleaseControl,DataAccessGuard,多社区排序',
@@ -369,8 +312,9 @@ VALUES
NOW(), NOW(), 1
)
ON DUPLICATE KEY UPDATE
+ knowledge_base_id = VALUES(knowledge_base_id),
scope_name = VALUES(scope_name),
- parent_scope_code = VALUES(parent_scope_code),
+ parent_scope_id = VALUES(parent_scope_id),
description = VALUES(description),
aliases = VALUES(aliases),
examples = VALUES(examples),
@@ -380,35 +324,34 @@ ON DUPLICATE KEY UPDATE
/* =========================================================
4. 知识主题配置
- answer_shape 固定使用 explain/list/steps/compare/structure
- execution_preference 固定使用 retrieval/graph_assist/graph_then_evidence
========================================================= */
INSERT INTO super_agent_knowledge_topic_node (
- id, topic_code, topic_name, scope_code, description, aliases, examples,
+ id, knowledge_base_id, topic_name, scope_id, description, aliases, examples,
answer_shape, execution_preference, sort_order,
create_time, edit_time, status
)
VALUES
-(@base_id + 101, 'o2_parse_artifact', 'O2 Document Mind 解析产物', @scope_parse_code, '验证普通 PDF 的 layout、表格、FIGURE block、artifact、bbox 和 RAG 产物联动。', 'Document Mind,layout,artifact,bbox,FIGURE,表格解析', '["O2 固定样例中的表格能否被识别成结构化表格","这份 PDF 样例是否包含图示或图片区域"]', 'structure', 'retrieval', 10, NOW(), NOW(), 1),
-(@base_id + 102, 'o2_ocr_pdf', 'O2 图片型 PDF OCR', @scope_parse_code, '验证图片型 PDF 的 OCR 文本、页面图片、表格图片、bbox overlay 和关键业务短语。', '图片型PDF,OCR,PAGE_IMAGE,TABLE_IMAGE,扫描件', '["图片型 PDF OCR 是否识别到了编号条款和业务关键词"]', 'explain', 'retrieval', 20, NOW(), NOW(), 1),
-(@base_id + 103, 'o2_ocr_png', 'O2 图片 OCR', @scope_parse_code, '验证 PNG 图片文件进入解析主链路并识别关键短语和图片表格。', 'PNG OCR,文字截图,蓝桥订单,RAG-O2-20260630,支付回调延迟', '["O2 扫描 OCR 样例里有没有识别到蓝桥订单 7391"]', 'explain', 'retrieval', 30, NOW(), NOW(), 1),
-
-(@base_id + 201, 'operation_service_go_live', '客服平台上线与运营', @scope_operation_code, '回答星联智服客服平台上线、知识治理、机器人策略、灰度验证、上线观察、故障处理和质量评估问题。', '星联智服,客服平台,上线运营,知识治理,机器人策略,观察时长,检索命中率,人工转接率', '["检索命中率突然下降的可能原因都有哪些","人工转接率异常升高检查顺序是什么","上线观察与值班规则中观察时长有哪些"]', 'steps', 'retrieval', 10, NOW(), NOW(), 1),
-(@base_id + 202, 'operation_release_rollback', '生产发布与回滚', @scope_operation_code, '回答生产发布、灰度节奏、发布暂停、强制回滚、NovaRAG 召回成功率和发布风险控制问题。', '生产发布,回滚,灰度节奏,发布暂停,强制回滚,NovaRAG,召回成功率', '["生产发布默认灰度节奏分几个阶段","强制回滚条件有哪些","哪些情况下默认动作是暂停发布"]', 'steps', 'retrieval', 20, NOW(), NOW(), 1),
-(@base_id + 203, 'operation_incident_response', '故障应急响应', @scope_operation_code, '回答核心业务系统故障分级、NovaRAG 检索服务降级、应急处理顺序和升级边界。', '故障应急,NovaRAG降级,检索服务降级,P1,P2,人工转接激增', '["NovaRAG 检索服务降级时按什么顺序处理","连续 15 分钟无法返回检索结果故障等级怎么判断"]', 'steps', 'retrieval', 30, NOW(), NOW(), 1),
-(@base_id + 204, 'operation_data_access', '客户数据访问控制', @scope_operation_code, '回答客户数据分级、L4 高敏感数据访问、审批、导出限制、日志保存和审计要求。', '客户数据,L4高敏感,访问控制,审批,DataCleanRoom,日志保存,表格问答', '["L4 高敏感信息的审批要求和默认有效期是什么","L3 和 L4 数据的日志保存期限分别是多少"]', 'list', 'retrieval', 40, NOW(), NOW(), 1),
-(@base_id + 205, 'operation_travel_reimbursement', '差旅与费用报销', @scope_operation_code, '回答差旅住宿标准、报销金额阈值、审批流程和费用合规问题。', '差旅,费用报销,住宿标准,审批阈值,财务BP', '["北京出差酒店住宿上限是多少","10000 元以上报销需要哪些审批"]', 'list', 'retrieval', 50, NOW(), NOW(), 1),
-(@base_id + 206, 'operation_onboarding_training', '新员工入职培训', @scope_operation_code, '回答入职培训日程、首周安排、30/60/90 天关注重点和培训模块。', '入职培训,首周日程,30天,60天,90天,培训模块', '["入职当天 09:30-10:30 的培训模块是什么","第 30 天、第 60 天、第 90 天分别关注什么"]', 'list', 'retrieval', 60, NOW(), NOW(), 1),
-(@base_id + 207, 'operation_raptor_summary', '运营制度跨文档总结', @scope_operation_code, '用于 O7 RAPTOR 单文档和跨文档总结测试,聚合客服平台上线、生产发布和故障应急主线。', 'RAPTOR,跨文档总结,上线风险控制,灰度验证,回滚评估,质量复盘', '["请总结星联智服平台从灰度上线到生产发布再到质量复盘的完整治理流程","这两份规范中和上线风险控制相关的要求有哪些"]', 'compare', 'retrieval', 70, NOW(), NOW(), 1),
-
-(@base_id + 301, 'graph_audit_trail', 'AuditTrail 审计权限图谱', @scope_graph_code, '验证审计系统和 AuditTrail 的跨文档 canonical、别名、权限记录关系和负边界。', 'AuditTrail,审计系统,权限记录,异常权限扩散,信息安全部,系统管理员', '["审计系统有哪些权限相关要求","审计系统本身是否审批权限或直接回收权限"]', 'list', 'graph_then_evidence', 10, NOW(), NOW(), 1),
-(@base_id + 302, 'graph_release_control', 'ReleaseControl 生产发布图谱', @scope_graph_code, '验证 ReleaseControl、CAB、值班 SRE、发布申请、灰度观察和回滚演练的跨文档关系。', 'ReleaseControl,生产发布控制台,CAB,变更评审委员会,值班SRE,回滚演练', '["ReleaseControl 和变更评审委员会、值班 SRE 分别是什么关系","生产发布回滚相关的跨文档图谱社区总结是什么"]', 'list', 'graph_then_evidence', 20, NOW(), NOW(), 1),
-(@base_id + 303, 'graph_data_access_guard', 'DataAccessGuard 客户数据图谱', @scope_graph_code, '验证 DataAccessGuard、数据治理负责人、信息安全部、客户数据 Owner 和访问台账的跨文档关系。', 'DataAccessGuard,客户数据访问控制平台,数据治理负责人,信息安全部,客户数据Owner,安全复核组', '["DataAccessGuard 和数据治理负责人、信息安全部分别是什么关系","客户数据访问控制相关的跨文档图谱社区总结是什么"]', 'list', 'graph_then_evidence', 30, NOW(), NOW(), 1),
-(@base_id + 304, 'graph_multi_community_boundary', 'GraphRAG 多社区边界', @scope_graph_code, '验证生产发布 community 和客户数据访问 community 的排序、边界和负样例。', '多社区排序,community边界,负样例,职责边界,弱关系外推', '["ReleaseControl 是否负责 L4 高敏感客户数据访问范围确认","DataAccessGuard 是否负责回滚演练和发布窗口管控"]', 'explain', 'graph_assist', 40, NOW(), NOW(), 1)
+(@topic_o2_docmind_id, @kb_parse_id, 'Document Mind 与版面解析', @scope_parse_id, '验证普通 PDF 的 layout、表格、FIGURE block、artifact、bbox 和 RAG 产物联动。', 'Document Mind,layout,artifact,bbox,FIGURE,表格解析', '["O2 固定样例中的表格能否被识别成结构化表格","这份 PDF 样例是否包含图示或图片区域"]', 'structure', 'retrieval', 10, NOW(), NOW(), 1),
+(@topic_o2_ocr_id, @kb_parse_id, 'OCR 与图片文本解析', @scope_parse_id, '验证图片型 PDF 和 PNG 图片文件的 OCR 文本、页面图片、表格图片和关键业务短语。', '图片型PDF,OCR,PNG OCR,PAGE_IMAGE,TABLE_IMAGE,蓝桥订单,RAG-O2-20260630,支付回调延迟', '["O2 扫描 OCR 样例里有没有识别到蓝桥订单 7391","图片型 PDF OCR 是否识别到了编号条款和业务关键词"]', 'explain', 'retrieval', 20, NOW(), NOW(), 1),
+(@topic_o2_table_bbox_id, @kb_parse_id, '表格和页面定位产物', @scope_parse_id, '验证固定样例表格结构、table bbox、PAGE_IMAGE、TABLE_IMAGE 和页面 overlay。', '表格结构,table bbox,PAGE_IMAGE,TABLE_IMAGE,overlay,页面定位', '["O2 固定样例中的表格有几行几列","表格和页面定位产物是否可见"]', 'structure', 'retrieval', 30, NOW(), NOW(), 1),
+
+(@topic_operation_xinglian_id, @kb_operation_id, '星联智服上线运营', @scope_operation_id, '回答星联智服客服平台上线、知识治理、机器人策略设计、灰度验证、上线观察、故障处理和质量评估问题。', '星联智服,客服平台,上线运营,知识治理,机器人策略,观察时长,检索命中率,人工转接率', '["检索命中率突然下降的可能原因都有哪些","人工转接率异常升高检查顺序是什么","上线观察与值班规则中观察时长有哪些"]', 'steps', 'retrieval', 10, NOW(), NOW(), 1),
+(@topic_operation_release_id, @kb_operation_id, '生产发布与回滚', @scope_operation_id, '回答生产发布、灰度节奏、发布暂停、强制回滚、NovaRAG 召回成功率和发布风险控制问题。', '生产发布,回滚,灰度节奏,发布暂停,强制回滚,NovaRAG,召回成功率', '["生产发布默认灰度节奏分几个阶段","强制回滚条件有哪些","哪些情况下默认动作是暂停发布"]', 'steps', 'retrieval', 20, NOW(), NOW(), 1),
+(@topic_operation_incident_id, @kb_operation_id, 'NovaRAG 故障应急', @scope_operation_id, '回答核心业务系统故障分级、NovaRAG 检索服务降级、应急处理顺序和升级边界。', '故障应急,NovaRAG降级,检索服务降级,P1,P2,人工转接激增', '["NovaRAG 检索服务降级时按什么顺序处理","连续 15 分钟无法返回检索结果故障等级怎么判断"]', 'steps', 'retrieval', 30, NOW(), NOW(), 1),
+(@topic_operation_data_id, @kb_operation_id, '客户数据访问控制', @scope_operation_id, '回答客户数据分级、L4 高敏感数据访问、审批、导出限制、日志保存和审计要求。', '客户数据,L4高敏感,访问控制,审批,DataCleanRoom,日志保存,表格问答', '["L4 高敏感信息的审批要求和默认有效期是什么","L3 和 L4 数据的日志保存期限分别是多少"]', 'list', 'retrieval', 40, NOW(), NOW(), 1),
+(@topic_operation_travel_id, @kb_operation_id, '差旅费用报销', @scope_operation_id, '回答差旅住宿标准、报销金额阈值、审批流程和费用合规问题。', '差旅,费用报销,住宿标准,审批阈值,财务BP', '["北京出差酒店住宿上限是多少","10000 元以上报销需要哪些审批"]', 'list', 'retrieval', 50, NOW(), NOW(), 1),
+(@topic_operation_onboarding_id, @kb_operation_id, '入职培训与 30/60/90', @scope_operation_id, '回答入职培训日程、首周安排、30/60/90 天关注重点和培训模块。', '入职培训,首周日程,30天,60天,90天,培训模块', '["入职当天 09:30-10:30 的培训模块是什么","第 30 天、第 60 天、第 90 天分别关注什么"]', 'list', 'retrieval', 60, NOW(), NOW(), 1),
+(@topic_operation_raptor_id, @kb_operation_id, '运营制度跨文档总结', @scope_operation_id, '用于 O7 RAPTOR 单文档和跨文档总结测试,聚合客服平台上线、生产发布和故障应急主线。', 'RAPTOR,跨文档总结,上线风险控制,灰度验证,回滚评估,质量复盘', '["请总结星联智服平台从灰度上线到生产发布再到质量复盘的完整治理流程","这两份规范中和上线风险控制相关的要求有哪些"]', 'compare', 'retrieval', 70, NOW(), NOW(), 1),
+
+(@topic_graph_audit_id, @kb_graph_id, '审计系统权限图谱', @scope_graph_id, '验证审计系统和 AuditTrail 的跨文档 canonical、别名、权限记录关系和负边界。', 'AuditTrail,审计系统,权限记录,异常权限扩散,信息安全部,系统管理员', '["审计系统有哪些权限相关要求","审计系统本身是否审批权限或直接回收权限"]', 'list', 'graph_then_evidence', 10, NOW(), NOW(), 1),
+(@topic_graph_release_id, @kb_graph_id, 'ReleaseControl 生产发布回滚图谱', @scope_graph_id, '验证 ReleaseControl、CAB、值班 SRE、发布申请、灰度观察和回滚演练的跨文档关系。', 'ReleaseControl,生产发布控制台,CAB,变更评审委员会,值班SRE,回滚演练', '["ReleaseControl 和变更评审委员会、值班 SRE 分别是什么关系","生产发布回滚相关的跨文档图谱社区总结是什么"]', 'list', 'graph_then_evidence', 20, NOW(), NOW(), 1),
+(@topic_graph_data_id, @kb_graph_id, 'DataAccessGuard 客户数据访问控制图谱', @scope_graph_id, '验证 DataAccessGuard、数据治理负责人、信息安全部、客户数据 Owner 和访问台账的跨文档关系。', 'DataAccessGuard,客户数据访问控制平台,数据治理负责人,信息安全部,客户数据Owner,安全复核组', '["DataAccessGuard 和数据治理负责人、信息安全部分别是什么关系","客户数据访问控制相关的跨文档图谱社区总结是什么"]', 'list', 'graph_then_evidence', 30, NOW(), NOW(), 1),
+(@topic_graph_boundary_id, @kb_graph_id, 'GraphRAG 多社区边界', @scope_graph_id, '验证生产发布 community 和客户数据访问 community 的排序、边界和负样例。', '多社区排序,community边界,负样例,职责边界,弱关系外推', '["ReleaseControl 是否负责 L4 高敏感客户数据访问范围确认","DataAccessGuard 是否负责回滚演练和发布窗口管控"]', 'explain', 'graph_assist', 40, NOW(), NOW(), 1)
ON DUPLICATE KEY UPDATE
+ knowledge_base_id = VALUES(knowledge_base_id),
topic_name = VALUES(topic_name),
- scope_code = VALUES(scope_code),
+ scope_id = VALUES(scope_id),
description = VALUES(description),
aliases = VALUES(aliases),
examples = VALUES(examples),
@@ -420,7 +363,6 @@ ON DUPLICATE KEY UPDATE
/* =========================================================
5. 文档画像配置
- 说明:如果系统已自动生成画像,这里会覆盖为更适合验收的手工画像。
========================================================= */
INSERT INTO super_agent_document_profile (
@@ -433,7 +375,7 @@ VALUES
(@base_id + 401, @doc_o2_provider_pdf_id, 1, 'O2 固定解析样例,用于验证 Document Mind 解析、layout、表格、FIGURE block、bbox、artifact 和后续 RAG 产物联动。', 'spec', '["Document Mind解析","layout","表格解析","FIGURE block","bbox","artifact"]', '["O2 固定样例中的表格能否被识别成结构化表格","这份 PDF 样例是否包含图示或图片区域"]', 0, 1, 1, 0, 'manual', 2, NULL, NOW(), NOW(), 1),
(@base_id + 402, @doc_o2_ocr_pdf_id, 1, 'O2 图片型 PDF OCR 样例,用于验证扫描 PDF 的 OCR 文本、页面图片、表格图片、bbox overlay 和解析观测。', 'spec', '["图片型PDF OCR","PAGE_IMAGE","TABLE_IMAGE","bbox overlay","解析观测"]', '["图片型 PDF OCR 是否识别到了编号条款和业务关键词"]', 0, 1, 1, 0, 'manual', 2, NULL, NOW(), NOW(), 1),
(@base_id + 403, @doc_o2_ocr_png_id, 1, 'O2 PNG 图片 OCR 样例,用于验证图片文件进入解析主链路并识别蓝桥订单、RAG-O2-20260630 和支付回调延迟等关键短语。', 'spec', '["PNG OCR","图片文本","蓝桥订单","RAG-O2-20260630","支付回调延迟"]', '["O2 扫描 OCR 样例里有没有识别到蓝桥订单 7391"]', 0, 1, 1, 0, 'manual', 2, NULL, NOW(), NOW(), 1),
-(@base_id + 404, @doc_xinglian_id, 1, '星联智服客服平台上线运营手册,覆盖需求澄清、知识治理、机器人策略设计、灰度验证、生产发布、上线观察、故障应急和运营质量评估。', 'manual', '["客服平台上线","知识治理","机器人策略","灰度验证","上线观察","故障处理","运营质量评估"]', '["检索命中率突然下降的可能原因都有哪些","人工转接率异常升高检查顺序是什么","上线观察与值班规则中观察时长有哪些"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1),
+(@base_id + 404, @doc_xinglian_id, 1, '星联智服客服平台上线运营手册,覆盖需求澄清、知识治理、机器人策略设计、灰度验证、生产发布、上线观察、故障处置和运营质量评估。', 'manual', '["客服平台上线","知识治理","机器人策略","灰度验证","上线观察","故障处理","运营质量评估"]', '["检索命中率突然下降的可能原因都有哪些","人工转接率异常升高检查顺序是什么","上线观察与值班规则中观察时长有哪些"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1),
(@base_id + 405, @doc_release_id, 1, '生产环境发布与回滚操作规范,覆盖发布暂停原则、默认灰度节奏、强制回滚条件、NovaRAG 召回成功率和发布风险控制。', 'rule', '["生产发布","灰度节奏","发布暂停","强制回滚","NovaRAG召回成功率","风险控制"]', '["生产发布默认灰度节奏分几个阶段","强制回滚条件有哪些","哪些情况下默认动作是暂停发布"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1),
(@base_id + 406, @doc_incident_id, 1, '核心业务系统故障应急响应预案,覆盖故障分级、NovaRAG 检索服务降级、应急处置顺序和升级边界。', 'troubleshooting', '["故障应急","故障分级","NovaRAG降级","检索服务降级","人工转接激增"]', '["NovaRAG 检索服务降级时按什么顺序处理","连续 15 分钟无法返回检索结果故障等级怎么判断"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1),
(@base_id + 407, @doc_data_policy_id, 1, '客户数据分级与访问控制管理制度,覆盖数据等级、L4 高敏感数据访问、审批层级、导出限制、日志保存和审计要求。', 'rule', '["客户数据分级","L4高敏感","访问审批","DataCleanRoom","日志保存","表格问答"]', '["L4 高敏感信息的审批要求和默认有效期是什么","L3 和 L4 数据的日志保存期限分别是多少"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1),
@@ -446,7 +388,7 @@ VALUES
(@base_id + 414, @doc_data_graph_spec_id, 1, 'O6 客户数据访问控制规范 A,用于验证 DataAccessGuard、数据治理负责人、信息安全部和访问台账 community。', 'rule', '["DataAccessGuard","数据治理负责人","信息安全部","访问台账"]', '["DataAccessGuard 和数据治理负责人、信息安全部分别是什么关系"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1),
(@base_id + 415, @doc_data_graph_alias_id, 1, 'O6 客户数据访问控制别名 B,用于验证 DataAccessGuard、客户数据访问控制平台、客户数据 Owner 和安全复核组的别名归一与负边界。', 'rule', '["DataAccessGuard别名","客户数据Owner","安全复核组","负边界"]', '["客户数据访问控制相关的跨文档图谱社区总结是什么","DataAccessGuard 是否负责回滚演练和发布窗口管控"]', 1, 1, 1, 1, 'manual', 2, NULL, NOW(), NOW(), 1)
ON DUPLICATE KEY UPDATE
- profile_version = COALESCE(profile_version, 0) + 1,
+ profile_version = VALUES(profile_version),
document_summary = VALUES(document_summary),
document_type = VALUES(document_type),
core_topics = VALUES(core_topics),
@@ -463,59 +405,61 @@ ON DUPLICATE KEY UPDATE
/* =========================================================
6. 主题文档关联配置
- 说明:只清理本脚本引入主题下的旧跨文档关联,不清理其他业务主题。
========================================================= */
UPDATE super_agent_topic_document_relation
SET status = 0, edit_time = NOW()
-WHERE topic_code IN (
- 'o2_parse_artifact',
- 'o2_ocr_pdf',
- 'o2_ocr_png',
- 'operation_service_go_live',
- 'operation_release_rollback',
- 'operation_incident_response',
- 'operation_data_access',
- 'operation_travel_reimbursement',
- 'operation_onboarding_training',
- 'operation_raptor_summary',
- 'graph_audit_trail',
- 'graph_release_control',
- 'graph_data_access_guard',
- 'graph_multi_community_boundary'
-);
+WHERE knowledge_base_id IN (@kb_parse_id, @kb_operation_id, @kb_graph_id)
+ AND topic_id IN (
+ @topic_o2_docmind_id,
+ @topic_o2_ocr_id,
+ @topic_o2_table_bbox_id,
+ @topic_operation_xinglian_id,
+ @topic_operation_release_id,
+ @topic_operation_incident_id,
+ @topic_operation_data_id,
+ @topic_operation_travel_id,
+ @topic_operation_onboarding_id,
+ @topic_operation_raptor_id,
+ @topic_graph_audit_id,
+ @topic_graph_release_id,
+ @topic_graph_data_id,
+ @topic_graph_boundary_id
+ );
INSERT INTO super_agent_topic_document_relation (
- id, topic_code, document_id, relation_score, relation_source, reason,
+ id, knowledge_base_id, topic_id, document_id, relation_score, relation_source, reason,
create_time, edit_time, status
)
VALUES
-(@base_id + 501, 'o2_parse_artifact', @doc_o2_provider_pdf_id, 0.9800, 'manual', '该样例用于验证普通 PDF 的 Document Mind、layout、表格、FIGURE、bbox 和 artifact。', NOW(), NOW(), 1),
-(@base_id + 502, 'o2_ocr_pdf', @doc_o2_ocr_pdf_id, 0.9800, 'manual', '该样例用于验证图片型 PDF OCR、PAGE_IMAGE、TABLE_IMAGE 和 bbox overlay。', NOW(), NOW(), 1),
-(@base_id + 503, 'o2_ocr_png', @doc_o2_ocr_png_id, 0.9800, 'manual', '该样例用于验证 PNG 图片 OCR 和关键短语识别。', NOW(), NOW(), 1),
-
-(@base_id + 504, 'operation_service_go_live', @doc_xinglian_id, 0.9900, 'manual', '该手册是星联智服客服平台上线运营和 O8 主基线的核心文档。', NOW(), NOW(), 1),
-(@base_id + 505, 'operation_release_rollback', @doc_release_id, 0.9900, 'manual', '该规范集中描述生产发布、灰度节奏、发布暂停和强制回滚条件。', NOW(), NOW(), 1),
-(@base_id + 506, 'operation_release_rollback', @doc_xinglian_id, 0.6200, 'manual', '星联智服手册包含上线观察和回滚评估相关内容,可作为发布回滚跨文档对照。', NOW(), NOW(), 1),
-(@base_id + 507, 'operation_incident_response', @doc_incident_id, 0.9900, 'manual', '该预案集中描述 NovaRAG 检索服务降级和核心故障应急处理。', NOW(), NOW(), 1),
-(@base_id + 508, 'operation_incident_response', @doc_xinglian_id, 0.6500, 'manual', '星联智服手册包含检索命中率下降、回答口径不完整和人工转接率异常等故障处理章节。', NOW(), NOW(), 1),
-(@base_id + 509, 'operation_data_access', @doc_data_policy_id, 0.9900, 'manual', '该制度集中描述客户数据分级、L4 数据访问审批、导出限制和日志保存。', NOW(), NOW(), 1),
-(@base_id + 510, 'operation_travel_reimbursement', @doc_travel_id, 0.9900, 'manual', '该办法集中描述差旅住宿标准和报销审批金额阈值。', NOW(), NOW(), 1),
-(@base_id + 511, 'operation_onboarding_training', @doc_onboarding_id, 0.9900, 'manual', '该手册集中描述新员工入职培训日程和 30/60/90 天关注重点。', NOW(), NOW(), 1),
-(@base_id + 512, 'operation_raptor_summary', @doc_xinglian_id, 0.9600, 'manual', '该手册提供客服平台上线、运营监控和质量复盘主线,适合 RAPTOR 总结。', NOW(), NOW(), 1),
-(@base_id + 513, 'operation_raptor_summary', @doc_release_id, 0.9400, 'manual', '该规范提供生产发布、灰度验证和回滚评估主线,适合跨文档 RAPTOR 总结。', NOW(), NOW(), 1),
-(@base_id + 514, 'operation_raptor_summary', @doc_incident_id, 0.7200, 'manual', '该预案提供故障应急和降级处理干扰样例,用于验证跨文档总结边界。', NOW(), NOW(), 1),
-
-(@base_id + 515, 'graph_audit_trail', @doc_audit_evidence_id, 0.9900, 'manual', '该文档提供 AuditTrail 权限记录和异常权限扩散的关系证据。', NOW(), NOW(), 1),
-(@base_id + 516, 'graph_audit_trail', @doc_audit_alias_id, 0.9700, 'manual', '该文档提供审计系统与 AuditTrail 的别名、职责和负边界。', NOW(), NOW(), 1),
-(@base_id + 517, 'graph_release_control', @doc_release_graph_spec_id, 0.9900, 'manual', '该文档提供 ReleaseControl、CAB、值班 SRE 和回滚演练关系证据。', NOW(), NOW(), 1),
-(@base_id + 518, 'graph_release_control', @doc_release_graph_alias_id, 0.9700, 'manual', '该文档提供 ReleaseControl、生产发布控制台、变更评审委员会和 CAB 的别名归一证据。', NOW(), NOW(), 1),
-(@base_id + 519, 'graph_data_access_guard', @doc_data_graph_spec_id, 0.9900, 'manual', '该文档提供 DataAccessGuard、数据治理负责人、信息安全部和访问台账关系证据。', NOW(), NOW(), 1),
-(@base_id + 520, 'graph_data_access_guard', @doc_data_graph_alias_id, 0.9700, 'manual', '该文档提供 DataAccessGuard、客户数据访问控制平台、客户数据 Owner 和安全复核组的别名归一证据。', NOW(), NOW(), 1),
-(@base_id + 521, 'graph_multi_community_boundary', @doc_release_graph_spec_id, 0.9000, 'manual', '用于验证生产发布回滚 community 在多社区排序中的边界。', NOW(), NOW(), 1),
-(@base_id + 522, 'graph_multi_community_boundary', @doc_release_graph_alias_id, 0.8800, 'manual', '用于验证生产发布别名文档不会被客户数据访问问题错误选中。', NOW(), NOW(), 1),
-(@base_id + 523, 'graph_multi_community_boundary', @doc_data_graph_spec_id, 0.9000, 'manual', '用于验证客户数据访问 community 在多社区排序中的边界。', NOW(), NOW(), 1),
-(@base_id + 524, 'graph_multi_community_boundary', @doc_data_graph_alias_id, 0.8800, 'manual', '用于验证客户数据访问别名文档不会被生产发布问题错误选中。', NOW(), NOW(), 1)
+(@base_id + 501, @kb_parse_id, @topic_o2_docmind_id, @doc_o2_provider_pdf_id, 0.9800, 'manual', '该样例用于验证普通 PDF 的 Document Mind、layout、表格、FIGURE、bbox 和 artifact。', NOW(), NOW(), 1),
+(@base_id + 502, @kb_parse_id, @topic_o2_ocr_id, @doc_o2_ocr_pdf_id, 0.9800, 'manual', '该样例用于验证图片型 PDF OCR、PAGE_IMAGE、TABLE_IMAGE 和 bbox overlay。', NOW(), NOW(), 1),
+(@base_id + 503, @kb_parse_id, @topic_o2_ocr_id, @doc_o2_ocr_png_id, 0.9800, 'manual', '该样例用于验证 PNG 图片 OCR 和关键短语识别。', NOW(), NOW(), 1),
+(@base_id + 504, @kb_parse_id, @topic_o2_table_bbox_id, @doc_o2_provider_pdf_id, 0.9600, 'manual', '该样例用于验证结构化表格、table bbox 和页面定位产物。', NOW(), NOW(), 1),
+(@base_id + 505, @kb_parse_id, @topic_o2_table_bbox_id, @doc_o2_ocr_pdf_id, 0.9400, 'manual', '该样例用于验证 OCR 表格图片和 TABLE_IMAGE 产物。', NOW(), NOW(), 1),
+
+(@base_id + 506, @kb_operation_id, @topic_operation_xinglian_id, @doc_xinglian_id, 0.9900, 'manual', '该手册是星联智服客服平台上线运营和 O8 主基线的核心文档。', NOW(), NOW(), 1),
+(@base_id + 507, @kb_operation_id, @topic_operation_release_id, @doc_release_id, 0.9900, 'manual', '该规范集中描述生产发布、灰度节奏、发布暂停和强制回滚条件。', NOW(), NOW(), 1),
+(@base_id + 508, @kb_operation_id, @topic_operation_release_id, @doc_xinglian_id, 0.6200, 'manual', '星联智服手册包含上线观察和回滚评估相关内容,可作为发布回滚跨文档对照。', NOW(), NOW(), 1),
+(@base_id + 509, @kb_operation_id, @topic_operation_incident_id, @doc_incident_id, 0.9900, 'manual', '该预案集中描述 NovaRAG 检索服务降级和核心故障应急处理。', NOW(), NOW(), 1),
+(@base_id + 510, @kb_operation_id, @topic_operation_incident_id, @doc_xinglian_id, 0.6500, 'manual', '星联智服手册包含检索命中率下降、回答口径不完整和人工转接率异常等故障处理章节。', NOW(), NOW(), 1),
+(@base_id + 511, @kb_operation_id, @topic_operation_data_id, @doc_data_policy_id, 0.9900, 'manual', '该制度集中描述客户数据分级、L4 数据访问审批、导出限制和日志保存。', NOW(), NOW(), 1),
+(@base_id + 512, @kb_operation_id, @topic_operation_travel_id, @doc_travel_id, 0.9900, 'manual', '该办法集中描述差旅住宿标准和报销审批金额阈值。', NOW(), NOW(), 1),
+(@base_id + 513, @kb_operation_id, @topic_operation_onboarding_id, @doc_onboarding_id, 0.9900, 'manual', '该手册集中描述新员工入职培训日程和 30/60/90 天关注重点。', NOW(), NOW(), 1),
+(@base_id + 514, @kb_operation_id, @topic_operation_raptor_id, @doc_xinglian_id, 0.9600, 'manual', '该手册提供客服平台上线、运营监控和质量复盘主线,适合 RAPTOR 总结。', NOW(), NOW(), 1),
+(@base_id + 515, @kb_operation_id, @topic_operation_raptor_id, @doc_release_id, 0.9400, 'manual', '该规范提供生产发布、灰度验证和回滚评估主线,适合跨文档 RAPTOR 总结。', NOW(), NOW(), 1),
+(@base_id + 516, @kb_operation_id, @topic_operation_raptor_id, @doc_incident_id, 0.7200, 'manual', '该预案提供故障应急和降级处理干扰样例,用于验证跨文档总结边界。', NOW(), NOW(), 1),
+
+(@base_id + 517, @kb_graph_id, @topic_graph_audit_id, @doc_audit_evidence_id, 0.9900, 'manual', '该文档提供 AuditTrail 权限记录和异常权限扩散的关系证据。', NOW(), NOW(), 1),
+(@base_id + 518, @kb_graph_id, @topic_graph_audit_id, @doc_audit_alias_id, 0.9700, 'manual', '该文档提供审计系统与 AuditTrail 的别名、职责和负边界。', NOW(), NOW(), 1),
+(@base_id + 519, @kb_graph_id, @topic_graph_release_id, @doc_release_graph_spec_id, 0.9900, 'manual', '该文档提供 ReleaseControl、CAB、值班 SRE 和回滚演练关系证据。', NOW(), NOW(), 1),
+(@base_id + 520, @kb_graph_id, @topic_graph_release_id, @doc_release_graph_alias_id, 0.9700, 'manual', '该文档提供 ReleaseControl、生产发布控制台、变更评审委员会和 CAB 的别名归一证据。', NOW(), NOW(), 1),
+(@base_id + 521, @kb_graph_id, @topic_graph_data_id, @doc_data_graph_spec_id, 0.9900, 'manual', '该文档提供 DataAccessGuard、数据治理负责人、信息安全部和访问台账关系证据。', NOW(), NOW(), 1),
+(@base_id + 522, @kb_graph_id, @topic_graph_data_id, @doc_data_graph_alias_id, 0.9700, 'manual', '该文档提供 DataAccessGuard、客户数据访问控制平台、客户数据 Owner 和安全复核组的别名归一证据。', NOW(), NOW(), 1),
+(@base_id + 523, @kb_graph_id, @topic_graph_boundary_id, @doc_release_graph_spec_id, 0.9000, 'manual', '用于验证生产发布回滚 community 在多社区排序中的边界。', NOW(), NOW(), 1),
+(@base_id + 524, @kb_graph_id, @topic_graph_boundary_id, @doc_release_graph_alias_id, 0.8800, 'manual', '用于验证生产发布别名文档不会被客户数据访问问题错误选中。', NOW(), NOW(), 1),
+(@base_id + 525, @kb_graph_id, @topic_graph_boundary_id, @doc_data_graph_spec_id, 0.9000, 'manual', '用于验证客户数据访问 community 在多社区排序中的边界。', NOW(), NOW(), 1),
+(@base_id + 526, @kb_graph_id, @topic_graph_boundary_id, @doc_data_graph_alias_id, 0.8800, 'manual', '用于验证客户数据访问别名文档不会被生产发布问题错误选中。', NOW(), NOW(), 1)
ON DUPLICATE KEY UPDATE
relation_score = VALUES(relation_score),
relation_source = VALUES(relation_source),
@@ -528,63 +472,63 @@ ON DUPLICATE KEY UPDATE
========================================================= */
SELECT
- id,
- original_file_name,
- document_name,
- knowledge_scope_code,
- knowledge_scope_name,
- business_category,
- document_tags,
- parse_status,
- strategy_status,
- index_status,
- last_index_task_id
-FROM super_agent_document
-WHERE id IN (
- @doc_o2_provider_pdf_id,
- @doc_o2_ocr_pdf_id,
- @doc_o2_ocr_png_id,
- @doc_xinglian_id,
- @doc_release_id,
- @doc_incident_id,
- @doc_data_policy_id,
- @doc_travel_id,
- @doc_onboarding_id,
- @doc_audit_evidence_id,
- @doc_audit_alias_id,
- @doc_release_graph_spec_id,
- @doc_release_graph_alias_id,
- @doc_data_graph_spec_id,
- @doc_data_graph_alias_id
-)
-ORDER BY knowledge_scope_code, original_file_name;
+ kb.kb_name,
+ COUNT(d.id) AS doc_count
+FROM tmp_o2_o8_latest_document latest
+JOIN super_agent_document d ON d.id = latest.document_id
+JOIN tmp_o2_o8_kb kb ON kb.knowledge_base_id = d.knowledge_base_id
+GROUP BY kb.kb_name
+ORDER BY kb.kb_name;
SELECT
- scope_code,
- scope_name,
- aliases,
- sort_order,
- status
-FROM super_agent_knowledge_scope_node
-WHERE scope_code IN (@scope_parse_code, @scope_operation_code, @scope_graph_code)
-ORDER BY sort_order;
+ s.knowledge_base_id,
+ kb.base_name AS knowledge_base_name,
+ s.id AS scope_id,
+ s.scope_name,
+ s.aliases,
+ s.sort_order,
+ s.status
+FROM super_agent_knowledge_scope_node s
+JOIN super_agent_knowledge_base kb ON kb.id = s.knowledge_base_id
+WHERE s.id IN (@scope_parse_id, @scope_operation_id, @scope_graph_id)
+ORDER BY kb.base_name, s.sort_order;
SELECT
- topic_code,
- topic_name,
- scope_code,
- answer_shape,
- execution_preference,
- sort_order,
- status
-FROM super_agent_knowledge_topic_node
-WHERE scope_code IN (@scope_parse_code, @scope_operation_code, @scope_graph_code)
-ORDER BY scope_code, sort_order;
+ t.knowledge_base_id,
+ kb.base_name AS knowledge_base_name,
+ t.id AS topic_id,
+ t.topic_name,
+ t.scope_id,
+ s.scope_name,
+ t.answer_shape,
+ t.execution_preference,
+ t.sort_order,
+ t.status
+FROM super_agent_knowledge_topic_node t
+JOIN super_agent_knowledge_base kb ON kb.id = t.knowledge_base_id
+JOIN super_agent_knowledge_scope_node s ON s.id = t.scope_id
+WHERE t.id IN (
+ @topic_o2_docmind_id,
+ @topic_o2_ocr_id,
+ @topic_o2_table_bbox_id,
+ @topic_operation_xinglian_id,
+ @topic_operation_release_id,
+ @topic_operation_incident_id,
+ @topic_operation_data_id,
+ @topic_operation_travel_id,
+ @topic_operation_onboarding_id,
+ @topic_operation_raptor_id,
+ @topic_graph_audit_id,
+ @topic_graph_release_id,
+ @topic_graph_data_id,
+ @topic_graph_boundary_id
+)
+ORDER BY kb.base_name, s.sort_order, t.sort_order;
SELECT
p.document_id,
- d.document_name,
- d.knowledge_scope_code,
+ d.knowledge_base_name,
+ d.original_file_name,
p.document_type,
p.profile_status,
p.graph_friendly,
@@ -593,7 +537,7 @@ SELECT
p.supports_graph_assist,
p.status
FROM super_agent_document_profile p
-LEFT JOIN super_agent_document d ON d.id = p.document_id
+JOIN super_agent_document d ON d.id = p.document_id
WHERE p.document_id IN (
@doc_o2_provider_pdf_id,
@doc_o2_ocr_pdf_id,
@@ -611,37 +555,68 @@ WHERE p.document_id IN (
@doc_data_graph_spec_id,
@doc_data_graph_alias_id
)
-ORDER BY d.knowledge_scope_code, d.original_file_name;
+ORDER BY d.knowledge_base_name, d.original_file_name;
SELECT
- r.topic_code,
+ kb.base_name AS knowledge_base_name,
+ s.scope_name,
t.topic_name,
- t.scope_code,
- r.document_id,
- d.document_name,
+ d.original_file_name,
r.relation_score,
r.relation_source,
r.reason,
r.status
FROM super_agent_topic_document_relation r
-LEFT JOIN super_agent_knowledge_topic_node t ON t.topic_code = r.topic_code
-LEFT JOIN super_agent_document d ON d.id = r.document_id
-WHERE r.topic_code IN (
- 'o2_parse_artifact',
- 'o2_ocr_pdf',
- 'o2_ocr_png',
- 'operation_service_go_live',
- 'operation_release_rollback',
- 'operation_incident_response',
- 'operation_data_access',
- 'operation_travel_reimbursement',
- 'operation_onboarding_training',
- 'operation_raptor_summary',
- 'graph_audit_trail',
- 'graph_release_control',
- 'graph_data_access_guard',
- 'graph_multi_community_boundary'
-)
-ORDER BY t.scope_code, t.sort_order, r.relation_score DESC;
+JOIN super_agent_knowledge_base kb ON kb.id = r.knowledge_base_id
+JOIN super_agent_knowledge_topic_node t ON t.id = r.topic_id
+JOIN super_agent_knowledge_scope_node s ON s.id = t.scope_id
+JOIN super_agent_document d ON d.id = r.document_id
+WHERE r.knowledge_base_id IN (@kb_parse_id, @kb_operation_id, @kb_graph_id)
+ AND r.topic_id IN (
+ @topic_o2_docmind_id,
+ @topic_o2_ocr_id,
+ @topic_o2_table_bbox_id,
+ @topic_operation_xinglian_id,
+ @topic_operation_release_id,
+ @topic_operation_incident_id,
+ @topic_operation_data_id,
+ @topic_operation_travel_id,
+ @topic_operation_onboarding_id,
+ @topic_operation_raptor_id,
+ @topic_graph_audit_id,
+ @topic_graph_release_id,
+ @topic_graph_data_id,
+ @topic_graph_boundary_id
+ )
+ AND r.status = 1
+ORDER BY kb.base_name, s.sort_order, t.sort_order, r.relation_score DESC, d.original_file_name;
+
+SELECT
+ r.id,
+ r.knowledge_base_id AS relation_kb,
+ t.knowledge_base_id AS topic_kb,
+ d.knowledge_base_id AS doc_kb,
+ d.original_file_name
+FROM super_agent_topic_document_relation r
+JOIN super_agent_knowledge_topic_node t ON t.id = r.topic_id
+JOIN super_agent_document d ON d.id = r.document_id
+WHERE r.status = 1
+ AND r.topic_id IN (
+ @topic_o2_docmind_id,
+ @topic_o2_ocr_id,
+ @topic_o2_table_bbox_id,
+ @topic_operation_xinglian_id,
+ @topic_operation_release_id,
+ @topic_operation_incident_id,
+ @topic_operation_data_id,
+ @topic_operation_travel_id,
+ @topic_operation_onboarding_id,
+ @topic_operation_raptor_id,
+ @topic_graph_audit_id,
+ @topic_graph_release_id,
+ @topic_graph_data_id,
+ @topic_graph_boundary_id
+ )
+ AND (r.knowledge_base_id <> t.knowledge_base_id OR r.knowledge_base_id <> d.knowledge_base_id);
COMMIT;
diff --git "a/sql/Mysql/O8-\350\257\201\346\215\256\350\272\253\344\273\275\345\255\227\346\256\265\345\242\236\351\207\217\350\204\232\346\234\254-2026-07-05.sql" "b/sql/Mysql/O8-\350\257\201\346\215\256\350\272\253\344\273\275\345\255\227\346\256\265\345\242\236\351\207\217\350\204\232\346\234\254-2026-07-05.sql"
new file mode 100644
index 0000000000000000000000000000000000000000..2dc91f0ae13bda14572b809e4a3b833ddb9bded3
--- /dev/null
+++ "b/sql/Mysql/O8-\350\257\201\346\215\256\350\272\253\344\273\275\345\255\227\346\256\265\345\242\236\351\207\217\350\204\232\346\234\254-2026-07-05.sql"
@@ -0,0 +1,16 @@
+-- O8 证据身份原则修复:为现有 super_agent_chat_retrieval_result 表补齐观测字段。
+-- 适用场景:已存在旧表结构时执行;全新初始化可直接使用 create_table_mysql.sql。
+
+ALTER TABLE super_agent_chat_retrieval_result
+ ADD COLUMN chunk_type VARCHAR(32) DEFAULT NULL COMMENT '切块类型:TEXT/LIST/TABLE/TITLE/RAPTOR_SOURCE_CHUNK等' AFTER chunk_id,
+ ADD COLUMN context_identity VARCHAR(255) DEFAULT NULL COMMENT '上下文身份:ParentBlock、GraphRAG包装、RAPTOR摘要等' AFTER chunk_char_count,
+ ADD COLUMN citation_identity VARCHAR(255) DEFAULT NULL COMMENT '真实可引用证据身份:chunk/quote/table cell/source chunk' AFTER context_identity,
+ ADD COLUMN citation_evidence_type VARCHAR(64) DEFAULT NULL COMMENT '引用证据类型:CHUNK/TABLE_CELL_OR_ROW/KG_QUOTE_SOURCE/RAPTOR_SOURCE_CHUNK/CONTEXT_ONLY' AFTER citation_identity,
+ ADD COLUMN context_only TINYINT(1) DEFAULT '0' COMMENT '是否仅为上下文,不可直接作为citation证据' AFTER citation_evidence_type,
+ ADD COLUMN source_evidence_resolved TINYINT(1) DEFAULT '0' COMMENT '是否已解析到真实可引用source evidence' AFTER context_only;
+
+CREATE INDEX idx_retrieval_result_citation_identity
+ ON super_agent_chat_retrieval_result (citation_identity);
+
+CREATE INDEX idx_retrieval_result_context_only
+ ON super_agent_chat_retrieval_result (context_only, source_evidence_resolved);
diff --git a/sql/Mysql/create_table_mysql.sql b/sql/Mysql/create_table_mysql.sql
index 31548bbb6718f8ad4961a5b117b2ec0ec40749ee..007d107e522f33a4ad8d6f80c4d66e0f4bb546c9 100644
--- a/sql/Mysql/create_table_mysql.sql
+++ b/sql/Mysql/create_table_mysql.sql
@@ -5,6 +5,9 @@ CREATE TABLE IF NOT EXISTS super_agent_chat_dialogue (
chat_mode TINYINT(1) NOT NULL DEFAULT '1' COMMENT '1:当前文档问答 2:开放式提问',
selected_document_id BIGINT DEFAULT NULL COMMENT '当前会话显式锁定的提问文档id',
selected_document_name VARCHAR(255) DEFAULT NULL COMMENT '当前会话显式锁定的提问文档名称',
+ knowledge_base_selection_mode VARCHAR(16) NOT NULL DEFAULT 'NONE' COMMENT '当前会话知识库选择模式 NONE/ALL/SELECTED',
+ selected_knowledge_base_ids_json JSON DEFAULT NULL COMMENT '当前会话已选知识库id快照',
+ selected_knowledge_base_names_json JSON DEFAULT NULL COMMENT '当前会话已选知识库名称快照',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
edit_time DATETIME DEFAULT NULL COMMENT '编辑时间',
status TINYINT(1) DEFAULT '1' COMMENT '1:正常 0:删除',
@@ -28,6 +31,10 @@ CREATE TABLE IF NOT EXISTS super_agent_chat_exchange (
finish_note TEXT DEFAULT NULL COMMENT '失败或终止说明',
first_token_latency_ms BIGINT DEFAULT NULL COMMENT '首包耗时,毫秒',
total_latency_ms BIGINT DEFAULT NULL COMMENT '总耗时,毫秒',
+ knowledge_base_selection_mode VARCHAR(16) NOT NULL DEFAULT 'NONE' COMMENT '当轮知识库选择模式 NONE/ALL/SELECTED',
+ selected_knowledge_base_ids_json JSON DEFAULT NULL COMMENT '当轮已选知识库id快照',
+ selected_knowledge_base_names_json JSON DEFAULT NULL COMMENT '当轮已选知识库名称快照',
+ retrieval_config_snapshot_json JSON DEFAULT NULL COMMENT '当轮生效RAG检索配置快照',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
edit_time DATETIME DEFAULT NULL COMMENT '编辑时间',
status TINYINT(1) DEFAULT '1' COMMENT '1:正常 0:删除',
@@ -104,6 +111,26 @@ CREATE TABLE IF NOT EXISTS GRAPH_CHECKPOINT (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Spring AI Alibaba Graph checkpoint 表';
+CREATE TABLE IF NOT EXISTS `super_agent_knowledge_base` (
+ `id` bigint NOT NULL COMMENT '主键id',
+ `base_name` varchar(128) NOT NULL COMMENT '知识库名称',
+ `description` varchar(1024) DEFAULT NULL COMMENT '知识库描述',
+ `embedding_model` varchar(128) DEFAULT NULL COMMENT '向量模型快照',
+ `retrieval_config_json` JSON DEFAULT NULL COMMENT '检索配置JSON',
+ `graph_rag_config_json` JSON DEFAULT NULL COMMENT 'GraphRAG配置JSON',
+ `raptor_config_json` JSON DEFAULT NULL COMMENT 'RAPTOR配置JSON',
+ `metadata_filter_json` JSON DEFAULT NULL COMMENT '元数据过滤配置JSON',
+ `is_default` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否默认知识库 1:是 0:否',
+ `sort_order` int DEFAULT '0' COMMENT '排序值',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `edit_time` datetime DEFAULT NULL COMMENT '编辑时间',
+ `status` tinyint(1) DEFAULT '1' COMMENT '1:正常 0:删除',
+ PRIMARY KEY (`id`),
+ KEY `idx_knowledge_base_default` (`is_default`, `status`),
+ KEY `idx_knowledge_base_sort` (`sort_order`, `id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='轻量知识库表';
+
+
CREATE TABLE IF NOT EXISTS `super_agent_document` (
`id` bigint NOT NULL COMMENT '主键id',
`document_name` varchar(255) NOT NULL COMMENT '文档名称',
@@ -124,10 +151,8 @@ CREATE TABLE IF NOT EXISTS `super_agent_document` (
`content_quality_level` tinyint DEFAULT '0' COMMENT '内容质量 0:未知 1:低 2:中 3:高',
`parse_text_path` varchar(512) DEFAULT NULL COMMENT '解析文本存储路径',
`parse_error_msg` varchar(1000) DEFAULT NULL COMMENT '解析失败原因',
- `knowledge_scope_code` varchar(64) DEFAULT NULL COMMENT '业务知识域编码,例如 oa / crm / finance',
- `knowledge_scope_name` varchar(128) DEFAULT NULL COMMENT '业务知识域名称,例如 OA系统 / CRM系统',
- `business_category` varchar(128) DEFAULT NULL COMMENT '业务分类,例如 流程 / 规则 / 操作手册',
- `document_tags` varchar(512) DEFAULT NULL COMMENT '逗号分隔标签快照',
+ `knowledge_base_id` bigint NOT NULL COMMENT '所属知识库id',
+ `knowledge_base_name` varchar(128) NOT NULL COMMENT '所属知识库名称快照',
`current_plan_id` bigint DEFAULT NULL COMMENT '当前策略方案id',
`last_parse_task_id` bigint DEFAULT NULL COMMENT '最近一次成功解析任务id',
`structure_node_count` int DEFAULT '0' COMMENT '最近一次结构化解析生成的节点数',
@@ -140,7 +165,7 @@ CREATE TABLE IF NOT EXISTS `super_agent_document` (
KEY `idx_parse_status` (`parse_status`),
KEY `idx_strategy_status` (`strategy_status`),
KEY `idx_index_status` (`index_status`),
- KEY `idx_knowledge_scope_code` (`knowledge_scope_code`),
+ KEY `idx_knowledge_base_id` (`knowledge_base_id`),
KEY `idx_current_plan_id` (`current_plan_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文档表';
@@ -716,9 +741,9 @@ CREATE TABLE IF NOT EXISTS `super_agent_raptor_node` (
CREATE TABLE IF NOT EXISTS `super_agent_knowledge_scope_node` (
`id` bigint NOT NULL COMMENT '主键id',
- `scope_code` varchar(64) NOT NULL COMMENT '知识范围编码',
+ `knowledge_base_id` bigint NOT NULL COMMENT '所属知识库id',
`scope_name` varchar(128) NOT NULL COMMENT '知识范围名称',
- `parent_scope_code` varchar(64) DEFAULT NULL COMMENT '父级知识范围编码',
+ `parent_scope_id` bigint DEFAULT NULL COMMENT '父级知识范围id',
`description` varchar(1024) DEFAULT NULL COMMENT '范围描述',
`aliases` varchar(512) DEFAULT NULL COMMENT '别名,英文逗号分隔',
`examples` text COMMENT '典型问题 JSON 数组',
@@ -727,17 +752,17 @@ CREATE TABLE IF NOT EXISTS `super_agent_knowledge_scope_node` (
`edit_time` datetime DEFAULT NULL COMMENT '编辑时间',
`status` tinyint(1) DEFAULT '1' COMMENT '1:正常 0:删除',
PRIMARY KEY (`id`),
- UNIQUE KEY `uk_scope_code` (`scope_code`),
- KEY `idx_parent_scope_code` (`parent_scope_code`),
+ KEY `idx_knowledge_base_id` (`knowledge_base_id`),
+ KEY `idx_parent_scope_id` (`parent_scope_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识范围节点表';
CREATE TABLE IF NOT EXISTS `super_agent_knowledge_topic_node` (
`id` bigint NOT NULL COMMENT '主键id',
- `topic_code` varchar(64) NOT NULL COMMENT '主题编码',
+ `knowledge_base_id` bigint NOT NULL COMMENT '所属知识库id',
`topic_name` varchar(128) NOT NULL COMMENT '主题名称',
- `scope_code` varchar(64) NOT NULL COMMENT '所属知识范围编码',
+ `scope_id` bigint NOT NULL COMMENT '所属知识范围id',
`description` varchar(1024) DEFAULT NULL COMMENT '主题描述',
`aliases` varchar(512) DEFAULT NULL COMMENT '别名,英文逗号分隔',
`examples` text COMMENT '典型问题 JSON 数组',
@@ -748,8 +773,8 @@ CREATE TABLE IF NOT EXISTS `super_agent_knowledge_topic_node` (
`edit_time` datetime DEFAULT NULL COMMENT '编辑时间',
`status` tinyint(1) DEFAULT '1' COMMENT '1:正常 0:删除',
PRIMARY KEY (`id`),
- UNIQUE KEY `uk_topic_code` (`topic_code`),
- KEY `idx_scope_code` (`scope_code`),
+ KEY `idx_knowledge_base_id` (`knowledge_base_id`),
+ KEY `idx_scope_id` (`scope_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='知识主题节点表';
@@ -782,7 +807,8 @@ CREATE TABLE IF NOT EXISTS `super_agent_document_profile` (
CREATE TABLE IF NOT EXISTS `super_agent_topic_document_relation` (
`id` bigint NOT NULL COMMENT '主键id',
- `topic_code` varchar(64) NOT NULL COMMENT '主题编码',
+ `knowledge_base_id` bigint NOT NULL COMMENT '所属知识库id',
+ `topic_id` bigint NOT NULL COMMENT '主题id',
`document_id` bigint NOT NULL COMMENT '文档id',
`relation_score` decimal(8,4) DEFAULT '0.0000' COMMENT '关联分数',
`relation_source` varchar(64) DEFAULT NULL COMMENT '关联来源 auto/manual/mixed',
@@ -791,9 +817,10 @@ CREATE TABLE IF NOT EXISTS `super_agent_topic_document_relation` (
`edit_time` datetime DEFAULT NULL COMMENT '编辑时间',
`status` tinyint(1) DEFAULT '1' COMMENT '1:正常 0:删除',
PRIMARY KEY (`id`),
- UNIQUE KEY `uk_topic_document` (`topic_code`, `document_id`),
+ UNIQUE KEY `uk_base_topic_document` (`knowledge_base_id`, `topic_id`, `document_id`),
+ KEY `idx_knowledge_base_id` (`knowledge_base_id`),
KEY `idx_document_id` (`document_id`),
- KEY `idx_topic_code` (`topic_code`),
+ KEY `idx_topic_id` (`topic_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主题文档关联表';
@@ -805,6 +832,10 @@ CREATE TABLE IF NOT EXISTS `super_agent_knowledge_route_trace` (
`question` text COMMENT '原始问题',
`rewrite_question` text COMMENT '改写问题',
`mode` varchar(32) DEFAULT NULL COMMENT '运行模式 shadow/auto',
+ `knowledge_base_selection_mode` varchar(16) DEFAULT NULL COMMENT '知识库选择模式 NONE/ALL/SELECTED',
+ `selected_knowledge_base_ids_json` JSON DEFAULT NULL COMMENT '已选知识库id快照',
+ `selected_knowledge_base_names_json` JSON DEFAULT NULL COMMENT '已选知识库名称快照',
+ `allowed_document_ids_json` JSON DEFAULT NULL COMMENT '知识库硬边界允许检索的文档id快照',
`top_scopes_json` text COMMENT '候选知识范围 JSON',
`top_topics_json` text COMMENT '候选主题 JSON',
`top_documents_json` text COMMENT '候选文档 JSON',
@@ -849,12 +880,18 @@ CREATE TABLE IF NOT EXISTS super_agent_chat_retrieval_result (
document_id BIGINT DEFAULT NULL COMMENT '文档id',
document_name VARCHAR(255) DEFAULT NULL COMMENT '文档名称',
chunk_id BIGINT DEFAULT NULL COMMENT '文档切块id',
+ chunk_type VARCHAR(32) DEFAULT NULL COMMENT '切块类型:TEXT/LIST/TABLE/TITLE/RAPTOR_SOURCE_CHUNK等',
chunk_no INT DEFAULT NULL COMMENT '切块序号',
parent_block_id BIGINT DEFAULT NULL COMMENT '父块id',
parent_block_no INT DEFAULT NULL COMMENT '父块序号',
section_path VARCHAR(512) DEFAULT NULL COMMENT '章节路径',
chunk_text_preview VARCHAR(500) DEFAULT NULL COMMENT '文档块内容预览(前500字符)',
chunk_char_count INT DEFAULT NULL COMMENT '文档块字符数',
+ context_identity VARCHAR(255) DEFAULT NULL COMMENT '上下文身份:ParentBlock、GraphRAG包装、RAPTOR摘要等',
+ citation_identity VARCHAR(255) DEFAULT NULL COMMENT '真实可引用证据身份:chunk/quote/table cell/source chunk',
+ citation_evidence_type VARCHAR(64) DEFAULT NULL COMMENT '引用证据类型:CHUNK/TABLE_CELL_OR_ROW/KG_QUOTE_SOURCE/RAPTOR_SOURCE_CHUNK/CONTEXT_ONLY',
+ context_only TINYINT(1) DEFAULT '0' COMMENT '是否仅为上下文,不可直接作为citation证据',
+ source_evidence_resolved TINYINT(1) DEFAULT '0' COMMENT '是否已解析到真实可引用source evidence',
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
edit_time DATETIME DEFAULT NULL COMMENT '编辑时间',
status TINYINT(1) DEFAULT '1' COMMENT '1:正常 0:删除',
@@ -863,7 +900,9 @@ CREATE TABLE IF NOT EXISTS super_agent_chat_retrieval_result (
KEY idx_retrieval_result_trace (trace_id),
KEY idx_retrieval_result_sub_question (exchange_id, sub_question_index),
KEY idx_retrieval_result_channel (channel_type, is_selected),
- KEY idx_retrieval_result_document (document_id, chunk_id)
+ KEY idx_retrieval_result_document (document_id, chunk_id),
+ KEY idx_retrieval_result_citation_identity (citation_identity),
+ KEY idx_retrieval_result_context_only (context_only, source_evidence_resolved)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='检索结果快照表';
CREATE TABLE IF NOT EXISTS super_agent_chat_channel_execution (
diff --git "a/sql/Mysql/\346\230\237\350\201\224\346\231\272\346\234\215\344\270\216XX200\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql" "b/sql/Mysql/\346\230\237\350\201\224\346\231\272\346\234\215\344\270\216XX200\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql"
deleted file mode 100644
index a353506b63ad5e475c680bd6b05382ebab132e21..0000000000000000000000000000000000000000
--- "a/sql/Mysql/\346\230\237\350\201\224\346\231\272\346\234\215\344\270\216XX200\347\237\245\350\257\206\350\267\257\347\224\261\345\210\235\345\247\213\345\214\226\350\204\232\346\234\254.sql"
+++ /dev/null
@@ -1,577 +0,0 @@
-/*
- 星联智服全渠道客服平台上线与运营管理手册.md
- XX-200智能网关产品手册.pdf
- 知识路由初始化脚本
-
- 使用顺序:
- 1. 先在管理台上传这两份文档。
- 2. 进入每份文档详情页,完成“确认策略方案”和“构建索引执行”。
- 3. 回到文档列表或数据库中拿到两份文档的 document_id。
- 4. 替换下面两个变量 @doc_customer_service_id / @doc_xx200_id。
- 5. 执行本脚本。
-
- 注意:
- - 本脚本会更新 super_agent_document 的知识域编码、名称、业务分类和标签。
- - 本脚本会写入知识范围、知识主题、文档画像、主题文档关联。
- - 本脚本使用 INSERT ... ON DUPLICATE KEY UPDATE,可重复执行。
- - 本脚本不会修改文档解析、策略方案、索引状态、chunk、向量库数据。
-*/
-
-START TRANSACTION;
-
-/* =========================================================
- 0. 请先替换这里的两个文档 ID
- ========================================================= */
-
-SET @doc_customer_service_id = 0; -- TODO: 替换为“星联智服全渠道客服平台上线与运营管理手册.md”的 document_id
-SET @doc_xx200_id = 0; -- TODO: 替换为“XX-200智能网关产品手册.pdf”的 document_id
-
-/*
- 如果你不确定文档 ID,可以先执行下面的查询,再把查出来的 id 填到上面变量里:
-
- SELECT id, document_name, original_file_name, index_status, last_index_task_id
- FROM super_agent_document
- WHERE status = 1
- AND (
- document_name LIKE '%星联智服%'
- OR original_file_name LIKE '%星联智服%'
- OR document_name LIKE '%XX-200%'
- OR original_file_name LIKE '%XX-200%'
- )
- ORDER BY create_time DESC;
-*/
-
-/* =========================================================
- 1. 固定配置编码
- ========================================================= */
-
-SET @scope_customer_service_code = 'customer_service_platform_ops';
-SET @scope_customer_service_name = '客服平台上线运营';
-SET @scope_xx200_code = 'xx200_gateway_product';
-SET @scope_xx200_name = 'XX-200智能网关';
-
-/*
- 这些 id 只用于新插入范围、主题、关系、画像时。
- 如果你的数据库里极端情况下已经占用了这些 id,可以把 @base_id 改成其他未使用的大整数。
-*/
-SET @base_id = 8800041600000000000;
-
-/* =========================================================
- 2. 更新两份文档主表元数据
- ========================================================= */
-
-UPDATE super_agent_document
-SET
- document_name = '星联智服全渠道客服平台上线与运营管理手册',
- knowledge_scope_code = @scope_customer_service_code,
- knowledge_scope_name = @scope_customer_service_name,
- business_category = '平台运营手册',
- document_tags = '客服平台,上线,灰度发布,生产发布,回滚,知识治理,RAG,值班,故障应急,质量评估',
- edit_time = NOW()
-WHERE id = @doc_customer_service_id
- AND status = 1;
-
-UPDATE super_agent_document
-SET
- document_name = 'XX-200智能网关产品技术手册',
- knowledge_scope_code = @scope_xx200_code,
- knowledge_scope_name = @scope_xx200_name,
- business_category = '产品技术手册',
- document_tags = 'XX-200,智能网关,边缘计算,安装部署,网络配置,协议配置,Modbus,日志查看,故障排查,工业物联网',
- edit_time = NOW()
-WHERE id = @doc_xx200_id
- AND status = 1;
-
-/* =========================================================
- 3. 知识范围配置
- ========================================================= */
-
-INSERT INTO super_agent_knowledge_scope_node (
- id, scope_code, scope_name, parent_scope_code, description, aliases, examples, sort_order,
- create_time, edit_time, status
-)
-VALUES
-(
- @base_id + 1,
- @scope_customer_service_code,
- @scope_customer_service_name,
- NULL,
- '用于承接客服平台项目上线、知识治理、灰度发布、生产发布、值班观察、故障应急、质量评估等运营管理类问题。',
- '客服平台,全渠道客服平台,星联智服,上线运营,知识治理,灰度发布',
- '["平台上线总流程有哪几个阶段","灰度验证期间要看哪些指标","什么时候需要回滚","上线后要观察多久"]',
- 10,
- NOW(), NOW(), 1
-),
-(
- @base_id + 2,
- @scope_xx200_code,
- @scope_xx200_name,
- NULL,
- '用于承接XX-200智能网关的产品规格、安装部署、网络配置、协议配置、日志查看与故障排查等产品技术问题。',
- 'XX-200,智能网关,工业网关,边缘网关,网关产品,安装部署',
- '["XX-200支持哪些协议","XX-200怎么安装部署","默认登录地址和账号是什么","双WAN怎么配置","故障日志在哪里看"]',
- 20,
- NOW(), NOW(), 1
-)
-ON DUPLICATE KEY UPDATE
- scope_name = VALUES(scope_name),
- parent_scope_code = VALUES(parent_scope_code),
- description = VALUES(description),
- aliases = VALUES(aliases),
- examples = VALUES(examples),
- sort_order = VALUES(sort_order),
- edit_time = NOW(),
- status = 1;
-
-/* =========================================================
- 4. 知识主题配置
- answer_shape 固定为 list / explain / steps
- execution_preference 固定为 retrieval / graph_assist
- ========================================================= */
-
-INSERT INTO super_agent_knowledge_topic_node (
- id, topic_code, topic_name, scope_code, description, aliases, examples,
- answer_shape, execution_preference, sort_order,
- create_time, edit_time, status
-)
-VALUES
-(
- @base_id + 101,
- 'platform_go_live_process',
- '平台上线总流程',
- @scope_customer_service_code,
- '回答客服平台项目上线从立项、知识治理、灰度验证到生产发布的整体流程。',
- '上线流程,上线步骤,项目上线,上线里程碑',
- '["平台上线总流程有哪几个阶段","项目上线要经过哪些里程碑"]',
- 'steps',
- 'retrieval',
- 10,
- NOW(), NOW(), 1
-),
-(
- @base_id + 102,
- 'knowledge_governance',
- '知识采集与治理',
- @scope_customer_service_code,
- '回答知识来源分类、知识接入前检查、知识域划分和不适合接入知识库的内容。',
- '知识治理,知识接入,知识域,知识库治理',
- '["知识接入前要检查什么","知识域应该怎么划分","哪些内容不适合接入机器人知识库"]',
- 'list',
- 'retrieval',
- 20,
- NOW(), NOW(), 1
-),
-(
- @base_id + 103,
- 'gray_release_and_rollback',
- '灰度验证与回滚',
- @scope_customer_service_code,
- '回答灰度范围、灰度期指标、禁止事项,以及哪些情况要触发回滚评估。',
- '灰度验证,灰度发布,回滚,回滚条件,灰度指标',
- '["灰度期间必须看哪些指标","什么时候需要回滚","灰度期间有哪些禁止事项"]',
- 'list',
- 'retrieval',
- 30,
- NOW(), NOW(), 1
-),
-(
- @base_id + 104,
- 'post_launch_observation',
- '上线观察与值班',
- @scope_customer_service_code,
- '回答普通版本和高风险版本的观察时长、值班安排和观察日报要求。',
- '上线观察,观察时长,值班规则,观察日报',
- '["上线后要观察多久","值班安排怎么配","观察日报至少写什么"]',
- 'list',
- 'retrieval',
- 40,
- NOW(), NOW(), 1
-),
-(
- @base_id + 105,
- 'fault_response',
- '典型故障处理',
- @scope_customer_service_code,
- '回答平台在检索命中率下降、回答不完整、转人工率升高等场景下的处理方法。',
- '故障处理,检索命中率下降,人工转接率异常,回答口径不完整',
- '["检索命中率突然下降怎么排查","回答口径不完整怎么办","人工转接率异常升高怎么查"]',
- 'steps',
- 'retrieval',
- 50,
- NOW(), NOW(), 1
-),
-(
- @base_id + 106,
- 'quality_evaluation',
- '运营质量评估',
- @scope_customer_service_code,
- '回答质量评估层次、指标定义和每周每月每季度的评审节奏。',
- '质量评估,质量指标,运营指标,复盘节奏',
- '["运营质量指标怎么分层","质量评审节奏是怎样的","常见质量指标有哪些"]',
- 'list',
- 'retrieval',
- 60,
- NOW(), NOW(), 1
-),
-(
- @base_id + 201,
- 'product_overview_spec',
- '产品概述与技术规格',
- @scope_xx200_code,
- '回答XX-200的产品简介、核心特性、处理器、内存、网络接口、串口、电源和工作环境等规格。',
- '产品概述,技术规格,核心特性,参数规格',
- '["XX-200有哪些核心特性","XX-200的技术规格是什么","支持哪些协议和接口"]',
- 'list',
- 'retrieval',
- 10,
- NOW(), NOW(), 1
-),
-(
- @base_id + 202,
- 'installation_deployment',
- '安装部署',
- @scope_xx200_code,
- '回答安装前准备、DIN导轨安装、接电、连线和浏览器访问管理界面等步骤。',
- '安装部署,安装前准备,硬件安装,上电安装',
- '["安装前要准备什么","XX-200怎么安装","上电部署步骤是什么"]',
- 'steps',
- 'retrieval',
- 20,
- NOW(), NOW(), 1
-),
-(
- @base_id + 203,
- 'initial_access_login',
- '初始访问与首次登录',
- @scope_xx200_code,
- '回答LAN1默认IP、浏览器访问地址、默认账号密码和首次登录改密要求。',
- '默认IP,默认账号,首次登录,初始配置',
- '["默认登录地址是什么","默认账号密码是什么","首次登录后密码要求是什么"]',
- 'steps',
- 'retrieval',
- 30,
- NOW(), NOW(), 1
-),
-(
- @base_id + 204,
- 'network_configuration',
- '网络配置',
- @scope_xx200_code,
- '回答LAN/WAN使用方式、双WAN负载均衡示例、DNS、健康检查和故障切换。',
- '网络配置,双WAN,静态IP,DHCP,PPPoE,负载均衡',
- '["双WAN负载均衡怎么配置","WAN支持哪些接入方式","健康检查怎么设置"]',
- 'steps',
- 'retrieval',
- 40,
- NOW(), NOW(), 1
-),
-(
- @base_id + 205,
- 'protocol_configuration',
- '协议配置',
- @scope_xx200_code,
- '回答Modbus RTU采集的串口配置、设备模板、点位定义和采集周期建议。',
- '协议配置,Modbus,RS-485,设备模板,采集点位',
- '["Modbus RTU怎么配置","温湿度传感器点位怎么建","采集周期建议多少"]',
- 'steps',
- 'retrieval',
- 50,
- NOW(), NOW(), 1
-),
-(
- @base_id + 206,
- 'troubleshooting_and_logs',
- '故障排查与日志查看',
- @scope_xx200_code,
- '回答常见故障现象、可能原因、解决方案,以及系统日志、应用日志、审计日志和导出诊断包。',
- '故障排查,日志查看,诊断包,系统日志,应用日志,审计日志',
- '["PWR灯不亮怎么处理","无法访问管理界面怎么排查","日志在哪里看","怎么导出诊断包"]',
- 'steps',
- 'retrieval',
- 60,
- NOW(), NOW(), 1
-)
-ON DUPLICATE KEY UPDATE
- topic_name = VALUES(topic_name),
- scope_code = VALUES(scope_code),
- description = VALUES(description),
- aliases = VALUES(aliases),
- examples = VALUES(examples),
- answer_shape = VALUES(answer_shape),
- execution_preference = VALUES(execution_preference),
- sort_order = VALUES(sort_order),
- edit_time = NOW(),
- status = 1;
-
-/* =========================================================
- 5. 文档画像配置
- 说明:如果系统已自动生成画像,这里会覆盖为更适合演示的手工画像。
- ========================================================= */
-
-INSERT INTO super_agent_document_profile (
- id, document_id, profile_version, document_summary, document_type, core_topics, example_questions,
- graph_friendly, supports_graph_outline, supports_item_lookup, supports_graph_assist,
- profile_source, profile_status, error_msg,
- create_time, edit_time, status
-)
-VALUES
-(
- @base_id + 301,
- @doc_customer_service_id,
- 1,
- '本手册用于规范星联智服全渠道客服平台从需求澄清、知识治理、机器人策略设计、灰度验证、生产发布、上线观察、故障应急到运营质量评估的全链路管理要求。',
- 'manual',
- '["平台上线总流程","知识采集与治理","灰度验证与回滚","上线观察与值班","典型故障处理","运营质量评估"]',
- '["平台上线总流程有哪几个阶段","灰度验证期间必须看哪些指标","什么时候需要触发回滚评估","上线后要观察多久","检索命中率突然下降怎么排查"]',
- 1, 1, 1, 1,
- 'manual',
- 2,
- NULL,
- NOW(), NOW(), 1
-),
-(
- @base_id + 302,
- @doc_xx200_id,
- 1,
- '本手册介绍XX-200智能网关的产品概述、核心特性、技术规格、安装部署、初始访问、网络配置、协议配置、常见故障排查和日志查看方式。',
- 'manual',
- '["产品概述与技术规格","安装部署","初始访问与首次登录","网络配置","协议配置","故障排查与日志查看"]',
- '["XX-200支持哪些工业协议","默认登录地址和账号密码是什么","安装前要准备什么","双WAN负载均衡怎么配置","Modbus RTU怎么配置","日志在哪里看"]',
- 1, 1, 1, 1,
- 'manual',
- 2,
- NULL,
- NOW(), NOW(), 1
-)
-ON DUPLICATE KEY UPDATE
- profile_version = COALESCE(profile_version, 0) + 1,
- document_summary = VALUES(document_summary),
- document_type = VALUES(document_type),
- core_topics = VALUES(core_topics),
- example_questions = VALUES(example_questions),
- graph_friendly = VALUES(graph_friendly),
- supports_graph_outline = VALUES(supports_graph_outline),
- supports_item_lookup = VALUES(supports_item_lookup),
- supports_graph_assist = VALUES(supports_graph_assist),
- profile_source = VALUES(profile_source),
- profile_status = VALUES(profile_status),
- error_msg = VALUES(error_msg),
- edit_time = NOW(),
- status = 1;
-
-/* =========================================================
- 6. 主题文档关联配置
- 说明:先清理这些主题下的旧跨文档关联,再写入目标关联。
- ========================================================= */
-
-UPDATE super_agent_topic_document_relation
-SET status = 0, edit_time = NOW()
-WHERE topic_code IN (
- 'platform_go_live_process',
- 'knowledge_governance',
- 'gray_release_and_rollback',
- 'post_launch_observation',
- 'fault_response',
- 'quality_evaluation'
-)
-AND document_id <> @doc_customer_service_id;
-
-UPDATE super_agent_topic_document_relation
-SET status = 0, edit_time = NOW()
-WHERE topic_code IN (
- 'product_overview_spec',
- 'installation_deployment',
- 'initial_access_login',
- 'network_configuration',
- 'protocol_configuration',
- 'troubleshooting_and_logs'
-)
-AND document_id <> @doc_xx200_id;
-
-INSERT INTO super_agent_topic_document_relation (
- id, topic_code, document_id, relation_score, relation_source, reason,
- create_time, edit_time, status
-)
-VALUES
-(
- @base_id + 401,
- 'platform_go_live_process',
- @doc_customer_service_id,
- 0.9800,
- 'manual',
- '该手册完整描述了客服平台从立项到上线观察的全流程。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 402,
- 'knowledge_governance',
- @doc_customer_service_id,
- 0.9700,
- 'manual',
- '该手册包含知识来源分类、知识接入检查和知识域划分建议。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 403,
- 'gray_release_and_rollback',
- @doc_customer_service_id,
- 0.9800,
- 'manual',
- '该手册明确给出了灰度范围、灰度指标、禁止事项和回滚触发条件。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 404,
- 'post_launch_observation',
- @doc_customer_service_id,
- 0.9700,
- 'manual',
- '该手册包含上线观察时长、值班安排和观察日报模板。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 405,
- 'fault_response',
- @doc_customer_service_id,
- 0.9600,
- 'manual',
- '该手册给出了检索命中率下降、回答不完整和人工转接异常的处理方法。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 406,
- 'quality_evaluation',
- @doc_customer_service_id,
- 0.9500,
- 'manual',
- '该手册明确了质量评估层次、指标定义和评审节奏。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 501,
- 'product_overview_spec',
- @doc_xx200_id,
- 0.9800,
- 'manual',
- '该手册集中描述了XX-200的产品简介、核心特性和完整技术规格。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 502,
- 'installation_deployment',
- @doc_xx200_id,
- 0.9800,
- 'manual',
- '该手册包含安装前准备、硬件安装和上电部署步骤。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 503,
- 'initial_access_login',
- @doc_xx200_id,
- 0.9700,
- 'manual',
- '该手册明确给出了默认IP、访问地址、默认账号密码和首次登录改密要求。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 504,
- 'network_configuration',
- @doc_xx200_id,
- 0.9800,
- 'manual',
- '该手册包含双WAN、静态IP、DHCP、DNS、健康检查和故障切换配置。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 505,
- 'protocol_configuration',
- @doc_xx200_id,
- 0.9700,
- 'manual',
- '该手册包含Modbus RTU串口配置、设备模板和点位定义。',
- NOW(), NOW(), 1
-),
-(
- @base_id + 506,
- 'troubleshooting_and_logs',
- @doc_xx200_id,
- 0.9800,
- 'manual',
- '该手册包含常见故障现象、可能原因、解决方案和日志查看方式。',
- NOW(), NOW(), 1
-)
-ON DUPLICATE KEY UPDATE
- relation_score = VALUES(relation_score),
- relation_source = VALUES(relation_source),
- reason = VALUES(reason),
- edit_time = NOW(),
- status = 1;
-
-/* =========================================================
- 7. 执行后检查
- ========================================================= */
-
-SELECT
- id,
- document_name,
- knowledge_scope_code,
- knowledge_scope_name,
- business_category,
- document_tags,
- index_status,
- last_index_task_id
-FROM super_agent_document
-WHERE id IN (@doc_customer_service_id, @doc_xx200_id);
-
-SELECT
- scope_code,
- scope_name,
- aliases,
- sort_order,
- status
-FROM super_agent_knowledge_scope_node
-WHERE scope_code IN (@scope_customer_service_code, @scope_xx200_code)
-ORDER BY sort_order;
-
-SELECT
- topic_code,
- topic_name,
- scope_code,
- answer_shape,
- execution_preference,
- sort_order,
- status
-FROM super_agent_knowledge_topic_node
-WHERE scope_code IN (@scope_customer_service_code, @scope_xx200_code)
-ORDER BY scope_code, sort_order;
-
-SELECT
- r.topic_code,
- t.topic_name,
- r.document_id,
- d.document_name,
- r.relation_score,
- r.relation_source,
- r.reason,
- r.status
-FROM super_agent_topic_document_relation r
-LEFT JOIN super_agent_knowledge_topic_node t ON t.topic_code = r.topic_code
-LEFT JOIN super_agent_document d ON d.id = r.document_id
-WHERE r.topic_code IN (
- 'platform_go_live_process',
- 'knowledge_governance',
- 'gray_release_and_rollback',
- 'post_launch_observation',
- 'fault_response',
- 'quality_evaluation',
- 'product_overview_spec',
- 'installation_deployment',
- 'initial_access_login',
- 'network_configuration',
- 'protocol_configuration',
- 'troubleshooting_and_logs'
-)
-ORDER BY t.scope_code, t.sort_order, r.relation_score DESC;
-
-COMMIT;
diff --git "a/sql/Mysql/\346\270\205\347\251\272\350\241\250\346\225\260\346\215\256.sql" "b/sql/Mysql/\346\270\205\347\251\272\350\241\250\346\225\260\346\215\256.sql"
deleted file mode 100644
index 1356971a92d157e0f63a86d941f0be4520579f98..0000000000000000000000000000000000000000
--- "a/sql/Mysql/\346\270\205\347\251\272\350\241\250\346\225\260\346\215\256.sql"
+++ /dev/null
@@ -1,37 +0,0 @@
-SET FOREIGN_KEY_CHECKS = 0;
-
-TRUNCATE TABLE `super_agent_chat_dialogue`;
-TRUNCATE TABLE `super_agent_chat_exchange`;
-TRUNCATE TABLE `super_agent_chat_memory_summary`;
-TRUNCATE TABLE `super_agent_chat_exchange_trace_stage`;
-TRUNCATE TABLE `GRAPH_THREAD`;
-TRUNCATE TABLE `GRAPH_CHECKPOINT`;
-TRUNCATE TABLE `super_agent_document`;
-TRUNCATE TABLE `super_agent_document_strategy_plan`;
-TRUNCATE TABLE `super_agent_document_strategy_step`;
-TRUNCATE TABLE `super_agent_document_task`;
-TRUNCATE TABLE `super_agent_document_task_log`;
-TRUNCATE TABLE `super_agent_document_parse_artifact`;
-TRUNCATE TABLE `super_agent_document_block`;
-TRUNCATE TABLE `super_agent_document_table`;
-TRUNCATE TABLE `super_agent_document_table_column`;
-TRUNCATE TABLE `super_agent_document_table_row`;
-TRUNCATE TABLE `super_agent_document_table_cell`;
-TRUNCATE TABLE `super_agent_document_structure_node`;
-TRUNCATE TABLE `super_agent_document_parent_block`;
-TRUNCATE TABLE `super_agent_document_chunk`;
-TRUNCATE TABLE `super_agent_kg_entity`;
-TRUNCATE TABLE `super_agent_kg_relation`;
-TRUNCATE TABLE `super_agent_kg_evidence`;
-TRUNCATE TABLE `super_agent_kg_community`;
-TRUNCATE TABLE `super_agent_raptor_node`;
-TRUNCATE TABLE `super_agent_knowledge_scope_node`;
-TRUNCATE TABLE `super_agent_knowledge_topic_node`;
-TRUNCATE TABLE `super_agent_document_profile`;
-TRUNCATE TABLE `super_agent_topic_document_relation`;
-TRUNCATE TABLE `super_agent_knowledge_route_trace`;
-TRUNCATE TABLE `super_agent_chat_retrieval_result`;
-TRUNCATE TABLE `super_agent_chat_channel_execution`;
-TRUNCATE TABLE `super_agent_chat_stage_benchmark`;
-
-SET FOREIGN_KEY_CHECKS = 1;
\ No newline at end of file
diff --git "a/sql/PostgresSql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\346\270\205\347\220\206\346\227\247PG\345\220\221\351\207\217\346\225\260\346\215\256.sql" "b/sql/PostgresSql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\346\270\205\347\220\206\346\227\247PG\345\220\221\351\207\217\346\225\260\346\215\256.sql"
deleted file mode 100644
index f443d8af879b31026e34d6fb8f5a2a45ff36ffe8..0000000000000000000000000000000000000000
--- "a/sql/PostgresSql/O2-O8-RAG\345\256\214\346\225\264\346\265\213\350\257\225\346\270\205\347\220\206\346\227\247PG\345\220\221\351\207\217\346\225\260\346\215\256.sql"
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- O2-O8 RAG 完整测试:按当前 15 份文档物理清理旧 PG 向量数据
-
- 用途:
- - MySQL 已经删除旧数据并重新上传了 15 份测试文档。
- - PostgreSQL 的 public.super_agent_document_embedding 和 public.super_agent_raptor_embedding
- 还残留旧 document_id 的向量数据。
- - 本脚本只保留下面 15 个 document_id 对应的数据,其他 document_id 的数据全部物理删除。
-
- 文档 ID 来源:
- - 已按用户提供的 MySQL 查询从 super_agent_document 获取。
- - 查询条件为 status = 1 且 original_file_name in 15 份必传样例。
-
- 执行方式:
- PGPASSWORD=postgres psql -h 127.0.0.1 -p 5432 -U postgres -d super_agent_pgvector \
- -f sql/PostgresSql/O2-O8-RAG完整测试清理旧PG向量数据.sql
-
- 注意:
- - 本脚本只操作 PostgreSQL,不改 MySQL。
- - 本脚本是物理 DELETE,不是软删除。
- - super_agent_raptor_embedding 中 document_id = 0 的 dataset-level 旧数据也会被删除;
- 因为它不属于这 15 个 document_id。
-*/
-
-BEGIN;
-
-CREATE TEMP TABLE keep_document_ids (
- document_id BIGINT PRIMARY KEY,
- file_name TEXT NOT NULL
-) ON COMMIT DROP;
-
-INSERT INTO keep_document_ids (document_id, file_name) VALUES
-(2296737919064432640, 'O2-provider-artifact验收样例.pdf'),
-(2296737919064433640, 'O2-扫描OCR验收样例-图片型PDF.pdf'),
-(2296737919064434525, 'O2-扫描OCR验收样例-文字截图.png'),
-(2296738159582601310, 'O6多社区排序-客户数据访问控制别名B.md'),
-(2296738125222865466, 'O6多社区排序-客户数据访问控制规范A.md'),
-(2296738090863129667, 'O6多社区排序-生产发布回滚别名B.md'),
-(2296738056503393982, 'O6多社区排序-生产发布回滚规范A.md'),
-(2296738056503388553, 'O6跨文档图谱-审计系统别名说明B.md'),
-(2296738022143652966, 'O6跨文档图谱-审计证据规范A.md'),
-(2296737953424176918, '客户数据分级与访问控制管理制度.md'),
-(2296737987783912016, '差旅与费用报销管理办法.md'),
-(2296737919064435493, '星联智服全渠道客服平台上线与运营管理手册.md'),
-(2296737953424172808, '核心业务系统故障应急响应预案.md'),
-(2296737987783916947, '澄星智能新员工入职培训手册.md'),
-(2296737919064439185, '生产环境发布与回滚操作规范.md');
-
-/* =========================================================
- 1. 执行前预览
- ========================================================= */
-
-SELECT
- 'keep_document_ids' AS item,
- COUNT(*) AS count
-FROM keep_document_ids;
-
-SELECT
- 'super_agent_document_embedding_before' AS item,
- COUNT(*) AS total_rows,
- COUNT(*) FILTER (WHERE document_id IN (SELECT document_id FROM keep_document_ids)) AS keep_rows,
- COUNT(*) FILTER (WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)) AS delete_rows
-FROM public.super_agent_document_embedding;
-
-SELECT
- 'super_agent_raptor_embedding_before' AS item,
- COUNT(*) AS total_rows,
- COUNT(*) FILTER (WHERE document_id IN (SELECT document_id FROM keep_document_ids)) AS keep_rows,
- COUNT(*) FILTER (WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)) AS delete_rows
-FROM public.super_agent_raptor_embedding;
-
-SELECT
- 'document_embedding_delete_by_document_id' AS item,
- document_id,
- COUNT(*) AS rows_to_delete
-FROM public.super_agent_document_embedding
-WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)
-GROUP BY document_id
-ORDER BY document_id;
-
-SELECT
- 'raptor_embedding_delete_by_document_id' AS item,
- document_id,
- COUNT(*) AS rows_to_delete
-FROM public.super_agent_raptor_embedding
-WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)
-GROUP BY document_id
-ORDER BY document_id;
-
-/* =========================================================
- 2. 物理删除旧数据
- ========================================================= */
-
-DELETE FROM public.super_agent_document_embedding
-WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids);
-
-DELETE FROM public.super_agent_raptor_embedding
-WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids);
-
-/* =========================================================
- 3. 删除后复核
- ========================================================= */
-
-SELECT
- 'super_agent_document_embedding_after' AS item,
- COUNT(*) AS total_rows,
- COUNT(*) FILTER (WHERE document_id IN (SELECT document_id FROM keep_document_ids)) AS keep_rows,
- COUNT(*) FILTER (WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)) AS old_rows_remaining
-FROM public.super_agent_document_embedding;
-
-SELECT
- 'super_agent_raptor_embedding_after' AS item,
- COUNT(*) AS total_rows,
- COUNT(*) FILTER (WHERE document_id IN (SELECT document_id FROM keep_document_ids)) AS keep_rows,
- COUNT(*) FILTER (WHERE document_id NOT IN (SELECT document_id FROM keep_document_ids)) AS old_rows_remaining
-FROM public.super_agent_raptor_embedding;
-
-SELECT
- 'document_embedding_keep_by_document_id' AS item,
- e.document_id,
- k.file_name,
- COUNT(*) AS rows_kept
-FROM public.super_agent_document_embedding e
-JOIN keep_document_ids k ON k.document_id = e.document_id
-GROUP BY e.document_id, k.file_name
-ORDER BY k.file_name;
-
-SELECT
- 'raptor_embedding_keep_by_document_id' AS item,
- e.document_id,
- k.file_name,
- COUNT(*) AS rows_kept
-FROM public.super_agent_raptor_embedding e
-JOIN keep_document_ids k ON k.document_id = e.document_id
-GROUP BY e.document_id, k.file_name
-ORDER BY k.file_name;
-
-COMMIT;
-
-/*
- 可选:删除后如果要让备份文件更小,建议执行 VACUUM。
-
- 普通回收统计:
- VACUUM (ANALYZE) public.super_agent_document_embedding;
- VACUUM (ANALYZE) public.super_agent_raptor_embedding;
-
- 强制压缩物理文件,需低峰期执行,会锁表:
- VACUUM FULL public.super_agent_document_embedding;
- VACUUM FULL public.super_agent_raptor_embedding;
-*/
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/controller/BusinessChatController.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/controller/BusinessChatController.java
index dd57f71de83df13aee1e731ac3c89fbd9ec1e3ec..5ab87cd04ed80840e9a5ba79c29933df20e3c643 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/controller/BusinessChatController.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/controller/BusinessChatController.java
@@ -15,6 +15,7 @@ import org.javaup.ai.chatagent.model.KnowledgeDocumentOptionView;
import org.javaup.ai.chatagent.model.RetrievalResultView;
import org.javaup.ai.chatagent.model.StageBenchmarkView;
import org.javaup.ai.chatagent.service.BusinessChatService;
+import org.javaup.ai.manage.vo.KnowledgeBaseOptionVo;
import org.javaup.ai.chatagent.vo.ConversationResetVo;
import org.javaup.ai.chatagent.vo.ConversationSessionListVo;
import org.javaup.ai.chatagent.vo.ConversationStopVo;
@@ -49,6 +50,11 @@ public class BusinessChatController {
return ApiResponse.ok(businessChatService.listKnowledgeDocumentOptions());
}
+ @PostMapping("/knowledge-base/options")
+ public ApiResponse> knowledgeBaseOptions() {
+ return ApiResponse.ok(businessChatService.listKnowledgeBaseOptions());
+ }
+
@PostMapping("/session/stop")
public ApiResponse stop(@Valid @RequestBody ConversationIdentityDto dto) {
return ApiResponse.ok(businessChatService.stopConversation(dto.getConversationId()));
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatDialogue.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatDialogue.java
index b0b18dacfc63848e20cd5ed05ba6a41bb406a444..03f21bb225e8ddb6ce6b10991dd3fdc935ee084a 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatDialogue.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatDialogue.java
@@ -40,4 +40,13 @@ public class SuperAgentChatDialogue extends BaseTableData {
@TableField("selected_document_name")
private String selectedDocumentName;
+
+ @TableField("knowledge_base_selection_mode")
+ private String knowledgeBaseSelectionMode;
+
+ @TableField("selected_knowledge_base_ids_json")
+ private String selectedKnowledgeBaseIdsJson;
+
+ @TableField("selected_knowledge_base_names_json")
+ private String selectedKnowledgeBaseNamesJson;
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatExchange.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatExchange.java
index 3e5b4cbf1abe89e1c229e2f91a0d54df6b9b9054..ea5b3cd3bf0b425c7f0ae04992b0900bbbea6f5e 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatExchange.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatExchange.java
@@ -61,4 +61,16 @@ public class SuperAgentChatExchange extends BaseTableData {
@TableField("total_latency_ms")
private Long totalResponseTimeMs;
+
+ @TableField("knowledge_base_selection_mode")
+ private String knowledgeBaseSelectionMode;
+
+ @TableField("selected_knowledge_base_ids_json")
+ private String selectedKnowledgeBaseIdsJson;
+
+ @TableField("selected_knowledge_base_names_json")
+ private String selectedKnowledgeBaseNamesJson;
+
+ @TableField("retrieval_config_snapshot_json")
+ private String retrievalConfigSnapshotJson;
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatRetrievalResult.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatRetrievalResult.java
index d81315e1cfaf7c1b7c81185881641060bd2b561f..4342ce110f83cd48fdabc9212be7d4e17d95e2e4 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatRetrievalResult.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/data/SuperAgentChatRetrievalResult.java
@@ -97,6 +97,9 @@ public class SuperAgentChatRetrievalResult extends BaseTableData {
@TableField("chunk_id")
private Long chunkId;
+ @TableField("chunk_type")
+ private String chunkType;
+
@TableField("chunk_no")
private Integer chunkNo;
@@ -114,4 +117,19 @@ public class SuperAgentChatRetrievalResult extends BaseTableData {
@TableField("chunk_char_count")
private Integer chunkCharCount;
+
+ @TableField("context_identity")
+ private String contextIdentity;
+
+ @TableField("citation_identity")
+ private String citationIdentity;
+
+ @TableField("citation_evidence_type")
+ private String citationEvidenceType;
+
+ @TableField("context_only")
+ private Integer contextOnly;
+
+ @TableField("source_evidence_resolved")
+ private Integer sourceEvidenceResolved;
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/dto/ChatRequestDto.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/dto/ChatRequestDto.java
index f96b2f5a7579704c4cc38ff1df9ccd2a482086e6..360ae78989723803c5fe1df4bb1ca62293b5bf40 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/dto/ChatRequestDto.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/dto/ChatRequestDto.java
@@ -5,6 +5,8 @@ import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.util.List;
+
/**
* @program: 企业级别深度设计 AI Agent。添加 阿星不是程序员 微信,添加时备注 super 来获取项目的完整资料
* @description: 数据传输对象
@@ -24,4 +26,8 @@ public class ChatRequestDto {
private String chatMode;
private String selectedDocumentId;
+
+ private String knowledgeBaseSelectionMode;
+
+ private List selectedKnowledgeBaseIds;
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationExchangeView.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationExchangeView.java
index baefc60c445be5ff0a6ab144b42a395f88cf0400..e9f07e15552a74c57466a04e7e0943192331ea3d 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationExchangeView.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationExchangeView.java
@@ -32,6 +32,10 @@ public class ConversationExchangeView {
private String errorMessage;
private Long firstResponseTimeMs;
private Long totalResponseTimeMs;
+ private String knowledgeBaseSelectionMode;
+ private List selectedKnowledgeBaseIds;
+ private List selectedKnowledgeBaseNames;
+ private String retrievalConfigSnapshotJson;
private Date createTime;
private Date editTime;
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationSessionView.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationSessionView.java
index e489145f9be9c3111d11bfe912935de0d5b37b50..771380487e608d23cc58fe5021484e7ef1476a62 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationSessionView.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/ConversationSessionView.java
@@ -31,6 +31,9 @@ public class ConversationSessionView {
private ChatQueryMode chatMode;
private String selectedDocumentId;
private String selectedDocumentName;
+ private String knowledgeBaseSelectionMode;
+ private List selectedKnowledgeBaseIds;
+ private List selectedKnowledgeBaseNames;
private Instant createdAt;
private Instant updatedAt;
private List exchanges;
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/KnowledgeDocumentOptionView.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/KnowledgeDocumentOptionView.java
index 358f8b34191973266dba909928d8c870d2f54197..266c968e4b765ab3f395cb5048fba35ced0c2a71 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/KnowledgeDocumentOptionView.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/KnowledgeDocumentOptionView.java
@@ -17,7 +17,6 @@ public class KnowledgeDocumentOptionView {
private String documentId;
private String documentName;
- private String knowledgeScopeName;
- private String businessCategory;
- private String documentTags;
+ private String knowledgeBaseId;
+ private String knowledgeBaseName;
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/RetrievalResultView.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/RetrievalResultView.java
index 96cfe1ae9f9c9c28078086378b49340e7236af28..531a79fe2f0a3e047f3378179094d0821c45da75 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/RetrievalResultView.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/RetrievalResultView.java
@@ -40,11 +40,17 @@ public class RetrievalResultView {
private Long documentId;
private String documentName;
private Long chunkId;
+ private String chunkType;
private Integer chunkNo;
private Long parentBlockId;
private Integer parentBlockNo;
private String sectionPath;
private String chunkTextPreview;
private Integer chunkCharCount;
+ private String contextIdentity;
+ private String citationIdentity;
+ private String citationEvidenceType;
+ private boolean contextOnly;
+ private boolean sourceEvidenceResolved;
private Instant createTime;
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/SearchReference.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/SearchReference.java
index 727afb814c10af603a456417c564951c62a08a11..eaadedc57ed7e8977d33ad7b12f7cb02852453b5 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/SearchReference.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/model/SearchReference.java
@@ -2,6 +2,8 @@ package org.javaup.ai.chatagent.model;
import lombok.Data;
import lombok.NoArgsConstructor;
+import org.javaup.ai.chatagent.rag.model.EvidenceIdentity;
+import org.javaup.ai.chatagent.rag.support.EvidenceIdentityResolver;
import java.util.List;
@@ -29,8 +31,14 @@ public class SearchReference {
private String documentName;
+ private Long knowledgeBaseId;
+
+ private String knowledgeBaseName;
+
private Long chunkId;
+ private String chunkType;
+
private Long parentBlockId;
private Integer parentBlockNo;
@@ -57,10 +65,6 @@ public class SearchReference {
private String toolName;
- private String knowledgeScopeCode;
-
- private String knowledgeScopeName;
-
private Integer pageNo;
private String pageRange;
@@ -129,6 +133,8 @@ public class SearchReference {
private Long kgEvidenceId;
+ private String kgEvidenceGroundingLevel;
+
private String kgGraphPath;
private Integer kgHopCount;
@@ -147,6 +153,8 @@ public class SearchReference {
private String kgCrossDocumentCommunityKey;
+ private boolean kgCommunitySummaryOnly;
+
private Integer kgCrossDocumentCommunityEntityCount;
private Integer kgCrossDocumentCommunityRelationGroupCount;
@@ -179,6 +187,8 @@ public class SearchReference {
private String raptorSummary;
+ private String raptorSourceStatus;
+
private String answerSegment;
private String quoteText;
@@ -191,6 +201,24 @@ public class SearchReference {
private boolean citationRepaired;
+ private String finalSelectionReason;
+
+ private String evidenceApplicabilityStatus;
+
+ private String evidenceApplicabilityReason;
+
+ private String evidenceRole;
+
+ private String contextIdentity;
+
+ private String citationIdentity;
+
+ private String citationEvidenceType;
+
+ private boolean contextOnly;
+
+ private boolean sourceEvidenceResolved;
+
public SearchReference(String title, String url, String snippet) {
this.sourceType = "WEB";
this.title = title;
@@ -201,24 +229,13 @@ public class SearchReference {
}
public String uniqueKey() {
- if (raptorNodeId != null && chunkId != null) {
- return "RAPTOR:" + raptorNodeId + ":" + chunkId;
- }
- if (kgEvidenceId != null) {
- return "GRAPH_RAG:" + kgEvidenceId;
- }
- if (tableId != null) {
- return "TABLE:" + tableId
- + ":" + (tableOperation == null ? "" : tableOperation)
- + ":" + (tableMetricColumn == null ? "" : tableMetricColumn)
- + ":" + (tableGroupByColumn == null ? "" : tableGroupByColumn)
- + ":" + (snippet == null ? 0 : snippet.hashCode());
- }
- if (parentBlockId != null) {
- return "PARENT:" + parentBlockId;
+ EvidenceIdentity citation = EvidenceIdentityResolver.citationIdentity(this);
+ if (citation != null && citation.present()) {
+ return citation.value();
}
- if (chunkId != null) {
- return "DOCUMENT:" + chunkId;
+ EvidenceIdentity context = EvidenceIdentityResolver.contextIdentity(this);
+ if (context != null && context.present()) {
+ return context.value();
}
if (url != null && !url.isBlank()) {
return "WEB:" + url;
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/config/ChatRagProperties.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/config/ChatRagProperties.java
index 77fb74357c01a651f7b0b40536faade89d970e89..67336407dfe688d321fbc6ad97c12fd7a9353762 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/config/ChatRagProperties.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/config/ChatRagProperties.java
@@ -25,9 +25,9 @@ public class ChatRagProperties {
private int maxSubQuestions = 4;
- private int vectorTopK = 8;
+ private int vectorTopK = 10;
- private int keywordTopK = 8;
+ private int keywordTopK = 10;
private int graphRagTopK = 5;
@@ -49,9 +49,13 @@ public class ChatRagProperties {
private double raptorSummaryQualityFloor = 0.42D;
- private int candidateTopK = 10;
+ private int candidateTopK = 40;
- private int finalTopK = 5;
+ private int rerankCandidateTopK = 24;
+
+ private int reserveCandidateTopK = 30;
+
+ private int finalTopK = 6;
private double minVectorSimilarity = 0.45D;
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/GraphThenEvidenceExecutor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/GraphThenEvidenceExecutor.java
index 4da0f97c2b2fd05ef0e2620719e5040433331467..0161a5cbca1bc6b07f9846d7ad4ace98fe423970 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/GraphThenEvidenceExecutor.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/GraphThenEvidenceExecutor.java
@@ -81,6 +81,8 @@ public class GraphThenEvidenceExecutor implements ConversationExecutor {
taskInfo.traceRecorder().completeStage(graphStage, "结构图定位完成,但证据不满足约束。", Map.of(
"targetSection", graphResult == null || graphResult.getTargetSection() == null ? "" : StrUtil.blankToDefault(graphResult.getTargetSection().displayTitle(), ""),
"targetItemIndex", graphResult == null || graphResult.getTargetItem() == null || graphResult.getTargetItem().getItemIndex() == null ? "" : String.valueOf(graphResult.getTargetItem().getItemIndex()),
+ "graphThenEvidenceFailed", true,
+ "fallbackRecommendation", ExecutionMode.RETRIEVAL.name(),
"notes", List.of("结构图未定位到满足条件的章节或编号项。")
));
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/RagChatExecutor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/RagChatExecutor.java
index 9bc76877ddbd02f38b12038d0608f83fcf58f27f..543833b7570a676b21870b5eaf6bbfa35383dc11 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/RagChatExecutor.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/executor/RagChatExecutor.java
@@ -132,6 +132,9 @@ public class RagChatExecutor implements ConversationExecutor {
item.put("pageNo", reference.getPageNo());
item.put("pageRange", StrUtil.blankToDefault(reference.getPageRange(), ""));
item.put("bboxJson", StrUtil.blankToDefault(reference.getBboxJson(), ""));
+ item.put("finalSelectionReason", StrUtil.blankToDefault(reference.getFinalSelectionReason(), ""));
+ item.put("evidenceApplicabilityStatus", StrUtil.blankToDefault(reference.getEvidenceApplicabilityStatus(), ""));
+ item.put("evidenceApplicabilityReason", StrUtil.blankToDefault(reference.getEvidenceApplicabilityReason(), ""));
item.put("tableId", reference.getTableId());
item.put("tableNo", reference.getTableNo());
item.put("tableTitle", StrUtil.blankToDefault(reference.getTableTitle(), ""));
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerHistoryContext.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerHistoryContext.java
index 7fffae751512ef4ff54c9332aa0dee7010ffdeb1..cdf9fd3aa6c8c1a9bab4f7f785d2179e3c73835e 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerHistoryContext.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerHistoryContext.java
@@ -5,6 +5,9 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* @program: 企业级别深度设计 AI Agent。添加 阿星不是程序员 微信,添加时备注 super 来获取项目的完整资料
* @description: 回答阶段最终使用的历史上下文
@@ -23,6 +26,11 @@ public class AnswerHistoryContext {
private String recentContext;
+ @Builder.Default
+ private List evidenceAnchors = new ArrayList<>();
+
+ private String resolvedTopic;
+
private boolean followUpQuestion;
private Integer totalBudget;
@@ -32,6 +40,7 @@ public class AnswerHistoryContext {
private Integer structuredBudget;
public boolean isEmpty() {
- return renderedText == null || renderedText.isBlank();
+ return (renderedText == null || renderedText.isBlank())
+ && (evidenceAnchors == null || evidenceAnchors.isEmpty());
}
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerPlan.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerPlan.java
new file mode 100644
index 0000000000000000000000000000000000000000..7c23f6130b60f0d9d05b272c194c5c5a6c817672
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/AnswerPlan.java
@@ -0,0 +1,28 @@
+package org.javaup.ai.chatagent.rag.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AnswerPlan {
+
+ @Builder.Default
+ private List requiredRoles = new ArrayList<>();
+
+ @Builder.Default
+ private List optionalRoles = new ArrayList<>();
+
+ private boolean requireExplicitEvidence;
+
+ private boolean allowRoleFallback;
+
+ private String instruction;
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/CitationEvidenceType.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/CitationEvidenceType.java
new file mode 100644
index 0000000000000000000000000000000000000000..37cc3b89a42f70f21834c29543a19f3259eb4431
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/CitationEvidenceType.java
@@ -0,0 +1,9 @@
+package org.javaup.ai.chatagent.rag.model;
+
+public enum CitationEvidenceType {
+ CHUNK,
+ TABLE_CELL_OR_ROW,
+ KG_QUOTE_SOURCE,
+ RAPTOR_SOURCE_CHUNK,
+ CONTEXT_ONLY
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/ConversationExecutionPlan.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/ConversationExecutionPlan.java
index 2528b9fc34c255644ff0e1905ff261e38dea9baa..3c05e37a28a4ca71088c1c4275060fa63c182601 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/ConversationExecutionPlan.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/ConversationExecutionPlan.java
@@ -5,6 +5,7 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.javaup.enums.ChatQueryMode;
+import org.javaup.enums.KnowledgeBaseSelectionMode;
import java.time.LocalDate;
import java.util.ArrayList;
@@ -88,6 +89,20 @@ public class ConversationExecutionPlan {
@Builder.Default
private List retrievalTaskIds = new ArrayList<>();
+ @Builder.Default
+ private KnowledgeBaseSelectionMode knowledgeBaseSelectionMode = KnowledgeBaseSelectionMode.NONE;
+
+ @Builder.Default
+ private List selectedKnowledgeBaseIds = new ArrayList<>();
+
+ @Builder.Default
+ private List selectedKnowledgeBaseNames = new ArrayList<>();
+
+ @Builder.Default
+ private List allowedKnowledgeBaseDocumentIds = new ArrayList<>();
+
+ private RagRuntimeOptions ragRuntimeOptions;
+
private String clarificationReply;
@Builder.Default
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/DocumentNavigationDecision.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/DocumentNavigationDecision.java
index 061825c646894e0d7b30398303c471613a01d476..14fb64b6e10a7207bb48d55b1ed5ecda3dcc5bf2 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/DocumentNavigationDecision.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/DocumentNavigationDecision.java
@@ -32,6 +32,8 @@ public class DocumentNavigationDecision {
private QueryUnderstandingResult queryUnderstanding;
+ private StructureNavigationResult structureNavigationResult;
+
@Builder.Default
private RetrievalIntent retrievalIntent = RetrievalIntent.GENERAL;
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceAnchor.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceAnchor.java
new file mode 100644
index 0000000000000000000000000000000000000000..4de82850072715c402bec828cb879b2be342e699
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceAnchor.java
@@ -0,0 +1,46 @@
+package org.javaup.ai.chatagent.rag.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 上一轮最终引用形成的证据锚点,只用于追问指代和检索范围限定。
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class EvidenceAnchor {
+
+ private Long documentId;
+
+ private String documentName;
+
+ private Long taskId;
+
+ private Long knowledgeBaseId;
+
+ private String knowledgeBaseName;
+
+ private Long structureNodeId;
+
+ private String sectionPath;
+
+ private String canonicalPath;
+
+ private Integer itemIndex;
+
+ private Long parentBlockId;
+
+ private Long chunkId;
+
+ private String sourceType;
+
+ private String channel;
+
+ private String snippet;
+
+ private Double score;
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceApplicabilityResult.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceApplicabilityResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..59589cd77e21efead35845d56a6f866b5ff06395
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceApplicabilityResult.java
@@ -0,0 +1,47 @@
+package org.javaup.ai.chatagent.rag.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class EvidenceApplicabilityResult {
+
+ public static final String APPLICABLE = "APPLICABLE";
+ public static final String APPLICABLE_UNKNOWN = "APPLICABLE_UNKNOWN";
+ public static final String NOT_APPLICABLE = "NOT_APPLICABLE";
+
+ private String status;
+
+ private boolean applicable;
+
+ private String reason;
+
+ public static EvidenceApplicabilityResult applicable(String reason) {
+ return EvidenceApplicabilityResult.builder()
+ .status(APPLICABLE)
+ .applicable(true)
+ .reason(reason)
+ .build();
+ }
+
+ public static EvidenceApplicabilityResult unknown(String reason) {
+ return EvidenceApplicabilityResult.builder()
+ .status(APPLICABLE_UNKNOWN)
+ .applicable(true)
+ .reason(reason)
+ .build();
+ }
+
+ public static EvidenceApplicabilityResult notApplicable(String reason) {
+ return EvidenceApplicabilityResult.builder()
+ .status(NOT_APPLICABLE)
+ .applicable(false)
+ .reason(reason)
+ .build();
+ }
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceIdentity.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceIdentity.java
new file mode 100644
index 0000000000000000000000000000000000000000..88ba40952ebaa98ab4e7ab08336f5951b3e7dbad
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceIdentity.java
@@ -0,0 +1,16 @@
+package org.javaup.ai.chatagent.rag.model;
+
+public record EvidenceIdentity(String value, CitationEvidenceType type, boolean citationCapable) {
+
+ public static EvidenceIdentity citation(String value, CitationEvidenceType type) {
+ return new EvidenceIdentity(value, type, true);
+ }
+
+ public static EvidenceIdentity context(String value) {
+ return new EvidenceIdentity(value, CitationEvidenceType.CONTEXT_ONLY, false);
+ }
+
+ public boolean present() {
+ return value != null && !value.isBlank();
+ }
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceRole.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceRole.java
new file mode 100644
index 0000000000000000000000000000000000000000..ed35c88f5cecfc2c1bb77091e5063fed441a3e83
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/EvidenceRole.java
@@ -0,0 +1,15 @@
+package org.javaup.ai.chatagent.rag.model;
+
+public enum EvidenceRole {
+ SYMPTOM,
+ CAUSE,
+ HANDLING_STEP,
+ CHECK_ORDER,
+ THRESHOLD,
+ RESPONSIBILITY,
+ CONFIGURATION,
+ BOUNDARY,
+ RELATION,
+ SUMMARY,
+ GENERAL
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/QueryUnderstandingResult.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/QueryUnderstandingResult.java
index df6998464409a00f4fc00953fe6c2664db8de1b2..433303db0556a9cfff068d22b52463287a8f6757 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/QueryUnderstandingResult.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/QueryUnderstandingResult.java
@@ -26,14 +26,27 @@ public class QueryUnderstandingResult {
@Builder.Default
private List entities = new ArrayList<>();
+ @Builder.Default
+ private List targetEntities = new ArrayList<>();
+
+ @Builder.Default
+ private List excludedEntities = new ArrayList<>();
+
@Builder.Default
private List sectionAnchors = new ArrayList<>();
+ private StructureNavigationIntent structureNavigationIntent;
+
+ @Builder.Default
+ private List expectedEvidenceRoles = new ArrayList<>();
+
@Builder.Default
private List tableOps = new ArrayList<>();
private boolean negativeBoundary;
+ private String answerExpectation;
+
private double confidence;
@Builder.Default
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/RagRuntimeOptions.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/RagRuntimeOptions.java
new file mode 100644
index 0000000000000000000000000000000000000000..de22bb171c08a96a40452691aeee570ca8b31d62
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/RagRuntimeOptions.java
@@ -0,0 +1,123 @@
+package org.javaup.ai.chatagent.rag.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RagRuntimeOptions {
+
+ private int vectorTopK;
+
+ private int keywordTopK;
+
+ private int graphRagTopK;
+
+ private int graphRagMaxHops;
+
+ private int raptorTopK;
+
+ private int raptorSourceChunkTopK;
+
+ private int candidateTopK;
+
+ private int rerankCandidateTopK;
+
+ private int reserveCandidateTopK;
+
+ private int finalTopK;
+
+ private double minVectorSimilarity;
+
+ private double keywordRelativeScoreFloor;
+
+ private boolean keywordChannelEnabled;
+
+ private boolean tableChannelEnabled;
+
+ private boolean graphRagChannelEnabled;
+
+ private boolean raptorChannelEnabled;
+
+ private HybridOptions hybrid;
+
+ @Builder.Default
+ private List kbConfigConflictFields = new ArrayList<>();
+
+ public static RagRuntimeOptions from(ChatRagProperties properties) {
+ ChatRagProperties.HybridProperties hybridProperties = properties == null ? null : properties.getHybrid();
+ return RagRuntimeOptions.builder()
+ .vectorTopK(properties == null ? 10 : properties.getVectorTopK())
+ .keywordTopK(properties == null ? 10 : properties.getKeywordTopK())
+ .graphRagTopK(properties == null ? 5 : properties.getGraphRagTopK())
+ .graphRagMaxHops(properties == null ? 2 : properties.getGraphRagMaxHops())
+ .raptorTopK(properties == null ? 5 : properties.getRaptorTopK())
+ .raptorSourceChunkTopK(properties == null ? 3 : properties.getRaptorSourceChunkTopK())
+ .candidateTopK(properties == null ? 40 : properties.getCandidateTopK())
+ .rerankCandidateTopK(properties == null ? 24 : properties.getRerankCandidateTopK())
+ .reserveCandidateTopK(properties == null ? 30 : properties.getReserveCandidateTopK())
+ .finalTopK(properties == null ? 6 : properties.getFinalTopK())
+ .minVectorSimilarity(properties == null ? 0.45D : properties.getMinVectorSimilarity())
+ .keywordRelativeScoreFloor(properties == null ? 0.35D : properties.getKeywordRelativeScoreFloor())
+ .keywordChannelEnabled(properties == null || properties.isKeywordChannelEnabled())
+ .tableChannelEnabled(properties == null || properties.isTableChannelEnabled())
+ .graphRagChannelEnabled(properties == null || properties.isGraphRagChannelEnabled())
+ .raptorChannelEnabled(properties == null || properties.isRaptorChannelEnabled())
+ .hybrid(HybridOptions.from(hybridProperties))
+ .kbConfigConflictFields(new ArrayList<>())
+ .build();
+ }
+
+ public static RagRuntimeOptions resolve(ConversationExecutionPlan plan, ChatRagProperties properties) {
+ return plan == null || plan.getRagRuntimeOptions() == null
+ ? from(properties)
+ : plan.getRagRuntimeOptions();
+ }
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class HybridOptions {
+
+ private double vectorWeight;
+
+ private double keywordWeight;
+
+ private double tableWeight;
+
+ private double graphRagWeight;
+
+ private double raptorWeight;
+
+ private double rankWeight;
+
+ private double originalScoreWeight;
+
+ private double metadataBoostWeight;
+
+ private double maxMetadataBoost;
+
+ public static HybridOptions from(ChatRagProperties.HybridProperties properties) {
+ return HybridOptions.builder()
+ .vectorWeight(properties == null ? 1.0D : properties.getVectorWeight())
+ .keywordWeight(properties == null ? 1.0D : properties.getKeywordWeight())
+ .tableWeight(properties == null ? 1.2D : properties.getTableWeight())
+ .graphRagWeight(properties == null ? 1.1D : properties.getGraphRagWeight())
+ .raptorWeight(properties == null ? 1.05D : properties.getRaptorWeight())
+ .rankWeight(properties == null ? 1.0D : properties.getRankWeight())
+ .originalScoreWeight(properties == null ? 0.08D : properties.getOriginalScoreWeight())
+ .metadataBoostWeight(properties == null ? 0.04D : properties.getMetadataBoostWeight())
+ .maxMetadataBoost(properties == null ? 1.0D : properties.getMaxMetadataBoost())
+ .build();
+ }
+ }
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationIntent.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationIntent.java
new file mode 100644
index 0000000000000000000000000000000000000000..ecd161372ac03d27980086b56299095a49565a2a
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationIntent.java
@@ -0,0 +1,32 @@
+package org.javaup.ai.chatagent.rag.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class StructureNavigationIntent {
+
+ @Builder.Default
+ private List operations = new ArrayList<>();
+
+ private Long anchorStructureNodeId;
+
+ private String anchorSectionPath;
+
+ private String anchorCanonicalPath;
+
+ @Builder.Default
+ private List sectionAnchors = new ArrayList<>();
+
+ private double confidence;
+
+ private String source;
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationOperation.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationOperation.java
new file mode 100644
index 0000000000000000000000000000000000000000..a7f8d1090d3e377c4314485a114a4d618cd091ad
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationOperation.java
@@ -0,0 +1,18 @@
+package org.javaup.ai.chatagent.rag.model;
+
+public enum StructureNavigationOperation {
+
+ CURRENT_SECTION,
+
+ PARENT_SECTION,
+
+ PREVIOUS_SIBLING,
+
+ NEXT_SIBLING,
+
+ DIRECT_CHILDREN,
+
+ SECTION_WITH_SIBLINGS,
+
+ SECTION_WITH_CHILDREN
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationResult.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationResult.java
new file mode 100644
index 0000000000000000000000000000000000000000..79c9e8c7a52fbe40d77dafd82686fdc6cef835e4
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/model/StructureNavigationResult.java
@@ -0,0 +1,36 @@
+package org.javaup.ai.chatagent.rag.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.javaup.ai.manage.data.SuperAgentDocumentStructureNode;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class StructureNavigationResult {
+
+ private Long documentId;
+
+ private Long anchorNodeId;
+
+ private SuperAgentDocumentStructureNode current;
+
+ private SuperAgentDocumentStructureNode parent;
+
+ private SuperAgentDocumentStructureNode previousSibling;
+
+ private SuperAgentDocumentStructureNode nextSibling;
+
+ @Builder.Default
+ private List directChildren = new ArrayList<>();
+
+ private boolean deterministic;
+
+ private String missReason;
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannel.java
index ae646ec203ea416b1d0563e848b9332d059fe954..22329e8d35ec45cea28b3d8d7d1c8aa9ccacd22a 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannel.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/GraphRagRetrievalChannel.java
@@ -3,6 +3,7 @@ package org.javaup.ai.chatagent.rag.retrieve.channel;
import cn.hutool.core.util.StrUtil;
import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan;
+import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions;
import org.javaup.ai.chatagent.rag.service.DocumentRetrieveRequestFactory;
import org.javaup.ai.manage.model.DocumentRetrieveRequest;
import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor;
@@ -52,33 +53,37 @@ public class GraphRagRetrievalChannel implements RetrievalChannel {
@Override
public boolean supports(ConversationExecutionPlan plan) {
return plan != null
- && properties.isGraphRagChannelEnabled()
+ && RagRuntimeOptions.resolve(plan, properties).isGraphRagChannelEnabled()
&& !resolvedDocumentIds(plan).isEmpty();
}
@Override
public RetrievalChannelResult retrieve(String subQuestion, ConversationExecutionPlan plan) {
- DocumentRetrieveRequest request = documentRetrieveRequestFactory.build(subQuestion, plan, properties.getGraphRagTopK());
+ RagRuntimeOptions options = RagRuntimeOptions.resolve(plan, properties);
+ DocumentRetrieveRequest request = documentRetrieveRequestFactory.build(subQuestion, plan, options.getGraphRagTopK());
List results = graphRagSearchService.search(
StrUtil.blankToDefault(request.getRetrievalQuery(), subQuestion),
request.resolvedDocumentIds(),
request.resolvedTaskIds(),
- properties.getGraphRagTopK(),
- properties.getGraphRagMaxHops()
+ options.getGraphRagTopK(),
+ options.getGraphRagMaxHops()
);
if (results.isEmpty()) {
return new RetrievalChannelResult(channelName(), List.of());
}
- Map documentNames = resolveDocumentNames();
+ Map documentDescriptors = resolveDocumentDescriptors(plan);
List documents = results.stream()
- .map(result -> toDocument(subQuestion, result, documentNames))
+ .map(result -> toDocument(subQuestion, result, documentDescriptors))
.toList();
return new RetrievalChannelResult(channelName(), documents);
}
- private Document toDocument(String subQuestion, GraphRagSearchResult result, Map documentNames) {
- String documentName = StrUtil.blankToDefault(documentNames.get(result.getDocumentId()), "文档图谱");
+ private Document toDocument(String subQuestion,
+ GraphRagSearchResult result,
+ Map documentDescriptors) {
+ KnowledgeDocumentDescriptor descriptor = documentDescriptors.get(result.getDocumentId());
+ String documentName = StrUtil.blankToDefault(descriptor == null ? null : descriptor.getDocumentName(), "文档图谱");
String text = renderEvidenceText(subQuestion, result);
Map metadata = new LinkedHashMap<>();
metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, SOURCE_TYPE);
@@ -86,6 +91,10 @@ public class GraphRagRetrievalChannel implements RetrievalChannel {
metadata.put(DocumentKnowledgeMetadataKeys.SCORE, result.getScore());
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.DOCUMENT_ID, result.getDocumentId());
metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, documentName);
+ if (descriptor != null) {
+ putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, descriptor.getKnowledgeBaseId());
+ metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, StrUtil.blankToDefault(descriptor.getKnowledgeBaseName(), ""));
+ }
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.TASK_ID, result.getTaskId());
if (!isCommunityReportResult(result)) {
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, result.getParentBlockId());
@@ -113,6 +122,7 @@ public class GraphRagRetrievalChannel implements RetrievalChannel {
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_EVIDENCE_COUNT, result.getRelationGroupEvidenceCount());
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_DOCUMENT_COUNT, result.getRelationGroupDocumentCount());
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID, result.getEvidenceId());
+ metadata.put(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL, groundingLevel(result));
metadata.put(DocumentKnowledgeMetadataKeys.KG_GRAPH_PATH, StrUtil.blankToDefault(result.getGraphPath(), ""));
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_HOP_COUNT, result.getHopCount());
metadata.put(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE, StrUtil.blankToDefault(result.getQueryPlanSource(), ""));
@@ -124,6 +134,7 @@ public class GraphRagRetrievalChannel implements RetrievalChannel {
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_COMMUNITY_ID, result.getCommunityId());
metadata.put(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_TITLE, StrUtil.blankToDefault(result.getCommunityTitle(), ""));
metadata.put(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY, StrUtil.blankToDefault(result.getCommunitySummary(), ""));
+ metadata.put(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY, isCommunitySummaryOnly(result));
metadata.put(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_KEY, StrUtil.blankToDefault(result.getCrossDocumentCommunityKey(), ""));
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_ENTITY_COUNT, result.getCrossDocumentCommunityEntityCount());
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_RELATION_GROUP_COUNT, result.getCrossDocumentCommunityRelationGroupCount());
@@ -162,6 +173,9 @@ public class GraphRagRetrievalChannel implements RetrievalChannel {
}
if (StrUtil.isNotBlank(result.getCommunitySummary())) {
builder.append("社区报告:").append(result.getCommunitySummary()).append('\n');
+ if (isCommunitySummaryOnly(result)) {
+ builder.append("社区报告边界:该候选缺少可回到原文 quote 的 KG evidence,只能作为背景线索,不能单独支撑具体事实结论。\n");
+ }
}
if (StrUtil.isNotBlank(result.getNHopPath())) {
builder.append("n-hop路径:").append(result.getNHopPath()).append('\n');
@@ -190,6 +204,42 @@ public class GraphRagRetrievalChannel implements RetrievalChannel {
return builder.toString().trim();
}
+ private String groundingLevel(GraphRagSearchResult result) {
+ if (result == null) {
+ return "NONE";
+ }
+ boolean hasSourceQuote = hasSourceQuoteEvidence(result);
+ if (result.getRelationId() != null) {
+ if (!hasSourceQuote) {
+ return "RELATION_NO_QUOTE";
+ }
+ String relationType = StrUtil.blankToDefault(result.getRelationType(), "")
+ .trim()
+ .toUpperCase();
+ if ("RECORDS".equals(relationType) || "ASSOCIATED_WITH".equals(relationType) || "RELATED_TO".equals(relationType)) {
+ return "RELATION_WEAK_QUOTE";
+ }
+ return "RELATION_STRONG_QUOTE";
+ }
+ if (result.getEntityId() != null) {
+ return hasSourceQuote ? "ENTITY_QUOTE" : "ENTITY_NO_QUOTE";
+ }
+ if (isCommunityReportResult(result)) {
+ return hasSourceQuote ? "COMMUNITY_SOURCE_QUOTE" : "COMMUNITY_SUMMARY_ONLY";
+ }
+ return hasSourceQuote ? "SOURCE_QUOTE" : "NONE";
+ }
+
+ private boolean isCommunitySummaryOnly(GraphRagSearchResult result) {
+ return isCommunityReportResult(result) && !hasSourceQuoteEvidence(result);
+ }
+
+ private boolean hasSourceQuoteEvidence(GraphRagSearchResult result) {
+ return result != null
+ && result.getEvidenceId() != null
+ && StrUtil.isNotBlank(result.getQuoteText());
+ }
+
private Set resolveRelationTypes(GraphRagSearchResult result) {
LinkedHashSet relationTypes = new LinkedHashSet<>();
addRelationType(relationTypes, result.getRelationType());
@@ -249,14 +299,21 @@ public class GraphRagRetrievalChannel implements RetrievalChannel {
return plan.getSelectedTaskId() == null ? List.of() : List.of(plan.getSelectedTaskId());
}
- private Map resolveDocumentNames() {
- Map documentNames = new LinkedHashMap<>();
- for (KnowledgeDocumentDescriptor descriptor : documentKnowledgeService.listRetrievableDocuments()) {
+ private Map resolveDocumentDescriptors(ConversationExecutionPlan plan) {
+ Map documentDescriptors = new LinkedHashMap<>();
+ List documentIds = resolvedDocumentIds(plan);
+ List descriptors = plan == null
+ || plan.getSelectedKnowledgeBaseIds() == null
+ || plan.getSelectedKnowledgeBaseIds().isEmpty()
+ ? documentKnowledgeService.listRetrievableDocuments()
+ : documentKnowledgeService.listRetrievableDocumentsByKnowledgeBaseIds(plan.getSelectedKnowledgeBaseIds());
+ for (KnowledgeDocumentDescriptor descriptor : descriptors) {
if (descriptor.getDocumentId() != null) {
- documentNames.put(descriptor.getDocumentId(), descriptor.getDocumentName());
+ documentDescriptors.put(descriptor.getDocumentId(), descriptor);
}
}
- return documentNames;
+ documentDescriptors.keySet().retainAll(documentIds);
+ return documentDescriptors;
}
private void putIfNotNull(Map metadata, String key, Object value) {
@@ -268,10 +325,10 @@ public class GraphRagRetrievalChannel implements RetrievalChannel {
private String documentId(GraphRagSearchResult result) {
if (isCommunityReportResult(result)) {
if (StrUtil.isNotBlank(result.getCrossDocumentCommunityKey())) {
- return "graphrag-xcommunity-" + stableIdPart(result.getCrossDocumentCommunityKey()) + "-evidence-" + result.getEvidenceId();
+ return "graphrag-xcommunity-" + stableIdPart(result.getCrossDocumentCommunityKey()) + "-evidence-" + stableEvidenceIdPart(result);
}
if (result.getCommunityId() != null) {
- return "graphrag-community-" + result.getCommunityId() + "-evidence-" + result.getEvidenceId();
+ return "graphrag-community-" + result.getCommunityId() + "-evidence-" + stableEvidenceIdPart(result);
}
}
if (result.getEvidenceId() != null) {
@@ -306,4 +363,11 @@ public class GraphRagRetrievalChannel implements RetrievalChannel {
}
return normalized.length() <= 80 ? normalized : normalized.substring(0, 80);
}
+
+ private String stableEvidenceIdPart(GraphRagSearchResult result) {
+ if (result != null && result.getEvidenceId() != null) {
+ return String.valueOf(result.getEvidenceId());
+ }
+ return "summary";
+ }
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/KeywordRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/KeywordRetrievalChannel.java
index 7a5433daf875a8c96e7d29a6683326d279d3741a..806e8e2d0350b7c188944d78ccf6e66112556e97 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/KeywordRetrievalChannel.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/KeywordRetrievalChannel.java
@@ -3,6 +3,7 @@ package org.javaup.ai.chatagent.rag.retrieve.channel;
import cn.hutool.core.collection.CollectionUtil;
import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan;
+import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions;
import org.javaup.ai.chatagent.rag.service.DocumentRetrieveRequestFactory;
import org.javaup.ai.manage.service.DocumentKnowledgeService;
import org.javaup.enums.RetrievalChannelEnum;
@@ -40,14 +41,14 @@ public class KeywordRetrievalChannel implements RetrievalChannel {
@Override
public boolean supports(ConversationExecutionPlan plan) {
- return properties.isKeywordChannelEnabled()
+ return RagRuntimeOptions.resolve(plan, properties).isKeywordChannelEnabled()
&& hasDocumentScope(plan);
}
@Override
public RetrievalChannelResult retrieve(String subQuestion, ConversationExecutionPlan plan) {
List documentList = documentKnowledgeService.keywordSearch(
- documentRetrieveRequestFactory.build(subQuestion, plan, properties.getKeywordTopK())
+ documentRetrieveRequestFactory.build(subQuestion, plan, RagRuntimeOptions.resolve(plan, properties).getKeywordTopK())
);
return new RetrievalChannelResult(
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannel.java
index e6f06797b667dbe7bae2487ccdaefc9aa4d2f260..9b134ad5b1336b018aeb98e33c990bb1dafca074 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannel.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/RaptorRetrievalChannel.java
@@ -3,6 +3,7 @@ package org.javaup.ai.chatagent.rag.retrieve.channel;
import cn.hutool.core.util.StrUtil;
import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan;
+import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions;
import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor;
import org.javaup.ai.manage.model.raptor.RaptorSearchResult;
import org.javaup.ai.manage.service.DocumentKnowledgeService;
@@ -20,6 +21,9 @@ import java.util.Map;
public class RaptorRetrievalChannel implements RetrievalChannel {
private static final String SOURCE_TYPE = "RAPTOR";
+ private static final String SOURCE_STATUS_SOURCE_CHUNK = "SOURCE_CHUNK";
+ private static final String SOURCE_STATUS_SOURCE_PARENT_BLOCK = "SOURCE_PARENT_BLOCK";
+ private static final String SOURCE_STATUS_SUMMARY_ONLY = "SUMMARY_ONLY";
private final RaptorSearchService raptorSearchService;
private final DocumentKnowledgeService documentKnowledgeService;
@@ -41,58 +45,70 @@ public class RaptorRetrievalChannel implements RetrievalChannel {
@Override
public boolean supports(ConversationExecutionPlan plan) {
return plan != null
- && properties.isRaptorChannelEnabled()
+ && RagRuntimeOptions.resolve(plan, properties).isRaptorChannelEnabled()
&& !resolvedDocumentIds(plan).isEmpty();
}
@Override
public RetrievalChannelResult retrieve(String subQuestion, ConversationExecutionPlan plan) {
+ RagRuntimeOptions options = RagRuntimeOptions.resolve(plan, properties);
List results = raptorSearchService.search(
subQuestion,
resolvedDocumentIds(plan),
resolvedTaskIds(plan),
- properties.getRaptorTopK(),
- properties.getRaptorSourceChunkTopK()
+ options.getRaptorTopK(),
+ options.getRaptorSourceChunkTopK()
);
if (results.isEmpty()) {
return new RetrievalChannelResult(channelName(), List.of());
}
- Map documentNames = resolveDocumentNames();
+ Map documentDescriptors = resolveDocumentDescriptors(plan);
List documents = results.stream()
- .map(result -> toDocument(subQuestion, result, documentNames))
+ .map(result -> toDocument(subQuestion, result, documentDescriptors))
.toList();
return new RetrievalChannelResult(channelName(), documents);
}
- private Document toDocument(String subQuestion, RaptorSearchResult result, Map documentNames) {
- String documentName = StrUtil.blankToDefault(documentNames.get(result.getDocumentId()), "文档摘要树");
+ private Document toDocument(String subQuestion,
+ RaptorSearchResult result,
+ Map documentDescriptors) {
+ KnowledgeDocumentDescriptor descriptor = documentDescriptors.get(result.getDocumentId());
+ String documentName = StrUtil.blankToDefault(descriptor == null ? null : descriptor.getDocumentName(), "文档摘要树");
String text = renderEvidenceText(subQuestion, result);
+ String sourceStatus = resolveSourceStatus(result);
Map metadata = new LinkedHashMap<>();
metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, SOURCE_TYPE);
metadata.put(DocumentKnowledgeMetadataKeys.CHANNEL, channelName());
metadata.put(DocumentKnowledgeMetadataKeys.SCORE, result.getScore());
metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, result.getDocumentId());
metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, documentName);
+ if (descriptor != null) {
+ putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, descriptor.getKnowledgeBaseId());
+ metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, StrUtil.blankToDefault(descriptor.getKnowledgeBaseName(), ""));
+ }
metadata.put(DocumentKnowledgeMetadataKeys.TASK_ID, result.getTaskId());
- metadata.put(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, result.getParentBlockId());
- metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_ID, result.getChunkId());
- metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_NO, result.getChunkNo());
+ putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID, result.getParentBlockId());
+ putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.CHUNK_ID, result.getChunkId());
+ putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.CHUNK_NO, result.getChunkNo());
metadata.put(DocumentKnowledgeMetadataKeys.SECTION_PATH, StrUtil.blankToDefault(result.getSectionPath(), ""));
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.PAGE_NO, result.getPageNo());
metadata.put(DocumentKnowledgeMetadataKeys.PAGE_RANGE, StrUtil.blankToDefault(result.getPageRange(), ""));
metadata.put(DocumentKnowledgeMetadataKeys.BBOX_JSON, StrUtil.blankToDefault(result.getBboxJson(), ""));
metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_BLOCK_IDS, StrUtil.blankToDefault(result.getSourceBlockIds(), ""));
- metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE, "RAPTOR_SOURCE_CHUNK");
+ metadata.put(DocumentKnowledgeMetadataKeys.CHUNK_TYPE,
+ SOURCE_STATUS_SUMMARY_ONLY.equals(sourceStatus) ? "RAPTOR_SUMMARY" : "RAPTOR_SOURCE_CHUNK");
metadata.put(DocumentKnowledgeMetadataKeys.TITLE, StrUtil.blankToDefault(result.getTitle(), result.getRaptorNodeTitle()));
- metadata.put(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET, StrUtil.blankToDefault(result.getChunkText(), ""));
+ metadata.put(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET,
+ StrUtil.blankToDefault(result.getChunkText(), StrUtil.blankToDefault(result.getRaptorSummary(), "")));
metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID, result.getRaptorNodeId());
metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_TITLE, StrUtil.blankToDefault(result.getRaptorNodeTitle(), ""));
metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_LEVEL, result.getRaptorNodeLevel());
metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_SUMMARY, StrUtil.blankToDefault(result.getRaptorSummary(), ""));
+ metadata.put(DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS, sourceStatus);
return Document.builder()
- .id("raptor-" + result.getRaptorNodeId() + "-" + result.getChunkId())
+ .id(raptorDocumentId(result))
.text(text)
.metadata(metadata)
.score(result.getScore())
@@ -112,10 +128,43 @@ public class RaptorRetrievalChannel implements RetrievalChannel {
if (result.getPageNo() != null) {
builder.append("原文页码:").append(result.getPageNo()).append('\n');
}
- builder.append("下钻原文:").append(StrUtil.blankToDefault(result.getChunkText(), "")).append('\n');
+ String sourceStatus = resolveSourceStatus(result);
+ if (SOURCE_STATUS_SUMMARY_ONLY.equals(sourceStatus)) {
+ builder.append("下钻状态:未找到可引用 source chunk 或 ParentBlock,本证据仅作为摘要背景。\n");
+ }
+ else if (SOURCE_STATUS_SOURCE_PARENT_BLOCK.equals(sourceStatus) && StrUtil.isBlank(result.getChunkText())) {
+ builder.append("下钻状态:已定位到 ParentBlock,但当前结果未携带 chunk 原文。\n");
+ }
+ if (StrUtil.isNotBlank(result.getChunkText())) {
+ builder.append("下钻原文:").append(result.getChunkText()).append('\n');
+ }
return builder.toString().trim();
}
+ private String resolveSourceStatus(RaptorSearchResult result) {
+ String sourceStatus = StrUtil.blankToDefault(result.getSourceStatus(), "");
+ if (StrUtil.isNotBlank(sourceStatus)) {
+ return sourceStatus;
+ }
+ if (result.getChunkId() != null) {
+ return SOURCE_STATUS_SOURCE_CHUNK;
+ }
+ if (result.getParentBlockId() != null) {
+ return SOURCE_STATUS_SOURCE_PARENT_BLOCK;
+ }
+ return SOURCE_STATUS_SUMMARY_ONLY;
+ }
+
+ private String raptorDocumentId(RaptorSearchResult result) {
+ if (result.getChunkId() != null) {
+ return "raptor-" + result.getRaptorNodeId() + "-" + result.getChunkId();
+ }
+ if (result.getParentBlockId() != null) {
+ return "raptor-" + result.getRaptorNodeId() + "-parent-" + result.getParentBlockId();
+ }
+ return "raptor-" + result.getRaptorNodeId() + "-summary";
+ }
+
private List resolvedDocumentIds(ConversationExecutionPlan plan) {
if (plan.getRetrievalDocumentIds() != null && !plan.getRetrievalDocumentIds().isEmpty()) {
return plan.getRetrievalDocumentIds();
@@ -130,14 +179,21 @@ public class RaptorRetrievalChannel implements RetrievalChannel {
return plan.getSelectedTaskId() == null ? List.of() : List.of(plan.getSelectedTaskId());
}
- private Map resolveDocumentNames() {
- Map documentNames = new LinkedHashMap<>();
- for (KnowledgeDocumentDescriptor descriptor : documentKnowledgeService.listRetrievableDocuments()) {
+ private Map resolveDocumentDescriptors(ConversationExecutionPlan plan) {
+ Map documentDescriptors = new LinkedHashMap<>();
+ List documentIds = resolvedDocumentIds(plan);
+ List descriptors = plan == null
+ || plan.getSelectedKnowledgeBaseIds() == null
+ || plan.getSelectedKnowledgeBaseIds().isEmpty()
+ ? documentKnowledgeService.listRetrievableDocuments()
+ : documentKnowledgeService.listRetrievableDocumentsByKnowledgeBaseIds(plan.getSelectedKnowledgeBaseIds());
+ for (KnowledgeDocumentDescriptor descriptor : descriptors) {
if (descriptor.getDocumentId() != null) {
- documentNames.put(descriptor.getDocumentId(), descriptor.getDocumentName());
+ documentDescriptors.put(descriptor.getDocumentId(), descriptor);
}
}
- return documentNames;
+ documentDescriptors.keySet().retainAll(documentIds);
+ return documentDescriptors;
}
private void putIfNotNull(Map metadata, String key, Object value) {
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannel.java
index 9aebd1492f0ebdd53d65b0deba469db22ee24c14..b157a86e2dad814cbe96e8fabfcbe4f6db58cfdf 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannel.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/TableRetrievalChannel.java
@@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil;
import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan;
import org.javaup.ai.chatagent.rag.model.DocumentTableQueryPlan;
import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
+import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions;
import org.javaup.ai.chatagent.rag.service.DocumentTableQueryPlanner;
import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor;
import org.javaup.ai.manage.model.table.DocumentTableDescriptor;
@@ -50,7 +51,7 @@ public class TableRetrievalChannel implements RetrievalChannel {
@Override
public boolean supports(ConversationExecutionPlan plan) {
return plan != null
- && properties.isTableChannelEnabled()
+ && RagRuntimeOptions.resolve(plan, properties).isTableChannelEnabled()
&& !resolvedDocumentIds(plan).isEmpty();
}
@@ -67,15 +68,16 @@ public class TableRetrievalChannel implements RetrievalChannel {
DocumentTableQueryPlan planned = queryPlan.get();
DocumentTableQueryResult result = tableStructureService.query(planned.getQuery());
- Document document = buildEvidenceDocument(subQuestion, planned, result, resolveDocumentNames());
+ Document document = buildEvidenceDocument(subQuestion, planned, result, resolveDocumentDescriptors(plan));
return new RetrievalChannelResult(channelName(), List.of(document));
}
private Document buildEvidenceDocument(String subQuestion,
DocumentTableQueryPlan queryPlan,
DocumentTableQueryResult result,
- Map documentNames) {
- String documentName = StrUtil.blankToDefault(documentNames.get(result.getDocumentId()), "文档表格");
+ Map documentDescriptors) {
+ KnowledgeDocumentDescriptor descriptor = documentDescriptors.get(result.getDocumentId());
+ String documentName = StrUtil.blankToDefault(descriptor == null ? null : descriptor.getDocumentName(), "文档表格");
String text = renderEvidenceText(subQuestion, queryPlan, result);
Map metadata = new LinkedHashMap<>();
metadata.put(DocumentKnowledgeMetadataKeys.SOURCE_TYPE, SOURCE_TYPE);
@@ -83,6 +85,10 @@ public class TableRetrievalChannel implements RetrievalChannel {
metadata.put(DocumentKnowledgeMetadataKeys.SCORE, TABLE_QUERY_SCORE);
metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_ID, result.getDocumentId());
metadata.put(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME, documentName);
+ if (descriptor != null) {
+ putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_ID, descriptor.getKnowledgeBaseId());
+ metadata.put(DocumentKnowledgeMetadataKeys.KNOWLEDGE_BASE_NAME, StrUtil.blankToDefault(descriptor.getKnowledgeBaseName(), ""));
+ }
metadata.put(DocumentKnowledgeMetadataKeys.TASK_ID, result.getTaskId());
metadata.put(DocumentKnowledgeMetadataKeys.SECTION_PATH, StrUtil.blankToDefault(result.getSectionPath(), ""));
putIfNotNull(metadata, DocumentKnowledgeMetadataKeys.PAGE_NO, result.getPageNo());
@@ -165,14 +171,21 @@ public class TableRetrievalChannel implements RetrievalChannel {
return plan.getSelectedTaskId() == null ? List.of() : List.of(plan.getSelectedTaskId());
}
- private Map resolveDocumentNames() {
- Map documentNames = new LinkedHashMap<>();
- for (KnowledgeDocumentDescriptor descriptor : documentKnowledgeService.listRetrievableDocuments()) {
+ private Map resolveDocumentDescriptors(ConversationExecutionPlan plan) {
+ Map documentDescriptors = new LinkedHashMap<>();
+ List documentIds = resolvedDocumentIds(plan);
+ List descriptors = plan == null
+ || plan.getSelectedKnowledgeBaseIds() == null
+ || plan.getSelectedKnowledgeBaseIds().isEmpty()
+ ? documentKnowledgeService.listRetrievableDocuments()
+ : documentKnowledgeService.listRetrievableDocumentsByKnowledgeBaseIds(plan.getSelectedKnowledgeBaseIds());
+ for (KnowledgeDocumentDescriptor descriptor : descriptors) {
if (descriptor.getDocumentId() != null) {
- documentNames.put(descriptor.getDocumentId(), descriptor.getDocumentName());
+ documentDescriptors.put(descriptor.getDocumentId(), descriptor);
}
}
- return documentNames;
+ documentDescriptors.keySet().retainAll(documentIds);
+ return documentDescriptors;
}
private void putIfNotNull(Map metadata, String key, Object value) {
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/VectorRetrievalChannel.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/VectorRetrievalChannel.java
index 7c3adfdf2725f086d56657cd35ec2a10388a206d..fa4e83eccd7fac210f5b51ace05949af7575d383 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/VectorRetrievalChannel.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/retrieve/channel/VectorRetrievalChannel.java
@@ -3,6 +3,7 @@ package org.javaup.ai.chatagent.rag.retrieve.channel;
import cn.hutool.core.collection.CollectionUtil;
import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan;
+import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions;
import org.javaup.ai.chatagent.rag.service.DocumentRetrieveRequestFactory;
import org.javaup.ai.manage.service.DocumentKnowledgeService;
import org.javaup.enums.RetrievalChannelEnum;
@@ -47,7 +48,7 @@ public class VectorRetrievalChannel implements RetrievalChannel {
public RetrievalChannelResult retrieve(String subQuestion, ConversationExecutionPlan plan) {
List documentList = documentKnowledgeService.vectorSearch(
- documentRetrieveRequestFactory.build(subQuestion, plan, properties.getVectorTopK())
+ documentRetrieveRequestFactory.build(subQuestion, plan, RagRuntimeOptions.resolve(plan, properties).getVectorTopK())
);
return new RetrievalChannelResult(
channelName(), documentList
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssembler.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssembler.java
index acad7a0c72e80036203e3a2747bfbf6c87b55276..ef50b5f7a3707378647de014a612700dd12b254c 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssembler.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerHistoryContextAssembler.java
@@ -3,10 +3,13 @@ package org.javaup.ai.chatagent.rag.service;
import cn.hutool.core.util.StrUtil;
import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
import org.javaup.ai.chatagent.rag.model.AnswerHistoryContext;
+import org.javaup.ai.chatagent.rag.model.EvidenceAnchor;
import org.javaup.ai.chatagent.rag.model.QueryType;
import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult;
import org.springframework.stereotype.Service;
+import java.util.List;
+
/**
* @program: 企业级别深度设计 AI Agent。添加 阿星不是程序员 微信,添加时备注 super 来获取项目的完整资料
* @description: 服务层
@@ -29,28 +32,40 @@ public class AnswerHistoryContextAssembler {
public AnswerHistoryContext assemble(String question,
String answerRecentTranscript,
QueryUnderstandingResult queryUnderstanding) {
+ return assemble(question, answerRecentTranscript, queryUnderstanding, List.of());
+ }
+
+ public AnswerHistoryContext assemble(String question,
+ String answerRecentTranscript,
+ QueryUnderstandingResult queryUnderstanding,
+ List recentEvidenceAnchors) {
String normalizedQuestion = safeText(question);
String recentUserContext = extractRecentUserQuestions(answerRecentTranscript);
int totalBudget = Math.max(1, properties.getAnswerHistoryMaxChars());
boolean hasRecentContext = StrUtil.isNotBlank(recentUserContext);
- boolean followUpQuestion = looksLikeFollowUpQuestion(normalizedQuestion, hasRecentContext, queryUnderstanding);
+ boolean followUpQuestion = looksLikeFollowUpQuestion(normalizedQuestion, queryUnderstanding);
+ List anchors = safeAnchors(recentEvidenceAnchors);
- if (!followUpQuestion || !hasRecentContext) {
+ if (!followUpQuestion || (!hasRecentContext && anchors.isEmpty())) {
return emptyContext(totalBudget, followUpQuestion);
}
String recentPart = renderRecentContext(recentUserContext, totalBudget);
- if (recentPart.isBlank()) {
+ String structuredPart = renderStructuredContext(anchors, totalBudget - recentPart.length());
+ String renderedText = joinNonBlank(structuredPart, recentPart);
+ if (renderedText.isBlank() && anchors.isEmpty()) {
return emptyContext(totalBudget, followUpQuestion);
}
return AnswerHistoryContext.builder()
- .renderedText(recentPart)
- .structuredContext("")
+ .renderedText(renderedText)
+ .structuredContext(structuredPart)
.recentContext(recentPart)
+ .evidenceAnchors(anchors)
+ .resolvedTopic(resolveTopic(anchors))
.followUpQuestion(followUpQuestion)
.totalBudget(totalBudget)
- .recentBudget(totalBudget)
- .structuredBudget(0)
+ .recentBudget(recentPart.length())
+ .structuredBudget(structuredPart.length())
.build();
}
@@ -59,6 +74,8 @@ public class AnswerHistoryContextAssembler {
.renderedText("")
.structuredContext("")
.recentContext("")
+ .evidenceAnchors(List.of())
+ .resolvedTopic("")
.followUpQuestion(followUpQuestion)
.totalBudget(totalBudget)
.recentBudget(0)
@@ -89,9 +106,8 @@ public class AnswerHistoryContextAssembler {
}
private boolean looksLikeFollowUpQuestion(String normalizedQuestion,
- boolean hasRecentContext,
QueryUnderstandingResult queryUnderstanding) {
- if (!hasRecentContext || StrUtil.isBlank(normalizedQuestion)) {
+ if (StrUtil.isBlank(normalizedQuestion)) {
return false;
}
QueryType queryType = queryUnderstanding == null || queryUnderstanding.getQueryType() == null
@@ -103,6 +119,62 @@ public class AnswerHistoryContextAssembler {
return false;
}
+ private List safeAnchors(List anchors) {
+ if (anchors == null || anchors.isEmpty()) {
+ return List.of();
+ }
+ return anchors.stream()
+ .filter(anchor -> anchor != null && hasAnchorIdentity(anchor))
+ .limit(5)
+ .toList();
+ }
+
+ private boolean hasAnchorIdentity(EvidenceAnchor anchor) {
+ return anchor.getDocumentId() != null
+ || anchor.getStructureNodeId() != null
+ || anchor.getParentBlockId() != null
+ || anchor.getChunkId() != null
+ || StrUtil.isNotBlank(anchor.getSectionPath());
+ }
+
+ private String renderStructuredContext(List anchors, int budget) {
+ if (anchors == null || anchors.isEmpty() || budget <= 0) {
+ return "";
+ }
+ StringBuilder builder = new StringBuilder("上一轮可继承证据锚点(仅用于解析指代和限定范围,不作为事实证据):\n");
+ for (EvidenceAnchor anchor : anchors) {
+ if (anchor == null) {
+ continue;
+ }
+ builder.append("- 文档: ").append(blankToDash(anchor.getDocumentName())).append('\n');
+ appendAnchorField(builder, " 章节", anchor.getSectionPath());
+ appendAnchorField(builder, " canonicalPath", anchor.getCanonicalPath());
+ appendAnchorField(builder, " structureNodeId", anchor.getStructureNodeId());
+ appendAnchorField(builder, " parentBlockId", anchor.getParentBlockId());
+ appendAnchorField(builder, " chunkId", anchor.getChunkId());
+ appendAnchorField(builder, " itemIndex", anchor.getItemIndex());
+ String snippet = clipHead(anchor.getSnippet(), 300);
+ appendAnchorField(builder, " snippet", snippet);
+ }
+ return clipHead(builder.toString().trim(), budget);
+ }
+
+ private void appendAnchorField(StringBuilder builder, String name, Object value) {
+ String text = value == null ? "" : String.valueOf(value).trim();
+ if (text.isBlank()) {
+ return;
+ }
+ builder.append(name).append(": ").append(text).append('\n');
+ }
+
+ private String resolveTopic(List anchors) {
+ if (anchors == null || anchors.isEmpty()) {
+ return "";
+ }
+ EvidenceAnchor anchor = anchors.get(0);
+ return StrUtil.blankToDefault(anchor.getSectionPath(), StrUtil.blankToDefault(anchor.getDocumentName(), ""));
+ }
+
private String renderRecentContext(String recentUserContext, int budget) {
if (budget <= 0 || StrUtil.isBlank(recentUserContext)) {
return "";
@@ -118,6 +190,29 @@ public class AnswerHistoryContextAssembler {
return title + body;
}
+ private String joinNonBlank(String first, String second) {
+ String left = safeText(first);
+ String right = safeText(second);
+ if (left.isBlank()) {
+ return right;
+ }
+ if (right.isBlank()) {
+ return left;
+ }
+ return left + "\n" + right;
+ }
+
+ private String clipHead(String text, int maxChars) {
+ String normalized = safeText(text);
+ if (normalized.length() <= maxChars) {
+ return normalized;
+ }
+ if (maxChars <= 1) {
+ return "";
+ }
+ return normalized.substring(0, maxChars - 1) + "…";
+ }
+
private String clipTail(String text, int maxChars) {
String normalized = safeText(text);
if (normalized.length() <= maxChars) {
@@ -130,6 +225,10 @@ public class AnswerHistoryContextAssembler {
return "…" + normalized.substring(start);
}
+ private String blankToDash(String text) {
+ return StrUtil.blankToDefault(text, "-");
+ }
+
private String safeText(String text) {
return text == null ? "" : text.trim();
}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerPlanService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerPlanService.java
new file mode 100644
index 0000000000000000000000000000000000000000..d99bc338d06bce5738f22d31f52c52aa6a812b15
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/AnswerPlanService.java
@@ -0,0 +1,61 @@
+package org.javaup.ai.chatagent.rag.service;
+
+import cn.hutool.core.util.StrUtil;
+import org.javaup.ai.chatagent.rag.model.AnswerPlan;
+import org.javaup.ai.chatagent.rag.model.EvidenceRole;
+import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult;
+import org.springframework.stereotype.Service;
+
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+public class AnswerPlanService {
+
+ public AnswerPlan build(QueryUnderstandingResult understanding) {
+ List requiredRoles = normalizeRoles(
+ understanding == null ? null : understanding.getExpectedEvidenceRoles()
+ );
+ if (requiredRoles.isEmpty()) {
+ return AnswerPlan.builder()
+ .requiredRoles(List.of())
+ .optionalRoles(List.of(EvidenceRole.GENERAL))
+ .requireExplicitEvidence(false)
+ .allowRoleFallback(true)
+ .instruction("")
+ .build();
+ }
+ return AnswerPlan.builder()
+ .requiredRoles(requiredRoles)
+ .optionalRoles(List.of(EvidenceRole.GENERAL))
+ .requireExplicitEvidence(true)
+ .allowRoleFallback(false)
+ .instruction(buildInstruction(requiredRoles))
+ .build();
+ }
+
+ private List normalizeRoles(List roles) {
+ if (roles == null || roles.isEmpty()) {
+ return List.of();
+ }
+ return roles.stream()
+ .filter(role -> role != null && role != EvidenceRole.GENERAL)
+ .collect(Collectors.toCollection(LinkedHashSet::new))
+ .stream()
+ .limit(4)
+ .toList();
+ }
+
+ private String buildInstruction(List requiredRoles) {
+ String joined = requiredRoles.stream()
+ .map(Enum::name)
+ .collect(Collectors.joining(" / "));
+ if (StrUtil.isBlank(joined)) {
+ return "";
+ }
+ return "本轮问题期望证据角色:" + joined + "。\n"
+ + "只能用对应角色的证据回答对应问题;如果 final evidence 中没有这些角色,必须说明文档没有明确给出。\n"
+ + "背景证据不能替代对应角色证据。";
+ }
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestrator.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestrator.java
index 0e2b20a4cbcb6e60fb94df4798c664c5d9930096..ba06f435903d151ed8993f4a218c01019b095d50 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestrator.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ChatPreparationOrchestrator.java
@@ -9,22 +9,30 @@ import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
import org.javaup.ai.chatagent.rag.model.AnswerHistoryContext;
import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan;
import org.javaup.ai.chatagent.rag.model.DocumentNavigationDecision;
+import org.javaup.ai.chatagent.rag.model.EvidenceAnchor;
import org.javaup.ai.chatagent.rag.model.ExecutionMode;
import org.javaup.ai.chatagent.rag.model.HistoryPlanningContext;
import org.javaup.ai.chatagent.rag.model.QueryType;
import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult;
import org.javaup.ai.chatagent.rag.model.RagRewriteResult;
import org.javaup.ai.chatagent.rag.model.RetrievalIntent;
+import org.javaup.ai.chatagent.rag.model.StructureNavigationIntent;
+import org.javaup.ai.chatagent.rag.model.StructureNavigationResult;
+import org.javaup.ai.manage.data.SuperAgentDocumentStructureNode;
import org.javaup.ai.chatagent.service.ConversationMemoryService;
import org.javaup.ai.chatagent.service.ConversationTraceRecorder;
import org.javaup.ai.chatagent.service.TaskInfo;
import org.javaup.ai.chatagent.support.TimeSensitiveQueryHelper;
import org.javaup.ai.manage.model.KnowledgeDocumentDescriptor;
+import org.javaup.ai.manage.model.KnowledgeBaseSelectionSnapshot;
import org.javaup.ai.manage.model.route.DocumentRouteCandidate;
+import org.javaup.ai.manage.model.route.KnowledgeRouteContext;
import org.javaup.ai.manage.model.route.KnowledgeRouteDecision;
import org.javaup.ai.manage.service.DocumentKnowledgeService;
import org.javaup.ai.manage.service.KnowledgeRouteService;
import org.javaup.enums.ChatQueryMode;
+import org.javaup.enums.KnowledgeBaseSelectionMode;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
@@ -56,6 +64,8 @@ public class ChatPreparationOrchestrator {
private final DocumentQuestionRouter documentQuestionRouter;
private final KnowledgeRouteService knowledgeRouteService;
private final DocumentKnowledgeService documentKnowledgeService;
+ private final ConversationEvidenceAnchorService conversationEvidenceAnchorService;
+ private final StructureNavigationResolver structureNavigationResolver;
public ChatPreparationOrchestrator(ChatRagProperties properties,
ConversationMemoryService conversationMemoryService,
@@ -64,6 +74,32 @@ public class ChatPreparationOrchestrator {
DocumentQuestionRouter documentQuestionRouter,
KnowledgeRouteService knowledgeRouteService,
DocumentKnowledgeService documentKnowledgeService) {
+ this(properties, conversationMemoryService, answerHistoryContextAssembler, chatQueryRewriteService,
+ documentQuestionRouter, knowledgeRouteService, documentKnowledgeService, null, null);
+ }
+
+ public ChatPreparationOrchestrator(ChatRagProperties properties,
+ ConversationMemoryService conversationMemoryService,
+ AnswerHistoryContextAssembler answerHistoryContextAssembler,
+ ChatQueryRewriteService chatQueryRewriteService,
+ DocumentQuestionRouter documentQuestionRouter,
+ KnowledgeRouteService knowledgeRouteService,
+ DocumentKnowledgeService documentKnowledgeService,
+ ConversationEvidenceAnchorService conversationEvidenceAnchorService) {
+ this(properties, conversationMemoryService, answerHistoryContextAssembler, chatQueryRewriteService,
+ documentQuestionRouter, knowledgeRouteService, documentKnowledgeService, conversationEvidenceAnchorService, null);
+ }
+
+ @Autowired
+ public ChatPreparationOrchestrator(ChatRagProperties properties,
+ ConversationMemoryService conversationMemoryService,
+ AnswerHistoryContextAssembler answerHistoryContextAssembler,
+ ChatQueryRewriteService chatQueryRewriteService,
+ DocumentQuestionRouter documentQuestionRouter,
+ KnowledgeRouteService knowledgeRouteService,
+ DocumentKnowledgeService documentKnowledgeService,
+ ConversationEvidenceAnchorService conversationEvidenceAnchorService,
+ StructureNavigationResolver structureNavigationResolver) {
this.properties = properties;
this.conversationMemoryService = conversationMemoryService;
this.answerHistoryContextAssembler = answerHistoryContextAssembler;
@@ -71,6 +107,8 @@ public class ChatPreparationOrchestrator {
this.documentQuestionRouter = documentQuestionRouter;
this.knowledgeRouteService = knowledgeRouteService;
this.documentKnowledgeService = documentKnowledgeService;
+ this.conversationEvidenceAnchorService = conversationEvidenceAnchorService;
+ this.structureNavigationResolver = structureNavigationResolver;
}
public ConversationExecutionPlan prepare(TaskInfo taskInfo) {
@@ -80,6 +118,7 @@ public class ChatPreparationOrchestrator {
Long selectedDocumentId = taskInfo.selectedDocumentId();
String selectedDocumentName = taskInfo.selectedDocumentName();
Long selectedTaskId = taskInfo.selectedTaskId();
+ KnowledgeBaseSelectionSnapshot knowledgeBaseSelection = taskInfo.knowledgeBaseSelectionSnapshot();
LocalDate currentDate = taskInfo.currentDate();
String currentDateText = taskInfo.currentDateText();
ConversationTraceRecorder traceRecorder = taskInfo.traceRecorder();
@@ -111,10 +150,12 @@ public class ChatPreparationOrchestrator {
HistoryPlanningContext historyPlanningContext = buildHistoryPlanningContext(memoryContext);
String historySummary = buildPlanningHistory(memoryContext, historyPlanningContext);
+ List recentEvidenceAnchors = loadRecentEvidenceAnchors(conversationId);
AnswerHistoryContext answerHistoryContext = buildAnswerHistoryContext(
question,
memoryContext == null ? "" : memoryContext.getAnswerRecentTranscript(),
- null
+ null,
+ List.of()
);
boolean requiresCurrentDateAnchoring = TimeSensitiveQueryHelper.requiresCurrentDateAnchoring(question);
@@ -125,7 +166,7 @@ public class ChatPreparationOrchestrator {
if (chatMode == ChatQueryMode.OPEN_CHAT) {
ConversationExecutionPlan plan = basePlan(question, chatMode, memoryContext, historyPlanningContext, historySummary, answerHistoryContext, currentDate, currentDateText,
- requiresCurrentDateAnchoring, requiresFreshSearch)
+ requiresCurrentDateAnchoring, requiresFreshSearch, knowledgeBaseSelection)
.mode(ExecutionMode.REACT_AGENT)
.build();
if (traceRecorder != null) {
@@ -139,6 +180,12 @@ public class ChatPreparationOrchestrator {
}
return plan;
}
+ if (selectionMode(knowledgeBaseSelection) == KnowledgeBaseSelectionMode.NONE) {
+ return basePlan(question, ChatQueryMode.OPEN_CHAT, memoryContext, historyPlanningContext, historySummary, answerHistoryContext, currentDate, currentDateText,
+ requiresCurrentDateAnchoring, requiresFreshSearch, knowledgeBaseSelection)
+ .mode(ExecutionMode.REACT_AGENT)
+ .build();
+ }
if (!properties.isEnabled()) {
throw new IllegalStateException("当前文档问答模式未启用,请先开启聊天侧 RAG 编排");
@@ -185,9 +232,10 @@ public class ChatPreparationOrchestrator {
List routedDocumentIds = routedDocumentId == null ? List.of() : List.of(routedDocumentId);
List routedTaskIds = routedTaskId == null ? List.of() : List.of(routedTaskId);
if (chatMode == ChatQueryMode.AUTO_DOCUMENT) {
- KnowledgeRouteDecision routeDecision = knowledgeRouteService.route(question, rewriteQuestion);
- knowledgeRouteService.recordAutoRoute(conversationId, taskInfo.exchangeId(), question, rewriteQuestion, routeDecision);
- List candidateDocuments = selectAutoCandidates(routeDecision, question, rewriteQuestion);
+ KnowledgeRouteContext routeContext = buildRouteContext(question, rewriteQuestion, knowledgeBaseSelection);
+ KnowledgeRouteDecision routeDecision = knowledgeRouteService.route(routeContext);
+ knowledgeRouteService.recordAutoRoute(conversationId, taskInfo.exchangeId(), routeContext, routeDecision);
+ List candidateDocuments = selectAutoCandidates(routeDecision, question, rewriteQuestion, allowedDocuments(knowledgeBaseSelection));
boolean lowConfidenceMultiDocumentRetrieval = shouldAllowLowConfidenceMultiDocumentRetrieval(routeDecision, candidateDocuments);
if (lowConfidenceMultiDocumentRetrieval) {
log.info("自动知识路由低置信多文档候选进入检索: conversationId={}, confidence={}, candidateDocumentCount={}, threshold=[{}, {})",
@@ -200,7 +248,7 @@ public class ChatPreparationOrchestrator {
if (shouldAskClarification(routeDecision, candidateDocuments, lowConfidenceMultiDocumentRetrieval)) {
recordAutoDocumentRouteTrace(traceRecorder, routeDecision, candidateDocuments, true, false, null);
return basePlan(question, chatMode, memoryContext, historyPlanningContext, historySummary, answerHistoryContext, currentDate, currentDateText,
- requiresCurrentDateAnchoring, requiresFreshSearch)
+ requiresCurrentDateAnchoring, requiresFreshSearch, knowledgeBaseSelection)
.mode(ExecutionMode.CLARIFICATION)
.rewriteQuestion(rewriteQuestion)
.rewriteSubQuestions(rewriteSubQuestions)
@@ -249,7 +297,8 @@ public class ChatPreparationOrchestrator {
lowConfidenceMultiDocumentRetrieval);
}
else if (chatMode == ChatQueryMode.DOCUMENT) {
- knowledgeRouteService.recordShadowRoute(conversationId, taskInfo.exchangeId(), selectedDocumentId, question, rewriteQuestion);
+ knowledgeRouteService.recordShadowRoute(conversationId, taskInfo.exchangeId(), selectedDocumentId,
+ buildRouteContext(question, rewriteQuestion, knowledgeBaseSelection));
}
ConversationTraceRecorder.StageHandle routeStage = traceRecorder == null
@@ -265,6 +314,11 @@ public class ChatPreparationOrchestrator {
memoryContext == null ? "" : memoryContext.getAnswerRecentTranscript()
);
QueryUnderstandingResult queryUnderstanding = navigationDecision == null ? null : navigationDecision.getQueryUnderstanding();
+ StructureNavigationResult structureNavigationResult = resolveStructureNavigationResult(
+ navigationDecision,
+ routedDocumentId,
+ routedTaskId
+ );
if (traceRecorder != null) {
traceRecorder.completeStage(routeStage, "执行路由完成。", Map.of(
"executionMode", navigationDecision == null || navigationDecision.getExecutionMode() == null ? "" : navigationDecision.getExecutionMode().name(),
@@ -276,6 +330,7 @@ public class ChatPreparationOrchestrator {
? RetrievalIntent.GENERAL.name()
: navigationDecision.getRetrievalIntent().name(),
"queryUnderstanding", buildQueryUnderstandingTrace(queryUnderstanding),
+ "structureNavigation", buildStructureNavigationTrace(structureNavigationResult),
"navigationSummary", navigationDecision == null ? "" : StrUtil.blankToDefault(navigationDecision.getSummaryText(), "")
));
}
@@ -301,10 +356,18 @@ public class ChatPreparationOrchestrator {
? RetrievalIntent.GENERAL
: navigationDecision.getRetrievalIntent();
QueryUnderstandingResult queryUnderstanding = navigationDecision == null ? null : navigationDecision.getQueryUnderstanding();
+ List scopedEvidenceAnchors = filterEvidenceAnchors(
+ recentEvidenceAnchors,
+ chatMode,
+ routedDocumentId,
+ knowledgeBaseSelection
+ );
+ appendAnchorHints(historyPlanningContext, scopedEvidenceAnchors);
AnswerHistoryContext routedAnswerHistoryContext = buildAnswerHistoryContext(
question,
memoryContext == null ? "" : memoryContext.getAnswerRecentTranscript(),
- queryUnderstanding
+ queryUnderstanding,
+ scopedEvidenceAnchors
);
log.info("聊天编排完成: conversationId={}, chatMode={}, originalQuestion='{}', rewriteQuestion='{}', retrievalQuestion='{}', executionMode={}, retrievalIntent={}, targetSection='{}'",
@@ -318,7 +381,7 @@ public class ChatPreparationOrchestrator {
navigationDecision == null || navigationDecision.getStructureAnchor() == null ? "" : safeText(navigationDecision.getStructureAnchor().getTargetSectionHint()));
return basePlan(question, chatMode, memoryContext, historyPlanningContext, historySummary, routedAnswerHistoryContext, currentDate, currentDateText,
- requiresCurrentDateAnchoring, requiresFreshSearch)
+ requiresCurrentDateAnchoring, requiresFreshSearch, knowledgeBaseSelection)
.mode(executionMode)
.navigationDecision(navigationDecision)
.queryUnderstanding(queryUnderstanding)
@@ -336,6 +399,27 @@ public class ChatPreparationOrchestrator {
.build();
}
+ private StructureNavigationResult resolveStructureNavigationResult(DocumentNavigationDecision navigationDecision,
+ Long routedDocumentId,
+ Long routedTaskId) {
+ if (structureNavigationResolver == null || navigationDecision == null || routedDocumentId == null) {
+ return null;
+ }
+ QueryUnderstandingResult queryUnderstanding = navigationDecision.getQueryUnderstanding();
+ StructureNavigationIntent intent = queryUnderstanding == null ? null : queryUnderstanding.getStructureNavigationIntent();
+ if (intent == null) {
+ return null;
+ }
+ StructureNavigationResult result = structureNavigationResolver.resolve(
+ routedDocumentId,
+ routedTaskId,
+ intent,
+ navigationDecision.getStructureAnchor()
+ );
+ navigationDecision.setStructureNavigationResult(result);
+ return result;
+ }
+
private Map buildQueryUnderstandingTrace(QueryUnderstandingResult queryUnderstanding) {
if (queryUnderstanding == null) {
return Map.of();
@@ -344,15 +428,69 @@ public class ChatPreparationOrchestrator {
snapshot.put("queryType", queryUnderstanding.getQueryType() == null ? "" : queryUnderstanding.getQueryType().name());
snapshot.put("channels", queryUnderstanding.getChannels() == null ? List.of() : queryUnderstanding.getChannels().stream().map(Enum::name).toList());
snapshot.put("entities", queryUnderstanding.getEntities() == null ? List.of() : queryUnderstanding.getEntities());
+ snapshot.put("targetEntities", queryUnderstanding.getTargetEntities() == null ? List.of() : queryUnderstanding.getTargetEntities());
+ snapshot.put("excludedEntities", queryUnderstanding.getExcludedEntities() == null ? List.of() : queryUnderstanding.getExcludedEntities());
snapshot.put("sectionAnchors", queryUnderstanding.getSectionAnchors() == null ? List.of() : queryUnderstanding.getSectionAnchors());
+ snapshot.put("structureNavigationIntent", buildStructureNavigationIntentTrace(queryUnderstanding.getStructureNavigationIntent()));
snapshot.put("tableOps", queryUnderstanding.getTableOps() == null ? List.of() : queryUnderstanding.getTableOps());
snapshot.put("negativeBoundary", queryUnderstanding.isNegativeBoundary());
+ snapshot.put("answerExpectation", StrUtil.blankToDefault(queryUnderstanding.getAnswerExpectation(), ""));
snapshot.put("confidence", queryUnderstanding.getConfidence());
snapshot.put("source", StrUtil.blankToDefault(queryUnderstanding.getSource(), ""));
snapshot.put("reasons", queryUnderstanding.getReasons() == null ? List.of() : queryUnderstanding.getReasons());
return snapshot;
}
+ private Map buildStructureNavigationIntentTrace(StructureNavigationIntent intent) {
+ if (intent == null) {
+ return Map.of();
+ }
+ Map snapshot = new LinkedHashMap<>();
+ snapshot.put("operations", intent.getOperations() == null ? List.of() : intent.getOperations().stream().map(Enum::name).toList());
+ snapshot.put("anchorStructureNodeId", intent.getAnchorStructureNodeId() == null ? "" : String.valueOf(intent.getAnchorStructureNodeId()));
+ snapshot.put("anchorSectionPath", StrUtil.blankToDefault(intent.getAnchorSectionPath(), ""));
+ snapshot.put("anchorCanonicalPath", StrUtil.blankToDefault(intent.getAnchorCanonicalPath(), ""));
+ snapshot.put("sectionAnchors", intent.getSectionAnchors() == null ? List.of() : intent.getSectionAnchors());
+ snapshot.put("confidence", intent.getConfidence());
+ snapshot.put("source", StrUtil.blankToDefault(intent.getSource(), ""));
+ return snapshot;
+ }
+
+ private Map buildStructureNavigationTrace(StructureNavigationResult result) {
+ if (result == null) {
+ return Map.of();
+ }
+ Map snapshot = new LinkedHashMap<>();
+ snapshot.put("documentId", result.getDocumentId() == null ? "" : String.valueOf(result.getDocumentId()));
+ snapshot.put("anchorNodeId", result.getAnchorNodeId() == null ? "" : String.valueOf(result.getAnchorNodeId()));
+ snapshot.put("current", buildStructureNodeTrace(result.getCurrent()));
+ snapshot.put("parent", buildStructureNodeTrace(result.getParent()));
+ snapshot.put("previous", buildStructureNodeTrace(result.getPreviousSibling()));
+ snapshot.put("next", buildStructureNodeTrace(result.getNextSibling()));
+ snapshot.put("directChildren", result.getDirectChildren() == null
+ ? List.of()
+ : result.getDirectChildren().stream().map(this::buildStructureNodeTrace).toList());
+ snapshot.put("deterministic", result.isDeterministic());
+ snapshot.put("missReason", StrUtil.blankToDefault(result.getMissReason(), ""));
+ return snapshot;
+ }
+
+ private Map buildStructureNodeTrace(SuperAgentDocumentStructureNode node) {
+ if (node == null) {
+ return Map.of();
+ }
+ Map snapshot = new LinkedHashMap<>();
+ snapshot.put("nodeId", node.getId() == null ? "" : String.valueOf(node.getId()));
+ snapshot.put("nodeNo", node.getNodeNo() == null ? "" : String.valueOf(node.getNodeNo()));
+ snapshot.put("title", StrUtil.blankToDefault(node.getTitle(), ""));
+ snapshot.put("sectionPath", StrUtil.blankToDefault(node.getSectionPath(), ""));
+ snapshot.put("canonicalPath", StrUtil.blankToDefault(node.getCanonicalPath(), ""));
+ snapshot.put("parentNodeId", node.getParentNodeId() == null ? "" : String.valueOf(node.getParentNodeId()));
+ snapshot.put("prevSiblingNodeId", node.getPrevSiblingNodeId() == null ? "" : String.valueOf(node.getPrevSiblingNodeId()));
+ snapshot.put("nextSiblingNodeId", node.getNextSiblingNodeId() == null ? "" : String.valueOf(node.getNextSiblingNodeId()));
+ return snapshot;
+ }
+
private ConversationExecutionPlan.ConversationExecutionPlanBuilder basePlan(String question,
ChatQueryMode chatMode,
ConversationMemoryContext memoryContext,
@@ -362,7 +500,8 @@ public class ChatPreparationOrchestrator {
LocalDate currentDate,
String currentDateText,
boolean requiresCurrentDateAnchoring,
- boolean requiresFreshSearch) {
+ boolean requiresFreshSearch,
+ KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) {
return ConversationExecutionPlan.builder()
.chatMode(chatMode)
.originalQuestion(question)
@@ -385,6 +524,11 @@ public class ChatPreparationOrchestrator {
.currentDateText(currentDateText)
.requiresCurrentDateAnchoring(requiresCurrentDateAnchoring)
.requiresFreshSearch(requiresFreshSearch)
+ .knowledgeBaseSelectionMode(selectionMode(knowledgeBaseSelection))
+ .selectedKnowledgeBaseIds(selectedKnowledgeBaseIds(knowledgeBaseSelection))
+ .selectedKnowledgeBaseNames(selectedKnowledgeBaseNames(knowledgeBaseSelection))
+ .allowedKnowledgeBaseDocumentIds(allowedDocumentIds(knowledgeBaseSelection))
+ .ragRuntimeOptions(knowledgeBaseSelection == null ? null : knowledgeBaseSelection.getRagRuntimeOptions())
.noEvidenceReply(properties.getNoEvidenceReply());
}
@@ -468,7 +612,7 @@ public class ChatPreparationOrchestrator {
.limit(5)
.map(scope -> {
Map item = new LinkedHashMap<>();
- item.put("scopeCode", StrUtil.blankToDefault(scope.getScopeCode(), ""));
+ item.put("scopeId", scope.getScopeId() == null ? "" : String.valueOf(scope.getScopeId()));
item.put("scopeName", StrUtil.blankToDefault(scope.getScopeName(), ""));
item.put("score", scope.getScore() == null ? "" : scope.getScore().toPlainString());
item.put("reason", StrUtil.blankToDefault(scope.getReason(), ""));
@@ -485,8 +629,8 @@ public class ChatPreparationOrchestrator {
.limit(5)
.map(topic -> {
Map item = new LinkedHashMap<>();
- item.put("scopeCode", StrUtil.blankToDefault(topic.getScopeCode(), ""));
- item.put("topicCode", StrUtil.blankToDefault(topic.getTopicCode(), ""));
+ item.put("scopeId", topic.getScopeId() == null ? "" : String.valueOf(topic.getScopeId()));
+ item.put("topicId", topic.getTopicId() == null ? "" : String.valueOf(topic.getTopicId()));
item.put("topicName", StrUtil.blankToDefault(topic.getTopicName(), ""));
item.put("score", topic.getScore() == null ? "" : topic.getScore().toPlainString());
item.put("reason", StrUtil.blankToDefault(topic.getReason(), ""));
@@ -506,10 +650,6 @@ public class ChatPreparationOrchestrator {
item.put("documentId", StrUtil.blankToDefault(document.getDocumentId(), ""));
item.put("documentName", StrUtil.blankToDefault(document.getDocumentName(), ""));
item.put("lastIndexTaskId", StrUtil.blankToDefault(document.getLastIndexTaskId(), ""));
- item.put("knowledgeScopeCode", StrUtil.blankToDefault(document.getKnowledgeScopeCode(), ""));
- item.put("knowledgeScopeName", StrUtil.blankToDefault(document.getKnowledgeScopeName(), ""));
- item.put("businessCategory", StrUtil.blankToDefault(document.getBusinessCategory(), ""));
- item.put("documentTags", StrUtil.blankToDefault(document.getDocumentTags(), ""));
item.put("score", document.getScore() == null ? "" : document.getScore().toPlainString());
item.put("reason", StrUtil.blankToDefault(document.getReason(), ""));
return item;
@@ -572,7 +712,96 @@ public class ChatPreparationOrchestrator {
private AnswerHistoryContext buildAnswerHistoryContext(String question,
String answerRecentTranscript,
QueryUnderstandingResult queryUnderstanding) {
- return answerHistoryContextAssembler.assemble(question, answerRecentTranscript, queryUnderstanding);
+ return buildAnswerHistoryContext(question, answerRecentTranscript, queryUnderstanding, List.of());
+ }
+
+ private AnswerHistoryContext buildAnswerHistoryContext(String question,
+ String answerRecentTranscript,
+ QueryUnderstandingResult queryUnderstanding,
+ List recentEvidenceAnchors) {
+ return answerHistoryContextAssembler.assemble(question, answerRecentTranscript, queryUnderstanding, recentEvidenceAnchors);
+ }
+
+ private List loadRecentEvidenceAnchors(String conversationId) {
+ if (conversationEvidenceAnchorService == null || StrUtil.isBlank(conversationId)) {
+ return List.of();
+ }
+ try {
+ return conversationEvidenceAnchorService.loadRecentEvidenceAnchors(conversationId, 5);
+ }
+ catch (RuntimeException exception) {
+ log.warn("加载上一轮 evidence anchor 失败: conversationId={}, message={}",
+ conversationId,
+ exception.getMessage(),
+ exception);
+ return List.of();
+ }
+ }
+
+ private List filterEvidenceAnchors(List anchors,
+ ChatQueryMode chatMode,
+ Long selectedDocumentId,
+ KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) {
+ if (anchors == null || anchors.isEmpty()) {
+ return List.of();
+ }
+ if (chatMode == ChatQueryMode.DOCUMENT && selectedDocumentId != null) {
+ return anchors.stream()
+ .filter(anchor -> anchor != null && Objects.equals(anchor.getDocumentId(), selectedDocumentId))
+ .toList();
+ }
+ if (chatMode == ChatQueryMode.AUTO_DOCUMENT
+ && knowledgeBaseSelection != null
+ && knowledgeBaseSelection.getAllowedDocumentIds() != null
+ && !knowledgeBaseSelection.getAllowedDocumentIds().isEmpty()) {
+ return anchors.stream()
+ .filter(anchor -> anchor != null && anchor.getDocumentId() != null)
+ .filter(anchor -> knowledgeBaseSelection.getAllowedDocumentIds().contains(anchor.getDocumentId()))
+ .toList();
+ }
+ return anchors;
+ }
+
+ private void appendAnchorHints(HistoryPlanningContext historyPlanningContext, List anchors) {
+ if (historyPlanningContext == null || anchors == null || anchors.isEmpty()) {
+ return;
+ }
+ List hints = new ArrayList<>(historyPlanningContext.getQueryContextHints() == null
+ ? List.of()
+ : historyPlanningContext.getQueryContextHints());
+ anchors.stream()
+ .map(this::anchorHint)
+ .filter(StrUtil::isNotBlank)
+ .limit(5)
+ .forEach(hints::add);
+ historyPlanningContext.setQueryContextHints(hints);
+ }
+
+ private String anchorHint(EvidenceAnchor anchor) {
+ if (anchor == null) {
+ return "";
+ }
+ StringBuilder builder = new StringBuilder();
+ appendHintPart(builder, "documentId", anchor.getDocumentId());
+ appendHintPart(builder, "sectionPath", anchor.getSectionPath());
+ appendHintPart(builder, "structureNodeId", anchor.getStructureNodeId());
+ appendHintPart(builder, "parentBlockId", anchor.getParentBlockId());
+ appendHintPart(builder, "chunkId", anchor.getChunkId());
+ return builder.toString().trim();
+ }
+
+ private void appendHintPart(StringBuilder builder, String name, Object value) {
+ if (value == null) {
+ return;
+ }
+ String text = String.valueOf(value).trim();
+ if (text.isBlank()) {
+ return;
+ }
+ if (!builder.isEmpty()) {
+ builder.append("; ");
+ }
+ builder.append(name).append('=').append(text);
}
private String buildStructuredPlanningHistory(HistoryPlanningContext historyPlanningContext) {
@@ -657,28 +886,36 @@ public class ChatPreparationOrchestrator {
private List selectAutoCandidates(KnowledgeRouteDecision routeDecision,
String question,
- String rewriteQuestion) {
+ String rewriteQuestion,
+ List allowedDocuments) {
if (routeDecision == null || routeDecision.getDocuments() == null || routeDecision.getDocuments().isEmpty()) {
- return expandCandidatesByDocumentProfile(question, rewriteQuestion, 5);
+ return expandCandidatesByDocumentProfile(question, rewriteQuestion, allowedDocuments, 5);
}
int candidateLimit = routeDecision.getConfidence() != null && routeDecision.getConfidence().doubleValue() >= 0.80D ? 3 : 5;
+ List allowedDocumentIds = allowedDocuments == null
+ ? List.of()
+ : allowedDocuments.stream().map(KnowledgeDocumentDescriptor::getDocumentId).filter(Objects::nonNull).toList();
List candidates = routeDecision.getDocuments().stream()
.filter(item -> StrUtil.isNotBlank(item.getDocumentId()) && StrUtil.isNotBlank(item.getLastIndexTaskId()))
+ .filter(item -> allowedDocumentIds.isEmpty() || allowedDocumentIds.contains(Long.valueOf(item.getDocumentId())))
.limit(candidateLimit)
.toList();
if (candidates.isEmpty()) {
- return expandCandidatesByDocumentProfile(question, rewriteQuestion, candidateLimit);
+ return expandCandidatesByDocumentProfile(question, rewriteQuestion, allowedDocuments, candidateLimit);
}
if (routeDecision.getConfidence() != null && routeDecision.getConfidence().doubleValue() < confidentDocumentThreshold()) {
- return mergeCandidates(candidates, expandCandidatesByDocumentProfile(question, rewriteQuestion, candidateLimit), candidateLimit);
+ return mergeCandidates(candidates, expandCandidatesByDocumentProfile(question, rewriteQuestion, allowedDocuments, candidateLimit), candidateLimit);
}
return candidates;
}
private List expandCandidatesByDocumentProfile(String question,
String rewriteQuestion,
+ List allowedDocuments,
int limit) {
- List descriptors = documentKnowledgeService.listRetrievableDocuments();
+ List descriptors = allowedDocuments == null || allowedDocuments.isEmpty()
+ ? List.of()
+ : allowedDocuments;
if (descriptors == null || descriptors.isEmpty()) {
return List.of();
}
@@ -693,16 +930,56 @@ public class ChatPreparationOrchestrator {
String.valueOf(item.getDocumentId()),
item.getDocumentName(),
item.getLastIndexTaskId() == null ? "" : String.valueOf(item.getLastIndexTaskId()),
- StrUtil.blankToDefault(item.getKnowledgeScopeCode(), ""),
- StrUtil.blankToDefault(item.getKnowledgeScopeName(), ""),
- StrUtil.blankToDefault(item.getBusinessCategory(), ""),
- StrUtil.blankToDefault(item.getDocumentTags(), ""),
BigDecimal.valueOf(descriptorRouteScore(item, queryTerms)).setScale(4, RoundingMode.HALF_UP),
"低置信度时基于文档画像扩展候选范围"
))
.toList();
}
+ private KnowledgeRouteContext buildRouteContext(String question,
+ String rewriteQuestion,
+ KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) {
+ return KnowledgeRouteContext.builder()
+ .question(question)
+ .rewriteQuestion(rewriteQuestion)
+ .knowledgeBaseSelectionMode(selectionMode(knowledgeBaseSelection))
+ .selectedKnowledgeBaseIds(selectedKnowledgeBaseIds(knowledgeBaseSelection))
+ .selectedKnowledgeBaseNames(selectedKnowledgeBaseNames(knowledgeBaseSelection))
+ .allowedDocuments(allowedDocuments(knowledgeBaseSelection))
+ .allowedDocumentIds(allowedDocumentIds(knowledgeBaseSelection))
+ .build();
+ }
+
+ private KnowledgeBaseSelectionMode selectionMode(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) {
+ return knowledgeBaseSelection == null || knowledgeBaseSelection.getSelectionMode() == null
+ ? KnowledgeBaseSelectionMode.NONE
+ : knowledgeBaseSelection.getSelectionMode();
+ }
+
+ private List selectedKnowledgeBaseIds(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) {
+ return knowledgeBaseSelection == null || knowledgeBaseSelection.getSelectedKnowledgeBaseIds() == null
+ ? List.of()
+ : knowledgeBaseSelection.getSelectedKnowledgeBaseIds();
+ }
+
+ private List selectedKnowledgeBaseNames(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) {
+ return knowledgeBaseSelection == null || knowledgeBaseSelection.getSelectedKnowledgeBaseNames() == null
+ ? List.of()
+ : knowledgeBaseSelection.getSelectedKnowledgeBaseNames();
+ }
+
+ private List allowedDocuments(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) {
+ return knowledgeBaseSelection == null || knowledgeBaseSelection.getAllowedDocuments() == null
+ ? List.of()
+ : knowledgeBaseSelection.getAllowedDocuments();
+ }
+
+ private List allowedDocumentIds(KnowledgeBaseSelectionSnapshot knowledgeBaseSelection) {
+ return knowledgeBaseSelection == null || knowledgeBaseSelection.getAllowedDocumentIds() == null
+ ? List.of()
+ : knowledgeBaseSelection.getAllowedDocumentIds();
+ }
+
private List mergeCandidates(List primary,
List secondary,
int limit) {
@@ -735,8 +1012,7 @@ public class ChatPreparationOrchestrator {
if (topScore == null || secondScore == null) {
return false;
}
- return topScore.subtract(secondScore).doubleValue() <= 3D
- && !Objects.equals(candidateDocuments.get(0).getKnowledgeScopeCode(), candidateDocuments.get(1).getKnowledgeScopeCode());
+ return topScore.subtract(secondScore).doubleValue() <= 3D;
}
private boolean shouldAllowLowConfidenceMultiDocumentRetrieval(KnowledgeRouteDecision routeDecision,
@@ -790,11 +1066,6 @@ public class ChatPreparationOrchestrator {
.append(". 《")
.append(StrUtil.blankToDefault(item.getDocumentName(), item.getDocumentId()))
.append("》");
- if (StrUtil.isNotBlank(item.getKnowledgeScopeName()) || StrUtil.isNotBlank(item.getKnowledgeScopeCode())) {
- builder.append("(")
- .append(StrUtil.blankToDefault(item.getKnowledgeScopeName(), item.getKnowledgeScopeCode()))
- .append(")");
- }
builder.append('\n');
}
builder.append("你可以直接回复文档名,或者改用“当前文档问答”模式明确指定文档。");
@@ -844,10 +1115,7 @@ public class ChatPreparationOrchestrator {
private double descriptorRouteScore(KnowledgeDocumentDescriptor descriptor, List queryTerms) {
String content = normalizeRouteExpansionText(String.join(" ",
StrUtil.blankToDefault(descriptor.getDocumentName(), ""),
- StrUtil.blankToDefault(descriptor.getKnowledgeScopeCode(), ""),
- StrUtil.blankToDefault(descriptor.getKnowledgeScopeName(), ""),
- StrUtil.blankToDefault(descriptor.getBusinessCategory(), ""),
- StrUtil.blankToDefault(descriptor.getDocumentTags(), "")
+ StrUtil.blankToDefault(descriptor.getKnowledgeBaseName(), "")
));
if (queryTerms == null || queryTerms.isEmpty() || content.isBlank()) {
return 0D;
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ConversationEvidenceAnchorService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ConversationEvidenceAnchorService.java
new file mode 100644
index 0000000000000000000000000000000000000000..b81ff984ad1e1a55fdb46ddac2e7b998390d0309
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/ConversationEvidenceAnchorService.java
@@ -0,0 +1,97 @@
+package org.javaup.ai.chatagent.rag.service;
+
+import cn.hutool.core.util.StrUtil;
+import org.javaup.ai.chatagent.model.ConversationExchangeView;
+import org.javaup.ai.chatagent.model.SearchReference;
+import org.javaup.ai.chatagent.rag.model.EvidenceAnchor;
+import org.javaup.ai.chatagent.service.ConversationArchiveStore;
+import org.javaup.enums.ChatTurnStatus;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 从上一轮最终引用中抽取追问可继承的结构锚点。
+ */
+@Service
+public class ConversationEvidenceAnchorService {
+
+ private final ConversationArchiveStore conversationArchiveStore;
+
+ public ConversationEvidenceAnchorService(ConversationArchiveStore conversationArchiveStore) {
+ this.conversationArchiveStore = conversationArchiveStore;
+ }
+
+ public List loadRecentEvidenceAnchors(String conversationId, int limit) {
+ if (StrUtil.isBlank(conversationId) || limit <= 0 || conversationArchiveStore == null) {
+ return List.of();
+ }
+ List exchanges = conversationArchiveStore.listRecentExchanges(conversationId, 3);
+ if (exchanges == null || exchanges.isEmpty()) {
+ return List.of();
+ }
+ List anchors = new ArrayList<>();
+ for (ConversationExchangeView exchange : exchanges) {
+ if (exchange == null || !completed(exchange) || exchange.getReferences() == null || exchange.getReferences().isEmpty()) {
+ continue;
+ }
+ for (SearchReference reference : exchange.getReferences()) {
+ EvidenceAnchor anchor = fromReference(reference);
+ if (anchor == null) {
+ continue;
+ }
+ anchors.add(anchor);
+ if (anchors.size() >= limit) {
+ return anchors;
+ }
+ }
+ }
+ return anchors;
+ }
+
+ private boolean completed(ConversationExchangeView exchange) {
+ return exchange.getStatus() == null || exchange.getStatus() == ChatTurnStatus.COMPLETED;
+ }
+
+ private EvidenceAnchor fromReference(SearchReference reference) {
+ if (reference == null || !hasUsableAnchor(reference)) {
+ return null;
+ }
+ return EvidenceAnchor.builder()
+ .documentId(reference.getDocumentId())
+ .documentName(reference.getDocumentName())
+ .knowledgeBaseId(reference.getKnowledgeBaseId())
+ .knowledgeBaseName(reference.getKnowledgeBaseName())
+ .structureNodeId(reference.getStructureNodeId())
+ .sectionPath(reference.getSectionPath())
+ .canonicalPath(reference.getCanonicalPath())
+ .itemIndex(reference.getItemIndex())
+ .parentBlockId(reference.getParentBlockId())
+ .chunkId(reference.getChunkId())
+ .sourceType(reference.getSourceType())
+ .channel(reference.getChannel())
+ .snippet(clip(reference.getSnippet(), 300))
+ .score(reference.getScore())
+ .build();
+ }
+
+ private boolean hasUsableAnchor(SearchReference reference) {
+ return reference.getDocumentId() != null
+ || reference.getStructureNodeId() != null
+ || reference.getParentBlockId() != null
+ || reference.getChunkId() != null
+ || StrUtil.isNotBlank(reference.getSectionPath());
+ }
+
+ private String clip(String text, int maxChars) {
+ String normalized = StrUtil.blankToDefault(text, "").trim();
+ if (normalized.length() <= maxChars) {
+ return normalized;
+ }
+ if (maxChars <= 1) {
+ return "";
+ }
+ return normalized.substring(0, maxChars - 1) + "…";
+ }
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouter.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouter.java
index 155788b18c3f08208fc5534969805ae09d16c159..441fc48d5a962fc398d4a31e7b59e2a842a17e19 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouter.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentQuestionRouter.java
@@ -12,6 +12,8 @@ import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult;
import org.javaup.ai.chatagent.rag.model.RagRewriteResult;
import org.javaup.ai.chatagent.rag.model.RetrievalQuestionPlan;
import org.javaup.ai.chatagent.rag.model.RetrievalIntent;
+import org.javaup.ai.chatagent.rag.model.StructureNavigationIntent;
+import org.javaup.ai.chatagent.rag.model.StructureNavigationOperation;
import org.javaup.ai.manage.model.graph.GraphSection;
import org.javaup.ai.manage.service.DocumentNavigationIndexService;
import org.javaup.ai.manage.service.DocumentStructureGraphService;
@@ -92,6 +94,21 @@ public class DocumentQuestionRouter {
RetrievalIntent retrievalIntent = detectRetrievalIntent(questionIntent, queryUnderstanding);
GraphOnlyIntentDecision graphOnlyIntent = questionIntent.graphOnlyIntent();
boolean analyticQuestion = questionIntent.analytic();
+
+ DocumentNavigationAction structureNavigationAction = resolveStructureNavigationAction(queryUnderstanding);
+ if (structureNavigationAction != null && subQuestions.size() <= 1) {
+ GraphSection section = resolveSection(documentId, originalQuestion, rewrittenQuestion);
+ return buildDecision(
+ ExecutionMode.RETRIEVAL,
+ structureNavigationAction,
+ section,
+ null,
+ retrievalPlan,
+ queryUnderstanding,
+ RetrievalIntent.STRUCTURE,
+ "高置信结构导航走结构树确定性查询,结构结果作为检索上下文和观测信号。"
+ );
+ }
boolean singleQuestionGraphOnlyMatched = graphOnlyIntent.matched() && subQuestions.size() <= 1;
if (singleQuestionGraphOnlyMatched) {
@@ -110,7 +127,8 @@ public class DocumentQuestionRouter {
Integer itemIndex = resolveExplicitItemIndex(routeText);
boolean itemLookupMatched = itemIndex != null || questionIntent.itemLookup();
- boolean shouldUseGraphThenEvidence = itemLookupMatched && !analyticQuestion;
+ boolean shouldUseGraphThenEvidence = itemLookupMatched
+ && shouldUseGraphThenEvidence(routeText, itemIndex, queryUnderstanding);
if (shouldUseGraphThenEvidence) {
GraphSection section = resolveSection(documentId, originalQuestion, rewrittenQuestion);
return buildDecision(
@@ -121,7 +139,7 @@ public class DocumentQuestionRouter {
retrievalPlan,
queryUnderstanding,
retrievalIntent,
- "编号项或步骤型问题走图定位取证"
+ "高置信结构导航编号项问题走图定位取证"
);
}
@@ -218,6 +236,36 @@ public class DocumentQuestionRouter {
return primaryRetrievalIntent(queryUnderstanding);
}
+ private DocumentNavigationAction resolveStructureNavigationAction(QueryUnderstandingResult queryUnderstanding) {
+ if (queryUnderstanding == null || queryUnderstanding.getQueryType() != QueryType.STRUCTURE_NAVIGATION) {
+ return null;
+ }
+ if (confidence(queryUnderstanding) < 0.65D) {
+ return null;
+ }
+ StructureNavigationIntent intent = queryUnderstanding.getStructureNavigationIntent();
+ if (intent == null || intent.getOperations() == null || intent.getOperations().isEmpty()) {
+ return null;
+ }
+ List operations = intent.getOperations();
+ if (operations.contains(StructureNavigationOperation.SECTION_WITH_CHILDREN)
+ || operations.contains(StructureNavigationOperation.DIRECT_CHILDREN)) {
+ return DocumentNavigationAction.CHILD_SECTION_DESCEND;
+ }
+ if (operations.contains(StructureNavigationOperation.SECTION_WITH_SIBLINGS)
+ || operations.contains(StructureNavigationOperation.PREVIOUS_SIBLING)
+ || operations.contains(StructureNavigationOperation.NEXT_SIBLING)) {
+ return DocumentNavigationAction.SECTION_ADJACENCY_LOOKUP;
+ }
+ if (operations.contains(StructureNavigationOperation.PARENT_SECTION)) {
+ return DocumentNavigationAction.ANCESTOR_SECTION_RETURN;
+ }
+ if (operations.contains(StructureNavigationOperation.CURRENT_SECTION)) {
+ return DocumentNavigationAction.FRESH_TOPIC;
+ }
+ return null;
+ }
+
private RetrievalIntent primaryRetrievalIntent(QueryUnderstandingResult queryUnderstanding) {
if (queryUnderstanding == null) {
return RetrievalIntent.GENERAL;
@@ -344,6 +392,24 @@ public class DocumentQuestionRouter {
return false;
}
+ private boolean shouldUseGraphThenEvidence(String routeText,
+ Integer itemIndex,
+ QueryUnderstandingResult queryUnderstanding) {
+ if (itemIndex == null || queryUnderstanding == null) {
+ return false;
+ }
+ QueryType queryType = queryUnderstanding.getQueryType() == null
+ ? QueryType.DOCUMENT_QA
+ : queryUnderstanding.getQueryType();
+ if (queryType != QueryType.STRUCTURE_NAVIGATION) {
+ return false;
+ }
+ if (confidence(queryUnderstanding) < 0.72D) {
+ return false;
+ }
+ return hasExplicitSectionAnchor(routeText) || hasSectionAnchor(queryUnderstanding);
+ }
+
private GraphOnlyIntentDecision detectGraphOnlyIntentByControlledPlan(String question,
QueryUnderstandingResult queryUnderstanding) {
if (asksOutlineByAnchors(queryUnderstanding)) {
@@ -745,6 +811,10 @@ public class DocumentQuestionRouter {
if (queryUnderstanding.getSectionAnchors() != null) {
queryUnderstanding.getSectionAnchors().forEach(item -> addHint(hints, item));
}
+ StructureNavigationIntent structureIntent = queryUnderstanding.getStructureNavigationIntent();
+ if (structureIntent != null && structureIntent.getSectionAnchors() != null) {
+ structureIntent.getSectionAnchors().forEach(item -> addHint(hints, item));
+ }
}
if (section != null) {
addHint(hints, section.displayTitle());
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentRetrieveRequestFactory.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentRetrieveRequestFactory.java
index d91d30df4f89bda82c637c93326a8da8a5075283..3295c6a9f618dbfa800bcb2042d9888deb3cf2d7 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentRetrieveRequestFactory.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentRetrieveRequestFactory.java
@@ -114,8 +114,6 @@ public class DocumentRetrieveRequestFactory {
return DocumentRetrieveFilters.builder().build();
}
LinkedHashSet documentNameHints = new LinkedHashSet<>();
- LinkedHashSet businessCategoryHints = new LinkedHashSet<>();
- LinkedHashSet documentTagHints = new LinkedHashSet<>();
LinkedHashSet sectionPathHints = new LinkedHashSet<>();
LinkedHashSet yearHints = new LinkedHashSet<>();
@@ -131,12 +129,10 @@ public class DocumentRetrieveRequestFactory {
}
}
- collectQueryUnderstandingHints(queryUnderstanding, documentNameHints, businessCategoryHints, documentTagHints, sectionPathHints);
+ collectQueryUnderstandingHints(queryUnderstanding, documentNameHints, sectionPathHints);
return DocumentRetrieveFilters.builder()
.documentNameHints(new ArrayList<>(documentNameHints))
- .businessCategoryHints(new ArrayList<>(businessCategoryHints))
- .documentTagHints(new ArrayList<>(documentTagHints))
.sectionPathHints(new ArrayList<>(sectionPathHints))
.yearHints(new ArrayList<>(yearHints))
.build();
@@ -144,8 +140,6 @@ public class DocumentRetrieveRequestFactory {
private void collectQueryUnderstandingHints(QueryUnderstandingResult queryUnderstanding,
LinkedHashSet documentNameHints,
- LinkedHashSet businessCategoryHints,
- LinkedHashSet documentTagHints,
LinkedHashSet sectionPathHints) {
if (queryUnderstanding == null) {
return;
@@ -155,11 +149,7 @@ public class DocumentRetrieveRequestFactory {
.filter(StrUtil::isNotBlank)
.map(String::trim)
.limit(8)
- .forEach(entity -> {
- documentNameHints.add(entity);
- businessCategoryHints.add(entity);
- documentTagHints.add(entity);
- });
+ .forEach(documentNameHints::add);
}
if (queryUnderstanding.getSectionAnchors() != null) {
queryUnderstanding.getSectionAnchors().stream()
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentTableQueryPlanner.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentTableQueryPlanner.java
index fe1d8760b4b9dd30afd45338aea5d98476b415bf..6c170dc2cf3ce0d4f437c7ea5ba957e99a581cce 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentTableQueryPlanner.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/DocumentTableQueryPlanner.java
@@ -300,8 +300,20 @@ public class DocumentTableQueryPlanner {
}
DocumentTableQuery.Operation operation = DocumentTableQuery.Operation.COUNT;
Optional metricColumn = resolveMetricColumn(question, table);
- Optional groupColumn = Optional.empty();
- List filters = List.of();
+ Optional groupColumn = resolveGroupColumn(question, table, metricColumn.orElse(null));
+ List filters = resolveFilters(question, table);
+
+ String normalizedQuestion = normalize(question);
+ boolean aggregateCue = containsAny(normalizedQuestion, List.of("合计", "总计", "求和", "总和", "sum"));
+ if (aggregateCue && metricColumn.isPresent()) {
+ operation = DocumentTableQuery.Operation.SUM;
+ }
+ if (groupColumn.isPresent() && aggregateCue && metricColumn.isPresent()) {
+ operation = DocumentTableQuery.Operation.GROUP_SUM;
+ }
+ else if (groupColumn.isPresent() && containsAny(normalizedQuestion, List.of("数量", "个数", "多少", "统计"))) {
+ operation = DocumentTableQuery.Operation.GROUP_COUNT;
+ }
if (operation == DocumentTableQuery.Operation.COUNT && groupColumn.isPresent()) {
operation = DocumentTableQuery.Operation.GROUP_COUNT;
@@ -335,7 +347,9 @@ public class DocumentTableQueryPlanner {
}
private boolean mentionsKnownTableSignal(String question, List tables) {
- return tables.stream()
+ String normalizedQuestion = normalize(question);
+ boolean operationCue = containsAny(normalizedQuestion, List.of("合计", "总计", "求和", "总和", "统计", "数量", "个数", "多少", "最大", "最小", "sum", "count"));
+ return operationCue && tables.stream()
.filter(table -> table != null)
.anyMatch(table -> textMentionScore(question, table.getTitle()) > 0
|| textMentionScore(question, table.getSectionPath()) > 0
@@ -355,6 +369,77 @@ public class DocumentTableQueryPlanner {
.findFirst());
}
+ private Optional resolveGroupColumn(String question,
+ DocumentTableDescriptor table,
+ DocumentTableDescriptor.Column metricColumn) {
+ String normalizedQuestion = normalize(question);
+ if (!containsAny(normalizedQuestion, List.of("按", "分别", "分组", "各", "每"))) {
+ return Optional.empty();
+ }
+ return table.getColumns().stream()
+ .filter(column -> metricColumn == null || !StrUtil.equals(column.getColumnName(), metricColumn.getColumnName()))
+ .filter(column -> !"NUMBER".equalsIgnoreCase(StrUtil.blankToDefault(column.getValueType(), "")))
+ .map(column -> new ColumnScore(column, columnMentionScore(question, column)))
+ .filter(item -> item.score() > 0)
+ .max(Comparator.comparingInt(ColumnScore::score))
+ .map(ColumnScore::column);
+ }
+
+ private List resolveFilters(String question, DocumentTableDescriptor table) {
+ String normalizedQuestion = normalize(question);
+ List filters = new ArrayList<>();
+ for (DocumentTableDescriptor.Column column : table.getColumns()) {
+ if (column == null || StrUtil.isBlank(column.getColumnName())) {
+ continue;
+ }
+ String normalizedColumn = normalize(column.getColumnName());
+ if (normalizedColumn.isBlank()) {
+ continue;
+ }
+ for (String marker : List.of("为", "是", "等于")) {
+ int start = normalizedQuestion.indexOf(normalizedColumn + marker);
+ if (start < 0) {
+ continue;
+ }
+ int valueStart = start + normalizedColumn.length() + normalize(marker).length();
+ String value = extractFilterValue(normalizedQuestion.substring(valueStart), table);
+ if (StrUtil.isNotBlank(value)) {
+ filters.add(DocumentTableQuery.Filter.builder()
+ .column(column.getColumnName())
+ .operator(DocumentTableQuery.Operator.EQ)
+ .value(value)
+ .build());
+ break;
+ }
+ }
+ }
+ return filters;
+ }
+
+ private String extractFilterValue(String normalizedRemainder, DocumentTableDescriptor table) {
+ if (StrUtil.isBlank(normalizedRemainder)) {
+ return "";
+ }
+ String value = normalizedRemainder;
+ for (DocumentTableDescriptor.Column column : table.getColumns()) {
+ String normalizedColumn = normalize(column.getColumnName());
+ if (StrUtil.isBlank(normalizedColumn)) {
+ continue;
+ }
+ int index = value.indexOf(normalizedColumn);
+ if (index > 0) {
+ value = value.substring(0, index);
+ }
+ }
+ for (String cue : List.of("的", "数量", "个数", "多少", "合计", "总计", "求和", "总和", "统计", "sum", "count")) {
+ int index = value.indexOf(normalize(cue));
+ if (index > 0) {
+ value = value.substring(0, index);
+ }
+ }
+ return value.trim();
+ }
+
private int scorePlan(String question, DocumentTableDescriptor table, DocumentTableQuery query) {
int score = 0;
score += textMentionScore(question, table.getTitle()) * 3;
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceApplicabilityService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceApplicabilityService.java
new file mode 100644
index 0000000000000000000000000000000000000000..c2c45bedc2eec72d466688a800cadf2d3716791d
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceApplicabilityService.java
@@ -0,0 +1,158 @@
+package org.javaup.ai.chatagent.rag.service;
+
+import cn.hutool.core.util.StrUtil;
+import org.javaup.ai.chatagent.rag.model.EvidenceRole;
+import org.javaup.ai.chatagent.rag.model.EvidenceApplicabilityResult;
+import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult;
+import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys;
+import org.springframework.ai.document.Document;
+import org.springframework.stereotype.Service;
+
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * 判断候选证据是否适用于当前 query understanding 的目标实体。
+ */
+@Service
+public class EvidenceApplicabilityService {
+
+ private final EvidenceRoleClassifier evidenceRoleClassifier;
+
+ public EvidenceApplicabilityService() {
+ this(new EvidenceRoleClassifier());
+ }
+
+ public EvidenceApplicabilityService(EvidenceRoleClassifier evidenceRoleClassifier) {
+ this.evidenceRoleClassifier = evidenceRoleClassifier == null ? new EvidenceRoleClassifier() : evidenceRoleClassifier;
+ }
+
+ public EvidenceApplicabilityResult evaluate(QueryUnderstandingResult understanding, Document document) {
+ if (understanding == null || document == null) {
+ return EvidenceApplicabilityResult.unknown("missing understanding or evidence");
+ }
+ EvidenceApplicabilityResult roleResult = evaluateExpectedRole(understanding, document);
+ if (roleResult != null && !roleResult.isApplicable()) {
+ return roleResult;
+ }
+ List targets = normalizedTerms(understanding.getTargetEntities());
+ if (targets.isEmpty()) {
+ return roleResult == null ? EvidenceApplicabilityResult.unknown("target entity is empty") : roleResult;
+ }
+ String evidenceText = normalizedEvidenceText(document);
+ boolean targetSupported = targets.stream().anyMatch(evidenceText::contains);
+ if (targetSupported) {
+ return EvidenceApplicabilityResult.applicable("target entity supported by evidence");
+ }
+
+ List excluded = normalizedTerms(understanding.getExcludedEntities());
+ boolean excludedOnly = !excluded.isEmpty() && excluded.stream().anyMatch(evidenceText::contains);
+ if (excludedOnly) {
+ return EvidenceApplicabilityResult.notApplicable("evidence only supports excluded entity");
+ }
+
+ if (understanding.isNegativeBoundary() || explicitEvidenceRequired(understanding)) {
+ return EvidenceApplicabilityResult.notApplicable("target entity is not explicitly supported by evidence");
+ }
+ return EvidenceApplicabilityResult.unknown("target entity not found in evidence");
+ }
+
+ private EvidenceApplicabilityResult evaluateExpectedRole(QueryUnderstandingResult understanding, Document document) {
+ List expectedRoles = normalizeExpectedRoles(understanding.getExpectedEvidenceRoles());
+ EvidenceRole actualRole = evidenceRoleClassifier.classify(document);
+ if (document.getMetadata() != null) {
+ document.getMetadata().put(DocumentKnowledgeMetadataKeys.EVIDENCE_ROLE, actualRole.name());
+ if (!expectedRoles.isEmpty()) {
+ document.getMetadata().put(
+ DocumentKnowledgeMetadataKeys.EXPECTED_EVIDENCE_ROLES,
+ expectedRoles.stream().map(Enum::name).toList()
+ );
+ }
+ }
+ if (expectedRoles.isEmpty()) {
+ return null;
+ }
+ if (actualRole == EvidenceRole.GENERAL) {
+ return EvidenceApplicabilityResult.unknown("expected evidence role is " + roleText(expectedRoles) + " but evidence role is GENERAL");
+ }
+ if (expectedRoles.contains(actualRole)) {
+ return EvidenceApplicabilityResult.applicable("evidence role matched: " + actualRole.name());
+ }
+ return EvidenceApplicabilityResult.notApplicable(
+ "evidence role mismatch: expected " + roleText(expectedRoles) + ", actual " + actualRole.name()
+ );
+ }
+
+ private List normalizeExpectedRoles(List roles) {
+ if (roles == null || roles.isEmpty()) {
+ return List.of();
+ }
+ return roles.stream()
+ .filter(role -> role != null && role != EvidenceRole.GENERAL)
+ .distinct()
+ .limit(4)
+ .toList();
+ }
+
+ private String roleText(List roles) {
+ return roles == null || roles.isEmpty()
+ ? "GENERAL"
+ : String.join("/", roles.stream().map(Enum::name).toList());
+ }
+
+ private boolean explicitEvidenceRequired(QueryUnderstandingResult understanding) {
+ return "EXPLICIT_EVIDENCE_REQUIRED".equalsIgnoreCase(StrUtil.blankToDefault(understanding.getAnswerExpectation(), ""));
+ }
+
+ private List normalizedTerms(List terms) {
+ if (terms == null || terms.isEmpty()) {
+ return List.of();
+ }
+ return terms.stream()
+ .map(this::normalize)
+ .filter(term -> term.length() >= 2)
+ .distinct()
+ .limit(8)
+ .toList();
+ }
+
+ private String normalizedEvidenceText(Document document) {
+ List values = new ArrayList<>();
+ if (document.getMetadata() != null) {
+ Map metadata = document.getMetadata();
+ add(values, metadata.get(DocumentKnowledgeMetadataKeys.KG_ENTITY_NAME));
+ add(values, metadata.get(DocumentKnowledgeMetadataKeys.KG_CANONICAL_ENTITY_NAME));
+ add(values, metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATED_ENTITY_NAME));
+ add(values, metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ENTITIES));
+ add(values, metadata.get(DocumentKnowledgeMetadataKeys.TITLE));
+ add(values, metadata.get(DocumentKnowledgeMetadataKeys.SECTION_PATH));
+ add(values, metadata.get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH));
+ add(values, metadata.get(DocumentKnowledgeMetadataKeys.DOCUMENT_NAME));
+ }
+ add(values, document.getText());
+ return normalize(String.join(" ", values));
+ }
+
+ private void add(List values, Object value) {
+ if (value == null) {
+ return;
+ }
+ String text = String.valueOf(value);
+ if (StrUtil.isNotBlank(text)) {
+ values.add(text);
+ }
+ }
+
+ private String normalize(String value) {
+ if (value == null) {
+ return "";
+ }
+ return Normalizer.normalize(value, Normalizer.Form.NFKC)
+ .replaceAll("[\\s>`*#_\\-,,。;;::()()“”\"'\\[\\]{}]+", "")
+ .toLowerCase(Locale.ROOT)
+ .trim();
+ }
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceRoleClassifier.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceRoleClassifier.java
new file mode 100644
index 0000000000000000000000000000000000000000..65150219b29ba7b819b16decaeea8d7238a6b669
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/EvidenceRoleClassifier.java
@@ -0,0 +1,36 @@
+package org.javaup.ai.chatagent.rag.service;
+
+import org.javaup.ai.chatagent.rag.model.EvidenceRole;
+import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys;
+import org.springframework.ai.document.Document;
+import org.springframework.stereotype.Service;
+
+import java.util.Map;
+
+@Service
+public class EvidenceRoleClassifier {
+
+ /**
+ * Evidence role must come from controlled structured metadata. Do not infer it
+ * from section titles, question text, or document content with contains rules.
+ */
+ public EvidenceRole classify(Document evidence) {
+ if (evidence == null || evidence.getMetadata() == null) {
+ return EvidenceRole.GENERAL;
+ }
+ return readStructuredRole(evidence.getMetadata());
+ }
+
+ private EvidenceRole readStructuredRole(Map metadata) {
+ Object raw = metadata.get(DocumentKnowledgeMetadataKeys.EVIDENCE_ROLE);
+ if (raw == null) {
+ return EvidenceRole.GENERAL;
+ }
+ try {
+ return EvidenceRole.valueOf(String.valueOf(raw).trim().toUpperCase());
+ }
+ catch (IllegalArgumentException exception) {
+ return EvidenceRole.GENERAL;
+ }
+ }
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/FinalEvidenceSelectionPolicy.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/FinalEvidenceSelectionPolicy.java
new file mode 100644
index 0000000000000000000000000000000000000000..d99acebcc743be03a28ca8a3f5df8bc47057ffe1
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/FinalEvidenceSelectionPolicy.java
@@ -0,0 +1,1019 @@
+package org.javaup.ai.chatagent.rag.service;
+
+import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
+import org.javaup.ai.chatagent.rag.model.ConversationExecutionPlan;
+import org.javaup.ai.chatagent.rag.model.QueryType;
+import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult;
+import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions;
+import org.javaup.ai.chatagent.rag.model.RetrievalIntent;
+import org.javaup.ai.chatagent.rag.support.EvidenceIdentityResolver;
+import org.javaup.ai.manage.support.DocumentKnowledgeMetadataKeys;
+import org.javaup.enums.RetrievalChannelEnum;
+import org.springframework.ai.document.Document;
+
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 最终证据预算策略。只使用结构化 metadata 和受控查询理解结果,不读取业务词。
+ */
+public class FinalEvidenceSelectionPolicy {
+
+ public static final String RESERVE_TOP_RANK = "TOP_RANK";
+ public static final String RESERVE_SAME_SECTION_BODY = "SAME_SECTION_BODY";
+ public static final String RESERVE_STRUCTURE_ANCHOR = "STRUCTURE_ANCHOR";
+ public static final String RESERVE_STRUCTURE_ANCHOR_BODY = "STRUCTURE_ANCHOR_BODY";
+ public static final String RESERVE_STRUCTURE_DESCENDANT_BODY = "STRUCTURE_DESCENDANT_BODY";
+ public static final String RESERVE_STRUCTURE_NAVIGATION_CURRENT = "STRUCTURE_NAVIGATION_CURRENT";
+ public static final String RESERVE_STRUCTURE_NAVIGATION_PARENT = "STRUCTURE_NAVIGATION_PARENT";
+ public static final String RESERVE_STRUCTURE_NAVIGATION_SIBLING = "STRUCTURE_NAVIGATION_SIBLING";
+ public static final String RESERVE_STRUCTURE_NAVIGATION_CHILD = "STRUCTURE_NAVIGATION_CHILD";
+ public static final String RESERVE_ROUTE_CANDIDATE_SOURCE = "ROUTE_CANDIDATE_SOURCE";
+ public static final String RESERVE_MULTI_DOC_DIVERSITY = "MULTI_DOC_DIVERSITY_RESERVE";
+ public static final String RESERVE_GRAPH_RAG_QUOTE = "GRAPH_RAG_QUOTE";
+ public static final String RESERVE_RAPTOR_SOURCE_CHUNK = "RAPTOR_SOURCE_CHUNK";
+ public static final String SELECTED_TOP_RANK = "SELECTED_TOP_RANK";
+ public static final String SELECTED_SAME_SECTION_BODY = "SELECTED_SAME_SECTION_BODY";
+ public static final String SELECTED_STRUCTURE_ANCHOR = "SELECTED_STRUCTURE_ANCHOR";
+ public static final String SELECTED_STRUCTURE_ANCHOR_BODY = "SELECTED_STRUCTURE_ANCHOR_BODY";
+ public static final String SELECTED_STRUCTURE_DESCENDANT_BODY = "SELECTED_STRUCTURE_DESCENDANT_BODY";
+ public static final String SELECTED_STRUCTURE_NAVIGATION_CURRENT = "SELECTED_STRUCTURE_NAVIGATION_CURRENT";
+ public static final String SELECTED_STRUCTURE_NAVIGATION_PARENT = "SELECTED_STRUCTURE_NAVIGATION_PARENT";
+ public static final String SELECTED_STRUCTURE_NAVIGATION_SIBLING = "SELECTED_STRUCTURE_NAVIGATION_SIBLING";
+ public static final String SELECTED_STRUCTURE_NAVIGATION_CHILD = "SELECTED_STRUCTURE_NAVIGATION_CHILD";
+ public static final String SELECTED_ROUTE_CANDIDATE_RESERVE = "SELECTED_ROUTE_CANDIDATE_RESERVE";
+ public static final String SELECTED_MULTI_DOC_DIVERSITY_RESERVE = "SELECTED_MULTI_DOC_DIVERSITY_RESERVE";
+ public static final String REPLACED_TITLE_ONLY_WITH_BODY = "REPLACED_TITLE_ONLY_WITH_BODY";
+ public static final String SELECTED_GRAPH_RAG_QUOTE = "SELECTED_GRAPH_RAG_QUOTE";
+ public static final String SELECTED_RAPTOR_SOURCE_CHUNK = "SELECTED_RAPTOR_SOURCE_CHUNK";
+
+ private final ChatRagProperties properties;
+
+ public FinalEvidenceSelectionPolicy(ChatRagProperties properties) {
+ this.properties = properties;
+ }
+
+ public List select(List rerankedCandidates, ConversationExecutionPlan plan) {
+ if (rerankedCandidates == null || rerankedCandidates.isEmpty()) {
+ return List.of();
+ }
+ int finalTopK = Math.max(runtimeOptions(plan).getFinalTopK(), 0);
+ if (finalTopK <= 0) {
+ return List.of();
+ }
+
+ List selected = new ArrayList<>(rerankedCandidates.stream()
+ .limit(finalTopK)
+ .toList());
+ selected.forEach(this::markInitialSelection);
+ reserveStructureAnchorBodyCandidates(rerankedCandidates, selected);
+ if (rerankedCandidates.size() <= finalTopK) {
+ return selected;
+ }
+ reserveSameSectionBody(rerankedCandidates, selected);
+ reserveStructureAnchor(rerankedCandidates, selected, plan);
+ reserveRouteCandidateSourceEvidence(rerankedCandidates, selected, plan);
+ reserveRaptorSourceEvidence(rerankedCandidates, finalTopK, selected, plan);
+ reserveGraphRagEvidence(rerankedCandidates, finalTopK, selected, plan);
+ return selected;
+ }
+
+ private void reserveStructureAnchorBodyCandidates(List candidates, List selected) {
+ List bodyCandidates = candidates.stream()
+ .filter(this::isStructureAnchorBodyCandidate)
+ .filter(this::isBodyEvidence)
+ .sorted(Comparator.comparingDouble(this::structureAnchorBodyPriority).reversed())
+ .toList();
+ if (bodyCandidates.isEmpty()) {
+ return;
+ }
+ for (Document bodyCandidate : bodyCandidates) {
+ Document alreadySelected = findSameCitationEvidence(selected, bodyCandidate);
+ String reserveType = structureAnchorReserveType(bodyCandidate);
+ if (alreadySelected != null) {
+ markReserve(alreadySelected, reserveType);
+ continue;
+ }
+ int replaceIndex = firstReplaceableTitleOrSummaryOnlyIndex(selected);
+ boolean replacesTitleOrSummary = replaceIndex >= 0;
+ if (replaceIndex < 0) {
+ replaceIndex = weakestReplaceableEvidenceIndex(selected);
+ }
+ if (replaceIndex < 0) {
+ return;
+ }
+ String reason = replacesTitleOrSummary ? REPLACED_TITLE_ONLY_WITH_BODY : selectedReasonCode(reserveType);
+ markReserve(bodyCandidate, reserveType, reason);
+ selected.set(replaceIndex, bodyCandidate);
+ }
+ }
+
+ private void reserveSameSectionBody(List candidates, List selected) {
+ List selectedTitles = selected.stream()
+ .filter(this::isTitleEvidence)
+ .toList();
+ if (selectedTitles.isEmpty()) {
+ return;
+ }
+ for (Document title : selectedTitles) {
+ Document body = candidates.stream()
+ .filter(candidate -> !containsSameCitationEvidence(selected, candidate))
+ .filter(this::isBodyEvidence)
+ .filter(candidate -> sameStructureAnchor(title, candidate))
+ .max(Comparator.comparingDouble(this::finalDocumentScore))
+ .orElse(null);
+ if (body == null) {
+ continue;
+ }
+ replaceWeakestEvidence(selected, body, RESERVE_SAME_SECTION_BODY);
+ }
+ }
+
+ private void reserveStructureAnchor(List candidates,
+ List selected,
+ ConversationExecutionPlan plan) {
+ List anchors = sectionAnchors(plan);
+ if (anchors.isEmpty()) {
+ return;
+ }
+ Document anchored = candidates.stream()
+ .filter(candidate -> !containsSameCitationEvidence(selected, candidate))
+ .filter(this::isBodyEvidence)
+ .filter(candidate -> matchesAnySectionAnchor(candidate, anchors))
+ .max(Comparator.comparingDouble(this::finalDocumentScore))
+ .orElse(null);
+ if (anchored != null) {
+ replaceWeakestEvidence(selected, anchored, RESERVE_STRUCTURE_ANCHOR);
+ }
+ }
+
+ private void reserveGraphRagEvidence(List rerankedCandidates,
+ int finalTopK,
+ List selected,
+ ConversationExecutionPlan plan) {
+ boolean preferCrossDocumentCommunity = shouldReserveCrossDocumentCommunityEvidence(plan);
+ if (selected.stream().anyMatch(document -> isRequiredGraphRagReserveCandidate(document, plan, preferCrossDocumentCommunity))) {
+ return;
+ }
+ Document graphRagReserve = selectGraphRagReserveCandidate(
+ rerankedCandidates,
+ finalTopK,
+ selected,
+ plan,
+ preferCrossDocumentCommunity
+ );
+ if (graphRagReserve == null && preferCrossDocumentCommunity) {
+ graphRagReserve = selectGraphRagReserveCandidate(rerankedCandidates, finalTopK, selected, plan, false);
+ }
+ if (graphRagReserve != null) {
+ replaceGraphRagSummaryOnlyOrWeakestEvidence(selected, graphRagReserve, RESERVE_GRAPH_RAG_QUOTE);
+ }
+ }
+
+ private void reserveRouteCandidateSourceEvidence(List candidates,
+ List selected,
+ ConversationExecutionPlan plan) {
+ if (!isMultiDocumentAutoRetrieval(plan) || candidates == null || candidates.isEmpty()) {
+ return;
+ }
+ List reserves = candidates.stream()
+ .filter(this::isRouteCandidateSourceReserve)
+ .filter(EvidenceIdentityResolver::isCitationCapable)
+ .filter(candidate -> !containsSameCitationEvidence(selected, candidate))
+ .sorted(Comparator.comparingDouble(this::finalDocumentScore).reversed())
+ .toList();
+ if (reserves.isEmpty()) {
+ return;
+ }
+ for (Document reserve : reserves) {
+ if (containsSameCitationEvidence(selected, reserve)) {
+ continue;
+ }
+ int replaceIndex = firstReplaceableContextOnlyOrSummaryIndex(selected);
+ if (replaceIndex < 0) {
+ replaceIndex = weakestReplaceableEvidenceIndex(selected);
+ }
+ if (replaceIndex < 0) {
+ return;
+ }
+ markReserve(reserve, RESERVE_ROUTE_CANDIDATE_SOURCE);
+ selected.set(replaceIndex, reserve);
+ }
+ }
+
+ private void reserveRaptorSourceEvidence(List rerankedCandidates,
+ int finalTopK,
+ List selected,
+ ConversationExecutionPlan plan) {
+ if (selected.stream().anyMatch(this::isRaptorSourceCandidate)) {
+ return;
+ }
+ Document raptorReserve = rerankedCandidates.stream()
+ .skip(finalTopK)
+ .filter(this::isRaptorSourceCandidate)
+ .filter(candidate -> !containsSameCitationEvidence(selected, candidate))
+ .max(Comparator.comparingDouble(document -> raptorEvidenceBudgetPriority(document, plan)))
+ .orElse(null);
+ if (raptorReserve != null) {
+ replaceSummaryOnlyOrWeakestEvidence(selected, raptorReserve, RESERVE_RAPTOR_SOURCE_CHUNK);
+ }
+ }
+
+ private Document selectGraphRagReserveCandidate(List rerankedCandidates,
+ int finalTopK,
+ List selected,
+ ConversationExecutionPlan plan,
+ boolean crossDocumentCommunityOnly) {
+ return rerankedCandidates.stream()
+ .skip(finalTopK)
+ .filter(document -> isGraphRagReserveCandidate(document, plan))
+ .filter(document -> !crossDocumentCommunityOnly || isGraphRagCrossDocumentCommunityReserveCandidate(document, plan))
+ .filter(candidate -> !containsSameCitationEvidence(selected, candidate))
+ .max(Comparator.comparingDouble(document -> graphRagEvidenceBudgetPriority(document, plan)))
+ .orElse(null);
+ }
+
+ private void replaceWeakestEvidence(List selected,
+ Document reserve,
+ String reserveType) {
+ if (selected == null || selected.isEmpty() || reserve == null) {
+ return;
+ }
+ if (containsSameCitationEvidence(selected, reserve)) {
+ return;
+ }
+ int replaceIndex = weakestReplaceableEvidenceIndex(selected);
+ if (replaceIndex < 0) {
+ return;
+ }
+ markReserve(reserve, reserveType);
+ selected.set(replaceIndex, reserve);
+ }
+
+ private void replaceSummaryOnlyOrWeakestEvidence(List selected,
+ Document reserve,
+ String reserveType) {
+ if (selected == null || selected.isEmpty() || reserve == null || containsSameCitationEvidence(selected, reserve)) {
+ return;
+ }
+ int replaceIndex = firstReplaceableRaptorSummaryOnlyIndex(selected);
+ if (replaceIndex < 0) {
+ replaceIndex = weakestReplaceableEvidenceIndex(selected);
+ }
+ if (replaceIndex < 0) {
+ return;
+ }
+ markReserve(reserve, reserveType);
+ selected.set(replaceIndex, reserve);
+ }
+
+ private void replaceGraphRagSummaryOnlyOrWeakestEvidence(List selected,
+ Document reserve,
+ String reserveType) {
+ if (selected == null || selected.isEmpty() || reserve == null || containsSameCitationEvidence(selected, reserve)) {
+ return;
+ }
+ int replaceIndex = firstReplaceableGraphRagCommunitySummaryOnlyIndex(selected);
+ if (replaceIndex < 0) {
+ replaceIndex = weakestReplaceableEvidenceIndex(selected);
+ }
+ if (replaceIndex < 0) {
+ return;
+ }
+ markReserve(reserve, reserveType);
+ selected.set(replaceIndex, reserve);
+ }
+
+ private int firstReplaceableRaptorSummaryOnlyIndex(List selected) {
+ for (int index = 0; index < selected.size(); index++) {
+ Document document = selected.get(index);
+ if (!isProtectedReserve(document) && isRaptorSummaryOnly(document)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ private int firstReplaceableGraphRagCommunitySummaryOnlyIndex(List selected) {
+ for (int index = 0; index < selected.size(); index++) {
+ Document document = selected.get(index);
+ if (!isProtectedReserve(document) && isGraphRagCommunitySummaryOnly(document)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ private int firstReplaceableTitleOrSummaryOnlyIndex(List selected) {
+ for (int index = 0; index < selected.size(); index++) {
+ Document document = selected.get(index);
+ if (isProtectedReserve(document)) {
+ continue;
+ }
+ if (isTitleEvidence(document) || isRaptorSummaryOnly(document) || isGraphRagCommunitySummaryOnly(document)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ private int firstReplaceableContextOnlyOrSummaryIndex(List selected) {
+ for (int index = 0; index < selected.size(); index++) {
+ Document document = selected.get(index);
+ if (isProtectedReserve(document)) {
+ continue;
+ }
+ if (EvidenceIdentityResolver.isContextOnly(document)
+ || isTitleEvidence(document)
+ || isRaptorSummaryOnly(document)
+ || isGraphRagCommunitySummaryOnly(document)) {
+ return index;
+ }
+ }
+ return -1;
+ }
+
+ private int weakestReplaceableEvidenceIndex(List selected) {
+ int replaceIndex = -1;
+ double weakestScore = Double.MAX_VALUE;
+ for (int index = 0; index < selected.size(); index++) {
+ Document document = selected.get(index);
+ if (isProtectedReserve(document)) {
+ continue;
+ }
+ double score = finalDocumentScore(document);
+ if (score < weakestScore) {
+ weakestScore = score;
+ replaceIndex = index;
+ }
+ }
+ return replaceIndex >= 0 ? replaceIndex : selected.size() - 1;
+ }
+
+ private boolean isProtectedReserve(Document document) {
+ if (document == null || document.getMetadata() == null) {
+ return false;
+ }
+ String reserveType = safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE));
+ return RESERVE_SAME_SECTION_BODY.equals(reserveType)
+ || RESERVE_STRUCTURE_ANCHOR.equals(reserveType)
+ || RESERVE_STRUCTURE_ANCHOR_BODY.equals(reserveType)
+ || RESERVE_STRUCTURE_DESCENDANT_BODY.equals(reserveType)
+ || isStructureNavigationReserveType(reserveType)
+ || RESERVE_ROUTE_CANDIDATE_SOURCE.equals(reserveType)
+ || RESERVE_MULTI_DOC_DIVERSITY.equals(reserveType)
+ || RESERVE_GRAPH_RAG_QUOTE.equals(reserveType)
+ || RESERVE_RAPTOR_SOURCE_CHUNK.equals(reserveType);
+ }
+
+ private void markReserve(Document document, String reserveType) {
+ if (document == null || document.getMetadata() == null) {
+ return;
+ }
+ document.getMetadata().put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, selectedReasonCode(reserveType));
+ document.getMetadata().putIfAbsent(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, reserveType);
+ }
+
+ private void markInitialSelection(Document document) {
+ String reserveType = safeText(document == null || document.getMetadata() == null
+ ? null
+ : document.getMetadata().get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE));
+ if (isStructureNavigationReserveType(reserveType)) {
+ markReserve(document, reserveType, selectedReasonCode(reserveType));
+ return;
+ }
+ if (RESERVE_ROUTE_CANDIDATE_SOURCE.equals(reserveType) || RESERVE_MULTI_DOC_DIVERSITY.equals(reserveType)) {
+ markReserve(document, reserveType, selectedReasonCode(reserveType));
+ return;
+ }
+ markReserve(document, RESERVE_TOP_RANK);
+ }
+
+ private void markReserve(Document document, String reserveType, String reasonCode) {
+ if (document == null || document.getMetadata() == null) {
+ return;
+ }
+ document.getMetadata().put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_REASON, reasonCode);
+ document.getMetadata().put(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE, reserveType);
+ }
+
+ private String selectedReasonCode(String reserveType) {
+ return switch (reserveType) {
+ case RESERVE_SAME_SECTION_BODY -> SELECTED_SAME_SECTION_BODY;
+ case RESERVE_STRUCTURE_ANCHOR -> SELECTED_STRUCTURE_ANCHOR;
+ case RESERVE_STRUCTURE_ANCHOR_BODY -> SELECTED_STRUCTURE_ANCHOR_BODY;
+ case RESERVE_STRUCTURE_DESCENDANT_BODY -> SELECTED_STRUCTURE_DESCENDANT_BODY;
+ case RESERVE_STRUCTURE_NAVIGATION_CURRENT -> SELECTED_STRUCTURE_NAVIGATION_CURRENT;
+ case RESERVE_STRUCTURE_NAVIGATION_PARENT -> SELECTED_STRUCTURE_NAVIGATION_PARENT;
+ case RESERVE_STRUCTURE_NAVIGATION_SIBLING -> SELECTED_STRUCTURE_NAVIGATION_SIBLING;
+ case RESERVE_STRUCTURE_NAVIGATION_CHILD -> SELECTED_STRUCTURE_NAVIGATION_CHILD;
+ case RESERVE_ROUTE_CANDIDATE_SOURCE -> SELECTED_ROUTE_CANDIDATE_RESERVE;
+ case RESERVE_MULTI_DOC_DIVERSITY -> SELECTED_MULTI_DOC_DIVERSITY_RESERVE;
+ case RESERVE_GRAPH_RAG_QUOTE -> SELECTED_GRAPH_RAG_QUOTE;
+ case RESERVE_RAPTOR_SOURCE_CHUNK -> SELECTED_RAPTOR_SOURCE_CHUNK;
+ default -> SELECTED_TOP_RANK;
+ };
+ }
+
+ private boolean isStructureNavigationReserveType(String reserveType) {
+ return RESERVE_STRUCTURE_NAVIGATION_CURRENT.equals(reserveType)
+ || RESERVE_STRUCTURE_NAVIGATION_PARENT.equals(reserveType)
+ || RESERVE_STRUCTURE_NAVIGATION_SIBLING.equals(reserveType)
+ || RESERVE_STRUCTURE_NAVIGATION_CHILD.equals(reserveType);
+ }
+
+ private boolean isTitleEvidence(Document document) {
+ if (document == null || document.getMetadata() == null) {
+ return false;
+ }
+ String chunkType = normalizeSimple(safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE)));
+ if ("title".equals(chunkType) || "heading".equals(chunkType)) {
+ return true;
+ }
+ String nodeType = normalizeSimple(safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_TYPE)));
+ if ("title".equals(nodeType) || "heading".equals(nodeType)) {
+ return true;
+ }
+ String text = safeText(document.getText());
+ return text.length() <= 120 && (text.startsWith("#") || text.matches("^\\d+(\\.\\d+){1,5}\\s+\\S.*$"));
+ }
+
+ private boolean isBodyEvidence(Document document) {
+ if (document == null) {
+ return false;
+ }
+ if (isTechnicalWrapperEvidence(document)) {
+ return false;
+ }
+ if (isTitleEvidence(document)) {
+ return false;
+ }
+ return safeText(document.getText()).length() >= 12;
+ }
+
+ private boolean isStructureAnchorBodyCandidate(Document document) {
+ if (document == null || document.getMetadata() == null) {
+ return false;
+ }
+ Map metadata = document.getMetadata();
+ if (!booleanMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_RAW_BODY))) {
+ return false;
+ }
+ if (!EvidenceIdentityResolver.isCitationCapable(document)) {
+ return false;
+ }
+ String reserveType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE));
+ String channel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHANNEL));
+ Object bypass = metadata.get(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_BYPASS_RESERVE_WINDOW);
+ return "STRUCTURE_ANCHOR_BODY_CANDIDATE".equalsIgnoreCase(reserveType)
+ || "structure-anchor".equalsIgnoreCase(channel)
+ || Boolean.TRUE.equals(bypass)
+ || Boolean.parseBoolean(String.valueOf(bypass));
+ }
+
+ private boolean isRouteCandidateSourceReserve(Document document) {
+ if (document == null || document.getMetadata() == null) {
+ return false;
+ }
+ String reserveType = safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.FINAL_SELECTION_RESERVE_TYPE));
+ return RESERVE_ROUTE_CANDIDATE_SOURCE.equals(reserveType);
+ }
+
+ private boolean isMultiDocumentAutoRetrieval(ConversationExecutionPlan plan) {
+ return plan != null
+ && plan.getRetrievalDocumentIds() != null
+ && plan.getRetrievalDocumentIds().stream().filter(Objects::nonNull).distinct().limit(2).count() > 1;
+ }
+
+ private boolean isTechnicalWrapperEvidence(Document document) {
+ if (document == null || document.getMetadata() == null) {
+ return false;
+ }
+ Map metadata = document.getMetadata();
+ String chunkType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE));
+ if ("RAPTOR_SUMMARY".equalsIgnoreCase(chunkType)) {
+ return true;
+ }
+ if (isRaptorSummaryOnly(document) || isGraphRagCommunitySummaryOnly(document)) {
+ return true;
+ }
+ String channel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHANNEL));
+ String sourceType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.SOURCE_TYPE));
+ if ("graph-rag".equalsIgnoreCase(channel) || "GRAPH_RAG".equalsIgnoreCase(sourceType)) {
+ return !hasGraphRagSourceQuote(document);
+ }
+ String text = safeText(document.getText()).trim();
+ return text.startsWith("[GraphRAG") || text.startsWith("[RAPTOR");
+ }
+
+ private String structureAnchorReserveType(Document document) {
+ if (document == null || document.getMetadata() == null) {
+ return RESERVE_STRUCTURE_ANCHOR_BODY;
+ }
+ String matchType = safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_MATCH_TYPE));
+ return "CANONICAL_DESCENDANT".equalsIgnoreCase(matchType)
+ ? RESERVE_STRUCTURE_DESCENDANT_BODY
+ : RESERVE_STRUCTURE_ANCHOR_BODY;
+ }
+
+ private double structureAnchorBodyPriority(Document document) {
+ if (document == null || document.getMetadata() == null) {
+ return 0D;
+ }
+ double priority = finalDocumentScore(document) + 2D;
+ String matchType = safeText(document.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_ANCHOR_MATCH_TYPE));
+ if ("NODE_ID".equalsIgnoreCase(matchType)) {
+ priority += 0.6D;
+ }
+ else if ("CANONICAL_EXACT".equalsIgnoreCase(matchType)) {
+ priority += 0.45D;
+ }
+ else if ("CANONICAL_DESCENDANT".equalsIgnoreCase(matchType)) {
+ priority += 0.25D;
+ }
+ if (isMeaningfulMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID))) {
+ priority += 0.3D;
+ }
+ return priority;
+ }
+
+ private boolean sameStructureAnchor(Document left, Document right) {
+ if (left == null || right == null || left.getMetadata() == null || right.getMetadata() == null) {
+ return false;
+ }
+ Long leftDocumentId = longMetadataValue(left.getMetadata().get(DocumentKnowledgeMetadataKeys.DOCUMENT_ID));
+ Long rightDocumentId = longMetadataValue(right.getMetadata().get(DocumentKnowledgeMetadataKeys.DOCUMENT_ID));
+ if (leftDocumentId != null && rightDocumentId != null && !Objects.equals(leftDocumentId, rightDocumentId)) {
+ return false;
+ }
+ Long leftNodeId = longMetadataValue(left.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID));
+ Long rightNodeId = longMetadataValue(right.getMetadata().get(DocumentKnowledgeMetadataKeys.STRUCTURE_NODE_ID));
+ if (leftNodeId != null && rightNodeId != null && Objects.equals(leftNodeId, rightNodeId)) {
+ return true;
+ }
+ String leftCanonical = normalizedAnchor(left.getMetadata().get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH));
+ String rightCanonical = normalizedAnchor(right.getMetadata().get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH));
+ if (!leftCanonical.isBlank() && leftCanonical.equals(rightCanonical)) {
+ return true;
+ }
+ String leftSection = normalizedAnchor(left.getMetadata().get(DocumentKnowledgeMetadataKeys.SECTION_PATH));
+ String rightSection = normalizedAnchor(right.getMetadata().get(DocumentKnowledgeMetadataKeys.SECTION_PATH));
+ return !leftSection.isBlank() && leftSection.equals(rightSection);
+ }
+
+ private boolean matchesAnySectionAnchor(Document document, List anchors) {
+ if (document == null || document.getMetadata() == null || anchors == null || anchors.isEmpty()) {
+ return false;
+ }
+ String section = normalizedAnchor(document.getMetadata().get(DocumentKnowledgeMetadataKeys.SECTION_PATH));
+ String canonical = normalizedAnchor(document.getMetadata().get(DocumentKnowledgeMetadataKeys.CANONICAL_PATH));
+ String title = normalizedAnchor(document.getMetadata().get(DocumentKnowledgeMetadataKeys.TITLE));
+ return anchors.stream()
+ .map(this::normalizedAnchor)
+ .filter(anchor -> !anchor.isBlank())
+ .anyMatch(anchor -> equalsOrContains(section, anchor)
+ || equalsOrContains(canonical, anchor)
+ || equalsOrContains(title, anchor));
+ }
+
+ private boolean equalsOrContains(String value, String anchor) {
+ if (value == null || value.isBlank() || anchor == null || anchor.isBlank()) {
+ return false;
+ }
+ return value.equals(anchor) || value.contains(anchor) || anchor.contains(value);
+ }
+
+ private List sectionAnchors(ConversationExecutionPlan plan) {
+ QueryUnderstandingResult queryUnderstanding = plan == null ? null : plan.getQueryUnderstanding();
+ if (queryUnderstanding == null || queryUnderstanding.getSectionAnchors() == null) {
+ return List.of();
+ }
+ return queryUnderstanding.getSectionAnchors();
+ }
+
+ private boolean containsSameCitationEvidence(List documents, Document candidate) {
+ return documents != null
+ && documents.stream().anyMatch(document -> sameCitationEvidence(document, candidate));
+ }
+
+ private Document findSameCitationEvidence(List documents, Document candidate) {
+ if (documents == null || candidate == null) {
+ return null;
+ }
+ return documents.stream()
+ .filter(document -> sameCitationEvidence(document, candidate))
+ .findFirst()
+ .orElse(null);
+ }
+
+ private boolean sameCitationEvidence(Document left, Document right) {
+ if (left == null || right == null) {
+ return false;
+ }
+ if (Objects.equals(left.getId(), right.getId())) {
+ return true;
+ }
+ return EvidenceIdentityResolver.sameCitationEvidence(left, right);
+ }
+
+ private boolean isRequiredGraphRagReserveCandidate(Document document,
+ ConversationExecutionPlan plan,
+ boolean preferCrossDocumentCommunity) {
+ if (!isGraphRagReserveCandidate(document, plan)) {
+ return false;
+ }
+ return !preferCrossDocumentCommunity || isGraphRagCrossDocumentCommunityReserveCandidate(document, plan);
+ }
+
+ private boolean isGraphRagReserveCandidate(Document document, ConversationExecutionPlan plan) {
+ if (document == null || document.getMetadata() == null || !isGraphRagMetadata(document.getMetadata())) {
+ return false;
+ }
+ Map metadata = document.getMetadata();
+ if (isGraphRagCommunitySummaryOnly(document)) {
+ return false;
+ }
+ if (isGraphRagCommunityReportReserveCandidate(document, plan)) {
+ return true;
+ }
+ boolean hasRelationEvidence = isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID))
+ && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID))
+ && hasGraphRagSourceQuote(document);
+ if (!hasRelationEvidence) {
+ return false;
+ }
+ if (!hasGraphRagRelationGroundingContext(metadata)) {
+ return false;
+ }
+ Double qualityScore = numericMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUALITY_SCORE));
+ return qualityScore == null || qualityScore >= 0.55D;
+ }
+
+ private boolean isGraphRagCrossDocumentCommunityReserveCandidate(Document document, ConversationExecutionPlan plan) {
+ if (!isGraphRagCommunityReportReserveCandidate(document, plan)) {
+ return false;
+ }
+ Map metadata = document.getMetadata();
+ return isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_KEY));
+ }
+
+ private boolean isGraphRagCommunityReportReserveCandidate(Document document, ConversationExecutionPlan plan) {
+ if (document == null || document.getMetadata() == null || !isGraphRagMetadata(document.getMetadata())
+ || !shouldReserveCrossDocumentCommunityEvidence(plan)) {
+ return false;
+ }
+ Map metadata = document.getMetadata();
+ if (!isGraphRagCommunityReportCandidate(metadata)) {
+ return false;
+ }
+ if (isGraphRagCommunitySummaryOnly(document)) {
+ return false;
+ }
+ Integer communityDocumentCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_DOCUMENT_COUNT));
+ if (communityDocumentCount != null && communityDocumentCount < 2) {
+ return false;
+ }
+ boolean grounded = isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID))
+ && hasGraphRagSourceQuote(document)
+ && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY));
+ if (!grounded) {
+ return false;
+ }
+ Double qualityScore = numericMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUALITY_SCORE));
+ return qualityScore == null || qualityScore >= 0.55D;
+ }
+
+ private boolean isRaptorSourceCandidate(Document document) {
+ if (document == null || document.getMetadata() == null || !isRaptorMetadata(document.getMetadata())) {
+ return false;
+ }
+ if (isRaptorSummaryOnly(document)) {
+ return false;
+ }
+ Map metadata = document.getMetadata();
+ return isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_ID))
+ || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID));
+ }
+
+ private boolean isRaptorSummaryOnly(Document document) {
+ if (document == null || document.getMetadata() == null || !isRaptorMetadata(document.getMetadata())) {
+ return false;
+ }
+ Map metadata = document.getMetadata();
+ String sourceStatus = safeText(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_SOURCE_STATUS));
+ if ("SUMMARY_ONLY".equalsIgnoreCase(sourceStatus)) {
+ return true;
+ }
+ String chunkType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_TYPE));
+ if ("RAPTOR_SUMMARY".equalsIgnoreCase(chunkType)) {
+ return true;
+ }
+ return !isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_ID))
+ && !isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID));
+ }
+
+ private double raptorEvidenceBudgetPriority(Document document, ConversationExecutionPlan plan) {
+ if (document == null || document.getMetadata() == null) {
+ return 0D;
+ }
+ Map metadata = document.getMetadata();
+ double priority = finalDocumentScore(document);
+ if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.CHUNK_ID))) {
+ priority += 1.0D;
+ }
+ if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.PARENT_BLOCK_ID))) {
+ priority += 0.55D;
+ }
+ if (isSuggestedChannel(RetrievalChannelEnum.RAPTOR.getName(), plan)
+ || resolveRetrievalIntent(plan) == RetrievalIntent.RAPTOR) {
+ priority += 0.25D;
+ }
+ Integer nodeLevel = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_LEVEL));
+ if (nodeLevel != null && nodeLevel <= 1) {
+ priority += 0.08D;
+ }
+ return priority;
+ }
+
+ private boolean isGraphRagCommunityReportCandidate(Map metadata) {
+ if (metadata == null) {
+ return false;
+ }
+ boolean hasCommunityIdentity = isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_ID))
+ || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_KEY));
+ if (!hasCommunityIdentity) {
+ return false;
+ }
+ if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID))
+ || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_ENTITY_ID))) {
+ return false;
+ }
+ return isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_TITLE))
+ || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY));
+ }
+
+ private boolean shouldReserveCrossDocumentCommunityEvidence(ConversationExecutionPlan plan) {
+ if (plan == null) {
+ return false;
+ }
+ RetrievalIntent intent = resolveRetrievalIntent(plan);
+ if (intent == RetrievalIntent.GRAPH_RAG || intent == RetrievalIntent.RAPTOR) {
+ return true;
+ }
+ QueryUnderstandingResult queryUnderstanding = plan.getQueryUnderstanding();
+ if (queryUnderstanding == null) {
+ return false;
+ }
+ QueryType queryType = queryUnderstanding.getQueryType();
+ if (queryType == QueryType.GRAPH_RELATION || queryType == QueryType.GLOBAL_SUMMARY) {
+ return true;
+ }
+ List channels = queryUnderstanding.getChannels();
+ return channels != null
+ && (channels.contains(RetrievalIntent.GRAPH_RAG) || channels.contains(RetrievalIntent.RAPTOR));
+ }
+
+ private double graphRagEvidenceBudgetPriority(Document document, ConversationExecutionPlan plan) {
+ if (document == null || document.getMetadata() == null) {
+ return 0D;
+ }
+ Map metadata = document.getMetadata();
+ double priority = finalDocumentScore(document);
+ if (isGraphRagCommunitySummaryOnly(document)) {
+ priority -= 2.0D;
+ }
+ if (hasGraphRagSourceQuote(document)) {
+ priority += 0.75D;
+ }
+ if (isGraphRagCommunityReportReserveCandidate(document, plan)) {
+ priority += 1.35D;
+ Integer communityDocumentCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_DOCUMENT_COUNT));
+ Integer communityEvidenceCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_EVIDENCE_COUNT));
+ Integer communityRelationGroupCount = integerMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_RELATION_GROUP_COUNT));
+ if (communityDocumentCount != null && communityDocumentCount > 1) {
+ priority += Math.min(0.36D, communityDocumentCount * 0.08D);
+ }
+ if (communityEvidenceCount != null && communityEvidenceCount > 1) {
+ priority += Math.min(0.30D, communityEvidenceCount * 0.04D);
+ }
+ if (communityRelationGroupCount != null && communityRelationGroupCount > 1) {
+ priority += Math.min(0.24D, communityRelationGroupCount * 0.04D);
+ }
+ }
+ if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID))
+ && isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID))
+ && hasGraphRagSourceQuote(document)) {
+ priority += 1.10D;
+ }
+ String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL));
+ if ("RELATION_STRONG_QUOTE".equalsIgnoreCase(groundingLevel)) {
+ priority += 0.55D;
+ }
+ else if ("RELATION_WEAK_QUOTE".equalsIgnoreCase(groundingLevel)) {
+ priority += 0.25D;
+ }
+ else if (groundingLevel.toUpperCase(Locale.ROOT).startsWith("RELATION_")) {
+ priority += 0.15D;
+ }
+ else if ("COMMUNITY_SOURCE_QUOTE".equalsIgnoreCase(groundingLevel)) {
+ priority += 0.12D;
+ }
+ if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH))) {
+ priority += 0.55D;
+ }
+ if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE))) {
+ priority += 0.45D;
+ }
+ if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ANSWER_TYPES))) {
+ priority += 0.20D;
+ }
+ if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_ENTITIES))) {
+ priority += 0.16D;
+ }
+ if (isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY))) {
+ priority += 0.35D;
+ }
+ if (isSuggestedChannel(RetrievalChannelEnum.GRAPH_RAG.getName(), plan)) {
+ priority += 0.08D;
+ }
+ Double qualityScore = numericMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUALITY_SCORE));
+ if (qualityScore != null) {
+ priority += Math.min(0.45D, Math.max(0D, qualityScore) * 0.45D);
+ }
+ return priority;
+ }
+
+ private boolean hasGraphRagRelationGroundingContext(Map metadata) {
+ if (metadata == null) {
+ return false;
+ }
+ String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL));
+ return isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_QUERY_PLAN_SOURCE))
+ || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_NHOP_PATH))
+ || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY))
+ || groundingLevel.toUpperCase(Locale.ROOT).startsWith("RELATION_");
+ }
+
+ private boolean isGraphRagCommunitySummaryOnly(Document document) {
+ if (document == null || document.getMetadata() == null || !isGraphRagMetadata(document.getMetadata())) {
+ return false;
+ }
+ Map metadata = document.getMetadata();
+ Object summaryOnly = metadata.get(DocumentKnowledgeMetadataKeys.KG_COMMUNITY_SUMMARY_ONLY);
+ if (summaryOnly instanceof Boolean bool) {
+ return bool;
+ }
+ if (summaryOnly != null && Boolean.parseBoolean(String.valueOf(summaryOnly))) {
+ return true;
+ }
+ String groundingLevel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_GROUNDING_LEVEL));
+ if ("COMMUNITY_SUMMARY_ONLY".equalsIgnoreCase(groundingLevel)) {
+ return true;
+ }
+ return isGraphRagCommunityReportCandidate(metadata) && !hasGraphRagSourceQuote(document);
+ }
+
+ private boolean hasGraphRagSourceQuote(Document document) {
+ if (document == null || document.getMetadata() == null) {
+ return false;
+ }
+ Object originalSnippet = document.getMetadata().get(DocumentKnowledgeMetadataKeys.ORIGINAL_SNIPPET);
+ return isMeaningfulMetadataValue(originalSnippet)
+ || (isMeaningfulMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID))
+ && isMeaningfulMetadataValue(document.getText()));
+ }
+
+ private boolean isSuggestedChannel(String channelName, ConversationExecutionPlan plan) {
+ QueryUnderstandingResult queryUnderstanding = plan == null ? null : plan.getQueryUnderstanding();
+ if (queryUnderstanding == null || queryUnderstanding.getChannels() == null || queryUnderstanding.getChannels().isEmpty()) {
+ return false;
+ }
+ RetrievalIntent channelIntent = channelIntent(channelName);
+ return channelIntent != null && queryUnderstanding.getChannels().contains(channelIntent);
+ }
+
+ private RetrievalIntent channelIntent(String channelName) {
+ if (RetrievalChannelEnum.TABLE.getName().equals(channelName)) {
+ return RetrievalIntent.TABLE;
+ }
+ if (RetrievalChannelEnum.GRAPH_RAG.getName().equals(channelName)) {
+ return RetrievalIntent.GRAPH_RAG;
+ }
+ if (RetrievalChannelEnum.RAPTOR.getName().equals(channelName)) {
+ return RetrievalIntent.RAPTOR;
+ }
+ if (RetrievalChannelEnum.VECTOR.getName().equals(channelName)
+ || RetrievalChannelEnum.KEYWORD.getName().equals(channelName)) {
+ return RetrievalIntent.GENERAL;
+ }
+ return null;
+ }
+
+ private RetrievalIntent resolveRetrievalIntent(ConversationExecutionPlan plan) {
+ return plan == null || plan.getRetrievalIntent() == null ? RetrievalIntent.GENERAL : plan.getRetrievalIntent();
+ }
+
+ private boolean isGraphRagMetadata(Map metadata) {
+ String channel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHANNEL));
+ String sourceType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.SOURCE_TYPE));
+ return RetrievalChannelEnum.GRAPH_RAG.getName().equals(channel)
+ || "GRAPH_RAG".equalsIgnoreCase(sourceType)
+ || metadata.get(DocumentKnowledgeMetadataKeys.KG_EVIDENCE_ID) != null
+ || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CANONICAL_ENTITY_KEY))
+ || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_GROUP_KEY))
+ || isMeaningfulMetadataValue(metadata.get(DocumentKnowledgeMetadataKeys.KG_CROSS_DOCUMENT_COMMUNITY_KEY))
+ || metadata.get(DocumentKnowledgeMetadataKeys.KG_ENTITY_ID) != null
+ || metadata.get(DocumentKnowledgeMetadataKeys.KG_RELATION_ID) != null;
+ }
+
+ private boolean isRaptorMetadata(Map metadata) {
+ if (metadata == null) {
+ return false;
+ }
+ String channel = safeText(metadata.get(DocumentKnowledgeMetadataKeys.CHANNEL));
+ String sourceType = safeText(metadata.get(DocumentKnowledgeMetadataKeys.SOURCE_TYPE));
+ return RetrievalChannelEnum.RAPTOR.getName().equals(channel)
+ || "RAPTOR".equalsIgnoreCase(sourceType)
+ || metadata.get(DocumentKnowledgeMetadataKeys.RAPTOR_NODE_ID) != null;
+ }
+
+ private boolean isMeaningfulMetadataValue(Object value) {
+ if (value == null) {
+ return false;
+ }
+ return !(value instanceof String text) || !text.isBlank();
+ }
+
+ private boolean booleanMetadataValue(Object value) {
+ if (value instanceof Boolean bool) {
+ return bool;
+ }
+ if (value == null) {
+ return false;
+ }
+ return Boolean.parseBoolean(String.valueOf(value));
+ }
+
+ private double finalDocumentScore(Document document) {
+ if (document == null) {
+ return 0D;
+ }
+ Double score = numericMetadataValue(document.getMetadata().get(DocumentKnowledgeMetadataKeys.SCORE));
+ if (score != null) {
+ return score;
+ }
+ return document.getScore() == null ? 0D : document.getScore();
+ }
+
+ private RagRuntimeOptions runtimeOptions(ConversationExecutionPlan plan) {
+ return RagRuntimeOptions.resolve(plan, properties);
+ }
+
+ private Integer integerMetadataValue(Object value) {
+ Double number = numericMetadataValue(value);
+ return number == null ? null : number.intValue();
+ }
+
+ private Long longMetadataValue(Object value) {
+ Double number = numericMetadataValue(value);
+ return number == null ? null : number.longValue();
+ }
+
+ private Double numericMetadataValue(Object value) {
+ if (value instanceof Number number) {
+ return number.doubleValue();
+ }
+ if (value == null) {
+ return null;
+ }
+ try {
+ return Double.parseDouble(String.valueOf(value));
+ }
+ catch (NumberFormatException exception) {
+ return null;
+ }
+ }
+
+ private String normalizedAnchor(Object value) {
+ return normalizeSimple(safeText(value)
+ .replaceAll("[\\s>`*#_\\-,,。;;::()()“”\"'\\[\\]{}]+", ""));
+ }
+
+ private String normalizeSimple(String value) {
+ if (value == null) {
+ return "";
+ }
+ return Normalizer.normalize(value, Normalizer.Form.NFKC)
+ .trim()
+ .toLowerCase(Locale.ROOT);
+ }
+
+ private String safeText(Object value) {
+ return value == null ? "" : String.valueOf(value).trim();
+ }
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/KnowledgeBaseRuntimeConfigResolver.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/KnowledgeBaseRuntimeConfigResolver.java
new file mode 100644
index 0000000000000000000000000000000000000000..4cb841aae3d2aa577f5541c07599e6def409549f
--- /dev/null
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/KnowledgeBaseRuntimeConfigResolver.java
@@ -0,0 +1,261 @@
+package org.javaup.ai.chatagent.rag.service;
+
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.javaup.ai.chatagent.rag.config.ChatRagProperties;
+import org.javaup.ai.chatagent.rag.model.RagRuntimeOptions;
+import org.javaup.ai.manage.data.SuperAgentKnowledgeBase;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Function;
+
+@Slf4j
+@Service
+public class KnowledgeBaseRuntimeConfigResolver {
+
+ private final ChatRagProperties properties;
+ private final ObjectMapper objectMapper;
+
+ public KnowledgeBaseRuntimeConfigResolver(ChatRagProperties properties) {
+ this.properties = properties;
+ this.objectMapper = new ObjectMapper()
+ .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ }
+
+ public RagRuntimeOptions resolve(List knowledgeBases) {
+ RagRuntimeOptions options = RagRuntimeOptions.from(properties);
+ List configs = knowledgeBases == null
+ ? List.of()
+ : knowledgeBases.stream()
+ .filter(Objects::nonNull)
+ .map(this::parseConfig)
+ .toList();
+ if (configs.isEmpty()) {
+ return options;
+ }
+ if (configs.size() == 1) {
+ applySingle(options, configs.get(0));
+ return options;
+ }
+ applyMerged(options, configs);
+ return options;
+ }
+
+ private void applySingle(RagRuntimeOptions options, RuntimeConfig config) {
+ applyIfPresent(config.getVectorTopK(), options::setVectorTopK);
+ applyIfPresent(config.getKeywordTopK(), options::setKeywordTopK);
+ applyIfPresent(config.getGraphRagTopK(), options::setGraphRagTopK);
+ applyIfPresent(config.getGraphRagMaxHops(), options::setGraphRagMaxHops);
+ applyIfPresent(config.getRaptorTopK(), options::setRaptorTopK);
+ applyIfPresent(config.getRaptorSourceChunkTopK(), options::setRaptorSourceChunkTopK);
+ applyIfPresent(config.getCandidateTopK(), options::setCandidateTopK);
+ applyIfPresent(config.getRerankCandidateTopK(), options::setRerankCandidateTopK);
+ applyIfPresent(config.getReserveCandidateTopK(), options::setReserveCandidateTopK);
+ applyIfPresent(config.getFinalTopK(), options::setFinalTopK);
+ applyIfPresent(config.getMinVectorSimilarity(), options::setMinVectorSimilarity);
+ applyIfPresent(config.getKeywordRelativeScoreFloor(), options::setKeywordRelativeScoreFloor);
+ applyIfPresent(config.getKeywordChannelEnabled(), options::setKeywordChannelEnabled);
+ applyIfPresent(config.getTableChannelEnabled(), options::setTableChannelEnabled);
+ applyIfPresent(config.getGraphRagChannelEnabled(), options::setGraphRagChannelEnabled);
+ applyIfPresent(config.getRaptorChannelEnabled(), options::setRaptorChannelEnabled);
+ applySingleHybrid(options.getHybrid(), config.getHybrid());
+ }
+
+ private void applySingleHybrid(RagRuntimeOptions.HybridOptions options, HybridConfig config) {
+ if (config == null || options == null) {
+ return;
+ }
+ applyIfPresent(config.getVectorWeight(), options::setVectorWeight);
+ applyIfPresent(config.getKeywordWeight(), options::setKeywordWeight);
+ applyIfPresent(config.getTableWeight(), options::setTableWeight);
+ applyIfPresent(config.getGraphRagWeight(), options::setGraphRagWeight);
+ applyIfPresent(config.getRaptorWeight(), options::setRaptorWeight);
+ applyIfPresent(config.getRankWeight(), options::setRankWeight);
+ applyIfPresent(config.getOriginalScoreWeight(), options::setOriginalScoreWeight);
+ applyIfPresent(config.getMetadataBoostWeight(), options::setMetadataBoostWeight);
+ applyIfPresent(config.getMaxMetadataBoost(), options::setMaxMetadataBoost);
+ }
+
+ private void applyMerged(RagRuntimeOptions options, List configs) {
+ List conflicts = new ArrayList<>();
+ mergeField(configs, RuntimeConfig::getVectorTopK, options::setVectorTopK, "vectorTopK", conflicts);
+ mergeField(configs, RuntimeConfig::getKeywordTopK, options::setKeywordTopK, "keywordTopK", conflicts);
+ mergeField(configs, RuntimeConfig::getGraphRagTopK, options::setGraphRagTopK, "graphRagTopK", conflicts);
+ mergeField(configs, RuntimeConfig::getGraphRagMaxHops, options::setGraphRagMaxHops, "graphRagMaxHops", conflicts);
+ mergeField(configs, RuntimeConfig::getRaptorTopK, options::setRaptorTopK, "raptorTopK", conflicts);
+ mergeField(configs, RuntimeConfig::getRaptorSourceChunkTopK, options::setRaptorSourceChunkTopK, "raptorSourceChunkTopK", conflicts);
+ mergeField(configs, RuntimeConfig::getCandidateTopK, options::setCandidateTopK, "candidateTopK", conflicts);
+ mergeField(configs, RuntimeConfig::getRerankCandidateTopK, options::setRerankCandidateTopK, "rerankCandidateTopK", conflicts);
+ mergeField(configs, RuntimeConfig::getReserveCandidateTopK, options::setReserveCandidateTopK, "reserveCandidateTopK", conflicts);
+ mergeField(configs, RuntimeConfig::getFinalTopK, options::setFinalTopK, "finalTopK", conflicts);
+ mergeField(configs, RuntimeConfig::getMinVectorSimilarity, options::setMinVectorSimilarity, "minVectorSimilarity", conflicts);
+ mergeField(configs, RuntimeConfig::getKeywordRelativeScoreFloor, options::setKeywordRelativeScoreFloor, "keywordRelativeScoreFloor", conflicts);
+ mergeField(configs, RuntimeConfig::getKeywordChannelEnabled, options::setKeywordChannelEnabled, "keywordChannelEnabled", conflicts);
+ mergeField(configs, RuntimeConfig::getTableChannelEnabled, options::setTableChannelEnabled, "tableChannelEnabled", conflicts);
+ mergeField(configs, RuntimeConfig::getGraphRagChannelEnabled, options::setGraphRagChannelEnabled, "graphRagChannelEnabled", conflicts);
+ mergeField(configs, RuntimeConfig::getRaptorChannelEnabled, options::setRaptorChannelEnabled, "raptorChannelEnabled", conflicts);
+
+ mergeHybridField(configs, HybridConfig::getVectorWeight, options.getHybrid()::setVectorWeight, "hybrid.vectorWeight", conflicts);
+ mergeHybridField(configs, HybridConfig::getKeywordWeight, options.getHybrid()::setKeywordWeight, "hybrid.keywordWeight", conflicts);
+ mergeHybridField(configs, HybridConfig::getTableWeight, options.getHybrid()::setTableWeight, "hybrid.tableWeight", conflicts);
+ mergeHybridField(configs, HybridConfig::getGraphRagWeight, options.getHybrid()::setGraphRagWeight, "hybrid.graphRagWeight", conflicts);
+ mergeHybridField(configs, HybridConfig::getRaptorWeight, options.getHybrid()::setRaptorWeight, "hybrid.raptorWeight", conflicts);
+ mergeHybridField(configs, HybridConfig::getRankWeight, options.getHybrid()::setRankWeight, "hybrid.rankWeight", conflicts);
+ mergeHybridField(configs, HybridConfig::getOriginalScoreWeight, options.getHybrid()::setOriginalScoreWeight, "hybrid.originalScoreWeight", conflicts);
+ mergeHybridField(configs, HybridConfig::getMetadataBoostWeight, options.getHybrid()::setMetadataBoostWeight, "hybrid.metadataBoostWeight", conflicts);
+ mergeHybridField(configs, HybridConfig::getMaxMetadataBoost, options.getHybrid()::setMaxMetadataBoost, "hybrid.maxMetadataBoost", conflicts);
+
+ options.setKbConfigConflictFields(new ArrayList<>(new LinkedHashSet<>(conflicts)));
+ }
+
+ private void mergeHybridField(List configs,
+ Function getter,
+ java.util.function.Consumer setter,
+ String field,
+ List conflicts) {
+ mergeField(configs,
+ config -> config.getHybrid() == null ? null : getter.apply(config.getHybrid()),
+ setter,
+ field,
+ conflicts);
+ }
+
+ private void mergeField(List configs,
+ Function getter,
+ java.util.function.Consumer setter,
+ String field,
+ List conflicts) {
+ List values = configs.stream().map(getter).toList();
+ if (values.stream().allMatch(Objects::isNull)) {
+ return;
+ }
+ if (values.stream().anyMatch(Objects::isNull)) {
+ conflicts.add(field);
+ return;
+ }
+ T first = values.get(0);
+ boolean same = values.stream().allMatch(value -> Objects.equals(first, value));
+ if (same) {
+ setter.accept(first);
+ }
+ else {
+ conflicts.add(field);
+ }
+ }
+
+ private RuntimeConfig parseConfig(SuperAgentKnowledgeBase knowledgeBase) {
+ RuntimeConfig merged = new RuntimeConfig();
+ mergeInto(merged, parseJson(knowledgeBase.getRetrievalConfigJson(), knowledgeBase));
+ mergeInto(merged, parseJson(knowledgeBase.getGraphRagConfigJson(), knowledgeBase));
+ mergeInto(merged, parseJson(knowledgeBase.getRaptorConfigJson(), knowledgeBase));
+ return merged;
+ }
+
+ private RuntimeConfig parseJson(String rawJson, SuperAgentKnowledgeBase knowledgeBase) {
+ if (StrUtil.isBlank(rawJson)) {
+ return new RuntimeConfig();
+ }
+ try {
+ return objectMapper.readValue(rawJson, RuntimeConfig.class);
+ }
+ catch (JsonProcessingException | RuntimeException exception) {
+ log.warn("知识库 RAG 配置 JSON 解析失败,将忽略该段配置: knowledgeBaseId={}, knowledgeBaseName={}",
+ knowledgeBase == null ? null : knowledgeBase.getId(),
+ knowledgeBase == null ? "" : knowledgeBase.getBaseName(),
+ exception);
+ return new RuntimeConfig();
+ }
+ }
+
+ private void mergeInto(RuntimeConfig target, RuntimeConfig source) {
+ if (source == null) {
+ return;
+ }
+ copyIfPresent(source.getVectorTopK(), target::setVectorTopK);
+ copyIfPresent(source.getKeywordTopK(), target::setKeywordTopK);
+ copyIfPresent(source.getGraphRagTopK(), target::setGraphRagTopK);
+ copyIfPresent(source.getGraphRagMaxHops(), target::setGraphRagMaxHops);
+ copyIfPresent(source.getRaptorTopK(), target::setRaptorTopK);
+ copyIfPresent(source.getRaptorSourceChunkTopK(), target::setRaptorSourceChunkTopK);
+ copyIfPresent(source.getCandidateTopK(), target::setCandidateTopK);
+ copyIfPresent(source.getRerankCandidateTopK(), target::setRerankCandidateTopK);
+ copyIfPresent(source.getReserveCandidateTopK(), target::setReserveCandidateTopK);
+ copyIfPresent(source.getFinalTopK(), target::setFinalTopK);
+ copyIfPresent(source.getMinVectorSimilarity(), target::setMinVectorSimilarity);
+ copyIfPresent(source.getKeywordRelativeScoreFloor(), target::setKeywordRelativeScoreFloor);
+ copyIfPresent(source.getKeywordChannelEnabled(), target::setKeywordChannelEnabled);
+ copyIfPresent(source.getTableChannelEnabled(), target::setTableChannelEnabled);
+ copyIfPresent(source.getGraphRagChannelEnabled(), target::setGraphRagChannelEnabled);
+ copyIfPresent(source.getRaptorChannelEnabled(), target::setRaptorChannelEnabled);
+ if (source.getHybrid() != null) {
+ if (target.getHybrid() == null) {
+ target.setHybrid(new HybridConfig());
+ }
+ mergeHybridInto(target.getHybrid(), source.getHybrid());
+ }
+ }
+
+ private void mergeHybridInto(HybridConfig target, HybridConfig source) {
+ copyIfPresent(source.getVectorWeight(), target::setVectorWeight);
+ copyIfPresent(source.getKeywordWeight(), target::setKeywordWeight);
+ copyIfPresent(source.getTableWeight(), target::setTableWeight);
+ copyIfPresent(source.getGraphRagWeight(), target::setGraphRagWeight);
+ copyIfPresent(source.getRaptorWeight(), target::setRaptorWeight);
+ copyIfPresent(source.getRankWeight(), target::setRankWeight);
+ copyIfPresent(source.getOriginalScoreWeight(), target::setOriginalScoreWeight);
+ copyIfPresent(source.getMetadataBoostWeight(), target::setMetadataBoostWeight);
+ copyIfPresent(source.getMaxMetadataBoost(), target::setMaxMetadataBoost);
+ }
+
+ private void applyIfPresent(T value, java.util.function.Consumer setter) {
+ copyIfPresent(value, setter);
+ }
+
+ private void copyIfPresent(T value, java.util.function.Consumer setter) {
+ if (value != null) {
+ setter.accept(value);
+ }
+ }
+
+ @Data
+ public static class RuntimeConfig {
+ private Integer vectorTopK;
+ private Integer keywordTopK;
+ private Integer graphRagTopK;
+ private Integer graphRagMaxHops;
+ private Integer raptorTopK;
+ private Integer raptorSourceChunkTopK;
+ private Integer candidateTopK;
+ private Integer rerankCandidateTopK;
+ private Integer reserveCandidateTopK;
+ private Integer finalTopK;
+ private Double minVectorSimilarity;
+ private Double keywordRelativeScoreFloor;
+ private Boolean keywordChannelEnabled;
+ private Boolean tableChannelEnabled;
+ private Boolean graphRagChannelEnabled;
+ private Boolean raptorChannelEnabled;
+ private HybridConfig hybrid;
+ }
+
+ @Data
+ public static class HybridConfig {
+ private Double vectorWeight;
+ private Double keywordWeight;
+ private Double tableWeight;
+ private Double graphRagWeight;
+ private Double raptorWeight;
+ private Double rankWeight;
+ private Double originalScoreWeight;
+ private Double metadataBoostWeight;
+ private Double maxMetadataBoost;
+ }
+}
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingService.java
index 1c86eb7837f9433e530b7fe7cad7566e94fce3b3..69e5a6df15bc643396f2942c4f8f6c2ec6954eed 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingService.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/QueryUnderstandingService.java
@@ -4,9 +4,12 @@ import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
+import org.javaup.ai.chatagent.rag.model.EvidenceRole;
import org.javaup.ai.chatagent.rag.model.QueryType;
import org.javaup.ai.chatagent.rag.model.QueryUnderstandingResult;
import org.javaup.ai.chatagent.rag.model.RetrievalIntent;
+import org.javaup.ai.chatagent.rag.model.StructureNavigationIntent;
+import org.javaup.ai.chatagent.rag.model.StructureNavigationOperation;
import org.javaup.ai.chatagent.service.ObservedChatModelService;
import org.javaup.ai.prompt.PromptTemplateNames;
import org.javaup.ai.prompt.PromptTemplateService;
@@ -37,6 +40,7 @@ public class QueryUnderstandingService {
private static final Pattern QUOTED_TEXT_PATTERN = Pattern.compile("[“\"']([^”\"']{2,40})[”\"']");
private static final double ADVISOR_CONFIDENCE_THRESHOLD = 0.72D;
+ private static final double STRUCTURE_NAVIGATION_CONFIDENCE_THRESHOLD = 0.65D;
private final ObjectProvider observedChatModelServiceProvider;
private final ObjectProvider promptTemplateServiceProvider;
@@ -106,9 +110,14 @@ public class QueryUnderstandingService {
.queryType(parseQueryType(root.path("queryType").asText("")))
.channels(parseChannels(root.path("channels")))
.entities(readStringArray(root.path("entities"), 8))
+ .targetEntities(readStringArray(root.path("targetEntities"), 8))
+ .excludedEntities(readStringArray(root.path("excludedEntities"), 8))
.sectionAnchors(readStringArray(root.path("sectionAnchors"), 8))
+ .structureNavigationIntent(parseStructureNavigationIntent(root.path("structureNavigationIntent")))
+ .expectedEvidenceRoles(parseEvidenceRoles(root.path("expectedEvidenceRoles")))
.tableOps(readStringArray(root.path("tableOps"), 8))
.negativeBoundary(root.path("negativeBoundary").asBoolean(false))
+ .answerExpectation(root.path("answerExpectation").asText(""))
.confidence(normalizeConfidence(root.path("confidence").asDouble(0D)))
.reasons(readStringArray(root.path("reasons"), 8))
.source("llm-query-understanding")
@@ -137,6 +146,16 @@ public class QueryUnderstandingService {
channels.add(RetrievalIntent.GENERAL);
}
QueryType effectiveType = highConfidence ? queryType : fallback == null ? QueryType.DOCUMENT_QA : fallback.getQueryType();
+ double effectiveConfidence = highConfidence
+ ? confidence
+ : fallback == null ? confidence : normalizeConfidence(fallback.getConfidence());
+ List sectionAnchors = mergeStrings(fallback == null ? null : fallback.getSectionAnchors(), advised.getSectionAnchors(), 8);
+ StructureNavigationIntent structureNavigationIntent = selectStructureNavigationIntent(effectiveType, effectiveConfidence, advised, fallback, sectionAnchors);
+ List expectedEvidenceRoles = mergeRoles(
+ fallback == null ? null : fallback.getExpectedEvidenceRoles(),
+ advised.getExpectedEvidenceRoles(),
+ 4
+ );
List reasons = new ArrayList<>();
if (advised.getReasons() != null) {
reasons.addAll(advised.getReasons());
@@ -148,10 +167,15 @@ public class QueryUnderstandingService {
.queryType(effectiveType == null ? QueryType.DOCUMENT_QA : effectiveType)
.channels(new ArrayList<>(channels))
.entities(limitStrings(advised.getEntities(), 8))
- .sectionAnchors(mergeStrings(fallback == null ? null : fallback.getSectionAnchors(), advised.getSectionAnchors(), 8))
+ .targetEntities(limitStrings(advised.getTargetEntities(), 8))
+ .excludedEntities(limitStrings(advised.getExcludedEntities(), 8))
+ .sectionAnchors(sectionAnchors)
+ .structureNavigationIntent(structureNavigationIntent)
+ .expectedEvidenceRoles(expectedEvidenceRoles)
.tableOps(limitStrings(advised.getTableOps(), 8))
.negativeBoundary(advised.isNegativeBoundary())
- .confidence(confidence)
+ .answerExpectation(StrUtil.blankToDefault(advised.getAnswerExpectation(), "NORMAL_QA"))
+ .confidence(effectiveConfidence)
.reasons(limitStrings(reasons, 10))
.source(StrUtil.blankToDefault(advised.getSource(), "query-understanding"));
return builder.build();
@@ -164,6 +188,10 @@ public class QueryUnderstandingService {
boolean strictStructureNavigation = !hasMultipleSubQuestions && looksStrictStructureNavigation(normalized);
boolean outline = !hasMultipleSubQuestions && looksOutlineNavigation(normalized);
boolean explicitTableQuery = !hasMultipleSubQuestions && looksExplicitTableQuery(normalized);
+ List structureOperations = determineStructureOperations(normalized, strictStructureNavigation, outline);
+ if (outline && anchors.isEmpty()) {
+ anchors = mergeStrings(anchors, extractOutlineAnchors(normalized), 8);
+ }
QueryType queryType = explicitTableQuery
? QueryType.TABLE_QUERY
: strictStructureNavigation || outline
@@ -183,6 +211,16 @@ public class QueryUnderstandingService {
.queryType(queryType)
.channels(new ArrayList<>(channels))
.sectionAnchors(anchors)
+ .structureNavigationIntent(structureOperations.isEmpty()
+ ? null
+ : StructureNavigationIntent.builder()
+ .operations(structureOperations)
+ .sectionAnchors(anchors)
+ .confidence(strictStructureNavigation || outline ? 0.86D : 0.55D)
+ .source("java-deterministic-fallback")
+ .build())
+ .expectedEvidenceRoles(List.of())
+ .answerExpectation("NORMAL_QA")
.confidence(strictStructureNavigation || outline || explicitTableQuery ? 0.86D : 0.55D)
.reasons(reasons)
.source("java-deterministic-fallback")
@@ -210,6 +248,25 @@ public class QueryUnderstandingService {
return containsAny(normalized, List.of("包含哪些章节", "都包含哪些章节", "有哪些章节", "有哪些小节", "包含哪些小节", "章节列表", "展开目录"));
}
+ private List determineStructureOperations(String question,
+ boolean strictStructureNavigation,
+ boolean outline) {
+ if (outline) {
+ return List.of(StructureNavigationOperation.SECTION_WITH_CHILDREN);
+ }
+ if (!strictStructureNavigation) {
+ return List.of();
+ }
+ String normalized = safeText(question);
+ if (containsAny(normalized, List.of("上一节", "下一节", "前一节", "后一节", "上一章", "下一章", "相邻章节", "同一一级章节"))) {
+ return List.of(StructureNavigationOperation.SECTION_WITH_SIBLINGS);
+ }
+ if (containsAny(normalized, List.of("属于哪个章节", "哪个章节", "哪个小节", "哪一节", "哪一章", "章节位置"))) {
+ return List.of(StructureNavigationOperation.PARENT_SECTION);
+ }
+ return List.of(StructureNavigationOperation.CURRENT_SECTION);
+ }
+
private boolean looksExplicitTableQuery(String question) {
String normalized = safeText(question);
if (normalized.isBlank()) {
@@ -238,6 +295,100 @@ public class QueryUnderstandingService {
return anchors.stream().limit(8).toList();
}
+ private List extractOutlineAnchors(String text) {
+ String cleaned = safeText(text)
+ .replace("都包含哪些章节", "")
+ .replace("包含哪些章节", "")
+ .replace("都有哪些章节", "")
+ .replace("有哪些章节", "")
+ .replace("包含哪些小节", "")
+ .replace("有哪些小节", "")
+ .replace("章节列表", "")
+ .replace("展开目录", "")
+ .replace("?", "")
+ .replace("?", "")
+ .trim();
+ return StrUtil.isBlank(cleaned) ? List.of() : List.of(cleaned);
+ }
+
+ private StructureNavigationIntent parseStructureNavigationIntent(JsonNode node) {
+ if (node == null || !node.isObject()) {
+ return null;
+ }
+ List operations = parseStructureOperations(node.path("operations"));
+ return StructureNavigationIntent.builder()
+ .operations(operations)
+ .anchorStructureNodeId(node.path("anchorStructureNodeId").isNumber() ? node.path("anchorStructureNodeId").asLong() : null)
+ .anchorSectionPath(node.path("anchorSectionPath").asText(""))
+ .anchorCanonicalPath(node.path("anchorCanonicalPath").asText(""))
+ .sectionAnchors(readStringArray(node.path("sectionAnchors"), 8))
+ .confidence(normalizeConfidence(node.path("confidence").asDouble(0D)))
+ .source("llm-query-understanding")
+ .build();
+ }
+
+ private List parseStructureOperations(JsonNode node) {
+ if (node == null || !node.isArray()) {
+ return List.of();
+ }
+ LinkedHashSet operations = new LinkedHashSet<>();
+ for (JsonNode item : node) {
+ String normalized = item.asText("").trim().toUpperCase(Locale.ROOT);
+ try {
+ operations.add(StructureNavigationOperation.valueOf(normalized));
+ }
+ catch (IllegalArgumentException ignored) {
+ // 丢弃未知结构导航操作,Java 主链路只接受白名单枚举。
+ }
+ }
+ return new ArrayList<>(operations);
+ }
+
+ private StructureNavigationIntent selectStructureNavigationIntent(QueryType effectiveType,
+ double confidence,
+ QueryUnderstandingResult advised,
+ QueryUnderstandingResult fallback,
+ List sectionAnchors) {
+ if (effectiveType != QueryType.STRUCTURE_NAVIGATION || confidence < STRUCTURE_NAVIGATION_CONFIDENCE_THRESHOLD) {
+ return null;
+ }
+ StructureNavigationIntent advisedIntent = advised == null ? null : advised.getStructureNavigationIntent();
+ if (isValidStructureNavigationIntent(advisedIntent)) {
+ return normalizeStructureNavigationIntent(advisedIntent, sectionAnchors, "llm-query-understanding");
+ }
+ StructureNavigationIntent fallbackIntent = fallback == null ? null : fallback.getStructureNavigationIntent();
+ if (isValidStructureNavigationIntent(fallbackIntent)) {
+ return normalizeStructureNavigationIntent(fallbackIntent, sectionAnchors, "java-deterministic-fallback");
+ }
+ return null;
+ }
+
+ private boolean isValidStructureNavigationIntent(StructureNavigationIntent intent) {
+ return intent != null && intent.getOperations() != null && !intent.getOperations().isEmpty();
+ }
+
+ private StructureNavigationIntent normalizeStructureNavigationIntent(StructureNavigationIntent intent,
+ List sectionAnchors,
+ String fallbackSource) {
+ List operations = intent.getOperations().stream()
+ .filter(operation -> operation != null)
+ .distinct()
+ .limit(4)
+ .toList();
+ if (operations.isEmpty()) {
+ return null;
+ }
+ return StructureNavigationIntent.builder()
+ .operations(operations)
+ .anchorStructureNodeId(intent.getAnchorStructureNodeId())
+ .anchorSectionPath(StrUtil.blankToDefault(intent.getAnchorSectionPath(), ""))
+ .anchorCanonicalPath(StrUtil.blankToDefault(intent.getAnchorCanonicalPath(), ""))
+ .sectionAnchors(mergeStrings(sectionAnchors, intent.getSectionAnchors(), 8))
+ .confidence(normalizeConfidence(intent.getConfidence()))
+ .source(StrUtil.blankToDefault(intent.getSource(), fallbackSource))
+ .build();
+ }
+
private QueryType parseQueryType(String raw) {
String normalized = StrUtil.blankToDefault(raw, "").trim().toUpperCase(Locale.ROOT);
try {
@@ -269,6 +420,26 @@ public class QueryUnderstandingService {
return new ArrayList<>(channels);
}
+ private List parseEvidenceRoles(JsonNode node) {
+ if (node == null || !node.isArray()) {
+ return List.of();
+ }
+ LinkedHashSet roles = new LinkedHashSet<>();
+ for (JsonNode item : node) {
+ String normalized = item.asText("").trim().toUpperCase(Locale.ROOT);
+ try {
+ EvidenceRole role = EvidenceRole.valueOf(normalized);
+ if (role != EvidenceRole.GENERAL) {
+ roles.add(role);
+ }
+ }
+ catch (IllegalArgumentException ignored) {
+ // 丢弃未知证据角色,Java 主链路只接受白名单枚举。
+ }
+ }
+ return new ArrayList<>(roles);
+ }
+
private List readStringArray(JsonNode node, int limit) {
if (node == null || !node.isArray()) {
return List.of();
@@ -297,6 +468,21 @@ public class QueryUnderstandingService {
return values.stream().limit(limit).toList();
}
+ private List mergeRoles(List first, List second, int limit) {
+ LinkedHashSet values = new LinkedHashSet<>();
+ if (first != null) {
+ first.stream()
+ .filter(role -> role != null && role != EvidenceRole.GENERAL)
+ .forEach(values::add);
+ }
+ if (second != null) {
+ second.stream()
+ .filter(role -> role != null && role != EvidenceRole.GENERAL)
+ .forEach(values::add);
+ }
+ return values.stream().limit(limit).toList();
+ }
+
private List limitStrings(List values, int limit) {
if (values == null || values.isEmpty()) {
return List.of();
diff --git a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairService.java b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairService.java
index eef7b709b43b3297192cacbd49873bc6d3e87dbc..1a5ad679ce56cc89e7cd613e48940cbdc58f67e5 100644
--- a/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairService.java
+++ b/super-agent-business/super-agent-business-chat/src/main/java/org/javaup/ai/chatagent/rag/service/RagCitationRepairService.java
@@ -4,6 +4,8 @@ import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.javaup.ai.chatagent.model.SearchReference;
import org.javaup.ai.chatagent.model.trace.ConversationTraceStageCode;
+import org.javaup.ai.chatagent.rag.model.EvidenceIdentity;
+import org.javaup.ai.chatagent.rag.support.EvidenceIdentityResolver;
import org.javaup.ai.chatagent.service.ConversationTraceRecorder;
import org.javaup.ai.ragtools.client.RagToolsClient;
import org.javaup.ai.ragtools.model.RagToolsCitationRepairRequest;
@@ -24,6 +26,9 @@ public class RagCitationRepairService {
private static final double MIN_SCORE = 0.18D;
private static final int MAX_TRACE_CANDIDATES = 24;
private static final int MAX_TRACE_CITATIONS = 24;
+ private static final String NOT_APPLICABLE_STATUS = "NOT_APPLICABLE";
+ private static final String FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY = "FILTERED_NOT_APPLICABLE_TO_TARGET_ENTITY";
+ private static final String LEGACY_NOT_APPLICABLE_REASON = "NOT_APPLICABLE_TO_TARGET_ENTITY";
private final RagToolsClient ragToolsClient;
@@ -41,8 +46,12 @@ public class RagCitationRepairService {
List documentReferences = references.stream()
.filter(this::isRepairableDocumentReference)
+ .filter(reference -> !isEvidenceNotApplicable(reference))
.toList();
- if (documentReferences.isEmpty()) {
+ List applicabilityFilteredReferences = references.stream()
+ .filter(this::isEvidenceNotApplicable)
+ .toList();
+ if (documentReferences.isEmpty() && applicabilityFilteredReferences.isEmpty()) {
return references;
}
@@ -54,26 +63,32 @@ public class RagCitationRepairService {
"正在修复回答句与原文证据的引用关系。",
Map.of(
"candidateReferenceCount", references.size(),
- "documentReferenceCount", documentReferences.size()
+ "documentReferenceCount", documentReferences.size(),
+ "applicabilityFilteredReferenceCount", applicabilityFilteredReferences.size()
)
);
try {
- RagToolsCitationRepairRequest request = buildRequest(answer, documentReferences);
- RagToolsCitationRepairResponse response = ragToolsClient.repairCitations(request);
- if (response == null) {
- throw new IllegalStateException("rag-tools citation repair 返回空响应");
+ List citations = List.of();
+ if (!documentReferences.isEmpty()) {
+ RagToolsCitationRepairRequest request = buildRequest(answer, documentReferences);
+ RagToolsCitationRepairResponse response = ragToolsClient.repairCitations(request);
+ if (response == null) {
+ throw new IllegalStateException("rag-tools citation repair 返回空响应");
+ }
+ citations = response.getCitations() == null ? List.of() : response.getCitations();
}
- List repairedReferences = applyRepairResults(references, documentReferences, response.getCitations());
+ List repairedReferences = applyRepairResults(references, documentReferences, citations);
if (traceRecorder != null) {
traceRecorder.completeStage(
citationStage,
"引用修复完成。",
- buildRepairTraceSnapshot(references, documentReferences, response.getCitations(), repairedReferences)
+ buildRepairTraceSnapshot(references, documentReferences, applicabilityFilteredReferences, citations, repairedReferences)
);
}
- log.info("引用修复完成: candidateReferenceCount={}, documentReferenceCount={}, repairedReferenceCount={}",
+ log.info("引用修复完成: candidateReferenceCount={}, documentReferenceCount={}, applicabilityFilteredReferenceCount={}, repairedReferenceCount={}",
references.size(),
documentReferences.size(),
+ applicabilityFilteredReferences.size(),
repairedReferences.size());
return repairedReferences;
}
@@ -132,6 +147,7 @@ public class RagCitationRepairService {
List repairedReferences = new ArrayList<>();
allReferences.stream()
.filter(reference -> !isRepairableDocumentReference(reference))
+ .filter(reference -> !isEvidenceNotApplicable(reference))
.forEach(repairedReferences::add);
repairedReferences.addAll(repairedDocumentMap.values());
return repairedReferences;
@@ -162,16 +178,37 @@ public class RagCitationRepairService {
if (StrUtil.isNotBlank(citation.getSectionPath())) {
reference.setSectionPath(citation.getSectionPath());
}
+ refreshEvidenceIdentity(reference);
+ }
+
+ private void refreshEvidenceIdentity(SearchReference reference) {
+ EvidenceIdentity citationIdentity = EvidenceIdentityResolver.citationIdentity(reference);
+ EvidenceIdentity contextIdentity = EvidenceIdentityResolver.contextIdentity(reference);
+ if (citationIdentity != null && citationIdentity.present()) {
+ reference.setCitationIdentity(citationIdentity.value());
+ reference.setCitationEvidenceType(citationIdentity.type().name());
+ reference.setSourceEvidenceResolved(true);
+ reference.setContextOnly(false);
+ }
+ else {
+ reference.setCitationIdentity("");
+ reference.setCitationEvidenceType("CONTEXT_ONLY");
+ reference.setSourceEvidenceResolved(false);
+ reference.setContextOnly(true);
+ }
+ reference.setContextIdentity(contextIdentity == null || !contextIdentity.present() ? "" : contextIdentity.value());
}
private Map buildEvidenceMetadata(SearchReference reference) {
Map metadata = new LinkedHashMap<>();
metadata.put("referenceId", StrUtil.blankToDefault(reference.getReferenceId(), ""));
+ metadata.put("finalSelectionReason", StrUtil.blankToDefault(reference.getFinalSelectionReason(), ""));
+ metadata.put("evidenceApplicabilityStatus", StrUtil.blankToDefault(reference.getEvidenceApplicabilityStatus(), ""));
+ metadata.put("evidenceApplicabilityReason", StrUtil.blankToDefault(reference.getEvidenceApplicabilityReason(), ""));
metadata.put("chunkNo", reference.getChunkNo());
+ metadata.put("chunkType", StrUtil.blankToDefault(reference.getChunkType(), ""));
metadata.put("parentBlockNo", reference.getParentBlockNo());
metadata.put("sourceBlockIds", StrUtil.blankToDefault(reference.getSourceBlockIds(), ""));
- metadata.put("knowledgeScopeCode", StrUtil.blankToDefault(reference.getKnowledgeScopeCode(), ""));
- metadata.put("knowledgeScopeName", StrUtil.blankToDefault(reference.getKnowledgeScopeName(), ""));
metadata.put("channel", StrUtil.blankToDefault(reference.getChannel(), ""));
metadata.put("tableId", reference.getTableId());
metadata.put("tableNo", reference.getTableNo());
@@ -193,6 +230,7 @@ public class RagCitationRepairService {
private Map buildRepairTraceSnapshot(List allReferences,
List documentReferences,
+ List applicabilityFilteredReferences,
List citations,
List repairedReferences) {
List