Cleaning Data Returned by Report Engine's LLM

This commit is contained in:
马一丁
2025-11-17 15:39:02 +08:00
parent 26c133c998
commit 50b6ab403e
6 changed files with 985 additions and 64 deletions
+32 -13
View File
@@ -14,6 +14,7 @@ from ..prompts import (
SYSTEM_PROMPT_DOCUMENT_LAYOUT,
build_document_layout_prompt,
)
from ..utils.json_parser import RobustJSONParser, JSONParseError
from .base_node import BaseNode
@@ -27,6 +28,12 @@ class DocumentLayoutNode(BaseNode):
def __init__(self, llm_client):
"""记录LLM客户端并设置节点名字,供BaseNode日志使用"""
super().__init__(llm_client, "DocumentLayoutNode")
# 初始化鲁棒JSON解析器,启用所有修复策略
self.json_parser = RobustJSONParser(
enable_json_repair=True,
enable_llm_repair=False, # 可以根据需要启用LLM修复
max_repair_attempts=3,
)
def run(
self,
@@ -82,8 +89,14 @@ class DocumentLayoutNode(BaseNode):
"""
解析LLM返回的JSON文本,若失败则抛出友好错误。
使用鲁棒JSON解析器进行多重修复尝试:
1. 清理markdown标记和思考内容
2. 本地语法修复(括号平衡、逗号补全、控制字符转义等)
3. 使用json_repair库进行高级修复
4. 可选的LLM辅助修复
参数:
raw: LLM原始返回字符串,允许带```包裹。
raw: LLM原始返回字符串,允许带```包裹、思考内容等
返回:
dict: 结构化的设计稿。
@@ -91,19 +104,25 @@ class DocumentLayoutNode(BaseNode):
异常:
ValueError: 当响应为空或JSON解析失败时抛出。
"""
cleaned = raw.strip()
if cleaned.startswith("```json"):
cleaned = cleaned[7:]
if cleaned.startswith("```"):
cleaned = cleaned[3:]
if cleaned.endswith("```"):
cleaned = cleaned[:-3]
cleaned = cleaned.strip()
if not cleaned:
raise ValueError("文档设计LLM返回空内容")
try:
return json.loads(cleaned)
except json.JSONDecodeError as exc:
result = self.json_parser.parse(
raw,
context_name="文档设计",
expected_keys=["title", "toc", "hero"],
)
# 验证关键字段的类型
if not isinstance(result.get("title"), str):
logger.warning("文档设计缺少title字段或类型错误,使用默认值")
result.setdefault("title", "未命名报告")
if not isinstance(result.get("toc"), (list, dict)):
logger.warning("文档设计缺少toc字段或类型错误,使用空列表")
result.setdefault("toc", [])
if not isinstance(result.get("hero"), dict):
logger.warning("文档设计缺少hero字段或类型错误,使用空对象")
result.setdefault("hero", {})
return result
except JSONParseError as exc:
# 转换为原有的异常类型以保持向后兼容
raise ValueError(f"文档设计JSON解析失败: {exc}") from exc
+21 -34
View File
@@ -12,6 +12,7 @@ from loguru import logger
from .base_node import BaseNode
from ..prompts import SYSTEM_PROMPT_TEMPLATE_SELECTION
from ..utils.json_parser import RobustJSONParser, JSONParseError
class TemplateSelectionNode(BaseNode):
@@ -25,13 +26,19 @@ class TemplateSelectionNode(BaseNode):
def __init__(self, llm_client, template_dir: str = "ReportEngine/report_template"):
"""
初始化模板选择节点
Args:
llm_client: LLM客户端
template_dir: 模板目录路径
"""
super().__init__(llm_client, "TemplateSelectionNode")
self.template_dir = template_dir
# 初始化鲁棒JSON解析器,启用所有修复策略
self.json_parser = RobustJSONParser(
enable_json_repair=True,
enable_llm_repair=False,
max_repair_attempts=3,
)
def run(self, input_data: Dict[str, Any], **kwargs) -> Dict[str, Any]:
"""
@@ -137,20 +144,22 @@ class TemplateSelectionNode(BaseNode):
# 调用LLM
response = self.llm_client.stream_invoke_to_string(SYSTEM_PROMPT_TEMPLATE_SELECTION, user_message)
# 检查响应是否为空
if not response or not response.strip():
logger.error("LLM返回空响应")
return None
logger.info(f"LLM原始响应: {response}")
# 尝试解析JSON响应
# 尝试解析JSON响应,使用鲁棒解析器
try:
# 清理响应文本
cleaned_response = self._clean_llm_response(response)
result = json.loads(cleaned_response)
result = self.json_parser.parse(
response,
context_name="模板选择",
expected_keys=["template_name", "selection_reason"],
)
# 验证选择的模板是否存在
selected_template_name = result.get('template_name', '')
for template in available_templates:
@@ -161,38 +170,16 @@ class TemplateSelectionNode(BaseNode):
'template_content': template['content'],
'selection_reason': result.get('selection_reason', 'LLM智能选择')
}
logger.error(f"LLM选择的模板不存在: {selected_template_name}")
return None
except json.JSONDecodeError as e:
except JSONParseError as e:
logger.error(f"JSON解析失败: {str(e)}")
# 尝试从文本响应中提取模板信息
return self._extract_template_from_text(response, available_templates)
def _clean_llm_response(self, response: str) -> str:
"""
清理LLM响应。
去掉 ```json``` 包裹以及前后空白,方便 `json.loads`。
参数:
response: LLM原始响应。
返回:
str: 适合直接做JSON解析的纯文本。
"""
# 移除可能的markdown代码块标记
if '```json' in response:
response = response.split('```json')[1].split('```')[0]
elif '```' in response:
response = response.split('```')[1].split('```')[0]
# 移除前后空白
response = response.strip()
return response
def _extract_template_from_text(self, response: str, available_templates: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""
从文本响应中提取模板信息。
+32 -13
View File
@@ -14,6 +14,7 @@ from ..prompts import (
SYSTEM_PROMPT_WORD_BUDGET,
build_word_budget_prompt,
)
from ..utils.json_parser import RobustJSONParser, JSONParseError
from .base_node import BaseNode
@@ -27,6 +28,12 @@ class WordBudgetNode(BaseNode):
def __init__(self, llm_client):
"""仅记录LLM客户端引用,方便run阶段发起请求"""
super().__init__(llm_client, "WordBudgetNode")
# 初始化鲁棒JSON解析器,启用所有修复策略
self.json_parser = RobustJSONParser(
enable_json_repair=True,
enable_llm_repair=False, # 可以根据需要启用LLM修复
max_repair_attempts=3,
)
def run(
self,
@@ -79,8 +86,14 @@ class WordBudgetNode(BaseNode):
"""
将LLM输出的JSON文本转为字典,失败时提示规划异常。
使用鲁棒JSON解析器进行多重修复尝试:
1. 清理markdown标记和思考内容
2. 本地语法修复(括号平衡、逗号补全、控制字符转义等)
3. 使用json_repair库进行高级修复
4. 可选的LLM辅助修复
参数:
raw: LLM返回值,可能包含```包裹。
raw: LLM返回值,可能包含```包裹、思考内容等
返回:
dict: 合法的篇幅规划JSON。
@@ -88,19 +101,25 @@ class WordBudgetNode(BaseNode):
异常:
ValueError: 当响应为空或JSON解析失败时抛出。
"""
cleaned = raw.strip()
if cleaned.startswith("```json"):
cleaned = cleaned[7:]
if cleaned.startswith("```"):
cleaned = cleaned[3:]
if cleaned.endswith("```"):
cleaned = cleaned[:-3]
cleaned = cleaned.strip()
if not cleaned:
raise ValueError("篇幅规划LLM返回空内容")
try:
return json.loads(cleaned)
except json.JSONDecodeError as exc:
result = self.json_parser.parse(
raw,
context_name="篇幅规划",
expected_keys=["totalWords", "globalGuidelines", "chapters"],
)
# 验证关键字段的类型
if not isinstance(result.get("totalWords"), (int, float)):
logger.warning("篇幅规划缺少totalWords字段或类型错误,使用默认值")
result.setdefault("totalWords", 10000)
if not isinstance(result.get("globalGuidelines"), list):
logger.warning("篇幅规划缺少globalGuidelines字段或类型错误,使用空列表")
result.setdefault("globalGuidelines", [])
if not isinstance(result.get("chapters"), (list, dict)):
logger.warning("篇幅规划缺少chapters字段或类型错误,使用空列表")
result.setdefault("chapters", [])
return result
except JSONParseError as exc:
# 转换为原有的异常类型以保持向后兼容
raise ValueError(f"篇幅规划JSON解析失败: {exc}") from exc