效果优化
This commit is contained in:
1 parent
2402a84c71
commit
ab85e4df72
15 files changed
+548
-35
No files matched your search
@@ -42,6 +42,8 @@ class StartWritingRequest(BaseModel):
|
||||
model_name: str = ""
|
||||
# 素材检索关键词数量:用户指定用几个关键词检索(1~10,默认 6)
|
||||
keyword_count: int = 6
|
||||
# 写作模式:strict=严格依据素材 / loose=可结合通用知识(默认 loose)
|
||||
writing_mode: str = "loose"
|
||||
|
||||
|
||||
class StartWritingResponse(BaseModel):
|
||||
@@ -59,6 +61,9 @@ class ResumeRequest(BaseModel):
|
||||
user_revision_notes: str = ""
|
||||
override_verdict: bool = False
|
||||
selected_issue_indices: Optional[list[int]] = None
|
||||
# 素材不足求助点:每个被阻塞章节的处理决策
|
||||
# [{"sectionTitle": "...", "decision": "supplement|delete|loose"}]
|
||||
section_decisions: list[dict] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RenameSessionRequest(BaseModel):
|
||||
@@ -234,6 +239,8 @@ async def start_writing(body: StartWritingRequest, request: Request):
|
||||
"word_count_target": body.word_count_target,
|
||||
# 规整到 [1, 10],非法值回退默认 6
|
||||
"keyword_count": max(1, min(10, body.keyword_count)),
|
||||
# 写作模式,非法值回退 loose
|
||||
"writing_mode": body.writing_mode if body.writing_mode in ("strict", "loose") else "loose",
|
||||
"revision_count": 0,
|
||||
"max_revisions": 3,
|
||||
"status": "researching",
|
||||
@@ -425,6 +432,7 @@ async def resume_writing(session_id: str, body: ResumeRequest, request: Request)
|
||||
"userRevisionNotes": body.user_revision_notes,
|
||||
"overrideVerdict": body.override_verdict,
|
||||
"selectedIssueIndices": body.selected_issue_indices,
|
||||
"sectionDecisions": body.section_decisions,
|
||||
}
|
||||
|
||||
# Record this intervention for session history
|
||||
|
||||
+117
-7
@@ -61,6 +61,26 @@ def _normalize_outline(outline: dict) -> dict:
|
||||
return {"title": outline.get("title", ""), "sections": sections}
|
||||
|
||||
|
||||
def _check_outline_material_support(outline: dict) -> str:
|
||||
"""轻量启发式:统计未关联任何素材的章节,返回预警文案(空串=无需预警)。
|
||||
|
||||
仅做提示、不拦截流程;真正的素材不足会在写作节点被检出并触发求助暂停。
|
||||
"""
|
||||
sections = (outline or {}).get("sections", [])
|
||||
weak = [
|
||||
(s.get("section_title") or s.get("sectionTitle") or "")
|
||||
for s in sections
|
||||
if not (s.get("material_ids") or s.get("materialIds"))
|
||||
]
|
||||
weak = [t for t in weak if t]
|
||||
if not weak:
|
||||
return ""
|
||||
return (
|
||||
f"有 {len(weak)} 个章节未关联检索素材({('、'.join(weak))})。"
|
||||
"严格依据素材模式下,这些章节在写作时可能因素材不足而暂停向你求助。"
|
||||
)
|
||||
|
||||
|
||||
async def pause_outline_confirm(state: AIWritingState) -> dict:
|
||||
"""PAUSE-2:大纲确认"""
|
||||
user_input: dict = interrupt({
|
||||
@@ -74,11 +94,20 @@ async def pause_outline_confirm(state: AIWritingState) -> dict:
|
||||
if action == "confirm":
|
||||
raw_outline = user_input.get("editedOutline") or state["current_outline"]
|
||||
edited = _normalize_outline(raw_outline) if raw_outline else state["current_outline"]
|
||||
events: list[dict] = [{"type": "resumed", "agent_name": "系统", "message": "用户已确认大纲,开始写作"}]
|
||||
# 阶段四:严格模式下对大纲做轻量素材充分性预警(仅提示,不拦截流程)
|
||||
if state.get("writing_mode") == "strict":
|
||||
warning = _check_outline_material_support(edited)
|
||||
if warning:
|
||||
events.append({
|
||||
"type": "notice", "agent_name": "系统",
|
||||
"title": "素材充分性提示", "detail": warning,
|
||||
})
|
||||
return {
|
||||
"current_outline": edited,
|
||||
"last_user_intervention": user_input,
|
||||
"status": "writing_draft",
|
||||
"progress_events": [{"type": "resumed", "agent_name": "系统", "message": "用户已确认大纲,开始写作"}],
|
||||
"progress_events": events,
|
||||
}
|
||||
|
||||
# re_outline
|
||||
@@ -127,6 +156,65 @@ async def pause_draft_confirm(state: AIWritingState) -> dict:
|
||||
}
|
||||
|
||||
|
||||
async def pause_section_help(state: AIWritingState) -> dict:
|
||||
"""素材不足求助点:严格模式下作家发现章节缺素材,让用户逐章决定如何处理。
|
||||
|
||||
用户对每个被阻塞章节选择:
|
||||
- supplement:补充检索素材(整体回到检索流程重走)
|
||||
- delete:删除该章节
|
||||
- loose:按通用知识续写该章节
|
||||
"""
|
||||
blocked = state.get("blocked_sections") or []
|
||||
user_input: dict = interrupt({
|
||||
"pause_point": "section_help",
|
||||
"message": "部分章节缺少足够素材支撑写作,请决定如何处理。",
|
||||
"blocked_sections": blocked,
|
||||
})
|
||||
|
||||
# sectionDecisions: [{"sectionTitle": "...", "decision": "supplement|delete|loose"}]
|
||||
by_title: dict = {}
|
||||
for d in user_input.get("sectionDecisions") or []:
|
||||
title = d.get("sectionTitle") or d.get("section_title")
|
||||
if title:
|
||||
by_title[title] = d.get("decision", "loose")
|
||||
|
||||
# 任一章节选「补充检索」→ 整体回到检索,重走「检索 → 素材确认 → 大纲 → 写作」
|
||||
supplement_reasons = [
|
||||
b["reason"] for b in blocked if by_title.get(b["section_title"]) == "supplement"
|
||||
]
|
||||
if supplement_reasons:
|
||||
return {
|
||||
# supplement_search:补充检索(保留已有素材、追加新素材),完成后直接回写作、不重排大纲
|
||||
"last_user_intervention": {**user_input, "action": "supplement_search", "_extra_keywords": supplement_reasons},
|
||||
"status": "researching",
|
||||
"blocked_sections": None,
|
||||
"progress_events": [{"type": "resumed", "agent_name": "系统", "message": "补充检索素材中…"}],
|
||||
}
|
||||
|
||||
# 否则:删除标记章节、记录「通用知识续写」章节,直接回写作
|
||||
outline = state.get("current_outline") or {"title": "", "sections": []}
|
||||
kept, loose_titles = [], []
|
||||
for s in outline.get("sections", []):
|
||||
title = s.get("section_title") or s.get("sectionTitle", "")
|
||||
decision = by_title.get(title)
|
||||
if decision == "delete":
|
||||
continue
|
||||
if decision == "loose":
|
||||
loose_titles.append(title)
|
||||
kept.append(s)
|
||||
# 防御:用户把所有章节都删了 → 保留原大纲并全部按通用知识写,避免空文章
|
||||
if not kept:
|
||||
kept = outline.get("sections", [])
|
||||
loose_titles = [s.get("section_title") or s.get("sectionTitle", "") for s in kept]
|
||||
return {
|
||||
"current_outline": {**outline, "sections": kept},
|
||||
"last_user_intervention": {**user_input, "_loose_sections": loose_titles},
|
||||
"status": "writing_draft",
|
||||
"blocked_sections": None,
|
||||
"progress_events": [{"type": "resumed", "agent_name": "系统", "message": "已按你的选择继续写作…"}],
|
||||
}
|
||||
|
||||
|
||||
async def pause_review_confirm(state: AIWritingState) -> dict:
|
||||
"""PAUSE-4:编辑打回确认"""
|
||||
user_input: dict = interrupt({
|
||||
@@ -156,6 +244,11 @@ async def pause_review_confirm(state: AIWritingState) -> dict:
|
||||
|
||||
# ── 条件路由 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def after_researcher(state: AIWritingState) -> str:
|
||||
"""检索完成后:补充检索(素材不足求助)直接回写作,否则去素材确认。"""
|
||||
return "writer_draft" if state.get("status") == "writing_draft" else "pause_material"
|
||||
|
||||
|
||||
def after_material_pause(state: AIWritingState) -> str:
|
||||
return "researcher" if state["status"] == "researching" else "writer_outline"
|
||||
|
||||
@@ -164,6 +257,16 @@ def after_outline_pause(state: AIWritingState) -> str:
|
||||
return "writer_outline" if state["status"] == "writing_outline" else "writer_draft"
|
||||
|
||||
|
||||
def after_writer_draft(state: AIWritingState) -> str:
|
||||
"""写作完成后:素材不足则去求助暂停点,否则去草稿确认。"""
|
||||
return "pause_section_help" if state["status"] == "awaiting_section_help" else "pause_draft"
|
||||
|
||||
|
||||
def after_section_help_pause(state: AIWritingState) -> str:
|
||||
"""求助暂停点之后:补充检索回 researcher,否则回写作。"""
|
||||
return "researcher" if state["status"] == "researching" else "writer_draft"
|
||||
|
||||
|
||||
def after_draft_pause(state: AIWritingState) -> str:
|
||||
status = state["status"]
|
||||
if status == "done":
|
||||
@@ -198,14 +301,16 @@ def build_ai_writing_graph():
|
||||
g.add_node("pause_material", pause_material_confirm)
|
||||
g.add_node("writer_outline", writer_outline_node)
|
||||
g.add_node("pause_outline", pause_outline_confirm)
|
||||
g.add_node("writer_draft", writer_draft_node)
|
||||
g.add_node("pause_draft", pause_draft_confirm)
|
||||
g.add_node("editor", editor_node)
|
||||
g.add_node("pause_review", pause_review_confirm)
|
||||
g.add_node("writer_draft", writer_draft_node)
|
||||
g.add_node("pause_section_help", pause_section_help)
|
||||
g.add_node("pause_draft", pause_draft_confirm)
|
||||
g.add_node("editor", editor_node)
|
||||
g.add_node("pause_review", pause_review_confirm)
|
||||
|
||||
g.set_entry_point("intent_parser")
|
||||
g.add_edge("intent_parser", "researcher")
|
||||
g.add_edge("researcher", "pause_material")
|
||||
g.add_conditional_edges("researcher", after_researcher,
|
||||
{"pause_material": "pause_material", "writer_draft": "writer_draft"})
|
||||
|
||||
g.add_conditional_edges("pause_material", after_material_pause,
|
||||
{"researcher": "researcher", "writer_outline": "writer_outline"})
|
||||
@@ -214,7 +319,12 @@ def build_ai_writing_graph():
|
||||
g.add_conditional_edges("pause_outline", after_outline_pause,
|
||||
{"writer_outline": "writer_outline", "writer_draft": "writer_draft"})
|
||||
|
||||
g.add_edge("writer_draft", "pause_draft")
|
||||
g.add_conditional_edges("writer_draft", after_writer_draft,
|
||||
{"pause_section_help": "pause_section_help", "pause_draft": "pause_draft"})
|
||||
|
||||
g.add_conditional_edges("pause_section_help", after_section_help_pause,
|
||||
{"researcher": "researcher", "writer_draft": "writer_draft"})
|
||||
|
||||
g.add_conditional_edges("pause_draft", after_draft_pause,
|
||||
{"end": END, "writer_draft": "writer_draft", "editor": "editor"})
|
||||
|
||||
|
||||
+27
-2
@@ -3,6 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from typing import List
|
||||
|
||||
@@ -46,6 +47,25 @@ def _resolve_max_materials(raw: object) -> int:
|
||||
return max(_MIN_MAX_MATERIALS, min(_MAX_MAX_MATERIALS, n))
|
||||
|
||||
|
||||
# 占位/示意性关键词(模型误抄提示词模板时产生),检索前剔除
|
||||
_PLACEHOLDER_KEYWORD_RE = re.compile(r"^(关键词\d*|keyword\d*|kw\d*|\.+|…+)$", re.IGNORECASE)
|
||||
|
||||
|
||||
def _clean_keywords(raw: object, fallback: str) -> list[str]:
|
||||
"""清洗 LLM 产出的关键词:去空白、丢弃占位符(如「关键词1」「...」)、去重。
|
||||
|
||||
全部被滤掉时回退为写作意图本身,避免拿占位词检索出一堆无关素材。
|
||||
"""
|
||||
cleaned: list[str] = []
|
||||
for kw in raw if isinstance(raw, list) else []:
|
||||
s = str(kw).strip()
|
||||
if not s or _PLACEHOLDER_KEYWORD_RE.match(s):
|
||||
continue
|
||||
cleaned.append(s)
|
||||
deduped = list(dict.fromkeys(cleaned))
|
||||
return deduped or [fallback]
|
||||
|
||||
|
||||
def _per_keyword_limit(max_materials: int, total_keywords: int) -> int:
|
||||
"""把素材总量上限均分到每个检索词上的检索条数(向上取整)。
|
||||
|
||||
@@ -77,6 +97,9 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
||||
intervention = state.get("last_user_intervention") or {}
|
||||
extra_keywords: list[str] = intervention.get("_extra_keywords") or intervention.get("extra_keywords") or []
|
||||
|
||||
# 素材不足求助点触发的「补充检索」:保留原大纲,检索完直接回写作、不走素材确认/大纲
|
||||
is_supplement = intervention.get("action") == "supplement_search"
|
||||
|
||||
# 用户指定的检索关键词数量(1~10),决定生成多少个检索词、检索多少个方向
|
||||
keyword_count = _resolve_keyword_count(state.get("keyword_count"))
|
||||
|
||||
@@ -115,7 +138,8 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
||||
config=config,
|
||||
model_name=keyword_model,
|
||||
)
|
||||
keywords: list[str] = kw_result.get("keywords", [intent])
|
||||
# 清洗关键词:剔除占位符 / 空值,全空时回退写作意图
|
||||
keywords: list[str] = _clean_keywords(kw_result.get("keywords"), intent)
|
||||
if extra_keywords:
|
||||
keywords = list(dict.fromkeys(keywords + extra_keywords))
|
||||
|
||||
@@ -237,7 +261,8 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
||||
|
||||
return {
|
||||
"material_package": pkg,
|
||||
"status": "awaiting_material_confirm",
|
||||
# 补充检索:直接回写作(沿用原大纲);常规检索:进入素材确认暂停点
|
||||
"status": "writing_draft" if is_supplement else "awaiting_material_confirm",
|
||||
"progress_events": [
|
||||
{"type": "step_start", "agent_name": "素材收集专家", "message": f"搜索关键词:{', '.join(keywords[:3])}"},
|
||||
*notices,
|
||||
|
||||
+87
-3
@@ -3,16 +3,53 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
|
||||
from langchain_core.messages import HumanMessage, SystemMessage
|
||||
from langchain_core.runnables import RunnableConfig
|
||||
|
||||
from ..state import AIWritingState, DraftArticle
|
||||
from ..prompts.writer_prompts import DRAFT_SECTION_SYSTEM, DRAFT_SECTION_USER, DRAFT_EXPAND_USER
|
||||
from ..prompts.writer_prompts import (
|
||||
DRAFT_SECTION_SYSTEM, DRAFT_SECTION_USER, DRAFT_EXPAND_USER,
|
||||
WRITER_MODE_LOOSE, WRITER_MODE_STRICT, SECTION_BLOCKED_MARKER,
|
||||
)
|
||||
from ._utils import get_model, _content_to_text, count_words, ThinkTagParser
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 章节正文里的 Markdown 标题行(允许前导空白)
|
||||
_HEADING_PREFIX_RE = re.compile(r"^[ \t]*#{1,6}[ \t]+", re.MULTILINE)
|
||||
|
||||
|
||||
def _normalize_section_headings(text: str) -> str:
|
||||
"""把章节正文里所有 Markdown 标题行统一压成三级标题(###)。
|
||||
|
||||
作家被要求只用 ### 作为「要点」小标题;模型偶尔误用 #/##/#### 时在此纠正,
|
||||
保证文档标题层级符合「文章 # → 章节 ## → 要点 ###」,
|
||||
前端目录栏据此才能把要点正确识别为二级标题。
|
||||
"""
|
||||
return _HEADING_PREFIX_RE.sub("### ", text)
|
||||
|
||||
|
||||
# 疑似「拒绝 / 元话语」的强特征短语:模型把对用户说的话当成正文输出时出现,
|
||||
# 这些短语几乎不可能出现在正常文章里,用作安全网识别。
|
||||
_REFUSAL_MARKERS = (
|
||||
"我无法完成", "无法完成此任务", "无法完成该任务", "无法完成这个任务",
|
||||
"无法完成本次", "无法写出符合", "无法撰写这篇", "无法基于现有素材",
|
||||
"无法基于当前素材", "无法基于提供的素材",
|
||||
)
|
||||
|
||||
|
||||
def _looks_like_refusal(text: str) -> bool:
|
||||
"""判断一段章节产出是否疑似「拒绝 / 元话语」而非真正的文章正文。
|
||||
|
||||
模型遇到素材与意图不匹配时,可能把「我无法完成此任务」之类对用户说的话
|
||||
当成正文输出。这里用强特征短语做启发式识别(安全网,非强校验)。
|
||||
"""
|
||||
if not text or not text.strip():
|
||||
return False
|
||||
return any(marker in text for marker in _REFUSAL_MARKERS)
|
||||
|
||||
|
||||
def _get_section_materials(material_ids: list[str], pkg: dict) -> str:
|
||||
mat_map = {m["id"]: m for m in pkg.get("materials", [])}
|
||||
@@ -39,6 +76,10 @@ async def writer_draft_node(state: AIWritingState, config: RunnableConfig) -> di
|
||||
intervention = state.get("last_user_intervention") or {}
|
||||
revision_count = state.get("revision_count", 0)
|
||||
|
||||
# 写作模式;用户在「素材不足求助」里选「按通用知识续写」的章节强制走宽松
|
||||
writing_mode = state.get("writing_mode", "loose")
|
||||
loose_titles: set[str] = set(intervention.get("_loose_sections") or [])
|
||||
|
||||
# SSE 流式推送所需
|
||||
cfg = config.get("configurable", {})
|
||||
sse_queue: asyncio.Queue | None = cfg.get("sse_queue")
|
||||
@@ -89,6 +130,7 @@ async def writer_draft_node(state: AIWritingState, config: RunnableConfig) -> di
|
||||
section_titles: list[str] = []
|
||||
section_bufs: list[str] = [] # 各章节正文(不含 ## 标题),供扩写兜底逐节替换
|
||||
section_errors: list[str] = [] # 分节写作的模型异常,供完成后非致命提示
|
||||
blocked_sections: list[dict] = [] # 严格模式下素材不足、无法写作的章节
|
||||
|
||||
for i, section in enumerate(sections):
|
||||
sec_title = _section_title(i, section)
|
||||
@@ -104,6 +146,10 @@ async def writer_draft_node(state: AIWritingState, config: RunnableConfig) -> di
|
||||
for j, s in enumerate(sections)
|
||||
)
|
||||
|
||||
# 本章节是否走严格模式(全局严格 且 未被用户标记为「通用知识续写」)
|
||||
section_is_strict = writing_mode == "strict" and sec_title not in loose_titles
|
||||
mode_section = WRITER_MODE_STRICT if section_is_strict else WRITER_MODE_LOOSE
|
||||
|
||||
user_msg = DRAFT_SECTION_USER.format(
|
||||
article_title=article_title,
|
||||
outline_context=outline_context,
|
||||
@@ -111,6 +157,7 @@ async def writer_draft_node(state: AIWritingState, config: RunnableConfig) -> di
|
||||
key_points=key_points_str,
|
||||
section_materials=section_materials,
|
||||
revision_section=revision_notes,
|
||||
mode_section=mode_section,
|
||||
target_words=words_per_section,
|
||||
)
|
||||
|
||||
@@ -147,8 +194,27 @@ async def writer_draft_node(state: AIWritingState, config: RunnableConfig) -> di
|
||||
"is_section_done": True,
|
||||
}))
|
||||
|
||||
cleaned_buf = section_buf.strip()
|
||||
blocked_reason = ""
|
||||
# 严格模式:作家用哨兵声明素材不足,或仍输出了拒绝话术 → 标记为待求助章节
|
||||
if section_is_strict and SECTION_BLOCKED_MARKER in cleaned_buf:
|
||||
blocked_reason = (
|
||||
cleaned_buf.split(SECTION_BLOCKED_MARKER, 1)[1].strip()[:120]
|
||||
or "素材不足以支撑本章节写作"
|
||||
)
|
||||
elif _looks_like_refusal(cleaned_buf):
|
||||
if section_is_strict:
|
||||
blocked_reason = "素材不足以支撑本章节写作"
|
||||
else:
|
||||
# 宽松模式:模型仍拒绝,安全网占位,不打断流程
|
||||
logger.warning("draft section [%s] looks like a refusal, replaced with placeholder", sec_title)
|
||||
section_errors.append(f"「{sec_title}」生成内容疑似拒绝/无关,已占位")
|
||||
cleaned_buf = "(本节内容生成异常,建议重新生成或补充相关素材)"
|
||||
if blocked_reason:
|
||||
blocked_sections.append({"section_title": sec_title, "reason": blocked_reason})
|
||||
cleaned_buf = f"(本节素材不足:{blocked_reason})"
|
||||
section_titles.append(sec_title)
|
||||
section_bufs.append(section_buf.strip())
|
||||
section_bufs.append(cleaned_buf)
|
||||
|
||||
except Exception as e:
|
||||
logger.error("draft section [%s] failed: %s", sec_title, e, exc_info=True)
|
||||
@@ -156,8 +222,26 @@ async def writer_draft_node(state: AIWritingState, config: RunnableConfig) -> di
|
||||
section_bufs.append("(内容生成失败,请重试)")
|
||||
section_errors.append(f"「{sec_title}」{type(e).__name__}: {e}")
|
||||
|
||||
# 严格模式下有章节无法基于素材写作 → 暂停求助用户,不产出半成品草稿
|
||||
if blocked_sections:
|
||||
return {
|
||||
"blocked_sections": blocked_sections,
|
||||
"status": "awaiting_section_help",
|
||||
"progress_events": [
|
||||
{"type": "step_start", "agent_name": "作家", "message": f"开始写作(第{revision_count+1}稿)"},
|
||||
{
|
||||
"type": "notice", "agent_name": "作家", "title": "素材不足",
|
||||
"detail": f"有 {len(blocked_sections)} 个章节缺少足够素材支撑写作,需要你决定如何处理。",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def _assemble() -> str:
|
||||
body = "\n\n".join(f"## {t}\n\n{b}" for t, b in zip(section_titles, section_bufs))
|
||||
# 章节正文里的标题统一归一到 ###(要点小标题),章节标题用 ##,文章标题用 #
|
||||
body = "\n\n".join(
|
||||
f"## {t}\n\n{_normalize_section_headings(b)}"
|
||||
for t, b in zip(section_titles, section_bufs)
|
||||
)
|
||||
return f"# {article_title}\n\n{body}"
|
||||
|
||||
full_markdown = _assemble()
|
||||
|
||||
+13
-3
@@ -1,3 +1,7 @@
|
||||
# 注:两个 USER 提示词末尾的 `/no_think` 是 Qwen3 系列的"软开关",
|
||||
# 收到当轮即跳过思考过程;对其它大模型只是一行普通文本、无副作用。
|
||||
# 中文「严禁思考」强令块对所有模型生效,保留不动。
|
||||
|
||||
EXTRACT_KEYWORDS_SYSTEM = """你是一名专业情报研究员(素材收集专家)。
|
||||
你的目标:为写作任务提取搜索关键词。
|
||||
你的工作背景:你在一家权威媒体机构任职,熟悉各类信息源的可信度评估。
|
||||
@@ -26,8 +30,12 @@ EXTRACT_KEYWORDS_USER = """写作意图:{user_intent}
|
||||
目标读者:{target_audience}
|
||||
|
||||
请提取 {keyword_count} 个搜索关键词,用于搜集相关素材。
|
||||
关键词数量必须为 {keyword_count} 个,不多不少。只输出 JSON:
|
||||
{{"keywords": ["关键词1", "关键词2"]}}"""
|
||||
关键词数量必须为 {keyword_count} 个,不多不少。
|
||||
关键词必须是依据上述写作意图提炼的真实检索词,不要照抄下方 JSON 模板里的占位符。
|
||||
只输出 JSON(下方 ... 是占位示意,不是真实关键词):
|
||||
{{"keywords": ["...", "..."]}}
|
||||
|
||||
/no_think"""
|
||||
|
||||
RANK_MATERIALS_SYSTEM = """你是一名专业情报研究员(素材收集专家)。
|
||||
你的目标:对搜索结果按相关度排序和筛选。
|
||||
@@ -57,4 +65,6 @@ RANK_MATERIALS_USER = """写作意图:{user_intent}
|
||||
{materials_list}
|
||||
|
||||
请选出最相关的素材ID(最多8条),按相关度从高到低排序。只输出 JSON:
|
||||
{{"ranked_ids": ["id1", "id2"], "summary": "共搜索到X条素材,精选Y条"}}"""
|
||||
{{"ranked_ids": ["id1", "id2"], "summary": "共搜索到X条素材,精选Y条"}}
|
||||
|
||||
/no_think"""
|
||||
+57
-12
@@ -5,7 +5,17 @@ OUTLINE_SYSTEM = """你是一名专业内容创作者(作家)。
|
||||
你只能做以下事情:
|
||||
1. 分析素材,提取核心观点
|
||||
2. 规划文章章节结构和每节要点
|
||||
你不能写正文,只写大纲。
|
||||
你不能写正文,只写大纲。大纲须紧扣写作意图,并尽量覆盖可用素材中的核心信息。
|
||||
|
||||
【严禁思考与表态】(最高优先级)
|
||||
- 禁止输出任何思考、分析、规划过程,禁止 <think></think> 标签及其中内容。
|
||||
- 禁止输出任何对用户说的话或对任务的评价(如"素材不足""无法规划大纲");无论素材是否充分,都必须直接产出大纲 JSON。
|
||||
- 收到输入后立即、直接输出最终 JSON,不得有任何前置内容。
|
||||
|
||||
【要点(key_points)的特殊要求】
|
||||
- 每个要点都会作为文章里的一个二级小标题呈现,所以要点必须写成"标题式短语"。
|
||||
- 用简洁的名词性短语,控制在 15 字以内,不要写成完整句子,不要带句末标点。
|
||||
- 每个章节规划 2-4 个要点,不要过多,避免文章被切得过碎。
|
||||
|
||||
【输出要求】
|
||||
- 只输出一个 JSON 对象,不要任何解释文字,不要 markdown 代码块。
|
||||
@@ -17,9 +27,9 @@ OUTLINE_SYSTEM = """你是一名专业内容创作者(作家)。
|
||||
{
|
||||
"title": "新能源汽车出口为何高速增长",
|
||||
"sections": [
|
||||
{"section_title": "引言:出口数据创新高", "key_points": ["今年出口量同比增长", "在全球市场的排名变化"], "material_ids": ["m001"]},
|
||||
{"section_title": "引言:出口数据创新高", "key_points": ["出口量同比增幅", "全球市场排名变化"], "material_ids": ["m001"]},
|
||||
{"section_title": "增长背后的三大动因", "key_points": ["产业链成本优势", "海外政策利好", "产品竞争力提升"], "material_ids": ["m002", "m003"]},
|
||||
{"section_title": "结论与展望", "key_points": ["未来增长面临的挑战", "行业趋势判断"], "material_ids": ["m004"]}
|
||||
{"section_title": "结论与展望", "key_points": ["未来增长挑战", "行业趋势判断"], "material_ids": ["m004"]}
|
||||
]
|
||||
}"""
|
||||
|
||||
@@ -34,20 +44,51 @@ OUTLINE_USER = """写作意图:{user_intent}
|
||||
请规划文章大纲(3-5个章节),只输出 JSON 格式。"""
|
||||
|
||||
DRAFT_SECTION_SYSTEM = """你是一名专业内容创作者(作家)。
|
||||
你的目标:根据大纲和素材,写出一段高质量的文章内容。
|
||||
你的目标:根据大纲和素材,写出一段结构清晰、高质量的文章内容。
|
||||
你的工作背景:你有十年内容创作经验,文字流畅,善于用事实说话。
|
||||
|
||||
你只能做以下事情:
|
||||
1. 根据给定的章节要点和素材写这一段内容
|
||||
2. 观点必须来自素材,不编造数据
|
||||
2. 优先采用素材中的事实与数据;素材不足时可结合你的通用知识与常识补充论述,但须紧扣写作意图与章节主题,不要脱离主题杜撰具体的数字、人名、机构名与日期
|
||||
你不能修改大纲,不能写其他章节,只写当前这一段。
|
||||
|
||||
【严禁思考与表态】(最高优先级)
|
||||
- 禁止输出任何思考、分析、规划过程,禁止"让我想想""首先""我需要""根据写作要求"之类的过渡语。
|
||||
- 禁止输出 <think></think> 标签及其中的任何内容。
|
||||
- 禁止输出任何对用户说的话或对任务本身的评价,例如"我无法完成此任务""您提供的素材无法支撑写作""建议您提供……素材""素材与意图不匹配"等——这类话绝不允许出现在文章里。
|
||||
- 无论素材是否充分,你都必须直接产出文章正文本身;收到输入后立即开始写正文(含 ### 小标题),不要有任何前置说明。
|
||||
|
||||
【结构要求】
|
||||
- 把本章节的每一个「章节要点」写成一个二级小标题,用 Markdown 三级标题语法:### 要点
|
||||
- 每个 ### 小标题下面紧跟该要点的论述正文,正文连贯成段。
|
||||
- 即使章节只有一个要点,也要为它写出对应的 ### 小标题。
|
||||
- 小标题文字简洁,直接沿用给定的要点措辞,不要加序号、不要加标点。
|
||||
- 只能使用 ### 这一级标题;绝不要输出 # 或 ##(文章标题、章节标题由系统统一添加)。
|
||||
|
||||
【输出要求】
|
||||
- 直接返回这一段的正文文字,不要 JSON,不要章节标题,不要解释。
|
||||
- 直接返回这一段的内容(含 ### 小标题与正文),不要 JSON,不要解释。
|
||||
- 不要写"以下是正文""本段内容如下"之类的开场白。
|
||||
- 段落要连贯成文,不要罗列要点。
|
||||
- 紧扣本章节主题,不要写「文章整体结构」中其他章节负责的内容,避免重复。
|
||||
- 充分展开每个要点:每个要点至少用 2-3 句话论述,结合素材中的事实与数据说理,写满要求的篇幅。"""
|
||||
- 充分展开每个要点:每个要点至少用 2-3 句话论述,尽量结合素材中的事实与数据说理,写满要求的篇幅。"""
|
||||
|
||||
# ── 写作模式说明:注入 DRAFT_SECTION_USER 的 {mode_section} ───────────────────
|
||||
|
||||
# 章节无法基于素材写作时,作家在严格模式下输出的哨兵标记
|
||||
SECTION_BLOCKED_MARKER = "[[SECTION_BLOCKED]]"
|
||||
|
||||
WRITER_MODE_LOOSE = """
|
||||
【写作模式:宽松】
|
||||
素材充分时优先采用素材;素材不足时可结合你的通用知识与常识补充论述,
|
||||
但必须紧扣写作意图与本章节主题,不要脱离主题杜撰具体数字、人名、机构名。"""
|
||||
|
||||
WRITER_MODE_STRICT = """
|
||||
【写作模式:严格依据素材】
|
||||
本章节论述必须严格基于上方「相关素材」,不得使用素材之外的知识,不得杜撰任何事实与数据。
|
||||
如果上方素材确实不足以支撑本章节的写作,不要勉强拼凑、不要写无关内容,
|
||||
此时只输出一行:{marker} 一句话说明缺什么素材(例:{marker} 缺少关于XX的具体数据与案例),
|
||||
此外不要输出任何内容。只要素材能支撑哪怕部分要点,就正常写作,不要输出该标记。""".format(
|
||||
marker=SECTION_BLOCKED_MARKER
|
||||
)
|
||||
|
||||
DRAFT_SECTION_USER = """文章标题:{article_title}
|
||||
文章整体结构:
|
||||
@@ -58,8 +99,9 @@ DRAFT_SECTION_USER = """文章标题:{article_title}
|
||||
|
||||
相关素材:
|
||||
{section_materials}
|
||||
{revision_section}
|
||||
请写出「{section_title}」这一章节的正文内容,不少于 {target_words} 字,论述充分、内容充实。"""
|
||||
{revision_section}{mode_section}
|
||||
请写出「{section_title}」这一章节的内容,不少于 {target_words} 字。
|
||||
把上述每个章节要点写成一个 ### 二级小标题,并在标题下展开论述,论述充分、内容充实。"""
|
||||
|
||||
DRAFT_EXPAND_USER = """文章标题:{article_title}
|
||||
当前章节:{section_title}
|
||||
@@ -71,5 +113,8 @@ DRAFT_EXPAND_USER = """文章标题:{article_title}
|
||||
这一章节已有内容如下:
|
||||
{existing_text}
|
||||
|
||||
以上内容篇幅不足。请在保留原意和事实的基础上重写并扩充这一章节,使正文不少于 {target_words} 字。
|
||||
要求:补充更多来自素材的事实、数据与分析,论述更充分、更具体;直接返回扩充后的完整章节正文,不要章节标题、不要解释。"""
|
||||
以上内容篇幅不足。请在保留原意、事实和 ### 二级小标题结构的基础上重写并扩充这一章节,使内容不少于 {target_words} 字。
|
||||
要求:
|
||||
- 补充更多来自素材的事实、数据与分析,论述更充分、更具体。
|
||||
- 保留并沿用原有的 ### 小标题(每个章节要点对应一个),只用 ### 这一级标题,不要输出 # 或 ##。
|
||||
- 直接返回扩充后的完整章节内容(含 ### 小标题),不要解释。"""
|
||||
+12
-1
@@ -73,11 +73,17 @@ class UserInterventionPayload(TypedDict):
|
||||
WritingStatus = Literal[
|
||||
"researching", "awaiting_material_confirm",
|
||||
"writing_outline", "awaiting_outline_confirm",
|
||||
"writing_draft", "awaiting_draft_confirm",
|
||||
"writing_draft", "awaiting_section_help", "awaiting_draft_confirm",
|
||||
"reviewing", "awaiting_review_confirm",
|
||||
"revising", "done", "error",
|
||||
]
|
||||
|
||||
|
||||
class BlockedSection(TypedDict):
|
||||
"""严格模式下素材不足、无法写作的章节。"""
|
||||
section_title: str
|
||||
reason: str
|
||||
|
||||
class AIWritingState(TypedDict):
|
||||
# 写作配置
|
||||
user_intent: str
|
||||
@@ -86,6 +92,8 @@ class AIWritingState(TypedDict):
|
||||
word_count_target: int
|
||||
# 素材检索关键词数量(用户指定,1~10,默认 6)
|
||||
keyword_count: int
|
||||
# 写作模式:strict=严格依据素材 / loose=可结合通用知识(默认 loose)
|
||||
writing_mode: str
|
||||
|
||||
# Agent A 输出
|
||||
material_package: Optional[MaterialPackage]
|
||||
@@ -101,6 +109,9 @@ class AIWritingState(TypedDict):
|
||||
# 用户干预数据(每个干预点会覆盖)
|
||||
last_user_intervention: Optional[UserInterventionPayload]
|
||||
|
||||
# 严格模式下素材不足、待用户决策的章节(写作节点产出,求助暂停点消费)
|
||||
blocked_sections: Optional[List[BlockedSection]]
|
||||
|
||||
# 流程控制
|
||||
revision_count: int
|
||||
max_revisions: int
|
||||
|
||||
Reference in new issue
Block a user