马一丁

Add Comments

... ... @@ -598,6 +598,7 @@ class ReportAgent:
emit('stage', {'stage': 'storage_ready', 'run_dir': str(run_dir)})
# ==================== GraphRAG 初始化 ====================
# 根据配置开关决定是否启用图谱构建/查询(需 .env 设置 GRAPHRAG_ENABLED=True)
graphrag_enabled = getattr(self.config, 'GRAPHRAG_ENABLED', False)
knowledge_graph = None
graphrag_query_node = None
... ... @@ -607,6 +608,7 @@ class ReportAgent:
emit('stage', {'stage': 'graphrag_building', 'message': '正在构建知识图谱'})
try:
# 将 state_*.json + forum.log 转为结构化图谱,并立即落盘 graphrag.json
knowledge_graph = self._build_knowledge_graph(
query, normalized_reports, forum_logs, run_dir
)
... ... @@ -687,6 +689,7 @@ class ReportAgent:
'emphasis': emphasis_value
}
# 先让 GraphRAG 节点多轮查询,再把结果附加到章节上下文
graph_results = graphrag_query_node.run(
section_info,
{
... ... @@ -699,7 +702,7 @@ class ReportAgent:
)
if graph_results and graph_results.get('total_nodes', 0) > 0:
# 将图谱结果注入生成上下文
# 将图谱结果注入生成上下文,后续章节 LLM 自动使用增强提示词
chapter_context['graph_results'] = graph_results
chapter_context['graph_enhancement_prompt'] = format_graph_results_for_prompt(graph_results)
logger.info(f"章节 {section.title} GraphRAG 查询完成: {graph_results.get('total_nodes', 0)} 节点")
... ...
... ... @@ -2,6 +2,11 @@
GraphRAG 知识图谱模块
提供基于结构化数据的知识图谱构建、存储与查询功能。
典型用法:
1) 使用 `StateParser`/`ForumParser` 解析三引擎 state JSON 与 forum.log;
2) 调用 `GraphBuilder.build` 生成纯结构化的图对象;
3) 通过 `GraphStorage.save/load` 持久化或读取图数据;
4) 以 `QueryEngine` 在章节侧执行多轮图查询。
"""
from .state_parser import StateParser, ParsedState, ParsedSection, SearchRecord
... ...
... ... @@ -2,6 +2,8 @@
Forum 日志解析器
解析 forum.log 文件,提取结构化的讨论记录用于构建知识图谱。
日志与 GraphRAG 的关系:仅将主持人/三引擎发言转为结构化节点,
用于补充 Host 总结或跨引擎观点。
"""
from dataclasses import dataclass
... ... @@ -41,6 +43,7 @@ class ForumParser:
解析 forum.log,提取结构化的讨论记录。
日志格式: [HH:MM:SS] [SPEAKER] content
SPEAKER 需属于 VALID_SPEAKERS;非规范行会被忽略,确保图谱不被噪音污染。
"""
# 匹配日志行的正则表达式
... ...
... ... @@ -19,6 +19,10 @@ class GraphBuilder:
基于已有的结构化数据(State JSON、Forum 日志)构建图谱,
无需 LLM 进行实体/关系提取。
ReportAgent 在 _build_knowledge_graph 中调用本构建器,将 load_input_files
提前解析好的 ParsedState / ForumEntry 转为 Graph 对象,再交由 GraphStorage
落盘并供 GraphRAGQueryNode 查询。
节点类型(5种):
- topic: 用户查询主题
- engine: 四个引擎来源 (insight/media/query/host)
... ... @@ -108,7 +112,7 @@ class GraphBuilder:
if not search.query:
continue
# 搜索词去重
# 搜索词去重(同一段落相同查询仅保留首条,避免图谱冗余)
query_key = search.query.strip().lower()
if query_key in seen_queries:
continue
... ...
... ... @@ -107,7 +107,12 @@ class Edge:
class Graph:
"""知识图谱"""
"""
知识图谱
仅负责存储节点/边与邻接表,不依赖外部数据库,便于在章节侧内存查询。
邻接表 _adjacency 用于 QueryEngine 按深度扩展邻居节点。
"""
def __init__(self):
self._nodes: Dict[str, Node] = {}
... ... @@ -290,7 +295,13 @@ class Graph:
class GraphStorage:
"""图谱存储管理器"""
"""
图谱存储管理器
将 Graph 对象序列化为 JSON(graphrag.json),路径与 ChapterStorage 输出目录一致,
便于 Web/Report 引擎共享。支持按报告ID查找、列举最新图谱,供 Flask API 或
GraphRAGQueryNode 直接读取。
"""
FILENAME = "graphrag.json"
... ... @@ -394,6 +405,11 @@ class GraphStorage:
Returns:
图谱文件路径,未找到返回 None
工作方式:
1) 优先匹配目录名是否含 report_id(兼容 _/- 差异);
2) 否则读取 graphrag.json 内 task_id/report_id 做兜底匹配;
适配 Agent 运行目录命名不一致的场景。
"""
# 在章节目录中搜索(与 ChapterStorage 保持一致)
chapters_dir = self.chapters_dir
... ... @@ -442,6 +458,8 @@ class GraphStorage:
Returns:
最新图谱文件路径,未找到返回 None
根据文件修改时间排序,用于前端“最近一次生成”快速预览。
"""
chapters_dir = self.chapters_dir
if not chapters_dir.exists():
... ...
... ... @@ -121,6 +121,9 @@ def format_graph_results_for_prompt(graph_results: dict) -> str:
Returns:
格式化的字符串
供 ReportAgent 在章节生成前注入 `graph_enhancement_prompt`,
将多轮查询结果以结构化文本交给章节 LLM,避免直接传递大 JSON。
"""
if not graph_results:
return ""
... ...
... ... @@ -12,7 +12,15 @@ from .graph_storage import Graph, Node
@dataclass
class QueryParams:
"""查询参数"""
"""
查询参数
由 GraphRAGQueryNode 或 Flask API 注入,控制查询范围:
- keywords: 关键词列表,可为空(空时默认返回各引擎 section 摘要);
- node_types: 限定节点类型;None 表示全量;
- engine_filter: 仅保留指定引擎来源;
- depth: 匹配节点向外扩展的层级。
"""
keywords: List[str] = field(default_factory=list)
node_types: Optional[List[str]] = None # None 表示全部类型
engine_filter: Optional[List[str]] = None # 限定引擎来源
... ...
... ... @@ -3,6 +3,10 @@ State JSON 解析器
解析 Insight/Media/Query 三引擎的 State JSON 文件,
提取结构化数据用于构建知识图谱。
默认假设 state_* 文件结构与三引擎输出一致:
- 顶层包含 query/report_title/paragraphs;
- 段落内的 research.search_history 记录搜索关键词、URL与摘要。
"""
from dataclasses import dataclass, field
... ... @@ -45,6 +49,8 @@ class StateParser:
State JSON 解析器
解析三引擎的 State JSON,提取用于构建知识图谱的结构化数据。
适用于 load_input_files 阶段:先查找与 MD 同目录的 state_*.json,
若存在则转为 ParsedState 供 GraphBuilder 直接消费。
"""
def parse(self, engine_name: str, state_json: Dict[str, Any]) -> ParsedState:
... ... @@ -84,7 +90,7 @@ class StateParser:
timestamp=search.get('timestamp', '')
))
# 获取摘要,优先使用 latest_summary
# 获取摘要,优先使用 latest_summary;若缺失则回退到段落正文
summary = research.get('latest_summary', '')
if not summary:
summary = para.get('content', '')
... ... @@ -124,6 +130,7 @@ class StateParser:
根据 Markdown 报告路径查找对应的 State JSON 文件
State JSON 通常与 MD 文件在同一目录下,命名格式为 state_*.json
用于 GraphRAG:在 load_input_files 时自动匹配最新或同名 state 文件。
Args:
md_path: Markdown 文件路径
... ...
... ... @@ -133,7 +133,7 @@ class GraphRAGQueryNode(BaseNode):
for round_idx in range(max_queries):
self.log_info(f"查询轮次 {round_idx + 1}/{max_queries}")
# 1. 构建决策提示词
# 1. 构建决策提示词:将章节目标+图谱概览+查询历史一起交给 LLM
prompt = self._build_decision_prompt(
section, context, query_engine, history
)
... ... @@ -145,12 +145,12 @@ class GraphRAGQueryNode(BaseNode):
self.log_error("LLM 返回无效决策,终止查询")
break
# 3. 检查是否停止
# 3. 检查是否停止:LLM 可主动返回 should_query=false 以节省轮次
if not decision.get('should_query', False):
self.log_info(f"LLM 决定停止查询: {decision.get('reasoning', '无原因')}")
break
# 4. 执行查询
# 4. 执行查询:按 LLM 给出的参数查询本地图谱
params = QueryParams(
keywords=decision.get('keywords', []),
node_types=decision.get('node_types'),
... ... @@ -222,7 +222,12 @@ class GraphRAGQueryNode(BaseNode):
context: Dict[str, Any],
query_engine: QueryEngine,
history: QueryHistory) -> Dict[str, str]:
"""构建查询决策提示词"""
"""
构建查询决策提示词
将章节目标、模板章节概览、图谱统计、历史查询摘要整合为
system/user prompt,指导 LLM 生成下一轮 QueryParams。
"""
# 获取图谱概览
summary = query_engine.get_node_summary()
stats = summary.get('stats', {})
... ... @@ -268,7 +273,12 @@ class GraphRAGQueryNode(BaseNode):
}
def _get_query_decision(self, prompt: Dict[str, str]) -> Optional[Dict[str, Any]]:
"""调用 LLM 获取查询决策"""
"""
调用 LLM 获取查询决策
返回的 JSON 将被转换为 QueryParams;任何解析失败都会终止后续轮次,
避免章节生成被异常输出阻断。
"""
try:
response = self.llm_client.invoke(
system_prompt=prompt['system'],
... ...
... ... @@ -1307,6 +1307,8 @@ def shutdown_system():
return jsonify({'success': False, 'message': f'系统关闭异常: {exc}'}), 500
# ==================== GraphRAG API 端点 ====================
# 前端控制台与 /graph-viewer 调用,均依赖 ReportEngine 在章节目录落盘的 graphrag.json。
# 若 GRAPHRAG_ENABLED 关闭,这些接口仅返回“未找到图谱”提示。
@app.route('/api/graph/<report_id>')
def get_graph_data(report_id):
... ... @@ -1493,6 +1495,7 @@ def query_graph():
if report_id:
graph_path = storage.find_graph_by_report_id(report_id)
else:
# 未指定报告ID时默认取最近一次生成的图谱,便于快速试用
graph_path = storage.find_latest_graph()
if not graph_path or not graph_path.exists():
... ...
... ... @@ -2282,6 +2282,7 @@
query: 8503
};
// ---------------- GraphRAG 开关与配置同步 ----------------
function syncGraphragFlag(config) {
if (!config || !Object.prototype.hasOwnProperty.call(config, 'GRAPHRAG_ENABLED')) {
return;
... ... @@ -2290,6 +2291,7 @@
graphragSettingLoaded = true;
}
// 前端懒加载配置:初次访问或强制刷新时请求 /api/config,决定是否展示图谱面板
async function ensureGraphragSetting(force = false) {
if (!force && graphragSettingLoaded) {
return graphragEnabled;
... ... @@ -5151,6 +5153,7 @@ function getConsoleContainer() {
}
}
// 入口:在报告界面渲染后初始化图谱面板,若 GraphRAG 关闭则隐藏
async function initializeGraphPanel(statusData) {
const panel = document.getElementById('graphPanel');
if (!panel) return;
... ... @@ -5175,6 +5178,7 @@ function getConsoleContainer() {
refreshGraphPanel(graphPanelTaskId, true);
}
// 注册图谱面板按钮/筛选/搜索事件,只绑定一次
function bindGraphPanelEvents() {
const refreshBtn = document.getElementById('graphRefreshBtn');
const collapseBtn = document.getElementById('graphCollapseBtn');
... ... @@ -5342,6 +5346,7 @@ function getConsoleContainer() {
});
}
// 将 graphrag.json 转换的节点/边渲染成 mini 版 vis.js 图谱
function renderGraphPanel(graph, resetPlaceholder = true) {
const panel = document.getElementById('graphPanel');
const canvasWrapper = document.getElementById('graphPanelCanvas');
... ...