mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(wiki): route emits metadata only; per-page create + merge in phase B (RFC-012 M2 v2)
This commit is contained in:
parent
26d45b0e35
commit
62ac1fb79c
@ -392,32 +392,20 @@ public class WikiProcessingService {
|
||||
return 0;
|
||||
}
|
||||
|
||||
String sourceRawIds = "[" + rawId + "]";
|
||||
int created = 0;
|
||||
int updated = 0;
|
||||
|
||||
// 应用 create 列表(直接落库,无需第二轮 LLM)
|
||||
// ─── 收集 route 输出(仅 metadata,无 content) ───
|
||||
List<JsonNode> createMetas = new ArrayList<>();
|
||||
JsonNode createNode = routeJson.path("create");
|
||||
if (createNode.isArray()) {
|
||||
for (JsonNode pageNode : createNode) {
|
||||
String slug = pageNode.path("slug").asText("");
|
||||
String title = pageNode.path("title").asText("");
|
||||
String content = pageNode.path("content").asText("");
|
||||
String summary = pageNode.path("summary").asText("");
|
||||
for (JsonNode metaNode : createNode) {
|
||||
String slug = metaNode.path("slug").asText("");
|
||||
String title = metaNode.path("title").asText("");
|
||||
if (slug.isBlank() || title.isBlank()) continue;
|
||||
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
|
||||
if (existing != null) {
|
||||
// LLM 误把已存在 slug 放进 create —— 走 update 路径兜底
|
||||
pageService.updatePageByAi(kbId, slug, content, summary, rawId);
|
||||
updated++;
|
||||
} else {
|
||||
pageService.createPage(kbId, slug, title, content, summary, sourceRawIds);
|
||||
created++;
|
||||
}
|
||||
createMetas.add(metaNode);
|
||||
}
|
||||
}
|
||||
|
||||
// 收集 update 列表(仅 slug)
|
||||
List<String> updateSlugs = new ArrayList<>();
|
||||
JsonNode updateNode = routeJson.path("update");
|
||||
if (updateNode.isArray()) {
|
||||
@ -427,52 +415,28 @@ public class WikiProcessingService {
|
||||
}
|
||||
}
|
||||
log.info("[Wiki] Route phase: kbId={}, rawId={}, planned create={}, planned update={}",
|
||||
kbId, rawId, createNode.isArray() ? createNode.size() : 0, updateSlugs.size());
|
||||
kbId, rawId, createMetas.size(), updateSlugs.size());
|
||||
|
||||
// ─── 阶段 B:逐页 merge ───
|
||||
if (!updateSlugs.isEmpty()) {
|
||||
String mergeSystem = PromptLoader.loadPrompt("wiki/merge-page-system");
|
||||
String mergeUserTemplate = PromptLoader.loadPrompt("wiki/merge-page-user");
|
||||
for (String slug : updateSlugs) {
|
||||
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
|
||||
if (existing == null) {
|
||||
log.warn("[Wiki] Merge phase: slug '{}' planned for update but not found in DB, skipping", slug);
|
||||
continue;
|
||||
// ─── 阶段 B-1:逐页 create(每页一次单独 LLM call,输入/输出都是单页规模) ───
|
||||
for (JsonNode meta : createMetas) {
|
||||
try {
|
||||
if (createOnePage(kb, raw, textContent, existingPagesIndex, meta)) {
|
||||
created++;
|
||||
}
|
||||
String mergeUser = mergeUserTemplate
|
||||
.replace("{config}", configContent)
|
||||
.replace("{page_slug}", existing.getSlug() != null ? existing.getSlug() : slug)
|
||||
.replace("{page_title}", existing.getTitle() != null ? existing.getTitle() : "")
|
||||
.replace("{page_last_updated_by}", existing.getLastUpdatedBy() != null ? existing.getLastUpdatedBy() : "ai")
|
||||
.replace("{page_content}", existing.getContent() != null ? existing.getContent() : "")
|
||||
.replace("{raw_title}", rawTitle)
|
||||
.replace("{raw_content}", textContent);
|
||||
Prompt mergePrompt = new Prompt(List.of(
|
||||
new SystemMessage(mergeSystem),
|
||||
new UserMessage(mergeUser)
|
||||
));
|
||||
String mergeResponse;
|
||||
try {
|
||||
mergeResponse = callLlmWithResilientRetry(mergePrompt,
|
||||
"merge page slug=" + slug + " of raw=" + rawId);
|
||||
} catch (RuntimeException e) {
|
||||
// 单页 merge 失败不影响其他页面,整 chunk 继续
|
||||
log.warn("[Wiki] Merge phase: slug '{}' failed: {}", slug, e.getMessage());
|
||||
continue;
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("[Wiki] Phase B create page slug='{}' failed: {}",
|
||||
meta.path("slug").asText(""), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 阶段 B-2:逐页 merge(每页一次单独 LLM call) ───
|
||||
for (String slug : updateSlugs) {
|
||||
try {
|
||||
if (mergeOnePage(kb, raw, textContent, slug)) {
|
||||
updated++;
|
||||
}
|
||||
JsonNode mergeJson = parseJsonResponse(mergeResponse);
|
||||
if (mergeJson == null) {
|
||||
log.warn("[Wiki] Merge phase: slug '{}' returned unparseable JSON, skipping", slug);
|
||||
continue;
|
||||
}
|
||||
String content = mergeJson.path("content").asText("");
|
||||
String summary = mergeJson.path("summary").asText("");
|
||||
if (content.isBlank()) {
|
||||
log.warn("[Wiki] Merge phase: slug '{}' returned blank content, skipping", slug);
|
||||
continue;
|
||||
}
|
||||
pageService.updatePageByAi(kbId, slug, content, summary, rawId);
|
||||
updated++;
|
||||
} catch (RuntimeException e) {
|
||||
log.warn("[Wiki] Phase B merge page slug='{}' failed: {}", slug, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@ -481,6 +445,118 @@ public class WikiProcessingService {
|
||||
return created + updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-012 M2 v2 — 阶段 B 单页生成:用 chunk 文本 + 该页 metadata 让 LLM 写出完整页面。
|
||||
* <p>
|
||||
* 输入仅几 KB(chunk 主题片段 + metadata + 已有页索引),输出仅一页 markdown,
|
||||
* 单次调用稳稳 ≤ 60 秒,避免 nginx 60s 网关。
|
||||
* <p>
|
||||
* 兜底:如果 slug 已存在(route 误判),改走 update 路径。
|
||||
*
|
||||
* @return true 表示成功 create 一页(或兜底 update 一页时返回 false 以让上层归到 update 计数)
|
||||
*/
|
||||
private boolean createOnePage(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||
String chunkText, String existingPagesIndex, JsonNode meta) {
|
||||
Long kbId = kb.getId();
|
||||
Long rawId = raw.getId();
|
||||
String slug = meta.path("slug").asText("");
|
||||
String title = meta.path("title").asText("");
|
||||
String summary = meta.path("summary").asText("");
|
||||
if (slug.isBlank() || title.isBlank()) return false;
|
||||
|
||||
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
||||
String createSystem = PromptLoader.loadPrompt("wiki/create-page-system");
|
||||
String createUserTemplate = PromptLoader.loadPrompt("wiki/create-page-user");
|
||||
String createUser = createUserTemplate
|
||||
.replace("{config}", configContent)
|
||||
.replace("{existing_pages}", existingPagesIndex)
|
||||
.replace("{page_slug}", slug)
|
||||
.replace("{page_title}", title)
|
||||
.replace("{page_summary}", summary)
|
||||
.replace("{raw_title}", raw.getTitle())
|
||||
.replace("{raw_content}", chunkText);
|
||||
Prompt prompt = new Prompt(List.of(
|
||||
new SystemMessage(createSystem),
|
||||
new UserMessage(createUser)
|
||||
));
|
||||
String response = callLlmWithResilientRetry(prompt,
|
||||
"create page slug=" + slug + " of raw=" + rawId);
|
||||
JsonNode pageJson = parseJsonResponse(response);
|
||||
if (pageJson == null) {
|
||||
log.warn("[Wiki] Phase B create page slug='{}' returned unparseable JSON, skipping", slug);
|
||||
return false;
|
||||
}
|
||||
String content = pageJson.path("content").asText("");
|
||||
String pageSummary = pageJson.path("summary").asText("");
|
||||
if (pageSummary.isBlank()) pageSummary = summary;
|
||||
if (content.isBlank()) {
|
||||
log.warn("[Wiki] Phase B create page slug='{}' returned blank content, skipping", slug);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 兜底:如果 slug 已存在(route 误判 / 并发),走 update 而不是 create
|
||||
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
|
||||
if (existing != null) {
|
||||
pageService.updatePageByAi(kbId, slug, content, pageSummary, rawId);
|
||||
log.info("[Wiki] Phase B create page slug='{}' done (updated existing)", slug);
|
||||
return false; // 不计入 created
|
||||
}
|
||||
String sourceRawIds = "[" + rawId + "]";
|
||||
pageService.createPage(kbId, slug, title, content, pageSummary, sourceRawIds);
|
||||
log.info("[Wiki] Phase B create page slug='{}' done (created)", slug);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* RFC-012 M2 v2 — 阶段 B 单页 merge:把 chunk 文本合并进一个已有页面。
|
||||
* <p>
|
||||
* 输入仅几 KB(该页现有 content + chunk 主题片段),输出仅一页 markdown。
|
||||
*
|
||||
* @return true 表示成功 update 一页
|
||||
*/
|
||||
private boolean mergeOnePage(WikiKnowledgeBaseEntity kb, WikiRawMaterialEntity raw,
|
||||
String chunkText, String slug) {
|
||||
Long kbId = kb.getId();
|
||||
Long rawId = raw.getId();
|
||||
WikiPageEntity existing = pageService.getBySlug(kbId, slug);
|
||||
if (existing == null) {
|
||||
log.warn("[Wiki] Phase B merge page slug='{}' planned for update but not found in DB, skipping", slug);
|
||||
return false;
|
||||
}
|
||||
|
||||
String configContent = kb.getConfigContent() != null ? kb.getConfigContent() : "";
|
||||
String mergeSystem = PromptLoader.loadPrompt("wiki/merge-page-system");
|
||||
String mergeUserTemplate = PromptLoader.loadPrompt("wiki/merge-page-user");
|
||||
String mergeUser = mergeUserTemplate
|
||||
.replace("{config}", configContent)
|
||||
.replace("{page_slug}", existing.getSlug() != null ? existing.getSlug() : slug)
|
||||
.replace("{page_title}", existing.getTitle() != null ? existing.getTitle() : "")
|
||||
.replace("{page_last_updated_by}", existing.getLastUpdatedBy() != null ? existing.getLastUpdatedBy() : "ai")
|
||||
.replace("{page_content}", existing.getContent() != null ? existing.getContent() : "")
|
||||
.replace("{raw_title}", raw.getTitle())
|
||||
.replace("{raw_content}", chunkText);
|
||||
Prompt prompt = new Prompt(List.of(
|
||||
new SystemMessage(mergeSystem),
|
||||
new UserMessage(mergeUser)
|
||||
));
|
||||
String response = callLlmWithResilientRetry(prompt,
|
||||
"merge page slug=" + slug + " of raw=" + rawId);
|
||||
JsonNode mergeJson = parseJsonResponse(response);
|
||||
if (mergeJson == null) {
|
||||
log.warn("[Wiki] Phase B merge page slug='{}' returned unparseable JSON, skipping", slug);
|
||||
return false;
|
||||
}
|
||||
String content = mergeJson.path("content").asText("");
|
||||
String summary = mergeJson.path("summary").asText("");
|
||||
if (content.isBlank()) {
|
||||
log.warn("[Wiki] Phase B merge page slug='{}' returned blank content, skipping", slug);
|
||||
return false;
|
||||
}
|
||||
pageService.updatePageByAi(kbId, slug, content, summary, rawId);
|
||||
log.info("[Wiki] Phase B merge page slug='{}' done", slug);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 LLM 响应并创建/更新 Wiki 页面
|
||||
*
|
||||
|
||||
@ -0,0 +1,53 @@
|
||||
你是一个知识库 Wiki 单页生成助手。你的唯一任务是:根据原始材料和该页的 metadata,**为这一个页面**生成完整的 markdown 正文。
|
||||
|
||||
## 你做什么
|
||||
|
||||
- 读懂该页 metadata 描述的主题
|
||||
- 从原始材料里抽取与该主题相关的信息
|
||||
- 为这一个页面生成完整的 markdown 内容
|
||||
- 在内容里使用 [[页面标题]] 双向链接到其他相关页面(无论是新建中还是已有)
|
||||
|
||||
## 你不做什么
|
||||
|
||||
- ❌ **不要生成其他页面** —— 你只负责输入中指定的这一个 slug
|
||||
- ❌ **不要输出 markdown 代码块包裹** —— 直接输出 JSON
|
||||
- ❌ **不要写脱离主题的内容** —— 与该页主题无关的信息留给其他页
|
||||
|
||||
## 页面格式规则
|
||||
|
||||
- 内容开头先一段话摘要(与 metadata 的 summary 一致或更详细)
|
||||
- 使用 Markdown 标题(## / ###)组织
|
||||
- 使用 [[页面标题]] 双向链接到其他页面
|
||||
- 每个不同的子主题用 ### 章节区分
|
||||
- 末尾可附"参见"段落,列出相关 [[链接]]
|
||||
|
||||
## 长度
|
||||
|
||||
- 单页内容控制在合理范围(一般 500~2000 字),不要为了凑长度而拖沓
|
||||
- 内容应有 3 句以上的实质信息
|
||||
|
||||
## 链接
|
||||
|
||||
- 已有页面索引中其他页的 slug 都可用 [[title]] 链接
|
||||
- 同批次将创建的其他页面也可以链接(按 metadata 中的 title)
|
||||
|
||||
## 语言
|
||||
|
||||
- 跟随原始材料的语言
|
||||
|
||||
## 输出格式
|
||||
|
||||
严格输出 JSON,不要 markdown 代码块包裹:
|
||||
|
||||
{
|
||||
"slug": "page-slug",
|
||||
"title": "页面标题",
|
||||
"content": "## 标题\n\n摘要段落...\n\n### 详细内容\n...\n\n参见:[[相关页面]]",
|
||||
"summary": "一段话摘要"
|
||||
}
|
||||
|
||||
字段说明:
|
||||
- `slug`:保持与输入 metadata 一致
|
||||
- `title`:通常与 metadata 一致;如有更精确的描述可微调
|
||||
- `content`:完整 markdown,开头一段摘要,后面分章节
|
||||
- `summary`:一段话简短摘要(可与 metadata.summary 一致或精炼)
|
||||
@ -0,0 +1,26 @@
|
||||
## 知识库处理规则
|
||||
|
||||
{config}
|
||||
|
||||
## 已有 Wiki 页面索引(用于建立 [[链接]])
|
||||
|
||||
{existing_pages}
|
||||
|
||||
## 待生成页面的 metadata
|
||||
|
||||
slug:`{page_slug}`
|
||||
标题:{page_title}
|
||||
摘要:{page_summary}
|
||||
|
||||
## 原始材料
|
||||
|
||||
标题:{raw_title}
|
||||
|
||||
{raw_content}
|
||||
|
||||
---
|
||||
|
||||
请为上面 metadata 描述的**这一个页面**生成完整的 markdown 内容(按 system 中规定的 JSON 格式输出)。
|
||||
- 只生成这一个 slug 对应的页面,不要顺便生成其他页面
|
||||
- 内容必须基于原始材料中与该主题相关的信息
|
||||
- 适当使用 [[页面标题]] 链接到其他相关页面
|
||||
@ -1,31 +1,31 @@
|
||||
你是一个知识库 Wiki 路由助手。你的唯一任务是:阅读一段原始材料,决定**哪些新页面需要创建**、**哪些已有页面需要合并更新**。
|
||||
你是一个知识库 Wiki 路由助手。你的唯一任务是:阅读一段原始材料,决定**哪些新页面需要创建**、**哪些已有页面需要合并更新**。**只输出 metadata,不要输出任何页面正文。**
|
||||
|
||||
## 你做什么
|
||||
|
||||
1. **阅读原始材料**,识别其中包含的概念、实体、流程
|
||||
2. **比对已有页面索引**(仅含 slug + title + summary,不含正文):
|
||||
- 材料中出现的概念**已经被某个已有页面充分覆盖** → 放入 `update` 列表(仅写 slug,**不要**写正文)
|
||||
- 材料中出现的概念**没有任何已有页面覆盖** → 放入 `create` 列表,**给出完整页面正文**
|
||||
- 材料中出现的概念**已经被某个已有页面充分覆盖** → 放入 `update` 列表(只写 slug)
|
||||
- 材料中出现的概念**没有任何已有页面覆盖** → 放入 `create` 列表(只给 slug + title + summary,**不要正文**)
|
||||
3. 不创建浅薄页面(< 3 句实质内容的概念跳过,不进任何列表)
|
||||
|
||||
## 你不做什么
|
||||
|
||||
- **不要重写或合并已有页面的内容** —— update 列表只写 slug,正文合并由后续步骤完成
|
||||
- **不要为已存在概念在 create 中复制一份**
|
||||
- 不要输出 markdown 代码块包裹
|
||||
- ❌ **绝不输出 content 字段** —— 正文由后续的"单页生成助手"独立生成,你只决定结构
|
||||
- ❌ **不要为已存在概念在 create 中复制一份** —— 已存在的就放 update
|
||||
- ❌ **不要重写或合并已有页面的内容** —— 那是后续阶段
|
||||
- ❌ 不要输出 markdown 代码块包裹
|
||||
|
||||
## 创建新页面(仅 `create` 列表使用)的格式规则
|
||||
## metadata 格式(仅 `create` 数组使用)
|
||||
|
||||
- 使用 Markdown 标题(## / ###)组织
|
||||
- 使用 [[页面标题]] 双向链接到其他页面(无论是新页面还是已有页面)
|
||||
- slug 用小写字母 + 连字符
|
||||
- summary 一段话简短摘要
|
||||
- content 完整 markdown 正文,开头先一段摘要
|
||||
每条 metadata 包含三个字段:
|
||||
- `slug`:URL 安全的标识符(小写字母 + 连字符)
|
||||
- `title`:人类可读的页面标题
|
||||
- `summary`:一段话简短摘要(一两句话即可,让"单页生成助手"知道这一页要写什么)
|
||||
|
||||
## 语言
|
||||
|
||||
- 跟随原始材料的语言(中文材料 → 中文输出)
|
||||
- 术语保持一致
|
||||
- 跟随原始材料的语言(中文材料 → 中文 metadata)
|
||||
- 术语保持与已有页面一致
|
||||
|
||||
## 输出格式
|
||||
|
||||
@ -36,8 +36,7 @@
|
||||
{
|
||||
"slug": "concept-name",
|
||||
"title": "概念名称",
|
||||
"content": "## 概念名称\n\n摘要段落...\n\n### 详细内容\n...\n\n参见:[[相关主题]]",
|
||||
"summary": "一段话摘要"
|
||||
"summary": "一两句话说明这页要讲什么"
|
||||
}
|
||||
],
|
||||
"update": [
|
||||
@ -47,6 +46,11 @@
|
||||
}
|
||||
|
||||
字段说明:
|
||||
- `create`:完全新增的页面,给出完整内容
|
||||
- `update`:已存在但需要根据新材料合并更新的页面,**只列 slug 字符串数组**
|
||||
- 两个数组都可以为空(材料完全无价值时全空,材料只更新已有页时 create 为空)
|
||||
- `create`:完全新增的页面,**只给 metadata,不给 content**
|
||||
- `update`:已存在但需要根据本材料合并更新的页面,**只列 slug 字符串数组**
|
||||
- 两个数组都可以为空(材料完全无价值时全空,材料只更新已有页时 create 为空,材料只新增页时 update 为空)
|
||||
|
||||
## 关键纪律
|
||||
|
||||
- 输出体积应该是几百到几千字,**不要超过几 KB**。如果你发现自己在写正文,立刻停下 —— 那是下一阶段的工作。
|
||||
- 即使概念主题非常丰富,也只列入 metadata,让后续每页 LLM 调用单独展开。
|
||||
|
||||
@ -14,8 +14,8 @@
|
||||
|
||||
---
|
||||
|
||||
请按 system 中规定的 JSON 格式输出:
|
||||
- `create`:完全没有对应页面的新概念(含完整内容)
|
||||
- `update`:已有页面但需根据本材料合并更新的(**仅 slug 列表,不要正文**)
|
||||
请按 system 中规定的 JSON 格式输出**只含 metadata**的路由结果:
|
||||
- `create`:完全没有对应页面的新概念,**只给 slug + title + summary,不要写 content**
|
||||
- `update`:已有页面但需根据本材料合并更新的,**仅 slug 字符串数组**
|
||||
|
||||
不要试图自己合并 update 列表里页面的内容 —— 那是下一阶段独立完成的工作。
|
||||
不要试图自己生成或合并页面正文 —— 那些都由独立的下阶段 LLM 调用完成。你的输出应该控制在几 KB 以内。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user