Add Comments

This commit is contained in:
马一丁
2025-11-14 19:44:04 +08:00
parent 52eed4d010
commit 6d0e8f4b8c
13 changed files with 655 additions and 61 deletions
+79 -6
View File
@@ -107,7 +107,23 @@ class ChapterGenerationNode(BaseNode):
stream_callback: Optional[Callable[[str, Dict[str, Any]], None]] = None,
**kwargs,
) -> Dict[str, Any]:
"""针对单个章节调用LLM,校验/落盘章节JSON并返回结构化结果"""
"""
针对单个章节调用LLM,校验/落盘章节JSON并返回结构化结果。
参数:
section: 模板切片生成的章节对象,包含标题/顺序/slug。
context: Agent构造的共享上下文(主题、篇幅、布局等)。
run_dir: 章节存盘目录,由 `ChapterStorage.start_session` 返回。
stream_callback: 可选流式回调,将LLM delta 推送给前端。
**kwargs: 透传温度、top_p等采样参数。
返回:
dict: 通过IR校验的章节JSON。
异常:
ChapterJsonParseError: 多次尝试后仍无法解析合法JSON。
ChapterContentError: 正文密度不足或只有标题,需要触发重试。
"""
chapter_meta = {
"chapterId": section.chapter_id,
"slug": section.slug,
@@ -167,7 +183,16 @@ class ChapterGenerationNode(BaseNode):
# ====== 内部方法 ======
def _build_payload(self, section: TemplateSection, context: Dict[str, Any]) -> Dict[str, Any]:
"""构造LLM输入payload"""
"""
构造LLM输入payload。
参数:
section: 当前要生成的章节,提供标题/编号/提纲。
context: 全局上下文字典,包含主题、三引擎报告、篇幅规划等。
返回:
dict: 可以直接序列化进提示词的payload,兼顾章节信息与全局约束。
"""
reports = context.get("reports", {})
# 章节篇幅规划(来自WordBudgetNode),用于指导字数与强调点
chapter_plan_map = context.get("chapter_directives", {})
@@ -233,7 +258,19 @@ class ChapterGenerationNode(BaseNode):
section_meta: Optional[Dict[str, Any]] = None,
**kwargs,
) -> str:
"""流式调用LLM并实时写入raw文件,同时通过回调将delta抛出。"""
"""
流式调用LLM并实时写入raw文件,同时通过回调将delta抛出。
参数:
user_message: 拼装好的用户提示词。
chapter_dir: 章节的本地缓存目录,用于存放 stream.raw。
stream_callback: SSE流式推送的回调函数。
section_meta: 附带的章节ID/标题,用于回调payload。
**kwargs: 透传温度、top_p等参数。
返回:
str: 将所有delta拼接后的原始文本。
"""
chunks: List[str] = []
with self.storage.capture_stream(chapter_dir) as stream_fp:
stream = self.llm_client.stream_invoke(
@@ -254,7 +291,18 @@ class ChapterGenerationNode(BaseNode):
return "".join(chunks)
def _parse_chapter(self, raw_text: str) -> Dict[str, Any]:
"""清洗LLM输出并解析JSON"""
"""
清洗LLM输出并解析JSON。
参数:
raw_text: LLM原始输出(可能包含```包裹或额外说明)。
返回:
dict: 章节JSON对象,至少包含 chapterId/title/blocks。
异常:
ChapterJsonParseError: 多种修复策略仍无法解析合法JSON。
"""
cleaned = raw_text.strip()
if cleaned.startswith("```json"):
cleaned = cleaned[7:]
@@ -304,7 +352,15 @@ class ChapterGenerationNode(BaseNode):
raise ValueError("章节JSON缺少chapter字段")
def _repair_llm_json(self, text: str) -> str:
"""处理常见的LLM错误(如\":=导致的非法JSON"""
"""
处理常见的LLM错误(如":=导致的非法JSON)。
参数:
text: 原始章节JSON文本。
返回:
str: 修复后的文本;若未做改动则返回原内容。
"""
repaired = text
mutated = False
@@ -482,7 +538,12 @@ class ChapterGenerationNode(BaseNode):
return fixed
def _sanitize_chapter_blocks(self, chapter: Dict[str, Any]):
"""修正常见的结构性错误(例如list.items嵌套过深)"""
"""
修正常见的结构性错误(例如list.items嵌套过深)。
参数:
chapter: 章节JSON对象,会在原地被清理和规整。
"""
def walk(blocks: List[Dict[str, Any]] | None):
"""递归检查并修复嵌套结构,保证每个block合法"""
@@ -527,6 +588,12 @@ class ChapterGenerationNode(BaseNode):
若blocks缺失、除标题外无有效区块,或正文字符数低于阈值,
则视为章节内容异常,触发ChapterContentError以便上游重试。
参数:
chapter: 当前章节JSON。
异常:
ChapterContentError: 当正文区块数量或字符数达不到下限时抛出。
"""
blocks = chapter.get("blocks")
if not isinstance(blocks, list) or not blocks:
@@ -552,6 +619,12 @@ class ChapterGenerationNode(BaseNode):
- 忽略heading/divider/widget等非正文类型;
- 对paragraph/list/table/callout等结构抽取嵌套文本;
- 仅用于粗粒度判断篇幅是否合理。
参数:
blocks: 章节的 blocks 列表或子树。
返回:
int: 估算的正文字符数量。
"""
def walk(node: Any) -> int:
+26 -2
View File
@@ -37,7 +37,20 @@ class DocumentLayoutNode(BaseNode):
query: str,
template_overview: Dict[str, Any] | None = None,
) -> Dict[str, Any]:
"""综合模板+多源内容,生成全书的标题、目录结构与主题色板"""
"""
综合模板+多源内容,生成全书的标题、目录结构与主题色板。
参数:
sections: 模板切片后的章节列表。
template_markdown: 模板原文,用于LLM理解上下文。
reports: 三个引擎的内容映射。
forum_logs: 论坛讨论摘要。
query: 用户查询词。
template_overview: 预生成的模板概览,可复用以减少提示词长度。
返回:
dict: 包含 title/subtitle/toc/hero/themeTokens 等设计信息的字典。
"""
# 将模板原文、切片结构与多源报告一并喂给LLM,便于其理解层级与素材
payload = {
"query": query,
@@ -66,7 +79,18 @@ class DocumentLayoutNode(BaseNode):
return design
def _parse_response(self, raw: str) -> Dict[str, Any]:
"""解析LLM返回的JSON文本,若失败则抛出友好错误"""
"""
解析LLM返回的JSON文本,若失败则抛出友好错误。
参数:
raw: LLM原始返回字符串,允许带```包裹。
返回:
dict: 结构化的设计稿。
异常:
ValueError: 当响应为空或JSON解析失败时抛出。
"""
cleaned = raw.strip()
if cleaned.startswith("```json"):
cleaned = cleaned[7:]
+31 -1
View File
@@ -79,6 +79,15 @@ class TemplateSelectionNode(BaseNode):
构造模板列表与报告摘要 → 调用LLM → 解析JSON →
验证模板是否存在并返回标准结构。
参数:
query: 用户输入的主题词。
reports: 多个分析引擎的报告内容。
forum_logs: 论坛日志,可能为空。
available_templates: 本地可用模板清单。
返回:
dict | None: 若LLM成功返回合法结果则包含模板信息,否则为None。
"""
logger.info("尝试使用LLM进行模板选择...")
@@ -166,6 +175,12 @@ class TemplateSelectionNode(BaseNode):
清理LLM响应。
去掉 ```json``` 包裹以及前后空白,方便 `json.loads`。
参数:
response: LLM原始响应。
返回:
str: 适合直接做JSON解析的纯文本。
"""
# 移除可能的markdown代码块标记
if '```json' in response:
@@ -183,6 +198,13 @@ class TemplateSelectionNode(BaseNode):
从文本响应中提取模板信息。
当LLM未输出合法JSON时,尝试匹配模板名称关键字做降级。
参数:
response: 非结构化的LLM文本。
available_templates: 可选模板列表。
返回:
dict | None: 匹配成功时返回模板详情,否则为None。
"""
logger.info("尝试从文本响应中提取模板信息")
@@ -210,6 +232,9 @@ class TemplateSelectionNode(BaseNode):
获取可用的模板列表。
枚举模板目录下的 `.md` 文件并读取内容与描述字段。
返回:
list[dict]: 每项包含 name/path/content/description。
"""
templates = []
@@ -259,7 +284,12 @@ class TemplateSelectionNode(BaseNode):
def _get_fallback_template(self) -> Dict[str, Any]:
"""获取备用默认模板(空模板,让LLM自行发挥)。"""
"""
获取备用默认模板(空模板,让LLM自行发挥)。
返回:
dict: 结构体字段与LLM返回一致,方便直接替换。
"""
logger.info("未找到合适模板,使用空模板让LLM自行发挥")
return {
+26 -2
View File
@@ -37,7 +37,20 @@ class WordBudgetNode(BaseNode):
query: str,
template_overview: Dict[str, Any] | None = None,
) -> Dict[str, Any]:
"""根据设计稿和所有素材规划章节字数,让LLM写作时有明确篇幅目标"""
"""
根据设计稿和所有素材规划章节字数,让LLM写作时有明确篇幅目标。
参数:
sections: 模板章节列表。
design: 布局节点返回的设计稿(title/toc/hero等)。
reports: 三引擎报告映射。
forum_logs: 论坛日志原文。
query: 用户查询词。
template_overview: 可选的模板概览,含章节元信息。
返回:
dict: 章节篇幅规划结果,包含 `totalWords`、`globalGuidelines` 与逐章 `chapters`。
"""
# 输入中除了章节骨架外,还包含布局节点输出,方便约束篇幅时参考视觉主次
payload = {
"query": query,
@@ -63,7 +76,18 @@ class WordBudgetNode(BaseNode):
return plan
def _parse_response(self, raw: str) -> Dict[str, Any]:
"""将LLM输出的JSON文本转为字典,失败时提示规划异常"""
"""
将LLM输出的JSON文本转为字典,失败时提示规划异常。
参数:
raw: LLM返回值,可能包含```包裹。
返回:
dict: 合法的篇幅规划JSON。
异常:
ValueError: 当响应为空或JSON解析失败时抛出。
"""
cleaned = raw.strip()
if cleaned.startswith("```json"):
cleaned = cleaned[7:]