refactor: 架构优化 - 移除重复代码并统一工具类模式

重大架构改进:
1. 移除重复的TocGeneratorAgent实现(585行),保留模块化的TocAgent
2. 修复LLMService模块级强制初始化问题,采用标准单例模式
3. 统一所有工具类的使用模式,提高架构一致性

具体更改:
- 删除 src/bidmaster/agents/toc_generator.py(完全重复实现)
- 移除 llm.py 中的模块级实例化(llm_service = LLMService())
- 更新 tools/__init__.py 导出LLMService类而非实例
- 更新使用方改为LLMService()调用(generate_sub_chapters.py, review_structure.py)
- 清理agents/__init__.py和analysis.py中的旧版本引用

收益:
- 减少约600行重复代码
- 解决模块导入时的配置加载错误
- 实现惰性加载,配置只在使用时加载
- 提高代码可维护性和架构一致性

🤖 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 09:59:45 +08:00
parent c74ae0b418
commit fb3e704bef
7 changed files with 11 additions and 599 deletions

View File

@ -1,14 +1,12 @@
"""Agent层 - LangGraph编排"""
from .analysis import AnalysisAgent
from .toc_generator import TocGeneratorAgent
from .builders import TocAgentBuilder
from .builders.toc_builder import TocAgent
from .interaction import InteractionHandler, InteractionMode
__all__ = [
"AnalysisAgent",
"TocGeneratorAgent",
"TocAgentBuilder",
"TocAgent",
"InteractionHandler",

View File

@ -254,8 +254,8 @@ def parse_content_node(state: AnalysisAgentState) -> AnalysisAgentState:
def generate_toc_with_agent_node(state: AnalysisAgentState) -> AnalysisAgentState:
"""节点5使用TocGeneratorAgent生成目录结构"""
logger.info("开始使用TocGeneratorAgent生成目录...")
"""节点5使用TocAgent生成目录结构"""
logger.info("开始使用TocAgent生成目录...")
try:
technical_criteria = state["technical_criteria"]
@ -306,7 +306,7 @@ def generate_toc_with_agent_node(state: AnalysisAgentState) -> AnalysisAgentStat
# 更新状态
state["preliminary_chapters"] = chapters
state["technical_criteria"] = technical_criteria
state["structure_review"] = {} # TocGeneratorAgent已包含审查
state["structure_review"] = {} # TocAgent已包含审查
state["current_step"] = "generate_toc"
state["progress"] = 0.85
@ -336,7 +336,7 @@ def finalize_structure_node(state: AnalysisAgentState) -> AnalysisAgentState:
project_name=f"标书项目-{Path(state['source_file']).stem}",
scoring_criteria=technical_criteria,
deviation_items=state["deviation_items"],
chapters=preliminary_chapters, # 使用TocGeneratorAgent生成的章节
chapters=preliminary_chapters, # 使用TocAgent生成的章节
scoring_file=state["source_file"]
)

View File

@ -1,585 +0,0 @@
"""目录生成Agent - 专门负责生成标书目录结构
基于LangGraph实现的目录生成Agent负责
1. 根据评分项类别生成一级章节
2. AI智能生成二三级子标题
3. 目录结构合理性审查与优化
"""
import asyncio
import json
import logging
from typing import List, Dict, Any, TypedDict, Optional
from langgraph.graph import StateGraph, END
from pydantic import BaseModel, Field
from ..tools.parser import ScoringCriteria, DocumentChapter
from ..tools.llm import llm_service
from ..config import get_settings
logger = logging.getLogger(__name__)
# 统一的类别名称映射
CATEGORY_NAMES = {
"compliance": "合规响应",
"technical_solution": "技术方案",
"equipment_spec": "设备规格",
"quality_safety": "质量安全",
"after_sales": "售后服务",
"implementation": "实施方案"
}
class TocGeneratorState(TypedDict):
"""目录生成Agent的状态定义"""
# 输入参数
technical_criteria: List[ScoringCriteria]
generation_mode: str # "ai" 或 "template"
template_file: Optional[str]
# 执行状态
current_step: str
should_continue: bool
# 中间数据
category_groups: Dict[str, List[ScoringCriteria]] # 按类别分组的评分项
preliminary_chapters: List[DocumentChapter] # 初步生成的章节
structure_review: Dict[str, Any] # AI审查结果
# 最终输出
final_chapters: List[DocumentChapter]
# 错误处理
error: str
warnings: List[str]
class TocGeneratorResult(BaseModel):
"""目录生成结果"""
success: bool = Field(description="是否执行成功")
chapters: List[DocumentChapter] = Field(default_factory=list, description="生成的章节结构")
error_message: Optional[str] = Field(default=None, description="错误信息")
warnings: List[str] = Field(default_factory=list, description="警告信息")
# ========== LangGraph节点函数 ==========
def group_criteria_node(state: TocGeneratorState) -> TocGeneratorState:
"""节点1按类别对评分项分组"""
logger.info("开始对评分项按类别分组...")
try:
technical_criteria = state["technical_criteria"]
if not technical_criteria:
raise ValueError("缺少技术评分项")
# 按类别分组
category_groups = {
"technical_solution": [],
"equipment_spec": [],
"implementation": [],
"quality_safety": [],
"after_sales": [],
"compliance": []
}
for criteria in technical_criteria:
category_key = criteria.category.value
if category_key in category_groups:
category_groups[category_key].append(criteria)
else:
category_groups["technical_solution"].append(criteria)
# 只保留有评分项的类别
category_groups = {k: v for k, v in category_groups.items() if v}
logger.info(f"分组完成: {len(category_groups)}个类别")
state["category_groups"] = category_groups
state["current_step"] = "group_criteria"
except Exception as e:
logger.error(f"评分项分组失败: {e}")
state["error"] = str(e)
state["should_continue"] = False
return state
def generate_first_level_node(state: TocGeneratorState) -> TocGeneratorState:
"""节点2生成一级章节"""
logger.info("开始生成一级章节...")
try:
category_groups = state["category_groups"]
technical_criteria = state["technical_criteria"]
chapters = []
chapter_index = 1
# 按原始顺序确定章节顺序
def get_category_first_index(category):
for i, criteria in enumerate(technical_criteria):
if criteria.category.value == category:
return i
return 999
sorted_categories = sorted(category_groups.keys(), key=get_category_first_index)
# 为每个类别创建一级章节
for category in sorted_categories:
criteria_list = category_groups[category]
if not criteria_list:
continue
total_score = sum(c.max_score for c in criteria_list)
chapter_id = f"chapter_{chapter_index:02d}_{category}"
category_name = CATEGORY_NAMES.get(category, category)
chapter_title = category_name
if total_score > 0:
chapter_title += f" ({total_score}分)"
chapter = DocumentChapter(
id=chapter_id,
title=chapter_title,
level=1,
score=total_score,
template_placeholder=f"{{{{{chapter_id}_content}}}}"
)
chapters.append(chapter)
chapter_index += 1
logger.info(f"生成{len(chapters)}个一级章节")
state["preliminary_chapters"] = chapters
state["current_step"] = "generate_first_level"
except Exception as e:
logger.error(f"一级章节生成失败: {e}")
state["error"] = str(e)
state["should_continue"] = False
return state
def generate_sub_chapters_node(state: TocGeneratorState) -> TocGeneratorState:
"""节点3生成二三级子标题"""
logger.info("开始生成子标题...")
try:
preliminary_chapters = state["preliminary_chapters"]
technical_criteria = state["technical_criteria"]
generation_mode = state.get("generation_mode", "ai")
template_file = state.get("template_file")
enhanced_chapters = []
for chapter in preliminary_chapters:
# 找到该章节对应的评分项
# 从章节ID提取类别
if "_" in chapter.id:
parts = chapter.id.split("_")
if len(parts) >= 3:
category = "_".join(parts[2:])
# 找到该类别的所有评分项
corresponding_criteria = [
c for c in technical_criteria
if c.category.value == category
]
if corresponding_criteria:
if generation_mode == "ai":
sub_chapters = _generate_ai_sub_chapters(corresponding_criteria, chapter)
else:
sub_chapters = _generate_template_sub_chapters(
corresponding_criteria[0], chapter, template_file
)
chapter.children = sub_chapters
logger.info(f"章节 {chapter.title} 生成了 {len(sub_chapters)} 个子标题")
enhanced_chapters.append(chapter)
state["preliminary_chapters"] = enhanced_chapters
state["current_step"] = "generate_sub_chapters"
except Exception as e:
logger.error(f"子标题生成失败: {e}")
state["error"] = str(e)
state["should_continue"] = False
return state
def review_structure_node(state: TocGeneratorState) -> TocGeneratorState:
"""节点4AI审查目录结构"""
logger.info("开始AI审查目录结构...")
try:
technical_criteria = state["technical_criteria"]
preliminary_chapters = state["preliminary_chapters"]
# 构建审查提示词
criteria_summary = _format_criteria_for_review(technical_criteria)
chapters_summary = _format_chapters_for_review(preliminary_chapters)
review_prompt = f"""
请审查这个标书目录结构的合理性和完整性
技术评分项分布:
{criteria_summary}
当前生成的章节结构:
{chapters_summary}
审查要求:
1. 是否缺少重要的标准章节
2. 章节顺序是否合理
3. 每个评分项是否都有对应章节
返回JSON格式
{{
"overall_assessment": "总体评价",
"suggestions": [
{{"type": "add/modify/reorder", "description": "建议内容", "priority": "high/medium/low"}}
],
"optimization_score": 85
}}
只返回JSON"""
# 使用统一的LLM服务
response = llm_service.call(review_prompt)
if response:
try:
clean_response = response.strip()
if clean_response.startswith("```json"):
clean_response = clean_response[7:]
if clean_response.endswith("```"):
clean_response = clean_response[:-3]
review_result = json.loads(clean_response.strip())
except json.JSONDecodeError:
review_result = {"overall_assessment": "解析失败", "suggestions": []}
else:
review_result = {"overall_assessment": "AI审查失败", "suggestions": []}
state["structure_review"] = review_result
state["current_step"] = "review_structure"
# 根据审查结果添加警告
if review_result.get("suggestions"):
state["warnings"].append(f"AI审查: {len(review_result['suggestions'])}条优化建议")
except Exception as e:
logger.error(f"AI审查失败: {e}")
state["warnings"].append(f"AI审查跳过: {str(e)}")
state["structure_review"] = {}
return state
def finalize_chapters_node(state: TocGeneratorState) -> TocGeneratorState:
"""节点5最终确定章节结构"""
logger.info("最终确定章节结构...")
try:
preliminary_chapters = state["preliminary_chapters"]
structure_review = state.get("structure_review", {})
final_chapters = preliminary_chapters # 直接使用,不做防护编程
# 应用高优先级建议
suggestions = structure_review.get("suggestions", [])
for suggestion in suggestions:
if suggestion.get("priority") == "high":
# 这里可以根据建议类型进行调整
logger.info(f"应用高优先级建议: {suggestion.get('description')}")
# 确保评标索引表存在(作为第一章)
has_index = any("评标索引" in ch.title for ch in final_chapters)
if not has_index:
index_chapter = DocumentChapter(
id="evaluation_index",
title="评标索引表(技术评分完全对应)", # 不包含编号
level=1,
template_placeholder="{{evaluation_index_content}}"
)
final_chapters.insert(0, index_chapter)
state["final_chapters"] = final_chapters
state["current_step"] = "finalize"
state["should_continue"] = False
logger.info(f"最终生成{len(final_chapters)}个章节")
except Exception as e:
logger.error(f"章节最终确定失败: {e}")
state["error"] = str(e)
state["should_continue"] = False
return state
# ========== 辅助函数 ==========
def _generate_ai_sub_chapters(criteria_list: List[ScoringCriteria],
parent_chapter: DocumentChapter) -> List[DocumentChapter]:
"""AI生成子标题"""
try:
criteria_info = []
for criteria in criteria_list:
criteria_info.append(f"- {criteria.item_name} ({criteria.max_score}分)")
prompt = f"""
为以下大类别生成专业的标书子标题
大类别: {parent_chapter.title}
评分项:
{chr(10).join(criteria_info)}
生成要求
1. 为每个评分项生成对应的子标题名称不要包含编号
2. 重要评分项可添加三级子标题不要包含编号
3. 只返回标题文本编号由Word自动管理
返回JSON格式
{{
"sub_chapters": [
{{"title": "技术架构设计", "level": 2, "score": 5, "children": []}}
]
}}
只返回JSON"""
# 使用统一的LLM服务
response = llm_service.call(prompt)
if not response:
raise ValueError("AI生成子标题失败: API无响应")
try:
clean_response = response.strip()
if clean_response.startswith("```json"):
clean_response = clean_response[7:]
if clean_response.endswith("```"):
clean_response = clean_response[:-3]
result_data = json.loads(clean_response.strip())
sub_chapters_data = result_data.get("sub_chapters", [])
sub_chapters = []
for i, sub_data in enumerate(sub_chapters_data, 1):
# 直接使用标题文本,不添加编号
title = sub_data.get("title", f"子标题{i}")
sub_chapter = DocumentChapter(
id=f"{parent_chapter.id}_sub_{i:02d}",
title=title, # 不添加编号
level=sub_data.get("level", 2),
score=sub_data.get("score", 0),
template_placeholder=f"{{{{{parent_chapter.id}_sub_{i:02d}_content}}}}"
)
# 处理三级标题
for j, child_data in enumerate(sub_data.get("children", []), 1):
child_title = child_data.get("title", f"三级标题{j}")
child_chapter = DocumentChapter(
id=f"{parent_chapter.id}_sub_{i:02d}_{j:02d}",
title=child_title, # 不添加编号
level=child_data.get("level", 3),
template_placeholder=f"{{{{{parent_chapter.id}_sub_{i:02d}_{j:02d}_content}}}}"
)
sub_chapter.children.append(child_chapter)
sub_chapters.append(sub_chapter)
return sub_chapters
except (json.JSONDecodeError, KeyError) as e:
logger.error(f"解析AI响应失败: {e}")
raise ValueError(f"解析AI响应失败: {e}")
except Exception as e:
logger.error(f"AI生成子标题失败: {e}")
raise # 立即失败,不掩盖错误
def _generate_template_sub_chapters(criteria: ScoringCriteria,
parent_chapter: DocumentChapter,
template_file: Optional[str]) -> List[DocumentChapter]:
"""基于模板生成子标题"""
# 提供默认结构(不包含编号)
default_sub_chapters = [
DocumentChapter(
id=f"{parent_chapter.id}_def_01",
title="方案概述", # 不包含编号
level=2,
template_placeholder=f"{{{{{parent_chapter.id}_def_01_content}}}}"
),
DocumentChapter(
id=f"{parent_chapter.id}_def_02",
title="具体实施", # 不包含编号
level=2,
template_placeholder=f"{{{{{parent_chapter.id}_def_02_content}}}}"
),
DocumentChapter(
id=f"{parent_chapter.id}_def_03",
title="保障措施", # 不包含编号
level=2,
template_placeholder=f"{{{{{parent_chapter.id}_def_03_content}}}}"
)
]
return default_sub_chapters
def _format_criteria_for_review(technical_criteria: List[ScoringCriteria]) -> str:
"""格式化评分项用于审查"""
lines = []
category_groups = {}
for criteria in technical_criteria:
category = criteria.category.value
if category not in category_groups:
category_groups[category] = []
category_groups[category].append(criteria)
for category, items in category_groups.items():
category_name = CATEGORY_NAMES.get(category, category)
lines.append(f"{category_name}】({len(items)}项):")
for item in items:
lines.append(f" - {item.item_name} ({item.max_score}分)")
return "\n".join(lines)
def _format_chapters_for_review(chapters: List[DocumentChapter]) -> str:
"""格式化章节用于审查"""
lines = []
for chapter in chapters:
lines.append(chapter.title)
for sub in chapter.children:
lines.append(f" {sub.title}")
for child in sub.children:
lines.append(f" {child.title}")
return "\n".join(lines)
# ========== 条件判断 ==========
def should_continue(state: TocGeneratorState) -> str:
"""判断是否继续"""
if not state.get("should_continue", True) or state.get("error"):
return "end"
return "continue"
class TocGeneratorAgent:
"""目录生成Agent"""
def __init__(self):
self.settings = get_settings()
self.graph = self._build_graph()
def _build_graph(self) -> StateGraph:
"""构建工作流"""
workflow = StateGraph(TocGeneratorState)
# 添加节点
workflow.add_node("group_criteria", group_criteria_node)
workflow.add_node("generate_first_level", generate_first_level_node)
workflow.add_node("generate_sub_chapters", generate_sub_chapters_node)
workflow.add_node("review_structure", review_structure_node)
workflow.add_node("finalize_chapters", finalize_chapters_node)
# 设置入口
workflow.set_entry_point("group_criteria")
# 添加边
workflow.add_conditional_edges(
"group_criteria",
should_continue,
{"continue": "generate_first_level", "end": END}
)
workflow.add_conditional_edges(
"generate_first_level",
should_continue,
{"continue": "generate_sub_chapters", "end": END}
)
workflow.add_conditional_edges(
"generate_sub_chapters",
should_continue,
{"continue": "review_structure", "end": END}
)
workflow.add_conditional_edges(
"review_structure",
should_continue,
{"continue": "finalize_chapters", "end": END}
)
workflow.add_edge("finalize_chapters", END)
return workflow.compile()
async def generate(self,
technical_criteria: List[ScoringCriteria],
generation_mode: str = "ai",
template_file: Optional[str] = None) -> TocGeneratorResult:
"""生成目录结构"""
logger.info("开始执行TocGeneratorAgent")
# 初始化状态
initial_state = TocGeneratorState(
technical_criteria=technical_criteria,
generation_mode=generation_mode,
template_file=template_file,
current_step="",
should_continue=True,
category_groups={},
preliminary_chapters=[],
structure_review={},
final_chapters=[],
error="",
warnings=[]
)
try:
# 执行工作流
final_state = await self.graph.ainvoke(initial_state)
if final_state.get("error"):
return TocGeneratorResult(
success=False,
error_message=final_state["error"],
warnings=final_state.get("warnings", [])
)
return TocGeneratorResult(
success=True,
chapters=final_state["final_chapters"],
warnings=final_state.get("warnings", [])
)
except Exception as e:
logger.error(f"TocGeneratorAgent执行异常: {e}")
return TocGeneratorResult(
success=False,
error_message=str(e)
)
def generate_sync(self,
technical_criteria: List[ScoringCriteria],
generation_mode: str = "ai",
template_file: Optional[str] = None) -> TocGeneratorResult:
"""同步接口"""
return asyncio.run(self.generate(technical_criteria, generation_mode, template_file))

View File

@ -9,7 +9,7 @@ from typing import Dict, List, Any, Optional
from ..base import BaseNode, NodeContext
from ...tools.parser import ScoringCriteria, DocumentChapter
from ...tools.llm import llm_service
from ...tools.llm import LLMService
logger = logging.getLogger(__name__)
@ -168,7 +168,7 @@ class GenerateSubChaptersNode(BaseNode):
只返回JSON"""
# 使用统一的LLM服务
response = llm_service.call(prompt)
response = LLMService().call(prompt)
if not response:
raise ValueError("AI生成子标题失败: API无响应")

View File

@ -9,7 +9,7 @@ from typing import Dict, List, Any
from ..base import BaseNode, NodeContext
from ...tools.parser import ScoringCriteria, DocumentChapter
from ...tools.llm import llm_service
from ...tools.llm import LLMService
logger = logging.getLogger(__name__)
@ -114,7 +114,7 @@ class ReviewStructureNode(BaseNode):
try:
# 使用统一的LLM服务
response = llm_service.call(review_prompt)
response = LLMService().call(review_prompt)
if response:
return self._parse_review_response(response)

View File

@ -1,13 +1,13 @@
"""工具层 - 原子化工具集"""
from .llm import llm_service
from .llm import LLMService
from .parser import BidParser
from .rag import RAGTool
from .table import TableGenerator
from .word import WordProcessor
__all__ = [
"llm_service",
"LLMService",
"BidParser",
"RAGTool",
"TableGenerator",

View File

@ -65,5 +65,4 @@ class LLMService:
raise # 立即失败
# 全局单例
llm_service = LLMService()
# 移除模块级实例化使用标准单例模式LLMService()