#模块分层
Graph Engine 是图驱动分析的表示与计算层。智能体提出具体证据需求,Graph Engine 按工具参数访问当前图或指定数据集,执行查询、算法或学习任务,再返回可定位的对象、数值、结构和状态。构图子智能体在需要时准备或优化这些表示;已有图上的读取、统计和学习不依赖每次重新执行构图。
Graph Engine 对外提供 HTTP 与 MCP 两种适配器。Backend 通过 HTTP 访问数据与任务;智能体通过 MCP 调用图工具。graph_engine 是一级模块入口,graph_data_service 承载 HTTP、图数据与计算实现,mcp_service 负责工具协议适配。它们共享工具规格和资源读写策略。
| 层 | 模块 | 责任 |
|---|---|---|
| 协议与目录 | graph_engine/api.py · mcp.py · tool_catalog.py | 统一入口、工具 JSON Schema、执行策略元信息。 |
| 数据与版本 | graph_data_service/graph_data.py · session.py | 当前图、稳定节点/关系标识、版本切换和派生缓存。 |
| 执行调度 | graph_engine/tool_execution.py · graph_data_service/tool_scheduler.py | 资源隔离、读写冲突、排队与调度和生命周期。 |
| 构图子智能体 | graph_engine/construction/ | 源数据解析、查询工作负载、候选转换、成本验收与领域经验。 |
| 图计算与 ML | graph_data_service/compute.py · isolated_compute.py · graph_ml/ | 算法后端、隔离工作进程、训练/推理与模型文件。 |
HTTP 工具转发使用 POST /sessions/{session_id}/tools/{tool_name},请求体为 {"arguments": {...}}。GraphDataClient 注入用户、会话、Run、generation 和操作身份;这是服务间入口,浏览器使用 Backend 公共 API。构图工具另有数据集作用域适配。
#图结果如何成为分析证据
图结果需要同时保留数值或对象本身,以及解释它们所需的上下文。节点名称、关系方向、边权含义、图版本和返回范围共同决定一条结果能支持什么说明。HTTP 或 MCP 请求成功不等于业务成功;工具结果中的 success、错误、截断标记和作业终态都参与后续判断。
| 证据层次 | 相关工具 | 解释前需要核对 |
|---|---|---|
| 图与对象身份 | get_graph_info / search_node / get_node_info | 已加载状态、graph_version、真实名称与类型,以及候选是否歧义。 |
| 连接与结构 | shortest_path / graph_connectivity / node_centrality / community_detection | 方向、权重解释、图是否完整、所选算法及多重边投影规则。 |
| 模型结果 | train / infer | model_id、任务、特征来源、输入图或数据集、评估划分及完成状态。 |
| 呈现与报告 | visualize_subgraph / create_analysis_report | 展示节点是否截断,报告 source_results 是否来自已完成操作;视图和报告不改变原始证据含义。 |
版本与资源调度帮助同一批相关操作围绕正确的输入执行,但不能消除数据缺失或分析方法选择不当带来的限制。例如,有界查询范围内未找到路径不能证明全图不可达,社区分组不能直接证明业务类别,链接预测分数不能充当因果关系证据。后续各节分别说明这些数据与算法契约。
create_analysis_report 返回用于界面展示的 analysis_report.v2 报告结构,不会自动生成 Markdown 或 PDF 文件。需要下载文件时,由 Agent Runtime 工作区工具在 Ephemeral 中生成内容,再用 workspace_publish(path=..., name=...) 发布到 Derived;显式指定带扩展名的 name。这些工作区工具不属于 Graph Engine 的 72 项默认工具目录。
#图表示与关系标识
GraphData 是只读图容器。节点在计算侧使用连续整数索引,_id2node / _node2id 保存业务节点名称与整数索引的双向映射。边以 edge_index 的两行表示源和目标;节点、边的特征、类型与标签保存在独立数组中。写入流程生成新图对象或版本,而不是就地修改分析中的图。
| 字段 | 形状 / 类型 | 含义 |
|---|---|---|
| edge_index | [2, E] · int64 | 关系的源与目标节点索引。 |
| edge_weight | [E] · float32 / float64 | 可选边权;构图属性图保留 float64 数值。 |
| node_feat / edge_feat | [N, …] / [E, …] | 模型输入特征;与语义属性分离。 |
| node_type / edge_type | [N] / [E] | 异构节点与关系类型。 |
| node_label / edge_label | [N] / [E] or multi-column | 分类等监督任务的标签。 |
| graph_metadata.construction_graph | construction_graph.v1 | 保留节点/边 ID、属性及构图语义。 |
属性图可以在相同端点之间保留多条关系。查询边时,端点对与唯一 edge_id 不等价:端点只确定一组候选关系,修改或定位具体关系应使用返回的 ID。通用 append 对已构建属性图返回 UNSUPPORTED_PROPERTY_GRAPH_APPEND,避免把不同关系和属性压缩成普通边表。
构造 GraphData 时先把 edge_index 转成 int64,并要求它是两行数组;节点名称与整数索引的映射必须成对提供且彼此一致。特征默认转成 float32,训练/验证/测试掩码转成布尔数组。node_feat 的第 i 行、node_label 的第 i 项及 id2node[i] 都指向同一节点,不能分别重排。边属性也必须和 edge_index 的列顺序对齐。
import numpy as np
from graph_data_service.graph_data import GraphData
# Three named nodes and three directed edges.
graph = GraphData(
num_nodes=3,
edge_index=np.array([[0, 0, 1], [1, 2, 2]], dtype=np.int64),
id2node={0: "customer:10", 1: "order:100", 2: "region:1"},
node2id={"customer:10": 0, "order:100": 1, "region:1": 2},
edge_weight=np.array([2.0, 5.0, 1.0]),
semantic_type="directed",
)
# Columns represent customer:10 -> order:100,
# customer:10 -> region:1, and order:100 -> region:1.上例说明数组与节点名称如何对齐,不代表电商外键的默认方向。对源表构图时,记录节点与外键方向由构图模块决定;手工导入图则应由调用方明确边的业务含义。
图的业务属性与模型特征分开存储。properties.data 中的订单金额、字符串状态等不会自动变成 node_feat。单图学习缺少 node_feat 时使用归一化的 [入度, 出度, 总度, 1] 结构特征;这不等于模型读取了业务字段。图分类另使用约定数据集中的节点类别编号。构图图对象将原始属性图保存在 graph_metadata.construction_graph 中,转换、切换版本及查询时据此保留属性和关系标识。
#图的方向与边的存储方式
判断“是否有向”需要读取业务语义,不能只检查邻接数据是否对称。例如,两个账号互相关注是两条有向关系;一条无向关系也可能以正反两条记录保存。Graph Engine 用 semantic_type 表示图语义,用 storage_type 表示边记录的实际组织方式。
| 字段 | 取值 | 解释 |
|---|---|---|
| semantic_type | directed / undirected / unknown | 分别表示有向、无向和缺少足够证据;显式设置优先于数据结构推断。 |
| storage_type | directed / bidirectional / mixed | 表示单向存储、成对双向存储或混合存储;不是第二套业务关系类型。 |
| num_edge_entries / num_unique_edges | 原始记录数 / 逻辑边数 | 前者是 edge_index 列数,后者按方向语义及关系标识统计。 |
| has_self_loops / has_duplicate_entries | 结构检查标志 | 识别自环及重复条目。重复判定使用端点与关系键,不逐项比较权重、特征或属性;构图属性图使用关系 ID,普通图使用关系类型作为关系键。 |
方向解析依次考虑显式 semantic_type、元数据、预定义数据集规则及结构证据。对称结构在没有其他信息时仍可能是 unknown。Cora、CiteSeer、PubMed 使用预定义的无向语义。to_undirected 产生新图,并可能在数组中补齐反向记录;计算逻辑边数或节点度时应先去除这类存储副本。
edge_index = [[0, 1], [1, 0]]
semantic_type = "directed" -> two directed edges
semantic_type = "undirected" -> one logical undirected edge
no semantic evidence -> symmetry alone is ambiguous节点度与邻居数也不同。无向图中,一个自环对节点度贡献 2,但只对应一个不同的邻居节点;有向图分别统计入度、出度和总度。多个关系可以连接同一个邻居,因此不能用去重后的邻居数量替代关系条数。
#图版本与缓存
会话中的加载、无向转换、子图提取和回滚改变当前图版本。get_graph_info 可读取 graph_version;list_graph_versions 列出版本,load_graph_version 切换已保存版本。构图数据集另有 revision:只有显式 load_constructed_graph 后,构图结果才成为会话活动图。
算法需要的 NetworkX 表示和实体检索索引是派生数据。按图版本复用缓存可避免同一图被每个工具重复转换;版本变化后必须使用对应的新表示。缓存重建属于图读取,不应发布图版本。大图第一次转换仍有与节点、边规模相关的成本。
版本按会话隔离,图数据存为 NPZ,graph_versions.json 记录清单。普通 save_snapshot 路径按 GRAPH_VERSION_HISTORY_LIMIT 裁剪历史,默认 50;受控 publish_snapshot 路径目前不执行该裁剪。切回旧版本后,新版本号仍在历史最大值之后递增。隔离工作进程写入暂存目录,Graph Engine 父进程检查 generation 与 graph_version 后原子发布清单,并记录 Run、Job 与 generation 回执,避免过期执行覆盖新的状态。
图对象、算法转换缓存与搜索索引均以版本为边界。GraphCompute 缓存 NetworkX 表示,EntitySearchIndex 保存创建时的 graph_version;版本不一致时重建。只读缓存的创建不会增加图版本号。请求应先读取实际 graph_version,再把该值记录在实验或分析结果中,避免把不同版本的指标直接比较。
构图修订号 revision、会话 graph_version 与运行 execution_generation 各自解决不同问题:revision 防止数据集编辑覆盖较新的修改;graph_version 选择算法输入;execution_generation 防止已停止的旧运行回写结果。只提供其中一个编号,不能替代其他两类校验。
#多重边与算法语义
| 算法 | 关系投影规则 | 原因 |
|---|---|---|
| 最短路径 / 接近中心性 | minimum_weight_per_pair | 平行边取最小代价,路径结果按节点序列区分。 |
| 最大流 / s-t 最小割 | sum_weights_per_pair | 同一方向平行边的容量累加。 |
| PageRank / 特征向量 / Katz | sum_weights_per_pair | 累积连接强度。 |
| 最小生成树 | minimum_weight_undirected | 按无向端点对保留最小权重。 |
| Louvain / greedy modularity | sum_weights_undirected | 合并无向连接强度。 |
| 未审查的多重图算法 | unsupported | 明确拒绝,避免隐式丢失关系语义。 |
边权必须是有限数值,布尔值和非数值不能作为合法权重。不同算法对权重的意义不同:路径使用代价,流/割使用容量,社区和部分中心性使用强度;调用前应先明确业务解释。
转换到 NetworkX 时,普通图沿用简单图表示;构图属性图若同一端点间存在不同关系标识,则使用 MultiGraph 或 MultiDiGraph。无向图同一业务关系的反向存储副本会去重。业务 edge_id 保存在属性中,NetworkX 的内部多重边键使用边索引,因此二者不能混用。
假设 A→B 有两条平行边,权重分别为 2 和 5。最短路径把它们解释为两个可选代价,投影后使用 2;最大流把它们解释为并行容量,合计为 7。这两种结果都遵循各自的算法定义。调用方必须先确定权重含义,再选择工具,不能把交易金额默认当作距离。
未声明多重图策略的方法返回 UNSUPPORTED_MULTIGRAPH_OPERATION。参数组合也参与检查:社区发现只批准了部分算法,子图提取中的 ego_graph 与 node_subset 可以保留关系,其余方法有各自限制。这个检查发生在算法执行前,避免转换时悄悄合并关系。
#工具依赖与并发控制
每个工具声明 reads、writes、barrier、background。资源还要加上用户/会话归属:graph、models、documents 等通常按会话隔离,datasets 与 construction:{dataset_id} 按用户或数据集隔离。RAW、WAR、WAW 冲突形成有向依赖,独立读操作可以并行;未知工具按 barrier 保守执行。
load_constructed_graph (writes graph)
├─ node_centrality (reads graph)
├─ community_detection (reads graph)
└─ shortest_path (reads graph)
↓ all finished
load_graph_version (writes graph)subgraph_extraction 默认写入新版本,只有 create_version=false 才按只读调度。前置工具失败或等待审批时,依赖项返回 TOOL_DEPENDENCY_BLOCKED。模型必须在得到真实结果后再传入依赖它的 ID 或 revision;依赖调度不会补造参数。
智能体侧的 tool_dag 调度同轮调用,Graph Engine 的 tool_scheduler 控制跨请求资源调度。后台任务提交后,资源租约持续到实际工作结束;仅返回 job_id 不能释放租约。状态查询和取消绕开图写锁等待,防止任务无法停止。
跨请求调度保留冲突请求的到达顺序。假设读取 R1 已进入执行,写入 W2 随后排队,新的读取 R3 不能绕过 W2;否则持续到来的读请求会使写请求长期无法执行。不同用户的资源互不冲突,同一用户的不同会话仍可能争用 datasets 或 construction:{dataset_id} 这类共享资源。
资源依赖和计算配额是两层控制。依赖检查决定是否会读取正在变化的数据;信号量决定最多同时运行多少普通计算或训练任务。即使两个训练任务只读同一张图,也可能因 GPU、随机数状态或用户计算配额而串行执行。
#计算进程与后台任务
NetworkX 算法的兼容路径使用有界 ProcessPool,其他同步处理器可走线程执行。受控执行在具备 execution host 与 Run 身份时使用可终止的隔离 WorkerProcess,维护停止确认与最终状态持久化。取消观察不等于计算停止;容量与租约在任务真正退出后回收。
| 配置 | 默认值 | 控制对象 |
|---|---|---|
| AGENT_TOOL_MAX_CONCURRENCY | 4 (1–32) | 同轮就绪工具调用。 |
| GRAPH_COMPUTE_CONCURRENCY | min(4, cpu_count) | 普通算法进程池。 |
| GRAPH_COMPUTE_GLOBAL_CONCURRENCY | 默认跟随计算 worker 数 | 全局普通计算并发限制。 |
| GRAPH_COMPUTE_PER_USER_CONCURRENCY | min(4, worker 数) | 单用户普通计算并发限制。 |
| GRAPH_HEAVY_PER_USER_CONCURRENCY | 1 | 训练、推理等重任务。 |
| GRAPH_HEAVY_GLOBAL_CONCURRENCY | max(1, min(2, worker 数)) | 全局重任务并发限制。 |
get_job_status 返回作业状态与进度,普通作业完成结果位于 job.result;也可通过 get_job_result 读取结果。GraphMCPTools 在图工具返回后台句柄时等待最终结果,支持嵌套作业,并拒绝循环引用或缺失结果。监督学习的 infer 加载已训练模型执行前向计算,预训练的 infer 查询保存的节点向量。图分类数据绑定与当前单图绑定分开管理。
{
"success": true,
"job": {
"job_id": "JOB_ID",
"status": "done",
"progress": 1,
"result": {
"success": true
}
},
"progress_updates": []
}此结构展示 get_job_status 的字段位置;最终业务结果位于 job.result。状态值和 result 的具体字段由实际任务决定,轮询可用 after_seq 增量读取进度。
#实体检索与索引
search_node 与 search_edge 用于把用户描述解析为可传给图工具的节点或边标识。节点索引包含名称、类型、标签、入度、出度及邻居预览;语义文本优先来自 node_text,没有文本时使用名称、类型和标签。索引不会自动把 properties.data 中全部业务字段转成可检索文本。
实体语义检索依赖 Graph Engine 自己的嵌入服务配置:EMBEDDING_PROVIDER 可显式选择 sentence-transformers、openai 或 lexical。默认先尝试本地 Sentence Transformers(需要依赖与模型权重),失败后在存在 OPENAI_API_KEY 时选择 OpenAI,否则使用词法哈希。当前 OpenAI 分支调用固定的 api.openai.com/v1/embeddings;Agent Runtime 的聊天模型或 EMBEDDING_BASE_URL 配置不会自动改写这个地址。词法模式不为实体查询提供语义相似度排序;索引编码失败时也会禁用语义检索,应检查 semantic_enabled 与检索错误信息。 显式选择 openai 时必须提供 OPENAI_API_KEY;显式提供方初始化失败不会执行上述自动选择。首次实体索引会编码全图节点与边文本,远程服务的可达性、延迟和额度也影响可用性。
| 阶段 | 实现规则 |
|---|---|
| 精确匹配 | 先检查业务名称是否命中 node2id,再尝试有效整数节点索引;边可按边名称或明确端点定位。 |
| 结构过滤 | 节点支持 node_type、label 和入度/出度/总度上下界;边支持 edge_type、label 与权重上下界。 |
| 向量评分 | 可用嵌入服务生成 float32 向量,按归一化向量内积排序;默认 top_k=8,最大 50。 |
| 查询缓存 | 查询向量按查询字符串缓存,最多 128 项;向量生成在缓存锁外执行。 |
| 结果歧义 | 除 best 外还返回 candidates、ambiguous 与检索模式;多个相近候选不能只凭排序第一项确定实体。 |
当前置信度是规则分级,不是概率校准结果。精确命中对应 high;非精确查询短于 3 个字符会标记歧义;候选分数差小于 0.05 时也可能标记歧义。semantic_enabled=false 时,普通实体检索回退到精确定位和结构过滤,未匹配项的语义得分为 0,不能解释为仍在做语义相似度排序。
{
"query": "",
"filters": {
"node_type": "customer",
"min_degree": 2
},
"sort": {
"field": "degree",
"order": "desc"
},
"top_k": 10
}这是按客户类型和度数筛选候选的参数示例。不同数据集的真实 node_type 值应先通过图概览或节点信息确认。rerank 当前用于返回配置状态,没有单独运行交叉编码器重排序模型。遇到 ambiguous=true 时,可补充类型、端点或节点 ID 后重新检索。
平行关系检索还有一项限制:search_edge 按精确端点定位时只取得最后一条关系,索引中的属性读取也通过端点查找,并不完整枚举同端点关系。逐条核对和编辑应以 get_constructed_graph 返回的真实 edge_id 为准,不能仅凭 best 或 ambiguous=false 确认关系唯一。
#文档分块与上下文检索
文档检索由 LightweightRAG 管理,和图节点检索使用不同索引。upload_document 把会话文档写入文档目录,保存 doc_index.json 与 doc_embeddings.npy;retrieve_context 根据查询向量返回相关文本块。文档命中说明文本相关,不等于图中存在相应关系,分析结论仍需图工具提供证据。
默认分块长度是 512 个按空白划分的片段,相邻块重叠 128 个片段,步长为 384。这不是模型分词器的 token 数;没有空格的长中文段落不会按汉字数量切成 512 字的块。准备中文资料时应保留合理分段,并检查返回 chunks 的实际数量和内容长度。
嵌入提供方可以是本地 Sentence Transformers、远程嵌入接口或词法哈希。文档检索在词法模式下使用 384 维哈希向量;Python 哈希受进程影响,因此旧文本块会在当前进程重新编码,避免把不同进程的哈希空间混合。向量维度改变时,检索返回错误,需重建该会话文档索引。
受控文档写入通过 .rag_versions 保存候选目录,再由 rag_manifest.json 选择当前版本。这样读请求可以定位一套完整的文本索引和向量文件。返回结果中的 doc_id、chunk_id、offset 与 score 用于定位引用片段;offset 是块序号,不是原文字符偏移。
#存储与扩展边界
内存表示至少包含 [2,E] 的 int64 边索引,即约 16E 字节;普通 float32 边权另需 4E 字节,N 个节点、F 维 float32 特征另需 4NF 字节。这只是数组主体,不含字符串、Python 对象、图属性、NetworkX 对象、向量索引和工作进程副本。评估大图内存时,不能只按源文件大小估算。
结构化大图还可以走独立的分区快照路径:prepare_large_graph 流式准备已上传的边列表或三元组文件,run_large_graph_algorithm 在发布版本上提交持久化作业。该路径不会先创建完整 GraphData 或 NetworkX 图。运行前读取 get_large_graph_capabilities,核对算法的精确、近似或有界语义及资源限制;完成后获取摘要、预览和结果文件引用。分区版本与普通活动图表示有独立的读取入口,不能把大图预览当作已加载的完整会话图。
图服务当前通过 GRAPH_STORAGE_BACKEND 选择 local 或 nebula。NebulaGraph 模式需要显式启用连接;部分查询在数据库端执行,需要 NetworkX 的算法则先按能力和规模限制载入内存。默认载入上限为 50,000 个节点、200,000 条边,分别通过 NEBULA_MAX_MATERIALIZE_NODES 和 NEBULA_MAX_MATERIALIZE_EDGES 配置。materialized_scope、target_in_scope、complete_for_query 用于说明结果范围;有限跳数范围内未找到目标,不能直接解释为全图不可达。
Graph Engine 管理源文件、图版本、构图报告、领域经验和模型文件;Agent Runtime 管理消息、Run、Memory 和 Trace。图服务可以配置 NebulaGraph 存储,但不能假定所有算法都在数据库内计算:远端图查询与有界物化是不同路径。