Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,21 @@ Markdown 可视化用于在报告正文中以 Mermaid 文本图表表达结构
- 同一章节可以插入多张 Mermaid 图表;多图来源于章节内多个高数据密度候选资料,而不是正文生成后的二次补图。
- 插入到报告正文的 Mermaid 图表会带有系统管理的居中图题,并在图题中保留对应 citation。
- 章节正文写作 Prompt 不允许模型直接输出 Mermaid 代码围栏、图表代码或手写图块;该约束不依赖 `visualization_enable`,因此 VLM 图表开启、Mermaid 可视化关闭时也不会允许正文草稿混入未受控 Mermaid。
- VLM 图表开启时,如果正文草稿仍意外包含未受控 Mermaid 代码块,VLM 节点会在进入溯源前清理这些代码块;该兜底仅作用于 VLM 链路,不影响本文档描述的受控 Markdown Mermaid 可视化图表。
- 若某个候选资料抽取、归一化、合规校验或 Mermaid 生成失败,该候选会被跳过;系统不会使用本地正则从正文中硬抽图表数据。
- 在进入多轮 LLM 生图链路前,系统会对候选资料做轻量预算控制:优先尝试数值结构清晰、来源可追溯、数据密度高且与其他候选不重复的资料;简短报告默认减少尝试数,用户明确要求图文并茂或多图时会适度放宽。
- 当章节只有一张有效 Mermaid 图表时,插入位置规划使用本地规则完成,优先插到匹配 citation 的正文行后;只有多图场景才调用 LLM 规划多张图的相对位置。

## 性能边界

Markdown 可视化会触发多轮 LLM 调用,因此当前实现只保留正文生成前的主链路:

1. 从章节的 `classified_content` 中选择数据密度较高的资料。
2. 对每个候选资料执行图表数据抽取、校验、单位归一化和 Mermaid 生成。
3. 子报告正文生成完成后,只执行插入位置规划和 Mermaid 片段渲染。
2. 对候选资料做轻量排序和预算控制,排序依据是数据密度、原始资料中的数值数量、来源/citation 可追溯性,以及候选之间的重复度。
3. 对预算内候选资料执行图表数据抽取、校验、单位归一化和 Mermaid 生成。
4. 子报告正文生成完成后,只执行插入位置规划和 Mermaid 片段渲染;单图场景本地确定插入位置,多图场景再请求 LLM 规划。

当前实现不在正文写完后再次扫描草稿正文、生成候选、重跑图表抽取或执行重复数据去重预算控制
当前实现不在正文写完后再次扫描草稿正文、生成候选或重跑图表抽取。预算控制发生在正文生成前的主可视化链路中,用于减少低价值候选进入昂贵 LLM 步骤

## 关键代码路径

Expand All @@ -56,13 +60,14 @@ Markdown 可视化会触发多轮 LLM 调用,因此当前实现只保留正文
## 核心流程

1. 报告生成阶段根据 `classified_content` 的数据密度选择适合可视化的章节资料。
2. 根据章节标题和章节大纲推断期望图型;该结果只作为软约束,不能覆盖真实数据形态。
3. LLM 从候选原始资料中抽取图表标题、类型、records 和单位。
4. 抽取结果通过 schema 校验;混合单位、空 records、字段缺失等结果会被拒绝。
5. 对需要数值单位的图表执行单位归一化。
6. 根据图表类型生成 Mermaid 片段。
7. 合规校验确认 Mermaid 语法、图表类型、数据一致性、可读性和引用上下文满足要求。
8. 子报告正文生成完成后,系统请求插入位置规划,将已生成的 Mermaid 片段插入正文,并在图题中保留 citation。
2. 系统在本地对候选做去重、排序和预算控制,避免低价值或重复候选进入多轮 LLM 生图链路。
3. 根据章节标题和章节大纲推断期望图型;该结果只作为软约束,不能覆盖真实数据形态。
4. LLM 从预算内候选原始资料中抽取图表标题、类型、records 和单位。
5. 抽取结果通过 schema 校验;混合单位、空 records、字段缺失等结果会被拒绝。
6. 对需要数值单位的图表执行单位归一化。
7. 根据图表类型生成 Mermaid 片段。
8. 合规校验确认 Mermaid 语法、图表类型、数据一致性、可读性和引用上下文满足要求。
9. 子报告正文生成完成后,系统将已生成的 Mermaid 片段插入正文,并在图题中保留 citation。单图章节优先使用本地 citation 锚点插入;多图章节请求 LLM 规划多张图的位置。

## 数据契约与依赖

Expand Down
101 changes: 101 additions & 0 deletions deepsearch/openjiuwen_deepsearch/algorithm/chart_generation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
import logging
import json
import re
from typing import List, Dict, NamedTuple, Optional
import base64

Expand All @@ -17,6 +18,106 @@

logger = logging.getLogger(__name__)
MAX_LLM_RETRY_TIMES = 3
MERMAID_START_PATTERNS = (
re.compile(r"^graph\s+(td|tb|bt|rl|lr)\b"),
re.compile(r"^flowchart\s+(td|tb|bt|rl|lr)\b"),
re.compile(r"^sequencediagram\b"),
re.compile(r"^classdiagram(?:-v2)?\b"),
re.compile(r"^statediagram(?:-v2)?\b"),
re.compile(r"^erdiagram\b"),
re.compile(r"^journey\b"),
re.compile(r"^gantt\b"),
re.compile(r"^pie\b"),
re.compile(r"^timeline\b"),
re.compile(r"^mindmap\b"),
re.compile(r"^quadrantchart\b"),
re.compile(r"^xychart-beta\b"),
re.compile(r"^sankey-beta\b"),
)


def _looks_like_mermaid_body(body_lines: List[str]) -> bool:
content_lines = [line.strip().lower() for line in body_lines if line.strip()]
if content_lines and content_lines[0] == "---":
try:
end_index = content_lines[1:].index("---") + 1
content_lines = content_lines[end_index + 1:]
except ValueError:
return False
if not content_lines:
return False
first_line = content_lines[0]
return any(pattern.match(first_line) for pattern in MERMAID_START_PATTERNS)


def remove_mermaid_code_blocks(markdown_text: str) -> str:
"""Remove fenced Mermaid source blocks that should not appear as report body text."""
if not markdown_text:
return markdown_text

lines = markdown_text.splitlines(keepends=True)
output_lines = []
index = 0
removed_block = False

while index < len(lines):
stripped = lines[index].strip()

if lines[index].startswith((" ", "\t")):
body_start = index
body_end = body_start
body_lines = []
while body_end < len(lines):
current_line = lines[body_end]
if not current_line.strip():
body_lines.append("")
body_end += 1
elif current_line.startswith(" "):
body_lines.append(current_line[4:])
body_end += 1
elif current_line.startswith("\t"):
body_lines.append(current_line[1:])
body_end += 1
else:
break

if _looks_like_mermaid_body(body_lines):
removed_block = True
index = body_end
continue

output_lines.extend(lines[body_start:body_end])
index = body_end
continue

fence_match = re.match(r"^(```+|~~~+)\s*([^`]*)$", stripped)
if not fence_match:
output_lines.append(lines[index])
index += 1
continue

fence = fence_match.group(1)
language = fence_match.group(2).strip().lower()
body_start = index + 1
body_end = body_start
while body_end < len(lines) and not lines[body_end].strip().startswith(fence):
body_end += 1

body_lines = lines[body_start:body_end]
is_mermaid = "mermaid" in language or _looks_like_mermaid_body(body_lines)
if is_mermaid:
removed_block = True
index = body_end + 1 if body_end < len(lines) else body_end
continue

output_lines.extend(lines[index:body_end + 1])
index = body_end + 1

if not removed_block:
return markdown_text

cleaned_text = "".join(output_lines)
return re.sub(r"\n{3,}", "\n\n", cleaned_text).strip()


def type_check(result, expected_type):
Expand Down
Loading