feat(wiki): inject pageType profile into route/create/merge prompts + content templates

This commit is contained in:
matevip 2026-05-31 07:59:13 +08:00
parent 551321b542
commit 8e8c1e34c7
7 changed files with 134 additions and 6 deletions

View File

@ -90,6 +90,56 @@ public class WikiPageTypeProfileService {
return resolveProfile(kbId).getPageTypes().keySet();
}
/**
* The per-stage LLM instruction declared for a pageType, or empty string.
* {@code stage} is {@code route} / {@code create} / {@code merge}. Used to
* inject type-specific guidance into the corresponding stage prompt.
*/
public String stageInstruction(Long kbId, String pageType, String stage) {
WikiPageTypeDef def = resolveProfile(kbId).get(pageType);
if (def == null || stage == null) {
return "";
}
WikiPageTypeDef.StageInstructions si = switch (stage) {
case "route" -> def.getRoute();
case "create" -> def.getCreate();
case "merge" -> def.getMerge();
default -> null;
};
return (si != null && si.getInstructions() != null) ? si.getInstructions().trim() : "";
}
/**
* Render the per-type Markdown templates as a prompt block for the
* multi-page batch-create stage (where one call generates pages of several
* types). Only types that declare a template are listed; empty when none.
*/
public String describeTemplatesForPrompt(Long kbId) {
WikiPageTypeProfile profile = resolveProfile(kbId);
StringBuilder sb = new StringBuilder();
profile.getPageTypes().forEach((name, def) -> {
if (def != null && def.getTemplate() != null
&& def.getTemplate().getMarkdown() != null
&& !def.getTemplate().getMarkdown().isBlank()) {
sb.append("### ").append(name).append(" 骨架\n")
.append(def.getTemplate().getMarkdown().trim()).append("\n\n");
}
});
return sb.toString().trim();
}
/**
* The Markdown content template for a pageType, or empty string. Injected
* into the generation prompt so the page follows the declared skeleton.
*/
public String templateMarkdown(Long kbId, String pageType) {
WikiPageTypeDef def = resolveProfile(kbId).get(pageType);
if (def == null || def.getTemplate() == null || def.getTemplate().getMarkdown() == null) {
return "";
}
return def.getTemplate().getMarkdown().trim();
}
/**
* Render the KB's allowed page types as a prompt fragment, one per line
* with description and required-metadata hints, e.g.

View File

@ -813,7 +813,8 @@ public class WikiProcessingService {
// this picks up changes from sequential chunks without an extra DB hit when nothing changed.
String freshIndex = buildExistingPagesIndex(kbId);
String routeSystem = PromptLoader.loadPrompt("wiki/route-system");
String routeSystem = PromptLoader.loadPrompt("wiki/route-system")
.replace("{allowed_page_types}", allowedTypesFragment(kbId));
String routeUserTemplate = PromptLoader.loadPrompt("wiki/route-user");
String documentMapSection = buildDocumentMapSection(documentMap);
String routeUser = routeUserTemplate
@ -1082,10 +1083,13 @@ public class WikiProcessingService {
// the profile recognises. Default-profile KBs get the same list
// as the previous hardcoded enum, so behaviour is unchanged.
batchSystem = batchSystem.replace("{allowed_page_types}",
pageTypeProfileService.describeForPrompt(kbId));
pageTypeProfileService.describeForPrompt(kbId))
.replace("{page_type_templates}",
emptyOr(pageTypeProfileService.describeTemplatesForPrompt(kbId)));
} else {
batchSystem = batchSystem.replace("{allowed_page_types}",
"concept / person / place / event / technology / organization / product / term / process / other");
"concept / person / place / event / technology / organization / product / term / process / other")
.replace("{page_type_templates}", "(无)");
}
String batchUserTemplate = PromptLoader.loadPrompt("wiki/batch-create-user");
String docMapSection = buildDocumentMapSection(documentMap);
@ -1264,7 +1268,9 @@ public class WikiProcessingService {
String title = pageMeta.path("title").asText("");
String summary = pageMeta.path("summary").asText("");
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
String createSystem = PromptLoader.loadPrompt("wiki/create-page-system");
String createSystem = PromptLoader.loadPrompt("wiki/create-page-system")
.replace("{page_type_instructions}",
typeGuidance(kb.getId(), pageMeta.path("page_type").asText(""), "create"));
String createUserTemplate = PromptLoader.loadPrompt("wiki/create-page-user");
String createUser = createUserTemplate
.replace("{config}", configContent)
@ -1492,6 +1498,38 @@ public class WikiProcessingService {
}
}
private String emptyOr(String s) {
return (s == null || s.isBlank()) ? "(无)" : s;
}
/** Allowed page types fragment for prompt injection (profile-driven; legacy fallback). */
private String allowedTypesFragment(Long kbId) {
return pageTypeProfileService != null
? pageTypeProfileService.describeForPrompt(kbId)
: "concept / person / place / event / technology / organization / product / term / process / other";
}
/**
* Per-type guidance for the create / merge prompts: the stage instruction
* plus, for the create stage, the Markdown template skeleton. Empty-safe.
*/
private String typeGuidance(Long kbId, String pageType, String stage) {
if (pageTypeProfileService == null || pageType == null || pageType.isBlank()) {
return "(无特定指引)";
}
String instr = pageTypeProfileService.stageInstruction(kbId, pageType, stage);
String tpl = "create".equals(stage) ? pageTypeProfileService.templateMarkdown(kbId, pageType) : "";
StringBuilder sb = new StringBuilder();
if (instr != null && !instr.isBlank()) {
sb.append(instr);
}
if (tpl != null && !tpl.isBlank()) {
if (sb.length() > 0) sb.append("\n\n");
sb.append("请按以下 Markdown 骨架组织正文:\n").append(tpl);
}
return sb.length() == 0 ? "(无特定指引)" : sb.toString();
}
private void applyValidatedMetadata(Long pageId, Long kbId, String pageType,
JsonNode metadataNode) {
if (pageId == null || pageTypeProfileService == null || metadataValidator == null) {
@ -1546,7 +1584,9 @@ public class WikiProcessingService {
}
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
String mergeSystem = PromptLoader.loadPrompt("wiki/merge-page-system");
String mergeSystem = PromptLoader.loadPrompt("wiki/merge-page-system")
.replace("{page_type_merge_instruction}",
typeGuidance(kbId, existing.getPageType(), "merge"));
// Trim existing content to prevent context overflow on small models (qwen-turbo: 4096 tokens).
// Merging a 3000-char page + 30K chunk blows past the limit truncated JSON parse failure.
// 1800 chars ~600 tokens, leaving ample room for the chunk and response.
@ -2348,7 +2388,9 @@ public class WikiProcessingService {
// Use existing two-phase single-page create logic
String existingPagesIndex = buildExistingPagesIndex(kb.getId());
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
String createSystem = PromptLoader.loadPrompt("wiki/create-page-system");
String createSystem = PromptLoader.loadPrompt("wiki/create-page-system")
.replace("{page_type_instructions}",
typeGuidance(kb.getId(), page.getPageType(), "create"));
String createUserTemplate = PromptLoader.loadPrompt("wiki/create-page-user");
String createUser = createUserTemplate
.replace("{config}", configContent)

View File

@ -55,3 +55,6 @@
允许的页面类型:
{allowed_page_types}
各类型内容模板(若某页的 page_type 有对应骨架,请按骨架组织正文;无则自由组织)
{page_type_templates}

View File

@ -55,3 +55,6 @@
- `title`:通常与 metadata 一致;如有更精确的描述可微调
- `content`:完整 markdown开头一段摘要后面分章节
- `summary`:一段话简短摘要(可与 metadata.summary 一致或精炼)
## 内容指引(本知识库 / 该页类型)
{page_type_instructions}

View File

@ -47,3 +47,6 @@
- `title`:通常保持不变;只有当新材料明确改变了页面主题时才改
- `content`:完整 markdown包含原内容中仍然有效的部分 + 来自新材料的新增信息
- `summary`:更新后的一段话摘要
## 该类型的合并策略(本知识库)
{page_type_merge_instruction}

View File

@ -68,6 +68,12 @@
- `update`:已存在但需要根据本材料合并更新的页面,**只列 slug 字符串数组,最多 5 条**
- 两个数组合计通常至少 1 条(材料完全无价值——如空白页、纯目录——才允许全空)
## 允许的页面类型(本知识库)
为每个 create 项判断最贴切的类型;不确定时按默认处理。可在 create 项加 `page_type` 字段。
{allowed_page_types}
## 关键纪律
- 输出体积应该是几百到几千字,**不要超过几 KB**。如果你发现自己在写正文,立刻停下 —— 那是下一阶段的工作。

View File

@ -89,6 +89,27 @@ class WikiPageTypeProfileServiceTest {
assertTrue(fragment.contains("- other"), fragment);
}
@Test
void stageInstructionAndTemplate_areResolvedPerType() {
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();
row.setKbId(1L);
row.setEnabled(1);
row.setConfigJson("{\"pageTypes\":{\"episode\":{"
+ "\"route\":{\"instructions\":\"仅当有明确日期时路由为 episode\"},"
+ "\"merge\":{\"instructions\":\"保留已审阅分析,追加新证据\"},"
+ "\"create\":{\"instructions\":\"抽取 event_date\"},"
+ "\"template\":{\"markdown\":\"## Summary\\n{{summary}}\\n## 事件\"}}}}");
when(mapper.selectOne(any())).thenReturn(row);
assertTrue(service.stageInstruction(1L, "episode", "route").contains("明确日期"));
assertTrue(service.stageInstruction(1L, "episode", "merge").contains("已审阅"));
assertTrue(service.stageInstruction(1L, "episode", "create").contains("event_date"));
assertTrue(service.templateMarkdown(1L, "episode").contains("## Summary"));
// unknown type / stage empty, never null
assertEquals("", service.stageInstruction(1L, "nope", "route"));
assertEquals("", service.templateMarkdown(1L, "nope"));
}
@Test
void describeForPrompt_customProfile_showsRequiredMetadata() {
WikiPageTypeProfileEntity row = new WikiPageTypeProfileEntity();