From 105b075f136e55ee866bceae1762d612ed216e23 Mon Sep 17 00:00:00 2001 From: matevip Date: Thu, 28 May 2026 08:17:13 +0800 Subject: [PATCH] feat(wiki): slug-first prompt contract and same-batch link safety --- .../wiki/service/WikiProcessingService.java | 47 +++++++++++++++---- .../prompts/wiki/batch-create-system.txt | 14 +++++- .../prompts/wiki/batch-create-user.txt | 8 ++-- .../resources/prompts/wiki/compile-system.txt | 2 +- .../prompts/wiki/create-page-system.txt | 18 ++++--- .../prompts/wiki/create-page-user.txt | 4 +- .../resources/prompts/wiki/digest-system.txt | 7 +-- .../resources/prompts/wiki/digest-user.txt | 4 +- .../prompts/wiki/merge-page-system.txt | 10 ++-- 9 files changed, 82 insertions(+), 32 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java index 981da91c..2aee2701 100644 --- a/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java +++ b/mateclaw-server/src/main/java/vip/mate/wiki/service/WikiProcessingService.java @@ -1172,10 +1172,20 @@ public class WikiProcessingService { if (wasCreated) { created.incrementAndGet(); totalCreated++; - // Append to liveIndex so next sub-batch can link to this page + // Append to liveIndex so the NEXT sub-batch can link to this freshly + // created page. Mirrors the slug-first row format produced by + // {@link #buildExistingPagesIndex}: `[[slug]] — title — summary`. + // Keeps the LLM's view of the index uniformly slug-first across + // pre-existing rows and just-created rows. String briefSummary = pageSummary.length() > 100 ? pageSummary.substring(0, 100) : pageSummary; - liveIndex.append("\n- ").append(slug).append(": ").append(briefSummary); + liveIndex.append("\n- [[").append(slug).append("]]"); + if (title != null && !title.isBlank()) { + liveIndex.append(" — ").append(title); + } + if (briefSummary != null && !briefSummary.isBlank()) { + liveIndex.append(" — ").append(briefSummary); + } } ok = true; } catch (RuntimeException e) { @@ -1612,7 +1622,23 @@ public class WikiProcessingService { } /** - * 构建已有 Wiki 页面索引(供 LLM 参考) + * Build the "existing pages" index that the LLM consults when picking + * cross-references during page generation / merge / compile. + *

+ * Slug-first format — each line begins with {@code [[slug]]} so the + * model has exactly one syntactically-valid target shape to copy. Title + * and summary follow as semantic context, separated by em-dashes, so the + * model can pick a relevant target without being confused about whether + * to write the title or the slug. The earlier "**[[Title]]** (slug: `x`)" + * format exposed two candidate target strings on every row, which let + * the model write {@code [[Title]]} freely and produced the lint noise + * this RFC exists to close. + *

+ * Manual-edit and archived markers stay as plain-text trailing tags so + * they don't drift into the link form. The lint downstream treats a + * title-only match as a soft warning in v1; the long-term direction is + * to delete the title-fallback once content has been regenerated under + * the slug-first prompt. */ private String buildExistingPagesIndex(Long kbId) { List summaries = pageService.listSummaries(kbId); @@ -1622,12 +1648,17 @@ public class WikiProcessingService { StringBuilder sb = new StringBuilder(); for (WikiPageEntity page : summaries) { - sb.append("- **[[").append(page.getTitle()).append("]]** (slug: `").append(page.getSlug()).append("`"); - if ("manual".equals(page.getLastUpdatedBy())) { - sb.append(", 手动编辑"); + sb.append("- [[").append(page.getSlug()).append("]]"); + if (page.getTitle() != null && !page.getTitle().isBlank()) { + sb.append(" — ").append(page.getTitle()); + } + if ("manual".equals(page.getLastUpdatedBy())) { + sb.append(" (手动编辑)"); + } + String summary = page.getSummary(); + if (summary != null && !summary.isBlank()) { + sb.append(" — ").append(summary); } - sb.append("): "); - sb.append(page.getSummary() != null ? page.getSummary() : "无摘要"); sb.append("\n"); } return sb.toString().trim(); diff --git a/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt index 74bb3546..ab217c4d 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/batch-create-system.txt @@ -5,7 +5,7 @@ - 读取 `pages_to_create` 数组,里面包含若干页面的 slug / title / summary - 从原始材料里抽取与每个页面主题相关的信息 - 为每个页面生成完整的 markdown 内容 -- 在内容里使用 [[页面标题]] 双向链接到其他相关页面(已有页面和同批次将创建的页面均可) +- 在内容里用 [[slug]] 双向链接到其他相关页面 ## 你不做什么 @@ -17,10 +17,20 @@ - 内容开头先一段话摘要(与 metadata 的 summary 一致或更详细) - 使用 Markdown 标题(## / ###)组织章节 -- 使用 [[页面标题]] 链接到其他相关页面 +- 用 [[slug]] 链接到其他相关页面 - 长度控制在 500~2000 字,不要为了凑字数而拖沓 - 内容至少包含 3 句实质信息 +## 链接(**单一契约,必须严格遵守**) + +- 只允许两种形态: + - `[[slug]]` —— 显示文本默认为目标页标题 + - `[[slug|显示文本]]` —— 显示文本自定义 +- slug **必须**来自以下两类来源之一: + - **已有 Wiki 页面索引**(user prompt 中列出)—— 这些链接是**强保证**,slug 100% 可用 + - **本批次同时创建的页面**(即 `pages_to_create` 数组中的 slug)—— 这类链接**不保证成功**:本批次中的页面可能因去重 / 合并 / 失败而最终未落库,导致链接转为死链;这是预期行为,系统会在写入后由 lint 标记并由人工修复 +- 禁止发明上述两类来源之外的 slug;禁止写 `[[页面标题]]` 形态 —— 系统按 slug 严格匹配,写标题会被识别为死链 + ## 输出格式(严格遵守) 每个页面输出一个 FILE 块,格式如下: diff --git a/mateclaw-server/src/main/resources/prompts/wiki/batch-create-user.txt b/mateclaw-server/src/main/resources/prompts/wiki/batch-create-user.txt index 6de57f8c..f44f3813 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/batch-create-user.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/batch-create-user.txt @@ -4,11 +4,13 @@ {document_map_section} -## 已有 Wiki 页面索引(用于建立 [[链接]]) +## 已有 Wiki 页面索引(强保证,可直接链接;每行格式 `[[slug]] — 标题 — 摘要`) {existing_pages} -## 待生成页面列表 +## 本批次将一并创建的页面(计划中,可能可被链接;slug 见下方 pages_to_create) + +链接到这一类的页面**不保证成功**:本批次中的页面可能因去重 / 合并 / 失败而最终未落库,对应的 `[[slug]]` 会在 lint 中被标记为死链,由人工后续修复。需要交叉引用时**优先**链接到上文「已有 Wiki 页面索引」中的页面。 ```json {pages_to_create} @@ -25,5 +27,5 @@ 请为上面 `pages_to_create` 数组中的**每一个页面**生成完整的 markdown 内容。 - 按 system 中规定的 FILE 块格式输出,每个页面一个 FILE 块 - 每个页面的内容必须基于原始材料中与该主题相关的信息 -- 适当使用 [[页面标题]] 链接到相关页面(同批次内其他页面也可链接) +- 适当使用 [[slug]] 链接到相关页面(slug 必须出自上文「已有 Wiki 页面索引」或本批次 `pages_to_create` 中的 slug) - 不要遗漏任何一个页面 diff --git a/mateclaw-server/src/main/resources/prompts/wiki/compile-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/compile-system.txt index 31411329..7830edb1 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/compile-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/compile-system.txt @@ -9,7 +9,7 @@ Strict output contract — return ONLY this JSON object, nothing else: Rules: - Do not invent facts beyond the supplied evidence. If something is not in the evidence, do not assert it. -- Use [[wikilinks]] when an evidence breadcrumb names a related concept; alias form [[slug|display]] is fine. +- Wikilink contract: use [[slug]] or [[slug|display text]]. The slug MUST come from the existing-pages index supplied in the user prompt. Never invent a slug that is not in that index. If the evidence names a concept that has no existing page, write it as plain text — do not guess a slug. - Keep summary concise (single paragraph, <= 300 characters). - content must be Markdown with at least one ## header. - Do not include any prose outside the JSON object. diff --git a/mateclaw-server/src/main/resources/prompts/wiki/create-page-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/create-page-system.txt index d0d48eb9..940637b4 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/create-page-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/create-page-system.txt @@ -5,7 +5,7 @@ - 读懂该页 metadata 描述的主题 - 从原始材料里抽取与该主题相关的信息 - 为这一个页面生成完整的 markdown 内容 -- 在内容里使用 [[页面标题]] 双向链接到其他相关页面(无论是新建中还是已有) +- 在内容里用 [[slug]] 双向链接到其他相关页面 ## 你不做什么 @@ -17,19 +17,23 @@ - 内容开头先一段话摘要(与 metadata 的 summary 一致或更详细) - 使用 Markdown 标题(## / ###)组织 -- 使用 [[页面标题]] 双向链接到其他页面 +- 在正文中用 [[slug]] 链接到其他页面 - 每个不同的子主题用 ### 章节区分 -- 末尾可附"参见"段落,列出相关 [[链接]] +- 末尾可附"参见"段落,列出相关 [[slug]] ## 长度 - 单页内容控制在合理范围(一般 500~2000 字),不要为了凑长度而拖沓 - 内容应有 3 句以上的实质信息 -## 链接 +## 链接(**单一契约,必须严格遵守**) -- 已有页面索引中其他页的 slug 都可用 [[title]] 链接 -- 同批次将创建的其他页面也可以链接(按 metadata 中的 title) +- 只允许两种形态: + - `[[slug]]` —— 显示文本默认为目标页标题 + - `[[slug|显示文本]]` —— 显示文本自定义 +- `slug` **必须**来自上文"已有 Wiki 页面索引"段落中列出的 slug,**禁止发明**索引中不存在的 slug +- 不要写 `[[页面标题]]`、`[[Title]]` 这种形态 —— 系统会按 slug 严格匹配,写标题会被识别为死链 +- 如果某个相关概念没有对应的页面,不要硬塞链接,直接用普通文本描述即可 ## 语言 @@ -42,7 +46,7 @@ { "slug": "page-slug", "title": "页面标题", - "content": "## 标题\n\n摘要段落...\n\n### 详细内容\n...\n\n参见:[[相关页面]]", + "content": "## 标题\n\n摘要段落...\n\n### 详细内容\n...\n\n参见:[[related-slug]]", "summary": "一段话摘要" } diff --git a/mateclaw-server/src/main/resources/prompts/wiki/create-page-user.txt b/mateclaw-server/src/main/resources/prompts/wiki/create-page-user.txt index 94d4da36..6ac2bb97 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/create-page-user.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/create-page-user.txt @@ -2,7 +2,7 @@ {config} -## 已有 Wiki 页面索引(用于建立 [[链接]]) +## 已有 Wiki 页面索引(用于建立 [[slug]] 链接;每行格式 `[[slug]] — 标题 — 摘要`) {existing_pages} @@ -23,4 +23,4 @@ slug:`{page_slug}` 请为上面 metadata 描述的**这一个页面**生成完整的 markdown 内容(按 system 中规定的 JSON 格式输出)。 - 只生成这一个 slug 对应的页面,不要顺便生成其他页面 - 内容必须基于原始材料中与该主题相关的信息 -- 适当使用 [[页面标题]] 链接到其他相关页面 +- 适当使用 [[slug]] 链接到其他相关页面(slug 必须来自上文索引) diff --git a/mateclaw-server/src/main/resources/prompts/wiki/digest-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/digest-system.txt index 8948915c..9b1dbab8 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/digest-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/digest-system.txt @@ -5,7 +5,7 @@ 1. **阅读并理解原始材料** 2. **创建新的 Wiki 页面**:每个页面聚焦一个概念、实体或主题 3. **更新已有 Wiki 页面**:当新材料包含已有页面的相关信息时,合并更新 -4. **建立双向链接**:使用 [[页面标题]] 语法在页面间建立交叉引用 +4. **建立双向链接**:用 [[slug]] 语法在页面间建立交叉引用(slug 必须存在于本批次的 pages 或上下文索引中) ## 页面质量标准 @@ -19,7 +19,8 @@ - 每个页面以一段话摘要开头 - 使用清晰的 Markdown 标题(## 和 ###)组织内容 -- 在提到相关概念时使用 [[链接标题]] 链接到其他页面 +- 引用相关概念时**只能**用 [[slug]] 或 [[slug|显示文本]] 形态;slug 必须是本批次输出中的 slug,或上文索引中已存在的 slug +- 禁止写 [[页面标题]] 这种形态 —— 系统按 slug 严格匹配,写标题会被识别为死链 - 页面标题应简洁准确,反映核心内容 - slug(URL 标识符)使用小写字母、数字和连字符 @@ -45,7 +46,7 @@ { "slug": "concept-name", "title": "概念名称", - "content": "## 概念名称\n\n一段话摘要...\n\n### 详细内容\n...\n\n参见:[[相关主题]]", + "content": "## 概念名称\n\n一段话摘要...\n\n### 详细内容\n...\n\n参见:[[related-concept-slug]]", "summary": "一段话摘要" } ], diff --git a/mateclaw-server/src/main/resources/prompts/wiki/digest-user.txt b/mateclaw-server/src/main/resources/prompts/wiki/digest-user.txt index ea314107..b3d71fb2 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/digest-user.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/digest-user.txt @@ -2,7 +2,7 @@ {config} -## 已有 Wiki 页面索引 +## 已有 Wiki 页面索引(用于建立 [[slug]] 链接;每行格式 `[[slug]] — 标题 — 摘要`) {existing_pages} @@ -17,5 +17,5 @@ 请根据以上原始材料: 1. 根据材料内容的丰富程度,创建合适数量的高质量页面(不追求数量,宁少勿多) 2. 如果已有页面与新材料相关,更新这些页面(不要重复创建已有概念的页面) -3. 确保页面间有充分的 [[双向链接]] +3. 确保页面间有充分的 [[slug]] 双向链接(slug 必须存在于本次输出的 pages 中,或上文索引中) 4. 每个页面聚焦单一主题,内容完整且有实质价值 diff --git a/mateclaw-server/src/main/resources/prompts/wiki/merge-page-system.txt b/mateclaw-server/src/main/resources/prompts/wiki/merge-page-system.txt index e1a0641e..868f077b 100644 --- a/mateclaw-server/src/main/resources/prompts/wiki/merge-page-system.txt +++ b/mateclaw-server/src/main/resources/prompts/wiki/merge-page-system.txt @@ -20,10 +20,12 @@ - 新材料对该页面无新增信息 → 输出原 content 即可(保持不变) - 已有页面 lastUpdatedBy=manual → 仍然合并,但优先保留手动编辑的措辞和结构,仅追加新事实 -## 链接 +## 链接(**单一契约,必须严格遵守**) -- 沿用已有页面里的 [[页面标题]] 双向链接 -- 如果新材料引出了对其他已知概念的引用,新增 [[…]] 链接 +- 只允许两种形态:`[[slug]]` 与 `[[slug|显示文本]]` +- 沿用已有页面里的所有 `[[slug]]` 链接(如果它们指向已存在的页面) +- 如果新材料引出了对其他已知页面的引用,**只能**用上文"已有 Wiki 页面索引"中列出的 slug 来建立新链接 +- 禁止发明索引中不存在的 slug;禁止写 `[[页面标题]]` —— 系统按 slug 严格匹配,写标题会被识别为死链 ## 语言 @@ -36,7 +38,7 @@ { "slug": "existing-slug", "title": "页面标题(可微调)", - "content": "## 标题\n\n摘要...\n\n### 章节...\n\n参见:[[相关]]", + "content": "## 标题\n\n摘要...\n\n### 章节...\n\n参见:[[related-slug]]", "summary": "更新后的一段话摘要" }