feat: 在标题生成Agent中增加AI审核交互节点

1. 新增ApplyReviewSuggestionsNode节点
   - 展示AI审查建议给用户
   - 支持用户选择要应用的建议
   - 支持交互/静默/程序化三种模式

2. 新增AdjustChaptersNode节点
   - 根据用户选择的AI建议调整章节结构
   - 完全基于AI的智能调整,无硬编码逻辑
   - 安全的错误处理和回退机制

3. 更新工作流架构
   - 新流程: ReviewStructure → ApplyReviewSuggestions → AdjustChapters → FinalizeChapters
   - 专业的单一职责原则,每个节点功能明确
   - 完善的状态传递和错误处理

4. 修复AnalysisAgent集成问题
   - 保留TocAgent的完整审查结果
   - 正确使用调整后的章节结构
   - 修复interaction_handler传递问题

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
sladro 2025-09-29 15:08:41 +08:00
parent bc3ea2e439
commit 52733d68c2
7 changed files with 404 additions and 51 deletions

View File

@ -307,10 +307,18 @@ def generate_toc_with_agent_node(state: AnalysisAgentState) -> AnalysisAgentStat
logger.info(f"目录生成完成: {len(chapters)}个章节") logger.info(f"目录生成完成: {len(chapters)}个章节")
# 更新状态 # 更新状态 - 使用TocAgent的完整结果
state["preliminary_chapters"] = chapters state["preliminary_chapters"] = chapters
state["technical_criteria"] = technical_criteria state["technical_criteria"] = technical_criteria
state["structure_review"] = {} # TocAgent已包含审查
# 保留TocAgent的审查结果和其他状态
if result.get("structure_review"):
state["structure_review"] = result["structure_review"]
if result.get("approved_suggestions"):
state["approved_suggestions"] = result["approved_suggestions"]
if result.get("adjusted_chapters"):
state["adjusted_chapters"] = result["adjusted_chapters"]
state["current_step"] = "generate_toc" state["current_step"] = "generate_toc"
state["progress"] = 0.85 state["progress"] = 0.85
@ -335,12 +343,17 @@ def finalize_structure_node(state: AnalysisAgentState) -> AnalysisAgentState:
preliminary_chapters = state["preliminary_chapters"] preliminary_chapters = state["preliminary_chapters"]
structure_review = state.get("structure_review", {}) structure_review = state.get("structure_review", {})
# 获取最终章节(优先使用调整后的章节)
final_chapters = state.get("adjusted_chapters")
if not final_chapters:
final_chapters = state.get("final_chapters", preliminary_chapters)
# 创建最终的标书结构 # 创建最终的标书结构
bid_structure = BidStructure( bid_structure = BidStructure(
project_name=f"标书项目-{Path(state['source_file']).stem}", project_name=f"标书项目-{Path(state['source_file']).stem}",
scoring_criteria=technical_criteria, scoring_criteria=technical_criteria,
deviation_items=state["deviation_items"], deviation_items=state["deviation_items"],
chapters=preliminary_chapters, # 使用TocAgent生成的章节 chapters=final_chapters, # 使用最终的章节(可能是调整后的)
scoring_file=state["source_file"] scoring_file=state["source_file"]
) )

View File

@ -14,6 +14,8 @@ from ...nodes.toc import (
GenerateFirstLevelNode, GenerateFirstLevelNode,
GenerateSubChaptersNode, GenerateSubChaptersNode,
ReviewStructureNode, ReviewStructureNode,
ApplyReviewSuggestionsNode,
AdjustChaptersNode,
FinalizeChaptersNode FinalizeChaptersNode
) )
from ...nodes.toc.base_mixins import WorkflowUtilsMixin from ...nodes.toc.base_mixins import WorkflowUtilsMixin
@ -45,6 +47,8 @@ class TocAgentBuilder(AgentBuilder):
.add_node(GenerateFirstLevelNode()) \ .add_node(GenerateFirstLevelNode()) \
.add_node(GenerateSubChaptersNode()) \ .add_node(GenerateSubChaptersNode()) \
.add_node(ReviewStructureNode()) \ .add_node(ReviewStructureNode()) \
.add_node(ApplyReviewSuggestionsNode()) \
.add_node(AdjustChaptersNode()) \
.add_node(FinalizeChaptersNode()) .add_node(FinalizeChaptersNode())
# 设置入口点 # 设置入口点
@ -80,6 +84,18 @@ class TocAgentBuilder(AgentBuilder):
self.add_conditional_edge( self.add_conditional_edge(
"review_structure", "review_structure",
should_continue, should_continue,
{"continue": "apply_suggestions", "end": "END"}
)
self.add_conditional_edge(
"apply_suggestions",
should_continue,
{"continue": "adjust_chapters", "end": "END"}
)
self.add_conditional_edge(
"adjust_chapters",
should_continue,
{"continue": "finalize_chapters", "end": "END"} {"continue": "finalize_chapters", "end": "END"}
) )
@ -134,6 +150,9 @@ class TocAgent(BaseAgent, BaseAgentFactory):
if template_file: if template_file:
initial_state["preset_template_file"] = template_file initial_state["preset_template_file"] = template_file
# 添加交互处理器到状态中
initial_state["interaction_handler"] = self.builder.interaction_handler
return await self.execute(initial_state) return await self.execute(initial_state)
def generate_toc_sync(self, def generate_toc_sync(self,

View File

@ -8,6 +8,8 @@ from .group_criteria import GroupCriteriaNode
from .generate_first_level import GenerateFirstLevelNode from .generate_first_level import GenerateFirstLevelNode
from .generate_sub_chapters import GenerateSubChaptersNode from .generate_sub_chapters import GenerateSubChaptersNode
from .review_structure import ReviewStructureNode from .review_structure import ReviewStructureNode
from .apply_suggestions import ApplyReviewSuggestionsNode
from .adjust_chapters import AdjustChaptersNode
from .finalize_chapters import FinalizeChaptersNode from .finalize_chapters import FinalizeChaptersNode
# 辅助组件 # 辅助组件
@ -27,6 +29,8 @@ __all__ = [
"GenerateFirstLevelNode", "GenerateFirstLevelNode",
"GenerateSubChaptersNode", "GenerateSubChaptersNode",
"ReviewStructureNode", "ReviewStructureNode",
"ApplyReviewSuggestionsNode",
"AdjustChaptersNode",
"FinalizeChaptersNode", "FinalizeChaptersNode",
# 辅助组件 # 辅助组件

View File

@ -0,0 +1,234 @@
"""AI调整章节结构的节点
专门负责根据用户选择的AI建议调整章节结构
"""
import logging
from typing import Dict, List, Any
from ..base import BaseNode, NodeContext
from ...tools.parser import DocumentChapter
from .base_mixins import TocNodeBase
logger = logging.getLogger(__name__)
class AdjustChaptersNode(BaseNode, TocNodeBase):
"""AI调整章节结构的节点"""
@property
def name(self) -> str:
return "adjust_chapters"
@property
def description(self) -> str:
return "AI调整章节结构"
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
"""执行AI章节调整"""
return self.safe_execute(self._do_adjust_chapters, state, "AI调整章节结构")
def _do_adjust_chapters(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""执行实际的AI章节调整逻辑"""
# 验证必需字段
required_fields = ["preliminary_chapters"]
if not self.validate_required_fields(state, required_fields):
raise ValueError("缺少初步章节数据")
preliminary_chapters = state["preliminary_chapters"]
approved_suggestions = state.get("approved_suggestions", [])
if not approved_suggestions:
# 没有用户选择的建议,直接返回原章节结构
logger.info("用户未选择任何AI建议保持原有章节结构")
return self._update_state(state, adjusted_chapters=preliminary_chapters)
# 使用AI根据建议调整章节结构
adjusted_chapters = self._ai_adjust_chapters_with_suggestions(
preliminary_chapters, approved_suggestions)
self.log_step_info("adjust_chapters",
f"AI根据{len(approved_suggestions)}条建议调整章节,生成{len(adjusted_chapters)}个章节")
return self._update_state(state, adjusted_chapters=adjusted_chapters)
def _ai_adjust_chapters_with_suggestions(self,
preliminary_chapters: List[DocumentChapter],
approved_suggestions: List[Dict[str, Any]]) -> List[DocumentChapter]:
"""让AI根据用户选择的建议调整章节结构
Args:
preliminary_chapters: 原始章节结构
approved_suggestions: 用户批准的AI建议
Returns:
AI调整后的章节结构
"""
try:
# 构建给AI的提示词
adjustment_prompt = self._build_adjustment_prompt(preliminary_chapters, approved_suggestions)
# 调用LLM进行章节调整
from .llm_helper import LLMHelper
response = LLMHelper.call_llm_with_retry(adjustment_prompt)
if not response:
logger.error("AI章节调整失败使用原始章节结构")
return preliminary_chapters
# 解析AI返回的调整结果
adjusted_data = LLMHelper.parse_ai_json_response(response)
adjusted_chapters = self._parse_adjusted_chapters(adjusted_data)
if adjusted_chapters:
logger.info(f"AI成功调整章节结构{len(adjusted_chapters)}个章节")
return adjusted_chapters
else:
logger.warning("AI调整结果解析失败使用原始章节结构")
return preliminary_chapters
except Exception as e:
logger.error(f"AI章节调整过程失败: {e}")
return preliminary_chapters
def _build_adjustment_prompt(self,
chapters: List[DocumentChapter],
suggestions: List[Dict[str, Any]]) -> str:
"""构建AI章节调整的提示词
Args:
chapters: 原始章节列表
suggestions: 用户选择的建议
Returns:
给AI的提示词
"""
# 格式化原始章节结构
chapters_text = self._format_chapters_for_prompt(chapters)
# 格式化用户选择的建议
suggestions_text = self._format_suggestions_for_prompt(suggestions)
prompt = f"""
请根据用户选择的建议调整以下标书章节结构
当前章节结构:
{chapters_text}
用户选择应用的建议:
{suggestions_text}
调整要求:
1. 根据用户选择的建议对章节结构进行合理调整
2. 保持章节的逻辑性和完整性
3. 每个章节都要有明确的标题和层级
4. 保持专业的标书格式
返回JSON格式
{{
"adjusted_chapters": [
{{
"id": "chapter_1_technical_solution",
"title": "技术方案",
"level": 1,
"score": 0,
"children": [
{{
"id": "chapter_2_1_architecture",
"title": "系统架构设计",
"level": 2,
"score": 0,
"children": []
}}
]
}}
]
}}
只返回JSON"""
return prompt
def _format_chapters_for_prompt(self, chapters: List[DocumentChapter]) -> str:
"""格式化章节结构用于提示词"""
lines = []
for chapter in chapters:
lines.append(f"{chapter.title} (level: {chapter.level})")
# 递归显示子章节
self._format_children_for_prompt(chapter.children, lines, 1)
return "\n".join(lines)
def _format_children_for_prompt(self, children: List[DocumentChapter], lines: List[str], indent: int):
"""递归格式化子章节"""
for child in children:
indent_str = " " * indent
lines.append(f"{indent_str}{child.title} (level: {child.level})")
if child.children:
self._format_children_for_prompt(child.children, lines, indent + 1)
def _format_suggestions_for_prompt(self, suggestions: List[Dict[str, Any]]) -> str:
"""格式化建议列表用于提示词"""
lines = []
for i, suggestion in enumerate(suggestions, 1):
# 动态格式化建议内容,不预设字段
suggestion_text = f"{i}. "
for key, value in suggestion.items():
if value:
suggestion_text += f"{key}: {value}, "
lines.append(suggestion_text.rstrip(", "))
return "\n".join(lines)
def _parse_adjusted_chapters(self, adjusted_data: Dict[str, Any]) -> List[DocumentChapter]:
"""解析AI调整后的章节数据
Args:
adjusted_data: AI返回的调整数据
Returns:
解析后的章节列表
"""
try:
chapters_data = adjusted_data.get("adjusted_chapters", [])
chapters = []
for chapter_data in chapters_data:
chapter = self._create_chapter_from_data(chapter_data)
if chapter:
chapters.append(chapter)
return chapters
except Exception as e:
logger.error(f"解析AI调整章节失败: {e}")
return []
def _create_chapter_from_data(self, chapter_data: Dict[str, Any]) -> DocumentChapter:
"""从数据创建章节对象
Args:
chapter_data: 章节数据
Returns:
创建的章节对象
"""
try:
chapter = DocumentChapter(
id=chapter_data.get("id", ""),
title=chapter_data.get("title", ""),
level=chapter_data.get("level", 1),
score=chapter_data.get("score", 0),
children=[]
)
# 递归处理子章节
children_data = chapter_data.get("children", [])
for child_data in children_data:
child_chapter = self._create_chapter_from_data(child_data)
if child_chapter:
chapter.children.append(child_chapter)
return chapter
except Exception as e:
logger.error(f"创建章节对象失败: {e}")
return None

View File

@ -0,0 +1,124 @@
"""应用AI审查建议的节点
让用户选择是否应用AI审查建议完全依赖AI的分类结果不做任何人为预设
"""
import logging
from typing import Dict, List, Any
from ..base import BaseNode, NodeContext
from .base_mixins import TocNodeBase
logger = logging.getLogger(__name__)
class ApplyReviewSuggestionsNode(BaseNode, TocNodeBase):
"""应用AI审查建议的节点"""
@property
def name(self) -> str:
return "apply_suggestions"
@property
def description(self) -> str:
return "应用AI审查建议"
def execute(self, state: Dict[str, Any], context: NodeContext) -> Dict[str, Any]:
"""执行建议应用交互"""
return self.safe_execute(self._do_apply_suggestions, state, "应用AI审查建议")
def _do_apply_suggestions(self, state: Dict[str, Any]) -> Dict[str, Any]:
"""执行实际的建议应用逻辑"""
logger.info("🔥🔥🔥 ApplyReviewSuggestionsNode 正在执行!!!")
# 验证必需字段
required_fields = ["preliminary_chapters", "structure_review"]
if not self.validate_required_fields(state, required_fields):
raise ValueError("缺少章节数据或审查结果")
structure_review = state["structure_review"]
# 获取交互处理器
interaction_handler = state.get("interaction_handler")
if not interaction_handler:
# 如果没有交互处理器,跳过用户选择,不应用任何建议
logger.warning("未找到交互处理器,跳过建议应用")
approved_suggestions = []
else:
# 用户选择要应用的建议
approved_suggestions = self._get_user_approval(structure_review, interaction_handler)
# 将用户选择的建议保存到状态中供FinalizeChaptersNode使用
self.log_step_info("apply_suggestions",
f"用户选择应用{len(approved_suggestions)}条AI建议")
return self._update_state(state, approved_suggestions=approved_suggestions)
def _get_user_approval(self, structure_review: Dict[str, Any],
interaction_handler) -> List[Dict[str, Any]]:
"""获取用户对AI建议的批准
Args:
structure_review: AI审查结果完全使用原始数据
interaction_handler: 交互处理器
Returns:
用户批准的建议列表
"""
# 直接使用AI返回的原始数据不做任何预设
suggestions = structure_review.get("suggestions", [])
overall_assessment = structure_review.get("overall_assessment", "")
optimization_score = structure_review.get("optimization_score", "")
if not suggestions:
logger.info("AI未提供优化建议")
return []
try:
# 展示AI的完整评估结果
assessment_text = f"AI审查评估:\n"
if overall_assessment:
assessment_text += f"总体评价: {overall_assessment}\n"
if optimization_score:
assessment_text += f"优化评分: {optimization_score}\n"
assessment_text += f"AI识别出{len(suggestions)}条建议"
# 询问用户是否要查看并选择建议
view_suggestions = interaction_handler(
interaction_type="confirm",
prompt=f"{assessment_text}\n是否查看具体建议并选择应用?",
default=True,
key="view_ai_suggestions"
)
if not view_suggestions:
return []
# 逐个展示AI建议让用户选择
approved_suggestions = []
for i, suggestion in enumerate(suggestions, 1):
# 完全使用AI提供的原始建议内容不做任何格式化预设
suggestion_text = f"AI建议{i}:\n"
# 动态展示AI建议中的所有字段不预设字段名
for key, value in suggestion.items():
if value: # 只显示有值的字段
suggestion_text += f"{key}: {value}\n"
# 让用户选择是否应用这条AI建议
apply_suggestion = interaction_handler(
interaction_type="confirm",
prompt=f"{suggestion_text}是否应用此AI建议",
default=False, # 默认不应用,让用户主动选择
key=f"apply_ai_suggestion_{i}"
)
if apply_suggestion:
approved_suggestions.append(suggestion)
return approved_suggestions
except Exception as e:
logger.error(f"获取用户建议选择失败: {e}")
# 出错时不应用任何建议,保持安全
return []

View File

@ -153,9 +153,9 @@ class WorkflowUtilsMixin:
Returns: Returns:
"continue" 如果应该继续"end" 如果应该停止 "continue" 如果应该继续"end" 如果应该停止
""" """
if state.get("error") or not state.get("should_continue", True): result = "end" if (state.get("error") or not state.get("should_continue", True)) else "continue"
return "end" logger.info(f"🔥🔥🔥 should_continue_workflow: {result}, error={state.get('error')}, should_continue={state.get('should_continue', True)}")
return "continue" return result
@staticmethod @staticmethod
def validate_required_fields(state: Dict[str, Any], def validate_required_fields(state: Dict[str, Any],

View File

@ -35,11 +35,11 @@ class FinalizeChaptersNode(BaseNode, TocNodeBase):
if not self.validate_required_fields(state, ["preliminary_chapters"]): if not self.validate_required_fields(state, ["preliminary_chapters"]):
raise ValueError("缺少初步章节数据") raise ValueError("缺少初步章节数据")
preliminary_chapters = state["preliminary_chapters"] # 获取调整后的章节(如果有)或原始章节
structure_review = state.get("structure_review", {}) adjusted_chapters = state.get("adjusted_chapters", state["preliminary_chapters"])
# 应用审查建议并最终确定 # 应用最终确定逻辑(主要是确保标准章节存在)
final_chapters = self._finalize_with_review(preliminary_chapters, structure_review) final_chapters = adjusted_chapters
# 确保标准章节存在 # 确保标准章节存在
final_chapters = self._ensure_standard_chapters(final_chapters) final_chapters = self._ensure_standard_chapters(final_chapters)
@ -50,47 +50,6 @@ class FinalizeChaptersNode(BaseNode, TocNodeBase):
final_chapters=final_chapters, final_chapters=final_chapters,
should_continue=False) should_continue=False)
def _finalize_with_review(self,
preliminary_chapters: List[DocumentChapter],
structure_review: Dict[str, Any]) -> List[DocumentChapter]:
"""根据审查结果最终确定章节
Args:
preliminary_chapters: 初步章节列表
structure_review: AI审查结果
Returns:
最终章节列表
"""
final_chapters = preliminary_chapters.copy()
# 应用高优先级建议
suggestions = structure_review.get("suggestions", [])
high_priority_suggestions = [
s for s in suggestions
if s.get("priority") == "high"
]
for suggestion in high_priority_suggestions:
suggestion_type = suggestion.get("type", "")
description = suggestion.get("description", "")
if suggestion_type == "add":
logger.info(f"应用高优先级建议-添加: {description}")
# 这里可以根据具体建议内容添加章节
# 当前版本保持简单,只记录日志
elif suggestion_type == "modify":
logger.info(f"应用高优先级建议-修改: {description}")
# 这里可以根据具体建议内容修改章节
# 当前版本保持简单,只记录日志
elif suggestion_type == "reorder":
logger.info(f"应用高优先级建议-重排: {description}")
# 这里可以根据具体建议内容重新排序
# 当前版本保持简单,只记录日志
return final_chapters
def _ensure_standard_chapters(self, chapters: List[DocumentChapter]) -> List[DocumentChapter]: def _ensure_standard_chapters(self, chapters: List[DocumentChapter]) -> List[DocumentChapter]:
"""确保包含标准章节 """确保包含标准章节