bidmaster-cli/src/bidmaster/nodes/content/generate_content.py
sladro 81e4947b99 refactor: 全面修复Word填充Agent代码规范问题
## 核心改进

### 1. 移除静默降级,快速失败 (interact_user.py)
- 删除无交互处理器时返回默认值的静默逻辑
- 改为立即抛出异常暴露配置缺失问题
- 符合"暴露问题、快速失败"原则

### 2. 统一配置源 (word.py + settings.py)
- 删除FormatConstants类,消除配置冗余
- Word格式参数统一从settings读取
- 新增Word格式配置项到settings.py
- 实现单一数据源原则

### 3. 统一RAGTool实例化 (init_config.py)
- 在InitConfigNode统一创建RAGTool实例
- generate_content.py和interact_user.py改为验证式获取
- 消除重复实例化逻辑
- 修复single_chapter_agent.py状态传递丢失rag_tool问题

### 4. 拆分复杂方法 (word.py)
- fill_placeholder拆分为6个原子操作:
  * _parse_chapter_number_from_placeholder
  * _find_heading_by_chapter_id
  * _get_chapter_level_from_heading
  * _parse_markdown_to_paragraphs
  * _insert_parsed_paragraphs
  * 保存文档
- 符合"小步快跑"原则

### 5. 优化配置继承 (init_config.py + generate_content.py)
- 新增_expand_config_inheritance预展开继承链
- _get_chapter_config从O(n)降为O(1)查找
- 移除运行时while循环向上查找

### 6. 统一异常处理策略
- 所有错误立即抛出,移除静默返回默认值
- interact_user.py: 获取知识库失败抛出异常
- generate_content.py: RAGTool未初始化抛出异常

### 7. 修复状态传递问题 (single_chapter_agent.py)
- 使用{**state, ...}继承所有字段
- 避免硬编码字典遗漏rag_tool和expanded_configs
- 确保初始化数据正确传递

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-02 08:25:15 +08:00

222 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""内容生成节点
调用RAG工具生成章节内容。
"""
import logging
from typing import Any, Dict, Optional
from ..base import BaseNode, NodeContext
from ...config.settings import get_settings
logger = logging.getLogger(__name__)
settings = get_settings()
class GenerateContentNode(BaseNode):
"""内容生成节点
职责:
1. 获取当前章节配置(如果是子章节,继承父章节配置)
2. 调用RAGTool生成内容
3. 保存生成结果到state
"""
@property
def name(self) -> str:
return "generate_content"
@property
def description(self) -> str:
return "生成章节内容"
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
"""执行内容生成
Args:
state: 当前状态
context: 执行上下文
Returns:
更新后的状态
"""
chapter = state.get("current_chapter", {})
chapter_id = chapter.get("id")
chapter_title = chapter.get("title")
if not chapter_id:
raise ValueError("当前章节信息缺失")
logger.info(f"开始生成章节内容: {chapter_id} - {chapter_title}")
# 获取章节配置(继承父章节)
config = self._get_chapter_config(state, chapter)
# 查找当前章节的所有子标题
sub_chapters = self._find_sub_chapters(state, chapter_id, max_level=settings.max_sub_chapter_level)
if sub_chapters:
# 有子标题:逐个生成后拼接
logger.info(f"章节 {chapter_id} 包含 {len(sub_chapters)} 个子标题,逐个生成")
content_parts = []
for sub in sub_chapters:
sub_content = self._generate_with_rag(sub, config, state)
content_parts.append(f"## {sub['title']}\n\n{sub_content}")
content = "\n\n".join(content_parts)
else:
# 无子标题:直接生成
content = self._generate_with_rag(chapter, config, state)
# 保存生成结果
state.setdefault("generated_contents", {})[chapter_id] = content
logger.info(f"章节内容生成完成: {chapter_id}, 长度: {len(content)}")
return state
def _get_chapter_config(
self, state: Dict[str, Any], chapter: Dict[str, Any]
) -> Dict[str, Any]:
"""获取章节配置,使用预展开的继承链
Args:
state: 当前状态
chapter: 当前章节
Returns:
章节配置
"""
chapter_id = chapter["id"]
chapter_configs = state.get("chapter_configs", {})
# 如果当前章节有配置,直接返回
if chapter_id in chapter_configs:
return chapter_configs[chapter_id]
# 使用预展开的配置继承链
expanded_configs = state.get("expanded_configs", {})
config_source_id = expanded_configs.get(chapter_id)
if config_source_id and config_source_id in chapter_configs:
logger.info(f"章节 {chapter_id} 继承 {config_source_id} 的配置")
return chapter_configs[config_source_id]
# 返回默认配置
logger.info(f"章节 {chapter_id} 使用默认配置")
return self._get_default_config()
def _get_default_config(self) -> Dict[str, Any]:
"""获取默认章节配置
Returns:
默认配置字典
"""
return {
"emphasis": "",
"rag_enabled": False,
"rag_store": None
}
def _find_chapter(self, state: Dict[str, Any], chapter_id: str) -> Optional[Dict[str, Any]]:
"""在队列中查找章节
Args:
state: 当前状态
chapter_id: 章节ID
Returns:
章节信息或None
"""
queue = state.get("chapter_queue", [])
for chapter in queue:
if chapter["id"] == chapter_id:
return chapter
return None
def _find_sub_chapters(
self, state: Dict[str, Any], parent_id: str, max_level: int = 3
) -> list:
"""查找指定章节的所有子标题最深到3级
Args:
state: 当前状态
parent_id: 父章节ID
max_level: 最大层级默认3级
Returns:
子章节列表
"""
queue = state.get("chapter_queue", [])
parent = self._find_chapter(state, parent_id)
if not parent:
return []
parent_level = parent.get("level", 1)
target_level = parent_level + 1
# 只查找直接子章节比父章节深1层且不超过max_level
sub_chapters = []
for chapter in queue:
if (
chapter.get("parent_id") == parent_id
and chapter.get("level") == target_level
and chapter.get("level") <= max_level
):
sub_chapters.append(chapter)
return sub_chapters
def _generate_with_rag(
self, chapter: Dict[str, Any], config: Dict[str, Any], state: Dict[str, Any]
) -> str:
"""使用RAG生成内容
Args:
chapter: 章节信息
config: 章节配置
state: 当前状态
Returns:
生成的内容
"""
# 从state获取RAGTool实例由InitConfigNode统一初始化
rag_tool = state.get("rag_tool")
if not rag_tool:
raise ValueError("RAGTool未初始化请检查InitConfigNode配置")
# 构建生成上下文
generation_context = {
"title": chapter["title"],
"level": chapter["level"],
"requirements": chapter.get("requirements", ""),
"emphasis": config.get("emphasis", ""),
}
# 如果启用RAG添加上下文信息
if config.get("rag_enabled"):
# 检索相关内容
query = f"{chapter['title']} {config.get('emphasis', '')}"
search_results = rag_tool.search(query, k=settings.rag_search_top_k)
if search_results:
relevant_context = "\n\n".join([r["content"] for r in search_results])
generation_context["rag_context"] = relevant_context
logger.info(f"RAG检索到 {len(search_results)} 条相关内容")
else:
logger.warning("RAG未检索到相关内容")
generation_context["rag_context"] = ""
else:
generation_context["rag_context"] = ""
# 添加父章节上下文
parent_context = state.get("last_generated_content", "")
if parent_context and chapter["level"] > 1:
generation_context["parent_context"] = parent_context[:settings.parent_context_length]
# 调用生成方法
try:
content = rag_tool.generate_content(chapter["id"], generation_context)
return content
except Exception as e:
logger.error(f"内容生成失败: {e}", exc_info=True)
raise ValueError(f"章节 {chapter['id']} - {chapter['title']} 内容生成失败") from e