mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-26 00:46:22 +02:00
Structure the tech specs directory (#836)
Tech spec some subdirectories for different languages
This commit is contained in:
parent
48da6c5f8b
commit
e7efb673ef
423 changed files with 0 additions and 0 deletions
135
docs/tech-specs/zh-cn/__TEMPLATE.zh-cn.md
Normal file
135
docs/tech-specs/zh-cn/__TEMPLATE.zh-cn.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
layout: default
|
||||
title: "命令行加载知识的技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 命令行加载知识的技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了用于将知识加载到 TrustGraph 的命令行接口,使用户能够通过命令行工具从各种来源导入数据。该集成支持四个主要用例:
|
||||
|
||||
1. **[用例 1]**: [描述]
|
||||
2. **[用例 2]**: [描述]
|
||||
3. **[用例 3]**: [描述]
|
||||
4. **[用例 4]**: [描述]
|
||||
|
||||
## 目标
|
||||
|
||||
- **[目标 1]**: [描述]
|
||||
- **[目标 2]**: [描述]
|
||||
- **[目标 3]**: [描述]
|
||||
- **[目标 4]**: [描述]
|
||||
- **[目标 5]**: [描述]
|
||||
- **[目标 6]**: [描述]
|
||||
- **[目标 7]**: [描述]
|
||||
- **[目标 8]**: [描述]
|
||||
|
||||
## 背景
|
||||
|
||||
[描述当前状态以及本规范所解决的限制]
|
||||
|
||||
当前的限制包括:
|
||||
- [限制 1]
|
||||
- [限制 2]
|
||||
- [限制 3]
|
||||
- [限制 4]
|
||||
|
||||
本规范通过 [描述] 解决了这些差距。 通过 [功能],TrustGraph 可以:
|
||||
- [优势 1]
|
||||
- [优势 2]
|
||||
- [优势 3]
|
||||
- [优势 4]
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 架构
|
||||
|
||||
命令行知识加载需要以下技术组件:
|
||||
|
||||
1. **[组件 1]**
|
||||
- [组件功能描述]
|
||||
- [主要特性]
|
||||
- [集成点]
|
||||
|
||||
模块: [module-path]
|
||||
|
||||
2. **[组件 2]**
|
||||
- [组件功能描述]
|
||||
- [主要特性]
|
||||
- [集成点]
|
||||
|
||||
模块: [module-path]
|
||||
|
||||
3. **[组件 3]**
|
||||
- [组件功能描述]
|
||||
- [主要特性]
|
||||
- [集成点]
|
||||
|
||||
模块: [module-path]
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### [数据模型 1]
|
||||
|
||||
[数据模型和结构的描述]
|
||||
|
||||
示例:
|
||||
```
|
||||
[Example data structure]
|
||||
```
|
||||
|
||||
这种方法允许:
|
||||
- [Benefit 1]
|
||||
- [Benefit 2]
|
||||
- [Benefit 3]
|
||||
- [Benefit 4]
|
||||
|
||||
### APIs
|
||||
|
||||
新的 API:
|
||||
- [API description 1]
|
||||
- [API description 2]
|
||||
- [API description 3]
|
||||
|
||||
修改后的 API:
|
||||
- [Modified API 1] - [Description of changes]
|
||||
- [Modified API 2] - [Description of changes]
|
||||
|
||||
### 实现细节
|
||||
|
||||
[Implementation approach and conventions]
|
||||
|
||||
[Additional implementation notes]
|
||||
|
||||
## 安全性考虑
|
||||
|
||||
[Security considerations specific to this implementation]
|
||||
|
||||
## 性能考虑
|
||||
|
||||
[Performance considerations and potential bottlenecks]
|
||||
|
||||
## 测试策略
|
||||
|
||||
[Testing approach and strategy]
|
||||
|
||||
## 迁移计划
|
||||
|
||||
[Migration strategy if applicable]
|
||||
|
||||
## 时间线
|
||||
|
||||
[Timeline information if specified]
|
||||
|
||||
## 待解决问题
|
||||
|
||||
- [Open question 1]
|
||||
- [Open question 2]
|
||||
|
||||
## 参考文献
|
||||
|
||||
[References if applicable]
|
||||
280
docs/tech-specs/zh-cn/agent-explainability.zh-cn.md
Normal file
280
docs/tech-specs/zh-cn/agent-explainability.zh-cn.md
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
---
|
||||
layout: default
|
||||
title: "Agent Explainability: Provenance Recording"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# Agent Explainability: Provenance Recording
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
为使代理会话可追溯和可调试,并将代理循环中的溯源记录添加到 React 代理中,从而使用与 GraphRAG 相同的可解释性基础设施。
|
||||
|
||||
**设计决策:**
|
||||
写入 `urn:graph:retrieval` (通用可解释性图)
|
||||
目前采用线性依赖链 (分析 N → wasDerivedFrom → 分析 N-1)
|
||||
工具是不可见的黑盒 (仅记录输入/输出)
|
||||
DAG 支持计划在未来迭代中实现
|
||||
|
||||
## 实体类型
|
||||
|
||||
GraphRAG 和 Agent 都使用 PROV-O 作为基础本体,并具有 TrustGraph 特定的子类型:
|
||||
|
||||
### GraphRAG 类型
|
||||
| 实体 | PROV-O 类型 | TG 类型 | 描述 |
|
||||
|--------|-------------|----------|-------------|
|
||||
| 问题 | `prov:Activity` | `tg:Question`, `tg:GraphRagQuestion` | 用户的查询 |
|
||||
| 探索 | `prov:Entity` | `tg:Exploration` | 从知识图谱检索的边 |
|
||||
| 重点 | `prov:Entity` | `tg:Focus` | 带有推理的选定边 |
|
||||
| 合成 | `prov:Entity` | `tg:Synthesis` | 最终答案 |
|
||||
|
||||
### Agent 类型
|
||||
| 实体 | PROV-O 类型 | TG 类型 | 描述 |
|
||||
|--------|-------------|----------|-------------|
|
||||
| 问题 | `prov:Activity` | `tg:Question`, `tg:AgentQuestion` | 用户的查询 |
|
||||
| 分析 | `prov:Entity` | `tg:Analysis` | 每个思考/行动/观察周期 |
|
||||
| 结论 | `prov:Entity` | `tg:Conclusion` | 最终答案 |
|
||||
|
||||
### Document RAG 类型
|
||||
| 实体 | PROV-O 类型 | TG 类型 | 描述 |
|
||||
|--------|-------------|----------|-------------|
|
||||
| 问题 | `prov:Activity` | `tg:Question`, `tg:DocRagQuestion` | 用户的查询 |
|
||||
| 探索 | `prov:Entity` | `tg:Exploration` | 从文档存储中检索的块 |
|
||||
| 合成 | `prov:Entity` | `tg:Synthesis` | 最终答案 |
|
||||
|
||||
**注意:** Document RAG 使用 GraphRAG 类型的子集 (没有“重点”步骤,因为没有边选择/推理阶段)。
|
||||
|
||||
### 问题子类型
|
||||
|
||||
所有“问题”实体都共享 `tg:Question` 作为基本类型,但具有特定的子类型以标识检索机制:
|
||||
|
||||
| 子类型 | URI 模式 | 机制 |
|
||||
|---------|-------------|-----------|
|
||||
| `tg:GraphRagQuestion` | `urn:trustgraph:question:{uuid}` | 知识图谱 RAG |
|
||||
| `tg:DocRagQuestion` | `urn:trustgraph:docrag:{uuid}` | 文档/块 RAG |
|
||||
| `tg:AgentQuestion` | `urn:trustgraph:agent:{uuid}` | ReAct 代理 |
|
||||
|
||||
这允许通过 `tg:Question` 查询所有问题,同时通过子类型过滤特定机制。
|
||||
|
||||
## 溯源模型
|
||||
|
||||
```
|
||||
Question (urn:trustgraph:agent:{uuid})
|
||||
│
|
||||
│ tg:query = "User's question"
|
||||
│ prov:startedAtTime = timestamp
|
||||
│ rdf:type = prov:Activity, tg:Question
|
||||
│
|
||||
↓ prov:wasDerivedFrom
|
||||
│
|
||||
Analysis1 (urn:trustgraph:agent:{uuid}/i1)
|
||||
│
|
||||
│ tg:thought = "I need to query the knowledge base..."
|
||||
│ tg:action = "knowledge-query"
|
||||
│ tg:arguments = {"question": "..."}
|
||||
│ tg:observation = "Result from tool..."
|
||||
│ rdf:type = prov:Entity, tg:Analysis
|
||||
│
|
||||
↓ prov:wasDerivedFrom
|
||||
│
|
||||
Analysis2 (urn:trustgraph:agent:{uuid}/i2)
|
||||
│ ...
|
||||
↓ prov:wasDerivedFrom
|
||||
│
|
||||
Conclusion (urn:trustgraph:agent:{uuid}/final)
|
||||
│
|
||||
│ tg:answer = "The final response..."
|
||||
│ rdf:type = prov:Entity, tg:Conclusion
|
||||
```
|
||||
|
||||
### 文档检索增强生成(RAG)溯源模型
|
||||
|
||||
```
|
||||
Question (urn:trustgraph:docrag:{uuid})
|
||||
│
|
||||
│ tg:query = "User's question"
|
||||
│ prov:startedAtTime = timestamp
|
||||
│ rdf:type = prov:Activity, tg:Question
|
||||
│
|
||||
↓ prov:wasGeneratedBy
|
||||
│
|
||||
Exploration (urn:trustgraph:docrag:{uuid}/exploration)
|
||||
│
|
||||
│ tg:chunkCount = 5
|
||||
│ tg:selectedChunk = "chunk-id-1"
|
||||
│ tg:selectedChunk = "chunk-id-2"
|
||||
│ ...
|
||||
│ rdf:type = prov:Entity, tg:Exploration
|
||||
│
|
||||
↓ prov:wasDerivedFrom
|
||||
│
|
||||
Synthesis (urn:trustgraph:docrag:{uuid}/synthesis)
|
||||
│
|
||||
│ tg:content = "The synthesized answer..."
|
||||
│ rdf:type = prov:Entity, tg:Synthesis
|
||||
```
|
||||
|
||||
## 需要修改的内容
|
||||
|
||||
### 1. 模式更改
|
||||
|
||||
**文件:** `trustgraph-base/trustgraph/schema/services/agent.py`
|
||||
|
||||
向 `AgentRequest` 添加 `session_id` 和 `collection` 字段:
|
||||
```python
|
||||
@dataclass
|
||||
class AgentRequest:
|
||||
question: str = ""
|
||||
state: str = ""
|
||||
group: list[str] | None = None
|
||||
history: list[AgentStep] = field(default_factory=list)
|
||||
user: str = ""
|
||||
collection: str = "default" # NEW: Collection for provenance traces
|
||||
streaming: bool = False
|
||||
session_id: str = "" # NEW: For provenance tracking across iterations
|
||||
```
|
||||
|
||||
**文件:** `trustgraph-base/trustgraph/messaging/translators/agent.py`
|
||||
|
||||
更新翻译器,使其能够处理 `session_id` 和 `collection`,并在 `to_pulsar()` 和 `from_pulsar()` 中均能正确处理。
|
||||
|
||||
### 2. 向 Agent Service 添加可解释性生产者
|
||||
|
||||
**文件:** `trustgraph-flow/trustgraph/agent/react/service.py`
|
||||
|
||||
注册一个“可解释性”生产者(与 GraphRAG 相同模式):
|
||||
```python
|
||||
from ... base import ProducerSpec
|
||||
from ... schema import Triples
|
||||
|
||||
# In __init__:
|
||||
self.register_specification(
|
||||
ProducerSpec(
|
||||
name = "explainability",
|
||||
schema = Triples,
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
### 3. 溯源三元组生成
|
||||
|
||||
**文件:** `trustgraph-base/trustgraph/provenance/agent.py`
|
||||
|
||||
创建辅助函数(类似于 GraphRAG 的 `question_triples`、`exploration_triples` 等):
|
||||
```python
|
||||
def agent_session_triples(session_uri, query, timestamp):
|
||||
"""Generate triples for agent Question."""
|
||||
return [
|
||||
Triple(s=session_uri, p=RDF_TYPE, o=PROV_ACTIVITY),
|
||||
Triple(s=session_uri, p=RDF_TYPE, o=TG_QUESTION),
|
||||
Triple(s=session_uri, p=TG_QUERY, o=query),
|
||||
Triple(s=session_uri, p=PROV_STARTED_AT_TIME, o=timestamp),
|
||||
]
|
||||
|
||||
def agent_iteration_triples(iteration_uri, parent_uri, thought, action, arguments, observation):
|
||||
"""Generate triples for one Analysis step."""
|
||||
return [
|
||||
Triple(s=iteration_uri, p=RDF_TYPE, o=PROV_ENTITY),
|
||||
Triple(s=iteration_uri, p=RDF_TYPE, o=TG_ANALYSIS),
|
||||
Triple(s=iteration_uri, p=TG_THOUGHT, o=thought),
|
||||
Triple(s=iteration_uri, p=TG_ACTION, o=action),
|
||||
Triple(s=iteration_uri, p=TG_ARGUMENTS, o=json.dumps(arguments)),
|
||||
Triple(s=iteration_uri, p=TG_OBSERVATION, o=observation),
|
||||
Triple(s=iteration_uri, p=PROV_WAS_DERIVED_FROM, o=parent_uri),
|
||||
]
|
||||
|
||||
def agent_final_triples(final_uri, parent_uri, answer):
|
||||
"""Generate triples for Conclusion."""
|
||||
return [
|
||||
Triple(s=final_uri, p=RDF_TYPE, o=PROV_ENTITY),
|
||||
Triple(s=final_uri, p=RDF_TYPE, o=TG_CONCLUSION),
|
||||
Triple(s=final_uri, p=TG_ANSWER, o=answer),
|
||||
Triple(s=final_uri, p=PROV_WAS_DERIVED_FROM, o=parent_uri),
|
||||
]
|
||||
```
|
||||
|
||||
### 4. 类型定义
|
||||
|
||||
**文件:** `trustgraph-base/trustgraph/provenance/namespaces.py`
|
||||
|
||||
添加可解释性实体类型和代理谓词:
|
||||
```python
|
||||
# Explainability entity types (used by both GraphRAG and Agent)
|
||||
TG_QUESTION = TG + "Question"
|
||||
TG_EXPLORATION = TG + "Exploration"
|
||||
TG_FOCUS = TG + "Focus"
|
||||
TG_SYNTHESIS = TG + "Synthesis"
|
||||
TG_ANALYSIS = TG + "Analysis"
|
||||
TG_CONCLUSION = TG + "Conclusion"
|
||||
|
||||
# Agent predicates
|
||||
TG_THOUGHT = TG + "thought"
|
||||
TG_ACTION = TG + "action"
|
||||
TG_ARGUMENTS = TG + "arguments"
|
||||
TG_OBSERVATION = TG + "observation"
|
||||
TG_ANSWER = TG + "answer"
|
||||
```
|
||||
|
||||
## 文件修改
|
||||
|
||||
| 文件 | 更改 |
|
||||
|------|--------|
|
||||
| `trustgraph-base/trustgraph/schema/services/agent.py` | 向 AgentRequest 添加 session_id 和 collection |
|
||||
| `trustgraph-base/trustgraph/messaging/translators/agent.py` | 更新翻译器以适应新字段 |
|
||||
| `trustgraph-base/trustgraph/provenance/namespaces.py` | 添加实体类型、agent谓词和 Document RAG 谓词 |
|
||||
| `trustgraph-base/trustgraph/provenance/triples.py` | 向 GraphRAG 三元组构建器添加 TG 类型,添加 Document RAG 三元组构建器 |
|
||||
| `trustgraph-base/trustgraph/provenance/uris.py` | 添加 Document RAG URI 生成器 |
|
||||
| `trustgraph-base/trustgraph/provenance/__init__.py` | 导出新类型、谓词和 Document RAG 函数 |
|
||||
| `trustgraph-base/trustgraph/schema/services/retrieval.py` | 向 DocumentRagResponse 添加 explain_id 和 explain_graph |
|
||||
| `trustgraph-base/trustgraph/messaging/translators/retrieval.py` | 更新 DocumentRagResponseTranslator 以适应可解释性字段 |
|
||||
| `trustgraph-flow/trustgraph/agent/react/service.py` | 添加可解释性生产者 + 记录逻辑 |
|
||||
| `trustgraph-flow/trustgraph/retrieval/document_rag/document_rag.py` | 添加可解释性回调并发出溯源三元组 |
|
||||
| `trustgraph-flow/trustgraph/retrieval/document_rag/rag.py` | 添加可解释性生产者并连接回调 |
|
||||
| `trustgraph-cli/trustgraph/cli/show_explain_trace.py` | 处理 agent 跟踪类型 |
|
||||
| `trustgraph-cli/trustgraph/cli/list_explain_traces.py` | 在 GraphRAG 旁边列出 agent 会话 |
|
||||
|
||||
## 创建的文件
|
||||
|
||||
| 文件 | 目的 |
|
||||
|------|---------|
|
||||
| `trustgraph-base/trustgraph/provenance/agent.py` | Agent 相关的三元组生成器 |
|
||||
|
||||
## CLI 更新
|
||||
|
||||
**检测:** 无论是 GraphRAG 还是 Agent 问题,都具有 `tg:Question` 类型。通过以下方式区分:
|
||||
1. URI 模式:`urn:trustgraph:agent:` vs `urn:trustgraph:question:`
|
||||
2. 派生实体:`tg:Analysis` (agent) vs `tg:Exploration` (GraphRAG)
|
||||
|
||||
**`list_explain_traces.py`:**
|
||||
显示类型列(Agent vs GraphRAG)
|
||||
|
||||
**`show_explain_trace.py`:**
|
||||
自动检测跟踪类型
|
||||
Agent 渲染显示:问题 → 分析步骤(s) → 结论
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
`session_id` 默认为 `""` - 旧请求有效,但将不具有溯源信息
|
||||
`collection` 默认为 `"default"` - 合理的备选方案
|
||||
CLI 能够优雅地处理两种跟踪类型
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
# Run an agent query
|
||||
tg-invoke-agent -q "What is the capital of France?"
|
||||
|
||||
# List traces (should show agent sessions with Type column)
|
||||
tg-list-explain-traces -U trustgraph -C default
|
||||
|
||||
# Show agent trace
|
||||
tg-show-explain-trace "urn:trustgraph:agent:xxx"
|
||||
```
|
||||
|
||||
## 未来工作(不在本次 PR 中)
|
||||
|
||||
DAG 依赖关系(当分析 N 使用来自多个先前分析的结果时)
|
||||
特定于工具的溯源链接(KnowledgeQuery → 它的 GraphRAG 跟踪)
|
||||
流式溯源输出(在过程中输出,而不是在最后批量输出)
|
||||
135
docs/tech-specs/zh-cn/architecture-principles.zh-cn.md
Normal file
135
docs/tech-specs/zh-cn/architecture-principles.zh-cn.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
---
|
||||
layout: default
|
||||
title: "知识图谱架构基础"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 知识图谱架构基础
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 基础 1:主谓宾 (SPO) 图模型
|
||||
**决策**: 采用 SPO/RDF 作为核心知识表示模型
|
||||
|
||||
**理由**:
|
||||
提供最大的灵活性和与现有图技术的互操作性
|
||||
能够无缝转换为其他图查询语言 (例如,SPO → Cypher,反之则不行)
|
||||
奠定基础,"解锁"许多下游功能
|
||||
支持节点到节点的关系 (SPO) 和节点到字面值关系 (RDF)
|
||||
|
||||
**实施**:
|
||||
核心数据结构: `node → edge → {node | literal}`
|
||||
在支持扩展的 SPO 操作的同时,保持与 RDF 标准的兼容性
|
||||
|
||||
## 基础 2:原生于 LLM 的知识图谱集成
|
||||
**决策**: 优化知识图谱结构和操作,以实现与 LLM 的交互
|
||||
|
||||
**理由**:
|
||||
主要用例涉及 LLM 与知识图谱的交互
|
||||
图技术选择必须优先考虑与 LLM 的兼容性,而不是其他考虑因素
|
||||
能够实现利用结构化知识的自然语言处理工作流程
|
||||
|
||||
**实施**:
|
||||
设计 LLM 可以有效推理的图模式
|
||||
针对常见的 LLM 交互模式进行优化
|
||||
|
||||
## 基础 3:基于嵌入的图导航
|
||||
**决策**: 通过嵌入将自然语言查询直接映射到图节点
|
||||
|
||||
**理由**:
|
||||
实现从 NLP 查询到图导航的最简单路径
|
||||
避免复杂的中间查询生成步骤
|
||||
提供图结构内部高效的语义搜索功能
|
||||
|
||||
**实施**:
|
||||
`NLP Query → Graph Embeddings → Graph Nodes`
|
||||
维护所有图实体的嵌入表示
|
||||
支持用于查询解析的直接语义相似性匹配
|
||||
|
||||
## 基础 4:分布式实体解析与确定性标识符
|
||||
**决策**: 支持并行知识提取,并使用确定性实体标识 (80% 规则)
|
||||
|
||||
**理由**:
|
||||
**理想**: 单进程提取,具有完整的状态可见性,可以实现完美的实体解析
|
||||
<<<<<<< HEAD
|
||||
**现实**: 可扩展性要求需要并行处理能力
|
||||
=======
|
||||
**现实**: 扩展性要求需要并行处理能力
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
**折衷**: 设计用于在分布式进程中实现确定性实体标识
|
||||
|
||||
**实施**:
|
||||
开发机制,以生成在不同知识提取器中保持一致且唯一的标识符
|
||||
在不同的进程中提到的相同实体必须解析为相同的标识符
|
||||
承认约 20% 的边缘情况可能需要替代处理模型
|
||||
设计用于处理复杂实体解析场景的后备机制
|
||||
|
||||
## 基础 5:事件驱动架构与发布-订阅
|
||||
<<<<<<< HEAD
|
||||
**决策**: 实施 pub-sub 消息系统,用于系统协调
|
||||
=======
|
||||
**决策**: 实现发布-订阅消息系统,用于系统协调
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
**理由**:
|
||||
允许知识提取、存储和查询组件之间的松散耦合
|
||||
支持实时更新和跨系统的通知
|
||||
促进可扩展的分布式处理工作流程
|
||||
|
||||
**实施**:
|
||||
使用消息驱动的系统组件协调
|
||||
用于知识更新、提取完成和查询结果的事件流
|
||||
|
||||
## 基础 6:可重入代理通信
|
||||
**决策**: 支持用于基于代理的处理的可重入发布-订阅操作
|
||||
|
||||
**理由**:
|
||||
允许代理触发和响应彼此,从而实现复杂的代理工作流程
|
||||
支持复杂的多步骤知识处理管道
|
||||
允许递归和迭代处理模式
|
||||
|
||||
<<<<<<< HEAD
|
||||
**实施**:
|
||||
pub-sub 系统必须安全地处理可重入调用
|
||||
=======
|
||||
**实现**:
|
||||
发布-订阅系统必须安全地处理可重入调用
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
防止无限循环的代理协调机制
|
||||
支持代理工作流程编排
|
||||
|
||||
## 基础 7:列式数据存储集成
|
||||
**决策**: 确保查询与列式存储系统兼容
|
||||
|
||||
**理由**:
|
||||
能够对大型知识数据集执行高效的分析查询
|
||||
支持商业智能和报告用例
|
||||
桥接基于图的知识表示与传统的分析工作流程
|
||||
|
||||
**实施**:
|
||||
查询转换层:图查询 → 列式查询
|
||||
<<<<<<< HEAD
|
||||
支持图操作和分析工作负载的混合存储策略
|
||||
=======
|
||||
混合存储策略,支持图操作和分析工作负载
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
在这两种范例中保持查询性能
|
||||
|
||||
--
|
||||
|
||||
## 架构原则摘要
|
||||
|
||||
1. **灵活性至上**: SPO/RDF 模型提供最大的适应性
|
||||
2. **LLM 优化**: 所有设计决策都考虑 LLM 交互要求
|
||||
3. **语义效率**: 直接的嵌入到节点映射,以实现最佳的查询性能
|
||||
<<<<<<< HEAD
|
||||
4. **务实的扩展性**: 在完美准确性和实际的分布式处理之间取得平衡
|
||||
5. **事件驱动协调**: pub-sub 实现松散耦合和可扩展性
|
||||
=======
|
||||
4. **务实的扩展性**: 在完美的准确性与实际的分布式处理之间取得平衡
|
||||
5. **事件驱动协调**: 发布-订阅实现松散耦合和可扩展性
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
6. **代理友好**: 支持复杂的多代理处理工作流程
|
||||
7. **分析兼容性**: 桥接图和列式范例,以实现全面的查询
|
||||
|
||||
这些基础构建了一个知识图谱架构,该架构在理论严谨性和实际可扩展性之间取得了平衡,并针对 LLM 集成和分布式处理进行了优化。
|
||||
339
docs/tech-specs/zh-cn/cassandra-consolidation.zh-cn.md
Normal file
339
docs/tech-specs/zh-cn/cassandra-consolidation.zh-cn.md
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
---
|
||||
layout: default
|
||||
title: "技术规范:Cassandra 配置整合"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 技术规范:Cassandra 配置整合
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
**状态:** 草稿
|
||||
**作者:** 助理
|
||||
**日期:** 2024-09-03
|
||||
|
||||
## 概述
|
||||
|
||||
本规范旨在解决 TrustGraph 代码库中 Cassandra 连接参数命名和配置模式的不一致问题。目前,存在两种不同的参数命名方案(`cassandra_*` 与 `graph_*`),这导致了混乱和维护复杂性。
|
||||
|
||||
## 问题陈述
|
||||
|
||||
当前代码库使用两组不同的 Cassandra 配置参数:
|
||||
|
||||
1. **知识/配置/库模块** 使用:
|
||||
`cassandra_host` (主机列表)
|
||||
`cassandra_user`
|
||||
`cassandra_password`
|
||||
|
||||
2. **图/存储模块** 使用:
|
||||
`graph_host` (单个主机,有时转换为列表)
|
||||
`graph_username`
|
||||
`graph_password`
|
||||
|
||||
3. **命令行暴露不一致:**
|
||||
一些处理器(例如,`kg-store`)不将 Cassandra 设置作为命令行参数暴露
|
||||
其他处理器以不同的名称和格式暴露这些设置
|
||||
帮助文本未反映环境变量的默认值
|
||||
|
||||
这两组参数都连接到同一个 Cassandra 集群,但使用不同的命名约定,导致:
|
||||
用户配置混乱
|
||||
维护负担增加
|
||||
文档不一致
|
||||
可能出现配置错误
|
||||
在某些处理器中,无法通过命令行覆盖设置
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 标准化参数名称
|
||||
|
||||
所有模块都将使用一致的 `cassandra_*` 参数名称:
|
||||
`cassandra_host` - 主机列表(内部存储为列表)
|
||||
`cassandra_username` - 身份验证用户名
|
||||
`cassandra_password` - 身份验证密码
|
||||
|
||||
### 2. 命令行参数
|
||||
|
||||
所有处理器都必须通过命令行参数暴露 Cassandra 配置:
|
||||
`--cassandra-host` - 逗号分隔的主机列表
|
||||
`--cassandra-username` - 身份验证用户名
|
||||
`--cassandra-password` - 身份验证密码
|
||||
|
||||
### 3. 环境变量回退
|
||||
|
||||
如果未显式提供命令行参数,系统将检查环境变量:
|
||||
`CASSANDRA_HOST` - 逗号分隔的主机列表
|
||||
`CASSANDRA_USERNAME` - 身份验证用户名
|
||||
`CASSANDRA_PASSWORD` - 身份验证密码
|
||||
|
||||
### 4. 默认值
|
||||
|
||||
如果未指定命令行参数或环境变量:
|
||||
`cassandra_host` 默认为 `["cassandra"]`
|
||||
`cassandra_username` 默认为 `None` (无身份验证)
|
||||
`cassandra_password` 默认为 `None` (无身份验证)
|
||||
|
||||
### 5. 帮助文本要求
|
||||
|
||||
`--help` 输出必须:
|
||||
显示已设置的环境变量值作为默认值
|
||||
绝不显示密码值(显示 `****` 或 `<set>` 代替)
|
||||
在帮助文本中清楚地指示解析顺序
|
||||
|
||||
示例帮助输出:
|
||||
```
|
||||
--cassandra-host HOST
|
||||
Cassandra host list, comma-separated (default: prod-cluster-1,prod-cluster-2)
|
||||
[from CASSANDRA_HOST environment variable]
|
||||
|
||||
--cassandra-username USERNAME
|
||||
Cassandra username (default: cassandra_user)
|
||||
[from CASSANDRA_USERNAME environment variable]
|
||||
|
||||
--cassandra-password PASSWORD
|
||||
Cassandra password (default: <set from environment>)
|
||||
```
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 参数解析顺序
|
||||
|
||||
对于每个 Cassandra 参数,解析顺序如下:
|
||||
1. 命令行参数值
|
||||
2. 环境变量 (`CASSANDRA_*`)
|
||||
3. 默认值
|
||||
|
||||
### 主机参数处理
|
||||
|
||||
`cassandra_host` 参数:
|
||||
命令行接受逗号分隔的字符串:`--cassandra-host "host1,host2,host3"`
|
||||
环境变量接受逗号分隔的字符串:`CASSANDRA_HOST="host1,host2,host3"`
|
||||
内部始终存储为列表:`["host1", "host2", "host3"]`
|
||||
单个主机:`"localhost"` → 转换为 `["localhost"]`
|
||||
已经是列表:`["host1", "host2"]` → 保持原样
|
||||
|
||||
### 认证逻辑
|
||||
|
||||
当同时提供 `cassandra_username` 和 `cassandra_password` 时,将使用认证:
|
||||
```python
|
||||
if cassandra_username and cassandra_password:
|
||||
# Use SSL context and PlainTextAuthProvider
|
||||
else:
|
||||
# Connect without authentication
|
||||
```
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### 使用 `graph_*` 参数的模块(需要修改):
|
||||
`trustgraph-flow/trustgraph/storage/triples/cassandra/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/objects/cassandra/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/rows/cassandra/write.py`
|
||||
`trustgraph-flow/trustgraph/query/triples/cassandra/service.py`
|
||||
|
||||
### 使用 `cassandra_*` 参数的模块(需要更新,并使用环境变量回退):
|
||||
`trustgraph-flow/trustgraph/tables/config.py`
|
||||
`trustgraph-flow/trustgraph/tables/knowledge.py`
|
||||
`trustgraph-flow/trustgraph/tables/library.py`
|
||||
`trustgraph-flow/trustgraph/storage/knowledge/store.py`
|
||||
`trustgraph-flow/trustgraph/cores/knowledge.py`
|
||||
`trustgraph-flow/trustgraph/librarian/librarian.py`
|
||||
`trustgraph-flow/trustgraph/librarian/service.py`
|
||||
`trustgraph-flow/trustgraph/config/service/service.py`
|
||||
`trustgraph-flow/trustgraph/cores/service.py`
|
||||
|
||||
### 需要更新的测试文件:
|
||||
`tests/unit/test_cores/test_knowledge_manager.py`
|
||||
`tests/unit/test_storage/test_triples_cassandra_storage.py`
|
||||
`tests/unit/test_query/test_triples_cassandra_query.py`
|
||||
`tests/integration/test_objects_cassandra_integration.py`
|
||||
|
||||
## 实施策略
|
||||
|
||||
### 第一阶段:创建通用的配置辅助工具
|
||||
创建实用函数,以标准化所有处理器中的 Cassandra 配置:
|
||||
|
||||
```python
|
||||
import os
|
||||
import argparse
|
||||
|
||||
def get_cassandra_defaults():
|
||||
"""Get default values from environment variables or fallback."""
|
||||
return {
|
||||
'host': os.getenv('CASSANDRA_HOST', 'cassandra'),
|
||||
'username': os.getenv('CASSANDRA_USERNAME'),
|
||||
'password': os.getenv('CASSANDRA_PASSWORD')
|
||||
}
|
||||
|
||||
def add_cassandra_args(parser: argparse.ArgumentParser):
|
||||
"""
|
||||
Add standardized Cassandra arguments to an argument parser.
|
||||
Shows environment variable values in help text.
|
||||
"""
|
||||
defaults = get_cassandra_defaults()
|
||||
|
||||
# Format help text with env var indication
|
||||
host_help = f"Cassandra host list, comma-separated (default: {defaults['host']})"
|
||||
if 'CASSANDRA_HOST' in os.environ:
|
||||
host_help += " [from CASSANDRA_HOST]"
|
||||
|
||||
username_help = f"Cassandra username"
|
||||
if defaults['username']:
|
||||
username_help += f" (default: {defaults['username']})"
|
||||
if 'CASSANDRA_USERNAME' in os.environ:
|
||||
username_help += " [from CASSANDRA_USERNAME]"
|
||||
|
||||
password_help = "Cassandra password"
|
||||
if defaults['password']:
|
||||
password_help += " (default: <set>)"
|
||||
if 'CASSANDRA_PASSWORD' in os.environ:
|
||||
password_help += " [from CASSANDRA_PASSWORD]"
|
||||
|
||||
parser.add_argument(
|
||||
'--cassandra-host',
|
||||
default=defaults['host'],
|
||||
help=host_help
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--cassandra-username',
|
||||
default=defaults['username'],
|
||||
help=username_help
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--cassandra-password',
|
||||
default=defaults['password'],
|
||||
help=password_help
|
||||
)
|
||||
|
||||
def resolve_cassandra_config(args) -> tuple[list[str], str|None, str|None]:
|
||||
"""
|
||||
Convert argparse args to Cassandra configuration.
|
||||
|
||||
Returns:
|
||||
tuple: (hosts_list, username, password)
|
||||
"""
|
||||
# Convert host string to list
|
||||
if isinstance(args.cassandra_host, str):
|
||||
hosts = [h.strip() for h in args.cassandra_host.split(',')]
|
||||
else:
|
||||
hosts = args.cassandra_host
|
||||
|
||||
return hosts, args.cassandra_username, args.cassandra_password
|
||||
```
|
||||
|
||||
### 第二阶段:使用 `graph_*` 参数更新模块
|
||||
1. 将参数名称从 `graph_*` 更改为 `cassandra_*`
|
||||
2. 将自定义 `add_args()` 方法替换为标准化的 `add_cassandra_args()`
|
||||
3. 使用通用的配置辅助函数
|
||||
4. 更新文档字符串
|
||||
|
||||
示例转换:
|
||||
```python
|
||||
# OLD CODE
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
parser.add_argument(
|
||||
'-g', '--graph-host',
|
||||
default="localhost",
|
||||
help=f'Graph host (default: localhost)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'--graph-username',
|
||||
default=None,
|
||||
help=f'Cassandra username'
|
||||
)
|
||||
|
||||
# NEW CODE
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
FlowProcessor.add_args(parser)
|
||||
add_cassandra_args(parser) # Use standard helper
|
||||
```
|
||||
|
||||
### 第三阶段:使用 `cassandra_*` 参数更新模块
|
||||
1. 在缺失的地方添加命令行参数支持(例如:`kg-store`)
|
||||
2. 将现有的参数定义替换为 `add_cassandra_args()`
|
||||
3. 使用 `resolve_cassandra_config()` 以实现一致的解析
|
||||
4. 确保一致的主机列表处理
|
||||
|
||||
### 第四阶段:更新测试和文档
|
||||
1. 更新所有测试文件以使用新的参数名称
|
||||
2. 更新命令行界面 (CLI) 文档
|
||||
3. 更新 API 文档
|
||||
4. 添加环境变量文档
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
为了在过渡期间保持向后兼容性:
|
||||
|
||||
1. **弃用警告**,用于 `graph_*` 参数
|
||||
2. **参数别名** - 初始阶段接受旧名称和新名称
|
||||
3. **分阶段发布**,在多个版本中进行
|
||||
4. **文档更新**,包含迁移指南
|
||||
|
||||
示例向后兼容代码:
|
||||
```python
|
||||
def __init__(self, **params):
|
||||
# Handle deprecated graph_* parameters
|
||||
if 'graph_host' in params:
|
||||
warnings.warn("graph_host is deprecated, use cassandra_host", DeprecationWarning)
|
||||
params.setdefault('cassandra_host', params.pop('graph_host'))
|
||||
|
||||
if 'graph_username' in params:
|
||||
warnings.warn("graph_username is deprecated, use cassandra_username", DeprecationWarning)
|
||||
params.setdefault('cassandra_username', params.pop('graph_username'))
|
||||
|
||||
# ... continue with standard resolution
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
1. **单元测试**,用于配置解析逻辑
|
||||
2. **集成测试**,使用各种配置组合
|
||||
3. **环境变量测试**
|
||||
4. **向后兼容性测试**,针对已弃用的参数
|
||||
5. **Docker Compose 测试**,使用环境变量
|
||||
|
||||
## 文档更新
|
||||
|
||||
1. 更新所有 CLI 命令文档
|
||||
2. 更新 API 文档
|
||||
3. 创建迁移指南
|
||||
4. 更新 Docker Compose 示例
|
||||
5. 更新配置参考文档
|
||||
|
||||
## 风险与缓解
|
||||
|
||||
| 风险 | 影响 | 缓解措施 |
|
||||
|------|--------|------------|
|
||||
| 对用户的破坏性更改 | 高 | 实施向后兼容性过渡期 |
|
||||
| 转换期间的配置混淆 | 中 | 清晰的文档和弃用警告 |
|
||||
| 测试失败 | 中 | 全面的测试更新 |
|
||||
| Docker 部署问题 | 高 | 更新所有 Docker Compose 示例 |
|
||||
|
||||
## 成功标准
|
||||
|
||||
[ ] 所有模块使用一致的 `cassandra_*` 参数名称
|
||||
[ ] 所有处理器通过命令行参数暴露 Cassandra 设置
|
||||
[ ] 命令行帮助文本显示环境变量的默认值
|
||||
[ ] 密码值绝不在帮助文本中显示
|
||||
[ ] 环境变量回退工作正常
|
||||
[ ] `cassandra_host` 内部始终被处理为列表
|
||||
[ ] 至少在 2 个版本中保持向后兼容性
|
||||
[ ] 所有测试在新配置系统中通过
|
||||
[ ] 文档已完全更新
|
||||
[ ] Docker Compose 示例使用环境变量
|
||||
|
||||
## 时间线
|
||||
|
||||
**第一周:** 实施通用的配置助手,并更新 `graph_*` 模块
|
||||
**第二周:** 为现有的 `cassandra_*` 模块添加环境变量支持
|
||||
**第三周:** 更新测试和文档
|
||||
**第四周:** 集成测试和错误修复
|
||||
|
||||
## 未来考虑
|
||||
|
||||
考虑将此模式扩展到其他数据库配置(例如,Elasticsearch)
|
||||
实施配置验证和更好的错误消息
|
||||
添加对 Cassandra 连接池配置的支持
|
||||
考虑添加对配置文件支持(.env 文件)
|
||||
687
docs/tech-specs/zh-cn/cassandra-performance-refactor.zh-cn.md
Normal file
687
docs/tech-specs/zh-cn/cassandra-performance-refactor.zh-cn.md
Normal file
|
|
@ -0,0 +1,687 @@
|
|||
---
|
||||
layout: default
|
||||
title: "技术规范:Cassandra 知识库性能重构"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 技术规范:Cassandra 知识库性能重构
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
**状态:** 草稿
|
||||
**作者:** 助理
|
||||
**日期:** 2025-09-18
|
||||
|
||||
## 概述
|
||||
|
||||
本规范解决了 TrustGraph Cassandra 知识库实现中的性能问题,并提出了针对 RDF 三元组存储和查询的优化方案。
|
||||
|
||||
## 当前实现
|
||||
|
||||
### 模式设计
|
||||
|
||||
当前实现使用单表设计,位于 `trustgraph-flow/trustgraph/direct/cassandra_kg.py`:
|
||||
|
||||
```sql
|
||||
CREATE TABLE triples (
|
||||
collection text,
|
||||
s text,
|
||||
p text,
|
||||
o text,
|
||||
PRIMARY KEY (collection, s, p, o)
|
||||
);
|
||||
```
|
||||
|
||||
**次级索引:**
|
||||
`triples_s` ON `s` (主语)
|
||||
`triples_p` ON `p` (谓语)
|
||||
`triples_o` ON `o` (宾语)
|
||||
|
||||
### 查询模式
|
||||
|
||||
当前实现支持 8 种不同的查询模式:
|
||||
|
||||
1. **get_all(collection, limit=50)** - 检索集合中的所有三元组
|
||||
```sql
|
||||
SELECT s, p, o FROM triples WHERE collection = ? LIMIT 50
|
||||
```
|
||||
|
||||
2. **get_s(collection, s, limit=10)** - 通过主题进行查询
|
||||
```sql
|
||||
SELECT p, o FROM triples WHERE collection = ? AND s = ? LIMIT 10
|
||||
```
|
||||
|
||||
3. **get_p(collection, p, limit=10)** - 通过谓词进行查询
|
||||
```sql
|
||||
SELECT s, o FROM triples WHERE collection = ? AND p = ? LIMIT 10
|
||||
```
|
||||
|
||||
4. **get_o(collection, o, limit=10)** - 通过对象查询
|
||||
```sql
|
||||
SELECT s, p FROM triples WHERE collection = ? AND o = ? LIMIT 10
|
||||
```
|
||||
|
||||
5. **get_sp(collection, s, p, limit=10)** - 通过主语 + 谓语进行查询。
|
||||
```sql
|
||||
SELECT o FROM triples WHERE collection = ? AND s = ? AND p = ? LIMIT 10
|
||||
```
|
||||
|
||||
6. **get_po(collection, p, o, limit=10)** - 通过谓词 + 对象进行查询 ⚠️
|
||||
```sql
|
||||
SELECT s FROM triples WHERE collection = ? AND p = ? AND o = ? LIMIT 10 ALLOW FILTERING
|
||||
```
|
||||
|
||||
7. **get_os(collection, o, s, limit=10)** - 通过对象 + 主题进行查询 ⚠️
|
||||
```sql
|
||||
SELECT p FROM triples WHERE collection = ? AND o = ? AND s = ? LIMIT 10 ALLOW FILTERING
|
||||
```
|
||||
|
||||
8. **get_spo(collection, s, p, o, limit=10)** - 精确三元组匹配
|
||||
```sql
|
||||
SELECT s as x FROM triples WHERE collection = ? AND s = ? AND p = ? AND o = ? LIMIT 10
|
||||
```
|
||||
|
||||
### 当前架构
|
||||
|
||||
**文件: `trustgraph-flow/trustgraph/direct/cassandra_kg.py`**
|
||||
单个 `KnowledgeGraph` 类处理所有操作
|
||||
通过全局 `_active_clusters` 列表进行连接池管理
|
||||
固定的表名: `"triples"`
|
||||
每个用户模型的 keyspace
|
||||
复制策略为 SimpleStrategy,因子为 1
|
||||
|
||||
**集成点:**
|
||||
**写入路径:** `trustgraph-flow/trustgraph/storage/triples/cassandra/write.py`
|
||||
**查询路径:** `trustgraph-flow/trustgraph/query/triples/cassandra/service.py`
|
||||
**知识存储:** `trustgraph-flow/trustgraph/tables/knowledge.py`
|
||||
|
||||
## 识别出的性能问题
|
||||
|
||||
### 模式层级问题
|
||||
|
||||
1. **低效的主键设计**
|
||||
当前: `PRIMARY KEY (collection, s, p, o)`
|
||||
导致常见访问模式下的聚类效果不佳
|
||||
强制使用昂贵的二级索引
|
||||
|
||||
2. **过度使用二级索引** ⚠️
|
||||
在高基数列(s, p, o)上使用了三个二级索引
|
||||
Cassandra 中的二级索引很昂贵,并且扩展性较差
|
||||
查询 6 和 7 需要 `ALLOW FILTERING`,表明数据建模存在问题
|
||||
|
||||
3. **分区热点风险**
|
||||
单个分区键 `collection` 可能会创建分区热点
|
||||
大型集合会集中在单个节点上
|
||||
没有用于负载均衡的分布策略
|
||||
|
||||
### 查询层级问题
|
||||
|
||||
1. **ALLOW FILTERING 的使用** ⚠️
|
||||
两种查询类型(get_po, get_os)需要 `ALLOW FILTERING`
|
||||
这些查询会扫描多个分区,并且非常昂贵
|
||||
性能会随着数据量的增加而线性下降
|
||||
|
||||
2. **低效的访问模式**
|
||||
没有针对常见的 RDF 查询模式进行优化
|
||||
缺少用于频繁查询组合的复合索引
|
||||
没有考虑图遍历模式
|
||||
|
||||
3. **缺乏查询优化**
|
||||
没有预处理语句缓存
|
||||
没有查询提示或优化策略
|
||||
没有考虑简单的 LIMIT 之外的分页
|
||||
|
||||
## 问题陈述
|
||||
|
||||
当前的 Cassandra 知识库实现存在两个关键的性能瓶颈:
|
||||
|
||||
### 1. get_po 查询性能低效
|
||||
|
||||
`get_po(collection, p, o)` 查询非常低效,因为它需要 `ALLOW FILTERING`:
|
||||
|
||||
```sql
|
||||
SELECT s FROM triples WHERE collection = ? AND p = ? AND o = ? LIMIT 10 ALLOW FILTERING
|
||||
```
|
||||
|
||||
**问题所在:**
|
||||
`ALLOW FILTERING` 迫使 Cassandra 扫描集合中的所有分区。
|
||||
性能会随着数据量的线性增长而下降。
|
||||
这是一个常见的 RDF 查询模式(查找具有特定谓词-对象关系的实体)。
|
||||
随着数据增长,这会在集群上产生显著的负载。
|
||||
|
||||
### 2. 聚类策略不佳
|
||||
|
||||
当前主键 `PRIMARY KEY (collection, s, p, o)` 提供的聚类优势有限:
|
||||
|
||||
**当前聚类的存在问题:**
|
||||
将 `collection` 作为分区键,无法有效分布数据。
|
||||
大多数集合包含各种数据,这使得聚类无效。
|
||||
未考虑 RDF 查询中的常见访问模式。
|
||||
大型集合会在单个节点上创建热点分区。
|
||||
聚类列 (s, p, o) 未针对典型的图遍历模式进行优化。
|
||||
|
||||
**影响:**
|
||||
查询无法受益于数据局部性。
|
||||
缓存利用率低下。
|
||||
集群节点上的负载分布不均匀。
|
||||
随着集合的增长,会出现可扩展性瓶颈。
|
||||
|
||||
## 建议的解决方案:4 表反规范化策略
|
||||
|
||||
### 概述
|
||||
|
||||
用四个专门设计的表替换单个 `triples` 表,每个表针对特定的查询模式进行了优化。 这消除了对辅助索引和 ALLOW FILTERING 的需求,同时为所有类型的查询提供最佳性能。 第四个表实现了在复合分区键下高效地删除集合的功能。
|
||||
|
||||
### 新的模式设计
|
||||
|
||||
**表 1:以实体为中心的查询 (triples_s)**
|
||||
```sql
|
||||
CREATE TABLE triples_s (
|
||||
collection text,
|
||||
s text,
|
||||
p text,
|
||||
o text,
|
||||
PRIMARY KEY ((collection, s), p, o)
|
||||
);
|
||||
```
|
||||
**优化:** get_s, get_sp, get_os
|
||||
**分区键:** (collection, s) - 比仅使用 collection 的分区方式分布更好
|
||||
**聚类:** (p, o) - 允许对主语进行高效的谓词/对象查找
|
||||
|
||||
**表 2:谓词-对象查询 (triples_p)**
|
||||
```sql
|
||||
CREATE TABLE triples_p (
|
||||
collection text,
|
||||
p text,
|
||||
o text,
|
||||
s text,
|
||||
PRIMARY KEY ((collection, p), o, s)
|
||||
);
|
||||
```
|
||||
**优化:** get_p, get_po (消除 ALLOW FILTERING!)
|
||||
**分区键:** (collection, p) - 通过谓词直接访问
|
||||
**聚类:** (o, s) - 高效的对象-主体遍历
|
||||
|
||||
**表 3:面向对象的查询 (triples_o)**
|
||||
```sql
|
||||
CREATE TABLE triples_o (
|
||||
collection text,
|
||||
o text,
|
||||
s text,
|
||||
p text,
|
||||
PRIMARY KEY ((collection, o), s, p)
|
||||
);
|
||||
```
|
||||
**优化:** get_o
|
||||
**分区键:** (collection, o) - 通过对象直接访问
|
||||
**聚类:** (s, p) - 高效的主体-谓语遍历
|
||||
|
||||
**表 4:集合管理与 SPO 查询 (triples_collection)**
|
||||
```sql
|
||||
CREATE TABLE triples_collection (
|
||||
collection text,
|
||||
s text,
|
||||
p text,
|
||||
o text,
|
||||
PRIMARY KEY (collection, s, p, o)
|
||||
);
|
||||
```
|
||||
**优化:** get_spo, delete_collection
|
||||
**分区键:** 仅限集合 - 启用高效的集合级别操作
|
||||
**聚类:** (s, p, o) - 标准三元组排序
|
||||
**目的:** 既用于精确的SPO查找,也用作删除索引
|
||||
|
||||
### 查询映射
|
||||
|
||||
| 原始查询 | 目标表 | 性能提升 |
|
||||
|----------------|-------------|------------------------|
|
||||
| get_all(collection) | triples_s | 允许过滤 (对于扫描是可以接受的) |
|
||||
| get_s(collection, s) | triples_s | 直接分区访问 |
|
||||
| get_p(collection, p) | triples_p | 直接分区访问 |
|
||||
| get_o(collection, o) | triples_o | 直接分区访问 |
|
||||
| get_sp(collection, s, p) | triples_s | 分区 + 聚类 |
|
||||
| get_po(collection, p, o) | triples_p | **不再需要 ALLOW FILTERING!** |
|
||||
| get_os(collection, o, s) | triples_o | 分区 + 聚类 |
|
||||
| get_spo(collection, s, p, o) | triples_collection | 精确键查找 |
|
||||
| delete_collection(collection) | triples_collection | 读取索引,批量删除所有 |
|
||||
|
||||
### 集合删除策略
|
||||
|
||||
对于复合分区键,我们不能简单地执行 `DELETE FROM table WHERE collection = ?`。 而是:
|
||||
|
||||
1. **读取阶段:** 查询 `triples_collection` 以枚举所有三元组:
|
||||
```sql
|
||||
SELECT s, p, o FROM triples_collection WHERE collection = ?
|
||||
```
|
||||
这很高效,因为 `collection` 是此表的分割键。
|
||||
|
||||
2. **删除阶段:** 对于每个三元组 (s, p, o),使用完整的分割键从所有 4 个表中删除数据。
|
||||
```sql
|
||||
DELETE FROM triples_s WHERE collection = ? AND s = ? AND p = ? AND o = ?
|
||||
DELETE FROM triples_p WHERE collection = ? AND p = ? AND o = ? AND s = ?
|
||||
DELETE FROM triples_o WHERE collection = ? AND o = ? AND s = ? AND p = ?
|
||||
DELETE FROM triples_collection WHERE collection = ? AND s = ? AND p = ? AND o = ?
|
||||
```
|
||||
批量处理,每次100个,以提高效率。
|
||||
|
||||
**权衡分析:**
|
||||
✅ 保持最佳查询性能,采用分布式分区。
|
||||
✅ 大型集合不会出现热点分区。
|
||||
❌ 删除逻辑更复杂(先读取,再删除)。
|
||||
❌ 删除时间与集合大小成正比。
|
||||
|
||||
### 优点
|
||||
|
||||
1. **消除 ALLOW FILTERING** - 每个查询都有最佳访问路径(除了全表扫描)。
|
||||
2. **无需二级索引** - 每个表本身就是其查询模式的索引。
|
||||
3. **更好的数据分布** - 组合分区键能有效分散负载。
|
||||
4. **可预测的性能** - 查询时间与结果大小成正比,而不是总数据量。
|
||||
5. **利用 Cassandra 的优势** - 专为 Cassandra 的架构设计。
|
||||
6. **支持集合删除** - `triples_collection` 作为删除索引。
|
||||
|
||||
## 实施计划
|
||||
|
||||
### 需要修改的文件
|
||||
|
||||
#### 主要实施文件
|
||||
|
||||
**`trustgraph-flow/trustgraph/direct/cassandra_kg.py`** - 需要完全重写。
|
||||
|
||||
**需要重构的现有方法:**
|
||||
```python
|
||||
# Schema initialization
|
||||
def init(self) -> None # Replace single table with three tables
|
||||
|
||||
# Insert operations
|
||||
def insert(self, collection, s, p, o) -> None # Write to all three tables
|
||||
|
||||
# Query operations (API unchanged, implementation optimized)
|
||||
def get_all(self, collection, limit=50) # Use triples_by_subject
|
||||
def get_s(self, collection, s, limit=10) # Use triples_by_subject
|
||||
def get_p(self, collection, p, limit=10) # Use triples_by_po
|
||||
def get_o(self, collection, o, limit=10) # Use triples_by_object
|
||||
def get_sp(self, collection, s, p, limit=10) # Use triples_by_subject
|
||||
def get_po(self, collection, p, o, limit=10) # Use triples_by_po (NO ALLOW FILTERING!)
|
||||
def get_os(self, collection, o, s, limit=10) # Use triples_by_subject
|
||||
def get_spo(self, collection, s, p, o, limit=10) # Use triples_by_subject
|
||||
|
||||
# Collection management
|
||||
def delete_collection(self, collection) -> None # Delete from all three tables
|
||||
```
|
||||
|
||||
#### 集成文件 (无需修改任何逻辑)
|
||||
|
||||
**`trustgraph-flow/trustgraph/storage/triples/cassandra/write.py`**
|
||||
无需修改 - 使用现有的知识图谱 API
|
||||
自动受益于性能改进
|
||||
|
||||
**`trustgraph-flow/trustgraph/query/triples/cassandra/service.py`**
|
||||
无需修改 - 使用现有的知识图谱 API
|
||||
自动受益于性能改进
|
||||
|
||||
### 需要更新的测试文件
|
||||
|
||||
#### 单元测试
|
||||
**`tests/unit/test_storage/test_triples_cassandra_storage.py`**
|
||||
更新测试预期,以适应模式更改
|
||||
添加多表一致性测试
|
||||
验证查询计划中是否包含 ALLOW FILTERING
|
||||
|
||||
**`tests/unit/test_query/test_triples_cassandra_query.py`**
|
||||
更新性能断言
|
||||
对所有 8 种查询模式进行针对新表的测试
|
||||
验证查询是否路由到正确的表
|
||||
|
||||
#### 集成测试
|
||||
**`tests/integration/test_cassandra_integration.py`**
|
||||
使用新模式进行端到端测试
|
||||
性能基准测试比较
|
||||
跨表的数据一致性验证
|
||||
|
||||
**`tests/unit/test_storage/test_cassandra_config_integration.py`**
|
||||
更新模式验证测试
|
||||
测试迁移场景
|
||||
|
||||
### 实施策略
|
||||
|
||||
#### 第一阶段:模式和核心方法
|
||||
1. **重写 `init()` 方法** - 创建四个表而不是一个
|
||||
2. **重写 `insert()` 方法** - 批量写入所有四个表
|
||||
3. **实现预处理语句** - 以获得最佳性能
|
||||
4. **添加表路由逻辑** - 将查询定向到最佳表
|
||||
5. **实现集合删除** - 从 triples_collection 中读取,批量删除所有表中的数据
|
||||
|
||||
#### 第二阶段:查询方法优化
|
||||
1. **重写每个 get_* 方法** 以使用最佳表
|
||||
2. **删除所有 ALLOW FILTERING 的用法**
|
||||
3. **实现高效的聚类键使用**
|
||||
4. **添加查询性能日志记录**
|
||||
|
||||
#### 第三阶段:集合管理
|
||||
1. **更新 `delete_collection()`** - 从所有三个表中删除
|
||||
2. **添加一致性验证** - 确保所有表保持同步
|
||||
3. **实现批量操作** - 用于原子级多表操作
|
||||
|
||||
### 关键实施细节
|
||||
|
||||
#### 批量写入策略
|
||||
```python
|
||||
def insert(self, collection, s, p, o):
|
||||
batch = BatchStatement()
|
||||
|
||||
# Insert into all four tables
|
||||
batch.add(self.insert_subject_stmt, (collection, s, p, o))
|
||||
batch.add(self.insert_po_stmt, (collection, p, o, s))
|
||||
batch.add(self.insert_object_stmt, (collection, o, s, p))
|
||||
batch.add(self.insert_collection_stmt, (collection, s, p, o))
|
||||
|
||||
self.session.execute(batch)
|
||||
```
|
||||
|
||||
#### 查询路由逻辑
|
||||
```python
|
||||
def get_po(self, collection, p, o, limit=10):
|
||||
# Route to triples_p table - NO ALLOW FILTERING!
|
||||
return self.session.execute(
|
||||
self.get_po_stmt,
|
||||
(collection, p, o, limit)
|
||||
)
|
||||
|
||||
def get_spo(self, collection, s, p, o, limit=10):
|
||||
# Route to triples_collection table for exact SPO lookup
|
||||
return self.session.execute(
|
||||
self.get_spo_stmt,
|
||||
(collection, s, p, o, limit)
|
||||
)
|
||||
```
|
||||
|
||||
#### 集合删除逻辑
|
||||
```python
|
||||
def delete_collection(self, collection):
|
||||
# Step 1: Read all triples from collection table
|
||||
rows = self.session.execute(
|
||||
f"SELECT s, p, o FROM {self.collection_table} WHERE collection = %s",
|
||||
(collection,)
|
||||
)
|
||||
|
||||
# Step 2: Batch delete from all 4 tables
|
||||
batch = BatchStatement()
|
||||
count = 0
|
||||
|
||||
for row in rows:
|
||||
s, p, o = row.s, row.p, row.o
|
||||
|
||||
# Delete using full partition keys for each table
|
||||
batch.add(SimpleStatement(
|
||||
f"DELETE FROM {self.subject_table} WHERE collection = ? AND s = ? AND p = ? AND o = ?"
|
||||
), (collection, s, p, o))
|
||||
|
||||
batch.add(SimpleStatement(
|
||||
f"DELETE FROM {self.po_table} WHERE collection = ? AND p = ? AND o = ? AND s = ?"
|
||||
), (collection, p, o, s))
|
||||
|
||||
batch.add(SimpleStatement(
|
||||
f"DELETE FROM {self.object_table} WHERE collection = ? AND o = ? AND s = ? AND p = ?"
|
||||
), (collection, o, s, p))
|
||||
|
||||
batch.add(SimpleStatement(
|
||||
f"DELETE FROM {self.collection_table} WHERE collection = ? AND s = ? AND p = ? AND o = ?"
|
||||
), (collection, s, p, o))
|
||||
|
||||
count += 1
|
||||
|
||||
# Execute every 100 triples to avoid oversized batches
|
||||
if count % 100 == 0:
|
||||
self.session.execute(batch)
|
||||
batch = BatchStatement()
|
||||
|
||||
# Execute remaining deletions
|
||||
if count % 100 != 0:
|
||||
self.session.execute(batch)
|
||||
|
||||
logger.info(f"Deleted {count} triples from collection {collection}")
|
||||
```
|
||||
|
||||
#### 预处理语句优化
|
||||
```python
|
||||
def prepare_statements(self):
|
||||
# Cache prepared statements for better performance
|
||||
self.insert_subject_stmt = self.session.prepare(
|
||||
f"INSERT INTO {self.subject_table} (collection, s, p, o) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
self.insert_po_stmt = self.session.prepare(
|
||||
f"INSERT INTO {self.po_table} (collection, p, o, s) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
self.insert_object_stmt = self.session.prepare(
|
||||
f"INSERT INTO {self.object_table} (collection, o, s, p) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
self.insert_collection_stmt = self.session.prepare(
|
||||
f"INSERT INTO {self.collection_table} (collection, s, p, o) VALUES (?, ?, ?, ?)"
|
||||
)
|
||||
# ... query statements
|
||||
```
|
||||
|
||||
## 迁移策略
|
||||
|
||||
### 数据迁移方法
|
||||
|
||||
#### 选项 1:蓝绿部署(推荐)
|
||||
1. **并行部署新模式和现有模式** - 暂时使用不同的表名
|
||||
2. **双写期** - 在过渡期间同时写入旧模式和新模式
|
||||
3. **后台迁移** - 将现有数据复制到新表中
|
||||
4. **切换读取** - 在数据迁移完成后,将查询路由到新表中
|
||||
5. **删除旧表** - 在验证期结束后
|
||||
|
||||
#### 选项 2:原地迁移
|
||||
1. **模式添加** - 在现有键空间中创建新表
|
||||
2. **数据迁移脚本** - 批量从旧表复制到新表
|
||||
3. **应用程序更新** - 在迁移完成后部署新代码
|
||||
4. **清理旧表** - 删除旧表和索引
|
||||
|
||||
### 向后兼容性
|
||||
|
||||
#### 部署策略
|
||||
```python
|
||||
# Environment variable to control table usage during migration
|
||||
USE_LEGACY_TABLES = os.getenv('CASSANDRA_USE_LEGACY', 'false').lower() == 'true'
|
||||
|
||||
class KnowledgeGraph:
|
||||
def __init__(self, ...):
|
||||
if USE_LEGACY_TABLES:
|
||||
self.init_legacy_schema()
|
||||
else:
|
||||
self.init_optimized_schema()
|
||||
```
|
||||
|
||||
#### 迁移脚本
|
||||
```python
|
||||
def migrate_data():
|
||||
# Read from old table
|
||||
old_triples = session.execute("SELECT collection, s, p, o FROM triples")
|
||||
|
||||
# Batch write to new tables
|
||||
for batch in batched(old_triples, 100):
|
||||
batch_stmt = BatchStatement()
|
||||
for row in batch:
|
||||
# Add to all three new tables
|
||||
batch_stmt.add(insert_subject_stmt, row)
|
||||
batch_stmt.add(insert_po_stmt, (row.collection, row.p, row.o, row.s))
|
||||
batch_stmt.add(insert_object_stmt, (row.collection, row.o, row.s, row.p))
|
||||
session.execute(batch_stmt)
|
||||
```
|
||||
|
||||
### 验证策略
|
||||
|
||||
#### 数据一致性检查
|
||||
```python
|
||||
def validate_migration():
|
||||
# Count total records in old vs new tables
|
||||
old_count = session.execute("SELECT COUNT(*) FROM triples WHERE collection = ?", (collection,))
|
||||
new_count = session.execute("SELECT COUNT(*) FROM triples_by_subject WHERE collection = ?", (collection,))
|
||||
|
||||
assert old_count == new_count, f"Record count mismatch: {old_count} vs {new_count}"
|
||||
|
||||
# Spot check random samples
|
||||
sample_queries = generate_test_queries()
|
||||
for query in sample_queries:
|
||||
old_result = execute_legacy_query(query)
|
||||
new_result = execute_optimized_query(query)
|
||||
assert old_result == new_result, f"Query results differ for {query}"
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 性能测试
|
||||
|
||||
#### 基准测试场景
|
||||
1. **查询性能比较**
|
||||
所有 8 种查询类型的性能指标(测试前后)
|
||||
重点关注 get_po 性能改进(消除 ALLOW FILTERING)
|
||||
测量不同数据规模下的查询延迟
|
||||
|
||||
2. **负载测试**
|
||||
并发查询执行
|
||||
批量操作的写入吞吐量
|
||||
内存和 CPU 利用率
|
||||
|
||||
3. **可伸缩性测试**
|
||||
随着集合大小增加的性能
|
||||
多集合查询的分布
|
||||
集群节点利用率
|
||||
|
||||
#### 测试数据集
|
||||
**小型:** 每个集合 10K 个三元组
|
||||
**中型:** 每个集合 100K 个三元组
|
||||
**大型:** 1M+ 个三元组
|
||||
**多个集合:** 测试分区分布
|
||||
|
||||
### 功能测试
|
||||
|
||||
#### 单元测试更新
|
||||
```python
|
||||
# Example test structure for new implementation
|
||||
class TestCassandraKGPerformance:
|
||||
def test_get_po_no_allow_filtering(self):
|
||||
# Verify get_po queries don't use ALLOW FILTERING
|
||||
with patch('cassandra.cluster.Session.execute') as mock_execute:
|
||||
kg.get_po('test_collection', 'predicate', 'object')
|
||||
executed_query = mock_execute.call_args[0][0]
|
||||
assert 'ALLOW FILTERING' not in executed_query
|
||||
|
||||
def test_multi_table_consistency(self):
|
||||
# Verify all tables stay in sync
|
||||
kg.insert('test', 's1', 'p1', 'o1')
|
||||
|
||||
# Check all tables contain the triple
|
||||
assert_triple_exists('triples_by_subject', 'test', 's1', 'p1', 'o1')
|
||||
assert_triple_exists('triples_by_po', 'test', 'p1', 'o1', 's1')
|
||||
assert_triple_exists('triples_by_object', 'test', 'o1', 's1', 'p1')
|
||||
```
|
||||
|
||||
#### 集成测试更新
|
||||
```python
|
||||
class TestCassandraIntegration:
|
||||
def test_query_performance_regression(self):
|
||||
# Ensure new implementation is faster than old
|
||||
old_time = benchmark_legacy_get_po()
|
||||
new_time = benchmark_optimized_get_po()
|
||||
assert new_time < old_time * 0.5 # At least 50% improvement
|
||||
|
||||
def test_end_to_end_workflow(self):
|
||||
# Test complete write -> query -> delete cycle
|
||||
# Verify no performance degradation in integration
|
||||
```
|
||||
|
||||
### 回滚计划
|
||||
|
||||
#### 快速回滚策略
|
||||
1. **环境变量切换** - 立即切换回旧版表
|
||||
2. **保留旧版表** - 在性能得到验证之前,不要删除
|
||||
3. **监控警报** - 基于错误率/延迟的自动化回滚触发
|
||||
|
||||
#### 回滚验证
|
||||
```python
|
||||
def rollback_to_legacy():
|
||||
# Set environment variable
|
||||
os.environ['CASSANDRA_USE_LEGACY'] = 'true'
|
||||
|
||||
# Restart services to pick up change
|
||||
restart_cassandra_services()
|
||||
|
||||
# Validate functionality
|
||||
run_smoke_tests()
|
||||
```
|
||||
|
||||
## 风险与考量
|
||||
|
||||
### 性能风险
|
||||
**写入延迟增加** - 每个插入操作需要 4 次写入(比 3 表方案多 33%)
|
||||
**存储开销** - 需要 4 倍的存储空间(比 3 表方案多 33%)
|
||||
**批量写入失败** - 需要适当的错误处理
|
||||
**删除复杂性** - 集合删除需要先读取再删除的循环
|
||||
|
||||
### 运维风险
|
||||
**迁移复杂性** - 大数据集的数据迁移
|
||||
**一致性挑战** - 确保所有表保持同步
|
||||
**监控缺失** - 需要新的指标来监控多表操作
|
||||
|
||||
### 缓解策略
|
||||
1. **逐步推广** - 从小型集合开始
|
||||
2. **全面监控** - 跟踪所有性能指标
|
||||
3. **自动化验证** - 持续进行一致性检查
|
||||
4. **快速回滚能力** - 基于环境选择表
|
||||
|
||||
## 成功标准
|
||||
|
||||
### 性能改进
|
||||
[ ] **消除 ALLOW FILTERING** - `get_po` 和 `get_os` 查询在没有过滤的情况下运行
|
||||
[ ] **查询延迟降低** - 查询响应时间提高 50% 以上
|
||||
[ ] **更好的负载分布** - 没有热分区,集群节点上的负载分布均匀
|
||||
[ ] **可扩展的性能** - 查询时间与结果大小成正比,而不是与总数据量成正比
|
||||
|
||||
### 功能需求
|
||||
[ ] **API 兼容性** - 所有现有代码继续保持不变
|
||||
[ ] **数据一致性** - 所有三个表保持同步
|
||||
[ ] **零数据丢失** - 迁移保留所有现有三元组
|
||||
[ ] **向后兼容性** - 能够回滚到旧模式
|
||||
|
||||
### 运维需求
|
||||
[ ] **安全迁移** - 蓝绿部署,具有回滚能力
|
||||
[ ] **监控覆盖** - 针对多表操作的全面指标
|
||||
[ ] **测试覆盖** - 所有查询模式都经过性能基准测试
|
||||
[ ] **文档** - 更新部署和运维流程
|
||||
|
||||
## 时间线
|
||||
|
||||
### 第一阶段:实施
|
||||
[ ] 使用多表模式重写 `cassandra_kg.py`
|
||||
[ ] 实施批量写入操作
|
||||
[ ] 添加准备语句优化
|
||||
[ ] 更新单元测试
|
||||
|
||||
### 第二阶段:集成测试
|
||||
[ ] 更新集成测试
|
||||
[ ] 性能基准测试
|
||||
[ ] 使用真实数据量的负载测试
|
||||
[ ] 数据一致性验证脚本
|
||||
|
||||
### 第三阶段:迁移规划
|
||||
[ ] 蓝绿部署脚本
|
||||
[ ] 数据迁移工具
|
||||
[ ] 监控仪表板更新
|
||||
[ ] 回滚流程
|
||||
|
||||
### 第四阶段:生产部署
|
||||
[ ] 逐步推广到生产环境
|
||||
[ ] 性能监控和验证
|
||||
[ ] 清理旧表
|
||||
[ ] 文档更新
|
||||
|
||||
## 结论
|
||||
|
||||
这种多表反规范化策略直接解决了两个关键的性能瓶颈:
|
||||
|
||||
1. **消除昂贵的 ALLOW FILTERING**,通过为每种查询模式提供最佳的表结构
|
||||
2. **提高聚类效率**,通过复合分区键来合理地分配负载
|
||||
|
||||
该方法利用了 Cassandra 的优势,同时保持完整的 API 兼容性,确保现有代码能够自动受益于性能改进。
|
||||
410
docs/tech-specs/zh-cn/collection-management.zh-cn.md
Normal file
410
docs/tech-specs/zh-cn/collection-management.zh-cn.md
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
---
|
||||
layout: default
|
||||
title: "Collection Management Technical Specification"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# Collection Management Technical Specification
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## Overview
|
||||
|
||||
本规范描述了 TrustGraph 的集合管理功能,要求显式创建集合,并提供对集合生命周期的直接控制。在开始使用之前,必须显式创建集合,以确保 librarian 元数据与所有存储后端之间的正确同步。该功能支持四个主要用例:
|
||||
|
||||
1. **集合创建 (Collection Creation)**: 在存储数据之前,显式创建集合。
|
||||
2. **集合列表 (Collection Listing)**: 查看系统中所有现有的集合。
|
||||
3. **集合元数据管理 (Collection Metadata Management)**: 更新集合的名称、描述和标签。
|
||||
4. **集合删除 (Collection Deletion)**: 删除集合及其关联的数据,包括所有存储类型。
|
||||
|
||||
## Goals
|
||||
|
||||
**显式集合创建 (Explicit Collection Creation)**: 要求在存储数据之前创建集合。
|
||||
**存储同步 (Storage Synchronization)**: 确保集合存在于所有存储后端(向量、对象、三元组)中。
|
||||
**集合可见性 (Collection Visibility)**: 允许用户列出并检查其环境中的所有集合。
|
||||
**集合清理 (Collection Cleanup)**: 允许删除不再需要的集合。
|
||||
**集合组织 (Collection Organization)**: 支持标签和标记,以便更好地跟踪和发现集合。
|
||||
**元数据管理 (Metadata Management)**: 为集合关联有意义的元数据,以提高操作的清晰度。
|
||||
**集合发现 (Collection Discovery)**: 通过过滤和搜索,更容易找到特定的集合。
|
||||
**操作透明性 (Operational Transparency)**: 提供对集合生命周期和使用的清晰可见性。
|
||||
**资源管理 (Resource Management)**: 允许清理未使用的集合,以优化资源利用率。
|
||||
**数据完整性 (Data Integrity)**: 阻止在存储中出现没有元数据跟踪的孤立集合。
|
||||
|
||||
## Background
|
||||
|
||||
以前,TrustGraph 中的集合是在数据加载操作期间隐式创建的,这会导致同步问题,即集合可能存在于存储后端中,但 librarian 中没有相应的元数据。这带来了管理挑战和潜在的数据孤立问题。
|
||||
|
||||
显式集合创建模型通过以下方式解决了这些问题:
|
||||
通过 `tg-set-collection` 要求在开始使用之前创建集合。
|
||||
将集合创建广播到所有存储后端。
|
||||
维护 librarian 元数据和存储之间的同步状态。
|
||||
阻止写入不存在的集合。
|
||||
提供清晰的集合生命周期管理。
|
||||
|
||||
本规范定义了显式集合管理模型。通过要求显式创建集合,TrustGraph 确保:
|
||||
集合从创建开始就在 librarian 元数据中进行跟踪。
|
||||
在接收数据之前,所有存储后端都了解集合。
|
||||
存储中不存在孤立的集合。
|
||||
对集合生命周期具有清晰的操作可见性和控制。
|
||||
当操作引用不存在的集合时,提供一致的错误处理。
|
||||
|
||||
## Technical Design
|
||||
|
||||
### Architecture
|
||||
|
||||
集合管理系统将实现于现有的 TrustGraph 基础设施中:
|
||||
|
||||
1. **Librarian Service Integration**
|
||||
集合管理操作将被添加到现有的 librarian 服务中。
|
||||
不需要新的服务 - 利用现有的身份验证和访问模式。
|
||||
处理集合列表、删除和元数据管理。
|
||||
|
||||
模块: trustgraph-librarian
|
||||
|
||||
2. **Cassandra Collection Metadata Table**
|
||||
在现有的 librarian keyspace 中创建一个新的表。
|
||||
存储具有用户范围访问权限的集合元数据。
|
||||
主键:(user_id, collection_id),用于适当的多租户支持。
|
||||
|
||||
模块: trustgraph-librarian
|
||||
|
||||
3. **Collection Management CLI**
|
||||
用于集合操作的命令行界面。
|
||||
提供列表、删除、标签和标记管理命令。
|
||||
与现有的 CLI 框架集成。
|
||||
|
||||
模块: trustgraph-cli
|
||||
|
||||
### Data Models
|
||||
|
||||
#### Cassandra Collection Metadata Table
|
||||
|
||||
集合元数据将存储在 librarian keyspace 中的结构化 Cassandra 表中:
|
||||
|
||||
```sql
|
||||
CREATE TABLE collections (
|
||||
user text,
|
||||
collection text,
|
||||
name text,
|
||||
description text,
|
||||
tags set<text>,
|
||||
created_at timestamp,
|
||||
updated_at timestamp,
|
||||
PRIMARY KEY (user, collection)
|
||||
);
|
||||
```
|
||||
|
||||
表结构:
|
||||
**user** + **collection**: 复合主键,确保用户隔离
|
||||
**name**: 人类可读的集合名称
|
||||
**description**: 集合用途的详细描述
|
||||
**tags**: 用于分类和过滤的标签集合
|
||||
**created_at**: 集合创建时间戳
|
||||
**updated_at**: 最后修改时间戳
|
||||
|
||||
此方法允许:
|
||||
多租户集合管理,具有用户隔离
|
||||
通过用户和集合进行高效查询
|
||||
灵活的标签系统,用于组织
|
||||
生命周期跟踪,用于运营洞察
|
||||
|
||||
#### 集合生命周期
|
||||
|
||||
在进行任何数据操作之前,必须在 librarian 中显式创建集合:
|
||||
|
||||
1. **集合创建** (两种路径):
|
||||
|
||||
**路径 A: 用户发起创建** 通过 `tg-set-collection`:
|
||||
用户提供集合 ID、名称、描述和标签
|
||||
Librarian 在 `collections` 表中创建元数据记录
|
||||
Librarian 向所有存储后端广播 "create-collection"
|
||||
所有存储处理器创建集合并确认成功
|
||||
集合现在可以用于数据操作
|
||||
|
||||
**路径 B: 在文档提交时自动创建**:
|
||||
用户提交指定集合 ID 的文档
|
||||
Librarian 检查集合是否存在于元数据表中
|
||||
如果不存在: Librarian 使用默认值创建元数据 (name=collection_id, 描述/标签为空)
|
||||
Librarian 向所有存储后端广播 "create-collection"
|
||||
所有存储处理器创建集合并确认成功
|
||||
文档处理继续,此时集合已建立
|
||||
|
||||
两种路径都确保集合存在于 librarian 元数据中,并且所有存储后端都已同步,然后再允许进行数据操作。
|
||||
|
||||
2. **存储验证**: 写入操作验证集合是否存在:
|
||||
存储处理器在接受写入操作之前检查集合状态
|
||||
写入不存在的集合会返回错误
|
||||
这可以防止直接写入绕过 librarian 的集合创建逻辑
|
||||
|
||||
3. **查询行为**: 查询操作可以优雅地处理不存在的集合:
|
||||
查询不存在的集合会返回空结果
|
||||
不会为查询操作抛出错误
|
||||
允许探索,而无需集合存在
|
||||
|
||||
4. **元数据更新**: 用户可以在创建后更新集合元数据:
|
||||
通过 `tg-set-collection` 更新名称、描述和标签
|
||||
更新仅应用于 librarian 元数据
|
||||
存储后端保留集合,但元数据更新不会传播
|
||||
|
||||
5. **显式删除**: 用户通过 `tg-delete-collection` 删除集合:
|
||||
Librarian 向所有存储后端广播 "delete-collection"
|
||||
等待来自所有存储处理器的确认
|
||||
仅在存储清理完成后删除 librarian 元数据记录
|
||||
确保存储中没有孤立数据
|
||||
|
||||
**关键原则**: librarian 是集合创建的唯一控制点。 无论是通过用户命令还是文档提交发起,librarian 都会确保在允许进行数据操作之前,正确跟踪元数据并与存储后端同步。
|
||||
|
||||
需要的操作:
|
||||
**创建集合**: 通过 `tg-set-collection` 进行用户操作,或在文档提交时自动进行
|
||||
**更新集合元数据**: 用户操作,用于修改名称、描述和标签
|
||||
**删除集合**: 用户操作,用于删除集合及其数据,删除所有存储中的数据
|
||||
**列出集合**: 用户操作,用于查看带有标签过滤的集合
|
||||
|
||||
#### 多存储集合管理
|
||||
|
||||
集合存在于 TrustGraph 中的多个存储后端中:
|
||||
**向量存储** (Qdrant, Milvus, Pinecone): 存储嵌入和向量数据
|
||||
**对象存储** (Cassandra): 存储文档和文件数据
|
||||
**三元存储** (Cassandra, Neo4j, Memgraph, FalkorDB): 存储图/RDF 数据
|
||||
|
||||
Each store type implements:
|
||||
**Collection State Tracking**: Maintain knowledge of which collections exist
|
||||
**Collection Creation**: Accept and process "create-collection" operations
|
||||
**Collection Validation**: Check collection exists before accepting writes
|
||||
**Collection Deletion**: Remove all data for specified collection
|
||||
|
||||
The librarian service coordinates collection operations across all store types, ensuring:
|
||||
Collections created in all backends before use
|
||||
All backends confirm creation before returning success
|
||||
Synchronized collection lifecycle across storage types
|
||||
Consistent error handling when collections don't exist
|
||||
|
||||
#### Collection State Tracking by Storage Type
|
||||
|
||||
Each storage backend tracks collection state differently based on its capabilities:
|
||||
|
||||
**Cassandra Triple Store:**
|
||||
Uses existing `triples_collection` table
|
||||
Creates system marker triple when collection created
|
||||
Query: `SELECT collection FROM triples_collection WHERE collection = ? LIMIT 1`
|
||||
Efficient single-partition check for collection existence
|
||||
|
||||
**Qdrant/Milvus/Pinecone Vector Stores:**
|
||||
Native collection APIs provide existence checking
|
||||
Collections created with proper vector configuration
|
||||
`collection_exists()` method uses storage API
|
||||
Collection creation validates dimension requirements
|
||||
|
||||
**Neo4j/Memgraph/FalkorDB Graph Stores:**
|
||||
Use `:CollectionMetadata` nodes to track collections
|
||||
Node properties: `{user, collection, created_at}`
|
||||
Query: `MATCH (c:CollectionMetadata {user: $user, collection: $collection})`
|
||||
Separate from data nodes for clean separation
|
||||
Enables efficient collection listing and validation
|
||||
|
||||
**Cassandra Object Store:**
|
||||
Uses collection metadata table or marker rows
|
||||
Similar pattern to triple store
|
||||
Validates collection before document writes
|
||||
|
||||
### APIs
|
||||
|
||||
Collection Management APIs (Librarian):
|
||||
**Create/Update Collection**: Create new collection or update existing metadata via `tg-set-collection`
|
||||
**List Collections**: Retrieve collections for a user with optional tag filtering
|
||||
**Delete Collection**: Remove collection and associated data, cascading to all store types
|
||||
|
||||
Storage Management APIs (All Storage Processors):
|
||||
**Create Collection**: Handle "create-collection" operation, establish collection in storage
|
||||
**Delete Collection**: Handle "delete-collection" operation, remove all collection data
|
||||
**Collection Exists Check**: Internal validation before accepting write operations
|
||||
|
||||
Data Operation APIs (Modified Behavior):
|
||||
**Write APIs**: Validate collection exists before accepting data, return error if not
|
||||
**Query APIs**: Return empty results for non-existent collections without error
|
||||
|
||||
### Implementation Details
|
||||
|
||||
The implementation will follow existing TrustGraph patterns for service integration and CLI command structure.
|
||||
|
||||
#### Collection Deletion Cascade
|
||||
|
||||
When a user initiates collection deletion through the librarian service:
|
||||
|
||||
1. **Metadata Validation**: Verify collection exists and user has permission to delete
|
||||
2. **Store Cascade**: Librarian coordinates deletion across all store writers:
|
||||
Vector store writer: Remove embeddings and vector indexes for the user and collection
|
||||
Object store writer: Remove documents and files for the user and collection
|
||||
Triple store writer: Remove graph data and triples for the user and collection
|
||||
3. **Metadata Cleanup**: Remove collection metadata record from Cassandra
|
||||
4. **Error Handling**: If any store deletion fails, maintain consistency through rollback or retry mechanisms
|
||||
|
||||
#### Collection Management Interface
|
||||
|
||||
**⚠️ LEGACY APPROACH - REPLACED BY CONFIG-BASED PATTERN**
|
||||
|
||||
The queue-based architecture described below has been replaced with a config-based approach using `CollectionConfigHandler`. All storage backends now receive collection updates via config push messages instead of dedicated management queues.
|
||||
|
||||
~~All store writers implement a standardized collection management interface with a common schema:~~
|
||||
|
||||
~~**Message Schema (`StorageManagementRequest`):**~~
|
||||
```json
|
||||
{
|
||||
"operation": "create-collection" | "delete-collection",
|
||||
"user": "user123",
|
||||
"collection": "documents-2024"
|
||||
}
|
||||
```
|
||||
|
||||
~~**队列架构:**~~
|
||||
~~**向量存储管理队列** (`vector-storage-management`): 向量/嵌入式存储~~
|
||||
~~**对象存储管理队列** (`object-storage-management`): 对象/文档存储~~
|
||||
~~**三元组存储管理队列** (`triples-storage-management`): 图/RDF 存储~~
|
||||
~~**存储响应队列** (`storage-management-response`): 所有响应都发送到此处~~
|
||||
|
||||
**当前实现:**
|
||||
|
||||
所有存储后端现在都使用 `CollectionConfigHandler`:
|
||||
**配置推送集成**: 存储服务注册以接收配置推送通知
|
||||
**自动同步**: 基于配置更改创建/删除集合
|
||||
**声明式模型**: 集合在配置服务中定义,后端同步以匹配
|
||||
**无请求/响应**: 消除协调开销和响应跟踪
|
||||
**集合状态跟踪**: 通过 `known_collections` 缓存维护
|
||||
**幂等操作**: 安全地多次处理相同的配置
|
||||
|
||||
每个存储后端实现:
|
||||
`create_collection(user: str, collection: str, metadata: dict)` - 创建集合结构
|
||||
`delete_collection(user: str, collection: str)` - 移除所有集合数据
|
||||
`collection_exists(user: str, collection: str) -> bool` - 在写入之前进行验证
|
||||
|
||||
#### Cassandra 三元组存储重构
|
||||
|
||||
作为此实现的组成部分,Cassandra 三元组存储将从每个集合一个表的模型重构为统一的表模型:
|
||||
|
||||
**当前架构:**
|
||||
每个用户的键空间,每个集合的单独表
|
||||
模式:`(s, p, o)` with `PRIMARY KEY (s, p, o)`
|
||||
表名:用户集合成为单独的 Cassandra 表
|
||||
|
||||
**新的架构:**
|
||||
每个用户的键空间,用于所有集合的单个 "triples" 表
|
||||
模式:`(collection, s, p, o)` with `PRIMARY KEY (collection, s, p, o)`
|
||||
通过集合分区实现集合隔离
|
||||
|
||||
**需要更改的内容:**
|
||||
|
||||
1. **TrustGraph 类重构** (`trustgraph/direct/cassandra.py`):
|
||||
移除构造函数中的 `table` 参数,使用固定的 "triples" 表
|
||||
在所有方法中添加 `collection` 参数
|
||||
更新模式以包含集合作为第一列
|
||||
**索引更新**: 将创建新的索引以支持所有 8 个查询模式:
|
||||
在 `(s)` 上创建索引以进行基于主体的查询
|
||||
在 `(p)` 上创建索引以进行基于谓词的查询
|
||||
在 `(o)` 上创建索引以进行基于对象的查询
|
||||
注意:Cassandra 不支持多列二级索引,因此这些是单列索引
|
||||
|
||||
**查询模式性能:**
|
||||
✅ `get_all()` - 在 `collection` 上进行分区扫描
|
||||
✅ `get_s(s)` - 效率地使用主键 (`collection, s`)
|
||||
✅ `get_p(p)` - 使用 `idx_p` 并进行 `collection` 过滤
|
||||
✅ `get_o(o)` - 使用 `idx_o` 并进行 `collection` 过滤
|
||||
✅ `get_sp(s, p)` - 效率地使用主键 (`collection, s, p`)
|
||||
⚠️ `get_po(p, o)` - 需要 `ALLOW FILTERING` (使用 `idx_p` 或 `idx_o` 加上过滤)
|
||||
✅ `get_os(o, s)` - 使用 `idx_o` 并进行额外的 `s` 过滤
|
||||
✅ `get_spo(s, p, o)` - 效率地使用完整主键
|
||||
|
||||
**关于 ALLOW FILTERING 的说明**: `get_po` 查询模式需要 `ALLOW FILTERING`,因为它需要谓词和对象约束,而没有合适的复合索引。 这是一个可以接受的,因为此查询模式比在典型的三元组存储用例中基于主体的查询不太常见。
|
||||
|
||||
2. **存储写入器更新** (`trustgraph/storage/triples/cassandra/write.py`):
|
||||
维护每个用户单个 TrustGraph 连接,而不是每个 (用户, 集合)
|
||||
将集合传递给插入操作
|
||||
通过减少连接数提高资源利用率
|
||||
|
||||
3. **查询服务更新** (`trustgraph/query/triples/cassandra/service.py`):
|
||||
每个用户的单个 TrustGraph 连接
|
||||
将集合传递给所有查询操作
|
||||
保持相同的查询逻辑,并带有集合参数
|
||||
|
||||
**优点:**
|
||||
**简化集合删除**: 使用 `collection` 分区键删除所有 4 个表
|
||||
**资源效率**: 减少数据库连接和表对象
|
||||
**跨集合操作**: 更容易实现跨多个集合的操作
|
||||
**一致的架构**: 与统一的集合元数据方法保持一致
|
||||
**集合验证**: 可以通过 `triples_collection` 表轻松检查集合是否存在
|
||||
|
||||
集合操作在可能的情况下将是原子操作,并提供适当的错误处理和验证。
|
||||
|
||||
## 安全性考虑
|
||||
|
||||
集合管理操作需要适当的授权,以防止未经授权的访问或删除集合。 访问控制将与现有的 TrustGraph 安全模型保持一致。
|
||||
|
||||
## 性能考虑
|
||||
|
||||
集合列表操作可能需要分页,以适应具有大量集合的环境。 元数据查询应针对常见的过滤模式进行优化。
|
||||
|
||||
## 测试策略
|
||||
|
||||
综合测试将涵盖:
|
||||
集合创建工作流程的端到端流程
|
||||
存储后端同步
|
||||
对不存在集合的写入验证
|
||||
对不存在集合的查询处理
|
||||
集合删除在所有存储中的级联操作
|
||||
错误处理和恢复场景
|
||||
每个存储后端的单元测试
|
||||
跨存储操作的集成测试
|
||||
|
||||
## 实施状态
|
||||
|
||||
### ✅ 已完成的组件
|
||||
|
||||
1. **Librarian 集合管理服务** (`trustgraph-flow/trustgraph/librarian/collection_manager.py`)
|
||||
集合元数据 CRUD 操作(列表、更新、删除)
|
||||
通过 `LibraryTableStore` 与 Cassandra 集合元数据表集成
|
||||
集合删除在所有存储类型上的级联协调
|
||||
具有适当错误管理的异步请求/响应处理
|
||||
|
||||
2. **集合元数据模式** (`trustgraph-base/trustgraph/schema/services/collection.py`)
|
||||
`CollectionManagementRequest` 和 `CollectionManagementResponse` 模式
|
||||
`CollectionMetadata` 模式用于集合记录
|
||||
集合请求/响应队列主题定义
|
||||
|
||||
3. **存储管理模式** (`trustgraph-base/trustgraph/schema/services/storage.py`)
|
||||
`StorageManagementRequest` 和 `StorageManagementResponse` 模式
|
||||
定义了存储管理队列主题
|
||||
存储级别集合操作的消息格式
|
||||
|
||||
4. **Cassandra 4-表模式** (`trustgraph-flow/trustgraph/direct/cassandra_kg.py`)
|
||||
用于查询性能的复合分区键
|
||||
`triples_collection` 表用于 SPO 查询和删除跟踪
|
||||
集合删除使用读后删除模式实现
|
||||
|
||||
### ✅ 迁移到基于配置的模式 - 已完成
|
||||
|
||||
**所有存储后端已从基于队列的模式迁移到基于配置的 `CollectionConfigHandler` 模式。**
|
||||
|
||||
已完成的迁移:
|
||||
✅ `trustgraph-flow/trustgraph/storage/triples/cassandra/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/triples/neo4j/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/triples/memgraph/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/triples/falkordb/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/doc_embeddings/qdrant/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/graph_embeddings/qdrant/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/doc_embeddings/milvus/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/graph_embeddings/milvus/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/doc_embeddings/pinecone/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/graph_embeddings/pinecone/write.py`
|
||||
✅ `trustgraph-flow/trustgraph/storage/objects/cassandra/write.py`
|
||||
|
||||
所有后端现在:
|
||||
继承自 `CollectionConfigHandler`
|
||||
通过 `self.register_config_handler(self.on_collection_config)` 注册配置推送通知
|
||||
实施 `create_collection(user, collection, metadata)` 和 `delete_collection(user, collection)`
|
||||
使用 `collection_exists(user, collection)` 在写入之前进行验证
|
||||
自动与配置服务更改同步
|
||||
|
||||
移除了旧的基于队列的基础设施:
|
||||
✅ 移除了 `StorageManagementRequest` 和 `StorageManagementResponse` 模式
|
||||
✅ 移除了存储管理队列主题定义
|
||||
✅ 移除了所有后端中的存储管理生产者/消费者
|
||||
✅ 移除了所有后端中的 `on_storage_management` 处理程序
|
||||
144
docs/tech-specs/zh-cn/document-embeddings-chunk-id.zh-cn.md
Normal file
144
docs/tech-specs/zh-cn/document-embeddings-chunk-id.zh-cn.md
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
---
|
||||
layout: default
|
||||
title: "文档嵌入块 ID"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 文档嵌入块 ID
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
目前,文档嵌入存储将块文本直接存储在向量存储的负载中,这会重复 Garage 中已存在的数据。此规范将块文本存储替换为对 `chunk_id` 的引用。
|
||||
|
||||
## 当前状态
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ChunkEmbeddings:
|
||||
chunk: bytes = b""
|
||||
vectors: list[list[float]] = field(default_factory=list)
|
||||
|
||||
@dataclass
|
||||
class DocumentEmbeddingsResponse:
|
||||
error: Error | None = None
|
||||
chunks: list[str] = field(default_factory=list)
|
||||
```
|
||||
|
||||
向量存储负载:
|
||||
```python
|
||||
payload={"doc": chunk} # Duplicates Garage content
|
||||
```
|
||||
|
||||
## 设计
|
||||
|
||||
### 模式变更
|
||||
|
||||
**ChunkEmbeddings** - 将 chunk 替换为 chunk_id:
|
||||
```python
|
||||
@dataclass
|
||||
class ChunkEmbeddings:
|
||||
chunk_id: str = ""
|
||||
vectors: list[list[float]] = field(default_factory=list)
|
||||
```
|
||||
|
||||
**DocumentEmbeddingsResponse** - 返回 chunk_ids 而不是 chunks:
|
||||
```python
|
||||
@dataclass
|
||||
class DocumentEmbeddingsResponse:
|
||||
error: Error | None = None
|
||||
chunk_ids: list[str] = field(default_factory=list)
|
||||
```
|
||||
|
||||
### 向量存储负载
|
||||
|
||||
所有存储(Qdrant、Milvus、Pinecone):
|
||||
```python
|
||||
payload={"chunk_id": chunk_id}
|
||||
```
|
||||
|
||||
### 文档 RAG 变更
|
||||
|
||||
文档 RAG 处理器从 Garage 中获取块内容:
|
||||
|
||||
```python
|
||||
# Get chunk_ids from embeddings store
|
||||
chunk_ids = await self.rag.doc_embeddings_client.query(...)
|
||||
|
||||
# Fetch chunk content from Garage
|
||||
docs = []
|
||||
for chunk_id in chunk_ids:
|
||||
content = await self.rag.librarian_client.get_document_content(
|
||||
chunk_id, self.user
|
||||
)
|
||||
docs.append(content)
|
||||
```
|
||||
|
||||
### API/SDK 变更
|
||||
|
||||
**DocumentEmbeddingsClient** 返回 chunk_ids:
|
||||
```python
|
||||
return resp.chunk_ids # Changed from resp.chunks
|
||||
```
|
||||
|
||||
**数据格式** (DocumentEmbeddingsResponseTranslator):
|
||||
```python
|
||||
result["chunk_ids"] = obj.chunk_ids # Changed from chunks
|
||||
```
|
||||
|
||||
### CLI 变更
|
||||
|
||||
CLI 工具显示 chunk_ids(如果需要,调用者可以单独获取内容)。
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
### Schema
|
||||
`trustgraph-base/trustgraph/schema/knowledge/embeddings.py` - ChunkEmbeddings
|
||||
`trustgraph-base/trustgraph/schema/services/query.py` - DocumentEmbeddingsResponse
|
||||
|
||||
### 消息/翻译器
|
||||
`trustgraph-base/trustgraph/messaging/translators/embeddings_query.py` - DocumentEmbeddingsResponseTranslator
|
||||
|
||||
### 客户端
|
||||
`trustgraph-base/trustgraph/base/document_embeddings_client.py` - 返回 chunk_ids
|
||||
|
||||
### Python SDK/API
|
||||
`trustgraph-base/trustgraph/api/flow.py` - document_embeddings_query
|
||||
`trustgraph-base/trustgraph/api/socket_client.py` - document_embeddings_query
|
||||
`trustgraph-base/trustgraph/api/async_flow.py` - 如果适用
|
||||
`trustgraph-base/trustgraph/api/bulk_client.py` - 导入/导出文档嵌入
|
||||
`trustgraph-base/trustgraph/api/async_bulk_client.py` - 导入/导出文档嵌入
|
||||
|
||||
### 嵌入服务
|
||||
`trustgraph-flow/trustgraph/embeddings/document_embeddings/embeddings.py` - 传递 chunk_id
|
||||
|
||||
### 存储写入器
|
||||
`trustgraph-flow/trustgraph/storage/doc_embeddings/qdrant/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/doc_embeddings/milvus/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/doc_embeddings/pinecone/write.py`
|
||||
|
||||
### 查询服务
|
||||
`trustgraph-flow/trustgraph/query/doc_embeddings/qdrant/service.py`
|
||||
`trustgraph-flow/trustgraph/query/doc_embeddings/milvus/service.py`
|
||||
`trustgraph-flow/trustgraph/query/doc_embeddings/pinecone/service.py`
|
||||
|
||||
### 网关
|
||||
`trustgraph-flow/trustgraph/gateway/dispatch/document_embeddings_query.py`
|
||||
`trustgraph-flow/trustgraph/gateway/dispatch/document_embeddings_export.py`
|
||||
`trustgraph-flow/trustgraph/gateway/dispatch/document_embeddings_import.py`
|
||||
|
||||
### 文档 RAG
|
||||
`trustgraph-flow/trustgraph/retrieval/document_rag/rag.py` - 添加 librarian 客户端
|
||||
`trustgraph-flow/trustgraph/retrieval/document_rag/document_rag.py` - 从 Garage 获取
|
||||
|
||||
### CLI
|
||||
`trustgraph-cli/trustgraph/cli/invoke_document_embeddings.py`
|
||||
`trustgraph-cli/trustgraph/cli/save_doc_embeds.py`
|
||||
`trustgraph-cli/trustgraph/cli/load_doc_embeds.py`
|
||||
|
||||
## 优点
|
||||
|
||||
1. 单一数据源 - 仅在 Garage 中存储文本块
|
||||
2. 减少向量存储空间
|
||||
3. 通过 chunk_id 实现查询时的数据溯源
|
||||
675
docs/tech-specs/zh-cn/embeddings-batch-processing.zh-cn.md
Normal file
675
docs/tech-specs/zh-cn/embeddings-batch-processing.zh-cn.md
Normal file
|
|
@ -0,0 +1,675 @@
|
|||
---
|
||||
layout: default
|
||||
title: "嵌入式批量处理技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 嵌入式批量处理技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了对嵌入式服务的优化,以支持在单个请求中批量处理多个文本。当前的实现方式一次处理一个文本,而没有利用嵌入式模型在处理批量数据时所能提供的显著性能优势。
|
||||
|
||||
1. **单文本处理效率低下**: 当前实现将单个文本包装在列表中,没有充分利用 FastEmbed 的批量处理能力。
|
||||
2. **每个文本的请求开销**: 每个文本都需要单独的 Pulsar 消息往返。
|
||||
3. **模型推理效率低下**: 嵌入式模型具有固定的批量处理开销;小批量会浪费 GPU/CPU 资源。
|
||||
4. **调用方中的串行处理**: 关键服务循环遍历项目,并一次调用一个嵌入式模型。
|
||||
|
||||
## 目标
|
||||
|
||||
**支持批量 API**: 允许在单个请求中处理多个文本。
|
||||
**向后兼容性**: 保持对单文本请求的支持。
|
||||
**显著的吞吐量提升**: 针对批量操作,目标是实现 5-10 倍的吞吐量提升。
|
||||
**每个文本的降低延迟**: 在嵌入多个文本时,降低平均延迟。
|
||||
**内存效率**: 在不产生过多内存消耗的情况下处理批量数据。
|
||||
**提供商无关性**: 支持 FastEmbed、Ollama 以及其他提供商的批量处理。
|
||||
**调用方迁移**: 更新所有嵌入式模型调用方,以便在有利的情况下使用批量 API。
|
||||
|
||||
## 背景
|
||||
|
||||
### 当前实现 - 嵌入式服务
|
||||
|
||||
位于 `trustgraph-flow/trustgraph/embeddings/fastembed/processor.py` 中的嵌入式实现存在显著的性能低效问题:
|
||||
|
||||
```python
|
||||
# fastembed/processor.py line 56
|
||||
async def on_embeddings(self, text, model=None):
|
||||
use_model = model or self.default_model
|
||||
self._load_model(use_model)
|
||||
|
||||
vecs = self.embeddings.embed([text]) # Single text wrapped in list
|
||||
|
||||
return [v.tolist() for v in vecs]
|
||||
```
|
||||
|
||||
**问题:**
|
||||
|
||||
1. **批处理大小为 1:** FastEmbed 的 `embed()` 方法针对批量处理进行了优化,但我们总是使用 `[text]` - 批处理大小为 1。
|
||||
|
||||
2. **每个请求的开销:** 每次嵌入请求都涉及:
|
||||
Pulsar 消息序列化/反序列化
|
||||
网络往返延迟
|
||||
模型推理启动开销
|
||||
Python 异步调度开销
|
||||
|
||||
3. **模式限制:** `EmbeddingsRequest` 模式仅支持单个文本:
|
||||
```python
|
||||
@dataclass
|
||||
class EmbeddingsRequest:
|
||||
text: str = "" # Single text only
|
||||
```
|
||||
|
||||
### 当前调用者 - 序列化处理
|
||||
|
||||
#### 1. API 网关
|
||||
|
||||
**文件:** `trustgraph-flow/trustgraph/gateway/dispatch/embeddings.py`
|
||||
|
||||
网关通过 HTTP/WebSocket 接收单文本嵌入请求,并将它们转发到嵌入服务。目前没有批量端点。
|
||||
|
||||
```python
|
||||
class EmbeddingsRequestor(ServiceRequestor):
|
||||
# Handles single EmbeddingsRequest -> EmbeddingsResponse
|
||||
request_schema=EmbeddingsRequest, # Single text only
|
||||
response_schema=EmbeddingsResponse,
|
||||
```
|
||||
|
||||
**影响:** 外部客户端(Web应用程序、脚本)必须发出N次HTTP请求才能嵌入N段文本。
|
||||
|
||||
#### 2. 文档嵌入服务
|
||||
|
||||
**文件:** `trustgraph-flow/trustgraph/embeddings/document_embeddings/embeddings.py`
|
||||
|
||||
逐个处理文档块:
|
||||
|
||||
```python
|
||||
async def on_message(self, msg, consumer, flow):
|
||||
v = msg.value()
|
||||
|
||||
# Single chunk per request
|
||||
resp = await flow("embeddings-request").request(
|
||||
EmbeddingsRequest(text=v.chunk)
|
||||
)
|
||||
vectors = resp.vectors
|
||||
```
|
||||
|
||||
**影响:** 每个文档块都需要单独的嵌入调用。一个包含 100 个块的文档 = 100 个嵌入请求。
|
||||
|
||||
#### 3. 图嵌入服务
|
||||
|
||||
**文件:** `trustgraph-flow/trustgraph/embeddings/graph_embeddings/embeddings.py`
|
||||
|
||||
循环遍历实体,并逐个嵌入每个实体:
|
||||
|
||||
```python
|
||||
async def on_message(self, msg, consumer, flow):
|
||||
for entity in v.entities:
|
||||
# Serial embedding - one entity at a time
|
||||
vectors = await flow("embeddings-request").embed(
|
||||
text=entity.context
|
||||
)
|
||||
entities.append(EntityEmbeddings(
|
||||
entity=entity.entity,
|
||||
vectors=vectors,
|
||||
chunk_id=entity.chunk_id,
|
||||
))
|
||||
```
|
||||
|
||||
**影响:** 一个包含 50 个实体的消息意味着 50 个序列化的嵌入请求。这在知识图谱构建过程中是一个主要的瓶颈。
|
||||
|
||||
#### 4. 行嵌入服务
|
||||
|
||||
**文件:** `trustgraph-flow/trustgraph/embeddings/row_embeddings/embeddings.py`
|
||||
|
||||
循环遍历唯一的文本,并逐个嵌入每个文本:
|
||||
|
||||
```python
|
||||
async def on_message(self, msg, consumer, flow):
|
||||
for text, (index_name, index_value) in texts_to_embed.items():
|
||||
# Serial embedding - one text at a time
|
||||
vectors = await flow("embeddings-request").embed(text=text)
|
||||
|
||||
embeddings_list.append(RowIndexEmbedding(
|
||||
index_name=index_name,
|
||||
index_value=index_value,
|
||||
text=text,
|
||||
vectors=vectors
|
||||
))
|
||||
```
|
||||
|
||||
**影响:** 处理包含 100 个唯一索引值的表格 = 100 个序列化嵌入请求。
|
||||
|
||||
#### 5. EmbeddingsClient (基础客户端)
|
||||
|
||||
**文件:** `trustgraph-base/trustgraph/base/embeddings_client.py`
|
||||
|
||||
所有流程处理器使用的客户端仅支持单文本嵌入:
|
||||
|
||||
```python
|
||||
class EmbeddingsClient(RequestResponse):
|
||||
async def embed(self, text, timeout=30):
|
||||
resp = await self.request(
|
||||
EmbeddingsRequest(text=text), # Single text
|
||||
timeout=timeout
|
||||
)
|
||||
return resp.vectors
|
||||
```
|
||||
|
||||
**影响:** 所有使用此客户端的调用者都仅限于执行单文本操作。
|
||||
|
||||
#### 6. 命令行工具
|
||||
|
||||
**文件:** `trustgraph-cli/trustgraph/cli/invoke_embeddings.py`
|
||||
|
||||
命令行工具接受单个文本参数:
|
||||
|
||||
```python
|
||||
def query(url, flow_id, text, token=None):
|
||||
result = flow.embeddings(text=text) # Single text
|
||||
vectors = result.get("vectors", [])
|
||||
```
|
||||
|
||||
**影响:** 用户无法通过命令行进行批量嵌入。处理一个文本文件需要 N 次调用。
|
||||
|
||||
#### 7. Python SDK
|
||||
|
||||
Python SDK 提供了两个客户端类,用于与 TrustGraph 服务进行交互。这两个客户端类仅支持单文本嵌入。
|
||||
|
||||
**文件:** `trustgraph-base/trustgraph/api/flow.py`
|
||||
|
||||
```python
|
||||
class FlowInstance:
|
||||
def embeddings(self, text):
|
||||
"""Get embeddings for a single text"""
|
||||
input = {"text": text}
|
||||
return self.request("service/embeddings", input)["vectors"]
|
||||
```
|
||||
|
||||
**文件:** `trustgraph-base/trustgraph/api/socket_client.py`
|
||||
|
||||
```python
|
||||
class SocketFlowInstance:
|
||||
def embeddings(self, text: str, **kwargs: Any) -> Dict[str, Any]:
|
||||
"""Get embeddings for a single text via WebSocket"""
|
||||
request = {"text": text}
|
||||
return self.client._send_request_sync(
|
||||
"embeddings", self.flow_id, request, False
|
||||
)
|
||||
```
|
||||
|
||||
**影响:** 使用 SDK 的 Python 开发者必须循环遍历文本,并进行 N 次单独的 API 调用。 SDK 用户没有批量嵌入支持。
|
||||
|
||||
### 性能影响
|
||||
|
||||
对于典型的文档导入(1000 个文本块):
|
||||
**当前:** 1000 个单独的请求,1000 次模型推理调用
|
||||
**批量(batch_size=32):** 32 个请求,32 次模型推理调用(减少 96.8%)
|
||||
|
||||
对于图嵌入(包含 50 个实体的消息):
|
||||
**当前:** 50 次序列等待调用,约 5-10 秒
|
||||
**批量:** 1-2 次批量调用,约 0.5-1 秒(提升 5-10 倍)
|
||||
|
||||
FastEmbed 和类似库在批量大小达到硬件限制时,可以实现接近线性的吞吐量扩展(通常每个批次 32-128 个文本)。
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 架构
|
||||
|
||||
嵌入批量处理优化需要修改以下组件:
|
||||
|
||||
#### 1. **模式增强**
|
||||
扩展 `EmbeddingsRequest` 以支持多个文本
|
||||
扩展 `EmbeddingsResponse` 以返回多个向量集合
|
||||
保持与单文本请求的向后兼容性
|
||||
|
||||
模块:`trustgraph-base/trustgraph/schema/services/llm.py`
|
||||
|
||||
#### 2. **基础服务增强**
|
||||
更新 `EmbeddingsService` 以处理批量请求
|
||||
添加批量大小配置
|
||||
实现支持批量请求的处理逻辑
|
||||
|
||||
模块:`trustgraph-base/trustgraph/base/embeddings_service.py`
|
||||
|
||||
#### 3. **提供者处理器更新**
|
||||
更新 FastEmbed 处理器,将整个批次传递给 `embed()`
|
||||
更新 Ollama 处理器,以处理批次(如果支持)
|
||||
为不支持批处理的提供商添加回退的序列化处理
|
||||
|
||||
模块:
|
||||
`trustgraph-flow/trustgraph/embeddings/fastembed/processor.py`
|
||||
`trustgraph-flow/trustgraph/embeddings/ollama/processor.py`
|
||||
|
||||
#### 4. **客户端增强**
|
||||
向 `EmbeddingsClient` 添加批量嵌入方法
|
||||
支持单次和批量 API
|
||||
为大型输入添加自动批量处理
|
||||
|
||||
模块:`trustgraph-base/trustgraph/base/embeddings_client.py`
|
||||
|
||||
#### 5. **调用方更新 - 流处理器**
|
||||
更新 `graph_embeddings` 以批量实体上下文
|
||||
更新 `row_embeddings` 以批量索引文本
|
||||
如果消息批量处理可行,则更新 `document_embeddings`
|
||||
|
||||
模块:
|
||||
`trustgraph-flow/trustgraph/embeddings/graph_embeddings/embeddings.py`
|
||||
`trustgraph-flow/trustgraph/embeddings/row_embeddings/embeddings.py`
|
||||
`trustgraph-flow/trustgraph/embeddings/document_embeddings/embeddings.py`
|
||||
|
||||
#### 6. **API 网关增强**
|
||||
添加批量嵌入端点
|
||||
支持请求体中的文本数组
|
||||
|
||||
模块:`trustgraph-flow/trustgraph/gateway/dispatch/embeddings.py`
|
||||
|
||||
#### 7. **CLI 工具增强**
|
||||
添加对多个文本或文件输入的支持
|
||||
添加批量大小参数
|
||||
|
||||
模块:`trustgraph-cli/trustgraph/cli/invoke_embeddings.py`
|
||||
|
||||
#### 8. **Python SDK 增强**
|
||||
向 `FlowInstance` 添加 `embeddings_batch()` 方法
|
||||
向 `SocketFlowInstance` 添加 `embeddings_batch()` 方法
|
||||
为 SDK 用户支持单次和批量 API
|
||||
|
||||
模块:
|
||||
`trustgraph-base/trustgraph/api/flow.py`
|
||||
`trustgraph-base/trustgraph/api/socket_client.py`
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### EmbeddingsRequest
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class EmbeddingsRequest:
|
||||
texts: list[str] = field(default_factory=list)
|
||||
```
|
||||
|
||||
用法:
|
||||
单个文本:`EmbeddingsRequest(texts=["hello world"])`
|
||||
批量:`EmbeddingsRequest(texts=["text1", "text2", "text3"])`
|
||||
|
||||
#### EmbeddingsResponse
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class EmbeddingsResponse:
|
||||
error: Error | None = None
|
||||
vectors: list[list[list[float]]] = field(default_factory=list)
|
||||
```
|
||||
|
||||
响应结构:
|
||||
`vectors[i]` 包含用于 `texts[i]` 的向量集合。
|
||||
每个向量集合都是 `list[list[float]]` (模型可能为每个文本返回多个向量)。
|
||||
示例:3 个文本 → `vectors` 有 3 个条目,每个条目包含该文本的嵌入向量。
|
||||
|
||||
### API 接口
|
||||
|
||||
#### EmbeddingsClient
|
||||
|
||||
```python
|
||||
class EmbeddingsClient(RequestResponse):
|
||||
async def embed(
|
||||
self,
|
||||
texts: list[str],
|
||||
timeout: float = 300,
|
||||
) -> list[list[list[float]]]:
|
||||
"""
|
||||
Embed one or more texts in a single request.
|
||||
|
||||
Args:
|
||||
texts: List of texts to embed
|
||||
timeout: Timeout for the operation
|
||||
|
||||
Returns:
|
||||
List of vector sets, one per input text
|
||||
"""
|
||||
resp = await self.request(
|
||||
EmbeddingsRequest(texts=texts),
|
||||
timeout=timeout
|
||||
)
|
||||
if resp.error:
|
||||
raise RuntimeError(resp.error.message)
|
||||
return resp.vectors
|
||||
```
|
||||
|
||||
#### API 网关嵌入式模型端点
|
||||
|
||||
更新后的端点支持单个或批量嵌入式模型:
|
||||
|
||||
```
|
||||
POST /api/v1/embeddings
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"texts": ["text1", "text2", "text3"],
|
||||
"flow_id": "default"
|
||||
}
|
||||
|
||||
Response:
|
||||
{
|
||||
"vectors": [
|
||||
[[0.1, 0.2, ...]],
|
||||
[[0.3, 0.4, ...]],
|
||||
[[0.5, 0.6, ...]]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 实现细节
|
||||
|
||||
#### 第一阶段:模式更改
|
||||
|
||||
**EmbeddingsRequest:**
|
||||
```python
|
||||
@dataclass
|
||||
class EmbeddingsRequest:
|
||||
texts: list[str] = field(default_factory=list)
|
||||
```
|
||||
|
||||
**EmbeddingsResponse:**
|
||||
```python
|
||||
@dataclass
|
||||
class EmbeddingsResponse:
|
||||
error: Error | None = None
|
||||
vectors: list[list[list[float]]] = field(default_factory=list)
|
||||
```
|
||||
|
||||
**更新了 EmbeddingsService.on_request:**
|
||||
```python
|
||||
async def on_request(self, msg, consumer, flow):
|
||||
request = msg.value()
|
||||
id = msg.properties()["id"]
|
||||
model = flow("model")
|
||||
|
||||
vectors = await self.on_embeddings(request.texts, model=model)
|
||||
response = EmbeddingsResponse(error=None, vectors=vectors)
|
||||
|
||||
await flow("response").send(response, properties={"id": id})
|
||||
```
|
||||
|
||||
#### 第二阶段:FastEmbed 处理器更新
|
||||
|
||||
**当前(效率低下):**
|
||||
```python
|
||||
async def on_embeddings(self, text, model=None):
|
||||
use_model = model or self.default_model
|
||||
self._load_model(use_model)
|
||||
vecs = self.embeddings.embed([text]) # Batch of 1
|
||||
return [v.tolist() for v in vecs]
|
||||
```
|
||||
|
||||
**更新:**
|
||||
```python
|
||||
async def on_embeddings(self, texts: list[str], model=None):
|
||||
"""Embed texts - processes all texts in single model call"""
|
||||
if not texts:
|
||||
return []
|
||||
|
||||
use_model = model or self.default_model
|
||||
self._load_model(use_model)
|
||||
|
||||
# FastEmbed handles the full batch efficiently
|
||||
all_vecs = list(self.embeddings.embed(texts))
|
||||
|
||||
# Return list of vector sets, one per input text
|
||||
return [[v.tolist()] for v in all_vecs]
|
||||
```
|
||||
|
||||
#### 第三阶段:图嵌入服务更新
|
||||
|
||||
**当前 (序列号):**
|
||||
```python
|
||||
async def on_message(self, msg, consumer, flow):
|
||||
entities = []
|
||||
for entity in v.entities:
|
||||
vectors = await flow("embeddings-request").embed(text=entity.context)
|
||||
entities.append(EntityEmbeddings(...))
|
||||
```
|
||||
|
||||
**更新 (批量):**
|
||||
```python
|
||||
async def on_message(self, msg, consumer, flow):
|
||||
# Collect all contexts
|
||||
contexts = [entity.context for entity in v.entities]
|
||||
|
||||
# Single batch embedding call
|
||||
all_vectors = await flow("embeddings-request").embed(texts=contexts)
|
||||
|
||||
# Pair results with entities
|
||||
entities = [
|
||||
EntityEmbeddings(
|
||||
entity=entity.entity,
|
||||
vectors=vectors[0], # First vector from the set
|
||||
chunk_id=entity.chunk_id,
|
||||
)
|
||||
for entity, vectors in zip(v.entities, all_vectors)
|
||||
]
|
||||
```
|
||||
|
||||
#### 第四阶段:行嵌入服务更新
|
||||
|
||||
**当前 (序列号):**
|
||||
```python
|
||||
for text, (index_name, index_value) in texts_to_embed.items():
|
||||
vectors = await flow("embeddings-request").embed(text=text)
|
||||
embeddings_list.append(RowIndexEmbedding(...))
|
||||
```
|
||||
|
||||
**更新 (批量):**
|
||||
```python
|
||||
# Collect texts and metadata
|
||||
texts = list(texts_to_embed.keys())
|
||||
metadata = list(texts_to_embed.values())
|
||||
|
||||
# Single batch embedding call
|
||||
all_vectors = await flow("embeddings-request").embed(texts=texts)
|
||||
|
||||
# Pair results
|
||||
embeddings_list = [
|
||||
RowIndexEmbedding(
|
||||
index_name=meta[0],
|
||||
index_value=meta[1],
|
||||
text=text,
|
||||
vectors=vectors[0] # First vector from the set
|
||||
)
|
||||
for text, meta, vectors in zip(texts, metadata, all_vectors)
|
||||
]
|
||||
```
|
||||
|
||||
#### 第五阶段:命令行工具增强
|
||||
|
||||
**更新后的命令行界面:**
|
||||
```python
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(...)
|
||||
|
||||
parser.add_argument(
|
||||
'text',
|
||||
nargs='*', # Zero or more texts
|
||||
help='Text(s) to convert to embedding vectors',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-f', '--file',
|
||||
help='File containing texts (one per line)',
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--batch-size',
|
||||
type=int,
|
||||
default=32,
|
||||
help='Batch size for processing (default: 32)',
|
||||
)
|
||||
```
|
||||
|
||||
用法:
|
||||
```bash
|
||||
# Single text (existing)
|
||||
tg-invoke-embeddings "hello world"
|
||||
|
||||
# Multiple texts
|
||||
tg-invoke-embeddings "text one" "text two" "text three"
|
||||
|
||||
# From file
|
||||
tg-invoke-embeddings -f texts.txt --batch-size 64
|
||||
```
|
||||
|
||||
#### 第六阶段:Python SDK 增强
|
||||
|
||||
**FlowInstance (HTTP 客户端):**
|
||||
|
||||
```python
|
||||
class FlowInstance:
|
||||
def embeddings(self, texts: list[str]) -> list[list[list[float]]]:
|
||||
"""
|
||||
Get embeddings for one or more texts.
|
||||
|
||||
Args:
|
||||
texts: List of texts to embed
|
||||
|
||||
Returns:
|
||||
List of vector sets, one per input text
|
||||
"""
|
||||
input = {"texts": texts}
|
||||
return self.request("service/embeddings", input)["vectors"]
|
||||
```
|
||||
|
||||
**SocketFlowInstance (WebSocket 客户端):**
|
||||
|
||||
```python
|
||||
class SocketFlowInstance:
|
||||
def embeddings(self, texts: list[str], **kwargs: Any) -> list[list[list[float]]]:
|
||||
"""
|
||||
Get embeddings for one or more texts via WebSocket.
|
||||
|
||||
Args:
|
||||
texts: List of texts to embed
|
||||
|
||||
Returns:
|
||||
List of vector sets, one per input text
|
||||
"""
|
||||
request = {"texts": texts}
|
||||
response = self.client._send_request_sync(
|
||||
"embeddings", self.flow_id, request, False
|
||||
)
|
||||
return response["vectors"]
|
||||
```
|
||||
|
||||
**SDK 使用示例:**
|
||||
|
||||
```python
|
||||
# Single text
|
||||
vectors = flow.embeddings(["hello world"])
|
||||
print(f"Dimensions: {len(vectors[0][0])}")
|
||||
|
||||
# Batch embedding
|
||||
texts = ["text one", "text two", "text three"]
|
||||
all_vectors = flow.embeddings(texts)
|
||||
|
||||
# Process results
|
||||
for text, vecs in zip(texts, all_vectors):
|
||||
print(f"{text}: {len(vecs[0])} dimensions")
|
||||
```
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
**请求大小限制**: 强制执行最大批处理大小,以防止资源耗尽。
|
||||
**超时处理**: 针对批处理大小,适当调整超时时间。
|
||||
**内存限制**: 监控大型批处理的内存使用情况。
|
||||
**输入验证**: 在处理之前,验证批处理中的所有文本。
|
||||
|
||||
## 性能注意事项
|
||||
|
||||
### 预期改进
|
||||
|
||||
**吞吐量**:
|
||||
单个文本: ~10-50 文本/秒 (取决于模型)
|
||||
批处理 (大小 32): ~200-500 文本/秒 (提升 5-10 倍)
|
||||
|
||||
**每个文本的延迟**:
|
||||
单个文本: 每个文本 50-200 毫秒
|
||||
批处理 (大小 32): 每个文本 5-20 毫秒 (摊销值)
|
||||
|
||||
**特定服务的改进**:
|
||||
|
||||
| 服务 | 当前 | 批处理 | 改进 |
|
||||
|---------|---------|---------|-------------|
|
||||
| 图嵌入 (50 个实体) | 5-10 秒 | 0.5-1 秒 | 5-10 倍 |
|
||||
| 行嵌入 (100 个文本) | 10-20 秒 | 1-2 秒 | 5-10 倍 |
|
||||
| 文档导入 (1000 个块) | 100-200 秒 | 10-30 秒 | 5-10 倍 |
|
||||
|
||||
### 配置参数
|
||||
|
||||
```python
|
||||
# Recommended defaults
|
||||
DEFAULT_BATCH_SIZE = 32
|
||||
MAX_BATCH_SIZE = 128
|
||||
BATCH_TIMEOUT_MULTIPLIER = 2.0
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 单元测试
|
||||
单个文本嵌入(向后兼容)
|
||||
空批处理的处理
|
||||
最大批处理大小的强制执行
|
||||
部分批处理失败的错误处理
|
||||
|
||||
### 集成测试
|
||||
通过 Pulsar 进行端到端批处理嵌入
|
||||
图嵌入服务批处理
|
||||
行嵌入服务批处理
|
||||
API 网关批处理端点
|
||||
|
||||
### 性能测试
|
||||
比较单批和批量吞吐量
|
||||
在各种批处理大小下的内存使用情况
|
||||
延迟分布分析
|
||||
|
||||
## 迁移计划
|
||||
|
||||
这是一个破坏性更改版本。所有阶段都一起实施。
|
||||
|
||||
### 第一阶段:Schema 更改
|
||||
将 EmbeddingsRequest 中的 `text: str` 替换为 `texts: list[str]`
|
||||
将 EmbeddingsResponse 中的 `vectors` 类型更改为 `list[list[list[float]]]`
|
||||
|
||||
### 第二阶段:处理器更新
|
||||
更新 FastEmbed 和 Ollama 处理器中的 `on_embeddings` 签名
|
||||
在单个模型调用中处理整个批次
|
||||
|
||||
### 第三阶段:客户端更新
|
||||
更新 `EmbeddingsClient.embed()` 以接受 `texts: list[str]`
|
||||
|
||||
### 第四阶段:调用方更新
|
||||
更新 graph_embeddings 以批处理实体上下文
|
||||
更新 row_embeddings 以批处理索引文本
|
||||
更新 document_embeddings 以使用新的 Schema
|
||||
更新 CLI 工具
|
||||
|
||||
### 第五阶段:API 网关
|
||||
更新用于新 Schema 的嵌入端点
|
||||
|
||||
### 第六阶段:Python SDK
|
||||
更新 `FlowInstance.embeddings()` 签名
|
||||
更新 `SocketFlowInstance.embeddings()` 签名
|
||||
|
||||
## 开放问题
|
||||
|
||||
**大型批处理的流式传输**: 我们是否应该支持对非常大的批处理(>100 个文本)进行流式传输结果?
|
||||
**特定于提供商的限制**: 我们应该如何处理具有不同最大批处理大小的提供商?
|
||||
**部分失败处理**: 如果批处理中的一个文本失败,我们应该使整个批处理失败,还是返回部分结果?
|
||||
**文档嵌入批处理**: 我们应该跨多个 Chunk 消息进行批处理,还是保持每个消息的处理?
|
||||
|
||||
## 参考文献
|
||||
|
||||
[FastEmbed 文档](https://github.com/qdrant/fastembed)
|
||||
[Ollama 嵌入 API](https://github.com/ollama/ollama)
|
||||
[EmbeddingsService 实现](trustgraph-base/trustgraph/base/embeddings_service.py)
|
||||
[GraphRAG 性能优化](graphrag-performance-optimization.md)
|
||||
269
docs/tech-specs/zh-cn/entity-centric-graph.zh-cn.md
Normal file
269
docs/tech-specs/zh-cn/entity-centric-graph.zh-cn.md
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
---
|
||||
layout: default
|
||||
title: "基于实体的知识图谱在 Cassandra 上的存储"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 基于实体的知识图谱在 Cassandra 上的存储
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了一种在 Apache Cassandra 上存储 RDF 风格知识图谱的存储模型。该模型采用一种**以实体为中心**的方法,其中每个实体都知道它参与的所有四元组以及它所扮演的角色。这用两个表替换了传统的多表 SPO 组合方法。
|
||||
|
||||
## 背景和动机
|
||||
|
||||
### 传统方法
|
||||
|
||||
在 Cassandra 上,标准的 RDF 四元组存储需要多个反规范化的表来覆盖查询模式,通常需要 6 个或更多的表,这些表代表主语、谓词、宾语和数据集 (SPOD) 的不同组合。每个四元组都会写入到每个表中,从而导致显著的写入放大、运维开销和模式复杂性。
|
||||
|
||||
此外,标签解析(用于实体的人类可读名称)需要单独的往返查询,这在 AI 和 GraphRAG 用例中尤其昂贵,因为标签对于 LLM 上下文至关重要。
|
||||
|
||||
### 以实体为中心的洞察
|
||||
|
||||
每个四元组 `(D, S, P, O)` 涉及最多 4 个实体。通过为每个实体参与四元组的记录创建一个行,我们保证**任何至少包含一个已知元素的查询都会命中分区键**。这使用单个数据表覆盖所有 16 种查询模式。
|
||||
|
||||
主要优点:
|
||||
|
||||
**2 个表**,而不是 7 个以上
|
||||
**每个四元组 4 次写入**,而不是 6 次以上
|
||||
**免费的标签解析**——实体的标签与其关系共存,从而自然地预热应用程序缓存
|
||||
**所有 16 种查询模式**都由单分区读取提供
|
||||
**更简单的操作**——只有一个数据表需要调整、压缩和修复
|
||||
|
||||
## 模式
|
||||
|
||||
### 表 1:quads_by_entity
|
||||
|
||||
主要数据表。每个实体都有一个分区,其中包含它参与的所有四元组。该名称反映了查询模式(按实体查找)。
|
||||
|
||||
```sql
|
||||
CREATE TABLE quads_by_entity (
|
||||
collection text, -- Collection/tenant scope (always specified)
|
||||
entity text, -- The entity this row is about
|
||||
role text, -- 'S', 'P', 'O', 'G' — how this entity participates
|
||||
p text, -- Predicate of the quad
|
||||
otype text, -- 'U' (URI), 'L' (literal), 'T' (triple/reification)
|
||||
s text, -- Subject of the quad
|
||||
o text, -- Object of the quad
|
||||
d text, -- Dataset/graph of the quad
|
||||
dtype text, -- XSD datatype (when otype = 'L'), e.g. 'xsd:string'
|
||||
lang text, -- Language tag (when otype = 'L'), e.g. 'en', 'fr'
|
||||
PRIMARY KEY ((collection, entity), role, p, otype, s, o, d, dtype, lang)
|
||||
);
|
||||
```
|
||||
|
||||
**分区键 (Partition key)**: `(collection, entity)` — 作用域限定在集合中,每个实体对应一个分区。
|
||||
|
||||
**聚类列顺序的理由 (Clustering column order rationale)**:
|
||||
|
||||
1. **role** — 大部分查询都以“这个实体是哪个主语/客体”开始。
|
||||
2. **p** — 下一个最常见的过滤条件,例如“给我所有 `knows` 关系”。
|
||||
3. **otype** — 允许根据 URI 值与字面值关系进行过滤。
|
||||
4. **s, o, d** — 剩余的列用于保证唯一性。
|
||||
5. **dtype, lang** — 区分具有相同值但不同类型元数据的字面值(例如,`"thing"` 与 `"thing"@en` 与 `"thing"^^xsd:string`)。
|
||||
|
||||
### 表 2: quads_by_collection
|
||||
|
||||
支持集合级别的查询和删除。提供属于某个集合的所有四元组的清单。命名方式反映了查询模式(按集合查找)。
|
||||
|
||||
```sql
|
||||
CREATE TABLE quads_by_collection (
|
||||
collection text,
|
||||
d text, -- Dataset/graph of the quad
|
||||
s text, -- Subject of the quad
|
||||
p text, -- Predicate of the quad
|
||||
o text, -- Object of the quad
|
||||
otype text, -- 'U' (URI), 'L' (literal), 'T' (triple/reification)
|
||||
dtype text, -- XSD datatype (when otype = 'L')
|
||||
lang text, -- Language tag (when otype = 'L')
|
||||
PRIMARY KEY (collection, d, s, p, o, otype, dtype, lang)
|
||||
);
|
||||
```
|
||||
|
||||
首先按照数据集进行聚类,从而可以在集合或数据集级别进行删除。 `otype`、`dtype` 和 `lang` 列包含在聚类键中,用于区分具有相同值但不同元数据类型的字面量——在 RDF 中,`"thing"`、`"thing"@en` 和 `"thing"^^xsd:string` 是语义上不同的值。
|
||||
|
||||
## 写入路径
|
||||
|
||||
对于每个在集合 `C` 中的四元组 `(D, S, P, O)`,需要向 `quads_by_entity` 写入 **4 行**,并向 `quads_by_collection` 写入 **1 行**。
|
||||
|
||||
### 示例
|
||||
|
||||
假设在集合 `tenant1` 中的四元组是:
|
||||
|
||||
```
|
||||
Dataset: https://example.org/graph1
|
||||
Subject: https://example.org/Alice
|
||||
Predicate: https://example.org/knows
|
||||
Object: https://example.org/Bob
|
||||
```
|
||||
|
||||
将 4 行写入到 `quads_by_entity`:
|
||||
|
||||
| collection | entity | role | p | otype | s | o | d |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| tenant1 | https://example.org/graph1 | G | https://example.org/knows | U | https://example.org/Alice | https://example.org/Bob | https://example.org/graph1 |
|
||||
| tenant1 | https://example.org/Alice | S | https://example.org/knows | U | https://example.org/Alice | https://example.org/Bob | https://example.org/graph1 |
|
||||
| tenant1 | https://example.org/knows | P | https://example.org/knows | U | https://example.org/Alice | https://example.org/Bob | https://example.org/graph1 |
|
||||
| tenant1 | https://example.org/Bob | O | https://example.org/knows | U | https://example.org/Alice | https://example.org/Bob | https://example.org/graph1 |
|
||||
|
||||
将 1 行写入到 `quads_by_collection`:
|
||||
|
||||
| collection | d | s | p | o | otype | dtype | lang |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| tenant1 | https://example.org/graph1 | https://example.org/Alice | https://example.org/knows | https://example.org/Bob | U | | |
|
||||
|
||||
### 示例
|
||||
|
||||
对于一个标签三元组:
|
||||
|
||||
```
|
||||
Dataset: https://example.org/graph1
|
||||
Subject: https://example.org/Alice
|
||||
Predicate: http://www.w3.org/2000/01/rdf-schema#label
|
||||
Object: "Alice Smith" (lang: en)
|
||||
```
|
||||
|
||||
`otype` 是 `'L'`,`dtype` 是 `'xsd:string'`,`lang` 是 `'en'`。 原始值 `"Alice Smith"` 存储在 `o` 中。 `quads_by_entity` 中只需要 3 行,因为没有为原始值作为实体的行,因为原始值不是可以独立查询的实体。
|
||||
|
||||
## 查询模式
|
||||
|
||||
### 所有 16 种 DSPO 模式
|
||||
|
||||
在下表中,“完美前缀”表示查询使用聚类列的连续前缀。“分区扫描 + 过滤”表示 Cassandra 读取一个分区的一部分并在内存中进行过滤,这仍然有效,但不是纯粹的前缀匹配。
|
||||
|
||||
| # | 已知 | 查找实体 | 聚类前缀 | 效率 |
|
||||
|---|---|---|---|---|
|
||||
| 1 | D,S,P,O | entity=S, role='S', p=P | 完全匹配 | 完美前缀 |
|
||||
| 2 | D,S,P,? | entity=S, role='S', p=P | 在 D 上过滤 | 分区扫描 + 过滤 |
|
||||
| 3 | D,S,?,O | entity=S, role='S' | 在 D, O 上过滤 | 分区扫描 + 过滤 |
|
||||
| 4 | D,?,P,O | entity=O, role='O', p=P | 在 D 上过滤 | 分区扫描 + 过滤 |
|
||||
| 5 | ?,S,P,O | entity=S, role='S', p=P | 在 O 上过滤 | 分区扫描 + 过滤 |
|
||||
| 6 | D,S,?,? | entity=S, role='S' | 在 D 上过滤 | 分区扫描 + 过滤 |
|
||||
| 7 | D,?,P,? | entity=P, role='P' | 在 D 上过滤 | 分区扫描 + 过滤 |
|
||||
| 8 | D,?,?,O | entity=O, role='O' | 在 D 上过滤 | 分区扫描 + 过滤 |
|
||||
| 9 | ?,S,P,? | entity=S, role='S', p=P | — | **完美前缀** |
|
||||
| 10 | ?,S,?,O | entity=S, role='S' | 在 O 上过滤 | 分区扫描 + 过滤 |
|
||||
| 11 | ?,?,P,O | entity=O, role='O', p=P | — | **完美前缀** |
|
||||
| 12 | D,?,?,? | entity=D, role='G' | — | **完美前缀** |
|
||||
| 13 | ?,S,?,? | entity=S, role='S' | — | **完美前缀** |
|
||||
| 14 | ?,?,P,? | entity=P, role='P' | — | **完美前缀** |
|
||||
| 15 | ?,?,?,O | entity=O, role='O' | — | **完美前缀** |
|
||||
| 16 | ?,?,?,? | — | 全扫描 | 仅探索 |
|
||||
|
||||
**关键结果:** 15 种非平凡模式中有 7 种是完美的聚类前缀匹配。 剩下的 8 种是单分区读取,并在分区内进行过滤。 包含至少一个已知元素的每个查询都会命中分区键。
|
||||
|
||||
模式 16 (?,?,?,?) 在实践中不会出现,因为集合始终是指定的,这将其简化为模式 12。
|
||||
|
||||
### 常见查询示例
|
||||
|
||||
**关于一个实体的所有信息:**
|
||||
|
||||
```sql
|
||||
SELECT * FROM quads_by_entity
|
||||
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice';
|
||||
```
|
||||
|
||||
**实体的所有出方向关系:**
|
||||
|
||||
```sql
|
||||
SELECT * FROM quads_by_entity
|
||||
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice'
|
||||
AND role = 'S';
|
||||
```
|
||||
|
||||
**特定实体谓词:**
|
||||
|
||||
```sql
|
||||
SELECT * FROM quads_by_entity
|
||||
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice'
|
||||
AND role = 'S' AND p = 'https://example.org/knows';
|
||||
```
|
||||
|
||||
**实体标签(特定语言):**
|
||||
|
||||
```sql
|
||||
SELECT * FROM quads_by_entity
|
||||
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice'
|
||||
AND role = 'S' AND p = 'http://www.w3.org/2000/01/rdf-schema#label'
|
||||
AND otype = 'L';
|
||||
```
|
||||
|
||||
然后,如果需要,可以在应用程序端通过 `lang = 'en'` 进行过滤。
|
||||
|
||||
**仅限 URI 值的关系(实体到实体的链接):**
|
||||
|
||||
```sql
|
||||
SELECT * FROM quads_by_entity
|
||||
WHERE collection = 'tenant1' AND entity = 'https://example.org/Alice'
|
||||
AND role = 'S' AND p = 'https://example.org/knows' AND otype = 'U';
|
||||
```
|
||||
|
||||
**反向查找 — 指向此实体的对象:**
|
||||
|
||||
```sql
|
||||
SELECT * FROM quads_by_entity
|
||||
WHERE collection = 'tenant1' AND entity = 'https://example.org/Bob'
|
||||
AND role = 'O';
|
||||
```
|
||||
|
||||
## 标签解析和缓存预热
|
||||
|
||||
实体中心模型最显著的优势之一是,**标签解析成为一个免费的副作用**。
|
||||
|
||||
在传统的基于多表的模型中,获取标签需要单独的轮询查询:检索三元组,识别结果中的实体 URI,然后为每个 URI 获取 `rdfs:label`。 这种 N+1 模式非常昂贵。
|
||||
|
||||
在实体中心模型中,查询一个实体会返回其**所有**四元组,包括其标签、类型和其他属性。 当应用程序缓存查询结果时,标签会在任何客户端请求之前被预热。
|
||||
|
||||
两种使用模式证实了这在实践中效果良好:
|
||||
|
||||
**面向用户的查询**:结果集通常很小,标签至关重要。 实体读取会预热缓存。
|
||||
**AI/批量查询**:结果集很大,但有严格的限制。 标签要么是不必要的,要么只需要用于已缓存的实体子集。
|
||||
|
||||
解决大型结果集(例如 30,000 个实体)的标签的理论问题,可以通过实际观察来缓解,即没有人类或 AI 消费者能够有效地处理如此多的标签。 应用程序级别的查询限制可确保缓存压力保持在可管理范围内。
|
||||
|
||||
## 宽分区和重构
|
||||
|
||||
重构(RDF-star 风格的关于语句的语句)会创建中心实体,例如,一个源文档支持数千个提取的事实。 这可能会导致宽分区。
|
||||
|
||||
缓解因素:
|
||||
|
||||
**应用程序级别的查询限制**:所有 GraphRAG 和面向用户的查询都强制执行严格的限制,因此宽分区永远不会在热读取路径上被完全扫描。
|
||||
**Cassandra 可以高效地执行部分读取**:即使在大型分区上,具有早期停止的聚类列扫描也是快速的。
|
||||
**集合删除**(唯一可能遍历整个分区的操作)是一个可接受的后台过程。
|
||||
|
||||
## 集合删除
|
||||
|
||||
由 API 调用触发,在后台运行(最终一致)。
|
||||
|
||||
1. 读取 `quads_by_collection` 以获取目标集合的所有四元组
|
||||
2. 从四元组中提取唯一的实体(s、p、o、d 值)
|
||||
3. 对于每个唯一的实体,从 `quads_by_entity` 中删除该分区
|
||||
4. 从 `quads_by_collection` 中删除行
|
||||
|
||||
`quads_by_collection` 表提供了用于定位所有实体分区而无需进行全表扫描所需的索引。 由于 `(collection, entity)` 是分区键,因此分区级别的删除是高效的。
|
||||
|
||||
## 从多表模型迁移的路径
|
||||
|
||||
在迁移过程中,实体中心模型可以与现有的多表模型共存:
|
||||
|
||||
1. 将 `quads_by_entity` 和 `quads_by_collection` 表与现有表一起部署
|
||||
2. 同时将新的四元组写入旧表和新表
|
||||
3. 将现有数据回填到新表中
|
||||
4. 每次迁移一种查询模式
|
||||
5. 在迁移所有读取路径后,停用旧表
|
||||
|
||||
## 总结
|
||||
|
||||
| 方面 | 传统 (6 个表) | 实体中心 (2 个表) |
|
||||
|---|---|---|
|
||||
| 表 | 7+ | 2 |
|
||||
| 每个四元组的写入次数 | 6+ | 5 (4 个数据 + 1 个清单) |
|
||||
| 标签解析 | 单独的轮询 | 通过缓存预热免费 |
|
||||
| 查询模式 | 6 个表中的 16 种 | 1 个表中的 16 种 |
|
||||
| 模式复杂性 | 高 | 低 |
|
||||
| 运维开销 | 6 个表需要调整/修复 | 1 个数据表 |
|
||||
| 重构支持 | 额外的复杂性 | 完美契合 |
|
||||
| 对象类型过滤 | 不可用 | 原生 (通过 otype 聚类) |
|
||||
输出合同(必须严格遵守以下格式):
|
||||
192
docs/tech-specs/zh-cn/explainability-cli.zh-cn.md
Normal file
192
docs/tech-specs/zh-cn/explainability-cli.zh-cn.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
---
|
||||
layout: default
|
||||
title: "可解释 CLI 技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 可解释 CLI 技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 状态
|
||||
|
||||
草稿
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了用于在 TrustGraph 中调试和探索可解释数据的 CLI 工具。这些工具使用户能够跟踪答案的生成方式,并从边向源文档追溯查询的来源链。
|
||||
|
||||
三个 CLI 工具:
|
||||
|
||||
1. **`tg-show-document-hierarchy`** - 显示文档 → 页面 → 块 → 边层级结构
|
||||
2. **`tg-list-explain-traces`** - 列出所有 GraphRAG 会话,包含问题
|
||||
3. **`tg-show-explain-trace`** - 显示会话的完整可解释性跟踪
|
||||
|
||||
## 目标
|
||||
|
||||
- **调试**: 允许开发者检查文档处理结果
|
||||
- **可追溯性**: 追踪任何提取的事实,追溯到其原始文档
|
||||
- **透明性**: 明确显示 GraphRAG 如何得出答案
|
||||
- **易用性**: 简单的 CLI 界面,带有合理的默认设置
|
||||
|
||||
## 背景
|
||||
|
||||
TrustGraph 拥有两个来源系统:
|
||||
|
||||
1. **摄取时来源**: (见 `extraction-time-provenance.md`) - 记录文档 → 页面 → 块 → 边的关系,发生在摄取时。存储在名为 `urn:graph:source` 的图表中,使用 `prov:wasDerivedFrom` 属性。
|
||||
|
||||
2. **查询时可解释性**: (见 `query-time-explainability.md`) - 记录问题 → 探索 → 重点 → 总结链,发生在 GraphRAG 查询时。存储在名为 `urn:graph:retrieval` 的图表中。
|
||||
|
||||
当前限制:
|
||||
- 没有简单的方法来可视化文档层级结构,在处理后
|
||||
- 必须手动查询三元组来查看可解释性数据
|
||||
- 没有 GraphRAG 会话的综合视图
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 工具 1: `tg-show-document-hierarchy`
|
||||
|
||||
**目的**: 针对特定文档 ID,遍历并显示所有派生的实体。
|
||||
|
||||
**用法**:
|
||||
```bash
|
||||
tg-show-document-hierarchy "urn:trustgraph:doc:abc123"
|
||||
tg-show-document-hierarchy --show-content --max-content 500 "urn:trustgraph:doc:abc123"
|
||||
```
|
||||
|
||||
**参数**:
|
||||
| 参数 | 描述 |
|
||||
|---|---|
|
||||
| `document_id` | 文档 URI (位置参数) |
|
||||
| `-u/--api-url` | API URL |
|
||||
| `-t/--token` | 身份验证令牌 |
|
||||
| `-U/--user` | 用户 ID (默认: `trustgraph`) |
|
||||
| `-C/--collection` | 集合 (默认: `default`) |
|
||||
| `--show-content` | 包含内容 (blob/文档内容) |
|
||||
| `--max-content` | 每个 blob 的最大字符数 (默认: 200) |
|
||||
| `--format` | 输出格式: `tree` (默认), `json` |
|
||||
|
||||
**实现**:
|
||||
1. 查询三元组: `?child prov:wasDerivedFrom <document_id>` 在 `urn:graph:source` 图表中
|
||||
2. 递归查询每个结果的子节点
|
||||
3. 构建树结构: 文档 → 页面 → 块
|
||||
4. 如果 `--show-content`,则从 librarian API 获取内容
|
||||
5. 以缩进树或 JSON 格式显示
|
||||
|
||||
**输出示例**:
|
||||
```
|
||||
Document: urn:trustgraph:doc:abc123
|
||||
Title: "Sample PDF"
|
||||
Type: application/pdf
|
||||
|
||||
└── Page 1: urn:trustgraph:doc:abc123/p1
|
||||
├── Chunk 0: urn:trustgraph:doc:abc123/p1/c0
|
||||
Content: "The quick brown fox..." [truncated]
|
||||
└── Chunk 1: urn:trustgraph:doc:abc123/p1/c1
|
||||
Content: "Machine learning is..." [truncated]
|
||||
```
|
||||
|
||||
### 工具 2: `tg-list-explain-traces`
|
||||
|
||||
**目的**: 列出 GraphRAG 会话(问题)在集合中的所有实例。
|
||||
|
||||
**用法**:
|
||||
```bash
|
||||
tg-list-explain-traces
|
||||
tg-list-explain-traces --limit 20 --format json
|
||||
```
|
||||
|
||||
**参数**:
|
||||
| 参数 | 描述 |
|
||||
|---|---|
|
||||
| `-u/--api-url` | API URL |
|
||||
| `-t/--token` | 身份验证令牌 |
|
||||
| `-U/--user` | 用户 ID |
|
||||
| `-C/--collection` | 集合 |
|
||||
| `--limit` | 最大结果数 (默认: 50) |
|
||||
| `--format` | 输出格式: `table` (默认), `json` |
|
||||
|
||||
**实现**:
|
||||
1. 查询: `?session tg:query ?text` 在 `urn:graph:retrieval` 图表中
|
||||
2. 查询时间戳: `?session prov:startedAtTime ?time`
|
||||
3. 以表格形式显示
|
||||
|
||||
**输出示例**:
|
||||
```
|
||||
Session ID | Question | Time
|
||||
----------------------------------------------|--------------------------------|---------------------
|
||||
urn:trustgraph:question:abc123 | What was the War on Terror? | 2024-01-15 10:30:00
|
||||
urn:trustgraph:question:def456 | Who founded OpenAI? | 2024-01-15 09:15:00
|
||||
```
|
||||
|
||||
### 工具 3: `tg-show-explain-trace`
|
||||
|
||||
**目的**: 显示 GraphRAG 会话的完整可解释性跟踪。
|
||||
|
||||
**用法**:
|
||||
```bash
|
||||
tg-show-explain-trace "urn:trustgraph:question:abc123"
|
||||
tg-show-explain-trace --max-answer 1000 --show-provenance "urn:trustgraph:question:abc123"
|
||||
```
|
||||
|
||||
**参数**:
|
||||
| 参数 | 描述 |
|
||||
|---|---|
|
||||
| `question_id` | 问题 URI (位置参数) |
|
||||
| `-u/--api-url` | API URL |
|
||||
| `-t/--token` | 身份验证令牌 |
|
||||
| `-U/--user` | 用户 ID |
|
||||
| `-C/--collection` | 集合 |
|
||||
| `--max-answer` | 答案的最大字符数 (默认: 500) |
|
||||
| `--show-provenance` | 显示来源文档的边 |
|
||||
| `--format` | 输出格式: `text` (默认), `json` |
|
||||
|
||||
**实现**:
|
||||
1. 从 `tg:query` 谓词中获取问题文本
|
||||
2. 查找探索: `?exp prov:wasGeneratedBy <question_id>`
|
||||
3. 查找重点: `?focus prov:wasDerivedFrom <exploration_id>`
|
||||
4. 获取选定的边: `<focus_id> tg:selectedEdge ?edge`
|
||||
5. 对于每个边,获取 `tg:edge` (三元组) 和 `tg:reasoning`
|
||||
6. 查找总结: `?synth prov:wasDerivedFrom <focus_id>`
|
||||
7. 通过 librarian API 获取答案
|
||||
8. 如果 `--show-provenance`,则跟踪指向来源文档的边
|
||||
|
||||
**输出示例**:
|
||||
```
|
||||
=== GraphRAG Session: urn:trustgraph:question:abc123 ===
|
||||
|
||||
Question: What was the War on Terror?
|
||||
Time: 2024-01-15 10:30:00
|
||||
|
||||
--- Exploration ---
|
||||
Retrieved 50 edges from knowledge graph
|
||||
|
||||
--- Focus (Edge Selection) ---
|
||||
Selected 12 edges:
|
||||
|
||||
1. (War on Terror, definition, "A military campaign...")
|
||||
Reasoning: Directly defines the subject of the query
|
||||
Source: chunk → page 2 → "Beyond the Vigilant State"
|
||||
|
||||
2. (Guantanamo Bay, part_of, War on Terror)
|
||||
Reasoning: Shows key component of the campaign
|
||||
|
||||
--- Synthesis ---
|
||||
Answer:
|
||||
The War on Terror was a military campaign initiated...
|
||||
[truncated at 500 chars]
|
||||
```
|
||||
|
||||
## 创建的文件
|
||||
|
||||
| 文件 | 目的 |
|
||||
|---|---|
|
||||
| `trustgraph-cli/trustgraph/cli/show_document_hierarchy.py` | 工具 1 |
|
||||
| `trustgraph-cli/trustgraph/cli/list_explain_traces.py` | 工具 2 |
|
||||
| `trustgraph-cli/trustgraph/cli/show_explain_trace.py` | 工具 3 |
|
||||
|
||||
## 引用
|
||||
|
||||
- 咨询时间可解释性: `docs/tech-specs/query-time-explainability.md`
|
||||
- 摄取时来源: `docs/tech-specs/extraction-time-provenance.md`
|
||||
- 现有 CLI 示例: `trustgraph-cli/trustgraph/cli/invoke_graph_rag.py`
|
||||
355
docs/tech-specs/zh-cn/extraction-flows.zh-cn.md
Normal file
355
docs/tech-specs/zh-cn/extraction-flows.zh-cn.md
Normal file
|
|
@ -0,0 +1,355 @@
|
|||
---
|
||||
layout: default
|
||||
title: "数据提取流程"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 数据提取流程
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
本文档描述了数据如何通过 TrustGraph 提取流程进行流动,从文档提交到存储在知识库中。
|
||||
|
||||
## 概述
|
||||
|
||||
```
|
||||
┌──────────┐ ┌─────────────┐ ┌─────────┐ ┌────────────────────┐
|
||||
│ Librarian│────▶│ PDF Decoder │────▶│ Chunker │────▶│ Knowledge │
|
||||
│ │ │ (PDF only) │ │ │ │ Extraction │
|
||||
│ │────────────────────────▶│ │ │ │
|
||||
└──────────┘ └─────────────┘ └─────────┘ └────────────────────┘
|
||||
│ │
|
||||
│ ├──▶ Triples
|
||||
│ ├──▶ Entity Contexts
|
||||
│ └──▶ Rows
|
||||
│
|
||||
└──▶ Document Embeddings
|
||||
```
|
||||
|
||||
## 内容存储
|
||||
|
||||
### 对象存储 (S3/Minio)
|
||||
|
||||
文档内容存储在兼容 S3 的对象存储中:
|
||||
路径格式:`doc/{object_id}`,其中 object_id 是一个 UUID
|
||||
所有文档类型都存储在此处:源文档、页面、分块
|
||||
|
||||
### 元数据存储 (Cassandra)
|
||||
|
||||
文档元数据存储在 Cassandra 中,包括:
|
||||
文档 ID、标题、类型 (MIME 类型)
|
||||
`object_id` 引用对象存储
|
||||
`parent_id` 用于子文档 (页面、分块)
|
||||
`document_type`: "source", "page", "chunk", "answer"
|
||||
|
||||
### 内联与流式传输阈值
|
||||
|
||||
内容传输使用基于大小的策略:
|
||||
**< 2MB**: 内容以内联方式包含在消息中 (base64 编码)
|
||||
**≥ 2MB**: 仅发送 `document_id`;处理器通过 librarian API 获取
|
||||
|
||||
## 阶段 1:文档提交 (Librarian)
|
||||
|
||||
### 入口点
|
||||
|
||||
文档通过 librarian 的 `add-document` 操作进入系统:
|
||||
1. 内容上传到对象存储
|
||||
2. 在 Cassandra 中创建元数据记录
|
||||
3. 返回文档 ID
|
||||
|
||||
### 触发提取
|
||||
|
||||
`add-processing` 操作触发提取:
|
||||
指定 `document_id`、`flow` (pipeline ID)、`collection` (目标存储)
|
||||
Librarian 的 `load_document()` 获取内容并发布到 flow 输入队列
|
||||
|
||||
### 模式:Document
|
||||
|
||||
```
|
||||
Document
|
||||
├── metadata: Metadata
|
||||
│ ├── id: str # Document identifier
|
||||
│ ├── user: str # Tenant/user ID
|
||||
│ ├── collection: str # Target collection
|
||||
│ └── metadata: list[Triple] # (largely unused, historical)
|
||||
├── data: bytes # PDF content (base64, if inline)
|
||||
└── document_id: str # Librarian reference (if streaming)
|
||||
```
|
||||
|
||||
**路由 (Routing)**: 基于 `kind` 字段:
|
||||
`application/pdf` → `document-load` 队列 → PDF 解码器
|
||||
`text/plain` → `text-load` 队列 → 分块器
|
||||
|
||||
## 第二阶段:PDF 解码器
|
||||
|
||||
将 PDF 文档转换为文本页面。
|
||||
|
||||
### 流程
|
||||
|
||||
1. 获取内容(内联 `data` 或通过 `document_id` 从管理员处获取)
|
||||
2. 使用 PyPDF 提取页面
|
||||
3. 对于每个页面:
|
||||
另存为管理员中的子文档(`{doc_id}/p{page_num}`)
|
||||
发出来源三元组(页面源自文档)
|
||||
转发到分块器
|
||||
|
||||
### 模式:TextDocument
|
||||
|
||||
```
|
||||
TextDocument
|
||||
├── metadata: Metadata
|
||||
│ ├── id: str # Page URI (e.g., https://trustgraph.ai/doc/xxx/p1)
|
||||
│ ├── user: str
|
||||
│ ├── collection: str
|
||||
│ └── metadata: list[Triple]
|
||||
├── text: bytes # Page text content (if inline)
|
||||
└── document_id: str # Librarian reference (e.g., "doc123/p1")
|
||||
```
|
||||
|
||||
## 第三阶段:分块器
|
||||
|
||||
将文本分割成配置大小的块。
|
||||
|
||||
### 参数(可配置)
|
||||
|
||||
`chunk_size`:目标块大小(以字符为单位)(默认:2000)
|
||||
`chunk_overlap`:块之间的重叠量(默认:100)
|
||||
|
||||
### 流程
|
||||
|
||||
1. 获取文本内容(内联或通过 librarian)
|
||||
2. 使用递归字符分割器进行分割
|
||||
3. 对于每个块:
|
||||
另存为 librarian 中的子文档(`{parent_id}/c{index}`)
|
||||
发出来源三元组(块源自页面/文档)
|
||||
转发到提取处理器
|
||||
|
||||
### 模式:Chunk
|
||||
|
||||
```
|
||||
Chunk
|
||||
├── metadata: Metadata
|
||||
│ ├── id: str # Chunk URI
|
||||
│ ├── user: str
|
||||
│ ├── collection: str
|
||||
│ └── metadata: list[Triple]
|
||||
├── chunk: bytes # Chunk text content
|
||||
└── document_id: str # Librarian chunk ID (e.g., "doc123/p1/c3")
|
||||
```
|
||||
|
||||
### 文档ID层级结构
|
||||
|
||||
子文档在其ID中编码了其来源信息:
|
||||
来源:`doc123`
|
||||
页面:`doc123/p5`
|
||||
页面中的块:`doc123/p5/c2`
|
||||
文本中的块:`doc123/c2`
|
||||
|
||||
## 第4阶段:知识提取
|
||||
|
||||
可用多种提取模式,由流程配置选择。
|
||||
|
||||
### 模式A:基本GraphRAG
|
||||
|
||||
两个并行处理器:
|
||||
|
||||
**kg-extract-definitions**
|
||||
输入:块
|
||||
输出:三元组(实体定义),实体上下文
|
||||
提取内容:实体标签,定义
|
||||
|
||||
**kg-extract-relationships**
|
||||
输入:块
|
||||
输出:三元组(关系),实体上下文
|
||||
提取内容:主语-谓语-宾语关系
|
||||
|
||||
### 模式B:基于本体论的 (kg-extract-ontology)
|
||||
|
||||
输入:块
|
||||
输出:三元组,实体上下文
|
||||
使用配置的本体论来指导提取
|
||||
|
||||
### 模式C:基于代理的 (kg-extract-agent)
|
||||
|
||||
输入:块
|
||||
输出:三元组,实体上下文
|
||||
使用代理框架进行提取
|
||||
|
||||
### 模式D:行提取 (kg-extract-rows)
|
||||
|
||||
输入:块
|
||||
输出:行(结构化数据,不是三元组)
|
||||
使用模式定义来提取结构化记录
|
||||
|
||||
### 模式:三元组
|
||||
|
||||
```
|
||||
Triples
|
||||
├── metadata: Metadata
|
||||
│ ├── id: str
|
||||
│ ├── user: str
|
||||
│ ├── collection: str
|
||||
│ └── metadata: list[Triple] # (set to [] by extractors)
|
||||
└── triples: list[Triple]
|
||||
└── Triple
|
||||
├── s: Term # Subject
|
||||
├── p: Term # Predicate
|
||||
├── o: Term # Object
|
||||
└── g: str | None # Named graph
|
||||
```
|
||||
|
||||
### Schema: EntityContexts
|
||||
|
||||
```
|
||||
EntityContexts
|
||||
├── metadata: Metadata
|
||||
└── entities: list[EntityContext]
|
||||
└── EntityContext
|
||||
├── entity: Term # Entity identifier (IRI)
|
||||
├── context: str # Textual description for embedding
|
||||
└── chunk_id: str # Source chunk ID (provenance)
|
||||
```
|
||||
|
||||
### Schema: Rows
|
||||
|
||||
```
|
||||
Rows
|
||||
├── metadata: Metadata
|
||||
├── row_schema: RowSchema
|
||||
│ ├── name: str
|
||||
│ ├── description: str
|
||||
│ └── fields: list[Field]
|
||||
└── rows: list[dict[str, str]] # Extracted records
|
||||
```
|
||||
|
||||
## 第 5 阶段:嵌入式表示生成
|
||||
|
||||
### 图嵌入
|
||||
|
||||
将实体上下文转换为向量嵌入。
|
||||
|
||||
**流程:**
|
||||
1. 接收 EntityContexts (实体上下文)
|
||||
2. 使用上下文文本调用嵌入服务
|
||||
3. 输出 GraphEmbeddings (实体 → 向量映射)
|
||||
|
||||
**模式:GraphEmbeddings**
|
||||
|
||||
```
|
||||
GraphEmbeddings
|
||||
├── metadata: Metadata
|
||||
└── entities: list[EntityEmbeddings]
|
||||
└── EntityEmbeddings
|
||||
├── entity: Term # Entity identifier
|
||||
├── vector: list[float] # Embedding vector
|
||||
└── chunk_id: str # Source chunk (provenance)
|
||||
```
|
||||
|
||||
### 文档嵌入
|
||||
|
||||
将文本块直接转换为向量嵌入。
|
||||
|
||||
**流程:**
|
||||
1. 接收文本块
|
||||
2. 使用文本块调用嵌入服务
|
||||
3. 输出文档嵌入
|
||||
|
||||
**模式:文档嵌入**
|
||||
|
||||
```
|
||||
DocumentEmbeddings
|
||||
├── metadata: Metadata
|
||||
└── chunks: list[ChunkEmbeddings]
|
||||
└── ChunkEmbeddings
|
||||
├── chunk_id: str # Chunk identifier
|
||||
└── vector: list[float] # Embedding vector
|
||||
```
|
||||
|
||||
### 行嵌入 (Row Embeddings)
|
||||
|
||||
将行索引字段转换为向量嵌入。
|
||||
|
||||
**流程:**
|
||||
1. 接收行 (Receive Rows)
|
||||
2. 嵌入配置的索引字段 (Embed configured index fields)
|
||||
3. 输出到行向量存储 (Output to row vector store)
|
||||
|
||||
## 第 6 阶段:存储 (Stage 6: Storage)
|
||||
|
||||
### 三元组存储 (Triple Store)
|
||||
|
||||
接收:三元组 (Receives: Triples)
|
||||
存储:Cassandra (以实体为中心的表) (Storage: Cassandra (entity-centric tables))
|
||||
命名图将核心知识与来源信息分开: (Named graphs separate core knowledge from provenance:)
|
||||
`""` (默认): 核心知识事实 (default): Core knowledge facts
|
||||
`urn:graph:source`: 提取来源 (Extraction provenance)
|
||||
`urn:graph:retrieval`: 查询时的可解释性 (Query-time explainability)
|
||||
|
||||
### 向量存储 (图嵌入) (Vector Store (Graph Embeddings))
|
||||
|
||||
接收:图嵌入 (Receives: GraphEmbeddings)
|
||||
存储:Qdrant、Milvus 或 Pinecone (Storage: Qdrant, Milvus, or Pinecone)
|
||||
索引:实体 IRI (Indexed by: entity IRI)
|
||||
元数据:用于来源信息的 chunk_id (Metadata: chunk_id for provenance)
|
||||
|
||||
### 向量存储 (文档嵌入) (Vector Store (Document Embeddings))
|
||||
|
||||
接收:文档嵌入 (Receives: DocumentEmbeddings)
|
||||
存储:Qdrant、Milvus 或 Pinecone (Storage: Qdrant, Milvus, or Pinecone)
|
||||
索引:chunk_id (Indexed by: chunk_id)
|
||||
|
||||
### 行存储 (Row Store)
|
||||
|
||||
接收:行 (Receives: Rows)
|
||||
存储:Cassandra (Storage: Cassandra)
|
||||
基于模式的表结构 (Schema-driven table structure)
|
||||
|
||||
### 行向量存储 (Row Vector Store)
|
||||
|
||||
接收:行嵌入
|
||||
存储:向量数据库
|
||||
索引依据:行索引字段
|
||||
|
||||
## 元数据字段分析
|
||||
|
||||
### 正在使用的字段
|
||||
|
||||
| 字段 | 用途 |
|
||||
|-------|-------|
|
||||
| `metadata.id` | 文档/块标识符,日志记录,来源 |
|
||||
| `metadata.user` | 多租户,存储路由 |
|
||||
| `metadata.collection` | 目标集合选择 |
|
||||
| `document_id` | 馆员引用,来源链接 |
|
||||
| `chunk_id` | 通过流水线进行来源跟踪 |
|
||||
|
||||
<<<<<<< HEAD
|
||||
### 潜在的冗余字段
|
||||
|
||||
| 字段 | 状态 |
|
||||
|-------|--------|
|
||||
| `metadata.metadata` | 由所有提取器设置为 `[]`;文档级别的元数据现在由馆员在提交时处理 |
|
||||
=======
|
||||
### 已移除的字段
|
||||
|
||||
| 字段 | 状态 |
|
||||
|-------|--------|
|
||||
| `metadata.metadata` | 从 `Metadata` 类中移除。文档级别的元数据三元组现在由馆员直接发送到三元存储,而不是通过提取流水线。 |
|
||||
>>>>>>> e3bcbf73 (The metadata field (list of triples) in the pipeline Metadata class)
|
||||
|
||||
### 字节字段模式
|
||||
|
||||
所有内容字段(`data`,`text`,`chunk`)都是 `bytes`,但立即被所有处理器解码为 UTF-8 字符串。没有处理器使用原始字节。
|
||||
|
||||
## 流配置
|
||||
|
||||
流在外部定义,并通过配置服务提供给馆员。每个流都指定:
|
||||
|
||||
输入队列(`text-load`,`document-load`)
|
||||
处理器链
|
||||
参数(块大小,提取方法等)
|
||||
|
||||
示例流模式:
|
||||
`pdf-graphrag`:PDF → 解码器 → 分块器 → 定义 + 关系 → 嵌入
|
||||
`text-graphrag`:文本 → 分块器 → 定义 + 关系 → 嵌入
|
||||
`pdf-ontology`:PDF → 解码器 → 分块器 → 本体提取 → 嵌入
|
||||
`text-rows`:文本 → 分块器 → 行提取 → 行存储
|
||||
267
docs/tech-specs/zh-cn/extraction-provenance-subgraph.zh-cn.md
Normal file
267
docs/tech-specs/zh-cn/extraction-provenance-subgraph.zh-cn.md
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
---
|
||||
layout: default
|
||||
title: "提取来源:子图模型"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 提取来源:子图模型
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 问题
|
||||
|
||||
<<<<<<< HEAD
|
||||
提取时 provenance 目前为每个提取的三元组生成完整的重构:一个唯一的 ⟦CODE_0⟧、⟦CODE_1⟧,以及与每个知识事实相关的 PROV-O 元数据。处理一个块
|
||||
提取时 provenance 目前为每个提取的三元组生成完整的重构:一个唯一的 `stmt_uri`、`activity_uri`,以及与每个知识事实相关的 PROV-O 元数据。处理一个块
|
||||
提取时 provenance 目前为每个提取的三元组生成完整的重构:一个唯一的 ⟦CODE_0⟧、⟦CODE_1⟧,以及与每个知识事实相关的 PROV-O 元数据。处理一个块
|
||||
这会产生 20 个关系,并在其基础上产生约 220 个溯源三元组,而知识三元组约为 20 个,这导致了大约 10:1 的开销。
|
||||
|
||||
|
||||
这既成本高昂(存储、索引、传输),又在语义上
|
||||
不准确。每个片段都由单个 LLM 调用处理,该调用在一个事务中生成
|
||||
所有其三元组。当前的每个三元组模型
|
||||
通过制造 20 个独立提取
|
||||
事件的假象来掩盖这一点。
|
||||
=======
|
||||
提取时段的溯源信息目前会生成完整的实体化表示。
|
||||
提取的三元组:一个唯一的 `stmt_uri`,`activity_uri`,以及与之相关的
|
||||
PROV-O 元数据,用于每个知识事实。处理一个块
|
||||
这会产生 20 个关系,并在其基础上产生约 220 个溯源三元组,而知识三元组约为 20 个,这导致了大约 10:1 的开销。
|
||||
|
||||
|
||||
这既昂贵(存储、索引、传输),又在语义上
|
||||
不准确。每个片段都由单个 LLM 调用处理,该调用在一个事务中生成
|
||||
所有三元组。当前的每个三元组模型
|
||||
通过产生 20 个独立提取
|
||||
事件的幻觉来掩盖这一点。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
此外,四个提取处理器中的两个(kg-extract-ontology、
|
||||
kg-extract-agent)完全没有来源信息,这在审计
|
||||
跟踪中留下了空白。
|
||||
|
||||
## 解决方案
|
||||
|
||||
<<<<<<< HEAD
|
||||
将每个三元组的显式化替换为**子图模型**:每个数据块提取一个溯源记录,该记录在从该数据块生成的所有三元组中共享。
|
||||
=======
|
||||
用 **子图模型** 替换三元组级别的显式化:每个数据块提取一个溯源记录,该记录与从该数据块生成的所有三元组共享。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
|
||||
|
||||
### 术语变更
|
||||
|
||||
| 旧术语 | 新术语 |
|
||||
|-----|-----|
|
||||
| `stmt_uri` (`https://trustgraph.ai/stmt/{uuid}`) | `subgraph_uri` (`https://trustgraph.ai/subgraph/{uuid}`) |
|
||||
| `statement_uri()` | `subgraph_uri()` |
|
||||
<<<<<<< HEAD
|
||||
| `tg:reifies` (1:1, 相同) | `tg:contains` (1:多, 包含) |
|
||||
|
||||
### 目标结构
|
||||
|
||||
所有溯源三元组都放入名为 `urn:graph:source` 的命名图中。
|
||||
=======
|
||||
| `tg:reifies` (1:1, 身份) | `tg:contains` (1:多, 包含) |
|
||||
|
||||
### 目标结构
|
||||
|
||||
所有溯源三元组都存储在名为 `urn:graph:source` 的图中。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
```
|
||||
# Subgraph contains each extracted triple (RDF-star quoted triples)
|
||||
<subgraph> tg:contains <<s1 p1 o1>> .
|
||||
<subgraph> tg:contains <<s2 p2 o2>> .
|
||||
<subgraph> tg:contains <<s3 p3 o3>> .
|
||||
|
||||
# Derivation from source chunk
|
||||
<subgraph> prov:wasDerivedFrom <chunk_uri> .
|
||||
<subgraph> prov:wasGeneratedBy <activity> .
|
||||
|
||||
# Activity: one per chunk extraction
|
||||
<activity> rdf:type prov:Activity .
|
||||
<activity> rdfs:label "{component_name} extraction" .
|
||||
<activity> prov:used <chunk_uri> .
|
||||
<activity> prov:wasAssociatedWith <agent> .
|
||||
<activity> prov:startedAtTime "2026-03-13T10:00:00Z" .
|
||||
<activity> tg:componentVersion "0.25.0" .
|
||||
<activity> tg:llmModel "gpt-4" . # if available
|
||||
<activity> tg:ontology <ontology_uri> . # if available
|
||||
|
||||
# Agent: stable per component
|
||||
<agent> rdf:type prov:Agent .
|
||||
<agent> rdfs:label "{component_name}" .
|
||||
```
|
||||
|
||||
### 比较数据量
|
||||
|
||||
对于一个产生 N 个提取三元组的模块:
|
||||
|
||||
| | 旧方式(每个三元组) | 新方式(子图) |
|
||||
|---|---|---|
|
||||
| `tg:contains` / `tg:reifies` | N | N |
|
||||
| 活动三元组 | ~9 x N | ~9 |
|
||||
| 代理三元组 | 2 x N | 2 |
|
||||
| 语句/子图元数据 | 2 x N | 2 |
|
||||
| **总的溯源三元组** | **~13N** | **N + 13** |
|
||||
| **示例(N=20)** | **~260** | **33** |
|
||||
|
||||
## 范围
|
||||
|
||||
### 需要更新的处理器(现有溯源,每个三元组)
|
||||
|
||||
**kg-extract-definitions**
|
||||
(`trustgraph-flow/trustgraph/extract/kg/definitions/extract.py`)
|
||||
|
||||
<<<<<<< HEAD
|
||||
目前,它在每个定义的循环内部调用 `statement_uri()` + `triple_provenance_triples()`。
|
||||
=======
|
||||
当前,它在每个定义的循环内部调用 `statement_uri()` + `triple_provenance_triples()`。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
|
||||
更改:
|
||||
将 `subgraph_uri()` 和 `activity_uri()` 的创建移到循环之前。
|
||||
在循环内部收集 `tg:contains` 三元组。
|
||||
循环结束后,一次性输出共享的活动/主体/推导块。
|
||||
|
||||
**kg-extract-relationships**
|
||||
(`trustgraph-flow/trustgraph/extract/kg/relationships/extract.py`)
|
||||
|
||||
模式与定义相同。 更改也相同。
|
||||
|
||||
<<<<<<< HEAD
|
||||
### 需要添加的处理器,用于添加来源信息(目前缺失)
|
||||
=======
|
||||
### 需要添加的处理器,用于添加来源信息(当前缺失)
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
**kg-extract-ontology**
|
||||
(`trustgraph-flow/trustgraph/extract/kg/ontology/extract.py`)
|
||||
|
||||
<<<<<<< HEAD
|
||||
目前会生成不带来源信息的三元组。添加子图来源信息。
|
||||
=======
|
||||
目前会输出不带来源信息的三元组。添加子图来源信息。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
使用相同的模式:每个块一个子图,对于每个提取的三元组使用 `tg:contains`。
|
||||
|
||||
|
||||
**kg-extract-agent**
|
||||
(`trustgraph-flow/trustgraph/extract/kg/agent/extract.py`)
|
||||
|
||||
<<<<<<< HEAD
|
||||
目前会生成不带来源信息的三元组。添加子图来源信息。
|
||||
=======
|
||||
目前会输出不带来源信息的三元组。添加子图来源信息。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
使用相同的模式。
|
||||
|
||||
### 共享来源库的更改
|
||||
|
||||
**`trustgraph-base/trustgraph/provenance/triples.py`**
|
||||
|
||||
将 `triple_provenance_triples()` 替换为 `subgraph_provenance_triples()`
|
||||
<<<<<<< HEAD
|
||||
新函数接受一个提取的三元组列表,而不是单个三元组。
|
||||
=======
|
||||
新函数接受提取的三元组列表,而不是单个三元组。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
为每个三元组生成一个 `tg:contains`,共享活动/代理块。
|
||||
移除旧的 `triple_provenance_triples()`
|
||||
|
||||
**`trustgraph-base/trustgraph/provenance/uris.py`**
|
||||
|
||||
将 `statement_uri()` 替换为 `subgraph_uri()`
|
||||
|
||||
**`trustgraph-base/trustgraph/provenance/namespaces.py`**
|
||||
|
||||
将 `TG_REIFIES` 替换为 `TG_CONTAINS`
|
||||
|
||||
### 不在范围之内
|
||||
|
||||
**kg-extract-topics**: 较旧的处理器,目前未在标准流程中使用。
|
||||
**kg-extract-rows**: 生成的是行,而不是三元组,具有不同的数据来源模型。
|
||||
**查询时的数据来源** (⟦CODE_0⟧): 独立的关注点。
|
||||
模型
|
||||
<<<<<<< HEAD
|
||||
**查询时的数据来源信息** (`urn:graph:retrieval`):独立的关注点,
|
||||
已经使用了不同的模式(提问/探索/聚焦/综合)。
|
||||
**文档/页面/块的来源**(PDF解码器,分块器):已经使用了。
|
||||
`derived_entity_triples()`,这对于每个实体而言,而不是每个三元组而言,因此没有
|
||||
重复的问题。
|
||||
=======
|
||||
**查询时的数据来源信息** (`urn:graph:retrieval`): 独立的关注点,
|
||||
已经使用了不同的模式(问题/探索/重点/综合)。
|
||||
**文档/页面/块的来源**(PDF解码器,分块器):已经使用了。
|
||||
`derived_entity_triples()`,这对于每个实体而言,而不是每个三元组而言——没有。
|
||||
重复问题。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
## 实现说明
|
||||
|
||||
### 处理器循环重构
|
||||
|
||||
之前(每个三元组,在关系中):
|
||||
```python
|
||||
for rel in rels:
|
||||
# ... build relationship_triple ...
|
||||
stmt_uri = statement_uri()
|
||||
prov_triples = triple_provenance_triples(
|
||||
stmt_uri=stmt_uri,
|
||||
extracted_triple=relationship_triple,
|
||||
...
|
||||
)
|
||||
triples.extend(set_graph(prov_triples, GRAPH_SOURCE))
|
||||
```
|
||||
|
||||
在 (子图) 之后:
|
||||
```python
|
||||
sg_uri = subgraph_uri()
|
||||
|
||||
for rel in rels:
|
||||
# ... build relationship_triple ...
|
||||
extracted_triples.append(relationship_triple)
|
||||
|
||||
prov_triples = subgraph_provenance_triples(
|
||||
subgraph_uri=sg_uri,
|
||||
extracted_triples=extracted_triples,
|
||||
chunk_uri=chunk_uri,
|
||||
component_name=default_ident,
|
||||
component_version=COMPONENT_VERSION,
|
||||
llm_model=llm_model,
|
||||
ontology_uri=ontology_uri,
|
||||
)
|
||||
triples.extend(set_graph(prov_triples, GRAPH_SOURCE))
|
||||
```
|
||||
|
||||
### 新的辅助签名
|
||||
|
||||
```python
|
||||
def subgraph_provenance_triples(
|
||||
subgraph_uri: str,
|
||||
extracted_triples: List[Triple],
|
||||
chunk_uri: str,
|
||||
component_name: str,
|
||||
component_version: str,
|
||||
llm_model: Optional[str] = None,
|
||||
ontology_uri: Optional[str] = None,
|
||||
timestamp: Optional[str] = None,
|
||||
) -> List[Triple]:
|
||||
"""
|
||||
Build provenance triples for a subgraph of extracted knowledge.
|
||||
|
||||
Creates:
|
||||
- tg:contains link for each extracted triple (RDF-star quoted)
|
||||
- One prov:wasDerivedFrom link to source chunk
|
||||
- One activity with agent metadata
|
||||
"""
|
||||
```
|
||||
|
||||
### 破坏性变更
|
||||
|
||||
这是一个对溯源模型的重大更改。
|
||||
溯源功能尚未发布,因此无需迁移。旧的 `tg:reifies` / `tg:reifies` 代码可以直接删除。
|
||||
`statement_uri` 代码可以直接删除。
|
||||
837
docs/tech-specs/zh-cn/extraction-time-provenance.zh-cn.md
Normal file
837
docs/tech-specs/zh-cn/extraction-time-provenance.zh-cn.md
Normal file
|
|
@ -0,0 +1,837 @@
|
|||
---
|
||||
layout: default
|
||||
title: "提取时的数据来源:源层"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 提取时的数据来源:源层
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录了关于提取时数据来源的笔记,用于未来的规范工作。提取时的数据来源记录了数据的“源层”,即数据最初来自哪里,以及它是如何提取和转换的。
|
||||
|
||||
这与查询时的数据来源(参见 `query-time-provenance.md`)不同,后者记录的是代理推理过程。
|
||||
|
||||
## 问题陈述
|
||||
|
||||
### 当前实现
|
||||
|
||||
<<<<<<< HEAD
|
||||
目前,数据来源的工作方式如下:
|
||||
=======
|
||||
当前的数据来源工作方式如下:
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
文档元数据以 RDF 三元组的形式存储在知识图谱中。
|
||||
文档 ID 将元数据与文档关联起来,因此文档在图中显示为节点。
|
||||
当从文档中提取出边(关系/事实)时,一个 `subjectOf` 关系将提取出的边链接回原始文档。
|
||||
|
||||
### 当前方法的缺点
|
||||
|
||||
<<<<<<< HEAD
|
||||
1. **重复加载元数据:** 文档元数据会被打包并重复加载,每次从该文档中提取一批三元组时都会重复。这既浪费又冗余,相同的元数据会作为“货物”随每次提取输出一起传输。
|
||||
=======
|
||||
1. **重复加载元数据:** 文档元数据被打包并重复加载,每次从该文档中提取一批三元组时都会重复。这既浪费又冗余,相同的元数据会作为“货物”随每次提取输出一起传输。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
2. **浅层数据来源:** 当前的 `subjectOf` 关系仅将事实直接链接到顶级文档。无法了解转换链,例如,该事实来自哪个页面,哪个块,使用了哪种提取方法。
|
||||
|
||||
### 期望状态
|
||||
|
||||
1. **一次加载元数据:** 文档元数据应该只加载一次,并附加到顶级文档节点,而不是重复包含在每个三元组批次中。
|
||||
|
||||
<<<<<<< HEAD
|
||||
2. **丰富的数据来源 DAG:** 捕获从原始文档到所有中间工件,再到提取出的事实的完整转换链。例如,一个 PDF 文档的转换过程:
|
||||
=======
|
||||
2. **丰富的数据来源 DAG:** 捕获从原始文档到所有中间工件再到提取事实的完整转换链。例如,一个 PDF 文档的转换过程:
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
```
|
||||
PDF file (source document with metadata)
|
||||
→ Page 1 (decoded text)
|
||||
→ Chunk 1
|
||||
→ Extracted edge/fact (via subjectOf)
|
||||
→ Extracted edge/fact
|
||||
→ Chunk 2
|
||||
→ Extracted edge/fact
|
||||
→ Page 2
|
||||
→ Chunk 3
|
||||
→ ...
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
3. **统一存储:** 提取的知识及其来源信息(provenance)都存储在同一个知识图谱中。这使得对来源信息的查询方式与对知识的查询方式相同,即可以从任何事实出发,沿着链条追溯到其确切的来源位置。
|
||||
|
||||
4. **稳定的ID:** 每个中间产物(页面、段落)都具有一个稳定的ID,该ID在图中表示为一个节点。
|
||||
|
||||
5. **父子链接:** 从派生文档到其父文档,一直链接到顶层源文档,使用一致的关系类型。
|
||||
=======
|
||||
3. **统一存储:** 提取的知识及其来源信息(provenance)都存储在同一个知识图谱中。这使得对来源信息的查询方式与对知识的查询方式相同——可以从任何事实出发,沿着链条追溯到其确切的来源位置。
|
||||
|
||||
4. **稳定的ID:** 每个中间产物(页面、段落)都有一个稳定的ID,作为图谱中的一个节点。
|
||||
|
||||
5. **父子链接:** 从派生文档到其父文档,一直链接到顶层原始文档,使用一致的关系类型。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
6. **精确的事实归属:** 提取的边上的 `subjectOf` 关系指向直接的父节点(段落),而不是顶层文档。可以通过遍历DAG来恢复完整的来源信息。
|
||||
|
||||
## 用例
|
||||
|
||||
### UC1:GraphRAG响应中的来源归属
|
||||
|
||||
**场景:** 用户运行GraphRAG查询,并从代理接收到响应。
|
||||
|
||||
**流程:**
|
||||
1. 用户向GraphRAG代理提交查询。
|
||||
2. 代理从知识图谱中检索相关的事实,以构建响应。
|
||||
3. 根据查询时期的来源信息规范,代理报告哪些事实对响应做出了贡献。
|
||||
<<<<<<< HEAD
|
||||
4. 每个事实通过来源信息DAG链接到其源段落。
|
||||
5. 段落链接到页面,页面链接到源文档。
|
||||
=======
|
||||
4. 每个事实通过来源信息DAG链接到其来源段落。
|
||||
5. 段落链接到页面,页面链接到原始文档。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
**用户体验结果:** 界面会显示LLM响应以及来源归属信息。用户可以:
|
||||
查看哪些事实支持了响应。
|
||||
从事实 → 段落 → 页面 → 文档进行深入了解。
|
||||
<<<<<<< HEAD
|
||||
浏览原始的源文档以验证声明。
|
||||
=======
|
||||
浏览原始来源文档以验证声明。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
准确了解事实的来源(哪个页面,哪个部分)。
|
||||
|
||||
**价值:** 用户可以根据原始来源验证AI生成的响应,从而建立信任并实现事实核查。
|
||||
|
||||
### UC2:调试提取质量
|
||||
|
||||
<<<<<<< HEAD
|
||||
某个事实看起来不正确。追溯到段落 → 页面 → 文档,查看原始文本。是提取出现问题,还是原始来源本身就是错误的?
|
||||
|
||||
### UC3:增量重提取
|
||||
|
||||
源文档已更新。哪些段落/事实是从它派生的?仅使这些段落/事实失效并重新生成,而不是重新处理所有内容。
|
||||
|
||||
### UC4:数据删除/被遗忘的权利
|
||||
|
||||
必须删除一个源文档(GDPR、法律等)。遍历DAG以查找并删除所有派生的事实。
|
||||
|
||||
### UC5:冲突解决
|
||||
|
||||
两个事实相互矛盾。追溯到它们的来源,以了解原因并决定应该信任哪个(更权威的来源、更新的来源等)。
|
||||
=======
|
||||
某个事实看起来不正确。追溯到段落 → 页面 → 文档,查看原始文本。是提取过程出现问题,还是原始来源本身就是错误的?
|
||||
|
||||
### UC3:增量重提取
|
||||
|
||||
原始文档被更新。哪些段落/事实是从它派生的?仅使这些段落/事实失效并重新生成,而不是重新处理所有内容。
|
||||
|
||||
### UC4:数据删除/被遗忘的权利
|
||||
|
||||
必须删除一个原始文档(GDPR、法律等)。遍历DAG以查找并删除所有派生的事实。
|
||||
|
||||
### UC5:冲突解决
|
||||
|
||||
两个事实相互矛盾。追溯到它们的来源,以了解原因,并决定应该信任哪个(更权威的来源、更新的来源等)。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
### UC6:来源权威性加权
|
||||
|
||||
某些来源比其他来源更具权威性。可以根据其原始文档的权威性/质量对事实进行加权或过滤。
|
||||
|
||||
### UC7:提取管道比较
|
||||
|
||||
<<<<<<< HEAD
|
||||
比较来自不同提取方法/版本的输出。哪个提取器从相同的来源生成了更好的事实?
|
||||
=======
|
||||
比较不同提取方法/版本的输出。哪个提取器从相同的来源生成了更好的事实?
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
## 集成点
|
||||
|
||||
### Librarian
|
||||
|
||||
librarian组件已经提供了文档存储,并具有唯一的文档ID。来源信息系统与此现有的基础设施集成。
|
||||
|
||||
#### 现有功能(已实现)
|
||||
|
||||
**父子文档链接:**
|
||||
<<<<<<< HEAD
|
||||
`parent_id` 字段在 `DocumentMetadata` 中 - 将子文档链接到父文档。
|
||||
`document_type` 字段 - 值:`"source"`(原始)或 `"extracted"`(派生)。
|
||||
`add-child-document` API - 创建具有自动 `document_type = "extracted"` 的子文档。
|
||||
`list-children` API - 检索父文档的所有子文档。
|
||||
级联删除 - 删除父文档会自动删除所有子文档。
|
||||
|
||||
**文档识别:**
|
||||
文档ID由客户端指定(不是自动生成)。
|
||||
文档按复合 `(user, document_id)` 在Cassandra中键入。
|
||||
对象ID(UUID)在内部生成,用于blob存储。
|
||||
|
||||
**元数据支持:**
|
||||
`metadata: list[Triple]` 字段 - RDF三元组用于结构化元数据。
|
||||
`title`、`comments`、`tags` - 基本文档元数据。
|
||||
`time` - 时间戳,`kind` - MIME类型。
|
||||
|
||||
**存储架构:**
|
||||
元数据存储在Cassandra中(`librarian` 键空间,`document` 表)。
|
||||
内容存储在MinIO/S3 blob存储中(`library` 存储桶)。
|
||||
智能内容交付:小于 2MB 的文档嵌入,较大的文档流式传输。
|
||||
|
||||
#### 关键文件
|
||||
|
||||
`trustgraph-flow/trustgraph/librarian/librarian.py` - 核心 librarian 操作。
|
||||
`trustgraph-flow/trustgraph/librarian/service.py` - 服务处理器,文档加载。
|
||||
`trustgraph-flow/trustgraph/tables/library.py` - Cassandra 表存储。
|
||||
`trustgraph-base/trustgraph/schema/services/library.py` - 模式定义。
|
||||
|
||||
#### 需要解决的问题
|
||||
|
||||
librarian 具有构建块,但目前:
|
||||
1. 父子链接仅限于一级深度 - 没有多级DAG遍历辅助功能。
|
||||
2. 没有标准的关系类型词汇表(例如,`derivedFrom`、`extractedFrom`)。
|
||||
3. 来源信息元数据(提取方法、置信度、段落位置)尚未标准化。
|
||||
4. 没有查询API来遍历从事实到源的全程来源信息链。
|
||||
=======
|
||||
`parent_id` 字段在 `DocumentMetadata` 中 - 将子文档链接到父文档
|
||||
`document_type` 字段 - 值:`"source"`(原始)或 `"extracted"`(派生)
|
||||
`add-child-document` API - 创建具有自动 `document_type = "extracted"` 的子文档
|
||||
`list-children` API - 检索父文档的所有子文档
|
||||
级联删除 - 删除父文档会自动删除所有子文档
|
||||
|
||||
**文档识别:**
|
||||
文档ID由客户端指定(不是自动生成)
|
||||
文档按复合 `(user, document_id)` 在Cassandra中键入
|
||||
对象ID(UUID)在内部生成,用于blob存储
|
||||
|
||||
**元数据支持:**
|
||||
`metadata: list[Triple]` 字段 - RDF三元组用于结构化元数据
|
||||
`title`、`comments`、`tags` - 基本文档元数据
|
||||
`time` - 时间戳,`kind` - MIME类型
|
||||
|
||||
**存储架构:**
|
||||
元数据存储在Cassandra中(`librarian` keyspace,`document` table)
|
||||
内容存储在MinIO/S3 blob存储中(`library` bucket)
|
||||
智能内容交付:小于 2MB 的文档嵌入,较大的文档流式传输
|
||||
|
||||
#### 关键文件
|
||||
|
||||
`trustgraph-flow/trustgraph/librarian/librarian.py` - 核心 librarian 操作
|
||||
`trustgraph-flow/trustgraph/librarian/service.py` - 服务处理器,文档加载
|
||||
`trustgraph-flow/trustgraph/tables/library.py` - Cassandra 表存储
|
||||
`trustgraph-base/trustgraph/schema/services/library.py` - 模式定义
|
||||
|
||||
#### 需要解决的关键问题
|
||||
|
||||
librarian 具有构建块,但目前:
|
||||
1. 父子链接仅限于一级深度 - 没有多级DAG遍历辅助功能
|
||||
2. 没有标准的关系类型词汇表(例如,`derivedFrom`、`extractedFrom`)
|
||||
3. 来源信息元数据(提取方法、置信度、段落位置)尚未标准化
|
||||
4. 没有查询API来遍历从事实到原始来源的完整来源信息链
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
## 端到端流程设计
|
||||
|
||||
管道中的每个处理器都遵循一致的模式:
|
||||
<<<<<<< HEAD
|
||||
从上游接收文档ID。
|
||||
从librarian中获取内容。
|
||||
生成子产物。
|
||||
对于每个子产物:保存到librarian,向图发出边,将ID转发到下游。
|
||||
|
||||
### 处理流程
|
||||
|
||||
有两个流程,具体取决于文档类型:
|
||||
=======
|
||||
从上游接收文档ID
|
||||
从librarian中获取内容
|
||||
生成子产物
|
||||
对于每个子产物:保存到librarian,向图发出边,向下游传递ID
|
||||
|
||||
### 处理流程
|
||||
|
||||
根据文档类型,有两种流程:
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
#### PDF文档流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Librarian (initiate processing) │
|
||||
│ 1. Emit root document metadata to knowledge graph (once) │
|
||||
│ 2. Send root document ID to PDF extractor │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ PDF Extractor (per page) │
|
||||
│ 1. Fetch PDF content from librarian using document ID │
|
||||
│ 2. Extract pages as text │
|
||||
│ 3. For each page: │
|
||||
│ a. Save page as child document in librarian (parent = root doc) │
|
||||
│ b. Emit parent-child edge to knowledge graph │
|
||||
│ c. Send page document ID to chunker │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Chunker (per chunk) │
|
||||
│ 1. Fetch page content from librarian using document ID │
|
||||
│ 2. Split text into chunks │
|
||||
│ 3. For each chunk: │
|
||||
│ a. Save chunk as child document in librarian (parent = page) │
|
||||
│ b. Emit parent-child edge to knowledge graph │
|
||||
│ c. Send chunk document ID + chunk content to next processor │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
Post-chunker optimization: messages carry both
|
||||
chunk ID (for provenance) and content (to avoid
|
||||
librarian round-trip). Chunks are small (2-4KB).
|
||||
─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Knowledge Extractor (per chunk) │
|
||||
│ 1. Receive chunk ID + content directly (no librarian fetch needed) │
|
||||
│ 2. Extract facts/triples and embeddings from chunk content │
|
||||
│ 3. For each triple: │
|
||||
│ a. Emit triple to knowledge graph │
|
||||
│ b. Emit reified edge linking triple → chunk ID (edge pointing │
|
||||
│ to edge - first use of reification support) │
|
||||
│ 4. For each embedding: │
|
||||
│ a. Emit embedding with its entity ID │
|
||||
│ b. Link entity ID → chunk ID in knowledge graph │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 文本文档流程
|
||||
|
||||
文本文档会跳过 PDF 提取器,直接进入分块处理:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Librarian (initiate processing) │
|
||||
│ 1. Emit root document metadata to knowledge graph (once) │
|
||||
│ 2. Send root document ID directly to chunker (skip PDF extractor) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Chunker (per chunk) │
|
||||
│ 1. Fetch text content from librarian using document ID │
|
||||
│ 2. Split text into chunks │
|
||||
│ 3. For each chunk: │
|
||||
│ a. Save chunk as child document in librarian (parent = root doc) │
|
||||
│ b. Emit parent-child edge to knowledge graph │
|
||||
│ c. Send chunk document ID + chunk content to next processor │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ Knowledge Extractor │
|
||||
│ (same as PDF flow) │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
结果生成的有向无环图(DAG)的层数减少了一层:
|
||||
|
||||
```
|
||||
PDF: Document → Pages → Chunks → Triples/Embeddings
|
||||
Text: Document → Chunks → Triples/Embeddings
|
||||
```
|
||||
|
||||
该设计同时适用于这两种情况,因为分块器以通用方式处理其输入 - 它使用接收到的任何文档 ID 作为父级,无论该 ID 是源文档还是页面。
|
||||
|
||||
### 元数据模式 (PROV-O)
|
||||
|
||||
溯源元数据使用 W3C PROV-O 本体。这提供了一个标准词汇表,并为未来提取输出的签名/身份验证提供了支持。
|
||||
|
||||
#### PROV-O 核心概念
|
||||
|
||||
| PROV-O 类型 | TrustGraph 用法 |
|
||||
|-------------|------------------|
|
||||
| `prov:Entity` | 文档、页面、块、三元组、嵌入 |
|
||||
| `prov:Activity` | 提取操作的实例 |
|
||||
| `prov:Agent` | TG 组件(PDF 提取器、分块器等)及其版本 |
|
||||
|
||||
#### PROV-O 关系
|
||||
|
||||
| 谓词 | 含义 | 示例 |
|
||||
|-----------|---------|---------|
|
||||
| `prov:wasDerivedFrom` | 一个实体源自另一个实体 | 页面 wasDerivedFrom 文档 |
|
||||
| `prov:wasGeneratedBy` | 一个实体由一个活动生成 | 页面 wasGeneratedBy PDFExtractionActivity |
|
||||
| `prov:used` | 一个活动使用一个实体作为输入 | PDFExtractionActivity used Document |
|
||||
| `prov:wasAssociatedWith` | 一个活动由一个代理执行 | PDFExtractionActivity wasAssociatedWith tg:PDFExtractor |
|
||||
|
||||
#### 每个级别的元数据
|
||||
|
||||
**源文档(由 Librarian 产生):**
|
||||
```
|
||||
doc:123 a prov:Entity .
|
||||
doc:123 dc:title "Research Paper" .
|
||||
doc:123 dc:source <https://example.com/paper.pdf> .
|
||||
doc:123 dc:date "2024-01-15" .
|
||||
doc:123 dc:creator "Author Name" .
|
||||
doc:123 tg:pageCount 42 .
|
||||
doc:123 tg:mimeType "application/pdf" .
|
||||
```
|
||||
|
||||
**页面 (由 PDF 提取器生成):**
|
||||
```
|
||||
page:123-1 a prov:Entity .
|
||||
page:123-1 prov:wasDerivedFrom doc:123 .
|
||||
page:123-1 prov:wasGeneratedBy activity:pdf-extract-456 .
|
||||
page:123-1 tg:pageNumber 1 .
|
||||
|
||||
activity:pdf-extract-456 a prov:Activity .
|
||||
activity:pdf-extract-456 prov:used doc:123 .
|
||||
activity:pdf-extract-456 prov:wasAssociatedWith tg:PDFExtractor .
|
||||
activity:pdf-extract-456 tg:componentVersion "1.2.3" .
|
||||
activity:pdf-extract-456 prov:startedAtTime "2024-01-15T10:30:00Z" .
|
||||
```
|
||||
|
||||
**块 (由分块器发出):**
|
||||
```
|
||||
chunk:123-1-1 a prov:Entity .
|
||||
chunk:123-1-1 prov:wasDerivedFrom page:123-1 .
|
||||
chunk:123-1-1 prov:wasGeneratedBy activity:chunk-789 .
|
||||
chunk:123-1-1 tg:chunkIndex 1 .
|
||||
chunk:123-1-1 tg:charOffset 0 .
|
||||
chunk:123-1-1 tg:charLength 2048 .
|
||||
|
||||
activity:chunk-789 a prov:Activity .
|
||||
activity:chunk-789 prov:used page:123-1 .
|
||||
activity:chunk-789 prov:wasAssociatedWith tg:Chunker .
|
||||
activity:chunk-789 tg:componentVersion "1.0.0" .
|
||||
activity:chunk-789 tg:chunkSize 2048 .
|
||||
activity:chunk-789 tg:chunkOverlap 200 .
|
||||
```
|
||||
|
||||
**强调 (由知识提取器发出):**
|
||||
```
|
||||
# The extracted triple (edge)
|
||||
entity:JohnSmith rel:worksAt entity:AcmeCorp .
|
||||
|
||||
# Subgraph containing the extracted triples
|
||||
subgraph:001 tg:contains <<entity:JohnSmith rel:worksAt entity:AcmeCorp>> .
|
||||
subgraph:001 prov:wasDerivedFrom chunk:123-1-1 .
|
||||
subgraph:001 prov:wasGeneratedBy activity:extract-999 .
|
||||
|
||||
activity:extract-999 a prov:Activity .
|
||||
activity:extract-999 prov:used chunk:123-1-1 .
|
||||
activity:extract-999 prov:wasAssociatedWith tg:KnowledgeExtractor .
|
||||
activity:extract-999 tg:componentVersion "2.1.0" .
|
||||
activity:extract-999 tg:llmModel "claude-3" .
|
||||
activity:extract-999 tg:ontology <http://example.org/ontologies/business-v1> .
|
||||
```
|
||||
|
||||
**嵌入 (存储在向量存储中,而不是三元组存储中):**
|
||||
|
||||
嵌入存储在向量存储中,包含元数据,而不是作为 RDF 三元组。每个嵌入记录包含:
|
||||
|
||||
| 字段 | 描述 | 示例 |
|
||||
|-------|-------------|---------|
|
||||
| vector | 嵌入向量 | [0.123, -0.456, ...] |
|
||||
| entity | 嵌入所代表的节点 URI | `entity:JohnSmith` |
|
||||
| chunk_id | 源块 (来源) | `chunk:123-1-1` |
|
||||
| model | 使用的嵌入模型 | `text-embedding-ada-002` |
|
||||
| component_version | TG 嵌入器版本 | `1.0.0` |
|
||||
|
||||
`entity` 字段将嵌入链接到知识图谱 (节点 URI)。 `chunk_id` 字段提供回溯到源块的来源信息,从而可以向上遍历 DAG,到达原始文档。
|
||||
|
||||
#### TrustGraph 命名空间扩展
|
||||
|
||||
在 `tg:` 命名空间下,定义了用于提取特定元数据的自定义谓词:
|
||||
|
||||
| 谓词 | 域 | 描述 |
|
||||
|-----------|--------|-------------|
|
||||
| `tg:contains` | Subgraph | 指向此提取子图中包含的三元组 |
|
||||
| `tg:pageCount` | Document | 源文档中的总页数 |
|
||||
| `tg:mimeType` | Document | 源文档的 MIME 类型 |
|
||||
| `tg:pageNumber` | Page | 源文档中的页码 |
|
||||
| `tg:chunkIndex` | Chunk | 父级中的块索引 |
|
||||
| `tg:charOffset` | Chunk | 父级文本中的字符偏移量 |
|
||||
| `tg:charLength` | Chunk | 块的字符长度 |
|
||||
| `tg:chunkSize` | Activity | 配置的块大小 |
|
||||
| `tg:chunkOverlap` | Activity | 配置的块之间的重叠 |
|
||||
| `tg:componentVersion` | Activity | TG 组件的版本 |
|
||||
| `tg:llmModel` | Activity | 用于提取的 LLM |
|
||||
| `tg:ontology` | Activity | 用于指导提取的本体 URI |
|
||||
| `tg:embeddingModel` | Activity | 用于嵌入的模型 |
|
||||
| `tg:sourceText` | Statement | 从提取的三元组的精确文本 |
|
||||
| `tg:sourceCharOffset` | Statement | 源文本在块中的字符偏移量 |
|
||||
| `tg:sourceCharLength` | Statement | 源文本的字符长度 |
|
||||
|
||||
#### 词汇引导 (每个集合)
|
||||
|
||||
<<<<<<< HEAD
|
||||
知识图谱是本体无关的,并且初始化为空。 当首次将 PROV-O provenance 数据写入集合时,必须使用 RDF 标签引导所有类和谓词的词汇。 这可确保在查询和 UI 中提供人类可读的显示。
|
||||
=======
|
||||
知识图谱是本体无关的,并且初始化为空。当首次将 PROV-O provenance 数据写入集合时,必须使用 RDF 标签引导所有类和谓词的词汇。这可确保在查询和 UI 中提供人类可读的显示。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
**PROV-O 类:**
|
||||
```
|
||||
prov:Entity rdfs:label "Entity" .
|
||||
prov:Activity rdfs:label "Activity" .
|
||||
prov:Agent rdfs:label "Agent" .
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
**PROV-O谓词:**
|
||||
=======
|
||||
**PROV-O 谓词:**
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
```
|
||||
prov:wasDerivedFrom rdfs:label "was derived from" .
|
||||
prov:wasGeneratedBy rdfs:label "was generated by" .
|
||||
prov:used rdfs:label "used" .
|
||||
prov:wasAssociatedWith rdfs:label "was associated with" .
|
||||
prov:startedAtTime rdfs:label "started at" .
|
||||
```
|
||||
|
||||
**TrustGraph谓词:**
|
||||
```
|
||||
tg:contains rdfs:label "contains" .
|
||||
tg:pageCount rdfs:label "page count" .
|
||||
tg:mimeType rdfs:label "MIME type" .
|
||||
tg:pageNumber rdfs:label "page number" .
|
||||
tg:chunkIndex rdfs:label "chunk index" .
|
||||
tg:charOffset rdfs:label "character offset" .
|
||||
tg:charLength rdfs:label "character length" .
|
||||
tg:chunkSize rdfs:label "chunk size" .
|
||||
tg:chunkOverlap rdfs:label "chunk overlap" .
|
||||
tg:componentVersion rdfs:label "component version" .
|
||||
tg:llmModel rdfs:label "LLM model" .
|
||||
tg:ontology rdfs:label "ontology" .
|
||||
tg:embeddingModel rdfs:label "embedding model" .
|
||||
tg:sourceText rdfs:label "source text" .
|
||||
tg:sourceCharOffset rdfs:label "source character offset" .
|
||||
tg:sourceCharLength rdfs:label "source character length" .
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
**实施说明:** 这个词汇库初始化应该具有幂等性,即可以多次运行而不会创建重复项。 它可以触发在集合中的第一个文档处理过程中,或者作为单独的集合初始化步骤。
|
||||
|
||||
#### 子块来源信息(期望)
|
||||
|
||||
为了获得更细粒度的来源信息,记录三元组是从块中的哪个位置提取出来的将非常有用。 这可以实现:
|
||||
=======
|
||||
**实施说明:** 此词汇引导过程应具有幂等性,即可以多次运行而不会创建重复项。 可以在集合中的首次文档处理过程中触发,也可以作为单独的集合初始化步骤。
|
||||
|
||||
#### 子块来源信息(期望)
|
||||
|
||||
为了获得更精细的来源信息,记录三元组是从块中的哪个位置提取出来的将非常有用。 这可以实现:
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
在用户界面中突出显示确切的原始文本
|
||||
验证提取的准确性与原始文本
|
||||
在句子级别调试提取质量
|
||||
|
||||
**带有位置跟踪的示例:**
|
||||
```
|
||||
# The extracted triple
|
||||
entity:JohnSmith rel:worksAt entity:AcmeCorp .
|
||||
|
||||
# Subgraph with sub-chunk provenance
|
||||
subgraph:001 tg:contains <<entity:JohnSmith rel:worksAt entity:AcmeCorp>> .
|
||||
subgraph:001 prov:wasDerivedFrom chunk:123-1-1 .
|
||||
subgraph:001 tg:sourceText "John Smith has worked at Acme Corp since 2019" .
|
||||
subgraph:001 tg:sourceCharOffset 1547 .
|
||||
subgraph:001 tg:sourceCharLength 46 .
|
||||
```
|
||||
|
||||
**带有文本范围的示例(备选方案):**
|
||||
```
|
||||
subgraph:001 tg:contains <<entity:JohnSmith rel:worksAt entity:AcmeCorp>> .
|
||||
subgraph:001 prov:wasDerivedFrom chunk:123-1-1 .
|
||||
subgraph:001 tg:sourceRange "1547-1593" .
|
||||
subgraph:001 tg:sourceText "John Smith has worked at Acme Corp since 2019" .
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
**实现注意事项:**
|
||||
|
||||
基于 LLM 的提取可能无法自然地提供字符位置。
|
||||
可以提示 LLM 在提取的三元组旁边返回原始句子/短语。
|
||||
或者,可以进行后处理,将提取的实体模糊匹配回原始文本。
|
||||
提取复杂度和溯源粒度之间的权衡。
|
||||
使用结构化提取方法可能更容易实现,而不是使用自由形式的 LLM 提取。
|
||||
|
||||
这被标记为期望目标 - 首先应实现基本的块级溯源,如果可行,子块跟踪可以作为未来的增强功能。
|
||||
=======
|
||||
**实现注意事项:**
|
||||
|
||||
基于 LLM 的提取可能无法自然地提供字符位置
|
||||
可以提示 LLM 同时返回提取的三元组以及原始句子/短语
|
||||
或者,可以进行后处理,将提取的实体模糊匹配回原始文本
|
||||
提取复杂度和溯源粒度之间的权衡
|
||||
使用结构化提取方法可能更容易实现,而不是使用自由形式的 LLM 提取
|
||||
|
||||
这被标记为期望目标 - 首先应该实现基本的块级溯源,如果可行,子块跟踪可以作为未来的增强功能。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
### 双重存储模型
|
||||
|
||||
溯源 DAG 是随着文档在流水线中流动而逐步构建的:
|
||||
|
||||
| 存储 | 存储内容 | 目的 |
|
||||
|-------|---------------|---------|
|
||||
| Librarian | 文档内容 + 父子链接 | 内容检索、级联删除 |
|
||||
| Knowledge Graph | 父子边 + 元数据 | 溯源查询、事实归属 |
|
||||
|
||||
<<<<<<< HEAD
|
||||
两个存储都维护相同的 DAG 结构。 Librarian 存储内容;Graph 存储关系,并支持遍历查询。
|
||||
=======
|
||||
两个存储都维护相同的 DAG 结构。 Librarian 存储内容;Knowledge Graph 存储关系,并支持遍历查询。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
### 关键设计原则
|
||||
|
||||
1. **文档 ID 作为流动的单位** - 处理程序传递 ID,而不是内容。 需要时从 Librarian 检索内容。
|
||||
|
||||
<<<<<<< HEAD
|
||||
2. **在源头一次发出** - 元数据在处理开始时写入到 Graph 中,而不是在下游重复。
|
||||
|
||||
3. **一致的处理程序模式** - 每个处理程序都遵循相同的接收/检索/生成/保存/发出/转发模式。
|
||||
|
||||
4. **渐进的 DAG 构建** - 每个处理程序添加其级别到 DAG 中。 完整的溯源链是逐步构建的。
|
||||
|
||||
5. **分块后优化** - 在分块之后,消息同时携带 ID 和内容。 块很小(2-4KB),因此包含内容可以避免不必要的 Librarian 往返,同时通过 ID 保持溯源。
|
||||
=======
|
||||
2. **在源头一次发出** - 元数据在处理开始时写入 Knowledge Graph 一次,而不是在下游重复。
|
||||
|
||||
3. **一致的处理程序模式** - 每个处理程序都遵循相同的接收/检索/生成/保存/发出/转发模式。
|
||||
|
||||
4. **渐进式 DAG 构建** - 每个处理程序添加其级别到 DAG。 完整的溯源链是逐步构建的。
|
||||
|
||||
5. **分块后优化** - 在分块之后,消息同时携带 ID 和内容。 块很小(2-4KB),因此包含内容可以避免不必要的 Librarian 交互,同时通过 ID 保持溯源。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
## 实现任务
|
||||
|
||||
### Librarian 更改
|
||||
|
||||
#### 当前状态
|
||||
|
||||
<<<<<<< HEAD
|
||||
通过将文档 ID 发送到第一个处理程序来启动文档处理。
|
||||
没有连接到三元存储 - 元数据与提取输出一起打包。
|
||||
`add-child-document` 创建一级父子链接。
|
||||
`list-children` 仅返回直接子节点。
|
||||
=======
|
||||
通过将文档 ID 发送到第一个处理程序来启动文档处理
|
||||
没有连接到三元存储 - 元数据与提取输出一起打包
|
||||
`add-child-document` 创建一级父子链接
|
||||
`list-children` 仅返回直接子节点
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
#### 需要的更改
|
||||
|
||||
**1. 新接口:三元存储连接**
|
||||
|
||||
<<<<<<< HEAD
|
||||
Librarian 需要直接将文档元数据边发射到知识图谱,以在启动处理时进行操作。
|
||||
向 Librarian 服务添加三元存储客户端/发布器。
|
||||
在处理启动时:以图谱边的方式(一次)发射根文档元数据。
|
||||
=======
|
||||
Librarian 需要直接将文档元数据边发射到 Knowledge Graph,以在启动处理时。
|
||||
向 Librarian 服务添加三元存储客户端/发布器
|
||||
在处理启动时:以图边形式(一次)发射根文档元数据
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
**2. 文档类型词汇表**
|
||||
|
||||
标准化子文档的 `document_type` 值:
|
||||
<<<<<<< HEAD
|
||||
`source` - 原始上传的文档。
|
||||
`page` - 从源头提取的页面(PDF 等)。
|
||||
`chunk` - 从页面或源头派生的文本块。
|
||||
=======
|
||||
`source` - 原始上传的文档
|
||||
`page` - 从源文件(PDF 等)提取的页面
|
||||
`chunk` - 从页面或源文件派生的文本块
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
#### 接口更改摘要
|
||||
|
||||
| 接口 | 更改 |
|
||||
|-----------|--------|
|
||||
<<<<<<< HEAD
|
||||
| 三元存储 | 新的出站连接 - 发射文档元数据边 |
|
||||
| 处理启动 | 在转发文档 ID 之前,向图谱发射元数据 |
|
||||
=======
|
||||
| 三元存储 | 新的外部连接 - 发射文档元数据边 |
|
||||
| 处理启动 | 在转发文档 ID 之前,向图发射元数据 |
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
### PDF 提取器更改
|
||||
|
||||
#### 当前状态
|
||||
|
||||
<<<<<<< HEAD
|
||||
接收文档内容(或流式传输大型文档)。
|
||||
从 PDF 页面提取文本。
|
||||
将页面内容转发到分块器。
|
||||
没有与 Librarian 或三元存储交互。
|
||||
=======
|
||||
接收文档内容(或流式传输大型文档)
|
||||
从 PDF 页面提取文本
|
||||
将页面内容转发到分块器
|
||||
没有与 Librarian 或三元存储交互
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
#### 需要的更改
|
||||
|
||||
**1. 新接口:Librarian 客户端**
|
||||
|
||||
PDF 提取器需要将每个页面作为子文档保存到 Librarian 中。
|
||||
<<<<<<< HEAD
|
||||
向 PDF 提取器服务添加 Librarian 客户端。
|
||||
对于每个页面:使用父文档 ID 调用 `add-child-document`。
|
||||
|
||||
**2. 新接口:三元存储连接**
|
||||
|
||||
PDF 提取器需要将父子边发射到知识图谱。
|
||||
添加三元存储客户端/发布器。
|
||||
对于每个页面:发射一个链接页面文档到父文档的边。
|
||||
=======
|
||||
向 PDF 提取器服务添加 Librarian 客户端
|
||||
对于每个页面:使用父文档 ID 调用 `add-child-document`
|
||||
|
||||
**2. 新接口:三元存储连接**
|
||||
|
||||
PDF 提取器需要将父子边发射到 Knowledge Graph。
|
||||
添加三元存储客户端/发布器
|
||||
对于每个页面:发射将页面文档链接到父文档的边
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
**3. 更改输出格式**
|
||||
|
||||
而是直接转发页面内容,而是转发页面文档 ID。
|
||||
Chunker 将使用 ID 从 librarian 获取内容
|
||||
|
||||
#### 接口变更摘要
|
||||
|
||||
| 接口 | 变更 |
|
||||
|-----------|--------|
|
||||
| Librarian | 新的输出 - 保存子文档 |
|
||||
| Triple store | 新的输出 - 发射父子边 |
|
||||
| 输出消息 | 从内容更改为文档 ID |
|
||||
|
||||
### Chunker 变更
|
||||
|
||||
#### 当前状态
|
||||
|
||||
接收页面/文本内容
|
||||
分割成块
|
||||
将块内容转发到下游处理器
|
||||
不与 librarian 或 triple store 交互
|
||||
|
||||
#### 必需的变更
|
||||
|
||||
**1. 更改输入处理**
|
||||
|
||||
接收文档 ID 而不是内容,从 librarian 获取。
|
||||
向 chunker 服务添加 librarian 客户端
|
||||
使用文档 ID 获取页面内容
|
||||
|
||||
<<<<<<< HEAD
|
||||
**2. 新接口:Librarian 客户端(写入)**
|
||||
=======
|
||||
**2. 新接口:Librarian 客户端 (写入)**
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
将每个块保存为 librarian 中的子文档。
|
||||
对于每个块:使用 parent = 页面文档 ID 调用 `add-child-document`
|
||||
|
||||
**3. 新接口:Triple store 连接**
|
||||
|
||||
向知识图谱发射父子边。
|
||||
添加 triple store 客户端/发布器
|
||||
对于每个块:发射链接块文档到页面文档的边
|
||||
|
||||
**4. 更改输出格式**
|
||||
|
||||
同时转发块文档 ID 和块内容(块处理后的优化)。
|
||||
下游处理器接收 ID 用于溯源 + 用于处理的内容
|
||||
|
||||
#### 接口变更摘要
|
||||
|
||||
| 接口 | 变更 |
|
||||
|-----------|--------|
|
||||
| 输入消息 | 从内容更改为文档 ID |
|
||||
<<<<<<< HEAD
|
||||
| Librarian | 新的输出(读取 + 写入)- 获取内容,保存子文档 |
|
||||
=======
|
||||
| Librarian | 新的输出 (读取 + 写入) - 获取内容,保存子文档 |
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
| Triple store | 新的输出 - 发射父子边 |
|
||||
| 输出消息 | 从仅包含内容更改为 ID + 内容 |
|
||||
|
||||
### Knowledge Extractor 变更
|
||||
|
||||
#### 当前状态
|
||||
|
||||
接收块内容
|
||||
提取三元组和嵌入
|
||||
发送到 triple store 和 embedding store
|
||||
<<<<<<< HEAD
|
||||
`subjectOf` 关系指向顶级文档(不是块)
|
||||
=======
|
||||
`subjectOf` 关系指向顶级文档 (而不是块)
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
#### 必需的变更
|
||||
|
||||
**1. 更改输入处理**
|
||||
|
||||
接收块文档 ID 以及内容。
|
||||
<<<<<<< HEAD
|
||||
使用块 ID 用于溯源链接(内容已包含在优化中)
|
||||
|
||||
**2. 更新三元组溯源**
|
||||
|
||||
将提取的三元组链接到块(而不是顶级文档)。
|
||||
=======
|
||||
使用块 ID 用于溯源链接 (内容已包含在优化中)
|
||||
|
||||
**2. 更新三元组溯源**
|
||||
|
||||
将提取的三元组链接到块 (而不是顶级文档)。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
使用重构来创建指向边的边
|
||||
`subjectOf` 关系:三元组 → 块文档 ID
|
||||
首次使用现有的重构支持
|
||||
|
||||
**3. 更新嵌入溯源**
|
||||
|
||||
将嵌入实体 ID 链接到块。
|
||||
发射边:嵌入实体 ID → 块文档 ID
|
||||
|
||||
#### 接口变更摘要
|
||||
|
||||
| 接口 | 变更 |
|
||||
|-----------|--------|
|
||||
<<<<<<< HEAD
|
||||
| 输入消息 | 期望块 ID + 内容(不是仅包含内容) |
|
||||
=======
|
||||
| 输入消息 | 期望块 ID + 内容 (而不是仅包含内容) |
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
| Triple store | 使用重构进行三元组 → 块溯源 |
|
||||
| 嵌入溯源 | 将实体 ID 链接到块 ID |
|
||||
|
||||
## 引用
|
||||
|
||||
查询时溯源:`docs/tech-specs/query-time-provenance.md`
|
||||
PROV-O 标准用于溯源建模
|
||||
<<<<<<< HEAD
|
||||
知识图谱中现有的源元数据(需要审计)
|
||||
=======
|
||||
知识图谱中现有的源元数据 (需要审计)
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
317
docs/tech-specs/zh-cn/flow-class-definition.zh-cn.md
Normal file
317
docs/tech-specs/zh-cn/flow-class-definition.zh-cn.md
Normal file
|
|
@ -0,0 +1,317 @@
|
|||
---
|
||||
layout: default
|
||||
title: "流程蓝图定义规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
<<<<<<< HEAD
|
||||
# 流程蓝图定义规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
流程蓝图在 TrustGraph 系统中定义了一个完整的数据流模式模板。当实例化时,它会创建一个相互连接的处理单元网络,该网络处理数据摄取、处理、存储和查询,作为一个统一的系统。
|
||||
|
||||
## 结构
|
||||
|
||||
流程蓝图定义由五个主要部分组成:
|
||||
|
||||
### 1. 类部分
|
||||
定义共享服务处理器,这些处理器每个流程蓝图实例化一次。这些处理器处理来自此类的所有流程实例的请求。
|
||||
=======
|
||||
# 流工作蓝图定义规范
|
||||
|
||||
## 概述
|
||||
|
||||
一个流工作蓝图定义了 TrustGraph 系统中完整的数据流模式模板。当实例化时,它会创建一个相互连接的处理单元网络,该网络处理数据摄取、处理、存储和查询,作为一个统一的系统。
|
||||
|
||||
## 结构
|
||||
|
||||
一个流工作蓝图定义由五个主要部分组成:
|
||||
|
||||
### 1. 类部分
|
||||
定义共享服务处理器,这些处理器每个流工作蓝图实例化一次。这些处理器处理来自此类的所有流实例的请求。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
```json
|
||||
"class": {
|
||||
"service-name:{class}": {
|
||||
"request": "queue-pattern:{class}",
|
||||
"response": "queue-pattern:{class}",
|
||||
"settings": {
|
||||
"setting-name": "fixed-value",
|
||||
"parameterized-setting": "{parameter-name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
**特性:**
|
||||
=======
|
||||
**特点:**
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
在同一类别的所有流程实例中共享。
|
||||
通常是昂贵或无状态的服务(例如:大型语言模型、嵌入模型)。
|
||||
使用 `{class}` 模板变量进行队列命名。
|
||||
设置可以是固定值,也可以使用 `{parameter-name}` 语法进行参数化。
|
||||
示例:`embeddings:{class}`, `text-completion:{class}`, `graph-rag:{class}`
|
||||
|
||||
### 2. 流程部分
|
||||
定义与流程相关的处理器,这些处理器为每个独立的流程实例进行实例化。 每个流程都拥有自己的一组隔离的处理器。
|
||||
|
||||
```json
|
||||
"flow": {
|
||||
"processor-name:{id}": {
|
||||
"input": "queue-pattern:{id}",
|
||||
"output": "queue-pattern:{id}",
|
||||
"settings": {
|
||||
"setting-name": "fixed-value",
|
||||
"parameterized-setting": "{parameter-name}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**特性:**
|
||||
每个流程只有一个实例。
|
||||
处理流程特定的数据和状态。
|
||||
使用 `{id}` 模板变量进行队列命名。
|
||||
设置可以是固定值,也可以使用 `{parameter-name}` 语法进行参数化。
|
||||
示例:`chunker:{id}`, `pdf-decoder:{id}`, `kg-extract-relationships:{id}`
|
||||
|
||||
### 3. 接口部分
|
||||
定义流程的入口点和交互协议。 这些构成了外部系统和内部组件通信的 API 表面。
|
||||
|
||||
<<<<<<< HEAD
|
||||
接口可以有两种形式:
|
||||
|
||||
**即发即弃模式**(单个队列):
|
||||
=======
|
||||
接口可以采用两种形式:
|
||||
|
||||
**发布-订阅模式**(单个队列):
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
```json
|
||||
"interfaces": {
|
||||
"document-load": "persistent://tg/flow/document-load:{id}",
|
||||
"triples-store": "persistent://tg/flow/triples-store:{id}"
|
||||
}
|
||||
```
|
||||
|
||||
**请求/响应模式** (包含请求/响应字段的对象):
|
||||
```json
|
||||
"interfaces": {
|
||||
"embeddings": {
|
||||
"request": "non-persistent://tg/request/embeddings:{class}",
|
||||
"response": "non-persistent://tg/response/embeddings:{class}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**接口类型:**
|
||||
**入口点:** 外部系统注入数据的入口点 (`document-load`, `agent`)
|
||||
**服务接口:** 服务请求/响应模式 (`embeddings`, `text-completion`)
|
||||
**数据接口:** 触发/完成式数据流连接点 (`triples-store`, `entity-contexts-load`)
|
||||
|
||||
### 4. 参数部分
|
||||
将流程特定的参数名称映射到集中存储的参数定义:
|
||||
|
||||
```json
|
||||
"parameters": {
|
||||
"model": "llm-model",
|
||||
"temp": "temperature",
|
||||
"chunk": "chunk-size"
|
||||
}
|
||||
```
|
||||
|
||||
**特性:**
|
||||
键是处理器设置中使用的参数名称(例如,`{model}`)
|
||||
值引用存储在 schema/config 中的参数定义
|
||||
允许在流程之间重用常见的参数定义
|
||||
减少参数模式的重复
|
||||
|
||||
### 5. 元数据
|
||||
关于流程蓝图的附加信息:
|
||||
|
||||
```json
|
||||
"description": "Human-readable description",
|
||||
"tags": ["capability-1", "capability-2"]
|
||||
```
|
||||
|
||||
## 模板变量
|
||||
|
||||
### 系统变量
|
||||
|
||||
#### {id}
|
||||
被替换为唯一的流程实例标识符
|
||||
为每个流程创建隔离的资源
|
||||
示例:`flow-123`, `customer-A-flow`
|
||||
|
||||
#### {class}
|
||||
被替换为流程蓝图名称
|
||||
为相同类别的流程创建共享资源
|
||||
示例:`standard-rag`, `enterprise-rag`
|
||||
|
||||
### 参数变量
|
||||
|
||||
#### {parameter-name}
|
||||
在流程启动时定义的自定义参数
|
||||
参数名称与流程的 `parameters` 部分中的键匹配
|
||||
用于在处理器设置中自定义行为
|
||||
示例:`{model}`, `{temp}`, `{chunk}`
|
||||
被替换为启动流程时提供的值
|
||||
验证通过与集中存储的参数定义进行比较
|
||||
|
||||
## 处理器设置
|
||||
|
||||
设置在实例化时向处理器提供配置值。 它们可以是:
|
||||
|
||||
### 静态设置
|
||||
直接值,不会改变:
|
||||
```json
|
||||
"settings": {
|
||||
"model": "gemma3:12b",
|
||||
"temperature": 0.7,
|
||||
"max_retries": 3
|
||||
}
|
||||
```
|
||||
|
||||
### 参数化设置
|
||||
使用在流程启动时提供的参数的值:
|
||||
```json
|
||||
"settings": {
|
||||
"model": "{model}",
|
||||
"temperature": "{temp}",
|
||||
"endpoint": "https://{region}.api.example.com"
|
||||
}
|
||||
```
|
||||
|
||||
参数名称在设置中对应于流程的 `parameters` 部分中的键。
|
||||
|
||||
### 设置示例
|
||||
|
||||
**带有参数的 LLM 处理器:**
|
||||
```json
|
||||
// In parameters section:
|
||||
"parameters": {
|
||||
"model": "llm-model",
|
||||
"temp": "temperature",
|
||||
"tokens": "max-tokens",
|
||||
"key": "openai-api-key"
|
||||
}
|
||||
|
||||
// In processor definition:
|
||||
"text-completion:{class}": {
|
||||
"request": "non-persistent://tg/request/text-completion:{class}",
|
||||
"response": "non-persistent://tg/response/text-completion:{class}",
|
||||
"settings": {
|
||||
"model": "{model}",
|
||||
"temperature": "{temp}",
|
||||
"max_tokens": "{tokens}",
|
||||
"api_key": "{key}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**具有固定和参数化设置的分块器:**
|
||||
```json
|
||||
// In parameters section:
|
||||
"parameters": {
|
||||
"chunk": "chunk-size"
|
||||
}
|
||||
|
||||
// In processor definition:
|
||||
"chunker:{id}": {
|
||||
"input": "persistent://tg/flow/chunk:{id}",
|
||||
"output": "persistent://tg/flow/chunk-load:{id}",
|
||||
"settings": {
|
||||
"chunk_size": "{chunk}",
|
||||
"chunk_overlap": 100,
|
||||
"encoding": "utf-8"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 队列模式 (Pulsar)
|
||||
|
||||
流程蓝图使用 Apache Pulsar 进行消息传递。队列名称遵循 Pulsar 格式:
|
||||
```
|
||||
<persistence>://<tenant>/<namespace>/<topic>
|
||||
```
|
||||
|
||||
### 组件:
|
||||
**持久性**: `persistent` 或 `non-persistent` (Pulsar 持久性模式)
|
||||
**租户**: `tg` 用于 TrustGraph 提供的流程蓝图定义
|
||||
**命名空间**: 表示消息模式
|
||||
`flow`: 适用于“发送即忘”服务
|
||||
`request`: 适用于请求/响应服务的请求部分
|
||||
`response`: 适用于请求/响应服务的响应部分
|
||||
**主题**: 具有模板变量的特定队列/主题名称
|
||||
|
||||
### 持久队列
|
||||
模式: `persistent://tg/flow/<topic>:{id}`
|
||||
用于“发送即忘”服务和持久数据流
|
||||
数据在 Pulsar 存储中持久化,即使在重启后也保留
|
||||
示例: `persistent://tg/flow/chunk-load:{id}`
|
||||
|
||||
### 非持久队列
|
||||
模式: `non-persistent://tg/request/<topic>:{class}` 或 `non-persistent://tg/response/<topic>:{class}`
|
||||
用于请求/响应消息模式
|
||||
瞬态的,不由 Pulsar 持久化到磁盘
|
||||
延迟较低,适用于 RPC 风格的通信
|
||||
示例: `non-persistent://tg/request/embeddings:{class}`
|
||||
|
||||
## 数据流架构
|
||||
|
||||
流程蓝图创建统一的数据流,其中:
|
||||
|
||||
<<<<<<< HEAD
|
||||
1. **文档处理管道**: 从摄取到转换再到存储的流程
|
||||
2. **查询服务**: 集成的处理器,查询相同的存储和服务
|
||||
3. **共享服务**: 所有流程都可以使用的集中式处理器
|
||||
4. **存储写入器**: 将处理后的数据持久化到适当的存储中
|
||||
=======
|
||||
1. **文档处理管道**: 从摄取到转换再到存储
|
||||
2. **查询服务**: 集成的处理器,查询相同的存储和服务
|
||||
3. **共享服务**: 所有流程都可以使用的集中处理器
|
||||
4. **存储写入器**: 将处理后的数据持久化到适当的存储
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
所有处理器(包括 `{id}` 和 `{class}`)协同工作,形成一个连贯的数据流图,而不是作为独立的系统。
|
||||
|
||||
## 流程实例化示例
|
||||
|
||||
假设:
|
||||
流程实例 ID: `customer-A-flow`
|
||||
流程蓝图: `standard-rag`
|
||||
流程参数映射:
|
||||
`"model": "llm-model"`
|
||||
`"temp": "temperature"`
|
||||
`"chunk": "chunk-size"`
|
||||
用户提供的参数:
|
||||
`model`: `gpt-4`
|
||||
`temp`: `0.5`
|
||||
`chunk`: `512`
|
||||
|
||||
模板扩展:
|
||||
`persistent://tg/flow/chunk-load:{id}` → `persistent://tg/flow/chunk-load:customer-A-flow`
|
||||
`non-persistent://tg/request/embeddings:{class}` → `non-persistent://tg/request/embeddings:standard-rag`
|
||||
`"model": "{model}"` → `"model": "gpt-4"`
|
||||
`"temperature": "{temp}"` → `"temperature": "0.5"`
|
||||
`"chunk_size": "{chunk}"` → `"chunk_size": "512"`
|
||||
|
||||
这会创建:
|
||||
用于 `customer-A-flow` 的隔离文档处理管道
|
||||
用于所有 `standard-rag` 流程的共享嵌入服务
|
||||
从文档摄取到查询的完整数据流
|
||||
使用提供的参数值配置的处理器
|
||||
|
||||
## 优点
|
||||
|
||||
1. **资源效率**: 昂贵的服务在流程之间共享
|
||||
2. **流程隔离**: 每个流程都有自己的数据处理管道
|
||||
3. **可扩展性**: 可以从相同的模板实例化多个流程
|
||||
4. **模块化**: 共享组件和流程特定组件之间有明确的分离
|
||||
5. **统一架构**: 查询和处理是相同的数据流的一部分
|
||||
586
docs/tech-specs/zh-cn/flow-configurable-parameters.zh-cn.md
Normal file
586
docs/tech-specs/zh-cn/flow-configurable-parameters.zh-cn.md
Normal file
|
|
@ -0,0 +1,586 @@
|
|||
---
|
||||
layout: default
|
||||
title: "Flow Blueprint Configurable Parameters Technical Specification"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# Flow Blueprint Configurable Parameters Technical Specification
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## Overview
|
||||
|
||||
本规范描述了 TrustGraph 中可配置参数在流程蓝图中的实现方式。参数允许用户在流程启动时自定义处理器参数,通过提供用于替换流程蓝图定义中参数占位符的值来实现。
|
||||
|
||||
<<<<<<< HEAD
|
||||
参数通过处理器参数中的模板变量替换来实现,类似于 `{id}` 和 `{class}` 变量的工作方式,但使用用户提供的值。
|
||||
|
||||
该集成支持四种主要用例:
|
||||
|
||||
1. **模型选择**: 允许用户选择不同的 LLM 模型(例如,`gemma3:8b`、`gpt-4`、`claude-3`)用于处理器。
|
||||
=======
|
||||
参数通过处理器参数中的模板变量替换来工作,类似于 `{id}` 和 `{class}` 变量的工作方式,但使用用户提供的值。
|
||||
|
||||
该集成支持四种主要用例:
|
||||
|
||||
1. **模型选择**: 允许用户选择不同的 LLM 模型 (例如,`gemma3:8b`, `gpt-4`, `claude-3`) 用于处理器。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
2. **资源配置**: 调整处理器参数,例如块大小、批处理大小和并发限制。
|
||||
3. **行为调整**: 通过参数修改处理器行为,例如温度、最大 token 数或检索阈值。
|
||||
4. **环境特定参数**: 配置每个部署的环境端点、API 密钥或区域特定 URL。
|
||||
|
||||
## 目标
|
||||
|
||||
**动态处理器配置**: 通过参数替换启用处理器参数的运行时配置。
|
||||
**参数验证**: 在流程启动时提供参数的类型检查和验证。
|
||||
<<<<<<< HEAD
|
||||
**默认值**: 提供合理的默认值,同时允许高级用户进行覆盖。
|
||||
=======
|
||||
**默认值**: 支持合理的默认值,同时允许高级用户进行覆盖。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
**模板替换**: Seamlessly 替换处理器参数中的参数占位符。
|
||||
**UI 集成**: 通过 API 和 UI 接口提供参数输入。
|
||||
**类型安全**: 确保参数类型与预期的处理器参数类型匹配。
|
||||
**文档**: 在流程蓝图定义中提供自文档化的参数模式。
|
||||
**向后兼容性**: 保持与不使用参数的现有流程蓝图的兼容性。
|
||||
|
||||
## 背景
|
||||
|
||||
<<<<<<< HEAD
|
||||
TrustGraph 中的流程蓝图现在支持处理器参数,这些参数可以包含固定值或参数占位符。这为运行时自定义提供了机会。
|
||||
=======
|
||||
TrustGraph 中的流程蓝图现在支持处理器参数,这些参数可以包含固定值或参数占位符。 这为运行时自定义提供了机会。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
当前处理器参数支持:
|
||||
固定值:`"model": "gemma3:12b"`
|
||||
参数占位符:`"model": "gemma3:{model-size}"`
|
||||
|
||||
本规范定义了参数的:
|
||||
<<<<<<< HEAD
|
||||
在流程蓝图定义中的声明方式
|
||||
流程启动时的验证方式
|
||||
在处理器参数中的替换方式
|
||||
通过 API 和 UI 的暴露方式
|
||||
=======
|
||||
在流程蓝图定义中的声明
|
||||
在流程启动时的验证
|
||||
在处理器参数中的替换
|
||||
通过 API 和 UI 的暴露
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
通过利用参数化的处理器参数,TrustGraph 可以:
|
||||
通过使用参数进行变体,减少流程蓝图的重复。
|
||||
允许用户在不修改定义的情况下调整处理器行为。
|
||||
通过参数值支持环境特定的配置。
|
||||
通过参数模式验证确保类型安全。
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 架构
|
||||
|
||||
可配置参数系统需要以下技术组件:
|
||||
|
||||
1. **参数模式定义**
|
||||
基于 JSON Schema 的参数定义,位于流程蓝图元数据中。
|
||||
类型定义,包括字符串、数字、布尔值、枚举和对象类型。
|
||||
验证规则,包括最小值/最大值、模式和必填字段。
|
||||
|
||||
模块:trustgraph-flow/trustgraph/flow/definition.py
|
||||
|
||||
2. **参数解析引擎**
|
||||
对模式进行运行时参数验证。
|
||||
为未指定的参数应用默认值。
|
||||
将参数注入到流程执行上下文。
|
||||
如有必要进行类型转换和转换。
|
||||
|
||||
模块:trustgraph-flow/trustgraph/flow/parameter_resolver.py
|
||||
|
||||
3. **参数存储集成**
|
||||
从模式/配置存储中检索参数定义。
|
||||
缓存常用的参数定义。
|
||||
<<<<<<< HEAD
|
||||
对其进行验证,以确保其与中心存储的模式一致。
|
||||
=======
|
||||
对其进行集中存储的模式验证。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
模块:trustgraph-flow/trustgraph/flow/parameter_store.py
|
||||
|
||||
4. **流程启动器扩展**
|
||||
API 扩展,用于在流程启动期间接受参数值。
|
||||
<<<<<<< HEAD
|
||||
参数映射解析(将流程名称映射到定义名称)。
|
||||
=======
|
||||
参数映射解析 (将流程名称映射到定义名称)。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
处理无效参数组合的错误。
|
||||
|
||||
模块:trustgraph-flow/trustgraph/flow/launcher.py
|
||||
|
||||
5. **UI 参数表单**
|
||||
从流程参数元数据动态生成表单。
|
||||
<<<<<<< HEAD
|
||||
使用 `order` 字段显示参数的顺序。
|
||||
使用 `description` 字段提供参数的描述性标签。
|
||||
=======
|
||||
使用 `order` 字段显示参数顺序。
|
||||
使用 `description` 字段提供描述性参数标签。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
根据参数类型定义进行输入验证。
|
||||
参数预设和模板。
|
||||
|
||||
模块:trustgraph-ui/components/flow-parameters/
|
||||
|
||||
### 数据模型
|
||||
|
||||
<<<<<<< HEAD
|
||||
#### 参数定义(存储在模式/配置中)
|
||||
|
||||
参数定义以类型为 "parameter-type" 的方式存储在模式和配置系统中。
|
||||
=======
|
||||
#### 参数定义 (存储在模式/配置中)
|
||||
|
||||
参数定义以类型 "parameter-type" 存储在模式和配置系统中。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
```json
|
||||
{
|
||||
"llm-model": {
|
||||
"type": "string",
|
||||
"description": "LLM model to use",
|
||||
"default": "gpt-4",
|
||||
"enum": [
|
||||
{
|
||||
"id": "gpt-4",
|
||||
"description": "OpenAI GPT-4 (Most Capable)"
|
||||
},
|
||||
{
|
||||
"id": "gpt-3.5-turbo",
|
||||
"description": "OpenAI GPT-3.5 Turbo (Fast & Efficient)"
|
||||
},
|
||||
{
|
||||
"id": "claude-3",
|
||||
"description": "Anthropic Claude 3 (Thoughtful & Safe)"
|
||||
},
|
||||
{
|
||||
"id": "gemma3:8b",
|
||||
"description": "Google Gemma 3 8B (Open Source)"
|
||||
}
|
||||
],
|
||||
"required": false
|
||||
},
|
||||
"model-size": {
|
||||
"type": "string",
|
||||
"description": "Model size variant",
|
||||
"default": "8b",
|
||||
"enum": ["2b", "8b", "12b", "70b"],
|
||||
"required": false
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"description": "Model temperature for generation",
|
||||
"default": 0.7,
|
||||
"minimum": 0.0,
|
||||
"maximum": 2.0,
|
||||
"required": false
|
||||
},
|
||||
"chunk-size": {
|
||||
"type": "integer",
|
||||
"description": "Document chunk size",
|
||||
"default": 512,
|
||||
"minimum": 128,
|
||||
"maximum": 2048,
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 带有参数引用的流程蓝图
|
||||
|
||||
流程蓝图定义了参数元数据,包括类型引用、描述和排序:
|
||||
|
||||
```json
|
||||
{
|
||||
"flow_class": "document-analysis",
|
||||
"parameters": {
|
||||
"llm-model": {
|
||||
"type": "llm-model",
|
||||
"description": "Primary LLM model for text completion",
|
||||
"order": 1
|
||||
},
|
||||
"llm-rag-model": {
|
||||
"type": "llm-model",
|
||||
"description": "LLM model for RAG operations",
|
||||
"order": 2,
|
||||
"advanced": true,
|
||||
"controlled-by": "llm-model"
|
||||
},
|
||||
"llm-temperature": {
|
||||
"type": "temperature",
|
||||
"description": "Generation temperature for creativity control",
|
||||
"order": 3,
|
||||
"advanced": true
|
||||
},
|
||||
"chunk-size": {
|
||||
"type": "chunk-size",
|
||||
"description": "Document chunk size for processing",
|
||||
"order": 4,
|
||||
"advanced": true
|
||||
},
|
||||
"chunk-overlap": {
|
||||
"type": "integer",
|
||||
"description": "Overlap between document chunks",
|
||||
"order": 5,
|
||||
"advanced": true,
|
||||
"controlled-by": "chunk-size"
|
||||
}
|
||||
},
|
||||
"class": {
|
||||
"text-completion:{class}": {
|
||||
"request": "non-persistent://tg/request/text-completion:{class}",
|
||||
"response": "non-persistent://tg/response/text-completion:{class}",
|
||||
"parameters": {
|
||||
"model": "{llm-model}",
|
||||
"temperature": "{llm-temperature}"
|
||||
}
|
||||
},
|
||||
"rag-completion:{class}": {
|
||||
"request": "non-persistent://tg/request/rag-completion:{class}",
|
||||
"response": "non-persistent://tg/response/rag-completion:{class}",
|
||||
"parameters": {
|
||||
"model": "{llm-rag-model}",
|
||||
"temperature": "{llm-temperature}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"flow": {
|
||||
"chunker:{id}": {
|
||||
"input": "persistent://tg/flow/chunk:{id}",
|
||||
"output": "persistent://tg/flow/chunk-load:{id}",
|
||||
"parameters": {
|
||||
"chunk_size": "{chunk-size}",
|
||||
"chunk_overlap": "{chunk-overlap}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`parameters` 部分将流程特定的参数名称(键)映射到包含以下内容的参数元数据对象:
|
||||
`type`:对中心定义的参数定义的引用(例如,“llm-model”)
|
||||
`description`:用于UI显示的易于理解的描述
|
||||
`order`:参数表单的显示顺序(较小的数字首先显示)
|
||||
`advanced`(可选):布尔标志,指示是否为高级参数(默认:false)。如果设置为true,UI可能会默认隐藏此参数或将其放置在“高级”部分
|
||||
<<<<<<< HEAD
|
||||
`controlled-by`(可选):控制此参数在简单模式下值的另一个参数的名称。如果指定,此参数将从控制参数继承其值,除非显式覆盖
|
||||
=======
|
||||
`controlled-by`(可选):控制此参数在简单模式下值的另一个参数的名称。如果指定,此参数将继承其值来自控制参数,除非显式覆盖
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
这种方法允许:
|
||||
在多个流程蓝图之间重用参数类型定义
|
||||
集中管理和验证参数类型
|
||||
流程特定的参数描述和排序
|
||||
通过描述性的参数表单增强UI体验
|
||||
流程中参数验证的一致性
|
||||
轻松添加新的标准参数类型
|
||||
通过基本/高级模式分离简化UI
|
||||
相关设置的参数值继承
|
||||
|
||||
#### 流程启动请求
|
||||
|
||||
流程启动API使用流程的参数名称来接受参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"flow_class": "document-analysis",
|
||||
"flow_id": "customer-A-flow",
|
||||
"parameters": {
|
||||
"llm-model": "claude-3",
|
||||
"llm-temperature": 0.5,
|
||||
"chunk-size": 1024
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
注意:在这个例子中,`llm-rag-model` 没有明确提供,但会从 `llm-model` 继承值 "claude-3",这是因为 `llm-rag-model` 与 `llm-model` 之间存在 `controlled-by` 关系。 类似地,`chunk-overlap` 可能会继承一个基于 `chunk-size` 计算的值。
|
||||
=======
|
||||
注意:在这个例子中,`llm-rag-model` 没有显式提供,但会从 `llm-model` 继承 "claude-3" 的值,这是因为 `llm-rag-model` 与 `llm-model` 之间存在 `controlled-by` 关系。 类似地,`chunk-overlap` 可能会继承一个基于 `chunk-size` 计算的值。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
系统将执行以下操作:
|
||||
1. 从流程蓝图定义中提取参数元数据
|
||||
2. 将流程参数名称映射到其类型定义(例如,`llm-model` → `llm-model` 类型)
|
||||
3. 解析受控关系(例如,`llm-rag-model` 从 `llm-model` 继承)
|
||||
4. 验证用户提供的和继承的值是否符合参数类型定义
|
||||
5. 在流程实例化期间,将解析的值替换到处理器参数中
|
||||
|
||||
### 实现细节
|
||||
|
||||
#### 参数解析过程
|
||||
|
||||
当启动流程时,系统执行以下参数解析步骤:
|
||||
|
||||
1. **流程蓝图加载**: 加载流程蓝图定义并提取参数元数据
|
||||
2. **元数据提取**: 提取每个参数的 `type`、`description`、`order`、`advanced` 和 `controlled-by`,这些信息位于流程蓝图的 `parameters` 部分
|
||||
3. **类型定义查找**: 对于流程蓝图中的每个参数:
|
||||
<<<<<<< HEAD
|
||||
使用 `type` 字段从 schema/config 存储中检索参数类型定义
|
||||
类型定义存储在配置系统中,类型为 "parameter-type"
|
||||
每个类型定义包含参数的 schema、默认值和验证规则
|
||||
=======
|
||||
使用 `type` 字段从模式/配置存储中检索参数类型定义
|
||||
类型定义存储在配置系统中,类型为 "parameter-type"
|
||||
每个类型定义包含参数的模式、默认值和验证规则
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
4. **默认值解析**:
|
||||
对于流程蓝图中定义的每个参数:
|
||||
检查用户是否为该参数提供了值
|
||||
如果未提供用户值,则使用参数类型定义中的 `default` 值
|
||||
构建一个完整的参数映射,其中包含用户提供的和默认值
|
||||
5. **参数继承解析**(受控关系):
|
||||
对于具有 `controlled-by` 字段的参数,检查是否已显式提供值
|
||||
如果未提供显式值,则从控制参数继承该值
|
||||
如果控制参数也无值,则从类型定义中获取默认值
|
||||
验证 `controlled-by` 关系中是否存在循环依赖
|
||||
6. **验证**: 验证完整的参数集(用户提供的、默认值和继承的值)是否符合类型定义
|
||||
7. **存储**: 将完整的解析后的参数集与流程实例一起存储,以进行审计
|
||||
<<<<<<< HEAD
|
||||
8. **模板替换**: 使用解析后的值替换处理器参数中的参数占位符
|
||||
=======
|
||||
8. **模板替换**: 使用解析的值替换处理器参数中的参数占位符
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
9. **处理器实例化**: 使用替换后的参数创建处理器
|
||||
|
||||
**重要的实现说明:**
|
||||
流程服务必须将用户提供的参数与参数类型定义中的默认值合并
|
||||
完整的参数集(包括应用的默认值)必须与流程一起存储,以进行可追溯性
|
||||
<<<<<<< HEAD
|
||||
参数解析发生在流程启动时间,而不是处理器实例化时间
|
||||
=======
|
||||
参数解析发生在流程启动时,而不是在处理器实例化时
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
缺少没有默认值的必需参数会导致流程启动失败,并显示清晰的错误消息
|
||||
|
||||
#### 具有 controlled-by 的参数继承
|
||||
|
||||
`controlled-by` 字段启用参数值继承,这对于简化用户界面同时保持灵活性非常有用:
|
||||
|
||||
**示例场景:**
|
||||
`llm-model` 参数控制主要的 LLM 模型
|
||||
`llm-rag-model` 参数具有 `"controlled-by": "llm-model"`
|
||||
在简单模式下,将 `llm-model` 设置为 "gpt-4" 会自动将 `llm-rag-model` 也设置为 "gpt-4"
|
||||
在高级模式下,用户可以覆盖 `llm-rag-model` 并使用不同的值
|
||||
|
||||
**解析规则:**
|
||||
1. 如果参数具有显式提供的值,则使用该值
|
||||
2. 如果没有显式值且 `controlled-by` 已设置,则使用控制参数的值
|
||||
3. 如果控制参数没有值,则回退到类型定义中的默认值
|
||||
4. `controlled-by` 关系中的循环依赖会导致验证错误
|
||||
|
||||
**UI 行为:**
|
||||
在基本/简单模式下:具有 `controlled-by` 的参数可能被隐藏或显示为只读,并显示继承的值
|
||||
在高级模式下:显示所有参数,并且可以单独配置
|
||||
当控制参数更改时,依赖参数会自动更新,除非显式覆盖
|
||||
|
||||
#### Pulsar 集成
|
||||
|
||||
1. **启动流程操作**
|
||||
Pulsar 启动流程操作需要接受一个 `parameters` 字段,该字段包含参数值的映射
|
||||
<<<<<<< HEAD
|
||||
Pulsar 启动流程请求的 schema 必须更新为包含可选的 `parameters` 字段
|
||||
=======
|
||||
Pulsar 用于启动流程的请求模式必须更新为包含可选的 `parameters` 字段
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
示例请求:
|
||||
```json
|
||||
{
|
||||
"flow_class": "document-analysis",
|
||||
"flow_id": "customer-A-flow",
|
||||
"parameters": {
|
||||
"model": "claude-3",
|
||||
"size": "12b",
|
||||
"temp": 0.5,
|
||||
"chunk": 1024
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **获取流程操作**
|
||||
<<<<<<< HEAD
|
||||
必须更新 Pulsar 模式,以包含 `parameters` 字段,用于获取流程的响应。
|
||||
=======
|
||||
Pulsar 用于获取流程响应的 schema 必须更新,以包含 `parameters` 字段。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
这允许客户端检索在启动流程时使用的参数值。
|
||||
示例响应:
|
||||
```json
|
||||
{
|
||||
"flow_id": "customer-A-flow",
|
||||
"flow_class": "document-analysis",
|
||||
"status": "running",
|
||||
"parameters": {
|
||||
"model": "claude-3",
|
||||
"size": "12b",
|
||||
"temp": 0.5,
|
||||
"chunk": 1024
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 流程服务实现
|
||||
|
||||
流程配置服务 (`trustgraph-flow/trustgraph/config/service/flow.py`) 需要以下增强:
|
||||
|
||||
1. **参数解析功能**
|
||||
```python
|
||||
async def resolve_parameters(self, flow_class, user_params):
|
||||
"""
|
||||
Resolve parameters by merging user-provided values with defaults.
|
||||
|
||||
Args:
|
||||
flow_class: The flow blueprint definition dict
|
||||
user_params: User-provided parameters dict
|
||||
|
||||
Returns:
|
||||
Complete parameter dict with user values and defaults merged
|
||||
"""
|
||||
```
|
||||
|
||||
此函数应该:
|
||||
从流程蓝图的 `parameters` 部分提取参数元数据
|
||||
对于每个参数,从配置存储中获取其类型定义
|
||||
为任何未由用户提供的参数应用默认值
|
||||
处理 `controlled-by` 继承关系
|
||||
返回完整的参数集
|
||||
|
||||
2. **修改后的 `handle_start_flow` 方法**
|
||||
在加载流程蓝图后调用 `resolve_parameters`
|
||||
使用完整的解析后的参数集进行模板替换
|
||||
将完整的参数集(不仅仅是用户提供的)与流程一起存储
|
||||
验证所有必需的参数是否具有值
|
||||
|
||||
3. **参数类型获取**
|
||||
参数类型定义存储在配置中,类型为 "parameter-type"
|
||||
每个类型定义包含模式、默认值和验证规则
|
||||
缓存常用的参数类型以减少配置查找
|
||||
|
||||
#### 配置系统集成
|
||||
|
||||
3. **流程对象存储**
|
||||
当流程组件在配置管理器中向配置系统添加流程时,流程对象必须包含解析后的参数值
|
||||
<<<<<<< HEAD
|
||||
配置管理器需要同时存储原始的用户提供的参数和解析后的值(已应用默认值)
|
||||
=======
|
||||
配置管理器需要存储原始的用户提供的参数以及解析后的值(已应用默认值)
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
配置系统中的流程对象应包含:
|
||||
`parameters`: 用于流程的最终解析后的参数值
|
||||
|
||||
#### CLI 集成
|
||||
|
||||
4. **库 CLI 命令**
|
||||
启动流程的 CLI 命令需要参数支持:
|
||||
通过命令行标志或配置文件接受参数值
|
||||
在提交之前,根据流程蓝图定义验证参数
|
||||
支持参数文件输入(JSON/YAML),用于复杂的参数集
|
||||
|
||||
显示流程的 CLI 命令需要显示参数信息:
|
||||
显示启动流程时使用的参数值
|
||||
显示流程蓝图的可用参数
|
||||
显示参数验证模式和默认值
|
||||
|
||||
#### 处理器基础类集成
|
||||
|
||||
5. **ParameterSpec 支持**
|
||||
处理器基础类需要支持通过现有的 ParametersSpec 机制进行参数替换
|
||||
如果需要,应增强 ParametersSpec 类(位于与 ConsumerSpec 和 ProducerSpec 相同的模块中),以支持参数模板替换
|
||||
处理器应能够调用 ParametersSpec 来使用在流程启动时解析的参数值配置其参数
|
||||
ParametersSpec 的实现需要:
|
||||
接受包含参数占位符(例如,`{model}`,`{temperature}`)的参数配置
|
||||
在实例化处理器时,支持运行时参数替换
|
||||
验证替换后的值是否符合预期的类型和约束
|
||||
为缺失或无效的参数引用提供错误处理
|
||||
|
||||
#### 替换规则
|
||||
|
||||
参数使用格式 `{parameter-name}` 在处理器参数中
|
||||
<<<<<<< HEAD
|
||||
参数名称与流程的 `parameters` 部分中的键匹配
|
||||
=======
|
||||
参数名称在参数中与流程的 `parameters` 部分中的键匹配
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
替换操作与 `{id}` 和 `{class}` 替换同时进行
|
||||
无效的参数引用会导致启动时出错
|
||||
基于中心存储的参数定义进行类型验证
|
||||
**重要提示**:所有参数值都以字符串形式存储和传输
|
||||
数字转换为字符串(例如,`0.7` 变为 `"0.7"`)
|
||||
布尔值转换为小写字符串(例如,`true` 变为 `"true"`)
|
||||
这是由 Pulsar 模式要求的,该模式定义了 `parameters = Map(String())`
|
||||
|
||||
示例解析:
|
||||
```
|
||||
Flow parameter mapping: "model": "llm-model"
|
||||
Processor parameter: "model": "{model}"
|
||||
User provides: "model": "gemma3:8b"
|
||||
Final parameter: "model": "gemma3:8b"
|
||||
|
||||
Example with type conversion:
|
||||
Parameter type default: 0.7 (number)
|
||||
Stored in flow: "0.7" (string)
|
||||
Substituted in processor: "0.7" (string)
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
用于参数模式验证的单元测试
|
||||
用于处理器参数中参数替换的集成测试
|
||||
用于使用不同参数值启动流程的端到端测试
|
||||
用于参数表单生成和验证的 UI 测试
|
||||
用于具有许多参数的流程的性能测试
|
||||
边界情况:缺少参数、无效类型、未定义的参数引用
|
||||
|
||||
## 迁移计划
|
||||
|
||||
<<<<<<< HEAD
|
||||
1. 系统应继续支持未声明参数的流程蓝图。
|
||||
2. 系统应继续支持未指定参数的流程:
|
||||
=======
|
||||
1. 系统应继续支持未声明任何参数的流程蓝图。
|
||||
2. 系统应继续支持未指定任何参数的流程:
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
这适用于没有参数的流程,以及具有参数的流程(它们具有默认值)。
|
||||
|
||||
(它们有默认值)。
|
||||
|
||||
## 开放问题
|
||||
|
||||
问:参数是否应该支持复杂的嵌套对象,还是仅限于简单类型?
|
||||
答:参数值将被字符串编码,我们可能更倾向于
|
||||
使用字符串。
|
||||
|
||||
<<<<<<< HEAD
|
||||
问:是否允许在队列名称中使用参数占位符,还是仅在
|
||||
=======
|
||||
问:是否允许在队列名称中使用参数占位符,或者仅在
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
参数中使用?
|
||||
答:仅在参数中使用,以避免奇怪的注入和边缘情况。
|
||||
|
||||
问:如何处理参数名称与系统变量(如
|
||||
`id` 和 `class`)之间的冲突?
|
||||
答:在启动流程时,指定 id 和 class 是无效的。
|
||||
|
||||
问:我们是否应该支持计算参数(从其他参数派生)?
|
||||
答:仅进行字符串替换,以避免奇怪的注入和边缘情况。
|
||||
|
||||
<<<<<<< HEAD
|
||||
## 引用
|
||||
=======
|
||||
## 参考文献
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
JSON Schema 规范:https://json-schema.org/
|
||||
流程蓝图定义规范:docs/tech-specs/flow-class-definition.md
|
||||
425
docs/tech-specs/zh-cn/graph-contexts.zh-cn.md
Normal file
425
docs/tech-specs/zh-cn/graph-contexts.zh-cn.md
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
---
|
||||
layout: default
|
||||
title: "Graph Contexts 技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# Graph Contexts 技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了 TrustGraph 核心图结构体的变更,以符合 RDF 1.2 标准并支持完整的 RDF 数据集语义。 这对 2.x 版本系列是一个破坏性变更。
|
||||
|
||||
### 版本控制
|
||||
|
||||
- **2.0**: 早期采用版本。核心功能可用,可能尚未完全达到生产级别。
|
||||
- **2.1 / 2.2**: 生产版本。稳定性和完整性已验证。
|
||||
|
||||
关于成熟度的灵活性是故意的 - 早期采用者可以访问新的功能,而所有功能尚未经过生产环境的验证。
|
||||
|
||||
## 目标
|
||||
|
||||
本次工作的首要目标是启用关于事实/声明的元数据:
|
||||
|
||||
- **时间信息**: 将事实与时间元数据关联
|
||||
- 确定一个事实何时被认为是正确的。
|
||||
- 确定一个事实何时开始成立。
|
||||
- 确定一个事实何时被发现是错误的。
|
||||
|
||||
- **来源/溯源**: 跟踪哪些来源支持一个事实
|
||||
- “这个事实由来源 X 支持”。
|
||||
- 将事实链接回其原始文档。
|
||||
|
||||
- **真实性/信任度**: 记录对真实性的断言
|
||||
- “Person P 声明这是真的”。
|
||||
- “Person Q 声称这是假的”。
|
||||
- 启用信任评分和冲突检测。
|
||||
|
||||
**假设**: 重构(RDF-star / 引用三元组)是实现这些目标的关键机制,因为所有这些都要求对声明进行声明。
|
||||
|
||||
## 背景
|
||||
|
||||
为了表达“事实(Alice 知道 Bob)于 2024-01-15 被发现”或“来源 X 支持(Y 导致 Z)的声明”,您需要引用一个边作为可以进行声明的对象。 标准的三元组不支持这种情况。
|
||||
|
||||
### 当前限制
|
||||
|
||||
`trustgraph-base/trustgraph/schema/core/primitives.py` 中的当前 `Value` 类可以表示:
|
||||
- URI 节点 (`is_uri=True`)
|
||||
- 原始值 (`is_uri=False`)
|
||||
|
||||
`type` 字段存在,但未用于表示 XSD 数据类型。
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 需要支持的 RDF 功能
|
||||
|
||||
#### 核心功能(与重构目标相关)
|
||||
|
||||
这些功能与时间、溯源和真实性目标直接相关:
|
||||
|
||||
1. **RDF 1.2 引用三元组 (RDF-star)**
|
||||
- 指向其他边的边。
|
||||
- 三元组可以作为另一个三元组的主语或宾语。
|
||||
- 启用关于声明的声明(重构)。
|
||||
- 注释单个事实的核心机制。
|
||||
|
||||
2. **RDF 数据集 / 命名图**
|
||||
- 支持数据集中的多个命名图。
|
||||
- 每个图由 IRI 标识。
|
||||
- 从三元组 (s, p, o) 转换为四元组 (s, p, o, g)。
|
||||
- 包含一个默认图以及零个或多个命名图。
|
||||
- 图 IRI 可以是语句的主语,例如:
|
||||
```
|
||||
<graph-source-A> <discoveredOn> "2024-01-15"
|
||||
<graph-source-A> <hasVeracity> "high"
|
||||
```
|
||||
- 注意:命名图是一个与重构分离的功能。 它们除了语句注释之外还有其他用途(分区、访问控制、数据集组织),并且应该被视为一种不同的能力。
|
||||
|
||||
3. **匿名节点** (有限支持)
|
||||
- 没有全局 URI 的匿名节点。
|
||||
- 用于兼容性,以便加载外部 RDF 数据。
|
||||
- **有限状态**: 对加载后身份的稳定性没有保证。
|
||||
- 通过通配符查询找到它们(通过连接匹配,而不是通过 ID)。
|
||||
- 不是首选功能 - 不要依赖于精确的匿名节点处理。
|
||||
|
||||
#### 机会性修复 (2.0 破坏性变更)
|
||||
|
||||
这些功能与重构目标没有直接关系,但包含在破坏性变更中是有价值的改进:
|
||||
|
||||
4. **原始数据类型**
|
||||
- 正确使用 `type` 字段表示 XSD 数据类型。
|
||||
- 示例:xsd:string, xsd:integer, xsd:dateTime 等。
|
||||
- 修复当前限制:无法正确表示日期或整数。
|
||||
|
||||
5. **语言标签**
|
||||
- 支持字符串原始值上的语言属性 (@en, @fr 等)。
|
||||
- 注意:一个原始值要么有一个语言标签,要么有一个数据类型,两者不能同时存在(除了 rdf:langString)。
|
||||
- 对于 AI/多语言用例非常重要。
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### Term (重命名自 Value)
|
||||
|
||||
`Value` 类将被重命名为 `Term`,以更好地反映 RDF 术语。
|
||||
此重命名有两个目的:
|
||||
1. 与 RDF 概念对齐命名(“Term”可以是 IRI、原始值、匿名节点或引用三元组,而不仅仅是“值”)。
|
||||
2. 在破坏性变更接口强制代码审查 - 任何仍然引用 `Value` 的代码都会立即显示为已损坏,并且需要更新。
|
||||
|
||||
一个 Term 可以表示:
|
||||
|
||||
- **IRI/URI** - 命名节点/资源。
|
||||
- **匿名节点** - 具有本地作用域的匿名节点。
|
||||
- **原始值** - 具有以下之一的数据值:
|
||||
- 数据类型 (XSD 类型),或
|
||||
- 语言标签。
|
||||
- **引用三元组** - 用作项的三元组 (RDF 1.2)。
|
||||
|
||||
##### 选定的方法:具有类型区分器的单个类
|
||||
|
||||
序列化要求驱动结构 - 无论 Python 表示形式如何,都需要类型区分器。 具有类型字段的单个类是最佳选择,并且与当前的 `Value` 模式一致。
|
||||
|
||||
单字符类型代码提供紧凑的序列化:
|
||||
|
||||
```python
|
||||
from dataclasses import dataclass
|
||||
|
||||
# Term 类型常量
|
||||
IRI = "i" # IRI/URI 节点
|
||||
BLANK = "b" # 匿名节点
|
||||
LITERAL = "l" # 原始值
|
||||
TRIPLE = "t" # 引用三元组 (RDF-star)
|
||||
|
||||
@dataclass
|
||||
class Term:
|
||||
type: str = "" # 之一: IRI, BLANK, LITERAL, TRIPLE
|
||||
|
||||
# 对于 IRI 项 (type == IRI)
|
||||
iri: str = ""
|
||||
|
||||
# 对于匿名节点 (type == BLANK)
|
||||
id: str = ""
|
||||
|
||||
# 对于原始值 (type == LITERAL)
|
||||
value: str = ""
|
||||
datatype: str = "" # XSD 数据类型 URI (与语言互斥)
|
||||
language: str = "" # 语言标签 (与数据类型互斥)
|
||||
|
||||
# 对于引用三元组 (type == TRIPLE)
|
||||
triple: "Triple | None" = None
|
||||
```
|
||||
|
||||
用法示例:
|
||||
|
||||
```python
|
||||
# IRI 项
|
||||
node = Term(type=IRI, iri="http://example.org/Alice")
|
||||
|
||||
# 带数据类型的原始值
|
||||
age = Term(type=LITERAL, value="42", datatype="xsd:integer")
|
||||
|
||||
# 带语言标签的原始值
|
||||
label = Term(type=LITERAL, value="Hello", language="en")
|
||||
|
||||
# 匿名节点
|
||||
anon = Term(type=BLANK, id="_:b1")
|
||||
|
||||
# 引用三元组 (关于声明的声明)
|
||||
inner = Triple(
|
||||
s=Term(type=IRI, iri="http://example.org/Alice"),
|
||||
p=Term(type=IRI, iri="http://example.org/knows"),
|
||||
o=Term(type=IRI, iri="http://example.org/Bob"),
|
||||
)
|
||||
reified = Term(type=TRIPLE, triple=inner)
|
||||
```
|
||||
|
||||
##### 考虑的替代方案
|
||||
|
||||
**选项 B:专用类的联合** (`Term = IRI | BlankNode | Literal | QuotedTriple`)
|
||||
- 拒绝:仍然需要类型区分器进行序列化,增加了复杂性。
|
||||
|
||||
**选项 C:具有子类的基本类**
|
||||
- 拒绝:与序列化问题相同,此外还有 dataclass 继承的复杂性。
|
||||
|
||||
#### Triple / Quad
|
||||
|
||||
`Triple` 类获得一个可选的图字段,以成为四元组:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Triple:
|
||||
s: Term | None = None # 主语
|
||||
p: Term | None = None # 谓词
|
||||
o: Term | None = None # 宾语
|
||||
g: str | None = None # 图名 (IRI),None = 默认图
|
||||
```
|
||||
|
||||
设计决策:
|
||||
- **字段名称**: `g` 用于与 `s`、`p`、`o` 一致。
|
||||
- **可选**: `None` 表示默认图(无命名)。
|
||||
- **类型**: 纯字符串 (IRI),而不是 Term
|
||||
- 图名始终是 IRI。
|
||||
- 匿名节点作为图名被排除(过于混乱)。
|
||||
- 不需要完整的 Term 机制。
|
||||
|
||||
注意:类名保持为 `Triple`,即使它现在实际上是一个四元组。
|
||||
这避免了变更,并且术语“三元组”仍然是常见的术语。 图上下文是关于三元组存储位置的元数据。
|
||||
|
||||
### 候选查询模式
|
||||
|
||||
当前查询引擎接受 S、P、O 项的组合。 使用引用三元组时,一个三元组本身可以成为这些位置上的有效项。 以下是支持原始目标的候选查询模式。
|
||||
|
||||
#### 图参数语义
|
||||
|
||||
遵循 SPARQL 的约定以实现向后兼容:
|
||||
|
||||
- **`g` 被省略 / None**: 仅查询默认图。
|
||||
- **`g` = 特定 IRI**: 仅查询该命名图。
|
||||
- **`g` = 通配符 / `*`**: 查询所有图 (相当于 SPARQL `GRAPH ?g { ... }`)
|
||||
|
||||
这保持简单查询的简单性,并使命名图查询成为可选。
|
||||
|
||||
完全支持跨图查询 (g=通配符)。 Cassandra 模式包含专门的表,其中 g 是一个聚类列,而不是分区键,从而实现高效的跨所有图的查询。
|
||||
|
||||
#### 时间查询
|
||||
|
||||
**查找在特定日期之后发现的所有事实:**
|
||||
```
|
||||
S: ? # 任何引用三元组
|
||||
P: <discoveredOn>
|
||||
O: > "2024-01-15"^^xsd:date # 日期比较
|
||||
```
|
||||
|
||||
**查找何时认为特定事实成立:**
|
||||
```
|
||||
S: << <Alice> <knows> <Bob> >> # 引用三元组作为主语
|
||||
P: <believedTrueFrom>
|
||||
O: ? # 返回日期
|
||||
```
|
||||
|
||||
**查找被发现为错误的 facts:**
|
||||
```
|
||||
S: ? # 任何引用三元组
|
||||
P: <discoveredFalseOn>
|
||||
O: ? # 具有任何值 (存在)
|
||||
```
|
||||
|
||||
#### 溯源查询
|
||||
|
||||
**查找由特定来源支持的所有事实:**
|
||||
```
|
||||
S: ? # 任何引用三元组
|
||||
P: <supportedBy>
|
||||
O: <source:document-123>
|
||||
```
|
||||
|
||||
**查找哪些来源支持特定事实:**
|
||||
```
|
||||
S: << <DrugA> <treats> <DiseaseB> >> # 引用三元组作为主语
|
||||
P: <supportedBy>
|
||||
O: ? # 返回来源 IRI
|
||||
```
|
||||
|
||||
#### 真实性查询
|
||||
|
||||
**查找 Person 声明为真的断言:**
|
||||
```
|
||||
S: ? # 任何引用三元组
|
||||
P: <assertedTrueBy>
|
||||
O: <person:Alice>
|
||||
```
|
||||
|
||||
**查找冲突的断言 (相同的 facts,不同的真实性):**
|
||||
```
|
||||
# 第一个查询:声明为真的事实
|
||||
S: ?
|
||||
P: <assertedTrueBy>
|
||||
O: ?
|
||||
|
||||
# 第二个查询:声明为假的 fact
|
||||
S: ?
|
||||
P: <assertedFalseBy>
|
||||
O: ?
|
||||
|
||||
# 应用程序逻辑:查找主语的交集
|
||||
```
|
||||
|
||||
**查找信任评分低于阈值的 facts:**
|
||||
```
|
||||
S: ? # 任何引用三元组
|
||||
P: <trustScore>
|
||||
O: < 0.5 # 数值比较
|
||||
```
|
||||
|
||||
### 架构
|
||||
|
||||
需要对多个组件进行重大更改:
|
||||
|
||||
#### 此仓库 (trustgraph)
|
||||
|
||||
- **模式原始数据类型** (`trustgraph-base/trustgraph/schema/core/primitives.py`)
|
||||
- `Value` → `Term` 重命名。
|
||||
- 具有类型区分器的新的 `Term` 结构。
|
||||
- `Triple` 获得 `g` 字段以进行图上下文。
|
||||
|
||||
- **消息翻译器** (`trustgraph-base/trustgraph/messaging/translators/`)
|
||||
- 更新以适应新的 `Term` 和 `Triple` 结构。
|
||||
- 用于新字段的序列化/反序列化。
|
||||
|
||||
- **网关组件**
|
||||
- 处理新的 `Term` 和四元组结构。
|
||||
|
||||
- **知识核心**
|
||||
- 支持四元组和重构的核心更改。
|
||||
|
||||
- **知识管理器**
|
||||
- 模式更改会在此处传播。
|
||||
|
||||
- **存储层**
|
||||
- Cassandra: 重新设计模式(参见“实施详细信息”)。
|
||||
- 其他后端:推迟到后续阶段。
|
||||
|
||||
- **命令行工具**
|
||||
- 更新以适应新的数据结构。
|
||||
|
||||
- **REST API 文档**
|
||||
- 更新 OpenAPI 规范。
|
||||
|
||||
#### 外部仓库
|
||||
|
||||
- **Python API** (此仓库)
|
||||
- 更新客户端库以匹配新的结构。
|
||||
|
||||
- **TypeScript API** (单独仓库)
|
||||
- 平行于 Python API 的更改。
|
||||
- 分离的发布协调。
|
||||
|
||||
- **Workbench** (单独仓库)
|
||||
- 状态管理方面的重大更改。
|
||||
- 用于图上下文功能的 UI 更新。
|
||||
|
||||
### API
|
||||
|
||||
#### REST API
|
||||
|
||||
- 在 OpenAPI 规范中记录。
|
||||
- 需要更新以适应新的 `Term` 和 `Triple` 结构。
|
||||
- 可能需要新的端点来处理图上下文操作。
|
||||
|
||||
#### Python API (此仓库)
|
||||
|
||||
- 客户端库更改以匹配新的原始数据类型。
|
||||
- `Term` (以前是 `Value`) 和 `Triple` 的破坏性更改。
|
||||
|
||||
#### TypeScript API (单独仓库)
|
||||
|
||||
- 与 Python API 平行的更改。
|
||||
- 分离的发布协调。
|
||||
|
||||
#### Workbench (单独仓库)
|
||||
|
||||
- 状态管理方面的重大更改。
|
||||
- 用于图上下文功能的 UI 更新。
|
||||
|
||||
### 实施细节
|
||||
|
||||
#### 分阶段的存储实施
|
||||
|
||||
存在多个图存储后端(Cassandra、Neo4j 等)。
|
||||
实施将分阶段进行:
|
||||
|
||||
1. **第一阶段:Cassandra**
|
||||
- 在完全控制的后端上验证设计,然后再承诺在所有存储系统中的实现。
|
||||
|
||||
2. **第二阶段+:其他后端**
|
||||
- 在后续阶段实施 Neo4j 和其他存储系统。
|
||||
|
||||
此方法通过在所有系统上承诺实现之前,在完全受控的后端上验证设计,从而降低风险。
|
||||
|
||||
#### `Value` → `Term` 重命名
|
||||
|
||||
`Value` 类将被重命名为 `Term`。 这会影响代码库中的约 78 个文件。
|
||||
重命名充当强制函数:任何仍然使用 `Value` 的代码都将立即显示为需要审查/更新以获得 2.0 兼容性。
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
命名图不是安全功能。 用户和集合仍然是安全边界。 命名图纯粹用于数据组织和重构支持。
|
||||
|
||||
## 性能注意事项
|
||||
|
||||
- 引用三元组会增加嵌套深度,这可能会影响查询性能。
|
||||
- 需要命名图索引策略以实现高效的范围查询。
|
||||
- Cassandra 模式设计需要适应四元组存储。
|
||||
|
||||
### 向量存储边界
|
||||
|
||||
向量存储始终只引用 IRI:
|
||||
- 永远不要边(引用三元组)。
|
||||
- 永远不要原始值。
|
||||
- 永远不要匿名节点。
|
||||
|
||||
这使向量存储保持简单 - 它处理命名实体语义相似性。 图结构处理关系、重构和元数据。 引用三元组和命名图不会使向量操作复杂化。
|
||||
|
||||
## 测试策略
|
||||
|
||||
使用现有的测试策略。 由于这是一个破坏性版本,因此需要重点关注端到端测试套件,以验证新的结构是否在所有组件中都正确工作。
|
||||
|
||||
## 迁移计划
|
||||
|
||||
- 2.0 是一个破坏性版本;不需要向后兼容性。
|
||||
- 现有数据可能需要迁移到新的模式(基于最终设计的确定)。
|
||||
- 考虑迁移工具,用于转换现有三元组。
|
||||
|
||||
## 开放问题
|
||||
|
||||
- **匿名节点**: 确认有限支持。 可能需要决定采用 Skolem 化策略(在加载时生成 IRI,或保留匿名节点 ID)。
|
||||
- **查询语法**: 引用三元组在查询中的具体语法是什么? 需要定义查询 API。
|
||||
- **谓词词汇表**: 已解决。 允许任何有效的 RDF 谓词,包括自定义的用户定义的谓词。 尽量减少对任何事情的锁定(例如,在某些地方使用 `rdfs:label`)。 策略:除非绝对必要,否则避免锁定任何内容。
|
||||
- **向量存储影响**: 已解决。 向量存储始终仅指向 IRI - 永远不要边、原始值或匿名节点。 引用三元组和重构不会影响向量存储。
|
||||
|
||||
## 参考文献
|
||||
|
||||
- [RDF 1.2 概念](https://www.w3.org/TR/rdf12-concepts/)
|
||||
- [RDF-star 和 SPARQL-star](https://w3c.github.io/rdf-star/)
|
||||
- [RDF 数据集](https://www.w3.org/TR/rdf11-concepts/#section-dataset)
|
||||
438
docs/tech-specs/zh-cn/graphql-query.zh-cn.md
Normal file
438
docs/tech-specs/zh-cn/graphql-query.zh-cn.md
Normal file
|
|
@ -0,0 +1,438 @@
|
|||
---
|
||||
layout: default
|
||||
title: "GraphQL 查询技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# GraphQL 查询技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
<<<<<<< HEAD
|
||||
本规范描述了用于 TrustGraph 结构化数据存储在 Apache Cassandra 中的 GraphQL 查询接口的实现。 在结构化数据功能方面,本规范基于结构化数据规范 (structured-data.md),详细说明了如何对包含提取和摄取的结构化对象的 Cassandra 表执行 GraphQL 查询。
|
||||
=======
|
||||
本规范描述了用于 TrustGraph 结构化数据存储在 Apache Cassandra 中的 GraphQL 查询接口的实现。 在结构化数据功能方面,本规范基于结构化数据规范 (structured-data.md),详细说明了如何针对包含提取和导入的结构化对象的 Cassandra 表执行 GraphQL 查询。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
GraphQL 查询服务将提供一个灵活、类型安全的接口,用于查询存储在 Cassandra 中的结构化数据。 它将动态适应模式更改,支持包括对象之间的关系在内的复杂查询,并与 TrustGraph 现有的基于消息的架构无缝集成。
|
||||
|
||||
## 目标
|
||||
|
||||
**动态模式支持**: 在不重新启动服务的情况下,自动适应配置中的模式更改
|
||||
**GraphQL 标准兼容性**: 提供与现有 GraphQL 工具和客户端兼容的标准 GraphQL 接口
|
||||
**高效的 Cassandra 查询**: 将 GraphQL 查询转换为高效的 Cassandra CQL 查询,同时尊重分区键和索引
|
||||
**关系解析**: 支持用于不同对象类型之间关系的 GraphQL 字段解析器
|
||||
**类型安全**: 基于模式定义,确保类型安全地执行查询并生成响应
|
||||
**可扩展的性能**: 通过适当的连接池和查询优化,高效地处理并发查询
|
||||
**请求/响应集成**: 保持与 TrustGraph 基于 Pulsar 的请求/响应模式的兼容性
|
||||
**错误处理**: 提供全面的错误报告,用于模式不匹配、查询错误和数据验证问题
|
||||
|
||||
## 背景
|
||||
|
||||
<<<<<<< HEAD
|
||||
结构化数据存储实现 (trustgraph-flow/trustgraph/storage/objects/cassandra/) 根据存储在 TrustGraph 配置系统中的模式定义,将对象写入 Cassandra 表。 这些表使用复合分区键结构,具有集合和基于模式定义的键,从而可以在集合中实现高效的查询。
|
||||
=======
|
||||
结构化数据存储实现 (trustgraph-flow/trustgraph/storage/objects/cassandra/) 根据存储在 TrustGraph 配置系统中的模式定义,将对象写入 Cassandra 表。 这些表使用复合分区键结构,以及基于集合和模式定义的键,从而实现集合内部的高效查询。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
当前的局限性,本规范旨在解决:
|
||||
缺少用于存储在 Cassandra 中的结构化数据的查询接口
|
||||
无法利用 GraphQL 的强大查询功能来处理结构化数据
|
||||
缺少对相关对象之间关系遍历的支持
|
||||
<<<<<<< HEAD
|
||||
缺少用于结构化数据访问的标准化查询语言
|
||||
|
||||
GraphQL 查询服务将通过以下方式弥补这些差距:
|
||||
提供用于查询 Cassandra 表的标准 GraphQL 接口
|
||||
=======
|
||||
缺少一种用于结构化数据访问的标准查询语言
|
||||
|
||||
GraphQL 查询服务将通过以下方式弥补这些差距:
|
||||
为查询 Cassandra 表提供标准的 GraphQL 接口
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
从 TrustGraph 配置动态生成 GraphQL 模式
|
||||
高效地将 GraphQL 查询转换为 Cassandra CQL
|
||||
通过字段解析器支持关系解析
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 架构
|
||||
|
||||
GraphQL 查询服务将作为新的 TrustGraph 流处理器实现,遵循既定的模式:
|
||||
|
||||
**模块位置**: `trustgraph-flow/trustgraph/query/objects/cassandra/`
|
||||
|
||||
**主要组件**:
|
||||
|
||||
1. **GraphQL 查询服务处理器**
|
||||
扩展基础 FlowProcessor 类
|
||||
实现类似于现有查询服务的请求/响应模式
|
||||
监控配置以进行模式更新
|
||||
保持与配置同步的 GraphQL 模式
|
||||
|
||||
2. **动态模式生成器**
|
||||
将 TrustGraph RowSchema 定义转换为 GraphQL 类型
|
||||
创建具有适当字段定义的 GraphQL 对象类型
|
||||
生成具有基于集合的解析器的根 Query 类型
|
||||
在配置更改时更新 GraphQL 模式
|
||||
|
||||
3. **查询执行器**
|
||||
使用 Strawberry 库解析传入的 GraphQL 查询
|
||||
根据当前模式验证查询
|
||||
执行查询并返回结构化响应
|
||||
通过详细的错误消息优雅地处理错误
|
||||
|
||||
4. **Cassandra 查询转换器**
|
||||
将 GraphQL 选择转换为 CQL 查询
|
||||
根据可用的索引和分区键优化查询
|
||||
处理过滤、分页和排序
|
||||
管理连接池和会话生命周期
|
||||
|
||||
5. **关系解析器**
|
||||
<<<<<<< HEAD
|
||||
实现用于对象之间关系的字段解析器
|
||||
=======
|
||||
实现对象之间关系的字段解析器
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
执行批量加载以避免 N+1 查询
|
||||
在请求上下文中缓存解析的关系
|
||||
支持正向和反向关系遍历
|
||||
|
||||
### 配置模式监控
|
||||
|
||||
该服务将注册一个配置处理程序以接收模式更新:
|
||||
|
||||
```python
|
||||
self.register_config_handler(self.on_schema_config)
|
||||
```
|
||||
|
||||
当模式发生变化时:
|
||||
1. 从配置中解析新的模式定义
|
||||
2. 重新生成 GraphQL 类型和解析器
|
||||
3. 更新可执行的模式
|
||||
4. 清除任何依赖于模式的缓存
|
||||
|
||||
### GraphQL 模式生成
|
||||
|
||||
对于配置中的每个 RowSchema,生成:
|
||||
|
||||
1. **GraphQL 对象类型**:
|
||||
映射字段类型(string → String, integer → Int, float → Float, boolean → Boolean)
|
||||
将必需字段标记为 GraphQL 中的非空值
|
||||
从模式中添加字段描述
|
||||
|
||||
2. **根查询字段**:
|
||||
<<<<<<< HEAD
|
||||
集合查询(例如,`customers`,`transactions`)
|
||||
=======
|
||||
集合查询(例如,`customers`, `transactions`)
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
基于索引字段的过滤参数
|
||||
分页支持(limit, offset)
|
||||
可排序字段的排序选项
|
||||
|
||||
3. **关系字段**:
|
||||
从模式中识别外键关系
|
||||
为相关对象创建字段解析器
|
||||
支持单对象和列表关系
|
||||
|
||||
### 查询执行流程
|
||||
|
||||
1. **请求接收**:
|
||||
从 Pulsar 接收 ObjectsQueryRequest
|
||||
提取 GraphQL 查询字符串和变量
|
||||
识别用户和集合上下文
|
||||
|
||||
2. **查询验证**:
|
||||
使用 Strawberry 解析 GraphQL 查询
|
||||
<<<<<<< HEAD
|
||||
验证与当前模式
|
||||
=======
|
||||
验证与当前模式的匹配
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
检查字段选择和参数类型
|
||||
|
||||
3. **CQL 生成**:
|
||||
分析 GraphQL 选择
|
||||
构建带有适当 WHERE 子句的 CQL 查询
|
||||
将集合包含在分区键中
|
||||
根据 GraphQL 参数应用过滤器
|
||||
|
||||
4. **查询执行**:
|
||||
对 Cassandra 执行 CQL 查询
|
||||
将结果映射到 GraphQL 响应结构
|
||||
解析任何关系字段
|
||||
格式化响应以符合 GraphQL 规范
|
||||
|
||||
5. **响应发送**:
|
||||
创建包含结果的 ObjectsQueryResponse
|
||||
包含任何执行错误
|
||||
通过 Pulsar 发送带有相关 ID 的响应
|
||||
|
||||
### 数据模型
|
||||
|
||||
<<<<<<< HEAD
|
||||
> **注意**: `trustgraph-base/trustgraph/schema/services/structured_query.py` 中存在现有的 StructuredQueryRequest/Response 模式。但是,它缺少关键字段(用户、集合),并且使用了次优类型。以下模式代表推荐的演进,应该替换现有的模式,或者创建新的 ObjectsQueryRequest/Response 类型。
|
||||
=======
|
||||
> **注意**: `trustgraph-base/trustgraph/schema/services/structured_query.py` 中已存在一个 StructuredQueryRequest/Response 模式。但是,它缺少关键字段(用户、集合),并且使用了次优类型。以下模式代表推荐的演进,应替换现有模式或创建为新的 ObjectsQueryRequest/Response 类型。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
#### 请求模式 (ObjectsQueryRequest)
|
||||
|
||||
```python
|
||||
from pulsar.schema import Record, String, Map, Array
|
||||
|
||||
class ObjectsQueryRequest(Record):
|
||||
user = String() # Cassandra keyspace (follows pattern from TriplesQueryRequest)
|
||||
collection = String() # Data collection identifier (required for partition key)
|
||||
query = String() # GraphQL query string
|
||||
variables = Map(String()) # GraphQL variables (consider enhancing to support all JSON types)
|
||||
operation_name = String() # Operation to execute for multi-operation documents
|
||||
```
|
||||
|
||||
**对现有 StructuredQueryRequest 的更改原因:**
|
||||
添加了 `user` 和 `collection` 字段,以匹配其他查询服务的模式。
|
||||
<<<<<<< HEAD
|
||||
这些字段对于识别 Cassandra 键空间和集合至关重要。
|
||||
=======
|
||||
这些字段对于标识 Cassandra 键空间和集合至关重要。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
变量目前仍然是 Map(String()),但理想情况下应该支持所有 JSON 类型。
|
||||
|
||||
#### 响应模式 (ObjectsQueryResponse)
|
||||
|
||||
```python
|
||||
from pulsar.schema import Record, String, Array
|
||||
from ..core.primitives import Error
|
||||
|
||||
class GraphQLError(Record):
|
||||
message = String()
|
||||
path = Array(String()) # Path to the field that caused the error
|
||||
extensions = Map(String()) # Additional error metadata
|
||||
|
||||
class ObjectsQueryResponse(Record):
|
||||
error = Error() # System-level error (connection, timeout, etc.)
|
||||
data = String() # JSON-encoded GraphQL response data
|
||||
errors = Array(GraphQLError) # GraphQL field-level errors
|
||||
extensions = Map(String()) # Query metadata (execution time, etc.)
|
||||
```
|
||||
|
||||
**对现有 StructuredQueryResponse 更改的理由:**
|
||||
区分系统错误 (`error`) 和 GraphQL 错误 (`errors`)
|
||||
使用结构化的 GraphQLError 对象,而不是字符串数组
|
||||
<<<<<<< HEAD
|
||||
添加 `extensions` 字段,以符合 GraphQL 规范
|
||||
=======
|
||||
添加 `extensions` 字段以符合 GraphQL 规范
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
为了兼容性,将数据保留为 JSON 字符串,尽管使用原生类型会更好
|
||||
|
||||
### Cassandra 查询优化
|
||||
|
||||
该服务将通过以下方式优化 Cassandra 查询:
|
||||
|
||||
1. **尊重分区键:**
|
||||
始终在查询中包含集合
|
||||
高效使用 schema 定义的主键
|
||||
避免全表扫描
|
||||
|
||||
2. **利用索引:**
|
||||
使用二级索引进行过滤
|
||||
尽可能组合多个过滤器
|
||||
当查询可能效率低下时,发出警告
|
||||
|
||||
3. **批量加载:**
|
||||
收集关系查询
|
||||
批量执行以减少网络请求次数
|
||||
在请求上下文中缓存结果
|
||||
|
||||
4. **连接管理:**
|
||||
维护持久的 Cassandra 会话
|
||||
使用连接池
|
||||
在发生故障时处理重连
|
||||
|
||||
### 示例 GraphQL 查询
|
||||
|
||||
<<<<<<< HEAD
|
||||
#### 简单的集合查询
|
||||
=======
|
||||
#### 简单集合查询
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
```graphql
|
||||
{
|
||||
customers(status: "active") {
|
||||
customer_id
|
||||
name
|
||||
email
|
||||
registration_date
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 查询与关系
|
||||
```graphql
|
||||
{
|
||||
orders(order_date_gt: "2024-01-01") {
|
||||
order_id
|
||||
total_amount
|
||||
customer {
|
||||
name
|
||||
email
|
||||
}
|
||||
items {
|
||||
product_name
|
||||
quantity
|
||||
price
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 分页查询
|
||||
```graphql
|
||||
{
|
||||
products(limit: 20, offset: 40) {
|
||||
product_id
|
||||
name
|
||||
price
|
||||
category
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 实现依赖
|
||||
|
||||
**Strawberry GraphQL**: 用于 GraphQL 模式定义和查询执行
|
||||
**Cassandra Driver**: 用于数据库连接(已在存储模块中使用)
|
||||
**TrustGraph Base**: 用于 FlowProcessor 和模式定义
|
||||
**Configuration System**: 用于模式监控和更新
|
||||
|
||||
### 命令行界面
|
||||
|
||||
该服务将提供一个 CLI 命令:`kg-query-objects-graphql-cassandra`
|
||||
|
||||
参数:
|
||||
`--cassandra-host`: Cassandra 集群联系点
|
||||
`--cassandra-username`: 身份验证用户名
|
||||
`--cassandra-password`: 身份验证密码
|
||||
`--config-type`: 用于模式的配置类型(默认:"schema")
|
||||
标准 FlowProcessor 参数(Pulsar 配置等)
|
||||
|
||||
## API 集成
|
||||
|
||||
### Pulsar 主题
|
||||
|
||||
**输入主题**: `objects-graphql-query-request`
|
||||
Schema: ObjectsQueryRequest
|
||||
接收来自网关服务的 GraphQL 查询
|
||||
|
||||
**输出主题**: `objects-graphql-query-response`
|
||||
Schema: ObjectsQueryResponse
|
||||
返回查询结果和错误
|
||||
|
||||
### 网关集成
|
||||
|
||||
网关和反向网关需要端点来:
|
||||
1. 接受来自客户端的 GraphQL 查询
|
||||
2. 通过 Pulsar 将其转发到查询服务
|
||||
3. 将响应返回给客户端
|
||||
4. 支持 GraphQL 内省查询
|
||||
|
||||
### 代理工具集成
|
||||
|
||||
一个新的代理工具类将启用:
|
||||
自然语言到 GraphQL 查询的生成
|
||||
直接 GraphQL 查询执行
|
||||
结果解释和格式化
|
||||
与代理决策流程的集成
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
**查询深度限制**: 阻止深度嵌套的查询,以防止性能问题
|
||||
**查询复杂度分析**: 限制查询复杂度以防止资源耗尽
|
||||
**字段级权限**: 未来支持基于用户角色的字段级访问控制
|
||||
**输入验证**: 验证和清理所有查询输入以防止注入攻击
|
||||
**速率限制**: 为每个用户/集合实施查询速率限制
|
||||
|
||||
## 性能注意事项
|
||||
|
||||
**查询规划**: 在执行之前分析查询以优化 CQL 生成
|
||||
**结果缓存**: 考虑缓存频繁访问的数据,位于字段解析器级别
|
||||
**连接池**: 维护与 Cassandra 的高效连接池
|
||||
**批量操作**: 尽可能组合多个查询以减少延迟
|
||||
**监控**: 跟踪查询性能指标以进行优化
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 单元测试
|
||||
从 RowSchema 定义生成模式
|
||||
GraphQL 查询解析和验证
|
||||
CQL 查询生成逻辑
|
||||
字段解析器实现
|
||||
|
||||
### 契约测试
|
||||
Pulsar 消息契约合规性
|
||||
GraphQL 模式有效性
|
||||
响应格式验证
|
||||
错误结构验证
|
||||
|
||||
### 集成测试
|
||||
对测试 Cassandra 实例执行端到端查询
|
||||
模式更新处理
|
||||
关系解析
|
||||
分页和过滤
|
||||
错误场景
|
||||
|
||||
### 性能测试
|
||||
负载下的查询吞吐量
|
||||
各种查询复杂度的响应时间
|
||||
大结果集下的内存使用情况
|
||||
连接池效率
|
||||
|
||||
## 迁移计划
|
||||
|
||||
<<<<<<< HEAD
|
||||
由于这是一个新功能,因此不需要迁移。该服务将:
|
||||
=======
|
||||
由于这是一个新功能,因此不需要迁移。 该服务将:
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
1. 从配置中读取现有模式
|
||||
2. 连接到存储模块创建的现有 Cassandra 表
|
||||
3. 立即在部署后开始接受查询
|
||||
|
||||
## 时间表
|
||||
|
||||
第 1-2 周:核心服务实现和模式生成
|
||||
第 3 周:查询执行和 CQL 转换
|
||||
第 4 周:关系解析和优化
|
||||
第 5 周:测试和性能调整
|
||||
第 6 周:网关集成和文档
|
||||
|
||||
## 开放问题
|
||||
|
||||
1. **模式演进**: 服务应该如何处理查询期间的模式转换?
|
||||
选项:在模式更新期间排队查询
|
||||
选项:同时支持多个模式版本
|
||||
|
||||
2. **缓存策略**: 是否应该缓存查询结果?
|
||||
考虑:基于时间的过期
|
||||
考虑:基于事件的失效
|
||||
|
||||
3. **联邦支持**: 该服务是否应该支持 GraphQL 联邦,以与其他数据源组合?
|
||||
将启用跨结构化和图形数据的统一查询
|
||||
|
||||
4. **订阅支持**: 该服务是否应该支持 GraphQL 订阅以进行实时更新?
|
||||
需要网关中的 WebSocket 支持
|
||||
|
||||
5. **自定义标量**: 是否应该支持自定义标量类型,用于特定领域的的数据类型?
|
||||
示例:DateTime、UUID、JSON 字段
|
||||
|
||||
## 参考文献
|
||||
|
||||
结构化数据技术规范:`docs/tech-specs/structured-data.md`
|
||||
Strawberry GraphQL 文档:https://strawberry.rocks/
|
||||
GraphQL 规范:https://spec.graphql.org/
|
||||
Apache Cassandra CQL 参考:https://cassandra.apache.org/doc/stable/cassandra/cql/
|
||||
TrustGraph Flow Processor 文档:内部文档
|
||||
774
docs/tech-specs/zh-cn/graphrag-performance-optimization.zh-cn.md
Normal file
774
docs/tech-specs/zh-cn/graphrag-performance-optimization.zh-cn.md
Normal file
|
|
@ -0,0 +1,774 @@
|
|||
---
|
||||
layout: default
|
||||
title: "GraphRAG 性能优化技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# GraphRAG 性能优化技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了 TrustGraph 中 GraphRAG (基于图的检索增强生成) 算法的全面性能优化。当前的实现存在严重的性能瓶颈,限制了可扩展性和响应时间。本规范解决了四个主要的优化领域:
|
||||
|
||||
1. **图遍历优化**: 消除低效的递归数据库查询,并实现批量图探索
|
||||
2. **标签解析优化**: 使用并行/批量操作替换顺序标签获取
|
||||
3. **缓存策略增强**: 实现智能缓存,采用 LRU 淘汰策略和预取
|
||||
4. **查询优化**: 添加结果记忆化和嵌入式缓存,以提高响应时间
|
||||
|
||||
## 目标
|
||||
|
||||
**减少数据库查询量**: 通过批量和缓存,实现总数据库查询量减少 50-80%
|
||||
**提高响应时间**: 目标是子图构建速度提高 3-5 倍,标签解析速度提高 2-3 倍
|
||||
<<<<<<< HEAD
|
||||
**增强可扩展性**: 更好地管理内存,支持更大的知识图谱
|
||||
=======
|
||||
**增强可扩展性**: 通过更好的内存管理,支持更大的知识图谱
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
**保持准确性**: 保持现有的 GraphRAG 功能和结果质量
|
||||
**启用并发性**: 提高并行处理能力,以支持多个并发请求
|
||||
**减少内存占用**: 实现高效的数据结构和内存管理
|
||||
**增加可观察性**: 包含性能指标和监控功能
|
||||
**确保可靠性**: 添加适当的错误处理和超时机制
|
||||
|
||||
## 背景
|
||||
|
||||
在 `trustgraph-flow/trustgraph/retrieval/graph_rag/graph_rag.py` 中的当前 GraphRAG 实现存在几个关键的性能问题,严重影响系统的可扩展性:
|
||||
|
||||
### 当前性能问题
|
||||
|
||||
**1. 低效的图遍历 (`follow_edges` 函数,第 79-127 行)**
|
||||
对每个实体,每个深度级别执行 3 次独立的数据库查询
|
||||
查询模式:针对主体、谓词和对象进行查询
|
||||
没有批量处理:每次查询仅处理一个实体
|
||||
没有循环检测:可以多次访问相同的节点
|
||||
没有记忆化的递归实现会导致指数级复杂度
|
||||
时间复杂度:O(entities × max_path_length × triple_limit³)
|
||||
|
||||
**2. 顺序的标签解析 (`get_labelgraph` 函数,第 144-171 行)**
|
||||
顺序处理每个三元组组件 (主体、谓词、对象)
|
||||
每次 `maybe_label` 调用可能触发数据库查询
|
||||
没有并行执行或批量标签查询
|
||||
导致最多 subgraph_size × 3 次独立的数据库调用
|
||||
|
||||
**3. 原始的缓存策略 (`maybe_label` 函数,第 62-77 行)**
|
||||
简单的字典缓存,没有大小限制或 TTL
|
||||
没有缓存淘汰策略会导致内存无限增长
|
||||
缓存未命中会触发独立的数据库查询
|
||||
没有预取或智能缓存预热
|
||||
|
||||
**4. 不佳的查询模式**
|
||||
实体向量相似性查询在相似请求之间没有缓存
|
||||
没有对重复查询模式进行结果记忆化
|
||||
缺少针对常见访问模式的查询优化
|
||||
|
||||
**5. 关键的对象生命周期问题 (`rag.py:96-102`)**
|
||||
**GraphRag 对象每个请求重新创建**: 为每个查询创建一个新的实例,失去所有缓存的好处
|
||||
**查询对象寿命极短**: 在单个查询执行中创建和销毁 (第 201-207 行)
|
||||
**标签缓存每个请求重置**: 缓存预热和积累的知识在请求之间丢失
|
||||
**客户端重新创建开销**: 数据库客户端可能为每个请求重新建立
|
||||
**没有跨请求优化**: 无法从查询模式或结果共享中受益
|
||||
|
||||
### 性能影响分析
|
||||
|
||||
当前典型查询的最坏情况:
|
||||
**实体检索**: 1 次向量相似性查询
|
||||
**图遍历**: entities × max_path_length × 3 × triple_limit 次查询
|
||||
**标签解析**: subgraph_size × 3 次独立的标签查询
|
||||
|
||||
对于默认参数(50个实体,路径长度2,30个三元组限制,150个子图大小):
|
||||
**最小查询次数**: 1 + (50 × 2 × 3 × 30) + (150 × 3) = **9,451次数据库查询**
|
||||
**响应时间**: 中等大小图的响应时间为15-30秒
|
||||
**内存使用**: 缓存会随着时间增长
|
||||
**缓存有效性**: 0% - 每次请求都会重置缓存
|
||||
<<<<<<< HEAD
|
||||
**对象创建开销**: 每个请求都会创建/销毁GraphRag + Query对象
|
||||
|
||||
本规范通过实现批量查询、智能缓存和并行处理来解决这些问题。通过优化查询模式和数据访问,TrustGraph可以:
|
||||
支持拥有数百万个实体的企业级知识图谱
|
||||
为典型的查询提供亚秒级的响应时间
|
||||
=======
|
||||
**对象创建开销**: 每个请求会创建/销毁GraphRag + Query对象
|
||||
|
||||
本规范通过实现批量查询、智能缓存和并行处理来解决这些问题。通过优化查询模式和数据访问,TrustGraph可以:
|
||||
支持拥有数百万个实体的企业级知识图谱
|
||||
为典型查询提供亚秒级的响应时间
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
处理数百个并发的GraphRAG请求
|
||||
随着图的大小和复杂性而高效扩展
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 架构
|
||||
|
||||
GraphRAG性能优化需要以下技术组件:
|
||||
|
||||
#### 1. **对象生命周期架构重构**
|
||||
**使GraphRag具有长期生命周期**: 将GraphRag实例移动到处理器级别,以便在请求之间保持持久性
|
||||
**保留缓存**: 在请求之间维护标签缓存、嵌入缓存和查询结果缓存
|
||||
**优化Query对象**: 将Query重构为轻量级的执行上下文,而不是数据容器
|
||||
**连接持久化**: 在请求之间保持数据库客户端连接
|
||||
|
||||
模块: `trustgraph-flow/trustgraph/retrieval/graph_rag/rag.py` (已修改)
|
||||
|
||||
#### 2. **优化的图遍历引擎**
|
||||
使用迭代广度优先搜索替换递归的`follow_edges`
|
||||
在每个遍历级别实现批量实体处理
|
||||
添加循环检测,使用已访问节点跟踪
|
||||
在达到限制时包含早期终止
|
||||
|
||||
模块: `trustgraph-flow/trustgraph/retrieval/graph_rag/optimized_traversal.py`
|
||||
|
||||
#### 3. **并行标签解析系统**
|
||||
批量查询多个实体的标签
|
||||
使用async/await模式进行并发数据库访问
|
||||
添加智能预取,用于常见的标签模式
|
||||
包含标签缓存预热策略
|
||||
|
||||
模块: `trustgraph-flow/trustgraph/retrieval/graph_rag/label_resolver.py`
|
||||
|
||||
#### 4. **保守的标签缓存层**
|
||||
使用LRU缓存,仅对标签使用,TTL为5分钟,以平衡性能与一致性
|
||||
监控缓存指标和命中率
|
||||
**不缓存嵌入**: 已经针对每个查询进行缓存,没有跨查询的好处
|
||||
**不缓存查询结果**: 由于图的突变一致性问题
|
||||
|
||||
模块: `trustgraph-flow/trustgraph/retrieval/graph_rag/cache_manager.py`
|
||||
|
||||
#### 5. **查询优化框架**
|
||||
查询模式分析和优化建议
|
||||
批量查询协调器,用于数据库访问
|
||||
连接池和查询超时管理
|
||||
性能监控和指标收集
|
||||
|
||||
模块: `trustgraph-flow/trustgraph/retrieval/graph_rag/query_optimizer.py`
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### 优化的图遍历状态
|
||||
|
||||
遍历引擎维护状态以避免重复操作:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class TraversalState:
|
||||
visited_entities: Set[str]
|
||||
current_level_entities: Set[str]
|
||||
next_level_entities: Set[str]
|
||||
subgraph: Set[Tuple[str, str, str]]
|
||||
depth: int
|
||||
query_batch: List[TripleQuery]
|
||||
```
|
||||
|
||||
这种方法允许:
|
||||
<<<<<<< HEAD
|
||||
通过跟踪访问的实体实现高效的循环检测
|
||||
=======
|
||||
通过跟踪访问过的实体实现高效的循环检测
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
在每个遍历层级进行批量查询准备
|
||||
内存效率高的状态管理
|
||||
当达到大小限制时,可以提前终止
|
||||
|
||||
#### 增强的缓存结构
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class CacheEntry:
|
||||
value: Any
|
||||
timestamp: float
|
||||
access_count: int
|
||||
ttl: Optional[float]
|
||||
|
||||
class CacheManager:
|
||||
label_cache: LRUCache[str, CacheEntry]
|
||||
embedding_cache: LRUCache[str, CacheEntry]
|
||||
query_result_cache: LRUCache[str, CacheEntry]
|
||||
cache_stats: CacheStatistics
|
||||
```
|
||||
|
||||
#### 批量查询结构
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class BatchTripleQuery:
|
||||
entities: List[str]
|
||||
query_type: QueryType # SUBJECT, PREDICATE, OBJECT
|
||||
limit_per_entity: int
|
||||
|
||||
@dataclass
|
||||
class BatchLabelQuery:
|
||||
entities: List[str]
|
||||
predicate: str = LABEL
|
||||
```
|
||||
|
||||
### API 接口
|
||||
|
||||
#### 新增 API 接口:
|
||||
|
||||
**图遍历 API**
|
||||
```python
|
||||
async def optimized_follow_edges_batch(
|
||||
entities: List[str],
|
||||
max_depth: int,
|
||||
triple_limit: int,
|
||||
max_subgraph_size: int
|
||||
) -> Set[Tuple[str, str, str]]
|
||||
```
|
||||
|
||||
**批量标签解析 API**
|
||||
```python
|
||||
async def resolve_labels_batch(
|
||||
entities: List[str],
|
||||
cache_manager: CacheManager
|
||||
) -> Dict[str, str]
|
||||
```
|
||||
|
||||
**缓存管理 API**
|
||||
```python
|
||||
class CacheManager:
|
||||
async def get_or_fetch_label(self, entity: str) -> str
|
||||
async def get_or_fetch_embeddings(self, query: str) -> List[float]
|
||||
async def cache_query_result(self, query_hash: str, result: Any, ttl: int)
|
||||
def get_cache_statistics(self) -> CacheStatistics
|
||||
```
|
||||
|
||||
#### 修改后的 API:
|
||||
|
||||
**GraphRag.query()** - 增强了性能优化:
|
||||
<<<<<<< HEAD
|
||||
添加了 cache_manager 参数以进行缓存控制
|
||||
包含 performance_metrics 返回值
|
||||
添加了 query_timeout 参数以提高可靠性
|
||||
|
||||
**Query 类** - 进行了重构,用于批量处理:
|
||||
将单个实体处理替换为批量操作
|
||||
添加了异步上下文管理器以进行资源清理
|
||||
=======
|
||||
添加 `cache_manager` 参数以进行缓存控制
|
||||
包含 `performance_metrics` 返回值
|
||||
添加 `query_timeout` 参数以提高可靠性
|
||||
|
||||
**Query 类** - 重新设计,用于批量处理:
|
||||
将单个实体处理替换为批量操作
|
||||
添加异步上下文管理器以进行资源清理
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
包含进度回调函数,用于长时间运行的操作
|
||||
|
||||
### 实施细节
|
||||
|
||||
#### 阶段 0:关键架构生命周期重构
|
||||
|
||||
**当前存在问题的实现:**
|
||||
```python
|
||||
# INEFFICIENT: GraphRag recreated every request
|
||||
class Processor(FlowProcessor):
|
||||
async def on_request(self, msg, consumer, flow):
|
||||
# PROBLEM: New GraphRag instance per request!
|
||||
self.rag = GraphRag(
|
||||
embeddings_client = flow("embeddings-request"),
|
||||
graph_embeddings_client = flow("graph-embeddings-request"),
|
||||
triples_client = flow("triples-request"),
|
||||
prompt_client = flow("prompt-request"),
|
||||
verbose=True,
|
||||
)
|
||||
# Cache starts empty every time - no benefit from previous requests
|
||||
response = await self.rag.query(...)
|
||||
|
||||
# VERY SHORT-LIVED: Query object created/destroyed per request
|
||||
class GraphRag:
|
||||
async def query(self, query, user="trustgraph", collection="default", ...):
|
||||
q = Query(rag=self, user=user, collection=collection, ...) # Created
|
||||
kg = await q.get_labelgraph(query) # Used briefly
|
||||
# q automatically destroyed when function exits
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
**优化后的长期运行架构:**
|
||||
=======
|
||||
**优化后的长寿命架构:**
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
```python
|
||||
class Processor(FlowProcessor):
|
||||
def __init__(self, **params):
|
||||
super().__init__(**params)
|
||||
self.rag_instance = None # Will be initialized once
|
||||
self.client_connections = {}
|
||||
|
||||
async def initialize_rag(self, flow):
|
||||
"""Initialize GraphRag once, reuse for all requests"""
|
||||
if self.rag_instance is None:
|
||||
self.rag_instance = LongLivedGraphRag(
|
||||
embeddings_client=flow("embeddings-request"),
|
||||
graph_embeddings_client=flow("graph-embeddings-request"),
|
||||
triples_client=flow("triples-request"),
|
||||
prompt_client=flow("prompt-request"),
|
||||
verbose=True,
|
||||
)
|
||||
return self.rag_instance
|
||||
|
||||
async def on_request(self, msg, consumer, flow):
|
||||
# REUSE the same GraphRag instance - caches persist!
|
||||
rag = await self.initialize_rag(flow)
|
||||
|
||||
# Query object becomes lightweight execution context
|
||||
response = await rag.query_with_context(
|
||||
query=v.query,
|
||||
execution_context=QueryContext(
|
||||
user=v.user,
|
||||
collection=v.collection,
|
||||
entity_limit=entity_limit,
|
||||
# ... other params
|
||||
)
|
||||
)
|
||||
|
||||
class LongLivedGraphRag:
|
||||
def __init__(self, ...):
|
||||
# CONSERVATIVE caches - balance performance vs consistency
|
||||
self.label_cache = LRUCacheWithTTL(max_size=5000, ttl=300) # 5min TTL for freshness
|
||||
# Note: No embedding cache - already cached per-query, no cross-query benefit
|
||||
# Note: No query result cache due to consistency concerns
|
||||
self.performance_metrics = PerformanceTracker()
|
||||
|
||||
async def query_with_context(self, query: str, context: QueryContext):
|
||||
# Use lightweight QueryExecutor instead of heavyweight Query object
|
||||
executor = QueryExecutor(self, context) # Minimal object
|
||||
return await executor.execute(query)
|
||||
|
||||
@dataclass
|
||||
class QueryContext:
|
||||
"""Lightweight execution context - no heavy operations"""
|
||||
user: str
|
||||
collection: str
|
||||
entity_limit: int
|
||||
triple_limit: int
|
||||
max_subgraph_size: int
|
||||
max_path_length: int
|
||||
|
||||
class QueryExecutor:
|
||||
"""Lightweight execution context - replaces old Query class"""
|
||||
def __init__(self, rag: LongLivedGraphRag, context: QueryContext):
|
||||
self.rag = rag
|
||||
self.context = context
|
||||
# No heavy initialization - just references
|
||||
|
||||
async def execute(self, query: str):
|
||||
# All heavy lifting uses persistent rag caches
|
||||
return await self.rag.execute_optimized_query(query, self.context)
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
这种架构上的改变提供了:
|
||||
**对于具有常见关系的图,数据库查询减少 10-20%**(相对于目前 0% 的减少)
|
||||
**消除了每个请求中的对象创建开销**
|
||||
=======
|
||||
这一架构变更提供了:
|
||||
**对于具有常见关系的图,数据库查询减少 10-20%**(相对于目前 0% 的减少)
|
||||
**消除了每个请求的对象的创建开销**
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
**持久连接池和客户端重用**
|
||||
**缓存 TTL 窗口内的跨请求优化**
|
||||
|
||||
**重要的缓存一致性限制:**
|
||||
长期缓存会带来数据陈旧的风险,尤其是在底层图中删除或修改实体/标签时。 LRU 缓存和 TTL 在性能提升和数据新鲜度之间提供了一种平衡,但无法检测到实时图的变化。
|
||||
|
||||
#### 第一阶段:图遍历优化
|
||||
|
||||
**当前实现的缺点:**
|
||||
```python
|
||||
# INEFFICIENT: 3 queries per entity per level
|
||||
async def follow_edges(self, ent, subgraph, path_length):
|
||||
# Query 1: s=ent, p=None, o=None
|
||||
res = await self.rag.triples_client.query(s=ent, p=None, o=None, limit=self.triple_limit)
|
||||
# Query 2: s=None, p=ent, o=None
|
||||
res = await self.rag.triples_client.query(s=None, p=ent, o=None, limit=self.triple_limit)
|
||||
# Query 3: s=None, p=None, o=ent
|
||||
res = await self.rag.triples_client.query(s=None, p=None, o=ent, limit=self.triple_limit)
|
||||
```
|
||||
|
||||
**优化实现:**
|
||||
```python
|
||||
async def optimized_traversal(self, entities: List[str], max_depth: int) -> Set[Triple]:
|
||||
visited = set()
|
||||
current_level = set(entities)
|
||||
subgraph = set()
|
||||
|
||||
for depth in range(max_depth):
|
||||
if not current_level or len(subgraph) >= self.max_subgraph_size:
|
||||
break
|
||||
|
||||
# Batch all queries for current level
|
||||
batch_queries = []
|
||||
for entity in current_level:
|
||||
if entity not in visited:
|
||||
batch_queries.extend([
|
||||
TripleQuery(s=entity, p=None, o=None),
|
||||
TripleQuery(s=None, p=entity, o=None),
|
||||
TripleQuery(s=None, p=None, o=entity)
|
||||
])
|
||||
|
||||
# Execute all queries concurrently
|
||||
results = await self.execute_batch_queries(batch_queries)
|
||||
|
||||
# Process results and prepare next level
|
||||
next_level = set()
|
||||
for result in results:
|
||||
subgraph.update(result.triples)
|
||||
next_level.update(result.new_entities)
|
||||
|
||||
visited.update(current_level)
|
||||
current_level = next_level - visited
|
||||
|
||||
return subgraph
|
||||
```
|
||||
|
||||
#### 第二阶段:并行标签解析
|
||||
|
||||
**当前的顺序实现方式:**
|
||||
```python
|
||||
# INEFFICIENT: Sequential processing
|
||||
for edge in subgraph:
|
||||
s = await self.maybe_label(edge[0]) # Individual query
|
||||
p = await self.maybe_label(edge[1]) # Individual query
|
||||
o = await self.maybe_label(edge[2]) # Individual query
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
**优化后的并行实现:**
|
||||
=======
|
||||
**优化并行实现:**
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
```python
|
||||
async def resolve_labels_parallel(self, subgraph: List[Triple]) -> List[Triple]:
|
||||
# Collect all unique entities needing labels
|
||||
entities_to_resolve = set()
|
||||
for s, p, o in subgraph:
|
||||
entities_to_resolve.update([s, p, o])
|
||||
|
||||
# Remove already cached entities
|
||||
uncached_entities = [e for e in entities_to_resolve if e not in self.label_cache]
|
||||
|
||||
# Batch query for all uncached labels
|
||||
if uncached_entities:
|
||||
label_results = await self.batch_label_query(uncached_entities)
|
||||
self.label_cache.update(label_results)
|
||||
|
||||
# Apply labels to subgraph
|
||||
return [
|
||||
(self.label_cache.get(s, s), self.label_cache.get(p, p), self.label_cache.get(o, o))
|
||||
for s, p, o in subgraph
|
||||
]
|
||||
```
|
||||
|
||||
#### 第三阶段:高级缓存策略
|
||||
|
||||
**LRU 缓存与 TTL:**
|
||||
```python
|
||||
class LRUCacheWithTTL:
|
||||
def __init__(self, max_size: int, default_ttl: int = 3600):
|
||||
self.cache = OrderedDict()
|
||||
self.max_size = max_size
|
||||
self.default_ttl = default_ttl
|
||||
self.access_times = {}
|
||||
|
||||
async def get(self, key: str) -> Optional[Any]:
|
||||
if key in self.cache:
|
||||
# Check TTL expiration
|
||||
if time.time() - self.access_times[key] > self.default_ttl:
|
||||
del self.cache[key]
|
||||
del self.access_times[key]
|
||||
return None
|
||||
|
||||
# Move to end (most recently used)
|
||||
self.cache.move_to_end(key)
|
||||
return self.cache[key]
|
||||
return None
|
||||
|
||||
async def put(self, key: str, value: Any):
|
||||
if key in self.cache:
|
||||
self.cache.move_to_end(key)
|
||||
else:
|
||||
if len(self.cache) >= self.max_size:
|
||||
# Remove least recently used
|
||||
oldest_key = next(iter(self.cache))
|
||||
del self.cache[oldest_key]
|
||||
del self.access_times[oldest_key]
|
||||
|
||||
self.cache[key] = value
|
||||
self.access_times[key] = time.time()
|
||||
```
|
||||
|
||||
#### 第四阶段:查询优化和监控
|
||||
|
||||
**性能指标收集:**
|
||||
```python
|
||||
@dataclass
|
||||
class PerformanceMetrics:
|
||||
total_queries: int
|
||||
cache_hits: int
|
||||
cache_misses: int
|
||||
avg_response_time: float
|
||||
subgraph_construction_time: float
|
||||
label_resolution_time: float
|
||||
total_entities_processed: int
|
||||
memory_usage_mb: float
|
||||
```
|
||||
|
||||
**查询超时和断路器:**
|
||||
```python
|
||||
async def execute_with_timeout(self, query_func, timeout: int = 30):
|
||||
try:
|
||||
return await asyncio.wait_for(query_func(), timeout=timeout)
|
||||
except asyncio.TimeoutError:
|
||||
logger.error(f"Query timeout after {timeout}s")
|
||||
raise GraphRagTimeoutError(f"Query exceeded timeout of {timeout}s")
|
||||
```
|
||||
|
||||
## 缓存一致性考虑
|
||||
|
||||
**数据时效性权衡:**
|
||||
**标签缓存 (5 分钟 TTL)**:存在提供已删除/重命名的实体标签的风险。
|
||||
**不缓存嵌入 (embeddings)**:不需要 - 嵌入已按查询缓存。
|
||||
**不缓存结果**:防止从已删除的实体/关系中获取过时的子图结果。
|
||||
|
||||
**缓解策略:**
|
||||
**保守的 TTL 值**:在性能提升 (10-20%) 和数据新鲜度之间取得平衡。
|
||||
**缓存失效钩子**:可选地与图的修改事件集成。
|
||||
<<<<<<< HEAD
|
||||
**监控仪表板**:跟踪缓存命中率与数据时效性事件。
|
||||
**可配置的缓存策略**:允许根据修改频率进行按部署的调整。
|
||||
|
||||
**根据图修改速率推荐的缓存配置:**
|
||||
=======
|
||||
**监控仪表板**:跟踪缓存命中率与数据陈旧事件。
|
||||
**可配置的缓存策略**:允许根据修改频率进行按部署的调整。
|
||||
|
||||
**基于图修改速率的推荐缓存配置:**
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
**高修改速率 (>100 次/小时)**:TTL=60 秒,较小的缓存大小。
|
||||
**中等修改速率 (10-100 次/小时)**:TTL=300 秒 (默认值)。
|
||||
**低修改速率 (<10 次/小时)**:TTL=600 秒,较大的缓存大小。
|
||||
|
||||
## 安全性考虑
|
||||
|
||||
**查询注入防护:**
|
||||
验证所有实体标识符和查询参数。
|
||||
对所有数据库交互使用参数化查询。
|
||||
实施查询复杂度限制以防止拒绝服务 (DoS) 攻击。
|
||||
|
||||
**资源保护:**
|
||||
强制执行最大子图大小限制。
|
||||
实施查询超时以防止资源耗尽。
|
||||
添加内存使用情况监控和限制。
|
||||
|
||||
**访问控制:**
|
||||
维护现有的用户和集合隔离。
|
||||
添加对影响性能的操作的审计日志。
|
||||
实施速率限制以防止对昂贵操作的滥用。
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 预期的性能提升
|
||||
|
||||
**查询减少:**
|
||||
<<<<<<< HEAD
|
||||
当前:典型请求需要 ~9,000+ 次查询。
|
||||
优化后:~50-100 个批处理查询 (减少 98%)。
|
||||
=======
|
||||
当前:典型请求约 9,000+ 个查询。
|
||||
优化后:约 50-100 个批处理查询 (减少 98%)。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
**响应时间改进:**
|
||||
图遍历:15-20 秒 → 3-5 秒 (快 4-5 倍)。
|
||||
标签解析:8-12 秒 → 2-4 秒 (快 3 倍)。
|
||||
总体查询:25-35 秒 → 6-10 秒 (提升 3-4 倍)。
|
||||
|
||||
**内存效率:**
|
||||
限制的缓存大小可防止内存泄漏。
|
||||
<<<<<<< HEAD
|
||||
高效的数据结构可减少内存占用 ~40%。
|
||||
=======
|
||||
高效的数据结构可将内存占用减少约 40%。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
通过适当的资源清理实现更好的垃圾回收。
|
||||
|
||||
**现实的性能期望:**
|
||||
**标签缓存**:对于具有常见关系的图,查询减少 10-20%。
|
||||
**批处理优化**:查询减少 50-80% (主要优化)。
|
||||
**对象生命周期优化**:消除每个请求的创建开销。
|
||||
**总体改进**:主要通过批处理实现响应时间提升 3-4 倍。
|
||||
|
||||
**可扩展性改进:**
|
||||
支持 3-5 倍更大的知识图 (受缓存一致性需求限制)。
|
||||
3-5 倍更高的并发请求容量。
|
||||
通过连接重用实现更好的资源利用率。
|
||||
|
||||
### 性能监控
|
||||
|
||||
**实时指标:**
|
||||
按操作类型划分的查询执行时间。
|
||||
缓存命中率和有效性。
|
||||
数据库连接池利用率。
|
||||
内存使用情况和垃圾回收影响。
|
||||
|
||||
<<<<<<< HEAD
|
||||
**性能基准测试 (Xìngnéng Jīzhǔ Cèshì):**
|
||||
自动化性能回归测试 (Zìdònghuà xìngnéng huíguī cèshì)
|
||||
使用真实数据量的负载测试 (Shǐyòng zhēnshí shùjùliàng de fùzài cèshì)
|
||||
与当前实现相比的基准测试 (Yǔ xiàncái shíxiàn xiāngbǐ de jīzhǔ cèshì)
|
||||
=======
|
||||
**性能基准测试:**
|
||||
自动化性能回归测试
|
||||
使用真实数据量的负载测试
|
||||
与当前实现相比的基准测试
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
## 测试策略 (Cèshì Cèlüè)
|
||||
|
||||
<<<<<<< HEAD
|
||||
### 单元测试 (Dānyuán Cèshì)
|
||||
对遍历、缓存和标签解析等各个组件进行单独测试 (Duì biànlì, cáichǔ hé biāoqiān jiěshì děng gège zǔjiàn jìnxíng dānduǒ cèshì)
|
||||
模拟数据库交互以进行性能测试 (Mónǐ shùjùkù jiāohù yǐ jìnxíng xìngnéng cèshì)
|
||||
缓存驱逐和 TTL 过期测试 (Cáichǔ qūzhí hé TTL guòqí cèshì)
|
||||
错误处理和超时场景测试 (Cuòwù chǔlǐ hé chāoshí chǎngjǐng cèshì)
|
||||
|
||||
### 集成测试 (Jíchéng Cèshì)
|
||||
使用优化后的 GraphRAG 查询的端到端测试 (Shǐyòng yōuhuà hòu de GraphRAG shōuchá de duān dào duān cèshì)
|
||||
使用真实数据的数据库交互测试 (Shǐyòng zhēnshí shùjù de shùjùkù jiāohù cèshì)
|
||||
并发请求处理和资源管理测试 (Bìngfā qǐngqiú chǔlǐ hé zīyuán guǎnlǐ cèshì)
|
||||
内存泄漏检测和资源清理验证 (Nèicún liūlèi jiǎncè hé zīyuán qīnglǐ yànzhèng)
|
||||
|
||||
### 性能测试 (Xìngnéng Cèshì)
|
||||
与当前实现相比的基准测试 (Yǔ xiàncái shíxiàn xiāngbǐ de jīzhǔ cèshì)
|
||||
使用不同大小和复杂度的图的负载测试 (Shǐyòng bùtóng dàxiǎo hé fùzá dù de tú de fùzài cèshì)
|
||||
压力测试以确定内存和连接限制 (Yālì cèshì yǐ quèdìng nèicún hé liánjiē xiànzhì)
|
||||
用于性能改进的回归测试 (Yòng yú xìngnéng gǎijìn de huíguī cèshì)
|
||||
|
||||
### 兼容性测试 (Jiānróng Xìng Cèshì)
|
||||
验证现有 GraphRAG API 的兼容性 (Yànzhèng xiàn yǒu GraphRAG API de jiānróng xìng)
|
||||
使用各种图数据库后端进行测试 (Shǐyòng gè zhǒng tú shùjùkù hòudùàn jìnxíng cèshì)
|
||||
验证与当前实现相比的结果准确性 (Yànzhèng yǔ xiàncái shíxiàn xiāngbǐ de jiéguǒ zhǔnquè xìng)
|
||||
=======
|
||||
### 单元测试
|
||||
对遍历、缓存和标签解析的各个组件进行测试
|
||||
模拟数据库交互以进行性能测试
|
||||
缓存驱逐和 TTL 过期测试
|
||||
错误处理和超时场景
|
||||
|
||||
### 集成测试
|
||||
使用优化后的 GraphRAG 查询进行端到端测试
|
||||
使用真实数据进行数据库交互测试
|
||||
并发请求处理和资源管理
|
||||
内存泄漏检测和资源清理验证
|
||||
|
||||
### 性能测试
|
||||
与当前实现相比的基准测试
|
||||
使用不同大小和复杂度的图进行负载测试
|
||||
压力测试以检查内存和连接限制
|
||||
针对性能改进进行的回归测试
|
||||
|
||||
### 兼容性测试
|
||||
验证现有 GraphRAG API 的兼容性
|
||||
使用各种图数据库后端进行测试
|
||||
验证与当前实现相比的结果准确性
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
## 实施计划 (Shíshī Jìhuà)
|
||||
|
||||
<<<<<<< HEAD
|
||||
### 直接实施方法 (Zhíjiē Shíshī Fāngfǎ)
|
||||
由于允许 API 更改,因此在不引入迁移复杂性的情况下,直接实施优化:(Yóuyú yǔnxǔ API gēnggǎi, yīncǐ zài bù yǐnrù qiān yí fùzá xìng de qíngkuàng xià, zhíjiē shíshī yōuhuà:)
|
||||
|
||||
1. **替换 `follow_edges` 方法**: 使用迭代批量遍历重写 (Tiānyuē `follow_edges` fāngfǎ: Shǐyòng diànxìng pīliàng biànlì chóngxīn xiě)
|
||||
2. **优化 `get_labelgraph`**: 实施并行标签解析 (Yōuhuà `get_labelgraph`: Shíshī bìngxíng biāoqiān jiěshì)
|
||||
3. **添加长期 GraphRag**: 修改处理器以维护持久实例 (Tiānjiā chángqí GraphRag: Gǎixiāng chǔlǐ qì yǐ wéihù chíjiǔ yǐnshì)
|
||||
4. **实施标签缓存**: 为 GraphRag 类添加 LRU 缓存和 TTL (Shíshī biāoqiān cáichǔ: Wèi GraphRag lèi tiānjiā LRU cáichǔ hé TTL)
|
||||
|
||||
### 变更范围 (Biàngēng Fànwéi)
|
||||
**查询类 (Query Class)**: 替换 `follow_edges` 中的 ~50 行代码,添加 ~30 行批量处理代码 (Tiānyuē `follow_edges` zhōng de ~50 háng dàimǎ, tiānjiā ~30 háng pīliàng chǔlǐ dàimǎ)
|
||||
**GraphRag 类 (GraphRag Class)**: 添加缓存层 (~40 行代码) (Tiānjiā cáichǔ céng (~40 háng dàimǎ))
|
||||
**处理器类 (Processor Class)**: 修改为使用持久的 GraphRag 实例 (~20 行代码) (Gǎixiāng wéi shǐyòng chíjiǔ de GraphRag yǐnshì (~20 háng dàimǎ))
|
||||
**总计 (Zǒngjì)**: ~140 行专注于的变更,主要在现有类中 (~140 háng zhuānzhù yú de biàngēng, zhǔyào zài xiàn yǒu lèi zhōng)
|
||||
=======
|
||||
### 直接实施方法
|
||||
由于允许 API 更改,因此无需迁移复杂性,可以直接实现优化:
|
||||
|
||||
1. **替换 `follow_edges` 方法**: 使用迭代批量遍历重写
|
||||
2. **优化 `get_labelgraph`**: 实现并行标签解析
|
||||
3. **添加长期 GraphRag**: 修改 Processor 以维护持久实例
|
||||
4. **实现标签缓存**: 为 GraphRag 类添加 LRU 缓存和 TTL
|
||||
|
||||
### 变更范围
|
||||
**查询类**: 替换 `follow_edges` 中的约 50 行代码,添加约 30 行批量处理代码
|
||||
**GraphRag 类**: 添加缓存层(约 40 行代码)
|
||||
**Processor 类**: 修改为使用持久的 GraphRag 实例(约 20 行代码)
|
||||
**总计**: 约 140 行代码的集中性变更,主要在现有类中
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
## 时间线 (Shíjiān Xiàn)
|
||||
|
||||
<<<<<<< HEAD
|
||||
**第一周: 核心实施 (Dì Yī Zhōu: Héxīn Shíshī)**
|
||||
使用批量迭代遍历替换 `follow_edges` (Shǐyòng pīliàng diànxìng biànlì tiānyuē `follow_edges`)
|
||||
在 `get_labelgraph` 中实施并行标签解析 (Zài `get_labelgraph` zhōng shíshī bìngxíng biāoqiān jiěshì)
|
||||
向处理器添加长期 GraphRag 实例 (Xiàng chǔlǐ qì tiānjiā chángqí GraphRag yǐnshì)
|
||||
实施标签缓存层 (Shíshī biāoqiān cáichǔ céng)
|
||||
|
||||
**第二周: 测试和集成 (Dì Èr Zhōu: Cèshì hé Jíchéng)**
|
||||
对新的遍历和缓存逻辑进行单元测试 (Duì xīn de biànlì hé cáichǔ luójí jìnxíng dānyuán cèshì)
|
||||
与当前实现相比进行性能基准测试 (Yǔ xiàncái shíxiàn xiāngbǐ jìnxíng xìngnéng jīzhǔ cèshì)
|
||||
使用真实图数据进行集成测试 (Shǐyòng zhēnshí tú shùjù jìnxíng jíchéng cèshì)
|
||||
代码审查和优化 (Dàimǎ shěnchá hé yōuhuà)
|
||||
|
||||
**第三周: 部署 (Dì Sān Zhōu: Bùshǔ)**
|
||||
部署优化后的实现 (Bùshǔ yōuhuà hòu de shíshī)
|
||||
监控性能改进 (Jiānkòng xìngnéng gǎijìn)
|
||||
根据实际使用情况微调缓存 TTL 和批量大小 (Gēnjù shíjì shǐyòng qíngkuàng wēitiáo cáichǔ TTL hé pīliàng dàxiǎo)
|
||||
=======
|
||||
**第一周:核心实施**
|
||||
使用批量迭代遍历替换 `follow_edges`
|
||||
在 `get_labelgraph` 中实现并行标签解析
|
||||
在 Processor 中添加长期 GraphRag 实例
|
||||
实现标签缓存层
|
||||
|
||||
**第二周:测试和集成**
|
||||
对新的遍历和缓存逻辑进行单元测试
|
||||
与当前实现相比的性能基准测试
|
||||
使用真实图数据的集成测试
|
||||
代码审查和优化
|
||||
|
||||
**第三周:部署**
|
||||
部署优化的实现
|
||||
监控性能改进
|
||||
根据实际使用情况微调缓存 TTL 和批次大小
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
## 开放性问题 (Kāifàng Xìng Wèntí)
|
||||
|
||||
<<<<<<< HEAD
|
||||
**数据库连接池 (Shùjùkù Liánjiē Chí)**: 我们应该实施自定义连接池,还是依赖于现有的数据库客户端连接池?(Wǒmen yīnggāi shíshī zìdìngyì liánjiē chí, háishì yīlài yú xiàn yǒu de shùjùkù kèfāngliánjiē chí?)
|
||||
**缓存持久性 (Cáichǔ Chíjiǔ Xìng)**: 标签和嵌入式缓存是否应该在服务重启后持久存在?(Biāoqiān hé qiànrùshì cáichǔ shìfǒu yīnggāi zài fúwù chóngqí hòu chíjiǔ cúnzài?)
|
||||
**分布式缓存 (Fēn Bùshì Cáichǔ)**: 对于多实例部署,我们是否应该实施具有 Redis/Memcached 的分布式缓存?(Duìyú duō yǐnshì bùshǔ, wǒmen shìfǒu yīnggāi shíshī yǒuyú Redis/Memcached de fēn bùshì cáichǔ?)
|
||||
**查询结果格式 (Shōuchá Jiéguǒ Géshì)**: 我们是否应该优化内部三元组表示以获得更好的内存效率?(Wǒmen shìfǒu yīnggāi yōuhuà nèibù sān yuánzǔ biǎoshì yǐ huòdé gèng hǎo de nèicún xiàolǜ?)
|
||||
**监控集成 (Jiānkòng Jíchéng)**: 哪些指标应该暴露给现有的监控系统(Prometheus 等)?(Nǎxiē zhǐbiāo yīnggāi bàolù gěi xiàn yǒu de jiānkòng xìtǒng (Prometheus děng)?)
|
||||
|
||||
## 参考文献 (Cānkǎo Z tàiliào)
|
||||
|
||||
GraphRAG API 文档 (GraphRAG API 文档)
|
||||
示例代码 (Lìmiàn dàimǎ)
|
||||
相关论文 (Xiāngguān lùnwén)
|
||||
=======
|
||||
**数据库连接池**: 我们应该实现自定义连接池,还是依赖于现有的数据库客户端池?
|
||||
**缓存持久性**: 标签和嵌入式缓存是否应该在服务重启后持久存在?
|
||||
**分布式缓存**: 对于多实例部署,我们是否应该实现使用 Redis/Memcached 的分布式缓存?
|
||||
**查询结果格式**: 我们是否应该优化内部三元组表示以获得更好的内存效率?
|
||||
**监控集成**: 哪些指标应该暴露给现有的监控系统(Prometheus 等)?
|
||||
|
||||
## 参考文献
|
||||
|
||||
[GraphRAG 原始实现](trustgraph-flow/trustgraph/retrieval/graph_rag/graph_rag.py)
|
||||
[TrustGraph 架构原则](architecture-principles.md)
|
||||
[集合管理规范](collection-management.md)
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
725
docs/tech-specs/zh-cn/import-export-graceful-shutdown.zh-cn.md
Normal file
725
docs/tech-specs/zh-cn/import-export-graceful-shutdown.zh-cn.md
Normal file
|
|
@ -0,0 +1,725 @@
|
|||
---
|
||||
layout: default
|
||||
title: "导入/导出 优雅关闭 技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 导入/导出 优雅关闭 技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 问题陈述
|
||||
|
||||
<<<<<<< HEAD
|
||||
TrustGraph 网关在导入和导出操作中,在 WebSocket 连接关闭时,目前存在消息丢失的问题。 这是由于存在竞争条件,导致正在传输的消息在到达目的地之前被丢弃(对于导入,是丢弃到 Pulsar 队列的消息;对于导出,是丢弃到 WebSocket 客户端的消息)。
|
||||
|
||||
### 导入端问题
|
||||
1. 发布者的 asyncio.Queue 缓冲区在关闭时未被清空
|
||||
2. 在确保所有排队的消息到达 Pulsar 之前,WebSocket 连接关闭
|
||||
=======
|
||||
TrustGraph 网关在导入和导出操作的 WebSocket 关闭过程中,目前存在消息丢失的问题。 这是由于存在竞争条件,导致正在传输的消息在到达目的地之前被丢弃(对于导入,是丢弃到 Pulsar 队列的消息;对于导出,是丢弃到 WebSocket 客户端的消息)。
|
||||
|
||||
### 导入端问题
|
||||
1. 发布者的 asyncio.Queue 缓冲区在关闭时未被清空
|
||||
2. 在确保所有排队的消息到达 Pulsar 之前,WebSocket 被关闭
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
3. 没有用于确认消息成功传递的机制
|
||||
|
||||
### 导出端问题
|
||||
1. 消息在成功传递到客户端之前,已在 Pulsar 中得到确认
|
||||
2. 预定义的超时时间会导致队列已满时消息丢失
|
||||
3. 没有用于处理慢速消费者的反压机制
|
||||
4. 存在多个缓冲区,数据可能在此处丢失
|
||||
|
||||
## 架构概述
|
||||
|
||||
```
|
||||
Import Flow:
|
||||
Client -> Websocket -> TriplesImport -> Publisher -> Pulsar Queue
|
||||
|
||||
Export Flow:
|
||||
Pulsar Queue -> Subscriber -> TriplesExport -> Websocket -> Client
|
||||
```
|
||||
|
||||
## 建议的修复方案
|
||||
|
||||
### 1. 发布者改进(导入端)
|
||||
|
||||
#### A. 优雅的队列清空
|
||||
|
||||
**文件**: `trustgraph-base/trustgraph/base/publisher.py`
|
||||
|
||||
```python
|
||||
class Publisher:
|
||||
def __init__(self, client, topic, schema=None, max_size=10,
|
||||
chunking_enabled=True, drain_timeout=5.0):
|
||||
self.client = client
|
||||
self.topic = topic
|
||||
self.schema = schema
|
||||
self.q = asyncio.Queue(maxsize=max_size)
|
||||
self.chunking_enabled = chunking_enabled
|
||||
self.running = True
|
||||
self.draining = False # New state for graceful shutdown
|
||||
self.task = None
|
||||
self.drain_timeout = drain_timeout
|
||||
|
||||
async def stop(self):
|
||||
"""Initiate graceful shutdown with draining"""
|
||||
self.running = False
|
||||
self.draining = True
|
||||
|
||||
if self.task:
|
||||
# Wait for run() to complete draining
|
||||
await self.task
|
||||
|
||||
async def run(self):
|
||||
"""Enhanced run method with integrated draining logic"""
|
||||
while self.running or self.draining:
|
||||
try:
|
||||
producer = self.client.create_producer(
|
||||
topic=self.topic,
|
||||
schema=JsonSchema(self.schema),
|
||||
chunking_enabled=self.chunking_enabled,
|
||||
)
|
||||
|
||||
drain_end_time = None
|
||||
|
||||
while self.running or self.draining:
|
||||
try:
|
||||
# Start drain timeout when entering drain mode
|
||||
if self.draining and drain_end_time is None:
|
||||
drain_end_time = time.time() + self.drain_timeout
|
||||
logger.info(f"Publisher entering drain mode, timeout={self.drain_timeout}s")
|
||||
|
||||
# Check drain timeout
|
||||
if self.draining and time.time() > drain_end_time:
|
||||
if not self.q.empty():
|
||||
logger.warning(f"Drain timeout reached with {self.q.qsize()} messages remaining")
|
||||
self.draining = False
|
||||
break
|
||||
|
||||
# Calculate wait timeout based on mode
|
||||
if self.draining:
|
||||
# Shorter timeout during draining to exit quickly when empty
|
||||
timeout = min(0.1, drain_end_time - time.time())
|
||||
else:
|
||||
# Normal operation timeout
|
||||
timeout = 0.25
|
||||
|
||||
# Get message from queue
|
||||
id, item = await asyncio.wait_for(
|
||||
self.q.get(),
|
||||
timeout=timeout
|
||||
)
|
||||
|
||||
# Send the message (single place for sending)
|
||||
if id:
|
||||
producer.send(item, { "id": id })
|
||||
else:
|
||||
producer.send(item)
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
# If draining and queue is empty, we're done
|
||||
if self.draining and self.q.empty():
|
||||
logger.info("Publisher queue drained successfully")
|
||||
self.draining = False
|
||||
break
|
||||
continue
|
||||
|
||||
except asyncio.QueueEmpty:
|
||||
# If draining and queue is empty, we're done
|
||||
if self.draining and self.q.empty():
|
||||
logger.info("Publisher queue drained successfully")
|
||||
self.draining = False
|
||||
break
|
||||
continue
|
||||
|
||||
# Flush producer before closing
|
||||
if producer:
|
||||
producer.flush()
|
||||
producer.close()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in publisher: {e}", exc_info=True)
|
||||
|
||||
if not self.running and not self.draining:
|
||||
return
|
||||
|
||||
# If handler drops out, sleep a retry
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def send(self, id, item):
|
||||
"""Send still works normally - just adds to queue"""
|
||||
if self.draining:
|
||||
# Optionally reject new messages during drain
|
||||
raise RuntimeError("Publisher is shutting down, not accepting new messages")
|
||||
await self.q.put((id, item))
|
||||
```
|
||||
|
||||
**主要设计优势:**
|
||||
**单一发送位置:** 所有 `producer.send()` 调用都发生在 `run()` 方法内的单一位置。
|
||||
**清晰的状态机:** 三个明确的状态 - 运行中、正在排空、已停止。
|
||||
**超时保护:** 在排空过程中不会无限期挂起。
|
||||
**更好的可观察性:** 清晰地记录排空进度和状态转换。
|
||||
**可选的消息拒绝:** 可以在关闭阶段拒绝新的消息。
|
||||
|
||||
#### B. 改进的关闭顺序
|
||||
|
||||
**文件:** `trustgraph-flow/trustgraph/gateway/dispatch/triples_import.py`
|
||||
|
||||
```python
|
||||
class TriplesImport:
|
||||
async def destroy(self):
|
||||
"""Enhanced destroy with proper shutdown order"""
|
||||
# Step 1: Stop accepting new messages
|
||||
self.running.stop()
|
||||
|
||||
# Step 2: Wait for publisher to drain its queue
|
||||
logger.info("Draining publisher queue...")
|
||||
await self.publisher.stop()
|
||||
|
||||
# Step 3: Close websocket only after queue is drained
|
||||
if self.ws:
|
||||
await self.ws.close()
|
||||
```
|
||||
|
||||
### 2. 订阅者改进(导出端)
|
||||
|
||||
#### A. 集成排空模式
|
||||
|
||||
**文件**: `trustgraph-base/trustgraph/base/subscriber.py`
|
||||
|
||||
```python
|
||||
class Subscriber:
|
||||
def __init__(self, client, topic, subscription, consumer_name,
|
||||
schema=None, max_size=100, metrics=None,
|
||||
backpressure_strategy="block", drain_timeout=5.0):
|
||||
# ... existing init ...
|
||||
self.backpressure_strategy = backpressure_strategy
|
||||
self.running = True
|
||||
self.draining = False # New state for graceful shutdown
|
||||
self.drain_timeout = drain_timeout
|
||||
self.pending_acks = {} # Track messages awaiting delivery
|
||||
|
||||
async def stop(self):
|
||||
"""Initiate graceful shutdown with draining"""
|
||||
self.running = False
|
||||
self.draining = True
|
||||
|
||||
if self.task:
|
||||
# Wait for run() to complete draining
|
||||
await self.task
|
||||
|
||||
async def run(self):
|
||||
"""Enhanced run method with integrated draining logic"""
|
||||
while self.running or self.draining:
|
||||
if self.metrics:
|
||||
self.metrics.state("stopped")
|
||||
|
||||
try:
|
||||
self.consumer = self.client.subscribe(
|
||||
topic = self.topic,
|
||||
subscription_name = self.subscription,
|
||||
consumer_name = self.consumer_name,
|
||||
schema = JsonSchema(self.schema),
|
||||
)
|
||||
|
||||
if self.metrics:
|
||||
self.metrics.state("running")
|
||||
|
||||
logger.info("Subscriber running...")
|
||||
drain_end_time = None
|
||||
|
||||
while self.running or self.draining:
|
||||
# Start drain timeout when entering drain mode
|
||||
if self.draining and drain_end_time is None:
|
||||
drain_end_time = time.time() + self.drain_timeout
|
||||
logger.info(f"Subscriber entering drain mode, timeout={self.drain_timeout}s")
|
||||
|
||||
# Stop accepting new messages from Pulsar during drain
|
||||
self.consumer.pause_message_listener()
|
||||
|
||||
# Check drain timeout
|
||||
if self.draining and time.time() > drain_end_time:
|
||||
async with self.lock:
|
||||
total_pending = sum(
|
||||
q.qsize() for q in
|
||||
list(self.q.values()) + list(self.full.values())
|
||||
)
|
||||
if total_pending > 0:
|
||||
logger.warning(f"Drain timeout reached with {total_pending} messages in queues")
|
||||
self.draining = False
|
||||
break
|
||||
|
||||
# Check if we can exit drain mode
|
||||
if self.draining:
|
||||
async with self.lock:
|
||||
all_empty = all(
|
||||
q.empty() for q in
|
||||
list(self.q.values()) + list(self.full.values())
|
||||
)
|
||||
if all_empty and len(self.pending_acks) == 0:
|
||||
logger.info("Subscriber queues drained successfully")
|
||||
self.draining = False
|
||||
break
|
||||
|
||||
# Process messages only if not draining
|
||||
if not self.draining:
|
||||
try:
|
||||
msg = await asyncio.to_thread(
|
||||
self.consumer.receive,
|
||||
timeout_millis=250
|
||||
)
|
||||
except _pulsar.Timeout:
|
||||
continue
|
||||
except Exception as e:
|
||||
logger.error(f"Exception in subscriber receive: {e}", exc_info=True)
|
||||
raise e
|
||||
|
||||
if self.metrics:
|
||||
self.metrics.received()
|
||||
|
||||
# Process the message
|
||||
await self._process_message(msg)
|
||||
else:
|
||||
# During draining, just wait for queues to empty
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Subscriber exception: {e}", exc_info=True)
|
||||
|
||||
finally:
|
||||
# Negative acknowledge any pending messages
|
||||
for msg in self.pending_acks.values():
|
||||
self.consumer.negative_acknowledge(msg)
|
||||
self.pending_acks.clear()
|
||||
|
||||
if self.consumer:
|
||||
self.consumer.unsubscribe()
|
||||
self.consumer.close()
|
||||
self.consumer = None
|
||||
|
||||
if self.metrics:
|
||||
self.metrics.state("stopped")
|
||||
|
||||
if not self.running and not self.draining:
|
||||
return
|
||||
|
||||
# If handler drops out, sleep a retry
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _process_message(self, msg):
|
||||
"""Process a single message with deferred acknowledgment"""
|
||||
# Store message for later acknowledgment
|
||||
msg_id = str(uuid.uuid4())
|
||||
self.pending_acks[msg_id] = msg
|
||||
|
||||
try:
|
||||
id = msg.properties()["id"]
|
||||
except:
|
||||
id = None
|
||||
|
||||
value = msg.value()
|
||||
delivery_success = False
|
||||
|
||||
async with self.lock:
|
||||
# Deliver to specific subscribers
|
||||
if id in self.q:
|
||||
delivery_success = await self._deliver_to_queue(
|
||||
self.q[id], value
|
||||
)
|
||||
|
||||
# Deliver to all subscribers
|
||||
for q in self.full.values():
|
||||
if await self._deliver_to_queue(q, value):
|
||||
delivery_success = True
|
||||
|
||||
# Acknowledge only on successful delivery
|
||||
if delivery_success:
|
||||
self.consumer.acknowledge(msg)
|
||||
del self.pending_acks[msg_id]
|
||||
else:
|
||||
# Negative acknowledge for retry
|
||||
self.consumer.negative_acknowledge(msg)
|
||||
del self.pending_acks[msg_id]
|
||||
|
||||
async def _deliver_to_queue(self, queue, value):
|
||||
"""Deliver message to queue with backpressure handling"""
|
||||
try:
|
||||
if self.backpressure_strategy == "block":
|
||||
# Block until space available (no timeout)
|
||||
await queue.put(value)
|
||||
return True
|
||||
|
||||
elif self.backpressure_strategy == "drop_oldest":
|
||||
# Drop oldest message if queue full
|
||||
if queue.full():
|
||||
try:
|
||||
queue.get_nowait()
|
||||
if self.metrics:
|
||||
self.metrics.dropped()
|
||||
except asyncio.QueueEmpty:
|
||||
pass
|
||||
await queue.put(value)
|
||||
return True
|
||||
|
||||
elif self.backpressure_strategy == "drop_new":
|
||||
# Drop new message if queue full
|
||||
if queue.full():
|
||||
if self.metrics:
|
||||
self.metrics.dropped()
|
||||
return False
|
||||
await queue.put(value)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to deliver message: {e}")
|
||||
return False
|
||||
```
|
||||
|
||||
**主要设计优势(与发布者模式匹配):**
|
||||
<<<<<<< HEAD
|
||||
**单一处理位置:** 所有消息处理都在 `run()` 方法中进行。
|
||||
=======
|
||||
**单一处理位置:** 所有消息处理都发生在 `run()` 方法中。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
**清晰的状态机:** 三个明确的状态 - 运行中、正在排空、已停止。
|
||||
**排空期间暂停:** 在排空现有队列时,停止从 Pulsar 接收新消息。
|
||||
**超时保护:** 在排空过程中不会无限期挂起。
|
||||
**正确的清理:** 在关闭时,会对任何未发送的消息进行否定确认。
|
||||
|
||||
#### B. 导出处理程序改进
|
||||
|
||||
**文件:** `trustgraph-flow/trustgraph/gateway/dispatch/triples_export.py`
|
||||
|
||||
```python
|
||||
class TriplesExport:
|
||||
async def destroy(self):
|
||||
"""Enhanced destroy with graceful shutdown"""
|
||||
# Step 1: Signal stop to prevent new messages
|
||||
self.running.stop()
|
||||
|
||||
# Step 2: Wait briefly for in-flight messages
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Step 3: Unsubscribe and stop subscriber (triggers queue drain)
|
||||
if hasattr(self, 'subs'):
|
||||
await self.subs.unsubscribe_all(self.id)
|
||||
await self.subs.stop()
|
||||
|
||||
# Step 4: Close websocket last
|
||||
if self.ws and not self.ws.closed:
|
||||
await self.ws.close()
|
||||
|
||||
async def run(self):
|
||||
"""Enhanced run with better error handling"""
|
||||
self.subs = Subscriber(
|
||||
client = self.pulsar_client,
|
||||
topic = self.queue,
|
||||
consumer_name = self.consumer,
|
||||
subscription = self.subscriber,
|
||||
schema = Triples,
|
||||
backpressure_strategy = "block" # Configurable
|
||||
)
|
||||
|
||||
await self.subs.start()
|
||||
|
||||
self.id = str(uuid.uuid4())
|
||||
q = await self.subs.subscribe_all(self.id)
|
||||
|
||||
consecutive_errors = 0
|
||||
max_consecutive_errors = 5
|
||||
|
||||
while self.running.get():
|
||||
try:
|
||||
resp = await asyncio.wait_for(q.get(), timeout=0.5)
|
||||
await self.ws.send_json(serialize_triples(resp))
|
||||
consecutive_errors = 0 # Reset on success
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
|
||||
except queue.Empty:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Exception sending to websocket: {str(e)}")
|
||||
consecutive_errors += 1
|
||||
|
||||
if consecutive_errors >= max_consecutive_errors:
|
||||
logger.error("Too many consecutive errors, shutting down")
|
||||
break
|
||||
|
||||
# Brief pause before retry
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Graceful cleanup handled in destroy()
|
||||
```
|
||||
|
||||
### 3. Socket 级别改进
|
||||
|
||||
**文件**: `trustgraph-flow/trustgraph/gateway/endpoint/socket.py`
|
||||
|
||||
```python
|
||||
class SocketEndpoint:
|
||||
async def listener(self, ws, dispatcher, running):
|
||||
"""Enhanced listener with graceful shutdown"""
|
||||
async for msg in ws:
|
||||
if msg.type == WSMsgType.TEXT:
|
||||
await dispatcher.receive(msg)
|
||||
continue
|
||||
elif msg.type == WSMsgType.BINARY:
|
||||
await dispatcher.receive(msg)
|
||||
continue
|
||||
else:
|
||||
# Graceful shutdown on close
|
||||
logger.info("Websocket closing, initiating graceful shutdown")
|
||||
running.stop()
|
||||
|
||||
# Allow time for dispatcher cleanup
|
||||
await asyncio.sleep(1.0)
|
||||
break
|
||||
|
||||
async def handle(self, request):
|
||||
"""Enhanced handler with better cleanup"""
|
||||
# ... existing setup code ...
|
||||
|
||||
try:
|
||||
async with asyncio.TaskGroup() as tg:
|
||||
running = Running()
|
||||
|
||||
dispatcher = await self.dispatcher(
|
||||
ws, running, request.match_info
|
||||
)
|
||||
|
||||
worker_task = tg.create_task(
|
||||
self.worker(ws, dispatcher, running)
|
||||
)
|
||||
|
||||
lsnr_task = tg.create_task(
|
||||
self.listener(ws, dispatcher, running)
|
||||
)
|
||||
|
||||
except ExceptionGroup as e:
|
||||
logger.error("Exception group occurred:", exc_info=True)
|
||||
|
||||
# Attempt graceful dispatcher shutdown
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
dispatcher.destroy(),
|
||||
timeout=5.0
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning("Dispatcher shutdown timed out")
|
||||
except Exception as de:
|
||||
logger.error(f"Error during dispatcher cleanup: {de}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Socket exception: {e}", exc_info=True)
|
||||
|
||||
finally:
|
||||
# Ensure dispatcher cleanup
|
||||
if dispatcher and hasattr(dispatcher, 'destroy'):
|
||||
try:
|
||||
await dispatcher.destroy()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Ensure websocket is closed
|
||||
if ws and not ws.closed:
|
||||
await ws.close()
|
||||
|
||||
return ws
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
添加配置支持,用于调整行为:
|
||||
|
||||
```python
|
||||
# config.py
|
||||
class GracefulShutdownConfig:
|
||||
# Publisher settings
|
||||
PUBLISHER_DRAIN_TIMEOUT = 5.0 # Seconds to wait for queue drain
|
||||
PUBLISHER_FLUSH_TIMEOUT = 2.0 # Producer flush timeout
|
||||
|
||||
# Subscriber settings
|
||||
SUBSCRIBER_DRAIN_TIMEOUT = 5.0 # Seconds to wait for queue drain
|
||||
BACKPRESSURE_STRATEGY = "block" # Options: "block", "drop_oldest", "drop_new"
|
||||
SUBSCRIBER_MAX_QUEUE_SIZE = 100 # Maximum queue size before backpressure
|
||||
|
||||
# Socket settings
|
||||
SHUTDOWN_GRACE_PERIOD = 1.0 # Seconds to wait for graceful shutdown
|
||||
MAX_CONSECUTIVE_ERRORS = 5 # Maximum errors before forced shutdown
|
||||
|
||||
# Monitoring
|
||||
LOG_QUEUE_STATS = True # Log queue statistics on shutdown
|
||||
METRICS_ENABLED = True # Enable metrics collection
|
||||
```
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 单元测试
|
||||
|
||||
```python
|
||||
async def test_publisher_queue_drain():
|
||||
"""Verify Publisher drains queue on shutdown"""
|
||||
publisher = Publisher(...)
|
||||
|
||||
# Fill queue with messages
|
||||
for i in range(10):
|
||||
await publisher.send(f"id-{i}", {"data": i})
|
||||
|
||||
# Stop publisher
|
||||
await publisher.stop()
|
||||
|
||||
# Verify all messages were sent
|
||||
assert publisher.q.empty()
|
||||
assert mock_producer.send.call_count == 10
|
||||
|
||||
async def test_subscriber_deferred_ack():
|
||||
"""Verify Subscriber only acks on successful delivery"""
|
||||
subscriber = Subscriber(..., backpressure_strategy="drop_new")
|
||||
|
||||
# Fill queue to capacity
|
||||
queue = await subscriber.subscribe("test")
|
||||
for i in range(100):
|
||||
await queue.put({"data": i})
|
||||
|
||||
# Try to add message when full
|
||||
msg = create_mock_message()
|
||||
await subscriber._process_message(msg)
|
||||
|
||||
# Verify negative acknowledgment
|
||||
assert msg.negative_acknowledge.called
|
||||
assert not msg.acknowledge.called
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
```python
|
||||
async def test_import_graceful_shutdown():
|
||||
"""Test import path handles shutdown gracefully"""
|
||||
# Setup
|
||||
import_handler = TriplesImport(...)
|
||||
await import_handler.start()
|
||||
|
||||
# Send messages
|
||||
messages = []
|
||||
for i in range(100):
|
||||
msg = {"metadata": {...}, "triples": [...]}
|
||||
await import_handler.receive(msg)
|
||||
messages.append(msg)
|
||||
|
||||
# Shutdown while messages in flight
|
||||
await import_handler.destroy()
|
||||
|
||||
# Verify all messages reached Pulsar
|
||||
received = await pulsar_consumer.receive_all()
|
||||
assert len(received) == 100
|
||||
|
||||
async def test_export_no_message_loss():
|
||||
"""Test export path doesn't lose acknowledged messages"""
|
||||
# Setup Pulsar with test messages
|
||||
for i in range(100):
|
||||
await pulsar_producer.send({"data": i})
|
||||
|
||||
# Start export handler
|
||||
export_handler = TriplesExport(...)
|
||||
export_task = asyncio.create_task(export_handler.run())
|
||||
|
||||
# Receive some messages
|
||||
received = []
|
||||
for _ in range(50):
|
||||
msg = await websocket.receive()
|
||||
received.append(msg)
|
||||
|
||||
# Force shutdown
|
||||
await export_handler.destroy()
|
||||
|
||||
# Continue receiving until websocket closes
|
||||
while not websocket.closed:
|
||||
try:
|
||||
msg = await websocket.receive()
|
||||
received.append(msg)
|
||||
except:
|
||||
break
|
||||
|
||||
# Verify no acknowledged messages were lost
|
||||
assert len(received) >= 50
|
||||
```
|
||||
|
||||
## 部署计划
|
||||
|
||||
<<<<<<< HEAD
|
||||
### 第一阶段:关键修复(第一周)
|
||||
修复订阅者确认时序问题(防止消息丢失)
|
||||
添加发布者队列排空功能
|
||||
部署到测试环境
|
||||
|
||||
### 第二阶段:平滑关闭(第二周)
|
||||
=======
|
||||
### 第一阶段:关键修复 (第一周)
|
||||
修复订阅者确认时序问题 (防止消息丢失)
|
||||
添加发布者队列排空功能
|
||||
部署到测试环境
|
||||
|
||||
### 第二阶段:平滑关闭 (第二周)
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
实施关闭协调
|
||||
添加反压策略
|
||||
性能测试
|
||||
|
||||
<<<<<<< HEAD
|
||||
### 第三阶段:监控与调优(第三周)
|
||||
=======
|
||||
### 第三阶段:监控与调优 (第三周)
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
添加队列深度的指标
|
||||
添加消息丢失的告警
|
||||
根据生产数据调整超时值
|
||||
|
||||
## 监控与告警
|
||||
|
||||
### 跟踪指标
|
||||
`publisher.queue.depth` - 当前发布者队列大小
|
||||
`publisher.messages.dropped` - 关闭期间丢失的消息数量
|
||||
`subscriber.messages.negatively_acknowledged` - 失败的交付
|
||||
`websocket.graceful_shutdowns` - 成功的平滑关闭
|
||||
`websocket.forced_shutdowns` - 强制/超时关闭
|
||||
|
||||
### 告警
|
||||
<<<<<<< HEAD
|
||||
发布者队列深度超过 80% 的容量
|
||||
=======
|
||||
发布者队列深度超过 80% 容量
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
关闭期间出现任何消息丢失
|
||||
订阅者否定确认率 > 1%
|
||||
关闭超时
|
||||
|
||||
## 向后兼容性
|
||||
|
||||
所有更改都保持向后兼容性:
|
||||
在没有配置的情况下,默认行为保持不变
|
||||
现有部署继续正常运行
|
||||
如果新的功能不可用,则会进行优雅降级
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
没有引入新的攻击向量
|
||||
反压可防止内存耗尽攻击
|
||||
可配置的限制可防止资源滥用
|
||||
|
||||
## 性能影响
|
||||
|
||||
正常运行期间的开销很小
|
||||
<<<<<<< HEAD
|
||||
关闭可能需要长达 5 秒(可配置)
|
||||
内存使用量受队列大小限制
|
||||
CPU 影响可以忽略不计(<1% 的增加)
|
||||
=======
|
||||
关闭可能需要长达 5 秒钟 (可配置)
|
||||
内存使用量受队列大小限制
|
||||
CPU 影响可以忽略不计 (<1% 的增长)
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
532
docs/tech-specs/zh-cn/jsonl-prompt-output.zh-cn.md
Normal file
532
docs/tech-specs/zh-cn/jsonl-prompt-output.zh-cn.md
Normal file
|
|
@ -0,0 +1,532 @@
|
|||
---
|
||||
layout: default
|
||||
title: "JSONL 提示输出技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# JSONL 提示输出技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
<<<<<<< HEAD
|
||||
本规范描述了在 TrustGraph 中,用于提示响应的 JSONL(JSON Lines)输出格式的实现。JSONL 允许在大型语言模型(LLM)的响应中,以一种能够抵抗截断的方式提取结构化数据,从而解决了当 LLM 响应达到输出令牌限制时,JSON 数组输出可能被损坏的关键问题。
|
||||
=======
|
||||
本规范描述了在 TrustGraph 中,用于提示响应的 JSONL(JSON Lines)输出格式的实现。JSONL 允许在大型语言模型 (LLM) 响应中提取结构化数据,即使在响应达到输出令牌限制时,也能保持数据的完整性。
|
||||
JSONL 能够实现对结构化数据的截断鲁棒性提取,解决了当 LLM 响应达到输出令牌限制时,JSON 数组输出可能被破坏的关键问题。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
|
||||
|
||||
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
本实现支持以下用例:
|
||||
|
||||
1. **抗截断提取**: 即使当
|
||||
LLM 输出在响应过程中被截断时,也能提取有效的中间结果。
|
||||
2. **大规模提取**: 能够处理大量项的提取,而不会因 token 限制导致完全失败的风险。
|
||||
3. **混合类型提取**: 支持在单个提示中提取多种实体类型(定义、关系、实体、属性)。
|
||||
4. **支持流式输出**: 允许对提取结果进行未来的流式/增量处理。
|
||||
|
||||
## 目标
|
||||
|
||||
|
||||
## 目标
|
||||
|
||||
**向后兼容性**: 仍然可以使用 `response-type: "text"` 和
|
||||
`response-type: "json"` 的现有提示,无需修改即可继续工作。
|
||||
<<<<<<< HEAD
|
||||
**截断恢复能力**: 即使是部分 LLM 输出,也能产生部分有效的结果,
|
||||
而不是完全失败。
|
||||
**模式验证**: 支持对单个对象进行 JSON 模式验证。
|
||||
**区分联合类型**: 支持使用 `type` 字段作为区分器的混合类型输出。
|
||||
**最小的 API 变更**: 通过新的
|
||||
=======
|
||||
**截断鲁棒性**: 即使是部分 LLM 输出,也能产生部分有效的结果,
|
||||
而不是完全失败。
|
||||
**模式验证**: 支持对单个对象进行 JSON 模式验证。
|
||||
**区分联合**: 支持使用 `type` 字段作为区分器的混合类型输出。
|
||||
**最小的 API 更改**: 通过新的
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
响应类型和模式键来扩展现有的提示配置。
|
||||
|
||||
|
||||
## 背景
|
||||
|
||||
### 当前架构
|
||||
|
||||
提示服务支持两种响应类型:
|
||||
|
||||
1. `response-type: "text"` - 原始文本响应,按原样返回。
|
||||
2. `response-type: "json"` - 从响应中解析的 JSON 数据,并根据
|
||||
可选的 `schema` 进行验证。
|
||||
|
||||
当前在 `trustgraph-flow/trustgraph/template/prompt_manager.py` 中的实现:
|
||||
|
||||
```python
|
||||
class Prompt:
|
||||
def __init__(self, template, response_type = "text", terms=None, schema=None):
|
||||
self.template = template
|
||||
self.response_type = response_type
|
||||
self.terms = terms
|
||||
self.schema = schema
|
||||
```
|
||||
|
||||
### 当前限制
|
||||
|
||||
当提取提示要求输出为 JSON 数组 (`[{...}, {...}, ...]`) 时:
|
||||
|
||||
**截断损坏:** 如果 LLM 在数组中间达到输出令牌限制,则整个响应变为无效的 JSON,无法解析。
|
||||
**全或无解析:** 必须接收完整的输出才能进行解析。
|
||||
**没有部分结果:** 截断的响应会产生零可用数据。
|
||||
**不适用于大型提取:** 提取的项目越多,失败的风险越高。
|
||||
|
||||
|
||||
此规范通过引入 JSONL 格式来解决这些限制,其中每个提取的项目都是一个完整的 JSON 对象,位于其自己的行上。
|
||||
|
||||
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 响应类型扩展
|
||||
|
||||
添加一个新的响应类型 `"jsonl"`,与现有的 `"text"` 和 `"json"` 类型并列。
|
||||
|
||||
#### 配置更改
|
||||
|
||||
**新的响应类型值:**
|
||||
|
||||
```
|
||||
"response-type": "jsonl"
|
||||
```
|
||||
|
||||
**模式解释:**
|
||||
|
||||
现有的 `"schema"` 键同时用于 `"json"` 和 `"jsonl"` 响应类型。
|
||||
解释取决于响应类型:
|
||||
|
||||
`"json"`:模式描述整个响应(通常是数组或对象)。
|
||||
`"jsonl"`:模式描述每个单独的行/对象。
|
||||
|
||||
```json
|
||||
{
|
||||
"response-type": "jsonl",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entity": { "type": "string" },
|
||||
"definition": { "type": "string" }
|
||||
},
|
||||
"required": ["entity", "definition"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
这避免了对提示配置工具和编辑器的修改。
|
||||
=======
|
||||
这避免了对提示配置工具和编辑器的更改。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
### JSONL 格式规范
|
||||
|
||||
#### 简单提取
|
||||
|
||||
对于提取单一类型对象(定义、关系、
|
||||
主题、行)的提示,输出为每行一个 JSON 对象,没有包装:
|
||||
|
||||
**提示输出格式:**
|
||||
```
|
||||
{"entity": "photosynthesis", "definition": "Process by which plants convert sunlight"}
|
||||
{"entity": "chlorophyll", "definition": "Green pigment in plants"}
|
||||
{"entity": "mitochondria", "definition": "Powerhouse of the cell"}
|
||||
```
|
||||
|
||||
**与之前的 JSON 数组格式对比:**
|
||||
```json
|
||||
[
|
||||
{"entity": "photosynthesis", "definition": "Process by which plants convert sunlight"},
|
||||
{"entity": "chlorophyll", "definition": "Green pigment in plants"},
|
||||
{"entity": "mitochondria", "definition": "Powerhouse of the cell"}
|
||||
]
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
如果大型语言模型在第2行之后截断,JSON数组格式会产生无效的JSON,
|
||||
而JSONL会产生两个有效的对象。
|
||||
=======
|
||||
如果大型语言模型在第2行之后截断,JSON数组格式将产生无效的JSON,
|
||||
而JSONL格式将产生两个有效的对象。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
#### 混合类型提取(区分联合)
|
||||
|
||||
对于需要提取多种类型对象(例如,定义和
|
||||
关系,或者实体、关系和属性)的提示,请使用一个 `"type"`
|
||||
字段作为区分器:
|
||||
|
||||
**提示输出格式:**
|
||||
```
|
||||
{"type": "definition", "entity": "DNA", "definition": "Molecule carrying genetic instructions"}
|
||||
{"type": "relationship", "subject": "DNA", "predicate": "located_in", "object": "cell nucleus", "object-entity": true}
|
||||
{"type": "definition", "entity": "RNA", "definition": "Molecule that carries genetic information"}
|
||||
{"type": "relationship", "subject": "RNA", "predicate": "transcribed_from", "object": "DNA", "object-entity": true}
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
**区分联合的模式使用 `oneOf`:**
|
||||
=======
|
||||
**区分联合的模式使用 `oneOf`:**
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
```json
|
||||
{
|
||||
"response-type": "jsonl",
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "const": "definition" },
|
||||
"entity": { "type": "string" },
|
||||
"definition": { "type": "string" }
|
||||
},
|
||||
"required": ["type", "entity", "definition"]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": { "const": "relationship" },
|
||||
"subject": { "type": "string" },
|
||||
"predicate": { "type": "string" },
|
||||
"object": { "type": "string" },
|
||||
"object-entity": { "type": "boolean" }
|
||||
},
|
||||
"required": ["type", "subject", "predicate", "object", "object-entity"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<<<<<<< HEAD
|
||||
#### 本体抽取
|
||||
|
||||
对于基于本体的抽取,涉及实体、关系和属性:
|
||||
=======
|
||||
#### 本体提取
|
||||
|
||||
对于基于本体的实体、关系和属性提取:
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
**提示输出格式:**
|
||||
```
|
||||
{"type": "entity", "entity": "Cornish pasty", "entity_type": "fo/Recipe"}
|
||||
{"type": "entity", "entity": "beef", "entity_type": "fo/Food"}
|
||||
{"type": "relationship", "subject": "Cornish pasty", "subject_type": "fo/Recipe", "relation": "fo/has_ingredient", "object": "beef", "object_type": "fo/Food"}
|
||||
{"type": "attribute", "entity": "Cornish pasty", "entity_type": "fo/Recipe", "attribute": "fo/serves", "value": "4 people"}
|
||||
```
|
||||
|
||||
### 实现细节
|
||||
|
||||
#### 提示类
|
||||
|
||||
现有的`Prompt`类不需要进行任何更改。 `schema`字段将被重用。
|
||||
用于 JSONL 格式,其解释由 `response_type` 决定:
|
||||
|
||||
```python
|
||||
class Prompt:
|
||||
def __init__(self, template, response_type="text", terms=None, schema=None):
|
||||
self.template = template
|
||||
self.response_type = response_type
|
||||
self.terms = terms
|
||||
self.schema = schema # Interpretation depends on response_type
|
||||
```
|
||||
|
||||
#### PromptManager.load_config
|
||||
|
||||
无需修改 - 现有的配置加载已经处理了
|
||||
`schema` 键。
|
||||
|
||||
#### JSONL 解析
|
||||
|
||||
添加一种新的解析方法,用于解析 JSONL 格式的响应:
|
||||
|
||||
```python
|
||||
def parse_jsonl(self, text):
|
||||
"""
|
||||
Parse JSONL response, returning list of valid objects.
|
||||
|
||||
Invalid lines (malformed JSON, empty lines) are skipped with warnings.
|
||||
This provides truncation resilience - partial output yields partial results.
|
||||
"""
|
||||
results = []
|
||||
|
||||
for line_num, line in enumerate(text.strip().split('\n'), 1):
|
||||
line = line.strip()
|
||||
|
||||
# Skip empty lines
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Skip markdown code fence markers if present
|
||||
if line.startswith('```'):
|
||||
continue
|
||||
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
results.append(obj)
|
||||
except json.JSONDecodeError as e:
|
||||
# Log warning but continue - this provides truncation resilience
|
||||
logger.warning(f"JSONL parse error on line {line_num}: {e}")
|
||||
|
||||
return results
|
||||
```
|
||||
|
||||
#### PromptManager.invoke 变更
|
||||
|
||||
扩展 invoke 方法以处理新的响应类型:
|
||||
|
||||
```python
|
||||
async def invoke(self, id, input, llm):
|
||||
logger.debug("Invoking prompt template...")
|
||||
|
||||
terms = self.terms | self.prompts[id].terms | input
|
||||
resp_type = self.prompts[id].response_type
|
||||
|
||||
prompt = {
|
||||
"system": self.system_template.render(terms),
|
||||
"prompt": self.render(id, input)
|
||||
}
|
||||
|
||||
resp = await llm(**prompt)
|
||||
|
||||
if resp_type == "text":
|
||||
return resp
|
||||
|
||||
if resp_type == "json":
|
||||
try:
|
||||
obj = self.parse_json(resp)
|
||||
except:
|
||||
logger.error(f"JSON parse failed: {resp}")
|
||||
raise RuntimeError("JSON parse fail")
|
||||
|
||||
if self.prompts[id].schema:
|
||||
try:
|
||||
validate(instance=obj, schema=self.prompts[id].schema)
|
||||
logger.debug("Schema validation successful")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Schema validation fail: {e}")
|
||||
|
||||
return obj
|
||||
|
||||
if resp_type == "jsonl":
|
||||
objects = self.parse_jsonl(resp)
|
||||
|
||||
if not objects:
|
||||
logger.warning("JSONL parse returned no valid objects")
|
||||
return []
|
||||
|
||||
# Validate each object against schema if provided
|
||||
if self.prompts[id].schema:
|
||||
validated = []
|
||||
for i, obj in enumerate(objects):
|
||||
try:
|
||||
validate(instance=obj, schema=self.prompts[id].schema)
|
||||
validated.append(obj)
|
||||
except Exception as e:
|
||||
logger.warning(f"Object {i} failed schema validation: {e}")
|
||||
return validated
|
||||
|
||||
return objects
|
||||
|
||||
raise RuntimeError(f"Response type {resp_type} not known")
|
||||
```
|
||||
|
||||
### 受影响的提示语
|
||||
|
||||
以下提示语应迁移到 JSONL 格式:
|
||||
|
||||
| 提示语 ID | 描述 | 类型字段 |
|
||||
|-----------|-------------|------------|
|
||||
| `extract-definitions` | 实体/定义提取 | 否(单个类型)|
|
||||
| `extract-relationships` | 关系提取 | 否(单个类型)|
|
||||
| `extract-topics` | 主题/定义提取 | 否(单个类型)|
|
||||
| `extract-rows` | 结构化行提取 | 否(单个类型)|
|
||||
| `agent-kg-extract` | 组合定义 + 关系提取 | 是:`"definition"`, `"relationship"` |
|
||||
| `extract-with-ontologies` / `ontology-extract` | 基于本体的提取 | 是:`"entity"`, `"relationship"`, `"attribute"` |
|
||||
|
||||
### API 变更
|
||||
|
||||
#### 客户端视角
|
||||
|
||||
JSONL 解析对提示服务 API 的调用者是透明的。解析过程
|
||||
在提示服务的服务器端进行,响应通过标准的
|
||||
`PromptResponse.object` 字段以序列化的 JSON 数组形式返回。
|
||||
|
||||
当客户端调用提示服务(通过 `PromptClient.prompt()` 或类似方式时):
|
||||
|
||||
<<<<<<< HEAD
|
||||
**`response-type: "json"`**(带有数组模式)→ 客户端接收 Python `list`
|
||||
**`response-type: "jsonl"`** → 客户端接收 Python `list`
|
||||
|
||||
从客户端的角度来看,两者都返回相同的的数据结构。
|
||||
=======
|
||||
**`response-type: "json"`** (带有数组模式) → 客户端接收 Python `list`
|
||||
**`response-type: "jsonl"`** → 客户端接收 Python `list`
|
||||
|
||||
从客户端的角度来看,两者都返回相同的数据结构。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
区别完全在于服务器端如何解析 LLM 的输出:
|
||||
|
||||
JSON 数组格式:单个 `json.loads()` 调用;如果被截断,则完全失败。
|
||||
JSONL 格式:逐行解析;如果被截断,则产生部分结果。
|
||||
|
||||
这意味着,现有的客户端代码期望从提取提示中获得列表,
|
||||
在将提示从 JSON 迁移到 JSONL 格式时,不需要进行任何更改。
|
||||
|
||||
#### 服务器返回值
|
||||
|
||||
对于 `response-type: "jsonl"`,`PromptManager.invoke()` 方法返回一个
|
||||
`list[dict]`,其中包含所有成功解析和验证的对象。 此
|
||||
列表随后被序列化为 JSON,用于 `PromptResponse.object` 字段。
|
||||
|
||||
#### 错误处理
|
||||
|
||||
空结果:返回一个空列表 `[]`,并带有警告日志。
|
||||
部分解析失败:返回成功解析的对象列表,
|
||||
并为解析失败的情况记录警告日志。
|
||||
完全解析失败:返回一个空列表 `[]`,并带有警告日志。
|
||||
|
||||
这与 `response-type: "json"` 不同,后者会在解析失败时引发 `RuntimeError`。
|
||||
对于 JSONL 的宽松处理是故意的,目的是为了提供截断恢复能力。
|
||||
|
||||
|
||||
### 配置示例
|
||||
|
||||
完整的提示配置示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "Extract all entities and their definitions from the following text. Output one JSON object per line.\n\nText:\n{{text}}\n\nOutput format per line:\n{\"entity\": \"<name>\", \"definition\": \"<definition>\"}",
|
||||
"response-type": "jsonl",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"entity": {
|
||||
"type": "string",
|
||||
"description": "The entity name"
|
||||
},
|
||||
"definition": {
|
||||
"type": "string",
|
||||
"description": "A clear definition of the entity"
|
||||
}
|
||||
},
|
||||
"required": ["entity", "definition"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
**输入验证**: JSON 解析使用标准的 `json.loads()`,这可以防止注入攻击。
|
||||
**模式验证**: 使用 ⟦CODE_0⟧ 来强制执行模式。
|
||||
**模式验证**: 使用 `jsonschema.validate()` 进行模式强制。
|
||||
<<<<<<< HEAD
|
||||
**没有新的攻击面:** JSONL 解析比 JSON 数组解析更安全,因为它采用逐行处理的方式。
|
||||
=======
|
||||
**无新的攻击面**: JSONL 解析比 JSON 数组解析更安全,因为采用逐行处理的方式。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
parsing due to line-by-line processing
|
||||
|
||||
## 性能考量
|
||||
|
||||
**内存**: 逐行解析比加载完整的 JSON 数组消耗更少的峰值内存。
|
||||
**延迟**: 解析性能与 JSON 数组解析相当。
|
||||
**验证**: 模式验证按对象进行,这会增加开销,但
|
||||
可以在验证失败时提供部分结果。
|
||||
|
||||
## 测试策略
|
||||
|
||||
|
||||
### 单元测试
|
||||
|
||||
使用有效输入的 JSONL 解析
|
||||
使用空行的 JSONL 解析
|
||||
使用 Markdown 代码块的 JSONL 解析
|
||||
使用截断的最终行的 JSONL 解析
|
||||
包含穿插无效 JSON 行的 JSONL 解析
|
||||
使用 `oneOf` 区分联合的模式验证
|
||||
<<<<<<< HEAD
|
||||
向后兼容性:现有的 `"text"` 和 `"json"` 提示词保持不变
|
||||
|
||||
### 集成测试
|
||||
|
||||
使用 JSONL 提示词的端到端提取
|
||||
=======
|
||||
向后兼容性:现有的 `"text"` 和 `"json"` 提示保持不变
|
||||
|
||||
### 集成测试
|
||||
|
||||
使用 JSONL 提示的端到端提取
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
使用模拟截断的提取(人为限制响应)
|
||||
使用类型区分器的混合类型提取
|
||||
使用所有三种类型的本体提取
|
||||
|
||||
### 提取质量测试
|
||||
|
||||
比较提取结果:JSONL 与 JSON 数组格式
|
||||
验证截断恢复能力:JSONL 在 JSON 失败的情况下,可以产生部分结果
|
||||
|
||||
## 迁移计划
|
||||
|
||||
### 第一阶段:实施
|
||||
|
||||
1. 在 `parse_jsonl()` 中实现 `PromptManager` 方法
|
||||
2. 扩展 `invoke()` 以处理 `response-type: "jsonl"`
|
||||
3. 添加单元测试
|
||||
|
||||
<<<<<<< HEAD
|
||||
### 第二阶段:提示词迁移
|
||||
|
||||
1. 更新 `extract-definitions` 提示词和配置
|
||||
2. 更新 `extract-relationships` 提示词和配置
|
||||
3. 更新 `extract-topics` 提示词和配置
|
||||
4. 更新 `extract-rows` 提示词和配置
|
||||
5. 更新 `agent-kg-extract` 提示词和配置
|
||||
6. 更新 `extract-with-ontologies` 提示词和配置
|
||||
=======
|
||||
### 第二阶段:提示迁移
|
||||
|
||||
1. 更新 `extract-definitions` 提示和配置
|
||||
2. 更新 `extract-relationships` 提示和配置
|
||||
3. 更新 `extract-topics` 提示和配置
|
||||
4. 更新 `extract-rows` 提示和配置
|
||||
5. 更新 `agent-kg-extract` 提示和配置
|
||||
6. 更新 `extract-with-ontologies` 提示和配置
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
### 第三阶段:下游更新
|
||||
|
||||
1. 更新任何使用提取结果的代码,使其能够处理列表返回类型。
|
||||
2. 更新根据 `type` 字段对混合类型提取进行分类的代码。
|
||||
<<<<<<< HEAD
|
||||
3. 更新用于断言提取输出格式的测试。
|
||||
=======
|
||||
3. 更新断言提取输出格式的测试。
|
||||
>>>>>>> 82edf2d (New md files from RunPod)
|
||||
|
||||
## 待解决问题
|
||||
|
||||
目前没有。
|
||||
|
||||
## 参考文献
|
||||
|
||||
当前实现:`trustgraph-flow/trustgraph/template/prompt_manager.py`
|
||||
JSON Lines 规范:https://jsonlines.org/
|
||||
JSON Schema `oneOf`:https://json-schema.org/understanding-json-schema/reference/combining.html#oneof
|
||||
相关规范:Streaming LLM Responses (`docs/tech-specs/streaming-llm-responses.md`)
|
||||
992
docs/tech-specs/zh-cn/large-document-loading.zh-cn.md
Normal file
992
docs/tech-specs/zh-cn/large-document-loading.zh-cn.md
Normal file
|
|
@ -0,0 +1,992 @@
|
|||
---
|
||||
layout: default
|
||||
title: "大型文档加载技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 大型文档加载技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范解决了在将大型文档加载到 TrustGraph 时出现的可扩展性和用户体验问题。 当前架构将文档上传视为单个原子操作,这会在管道的多个阶段导致内存压力,并且不为用户提供任何反馈或恢复选项。
|
||||
|
||||
本次实施的目标是以下用例:
|
||||
|
||||
|
||||
1. **大型 PDF 处理**: 在不耗尽内存的情况下,上传和处理数百兆字节的 PDF 文件。
|
||||
|
||||
2. **可恢复的上传**: 允许中断的上传从中断的地方继续,而不是重新开始。
|
||||
|
||||
3. **进度反馈**: 向用户提供有关上传和处理进度的实时可见性。
|
||||
|
||||
4. **内存高效处理**: 以流式方式处理文档,而无需将整个文件保存在内存中。
|
||||
|
||||
|
||||
## 目标
|
||||
|
||||
|
||||
**增量上传**: 支持通过 REST 和 WebSocket 进行分块文档上传。
|
||||
**可恢复传输**: 启用从中断的上传中恢复。
|
||||
**进度可见性**: 向客户端提供上传/处理进度反馈。
|
||||
**内存效率**: 消除整个管道中的完整文档缓冲。
|
||||
**向后兼容性**: 现有的小型文档工作流程保持不变。
|
||||
**流式处理**: PDF 解码和文本分块操作在流上进行。
|
||||
|
||||
## 背景
|
||||
|
||||
|
||||
### 当前架构
|
||||
|
||||
文档提交流程如下:
|
||||
|
||||
1. **客户端** 通过 REST (`POST /api/v1/librarian`) 或 WebSocket 提交文档。
|
||||
2. **API 网关** 接收包含 base64 编码的文档内容的完整请求。
|
||||
3. **LibrarianRequestor** 将请求转换为 Pulsar 消息。
|
||||
4. **Librarian 服务** 接收消息,将文档解码到内存中。
|
||||
5. **BlobStore** 将文档上传到 Garage/S3。
|
||||
6. **Cassandra** 存储包含对象引用的元数据。
|
||||
7. 为了处理:从 S3 中检索文档,解码,分块——所有都在内存中。
|
||||
|
||||
关键文件:
|
||||
REST/WebSocket 入口:`trustgraph-flow/trustgraph/gateway/service.py`
|
||||
Librarian 核心:`trustgraph-flow/trustgraph/librarian/librarian.py`
|
||||
Blob 存储:`trustgraph-flow/trustgraph/librarian/blob_store.py`
|
||||
Cassandra 表:`trustgraph-flow/trustgraph/tables/library.py`
|
||||
API 模式:`trustgraph-base/trustgraph/schema/services/library.py`
|
||||
|
||||
### 当前限制
|
||||
|
||||
当前设计存在几个相互影响的内存和用户体验问题:
|
||||
|
||||
1. **原子上传操作**: 整个文档必须在一个请求中传输。
|
||||
大型文档需要长时间运行的请求,并且在连接失败时没有进度指示和重试机制。
|
||||
|
||||
|
||||
2. **API 设计**: 无论是 REST 还是 WebSocket API,都期望在单个消息中接收完整的文档。
|
||||
模式 (`LibrarianRequest`) 包含一个 `content` 字段,其中包含整个 base64 编码的文档。
|
||||
|
||||
|
||||
3. **Librarian 内存**: librarian 服务在将文档上传到 S3 之前,将其完全解码到内存中。
|
||||
对于一个 500MB 的 PDF 文件,这意味着需要在进程内存中保留 500MB+。
|
||||
|
||||
|
||||
4. **PDF 解码器内存**: 当处理开始时,PDF 解码器将整个 PDF 文件加载到内存中以提取文本。
|
||||
PyPDF 和类似的库通常需要访问整个文档。
|
||||
|
||||
|
||||
5. **分块器内存**: 文本分块器接收完整的提取文本,并在生成块时将其保存在内存中。
|
||||
|
||||
|
||||
**内存影响示例** (500MB PDF):
|
||||
网关:~700MB (base64 编码开销)
|
||||
Librarian:~500MB (解码后的字节)
|
||||
PDF 解码器:~500MB + 提取缓冲区
|
||||
分块器:提取的文本 (可变,可能高达 100MB+)
|
||||
|
||||
对于单个大型文档,峰值内存总和可能超过 2GB。
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 设计原则
|
||||
|
||||
1. **API 接口**: 所有客户端交互都通过 librarian API 进行。客户端
|
||||
没有直接访问或了解底层 S3/Garage 存储的权限。
|
||||
|
||||
2. **S3 多部分上传**: 使用标准的 S3 多部分上传。
|
||||
这在所有兼容 S3 的系统中都得到广泛支持 (AWS S3, MinIO, Garage,
|
||||
Ceph, DigitalOcean Spaces, Backblaze B2 等),确保可移植性。
|
||||
|
||||
3. **原子完成**: S3 多部分上传本质上是原子的 - 上传的
|
||||
部分在调用 `CompleteMultipartUpload` 之前是不可见的。不需要临时
|
||||
文件或重命名操作。
|
||||
|
||||
4. **可跟踪的状态**: 上传会话在 Cassandra 中跟踪,从而
|
||||
提供对未完成上传的可见性,并启用恢复功能。
|
||||
|
||||
### 分块上传流程
|
||||
|
||||
```
|
||||
Client Librarian API S3/Garage
|
||||
│ │ │
|
||||
│── begin-upload ───────────►│ │
|
||||
│ (metadata, size) │── CreateMultipartUpload ────►│
|
||||
│ │◄── s3_upload_id ─────────────│
|
||||
│◄── upload_id ──────────────│ (store session in │
|
||||
│ │ Cassandra) │
|
||||
│ │ │
|
||||
│── upload-chunk ───────────►│ │
|
||||
│ (upload_id, index, data) │── UploadPart ───────────────►│
|
||||
│ │◄── etag ─────────────────────│
|
||||
│◄── ack + progress ─────────│ (store etag in session) │
|
||||
│ ⋮ │ ⋮ │
|
||||
│ (repeat for all chunks) │ │
|
||||
│ │ │
|
||||
│── complete-upload ────────►│ │
|
||||
│ (upload_id) │── CompleteMultipartUpload ──►│
|
||||
│ │ (parts coalesced by S3) │
|
||||
│ │── store doc metadata ───────►│ Cassandra
|
||||
│◄── document_id ────────────│ (delete session) │
|
||||
```
|
||||
|
||||
客户端从不直接与 S3 交互。 库(librarian)在内部将我们的分块上传 API 转换为 S3 的多部分操作。
|
||||
|
||||
### Librarian API 操作
|
||||
### 图书管理员API操作
|
||||
|
||||
#### `begin-upload`
|
||||
|
||||
初始化一个分块上传会话。
|
||||
|
||||
请求:
|
||||
```json
|
||||
{
|
||||
"operation": "begin-upload",
|
||||
"document-metadata": {
|
||||
"id": "doc-123",
|
||||
"kind": "application/pdf",
|
||||
"title": "Large Document",
|
||||
"user": "user-id",
|
||||
"tags": ["tag1", "tag2"]
|
||||
},
|
||||
"total-size": 524288000,
|
||||
"chunk-size": 5242880
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"upload-id": "upload-abc-123",
|
||||
"chunk-size": 5242880,
|
||||
"total-chunks": 100
|
||||
}
|
||||
```
|
||||
|
||||
馆员:
|
||||
1. 生成一个唯一的 `upload_id` 和 `object_id` (用于对象存储的 UUID)
|
||||
2. 调用 S3 `CreateMultipartUpload`,接收 `s3_upload_id`
|
||||
3. 在 Cassandra 中创建会话记录
|
||||
4. 将 `upload_id` 返回给客户端
|
||||
|
||||
#### `upload-chunk`
|
||||
|
||||
上传单个分块。
|
||||
|
||||
请求:
|
||||
```json
|
||||
{
|
||||
"operation": "upload-chunk",
|
||||
"upload-id": "upload-abc-123",
|
||||
"chunk-index": 0,
|
||||
"content": "<base64-encoded-chunk>"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"upload-id": "upload-abc-123",
|
||||
"chunk-index": 0,
|
||||
"chunks-received": 1,
|
||||
"total-chunks": 100,
|
||||
"bytes-received": 5242880,
|
||||
"total-bytes": 524288000
|
||||
}
|
||||
```
|
||||
|
||||
馆员:
|
||||
1. 通过 `upload_id` 查找会话。
|
||||
2. 验证所有权(用户必须与会话创建者匹配)。
|
||||
3. 使用块数据调用 S3 `UploadPart`,接收 `etag`。
|
||||
4. 使用块索引和 etag 更新会话记录。
|
||||
5. 将进度返回给客户端。
|
||||
|
||||
失败的块可以重试 - 只需要再次发送相同的 `chunk-index`。
|
||||
|
||||
#### `complete-upload`
|
||||
|
||||
完成上传并创建文档。
|
||||
|
||||
请求:
|
||||
```json
|
||||
{
|
||||
"operation": "complete-upload",
|
||||
"upload-id": "upload-abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"document-id": "doc-123",
|
||||
"object-id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
}
|
||||
```
|
||||
|
||||
馆员:
|
||||
1. 查找会话,验证是否接收到所有分块。
|
||||
2. 使用 part etags 调用 S3 `CompleteMultipartUpload` (S3 内部合并分块,馆员无需消耗内存)。
|
||||
3. 在 Cassandra 中创建文档记录,包含元数据和对象引用。
|
||||
4. 删除上传会话记录。
|
||||
5. 将文档 ID 返回给客户端。
|
||||
|
||||
|
||||
#### `abort-upload`
|
||||
|
||||
取消正在进行的上传。
|
||||
|
||||
请求:
|
||||
```json
|
||||
{
|
||||
"operation": "abort-upload",
|
||||
"upload-id": "upload-abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
馆员:
|
||||
1. 调用 S3 `AbortMultipartUpload` 清理部分数据。
|
||||
2. 从 Cassandra 中删除会话记录。
|
||||
|
||||
#### `get-upload-status`
|
||||
|
||||
查询上传状态(用于断点续传功能)。
|
||||
|
||||
请求:
|
||||
```json
|
||||
{
|
||||
"operation": "get-upload-status",
|
||||
"upload-id": "upload-abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"upload-id": "upload-abc-123",
|
||||
"state": "in-progress",
|
||||
"chunks-received": [0, 1, 2, 5, 6],
|
||||
"missing-chunks": [3, 4, 7, 8],
|
||||
"total-chunks": 100,
|
||||
"bytes-received": 36700160,
|
||||
"total-bytes": 524288000
|
||||
}
|
||||
```
|
||||
|
||||
#### `list-uploads`
|
||||
|
||||
获取用户未完成上传的列表。
|
||||
|
||||
请求:
|
||||
```json
|
||||
{
|
||||
"operation": "list-uploads"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"uploads": [
|
||||
{
|
||||
"upload-id": "upload-abc-123",
|
||||
"document-metadata": { "title": "Large Document", ... },
|
||||
"progress": { "chunks-received": 43, "total-chunks": 100 },
|
||||
"created-at": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 上传会话存储
|
||||
|
||||
在 Cassandra 中跟踪正在进行的上传:
|
||||
|
||||
```sql
|
||||
CREATE TABLE upload_session (
|
||||
upload_id text PRIMARY KEY,
|
||||
user text,
|
||||
document_id text,
|
||||
document_metadata text, -- JSON: title, kind, tags, comments, etc.
|
||||
s3_upload_id text, -- internal, for S3 operations
|
||||
object_id uuid, -- target blob ID
|
||||
total_size bigint,
|
||||
chunk_size int,
|
||||
total_chunks int,
|
||||
chunks_received map<int, text>, -- chunk_index → etag
|
||||
created_at timestamp,
|
||||
updated_at timestamp
|
||||
) WITH default_time_to_live = 86400; -- 24 hour TTL
|
||||
|
||||
CREATE INDEX upload_session_user ON upload_session (user);
|
||||
```
|
||||
|
||||
**TTL 行为:**
|
||||
如果未完成,会话将在 24 小时后过期。
|
||||
当 Cassandra TTL 过期时,会话记录将被删除。
|
||||
孤立的 S3 分片由 S3 生命周期策略清理(在桶上配置)。
|
||||
|
||||
### 故障处理和原子性
|
||||
|
||||
**分块上传失败:**
|
||||
客户端重试失败的分块(使用相同的 `upload_id` 和 `chunk-index`)。
|
||||
S3 `UploadPart` 对于相同的分片编号是幂等的。
|
||||
会话跟踪哪些分块已成功。
|
||||
|
||||
**客户端在上传过程中断连接:**
|
||||
会话仍然存在于 Cassandra 中,记录了已接收的分块。
|
||||
客户端可以调用 `get-upload-status` 以查看缺少的内容。
|
||||
通过仅上传缺失的分块来恢复,然后调用 `complete-upload`。
|
||||
|
||||
**完整上传失败:**
|
||||
S3 `CompleteMultipartUpload` 是原子性的 - 要么完全成功,要么完全失败。
|
||||
如果失败,分片仍然存在,客户端可以重试 `complete-upload`。
|
||||
永远不会出现部分文档。
|
||||
|
||||
**会话过期:**
|
||||
Cassandra TTL 在 24 小时后删除会话记录。
|
||||
S3 桶生命周期策略清理不完整的多部分上传。
|
||||
不需要手动清理。
|
||||
|
||||
### S3 多部分原子性
|
||||
|
||||
S3 多部分上传提供了内置的原子性:
|
||||
|
||||
1. **分片不可见:** 上传的分片不能作为对象访问。
|
||||
它们仅作为不完整的多部分上传的分片存在。
|
||||
|
||||
2. **原子完成:** `CompleteMultipartUpload` 要么成功(对象
|
||||
以原子方式出现),要么失败(未创建对象)。 没有部分状态。
|
||||
|
||||
3. **无需重命名:** 最终对象键在
|
||||
`CreateMultipartUpload` 时指定。 分片直接合并到该键。
|
||||
|
||||
4. **服务器端合并:** S3 内部合并分片。 库管理员
|
||||
永远不会读取分片 - 无论文档大小如何,都没有内存开销。
|
||||
|
||||
### BlobStore 扩展
|
||||
|
||||
**文件:** `trustgraph-flow/trustgraph/librarian/blob_store.py`
|
||||
|
||||
添加多部分上传方法:
|
||||
|
||||
```python
|
||||
class BlobStore:
|
||||
# Existing methods...
|
||||
|
||||
def create_multipart_upload(self, object_id: UUID, kind: str) -> str:
|
||||
"""Initialize multipart upload, return s3_upload_id."""
|
||||
# minio client: create_multipart_upload()
|
||||
|
||||
def upload_part(
|
||||
self, object_id: UUID, s3_upload_id: str,
|
||||
part_number: int, data: bytes
|
||||
) -> str:
|
||||
"""Upload a single part, return etag."""
|
||||
# minio client: upload_part()
|
||||
# Note: S3 part numbers are 1-indexed
|
||||
|
||||
def complete_multipart_upload(
|
||||
self, object_id: UUID, s3_upload_id: str,
|
||||
parts: List[Tuple[int, str]] # [(part_number, etag), ...]
|
||||
) -> None:
|
||||
"""Finalize multipart upload."""
|
||||
# minio client: complete_multipart_upload()
|
||||
|
||||
def abort_multipart_upload(
|
||||
self, object_id: UUID, s3_upload_id: str
|
||||
) -> None:
|
||||
"""Cancel multipart upload, clean up parts."""
|
||||
# minio client: abort_multipart_upload()
|
||||
```
|
||||
|
||||
### 分块大小的考量
|
||||
|
||||
**S3 最小值**: 每个分块 5MB (除了最后一个分块)
|
||||
**S3 最大值**: 每个上传 10,000 个分块
|
||||
**实际默认值**: 5MB 分块
|
||||
500MB 文档 = 100 个分块
|
||||
5GB 文档 = 1,000 个分块
|
||||
**进度粒度**: 较小的分块 = 更精细的进度更新
|
||||
**网络效率**: 较大的分块 = 更少的网络请求
|
||||
|
||||
分块大小可以在一定范围内由客户端配置 (5MB - 100MB)。
|
||||
|
||||
### 文档处理:流式检索
|
||||
|
||||
上传流程旨在高效地将文档存储到存储系统中。处理流程旨在提取和分块文档,而无需将整个文档加载到内存中。
|
||||
|
||||
|
||||
|
||||
#### 设计原则:标识符,而非内容
|
||||
|
||||
目前,当触发处理时,文档内容通过 Pulsar 消息传递。这会将整个文档加载到内存中。 应该这样做:
|
||||
|
||||
|
||||
Pulsar 消息仅携带 **文档标识符**
|
||||
处理程序直接从 librarian 获取文档内容
|
||||
获取过程是 **流式传输到临时文件**
|
||||
文档特定的解析 (PDF、文本等) 使用文件,而不是内存缓冲区
|
||||
|
||||
这使得 librarian 不依赖于文档结构。 PDF 解析、文本
|
||||
提取和其他特定于格式的逻辑保留在各自的解码器中。
|
||||
|
||||
#### 处理流程
|
||||
|
||||
```
|
||||
Pulsar PDF Decoder Librarian S3
|
||||
│ │ │ │
|
||||
│── doc-id ───────────►│ │ │
|
||||
│ (processing msg) │ │ │
|
||||
│ │ │ │
|
||||
│ │── stream-document ──────►│ │
|
||||
│ │ (doc-id) │── GetObject ────►│
|
||||
│ │ │ │
|
||||
│ │◄── chunk ────────────────│◄── stream ───────│
|
||||
│ │ (write to temp file) │ │
|
||||
│ │◄── chunk ────────────────│◄── stream ───────│
|
||||
│ │ (append to temp file) │ │
|
||||
│ │ ⋮ │ ⋮ │
|
||||
│ │◄── EOF ──────────────────│ │
|
||||
│ │ │ │
|
||||
│ │ ┌──────────────────────────┐ │
|
||||
│ │ │ temp file on disk │ │
|
||||
│ │ │ (memory stays bounded) │ │
|
||||
│ │ └────────────┬─────────────┘ │
|
||||
│ │ │ │
|
||||
│ │ PDF library opens file │
|
||||
│ │ extract page 1 text ──► chunker │
|
||||
│ │ extract page 2 text ──► chunker │
|
||||
│ │ ⋮ │
|
||||
│ │ close file │
|
||||
│ │ delete temp file │
|
||||
```
|
||||
|
||||
#### 图书管理员流式 API
|
||||
|
||||
添加一个流式文档检索操作:
|
||||
|
||||
**`stream-document`**
|
||||
|
||||
请求:
|
||||
```json
|
||||
{
|
||||
"operation": "stream-document",
|
||||
"document-id": "doc-123"
|
||||
}
|
||||
```
|
||||
|
||||
响应:流式传输的二进制数据块(不是单个响应)。
|
||||
|
||||
对于 REST API,这会返回一个带有 `Transfer-Encoding: chunked` 的流式响应。
|
||||
|
||||
对于内部服务之间的调用(处理器到图书管理员),可能如下:
|
||||
通过预签名 URL 进行直接 S3 流式传输(如果内部网络允许)。
|
||||
通过服务协议进行分块响应。
|
||||
一个专用的流式传输端点。
|
||||
|
||||
关键要求:数据以块的形式流动,永远不会完全缓存在图书管理员中。
|
||||
|
||||
#### PDF 解码器更改
|
||||
|
||||
**当前实现**(占用大量内存):
|
||||
|
||||
```python
|
||||
def decode_pdf(document_content: bytes) -> str:
|
||||
reader = PdfReader(BytesIO(document_content)) # full doc in memory
|
||||
text = ""
|
||||
for page in reader.pages:
|
||||
text += page.extract_text() # accumulating
|
||||
return text # full text in memory
|
||||
```
|
||||
|
||||
**新实现** (临时文件,增量式):
|
||||
|
||||
```python
|
||||
def decode_pdf_streaming(doc_id: str, librarian_client) -> Iterator[str]:
|
||||
"""Yield extracted text page by page."""
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=True, suffix='.pdf') as tmp:
|
||||
# Stream document to temp file
|
||||
for chunk in librarian_client.stream_document(doc_id):
|
||||
tmp.write(chunk)
|
||||
tmp.flush()
|
||||
|
||||
# Open PDF from file (not memory)
|
||||
reader = PdfReader(tmp.name)
|
||||
|
||||
# Yield pages incrementally
|
||||
for page in reader.pages:
|
||||
yield page.extract_text()
|
||||
|
||||
# tmp file auto-deleted on context exit
|
||||
```
|
||||
|
||||
内存配置:
|
||||
临时文件在磁盘上:大小为 PDF 文件的大小(磁盘很便宜)
|
||||
内存中:一次加载一页的文本
|
||||
峰值内存:受限,与文档大小无关
|
||||
|
||||
#### 文本文档解码器更改
|
||||
|
||||
对于纯文本文档,更加简单 - 不需要临时文件:
|
||||
|
||||
```python
|
||||
def decode_text_streaming(doc_id: str, librarian_client) -> Iterator[str]:
|
||||
"""Yield text in chunks as it streams from storage."""
|
||||
|
||||
buffer = ""
|
||||
for chunk in librarian_client.stream_document(doc_id):
|
||||
buffer += chunk.decode('utf-8')
|
||||
|
||||
# Yield complete lines/paragraphs as they arrive
|
||||
while '\n\n' in buffer:
|
||||
paragraph, buffer = buffer.split('\n\n', 1)
|
||||
yield paragraph + '\n\n'
|
||||
|
||||
# Yield remaining buffer
|
||||
if buffer:
|
||||
yield buffer
|
||||
```
|
||||
|
||||
文本文件可以直接流式传输,无需临时文件,因为它们的结构是线性的。
|
||||
|
||||
|
||||
#### 流式分块集成
|
||||
|
||||
分块器接收文本的迭代器(页面或段落),并逐步生成块:
|
||||
|
||||
|
||||
```python
|
||||
class StreamingChunker:
|
||||
def __init__(self, chunk_size: int, overlap: int):
|
||||
self.chunk_size = chunk_size
|
||||
self.overlap = overlap
|
||||
|
||||
def process(self, text_stream: Iterator[str]) -> Iterator[str]:
|
||||
"""Yield chunks as text arrives."""
|
||||
buffer = ""
|
||||
|
||||
for text_segment in text_stream:
|
||||
buffer += text_segment
|
||||
|
||||
while len(buffer) >= self.chunk_size:
|
||||
chunk = buffer[:self.chunk_size]
|
||||
yield chunk
|
||||
# Keep overlap for context continuity
|
||||
buffer = buffer[self.chunk_size - self.overlap:]
|
||||
|
||||
# Yield remaining buffer as final chunk
|
||||
if buffer.strip():
|
||||
yield buffer
|
||||
```
|
||||
|
||||
#### 端到端处理流程
|
||||
|
||||
```python
|
||||
async def process_document(doc_id: str, librarian_client, embedder):
|
||||
"""Process document with bounded memory."""
|
||||
|
||||
# Get document metadata to determine type
|
||||
metadata = await librarian_client.get_document_metadata(doc_id)
|
||||
|
||||
# Select decoder based on document type
|
||||
if metadata.kind == 'application/pdf':
|
||||
text_stream = decode_pdf_streaming(doc_id, librarian_client)
|
||||
elif metadata.kind == 'text/plain':
|
||||
text_stream = decode_text_streaming(doc_id, librarian_client)
|
||||
else:
|
||||
raise UnsupportedDocumentType(metadata.kind)
|
||||
|
||||
# Chunk incrementally
|
||||
chunker = StreamingChunker(chunk_size=1000, overlap=100)
|
||||
|
||||
# Process each chunk as it's produced
|
||||
for chunk in chunker.process(text_stream):
|
||||
# Generate embeddings, store in vector DB, etc.
|
||||
embedding = await embedder.embed(chunk)
|
||||
await store_chunk(doc_id, chunk, embedding)
|
||||
```
|
||||
|
||||
在任何时候,完整的文档或完整的提取文本都不会保存在内存中。
|
||||
|
||||
#### 临时文件注意事项
|
||||
|
||||
**位置:** 使用系统临时目录(`/tmp` 或等效目录)。对于
|
||||
容器化部署,请确保临时目录有足够的空间,并且位于快速存储上(如果可能,不要使用网络挂载)。
|
||||
|
||||
|
||||
**清理:** 使用上下文管理器(`with tempfile...`)以确保即使在出现异常时也能进行清理。
|
||||
|
||||
|
||||
**并发处理:** 每个处理任务都有自己的临时文件。
|
||||
并行文档处理之间不会发生冲突。
|
||||
|
||||
**磁盘空间:** 临时文件是短暂存在的(处理过程中的持续时间)。
|
||||
对于一个 500MB 的 PDF 文件,在处理过程中需要 500MB 的临时空间。如果磁盘空间受限,可以在上传时强制执行大小限制。
|
||||
|
||||
|
||||
### 统一处理接口:子文档
|
||||
|
||||
PDF 提取和文本文档处理需要输入到相同的下游流程(分块 → 嵌入 → 存储)。为了实现这一点,并使用一致的“按 ID 检索”接口,提取的文本块被存储回 librarian,作为子文档。
|
||||
|
||||
|
||||
|
||||
|
||||
#### 使用子文档的处理流程
|
||||
|
||||
```
|
||||
PDF Document Text Document
|
||||
│ │
|
||||
▼ │
|
||||
pdf-extractor │
|
||||
│ │
|
||||
│ (stream PDF from librarian) │
|
||||
│ (extract page 1 text) │
|
||||
│ (store as child doc → librarian) │
|
||||
│ (extract page 2 text) │
|
||||
│ (store as child doc → librarian) │
|
||||
│ ⋮ │
|
||||
▼ ▼
|
||||
[child-doc-id, child-doc-id, ...] [doc-id]
|
||||
│ │
|
||||
└─────────────────────┬───────────────────────────────┘
|
||||
▼
|
||||
chunker
|
||||
│
|
||||
│ (receives document ID)
|
||||
│ (streams content from librarian)
|
||||
│ (chunks incrementally)
|
||||
▼
|
||||
[chunks → embedding → storage]
|
||||
```
|
||||
|
||||
分块器具有一个统一的接口:
|
||||
接收文档 ID(通过 Pulsar)
|
||||
从 Librarian 流式传输内容
|
||||
将其分块
|
||||
|
||||
它不知道也不关心该 ID 指的是:
|
||||
用户上传的文本文档
|
||||
从 PDF 页面提取的文本块
|
||||
任何未来的文档类型
|
||||
|
||||
#### 子文档元数据
|
||||
|
||||
扩展文档模式以跟踪父/子关系:
|
||||
|
||||
```sql
|
||||
-- Add columns to document table
|
||||
ALTER TABLE document ADD parent_id text;
|
||||
ALTER TABLE document ADD document_type text;
|
||||
|
||||
-- Index for finding children of a parent
|
||||
CREATE INDEX document_parent ON document (parent_id);
|
||||
```
|
||||
|
||||
**文档类型:**
|
||||
|
||||
| `document_type` | 描述 |
|
||||
|-----------------|-------------|
|
||||
| `source` | 用户上传的文档(PDF、文本等) |
|
||||
| `extracted` | 源自源文档(例如,PDF 页面文本) |
|
||||
|
||||
**元数据字段:**
|
||||
|
||||
| 字段 | 源文档 | 提取的子文档 |
|
||||
|-------|-----------------|-----------------|
|
||||
| `id` | 用户提供或生成 | 生成(例如,`{parent-id}-page-{n}`) |
|
||||
| `parent_id` | `NULL` | 父文档 ID |
|
||||
| `document_type` | `source` | `extracted` |
|
||||
| `kind` | `application/pdf`,等等 | `text/plain` |
|
||||
| `title` | 用户提供 | 生成(例如,“Report.pdf 的第 3 页”) |
|
||||
| `user` | 经过身份验证的用户 | 与父文档相同 |
|
||||
|
||||
#### 子文档的 Librarian API
|
||||
|
||||
**创建子文档**(内部,由 pdf-extractor 使用):
|
||||
|
||||
```json
|
||||
{
|
||||
"operation": "add-child-document",
|
||||
"parent-id": "doc-123",
|
||||
"document-metadata": {
|
||||
"id": "doc-123-page-1",
|
||||
"kind": "text/plain",
|
||||
"title": "Page 1"
|
||||
},
|
||||
"content": "<base64-encoded-text>"
|
||||
}
|
||||
```
|
||||
|
||||
对于较小的提取文本(典型页面文本通常小于 100KB),单次上传是可以接受的。对于非常大的文本提取,可以使用分块上传。
|
||||
|
||||
|
||||
|
||||
**列出子文档(用于调试/管理):**
|
||||
|
||||
```json
|
||||
{
|
||||
"operation": "list-children",
|
||||
"parent-id": "doc-123"
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
```json
|
||||
{
|
||||
"children": [
|
||||
{ "id": "doc-123-page-1", "title": "Page 1", "kind": "text/plain" },
|
||||
{ "id": "doc-123-page-2", "title": "Page 2", "kind": "text/plain" },
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 用户可见的行为
|
||||
|
||||
**`list-documents` 默认行为:**
|
||||
|
||||
```sql
|
||||
SELECT * FROM document WHERE user = ? AND parent_id IS NULL;
|
||||
```
|
||||
|
||||
只有顶层(源)文档才会出现在用户的文档列表中。
|
||||
子文档默认会被过滤掉。
|
||||
|
||||
**可选的包含子文档标志**(用于管理员/调试):
|
||||
|
||||
```json
|
||||
{
|
||||
"operation": "list-documents",
|
||||
"include-children": true
|
||||
}
|
||||
```
|
||||
|
||||
#### 级联删除
|
||||
|
||||
当父级文档被删除时,所有子级必须被删除:
|
||||
|
||||
```python
|
||||
def delete_document(doc_id: str):
|
||||
# Find all children
|
||||
children = query("SELECT id, object_id FROM document WHERE parent_id = ?", doc_id)
|
||||
|
||||
# Delete child blobs from S3
|
||||
for child in children:
|
||||
blob_store.delete(child.object_id)
|
||||
|
||||
# Delete child metadata from Cassandra
|
||||
execute("DELETE FROM document WHERE parent_id = ?", doc_id)
|
||||
|
||||
# Delete parent blob and metadata
|
||||
parent = get_document(doc_id)
|
||||
blob_store.delete(parent.object_id)
|
||||
execute("DELETE FROM document WHERE id = ? AND user = ?", doc_id, user)
|
||||
```
|
||||
|
||||
#### 存储注意事项
|
||||
|
||||
提取的文本块会重复内容:
|
||||
原始 PDF 文件存储在 Garage 中
|
||||
每个页面的提取文本也存储在 Garage 中
|
||||
|
||||
这种权衡方案可以实现:
|
||||
**统一的分块接口**: 分块器始终通过 ID 获取数据
|
||||
**恢复/重试**: 可以在分块阶段重新启动,而无需重新提取 PDF
|
||||
**调试**: 可以检查提取的文本
|
||||
**职责分离**: PDF 提取器和分块器是独立的服务
|
||||
|
||||
对于一个 500MB 的 PDF 文件,其中包含 200 页,平均每页 5KB 的文本:
|
||||
PDF 存储:500MB
|
||||
提取的文本存储:约 1MB
|
||||
额外开销:可以忽略不计
|
||||
|
||||
#### PDF 提取器输出
|
||||
|
||||
PDF 提取器在处理文档后:
|
||||
|
||||
1. 从 librarian 流式传输 PDF 到临时文件
|
||||
2. 逐页提取文本
|
||||
3. 对于每一页,将提取的文本作为子文档通过 librarian 存储
|
||||
4. 将子文档 ID 发送到分块器队列
|
||||
|
||||
```python
|
||||
async def extract_pdf(doc_id: str, librarian_client, output_queue):
|
||||
"""Extract PDF pages and store as child documents."""
|
||||
|
||||
with tempfile.NamedTemporaryFile(delete=True, suffix='.pdf') as tmp:
|
||||
# Stream PDF to temp file
|
||||
for chunk in librarian_client.stream_document(doc_id):
|
||||
tmp.write(chunk)
|
||||
tmp.flush()
|
||||
|
||||
# Extract pages
|
||||
reader = PdfReader(tmp.name)
|
||||
for page_num, page in enumerate(reader.pages, start=1):
|
||||
text = page.extract_text()
|
||||
|
||||
# Store as child document
|
||||
child_id = f"{doc_id}-page-{page_num}"
|
||||
await librarian_client.add_child_document(
|
||||
parent_id=doc_id,
|
||||
document_id=child_id,
|
||||
kind="text/plain",
|
||||
title=f"Page {page_num}",
|
||||
content=text.encode('utf-8')
|
||||
)
|
||||
|
||||
# Send to chunker queue
|
||||
await output_queue.send(child_id)
|
||||
```
|
||||
|
||||
分块器接收这些子 ID,并以与处理用户上传的文本文档完全相同的方式处理它们。
|
||||
如何处理用户上传的文本文档。
|
||||
|
||||
### 客户端更新
|
||||
|
||||
#### Python SDK
|
||||
|
||||
Python SDK (`trustgraph-base/trustgraph/api/library.py`) 应该能够透明地处理分块上传。公共接口保持不变:
|
||||
分块上传。公共接口保持不变:
|
||||
|
||||
```python
|
||||
# Existing interface - no change for users
|
||||
library.add_document(
|
||||
id="doc-123",
|
||||
title="Large Report",
|
||||
kind="application/pdf",
|
||||
content=large_pdf_bytes, # Can be hundreds of MB
|
||||
tags=["reports"]
|
||||
)
|
||||
```
|
||||
|
||||
内部,SDK 会检测文档大小并切换策略:
|
||||
|
||||
```python
|
||||
class Library:
|
||||
CHUNKED_UPLOAD_THRESHOLD = 2 * 1024 * 1024 # 2MB
|
||||
|
||||
def add_document(self, id, title, kind, content, tags=None, ...):
|
||||
if len(content) < self.CHUNKED_UPLOAD_THRESHOLD:
|
||||
# Small document: single operation (existing behavior)
|
||||
return self._add_document_single(id, title, kind, content, tags)
|
||||
else:
|
||||
# Large document: chunked upload
|
||||
return self._add_document_chunked(id, title, kind, content, tags)
|
||||
|
||||
def _add_document_chunked(self, id, title, kind, content, tags):
|
||||
# 1. begin-upload
|
||||
session = self._begin_upload(
|
||||
document_metadata={...},
|
||||
total_size=len(content),
|
||||
chunk_size=5 * 1024 * 1024
|
||||
)
|
||||
|
||||
# 2. upload-chunk for each chunk
|
||||
for i, chunk in enumerate(self._chunk_bytes(content, session.chunk_size)):
|
||||
self._upload_chunk(session.upload_id, i, chunk)
|
||||
|
||||
# 3. complete-upload
|
||||
return self._complete_upload(session.upload_id)
|
||||
```
|
||||
|
||||
**进度回调** (可选的增强功能):
|
||||
|
||||
```python
|
||||
def add_document(self, ..., on_progress=None):
|
||||
"""
|
||||
on_progress: Optional callback(bytes_sent, total_bytes)
|
||||
"""
|
||||
```
|
||||
|
||||
这允许用户界面显示上传进度,而无需更改基本 API。
|
||||
|
||||
#### 命令行工具
|
||||
|
||||
**`tg-add-library-document`** 保持不变:
|
||||
|
||||
```bash
|
||||
# Works transparently for any size - SDK handles chunking internally
|
||||
tg-add-library-document --file large-report.pdf --title "Large Report"
|
||||
```
|
||||
|
||||
可以添加可选的进度显示:
|
||||
|
||||
```bash
|
||||
tg-add-library-document --file large-report.pdf --title "Large Report" --progress
|
||||
# Output:
|
||||
# Uploading: 45% (225MB / 500MB)
|
||||
```
|
||||
|
||||
**已移除的旧工具:**
|
||||
|
||||
`tg-load-pdf` - 已过时,请使用 `tg-add-library-document`
|
||||
`tg-load-text` - 已过时,请使用 `tg-add-library-document`
|
||||
|
||||
**管理员/调试命令**(可选,优先级较低):
|
||||
|
||||
```bash
|
||||
# List incomplete uploads (admin troubleshooting)
|
||||
tg-add-library-document --list-pending
|
||||
|
||||
# Resume specific upload (recovery scenario)
|
||||
tg-add-library-document --resume upload-abc-123 --file large-report.pdf
|
||||
```
|
||||
|
||||
这些可能只是现有命令上的标志,而不是单独的工具。
|
||||
|
||||
#### API 规范更新
|
||||
|
||||
OpenAPI 规范 (`specs/api/paths/librarian.yaml`) 需要更新以下内容:
|
||||
|
||||
**新操作:**
|
||||
|
||||
`begin-upload` - 初始化分块上传会话
|
||||
`upload-chunk` - 上传单个分块
|
||||
`complete-upload` - 完成上传
|
||||
`abort-upload` - 取消上传
|
||||
`get-upload-status` - 查询上传进度
|
||||
`list-uploads` - 列出用户未完成的上传
|
||||
`stream-document` - 流式文档检索
|
||||
`add-child-document` - 存储提取的文本(内部)
|
||||
`list-children` - 列出子文档(管理员)
|
||||
|
||||
**修改后的操作:**
|
||||
|
||||
`list-documents` - 添加 `include-children` 参数
|
||||
|
||||
**新的模式:**
|
||||
|
||||
`ChunkedUploadBeginRequest`
|
||||
`ChunkedUploadBeginResponse`
|
||||
`ChunkedUploadChunkRequest`
|
||||
`ChunkedUploadChunkResponse`
|
||||
`UploadSession`
|
||||
`UploadProgress`
|
||||
|
||||
**WebSocket 规范更新** (`specs/websocket/`):
|
||||
|
||||
镜像 REST 操作以供 WebSocket 客户端使用,从而实现上传过程的实时
|
||||
进度更新。
|
||||
|
||||
#### 用户体验注意事项
|
||||
|
||||
API 规范的更新可以实现前端改进:
|
||||
|
||||
**上传进度 UI:**
|
||||
显示已上传分块的进度条
|
||||
剩余预估时间
|
||||
暂停/恢复功能
|
||||
|
||||
**错误恢复:**
|
||||
中断的上传可以选择“恢复上传”
|
||||
重新连接时,显示挂起的上传列表
|
||||
|
||||
**大型文件处理:**
|
||||
客户端文件大小检测
|
||||
大型文件的自动分块上传
|
||||
长时间上传时提供清晰的反馈
|
||||
|
||||
这些用户体验改进需要前端工作,并由更新后的 API 规范提供指导。
|
||||
231
docs/tech-specs/zh-cn/logging-strategy.zh-cn.md
Normal file
231
docs/tech-specs/zh-cn/logging-strategy.zh-cn.md
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
---
|
||||
layout: default
|
||||
title: "默认 - INFO 级别,启用 Loki"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
## TrustGraph 日志策略
|
||||
|
||||
## 概述
|
||||
|
||||
TrustGraph 使用 Python 内置的 `logging` 模块进行所有日志操作,并具有集中配置以及可选的 Loki 集成,用于日志聚合。 这提供了一种标准且灵活的方式,可在系统的所有组件中进行日志记录。
|
||||
|
||||
## 默认配置
|
||||
|
||||
### 日志级别
|
||||
- **默认级别**: `INFO`
|
||||
- **可通过**: `--log-level` 命令行参数配置
|
||||
- **选项**: `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`
|
||||
|
||||
### 输出目标
|
||||
1. **控制台 (stdout)**: 始终启用 - 确保与容器化环境兼容
|
||||
2. **Loki**: 可选的集中式日志聚合 (默认启用,可禁用)
|
||||
|
||||
## 集中式日志模块
|
||||
|
||||
所有日志配置都由 `trustgraph.base.logging` 模块管理,该模块提供:
|
||||
- `add_logging_args(parser)` - 添加标准的日志 CLI 参数
|
||||
- `setup_logging(args)` - 从解析的参数配置日志
|
||||
|
||||
该模块用于所有服务器端组件:
|
||||
- 基于 AsyncProcessor 的服务
|
||||
- API 网关
|
||||
- MCP 服务器
|
||||
|
||||
## 实现指南
|
||||
|
||||
### 1. 日志记录器初始化
|
||||
|
||||
每个模块应使用模块的 `__name__` 创建自己的日志记录器:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
```
|
||||
|
||||
日志记录器的名称会自动用作在 Loki 中用于过滤和搜索的标签。
|
||||
|
||||
### 2. 服务初始化
|
||||
|
||||
所有服务器端服务都通过集中化模块自动获取日志配置:
|
||||
|
||||
```python
|
||||
from trustgraph.base import add_logging_args, setup_logging
|
||||
import argparse
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
# 添加标准的日志参数 (包括 Loki 配置)
|
||||
add_logging_args(parser)
|
||||
|
||||
# 添加您自己的服务特定的参数
|
||||
parser.add_argument('--port', type=int, default=8080)
|
||||
|
||||
args = parser.parse_args()
|
||||
args = vars(args)
|
||||
|
||||
# 提前设置日志
|
||||
setup_logging(args)
|
||||
|
||||
# 剩余的服务初始化
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info("服务开始...")
|
||||
```
|
||||
|
||||
### 3. 命令行参数
|
||||
|
||||
所有服务都支持以下日志参数:
|
||||
|
||||
**日志级别:**
|
||||
```bash
|
||||
--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
|
||||
```
|
||||
|
||||
**Loki 配置:**
|
||||
```bash
|
||||
--loki-enabled # 启用 Loki (默认)
|
||||
--no-loki-enabled # 禁用 Loki
|
||||
--loki-url URL # Loki 推送 URL (默认: http://loki:3100/loki/api/v1/push)
|
||||
--loki-username USERNAME # 可选身份验证
|
||||
--loki-password PASSWORD # 可选身份验证
|
||||
```
|
||||
|
||||
**示例:**
|
||||
```bash
|
||||
# 默认 - INFO 级别,启用 Loki
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
./my-service
|
||||
|
||||
# 调试模式,仅控制台
|
||||
./my-service --log-level DEBUG --no-loki-enabled
|
||||
|
||||
# 自定义 Loki 服务器,带身份验证
|
||||
./my-service --loki-url http://loki.prod:3100/loki/api/v1/push \
|
||||
--loki-username admin --loki-password secret
|
||||
```
|
||||
|
||||
### 4. 环境变量
|
||||
|
||||
Loki 配置支持环境变量的后备:
|
||||
|
||||
```bash
|
||||
export LOKI_URL=http://loki.prod:3100/loki/api/v1/push
|
||||
export LOKI_USERNAME=admin
|
||||
export LOKI_PASSWORD=secret
|
||||
```
|
||||
|
||||
命令行参数优先于环境变量。
|
||||
|
||||
### 5. 日志记录最佳实践
|
||||
|
||||
#### 日志级别使用
|
||||
- **DEBUG**: 用于诊断问题的详细信息 (变量值、函数入口/退出)
|
||||
- **INFO**: 通用信息消息 (服务启动、配置加载、处理里程碑)
|
||||
- **WARNING**: 警告消息,用于潜在的危险情况 (弃用功能、可恢复错误)
|
||||
- **ERROR**: 错误消息,用于严重问题 (失败的操作、异常)
|
||||
- **CRITICAL**: 关键消息,用于系统故障,需要立即关注
|
||||
|
||||
#### 消息格式
|
||||
```python
|
||||
# 良好 - 包含上下文
|
||||
logger.info(f"处理文档: {doc_id}, 大小: {doc_size} 字节")
|
||||
logger.error(f"无法连接到数据库: {error}", exc_info=True)
|
||||
|
||||
# 不好 - 缺少上下文
|
||||
logger.info("处理文档")
|
||||
logger.error("连接失败")
|
||||
```
|
||||
|
||||
#### 性能考虑
|
||||
```python
|
||||
# 使用延迟格式化进行昂贵的操作
|
||||
logger.debug("昂贵操作结果: %s", expensive_function())
|
||||
|
||||
# 检查日志级别以进行非常昂贵的调试操作
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
debug_data = compute_expensive_debug_info()
|
||||
logger.debug(f"调试数据: {debug_data}")
|
||||
```
|
||||
|
||||
### 6. 使用 Loki 的结构化日志记录
|
||||
|
||||
对于复杂的结构化数据,可以使用结构化日志记录,并添加额外的标签以供 Loki 使用:
|
||||
|
||||
```python
|
||||
logger.info("请求已处理", extra={
|
||||
'tags': {
|
||||
'request_id': request_id,
|
||||
'user_id': user_id,
|
||||
'status': 'success'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
这些标签将作为 Loki 中的可搜索标签:
|
||||
- `severity` - 日志级别 (DEBUG, INFO, 等)
|
||||
- `logger` - 模块名称 (来自 `__name__`)
|
||||
|
||||
### 7. 异常日志记录
|
||||
|
||||
始终包含异常堆栈跟踪:
|
||||
|
||||
```python
|
||||
try:
|
||||
process_data()
|
||||
except Exception as e:
|
||||
logger.error(f"无法处理数据: {e}", exc_info=True)
|
||||
raise
|
||||
```
|
||||
|
||||
### 8. 异步日志记录的注意事项
|
||||
|
||||
日志系统使用非阻塞的队列处理程序进行 Loki 集成:
|
||||
- 控制台输出是同步的 (快速)
|
||||
- Loki 输出是异步排队 (500 条消息缓冲区)
|
||||
- 后台线程处理 Loki 传输
|
||||
- 没有阻塞主应用程序代码
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
async def async_operation():
|
||||
logger = logging.getLogger(__name__)
|
||||
# 日志记录是线程安全的,不会阻塞异步操作
|
||||
logger.info(f"在任务中开始异步操作: {asyncio.current_task().get_name()}")
|
||||
```
|
||||
|
||||
## Loki 集成
|
||||
|
||||
### 架构
|
||||
|
||||
日志系统使用 Python 内置的 `logging` 模块进行 Loki 集成:
|
||||
- `python-logging-loki` - 用于 Loki 集成 (可选,如果缺少则回退)
|
||||
|
||||
已经在 `trustgraph-base/pyproject.toml` 和 `requirements.txt` 中包含。
|
||||
|
||||
### 从 print() 到 logging:
|
||||
```python
|
||||
# 之前
|
||||
print(f"处理文档 {doc_id}")
|
||||
|
||||
# 之后
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"处理文档 {doc_id}")
|
||||
```
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
- **永远不要记录敏感信息** (密码、API 密钥、个人数据、令牌)
|
||||
- **清理用户输入** 之前进行日志记录
|
||||
- **使用占位符** 来记录敏感字段: `user_id=****1234`
|
||||
- **使用 Loki 身份验证**: 对于生产环境,使用 `--loki-username` 和 `--loki-password`
|
||||
- **安全传输**: 在生产中使用 HTTPS: `https://loki.prod:3100/loki/api/v1/push`
|
||||
|
||||
## 依赖
|
||||
|
||||
集中式日志模块需要:
|
||||
- `python-logging-loki` - 用于 Loki 集成 (可选,如果缺失则回退)
|
||||
203
docs/tech-specs/zh-cn/mcp-tool-arguments.zh-cn.md
Normal file
203
docs/tech-specs/zh-cn/mcp-tool-arguments.zh-cn.md
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
---
|
||||
layout: default
|
||||
title: "MCP 工具参数规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# MCP 工具参数规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
**功能名称**: MCP 工具参数支持
|
||||
**作者**: Claude 代码助手
|
||||
**日期**: 2025-08-21
|
||||
**状态**: 最终
|
||||
|
||||
### 摘要
|
||||
|
||||
启用 ReACT 代理调用带有正确定义的参数的 MCP (模型上下文协议) 工具,通过在 MCP 工具配置中添加参数规范支持,类似于当前提示模板工具的工作方式。
|
||||
|
||||
### 问题陈述
|
||||
|
||||
目前,ReACT 代理框架中的 MCP 工具无法指定其预期的参数。`McpToolImpl.get_arguments()` 方法返回一个空列表,迫使 LLM 仅根据工具名称和描述来猜测正确的参数结构。这会导致:
|
||||
- 由于参数猜测导致的不可靠工具调用
|
||||
- 当工具因错误的参数而失败时,用户体验不佳
|
||||
- 在执行之前无法验证工具参数
|
||||
- 代理提示中缺少参数文档
|
||||
|
||||
### 目标
|
||||
|
||||
- [ ] 允许 MCP 工具配置指定预期的参数(名称、类型、描述)
|
||||
- [ ] 更新代理管理器,通过提示向 LLM 暴露 MCP 工具参数
|
||||
- [ ] 保持与现有 MCP 工具配置的向后兼容性
|
||||
- [ ] 支持与提示模板工具类似的参数验证
|
||||
|
||||
### 不属于目标
|
||||
- 从 MCP 服务器动态发现参数(未来增强功能)
|
||||
- 参数类型验证,超出基本结构
|
||||
- 复杂的参数模式(嵌套对象、数组)
|
||||
|
||||
## 背景和上下文
|
||||
|
||||
### 现状
|
||||
MCP 工具在 ReACT 代理系统中配置为具有最小的元数据:
|
||||
```json
|
||||
{
|
||||
"type": "mcp-tool",
|
||||
"name": "get_bank_balance",
|
||||
"description": "获取银行账户余额",
|
||||
"mcp-tool": "get_bank_balance"
|
||||
}
|
||||
```
|
||||
|
||||
`McpToolImpl.get_arguments()` 方法返回 `[]`,因此 LLM 在提示中不会收到任何参数指导。
|
||||
|
||||
### 局限性
|
||||
|
||||
1. **缺少参数指定**: MCP 工具无法定义预期的参数
|
||||
2. **LLM 参数猜测**: 代理必须根据工具名称/描述推断参数
|
||||
3. **提示信息缺失**: 代理提示中不包含 MCP 工具的参数详情
|
||||
4. **无验证**: 无效参数仅在 MCP 工具执行时捕获
|
||||
|
||||
### 相关组件
|
||||
- **trustgraph-flow/agent/react/service.py**: 工具配置加载和 AgentManager 创建
|
||||
- **trustgraph-flow/agent/react/tools.py**: McpToolImpl 实现
|
||||
- **trustgraph-flow/agent/react/agent_manager.py**: 使用工具参数生成提示
|
||||
- **tg-invoke-mcp-tool**: 用于 MCP 工具管理的 CLI 工具
|
||||
- **Workbench**: 代理工具配置的外部 UI
|
||||
|
||||
## 要求
|
||||
|
||||
### 功能要求
|
||||
|
||||
1. **MCP 工具配置参数**: MCP 工具配置 **必须** 支持一个可选的 `arguments` 数组,包含名称、类型和描述字段
|
||||
2. **参数暴露**: `McpToolImpl.get_arguments()` **必须** 返回配置的参数,而不是空列表
|
||||
3. **提示集成**: 代理提示 **必须** 在指定有参数时,包含 MCP 工具参数详情
|
||||
4. **向后兼容性**: 没有参数的现有 MCP 工具配置 **必须** 保持正常工作
|
||||
5. **CLI 支持**: 现有的 `tg-invoke-mcp-tool` CLI 支持参数(已实现)
|
||||
|
||||
### 非功能要求
|
||||
1. **向后兼容性**: 现有 MCP 工具配置零中断
|
||||
2. **性能**: 代理提示生成没有显著性能影响
|
||||
3. **一致性**: 参数处理 **必须** 与提示模板工具模式匹配
|
||||
|
||||
### 用户故事
|
||||
|
||||
1. 作为 **代理开发者**,我希望在配置中指定 MCP 工具参数,以便 LLM 可以使用正确的参数调用工具
|
||||
2. 作为 **Workbench 用户**,我希望在 UI 中配置 MCP 工具参数,以便代理正确使用工具
|
||||
3. 作为 **ReACT 代理中的 LLM**,我希望在提示中看到工具参数规范,以便我可以提供正确的参数
|
||||
|
||||
## 设计
|
||||
|
||||
### 高级架构
|
||||
通过以下方式,扩展 MCP 工具配置以匹配提示模板模式:
|
||||
1. 在 MCP 工具配置中添加可选的 `arguments` 数组,包含名称、类型和描述字段
|
||||
2. 修改 `McpToolImpl` 以接受和返回配置的参数
|
||||
3. 修改工具配置加载以处理 MCP 工具参数
|
||||
4. 确保代理提示包含 MCP 工具参数信息
|
||||
|
||||
### 配置方案
|
||||
```json
|
||||
{
|
||||
"type": "mcp-tool",
|
||||
"name": "get_bank_balance",
|
||||
"description": "获取银行账户余额",
|
||||
"mcp-tool": "get_bank_balance",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "account_id",
|
||||
"type": "string",
|
||||
"description": "银行账户标识符"
|
||||
},
|
||||
{
|
||||
"name": "date",
|
||||
"type": "string",
|
||||
"description": "查询余额的日期(可选,格式:YYYY-MM-DD)"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 数据流程
|
||||
1. **配置加载**: 使用参数的 MCP 工具配置通过 `on_tools_config()` 加载
|
||||
2. **工具创建**: 参数解析并传递给 `McpToolImpl` 通过构造函数
|
||||
3. **提示生成**: `agent_manager.py` 调用 `tool.arguments` 以包含在 LLM 提示中
|
||||
4. **工具调用**: LLM 提供参数,这些参数传递给 MCP 服务不变
|
||||
|
||||
### API 更改
|
||||
- 无外部 API 更改 - 这是一个纯粹的内部配置和参数处理
|
||||
|
||||
### 组件详情
|
||||
|
||||
#### 组件 1: service.py (工具配置加载)
|
||||
- **目的**: 解析 MCP 工具配置并创建工具实例
|
||||
- **更改要求**: 解析 MCP 工具配置中的参数(类似于提示工具)
|
||||
- **新功能**: 从 MCP 工具配置中提取 `arguments` 数组,并创建 `Argument` 对象
|
||||
|
||||
#### 组件 2: tools.py (McpToolImpl)
|
||||
- **目的**: MCP 工具实现包装器
|
||||
- **更改要求**: 接受构造函数中的参数并从 `get_arguments()` 中返回
|
||||
- **新功能**: 存储并公开配置的参数,而不是返回空列表
|
||||
|
||||
#### 组件 3: Workbench (外部仓库)
|
||||
- **目的**: 代理工具配置的 UI
|
||||
- **更改要求**: 为 MCP 工具添加参数规范 UI
|
||||
- **新功能**: 允许用户添加/编辑/删除 MCP 工具的参数
|
||||
|
||||
#### 组件 4: CLI 工具
|
||||
- **目的**: 命令列工具管理
|
||||
- **更改要求**: 支持在工具创建/更新命令中指定参数
|
||||
- **新功能**: 在工具配置命令中接受 `arguments` 参数
|
||||
|
||||
## 实施计划
|
||||
|
||||
### 阶段 1: 核心代理框架更改
|
||||
- [ ] 更新 `McpToolImpl` 构造函数以接受 `arguments` 参数
|
||||
- [ ] 修改 `McpToolImpl.get_arguments()` 以返回存储的参数,而不是空列表
|
||||
- [ ] 修改 `service.py` 中的 MCP 工具配置解析以处理参数
|
||||
- [ ] 添加 MCP 工具参数的测试
|
||||
- [ ] 更新 McpToolImpl 类文档
|
||||
- [ ] 添加参数解析逻辑的注释
|
||||
- [ ] 更新系统架构中的参数流程文档
|
||||
|
||||
### 阶段 4: 生产部署
|
||||
- 核心更改具有向后兼容性 - 无需回滚,因为功能正常
|
||||
- 如果出现问题,请通过回滚 MCP 工具配置加载逻辑来禁用参数解析
|
||||
- Workbench 和 CLI 更改是独立的,可以单独回滚
|
||||
|
||||
## 安全注意事项
|
||||
- **没有新的攻击面**: 参数是从现有配置源解析的,没有新的输入
|
||||
- **参数验证**: 参数传递给 MCP 工具不变 - 验证在 MCP 工具级别
|
||||
- **配置完整性**: 参数规范是工具配置的一部分 - 相同的安全模型适用
|
||||
|
||||
## 性能影响
|
||||
- **极小的开销**: 参数解析仅在配置加载期间发生,而不是请求时
|
||||
- **提示大小增加**: 代理提示将包含 MCP 工具参数详情,稍微增加令牌使用量
|
||||
- **内存使用**: 存储参数规范在对象中的增加,可以忽略不计
|
||||
|
||||
## 文档
|
||||
|
||||
### 用户文档
|
||||
- [ ] 更新 MCP 工具配置指南,提供参数示例
|
||||
- [ ] 在 CLI 工具的帮助文本中添加参数规范
|
||||
- [ ] 创建常见 MCP 工具参数模式的示例
|
||||
|
||||
### 开发者文档
|
||||
- [ ] 更新 `McpToolImpl` 类文档
|
||||
- [ ] 添加参数解析逻辑的注释
|
||||
- [ ] 文档系统架构中的参数流程
|
||||
|
||||
## 未解决的问题
|
||||
1. **参数验证**: 是否应该在基本结构检查之外验证参数类型/格式?
|
||||
2. **动态发现**: 将来增强功能,以在运行时自动从 MCP 服务器查询工具模式?
|
||||
|
||||
## 考虑的替代方案
|
||||
1. **动态的 MCP 模式发现**: 在运行时查询 MCP 服务器以获取工具参数模式 - 已拒绝,因为它复杂且不可靠
|
||||
2. **单独的参数注册表**: 将 MCP 工具参数存储在单独的配置部分中 - 已拒绝,因为与提示模板方法保持一致
|
||||
3. **类型验证**: JSON 模式验证参数 - 延迟,以便在初始实现中保持简单
|
||||
|
||||
## 引用
|
||||
- [MCP 协议规范](https://github.com/modelcontextprotocol/spec)
|
||||
- [提示模板工具实现](./trustgraph-flow/trustgraph/agent/react/service.py#L114-129)
|
||||
- [当前的 MCP 工具实现](./trustgraph-flow/trustgraph/agent/react/tools.py#L58-86)
|
||||
562
docs/tech-specs/zh-cn/mcp-tool-bearer-token.zh-cn.md
Normal file
562
docs/tech-specs/zh-cn/mcp-tool-bearer-token.zh-cn.md
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
---
|
||||
layout: default
|
||||
title: "MCP 工具承载令牌身份验证规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# MCP 工具承载令牌身份验证规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
> **⚠️ 重要提示:仅限单租户环境**
|
||||
>
|
||||
> 本规范描述了一种用于 MCP 工具的**基本服务级别身份验证机制**。它**不是**一个完整的身份验证解决方案,**不适用于**以下情况:
|
||||
> - 多用户环境
|
||||
> - 多租户部署
|
||||
> - 联合身份验证
|
||||
> - 用户上下文传播
|
||||
> - 基于用户的授权
|
||||
>
|
||||
> 此功能为**每个 MCP 工具提供一个静态令牌**,该令牌在所有用户和会话中共享。 如果您需要基于用户的身份验证或基于租户的身份验证,则此功能不适合您。
|
||||
|
||||
## 概述
|
||||
**功能名称**: MCP 工具承载令牌身份验证支持
|
||||
**作者**: Claude 代码助手
|
||||
**日期**: 2025-11-11
|
||||
**状态**: 正在开发中
|
||||
|
||||
### 执行摘要
|
||||
|
||||
允许 MCP 工具配置指定可选的承载令牌,用于对受保护的 MCP 服务器进行身份验证。 这允许 TrustGraph 安全地调用托管在需要身份验证的服务器上的 MCP 工具,而无需修改代理或工具调用接口。
|
||||
|
||||
**重要提示**: 这是一个基本身份验证机制,旨在用于单租户、服务到服务的身份验证场景。 它**不适用于**以下情况:
|
||||
需要不同用户凭据的多用户环境
|
||||
需要每个租户隔离的多租户部署
|
||||
联合身份验证场景
|
||||
基于用户的身份验证或授权
|
||||
动态凭据管理或令牌刷新
|
||||
|
||||
此功能为每个 MCP 工具配置提供一个静态、系统范围的承载令牌,该令牌在所有用户和该工具的调用中共享。
|
||||
|
||||
### 问题陈述
|
||||
|
||||
目前,MCP 工具只能连接到公共可访问的 MCP 服务器。 许多生产 MCP 部署需要通过承载令牌进行身份验证以提高安全性。 如果没有身份验证支持:
|
||||
MCP 工具无法连接到受保护的 MCP 服务器
|
||||
用户必须要么公开 MCP 服务器,要么实现反向代理
|
||||
没有一种标准化的方法将凭据传递给 MCP 连接
|
||||
无法在 MCP 终端节点上强制执行安全最佳实践
|
||||
|
||||
### 目标
|
||||
|
||||
[ ] 允许 MCP 工具配置指定可选的 `auth-token` 参数
|
||||
[ ] 更新 MCP 工具服务,使其在连接到 MCP 服务器时使用承载令牌
|
||||
[ ] 更新 CLI 工具以支持设置/显示身份验证令牌
|
||||
[ ] 保持与未进行身份验证的 MCP 配置的向后兼容性
|
||||
[ ] 记录令牌存储的安全注意事项
|
||||
|
||||
### 非目标
|
||||
动态令牌刷新或 OAuth 流程(仅限静态令牌)
|
||||
存储令牌的加密(配置系统安全性不在范围之内)
|
||||
替代身份验证方法(基本身份验证、API 密钥等)
|
||||
令牌验证或到期检查
|
||||
**基于用户的身份验证**: 此功能**不支持**用户特定的凭据
|
||||
**多租户隔离**: 此功能**不提供**基于租户的令牌管理
|
||||
**联合身份验证**: 此功能**不与**身份提供商(SSO、OAuth、SAML 等)集成
|
||||
**基于上下文的身份验证**: 令牌不是基于用户上下文或会话传递的
|
||||
|
||||
## 背景和上下文
|
||||
|
||||
### 当前状态
|
||||
MCP 工具配置存储在 `mcp` 配置组中,具有以下结构:
|
||||
```json
|
||||
{
|
||||
"remote-name": "tool_name",
|
||||
"url": "http://mcp-server:3000/api"
|
||||
}
|
||||
```
|
||||
|
||||
MCP 工具服务使用 `streamablehttp_client(url)` 连接到服务器,而无需任何身份验证头信息。
|
||||
|
||||
### 限制
|
||||
|
||||
**当前系统限制:**
|
||||
1. **不支持身份验证:** 无法连接到需要身份验证的 MCP 服务器。
|
||||
2. **安全风险:** MCP 服务器必须是公开可访问的,或者仅使用网络级别的安全措施。
|
||||
3. **生产部署问题:** 无法遵循 API 端点的安全最佳实践。
|
||||
|
||||
**此解决方案的限制:**
|
||||
1. **仅支持单租户:** 每个 MCP 工具只有一个静态令牌,所有用户共享该令牌。
|
||||
2. **不支持按用户身份验证:** 无法以不同的用户身份进行身份验证,也无法传递用户上下文。
|
||||
3. **不支持多租户:** 无法按租户或组织隔离凭据。
|
||||
4. **仅支持静态令牌:** 不支持令牌刷新、轮换或过期处理。
|
||||
5. **服务级别身份验证:** 仅对 TrustGraph 服务进行身份验证,而不是对单个用户进行身份验证。
|
||||
6. **共享安全上下文:** 对 MCP 工具的所有调用都使用相同的凭据。
|
||||
|
||||
### 使用场景适用性
|
||||
|
||||
**✅ 适用场景:**
|
||||
单租户 TrustGraph 部署。
|
||||
服务到服务身份验证(TrustGraph → MCP 服务器)。
|
||||
开发和测试环境。
|
||||
TrustGraph 系统访问的内部 MCP 工具。
|
||||
所有用户共享相同 MCP 工具访问级别的场景。
|
||||
静态、长期有效的服务凭据。
|
||||
|
||||
**❌ 不适用场景:**
|
||||
需要按用户身份验证的多用户系统。
|
||||
需要租户隔离的多租户 SaaS 部署。
|
||||
联合身份验证场景(SSO、OAuth、SAML)。
|
||||
需要将用户上下文传播到 MCP 服务器的系统。
|
||||
需要动态令牌刷新或短生命周期的令牌的环境。
|
||||
不同的用户需要不同权限级别的应用程序。
|
||||
用户级别审计跟踪的合规性要求。
|
||||
|
||||
**示例适用场景:**
|
||||
单个组织的 TrustGraph 部署,其中所有员工都使用相同的内部 MCP 工具(例如,公司数据库查询)。MCP 服务器需要身份验证以防止外部访问,但所有内部用户都具有相同的访问级别。
|
||||
|
||||
**示例不适用场景:**
|
||||
一个多租户 TrustGraph SaaS 平台,其中租户 A 和租户 B 需要访问各自隔离的 MCP 服务器,并使用不同的凭据。此功能不支持按租户管理令牌。
|
||||
|
||||
### 相关组件
|
||||
**trustgraph-flow/trustgraph/agent/mcp_tool/service.py**: MCP 工具调用服务。
|
||||
**trustgraph-cli/trustgraph/cli/set_mcp_tool.py**: 用于创建/更新 MCP 配置的 CLI 工具。
|
||||
**trustgraph-cli/trustgraph/cli/show_mcp_tools.py**: 用于显示 MCP 配置的 CLI 工具。
|
||||
**MCP Python SDK**: `streamablehttp_client` from `mcp.client.streamable_http`
|
||||
|
||||
## 要求
|
||||
|
||||
### 功能性要求
|
||||
|
||||
1. **MCP 配置身份验证令牌:** MCP 工具配置必须支持一个可选的 `auth-token` 字段。
|
||||
2. **Bearer 令牌使用:** 当配置了 auth-token 时,MCP 工具服务必须发送 `Authorization: Bearer {token}` 头信息。
|
||||
3. **CLI 支持:** `tg-set-mcp-tool` 必须接受一个可选的 `--auth-token` 参数。
|
||||
4. **令牌显示:** `tg-show-mcp-tools` 必须指示是否配置了 auth-token(出于安全考虑,已屏蔽)。
|
||||
5. **向后兼容性:** 现有不带 auth-token 的 MCP 工具配置必须继续正常工作。
|
||||
|
||||
### 非功能性要求
|
||||
1. **向后兼容性:** 对于现有的 MCP 工具配置,不得有任何破坏性更改。
|
||||
2. **性能:** 对 MCP 工具的调用不会产生任何显著的性能影响。
|
||||
3. **安全性:** 令牌存储在配置中(请注意安全隐患)。
|
||||
|
||||
### 用户故事
|
||||
|
||||
1. 作为 **DevOps 工程师**,我希望配置 MCP 工具的 bearer 令牌,以便我可以保护 MCP 服务器端点。
|
||||
2. 作为 **CLI 用户**,我希望在创建 MCP 工具时设置身份验证令牌,以便我可以连接到受保护的服务器。
|
||||
3. 作为 **系统管理员**,我希望查看哪些 MCP 工具配置了身份验证,以便我可以审计安全设置。
|
||||
|
||||
## 设计
|
||||
|
||||
### 高级架构
|
||||
扩展 MCP 工具配置和服务以支持 bearer 令牌身份验证:
|
||||
1. 向 MCP 工具配置模式添加一个可选的 `auth-token` 字段。
|
||||
2. 修改 MCP 工具服务以读取 auth-token 并将其传递给 HTTP 客户端。
|
||||
3. 更新 CLI 工具以支持设置和显示身份验证令牌。
|
||||
4. 记录安全注意事项和最佳实践。
|
||||
|
||||
### 配置模式
|
||||
|
||||
**当前模式:**
|
||||
```json
|
||||
{
|
||||
"remote-name": "tool_name",
|
||||
"url": "http://mcp-server:3000/api"
|
||||
}
|
||||
```
|
||||
|
||||
**新的模式**(带可选的认证令牌):
|
||||
```json
|
||||
{
|
||||
"remote-name": "tool_name",
|
||||
"url": "http://mcp-server:3000/api",
|
||||
"auth-token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
**字段描述**:
|
||||
`remote-name` (可选): MCP 服务器使用的名称(默认为配置键)
|
||||
`url` (必需): MCP 服务器端点 URL
|
||||
`auth-token` (可选): 用于身份验证的 Bearer token
|
||||
|
||||
### 数据流
|
||||
|
||||
1. **配置存储**: 用户运行 `tg-set-mcp-tool --id my-tool --tool-url http://server/api --auth-token xyz123`
|
||||
2. **配置加载**: MCP 工具服务通过 `on_mcp_config()` 回调接收配置更新
|
||||
3. **工具调用**: 当工具被调用时:
|
||||
服务从配置中读取 `auth-token`(如果存在)
|
||||
创建 headers 字典:`{"Authorization": "Bearer {token}"}`
|
||||
将 headers 传递给 `streamablehttp_client(url, headers=headers)`
|
||||
MCP 服务器验证 token 并处理请求
|
||||
|
||||
### API 变更
|
||||
没有外部 API 变更,仅为配置模式扩展。
|
||||
|
||||
### 组件详情
|
||||
|
||||
#### 组件 1: service.py (MCP 工具服务)
|
||||
**文件**: `trustgraph-flow/trustgraph/agent/mcp_tool/service.py`
|
||||
|
||||
**目的**: 在远程服务器上调用 MCP 工具
|
||||
|
||||
**所需变更**(在 `invoke_tool()` 方法中):
|
||||
1. 检查 `auth-token` 是否在 `self.mcp_services[name]` 配置中
|
||||
2. 如果 token 存在,则构建包含 Authorization header 的 headers 字典
|
||||
3. 将 headers 传递给 `streamablehttp_client(url, headers=headers)`
|
||||
|
||||
**当前代码**(42-89 行):
|
||||
```python
|
||||
async def invoke_tool(self, name, parameters):
|
||||
try:
|
||||
if name not in self.mcp_services:
|
||||
raise RuntimeError(f"MCP service {name} not known")
|
||||
if "url" not in self.mcp_services[name]:
|
||||
raise RuntimeError(f"MCP service {name} URL not defined")
|
||||
|
||||
url = self.mcp_services[name]["url"]
|
||||
|
||||
if "remote-name" in self.mcp_services[name]:
|
||||
remote_name = self.mcp_services[name]["remote-name"]
|
||||
else:
|
||||
remote_name = name
|
||||
|
||||
logger.info(f"Invoking {remote_name} at {url}")
|
||||
|
||||
# Connect to a streamable HTTP server
|
||||
async with streamablehttp_client(url) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
_,
|
||||
):
|
||||
# ... rest of method
|
||||
```
|
||||
|
||||
**修改后的代码**:
|
||||
```python
|
||||
async def invoke_tool(self, name, parameters):
|
||||
try:
|
||||
if name not in self.mcp_services:
|
||||
raise RuntimeError(f"MCP service {name} not known")
|
||||
if "url" not in self.mcp_services[name]:
|
||||
raise RuntimeError(f"MCP service {name} URL not defined")
|
||||
|
||||
url = self.mcp_services[name]["url"]
|
||||
|
||||
if "remote-name" in self.mcp_services[name]:
|
||||
remote_name = self.mcp_services[name]["remote-name"]
|
||||
else:
|
||||
remote_name = name
|
||||
|
||||
# Build headers with optional bearer token
|
||||
headers = {}
|
||||
if "auth-token" in self.mcp_services[name]:
|
||||
token = self.mcp_services[name]["auth-token"]
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
|
||||
logger.info(f"Invoking {remote_name} at {url}")
|
||||
|
||||
# Connect to a streamable HTTP server with headers
|
||||
async with streamablehttp_client(url, headers=headers) as (
|
||||
read_stream,
|
||||
write_stream,
|
||||
_,
|
||||
):
|
||||
# ... rest of method (unchanged)
|
||||
```
|
||||
|
||||
#### 组件 2: set_mcp_tool.py (命令行配置工具)
|
||||
**文件**: `trustgraph-cli/trustgraph/cli/set_mcp_tool.py`
|
||||
|
||||
**目的**: 创建/更新 MCP 工具配置
|
||||
|
||||
**所需更改**:
|
||||
1. 向 argparse 添加 `--auth-token` 可选参数
|
||||
2. 在提供时,将 `auth-token` 包含在配置 JSON 中
|
||||
|
||||
**当前参数**:
|
||||
`--id` (必需): MCP 工具标识符
|
||||
`--remote-name` (可选): 远程 MCP 工具名称
|
||||
`--tool-url` (必需): MCP 工具 URL 端点
|
||||
`-u, --api-url` (可选): TrustGraph API URL
|
||||
|
||||
**新参数**:
|
||||
`--auth-token` (可选): 用于身份验证的 Bearer token
|
||||
|
||||
**修改后的配置构建**:
|
||||
```python
|
||||
# Build configuration object
|
||||
config = {
|
||||
"url": args.tool_url,
|
||||
}
|
||||
|
||||
if args.remote_name:
|
||||
config["remote-name"] = args.remote_name
|
||||
|
||||
if args.auth_token:
|
||||
config["auth-token"] = args.auth_token
|
||||
|
||||
# Store configuration
|
||||
api.config().put([
|
||||
ConfigValue(type="mcp", key=args.id, value=json.dumps(config))
|
||||
])
|
||||
```
|
||||
|
||||
#### 组件 3:show_mcp_tools.py (命令行显示工具)
|
||||
**文件**: `trustgraph-cli/trustgraph/cli/show_mcp_tools.py`
|
||||
|
||||
**目的**: 显示 MCP 工具的配置
|
||||
|
||||
**需要修改的内容**:
|
||||
1. 在输出表中添加 "Auth" 列
|
||||
2. 根据是否存在 auth-token 显示 "是" 或 "否"
|
||||
3. 不要显示实际的 token 值(出于安全考虑)
|
||||
|
||||
**当前输出**:
|
||||
```
|
||||
ID Remote Name URL
|
||||
---------- ------------- ------------------------
|
||||
my-tool my-tool http://server:3000/api
|
||||
```
|
||||
|
||||
**新的输出:**
|
||||
```
|
||||
ID Remote Name URL Auth
|
||||
---------- ------------- ------------------------ ------
|
||||
my-tool my-tool http://server:3000/api Yes
|
||||
other-tool other-tool http://other:3000/api No
|
||||
```
|
||||
|
||||
#### 组件 4:文档
|
||||
**文件**: `docs/cli/tg-set-mcp-tool.md`
|
||||
|
||||
**需要修改的内容**:
|
||||
1. 文档新的 `--auth-token` 参数
|
||||
2. 提供带有身份验证的示例用法
|
||||
3. 文档安全注意事项
|
||||
|
||||
## 实施计划
|
||||
|
||||
### 第一阶段:创建技术规范
|
||||
[x] 编写全面的技术规范,记录所有更改
|
||||
|
||||
### 第二阶段:更新 MCP 工具服务
|
||||
[ ] 修改 `invoke_tool()` 中的 `service.py` 以从配置文件读取 auth-token
|
||||
[ ] 构建 headers 字典并传递给 `streamablehttp_client`
|
||||
[ ] 使用经过身份验证的 MCP 服务器进行测试
|
||||
|
||||
### 第三阶段:更新 CLI 工具
|
||||
[ ] 向 `set_mcp_tool.py` 添加 `--auth-token` 参数
|
||||
[ ] 在配置 JSON 中包含 auth-token
|
||||
[ ] 向 `show_mcp_tools.py` 的输出添加 "Auth" 列
|
||||
[ ] 测试 CLI 工具的更改
|
||||
|
||||
### 第四阶段:更新文档
|
||||
[ ] 在 `tg-set-mcp-tool.md` 中记录 `--auth-token` 参数
|
||||
[ ] 添加安全注意事项部分
|
||||
[ ] 提供示例用法
|
||||
|
||||
### 第五阶段:测试
|
||||
[ ] 测试 MCP 工具是否能够使用 auth-token 成功连接
|
||||
[ ] 测试向后兼容性(没有 auth-token 的工具仍然可以工作)
|
||||
[ ] 测试 CLI 工具是否能够正确接受和存储 auth-token
|
||||
[ ] 测试 "show" 命令是否能够正确显示身份验证状态
|
||||
|
||||
### 代码更改摘要
|
||||
| 文件 | 更改类型 | 行数 | 描述 |
|
||||
|------|------------|-------|-------------|
|
||||
| `service.py` | 修改 | ~52-66 | 添加 auth-token 读取和 header 构建 |
|
||||
| `set_mcp_tool.py` | 修改 | ~30-60 | 添加 --auth-token 参数和配置存储 |
|
||||
| `show_mcp_tools.py` | 修改 | ~40-70 | 向显示添加 Auth 列 |
|
||||
| `tg-set-mcp-tool.md` | 修改 | 各种 | 文档新的参数 |
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 单元测试
|
||||
**Auth Token 读取**: 测试 `invoke_tool()` 是否能够正确从配置文件读取 auth-token
|
||||
**Header 构建**: 测试 Authorization header 是否能够使用 Bearer 前缀正确构建
|
||||
**向后兼容性**: 测试没有 auth-token 的工具是否能够正常工作
|
||||
**CLI 参数解析**: 测试 `--auth-token` 参数是否能够正确解析
|
||||
|
||||
### 集成测试
|
||||
**经过身份验证的连接**: 测试 MCP 工具服务是否能够连接到经过身份验证的服务器
|
||||
**端到端**: 测试 CLI → 配置文件存储 → 服务调用,并使用 auth token
|
||||
**不需要 Token**: 测试连接到未经过身份验证的服务器是否仍然可以工作
|
||||
|
||||
### 手动测试
|
||||
**真实的 MCP 服务器**: 使用需要 bearer token 身份验证的实际 MCP 服务器进行测试
|
||||
**CLI 工作流程**: 测试完整的流程:使用 auth 设置工具 → 调用工具 → 验证成功
|
||||
**显示屏蔽**: 验证身份验证状态显示,但 token 值不暴露
|
||||
|
||||
## 迁移和发布
|
||||
|
||||
### 迁移策略
|
||||
不需要迁移 - 这是一个纯粹的附加功能:
|
||||
现有的 MCP 工具配置,如果没有 `auth-token`,可以继续正常工作
|
||||
新的配置可以选择包含 `auth-token` 字段
|
||||
CLI 工具接受,但不要求 `--auth-token` 参数
|
||||
|
||||
### 发布计划
|
||||
1. **第一阶段**: 将核心服务更改部署到开发/测试环境
|
||||
2. **第二阶段**: 部署 CLI 工具更新
|
||||
3. **第三阶段**: 更新文档
|
||||
4. **第四阶段**: 生产发布,并进行监控
|
||||
|
||||
### 回滚计划
|
||||
核心更改是向后兼容的 - 现有的工具不受影响
|
||||
如果出现问题,可以通过删除 header 构建逻辑来禁用身份验证 token 处理
|
||||
CLI 更改是独立的,并且可以单独回滚
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
### ⚠️ 关键限制:仅支持单租户身份验证
|
||||
|
||||
**此身份验证机制不适用于多用户或多租户环境。**
|
||||
|
||||
**共享凭据**: 所有用户和调用都共享每个 MCP 工具的相同 token
|
||||
**没有用户上下文**: MCP 服务器无法区分不同的 TrustGraph 用户
|
||||
**没有租户隔离**: 所有租户共享每个 MCP 工具的相同凭据
|
||||
**审计跟踪限制**: MCP 服务器日志显示来自相同凭据的所有请求
|
||||
**权限范围**: 无法为不同的用户强制执行不同的权限级别
|
||||
|
||||
**不要使用此功能,如果:**
|
||||
您的 TrustGraph 部署服务于多个组织(多租户)
|
||||
您需要跟踪哪些用户访问了哪些 MCP 工具
|
||||
不同的用户需要不同的权限级别
|
||||
您需要遵守用户级别的审计要求
|
||||
您的 MCP 服务器强制执行每个用户的速率限制或配额
|
||||
|
||||
**多用户/多租户场景的替代方案:**
|
||||
通过自定义头部实现用户上下文传播
|
||||
为每个租户部署独立的 TrustGraph 实例
|
||||
使用网络级别隔离(VPCs、服务网格)
|
||||
实现一个代理层,该层处理每个用户的身份验证
|
||||
|
||||
### 令牌存储
|
||||
**风险:** 身份验证令牌以明文形式存储在配置系统中
|
||||
|
||||
**缓解措施:**
|
||||
记录令牌以明文形式存储
|
||||
尽可能推荐使用短寿命令牌
|
||||
推荐对配置存储进行适当的访问控制
|
||||
考虑未来增强功能,用于加密令牌存储
|
||||
|
||||
### 令牌泄露
|
||||
**风险:** 令牌可能出现在日志或 CLI 输出中
|
||||
|
||||
**缓解措施:**
|
||||
不要记录令牌值(仅记录“身份验证已配置:是/否”)
|
||||
CLI 显示命令仅显示屏蔽状态,不显示实际令牌
|
||||
不要将令牌包含在错误消息中
|
||||
|
||||
### 网络安全
|
||||
**风险:** 令牌通过未加密的连接传输
|
||||
|
||||
**缓解措施:**
|
||||
记录推荐使用 HTTPS URL 进行 MCP 服务器访问
|
||||
警告用户使用 HTTP 传输的明文风险
|
||||
|
||||
### 配置访问
|
||||
**风险:** 未授权访问配置系统会暴露令牌
|
||||
|
||||
**缓解措施:**
|
||||
记录保护配置系统访问的重要性
|
||||
推荐对配置访问采用最小权限原则
|
||||
考虑对配置更改进行审计日志记录(未来增强功能)
|
||||
|
||||
### 多用户环境
|
||||
**风险:** 在多用户部署中,所有用户共享相同的 MCP 凭据
|
||||
|
||||
**风险理解:**
|
||||
用户 A 和用户 B 在访问 MCP 工具时都使用相同的令牌
|
||||
MCP 服务器无法区分不同的 TrustGraph 用户
|
||||
没有办法强制执行每个用户的权限或速率限制
|
||||
MCP 服务器上的审计日志显示来自相同凭据的所有请求
|
||||
如果一个用户的会话被盗,攻击者将拥有与所有用户相同的 MCP 访问权限
|
||||
|
||||
**这不是一个错误 - 这是一个该设计的基本限制。**
|
||||
|
||||
## 性能影响
|
||||
**最小开销:** 头部构建添加了可忽略的处理时间
|
||||
**网络影响:** 额外的 HTTP 头部增加了每个请求约 50-200 字节
|
||||
**内存使用:** 存储在配置中的令牌字符串会增加可忽略的内存
|
||||
|
||||
## 文档
|
||||
|
||||
### 用户文档
|
||||
[ ] 使用 `--auth-token` 参数更新 `tg-set-mcp-tool.md`
|
||||
[ ] 添加安全注意事项部分
|
||||
[ ] 提供使用 bearer 令牌的示例
|
||||
[ ] 记录令牌存储的影响
|
||||
|
||||
### 开发人员文档
|
||||
[ ] 在 `service.py` 中添加身份验证令牌处理的内联注释
|
||||
[ ] 记录头部构建逻辑
|
||||
[ ] 更新 MCP 工具配置模式文档
|
||||
|
||||
## 开放问题
|
||||
1. **令牌加密:** 我们是否应该在配置系统中实现加密令牌存储?
|
||||
2. **令牌刷新:** 未来是否支持 OAuth 刷新流程或令牌轮换?
|
||||
3. **替代身份验证方法:** 我们是否应该支持 Basic 身份验证、API 密钥或其他方法?
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
1. **环境变量用于令牌:** 将令牌存储在环境变量中而不是配置中
|
||||
**已拒绝:** complicates deployment and configuration management (使部署和配置管理复杂化)
|
||||
|
||||
2. **单独的密钥存储:** 使用专用的密钥管理系统
|
||||
**推迟:** Out of scope for initial implementation, consider future enhancement (超出初始实现的范围,考虑未来增强)
|
||||
|
||||
3. **多种身份验证方法:** 支持 Basic、API 密钥、OAuth 等。
|
||||
**已拒绝:** Bearer tokens cover most use cases, keep initial implementation simple (Bearer 令牌涵盖大多数用例,保持初始实现的简单性)
|
||||
|
||||
4. **加密令牌存储:** 在配置系统中加密令牌
|
||||
**推迟:** Configuration system security is broader concern, defer to future work (配置系统安全是一个更广泛的问题,推迟到未来工作)
|
||||
|
||||
5. **按调用令牌:** 允许在调用时传递令牌
|
||||
**已拒绝:** Violates separation of concerns, agent shouldn't handle credentials (违反了关注点分离,代理不应处理凭据)
|
||||
|
||||
## 引用
|
||||
[MCP 协议规范](https://github.com/modelcontextprotocol/spec)
|
||||
[HTTP Bearer 身份验证 (RFC 6750)](https://tools.ietf.org/html/rfc6750)
|
||||
[Current MCP Tool Service](../trustgraph-flow/trustgraph/agent/mcp_tool/service.py)
|
||||
[MCP 工具参数规范](./mcp-tool-arguments.md)
|
||||
|
||||
## 附录
|
||||
|
||||
### 用法示例
|
||||
|
||||
**设置带有身份验证的 MCP 工具:**
|
||||
```bash
|
||||
tg-set-mcp-tool \
|
||||
--id secure-tool \
|
||||
--tool-url https://secure-server.example.com/mcp \
|
||||
--auth-token eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
**展示 MCP 工具:**
|
||||
```bash
|
||||
tg-show-mcp-tools
|
||||
|
||||
ID Remote Name URL Auth
|
||||
----------- ----------- ------------------------------------ ------
|
||||
secure-tool secure-tool https://secure-server.example.com/mcp Yes
|
||||
public-tool public-tool http://localhost:3000/mcp No
|
||||
```
|
||||
|
||||
### 配置示例
|
||||
|
||||
**存储在配置系统中**:
|
||||
```json
|
||||
{
|
||||
"type": "mcp",
|
||||
"key": "secure-tool",
|
||||
"value": "{\"url\": \"https://secure-server.example.com/mcp\", \"auth-token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...\"}"
|
||||
}
|
||||
```
|
||||
|
||||
### 安全最佳实践
|
||||
|
||||
1. **使用 HTTPS**: 始终为具有身份验证的 MCP 服务器使用 HTTPS URL。
|
||||
2. **短生命周期令牌**: 尽可能使用具有过期时间的令牌。
|
||||
3. **最小权限**: 授予令牌最低要求的权限。
|
||||
4. **访问控制**: 限制对配置系统的访问。
|
||||
5. **令牌轮换**: 定期轮换令牌。
|
||||
6. **审计日志记录**: 监控配置更改以检测安全事件。
|
||||
266
docs/tech-specs/zh-cn/minio-to-s3-migration.zh-cn.md
Normal file
266
docs/tech-specs/zh-cn/minio-to-s3-migration.zh-cn.md
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
---
|
||||
layout: default
|
||||
title: "技术规范:S3 兼容的存储后端支持"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 技术规范:S3 兼容的存储后端支持
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
Librarian 服务使用 S3 兼容的对象存储来存储文档 Blob。此规范记录了实现,该实现支持任何 S3 兼容的后端,包括 MinIO、Ceph RADOS Gateway (RGW)、AWS S3、Cloudflare R2、DigitalOcean Spaces 等。
|
||||
|
||||
## 架构
|
||||
|
||||
### 存储组件
|
||||
**Blob 存储**: 通过 `minio` Python 客户端库实现的 S3 兼容对象存储
|
||||
**元数据存储**: Cassandra (存储 object_id 映射和文档元数据)
|
||||
**受影响的组件**: 仅限 Librarian 服务
|
||||
**存储模式**: 混合存储,元数据存储在 Cassandra 中,内容存储在 S3 兼容的存储中
|
||||
|
||||
### 实现
|
||||
**库**: `minio` Python 客户端 (支持任何 S3 兼容的 API)
|
||||
**位置**: `trustgraph-flow/trustgraph/librarian/blob_store.py`
|
||||
**操作**:
|
||||
`add()` - 使用 UUID object_id 存储 Blob
|
||||
`get()` - 根据 object_id 检索 Blob
|
||||
`remove()` - 根据 object_id 删除 Blob
|
||||
`ensure_bucket()` - 如果不存在,则创建 Bucket
|
||||
**Bucket**: `library`
|
||||
**对象路径**: `doc/{object_id}`
|
||||
**支持的 MIME 类型**: `text/plain`, `application/pdf`
|
||||
|
||||
### 关键文件
|
||||
1. `trustgraph-flow/trustgraph/librarian/blob_store.py` - BlobStore 实现
|
||||
2. `trustgraph-flow/trustgraph/librarian/librarian.py` - BlobStore 初始化
|
||||
3. `trustgraph-flow/trustgraph/librarian/service.py` - 服务配置
|
||||
4. `trustgraph-flow/pyproject.toml` - 依赖项 (`minio` 包)
|
||||
5. `docs/apis/api-librarian.md` - API 文档
|
||||
|
||||
## 支持的存储后端
|
||||
|
||||
此实现适用于任何 S3 兼容的对象存储系统:
|
||||
|
||||
### 已测试/支持
|
||||
**Ceph RADOS Gateway (RGW)** - 具有 S3 API 的分布式存储系统 (默认配置)
|
||||
**MinIO** - 轻量级的自托管对象存储
|
||||
**Garage** - 轻量级的分布式 S3 兼容存储
|
||||
|
||||
### 应该可以工作 (S3 兼容)
|
||||
**AWS S3** - Amazon 的云对象存储
|
||||
**Cloudflare R2** - Cloudflare 的 S3 兼容存储
|
||||
**DigitalOcean Spaces** - DigitalOcean 的对象存储
|
||||
**Wasabi** - S3 兼容的云存储
|
||||
**Backblaze B2** - S3 兼容的备份存储
|
||||
任何实现 S3 REST API 的服务
|
||||
|
||||
## 配置
|
||||
|
||||
### 命令行参数
|
||||
|
||||
```bash
|
||||
librarian \
|
||||
--object-store-endpoint <hostname:port> \
|
||||
--object-store-access-key <access_key> \
|
||||
--object-store-secret-key <secret_key> \
|
||||
[--object-store-use-ssl] \
|
||||
[--object-store-region <region>]
|
||||
```
|
||||
|
||||
**注意:** 请不要在端点中包含 `http://` 或 `https://`。 使用 `--object-store-use-ssl` 启用 HTTPS。
|
||||
|
||||
### 环境变量(备选方案)
|
||||
|
||||
```bash
|
||||
OBJECT_STORE_ENDPOINT=<hostname:port>
|
||||
OBJECT_STORE_ACCESS_KEY=<access_key>
|
||||
OBJECT_STORE_SECRET_KEY=<secret_key>
|
||||
OBJECT_STORE_USE_SSL=true|false # Optional, default: false
|
||||
OBJECT_STORE_REGION=<region> # Optional
|
||||
```
|
||||
|
||||
### 示例
|
||||
|
||||
**Ceph RADOS 网关(默认):**
|
||||
```bash
|
||||
--object-store-endpoint ceph-rgw:7480 \
|
||||
--object-store-access-key object-user \
|
||||
--object-store-secret-key object-password
|
||||
```
|
||||
|
||||
**MinIO:**
|
||||
```bash
|
||||
--object-store-endpoint minio:9000 \
|
||||
--object-store-access-key minioadmin \
|
||||
--object-store-secret-key minioadmin
|
||||
```
|
||||
|
||||
**云存储(兼容S3):**
|
||||
```bash
|
||||
--object-store-endpoint garage:3900 \
|
||||
--object-store-access-key GK000000000000000000000001 \
|
||||
--object-store-secret-key b171f00be9be4c32c734f4c05fe64c527a8ab5eb823b376cfa8c2531f70fc427
|
||||
```
|
||||
|
||||
**AWS S3 使用 SSL:**
|
||||
```bash
|
||||
--object-store-endpoint s3.amazonaws.com \
|
||||
--object-store-access-key AKIAIOSFODNN7EXAMPLE \
|
||||
--object-store-secret-key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY \
|
||||
--object-store-use-ssl \
|
||||
--object-store-region us-east-1
|
||||
```
|
||||
|
||||
## 身份验证
|
||||
|
||||
所有兼容 S3 的后端都需要 AWS 签名版本 4(或 v2)身份验证:
|
||||
|
||||
**访问密钥** - 公开标识符(类似于用户名)
|
||||
**密钥** - 私有签名密钥(类似于密码)
|
||||
|
||||
MinIO Python 客户端会自动处理所有签名计算。
|
||||
|
||||
### 创建凭证
|
||||
|
||||
**对于 MinIO:**
|
||||
```bash
|
||||
# Use default credentials or create user via MinIO Console
|
||||
minioadmin / minioadmin
|
||||
```
|
||||
|
||||
**适用于 Ceph RGW:**
|
||||
```bash
|
||||
radosgw-admin user create --uid="trustgraph" --display-name="TrustGraph Service"
|
||||
# Returns access_key and secret_key
|
||||
```
|
||||
|
||||
**适用于 AWS S3:**
|
||||
创建具有 S3 权限的 IAM 用户
|
||||
在 AWS 控制台中生成访问密钥
|
||||
|
||||
## 库选择:MinIO Python 客户端
|
||||
|
||||
**理由:**
|
||||
轻量级(约 500KB,而 boto3 约为 50MB)
|
||||
与 S3 兼容,适用于任何 S3 API 端点
|
||||
对于基本操作,API 比 boto3 更简单
|
||||
已在使用中,无需迁移
|
||||
经过 MinIO 和其他 S3 系统的严格测试
|
||||
|
||||
## BlobStore 实施
|
||||
|
||||
**位置:** `trustgraph-flow/trustgraph/librarian/blob_store.py`
|
||||
|
||||
```python
|
||||
from minio import Minio
|
||||
import io
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BlobStore:
|
||||
"""
|
||||
S3-compatible blob storage for document content.
|
||||
Supports MinIO, Ceph RGW, AWS S3, and other S3-compatible backends.
|
||||
"""
|
||||
|
||||
def __init__(self, endpoint, access_key, secret_key, bucket_name,
|
||||
use_ssl=False, region=None):
|
||||
"""
|
||||
Initialize S3-compatible blob storage.
|
||||
|
||||
Args:
|
||||
endpoint: S3 endpoint (e.g., "minio:9000", "ceph-rgw:7480")
|
||||
access_key: S3 access key
|
||||
secret_key: S3 secret key
|
||||
bucket_name: Bucket name for storage
|
||||
use_ssl: Use HTTPS instead of HTTP (default: False)
|
||||
region: S3 region (optional, e.g., "us-east-1")
|
||||
"""
|
||||
self.client = Minio(
|
||||
endpoint=endpoint,
|
||||
access_key=access_key,
|
||||
secret_key=secret_key,
|
||||
secure=use_ssl,
|
||||
region=region,
|
||||
)
|
||||
|
||||
self.bucket_name = bucket_name
|
||||
|
||||
protocol = "https" if use_ssl else "http"
|
||||
logger.info(f"Connected to S3-compatible storage at {protocol}://{endpoint}")
|
||||
|
||||
self.ensure_bucket()
|
||||
|
||||
def ensure_bucket(self):
|
||||
"""Create bucket if it doesn't exist"""
|
||||
found = self.client.bucket_exists(bucket_name=self.bucket_name)
|
||||
if not found:
|
||||
self.client.make_bucket(bucket_name=self.bucket_name)
|
||||
logger.info(f"Created bucket {self.bucket_name}")
|
||||
else:
|
||||
logger.debug(f"Bucket {self.bucket_name} already exists")
|
||||
|
||||
async def add(self, object_id, blob, kind):
|
||||
"""Store blob in S3-compatible storage"""
|
||||
self.client.put_object(
|
||||
bucket_name=self.bucket_name,
|
||||
object_name=f"doc/{object_id}",
|
||||
length=len(blob),
|
||||
data=io.BytesIO(blob),
|
||||
content_type=kind,
|
||||
)
|
||||
logger.debug("Add blob complete")
|
||||
|
||||
async def remove(self, object_id):
|
||||
"""Delete blob from S3-compatible storage"""
|
||||
self.client.remove_object(
|
||||
bucket_name=self.bucket_name,
|
||||
object_name=f"doc/{object_id}",
|
||||
)
|
||||
logger.debug("Remove blob complete")
|
||||
|
||||
async def get(self, object_id):
|
||||
"""Retrieve blob from S3-compatible storage"""
|
||||
resp = self.client.get_object(
|
||||
bucket_name=self.bucket_name,
|
||||
object_name=f"doc/{object_id}",
|
||||
)
|
||||
return resp.read()
|
||||
```
|
||||
|
||||
## 关键优势
|
||||
|
||||
1. **无厂商锁定** - 适用于任何兼容 S3 的存储
|
||||
2. **轻量级** - MinIO 客户端仅约 500KB
|
||||
3. **简单配置** - 仅需端点 + 凭据
|
||||
4. **无需数据迁移** - 作为后端之间的直接替代方案
|
||||
5. **经过严格测试** - MinIO 客户端与所有主要的 S3 实现兼容
|
||||
|
||||
## 实现状态
|
||||
|
||||
所有的代码都已更新为使用通用的 S3 参数名称:
|
||||
|
||||
✅ `blob_store.py` - 更新为接受 `endpoint`, `access_key`, `secret_key`
|
||||
✅ `librarian.py` - 更新了参数名称
|
||||
✅ `service.py` - 更新了 CLI 参数和配置
|
||||
✅ 文档已更新
|
||||
|
||||
## 未来增强功能
|
||||
|
||||
1. **SSL/TLS 支持** - 添加 `--s3-use-ssl` 标志以支持 HTTPS
|
||||
2. **重试逻辑** - 针对瞬时错误实现指数级退避
|
||||
3. **预签名 URL** - 生成临时上传/下载 URL
|
||||
4. **多区域支持** - 在区域之间复制数据块
|
||||
5. **CDN 集成** - 通过 CDN 提供数据块
|
||||
6. **存储类别** - 使用 S3 存储类别进行成本优化
|
||||
7. **生命周期策略** - 自动归档/删除
|
||||
8. **版本控制** - 存储数据块的多个版本
|
||||
|
||||
## 参考文献
|
||||
|
||||
MinIO Python 客户端: https://min.io/docs/minio/linux/developers/python/API.html
|
||||
Ceph RGW S3 API: https://docs.ceph.com/en/latest/radosgw/s3/
|
||||
S3 API 参考: https://docs.aws.amazon.com/AmazonS3/latest/API/Welcome.html
|
||||
186
docs/tech-specs/zh-cn/more-config-cli.zh-cn.md
Normal file
186
docs/tech-specs/zh-cn/more-config-cli.zh-cn.md
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
---
|
||||
layout: default
|
||||
title: "更高级的 CLI 技术规格"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 更高级的 CLI 技术规格
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
该规范描述了 TrustGraph 的增强型命令行配置功能,允许用户通过精细的 CLI 命令来管理单个配置项。 该集成支持四个主要用例:
|
||||
|
||||
1. **列出配置项**: 显示特定类型的配置键
|
||||
2. **获取配置项**: 获取特定配置值
|
||||
3. **设置配置项**: 设置或更新单个配置项
|
||||
4. **删除配置项**: 删除特定配置项
|
||||
|
||||
## 目标
|
||||
|
||||
- **精细控制**: 能够管理单个配置项,而不是批量操作
|
||||
- **基于类型的列出**: 允许用户按类型探索配置项
|
||||
- **单个项操作**: 提供获取/设置/删除单个配置项的命令
|
||||
- **API 集成**: 利用现有的 Config API 进行所有操作
|
||||
- **一致的 CLI 模式**: 遵循 TrustGraph 的标准 CLI 约定和模式
|
||||
- **错误处理**: 提供无效操作的清晰错误消息
|
||||
- **JSON 输出**: 支持结构化输出,用于程序化使用
|
||||
- **文档**: 包含全面的帮助和用法示例
|
||||
|
||||
## 背景
|
||||
|
||||
目前,TrustGraph 通过 Config API 和一个名为 `tg-show-config` 的单一 CLI 命令来管理配置。 该命令显示整个配置。 虽然这对于查看配置有效,但缺乏精细的管理功能。
|
||||
|
||||
目前存在以下限制:
|
||||
- 无法从 CLI 列出按类型配置项
|
||||
- 没有 CLI 命令用于检索特定配置值
|
||||
- 没有 CLI 命令用于设置单个配置项
|
||||
- 没有 CLI 命令用于删除特定配置项
|
||||
|
||||
该规范通过添加四个新的 CLI 命令来解决这些问题,从而提供精细的配置管理。 通过将单个 Config API 操作暴露到 CLI 命令中,TrustGraph 可以:
|
||||
- 启用脚本化的配置管理
|
||||
- 允许用户按类型探索配置结构
|
||||
- 支持有针对性的配置更新
|
||||
- 提供精细的配置控制
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 架构
|
||||
|
||||
增强的 CLI 配置需要以下技术组件:
|
||||
|
||||
1. **tg-list-config-items**
|
||||
- 列出指定类型的配置键
|
||||
- 调用 Config.list(type) API 方法
|
||||
- 输出配置键列表
|
||||
|
||||
模块:`trustgraph.cli.list_config_items`
|
||||
|
||||
2. **tg-get-config-item**
|
||||
- 检索特定配置项
|
||||
- 调用 Config.get(keys) API 方法
|
||||
- 以 JSON 格式输出配置值
|
||||
|
||||
模块:`trustgraph.cli.get_config_item`
|
||||
|
||||
3. **tg-put-config-item**
|
||||
- 设置或更新配置项
|
||||
- 调用 Config.put(values) API 方法
|
||||
- 接受类型、键和值参数
|
||||
|
||||
模块:`trustgraph.cli.put_config_item`
|
||||
|
||||
4. **tg-delete-config-item**
|
||||
- 删除配置项
|
||||
- 调用 Config.delete(keys) API 方法
|
||||
- 接受类型和键参数
|
||||
|
||||
模块:`trustgraph.cli.delete_config_item`
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### ConfigKey 和 ConfigValue
|
||||
|
||||
这些命令使用来自 `trustgraph.api.types` 的现有数据结构:
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass
|
||||
class ConfigKey:
|
||||
type : str
|
||||
key : str
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ConfigValue:
|
||||
type : str
|
||||
key : str
|
||||
value : str
|
||||
```
|
||||
|
||||
这种方法允许:
|
||||
- 在 CLI 和 API 中保持数据的一致性
|
||||
- 类型安全的配置操作
|
||||
- 结构化输入/输出格式
|
||||
- 与现有的 Config API 集成
|
||||
|
||||
### CLI 命令规范
|
||||
|
||||
#### tg-list-config-items
|
||||
```bash
|
||||
tg-list-config-items --type <config-type> [--format text|json] [--api-url <url>]
|
||||
```
|
||||
- **目的**: 列出给定类型的所有配置键
|
||||
- **API 调用**: `Config.list(type)`
|
||||
- **输出**:
|
||||
- `text` (默认): 键按换行分隔
|
||||
- `json`: 键的 JSON 数组
|
||||
|
||||
#### tg-get-config-item
|
||||
```bash
|
||||
tg-get-config-item --type <type> --key <key> [--format text|json] [--api-url <url>]
|
||||
```
|
||||
- **目的**: 获取特定配置项
|
||||
- **API 调用**: `Config.get([ConfigKey(type, key)])`
|
||||
- **输出**:
|
||||
- `text` (默认): 原始字符串值
|
||||
- `json`: JSON 编码的字符串值
|
||||
|
||||
#### tg-put-config-item
|
||||
```bash
|
||||
tg-put-config-item --type <type> --key <key> --value <value> [--api-url <url>]
|
||||
tg-put-config-item --type <type> --key <key> --stdin [--api-url <url>]
|
||||
```
|
||||
- **目的**: 设置或更新配置项
|
||||
- **API 调用**: `Config.put([ConfigValue(type, key, value)])`
|
||||
- **输入选项**:
|
||||
- `--value <字符串>`: 直接在命令行中提供的字符串值
|
||||
- `--stdin`: 从标准输入读取整个输入作为配置值
|
||||
- 标准输入读取为原始文本 (保留换行符、空格等)
|
||||
- 支持通过文件、命令或交互式输入
|
||||
|
||||
#### tg-delete-config-item
|
||||
```bash
|
||||
tg-delete-config-item --type <type> --key <key> [--api-url <url>]
|
||||
```
|
||||
- **目的**: 删除配置项
|
||||
- **API 调用**: `Config.delete([ConfigKey(type, key)])`
|
||||
- **输出**: 成功确认
|
||||
|
||||
### 实现细节
|
||||
|
||||
所有命令都遵循 TrustGraph 的标准 CLI 模式:
|
||||
- 使用 `argparse` 进行命令行参数解析
|
||||
- 导入和使用 `trustgraph.api.Api` 进行后端通信
|
||||
- 遵循现有 CLI 命令的相同错误处理模式
|
||||
- 支持标准 `--api-url` 参数,用于配置 API 端点
|
||||
- 提供描述性的帮助文本和用法示例
|
||||
|
||||
#### 输出格式处理
|
||||
|
||||
**文本格式 (默认)**:
|
||||
- `tg-list-config-items`: 键按新行分隔,纯文本
|
||||
- `tg-get-config-item`: 原始字符串值,无引号或编码
|
||||
|
||||
**JSON 格式**:
|
||||
- `tg-list-config-items`: 字符串数组 `["key1", "key2", "key3"]`
|
||||
- `tg-get-config-item`: JSON 编码的字符串值 `"实际字符串值"`
|
||||
|
||||
#### 输入处理
|
||||
|
||||
**tg-put-config-item** 支持两种互斥的输入方法:
|
||||
- `--value <字符串>`: 命令行中的直接字符串值
|
||||
- `--stdin`: 从标准输入读取整个输入作为配置值
|
||||
- 标准输入读取为原始文本 (保留换行符、空格等)
|
||||
- 支持通过文件、命令或交互式输入
|
||||
|
||||
## 安全性
|
||||
|
||||
- 如果配置项包含敏感信息,如何安全地处理这些信息?
|
||||
- 如何验证用户输入,以防止恶意攻击?
|
||||
- 如何控制对配置项的访问权限?
|
||||
|
||||
## 开放问题
|
||||
|
||||
- 是否应该支持命令以批量操作 (多个键) 之外的单个项?
|
||||
- 成功确认消息应该使用哪种格式?
|
||||
- 用户应该如何发现和使用配置类型?
|
||||
780
docs/tech-specs/zh-cn/multi-tenant-support.zh-cn.md
Normal file
780
docs/tech-specs/zh-cn/multi-tenant-support.zh-cn.md
Normal file
|
|
@ -0,0 +1,780 @@
|
|||
---
|
||||
layout: default
|
||||
title: "技术规范:多租户支持"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 技术规范:多租户支持
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
通过修复参数名称不匹配的问题,从而解决阻止队列自定义的问题,并添加 Cassandra 键空间参数化,以实现多租户部署。
|
||||
|
||||
## 架构上下文
|
||||
|
||||
### 基于流的队列解析
|
||||
|
||||
TrustGraph 系统使用**基于流的架构**进行动态队列解析,该架构本质上支持多租户:
|
||||
|
||||
**流定义**存储在 Cassandra 中,并通过接口定义指定队列名称。
|
||||
**队列名称使用模板**,其中包含 `{id}` 变量,这些变量会被替换为流实例 ID。
|
||||
**服务在请求时动态解析队列**,通过查找流配置。
|
||||
**每个租户可以拥有独特的流**,具有不同的队列名称,从而提供隔离。
|
||||
|
||||
示例流接口定义:
|
||||
```json
|
||||
{
|
||||
"interfaces": {
|
||||
"triples-store": "persistent://tg/flow/triples-store:{id}",
|
||||
"graph-embeddings-store": "persistent://tg/flow/graph-embeddings-store:{id}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
当租户 A 启动流程 `tenant-a-prod`,而租户 B 启动流程 `tenant-b-prod` 时,它们会自动获得隔离的队列:
|
||||
`persistent://tg/flow/triples-store:tenant-a-prod`
|
||||
`persistent://tg/flow/triples-store:tenant-b-prod`
|
||||
|
||||
**为多租户设计的服务:**
|
||||
✅ **知识管理 (核心)** - 动态解析从请求中传递的流程配置中的队列
|
||||
|
||||
**需要修复的服务:**
|
||||
🔴 **配置服务** - 参数名称不匹配,无法自定义队列
|
||||
🔴 **图书管理员服务** - 预定义的存储管理主题(见下文)
|
||||
🔴 **所有服务** - 无法自定义 Cassandra keyspace
|
||||
|
||||
## 问题描述
|
||||
|
||||
### 问题 #1:AsyncProcessor 中的参数名称不匹配
|
||||
**CLI 定义:** `--config-queue` (命名不明确)
|
||||
**Argparse 转换:** `config_queue` (在 params 字典中)
|
||||
**代码查找:** `config_push_queue`
|
||||
**结果:** 参数被忽略,默认为 `persistent://tg/config/config`
|
||||
**影响:** 影响所有 32 多个从 AsyncProcessor 继承的服务
|
||||
**问题:** 多租户部署无法使用租户特定的配置队列
|
||||
**解决方案:** 将 CLI 参数重命名为 `--config-push-queue`,以提高清晰度(可以接受破坏性更改,因为该功能当前已损坏)
|
||||
|
||||
### 问题 #2:配置服务中的参数名称不匹配
|
||||
**CLI 定义:** `--push-queue` (命名模糊)
|
||||
**Argparse 转换:** `push_queue` (在 params 字典中)
|
||||
**代码查找:** `config_push_queue`
|
||||
**结果:** 参数被忽略
|
||||
**影响:** 配置服务无法使用自定义推送队列
|
||||
**解决方案:** 将 CLI 参数重命名为 `--config-push-queue`,以提高一致性和清晰度(可以接受破坏性更改)
|
||||
|
||||
### 问题 #3:预定义的 Cassandra Keyspace
|
||||
**当前:** Keyspace 在各种服务中硬编码为 `"config"`、`"knowledge"`、`"librarian"`
|
||||
**结果:** 无法为多租户部署自定义 keyspace
|
||||
**影响:** 配置、核心和图书管理员服务
|
||||
**问题:** 多个租户无法使用单独的 Cassandra keyspace
|
||||
|
||||
### 问题 #4:集合管理架构 ✅ 已完成
|
||||
**之前:** 集合存储在图书管理员 keyspace 中的单独的集合表中
|
||||
**之前:** 图书管理员使用 4 个硬编码的存储管理主题来协调集合创建/删除:
|
||||
`vector_storage_management_topic`
|
||||
`object_storage_management_topic`
|
||||
`triples_storage_management_topic`
|
||||
`storage_management_response_topic`
|
||||
**问题(已解决):**
|
||||
无法为多租户部署自定义硬编码主题
|
||||
图书管理员和 4 个或更多存储服务之间的复杂异步协调
|
||||
单独的 Cassandra 表和管理基础设施
|
||||
关键操作的非持久性请求/响应队列
|
||||
**已实施的解决方案:** 将集合迁移到配置服务存储,使用配置推送进行分发
|
||||
**状态:** 所有存储后端已迁移到 `CollectionConfigHandler` 模式
|
||||
|
||||
## 解决方案
|
||||
|
||||
此规范解决了问题 #1、#2、#3 和 #4。
|
||||
|
||||
### 第一部分:修复参数名称不匹配
|
||||
|
||||
#### 更改 1:AsyncProcessor 基类 - 重命名 CLI 参数
|
||||
**文件:** `trustgraph-base/trustgraph/base/async_processor.py`
|
||||
**行:** 260-264
|
||||
|
||||
**当前:**
|
||||
```python
|
||||
parser.add_argument(
|
||||
'--config-queue',
|
||||
default=default_config_queue,
|
||||
help=f'Config push queue {default_config_queue}',
|
||||
)
|
||||
```
|
||||
|
||||
**已修复:**
|
||||
```python
|
||||
parser.add_argument(
|
||||
'--config-push-queue',
|
||||
default=default_config_queue,
|
||||
help=f'Config push queue (default: {default_config_queue})',
|
||||
)
|
||||
```
|
||||
|
||||
**理由:**
|
||||
命名更清晰、更明确
|
||||
与内部变量名 `config_push_queue` 匹配
|
||||
允许进行重大更改,因为该功能目前不可用
|
||||
params.get() 不需要任何代码更改,因为它已经查找正确的名称
|
||||
|
||||
#### 更改 2:配置服务 - 重命名 CLI 参数
|
||||
**文件:** `trustgraph-flow/trustgraph/config/service/service.py`
|
||||
**行:** 276-279
|
||||
|
||||
**当前:**
|
||||
```python
|
||||
parser.add_argument(
|
||||
'--push-queue',
|
||||
default=default_config_push_queue,
|
||||
help=f'Config push queue (default: {default_config_push_queue})'
|
||||
)
|
||||
```
|
||||
|
||||
**固定:**
|
||||
```python
|
||||
parser.add_argument(
|
||||
'--config-push-queue',
|
||||
default=default_config_push_queue,
|
||||
help=f'Config push queue (default: {default_config_push_queue})'
|
||||
)
|
||||
```
|
||||
|
||||
**理由:**
|
||||
更清晰的命名 - "config-push-queue" 比仅仅 "push-queue" 更明确。
|
||||
与内部变量名 `config_push_queue` 匹配。
|
||||
与 AsyncProcessor 的 `--config-push-queue` 参数一致。
|
||||
即使是重大更改,也是可以接受的,因为该功能目前不可用。
|
||||
params.get() 中不需要任何代码更改 - 它已经查找正确的名称。
|
||||
|
||||
### 第二部分:添加 Cassandra 键空间参数化
|
||||
|
||||
#### 更改 3:向 cassandra_config 模块添加键空间参数
|
||||
**文件:** `trustgraph-base/trustgraph/base/cassandra_config.py`
|
||||
|
||||
**添加 CLI 参数**(在 `add_cassandra_args()` 函数中):
|
||||
```python
|
||||
parser.add_argument(
|
||||
'--cassandra-keyspace',
|
||||
default=None,
|
||||
help='Cassandra keyspace (default: service-specific)'
|
||||
)
|
||||
```
|
||||
|
||||
**添加环境变量支持** (在 `resolve_cassandra_config()` 函数中):
|
||||
```python
|
||||
keyspace = params.get(
|
||||
"cassandra_keyspace",
|
||||
os.environ.get("CASSANDRA_KEYSPACE")
|
||||
)
|
||||
```
|
||||
|
||||
**更新 `resolve_cassandra_config()` 的返回值:**
|
||||
当前返回:`(hosts, username, password)`
|
||||
更改为返回:`(hosts, username, password, keyspace)`
|
||||
|
||||
**理由:**
|
||||
与现有的 Cassandra 配置模式一致
|
||||
通过 `add_cassandra_args()` 可供所有服务使用
|
||||
支持 CLI 和环境变量配置
|
||||
|
||||
#### 变更 4:配置服务 - 使用参数化 Keyspace
|
||||
**文件:** `trustgraph-flow/trustgraph/config/service/service.py`
|
||||
|
||||
**第 30 行** - 移除硬编码的 Keyspace:
|
||||
```python
|
||||
# DELETE THIS LINE:
|
||||
keyspace = "config"
|
||||
```
|
||||
|
||||
**第69-73行** - 更新 Cassandra 配置解析:
|
||||
|
||||
**当前:**
|
||||
```python
|
||||
cassandra_host, cassandra_username, cassandra_password = \
|
||||
resolve_cassandra_config(params)
|
||||
```
|
||||
|
||||
**已修复:**
|
||||
```python
|
||||
cassandra_host, cassandra_username, cassandra_password, keyspace = \
|
||||
resolve_cassandra_config(params, default_keyspace="config")
|
||||
```
|
||||
|
||||
**原因:**
|
||||
保持与默认值为 "config" 的配置向后兼容。
|
||||
允许通过 `--cassandra-keyspace` 或 `CASSANDRA_KEYSPACE` 进行覆盖。
|
||||
|
||||
#### 变更 5:核心/知识服务 - 使用参数化键空间
|
||||
**文件:** `trustgraph-flow/trustgraph/cores/service.py`
|
||||
|
||||
**第 37 行** - 移除硬编码的键空间:
|
||||
```python
|
||||
# DELETE THIS LINE:
|
||||
keyspace = "knowledge"
|
||||
```
|
||||
|
||||
**更新 Cassandra 配置解析**(位置类似于配置服务):
|
||||
```python
|
||||
cassandra_host, cassandra_username, cassandra_password, keyspace = \
|
||||
resolve_cassandra_config(params, default_keyspace="knowledge")
|
||||
```
|
||||
|
||||
#### 变更 6:图书管理员服务 - 使用参数化键空间
|
||||
**文件:** `trustgraph-flow/trustgraph/librarian/service.py`
|
||||
|
||||
**第 51 行** - 移除硬编码的键空间:
|
||||
```python
|
||||
# DELETE THIS LINE:
|
||||
keyspace = "librarian"
|
||||
```
|
||||
|
||||
**更新 Cassandra 配置解析**(位置与配置服务类似):
|
||||
```python
|
||||
cassandra_host, cassandra_username, cassandra_password, keyspace = \
|
||||
resolve_cassandra_config(params, default_keyspace="librarian")
|
||||
```
|
||||
|
||||
### 第三部分:将集合管理迁移到配置服务
|
||||
|
||||
#### 概述
|
||||
将集合从 Cassandra librarian 键空间迁移到配置服务存储。这消除了硬编码的存储管理主题,并通过使用现有的配置推送机制进行分发,简化了架构。
|
||||
|
||||
#### 当前架构
|
||||
```
|
||||
API Request → Gateway → Librarian Service
|
||||
↓
|
||||
CollectionManager
|
||||
↓
|
||||
Cassandra Collections Table (librarian keyspace)
|
||||
↓
|
||||
Broadcast to 4 Storage Management Topics (hardcoded)
|
||||
↓
|
||||
Wait for 4+ Storage Service Responses
|
||||
↓
|
||||
Response to Gateway
|
||||
```
|
||||
|
||||
#### 新架构
|
||||
```
|
||||
API Request → Gateway → Librarian Service
|
||||
↓
|
||||
CollectionManager
|
||||
↓
|
||||
Config Service API (put/delete/getvalues)
|
||||
↓
|
||||
Cassandra Config Table (class='collections', key='user:collection')
|
||||
↓
|
||||
Config Push (to all subscribers on config-push-queue)
|
||||
↓
|
||||
All Storage Services receive config update independently
|
||||
```
|
||||
|
||||
#### 变更 7:集合管理器 - 使用配置服务 API
|
||||
**文件:** `trustgraph-flow/trustgraph/librarian/collection_manager.py`
|
||||
|
||||
**移除:**
|
||||
`LibraryTableStore` 的使用(第 33 行,第 40-41 行)
|
||||
存储管理生产者初始化(第 86-140 行)
|
||||
`on_storage_response` 方法(第 400-430 行)
|
||||
`pending_deletions` 跟踪(第 57 行,第 90-96 行,以及整个使用过程)
|
||||
|
||||
**添加:**
|
||||
用于 API 调用的配置服务客户端(请求/响应模式)
|
||||
|
||||
**配置客户端设置:**
|
||||
```python
|
||||
# In __init__, add config request/response producers/consumers
|
||||
from trustgraph.schema.services.config import ConfigRequest, ConfigResponse
|
||||
|
||||
# Producer for config requests
|
||||
self.config_request_producer = Producer(
|
||||
client=pulsar_client,
|
||||
topic=config_request_queue,
|
||||
schema=ConfigRequest,
|
||||
)
|
||||
|
||||
# Consumer for config responses (with correlation ID)
|
||||
self.config_response_consumer = Consumer(
|
||||
taskgroup=taskgroup,
|
||||
client=pulsar_client,
|
||||
flow=None,
|
||||
topic=config_response_queue,
|
||||
subscriber=f"{id}-config",
|
||||
schema=ConfigResponse,
|
||||
handler=self.on_config_response,
|
||||
)
|
||||
|
||||
# Tracking for pending config requests
|
||||
self.pending_config_requests = {} # request_id -> asyncio.Event
|
||||
```
|
||||
|
||||
**修改 `list_collections` (第145-180行):**
|
||||
```python
|
||||
async def list_collections(self, user, tag_filter=None, limit=None):
|
||||
"""List collections from config service"""
|
||||
# Send getvalues request to config service
|
||||
request = ConfigRequest(
|
||||
id=str(uuid.uuid4()),
|
||||
operation='getvalues',
|
||||
type='collections',
|
||||
)
|
||||
|
||||
# Send request and wait for response
|
||||
response = await self.send_config_request(request)
|
||||
|
||||
# Parse collections from response
|
||||
collections = []
|
||||
for key, value_json in response.values.items():
|
||||
if ":" in key:
|
||||
coll_user, collection = key.split(":", 1)
|
||||
if coll_user == user:
|
||||
metadata = json.loads(value_json)
|
||||
collections.append(CollectionMetadata(**metadata))
|
||||
|
||||
# Apply tag filtering in-memory (as before)
|
||||
if tag_filter:
|
||||
collections = [c for c in collections if any(tag in c.tags for tag in tag_filter)]
|
||||
|
||||
# Apply limit
|
||||
if limit:
|
||||
collections = collections[:limit]
|
||||
|
||||
return collections
|
||||
|
||||
async def send_config_request(self, request):
|
||||
"""Send config request and wait for response"""
|
||||
event = asyncio.Event()
|
||||
self.pending_config_requests[request.id] = event
|
||||
|
||||
await self.config_request_producer.send(request)
|
||||
await event.wait()
|
||||
|
||||
return self.pending_config_requests.pop(request.id + "_response")
|
||||
|
||||
async def on_config_response(self, message, consumer, flow):
|
||||
"""Handle config response"""
|
||||
response = message.value()
|
||||
if response.id in self.pending_config_requests:
|
||||
self.pending_config_requests[response.id + "_response"] = response
|
||||
self.pending_config_requests[response.id].set()
|
||||
```
|
||||
|
||||
**修改 `update_collection` (第182-312行):**
|
||||
```python
|
||||
async def update_collection(self, user, collection, name, description, tags):
|
||||
"""Update collection via config service"""
|
||||
# Create metadata
|
||||
metadata = CollectionMetadata(
|
||||
user=user,
|
||||
collection=collection,
|
||||
name=name,
|
||||
description=description,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
# Send put request to config service
|
||||
request = ConfigRequest(
|
||||
id=str(uuid.uuid4()),
|
||||
operation='put',
|
||||
type='collections',
|
||||
key=f'{user}:{collection}',
|
||||
value=json.dumps(metadata.to_dict()),
|
||||
)
|
||||
|
||||
response = await self.send_config_request(request)
|
||||
|
||||
if response.error:
|
||||
raise RuntimeError(f"Config update failed: {response.error.message}")
|
||||
|
||||
# Config service will trigger config push automatically
|
||||
# Storage services will receive update and create collections
|
||||
```
|
||||
|
||||
**修改 `delete_collection` (第314-398行):**
|
||||
```python
|
||||
async def delete_collection(self, user, collection):
|
||||
"""Delete collection via config service"""
|
||||
# Send delete request to config service
|
||||
request = ConfigRequest(
|
||||
id=str(uuid.uuid4()),
|
||||
operation='delete',
|
||||
type='collections',
|
||||
key=f'{user}:{collection}',
|
||||
)
|
||||
|
||||
response = await self.send_config_request(request)
|
||||
|
||||
if response.error:
|
||||
raise RuntimeError(f"Config delete failed: {response.error.message}")
|
||||
|
||||
# Config service will trigger config push automatically
|
||||
# Storage services will receive update and delete collections
|
||||
```
|
||||
|
||||
**集合元数据格式:**
|
||||
存储在配置表中,格式为:`class='collections', key='user:collection'`
|
||||
值是 JSON 序列化的 CollectionMetadata (不包含时间戳字段)
|
||||
字段:`user`, `collection`, `name`, `description`, `tags`
|
||||
示例:`class='collections', key='alice:my-docs', value='{"user":"alice","collection":"my-docs","name":"My Documents","description":"...","tags":["work"]}'`
|
||||
|
||||
#### 变更 8: Librarian Service - 移除存储管理基础设施
|
||||
**文件:** `trustgraph-flow/trustgraph/librarian/service.py`
|
||||
|
||||
**移除:**
|
||||
存储管理生产者 (173-190 行):
|
||||
`vector_storage_management_producer`
|
||||
`object_storage_management_producer`
|
||||
`triples_storage_management_producer`
|
||||
存储响应消费者 (192-201 行)
|
||||
`on_storage_response` 处理程序 (467-473 行)
|
||||
|
||||
**修改:**
|
||||
CollectionManager 初始化 (215-224 行) - 移除存储生产者参数
|
||||
|
||||
**注意:** 外部集合 API 保持不变:
|
||||
`list-collections`
|
||||
`update-collection`
|
||||
`delete-collection`
|
||||
|
||||
#### 变更 9: 从 LibraryTableStore 中移除 Collections 表
|
||||
**文件:** `trustgraph-flow/trustgraph/tables/library.py`
|
||||
|
||||
**删除:**
|
||||
Collections 表的 CREATE 语句 (114-127 行)
|
||||
Collections 预处理语句 (205-240 行)
|
||||
所有集合方法 (578-717 行):
|
||||
`ensure_collection_exists`
|
||||
`list_collections`
|
||||
`update_collection`
|
||||
`delete_collection`
|
||||
`get_collection`
|
||||
`create_collection`
|
||||
|
||||
**原因:**
|
||||
集合现在存储在配置表中
|
||||
这是一个破坏性变更,但无需数据迁移
|
||||
显著简化了 librarian service
|
||||
|
||||
#### 变更 10: 存储服务 - 基于配置的集合管理 ✅ 已完成
|
||||
|
||||
**状态:** 所有 11 个存储后端都已迁移到使用 `CollectionConfigHandler`。
|
||||
|
||||
**受影响的服务 (总共 11 个):**
|
||||
文档嵌入: milvus, pinecone, qdrant
|
||||
图嵌入: milvus, pinecone, qdrant
|
||||
对象存储: cassandra
|
||||
三元组存储: cassandra, falkordb, memgraph, neo4j
|
||||
|
||||
**文件:**
|
||||
`trustgraph-flow/trustgraph/storage/doc_embeddings/milvus/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/doc_embeddings/pinecone/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/doc_embeddings/qdrant/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/graph_embeddings/milvus/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/graph_embeddings/pinecone/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/graph_embeddings/qdrant/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/objects/cassandra/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/triples/cassandra/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/triples/falkordb/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/triples/memgraph/write.py`
|
||||
`trustgraph-flow/trustgraph/storage/triples/neo4j/write.py`
|
||||
|
||||
**实现模式 (所有服务):**
|
||||
|
||||
1. **在 `__init__` 中注册配置处理程序:**
|
||||
```python
|
||||
# Add after AsyncProcessor initialization
|
||||
self.register_config_handler(self.on_collection_config)
|
||||
self.known_collections = set() # Track (user, collection) tuples
|
||||
```
|
||||
|
||||
2. **实现配置处理器:**
|
||||
```python
|
||||
async def on_collection_config(self, config, version):
|
||||
"""Handle collection configuration updates"""
|
||||
logger.info(f"Collection config version: {version}")
|
||||
|
||||
if "collections" not in config:
|
||||
return
|
||||
|
||||
# Parse collections from config
|
||||
# Key format: "user:collection" in config["collections"]
|
||||
config_collections = set()
|
||||
for key in config["collections"].keys():
|
||||
if ":" in key:
|
||||
user, collection = key.split(":", 1)
|
||||
config_collections.add((user, collection))
|
||||
|
||||
# Determine changes
|
||||
to_create = config_collections - self.known_collections
|
||||
to_delete = self.known_collections - config_collections
|
||||
|
||||
# Create new collections (idempotent)
|
||||
for user, collection in to_create:
|
||||
try:
|
||||
await self.create_collection_internal(user, collection)
|
||||
self.known_collections.add((user, collection))
|
||||
logger.info(f"Created collection: {user}/{collection}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create {user}/{collection}: {e}")
|
||||
|
||||
# Delete removed collections (idempotent)
|
||||
for user, collection in to_delete:
|
||||
try:
|
||||
await self.delete_collection_internal(user, collection)
|
||||
self.known_collections.discard((user, collection))
|
||||
logger.info(f"Deleted collection: {user}/{collection}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete {user}/{collection}: {e}")
|
||||
```
|
||||
|
||||
3. **初始化启动时的已知集合:**
|
||||
```python
|
||||
async def start(self):
|
||||
"""Start the processor"""
|
||||
await super().start()
|
||||
await self.sync_known_collections()
|
||||
|
||||
async def sync_known_collections(self):
|
||||
"""Query backend to populate known_collections set"""
|
||||
# Backend-specific implementation:
|
||||
# - Milvus/Pinecone/Qdrant: List collections/indexes matching naming pattern
|
||||
# - Cassandra: Query keyspaces or collection metadata
|
||||
# - Neo4j/Memgraph/FalkorDB: Query CollectionMetadata nodes
|
||||
pass
|
||||
```
|
||||
|
||||
4. **重构现有的处理方法:**
|
||||
```python
|
||||
# Rename and remove response sending:
|
||||
# handle_create_collection → create_collection_internal
|
||||
# handle_delete_collection → delete_collection_internal
|
||||
|
||||
async def create_collection_internal(self, user, collection):
|
||||
"""Create collection (idempotent)"""
|
||||
# Same logic as current handle_create_collection
|
||||
# But remove response producer calls
|
||||
# Handle "already exists" gracefully
|
||||
pass
|
||||
|
||||
async def delete_collection_internal(self, user, collection):
|
||||
"""Delete collection (idempotent)"""
|
||||
# Same logic as current handle_delete_collection
|
||||
# But remove response producer calls
|
||||
# Handle "not found" gracefully
|
||||
pass
|
||||
```
|
||||
|
||||
5. **移除存储管理基础设施:**
|
||||
移除 `self.storage_request_consumer` 的配置和启动
|
||||
移除 `self.storage_response_producer` 的配置
|
||||
移除 `on_storage_management` 的调度器方法
|
||||
移除存储管理的指标
|
||||
移除导入:`StorageManagementRequest`, `StorageManagementResponse`
|
||||
|
||||
**后端特定注意事项:**
|
||||
|
||||
**向量存储 (Milvus, Pinecone, Qdrant):** 跟踪 `(user, collection)` 在 `known_collections` 中的逻辑,但可能会为每个维度创建多个后端集合。继续采用延迟创建模式。删除操作必须删除所有维度变体。
|
||||
|
||||
**Cassandra Objects:** 集合是行属性,而不是结构。跟踪键空间级别的信息。
|
||||
|
||||
**图数据库 (Neo4j, Memgraph, FalkorDB):** 启动时查询 `CollectionMetadata` 节点。在同步时创建/删除元数据节点。
|
||||
|
||||
**Cassandra 三元组:** 使用 `KnowledgeGraph` API 进行集合操作。
|
||||
|
||||
**关键设计要点:**
|
||||
|
||||
**最终一致性:** 没有请求/响应机制,配置推送是广播的
|
||||
**幂等性:** 所有创建/删除操作都必须可以安全重试
|
||||
**错误处理:** 记录错误,但不要阻止配置更新
|
||||
**自愈:** 失败的操作将在下一次配置推送时重试
|
||||
**集合键格式:** `"user:collection"` 在 `config["collections"]` 中
|
||||
|
||||
#### 变更 11:更新集合模式 - 移除时间戳
|
||||
**文件:** `trustgraph-base/trustgraph/schema/services/collection.py`
|
||||
|
||||
**修改 CollectionMetadata (第 13-21 行):**
|
||||
移除 `created_at` 和 `updated_at` 字段:
|
||||
```python
|
||||
class CollectionMetadata(Record):
|
||||
user = String()
|
||||
collection = String()
|
||||
name = String()
|
||||
description = String()
|
||||
tags = Array(String())
|
||||
# Remove: created_at = String()
|
||||
# Remove: updated_at = String()
|
||||
```
|
||||
|
||||
**修改 CollectionManagementRequest (第 25-47 行):**
|
||||
移除时间戳字段:
|
||||
```python
|
||||
class CollectionManagementRequest(Record):
|
||||
operation = String()
|
||||
user = String()
|
||||
collection = String()
|
||||
timestamp = String()
|
||||
name = String()
|
||||
description = String()
|
||||
tags = Array(String())
|
||||
# Remove: created_at = String()
|
||||
# Remove: updated_at = String()
|
||||
tag_filter = Array(String())
|
||||
limit = Integer()
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
Timestamps don't add value for collections
|
||||
Config service maintains its own version tracking
|
||||
Simplifies schema and reduces storage
|
||||
|
||||
#### Benefits of Config Service Migration
|
||||
|
||||
1. ✅ **Eliminates hardcoded storage management topics** - Solves multi-tenant blocker
|
||||
2. ✅ **Simpler coordination** - No complex async waiting for 4+ storage responses
|
||||
3. ✅ **Eventual consistency** - Storage services update independently via config push
|
||||
4. ✅ **Better reliability** - Persistent config push vs non-persistent request/response
|
||||
5. ✅ **Unified configuration model** - Collections treated as configuration
|
||||
6. ✅ **Reduces complexity** - Removes ~300 lines of coordination code
|
||||
7. ✅ **Multi-tenant ready** - Config already supports tenant isolation via keyspace
|
||||
8. ✅ **Version tracking** - Config service version mechanism provides audit trail
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### Backward Compatibility
|
||||
|
||||
**Parameter Changes:**
|
||||
CLI parameter renames are breaking changes but acceptable (feature currently non-functional)
|
||||
Services work without parameters (use defaults)
|
||||
Default keyspaces preserved: "config", "knowledge", "librarian"
|
||||
Default queue: `persistent://tg/config/config`
|
||||
|
||||
**Collection Management:**
|
||||
**Breaking change:** Collections table removed from librarian keyspace
|
||||
**No data migration provided** - acceptable for this phase
|
||||
External collection API unchanged (list/update/delete operations)
|
||||
Collection metadata format simplified (timestamps removed)
|
||||
|
||||
### Testing Requirements
|
||||
|
||||
**Parameter Testing:**
|
||||
1. Verify `--config-push-queue` parameter works on graph-embeddings service
|
||||
2. Verify `--config-push-queue` parameter works on text-completion service
|
||||
3. Verify `--config-push-queue` parameter works on config service
|
||||
4. Verify `--cassandra-keyspace` parameter works for config service
|
||||
5. Verify `--cassandra-keyspace` parameter works for cores service
|
||||
6. Verify `--cassandra-keyspace` parameter works for librarian service
|
||||
7. Verify services work without parameters (uses defaults)
|
||||
8. Verify multi-tenant deployment with custom queue names and keyspace
|
||||
|
||||
**Collection Management Testing:**
|
||||
9. Verify `list-collections` operation via config service
|
||||
10. Verify `update-collection` creates/updates in config table
|
||||
11. Verify `delete-collection` removes from config table
|
||||
12. Verify config push is triggered on collection updates
|
||||
13. Verify tag filtering works with config-based storage
|
||||
14. Verify collection operations work without timestamp fields
|
||||
|
||||
### Multi-Tenant Deployment Example
|
||||
```bash
|
||||
# Tenant: tg-dev
|
||||
graph-embeddings \
|
||||
-p pulsar+ssl://broker:6651 \
|
||||
--pulsar-api-key <KEY> \
|
||||
--config-push-queue persistent://tg-dev/config/config
|
||||
|
||||
config-service \
|
||||
-p pulsar+ssl://broker:6651 \
|
||||
--pulsar-api-key <KEY> \
|
||||
--config-push-queue persistent://tg-dev/config/config \
|
||||
--cassandra-keyspace tg_dev_config
|
||||
```
|
||||
|
||||
## 影响分析
|
||||
|
||||
### 受变更 1-2 影响的服务 (CLI 参数重命名)
|
||||
所有继承自 AsyncProcessor 或 FlowProcessor 的服务:
|
||||
config-service
|
||||
cores-service
|
||||
librarian-service
|
||||
graph-embeddings
|
||||
document-embeddings
|
||||
text-completion-* (所有提供者)
|
||||
extract-* (所有提取器)
|
||||
query-* (所有查询服务)
|
||||
retrieval-* (所有 RAG 服务)
|
||||
storage-* (所有存储服务)
|
||||
还有 20 多个服务
|
||||
|
||||
### 受变更 3-6 影响的服务 (Cassandra Keyspace)
|
||||
config-service
|
||||
cores-service
|
||||
librarian-service
|
||||
|
||||
### 受变更 7-11 影响的服务 (集合管理)
|
||||
|
||||
**即时变更:**
|
||||
librarian-service (collection_manager.py, service.py)
|
||||
tables/library.py (删除 collections 表)
|
||||
schema/services/collection.py (删除时间戳)
|
||||
|
||||
**已完成的变更 (变更 10):** ✅
|
||||
所有存储服务 (共 11 个) - 已迁移到配置推送,用于通过 `CollectionConfigHandler` 更新集合
|
||||
存储管理模式已从 `storage.py` 中移除
|
||||
|
||||
## 未来考虑
|
||||
|
||||
### 基于用户的 Keyspace 模式
|
||||
|
||||
一些服务使用 **基于用户的 Keyspace** 动态模式,其中每个用户都拥有自己的 Cassandra Keyspace:
|
||||
|
||||
**使用基于用户的 Keyspace 的服务:**
|
||||
1. **三元组查询服务** (`trustgraph-flow/trustgraph/query/triples/cassandra/service.py:65`)
|
||||
使用 `keyspace=query.user`
|
||||
2. **对象查询服务** (`trustgraph-flow/trustgraph/query/objects/cassandra/service.py:479`)
|
||||
使用 `keyspace=self.sanitize_name(user)`
|
||||
3. **知识图谱直接访问** (`trustgraph-flow/trustgraph/direct/cassandra_kg.py:18`)
|
||||
默认参数 `keyspace="trustgraph"`
|
||||
|
||||
**状态:** 这些 **未进行修改**,在本规范中。
|
||||
|
||||
**需要未来审查:**
|
||||
评估基于用户的 Keyspace 模式是否会产生租户隔离问题
|
||||
考虑是否需要为多租户部署使用 Keyspace 前缀模式 (例如,`tenant_a_user1`)
|
||||
审查是否存在用户 ID 在租户之间的冲突
|
||||
评估是否更倾向于使用单个共享 Keyspace,每个租户使用基于用户的行隔离
|
||||
|
||||
**注意:** 这不会阻止当前的 multi-tenant 实现,但在进行生产 multi-tenant 部署之前应进行审查。
|
||||
|
||||
## 实施阶段
|
||||
|
||||
### 第一阶段:参数修复 (变更 1-6)
|
||||
修复 `--config-push-queue` 参数命名
|
||||
添加 `--cassandra-keyspace` 参数支持
|
||||
**结果:** 启用了 multi-tenant 队列和 Keyspace 配置
|
||||
|
||||
### 第二阶段:集合管理迁移 (变更 7-9, 11)
|
||||
将集合存储迁移到配置服务
|
||||
从 librarian 中删除 collections 表
|
||||
更新集合模式 (删除时间戳)
|
||||
**结果:** 消除硬编码的存储管理主题,简化 librarian
|
||||
|
||||
### 第三阶段:存储服务更新 (变更 10) ✅ 已完成
|
||||
所有存储服务已更新为使用配置推送进行集合管理,通过 `CollectionConfigHandler`
|
||||
移除了存储管理请求/响应基础设施
|
||||
移除了旧的模式定义
|
||||
**结果:** 实现了基于配置的集合管理
|
||||
|
||||
## 引用
|
||||
GitHub Issue: https://github.com/trustgraph-ai/trustgraph/issues/582
|
||||
相关文件:
|
||||
`trustgraph-base/trustgraph/base/async_processor.py`
|
||||
`trustgraph-base/trustgraph/base/cassandra_config.py`
|
||||
`trustgraph-base/trustgraph/schema/core/topic.py`
|
||||
`trustgraph-base/trustgraph/schema/services/collection.py`
|
||||
`trustgraph-flow/trustgraph/config/service/service.py`
|
||||
`trustgraph-flow/trustgraph/cores/service.py`
|
||||
`trustgraph-flow/trustgraph/librarian/service.py`
|
||||
`trustgraph-flow/trustgraph/librarian/collection_manager.py`
|
||||
`trustgraph-flow/trustgraph/tables/library.py`
|
||||
210
docs/tech-specs/zh-cn/neo4j-user-collection-isolation.zh-cn.md
Normal file
210
docs/tech-specs/zh-cn/neo4j-user-collection-isolation.zh-cn.md
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
---
|
||||
layout: default
|
||||
title: "Neo4j 用户/集合隔离支持"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# Neo4j 用户/集合隔离支持
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 问题陈述
|
||||
|
||||
Neo4j 的三元存储和查询实现目前缺乏用户/集合隔离,从而导致多租户安全问题。所有三元都存储在同一个图空间中,且没有任何机制来防止用户访问其他用户的或混合集合的数据。
|
||||
|
||||
与 TrustGraph 中的其他存储后端不同:
|
||||
- **Cassandra**: 使用每个用户和每个集合的单独键空间和表
|
||||
- **向量存储**(Milvus、Qdrant、Pinecone): 使用集合特定的命名空间
|
||||
- **Neo4j**: 目前共享所有数据在一个图(安全漏洞)
|
||||
|
||||
## 现有架构
|
||||
|
||||
### 数据模型
|
||||
- **节点**: 标签为 `:Node`,具有 `uri` 属性,标签为 `:Literal`,具有 `value` 属性
|
||||
- **关系**: 标签为 `:Rel`,具有 `uri` 属性
|
||||
- **索引**: `Node.uri`、`Literal.value`、`Rel.uri`
|
||||
|
||||
### 消息流程
|
||||
- `Triples` 消息包含 `metadata.user` 和 `metadata.collection` 字段
|
||||
- 存储服务接收用户/集合信息,但忽略它们
|
||||
- 查询服务期望在 `TriplesQueryRequest` 中包含 `user` 和 `collection`,但忽略它们
|
||||
|
||||
### 现有安全问题
|
||||
```cypher
|
||||
# 任何用户都可以查询任何数据 - 无隔离
|
||||
MATCH (src:Node)-[rel:Rel]->(dest:Node)
|
||||
RETURN src.uri, rel.uri, dest.uri
|
||||
```
|
||||
|
||||
## 建议解决方案:基于属性的过滤(推荐)
|
||||
|
||||
### 概述
|
||||
添加所有节点的和关系的 `user` 和 `collection` 属性,然后使用这些属性对所有操作进行过滤。 这种方法在保持查询灵活性和向后兼容性的同时,提供强大的隔离。
|
||||
|
||||
### 数据模型更改
|
||||
|
||||
#### 增强的节点结构
|
||||
```cypher
|
||||
// 节点实体
|
||||
CREATE (n:Node {
|
||||
uri: "http://example.com/entity1",
|
||||
user: "john_doe",
|
||||
collection: "production_v1"
|
||||
})
|
||||
|
||||
// 字面量实体
|
||||
CREATE (n:Literal {
|
||||
value: "literal value",
|
||||
user: "john_doe",
|
||||
collection: "production_v1"
|
||||
})
|
||||
```
|
||||
|
||||
#### 增强的关系结构
|
||||
```cypher
|
||||
// 具有 user/collection 属性的关系
|
||||
CREATE (src)-[:Rel {
|
||||
uri: "http://example.com/predicate1",
|
||||
user: "john_doe",
|
||||
collection: "production_v1"
|
||||
}]->(dest)
|
||||
```
|
||||
|
||||
#### 更新的索引
|
||||
```cypher
|
||||
// 用于高效过滤的复合索引
|
||||
CREATE INDEX node_user_collection_uri FOR (n:Node) ON (n.user, n.collection, n.uri);
|
||||
CREATE INDEX literal_user_collection_value FOR (n:Literal) ON (n.user, n.collection, n.value);
|
||||
CREATE INDEX rel_user_collection_uri FOR ()-[r:Rel]-() ON (r.user, r.collection, r.uri);
|
||||
|
||||
// 保持现有索引以实现向后兼容性(可选)
|
||||
CREATE INDEX Node_uri FOR (n:Node) ON (n.uri);
|
||||
CREATE INDEX Literal_value FOR (n:Literal) ON (n.value);
|
||||
CREATE INDEX Rel_uri FOR ()-[r:Rel]-() ON (r.uri);
|
||||
```
|
||||
|
||||
### 实现更改
|
||||
|
||||
#### 存储服务 (`write.py`)
|
||||
|
||||
**当前代码:**
|
||||
```python
|
||||
def create_node(self, uri):
|
||||
summary = self.io.execute_query(
|
||||
"MERGE (n:Node {uri: $uri})",
|
||||
uri=uri, database_=self.db,
|
||||
).summary
|
||||
```
|
||||
|
||||
**更新后的代码:**
|
||||
```python
|
||||
def create_node(self, uri, user, collection):
|
||||
summary = self.io.execute_query(
|
||||
"MERGE (n:Node {uri: $uri, user: $user, collection: $collection})",
|
||||
uri=uri, user=user, collection=collection, database_=self.db,
|
||||
).summary
|
||||
```
|
||||
|
||||
**增强的 store_triples 方法:**
|
||||
```python
|
||||
async def store_triples(self, message):
|
||||
user = message.metadata.user
|
||||
collection = message.metadata.collection
|
||||
# 存储 triples
|
||||
# ...
|
||||
```
|
||||
|
||||
#### 查询更新
|
||||
(此处应包含修改查询模式以包含 user 和 collection 的代码。 示例:`query_user1_coll1 = "MATCH (n)-[:Rel]->(m) WHERE n.user = 'john_doe' AND n.collection = 'production_v1' RETURN n, m"`)
|
||||
|
||||
### 阶段 2:查询更新(第 2 周)
|
||||
(此处应包含修改查询模式以包含 user 和 collection 的代码。 示例:`query_user1_coll1 = "MATCH (n)-[:Rel]->(m) WHERE n.user = 'john_doe' AND n.collection = 'production_v1' RETURN n, m"`)
|
||||
1. [ ] 更新所有查询模式,以便包含 user/collection 过滤器
|
||||
2. [ ] 添加查询验证和安全检查
|
||||
3. [ ] 更新集成测试
|
||||
4. [ ] 性能测试,使用过滤后的查询
|
||||
|
||||
### 阶段 3:迁移和部署(第 3 周)
|
||||
1. [ ] 创建现有 Neo4j 实例的迁移脚本
|
||||
2. [ ] 部署文档和操作手册
|
||||
3. [ ] 监控和警报,用于隔离违规
|
||||
4. [ ] 端到端测试,使用多个用户/集合
|
||||
|
||||
### 阶段 4:强化(第 4 周)
|
||||
1. [ ] 移除兼容性模式
|
||||
2. [ ] 添加全面的审计日志
|
||||
3. [ ] 安全审查和渗透测试
|
||||
4. [ ] 性能优化
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 单元测试
|
||||
(此处应包含使用 user 和 collection 进行隔离的单元测试示例。 示例:`def test_user_collection_isolation(): ...`)
|
||||
|
||||
### 集成测试
|
||||
- 具有重叠数据的多用户场景
|
||||
- 跨集合查询(应失败)
|
||||
- 迁移测试,使用现有数据
|
||||
- 性能基准测试,使用大量数据
|
||||
|
||||
### 安全测试
|
||||
- 尝试查询其他用户的
|
||||
- 尝试 SQL 注入,利用 user/collection 参数
|
||||
- 验证在各种查询模式下是否完全隔离
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### 索引策略
|
||||
- 使用 `(user, collection, uri)` 的复合索引,以实现最佳过滤
|
||||
- 如果某些集合非常大,请考虑使用部分索引
|
||||
- 监控索引使用情况和查询性能
|
||||
|
||||
### 查询优化
|
||||
- 使用 EXPLAIN 验证过滤查询是否使用了索引
|
||||
- 考虑查询结果缓存,用于频繁访问的数据
|
||||
- 剖析与大量用户/集合一起使用时的内存使用情况
|
||||
|
||||
### 可扩展性
|
||||
- 每个用户/集合组合都会创建一个单独的数据岛
|
||||
- 监控数据库大小和连接池使用情况
|
||||
- 考虑所需的水平扩展策略
|
||||
|
||||
## 安全与合规
|
||||
|
||||
### 数据隔离保证
|
||||
- **物理**: 所有用户数据都带有明确的用户/集合属性
|
||||
- **逻辑**: 所有查询都通过用户/集合上下文过滤
|
||||
- **访问控制**: 服务级别的验证防止未经授权的访问
|
||||
|
||||
### 审计要求
|
||||
- 记录所有数据访问,包括用户/集合上下文
|
||||
- 跟踪数据移动和迁移活动
|
||||
- 监控隔离违规尝试
|
||||
|
||||
### 合规性考虑
|
||||
- GDPR:更好地定位和删除用户特定数据
|
||||
- SOC2:清晰的数据隔离和访问控制
|
||||
- HIPAA:为医疗数据提供强大的租户隔离
|
||||
|
||||
## 风险与缓解措施
|
||||
|
||||
| 风险 | 影响 | 可能性 | 缓解措施 |
|
||||
|------|--------|------------|------------|
|
||||
| 缺少用户/集合过滤器 | 高 | 中 | 强制验证,全面测试 |
|
||||
| 查询性能下降 | 中 | 低 | 索引优化,查询剖析 |
|
||||
| 迁移数据损坏 | 高 | 低 | 备份策略,回滚程序 |
|
||||
| 复杂的跨集合查询 | 中 | 中 | 文档查询模式,提供示例 |
|
||||
|
||||
## 成功标准
|
||||
|
||||
1. **安全性**: 在生产环境中,没有任何跨用户的数据访问
|
||||
2. **性能**: 与未过滤的查询相比,影响小于 10%
|
||||
3. **迁移**: 100% 的现有 Neo4j 数据成功迁移,且没有数据丢失
|
||||
4. **可用性**: 所有现有的查询模式都与用户/集合上下文工作
|
||||
5. **合规性**: 完整地记录用户/集合数据访问
|
||||
|
||||
## 结论
|
||||
|
||||
基于属性的过滤方法,是实现 Neo4j 用户/集合隔离的最佳平衡方案,它既能保证安全性,又能保持查询的灵活性和强大的图查询功能。
|
||||
|
||||
这个解决方案确保 TrustGraph 的 Neo4j 后端符合其他存储后端的相同安全标准,同时最大限度地发挥图查询和索引的优势。
|
||||
769
docs/tech-specs/zh-cn/ontology-extract-phase-2.zh-cn.md
Normal file
769
docs/tech-specs/zh-cn/ontology-extract-phase-2.zh-cn.md
Normal file
|
|
@ -0,0 +1,769 @@
|
|||
---
|
||||
layout: default
|
||||
title: "本体知识抽取 - 第二阶段重构"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 本体知识抽取 - 第二阶段重构
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
**状态**: 草稿
|
||||
**作者**: 分析会议 2025-12-03
|
||||
**相关**: `ontology.md`, `ontorag.md`
|
||||
|
||||
## 概述
|
||||
|
||||
本文档识别了当前基于本体的知识抽取系统中存在的缺陷,并提出了重构方案,以提高 LLM 的性能并减少信息损失。
|
||||
|
||||
## 当前实现
|
||||
|
||||
### 当前工作方式
|
||||
|
||||
1. **本体加载** (`ontology_loader.py`)
|
||||
加载本体 JSON 文件,其中包含键,例如 `"fo/Recipe"`, `"fo/Food"`, `"fo/produces"`
|
||||
类 ID 在键本身中包含命名空间前缀
|
||||
示例来自 `food.ontology`:
|
||||
```json
|
||||
"classes": {
|
||||
"fo/Recipe": {
|
||||
"uri": "http://purl.org/ontology/fo/Recipe",
|
||||
"rdfs:comment": "A Recipe is a combination..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **提示构建** (`extract.py:299-307`, `ontology-prompt.md`)
|
||||
模板接收 `classes`, `object_properties`, `datatype_properties` 字典
|
||||
模板循环:`{% for class_id, class_def in classes.items() %}`
|
||||
LLM 看到:`**fo/Recipe**: A Recipe is a combination...`
|
||||
示例输出格式如下:
|
||||
```json
|
||||
{"subject": "recipe:cornish-pasty", "predicate": "rdf:type", "object": "Recipe"}
|
||||
{"subject": "recipe:cornish-pasty", "predicate": "has_ingredient", "object": "ingredient:flour"}
|
||||
```
|
||||
|
||||
3. **响应解析** (`extract.py:382-428`)
|
||||
期望接收 JSON 数组: `[{"subject": "...", "predicate": "...", "object": "..."}]`
|
||||
验证是否符合本体子集
|
||||
通过 `expand_uri()` 扩展 URI (extract.py:473-521)
|
||||
|
||||
4. **URI 扩展** (`extract.py:473-521`)
|
||||
检查值是否在 `ontology_subset.classes` 字典中
|
||||
如果找到,从类定义中提取 URI
|
||||
如果未找到,则构建 URI: `f"https://trustgraph.ai/ontology/{ontology_id}#{value}"`
|
||||
|
||||
### 数据流示例
|
||||
|
||||
**本体 JSON → 加载器 → 提示:**
|
||||
```
|
||||
"fo/Recipe" → classes["fo/Recipe"] → LLM sees "**fo/Recipe**"
|
||||
```
|
||||
|
||||
**LLM → 解析器 → 输出:**
|
||||
```
|
||||
"Recipe" → not in classes["fo/Recipe"] → constructs URI → LOSES original URI
|
||||
"fo/Recipe" → found in classes → uses original URI → PRESERVES URI
|
||||
```
|
||||
|
||||
## 发现的问题
|
||||
|
||||
### 1. **提示语中的示例不一致**
|
||||
|
||||
**问题**: 提示模板显示带有前缀的类ID (`fo/Recipe`),但示例输出使用不带前缀的类名 (`Recipe`)。
|
||||
|
||||
**位置**: `ontology-prompt.md:5-52`
|
||||
|
||||
```markdown
|
||||
## Ontology Classes:
|
||||
- **fo/Recipe**: A Recipe is...
|
||||
|
||||
## Example Output:
|
||||
{"subject": "recipe:cornish-pasty", "predicate": "rdf:type", "object": "Recipe"}
|
||||
```
|
||||
|
||||
**影响:** LLM 接收到关于应该使用哪种格式的冲突信号。
|
||||
|
||||
### 2. **URI 扩展中的信息丢失**
|
||||
|
||||
**问题:** 当 LLM 返回不带前缀的类名,例如示例中的情况时,`expand_uri()` 无法在本体字典中找到它们,而是构造了备用 URI,从而丢失了原始的正确 URI。
|
||||
|
||||
**位置:** `extract.py:494-500`
|
||||
|
||||
```python
|
||||
if value in ontology_subset.classes: # Looks for "Recipe"
|
||||
class_def = ontology_subset.classes[value] # But key is "fo/Recipe"
|
||||
if isinstance(class_def, dict) and 'uri' in class_def:
|
||||
return class_def['uri'] # Never reached!
|
||||
return f"https://trustgraph.ai/ontology/{ontology_id}#{value}" # Fallback
|
||||
```
|
||||
|
||||
**影响 (Impact):**
|
||||
原始 URI: `http://purl.org/ontology/fo/Recipe`
|
||||
构建的 URI: `https://trustgraph.ai/ontology/food#Recipe`
|
||||
语义信息丢失,破坏互操作性
|
||||
|
||||
### 3. **实体实例格式不明确 (Ambiguous Entity Instance Format)**
|
||||
|
||||
**问题 (Issue):** 没有关于实体实例 URI 格式的明确指导。
|
||||
|
||||
**提示中的示例 (Examples in prompt):**
|
||||
`"recipe:cornish-pasty"` (类似于命名空间的 前缀)
|
||||
`"ingredient:flour"` (不同的前缀)
|
||||
|
||||
**实际行为 (Actual behavior) (extract.py:517-520):**
|
||||
```python
|
||||
# Treat as entity instance - construct unique URI
|
||||
normalized = value.replace(" ", "-").lower()
|
||||
return f"https://trustgraph.ai/{ontology_id}/{normalized}"
|
||||
```
|
||||
|
||||
**影响:** LLM 必须在没有任何本体知识的情况下猜测前缀约定。
|
||||
|
||||
### 4. **没有命名空间前缀的指导**
|
||||
|
||||
**问题:** 本体 JSON 包含命名空间定义(food.ontology 中的第 10-25 行):
|
||||
```json
|
||||
"namespaces": {
|
||||
"fo": "http://purl.org/ontology/fo/",
|
||||
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
但是这些内容永远不会传递给大型语言模型。大型语言模型不知道:
|
||||
"fo" 的含义
|
||||
应使用哪个前缀来表示实体
|
||||
哪个命名空间适用于哪些元素
|
||||
|
||||
### 5. **未在提示中使用标签**
|
||||
|
||||
**问题:** 每一个类都有 `rdfs:label` 字段(例如,`{"value": "Recipe", "lang": "en-gb"}`),但提示模板没有使用它们。
|
||||
|
||||
**当前:** 只显示 `class_id` 和 `comment`
|
||||
```jinja
|
||||
- **{{class_id}}**{% if class_def.comment %}: {{class_def.comment}}{% endif %}
|
||||
```
|
||||
|
||||
**可用但未使用的:**
|
||||
```python
|
||||
"rdfs:label": [{"value": "Recipe", "lang": "en-gb"}]
|
||||
```
|
||||
|
||||
**影响:** 可以为技术 ID 旁边提供人类可读的名称。
|
||||
|
||||
## 提出的解决方案
|
||||
|
||||
### 方案 A:标准化为不带前缀的 ID
|
||||
|
||||
**方法:** 在向 LLM 显示之前,从类 ID 中移除前缀。
|
||||
|
||||
**变更:**
|
||||
1. 修改 `build_extraction_variables()` 以转换键:
|
||||
```python
|
||||
classes_for_prompt = {
|
||||
k.split('/')[-1]: v # "fo/Recipe" → "Recipe"
|
||||
for k, v in ontology_subset.classes.items()
|
||||
}
|
||||
```
|
||||
|
||||
2. 将提示示例更新为匹配项(已使用未加前缀的名称)。
|
||||
|
||||
3. 修改 `expand_uri()` 以处理两种格式:
|
||||
```python
|
||||
# Try exact match first
|
||||
if value in ontology_subset.classes:
|
||||
return ontology_subset.classes[value]['uri']
|
||||
|
||||
# Try with prefix
|
||||
for prefix in ['fo/', 'rdf:', 'rdfs:']:
|
||||
prefixed = f"{prefix}{value}"
|
||||
if prefixed in ontology_subset.classes:
|
||||
return ontology_subset.classes[prefixed]['uri']
|
||||
```
|
||||
|
||||
**优点:**
|
||||
更清晰,更易于人类阅读
|
||||
与现有的提示示例相符
|
||||
LLM(大型语言模型)在处理更简单的token时效果更好
|
||||
|
||||
**缺点:**
|
||||
如果多个本体具有相同的类名,则可能发生类名冲突
|
||||
丢失命名空间信息
|
||||
需要回退逻辑来执行查找
|
||||
|
||||
### 选项 B:始终使用完整的带前缀的 ID
|
||||
|
||||
**方法:** 更新示例,使其使用与类列表中显示的前缀 ID 匹配。
|
||||
|
||||
**更改:**
|
||||
1. 更新提示示例(ontology-prompt.md:46-52):
|
||||
```json
|
||||
[
|
||||
{"subject": "recipe:cornish-pasty", "predicate": "rdf:type", "object": "fo/Recipe"},
|
||||
{"subject": "recipe:cornish-pasty", "predicate": "rdfs:label", "object": "Cornish Pasty"},
|
||||
{"subject": "recipe:cornish-pasty", "predicate": "fo/produces", "object": "food:cornish-pasty"},
|
||||
{"subject": "food:cornish-pasty", "predicate": "rdf:type", "object": "fo/Food"}
|
||||
]
|
||||
```
|
||||
|
||||
2. 在提示语中添加命名空间说明:
|
||||
```markdown
|
||||
## Namespace Prefixes:
|
||||
- **fo/**: Food Ontology (http://purl.org/ontology/fo/)
|
||||
- **rdf:**: RDF Schema
|
||||
- **rdfs:**: RDF Schema
|
||||
|
||||
Use these prefixes exactly as shown when referencing classes and properties.
|
||||
```
|
||||
|
||||
3. 保持 `expand_uri()` 的原样 (当找到匹配项时,它可以正常工作)。
|
||||
|
||||
**优点:**
|
||||
输入 = 输出的一致性。
|
||||
没有信息损失。
|
||||
保留命名空间语义。
|
||||
适用于多个本体。
|
||||
|
||||
**缺点:**
|
||||
对于 LLM 来说,token 更加冗长。
|
||||
需要 LLM 跟踪前缀。
|
||||
|
||||
### 选项 C:混合 - 同时显示标签和 ID
|
||||
|
||||
**方法:** 增强提示,同时显示人类可读的标签和技术 ID。
|
||||
|
||||
**更改:**
|
||||
1. 更新提示模板:
|
||||
```jinja
|
||||
{% for class_id, class_def in classes.items() %}
|
||||
- **{{class_id}}** (label: "{{class_def.labels[0].value if class_def.labels else class_id}}"){% if class_def.comment %}: {{class_def.comment}}{% endif %}
|
||||
{% endfor %}
|
||||
```
|
||||
|
||||
示例输出:
|
||||
```markdown
|
||||
- **fo/Recipe** (label: "Recipe"): A Recipe is a combination...
|
||||
```
|
||||
|
||||
2. 更新说明:
|
||||
```markdown
|
||||
When referencing classes:
|
||||
- Use the full prefixed ID (e.g., "fo/Recipe") in JSON output
|
||||
- The label (e.g., "Recipe") is for human understanding only
|
||||
```
|
||||
|
||||
**优点 (Pros)**:
|
||||
对 LLM 最清晰
|
||||
保留所有信息
|
||||
明确说明应该使用什么
|
||||
|
||||
**缺点 (Cons)**:
|
||||
提示更长
|
||||
模板更复杂
|
||||
|
||||
## 实施方法 (Implemented Approach)
|
||||
|
||||
**简化的实体-关系-属性格式** - 完全取代了旧的三元组格式。
|
||||
|
||||
选择了这种新方法的原因是:
|
||||
|
||||
1. **无信息损失 (No Information Loss)**: 原始 URI 正确保留
|
||||
2. **更简单的逻辑 (Simpler Logic)**: 无需转换,可以直接使用字典查找
|
||||
3. **命名空间安全 (Namespace Safety)**: 能够处理多个本体而不会发生冲突
|
||||
4. **语义正确性 (Semantic Correctness)**: 保持 RDF/OWL 语义
|
||||
|
||||
## 实施完成 (Implementation Complete)
|
||||
|
||||
### 构建内容 (What Was Built):
|
||||
|
||||
1. **新的提示模板 (New Prompt Template)** (`prompts/ontology-extract-v2.txt`)
|
||||
✅ 清晰的章节:实体类型、关系、属性
|
||||
✅ 使用完整类型标识符的示例 (`fo/Recipe`, `fo/has_ingredient`)
|
||||
✅ 指示使用模式中确切的标识符
|
||||
✅ 新的 JSON 格式,包含实体/关系/属性数组
|
||||
|
||||
2. **实体规范化 (Entity Normalization)** (`entity_normalizer.py`)
|
||||
✅ `normalize_entity_name()` - 将名称转换为 URI 安全格式
|
||||
✅ `normalize_type_identifier()` - 处理类型中的斜杠 (`fo/Recipe` → `fo-recipe`)
|
||||
✅ `build_entity_uri()` - 使用 (名称, 类型) 元组创建唯一 URI
|
||||
✅ `EntityRegistry` - 跟踪实体以进行去重
|
||||
|
||||
3. **JSON 解析器 (JSON Parser)** (`simplified_parser.py`)
|
||||
✅ 解析新的格式:`{entities: [...], relationships: [...], attributes: [...]}`
|
||||
✅ 支持 kebab-case 和 snake_case 字段名称
|
||||
✅ 返回结构化的数据类
|
||||
✅ 具有优雅的错误处理和日志记录
|
||||
|
||||
4. **三元组转换器 (Triple Converter)** (`triple_converter.py`)
|
||||
✅ `convert_entity()` - 自动生成类型 + 标签三元组
|
||||
✅ `convert_relationship()` - 通过属性连接实体 URI
|
||||
✅ `convert_attribute()` - 添加字面值
|
||||
✅ 从本体定义中查找完整的 URI
|
||||
|
||||
5. **更新的主要处理器 (Updated Main Processor)** (`extract.py`)
|
||||
✅ 删除了旧的三元组提取代码
|
||||
✅ 添加了 `extract_with_simplified_format()` 方法
|
||||
✅ 现在仅使用新的简化格式
|
||||
✅ 使用 `extract-with-ontologies-v2` ID 调用提示
|
||||
|
||||
## 测试用例 (Test Cases)
|
||||
|
||||
### 测试 1: URI 保留 (Test 1: URI Preservation)
|
||||
```python
|
||||
# Given ontology class
|
||||
classes = {"fo/Recipe": {"uri": "http://purl.org/ontology/fo/Recipe", ...}}
|
||||
|
||||
# When LLM returns
|
||||
llm_output = {"subject": "x", "predicate": "rdf:type", "object": "fo/Recipe"}
|
||||
|
||||
# Then expanded URI should be
|
||||
assert expanded == "http://purl.org/ontology/fo/Recipe"
|
||||
# Not: "https://trustgraph.ai/ontology/food#Recipe"
|
||||
```
|
||||
|
||||
### 测试 2:多本体冲突
|
||||
```python
|
||||
# Given two ontologies
|
||||
ont1 = {"fo/Recipe": {...}}
|
||||
ont2 = {"cooking/Recipe": {...}}
|
||||
|
||||
# LLM should use full prefix to disambiguate
|
||||
llm_output = {"object": "fo/Recipe"} # Not just "Recipe"
|
||||
```
|
||||
|
||||
### 测试 3:实体实例格式
|
||||
```python
|
||||
# Given prompt with food ontology
|
||||
# LLM should create instances like
|
||||
{"subject": "recipe:cornish-pasty"} # Namespace-style
|
||||
{"subject": "food:beef"} # Consistent prefix
|
||||
```
|
||||
|
||||
## 待解决的问题
|
||||
|
||||
1. **实体实例是否应该使用命名空间前缀?**
|
||||
当前:`"recipe:cornish-pasty"` (任意)
|
||||
替代方案:使用本体前缀 `"fo:cornish-pasty"`?
|
||||
替代方案:不使用前缀,在 URI 中展开 `"cornish-pasty"` → 完整 URI?
|
||||
|
||||
2. **如何在提示中处理域/范围?**
|
||||
当前显示:`(Recipe → Food)`
|
||||
应该是:`(fo/Recipe → fo/Food)`?
|
||||
|
||||
3. **是否应该验证域/范围约束?**
|
||||
TODO 注释在 extract.py:470
|
||||
可以捕获更多错误,但更复杂
|
||||
|
||||
4. **关于反向属性和等价性?**
|
||||
本体有 `owl:inverseOf`,`owl:equivalentClass`
|
||||
当前未在提取中使用
|
||||
应该使用吗?
|
||||
|
||||
## 成功指标
|
||||
|
||||
✅ 零 URI 信息损失(100% 保留原始 URI)
|
||||
✅ LLM 输出格式与输入格式匹配
|
||||
✅ 提示中没有歧义的示例
|
||||
✅ 使用多个本体的测试通过
|
||||
✅ 提取质量得到改进(通过有效的三元组百分比衡量)
|
||||
|
||||
## 替代方法:简化的提取格式
|
||||
|
||||
### 理念
|
||||
|
||||
不要让 LLM 理解 RDF/OWL 语义,而是让它做擅长的事情:**在文本中查找实体和关系**。
|
||||
|
||||
让代码处理 URI 构造、RDF 转换和语义网规范。
|
||||
|
||||
### 示例:实体分类
|
||||
|
||||
**输入文本:**
|
||||
```
|
||||
Cornish pasty is a traditional British pastry filled with meat and vegetables.
|
||||
```
|
||||
|
||||
**本体模式(显示给LLM):**
|
||||
```markdown
|
||||
## Entity Types:
|
||||
- Recipe: A recipe is a combination of ingredients and a method
|
||||
- Food: A food is something that can be eaten
|
||||
- Ingredient: An ingredient combines a quantity and a food
|
||||
```
|
||||
|
||||
**LLM 返回的内容(简单 JSON):**
|
||||
```json
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"entity": "Cornish pasty",
|
||||
"type": "Recipe"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**生成的代码(RDF 三元组):**
|
||||
```python
|
||||
# 1. Normalize entity name + type to ID (type prevents collisions)
|
||||
entity_id = "recipe-cornish-pasty" # normalize("Cornish pasty", "Recipe")
|
||||
entity_uri = "https://trustgraph.ai/food/recipe-cornish-pasty"
|
||||
|
||||
# Note: Same name, different type = different URI
|
||||
# "Cornish pasty" (Recipe) → recipe-cornish-pasty
|
||||
# "Cornish pasty" (Food) → food-cornish-pasty
|
||||
|
||||
# 2. Generate triples
|
||||
triples = [
|
||||
# Type triple
|
||||
Triple(
|
||||
s=Value(value=entity_uri, is_uri=True),
|
||||
p=Value(value="http://www.w3.org/1999/02/22-rdf-syntax-ns#type", is_uri=True),
|
||||
o=Value(value="http://purl.org/ontology/fo/Recipe", is_uri=True)
|
||||
),
|
||||
# Label triple (automatic)
|
||||
Triple(
|
||||
s=Value(value=entity_uri, is_uri=True),
|
||||
p=Value(value="http://www.w3.org/2000/01/rdf-schema#label", is_uri=True),
|
||||
o=Value(value="Cornish pasty", is_uri=False)
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
### 优点
|
||||
|
||||
1. **LLM 不需要:**
|
||||
理解 URI 语法
|
||||
编造标识符前缀 (`recipe:`, `ingredient:`)
|
||||
了解 `rdf:type` 或 `rdfs:label`
|
||||
构建语义网标识符
|
||||
|
||||
2. **LLM 只需要:**
|
||||
在文本中找到实体
|
||||
将它们映射到本体类
|
||||
提取关系和属性
|
||||
|
||||
3. **代码负责:**
|
||||
URI 规范化和构建
|
||||
RDF 三元组生成
|
||||
自动标签分配
|
||||
命名空间管理
|
||||
|
||||
### 为什么这样更好
|
||||
|
||||
**更简单的提示** = 减少困惑 = 更少的错误
|
||||
**一致的 ID** = 代码控制规范化规则
|
||||
**自动生成的标签** = 没有遗漏的 rdfs:label 三元组
|
||||
**LLM 专注于提取** = 这是它擅长的事情
|
||||
|
||||
### 示例:实体关系
|
||||
|
||||
**输入文本:**
|
||||
```
|
||||
Cornish pasty is a traditional British pastry filled with beef and potatoes.
|
||||
```
|
||||
|
||||
**本体模式(显示给LLM):**
|
||||
```markdown
|
||||
## Entity Types:
|
||||
- Recipe: A recipe is a combination of ingredients and a method
|
||||
- Food: A food is something that can be eaten
|
||||
- Ingredient: An ingredient combines a quantity and a food
|
||||
|
||||
## Relationships:
|
||||
- has_ingredient: Relates a recipe to an ingredient it uses (Recipe → Ingredient)
|
||||
- food: Relates an ingredient to the food that is required (Ingredient → Food)
|
||||
```
|
||||
|
||||
**LLM 返回的内容(简单 JSON):**
|
||||
```json
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"entity": "Cornish pasty",
|
||||
"type": "Recipe"
|
||||
},
|
||||
{
|
||||
"entity": "beef",
|
||||
"type": "Food"
|
||||
},
|
||||
{
|
||||
"entity": "potatoes",
|
||||
"type": "Food"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"subject": "Cornish pasty",
|
||||
"subject-type": "Recipe",
|
||||
"relation": "has_ingredient",
|
||||
"object": "beef",
|
||||
"object-type": "Food"
|
||||
},
|
||||
{
|
||||
"subject": "Cornish pasty",
|
||||
"subject-type": "Recipe",
|
||||
"relation": "has_ingredient",
|
||||
"object": "potatoes",
|
||||
"object-type": "Food"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**生成的代码(RDF 三元组):**
|
||||
```python
|
||||
# Normalize entity names to URIs
|
||||
cornish_pasty_uri = "https://trustgraph.ai/food/cornish-pasty"
|
||||
beef_uri = "https://trustgraph.ai/food/beef"
|
||||
potatoes_uri = "https://trustgraph.ai/food/potatoes"
|
||||
|
||||
# Look up relation URI from ontology
|
||||
has_ingredient_uri = "http://purl.org/ontology/fo/ingredients" # from fo/has_ingredient
|
||||
|
||||
triples = [
|
||||
# Entity type triples (as before)
|
||||
Triple(s=cornish_pasty_uri, p=rdf_type, o="http://purl.org/ontology/fo/Recipe"),
|
||||
Triple(s=cornish_pasty_uri, p=rdfs_label, o="Cornish pasty"),
|
||||
|
||||
Triple(s=beef_uri, p=rdf_type, o="http://purl.org/ontology/fo/Food"),
|
||||
Triple(s=beef_uri, p=rdfs_label, o="beef"),
|
||||
|
||||
Triple(s=potatoes_uri, p=rdf_type, o="http://purl.org/ontology/fo/Food"),
|
||||
Triple(s=potatoes_uri, p=rdfs_label, o="potatoes"),
|
||||
|
||||
# Relationship triples
|
||||
Triple(
|
||||
s=Value(value=cornish_pasty_uri, is_uri=True),
|
||||
p=Value(value=has_ingredient_uri, is_uri=True),
|
||||
o=Value(value=beef_uri, is_uri=True)
|
||||
),
|
||||
Triple(
|
||||
s=Value(value=cornish_pasty_uri, is_uri=True),
|
||||
p=Value(value=has_ingredient_uri, is_uri=True),
|
||||
o=Value(value=potatoes_uri, is_uri=True)
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
**关键点:**
|
||||
LLM 返回自然语言实体名称:`"Cornish pasty"`, `"beef"`, `"potatoes"`
|
||||
LLM 包含类型信息以消除歧义:`subject-type`, `object-type`
|
||||
LLM 使用模式中的关系名称:`"has_ingredient"`
|
||||
代码使用 (名称, 类型) 推导一致的 ID:`("Cornish pasty", "Recipe")` → `recipe-cornish-pasty`
|
||||
代码从本体中查找关系 URI:`fo/has_ingredient` → 完整 URI
|
||||
相同的 (名称, 类型) 元组始终获得相同的 URI (去重)
|
||||
|
||||
### 示例:实体名称消除歧义
|
||||
|
||||
**问题:** 相同的名称可能指不同的实体类型。
|
||||
|
||||
**实际案例:**
|
||||
```
|
||||
"Cornish pasty" can be:
|
||||
- A Recipe (instructions for making it)
|
||||
- A Food (the dish itself)
|
||||
```
|
||||
|
||||
**处理方式:**
|
||||
|
||||
LLM 将两者都以独立的实体返回:
|
||||
```json
|
||||
{
|
||||
"entities": [
|
||||
{"entity": "Cornish pasty", "type": "Recipe"},
|
||||
{"entity": "Cornish pasty", "type": "Food"}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"subject": "Cornish pasty",
|
||||
"subject-type": "Recipe",
|
||||
"relation": "produces",
|
||||
"object": "Cornish pasty",
|
||||
"object-type": "Food"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**代码解析:**
|
||||
```python
|
||||
# Different types → different URIs
|
||||
recipe_uri = normalize("Cornish pasty", "Recipe")
|
||||
# → "https://trustgraph.ai/food/recipe-cornish-pasty"
|
||||
|
||||
food_uri = normalize("Cornish pasty", "Food")
|
||||
# → "https://trustgraph.ai/food/food-cornish-pasty"
|
||||
|
||||
# Relationship connects them correctly
|
||||
triple = Triple(
|
||||
s=recipe_uri, # The Recipe
|
||||
p="http://purl.org/ontology/fo/produces",
|
||||
o=food_uri # The Food
|
||||
)
|
||||
```
|
||||
|
||||
**原理:**
|
||||
类型包含在所有引用中(实体、关系、属性)。
|
||||
代码使用 `(name, type)` 元组作为查找键。
|
||||
没有歧义,没有冲突。
|
||||
|
||||
### 示例:实体属性
|
||||
|
||||
**输入文本:**
|
||||
```
|
||||
This Cornish pasty recipe serves 4-6 people and takes 45 minutes to prepare.
|
||||
```
|
||||
|
||||
**本体模式(显示给LLM):**
|
||||
```markdown
|
||||
## Entity Types:
|
||||
- Recipe: A recipe is a combination of ingredients and a method
|
||||
|
||||
## Attributes:
|
||||
- serves: Indicates what the recipe is intended to serve (Recipe → text)
|
||||
- preparation_time: Time needed to prepare the recipe (Recipe → text)
|
||||
```
|
||||
|
||||
**LLM 返回的内容(简单 JSON):**
|
||||
```json
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"entity": "Cornish pasty recipe",
|
||||
"type": "Recipe"
|
||||
}
|
||||
],
|
||||
"attributes": [
|
||||
{
|
||||
"entity": "Cornish pasty recipe",
|
||||
"entity-type": "Recipe",
|
||||
"attribute": "serves",
|
||||
"value": "4-6 people"
|
||||
},
|
||||
{
|
||||
"entity": "Cornish pasty recipe",
|
||||
"entity-type": "Recipe",
|
||||
"attribute": "preparation_time",
|
||||
"value": "45 minutes"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**生成的代码(RDF 三元组):**
|
||||
```python
|
||||
# Normalize entity name to URI
|
||||
recipe_uri = "https://trustgraph.ai/food/cornish-pasty-recipe"
|
||||
|
||||
# Look up attribute URIs from ontology
|
||||
serves_uri = "http://purl.org/ontology/fo/serves" # from fo/serves
|
||||
prep_time_uri = "http://purl.org/ontology/fo/preparation_time" # from fo/preparation_time
|
||||
|
||||
triples = [
|
||||
# Entity type triple
|
||||
Triple(
|
||||
s=Value(value=recipe_uri, is_uri=True),
|
||||
p=Value(value=rdf_type, is_uri=True),
|
||||
o=Value(value="http://purl.org/ontology/fo/Recipe", is_uri=True)
|
||||
),
|
||||
|
||||
# Label triple (automatic)
|
||||
Triple(
|
||||
s=Value(value=recipe_uri, is_uri=True),
|
||||
p=Value(value=rdfs_label, is_uri=True),
|
||||
o=Value(value="Cornish pasty recipe", is_uri=False)
|
||||
),
|
||||
|
||||
# Attribute triples (objects are literals, not URIs)
|
||||
Triple(
|
||||
s=Value(value=recipe_uri, is_uri=True),
|
||||
p=Value(value=serves_uri, is_uri=True),
|
||||
o=Value(value="4-6 people", is_uri=False) # Literal value!
|
||||
),
|
||||
Triple(
|
||||
s=Value(value=recipe_uri, is_uri=True),
|
||||
p=Value(value=prep_time_uri, is_uri=True),
|
||||
o=Value(value="45 minutes", is_uri=False) # Literal value!
|
||||
)
|
||||
]
|
||||
```
|
||||
|
||||
**关键点:**
|
||||
LLM 提取字面值:`"4-6 people"`, `"45 minutes"`
|
||||
LLM 包含实体类型以消除歧义:`entity-type`
|
||||
LLM 使用来自模式的属性名称:`"serves"`, `"preparation_time"`
|
||||
代码从本体数据类型属性中查找属性 URI
|
||||
**对象是字面值** (`is_uri=False`),而不是 URI 引用
|
||||
值保持为自然文本,无需进行任何标准化
|
||||
|
||||
**与关系的差异:**
|
||||
关系:主语和宾语都是实体(URI)
|
||||
属性:主语是实体(URI),宾语是字面值(字符串/数字)
|
||||
|
||||
### 完整示例:实体 + 关系 + 属性
|
||||
|
||||
**输入文本:**
|
||||
```
|
||||
Cornish pasty is a savory pastry filled with beef and potatoes.
|
||||
This recipe serves 4 people.
|
||||
```
|
||||
|
||||
**LLM 返回的内容:**
|
||||
```json
|
||||
{
|
||||
"entities": [
|
||||
{
|
||||
"entity": "Cornish pasty",
|
||||
"type": "Recipe"
|
||||
},
|
||||
{
|
||||
"entity": "beef",
|
||||
"type": "Food"
|
||||
},
|
||||
{
|
||||
"entity": "potatoes",
|
||||
"type": "Food"
|
||||
}
|
||||
],
|
||||
"relationships": [
|
||||
{
|
||||
"subject": "Cornish pasty",
|
||||
"subject-type": "Recipe",
|
||||
"relation": "has_ingredient",
|
||||
"object": "beef",
|
||||
"object-type": "Food"
|
||||
},
|
||||
{
|
||||
"subject": "Cornish pasty",
|
||||
"subject-type": "Recipe",
|
||||
"relation": "has_ingredient",
|
||||
"object": "potatoes",
|
||||
"object-type": "Food"
|
||||
}
|
||||
],
|
||||
"attributes": [
|
||||
{
|
||||
"entity": "Cornish pasty",
|
||||
"entity-type": "Recipe",
|
||||
"attribute": "serves",
|
||||
"value": "4 people"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**结果:** 生成了 11 个 RDF 三元组:
|
||||
3 个实体类型三元组 (rdf:type)
|
||||
3 个实体标签三元组 (rdfs:label) - 自动
|
||||
2 个关系三元组 (has_ingredient)
|
||||
1 个属性三元组 (serves)
|
||||
|
||||
所有内容均由 LLM 通过简单的、自然的语言提取得出!
|
||||
|
||||
## 参考文献
|
||||
|
||||
当前实现:`trustgraph-flow/trustgraph/extract/kg/ontology/extract.py`
|
||||
提示模板:`ontology-prompt.md`
|
||||
测试用例:`tests/unit/test_extract/test_ontology/`
|
||||
示例本体:`e2e/test-data/food.ontology`
|
||||
241
docs/tech-specs/zh-cn/ontology.zh-cn.md
Normal file
241
docs/tech-specs/zh-cn/ontology.zh-cn.md
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
---
|
||||
layout: default
|
||||
title: "本体结构技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 本体结构技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了 TrustGraph 系统中本体的结构和格式。 本体提供正式的知识模型,定义类、属性、以及它们之间的关系,从而支持推理和推断功能。 该系统采用基于 OWL 的配置格式,广泛地表示 OWL/RDFS 概念,同时针对 TrustGraph 的特定需求进行了优化。
|
||||
|
||||
**命名约定**: 该项目使用 kebab-case 格式(例如,`natural-world`、`domain-model`、`configuration keys`、`API endpoints`、`module names` 等)来表示所有标识符,而不是 snake_case。
|
||||
|
||||
## 目标
|
||||
|
||||
- **类和属性管理**: 定义类似 OWL 的类,并包含属性、域、范围以及类型约束
|
||||
- **强大的语义支持**: 启用全面的 RDFS/OWL 属性,包括标签、多语言支持以及正式约束
|
||||
- **多本体支持**: 允许多个本体共存并相互协作
|
||||
- **验证和推理**: 确保本体符合 OWL 类似的标准,包括一致性检查和推理支持
|
||||
- **标准兼容性**: 支持以标准格式(Turtle、RDF/XML、OWL/XML)进行导入/导出,同时保持内部优化
|
||||
|
||||
## 背景
|
||||
|
||||
TrustGraph 将本体存储为配置项,采用灵活的键值系统。 尽管该格式受到 OWL (Web Ontology Language) 的启发,但它针对 TrustGraph 的特定用例进行了优化,并且并非完全符合所有 OWL 规范。
|
||||
|
||||
在 TrustGraph 中,本体能够实现以下功能:
|
||||
|
||||
- 定义对象类型及其属性
|
||||
- 规范属性的域、范围和类型约束
|
||||
- 进行逻辑推理
|
||||
- 定义复杂的关系和数量约束
|
||||
- 支持多种语言,用于国际化
|
||||
|
||||
## 本体结构
|
||||
|
||||
### 配置存储
|
||||
|
||||
本体以配置项的形式存储,具有以下模式:
|
||||
|
||||
- **类型**: `ontology`
|
||||
- **键**: 唯一本体标识符(例如,`natural-world`、`domain-model`)
|
||||
- **值**: 完整的本体,采用 JSON 格式
|
||||
|
||||
### JSON 结构
|
||||
|
||||
本体的 JSON 格式主要包含四个部分:
|
||||
|
||||
#### 1. 元数据
|
||||
|
||||
包含本体的行政和描述性信息:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"name": "The natural world",
|
||||
"description": "Ontology covering the natural order",
|
||||
"version": "1.0.0",
|
||||
"created": "2025-09-20T12:07:37.068Z",
|
||||
"modified": "2025-09-20T12:12:20.725Z",
|
||||
"creator": "current-user",
|
||||
"namespace": "http://trustgraph.ai/ontologies/natural-world",
|
||||
"imports": ["http://www.w3.org/2002/07/owl#"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段**:
|
||||
- `name`: 本体的人类可读名称
|
||||
- `description`: 本体目的的简要描述
|
||||
- `version`: 语义版本号
|
||||
- `created`: 创建时间 (ISO 8601 格式)
|
||||
- `modified`: 最后修改时间 (ISO 8601 格式)
|
||||
- `creator`: 创建用户的/系统的标识符
|
||||
- `namespace`: 本体元素的基 URI
|
||||
- `imports`: 导入本体 URI 数组
|
||||
|
||||
#### 2. 类
|
||||
|
||||
定义对象类型及其层次关系:
|
||||
|
||||
```json
|
||||
{
|
||||
"classes": {
|
||||
"animal": {
|
||||
"uri": "http://trustgraph.ai/ontologies/natural-world#animal",
|
||||
"type": "owl:Class",
|
||||
"rdfs:label": [{"value": "Animal", "lang": "en"}],
|
||||
"rdfs:comment": "An animal",
|
||||
"rdfs:subClassOf": "lifeform",
|
||||
"owl:equivalentClass": ["creature"],
|
||||
"owl:disjointWith": ["plant"],
|
||||
"dcterms:identifier": "ANI-001"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**支持的属性**:
|
||||
- `uri`: 类的完整 URI
|
||||
- `type`: 始终为 `"owl:Class"`
|
||||
- `rdfs:label`: 语言标记的标签数组
|
||||
- `rdfs:comment`: 类的描述
|
||||
- `rdfs:subClassOf`: 上级类的标识符 (单继承)
|
||||
- `owl:equivalentClass`: 等效类的标识符数组
|
||||
- `owl:disjointWith`: 互斥类的标识符数组
|
||||
- `dcterms:identifier`: 外部参考标识符 (可选)
|
||||
|
||||
#### 3. 对象属性
|
||||
|
||||
用于链接实例的对象属性:
|
||||
|
||||
```json
|
||||
{
|
||||
"objectProperties": {
|
||||
"has-parent": {
|
||||
"uri": "http://trustgraph.ai/ontologies/natural-world#has-parent",
|
||||
"type": "owl:ObjectProperty",
|
||||
"rdfs:label": [{"value": "has parent", "lang": "en"}],
|
||||
"rdfs:comment": "Links an animal to its parent",
|
||||
"rdfs:domain": "animal",
|
||||
"rdfs:range": "animal",
|
||||
"owl:inverseOf": "parent-of",
|
||||
"owl:functionalProperty": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**支持的属性**:
|
||||
- `uri`: 属性的完整 URI
|
||||
- `type`: 始终为 `"owl:ObjectProperty"`
|
||||
- `rdfs:label`: 语言标记的标签数组
|
||||
- `rdfs:comment`: 属性的描述
|
||||
- `rdfs:domain`: 域的标识符
|
||||
- `rdfs:range`: 范围的标识符
|
||||
- `owl:inverseOf`: 互反属性的标识符
|
||||
- `owl:functionalProperty`: 功能属性的标识符 (可选)
|
||||
|
||||
#### 4. 数据类型属性
|
||||
|
||||
```json
|
||||
{
|
||||
"datatypeProperties": {
|
||||
"number-of-legs": {
|
||||
"uri": "http://trustgraph.ai/ontologies/natural-world#number-of-legs",
|
||||
"type": "owl:DatatypeProperty",
|
||||
"rdfs:label": [{"value": "number-of-legs", "lang": "en"}],
|
||||
"rdfs:comment": "Count of number of legs of the animal",
|
||||
"rdfs:range": "xsd:nonNegativeInteger",
|
||||
"rdfs:domain": "animal"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. 示例本体
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"name": "The natural world",
|
||||
"description": "Ontology covering the natural order",
|
||||
"version": "1.0.0",
|
||||
"created": "2025-09-20T12:07:37.068Z",
|
||||
"modified": "2025-09-20T12:12:20.725Z",
|
||||
"creator": "current-user",
|
||||
"namespace": "http://trustgraph.ai/ontologies/natural-world",
|
||||
"imports": ["http://www.w3.org/2002/07/owl#"]
|
||||
},
|
||||
"classes": {
|
||||
"animal": {
|
||||
"uri": "http://trustgraph.ai/ontologies/natural-world#animal",
|
||||
"type": "owl:Class",
|
||||
"rdfs:label": [{"value": "Animal", "lang": "en"}],
|
||||
"rdfs:comment": "An animal"
|
||||
},
|
||||
"cat": {
|
||||
"uri": "http://trustgraph.ai/ontologies/natural-world#cat",
|
||||
"type": "owl:Class",
|
||||
"rdfs:label": [{"value": "Cat", "lang": "en"}],
|
||||
"rdfs:comment": "A cat",
|
||||
"rdfs:subClassOf": "animal"
|
||||
},
|
||||
"dog": {
|
||||
"uri": "http://trustgraph.ai/ontologies/natural-world#dog",
|
||||
"type": "owl:Class",
|
||||
"rdfs:label": [{"value": "Dog", "lang": "en"}],
|
||||
"rdfs:comment": "A dog",
|
||||
"rdfs:subClassOf": "animal",
|
||||
"owl:disjointWith": ["cat"]
|
||||
}
|
||||
},
|
||||
"objectProperties": {},
|
||||
"datatypeProperties": {
|
||||
"number-of-legs": {
|
||||
"uri": "http://trustgraph.ai/ontologies/natural-world#number-of-legs",
|
||||
"type": "owl:DatatypeProperty",
|
||||
"rdfs:label": [{"value": "number-of-legs", "lang": "en"}],
|
||||
"rdfs:comment": "Count of number of legs of the animal",
|
||||
"rdfs:range": "xsd:nonNegativeInteger",
|
||||
"rdfs:domain": "animal"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 验证规则
|
||||
|
||||
### 结构验证
|
||||
|
||||
1. **URI 约束**: 所有 URI 必须遵循模式 `{namespace}#{identifier}`
|
||||
2. **类层次**: 避免在 `rdfs:subClassOf` 中出现循环继承
|
||||
3. **属性域/范围**: 必须引用现有类或有效的 XSD 类型
|
||||
4. **互斥类**: 互斥类不能是互斥类的子类
|
||||
5. **互反属性**: 如果指定,必须是双向的
|
||||
|
||||
### 语义验证
|
||||
|
||||
1. **唯一标识符**: 类的标识符和属性标识符必须是唯一的
|
||||
2. **语言标签**: 必须符合 BCP 47 语言标签格式
|
||||
3. **数量约束**: `minCardinality` ≤ `maxCardinality`,当两者都指定时
|
||||
4. **功能属性**: 不能有 `maxCardinality` > 1
|
||||
|
||||
## 导入/导出格式支持
|
||||
|
||||
虽然内部格式为 JSON,但该系统支持与标准本体格式的转换:
|
||||
|
||||
- **Turtle (.ttl)** - 紧凑的 RDF 序列化
|
||||
- **RDF/XML (.rdf, .owl)** - W3C 标准格式
|
||||
- **OWL/XML (.owx)** - OWL 特定 XML 格式
|
||||
- **JSON-LD (.jsonld)** - 用于链接数据的 JSON
|
||||
|
||||
## 参考文献
|
||||
|
||||
- [OWL 2 Web Ontology Language](https://www.w3.org/TR/owl2-overview/)
|
||||
- [RDF Schema 1.1](https://www.w3.org/TR/rdf-schema/)
|
||||
- [XML Schema Datatypes](https://www.w3.org/TR/xmlschema-2/)
|
||||
- [BCP 47 Language Tags](https://tools.ietf.org/html/bcp47)
|
||||
1075
docs/tech-specs/zh-cn/ontorag.zh-cn.md
Normal file
1075
docs/tech-specs/zh-cn/ontorag.zh-cn.md
Normal file
File diff suppressed because it is too large
Load diff
239
docs/tech-specs/zh-cn/openapi-spec.zh-cn.md
Normal file
239
docs/tech-specs/zh-cn/openapi-spec.zh-cn.md
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
---
|
||||
layout: default
|
||||
title: "OpenAPI 规范 - 技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# OpenAPI 规范 - 技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 目标
|
||||
|
||||
创建一个全面的、模块化的 OpenAPI 3.1 规范,用于 TrustGraph REST API 网关,该规范应:
|
||||
记录所有 REST 接口
|
||||
使用外部 `$ref` 以实现模块化和可维护性
|
||||
直接映射到消息转换器代码
|
||||
提供准确的请求/响应模式
|
||||
|
||||
## 权威来源
|
||||
|
||||
API 由以下内容定义:
|
||||
**消息转换器**: `trustgraph-base/trustgraph/messaging/translators/*.py`
|
||||
**分派器管理器**: `trustgraph-flow/trustgraph/gateway/dispatch/manager.py`
|
||||
**端点管理器**: `trustgraph-flow/trustgraph/gateway/endpoint/manager.py`
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
openapi/
|
||||
├── openapi.yaml # Main entry point
|
||||
├── paths/
|
||||
│ ├── config.yaml # Global services
|
||||
│ ├── flow.yaml
|
||||
│ ├── librarian.yaml
|
||||
│ ├── knowledge.yaml
|
||||
│ ├── collection-management.yaml
|
||||
│ ├── flow-services/ # Flow-hosted services
|
||||
│ │ ├── agent.yaml
|
||||
│ │ ├── document-rag.yaml
|
||||
│ │ ├── graph-rag.yaml
|
||||
│ │ ├── text-completion.yaml
|
||||
│ │ ├── prompt.yaml
|
||||
│ │ ├── embeddings.yaml
|
||||
│ │ ├── mcp-tool.yaml
|
||||
│ │ ├── triples.yaml
|
||||
│ │ ├── objects.yaml
|
||||
│ │ ├── nlp-query.yaml
|
||||
│ │ ├── structured-query.yaml
|
||||
│ │ ├── structured-diag.yaml
|
||||
│ │ ├── graph-embeddings.yaml
|
||||
│ │ ├── document-embeddings.yaml
|
||||
│ │ ├── text-load.yaml
|
||||
│ │ └── document-load.yaml
|
||||
│ ├── import-export/
|
||||
│ │ ├── core-import.yaml
|
||||
│ │ ├── core-export.yaml
|
||||
│ │ └── flow-import-export.yaml # WebSocket import/export
|
||||
│ ├── websocket.yaml
|
||||
│ └── metrics.yaml
|
||||
├── components/
|
||||
│ ├── schemas/
|
||||
│ │ ├── config/
|
||||
│ │ ├── flow/
|
||||
│ │ ├── librarian/
|
||||
│ │ ├── knowledge/
|
||||
│ │ ├── collection/
|
||||
│ │ ├── ai-services/
|
||||
│ │ ├── common/
|
||||
│ │ └── errors/
|
||||
│ ├── parameters/
|
||||
│ ├── responses/
|
||||
│ └── examples/
|
||||
└── security/
|
||||
└── bearerAuth.yaml
|
||||
```
|
||||
|
||||
## 服务映射
|
||||
|
||||
### 全局服务 (`/api/v1/{kind}`)
|
||||
`config` - 配置管理
|
||||
`flow` - 流生存周期
|
||||
`librarian` - 文档库
|
||||
`knowledge` - 知识核心
|
||||
`collection-management` - 集合元数据
|
||||
|
||||
### 托管于流的服务 (`/api/v1/flow/{flow}/service/{kind}`)
|
||||
|
||||
**请求/响应:**
|
||||
`agent`, `text-completion`, `prompt`, `mcp-tool`
|
||||
`graph-rag`, `document-rag`
|
||||
`embeddings`, `graph-embeddings`, `document-embeddings`
|
||||
`triples`, `objects`, `nlp-query`, `structured-query`, `structured-diag`
|
||||
|
||||
**发布/订阅:**
|
||||
`text-load`, `document-load`
|
||||
|
||||
### 导入/导出
|
||||
`/api/v1/import-core` (POST)
|
||||
`/api/v1/export-core` (GET)
|
||||
`/api/v1/flow/{flow}/import/{kind}` (WebSocket)
|
||||
`/api/v1/flow/{flow}/export/{kind}` (WebSocket)
|
||||
|
||||
### 其他
|
||||
`/api/v1/socket` (WebSocket 多路复用)
|
||||
`/api/metrics` (Prometheus)
|
||||
|
||||
## 方案
|
||||
|
||||
### 第一阶段:设置
|
||||
1. 创建目录结构
|
||||
2. 创建主 `openapi.yaml` 文件,包含元数据、服务器、安全配置
|
||||
3. 创建可重用组件(错误、通用参数、安全方案)
|
||||
|
||||
### 第二阶段:通用模式
|
||||
创建在服务间共享的模式:
|
||||
`RdfValue`, `Triple` - RDF/三元组结构
|
||||
`ErrorObject` - 错误响应
|
||||
`DocumentMetadata`, `ProcessingMetadata` - 元数据结构
|
||||
通用参数:`FlowId`, `User`, `Collection`
|
||||
|
||||
### 第三阶段:全球服务
|
||||
对于每个全球服务(配置、流程、库管理员、知识、集合管理):
|
||||
1. 在 `paths/` 中创建路径文件。
|
||||
2. 在 `components/schemas/{service}/` 中创建请求模式。
|
||||
3. 创建响应模式。
|
||||
4. 添加示例。
|
||||
5. 从主文件 `openapi.yaml` 中引用。
|
||||
|
||||
### 第四阶段:流程托管服务
|
||||
对于每个流程托管服务:
|
||||
1. 在 `paths/flow-services/` 中创建路径文件。
|
||||
2. 在 `components/schemas/ai-services/` 中创建请求/响应模式。
|
||||
3. 在适用情况下,添加流媒体标志文档。
|
||||
4. 从主文件 `openapi.yaml` 中引用。
|
||||
|
||||
### 第五阶段:导入/导出 & WebSocket
|
||||
1. 文档核心导入/导出端点。
|
||||
2. 文档 WebSocket 协议模式。
|
||||
3. 文档流程级别的导入/导出 WebSocket 端点。
|
||||
|
||||
### 第六阶段:验证
|
||||
1. 使用 OpenAPI 验证工具进行验证。
|
||||
2. 使用 Swagger UI 进行测试。
|
||||
3. 验证所有翻译器是否已覆盖。
|
||||
|
||||
## 字段命名约定
|
||||
|
||||
所有 JSON 字段使用 **kebab-case**:
|
||||
`flow-id`, `blueprint-name`, `doc-limit`, `entity-limit`, 等。
|
||||
|
||||
## 创建模式文件
|
||||
|
||||
对于 `trustgraph-base/trustgraph/messaging/translators/` 中的每个翻译器:
|
||||
|
||||
1. **读取翻译器 `to_pulsar()` 方法** - 定义请求模式。
|
||||
2. **读取翻译器 `from_pulsar()` 方法** - 定义响应模式。
|
||||
3. **提取字段名称和类型**。
|
||||
4. **创建 OpenAPI 模式**,包含:
|
||||
字段名称(kebab-case)。
|
||||
类型(字符串、整数、布尔值、对象、数组)。
|
||||
必需字段。
|
||||
默认值。
|
||||
描述。
|
||||
|
||||
### 示例映射过程
|
||||
|
||||
```python
|
||||
# From retrieval.py DocumentRagRequestTranslator
|
||||
def to_pulsar(self, data: Dict[str, Any]) -> DocumentRagQuery:
|
||||
return DocumentRagQuery(
|
||||
query=data["query"], # required string
|
||||
user=data.get("user", "trustgraph"), # optional string, default "trustgraph"
|
||||
collection=data.get("collection", "default"), # optional string, default "default"
|
||||
doc_limit=int(data.get("doc-limit", 20)), # optional integer, default 20
|
||||
streaming=data.get("streaming", False) # optional boolean, default false
|
||||
)
|
||||
```
|
||||
|
||||
对应于:
|
||||
|
||||
```yaml
|
||||
# components/schemas/ai-services/DocumentRagRequest.yaml
|
||||
type: object
|
||||
required:
|
||||
- query
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: Search query
|
||||
user:
|
||||
type: string
|
||||
default: trustgraph
|
||||
collection:
|
||||
type: string
|
||||
default: default
|
||||
doc-limit:
|
||||
type: integer
|
||||
default: 20
|
||||
description: Maximum number of documents to retrieve
|
||||
streaming:
|
||||
type: boolean
|
||||
default: false
|
||||
description: Enable streaming responses
|
||||
```
|
||||
|
||||
## 流式响应
|
||||
|
||||
支持流式传输的服务会返回带有 `end_of_stream` 标志的多个响应:
|
||||
`agent`, `text-completion`, `prompt`
|
||||
`document-rag`, `graph-rag`
|
||||
|
||||
在每个服务的响应模式中记录此模式。
|
||||
|
||||
## 错误响应
|
||||
|
||||
所有服务都可以返回:
|
||||
```yaml
|
||||
error:
|
||||
oneOf:
|
||||
- type: string
|
||||
- $ref: '#/components/schemas/ErrorObject'
|
||||
```
|
||||
|
||||
其中,`ErrorObject` 指的是:
|
||||
```yaml
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
```
|
||||
|
||||
## 参考文献
|
||||
|
||||
翻译人员:`trustgraph-base/trustgraph/messaging/translators/`
|
||||
调度映射:`trustgraph-flow/trustgraph/gateway/dispatch/manager.py`
|
||||
终端路由:`trustgraph-flow/trustgraph/gateway/endpoint/manager.py`
|
||||
服务摘要:`API_SERVICES_SUMMARY.md`
|
||||
965
docs/tech-specs/zh-cn/pubsub.zh-cn.md
Normal file
965
docs/tech-specs/zh-cn/pubsub.zh-cn.md
Normal file
|
|
@ -0,0 +1,965 @@
|
|||
---
|
||||
layout: default
|
||||
title: "Pub/Sub 基础设施"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# Pub/Sub 基础设施
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本文档记录了 TrustGraph 代码库与 pub/sub 基础设施之间的所有连接。目前,系统是硬编码为使用 Apache Pulsar。此分析识别了所有集成点,以促进未来向可配置的 pub/sub 抽象的重构。
|
||||
|
||||
## 当前状态:Pulsar 集成点
|
||||
|
||||
### 1. 直接 Pulsar 客户端使用
|
||||
|
||||
**位置:** `trustgraph-flow/trustgraph/gateway/service.py`
|
||||
|
||||
API 网关直接导入和实例化 Pulsar 客户端:
|
||||
|
||||
**第 20 行:** `import pulsar`
|
||||
**第 54-61 行:** 直接实例化 `pulsar.Client()`,带可选 `pulsar.AuthenticationToken()`
|
||||
**第 33-35 行:** 从环境变量获取默认 Pulsar 主机配置
|
||||
**第 178-192 行:** CLI 参数,用于 `--pulsar-host`、`--pulsar-api-key` 和 `--pulsar-listener`
|
||||
**第 78、124 行:** 将 `pulsar_client` 传递给 `ConfigReceiver` 和 `DispatcherManager`
|
||||
|
||||
这是唯一直接在抽象层之外实例化 Pulsar 客户端的位置。
|
||||
|
||||
### 2. 基础处理器框架
|
||||
|
||||
**位置:** `trustgraph-base/trustgraph/base/async_processor.py`
|
||||
|
||||
所有处理器的基本类提供 Pulsar 连接:
|
||||
|
||||
**第 9 行:** `import _pulsar` (用于异常处理)
|
||||
**第 18 行:** `from . pubsub import PulsarClient`
|
||||
**第 38 行:** 创建 `pulsar_client_object = PulsarClient(**params)`
|
||||
**第 104-108 行:** 暴露 `pulsar_host` 和 `pulsar_client` 的属性
|
||||
**第 250 行:** 静态方法 `add_args()` 调用 `PulsarClient.add_args(parser)` 以获取 CLI 参数
|
||||
**第 223-225 行:** `_pulsar.Interrupted` 的异常处理
|
||||
|
||||
所有处理器都继承自 `AsyncProcessor`,使其成为主要的集成点。
|
||||
|
||||
### 3. 消费者抽象
|
||||
|
||||
**位置:** `trustgraph-base/trustgraph/base/consumer.py`
|
||||
|
||||
从队列中消耗消息并调用处理函数:
|
||||
|
||||
**Pulsar 导入:**
|
||||
**第 12 行:** `from pulsar.schema import JsonSchema`
|
||||
**第 13 行:** `import pulsar`
|
||||
**第 14 行:** `import _pulsar`
|
||||
|
||||
**Pulsar 特定的用法:**
|
||||
**第 100、102 行:** `pulsar.InitialPosition.Earliest` / `pulsar.InitialPosition.Latest`
|
||||
**第 108 行:** `JsonSchema(self.schema)` 包装器
|
||||
**第 110 行:** `pulsar.ConsumerType.Shared`
|
||||
**第 104-111 行:** `self.client.subscribe()`,带有 Pulsar 特定的参数
|
||||
**第 143、150、65 行:** `consumer.unsubscribe()` 和 `consumer.close()` 方法
|
||||
**第 162 行:** `_pulsar.Timeout` 异常
|
||||
**第 182、205、232 行:** `consumer.acknowledge()` / `consumer.negative_acknowledge()`
|
||||
|
||||
**Spec 文件:** `trustgraph-base/trustgraph/base/consumer_spec.py`
|
||||
**第 22 行:** 引用 `processor.pulsar_client`
|
||||
|
||||
### 4. 生产者抽象
|
||||
|
||||
**位置:** `trustgraph-base/trustgraph/base/producer.py`
|
||||
|
||||
向队列发送消息:
|
||||
|
||||
**Pulsar 导入:**
|
||||
**第 2 行:** `from pulsar.schema import JsonSchema`
|
||||
|
||||
**Pulsar 特定的用法:**
|
||||
**第 49 行:** `JsonSchema(self.schema)` 包装器
|
||||
**第 47-51 行:** `self.client.create_producer()`,带有 Pulsar 特定的参数 (topic, schema, chunking_enabled)
|
||||
**第 31、76 行:** `producer.close()` 方法
|
||||
**第 64-65 行:** `producer.send()`,带有消息和属性
|
||||
|
||||
**Spec 文件:** `trustgraph-base/trustgraph/base/producer_spec.py`
|
||||
**第 18 行:** 引用 `processor.pulsar_client`
|
||||
|
||||
### 5. 发布者抽象
|
||||
|
||||
**位置:** `trustgraph-base/trustgraph/base/publisher.py`
|
||||
|
||||
具有队列缓冲的异步消息发布:
|
||||
|
||||
**Pulsar 导入:**
|
||||
**第 2 行:** `from pulsar.schema import JsonSchema`
|
||||
**第 6 行:** `import pulsar`
|
||||
|
||||
**Pulsar 特定的用法:**
|
||||
**第 52 行:** `JsonSchema(self.schema)` 包装器
|
||||
**第 50-54 行:** `self.client.create_producer()`,带有 Pulsar 特定的参数
|
||||
**第 101、103 行:** `producer.send()`,带有消息和可选属性
|
||||
**第 106-107 行:** `producer.flush()` 和 `producer.close()` 方法
|
||||
|
||||
### 6. 订阅者抽象
|
||||
|
||||
**位置:** `trustgraph-base/trustgraph/base/subscriber.py`
|
||||
|
||||
提供从队列分发给多个接收者的消息功能:
|
||||
|
||||
**Pulsar 导入:**
|
||||
**第 6 行:** `from pulsar.schema import JsonSchema`
|
||||
**第 8 行:** `import _pulsar`
|
||||
|
||||
**Pulsar 特定的用法:**
|
||||
**第 55 行:** `JsonSchema(self.schema)` 包装器
|
||||
**第 57 行:** `self.client.subscribe(**subscribe_args)`
|
||||
**第 101、136、160、167-172 行:** Pulsar 异常:`_pulsar.Timeout`, `_pulsar.InvalidConfiguration`, `_pulsar.AlreadyClosed`
|
||||
**第 159、166、170 行:** 消费者方法:`negative_acknowledge()`, `unsubscribe()`, `close()`
|
||||
**第 247、251 行:** 消息确认:`acknowledge()`, `negative_acknowledge()`
|
||||
|
||||
**Spec 文件:** `trustgraph-base/trustgraph/base/subscriber_spec.py`
|
||||
**第 19 行:** 引用 `processor.pulsar_client`
|
||||
|
||||
### 7. Schema 系统 (黑暗之心)
|
||||
|
||||
**位置:** `trustgraph-base/trustgraph/schema/`
|
||||
|
||||
系统中的每个消息模式都使用 Pulsar 的模式框架定义。
|
||||
|
||||
**核心原语:** `schema/core/primitives.py`
|
||||
**第 2 行:** `from pulsar.schema import Record, String, Boolean, Array, Integer`
|
||||
所有模式都继承自 Pulsar 的 `Record` 基类
|
||||
所有字段类型都是 Pulsar 类型:`String()`, `Integer()`, `Boolean()`, `Array()`, `Map()`, `Double()`
|
||||
|
||||
**示例模式:**
|
||||
`schema/services/llm.py` (第 2 行):`from pulsar.schema import Record, String, Array, Double, Integer, Boolean`
|
||||
`schema/services/config.py` (第 2 行):`from pulsar.schema import Record, Bytes, String, Boolean, Array, Map, Integer`
|
||||
|
||||
**主题命名:** `schema/core/topic.py`
|
||||
**第 2-3 行:** 主题格式:`{kind}://{tenant}/{namespace}/{topic}`
|
||||
此 URI 结构是 Pulsar 特定的(例如,`persistent://tg/flow/config`)
|
||||
|
||||
**影响:**
|
||||
代码库中所有请求/响应消息定义都使用 Pulsar 模式
|
||||
这包括用于:config、flow、llm、prompt、query、storage、agent、collection、diagnosis、library、lookup、nlp_query、objects_query、retrieval、structured_query 的服务
|
||||
模式定义被导入并广泛地用于所有处理器和服务中
|
||||
|
||||
## 总结
|
||||
|
||||
### Pulsar 依赖项按类别划分
|
||||
|
||||
1. **客户端实例化:**
|
||||
直接:`gateway/service.py`
|
||||
抽象:`async_processor.py` → `pubsub.py` (PulsarClient)
|
||||
|
||||
2. **消息传输:**
|
||||
消费者:`consumer.py`, `consumer_spec.py`
|
||||
生产者:`producer.py`, `producer_spec.py`
|
||||
发布者:`publisher.py`
|
||||
订阅者:`subscriber.py`, `subscriber_spec.py`
|
||||
|
||||
3. **模式系统:**
|
||||
基本类型:`schema/core/primitives.py`
|
||||
所有服务模式:`schema/services/*.py`
|
||||
主题命名:`schema/core/topic.py`
|
||||
|
||||
4. **需要 Pulsar 特定的概念:**
|
||||
基于主题的消息
|
||||
模式系统 (Record, 字段类型)
|
||||
共享订阅
|
||||
消息确认 (肯定/否定)
|
||||
消费者定位 (最早/最新)
|
||||
消息属性
|
||||
初始位置和消费者类型
|
||||
分块支持
|
||||
持久化与非持久化主题
|
||||
|
||||
### 重构挑战
|
||||
|
||||
好的消息:抽象层 (Consumer, Producer, Publisher, Subscriber) 提供了一个干净的 Pulsar 交互的封装。
|
||||
|
||||
挑战:
|
||||
1. **模式系统的普遍性:** 每个消息定义都使用 `pulsar.schema.Record` 和 Pulsar 字段类型
|
||||
2. **Pulsar 特定的枚举:** `InitialPosition`, `ConsumerType`
|
||||
3. **Pulsar 异常:** `_pulsar.Timeout`, `_pulsar.Interrupted`, `_pulsar.InvalidConfiguration`, `_pulsar.AlreadyClosed`
|
||||
4. **方法签名:** `acknowledge()`, `negative_acknowledge()`, `subscribe()`, `create_producer()`, 等等。
|
||||
5. **主题 URI 格式:** Pulsar 的 `kind://tenant/namespace/topic` 结构
|
||||
|
||||
### 下一步
|
||||
|
||||
为了使 pub/sub 基础设施可配置,我们需要:
|
||||
|
||||
1. 为客户端/模式系统创建抽象接口
|
||||
2. 抽象 Pulsar 特定的枚举和异常
|
||||
3. 创建模式包装器或替代模式定义
|
||||
4. 为 Pulsar 和其他系统(Kafka、RabbitMQ、Redis Streams 等)实现该接口
|
||||
5. 更新 `pubsub.py` 以使其可配置并支持多个后端
|
||||
6. 提供现有部署的迁移路径
|
||||
|
||||
## 方法草案 1:带模式转换层的适配器模式
|
||||
|
||||
### 关键洞察
|
||||
**模式系统** 是最深入的集成点 - 其他一切都由此而产生。 我们需要首先解决这个问题,否则我们将重写整个代码库。
|
||||
|
||||
### 策略:通过适配器实现最小的干扰
|
||||
|
||||
**1. 保持 Pulsar 模式作为内部表示**
|
||||
不要重写所有的模式定义
|
||||
模式在内部仍然是 `pulsar.schema.Record`
|
||||
使用适配器在我们的代码和发布/订阅后端之间的边界进行转换
|
||||
|
||||
**2. 创建一个发布/订阅抽象层:**
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Existing Code (unchanged) │
|
||||
│ - Uses Pulsar schemas internally │
|
||||
│ - Consumer/Producer/Publisher │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────┴──────────────────────┐
|
||||
│ PubSubFactory (configurable) │
|
||||
│ - Creates backend-specific client │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────┴──────┐
|
||||
│ │
|
||||
┌───────▼─────┐ ┌────▼─────────┐
|
||||
│ PulsarAdapter│ │ KafkaAdapter │ etc...
|
||||
│ (passthrough)│ │ (translates) │
|
||||
└──────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
**3. 定义抽象接口:**
|
||||
`PubSubClient` - 客户端连接
|
||||
`PubSubProducer` - 发送消息
|
||||
`PubSubConsumer` - 接收消息
|
||||
`SchemaAdapter` - 将 Pulsar 模式转换为 JSON 或后端特定的格式,反之亦然
|
||||
|
||||
**4. 实现细节:**
|
||||
|
||||
对于 **Pulsar 适配器**: 几乎是直通模式,最小化转换
|
||||
|
||||
对于 **其他后端** (Kafka, RabbitMQ 等):
|
||||
将 Pulsar Record 对象序列化为 JSON/字节
|
||||
映射概念,例如:
|
||||
`InitialPosition.Earliest/Latest` → Kafka 的 auto.offset.reset
|
||||
`acknowledge()` → Kafka 的 commit
|
||||
`negative_acknowledge()` → 重试或 DLQ 模式
|
||||
Topic URI → 后端特定的主题名称
|
||||
|
||||
### 分析
|
||||
|
||||
**优点:**
|
||||
✅ 对现有服务的代码更改最小
|
||||
✅ 模式保持不变 (无需大规模重写)
|
||||
✅ 逐步迁移路径
|
||||
✅ Pulsar 用户不会看到任何差异
|
||||
✅ 通过适配器添加新的后端
|
||||
|
||||
**缺点:**
|
||||
⚠️ 仍然依赖 Pulsar (用于模式定义)
|
||||
⚠️ 在转换概念时存在一些阻抗不匹配
|
||||
|
||||
### 替代方案
|
||||
|
||||
创建一个 **TrustGraph 模式系统**,该系统与 pub/sub 无关 (使用 dataclasses 或 Pydantic),然后从该系统生成 Pulsar/Kafka/等的模式。 这需要重写每个模式文件,并且可能导致不兼容的更改。
|
||||
|
||||
### 草案 1 的建议
|
||||
|
||||
从 **适配器方法** 开始,因为:
|
||||
1. 它实用 - 与现有代码兼容
|
||||
2. 以最小的风险验证概念
|
||||
3. 如果需要,可以演变为原生模式系统
|
||||
4. 基于配置:一个环境变量切换后端
|
||||
|
||||
## 草案 2 方法:具有 Dataclasses 的后端无关模式系统
|
||||
|
||||
### 核心概念
|
||||
|
||||
使用 Python **dataclasses** 作为中立的模式定义格式。 每个 pub/sub 后端都提供自己的序列化/反序列化,以处理 dataclasses,从而无需在代码库中保留 Pulsar 模式。
|
||||
|
||||
### 工厂级别的模式多态
|
||||
|
||||
而是翻译 Pulsar 模式,**每个后端都提供自己的模式处理**,该处理方式适用于标准的 Python dataclasses。
|
||||
|
||||
### 发布者流程
|
||||
|
||||
```python
|
||||
# 1. Get the configured backend from factory
|
||||
pubsub = get_pubsub() # Returns PulsarBackend, MQTTBackend, etc.
|
||||
|
||||
# 2. Get schema class from the backend
|
||||
# (Can be imported directly - backend-agnostic)
|
||||
from trustgraph.schema.services.llm import TextCompletionRequest
|
||||
|
||||
# 3. Create a producer/publisher for a specific topic
|
||||
producer = pubsub.create_producer(
|
||||
topic="text-completion-requests",
|
||||
schema=TextCompletionRequest # Tells backend what schema to use
|
||||
)
|
||||
|
||||
# 4. Create message instances (same API regardless of backend)
|
||||
request = TextCompletionRequest(
|
||||
system="You are helpful",
|
||||
prompt="Hello world",
|
||||
streaming=False
|
||||
)
|
||||
|
||||
# 5. Send the message
|
||||
producer.send(request) # Backend serializes appropriately
|
||||
```
|
||||
|
||||
### 消费者流程
|
||||
|
||||
```python
|
||||
# 1. Get the configured backend
|
||||
pubsub = get_pubsub()
|
||||
|
||||
# 2. Create a consumer
|
||||
consumer = pubsub.subscribe(
|
||||
topic="text-completion-requests",
|
||||
schema=TextCompletionRequest # Tells backend how to deserialize
|
||||
)
|
||||
|
||||
# 3. Receive and deserialize
|
||||
msg = consumer.receive()
|
||||
request = msg.value() # Returns TextCompletionRequest dataclass instance
|
||||
|
||||
# 4. Use the data (type-safe access)
|
||||
print(request.system) # "You are helpful"
|
||||
print(request.prompt) # "Hello world"
|
||||
print(request.streaming) # False
|
||||
```
|
||||
|
||||
### 幕后发生了什么
|
||||
|
||||
**对于 Pulsar 后端:**
|
||||
`create_producer()` → 创建具有 JSON 模式或动态生成的记录的 Pulsar 生产者。
|
||||
`send(request)` → 将数据类序列化为 JSON/Pulsar 格式,并发送到 Pulsar。
|
||||
`receive()` → 获取 Pulsar 消息,将其反序列化回数据类。
|
||||
|
||||
**对于 MQTT 后端:**
|
||||
`create_producer()` → 连接到 MQTT 代理,无需注册模式。
|
||||
`send(request)` → 将数据类转换为 JSON,并发布到 MQTT 主题。
|
||||
`receive()` → 订阅 MQTT 主题,并将 JSON 反序列化为数据类。
|
||||
|
||||
**对于 Kafka 后端:**
|
||||
`create_producer()` → 创建 Kafka 生产者,如果需要,注册 Avro 模式。
|
||||
`send(request)` → 将数据类序列化为 Avro 格式,并发送到 Kafka。
|
||||
`receive()` → 获取 Kafka 消息,并将 Avro 反序列化回数据类。
|
||||
|
||||
### 关键设计点
|
||||
|
||||
1. **模式对象创建:** 数据类实例 (`TextCompletionRequest(...)`) 无论后端如何都是相同的。
|
||||
2. **后端处理编码:** 每个后端都知道如何将数据类序列化为线格式。
|
||||
3. **在创建时定义模式:** 在创建生产者/消费者时,您指定模式类型。
|
||||
4. **保留类型安全:** 您会得到一个正确的 `TextCompletionRequest` 对象,而不是一个字典。
|
||||
5. **没有后端泄漏:** 应用程序代码永远不会导入特定于后端的库。
|
||||
|
||||
### 示例转换
|
||||
|
||||
**当前(特定于 Pulsar):**
|
||||
```python
|
||||
# schema/services/llm.py
|
||||
from pulsar.schema import Record, String, Boolean, Integer
|
||||
|
||||
class TextCompletionRequest(Record):
|
||||
system = String()
|
||||
prompt = String()
|
||||
streaming = Boolean()
|
||||
```
|
||||
|
||||
**新功能(与后端无关):**
|
||||
```python
|
||||
# schema/services/llm.py
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class TextCompletionRequest:
|
||||
system: str
|
||||
prompt: str
|
||||
streaming: bool = False
|
||||
```
|
||||
|
||||
### 后端集成
|
||||
|
||||
每个后端负责数据类的序列化/反序列化:
|
||||
|
||||
**Pulsar 后端:**
|
||||
从数据类动态生成 `pulsar.schema.Record` 类
|
||||
或者将数据类序列化为 JSON,并使用 Pulsar 的 JSON 模式
|
||||
保持与现有 Pulsar 部署的兼容性
|
||||
|
||||
**MQTT/Redis 后端:**
|
||||
直接对数据类实例进行 JSON 序列化
|
||||
使用 `dataclasses.asdict()` / `from_dict()`
|
||||
轻量级,无需模式注册表
|
||||
|
||||
**Kafka 后端:**
|
||||
从数据类定义生成 Avro 模式
|
||||
使用 Confluent 的模式注册表
|
||||
具有模式演进支持的类型安全序列化
|
||||
|
||||
### 架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Application Code │
|
||||
│ - Uses dataclass schemas │
|
||||
│ - Backend-agnostic │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────┴──────────────────────┐
|
||||
│ PubSubFactory (configurable) │
|
||||
│ - get_pubsub() returns backend │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────┴──────┐
|
||||
│ │
|
||||
┌───────▼─────────┐ ┌────▼──────────────┐
|
||||
│ PulsarBackend │ │ MQTTBackend │
|
||||
│ - JSON schema │ │ - JSON serialize │
|
||||
│ - or dynamic │ │ - Simple queues │
|
||||
│ Record gen │ │ │
|
||||
└─────────────────┘ └───────────────────┘
|
||||
```
|
||||
|
||||
### 实现细节
|
||||
|
||||
**1. 模式定义:** 使用带有类型提示的简单数据类
|
||||
`str`, `int`, `bool`, `float` 用于基本类型
|
||||
`list[T]` 用于数组
|
||||
`dict[str, T]` 用于映射
|
||||
用于复杂类型的嵌套数据类
|
||||
|
||||
**2. 每个后端提供:**
|
||||
序列化器:`dataclass → bytes/wire format`
|
||||
反序列化器:`bytes/wire format → dataclass`
|
||||
模式注册(如果需要,例如 Pulsar/Kafka)
|
||||
|
||||
**3. 消费者/生产者抽象:**
|
||||
已经存在 (consumer.py, producer.py)
|
||||
更新以使用后端的序列化
|
||||
移除直接的 Pulsar 导入
|
||||
|
||||
**4. 类型映射:**
|
||||
Pulsar `String()` → Python `str`
|
||||
Pulsar `Integer()` → Python `int`
|
||||
Pulsar `Boolean()` → Python `bool`
|
||||
Pulsar `Array(T)` → Python `list[T]`
|
||||
Pulsar `Map(K, V)` → Python `dict[K, V]`
|
||||
Pulsar `Double()` → Python `float`
|
||||
Pulsar `Bytes()` → Python `bytes`
|
||||
|
||||
### 迁移路径
|
||||
|
||||
1. **创建所有模式的数据类版本**,位于 `trustgraph/schema/`
|
||||
2. **更新后端类**(Consumer, Producer, Publisher, Subscriber),以使用后端提供的序列化
|
||||
3. **实现 PulsarBackend**,使用 JSON 模式或动态 Record 生成
|
||||
4. **使用 Pulsar 进行测试**,以确保与现有部署的向后兼容性
|
||||
5. **添加新的后端**(MQTT, Kafka, Redis 等),如果需要
|
||||
6. **从模式文件中移除 Pulsar 导入**
|
||||
|
||||
### 优点
|
||||
|
||||
✅ **模式定义中没有 pub/sub 依赖**
|
||||
✅ **标准 Python** - 易于理解、类型检查和记录
|
||||
✅ **现代工具** - 与 mypy、IDE 自动完成、linter 兼容
|
||||
✅ **后端优化** - 每个后端使用本机序列化
|
||||
✅ **没有翻译开销** - 直接序列化,没有适配器
|
||||
✅ **类型安全** - 具有正确类型的真实对象
|
||||
✅ **易于验证** - 如果需要,可以使用 Pydantic
|
||||
|
||||
### 挑战与解决方案
|
||||
|
||||
**挑战:** Pulsar 的 `Record` 具有运行时字段验证
|
||||
**解决方案:** 如果需要,可以使用 Pydantic 数据类进行验证,或者使用 Python 3.10+ 数据类功能,并结合 `__post_init__`
|
||||
|
||||
**挑战:** 一些 Pulsar 特定的功能(例如 `Bytes` 类型)
|
||||
**解决方案:** 将其映射到数据类中的 `bytes` 类型,后端负责适当的编码
|
||||
|
||||
**挑战:** 主题命名(`persistent://tenant/namespace/topic`)
|
||||
**解决方案:** 在模式定义中抽象主题名称,后端将其转换为正确的格式
|
||||
|
||||
**挑战:** 模式演化和版本控制
|
||||
**解决方案:** 每个后端根据其功能处理此问题(Pulsar 模式版本、Kafka 模式注册等)
|
||||
|
||||
**挑战:** 嵌套的复杂类型
|
||||
**解决方案:** 使用嵌套的数据类,后端递归地序列化/反序列化
|
||||
|
||||
### 设计决策
|
||||
|
||||
1. **使用纯数据类还是 Pydantic?**
|
||||
✅ **决策:使用纯 Python 数据类**
|
||||
更简单,没有额外的依赖
|
||||
实际上不需要验证
|
||||
更容易理解和维护
|
||||
|
||||
2. **模式演化:**
|
||||
✅ **决策:不需要版本机制**
|
||||
模式是稳定且持久的
|
||||
更新通常会添加新字段(向后兼容)
|
||||
后端根据其功能处理模式演化
|
||||
|
||||
3. **向后兼容性:**
|
||||
✅ **决策:进行主要版本更改,不需要向后兼容**
|
||||
这将是一个破坏性更改,并提供迁移说明
|
||||
清理的断开允许更好的设计
|
||||
将为现有部署提供迁移指南
|
||||
|
||||
4. **嵌套类型和复杂结构:**
|
||||
✅ **决策:自然地使用嵌套的数据类**
|
||||
Python 数据类完美地处理嵌套
|
||||
`list[T]` 用于数组,`dict[K, V]` 用于映射
|
||||
后端递归地序列化/反序列化
|
||||
示例:
|
||||
```python
|
||||
@dataclass
|
||||
class Value:
|
||||
value: str
|
||||
is_uri: bool
|
||||
|
||||
@dataclass
|
||||
class Triple:
|
||||
s: Value # Nested dataclass
|
||||
p: Value
|
||||
o: Value
|
||||
|
||||
@dataclass
|
||||
class GraphQuery:
|
||||
triples: list[Triple] # Array of nested dataclasses
|
||||
metadata: dict[str, str]
|
||||
```
|
||||
|
||||
5. **默认值和可选字段:**
|
||||
✅ **决策:强制、默认和可选字段的组合**
|
||||
强制字段:没有默认值
|
||||
具有默认值的字段:始终存在,具有合理的默认值
|
||||
真正可选的字段:`T | None = None`,当`None`时,从序列化中省略
|
||||
示例:
|
||||
```python
|
||||
@dataclass
|
||||
class TextCompletionRequest:
|
||||
system: str # Required, no default
|
||||
prompt: str # Required, no default
|
||||
streaming: bool = False # Optional with default value
|
||||
metadata: dict | None = None # Truly optional, can be absent
|
||||
```
|
||||
|
||||
**重要的序列化语义:**
|
||||
|
||||
当 `metadata = None`:
|
||||
```json
|
||||
{
|
||||
"system": "...",
|
||||
"prompt": "...",
|
||||
"streaming": false
|
||||
// metadata field NOT PRESENT
|
||||
}
|
||||
```
|
||||
|
||||
当 `metadata = {}` (显式为空):
|
||||
```json
|
||||
{
|
||||
"system": "...",
|
||||
"prompt": "...",
|
||||
"streaming": false,
|
||||
"metadata": {} // Field PRESENT but empty
|
||||
}
|
||||
```
|
||||
|
||||
**关键区别:**
|
||||
`None` → JSON 中不存在的字段(未序列化)
|
||||
空值(`{}`, `[]`, `""`)→ 字段存在,但值为空
|
||||
这在语义上很重要:“未提供”与“显式为空”
|
||||
序列化后端必须跳过 `None` 字段,而不是将其编码为 `null`
|
||||
|
||||
## 方案草案 3:实现细节
|
||||
|
||||
### 通用队列命名格式
|
||||
|
||||
将后端特定的队列名称替换为一种通用格式,以便后端可以进行适当的映射。
|
||||
|
||||
**格式:** `{qos}/{tenant}/{namespace}/{queue-name}`
|
||||
|
||||
其中:
|
||||
`qos`:服务质量级别
|
||||
`q0` = 尽力而为(发送并忘记,无需确认)
|
||||
`q1` = 至少一次(需要确认)
|
||||
`q2` = 恰好一次(两阶段确认)
|
||||
`tenant`:多租户逻辑分组
|
||||
`namespace`:租户内的子分组
|
||||
`queue-name`:实际的队列/主题名称
|
||||
|
||||
**示例:**
|
||||
```
|
||||
q1/tg/flow/text-completion-requests
|
||||
q2/tg/config/config-push
|
||||
q0/tg/metrics/stats
|
||||
```
|
||||
|
||||
### 后端主题映射
|
||||
|
||||
每个后端将通用格式映射到其原生格式:
|
||||
|
||||
**Pulsar 后端:**
|
||||
```python
|
||||
def map_topic(self, generic_topic: str) -> str:
|
||||
# Parse: q1/tg/flow/text-completion-requests
|
||||
qos, tenant, namespace, queue = generic_topic.split('/', 3)
|
||||
|
||||
# Map QoS to persistence
|
||||
persistence = 'persistent' if qos in ['q1', 'q2'] else 'non-persistent'
|
||||
|
||||
# Return Pulsar URI: persistent://tg/flow/text-completion-requests
|
||||
return f"{persistence}://{tenant}/{namespace}/{queue}"
|
||||
```
|
||||
|
||||
**MQTT 后端:**
|
||||
```python
|
||||
def map_topic(self, generic_topic: str) -> tuple[str, int]:
|
||||
# Parse: q1/tg/flow/text-completion-requests
|
||||
qos, tenant, namespace, queue = generic_topic.split('/', 3)
|
||||
|
||||
# Map QoS level
|
||||
qos_level = {'q0': 0, 'q1': 1, 'q2': 2}[qos]
|
||||
|
||||
# Build MQTT topic including tenant/namespace for proper namespacing
|
||||
mqtt_topic = f"{tenant}/{namespace}/{queue}"
|
||||
|
||||
return mqtt_topic, qos_level
|
||||
```
|
||||
|
||||
### 更新后的主题辅助函数
|
||||
|
||||
```python
|
||||
# schema/core/topic.py
|
||||
def topic(queue_name, qos='q1', tenant='tg', namespace='flow'):
|
||||
"""
|
||||
Create a generic topic identifier that can be mapped by backends.
|
||||
|
||||
Args:
|
||||
queue_name: The queue/topic name
|
||||
qos: Quality of service
|
||||
- 'q0' = best-effort (no ack)
|
||||
- 'q1' = at-least-once (ack required)
|
||||
- 'q2' = exactly-once (two-phase ack)
|
||||
tenant: Tenant identifier for multi-tenancy
|
||||
namespace: Namespace within tenant
|
||||
|
||||
Returns:
|
||||
Generic topic string: qos/tenant/namespace/queue_name
|
||||
|
||||
Examples:
|
||||
topic('my-queue') # q1/tg/flow/my-queue
|
||||
topic('config', qos='q2', namespace='config') # q2/tg/config/config
|
||||
"""
|
||||
return f"{qos}/{tenant}/{namespace}/{queue_name}"
|
||||
```
|
||||
|
||||
### 配置和初始化
|
||||
|
||||
**命令行参数 + 环境变量:**
|
||||
|
||||
```python
|
||||
# In base/async_processor.py - add_args() method
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
# Pub/sub backend selection
|
||||
parser.add_argument(
|
||||
'--pubsub-backend',
|
||||
default=os.getenv('PUBSUB_BACKEND', 'pulsar'),
|
||||
choices=['pulsar', 'mqtt'],
|
||||
help='Pub/sub backend (default: pulsar, env: PUBSUB_BACKEND)'
|
||||
)
|
||||
|
||||
# Pulsar-specific configuration
|
||||
parser.add_argument(
|
||||
'--pulsar-host',
|
||||
default=os.getenv('PULSAR_HOST', 'pulsar://localhost:6650'),
|
||||
help='Pulsar host (default: pulsar://localhost:6650, env: PULSAR_HOST)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--pulsar-api-key',
|
||||
default=os.getenv('PULSAR_API_KEY', None),
|
||||
help='Pulsar API key (env: PULSAR_API_KEY)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--pulsar-listener',
|
||||
default=os.getenv('PULSAR_LISTENER', None),
|
||||
help='Pulsar listener name (env: PULSAR_LISTENER)'
|
||||
)
|
||||
|
||||
# MQTT-specific configuration
|
||||
parser.add_argument(
|
||||
'--mqtt-host',
|
||||
default=os.getenv('MQTT_HOST', 'localhost'),
|
||||
help='MQTT broker host (default: localhost, env: MQTT_HOST)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--mqtt-port',
|
||||
type=int,
|
||||
default=int(os.getenv('MQTT_PORT', '1883')),
|
||||
help='MQTT broker port (default: 1883, env: MQTT_PORT)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--mqtt-username',
|
||||
default=os.getenv('MQTT_USERNAME', None),
|
||||
help='MQTT username (env: MQTT_USERNAME)'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--mqtt-password',
|
||||
default=os.getenv('MQTT_PASSWORD', None),
|
||||
help='MQTT password (env: MQTT_PASSWORD)'
|
||||
)
|
||||
```
|
||||
|
||||
**工厂函数:**
|
||||
|
||||
```python
|
||||
# In base/pubsub.py or base/pubsub_factory.py
|
||||
def get_pubsub(**config) -> PubSubBackend:
|
||||
"""
|
||||
Create and return a pub/sub backend based on configuration.
|
||||
|
||||
Args:
|
||||
config: Configuration dict from command-line args
|
||||
Must include 'pubsub_backend' key
|
||||
|
||||
Returns:
|
||||
Backend instance (PulsarBackend, MQTTBackend, etc.)
|
||||
"""
|
||||
backend_type = config.get('pubsub_backend', 'pulsar')
|
||||
|
||||
if backend_type == 'pulsar':
|
||||
return PulsarBackend(
|
||||
host=config.get('pulsar_host'),
|
||||
api_key=config.get('pulsar_api_key'),
|
||||
listener=config.get('pulsar_listener'),
|
||||
)
|
||||
elif backend_type == 'mqtt':
|
||||
return MQTTBackend(
|
||||
host=config.get('mqtt_host'),
|
||||
port=config.get('mqtt_port'),
|
||||
username=config.get('mqtt_username'),
|
||||
password=config.get('mqtt_password'),
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unknown pub/sub backend: {backend_type}")
|
||||
```
|
||||
|
||||
**在 AsyncProcessor 中的用法:**
|
||||
|
||||
```python
|
||||
# In async_processor.py
|
||||
class AsyncProcessor:
|
||||
def __init__(self, **params):
|
||||
self.id = params.get("id")
|
||||
|
||||
# Create backend from config (replaces PulsarClient)
|
||||
self.pubsub = get_pubsub(**params)
|
||||
|
||||
# Rest of initialization...
|
||||
```
|
||||
|
||||
### 后端接口
|
||||
|
||||
```python
|
||||
class PubSubBackend(Protocol):
|
||||
"""Protocol defining the interface all pub/sub backends must implement."""
|
||||
|
||||
def create_producer(self, topic: str, schema: type, **options) -> BackendProducer:
|
||||
"""
|
||||
Create a producer for a topic.
|
||||
|
||||
Args:
|
||||
topic: Generic topic format (qos/tenant/namespace/queue)
|
||||
schema: Dataclass type for messages
|
||||
options: Backend-specific options (e.g., chunking_enabled)
|
||||
|
||||
Returns:
|
||||
Backend-specific producer instance
|
||||
"""
|
||||
...
|
||||
|
||||
def create_consumer(
|
||||
self,
|
||||
topic: str,
|
||||
subscription: str,
|
||||
schema: type,
|
||||
initial_position: str = 'latest',
|
||||
consumer_type: str = 'shared',
|
||||
**options
|
||||
) -> BackendConsumer:
|
||||
"""
|
||||
Create a consumer for a topic.
|
||||
|
||||
Args:
|
||||
topic: Generic topic format (qos/tenant/namespace/queue)
|
||||
subscription: Subscription/consumer group name
|
||||
schema: Dataclass type for messages
|
||||
initial_position: 'earliest' or 'latest' (MQTT may ignore)
|
||||
consumer_type: 'shared', 'exclusive', 'failover' (MQTT may ignore)
|
||||
options: Backend-specific options
|
||||
|
||||
Returns:
|
||||
Backend-specific consumer instance
|
||||
"""
|
||||
...
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the backend connection."""
|
||||
...
|
||||
```
|
||||
|
||||
```python
|
||||
class BackendProducer(Protocol):
|
||||
"""Protocol for backend-specific producer."""
|
||||
|
||||
def send(self, message: Any, properties: dict = {}) -> None:
|
||||
"""Send a message (dataclass instance) with optional properties."""
|
||||
...
|
||||
|
||||
def flush(self) -> None:
|
||||
"""Flush any buffered messages."""
|
||||
...
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the producer."""
|
||||
...
|
||||
```
|
||||
|
||||
```python
|
||||
class BackendConsumer(Protocol):
|
||||
"""Protocol for backend-specific consumer."""
|
||||
|
||||
def receive(self, timeout_millis: int = 2000) -> Message:
|
||||
"""
|
||||
Receive a message from the topic.
|
||||
|
||||
Raises:
|
||||
TimeoutError: If no message received within timeout
|
||||
"""
|
||||
...
|
||||
|
||||
def acknowledge(self, message: Message) -> None:
|
||||
"""Acknowledge successful processing of a message."""
|
||||
...
|
||||
|
||||
def negative_acknowledge(self, message: Message) -> None:
|
||||
"""Negative acknowledge - triggers redelivery."""
|
||||
...
|
||||
|
||||
def unsubscribe(self) -> None:
|
||||
"""Unsubscribe from the topic."""
|
||||
...
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the consumer."""
|
||||
...
|
||||
```
|
||||
|
||||
```python
|
||||
class Message(Protocol):
|
||||
"""Protocol for a received message."""
|
||||
|
||||
def value(self) -> Any:
|
||||
"""Get the deserialized message (dataclass instance)."""
|
||||
...
|
||||
|
||||
def properties(self) -> dict:
|
||||
"""Get message properties/metadata."""
|
||||
...
|
||||
```
|
||||
|
||||
### 现有类的重构
|
||||
|
||||
现有的 `Consumer`、`Producer`、`Publisher`、`Subscriber` 类在很大程度上保持不变:
|
||||
|
||||
**当前职责(保留):**
|
||||
异步线程模型和任务组
|
||||
重连逻辑和重试处理
|
||||
指标收集
|
||||
速率限制
|
||||
并发管理
|
||||
|
||||
**需要更改的内容:**
|
||||
移除直接的 Pulsar 导入(`pulsar.schema`、`pulsar.InitialPosition` 等)
|
||||
接受 `BackendProducer`/`BackendConsumer` 而不是 Pulsar 客户端
|
||||
将实际的发布/订阅操作委托给后端实例
|
||||
将通用概念映射到后端调用
|
||||
|
||||
**重构示例:**
|
||||
|
||||
```python
|
||||
# OLD - consumer.py
|
||||
class Consumer:
|
||||
def __init__(self, client, topic, subscriber, schema, ...):
|
||||
self.client = client # Direct Pulsar client
|
||||
# ...
|
||||
|
||||
async def consumer_run(self):
|
||||
# Uses pulsar.InitialPosition, pulsar.ConsumerType
|
||||
self.consumer = self.client.subscribe(
|
||||
topic=self.topic,
|
||||
schema=JsonSchema(self.schema),
|
||||
initial_position=pulsar.InitialPosition.Earliest,
|
||||
consumer_type=pulsar.ConsumerType.Shared,
|
||||
)
|
||||
|
||||
# NEW - consumer.py
|
||||
class Consumer:
|
||||
def __init__(self, backend_consumer, schema, ...):
|
||||
self.backend_consumer = backend_consumer # Backend-specific consumer
|
||||
self.schema = schema
|
||||
# ...
|
||||
|
||||
async def consumer_run(self):
|
||||
# Backend consumer already created with right settings
|
||||
# Just use it directly
|
||||
while self.running:
|
||||
msg = await asyncio.to_thread(
|
||||
self.backend_consumer.receive,
|
||||
timeout_millis=2000
|
||||
)
|
||||
await self.handle_message(msg)
|
||||
```
|
||||
|
||||
### 后端特定行为
|
||||
|
||||
**Pulsar 后端:**
|
||||
将 `q0` 映射到 `non-persistent://`,将 `q1`/`q2` 映射到 `persistent://`
|
||||
支持所有类型的消费者(共享、独占、故障转移)
|
||||
支持初始位置(最早/最新)
|
||||
原生消息确认
|
||||
模式注册支持
|
||||
|
||||
**MQTT 后端:**
|
||||
将 `q0`/`q1`/`q2` 映射到 MQTT QoS 等级 0/1/2
|
||||
在主题路径中包含租户/命名空间,以进行命名空间划分
|
||||
根据订阅名称自动生成客户端 ID
|
||||
忽略初始位置(基本的 MQTT 中没有消息历史)
|
||||
忽略消费者类型(MQTT 使用客户端 ID,而不是消费者组)
|
||||
简单的发布/订阅模型
|
||||
|
||||
### 设计决策摘要
|
||||
|
||||
1. ✅ **通用队列命名:** `qos/tenant/namespace/queue-name` 格式
|
||||
2. ✅ **队列 ID 中的 QoS:** 由队列定义确定,而不是配置
|
||||
3. ✅ **重连:** 由 Consumer/Producer 类处理,而不是后端
|
||||
4. ✅ **MQTT 主题:** 包含租户/命名空间以进行正确的命名空间划分
|
||||
5. ✅ **消息历史:** MQTT 忽略 `initial_position` 参数(未来增强)
|
||||
6. ✅ **客户端 ID:** MQTT 后端根据订阅名称自动生成
|
||||
|
||||
### 未来增强
|
||||
|
||||
**MQTT 消息历史:**
|
||||
可以添加可选的持久层(例如,保留的消息、外部存储)
|
||||
将允许支持 `initial_position='earliest'`
|
||||
不适用于初始实现
|
||||
1516
docs/tech-specs/zh-cn/python-api-refactor.zh-cn.md
Normal file
1516
docs/tech-specs/zh-cn/python-api-refactor.zh-cn.md
Normal file
File diff suppressed because it is too large
Load diff
233
docs/tech-specs/zh-cn/query-time-explainability.zh-cn.md
Normal file
233
docs/tech-specs/zh-cn/query-time-explainability.zh-cn.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
---
|
||||
layout: default
|
||||
title: "查询时可解释性"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 查询时可解释性
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 状态
|
||||
|
||||
已实现
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了 GraphRAG 如何记录和在查询执行期间传递可解释性数据。目标是实现完全的可追溯性:从最终答案,再到选择的边,最后到源文档。
|
||||
|
||||
查询时可解释性捕获了 GraphRAG 管道在推理过程中的行为。它与提取时的来源信息相关联,该信息记录了知识图谱事实的来源。
|
||||
|
||||
## 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
|---|---|
|
||||
| **可解释性** | 结果的推导方式 |
|
||||
| **会话** | 单个 GraphRAG 查询执行 |
|
||||
| **边选择** | 使用 LLM 进行相关边的选择,并提供推理 |
|
||||
| **来源链** | 从边 → 块 → 页面 → 文档 |
|
||||
|
||||
## 架构
|
||||
|
||||
### 可解释性流程
|
||||
|
||||
```
|
||||
GraphRAG 查询
|
||||
│
|
||||
├─► 会话活动
|
||||
│ └─► 查询文本,时间戳
|
||||
│
|
||||
├─► 检索实体
|
||||
│ └─► 从子图检索的所有边
|
||||
│
|
||||
├─► 选择实体
|
||||
│ └─► 使用 LLM 推理选择的边
|
||||
│ └─► 每条边都与提取来源关联
|
||||
│
|
||||
└─► 答案实体
|
||||
└─► 指向合成响应 (在库员中)
|
||||
```
|
||||
|
||||
### 两阶段 GraphRAG 管道
|
||||
|
||||
1. **边选择**:LLM 从子图中选择相关的边,并提供每个边的推理
|
||||
2. **合成**:LLM 从选择的边生成答案
|
||||
|
||||
这种分离实现了可解释性:我们知道哪些边贡献了结果。
|
||||
|
||||
### 存储
|
||||
|
||||
- 可解释性三元组存储在可配置的集合中 (默认:`explainability`)
|
||||
- 使用 PROV-O 语义网进行来源关系
|
||||
- 使用 RDF-star 重新表达进行边引用
|
||||
- 答案内容存储在库员服务中 (不在内联位置 - 过于庞大)
|
||||
|
||||
### 实时流
|
||||
|
||||
可解释性事件在查询执行时流式传输到客户端:
|
||||
|
||||
1. 创建会话 → 发出事件
|
||||
2. 检索边 → 发出事件
|
||||
3. 使用推理选择边 → 发出事件
|
||||
4. 答案合成 → 发出事件
|
||||
|
||||
客户端接收 `explain_id` 和 `explain_collection` 以获取完整详细信息。
|
||||
|
||||
## URI 结构
|
||||
|
||||
所有 URI 使用 `urn:trustgraph:` 命名空间和 UUID:
|
||||
|
||||
| 实体 | URI 模式 |
|
||||
|---|---|
|
||||
| 会话 | `urn:trustgraph:session:{uuid}` |
|
||||
| 检索 | `urn:trustgraph:prov:retrieval:{uuid}` |
|
||||
| 选择 | `urn:trustgraph:prov:selection:{uuid}` |
|
||||
| 答案 | `urn:trustgraph:prov:answer:{uuid}` |
|
||||
| 边选择 | `urn:trustgraph:prov:edge:{uuid}:{index}` |
|
||||
|
||||
## RDF 模型 (PROV-O)
|
||||
|
||||
### 会话活动
|
||||
|
||||
```turtle
|
||||
<session-uri> a prov:Activity ;
|
||||
rdfs:label "GraphRAG 查询会话" ;
|
||||
prov:startedAtTime "2024-01-15T10:30:00Z" ;
|
||||
tg:query "What was the War on Terror?" .
|
||||
```
|
||||
|
||||
### 检索实体
|
||||
|
||||
```turtle
|
||||
<retrieval-uri> a prov:Entity ;
|
||||
rdfs:label "检索的边" ;
|
||||
prov:wasGeneratedBy <session-uri> ;
|
||||
tg:edgeCount 50 .
|
||||
```
|
||||
|
||||
### 选择实体
|
||||
|
||||
```turtle
|
||||
<selection-uri> a prov:Entity ;
|
||||
rdfs:label "选择的边" ;
|
||||
prov:wasDerivedFrom <retrieval-uri> ;
|
||||
tg:selectedEdge <edge-sel-0> ;
|
||||
tg:selectedEdge <edge-sel-1> .
|
||||
|
||||
<edge-sel-0> tg:edge << <s> <p> <o> >> ;
|
||||
tg:reasoning "This edge establishes the key relationship..." .
|
||||
```
|
||||
|
||||
### 答案实体
|
||||
|
||||
```turtle
|
||||
<answer-uri> a prov:Entity ;
|
||||
rdfs:label "GraphRAG 答案" ;
|
||||
prov:wasDerivedFrom <selection-uri> ;
|
||||
tg:document <urn:trustgraph:answer:{uuid}> .
|
||||
```
|
||||
|
||||
`tg:document` 引用库员服务中存储的答案。
|
||||
|
||||
## 名称空间常量
|
||||
|
||||
定义在 `trustgraph-base/trustgraph/provenance/namespaces.py`:
|
||||
|
||||
| 常量 | URI |
|
||||
|---|---|
|
||||
| `TG_QUERY` | `https://trustgraph.ai/ns/query` |
|
||||
| `TG_EDGE_COUNT` | `https://trustgraph.ai/ns/edgeCount` |
|
||||
| `TG_SELECTED_EDGE` | `https://trustgraph.ai/ns/selectedEdge` |
|
||||
| `TG_EDGE` | `https://trustgraph.ai/ns/edge` |
|
||||
| `TG_REASONING` | `https://trustgraph.ai/ns/reasoning` |
|
||||
| `TG_CONTENT` | `https://trustgraph.ai/ns/content` |
|
||||
| `TG_DOCUMENT` | `https://trustgraph.ai/ns/document` |
|
||||
|
||||
## GraphRagResponse 模式
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class GraphRagResponse:
|
||||
error: None | None = None
|
||||
response: str = ""
|
||||
end_of_stream: bool = False
|
||||
explain_id: str | None = None
|
||||
explain_collection: str | None = None
|
||||
message_type: str = "" # "chunk" 或 "explain"
|
||||
end_of_session: bool = False
|
||||
```
|
||||
|
||||
### 消息类型
|
||||
|
||||
| message_type | 目的 |
|
||||
|---|---|
|
||||
| `chunk` | 响应文本 (流式或最终) |
|
||||
| `explain` | 可解释性事件,包含 IRI 引用 |
|
||||
|
||||
### 会话生命周期
|
||||
|
||||
1. 多个 `explain` 消息 (会话、检索、选择、答案)
|
||||
2. 多个 `chunk` 消息 (流式响应)
|
||||
3. 最终的 `chunk`,`end_of_session=True`
|
||||
|
||||
## 边选择格式
|
||||
|
||||
LLM 返回 JSONL 格式的边:
|
||||
|
||||
```jsonl
|
||||
{"id": "edge-hash-1", "reasoning": "This edge shows the key relationship..."}
|
||||
{"id": "edge-hash-2", "reasoning": "Provides supporting evidence..."}
|
||||
```
|
||||
|
||||
`id` 是使用 `edge_id()` 计算的 `(labeled_s, labeled_p, labeled_o)` 的哈希值。
|
||||
|
||||
## URI 保留
|
||||
|
||||
### 问题
|
||||
|
||||
GraphRAG 向 LLM 显示了人类可读的标签,但为了可追溯性,需要原始 URI。
|
||||
|
||||
### 解决方案
|
||||
|
||||
`get_labelgraph()` 返回:
|
||||
- `labeled_edges`: 包含 `(label_s, label_p, label_o)` 的列表,供 LLM 使用
|
||||
- `uri_map`: 将 `edge_id(labels)` 映射到 `(uri_s, uri_p, uri_o)` 的字典
|
||||
|
||||
在存储可解释性数据时,使用 `uri_map` 中的 URI。
|
||||
|
||||
## 来源追踪
|
||||
|
||||
### 从边到源
|
||||
|
||||
可以追踪选择的边:
|
||||
|
||||
1. 查询包含的子图:`?subgraph tg:contains <<s p o>>`
|
||||
2. 遵循 `prov:wasDerivedFrom` 链,找到根文档
|
||||
3. 每个步骤中的链:块 → 页面 → 文档
|
||||
|
||||
### 支持 Cassandra 引用
|
||||
|
||||
Cassandra 查询服务支持引用:
|
||||
|
||||
```python
|
||||
# 在 get_term_value() 中:
|
||||
elif term.type == TRIPLE:
|
||||
return serialize_triple(term.triple)
|
||||
```
|
||||
|
||||
这允许查询:
|
||||
```
|
||||
?subgraph tg:contains <<http://example.org/s http://example.org/p "value">>
|
||||
```
|
||||
|
||||
## CLI 使用
|
||||
|
||||
```bash
|
||||
tg-invoke-graph-rag --explainable -q "What was the War on Terror?"
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
- PROV-O (W3C Provenance Ontology): https://www.w3.org/TR/prov-o/
|
||||
- RDF-star: https://w3c.github.io/rdf-star/
|
||||
- 提取时的来源信息: `docs/tech-specs/extraction-time-provenance.md`
|
||||
199
docs/tech-specs/zh-cn/rag-streaming-support.zh-cn.md
Normal file
199
docs/tech-specs/zh-cn/rag-streaming-support.zh-cn.md
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
---
|
||||
layout: default
|
||||
title: "RAG 实时支持技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# RAG 实时支持技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了为 GraphRAG 和 DocumentRAG 服务添加实时支持的方法,从而实现知识图谱和文档检索查询的实时分块响应。 这扩展了现有的实时架构,该架构已针对 LLM 文本补全、提示和代理服务实施。
|
||||
|
||||
## 目标
|
||||
|
||||
- **一致的实时体验**: 在所有 TrustGraph 服务中提供相同的时间体验
|
||||
- **最小的 API 更改**: 通过单个 `streaming` 标志添加实时支持,遵循已建立的模式
|
||||
- **保持现有兼容性**: 保持现有非实时行为作为默认
|
||||
- **重用现有基础设施**: 利用 PromptClient 现有的实时功能
|
||||
- **网关支持**: 通过 websocket 网关为客户端应用程序启用实时
|
||||
|
||||
## 背景
|
||||
|
||||
当前已实现的实时服务:
|
||||
- **LLM 文本补全服务**: 第一阶段 - 从 LLM 提供商获取
|
||||
- **提示服务**: 第二阶段 - 通过提示模板进行流式传输
|
||||
- **代理服务**: 第三阶段 - 使用 ReAct 响应和分块的思考/观察/答案进行流式传输
|
||||
|
||||
RAG 服务的当前限制:
|
||||
- GraphRAG 和 DocumentRAG 仅支持块响应
|
||||
- 用户必须等待 LLM 响应完成才能看到任何输出
|
||||
- 针对知识图谱或文档查询的较长响应给用户体验带来不便
|
||||
- 与其他 TrustGraph 服务相比,体验不一致
|
||||
|
||||
本规范通过为 GraphRAG 和 DocumentRAG 添加实时支持来解决这些问题。 通过实现分块响应,TrustGraph 可以:
|
||||
- 在所有查询类型中提供一致的实时体验
|
||||
- 减少 RAG 查询的感知延迟
|
||||
- 允许长时间运行查询的更好进度反馈
|
||||
- 客户端应用程序支持实时显示
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 架构
|
||||
|
||||
RAG 实时实现利用了现有的基础设施:
|
||||
|
||||
1. **PromptClient 实时** (已实现)
|
||||
- `kg_prompt()` 和 `document_prompt()` 已经接受 `streaming` 和 `chunk_callback` 参数
|
||||
- 这些调用 `prompt()` 内部,并启用实时功能
|
||||
- PromptClient 不需要任何更改
|
||||
|
||||
模块: `trustgraph-base/trustgraph/base/prompt_client.py`
|
||||
|
||||
2. **GraphRAG 服务** (需要传递实时参数)
|
||||
- 将 `streaming` 参数添加到 `query()` 方法
|
||||
- 将实时标志和回调函数传递给 `prompt_client.kg_prompt()`
|
||||
- GraphRagRequest 模式需要包含 `streaming` 字段
|
||||
|
||||
模块:
|
||||
- `trustgraph-flow/trustgraph/retrieval/graph_rag/graph_rag.py`
|
||||
- `trustgraph-flow/trustgraph/retrieval/graph_rag/rag.py` (处理器)
|
||||
- `trustgraph-base/trustgraph/schema/graph_rag.py` (Request 模式)
|
||||
- `trustgraph-flow/trustgraph/gateway/dispatch/graph_rag.py` (网关)
|
||||
|
||||
3. **DocumentRAG 服务** (需要传递实时参数)
|
||||
- 将 `streaming` 参数添加到 `query()` 方法
|
||||
- 将实时标志和回调函数传递给 `prompt_client.document_prompt()`
|
||||
- DocumentRagRequest 模式需要包含 `streaming` 字段
|
||||
|
||||
模块:
|
||||
- `trustgraph-flow/trustgraph/retrieval/document_rag/document_rag.py`
|
||||
- `trustgraph-flow/trustgraph/retrieval/document_rag/rag.py` (处理器)
|
||||
- `trustgraph-base/trustgraph/schema/document_rag.py` (Request 模式)
|
||||
- `trustgraph-flow/trustgraph/gateway/dispatch/document_rag.py` (网关)
|
||||
|
||||
### 数据流
|
||||
|
||||
**非实时 (当前)**:
|
||||
```
|
||||
Client → Gateway → RAG Service → PromptClient.kg_prompt(streaming=False)
|
||||
↓
|
||||
Prompt Service → LLM
|
||||
↓
|
||||
完整响应
|
||||
↓
|
||||
Client ← Gateway ← RAG Service ← Response
|
||||
```
|
||||
|
||||
**实时 (提案)**:
|
||||
```
|
||||
Client → Gateway → RAG Service → PromptClient.kg_prompt(streaming=True, chunk_callback=cb)
|
||||
↓
|
||||
Prompt Service → LLM (streaming)
|
||||
↓
|
||||
块 → 回调 → RAG 响应 (块)
|
||||
↓ ↓
|
||||
Client ← Gateway ← ────────────────────────────────── 响应流
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
**GraphRAG 更改**:
|
||||
|
||||
1. **GraphRag.query()** - 添加实时参数
|
||||
```python
|
||||
async def query(
|
||||
self, query, user, collection,
|
||||
verbose=False, streaming=False, chunk_callback=None # NEW
|
||||
):
|
||||
# ... 现有实体/三元组检索 ...
|
||||
|
||||
if streaming and chunk_callback:
|
||||
resp = await self.prompt_client.kg_prompt(
|
||||
query, kg,
|
||||
streaming=True,
|
||||
chunk_callback=chunk_callback
|
||||
)
|
||||
else:
|
||||
resp = await self.prompt_client.kg_prompt(query, kg)
|
||||
|
||||
return resp
|
||||
```
|
||||
|
||||
2. **GraphRagRequest 模式** - 添加实时字段
|
||||
```python
|
||||
class GraphRagRequest(Record):
|
||||
query = String()
|
||||
user = String()
|
||||
collection = String()
|
||||
streaming = Boolean() # NEW
|
||||
```
|
||||
|
||||
3. **GraphRagResponse 模式** - 添加实时字段 (遵循 Agent 模式)
|
||||
```python
|
||||
class GraphRagResponse(Record):
|
||||
response = String() # Legacy: complete response
|
||||
chunk = String() # NEW: streaming chunk
|
||||
end_of_stream = Boolean() # NEW: indicates last chunk
|
||||
```
|
||||
|
||||
4. **Processor** - 传递实时
|
||||
```python
|
||||
async def handle(self, msg):
|
||||
# ... 现有代码 ...
|
||||
|
||||
async def send_chunk(chunk):
|
||||
await self.respond(GraphRagResponse(
|
||||
chunk=chunk,
|
||||
end_of_stream=False,
|
||||
response=None
|
||||
))
|
||||
|
||||
if request.streaming:
|
||||
full_response = await self.rag.query(
|
||||
query=request.query,
|
||||
user=request.user,
|
||||
collection=request.collection,
|
||||
streaming=True,
|
||||
chunk_callback=send_chunk
|
||||
)
|
||||
# 发送最终消息
|
||||
await self.respond(GraphRagResponse(
|
||||
chunk=None,
|
||||
end_of_stream=True,
|
||||
response=full_response
|
||||
))
|
||||
else:
|
||||
# 现有非实时路径
|
||||
response = await self.rag.query(...)
|
||||
await self.respond(GraphRagResponse(response=response))
|
||||
```
|
||||
|
||||
**DocumentRAG 更改**:
|
||||
|
||||
与 GraphRAG 相同模式:
|
||||
1. 将 `streaming` 和 `chunk_callback` 参数添加到 `DocumentRag.query()`
|
||||
2. 将 `streaming` 字段添加到 `DocumentRagRequest`
|
||||
3. 将 `streaming` 字段添加到 `DocumentRagResponse`
|
||||
|
||||
### 时间线
|
||||
|
||||
估计实现时间:4-6 小时
|
||||
- 第一阶段 (2 小时): GraphRAG 实时支持
|
||||
- 第二阶段 (2 小时): DocumentRAG 实时支持
|
||||
- 第三阶段 (1-2 小时): 网关更新和 CLI 标志
|
||||
- 测试: 已包含在每个阶段
|
||||
|
||||
## 开放问题
|
||||
|
||||
- 是否应该为 NLP 查询服务添加实时支持?
|
||||
- 我们是否只想要实时输出中间步骤 (例如 "检索实体..."、"查询图..."),还是也想要实时输出?
|
||||
- GraphRAG/DocumentRAG 响应是否应该包含块元数据 (例如块编号、预期总数)?
|
||||
|
||||
## 参考
|
||||
|
||||
- 现有实现: `docs/tech-specs/streaming-llm-responses.md`
|
||||
- Agent 实时: `trustgraph-flow/trustgraph/agent/react/agent_manager.py`
|
||||
- PromptClient 实时: `trustgraph-base/trustgraph/base/prompt_client.py`
|
||||
99
docs/tech-specs/zh-cn/schema-refactoring-proposal.zh-cn.md
Normal file
99
docs/tech-specs/zh-cn/schema-refactoring-proposal.zh-cn.md
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
---
|
||||
layout: default
|
||||
title: "方案:Schema 目录重构"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 方案:Schema 目录重构
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 当前问题
|
||||
|
||||
1. **扁平结构** - 所有 Schema 都位于同一目录下,难以理解它们之间的关系
|
||||
2. **混杂的关注点** - 核心类型、领域对象和 API 契约都混合在一起
|
||||
3. **不明确的命名** - 文件如 "object.py", "types.py", "topic.py" 并不能清晰地表明其用途
|
||||
4. **缺乏明确的层级** - 无法轻松地看出哪些依赖于哪些
|
||||
|
||||
## 建议的结构
|
||||
|
||||
```
|
||||
trustgraph-base/trustgraph/schema/
|
||||
├── __init__.py
|
||||
├── core/ # 核心基本类型,在所有地方使用
|
||||
│ ├── __init__.py
|
||||
│ ├── primitives.py # Error, Value, Triple, Field, RowSchema
|
||||
│ ├── metadata.py # 元数据记录
|
||||
│ └── topic.py # Topic 工具
|
||||
│
|
||||
├── knowledge/ # 知识领域模型和提取
|
||||
│ ├── __init__.py
|
||||
│ ├── graph.py # EntityContext, EntityEmbeddings, Triples
|
||||
│ ├── document.py # Document, TextDocument, Chunk
|
||||
│ ├── knowledge.py # 知识提取类型
|
||||
│ ├── embeddings.py # 所有与嵌入相关的类型(从多个文件中移动)
|
||||
│ └── nlp.py # Definition, Topic, Relationship, Fact 类型
|
||||
│
|
||||
└── services/ # 服务请求/响应契约
|
||||
├── __init__.py
|
||||
├── llm.py # TextCompletion, Embeddings, Tool 请求/响应
|
||||
├── retrieval.py # GraphRAG, DocumentRAG 查询/响应
|
||||
├── query.py # GraphEmbeddingsRequest/Response, DocumentEmbeddingsRequest/Response
|
||||
├── agent.py # Agent 请求/响应
|
||||
├── flow.py # Flow 请求/响应
|
||||
├── prompt.py # Prompt 服务请求/响应
|
||||
├── config.py # 配置服务
|
||||
├── library.py # 库服务
|
||||
└── lookup.py # 查找服务
|
||||
```
|
||||
|
||||
## 关键变更
|
||||
|
||||
1. **分层组织** - 清晰地将核心类型、知识模型和服务契约分开
|
||||
2. **更好的命名**:
|
||||
- `types.py` → `core/primitives.py` (更清晰的用途)
|
||||
- `object.py` → 根据实际内容将文件拆分
|
||||
- `documents.py` → `knowledge/document.py` (单数,一致)
|
||||
- `models.py` → `services/llm.py` (更清晰地表明模型类型)
|
||||
- `prompt.py` → 拆分:服务部分到 `services/prompt.py`,数据类型到 `knowledge/nlp.py`
|
||||
|
||||
3. **逻辑分组**:
|
||||
- 所有嵌入类型集中在 `knowledge/embeddings.py`
|
||||
- 所有与 LLM 相关的服务契约在 `services/llm.py`
|
||||
- 在 services 目录中明确区分请求/响应对
|
||||
- 知识提取类型与其它知识领域模型分组
|
||||
|
||||
4. **依赖关系清晰**:
|
||||
- 核心类型没有依赖
|
||||
- 知识模型仅依赖核心
|
||||
- 服务契约可以依赖核心和知识模型
|
||||
|
||||
## 迁移的好处
|
||||
|
||||
1. **更轻松的导航** - 开发者可以快速找到所需的内容
|
||||
2. **更好的模块化** - 区分不同关注点更清晰
|
||||
3. **更简单的导入** - 更有意义的导入路径
|
||||
4. **更具未来性** - 轻松添加新的知识类型或服务,而无需增加混乱
|
||||
|
||||
## 示例导入变更
|
||||
|
||||
```python
|
||||
# 之前
|
||||
from trustgraph.schema import Error, Triple, GraphEmbeddings, TextCompletionRequest
|
||||
|
||||
# 之后
|
||||
from trustgraph.schema.core import Error, Triple
|
||||
from trustgraph.schema.knowledge import GraphEmbeddings
|
||||
from trustgraph.schema.services import TextCompletionRequest
|
||||
```
|
||||
|
||||
## 实施说明
|
||||
|
||||
1. 通过在根的 `__init__.py` 中保持导入,保持与以前的兼容性
|
||||
2. 逐步移动文件,并在需要时更新导入
|
||||
3. 考虑添加一个 `legacy.py`,用于在过渡期间导入所有内容
|
||||
4. 更新文档以反映新的结构
|
||||
|
||||
<function_calls>
|
||||
<invoke name="TodoWrite">
|
||||
<parameter name="todos">[{"id": "1", "content": "检查当前 Schema 目录结构", "status": "completed", "priority": "high"}, {"id": "2", "content": "分析 Schema 文件及其用途", "status": "completed", "priority": "high"}, {"id": "3", "content": "提出改进的命名和结构", "status": "completed", "priority": "high"}]
|
||||
578
docs/tech-specs/zh-cn/streaming-llm-responses.zh-cn.md
Normal file
578
docs/tech-specs/zh-cn/streaming-llm-responses.zh-cn.md
Normal file
|
|
@ -0,0 +1,578 @@
|
|||
---
|
||||
layout: default
|
||||
title: "流式LLM响应技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 流式LLM响应技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了在TrustGraph中实现LLM响应的流式支持。流式传输允许实时交付由LLM生成的token,而不是等待完整的响应生成。
|
||||
流式传输允许实时交付由LLM生成的token,而不是等待完整的响应生成。
|
||||
流式传输允许实时交付由LLM生成的token,而不是等待完整的响应生成。
|
||||
流式传输允许实时交付由LLM生成的token,而不是等待完整的响应生成。
|
||||
|
||||
此实现支持以下用例:
|
||||
|
||||
1. **实时用户界面**: 将生成的令牌流式传输到用户界面,
|
||||
从而提供即时的视觉反馈。
|
||||
2. **减少首次令牌时间**: 用户立即看到输出,
|
||||
而不是等待完整生成。
|
||||
3. **处理长响应**: 处理可能导致超时或超出内存限制的非常长的输出。
|
||||
4. **交互式应用程序**: 启用响应迅速的聊天和代理界面。
|
||||
|
||||
|
||||
## 目标
|
||||
|
||||
**向后兼容性:** 现有的非流式客户端继续工作
|
||||
无需修改。
|
||||
**一致的 API 设计:** 流式和非流式使用相同的模式,差异最小。
|
||||
|
||||
**提供商的灵活性:** 在可用时支持流式传输,在不可用时提供优雅的
|
||||
回退方案。
|
||||
**分阶段推出:** 逐步实施以降低风险。
|
||||
**端到端支持:** 从 LLM 提供商到客户端
|
||||
应用程序,通过 Pulsar、Gateway API 和 Python API 提供支持。
|
||||
|
||||
## 背景
|
||||
|
||||
### 当前架构
|
||||
|
||||
当前 LLM 文本补全流程如下:
|
||||
|
||||
1. 客户端发送 `TextCompletionRequest`,包含 `system` 和 `prompt` 字段。
|
||||
2. LLM 服务处理请求并等待完整生成。
|
||||
3. 返回单个 `TextCompletionResponse`,包含完整的 `response` 字符串。
|
||||
|
||||
当前模式 (`trustgraph-base/trustgraph/schema/services/llm.py`):
|
||||
|
||||
```python
|
||||
class TextCompletionRequest(Record):
|
||||
system = String()
|
||||
prompt = String()
|
||||
|
||||
class TextCompletionResponse(Record):
|
||||
error = Error()
|
||||
response = String()
|
||||
in_token = Integer()
|
||||
out_token = Integer()
|
||||
model = String()
|
||||
```
|
||||
|
||||
### 当前限制
|
||||
|
||||
**延迟:** 用户必须等待完整生成后才能看到任何输出。
|
||||
**超时风险:** 较长的生成过程可能超出客户端超时阈值。
|
||||
**糟糕的用户体验:** 在生成过程中没有反馈,会让人产生缓慢的感觉。
|
||||
**资源使用:** 完整的响应必须缓存在内存中。
|
||||
|
||||
本规范通过启用增量响应来解决这些限制,同时保持完全的向后兼容性。
|
||||
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 第一阶段:基础设施
|
||||
|
||||
第一阶段通过修改模式、API 和 CLI 工具,为流式传输奠定基础。
|
||||
|
||||
|
||||
#### 模式更改
|
||||
|
||||
##### LLM 模式 (`trustgraph-base/trustgraph/schema/services/llm.py`)
|
||||
|
||||
**请求更改:**
|
||||
|
||||
```python
|
||||
class TextCompletionRequest(Record):
|
||||
system = String()
|
||||
prompt = String()
|
||||
streaming = Boolean() # NEW: Default false for backward compatibility
|
||||
```
|
||||
|
||||
`streaming`:当 `true` 时,请求流式响应传输。
|
||||
默认:`false`(保留现有行为)。
|
||||
|
||||
**响应变更:**
|
||||
|
||||
```python
|
||||
class TextCompletionResponse(Record):
|
||||
error = Error()
|
||||
response = String()
|
||||
in_token = Integer()
|
||||
out_token = Integer()
|
||||
model = String()
|
||||
end_of_stream = Boolean() # NEW: Indicates final message
|
||||
```
|
||||
|
||||
`end_of_stream`:当 `true` 时,表示这是最终(或唯一的)响应。
|
||||
对于非流式请求:单个响应,包含 `end_of_stream=true`。
|
||||
对于流式请求:多个响应,全部包含 `end_of_stream=false`。
|
||||
除了最后一个响应。
|
||||
|
||||
##### 提示模式 (`trustgraph-base/trustgraph/schema/services/prompt.py`)
|
||||
|
||||
提示服务包装了文本补全功能,因此它遵循相同的模式:
|
||||
|
||||
**请求更改:**
|
||||
|
||||
```python
|
||||
class PromptRequest(Record):
|
||||
id = String()
|
||||
terms = Map(String())
|
||||
streaming = Boolean() # NEW: Default false
|
||||
```
|
||||
|
||||
**响应变更:**
|
||||
|
||||
```python
|
||||
class PromptResponse(Record):
|
||||
error = Error()
|
||||
text = String()
|
||||
object = String()
|
||||
end_of_stream = Boolean() # NEW: Indicates final message
|
||||
```
|
||||
|
||||
#### 网关 API 变更
|
||||
|
||||
网关 API 必须向 HTTP/WebSocket 客户端暴露流式传输功能。
|
||||
|
||||
**REST API 更新:**
|
||||
|
||||
`POST /api/v1/text-completion`: 接受请求体中的 `streaming` 参数
|
||||
响应行为取决于流式传输标志:
|
||||
`streaming=false`: 单个 JSON 响应(当前行为)
|
||||
`streaming=true`: 服务器发送事件 (SSE) 流或 WebSocket 消息
|
||||
|
||||
**响应格式(流式传输):**
|
||||
|
||||
每个流式传输的数据块都遵循相同的模式结构:
|
||||
```json
|
||||
{
|
||||
"response": "partial text...",
|
||||
"end_of_stream": false,
|
||||
"model": "model-name"
|
||||
}
|
||||
```
|
||||
|
||||
最终部分:
|
||||
```json
|
||||
{
|
||||
"response": "final text chunk",
|
||||
"end_of_stream": true,
|
||||
"in_token": 150,
|
||||
"out_token": 500,
|
||||
"model": "model-name"
|
||||
}
|
||||
```
|
||||
|
||||
#### Python API 变更
|
||||
|
||||
Python 客户端 API 必须同时支持流式和非流式模式,
|
||||
同时保持向后兼容性。
|
||||
|
||||
**LlmClient 更新** (`trustgraph-base/trustgraph/clients/llm_client.py`):
|
||||
|
||||
```python
|
||||
class LlmClient(BaseClient):
|
||||
def request(self, system, prompt, timeout=300, streaming=False):
|
||||
"""
|
||||
Non-streaming request (backward compatible).
|
||||
Returns complete response string.
|
||||
"""
|
||||
# Existing behavior when streaming=False
|
||||
|
||||
async def request_stream(self, system, prompt, timeout=300):
|
||||
"""
|
||||
Streaming request.
|
||||
Yields response chunks as they arrive.
|
||||
"""
|
||||
# New async generator method
|
||||
```
|
||||
|
||||
**PromptClient 更新** (`trustgraph-base/trustgraph/base/prompt_client.py`):
|
||||
|
||||
类似于使用 `streaming` 参数和异步生成器的变体。
|
||||
|
||||
#### CLI 工具更改
|
||||
|
||||
**tg-invoke-llm** (`trustgraph-cli/trustgraph/cli/invoke_llm.py`):
|
||||
|
||||
```
|
||||
tg-invoke-llm [system] [prompt] [--no-streaming] [-u URL] [-f flow-id]
|
||||
```
|
||||
|
||||
默认启用流式传输,以获得更好的交互式用户体验。
|
||||
`--no-streaming` 标志禁用流式传输。
|
||||
当启用流式传输时:将生成的 token 输出到标准输出,并在它们到达时立即输出。
|
||||
当禁用流式传输时:等待完整的响应,然后输出。
|
||||
|
||||
**tg-invoke-prompt** (`trustgraph-cli/trustgraph/cli/invoke_prompt.py`):
|
||||
|
||||
```
|
||||
tg-invoke-prompt [template-id] [var=value...] [--no-streaming] [-u URL] [-f flow-id]
|
||||
```
|
||||
|
||||
与 `tg-invoke-llm` 相同的模式。
|
||||
|
||||
#### LLM 服务基础类更改
|
||||
|
||||
**LlmService** (`trustgraph-base/trustgraph/base/llm_service.py`):
|
||||
|
||||
```python
|
||||
class LlmService(FlowProcessor):
|
||||
async def on_request(self, msg, consumer, flow):
|
||||
request = msg.value()
|
||||
streaming = getattr(request, 'streaming', False)
|
||||
|
||||
if streaming and self.supports_streaming():
|
||||
async for chunk in self.generate_content_stream(...):
|
||||
await self.send_response(chunk, end_of_stream=False)
|
||||
await self.send_response(final_chunk, end_of_stream=True)
|
||||
else:
|
||||
response = await self.generate_content(...)
|
||||
await self.send_response(response, end_of_stream=True)
|
||||
|
||||
def supports_streaming(self):
|
||||
"""Override in subclass to indicate streaming support."""
|
||||
return False
|
||||
|
||||
async def generate_content_stream(self, system, prompt, model, temperature):
|
||||
"""Override in subclass to implement streaming."""
|
||||
raise NotImplementedError()
|
||||
```
|
||||
|
||||
--
|
||||
|
||||
### 第二阶段:VertexAI 概念验证
|
||||
|
||||
第二阶段在单个提供商(VertexAI)中实现流式传输,以验证
|
||||
基础设施并实现端到端测试。
|
||||
|
||||
#### VertexAI 实施
|
||||
|
||||
**模块:** `trustgraph-vertexai/trustgraph/model/text_completion/vertexai/llm.py`
|
||||
|
||||
**变更:**
|
||||
|
||||
1. 覆盖 `supports_streaming()` 以返回 `True`
|
||||
2. 实现 `generate_content_stream()` 异步生成器
|
||||
3. 处理 Gemini 和 Claude 模型(通过 VertexAI Anthropic API)
|
||||
|
||||
**Gemini 流式传输:**
|
||||
|
||||
```python
|
||||
async def generate_content_stream(self, system, prompt, model, temperature):
|
||||
model_instance = self.get_model(model, temperature)
|
||||
response = model_instance.generate_content(
|
||||
[system, prompt],
|
||||
stream=True # Enable streaming
|
||||
)
|
||||
for chunk in response:
|
||||
yield LlmChunk(
|
||||
text=chunk.text,
|
||||
in_token=None, # Available only in final chunk
|
||||
out_token=None,
|
||||
)
|
||||
# Final chunk includes token counts from response.usage_metadata
|
||||
```
|
||||
|
||||
**Claude (通过 VertexAI Anthropic) 流式传输:**
|
||||
|
||||
```python
|
||||
async def generate_content_stream(self, system, prompt, model, temperature):
|
||||
with self.anthropic_client.messages.stream(...) as stream:
|
||||
for text in stream.text_stream:
|
||||
yield LlmChunk(text=text)
|
||||
# Token counts from stream.get_final_message()
|
||||
```
|
||||
|
||||
#### 测试
|
||||
|
||||
流式响应组装的单元测试
|
||||
与 VertexAI (Gemini 和 Claude) 的集成测试
|
||||
端到端测试:CLI -> 网关 -> Pulsar -> VertexAI -> 回调
|
||||
向后兼容性测试:非流式请求仍然有效
|
||||
|
||||
--
|
||||
|
||||
### 第三阶段:所有 LLM 提供商
|
||||
|
||||
第三阶段将流式支持扩展到系统中的所有 LLM 提供商。
|
||||
|
||||
#### 提供商实施状态
|
||||
|
||||
每个提供商必须:
|
||||
1. **完全流式支持**: 实施 `generate_content_stream()`
|
||||
2. **兼容模式**: 正确处理 `end_of_stream` 标志
|
||||
(返回单个响应,包含 `end_of_stream=true`)
|
||||
|
||||
| 提供商 | 包 | 流式支持 |
|
||||
|----------|---------|-------------------|
|
||||
| OpenAI | trustgraph-flow | 完全 (原生流式 API) |
|
||||
| Claude/Anthropic | trustgraph-flow | 完全 (原生流式 API) |
|
||||
| Ollama | trustgraph-flow | 完全 (原生流式 API) |
|
||||
| Cohere | trustgraph-flow | 完全 (原生流式 API) |
|
||||
| Mistral | trustgraph-flow | 完全 (原生流式 API) |
|
||||
| Azure OpenAI | trustgraph-flow | 完全 (原生流式 API) |
|
||||
| Google AI Studio | trustgraph-flow | 完全 (原生流式 API) |
|
||||
| VertexAI | trustgraph-vertexai | 完全 (第二阶段) |
|
||||
| Bedrock | trustgraph-bedrock | 完全 (原生流式 API) |
|
||||
| LM Studio | trustgraph-flow | 完全 (与 OpenAI 兼容) |
|
||||
| LlamaFile | trustgraph-flow | 完全 (与 OpenAI 兼容) |
|
||||
| vLLM | trustgraph-flow | 完全 (与 OpenAI 兼容) |
|
||||
| TGI | trustgraph-flow | 待定 |
|
||||
| Azure | trustgraph-flow | 待定 |
|
||||
|
||||
#### 实施模式
|
||||
|
||||
对于与 OpenAI 兼容的提供商 (OpenAI, LM Studio, LlamaFile, vLLM):
|
||||
|
||||
```python
|
||||
async def generate_content_stream(self, system, prompt, model, temperature):
|
||||
response = await self.client.chat.completions.create(
|
||||
model=model,
|
||||
messages=[
|
||||
{"role": "system", "content": system},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
temperature=temperature,
|
||||
stream=True
|
||||
)
|
||||
async for chunk in response:
|
||||
if chunk.choices[0].delta.content:
|
||||
yield LlmChunk(text=chunk.choices[0].delta.content)
|
||||
```
|
||||
|
||||
--
|
||||
|
||||
### 第四阶段:代理 API
|
||||
|
||||
第四阶段将流式传输扩展到代理 API。 这更加复杂,因为
|
||||
代理 API 本身就是多消息的(思考 → 行动 → 观察
|
||||
→ 重复 → 最终答案)。
|
||||
|
||||
#### 当前代理模式
|
||||
|
||||
```python
|
||||
class AgentStep(Record):
|
||||
thought = String()
|
||||
action = String()
|
||||
arguments = Map(String())
|
||||
observation = String()
|
||||
user = String()
|
||||
|
||||
class AgentRequest(Record):
|
||||
question = String()
|
||||
state = String()
|
||||
group = Array(String())
|
||||
history = Array(AgentStep())
|
||||
user = String()
|
||||
|
||||
class AgentResponse(Record):
|
||||
answer = String()
|
||||
error = Error()
|
||||
thought = String()
|
||||
observation = String()
|
||||
```
|
||||
|
||||
#### 建议的代理模式变更
|
||||
|
||||
**请求变更:**
|
||||
|
||||
```python
|
||||
class AgentRequest(Record):
|
||||
question = String()
|
||||
state = String()
|
||||
group = Array(String())
|
||||
history = Array(AgentStep())
|
||||
user = String()
|
||||
streaming = Boolean() # NEW: Default false
|
||||
```
|
||||
|
||||
**响应变化:**
|
||||
|
||||
代理在推理过程中会产生多种类型的输出:
|
||||
想法(推理)
|
||||
动作(工具调用)
|
||||
观察结果(工具结果)
|
||||
答案(最终回复)
|
||||
错误
|
||||
|
||||
由于 `chunk_type` 标识了正在发送的内容类型,因此可以合并单独的
|
||||
`answer`、`error`、`thought` 和 `observation` 字段,
|
||||
从而将其合并到一个 `content` 字段中:
|
||||
|
||||
```python
|
||||
class AgentResponse(Record):
|
||||
chunk_type = String() # "thought", "action", "observation", "answer", "error"
|
||||
content = String() # The actual content (interpretation depends on chunk_type)
|
||||
end_of_message = Boolean() # Current thought/action/observation/answer is complete
|
||||
end_of_dialog = Boolean() # Entire agent dialog is complete
|
||||
```
|
||||
|
||||
**字段语义:**
|
||||
|
||||
`chunk_type`: 指示 `content` 字段中包含的内容类型。
|
||||
`"thought"`: 代理的推理/思考。
|
||||
`"action"`: 正在调用的工具/动作。
|
||||
`"observation"`: 来自工具执行的结果。
|
||||
`"answer"`: 对用户问题的最终答案。
|
||||
`"error"`: 错误消息。
|
||||
|
||||
`content`: 实际的流式内容,根据 `chunk_type` 进行解释。
|
||||
|
||||
`end_of_message`: 当 `true` 时,当前块类型已完成。
|
||||
示例:当前思考的所有 token 都已发送。
|
||||
允许客户端知道何时进入下一个阶段。
|
||||
|
||||
`end_of_dialog`: 当 `true` 时,整个代理交互已完成。
|
||||
这是一个流中的最终消息。
|
||||
|
||||
#### 代理流式行为
|
||||
|
||||
当 `streaming=true` 时:
|
||||
|
||||
1. **思考流式传输:**
|
||||
具有 `chunk_type="thought"`、`end_of_message=false` 的多个块。
|
||||
最终的思考块具有 `end_of_message=true`。
|
||||
2. **动作通知:**
|
||||
具有 `chunk_type="action"`、`end_of_message=true` 的单个块。
|
||||
3. **观察:**
|
||||
具有 `chunk_type="observation"` 的块(块),最终块具有 `end_of_message=true`。
|
||||
4. **重复** 1-3 步骤,直到代理完成推理。
|
||||
5. **最终答案:**
|
||||
`chunk_type="answer"` 包含最终的响应,位于 `content` 中。
|
||||
最后一个块具有 `end_of_message=true`、`end_of_dialog=true`。
|
||||
|
||||
**示例流序列:**
|
||||
|
||||
```
|
||||
{chunk_type: "thought", content: "I need to", end_of_message: false, end_of_dialog: false}
|
||||
{chunk_type: "thought", content: " search for...", end_of_message: true, end_of_dialog: false}
|
||||
{chunk_type: "action", content: "search", end_of_message: true, end_of_dialog: false}
|
||||
{chunk_type: "observation", content: "Found: ...", end_of_message: true, end_of_dialog: false}
|
||||
{chunk_type: "thought", content: "Based on this", end_of_message: false, end_of_dialog: false}
|
||||
{chunk_type: "thought", content: " I can answer...", end_of_message: true, end_of_dialog: false}
|
||||
{chunk_type: "answer", content: "The answer is...", end_of_message: true, end_of_dialog: true}
|
||||
```
|
||||
|
||||
当 `streaming=false`:
|
||||
当前行为保持不变
|
||||
提供完整的答案的单个响应
|
||||
`end_of_message=true`, `end_of_dialog=true`
|
||||
|
||||
#### 网关和 Python API
|
||||
|
||||
网关:用于代理流的新 SSE/WebSocket 端点
|
||||
Python API:新的 `agent_stream()` 异步生成器方法
|
||||
|
||||
--
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
**没有新的攻击面**:流式传输使用相同的身份验证/授权机制
|
||||
**速率限制**:如果需要,请应用每个令牌或每个分块的速率限制
|
||||
**连接处理**:在客户端断开连接时,正确终止流
|
||||
**超时管理**:流式传输请求需要适当的超时处理
|
||||
|
||||
## 性能注意事项
|
||||
|
||||
**内存**:流式传输减少了峰值内存使用量(没有完整的响应缓冲)
|
||||
**延迟**:首次令牌的时间显著减少
|
||||
**连接开销**:SSE/WebSocket 连接具有保活开销
|
||||
**Pulsar 吞吐量**:多个小消息与单个大消息之间的权衡
|
||||
tradeoff
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 单元测试
|
||||
使用新字段的模式序列化/反序列化
|
||||
向后兼容性(缺少字段使用默认值)
|
||||
分块组装逻辑
|
||||
|
||||
### 集成测试
|
||||
每个 LLM 提供商的流式传输实现
|
||||
网关 API 流式传输端点
|
||||
Python 客户端流式传输方法
|
||||
|
||||
### 端到端测试
|
||||
CLI 工具的流式传输输出
|
||||
完整流程:客户端 → 网关 → Pulsar → LLM → 返回
|
||||
混合流式传输/非流式传输工作负载
|
||||
|
||||
### 向后兼容性测试
|
||||
现有客户端无需修改即可工作
|
||||
非流式传输请求的行为与之前相同
|
||||
|
||||
## 迁移计划
|
||||
|
||||
### 第一阶段:基础设施
|
||||
部署模式更改(向后兼容)
|
||||
部署网关 API 更新
|
||||
部署 Python API 更新
|
||||
发布 CLI 工具更新
|
||||
|
||||
### 第二阶段:VertexAI
|
||||
部署 VertexAI 流式实现。
|
||||
使用测试工作负载进行验证。
|
||||
|
||||
### 第三阶段:所有提供商
|
||||
逐步推广提供商更新。
|
||||
监控问题。
|
||||
|
||||
### 第四阶段:Agent API
|
||||
部署 Agent 模式变更。
|
||||
部署 Agent 流式实现。
|
||||
更新文档。
|
||||
|
||||
## 时间线
|
||||
|
||||
| 阶段 | 描述 | 依赖项 |
|
||||
|-------|-------------|--------------|
|
||||
| 第一阶段 | 基础设施 | 无 |
|
||||
| 第二阶段 | VertexAI 概念验证 | 第一阶段 |
|
||||
| 第三阶段 | 所有提供商 | 第二阶段 |
|
||||
| 第四阶段 | Agent API | 第三阶段 |
|
||||
|
||||
## 设计决策
|
||||
|
||||
在规范过程中,解决了以下问题:
|
||||
|
||||
1. **流式传输中的 Token 计数**: Token 计数是增量值,而不是累计总数。
|
||||
消费者可以根据需要对其进行求和。这与大多数提供商报告的使用方式一致,并简化了实现。
|
||||
|
||||
|
||||
2. **流中的错误处理:** 如果发生错误,则会填充 `error` 字段,并且不需要填充其他字段。 错误始终是最终的
|
||||
通信 - 在发生错误后,不允许也不期望发送任何后续消息。
|
||||
通信 - 在此之后,不允许也不需要发送任何后续消息。
|
||||
一个错误。对于 LLM/Prompt 流,`end_of_stream=true`。对于 Agent 流,
|
||||
`chunk_type="error"` 与 `end_of_dialog=true`。
|
||||
|
||||
3. **部分响应恢复:** 消息协议(Pulsar)具有容错性,
|
||||
因此不需要消息级别的重试。如果客户端丢失了流的信息
|
||||
或断开连接,则必须从头开始重试整个请求。
|
||||
|
||||
4. **快速响应流式传输**: 流式传输仅支持文本 (`text`)
|
||||
响应,不支持结构化 (`object`) 响应。 提示服务在开始时就知道输出将是 JSON 还是文本,这取决于提示
|
||||
模板。 如果对 JSON 输出提示执行流式传输请求,则
|
||||
会出现问题。
|
||||
服务应该要么:
|
||||
返回完整的 JSON 数据,以单个响应的形式,包含 `end_of_stream=true`,或者
|
||||
拒绝流式传输请求,并返回错误。
|
||||
|
||||
## 开放问题
|
||||
|
||||
目前没有。
|
||||
|
||||
## 参考文献
|
||||
|
||||
当前 LLM 模式:`trustgraph-base/trustgraph/schema/services/llm.py`
|
||||
当前提示词模式:`trustgraph-base/trustgraph/schema/services/prompt.py`
|
||||
当前代理模式:`trustgraph-base/trustgraph/schema/services/agent.py`
|
||||
LLM 服务基础地址:`trustgraph-base/trustgraph/base/llm_service.py`
|
||||
VertexAI 提供商:`trustgraph-vertexai/trustgraph/model/text_completion/vertexai/llm.py`
|
||||
网关 API:`trustgraph-base/trustgraph/api/`
|
||||
CLI 工具:`trustgraph-cli/trustgraph/cli/`
|
||||
621
docs/tech-specs/zh-cn/structured-data-2.zh-cn.md
Normal file
621
docs/tech-specs/zh-cn/structured-data-2.zh-cn.md
Normal file
|
|
@ -0,0 +1,621 @@
|
|||
---
|
||||
layout: default
|
||||
title: "结构化数据技术规范 (第二部分)"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 结构化数据技术规范 (第二部分)
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范解决了在 TrustGraph 结构化数据集成初始实施过程中发现的问题和差距,如 `structured-data.md` 中所述。
|
||||
|
||||
## 问题陈述
|
||||
|
||||
### 1. 命名不一致:"对象" 与 "行"
|
||||
|
||||
当前实现方案在整个代码中使用 "对象" 术语 (例如,`ExtractedObject`,对象提取,对象嵌入)。 这种命名方式过于通用,容易造成混淆:
|
||||
|
||||
"对象" 在软件中是一个 overloaded 的术语 (Python 对象,JSON 对象等)。
|
||||
正在处理的数据本质上是表格数据,即具有定义模式的表格中的行。
|
||||
"行" 更准确地描述了数据模型,并且与数据库术语一致。
|
||||
|
||||
这种不一致性出现在模块名称、类名称、消息类型和文档中。
|
||||
|
||||
### 2. 行存储查询限制
|
||||
|
||||
当前的行存储实现方案存在重大的查询限制:
|
||||
|
||||
**自然语言匹配问题**: 查询难以处理真实世界数据的变化。 例如:
|
||||
很难在包含 `"CHESTNUT ST"` 的街道数据库中找到关于 `"Chestnut Street"` 的信息。
|
||||
缩写、大小写差异和格式变化会破坏精确匹配查询。
|
||||
用户期望语义理解,但存储系统只提供字面匹配。
|
||||
|
||||
**模式演化问题**: 更改模式会导致问题:
|
||||
现有数据可能不符合更新后的模式。
|
||||
表结构的变化可能会破坏查询和数据完整性。
|
||||
没有明确的模式更新迁移路径。
|
||||
|
||||
### 3. 需要行嵌入
|
||||
|
||||
与问题 2 相关,系统需要行数据的向量嵌入,以便:
|
||||
|
||||
在结构化数据上进行语义搜索 (找到 "Chestnut Street" 时,数据包含 "CHESTNUT ST")。
|
||||
模糊查询的相似性匹配。
|
||||
结合结构化过滤器和语义相似性的混合搜索。
|
||||
更好的自然语言查询支持。
|
||||
|
||||
嵌入服务已指定,但尚未实施。
|
||||
|
||||
### 4. 行数据摄取不完整
|
||||
|
||||
结构化数据摄取管道尚未完全投入使用:
|
||||
|
||||
存在诊断提示,用于对输入格式进行分类 (CSV,JSON 等)。
|
||||
使用这些提示的摄取服务尚未集成到系统中。
|
||||
没有将预结构化数据加载到行存储的端到端路径。
|
||||
|
||||
## 目标
|
||||
|
||||
**模式灵活性**: 允许模式演化,而不会破坏现有数据或需要迁移。
|
||||
**命名一致性**: 在整个代码库中采用 "行" 术语。
|
||||
**语义可查询性**: 通过行嵌入支持模糊/语义匹配。
|
||||
**完整的摄取管道**: 提供将结构化数据加载到存储的端到端路径。
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 统一的行存储模式
|
||||
|
||||
之前的实现方案为每个模式创建了一个单独的 Cassandra 表。 这在模式演化时会导致问题,因为表结构的变化需要迁移。
|
||||
|
||||
新的设计使用一个统一的表来存储所有行数据:
|
||||
|
||||
```sql
|
||||
CREATE TABLE rows (
|
||||
collection text,
|
||||
schema_name text,
|
||||
index_name text,
|
||||
index_value frozen<list<text>>,
|
||||
data map<text, text>,
|
||||
source text,
|
||||
PRIMARY KEY ((collection, schema_name, index_name), index_value)
|
||||
)
|
||||
```
|
||||
|
||||
#### 列定义
|
||||
|
||||
| 列 | 类型 | 描述 |
|
||||
|--------|------|-------------|
|
||||
| `collection` | `text` | 数据收集/导入标识符(来自元数据)|
|
||||
| `schema_name` | `text` | 此行所遵循的模式名称 |
|
||||
| `index_name` | `text` | 索引字段名称,以逗号分隔的字符串表示复合索引 |
|
||||
| `index_value` | `frozen<list<text>>` | 索引值列表 |
|
||||
| `data` | `map<text, text>` | 行数据,以键值对形式存储 |
|
||||
| `source` | `text` | 可选的 URI,链接到知识图谱中的来源信息。空字符串或 NULL 表示没有来源。|
|
||||
|
||||
#### 索引处理
|
||||
|
||||
每行数据会被存储多次,对于模式中定义的每个索引字段,都会存储一次。主键字段被视为索引,没有特殊的标记,以提供未来的灵活性。
|
||||
|
||||
**单字段索引示例:**
|
||||
模式定义 `email` 为索引
|
||||
`index_name = "email"`
|
||||
`index_value = ['foo@bar.com']`
|
||||
|
||||
**复合索引示例:**
|
||||
模式定义在 `region` 和 `status` 上创建复合索引
|
||||
`index_name = "region,status"` (字段名称按排序方式连接)
|
||||
`index_value = ['US', 'active']` (值与字段名称的顺序相同)
|
||||
|
||||
**主键示例:**
|
||||
模式定义 `customer_id` 为主键
|
||||
`index_name = "customer_id"`
|
||||
`index_value = ['CUST001']`
|
||||
|
||||
#### 查询模式
|
||||
|
||||
无论使用哪个索引,所有查询都遵循相同的模式:
|
||||
|
||||
```sql
|
||||
SELECT * FROM rows
|
||||
WHERE collection = 'import_2024'
|
||||
AND schema_name = 'customers'
|
||||
AND index_name = 'email'
|
||||
AND index_value = ['foo@bar.com']
|
||||
```
|
||||
|
||||
#### 设计权衡
|
||||
|
||||
**优点:**
|
||||
模式更改不需要更改表结构
|
||||
行数据对 Cassandra 隐藏 - 字段的添加/删除是透明的
|
||||
所有访问方法都具有一致的查询模式
|
||||
不需要 Cassandra 的二级索引(这在大型环境中可能会很慢)
|
||||
整个过程中都使用 Cassandra 的原生类型(`map`,`frozen<list>`)
|
||||
|
||||
**权衡:**
|
||||
写入放大:每个行插入 = N 次插入(每个索引字段一次)
|
||||
重复行数据带来的存储开销
|
||||
类型信息存储在模式配置中,应用程序层进行转换
|
||||
|
||||
#### 一致性模型
|
||||
|
||||
该设计接受某些简化:
|
||||
|
||||
1. **没有行更新**:系统是只追加的。这消除了关于更新同一行的多个副本的一致性问题。
|
||||
|
||||
2. **模式更改容错性**:当模式发生更改(例如,添加/删除索引)时,现有行保留其原始索引。旧行将无法通过新索引发现。如果需要,用户可以删除并重新创建模式以确保一致性。
|
||||
|
||||
### 分区跟踪和删除
|
||||
|
||||
#### 问题
|
||||
|
||||
借助分区键 `(collection, schema_name, index_name)`,高效删除需要知道所有要删除的分区键。仅通过 `collection` 或 `collection + schema_name` 删除需要知道所有具有数据的 `index_name` 值。
|
||||
|
||||
#### 分区跟踪表
|
||||
|
||||
一个辅助查找表跟踪哪些分区存在:
|
||||
|
||||
```sql
|
||||
CREATE TABLE row_partitions (
|
||||
collection text,
|
||||
schema_name text,
|
||||
index_name text,
|
||||
PRIMARY KEY ((collection), schema_name, index_name)
|
||||
)
|
||||
```
|
||||
|
||||
这使得能够高效地发现用于删除操作的分区。
|
||||
|
||||
#### 行写入器行为
|
||||
|
||||
行写入器维护一个已注册的 `(collection, schema_name)` 对的内存缓存。 在处理一行时:
|
||||
|
||||
1. 检查 `(collection, schema_name)` 是否在缓存中
|
||||
2. 如果未缓存(对于此对的第一个行):
|
||||
查找模式配置以获取所有索引名称
|
||||
将条目插入到 `row_partitions` 中,对于每个 `(collection, schema_name, index_name)`
|
||||
将该对添加到缓存中
|
||||
3. 继续写入行数据
|
||||
|
||||
行写入器还监视模式配置更改事件。 当模式发生更改时,相关的缓存条目将被清除,以便下一行触发使用更新的索引名称重新注册。
|
||||
|
||||
这种方法确保:
|
||||
查找表写入仅针对每个 `(collection, schema_name)` 对发生一次,而不是针对每行。
|
||||
查找表反映了数据写入时活动的索引。
|
||||
导入过程中发生的模式更改可以正确处理。
|
||||
|
||||
#### 删除操作
|
||||
|
||||
**删除集合:**
|
||||
```sql
|
||||
-- 1. Discover all partitions
|
||||
SELECT schema_name, index_name FROM row_partitions WHERE collection = 'X';
|
||||
|
||||
-- 2. Delete each partition from rows table
|
||||
DELETE FROM rows WHERE collection = 'X' AND schema_name = '...' AND index_name = '...';
|
||||
-- (repeat for each discovered partition)
|
||||
|
||||
-- 3. Clean up the lookup table
|
||||
DELETE FROM row_partitions WHERE collection = 'X';
|
||||
```
|
||||
|
||||
**删除集合 + 模式:**
|
||||
```sql
|
||||
-- 1. Discover partitions for this schema
|
||||
SELECT index_name FROM row_partitions WHERE collection = 'X' AND schema_name = 'Y';
|
||||
|
||||
-- 2. Delete each partition from rows table
|
||||
DELETE FROM rows WHERE collection = 'X' AND schema_name = 'Y' AND index_name = '...';
|
||||
-- (repeat for each discovered partition)
|
||||
|
||||
-- 3. Clean up the lookup table entries
|
||||
DELETE FROM row_partitions WHERE collection = 'X' AND schema_name = 'Y';
|
||||
```
|
||||
|
||||
### 行嵌入
|
||||
|
||||
行嵌入允许对索引值进行语义/模糊匹配,从而解决自然语言不匹配的问题(例如,在搜索“Chestnut Street”时找到“CHESTNUT ST”)。
|
||||
|
||||
#### 设计概述
|
||||
|
||||
每个索引值都会被嵌入,并存储在向量存储中(Qdrant)。在查询时,查询内容也会被嵌入,找到相似的向量,然后使用相关的元数据来查找 Cassandra 中的实际行。
|
||||
|
||||
#### Qdrant 集合结构
|
||||
|
||||
每个 `(user, collection, schema_name, dimension)` 元组对应一个 Qdrant 集合:
|
||||
|
||||
**集合命名:** `rows_{user}_{collection}_{schema_name}_{dimension}`
|
||||
名称会被清理(非字母数字字符替换为 `_`,转换为小写,数字前缀添加 `r_` 前缀)
|
||||
**理由:** 允许通过删除匹配的 Qdrant 集合来干净地删除一个 `(user, collection, schema_name)` 实例;维度后缀允许不同的嵌入模型共存。
|
||||
|
||||
#### 哪些内容会被嵌入
|
||||
|
||||
索引值的文本表示:
|
||||
|
||||
| 索引类型 | 示例 `index_value` | 要嵌入的文本 |
|
||||
|------------|----------------------|---------------|
|
||||
| 单字段 | `['foo@bar.com']` | `"foo@bar.com"` |
|
||||
| 组合 | `['US', 'active']` | `"US active"` (空格连接) |
|
||||
|
||||
#### 点结构
|
||||
|
||||
每个 Qdrant 点包含:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<uuid>",
|
||||
"vector": [0.1, 0.2, ...],
|
||||
"payload": {
|
||||
"index_name": "street_name",
|
||||
"index_value": ["CHESTNUT ST"],
|
||||
"text": "CHESTNUT ST"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Payload Field | Description |
|
||||
|---------------|-------------|
|
||||
| `index_name` | 此嵌入所代表的索引字段。|
|
||||
| `index_value` | 原始值列表(用于 Cassandra 查找)。|
|
||||
| `text` | 嵌入的文本(用于调试/显示)。|
|
||||
|
||||
注意:`user`、`collection` 和 `schema_name` 可以从 Qdrant 集合名称中推断出来。|
|
||||
|
||||
#### 查询流程
|
||||
|
||||
1. 用户查询用户 U、集合 X、模式 Y 中的“Chestnut Street”。|
|
||||
2. 嵌入查询文本。|
|
||||
3. 确定与前缀 `rows_U_X_Y_` 匹配的 Qdrant 集合名称。|
|
||||
4. 在匹配的 Qdrant 集合中搜索最接近的向量。|
|
||||
5. 获取包含 `index_name` 和 `index_value` 的有效负载的匹配点。|
|
||||
6. 查询 Cassandra:|
|
||||
```sql
|
||||
SELECT * FROM rows
|
||||
WHERE collection = 'X'
|
||||
AND schema_name = 'Y'
|
||||
AND index_name = '<from payload>'
|
||||
AND index_value = <from payload>
|
||||
```
|
||||
7. 返回匹配的行
|
||||
|
||||
#### 可选:按索引名称过滤
|
||||
|
||||
查询可以选择性地按 `index_name` 进行过滤,以便在 Qdrant 中仅搜索特定字段:
|
||||
|
||||
**"查找所有匹配 'Chestnut' 的字段"** → 搜索集合中的所有向量
|
||||
**"查找 'Chestnut' 匹配的 street_name"** → 过滤 `payload.index_name = 'street_name'`
|
||||
|
||||
#### 架构
|
||||
|
||||
行嵌入遵循 GraphRAG 使用的 **两阶段模式**(图嵌入、文档嵌入):
|
||||
|
||||
**第一阶段:嵌入计算** (`trustgraph-flow/trustgraph/embeddings/row_embeddings/`) - 消耗 `ExtractedObject`,通过嵌入服务计算嵌入,输出 `RowEmbeddings`
|
||||
**第二阶段:嵌入存储** (`trustgraph-flow/trustgraph/storage/row_embeddings/qdrant/`) - 消耗 `RowEmbeddings`,将向量写入 Qdrant
|
||||
|
||||
Cassandra 行写入器是一个独立的并行消费者:
|
||||
|
||||
**Cassandra 行写入器** (`trustgraph-flow/trustgraph/storage/rows/cassandra`) - 消耗 `ExtractedObject`,将行写入 Cassandra
|
||||
|
||||
所有三个服务都从同一个流中读取,从而使它们彼此解耦。 这允许:
|
||||
独立地扩展 Cassandra 写入与嵌入生成与向量存储
|
||||
如果不需要,可以禁用嵌入服务
|
||||
一个服务中的故障不会影响其他服务
|
||||
与 GraphRAG 管道保持一致的架构
|
||||
|
||||
#### 写入路径
|
||||
|
||||
**第一阶段(行嵌入处理器):** 接收到 `ExtractedObject` 时:
|
||||
|
||||
1. 查找模式以找到索引字段
|
||||
2. 对于每个索引字段:
|
||||
构建索引值的文本表示
|
||||
通过嵌入服务计算嵌入
|
||||
3. 输出一个包含所有计算向量的 `RowEmbeddings` 消息
|
||||
|
||||
**第二阶段(行嵌入-写入-Qdrant):** 接收到 `RowEmbeddings` 时:
|
||||
|
||||
1. 对于消息中的每个嵌入:
|
||||
从 `(user, collection, schema_name, dimension)` 确定 Qdrant 集合
|
||||
如果需要,创建集合(首次写入时延迟创建)
|
||||
使用向量和有效载荷进行更新
|
||||
|
||||
#### 消息类型
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class RowIndexEmbedding:
|
||||
index_name: str # The indexed field name(s)
|
||||
index_value: list[str] # The field value(s)
|
||||
text: str # Text that was embedded
|
||||
vectors: list[list[float]] # Computed embedding vectors
|
||||
|
||||
@dataclass
|
||||
class RowEmbeddings:
|
||||
metadata: Metadata
|
||||
schema_name: str
|
||||
embeddings: list[RowIndexEmbedding]
|
||||
```
|
||||
|
||||
#### 删除集成
|
||||
|
||||
Qdrant 集合通过在集合名称模式上进行前缀匹配来发现:
|
||||
|
||||
**删除 `(user, collection)`:**
|
||||
1. 列出所有与前缀 `rows_{user}_{collection}_` 匹配的 Qdrant 集合
|
||||
2. 删除每个匹配的集合
|
||||
3. 删除 Cassandra 行分区(如上文所述)
|
||||
4. 清理 `row_partitions` 条目
|
||||
|
||||
**删除 `(user, collection, schema_name)`:**
|
||||
1. 列出所有与前缀 `rows_{user}_{collection}_{schema_name}_` 匹配的 Qdrant 集合
|
||||
2. 删除每个匹配的集合(处理多个维度)
|
||||
3. 删除 Cassandra 行分区
|
||||
4. 清理 `row_partitions`
|
||||
|
||||
#### 模块位置
|
||||
|
||||
| 阶段 | 模块 | 入口点 |
|
||||
|-------|--------|-------------|
|
||||
| 阶段 1 | `trustgraph-flow/trustgraph/embeddings/row_embeddings/` | `row-embeddings` |
|
||||
| 阶段 2 | `trustgraph-flow/trustgraph/storage/row_embeddings/qdrant/` | `row-embeddings-write-qdrant` |
|
||||
|
||||
### 行嵌入查询 API
|
||||
|
||||
行嵌入查询是与 GraphQL 行查询服务**不同的 API**:
|
||||
|
||||
| API | 目的 | 后端 |
|
||||
|-----|---------|---------|
|
||||
| 行查询 (GraphQL) | 对索引字段进行精确匹配 | Cassandra |
|
||||
| 行嵌入查询 | 模糊/语义匹配 | Qdrant |
|
||||
|
||||
这种分离保持了关注点的清晰:
|
||||
GraphQL 服务专注于精确、结构化的查询
|
||||
嵌入 API 处理语义相似性
|
||||
用户工作流程:通过嵌入进行模糊搜索以查找候选对象,然后进行精确查询以获取完整的行数据
|
||||
|
||||
#### 请求/响应模式
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class RowEmbeddingsRequest:
|
||||
vectors: list[list[float]] # Query vectors (pre-computed embeddings)
|
||||
user: str = ""
|
||||
collection: str = ""
|
||||
schema_name: str = ""
|
||||
index_name: str = "" # Optional: filter to specific index
|
||||
limit: int = 10 # Max results per vector
|
||||
|
||||
@dataclass
|
||||
class RowIndexMatch:
|
||||
index_name: str = "" # The matched index field(s)
|
||||
index_value: list[str] = [] # The matched value(s)
|
||||
text: str = "" # Original text that was embedded
|
||||
score: float = 0.0 # Similarity score
|
||||
|
||||
@dataclass
|
||||
class RowEmbeddingsResponse:
|
||||
error: Error | None = None
|
||||
matches: list[RowIndexMatch] = []
|
||||
```
|
||||
|
||||
#### 查询处理器
|
||||
|
||||
模块:`trustgraph-flow/trustgraph/query/row_embeddings/qdrant`
|
||||
|
||||
入口点:`row-embeddings-query-qdrant`
|
||||
|
||||
处理器:
|
||||
1. 接收带有查询向量的 `RowEmbeddingsRequest`
|
||||
2. 通过前缀匹配找到合适的 Qdrant 集合
|
||||
3. 搜索最近的向量,并可选择使用 `index_name` 过滤器
|
||||
4. 返回包含匹配索引信息的 `RowEmbeddingsResponse`
|
||||
|
||||
#### API 网关集成
|
||||
|
||||
网关通过标准的请求/响应模式公开行嵌入查询:
|
||||
|
||||
| 组件 | 位置 |
|
||||
|-----------|----------|
|
||||
| 调度器 | `trustgraph-flow/trustgraph/gateway/dispatch/row_embeddings_query.py` |
|
||||
| 注册 | 将 `"row-embeddings"` 添加到 `request_response_dispatchers` 中,位于 `manager.py` |
|
||||
|
||||
流程接口名称:`row-embeddings`
|
||||
|
||||
流程蓝图中的接口定义:
|
||||
```json
|
||||
{
|
||||
"interfaces": {
|
||||
"row-embeddings": {
|
||||
"request": "non-persistent://tg/request/row-embeddings:{id}",
|
||||
"response": "non-persistent://tg/response/row-embeddings:{id}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Python SDK 支持
|
||||
|
||||
SDK 提供了用于行嵌入查询的方法:
|
||||
|
||||
```python
|
||||
# Flow-scoped query (preferred)
|
||||
api = Api(url)
|
||||
flow = api.flow().id("default")
|
||||
|
||||
# Query with text (SDK computes embeddings)
|
||||
matches = flow.row_embeddings_query(
|
||||
text="Chestnut Street",
|
||||
collection="my_collection",
|
||||
schema_name="addresses",
|
||||
index_name="street_name", # Optional filter
|
||||
limit=10
|
||||
)
|
||||
|
||||
# Query with pre-computed vectors
|
||||
matches = flow.row_embeddings_query(
|
||||
vectors=[[0.1, 0.2, ...]],
|
||||
collection="my_collection",
|
||||
schema_name="addresses"
|
||||
)
|
||||
|
||||
# Each match contains:
|
||||
for match in matches:
|
||||
print(match.index_name) # e.g., "street_name"
|
||||
print(match.index_value) # e.g., ["CHESTNUT ST"]
|
||||
print(match.text) # e.g., "CHESTNUT ST"
|
||||
print(match.score) # e.g., 0.95
|
||||
```
|
||||
|
||||
#### 命令行工具
|
||||
|
||||
命令:`tg-invoke-row-embeddings`
|
||||
|
||||
```bash
|
||||
# Query by text (computes embedding automatically)
|
||||
tg-invoke-row-embeddings \
|
||||
--text "Chestnut Street" \
|
||||
--collection my_collection \
|
||||
--schema addresses \
|
||||
--index street_name \
|
||||
--limit 10
|
||||
|
||||
# Query by vector file
|
||||
tg-invoke-row-embeddings \
|
||||
--vectors vectors.json \
|
||||
--collection my_collection \
|
||||
--schema addresses
|
||||
|
||||
# Output formats
|
||||
tg-invoke-row-embeddings --text "..." --format json
|
||||
tg-invoke-row-embeddings --text "..." --format table
|
||||
```
|
||||
|
||||
#### 典型用法示例
|
||||
|
||||
行嵌入查询通常用作模糊到精确查找流程的一部分:
|
||||
|
||||
```python
|
||||
# Step 1: Fuzzy search via embeddings
|
||||
matches = flow.row_embeddings_query(
|
||||
text="chestnut street",
|
||||
collection="geo",
|
||||
schema_name="streets"
|
||||
)
|
||||
|
||||
# Step 2: Exact lookup via GraphQL for full row data
|
||||
for match in matches:
|
||||
query = f'''
|
||||
query {{
|
||||
streets(where: {{ {match.index_name}: {{ eq: "{match.index_value[0]}" }} }}) {{
|
||||
street_name
|
||||
city
|
||||
zip_code
|
||||
}}
|
||||
}}
|
||||
'''
|
||||
rows = flow.rows_query(query, collection="geo")
|
||||
```
|
||||
|
||||
这种两步模式可以实现:
|
||||
当用户搜索 "Chestnut Street" 时,找到 "CHESTNUT ST"
|
||||
检索包含所有字段的完整行数据
|
||||
结合语义相似性和结构化数据访问
|
||||
|
||||
### 行数据导入
|
||||
|
||||
暂定于后续阶段。将与其他导入更改一起设计。
|
||||
|
||||
## 实施影响
|
||||
|
||||
### 当前状态分析
|
||||
|
||||
现有的实现包含两个主要组件:
|
||||
|
||||
| 组件 | 位置 | 行数 | 描述 |
|
||||
|-----------|----------|-------|-------------|
|
||||
| 查询服务 | `trustgraph-flow/trustgraph/query/objects/cassandra/service.py` | ~740 | 整体式:GraphQL 模式生成、过滤器解析、Cassandra 查询、请求处理 |
|
||||
| 写入器 | `trustgraph-flow/trustgraph/storage/objects/cassandra/write.py` | ~540 | 每个模式的表创建、二级索引、插入/删除 |
|
||||
|
||||
**当前的查询模式:**
|
||||
```sql
|
||||
SELECT * FROM {keyspace}.o_{schema_name}
|
||||
WHERE collection = 'X' AND email = 'foo@bar.com'
|
||||
ALLOW FILTERING
|
||||
```
|
||||
|
||||
**新的查询模式:**
|
||||
```sql
|
||||
SELECT * FROM {keyspace}.rows
|
||||
WHERE collection = 'X' AND schema_name = 'customers'
|
||||
AND index_name = 'email' AND index_value = ['foo@bar.com']
|
||||
```
|
||||
|
||||
### 关键变更
|
||||
|
||||
1. **查询语义简化:** 新的模式仅支持对 `index_value` 的精确匹配。当前的 GraphQL 过滤器(`gt`、`lt`、`contains` 等)要么:
|
||||
变为对返回数据的后过滤(如果仍然需要)
|
||||
被移除,转而使用嵌入 API 进行模糊匹配
|
||||
|
||||
2. **GraphQL 代码紧密耦合:** 当前的 `service.py` 包含了 Strawberry 类型生成、过滤器解析以及 Cassandra 相关的查询。添加另一个行存储后端会重复大约 400 行的 GraphQL 代码。
|
||||
|
||||
### 提出的重构
|
||||
|
||||
重构分为两部分:
|
||||
|
||||
#### 1. 提取 GraphQL 代码
|
||||
|
||||
将可重用的 GraphQL 组件提取到共享模块中:
|
||||
|
||||
```
|
||||
trustgraph-flow/trustgraph/query/graphql/
|
||||
├── __init__.py
|
||||
├── types.py # Filter types (IntFilter, StringFilter, FloatFilter)
|
||||
├── schema.py # Dynamic schema generation from RowSchema
|
||||
└── filters.py # Filter parsing utilities
|
||||
```
|
||||
|
||||
这实现了:
|
||||
在不同的行存储后端中实现重用
|
||||
更清晰的分层
|
||||
更容易独立地测试 GraphQL 逻辑
|
||||
|
||||
#### 2. 实现新的表模式
|
||||
|
||||
重构特定于 Cassandra 的代码,以使用统一的表:
|
||||
|
||||
**写入器 (Writer)** (`trustgraph-flow/trustgraph/storage/rows/cassandra/`):
|
||||
使用单个 `rows` 表,而不是每个模式的表
|
||||
写入 N 个副本到每行(每个索引一个)
|
||||
注册到 `row_partitions` 表
|
||||
更简单的表创建(一次性设置)
|
||||
|
||||
**查询服务 (Query Service)** (`trustgraph-flow/trustgraph/query/rows/cassandra/`):
|
||||
查询统一的 `rows` 表
|
||||
使用提取的 GraphQL 模块进行模式生成
|
||||
简化的过滤处理(仅在数据库级别进行精确匹配)
|
||||
|
||||
### 模块重命名
|
||||
|
||||
作为“object”→“row”命名清理的一部分:
|
||||
|
||||
| 当前 (Current) | 新 (New) |
|
||||
|---------|-----|
|
||||
| `storage/objects/cassandra/` | `storage/rows/cassandra/` |
|
||||
| `query/objects/cassandra/` | `query/rows/cassandra/` |
|
||||
| `embeddings/object_embeddings/` | `embeddings/row_embeddings/` |
|
||||
|
||||
### 新模块
|
||||
|
||||
| 模块 (Module) | 目的 (Purpose) |
|
||||
|--------|---------|
|
||||
| `trustgraph-flow/trustgraph/query/graphql/` | 共享 GraphQL 工具
|
||||
| `trustgraph-flow/trustgraph/query/row_embeddings/qdrant/` | 行嵌入查询 API
|
||||
| `trustgraph-flow/trustgraph/embeddings/row_embeddings/` | 行嵌入计算(第一阶段)
|
||||
| `trustgraph-flow/trustgraph/storage/row_embeddings/qdrant/` | 行嵌入存储(第二阶段)
|
||||
|
||||
## 引用
|
||||
|
||||
[结构化数据技术规范](structured-data.md)
|
||||
567
docs/tech-specs/zh-cn/structured-data-descriptor.zh-cn.md
Normal file
567
docs/tech-specs/zh-cn/structured-data-descriptor.zh-cn.md
Normal file
|
|
@ -0,0 +1,567 @@
|
|||
---
|
||||
layout: default
|
||||
title: "结构化数据描述符规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 结构化数据描述符规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
结构化数据描述符是一种基于 JSON 的配置语言,用于描述如何解析、转换和导入结构化数据到 TrustGraph 中。它提供了一种声明式的数据导入方法,支持多种输入格式和复杂的转换流程,而无需自定义代码。
|
||||
|
||||
## 核心概念
|
||||
|
||||
### 1. 格式定义
|
||||
描述输入文件类型和解析选项。确定要使用的解析器以及如何解释源数据。
|
||||
|
||||
### 2. 字段映射
|
||||
将源路径映射到目标字段,并进行转换。定义数据如何从输入源流向输出模式字段。
|
||||
|
||||
### 3. 转换流程
|
||||
一系列应用于字段值的转换,包括:
|
||||
数据清洗(修剪、标准化)
|
||||
格式转换(日期解析、类型转换)
|
||||
计算(算术运算、字符串操作)
|
||||
查找(引用表、替换)
|
||||
|
||||
### 4. 验证规则
|
||||
应用于确保数据完整性的数据质量检查:
|
||||
类型验证
|
||||
范围检查
|
||||
模式匹配(正则表达式)
|
||||
必填字段验证
|
||||
自定义验证逻辑
|
||||
|
||||
### 5. 全局设置
|
||||
适用于整个导入过程的配置:
|
||||
用于数据增强的查找表
|
||||
全局变量和常量
|
||||
输出格式规范
|
||||
错误处理策略
|
||||
|
||||
## 实施策略
|
||||
|
||||
导入器的实现遵循以下流程:
|
||||
|
||||
1. **解析配置** - 加载和验证 JSON 描述符
|
||||
2. **初始化解析器** - 根据 `format.type` 加载适当的解析器(CSV、XML、JSON 等)
|
||||
3. **应用预处理** - 执行全局过滤器和转换
|
||||
4. **处理记录** - 对于每个输入记录:
|
||||
使用源路径(JSONPath、XPath、列名)提取数据
|
||||
按照顺序应用字段级别的转换
|
||||
根据定义的规则验证结果
|
||||
为缺失数据应用默认值
|
||||
5. **应用后处理** - 执行去重、聚合等操作
|
||||
6. **生成输出** - 以指定的目标格式生成数据
|
||||
|
||||
## 路径表达式支持
|
||||
|
||||
不同的输入格式使用适当的路径表达式语言:
|
||||
|
||||
**CSV**: 列名或索引(`"column_name"` 或 `"[2]"`)
|
||||
**JSON**: JSONPath 语法(`"$.user.profile.email"`)
|
||||
**XML**: XPath 表达式(`"//product[@id='123']/price"`)
|
||||
**定宽**: 来自字段定义的字段名
|
||||
|
||||
## 优点
|
||||
|
||||
**单一代码库** - 一个导入器处理多种输入格式
|
||||
**用户友好** - 非技术用户可以创建配置
|
||||
**可重用** - 可以共享和版本控制配置
|
||||
**灵活** - 无需自定义代码即可进行复杂的转换
|
||||
**健壮** - 内置验证和全面的错误处理
|
||||
**可维护** - 声明式方法减少了实施复杂性
|
||||
|
||||
## 语言规范
|
||||
|
||||
结构化数据描述符使用基于 JSON 的配置格式,其顶级结构如下:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"metadata": {
|
||||
"name": "Configuration Name",
|
||||
"description": "Description of what this config does",
|
||||
"author": "Author Name",
|
||||
"created": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
"format": { ... },
|
||||
"globals": { ... },
|
||||
"preprocessing": [ ... ],
|
||||
"mappings": [ ... ],
|
||||
"postprocessing": [ ... ],
|
||||
"output": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 格式定义
|
||||
|
||||
描述输入数据格式和解析选项:
|
||||
|
||||
```json
|
||||
{
|
||||
"format": {
|
||||
"type": "csv|json|xml|fixed-width|excel|parquet",
|
||||
"encoding": "utf-8",
|
||||
"options": {
|
||||
// Format-specific options
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### CSV 格式选项
|
||||
```json
|
||||
{
|
||||
"format": {
|
||||
"type": "csv",
|
||||
"options": {
|
||||
"delimiter": ",",
|
||||
"quote_char": "\"",
|
||||
"escape_char": "\\",
|
||||
"skip_rows": 1,
|
||||
"has_header": true,
|
||||
"null_values": ["", "NULL", "null", "N/A"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### JSON 格式选项
|
||||
```json
|
||||
{
|
||||
"format": {
|
||||
"type": "json",
|
||||
"options": {
|
||||
"root_path": "$.data",
|
||||
"array_mode": "records|single",
|
||||
"flatten": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### XML 格式选项
|
||||
```json
|
||||
{
|
||||
"format": {
|
||||
"type": "xml",
|
||||
"options": {
|
||||
"root_element": "//records/record",
|
||||
"namespaces": {
|
||||
"ns": "http://example.com/namespace"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 全局设置
|
||||
|
||||
定义查找表、变量和全局配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"globals": {
|
||||
"variables": {
|
||||
"current_date": "2024-01-01",
|
||||
"batch_id": "BATCH_001",
|
||||
"default_confidence": 0.8
|
||||
},
|
||||
"lookup_tables": {
|
||||
"country_codes": {
|
||||
"US": "United States",
|
||||
"UK": "United Kingdom",
|
||||
"CA": "Canada"
|
||||
},
|
||||
"status_mapping": {
|
||||
"1": "active",
|
||||
"0": "inactive"
|
||||
}
|
||||
},
|
||||
"constants": {
|
||||
"source_system": "legacy_crm",
|
||||
"import_type": "full"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字段映射
|
||||
|
||||
定义如何将源数据映射到目标字段,并进行转换:
|
||||
|
||||
```json
|
||||
{
|
||||
"mappings": [
|
||||
{
|
||||
"target_field": "person_name",
|
||||
"source": "$.name",
|
||||
"transforms": [
|
||||
{"type": "trim"},
|
||||
{"type": "title_case"},
|
||||
{"type": "required"}
|
||||
],
|
||||
"validation": [
|
||||
{"type": "min_length", "value": 2},
|
||||
{"type": "max_length", "value": 100},
|
||||
{"type": "pattern", "value": "^[A-Za-z\\s]+$"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"target_field": "age",
|
||||
"source": "$.age",
|
||||
"transforms": [
|
||||
{"type": "to_int"},
|
||||
{"type": "default", "value": 0}
|
||||
],
|
||||
"validation": [
|
||||
{"type": "range", "min": 0, "max": 150}
|
||||
]
|
||||
},
|
||||
{
|
||||
"target_field": "country",
|
||||
"source": "$.country_code",
|
||||
"transforms": [
|
||||
{"type": "lookup", "table": "country_codes"},
|
||||
{"type": "default", "value": "Unknown"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 转换类型
|
||||
|
||||
可用的转换函数:
|
||||
|
||||
#### 字符串转换
|
||||
```json
|
||||
{"type": "trim"},
|
||||
{"type": "upper"},
|
||||
{"type": "lower"},
|
||||
{"type": "title_case"},
|
||||
{"type": "replace", "pattern": "old", "replacement": "new"},
|
||||
{"type": "regex_replace", "pattern": "\\d+", "replacement": "XXX"},
|
||||
{"type": "substring", "start": 0, "end": 10},
|
||||
{"type": "pad_left", "length": 10, "char": "0"}
|
||||
```
|
||||
|
||||
#### 类型转换
|
||||
```json
|
||||
{"type": "to_string"},
|
||||
{"type": "to_int"},
|
||||
{"type": "to_float"},
|
||||
{"type": "to_bool"},
|
||||
{"type": "to_date", "format": "YYYY-MM-DD"},
|
||||
{"type": "parse_json"}
|
||||
```
|
||||
|
||||
#### 数据操作
|
||||
```json
|
||||
{"type": "default", "value": "default_value"},
|
||||
{"type": "lookup", "table": "table_name"},
|
||||
{"type": "concat", "values": ["field1", " - ", "field2"]},
|
||||
{"type": "calculate", "expression": "${field1} + ${field2}"},
|
||||
{"type": "conditional", "condition": "${age} > 18", "true_value": "adult", "false_value": "minor"}
|
||||
```
|
||||
|
||||
### 验证规则
|
||||
|
||||
数据质量检查,具有可配置的错误处理:
|
||||
|
||||
#### 基础验证
|
||||
```json
|
||||
{"type": "required"},
|
||||
{"type": "not_null"},
|
||||
{"type": "min_length", "value": 5},
|
||||
{"type": "max_length", "value": 100},
|
||||
{"type": "range", "min": 0, "max": 1000},
|
||||
{"type": "pattern", "value": "^[A-Z]{2,3}$"},
|
||||
{"type": "in_list", "values": ["active", "inactive", "pending"]}
|
||||
```
|
||||
|
||||
#### 自定义验证
|
||||
```json
|
||||
{
|
||||
"type": "custom",
|
||||
"expression": "${age} >= 18 && ${country} == 'US'",
|
||||
"message": "Must be 18+ and in US"
|
||||
},
|
||||
{
|
||||
"type": "cross_field",
|
||||
"fields": ["start_date", "end_date"],
|
||||
"expression": "${start_date} < ${end_date}",
|
||||
"message": "Start date must be before end date"
|
||||
}
|
||||
```
|
||||
|
||||
### 预处理和后处理
|
||||
|
||||
在字段映射之前/之后应用的全局操作:
|
||||
|
||||
```json
|
||||
{
|
||||
"preprocessing": [
|
||||
{
|
||||
"type": "filter",
|
||||
"condition": "${status} != 'deleted'"
|
||||
},
|
||||
{
|
||||
"type": "sort",
|
||||
"field": "created_date",
|
||||
"order": "asc"
|
||||
}
|
||||
],
|
||||
"postprocessing": [
|
||||
{
|
||||
"type": "deduplicate",
|
||||
"key_fields": ["email", "phone"]
|
||||
},
|
||||
{
|
||||
"type": "aggregate",
|
||||
"group_by": ["country"],
|
||||
"functions": {
|
||||
"total_count": {"type": "count"},
|
||||
"avg_age": {"type": "avg", "field": "age"}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 输出配置
|
||||
|
||||
定义如何输出处理后的数据:
|
||||
|
||||
```json
|
||||
{
|
||||
"output": {
|
||||
"format": "trustgraph-objects",
|
||||
"schema_name": "person",
|
||||
"options": {
|
||||
"batch_size": 1000,
|
||||
"confidence": 0.9,
|
||||
"source_span_field": "raw_text",
|
||||
"metadata": {
|
||||
"source": "crm_import",
|
||||
"version": "1.0"
|
||||
}
|
||||
},
|
||||
"error_handling": {
|
||||
"on_validation_error": "skip|fail|log",
|
||||
"on_transform_error": "skip|fail|default",
|
||||
"max_errors": 100,
|
||||
"error_output": "errors.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 完整示例
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"metadata": {
|
||||
"name": "Customer Import from CRM CSV",
|
||||
"description": "Imports customer data from legacy CRM system",
|
||||
"author": "Data Team",
|
||||
"created": "2024-01-01T00:00:00Z"
|
||||
},
|
||||
"format": {
|
||||
"type": "csv",
|
||||
"encoding": "utf-8",
|
||||
"options": {
|
||||
"delimiter": ",",
|
||||
"has_header": true,
|
||||
"skip_rows": 1
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"variables": {
|
||||
"import_date": "2024-01-01",
|
||||
"default_confidence": 0.85
|
||||
},
|
||||
"lookup_tables": {
|
||||
"country_codes": {
|
||||
"US": "United States",
|
||||
"CA": "Canada",
|
||||
"UK": "United Kingdom"
|
||||
}
|
||||
}
|
||||
},
|
||||
"preprocessing": [
|
||||
{
|
||||
"type": "filter",
|
||||
"condition": "${status} == 'active'"
|
||||
}
|
||||
],
|
||||
"mappings": [
|
||||
{
|
||||
"target_field": "full_name",
|
||||
"source": "customer_name",
|
||||
"transforms": [
|
||||
{"type": "trim"},
|
||||
{"type": "title_case"}
|
||||
],
|
||||
"validation": [
|
||||
{"type": "required"},
|
||||
{"type": "min_length", "value": 2}
|
||||
]
|
||||
},
|
||||
{
|
||||
"target_field": "email",
|
||||
"source": "email_address",
|
||||
"transforms": [
|
||||
{"type": "trim"},
|
||||
{"type": "lower"}
|
||||
],
|
||||
"validation": [
|
||||
{"type": "pattern", "value": "^[\\w.-]+@[\\w.-]+\\.[a-zA-Z]{2,}$"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"target_field": "age",
|
||||
"source": "age",
|
||||
"transforms": [
|
||||
{"type": "to_int"},
|
||||
{"type": "default", "value": 0}
|
||||
],
|
||||
"validation": [
|
||||
{"type": "range", "min": 0, "max": 120}
|
||||
]
|
||||
},
|
||||
{
|
||||
"target_field": "country",
|
||||
"source": "country_code",
|
||||
"transforms": [
|
||||
{"type": "lookup", "table": "country_codes"},
|
||||
{"type": "default", "value": "Unknown"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"output": {
|
||||
"format": "trustgraph-objects",
|
||||
"schema_name": "customer",
|
||||
"options": {
|
||||
"confidence": "${default_confidence}",
|
||||
"batch_size": 500
|
||||
},
|
||||
"error_handling": {
|
||||
"on_validation_error": "log",
|
||||
"max_errors": 50
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 用于描述符生成的 LLM 提示
|
||||
|
||||
以下提示可用于让 LLM 分析样本数据并生成描述符配置:
|
||||
|
||||
```
|
||||
I need you to analyze the provided data sample and create a Structured Data Descriptor configuration in JSON format.
|
||||
|
||||
The descriptor should follow this specification:
|
||||
- version: "1.0"
|
||||
- metadata: Configuration name, description, author, and creation date
|
||||
- format: Input format type and parsing options
|
||||
- globals: Variables, lookup tables, and constants
|
||||
- preprocessing: Filters and transformations applied before mapping
|
||||
- mappings: Field-by-field mapping from source to target with transformations and validations
|
||||
- postprocessing: Operations like deduplication or aggregation
|
||||
- output: Target format and error handling configuration
|
||||
|
||||
ANALYZE THE DATA:
|
||||
1. Identify the format (CSV, JSON, XML, etc.)
|
||||
2. Detect delimiters, encodings, and structure
|
||||
3. Find data types for each field
|
||||
4. Identify patterns and constraints
|
||||
5. Look for fields that need cleaning or transformation
|
||||
6. Find relationships between fields
|
||||
7. Identify lookup opportunities (codes that map to values)
|
||||
8. Detect required vs optional fields
|
||||
|
||||
CREATE THE DESCRIPTOR:
|
||||
For each field in the sample data:
|
||||
- Map it to an appropriate target field name
|
||||
- Add necessary transformations (trim, case conversion, type casting)
|
||||
- Include appropriate validations (required, patterns, ranges)
|
||||
- Set defaults for missing values
|
||||
|
||||
Include preprocessing if needed:
|
||||
- Filters to exclude invalid records
|
||||
- Sorting requirements
|
||||
|
||||
Include postprocessing if beneficial:
|
||||
- Deduplication on key fields
|
||||
- Aggregation for summary data
|
||||
|
||||
Configure output for TrustGraph:
|
||||
- format: "trustgraph-objects"
|
||||
- schema_name: Based on the data entity type
|
||||
- Appropriate error handling
|
||||
|
||||
DATA SAMPLE:
|
||||
[Insert data sample here]
|
||||
|
||||
ADDITIONAL CONTEXT (optional):
|
||||
- Target schema name: [if known]
|
||||
- Business rules: [any specific requirements]
|
||||
- Data quality issues to address: [known problems]
|
||||
|
||||
Generate a complete, valid Structured Data Descriptor configuration that will properly import this data into TrustGraph. Include comments explaining key decisions.
|
||||
```
|
||||
|
||||
### 示例用法提示
|
||||
|
||||
```
|
||||
I need you to analyze the provided data sample and create a Structured Data Descriptor configuration in JSON format.
|
||||
|
||||
[Standard instructions from above...]
|
||||
|
||||
DATA SAMPLE:
|
||||
```csv
|
||||
客户ID,姓名,电子邮件,年龄,国家,状态,加入日期,总购买额
|
||||
1001,"Smith, John",john.smith@email.com,35,美国,1,2023-01-15,5420.50
|
||||
1002,"doe, jane",JANE.DOE@GMAIL.COM,28,加拿大,1,2023-03-22,3200.00
|
||||
1003,"Bob Johnson",bob@,62,英国,0,2022-11-01,0
|
||||
1004,"Alice Chen","alice.chen@company.org",41,美国,1,2023-06-10,8900.25
|
||||
1005,,invalid-email,25,XX,1,2024-01-01,100
|
||||
```
|
||||
|
||||
ADDITIONAL CONTEXT:
|
||||
- Target schema name: customer
|
||||
- Business rules: Email should be valid and lowercase, names should be title case
|
||||
- Data quality issues: Some emails are invalid, some names are missing, country codes need mapping
|
||||
```
|
||||
|
||||
### 用于分析现有数据而无需样本的提示
|
||||
|
||||
```
|
||||
I need you to help me create a Structured Data Descriptor configuration for importing [data type] data.
|
||||
|
||||
The source data has these characteristics:
|
||||
- Format: [CSV/JSON/XML/etc]
|
||||
- Fields: [list the fields]
|
||||
- Data quality issues: [describe any known issues]
|
||||
- Volume: [approximate number of records]
|
||||
|
||||
Requirements:
|
||||
- [List any specific transformation needs]
|
||||
- [List any validation requirements]
|
||||
- [List any business rules]
|
||||
|
||||
Please generate a Structured Data Descriptor configuration that will:
|
||||
1. Parse the input format correctly
|
||||
2. Clean and standardize the data
|
||||
3. Validate according to the requirements
|
||||
4. Handle errors gracefully
|
||||
5. Output in TrustGraph ExtractedObject format
|
||||
|
||||
Focus on making the configuration robust and reusable.
|
||||
```
|
||||
147
docs/tech-specs/zh-cn/structured-data-schemas.zh-cn.md
Normal file
147
docs/tech-specs/zh-cn/structured-data-schemas.zh-cn.md
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
---
|
||||
layout: default
|
||||
title: "结构化数据 Pulsar 模式更改"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 结构化数据 Pulsar 模式更改
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
根据 `STRUCTURED_DATA.md` 规范,本文件提出必要的 Pulsar 模式添加和修改,以支持 TrustGraph 中的结构化数据功能。
|
||||
|
||||
## 必需的模式更改
|
||||
|
||||
### 1. 核心模式增强
|
||||
|
||||
#### 增强的字段定义
|
||||
现有的 `Field` 类在 `core/primitives.py` 中需要额外的属性:
|
||||
|
||||
```python
|
||||
class Field(Record):
|
||||
name = String()
|
||||
type = String() # int, string, long, bool, float, double, timestamp
|
||||
size = Integer()
|
||||
primary = Boolean()
|
||||
description = String()
|
||||
# 新字段:
|
||||
required = Boolean() # 字段是否必需
|
||||
enum_values = Array(String()) # 针对枚举类型的字段
|
||||
indexed = Boolean() # 字段是否应进行索引
|
||||
```
|
||||
|
||||
### 2. 新的知识模式
|
||||
|
||||
#### 2.1 结构化数据提交
|
||||
新的文件:`knowledge/structured.py`
|
||||
|
||||
```python
|
||||
from pulsar.schema import Record, String, Bytes, Map
|
||||
from ..core.metadata import Metadata
|
||||
|
||||
class StructuredDataSubmission(Record):
|
||||
metadata = Metadata()
|
||||
format = String() # "json", "csv", "xml"
|
||||
schema_name = String() # 引用配置中的模式
|
||||
data = Bytes() # 原始数据,用于导入
|
||||
options = Map(String()) # 格式特定的选项
|
||||
```
|
||||
|
||||
#### 2.2 结构化查询模式
|
||||
|
||||
#### 3.1 NLP 到结构化查询服务
|
||||
新的文件:`services/nlp_query.py`
|
||||
|
||||
```python
|
||||
from pulsar.schema import Record, String, Array, Map, Integer, Double
|
||||
from ..core.primitives import Error
|
||||
|
||||
class NLPToStructuredQueryRequest(Record):
|
||||
natural_language_query = String()
|
||||
max_results = Integer()
|
||||
context_hints = Map(String()) # 针对查询生成的可选上下文
|
||||
|
||||
class NLPToStructuredQueryResponse(Record):
|
||||
error = Error()
|
||||
graphql_query = String() # 生成的 GraphQL 查询
|
||||
variables = Map(String()) # 如果有的话,GraphQL 变量
|
||||
detected_schemas = Array(String()) # 查询的目标模式
|
||||
confidence = Double()
|
||||
```
|
||||
|
||||
#### 3.2 结构化查询服务
|
||||
新的文件:`services/structured_query.py`
|
||||
|
||||
```python
|
||||
from pulsar.schema import Record, String, Map, Array
|
||||
from ..core.primitives import Error
|
||||
|
||||
class StructuredQueryRequest(Record):
|
||||
query = String() # GraphQL 查询
|
||||
variables = Map(String()) # GraphQL 变量
|
||||
operation_name = String() # 针对多操作文档的可选操作名称
|
||||
|
||||
class StructuredQueryResponse(Record):
|
||||
error = Error()
|
||||
data = String() # JSON 编码的 GraphQL 响应数据
|
||||
errors = Array(String()) # 如果有的话,GraphQL 错误
|
||||
```
|
||||
|
||||
#### 2.2 对象提取输出
|
||||
新的文件:`knowledge/object.py`
|
||||
|
||||
```python
|
||||
from pulsar.schema import Record, String, Map, Double
|
||||
from ..core.metadata import Metadata
|
||||
|
||||
class ExtractedObject(Record):
|
||||
metadata = Metadata()
|
||||
schema_name = String() # 此对象属于哪个模式
|
||||
values = Map(String()) # 字段名称 -> 值
|
||||
confidence = Double()
|
||||
source_span = String() # 对象所在的文本范围
|
||||
```
|
||||
|
||||
### 4. 增强的知识模式
|
||||
|
||||
#### 4.1 对象嵌入增强
|
||||
更新 `knowledge/embeddings.py` 以更好地支持结构化对象嵌入:
|
||||
|
||||
```python
|
||||
class StructuredObjectEmbedding(Record):
|
||||
metadata = Metadata()
|
||||
vectors = Array(Array(Double()))
|
||||
schema_name = String()
|
||||
object_id = String() # 主键值
|
||||
field_embeddings = Map(Array(Double())) # 针对每个字段的嵌入
|
||||
```
|
||||
|
||||
## 集成点
|
||||
|
||||
### 流集成
|
||||
|
||||
这些模式将由新的流模块使用:
|
||||
- `trustgraph-flow/trustgraph/decoding/structured` - 使用 StructuredDataSubmission
|
||||
- `trustgraph-flow/trustgraph/query/nlp_query/cassandra` - 使用 NLP 查询模式
|
||||
- `trustgraph-flow/trustgraph/query/objects/cassandra` - 使用结构化查询模式
|
||||
- `trustgraph-flow/trustgraph/extract/object/row/` - 消耗 Chunk,产生 ExtractedObject
|
||||
- `trustgraph-flow/trustgraph/storage/objects/cassandra` - 使用 Rows 模式
|
||||
- `trustgraph-flow/trustgraph/embeddings/object_embeddings/qdrant` - 使用对象嵌入模式
|
||||
|
||||
## 实现说明
|
||||
|
||||
1. **模式版本控制**: 考虑为 RowSchema 添加 `version` 字段,以便进行未来迁移支持
|
||||
2. **类型系统**: `Field.type` 应该支持所有 Cassandra 原生类型
|
||||
3. **批量操作**: 大多数服务都应该支持单个和批量操作
|
||||
4. **错误处理**: 所有新服务的错误报告应保持一致
|
||||
5. **向后兼容性**: 现有的模式不受影响,只有字段进行了轻微增强
|
||||
|
||||
## 接下来要做的事
|
||||
|
||||
1. 在新的结构中实现模式文件
|
||||
2. 更新现有服务以识别新的模式类型
|
||||
3. 实现使用这些模式的流模块
|
||||
4. 为新的服务添加网关/反向网关端点
|
||||
5. 创建模式验证的单元测试
|
||||
260
docs/tech-specs/zh-cn/structured-data.zh-cn.md
Normal file
260
docs/tech-specs/zh-cn/structured-data.zh-cn.md
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
---
|
||||
layout: default
|
||||
title: "结构化数据技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 结构化数据技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了 TrustGraph 与结构化数据流的集成,使系统能够处理可以表示为表格中的行或对象存储中的对象的结构化数据。该集成支持四个主要用例:
|
||||
|
||||
1. **非结构化到结构化提取**: 读取非结构化数据源,识别和提取对象结构,并将它们存储在表格格式中。
|
||||
2. **结构化数据导入**: 将已经采用结构化格式的数据直接导入到结构化存储中,与提取的数据一起存储。
|
||||
3. **自然语言查询**: 将自然语言问题转换为结构化查询,以从存储中提取匹配的数据。
|
||||
4. **直接结构化查询**: 直接对数据存储执行结构化查询,以进行精确的数据检索。
|
||||
|
||||
## 目标
|
||||
|
||||
**统一数据访问**: 提供一个用于访问 TrustGraph 中结构化和非结构化数据的单一接口。
|
||||
**无缝集成**: 实现 TrustGraph 的基于图的知识表示与传统结构化数据格式之间的平滑互操作性。
|
||||
**灵活提取**: 支持从各种非结构化源(文档、文本等)自动提取结构化数据。
|
||||
**查询灵活性**: 允许用户使用自然语言和结构化查询语言查询数据。
|
||||
**数据一致性**: 维护不同数据表示形式之间的数据完整性和一致性。
|
||||
**性能优化**: 确保在大规模下高效地存储和检索结构化数据。
|
||||
**模式灵活性**: 支持“写时模式”和“读时模式”两种方法,以适应各种数据源。
|
||||
**向后兼容性**: 在添加结构化数据功能的同时,保留现有的 TrustGraph 功能。
|
||||
|
||||
## 背景
|
||||
|
||||
TrustGraph 目前擅长处理非结构化数据并从各种来源构建知识图。然而,许多企业用例涉及本质上是结构化的数据,例如客户记录、事务日志、库存数据库和其他表格数据集。这些结构化数据集通常需要与非结构化内容一起进行分析,以提供全面的见解。
|
||||
|
||||
当前的局限性包括:
|
||||
不支持导入预结构化数据格式(CSV、JSON 数组、数据库导出)。
|
||||
无法在从文档中提取表格数据时保留其固有的结构。
|
||||
缺乏用于结构化数据模式的有效查询机制。
|
||||
SQL 样式的查询与 TrustGraph 的图查询之间的桥梁缺失。
|
||||
|
||||
本规范通过引入补充 TrustGraph 现有功能的结构化数据层来解决这些差距。通过原生支持结构化数据,TrustGraph 可以:
|
||||
作为结构化和非结构化数据分析的统一平台。
|
||||
实现跨图关系和表格数据的混合查询。
|
||||
为习惯于处理结构化数据的用户提供熟悉的接口。
|
||||
释放数据集成和商业智能中的新用例。
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 架构
|
||||
|
||||
结构化数据集成需要以下技术组件:
|
||||
|
||||
1. **NLP-to-Structured-Query 服务**
|
||||
将自然语言问题转换为结构化查询。
|
||||
支持多种查询语言目标(最初为 SQL 样式的语法)。
|
||||
与现有的 TrustGraph NLP 功能集成。
|
||||
|
||||
模块: trustgraph-flow/trustgraph/query/nlp_query/cassandra
|
||||
|
||||
2. **配置模式支持** ✅ **[完成]**
|
||||
扩展配置系统以存储结构化数据模式。
|
||||
支持定义表结构、字段类型和关系。
|
||||
模式版本控制和迁移功能。
|
||||
|
||||
3. **对象提取模块** ✅ **[完成]**
|
||||
增强的知识提取流程集成。
|
||||
从非结构化源识别和提取结构化对象。
|
||||
维护溯源信息和置信度分数。
|
||||
注册一个配置处理程序(例如:trustgraph-flow/trustgraph/prompt/template/service.py),以接收配置数据并解码模式信息。
|
||||
接收对象并将其解码为 ExtractedObject 对象,并通过 Pulsar 队列进行传递。
|
||||
注意:存在于 `trustgraph-flow/trustgraph/extract/object/row/` 的现有代码。这是一个之前的尝试,需要进行重大重构,因为它不符合当前的 API。如果有用,可以使用它,否则从头开始。
|
||||
需要一个命令行界面:`kg-extract-objects`
|
||||
|
||||
模块: trustgraph-flow/trustgraph/extract/kg/objects/
|
||||
|
||||
4. **结构化存储写入模块** ✅ **[完成]**
|
||||
从 Pulsar 队列接收 ExtractedObject 格式的对象。
|
||||
初始实现针对 Apache Cassandra 作为结构化数据存储。
|
||||
处理基于遇到的模式动态创建表。
|
||||
管理模式到 Cassandra 表的映射以及数据转换。
|
||||
提供批量和流式写入操作以进行性能优化。
|
||||
没有 Pulsar 输出 - 这是一个数据流中的终端服务。
|
||||
|
||||
**模式处理**:
|
||||
监控传入的 ExtractedObject 消息以查找模式引用。
|
||||
首次遇到新模式时,自动创建相应的 Cassandra 表。
|
||||
维护已知模式的缓存,以避免重复的表创建尝试。
|
||||
应该考虑是直接接收模式定义,还是依赖于 ExtractedObject 消息中的模式名称。
|
||||
|
||||
**Cassandra 表映射**:
|
||||
键空间(Keyspace)的名称源自 ExtractedObject 的 Metadata 中的 `user` 字段。
|
||||
表的名称源自 ExtractedObject 中的 `schema_name` 字段。
|
||||
Metadata 中的集合(Collection)成为分区键的一部分,以确保:
|
||||
Cassandra 节点之间的数据自然分布。
|
||||
特定集合内的查询效率。
|
||||
不同数据导入/来源之间的逻辑隔离。
|
||||
主键结构:`PRIMARY KEY ((collection, <schema_primary_key_fields>), <clustering_keys>)`
|
||||
集合始终是分区键的第一部分。
|
||||
遵循定义的模式的主键字段作为组合分区键的一部分。
|
||||
这要求查询指定集合,以确保可预测的性能。
|
||||
字段定义映射到 Cassandra 列,并进行类型转换:
|
||||
`string` → `text`
|
||||
`integer` → `int` 或 `bigint`,具体取决于大小提示。
|
||||
`float` → `float` 或 `double`,具体取决于精度需求。
|
||||
`boolean` → `boolean`
|
||||
`timestamp` → `timestamp`
|
||||
`enum` → `text`,并进行应用程序级别的验证。
|
||||
索引字段创建 Cassandra 的二级索引(不包括主键中的字段)。
|
||||
必需字段在应用程序级别强制执行(Cassandra 不支持 NOT NULL)。
|
||||
|
||||
**对象存储**:
|
||||
从 ExtractedObject.values 映射中提取值。
|
||||
在插入之前执行类型转换和验证。
|
||||
优雅地处理缺失的可选字段。
|
||||
维护有关对象来源的元数据(源文档、置信度分数)。
|
||||
支持幂等写入,以处理消息重放场景。
|
||||
|
||||
**实现说明**:
|
||||
位于 `trustgraph-flow/trustgraph/storage/objects/cassandra/` 的现有代码已过时,不符合当前的 API。
|
||||
应该参考 `trustgraph-flow/trustgraph/storage/triples/cassandra` 作为工作存储处理器的示例。
|
||||
在决定重构或重写之前,需要评估现有代码以查找任何可重用组件。
|
||||
|
||||
模块:trustgraph-flow/trustgraph/storage/objects/cassandra
|
||||
|
||||
5. **结构化查询服务** ✅ **[完成]**
|
||||
接受定义格式的结构化查询。
|
||||
对结构化存储执行查询。
|
||||
返回与查询条件匹配的对象。
|
||||
支持分页和结果过滤。
|
||||
|
||||
模块:trustgraph-flow/trustgraph/query/objects/cassandra
|
||||
|
||||
6. **代理工具集成**
|
||||
用于代理框架的新工具类。
|
||||
使代理能够查询结构化数据存储。
|
||||
提供自然语言和结构化查询接口。
|
||||
与现有的代理决策过程集成。
|
||||
|
||||
7. **结构化数据摄取服务**
|
||||
接受多种格式(JSON、CSV、XML)的结构化数据。
|
||||
根据定义的模式解析和验证传入数据。
|
||||
将数据转换为规范化的对象流。
|
||||
将对象发送到适当的消息队列进行处理。
|
||||
支持批量上传和流式摄取。
|
||||
|
||||
模块:trustgraph-flow/trustgraph/decoding/structured
|
||||
|
||||
8. **对象嵌入服务**
|
||||
为结构化对象生成向量嵌入。
|
||||
启用结构化数据的语义搜索。
|
||||
支持结合结构化查询和语义相似性的混合搜索。
|
||||
与现有的向量存储集成。
|
||||
|
||||
模块:trustgraph-flow/trustgraph/embeddings/object_embeddings/qdrant
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### 模式存储机制
|
||||
|
||||
模式存储在 TrustGraph 的配置系统中使用以下结构:
|
||||
|
||||
**类型**: `schema`(所有结构化数据模式的固定值)
|
||||
**键**: 模式的唯一名称/标识符(例如,`customer_records`、`transaction_log`)
|
||||
**值**: 包含结构的 JSON 模式定义
|
||||
|
||||
示例配置条目:
|
||||
```
|
||||
Type: schema
|
||||
Key: customer_records
|
||||
Value: {
|
||||
"name": "customer_records",
|
||||
"description": "Customer information table",
|
||||
"fields": [
|
||||
{
|
||||
"name": "customer_id",
|
||||
"type": "string",
|
||||
"primary_key": true
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"type": "string",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "registration_date",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"type": "string",
|
||||
"enum": ["active", "inactive", "suspended"]
|
||||
}
|
||||
],
|
||||
"indexes": ["email", "registration_date"]
|
||||
}
|
||||
```
|
||||
|
||||
此方法允许:
|
||||
动态模式定义,无需代码更改
|
||||
轻松的模式更新和版本控制
|
||||
与现有 TrustGraph 配置管理的一致集成
|
||||
支持在单个部署中使用的多个模式
|
||||
|
||||
### API
|
||||
|
||||
新增 API:
|
||||
用于上述类型的 Pulsar 模式
|
||||
新流程中的 Pulsar 接口
|
||||
需要一种方法来指定模式类型,以便流程知道要加载哪些
|
||||
模式类型
|
||||
向网关和反向网关添加了 API
|
||||
|
||||
修改后的 API:
|
||||
知识提取端点 - 添加结构化对象输出选项
|
||||
代理端点 - 添加结构化数据工具支持
|
||||
|
||||
### 实施细节
|
||||
|
||||
遵循现有约定 - 这些只是新的处理模块。
|
||||
除了 trustgraph-base 中的模式项之外,所有内容都位于 trustgraph-flow 包中。
|
||||
|
||||
|
||||
需要在 Workbench 中进行一些 UI 工作,以便演示/试用此
|
||||
功能。
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
没有额外的注意事项。
|
||||
|
||||
## 性能注意事项
|
||||
|
||||
关于使用 Cassandra 查询和索引的一些问题,以确保查询
|
||||
不会降低速度。
|
||||
|
||||
## 测试策略
|
||||
|
||||
使用现有的测试策略,将构建单元测试、契约测试和集成测试。
|
||||
|
||||
## 迁移计划
|
||||
|
||||
无。
|
||||
|
||||
## 时间线
|
||||
|
||||
未指定。
|
||||
|
||||
## 开放问题
|
||||
|
||||
是否可以使其与其它存储类型一起工作? 我们的目标是使用
|
||||
接口,使适用于一种存储的模块也适用于
|
||||
其他存储。
|
||||
|
||||
## 参考文献
|
||||
|
||||
n/a。
|
||||
281
docs/tech-specs/zh-cn/structured-diag-service.zh-cn.md
Normal file
281
docs/tech-specs/zh-cn/structured-diag-service.zh-cn.md
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
---
|
||||
layout: default
|
||||
title: "结构化数据诊断服务技术规范"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 结构化数据诊断服务技术规范
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本规范描述了一种新的可调用服务,用于诊断和分析 TrustGraph 中的结构化数据。该服务从现有的 `tg-load-structured-data` 命令行工具中提取功能,并将其暴露为请求/响应服务,从而实现对数据类型检测和描述符生成功能的编程访问。
|
||||
|
||||
该服务支持三种主要操作:
|
||||
|
||||
1. **数据类型检测**: 分析数据样本以确定其格式(CSV、JSON 或 XML)
|
||||
2. **描述符生成**: 为给定的数据样本和类型生成 TrustGraph 结构化数据描述符
|
||||
3. **综合诊断**: 依次执行数据类型检测和描述符生成
|
||||
|
||||
## 目标
|
||||
|
||||
**模块化数据分析**: 将数据诊断逻辑从 CLI 提取到可重用的服务组件中
|
||||
**启用编程访问**: 提供基于 API 的访问数据分析能力
|
||||
**支持多种数据格式**: 始终如一地处理 CSV、JSON 和 XML 数据格式
|
||||
**生成准确的描述符**: 生成准确映射源数据到 TrustGraph 模式的结构化数据描述符
|
||||
**保持向后兼容性**: 确保现有的 CLI 功能继续正常工作
|
||||
**启用服务组合**: 允许其他服务利用数据诊断能力
|
||||
**提高可测试性**: 将业务逻辑与 CLI 接口分离,以获得更好的测试效果
|
||||
**支持流式分析**: 允许分析数据样本,而无需加载整个文件
|
||||
|
||||
## 背景
|
||||
|
||||
目前,`tg-load-structured-data` 命令提供了用于分析结构化数据和生成描述符的全面功能。但是,此功能与 CLI 接口紧密耦合,限制了其可重用性。
|
||||
|
||||
当前的限制包括:
|
||||
数据诊断逻辑嵌入在 CLI 代码中
|
||||
没有对数据类型检测和描述符生成的编程访问
|
||||
难以将诊断能力集成到其他服务中
|
||||
难以组合数据分析工作流程
|
||||
|
||||
本规范通过创建一个专用的结构化数据诊断服务来解决这些差距。通过将这些功能暴露为服务,TrustGraph 可以:
|
||||
允许其他服务以编程方式分析数据
|
||||
支持更复杂的数据处理管道
|
||||
促进与外部系统的集成
|
||||
通过分离关注点来提高可维护性
|
||||
|
||||
## 技术设计
|
||||
|
||||
### 架构
|
||||
|
||||
结构化数据诊断服务需要以下技术组件:
|
||||
|
||||
1. **诊断服务处理器**
|
||||
处理传入的诊断请求
|
||||
协调数据类型检测和描述符生成
|
||||
返回包含诊断结果的结构化响应
|
||||
|
||||
模块:`trustgraph-flow/trustgraph/diagnosis/structured_data/service.py`
|
||||
|
||||
2. **数据类型检测器**
|
||||
使用算法检测来识别数据格式(CSV、JSON、XML)
|
||||
分析数据结构、分隔符和语法模式
|
||||
返回检测到的格式和置信度分数
|
||||
|
||||
模块:`trustgraph-flow/trustgraph/diagnosis/structured_data/type_detector.py`
|
||||
|
||||
3. **描述符生成器**
|
||||
使用提示服务生成描述符
|
||||
调用特定于格式的提示(diagnose-csv、diagnose-json、diagnose-xml)
|
||||
通过提示响应将数据字段映射到 TrustGraph 模式字段
|
||||
|
||||
模块:`trustgraph-flow/trustgraph/diagnosis/structured_data/descriptor_generator.py`
|
||||
|
||||
### 数据模型
|
||||
|
||||
#### StructuredDataDiagnosisRequest
|
||||
|
||||
结构化数据诊断操作的请求消息:
|
||||
|
||||
```python
|
||||
class StructuredDataDiagnosisRequest:
|
||||
operation: str # "detect-type", "generate-descriptor", or "diagnose"
|
||||
sample: str # Data sample to analyze (text content)
|
||||
type: Optional[str] # Data type (csv, json, xml) - required for generate-descriptor
|
||||
schema_name: Optional[str] # Target schema name for descriptor generation
|
||||
options: Dict[str, Any] # Additional options (e.g., delimiter for CSV)
|
||||
```
|
||||
|
||||
#### 结构化数据诊断响应
|
||||
|
||||
包含诊断结果的响应消息:
|
||||
|
||||
```python
|
||||
class StructuredDataDiagnosisResponse:
|
||||
operation: str # The operation that was performed
|
||||
detected_type: Optional[str] # Detected data type (for detect-type/diagnose)
|
||||
confidence: Optional[float] # Confidence score for type detection
|
||||
descriptor: Optional[Dict] # Generated descriptor (for generate-descriptor/diagnose)
|
||||
error: Optional[str] # Error message if operation failed
|
||||
metadata: Dict[str, Any] # Additional metadata (e.g., field count, sample records)
|
||||
```
|
||||
|
||||
#### 描述符结构
|
||||
|
||||
生成的描述符遵循现有的结构化数据描述符格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"format": {
|
||||
"type": "csv",
|
||||
"encoding": "utf-8",
|
||||
"options": {
|
||||
"delimiter": ",",
|
||||
"has_header": true
|
||||
}
|
||||
},
|
||||
"mappings": [
|
||||
{
|
||||
"source_field": "customer_id",
|
||||
"target_field": "id",
|
||||
"transforms": [
|
||||
{"type": "trim"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"output": {
|
||||
"schema_name": "customer",
|
||||
"options": {
|
||||
"batch_size": 1000,
|
||||
"confidence": 0.9
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 服务接口
|
||||
|
||||
该服务将通过请求/响应模式提供以下操作:
|
||||
|
||||
1. **类型检测操作**
|
||||
输入:数据样本
|
||||
处理:使用算法检测分析数据结构
|
||||
输出:检测到的类型及其置信度分数
|
||||
|
||||
2. **描述符生成操作**
|
||||
输入:数据样本、类型、目标模式名称
|
||||
处理:
|
||||
使用特定格式的提示 ID(diagnose-csv、diagnose-json 或 diagnose-xml)调用提示服务
|
||||
将数据样本和可用模式传递给提示
|
||||
从提示响应接收生成的描述符
|
||||
输出:结构化数据描述符
|
||||
|
||||
3. **综合诊断操作**
|
||||
输入:数据样本、可选模式名称
|
||||
处理:
|
||||
首先使用算法检测识别格式
|
||||
根据检测到的类型选择适当的特定格式的提示
|
||||
调用提示服务以生成描述符
|
||||
输出:检测到的类型和描述符
|
||||
|
||||
### 实现细节
|
||||
|
||||
该服务将遵循 TrustGraph 服务约定:
|
||||
|
||||
1. **服务注册**
|
||||
注册为 `structured-diag` 服务类型
|
||||
使用标准的请求/响应主题
|
||||
实现 FlowProcessor 基础类
|
||||
注册 PromptClientSpec 以进行提示服务交互
|
||||
|
||||
2. **配置管理**
|
||||
通过配置服务访问模式配置
|
||||
缓存模式以提高性能
|
||||
动态处理配置更新
|
||||
|
||||
3. **提示集成**
|
||||
使用现有的提示服务基础设施
|
||||
使用特定格式的提示 ID调用提示服务:
|
||||
`diagnose-csv`:用于 CSV 数据分析
|
||||
`diagnose-json`:用于 JSON 数据分析
|
||||
`diagnose-xml`:用于 XML 数据分析
|
||||
提示配置在提示配置中,而不是硬编码在服务中
|
||||
将模式和数据样本作为提示变量传递
|
||||
解析提示响应以提取描述符
|
||||
|
||||
4. **错误处理**
|
||||
验证输入数据样本
|
||||
提供描述性的错误消息
|
||||
优雅地处理格式错误的数据
|
||||
处理提示服务故障
|
||||
|
||||
5. **数据采样**
|
||||
处理可配置的样本大小
|
||||
适当处理不完整的记录
|
||||
保持采样的一致性
|
||||
|
||||
### API 集成
|
||||
|
||||
该服务将与现有的 TrustGraph API 集成:
|
||||
|
||||
修改的组件:
|
||||
`tg-load-structured-data` CLI - 重新设计为使用新的服务进行诊断操作
|
||||
Flow API - 扩展以支持结构化数据诊断请求
|
||||
|
||||
新的服务端点:
|
||||
`/api/v1/flow/{flow}/diagnose/structured-data` - 用于诊断请求的 WebSocket 端点
|
||||
`/api/v1/diagnose/structured-data` - 用于同步诊断的 REST 端点
|
||||
|
||||
### 消息流
|
||||
|
||||
```
|
||||
Client → Gateway → Structured Diag Service → Config Service (for schemas)
|
||||
↓
|
||||
Type Detector (algorithmic)
|
||||
↓
|
||||
Prompt Service (diagnose-csv/json/xml)
|
||||
↓
|
||||
Descriptor Generator (parses prompt response)
|
||||
↓
|
||||
Client ← Gateway ← Structured Diag Service (response)
|
||||
```
|
||||
|
||||
## 安全注意事项
|
||||
|
||||
输入验证,以防止注入攻击
|
||||
对数据样本的大小设置限制,以防止拒绝服务 (DoS) 攻击
|
||||
清理生成的描述符
|
||||
通过现有的 TrustGraph 身份验证进行访问控制
|
||||
|
||||
## 性能注意事项
|
||||
|
||||
缓存模式定义,以减少对配置服务的调用
|
||||
限制样本大小,以保持响应性能
|
||||
对大型数据样本使用流式处理
|
||||
实施超时机制,用于长时间运行的分析
|
||||
|
||||
## 测试策略
|
||||
|
||||
1. **单元测试**
|
||||
对各种数据格式进行类型检测
|
||||
描述符生成准确性
|
||||
错误处理场景
|
||||
|
||||
2. **集成测试**
|
||||
服务请求/响应流程
|
||||
模式检索和缓存
|
||||
CLI 集成
|
||||
|
||||
3. **性能测试**
|
||||
处理大型样本
|
||||
并发请求处理
|
||||
在负载下的内存使用情况
|
||||
|
||||
## 迁移计划
|
||||
|
||||
1. **第一阶段**: 实施具有核心功能的服务
|
||||
2. **第二阶段**: 重构 CLI 以使用服务(保持向后兼容性)
|
||||
3. **第三阶段**: 添加 REST API 端点
|
||||
4. **第四阶段**: 弃用嵌入式 CLI 逻辑(提前通知)
|
||||
|
||||
## 时间表
|
||||
|
||||
第 1-2 周:实施核心服务和类型检测
|
||||
第 3-4 周:添加描述符生成和集成
|
||||
第 5 周:测试和文档
|
||||
第 6 周:CLI 重构和迁移
|
||||
|
||||
## 开放问题
|
||||
|
||||
该服务是否应支持其他数据格式(例如,Parquet、Avro)?
|
||||
分析的最大样本大小应为多少?
|
||||
诊断结果是否应针对重复请求进行缓存?
|
||||
该服务应如何处理多模式场景?
|
||||
提示 ID 是否应为服务的可配置参数?
|
||||
|
||||
## 参考文献
|
||||
|
||||
[结构化数据描述符规范](structured-data-descriptor.md)
|
||||
[结构化数据加载文档](structured-data.md)
|
||||
`tg-load-structured-data` 实现:`trustgraph-cli/trustgraph/cli/load_structured_data.py`
|
||||
499
docs/tech-specs/zh-cn/tool-group.zh-cn.md
Normal file
499
docs/tech-specs/zh-cn/tool-group.zh-cn.md
Normal file
|
|
@ -0,0 +1,499 @@
|
|||
---
|
||||
layout: default
|
||||
title: "TrustGraph 工具组系统"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# TrustGraph 工具组系统
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
## 技术规范 v1.0
|
||||
|
||||
### 摘要
|
||||
|
||||
本规范定义了一个用于 TrustGraph 代理的工具分组系统,该系统允许对哪些工具可用于特定请求进行细粒度控制。该系统通过配置和请求级别的指定,引入基于组的工具过滤,从而实现更好的安全边界、资源管理以及代理功能的划分。
|
||||
|
||||
### 1. 概述
|
||||
|
||||
#### 1.1 问题陈述
|
||||
|
||||
目前,TrustGraph 代理可以访问所有配置的工具,而与请求上下文或安全要求无关。这带来了一些挑战:
|
||||
|
||||
**安全风险**: 即使是只读查询,也可能访问到敏感工具(例如,数据修改)。
|
||||
**资源浪费**: 即使简单的查询也不需要,也会加载复杂的工具。
|
||||
**功能混淆**: 代理可能会选择不合适的工具,而存在更简单的替代方案。
|
||||
**多租户隔离**: 不同的用户组需要访问不同的工具集。
|
||||
|
||||
#### 1.2 解决方案概述
|
||||
|
||||
工具分组系统引入了:
|
||||
|
||||
1. **组分类**: 工具在配置时会被标记为所属的组。
|
||||
2. **请求级别过滤**: AgentRequest 指定允许使用的工具组。
|
||||
3. **运行时强制**: 代理只能访问与请求的组匹配的工具。
|
||||
4. **灵活分组**: 工具可以属于多个组,以适应复杂的场景。
|
||||
|
||||
### 2. 模式变更
|
||||
|
||||
#### 2.1 工具配置模式增强
|
||||
|
||||
现有的工具配置通过添加一个 `group` 字段进行增强:
|
||||
|
||||
**之前:**
|
||||
```json
|
||||
{
|
||||
"name": "knowledge-query",
|
||||
"type": "knowledge-query",
|
||||
"description": "Query the knowledge graph"
|
||||
}
|
||||
```
|
||||
|
||||
**翻译后:**
|
||||
```json
|
||||
{
|
||||
"name": "knowledge-query",
|
||||
"type": "knowledge-query",
|
||||
"description": "Query the knowledge graph",
|
||||
"group": ["read-only", "knowledge", "basic"]
|
||||
}
|
||||
```
|
||||
|
||||
**组字段规范:**
|
||||
`group`: Array(String) - 此工具所属的组列表
|
||||
**可选:** 没有组字段的工具属于 "默认" 组
|
||||
**多重隶属:** 工具可以属于多个组
|
||||
**区分大小写:** 组名必须是完全匹配的字符串
|
||||
|
||||
#### 2.1.2 工具状态转换增强
|
||||
|
||||
工具可以选择性地指定状态转换和基于状态的可用性:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "knowledge-query",
|
||||
"type": "knowledge-query",
|
||||
"description": "Query the knowledge graph",
|
||||
"group": ["read-only", "knowledge", "basic"],
|
||||
"state": "analysis",
|
||||
"available_in_states": ["undefined", "research"]
|
||||
}
|
||||
```
|
||||
|
||||
**状态字段规范:**
|
||||
`state`: String - **可选** - 成功执行工具后要转换到的状态
|
||||
`available_in_states`: Array(String) - **可选** - 此工具可用的状态
|
||||
**默认行为:** 没有 `available_in_states` 的工具在所有状态下都可用
|
||||
**状态转换:** 仅在成功执行工具后发生
|
||||
|
||||
#### 2.2 AgentRequest 模式增强
|
||||
|
||||
`trustgraph-base/trustgraph/schema/services/agent.py` 中的 `AgentRequest` 模式已增强:
|
||||
|
||||
**当前 AgentRequest:**
|
||||
`question`: String - 用户查询
|
||||
`plan`: String - 执行计划(可以删除)
|
||||
`state`: String - Agent 状态
|
||||
`history`: Array(AgentStep) - 执行历史
|
||||
|
||||
**增强后的 AgentRequest:**
|
||||
`question`: String - 用户查询
|
||||
`state`: String - Agent 执行状态(现在被积极用于工具过滤)
|
||||
`history`: Array(AgentStep) - 执行历史
|
||||
`group`: Array(String) - **新增** - 此请求允许的工具组
|
||||
|
||||
**模式变更:**
|
||||
**已移除:** `plan` 字段不再需要,可以删除(最初用于工具规范)
|
||||
**已添加:** `group` 字段用于工具组规范
|
||||
**已增强:** `state` 字段现在控制执行期间的工具可用性
|
||||
|
||||
**字段行为:**
|
||||
|
||||
**组字段:**
|
||||
**可选:** 如果未指定,默认为 ["default"]
|
||||
**交集:** 只有匹配至少一个指定组的工具才可用
|
||||
**空数组:** 没有可用工具(agent 只能使用内部推理)
|
||||
**通配符:** 特殊组 "*" 授予访问所有工具的权限
|
||||
|
||||
**状态字段:**
|
||||
**可选:** 如果未指定,默认为 "undefined"
|
||||
**基于状态的过滤:** 只有在当前状态下可用的工具才有资格
|
||||
**默认状态:** "undefined" 状态允许所有工具(受组过滤限制)
|
||||
**状态转换:** 成功执行后,工具可以更改状态
|
||||
|
||||
### 3. 自定义组示例
|
||||
|
||||
组织可以定义特定领域的组:
|
||||
|
||||
```json
|
||||
{
|
||||
"financial-tools": ["stock-query", "portfolio-analysis"],
|
||||
"medical-tools": ["diagnosis-assist", "drug-interaction"],
|
||||
"legal-tools": ["contract-analysis", "case-search"]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 实现细节
|
||||
|
||||
#### 4.1 工具加载和过滤
|
||||
|
||||
**配置阶段:**
|
||||
1. 所有工具从配置文件中加载,并带有其组分配信息。
|
||||
2. 没有明确组分配的工具将被分配到 "默认" 组。
|
||||
3. 组成员关系将被验证并存储在工具注册表中。
|
||||
|
||||
**请求处理阶段:**
|
||||
1. AgentRequest 携带可选的组指定信息。
|
||||
2. Agent 根据组的交集过滤可用的工具。
|
||||
3. 只有匹配的工具才会被传递到 Agent 执行上下文。
|
||||
4. Agent 在整个请求生命周期内都使用过滤后的工具集。
|
||||
|
||||
#### 4.2 工具过滤逻辑
|
||||
|
||||
**组合组和状态过滤:**
|
||||
|
||||
```
|
||||
For each configured tool:
|
||||
tool_groups = tool.group || ["default"]
|
||||
tool_states = tool.available_in_states || ["*"] // Available in all states
|
||||
|
||||
For each request:
|
||||
requested_groups = request.group || ["default"]
|
||||
current_state = request.state || "undefined"
|
||||
|
||||
Tool is available if:
|
||||
// Group filtering
|
||||
(intersection(tool_groups, requested_groups) is not empty OR "*" in requested_groups)
|
||||
AND
|
||||
// State filtering
|
||||
(current_state in tool_states OR "*" in tool_states)
|
||||
```
|
||||
|
||||
**状态转换逻辑:**
|
||||
|
||||
```
|
||||
After successful tool execution:
|
||||
if tool.state is defined:
|
||||
next_request.state = tool.state
|
||||
else:
|
||||
next_request.state = current_request.state // No change
|
||||
```
|
||||
|
||||
#### 4.3 代理集成点
|
||||
|
||||
**ReAct 代理:**
|
||||
工具过滤在 `agent_manager.py` 中在工具注册创建期间发生。
|
||||
可用工具列表在计划生成之前,会根据组和状态进行过滤。
|
||||
状态转换会在工具执行成功后更新 `AgentRequest.state` 字段。
|
||||
下一次迭代使用更新后的状态进行工具过滤。
|
||||
|
||||
**基于置信度的代理:**
|
||||
工具过滤在 `planner.py` 中在计划生成期间发生。
|
||||
`ExecutionStep` 验证确保只使用组+状态符合条件的工具。
|
||||
流程控制器在运行时强制执行工具可用性。
|
||||
状态转换由流程控制器在步骤之间管理。
|
||||
|
||||
### 5. 配置示例
|
||||
|
||||
#### 5.1 带有组和状态的工具配置
|
||||
|
||||
```yaml
|
||||
tool:
|
||||
knowledge-query:
|
||||
type: knowledge-query
|
||||
name: "Knowledge Graph Query"
|
||||
description: "Query the knowledge graph for entities and relationships"
|
||||
group: ["read-only", "knowledge", "basic"]
|
||||
state: "analysis"
|
||||
available_in_states: ["undefined", "research"]
|
||||
|
||||
graph-update:
|
||||
type: graph-update
|
||||
name: "Graph Update"
|
||||
description: "Add or modify entities in the knowledge graph"
|
||||
group: ["write", "knowledge", "admin"]
|
||||
available_in_states: ["analysis", "modification"]
|
||||
|
||||
text-completion:
|
||||
type: text-completion
|
||||
name: "Text Completion"
|
||||
description: "Generate text using language models"
|
||||
group: ["read-only", "text", "basic"]
|
||||
state: "undefined"
|
||||
# No available_in_states = available in all states
|
||||
|
||||
complex-analysis:
|
||||
type: mcp-tool
|
||||
name: "Complex Analysis Tool"
|
||||
description: "Perform complex data analysis"
|
||||
group: ["advanced", "compute", "expensive"]
|
||||
state: "results"
|
||||
available_in_states: ["analysis"]
|
||||
mcp_tool_id: "analysis-server"
|
||||
|
||||
reset-workflow:
|
||||
type: mcp-tool
|
||||
name: "Reset Workflow"
|
||||
description: "Reset to initial state"
|
||||
group: ["admin"]
|
||||
state: "undefined"
|
||||
available_in_states: ["analysis", "results"]
|
||||
```
|
||||
|
||||
#### 5.2 请求示例与状态工作流
|
||||
|
||||
**初始研究请求:**
|
||||
```json
|
||||
{
|
||||
"question": "What entities are connected to Company X?",
|
||||
"group": ["read-only", "knowledge"],
|
||||
"state": "undefined"
|
||||
}
|
||||
```
|
||||
*可用工具:知识查询,文本补全*
|
||||
*知识查询后:状态 → "分析"*
|
||||
|
||||
**分析阶段:**
|
||||
```json
|
||||
{
|
||||
"question": "Continue analysis based on previous results",
|
||||
"group": ["advanced", "compute", "write"],
|
||||
"state": "analysis"
|
||||
}
|
||||
```
|
||||
*可用工具:复杂分析、图更新、重置工作流程*
|
||||
*复杂分析之后:状态 → "结果"*
|
||||
|
||||
**结果阶段:**
|
||||
```json
|
||||
{
|
||||
"question": "What should I do with these results?",
|
||||
"group": ["admin"],
|
||||
"state": "results"
|
||||
}
|
||||
```
|
||||
*可用的工具:仅限 reset-workflow*
|
||||
*reset-workflow 之后:状态 → "undefined"*
|
||||
|
||||
**工作流程示例 - 完整流程:**
|
||||
1. **开始 (undefined)**:使用 knowledge-query → 转换到 "analysis"
|
||||
2. **分析状态:** 使用 complex-analysis → 转换到 "results"
|
||||
3. **结果状态:** 使用 reset-workflow → 转换回 "undefined"
|
||||
4. **返回开始:** 所有初始工具再次可用
|
||||
|
||||
### 6. 安全注意事项
|
||||
|
||||
#### 6.1 访问控制集成
|
||||
|
||||
**网关级别的过滤:**
|
||||
网关可以根据用户权限强制执行组限制
|
||||
防止通过请求修改来提升权限
|
||||
审计跟踪包括请求和授予的工具组
|
||||
|
||||
**示例网关逻辑:**
|
||||
```
|
||||
user_permissions = get_user_permissions(request.user_id)
|
||||
allowed_groups = user_permissions.tool_groups
|
||||
requested_groups = request.group
|
||||
|
||||
# Validate request doesn't exceed permissions
|
||||
if not is_subset(requested_groups, allowed_groups):
|
||||
reject_request("Insufficient permissions for requested tool groups")
|
||||
```
|
||||
|
||||
#### 6.2 审计和监控
|
||||
|
||||
**增强的审计跟踪:**
|
||||
记录每个请求所请求的工具组和初始状态
|
||||
跟踪按组成员划分的状态转换和工具使用情况
|
||||
监控未经授权的组访问尝试和无效的状态转换
|
||||
警报不寻常的组使用模式或可疑的状态工作流程
|
||||
|
||||
### 7. 迁移策略
|
||||
|
||||
#### 7.1 向后兼容性
|
||||
|
||||
**第一阶段:增量更改**
|
||||
向工具配置添加可选的 `group` 字段
|
||||
向 AgentRequest 模式添加可选的 `group` 字段
|
||||
默认行为:所有现有工具都属于 "默认" 组
|
||||
现有请求如果没有组字段,则使用 "默认" 组
|
||||
|
||||
**保留现有行为:**
|
||||
没有组配置的工具继续工作(默认组)
|
||||
没有状态配置的工具在所有状态下都可用
|
||||
没有组指定的请求可以访问所有工具(默认组)
|
||||
没有状态指定的请求使用 "未定义" 状态(所有工具可用)
|
||||
没有对现有部署进行破坏性更改
|
||||
|
||||
### 8. 监控和可观察性
|
||||
|
||||
#### 8.1 新指标
|
||||
|
||||
**工具组使用情况:**
|
||||
`agent_tool_group_requests_total` - 按组划分的请求计数器
|
||||
`agent_tool_group_availability` - 每个组可用的工具计量
|
||||
`agent_filtered_tools_count` - 组+状态过滤后工具计数的直方图
|
||||
|
||||
**状态工作流程指标:**
|
||||
`agent_state_transitions_total` - 按工具划分的状态转换计数器
|
||||
`agent_workflow_duration_seconds` - 每个状态花费时间的直方图
|
||||
`agent_state_availability` - 每个状态可用的工具计量
|
||||
|
||||
**安全指标:**
|
||||
`agent_group_access_denied_total` - 未经授权的组访问计数器
|
||||
`agent_invalid_state_transition_total` - 无效状态转换计数器
|
||||
`agent_privilege_escalation_attempts_total` - 具有可疑请求的计数器
|
||||
|
||||
#### 8.2 日志增强
|
||||
|
||||
**请求日志记录:**
|
||||
```json
|
||||
{
|
||||
"request_id": "req-123",
|
||||
"requested_groups": ["read-only", "knowledge"],
|
||||
"initial_state": "undefined",
|
||||
"state_transitions": [
|
||||
{"tool": "knowledge-query", "from": "undefined", "to": "analysis", "timestamp": "2024-01-01T10:00:01Z"}
|
||||
],
|
||||
"available_tools": ["knowledge-query", "text-completion"],
|
||||
"filtered_by_group": ["graph-update", "admin-tool"],
|
||||
"filtered_by_state": [],
|
||||
"execution_time": "1.2s"
|
||||
}
|
||||
```
|
||||
|
||||
### 9. 测试策略
|
||||
|
||||
#### 9.1 单元测试
|
||||
|
||||
**工具过滤逻辑:**
|
||||
测试组的交集计算
|
||||
测试基于状态的过滤逻辑
|
||||
验证默认组和状态的分配
|
||||
测试通配符组的行为
|
||||
验证空组的处理
|
||||
测试组合的组+状态过滤场景
|
||||
|
||||
**配置验证:**
|
||||
测试使用各种组和状态配置加载工具
|
||||
验证无效的组和状态规范的模式验证
|
||||
测试与现有配置的向后兼容性
|
||||
验证状态转换的定义和循环
|
||||
|
||||
#### 9.2 集成测试
|
||||
|
||||
**代理行为:**
|
||||
验证代理只看到组+状态过滤的工具
|
||||
测试使用各种组组合的请求执行
|
||||
测试代理执行期间的状态转换
|
||||
验证在没有可用工具时错误处理
|
||||
测试通过多个状态的工作流程进度
|
||||
|
||||
**安全测试:**
|
||||
测试特权提升预防
|
||||
验证审计跟踪的准确性
|
||||
测试网关与用户权限的集成
|
||||
|
||||
#### 9.3 完整场景
|
||||
|
||||
**具有状态工作流程的多租户使用:**
|
||||
```
|
||||
Scenario: Different users with different tool access and workflow states
|
||||
Given: User A has "read-only" permissions, state "undefined"
|
||||
And: User B has "write" permissions, state "analysis"
|
||||
When: Both request knowledge operations
|
||||
Then: User A gets read-only tools available in "undefined" state
|
||||
And: User B gets write tools available in "analysis" state
|
||||
And: State transitions are tracked per user session
|
||||
And: All usage and transitions are properly audited
|
||||
```
|
||||
|
||||
**工作流程状态演进:**
|
||||
```
|
||||
Scenario: Complete workflow execution
|
||||
Given: Request with groups ["knowledge", "compute"] and state "undefined"
|
||||
When: Agent executes knowledge-query tool (transitions to "analysis")
|
||||
And: Agent executes complex-analysis tool (transitions to "results")
|
||||
And: Agent executes reset-workflow tool (transitions to "undefined")
|
||||
Then: Each step has correctly filtered available tools
|
||||
And: State transitions are logged with timestamps
|
||||
And: Final state allows initial workflow to repeat
|
||||
```
|
||||
|
||||
### 10. 性能考虑
|
||||
|
||||
#### 10.1 工具加载的影响
|
||||
|
||||
**配置加载:**
|
||||
组和状态元数据在启动时加载一次
|
||||
每个工具的内存开销很小(附加字段)
|
||||
不会影响工具的初始化时间
|
||||
|
||||
**请求处理:**
|
||||
组+状态过滤的组合在每个请求中执行一次
|
||||
复杂度为 O(n),其中 n = 配置的工具数量
|
||||
状态转换会增加微小的开销(字符串赋值)
|
||||
对于典型的工具数量(< 100),影响可以忽略不计
|
||||
|
||||
#### 10.2 优化策略
|
||||
|
||||
**预计算的工具集:**
|
||||
根据组+状态组合缓存工具集
|
||||
避免重复过滤常见的组/状态模式
|
||||
内存与计算之间的权衡,适用于经常使用的组合
|
||||
|
||||
**延迟加载:**
|
||||
仅在需要时才加载工具实现
|
||||
减少具有许多工具的部署的启动时间
|
||||
基于组需求的动态工具注册
|
||||
|
||||
### 11. 未来增强功能
|
||||
|
||||
#### 11.1 动态组分配
|
||||
|
||||
**基于上下文的组:**
|
||||
根据请求上下文将工具分配到组
|
||||
基于时间的组可用性(仅在工作时间内)
|
||||
基于负载的组限制(在低使用情况下限制昂贵的工具)
|
||||
|
||||
#### 11.2 组层级结构
|
||||
|
||||
**嵌套的组结构:**
|
||||
```json
|
||||
{
|
||||
"knowledge": {
|
||||
"read": ["knowledge-query", "entity-search"],
|
||||
"write": ["graph-update", "entity-create"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 11.3 工具推荐
|
||||
|
||||
**基于组的建议:**
|
||||
针对请求类型,推荐最佳工具组。
|
||||
从使用模式中学习,以改进推荐。
|
||||
在首选工具不可用时,提供备用组。
|
||||
|
||||
### 12. 开放性问题
|
||||
|
||||
1. **组验证:** 请求中无效的组名是否应导致硬性错误或警告?
|
||||
|
||||
2. **组发现:** 系统是否应提供一个 API 来列出可用的组及其工具?
|
||||
|
||||
3. **动态组:** 组是否应在运行时配置,或者仅在启动时配置?
|
||||
|
||||
4. **组继承:** 工具是否应从其父类别或实现继承组?
|
||||
|
||||
5. **性能监控:** 需要哪些额外的指标来有效跟踪基于组的工具使用情况?
|
||||
|
||||
### 13. 结论
|
||||
|
||||
工具组系统提供:
|
||||
|
||||
**安全性:** 对代理功能进行细粒度的访问控制。
|
||||
**性能:** 减少工具加载和选择开销。
|
||||
**灵活性:** 多维工具分类。
|
||||
**兼容性:** 与现有代理架构无缝集成。
|
||||
|
||||
该系统使 TrustGraph 部署能够更好地管理工具访问,提高安全边界,并优化资源利用率,同时与现有的配置和请求完全兼容。
|
||||
479
docs/tech-specs/zh-cn/tool-services.zh-cn.md
Normal file
479
docs/tech-specs/zh-cn/tool-services.zh-cn.md
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
---
|
||||
layout: default
|
||||
title: "工具服务:动态可插拔的代理工具"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 工具服务:动态可插拔的代理工具
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 状态
|
||||
|
||||
已实现
|
||||
|
||||
## 概述
|
||||
|
||||
本规范定义了一种称为“工具服务”的动态可插拔代理工具的机制。 与现有的内置工具类型(`KnowledgeQueryImpl`,`McpToolImpl`等)不同,工具服务允许通过以下方式引入新的工具:
|
||||
|
||||
1. 部署一个新的基于Pulsar的服务
|
||||
2. 添加一个配置描述符,该描述符告诉代理如何调用它
|
||||
|
||||
这实现了可扩展性,而无需修改核心代理响应框架。
|
||||
|
||||
## 术语
|
||||
|
||||
| 术语 | 定义 |
|
||||
|------|------------|
|
||||
| **内置工具** | 具有硬编码实现的现有工具类型,位于`tools.py` |
|
||||
| **工具服务** | 可以作为代理工具调用的Pulsar服务,由服务描述符定义 |
|
||||
| **工具** | 引用工具服务的已配置实例,暴露给代理/LLM |
|
||||
|
||||
这是一个两层模型,类似于MCP工具:
|
||||
MCP:MCP服务器定义工具接口 → 工具配置引用它
|
||||
工具服务:工具服务定义Pulsar接口 → 工具配置引用它
|
||||
|
||||
## 背景:现有工具
|
||||
|
||||
### 内置工具实现
|
||||
|
||||
当前,工具在`trustgraph-flow/trustgraph/agent/react/tools.py`中定义,并具有类型化的实现:
|
||||
|
||||
```python
|
||||
class KnowledgeQueryImpl:
|
||||
async def invoke(self, question):
|
||||
client = self.context("graph-rag-request")
|
||||
return await client.rag(question, self.collection)
|
||||
```
|
||||
|
||||
每种工具类型:
|
||||
具有一个硬编码的 Pulsar 服务,它会调用该服务(例如:`graph-rag-request`)
|
||||
知道要调用的客户端上的确切方法(例如:`client.rag()`)
|
||||
在实现中定义了类型化的参数
|
||||
|
||||
### 工具注册 (service.py:105-214)
|
||||
|
||||
工具从配置文件中加载,其中有一个 `type` 字段,它映射到实现:
|
||||
|
||||
```python
|
||||
if impl_id == "knowledge-query":
|
||||
impl = functools.partial(KnowledgeQueryImpl, collection=data.get("collection"))
|
||||
elif impl_id == "text-completion":
|
||||
impl = TextCompletionImpl
|
||||
# ... etc
|
||||
```
|
||||
|
||||
## 架构
|
||||
|
||||
### 两层模型
|
||||
|
||||
#### 第一层:工具服务描述器
|
||||
|
||||
一个工具服务定义了 Pulsar 服务接口。它声明:
|
||||
用于请求/响应的 Pulsar 队列
|
||||
它需要的来自使用它的工具的配置参数
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "custom-rag",
|
||||
"request-queue": "non-persistent://tg/request/custom-rag",
|
||||
"response-queue": "non-persistent://tg/response/custom-rag",
|
||||
"config-params": [
|
||||
{"name": "collection", "required": true}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
一种不需要任何配置参数的工具服务:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "calculator",
|
||||
"request-queue": "non-persistent://tg/request/calc",
|
||||
"response-queue": "non-persistent://tg/response/calc",
|
||||
"config-params": []
|
||||
}
|
||||
```
|
||||
|
||||
#### 第二层:工具描述
|
||||
|
||||
一个工具引用一个工具服务,并提供:
|
||||
配置参数值(满足服务的要求)
|
||||
代理的工具元数据(名称、描述)
|
||||
LLM 的参数定义
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool-service",
|
||||
"name": "query-customers",
|
||||
"description": "Query the customer knowledge base",
|
||||
"service": "custom-rag",
|
||||
"collection": "customers",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "question",
|
||||
"type": "string",
|
||||
"description": "The question to ask about customers"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
多个工具可以使用不同的配置来引用相同的服务:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tool-service",
|
||||
"name": "query-products",
|
||||
"description": "Query the product knowledge base",
|
||||
"service": "custom-rag",
|
||||
"collection": "products",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "question",
|
||||
"type": "string",
|
||||
"description": "The question to ask about products"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 请求格式
|
||||
|
||||
当一个工具被调用时,发送给工具服务的请求包括:
|
||||
`user`: 来自代理的请求(多租户)
|
||||
`config`: 来自工具描述的 JSON 编码的配置值
|
||||
`arguments`: 来自 LLM 的 JSON 编码的参数
|
||||
|
||||
```json
|
||||
{
|
||||
"user": "alice",
|
||||
"config": "{\"collection\": \"customers\"}",
|
||||
"arguments": "{\"question\": \"What are the top customer complaints?\"}"
|
||||
}
|
||||
```
|
||||
|
||||
工具服务接收这些数据,并将其解析为字典,在 `invoke` 方法中进行处理。
|
||||
|
||||
### 通用工具服务实现
|
||||
|
||||
一个 `ToolServiceImpl` 类根据配置调用工具服务:
|
||||
|
||||
```python
|
||||
class ToolServiceImpl:
|
||||
def __init__(self, context, request_queue, response_queue, config_values, arguments, processor):
|
||||
self.request_queue = request_queue
|
||||
self.response_queue = response_queue
|
||||
self.config_values = config_values # e.g., {"collection": "customers"}
|
||||
# ...
|
||||
|
||||
async def invoke(self, **arguments):
|
||||
client = await self._get_or_create_client()
|
||||
response = await client.call(user, self.config_values, arguments)
|
||||
if isinstance(response, str):
|
||||
return response
|
||||
else:
|
||||
return json.dumps(response)
|
||||
```
|
||||
|
||||
## 设计决策
|
||||
|
||||
### 双层配置模型
|
||||
|
||||
工具服务遵循类似于 MCP 工具的双层模型:
|
||||
|
||||
1. **工具服务 (Tool Service)**:定义 Pulsar 服务接口(主题、所需的配置参数)。
|
||||
2. **工具 (Tool)**:引用工具服务,提供配置值,定义 LLM 参数。
|
||||
|
||||
这种分离方式允许:
|
||||
一个工具服务可以被多个具有不同配置的工具使用。
|
||||
清晰区分服务接口和工具配置。
|
||||
重用服务定义。
|
||||
|
||||
### 请求映射:带外壳的透传
|
||||
|
||||
发送到工具服务的请求是一个结构化的外壳,包含:
|
||||
`user`:从代理请求中传播,用于多租户。
|
||||
配置值:来自工具描述符(例如,`collection`)。
|
||||
`arguments`:LLM 提供的参数,作为字典传递。
|
||||
|
||||
代理管理器将 LLM 的响应解析为 `act.arguments`(作为字典,`agent_manager.py:117-154`)。此字典包含在请求外壳中。
|
||||
|
||||
### 模式处理:无类型
|
||||
|
||||
请求和响应使用无类型的字典。代理级别不进行任何模式验证 - 工具服务负责验证其输入。这提供了定义新服务的最大灵活性。
|
||||
|
||||
### 客户端接口:直接 Pulsar 主题
|
||||
|
||||
工具服务使用直接的 Pulsar 主题,无需配置流。工具服务描述符指定完整的队列名称:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "joke-service",
|
||||
"request-queue": "non-persistent://tg/request/joke",
|
||||
"response-queue": "non-persistent://tg/response/joke",
|
||||
"config-params": [...]
|
||||
}
|
||||
```
|
||||
|
||||
这允许服务在任何命名空间中被托管。
|
||||
|
||||
### 错误处理:标准错误约定
|
||||
|
||||
工具服务响应遵循现有的模式约定,包含一个 `error` 字段:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class Error:
|
||||
type: str = ""
|
||||
message: str = ""
|
||||
```
|
||||
|
||||
响应结构:
|
||||
成功: `error` 为 `None`,响应包含结果
|
||||
错误: `error` 被填充为 `type` 和 `message`
|
||||
|
||||
这符合现有服务模式(例如:`PromptResponse`,`QueryResponse`,`AgentResponse`)。
|
||||
|
||||
### 请求/响应关联
|
||||
|
||||
请求和响应使用 `id` 在 Pulsar 消息属性中进行关联:
|
||||
|
||||
请求包含 `id` 在属性中:`properties={"id": id}`
|
||||
响应包含相同的 `id`:`properties={"id": id}`
|
||||
|
||||
这遵循代码库中使用的现有模式(例如:`agent_service.py`,`llm_service.py`)。
|
||||
|
||||
### 流式支持
|
||||
|
||||
工具服务可以返回流式响应:
|
||||
|
||||
具有相同 `id` 在属性中的多个响应消息
|
||||
每个响应包含 `end_of_stream: bool` 字段
|
||||
最终响应具有 `end_of_stream: True`
|
||||
|
||||
这符合 `AgentResponse` 和其他流式服务中使用的模式。
|
||||
|
||||
### 响应处理:字符串返回
|
||||
|
||||
所有现有工具都遵循相同的模式:**接收参数作为字典,将观察结果作为字符串返回**。
|
||||
|
||||
| 工具 | 响应处理 |
|
||||
|------|------------------|
|
||||
| `KnowledgeQueryImpl` | 直接返回 `client.rag()` (字符串) |
|
||||
| `TextCompletionImpl` | 直接返回 `client.question()` (字符串) |
|
||||
| `McpToolImpl` | 返回字符串,或如果不是字符串则返回 `json.dumps(output)` |
|
||||
| `StructuredQueryImpl` | 将结果格式化为字符串 |
|
||||
| `PromptImpl` | 直接返回 `client.prompt()` (字符串) |
|
||||
|
||||
工具服务遵循相同的约定:
|
||||
服务返回一个字符串响应(观察结果)
|
||||
如果响应不是字符串,则通过 `json.dumps()` 进行转换
|
||||
描述符中不需要提取配置
|
||||
|
||||
这使描述符保持简单,并将责任放在服务上,使其为代理返回适当的文本响应。
|
||||
|
||||
## 配置指南
|
||||
|
||||
要添加一个新的工具服务,需要两个配置项:
|
||||
|
||||
### 1. 工具服务配置
|
||||
|
||||
存储在 `tool-service` 配置键下。定义了 Pulsar 队列和可用的配置参数。
|
||||
|
||||
| 字段 | 必需 | 描述 |
|
||||
|-------|----------|-------------|
|
||||
| `id` | 是 | 工具服务的唯一标识符 |
|
||||
| `request-queue` | 是 | 用于请求的完整 Pulsar 主题(例如:`non-persistent://tg/request/joke`) |
|
||||
| `response-queue` | 是 | 用于响应的完整 Pulsar 主题(例如:`non-persistent://tg/response/joke`) |
|
||||
| `config-params` | 否 | 服务接受的配置参数数组 |
|
||||
|
||||
每个配置参数可以指定:
|
||||
`name`: 参数名称 (必需)
|
||||
`required`: 是否需要工具提供该参数 (默认: 否)
|
||||
|
||||
示例:
|
||||
```json
|
||||
{
|
||||
"id": "joke-service",
|
||||
"request-queue": "non-persistent://tg/request/joke",
|
||||
"response-queue": "non-persistent://tg/response/joke",
|
||||
"config-params": [
|
||||
{"name": "style", "required": false}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 工具配置
|
||||
|
||||
存储在 `tool` 配置键下。定义了代理可以使用的工具。
|
||||
|
||||
| 字段 | 必需 | 描述 |
|
||||
|-------|----------|-------------|
|
||||
| `type` | 是 | 必须是 `"tool-service"` |
|
||||
| `name` | 是 | 暴露给 LLM 的工具名称 |
|
||||
| `description` | 是 | 描述工具的功能(显示给 LLM)|
|
||||
| `service` | 是 | 调用工具服务的 ID |
|
||||
| `arguments` | 否 | LLM 的参数定义数组 |
|
||||
| *(配置参数)* | 变化 | 服务定义的任何配置参数 |
|
||||
|
||||
每个参数可以指定:
|
||||
`name`:参数名称(必需)
|
||||
`type`:数据类型,例如 `"string"`(必需)
|
||||
`description`:显示给 LLM 的描述(必需)
|
||||
|
||||
示例:
|
||||
```json
|
||||
{
|
||||
"type": "tool-service",
|
||||
"name": "tell-joke",
|
||||
"description": "Tell a joke on a given topic",
|
||||
"service": "joke-service",
|
||||
"style": "pun",
|
||||
"arguments": [
|
||||
{
|
||||
"name": "topic",
|
||||
"type": "string",
|
||||
"description": "The topic for the joke (e.g., programming, animals, food)"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 加载配置
|
||||
|
||||
使用 `tg-put-config-item` 加载配置:
|
||||
|
||||
```bash
|
||||
# Load tool-service config
|
||||
tg-put-config-item tool-service/joke-service < joke-service.json
|
||||
|
||||
# Load tool config
|
||||
tg-put-config-item tool/tell-joke < tell-joke.json
|
||||
```
|
||||
|
||||
必须重启代理服务器才能加载新的配置。
|
||||
|
||||
## 实现细节
|
||||
|
||||
### 模式
|
||||
|
||||
`trustgraph-base/trustgraph/schema/services/tool_service.py` 中的请求和响应类型:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class ToolServiceRequest:
|
||||
user: str = "" # User context for multi-tenancy
|
||||
config: str = "" # JSON-encoded config values from tool descriptor
|
||||
arguments: str = "" # JSON-encoded arguments from LLM
|
||||
|
||||
@dataclass
|
||||
class ToolServiceResponse:
|
||||
error: Error | None = None
|
||||
response: str = "" # String response (the observation)
|
||||
end_of_stream: bool = False
|
||||
```
|
||||
|
||||
### 服务器端:DynamicToolService
|
||||
|
||||
基类位于 `trustgraph-base/trustgraph/base/dynamic_tool_service.py`:
|
||||
|
||||
```python
|
||||
class DynamicToolService(AsyncProcessor):
|
||||
"""Base class for implementing tool services."""
|
||||
|
||||
def __init__(self, **params):
|
||||
topic = params.get("topic", default_topic)
|
||||
# Constructs topics: non-persistent://tg/request/{topic}, non-persistent://tg/response/{topic}
|
||||
# Sets up Consumer and Producer
|
||||
|
||||
async def invoke(self, user, config, arguments):
|
||||
"""Override this method to implement the tool's logic."""
|
||||
raise NotImplementedError()
|
||||
```
|
||||
|
||||
### 客户端:ToolServiceImpl
|
||||
|
||||
在 `trustgraph-flow/trustgraph/agent/react/tools.py` 中的实现:
|
||||
|
||||
```python
|
||||
class ToolServiceImpl:
|
||||
def __init__(self, context, request_queue, response_queue, config_values, arguments, processor):
|
||||
# Uses the provided queue paths directly
|
||||
# Creates ToolServiceClient on first use
|
||||
|
||||
async def invoke(self, **arguments):
|
||||
client = await self._get_or_create_client()
|
||||
response = await client.call(user, config_values, arguments)
|
||||
return response if isinstance(response, str) else json.dumps(response)
|
||||
```
|
||||
|
||||
### 文件
|
||||
|
||||
| 文件 | 目的 |
|
||||
|------|---------|
|
||||
| `trustgraph-base/trustgraph/schema/services/tool_service.py` | 请求/响应模式 |
|
||||
| `trustgraph-base/trustgraph/base/tool_service_client.py` | 调用服务的客户端 |
|
||||
| `trustgraph-base/trustgraph/base/dynamic_tool_service.py` | 服务实现的基类 |
|
||||
| `trustgraph-flow/trustgraph/agent/react/tools.py` | `ToolServiceImpl` 类 |
|
||||
| `trustgraph-flow/trustgraph/agent/react/service.py` | 配置加载 |
|
||||
|
||||
### 示例:笑话服务
|
||||
|
||||
一个用 `trustgraph-flow/trustgraph/tool_service/joke/` 编写的示例服务:
|
||||
|
||||
```python
|
||||
class Processor(DynamicToolService):
|
||||
async def invoke(self, user, config, arguments):
|
||||
style = config.get("style", "pun")
|
||||
topic = arguments.get("topic", "")
|
||||
joke = pick_joke(topic, style)
|
||||
return f"Hey {user}! Here's a {style} for you:\n\n{joke}"
|
||||
```
|
||||
|
||||
工具服务配置:
|
||||
```json
|
||||
{
|
||||
"id": "joke-service",
|
||||
"request-queue": "non-persistent://tg/request/joke",
|
||||
"response-queue": "non-persistent://tg/response/joke",
|
||||
"config-params": [{"name": "style", "required": false}]
|
||||
}
|
||||
```
|
||||
|
||||
工具配置:
|
||||
```json
|
||||
{
|
||||
"type": "tool-service",
|
||||
"name": "tell-joke",
|
||||
"description": "Tell a joke on a given topic",
|
||||
"service": "joke-service",
|
||||
"style": "pun",
|
||||
"arguments": [
|
||||
{"name": "topic", "type": "string", "description": "The topic for the joke"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 向后兼容性
|
||||
|
||||
现有的内置工具类型继续以不变的方式工作。
|
||||
`tool-service` 是一种新的工具类型,与现有类型(`knowledge-query`、`mcp-tool`等)并存。
|
||||
|
||||
## 未来考虑事项
|
||||
|
||||
### 自定义服务
|
||||
|
||||
未来的增强功能可能允许服务发布自己的描述:
|
||||
|
||||
服务在启动时发布到已知的 `tool-descriptors` 主题。
|
||||
代理订阅并动态注册工具。
|
||||
实现了真正的即插即用,无需配置更改。
|
||||
|
||||
这超出了初始实现的范围。
|
||||
|
||||
## 参考文献
|
||||
|
||||
当前工具实现:`trustgraph-flow/trustgraph/agent/react/tools.py`
|
||||
工具注册:`trustgraph-flow/trustgraph/agent/react/service.py:105-214`
|
||||
代理模式:`trustgraph-base/trustgraph/schema/services/agent.py`
|
||||
297
docs/tech-specs/zh-cn/universal-decoder.zh-cn.md
Normal file
297
docs/tech-specs/zh-cn/universal-decoder.zh-cn.md
Normal file
|
|
@ -0,0 +1,297 @@
|
|||
---
|
||||
layout: default
|
||||
title: "通用文档解码器"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 通用文档解码器
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
一个由 `unstructured` 驱动的通用文档解码器,它支持通过单个服务处理各种常见的文档格式。该服务具有完整的溯源功能和库管理员集成,记录源位置作为知识图谱元数据,从而实现端到端的可追溯性。
|
||||
|
||||
## 问题
|
||||
|
||||
目前,TrustGraph 仅有一个用于 PDF 文件的解码器。支持其他格式(DOCX、XLSX、HTML、Markdown、纯文本、PPTX 等)需要要么为每种格式编写新的解码器,要么采用一个通用的提取库。每种格式的结构都不同——有些是基于页面的,有些不是——并且溯源链必须记录每个提取的文本片段在原始文档中的位置。
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 库:`unstructured`
|
||||
|
||||
使用 `unstructured.partition.auto.partition()` 函数,该函数可以自动检测格式(根据 MIME 类型或文件扩展名),并提取结构化元素(标题、正文、表格、列表项等)。每个元素都包含元数据,包括:
|
||||
|
||||
- `page_number`(对于基于页面的格式,如 PDF、PPTX)
|
||||
- `element_id`(每个元素唯一)
|
||||
- `coordinates`(PDF 文件的边界框)
|
||||
- `text`(提取的文本内容)
|
||||
- `category`(元素类型:标题、正文、表格等)
|
||||
|
||||
### 元素类型
|
||||
|
||||
`unstructured` 提取文档中的各种类型的元素,每个元素都有一个类别和相关的元数据:
|
||||
|
||||
**文本元素:**
|
||||
- `Title` — 段落标题
|
||||
- `NarrativeText` — 正文段落
|
||||
- `ListItem` — 项目符号/编号列表项
|
||||
- `Header`, `Footer` — 页面页眉/页脚
|
||||
- `FigureCaption` — 图形/图像的标题
|
||||
- `Formula` — 数学表达式
|
||||
- `Address`, `EmailAddress` — 联系信息
|
||||
- `CodeSnippet` — 代码块(来自 Markdown)
|
||||
|
||||
**表格:**
|
||||
- `Table` — 结构化的表格数据。`unstructured` 提供了 `element.text`(纯文本)和 `element.metadata.text_as_html`(完整的 HTML `<table>` 标签,保留了行、列和标题)。对于具有显式表格结构的格式(DOCX、XLSX、HTML),提取的可靠性很高。对于 PDF 文件,表格检测依赖于 `hi_res` 策略和布局分析。
|
||||
|
||||
**图像:**
|
||||
- `Image` — 检测到的嵌入图像(需要 `hi_res` 策略)。如果设置 `extract_image_block_to_payload=True`,则将图像数据作为 base64 编码的字符串返回到 `element.metadata.image_base64` 中。图像中的 OCR 文本位于 `element.text` 中。
|
||||
|
||||
### 表格处理
|
||||
|
||||
表格是主要的输出。当解码器遇到 `Table` 元素时,它会保留 HTML 结构,而不是将其扁平化为纯文本。这为下游的 LLM 提取器提供了更好的输入,以便从表格数据中提取结构化知识。
|
||||
|
||||
页面/部分文本的组装方式如下:
|
||||
- 文本元素:纯文本,用换行符连接
|
||||
- 表格元素:来自 `text_as_html` 的 HTML 表格标记,并用 `<table>` 标记包装,以便 LLM 区分表格和正文。
|
||||
|
||||
例如,一个页面包含标题、段落和表格,其内容如下:
|
||||
|
||||
```
|
||||
Financial Overview
|
||||
|
||||
Revenue grew 15% year-over-year driven by enterprise adoption.
|
||||
|
||||
<table>
|
||||
<tr><th>Quarter</th><th>Revenue</th><th>Growth</th></tr>
|
||||
<tr><td>Q1</td><td>$12M</td><td>12%</td></tr>
|
||||
<tr><td>Q2</td><td>$14M</td><td>17%</td></tr>
|
||||
</table>
|
||||
```
|
||||
|
||||
这在分块和提取流水线中保留了表格结构,LLM 可以直接从结构化单元格中提取关系,而无需猜测列的对齐方式。
|
||||
|
||||
### 图像处理
|
||||
|
||||
图像会被提取并存储在库管理员中,作为具有 `document_type="image"` 和 `urn:image:{uuid}` ID 的子文档。它们会生成带有类型 `tg:Image` 的溯源三元组,并通过 `prov:wasDerivedFrom` 链接到其父页面/部分。图像元数据(坐标、尺寸、`element_id`)会记录在溯源信息中。
|
||||
|
||||
**重要的是,图像不会作为 `TextDocument` 输出。** 它们仅被存储,不会被发送到分块器或任何文本处理流水线。这是故意的:
|
||||
|
||||
1. 尚未建立图像处理流水线(视觉模型集成是未来的工作)。
|
||||
2. 将 base64 编码的图像数据或 OCR 片段馈送到文本提取流水线会产生无效的知识图谱三元组。
|
||||
|
||||
图像也被排除在组装的页面文本之外。当连接一个页面/部分的元素文本时,任何 `Image` 元素都会被静默地跳过。溯源链会记录图像的存在以及它们在文档中出现的位置,以便在将来的图像处理流水线中可以拾取它们,而无需重新导入文档。
|
||||
|
||||
#### 未来工作
|
||||
|
||||
- 将 `tg:Image` 实体路由到视觉模型,用于描述、图表解释或图表数据提取。
|
||||
- 将图像描述存储为文本子文档,这些子文档会馈送到标准的 chunking/提取流水线。
|
||||
- 通过溯源将提取的知识链接回源图像。
|
||||
|
||||
### 部分策略
|
||||
|
||||
对于基于页面的格式(PDF、PPTX、XLSX),元素始终首先按页面分组。对于非基于页面的格式(DOCX、HTML、Markdown 等),解码器需要一种策略来将文档拆分为多个部分。这可以通过运行时配置的 `--section-strategy` 参数进行配置。
|
||||
|
||||
每种策略都是一个分组函数,作用于 `unstructured` 元素的列表。输出是一个元素组的列表;其余流水线(文本组装、库管理员存储、溯源、`TextDocument` 输出)与策略无关。
|
||||
|
||||
#### `whole-document` (默认)
|
||||
|
||||
将整个文档作为一个单独的部分输出。由下游的分块器处理所有拆分。
|
||||
|
||||
- 最简单的方案,作为基线。
|
||||
- 对于大型文件,可能会生成非常大的 `TextDocument`,但分块器会处理这种情况。
|
||||
- 当希望每个部分包含最大的上下文时,使用此选项。
|
||||
|
||||
#### `heading`
|
||||
|
||||
在标题元素(`Title`)处进行拆分。每个部分是一个标题以及直到下一个相同或更高级别的标题的所有内容。嵌套的标题会创建嵌套的部分。
|
||||
|
||||
- 生成具有主题相关性的单元。
|
||||
- 适用于结构化文档(报告、手册、规范)。
|
||||
- 向提取 LLM 提供标题上下文以及内容。
|
||||
- 如果未找到标题,则回退到 `whole-document`。
|
||||
|
||||
#### `element-type`
|
||||
|
||||
当元素类型发生显着变化时进行拆分,特别是,从正文文本和表格之间的转换开始一个新的部分。连续的相同宽泛类别的元素(文本、文本、文本或表格、表格、表格)保持分组。
|
||||
|
||||
- 保持表格作为独立的部分。
|
||||
- 适用于具有混合内容的文档(包含数据表格的报告)。
|
||||
- 表格会获得专门的提取关注。
|
||||
|
||||
#### `count`
|
||||
|
||||
每部分包含固定数量的元素。可以通过 `--section-element-count` 参数进行配置(默认:20)。
|
||||
|
||||
- 简单且可预测。
|
||||
- 不尊重文档结构。
|
||||
- 用作回退选项或用于实验。
|
||||
|
||||
#### `size`
|
||||
|
||||
持续累积元素,直到达到字符限制,然后开始一个新的部分。尊重元素边界,绝不会在元素内部进行拆分。可以通过 `--section-max-size` 参数进行配置(默认:4000 个字符)。
|
||||
|
||||
- 生成大致均匀大小的部分。
|
||||
- 尊重元素边界(与下游分块器不同)。
|
||||
- 在结构和大小控制之间取得良好的平衡。
|
||||
- 如果单个元素超过限制,则它会成为其自己的部分。
|
||||
|
||||
#### 基于页面的格式交互
|
||||
|
||||
对于基于页面的格式,页面分组始终具有优先性。部分策略可以选择性地在非常大的页面内部应用(例如,一个包含非常大的表格的 PDF 页面),由 `--section-within-pages` 参数控制(默认:false)。当设置为 false 时,每个页面始终是一个部分,无论其大小如何。
|
||||
|
||||
### 格式检测
|
||||
|
||||
解码器需要知道文档的 MIME 类型,以便将其传递给 `unstructured` 的 `partition()` 函数。有两种方法:
|
||||
|
||||
- **库管理员路径**(设置了 `document_id`):首先从库管理员中获取文档元数据,这会提供 `kind`(MIME 类型),然后获取文档内容。这需要两次库管理员调用,但元数据获取是轻量级的。
|
||||
- **内联路径**(后备方案,设置了 `data`):没有关于消息的元数据。使用 `python-magic` 从内容字节中检测格式,作为后备方案。
|
||||
|
||||
不需要对 `Document` 模式进行任何更改,因为库管理员已经存储了 MIME 类型。
|
||||
|
||||
### 架构
|
||||
|
||||
一个名为 `universal-decoder` 的单个服务,该服务:
|
||||
|
||||
1. 接收一个 `Document` 消息(内联或通过库管理员引用)。
|
||||
2. 如果使用库管理员路径:获取文档元数据(获取 MIME 类型),然后获取内容。如果使用内联路径:使用 `python-magic` 从内容字节检测格式。
|
||||
3. 调用 `partition()` 函数以提取元素。
|
||||
4. 按以下方式对元素进行分组:对于基于页面的格式,按页面分组;对于非基于页面的格式,按配置的策略分组。
|
||||
5. 对于每个页面/部分:
|
||||
- 生成一个 `urn:page:{uuid}` 或 `urn:section:{uuid}` ID。
|
||||
- 组装页面文本:正文作为纯文本,表格作为 HTML,图像被跳过。
|
||||
- 计算每个元素在页面文本中的字符偏移量。
|
||||
- 保存到库管理员中作为子文档。
|
||||
- 计算带有位置元数据的溯源三元组。
|
||||
- 将 `TextDocument` 发送到下游进行分块。
|
||||
6. 对于每个图像元素:
|
||||
- 生成一个 `urn:image:{uuid}` ID。
|
||||
- 将图像数据保存到库管理员中作为子文档。
|
||||
- 计算溯源三元组(仅存储,不发送到下游)。
|
||||
|
||||
### 格式处理
|
||||
|
||||
| 格式 | MIME 类型 | 基于页面 | 备注 |
|
||||
|-----------|--------------------------------------------|----------|--------------------------------------------|
|
||||
| PDF | `application/pdf` | 是 | 按页面分组 |
|
||||
| DOCX | `application/vnd.openxmlformats...` | 否 | 使用部分策略 |
|
||||
| PPTX | `application/vnd.openxmlformats...` | 是 | 按幻灯片分组 |
|
||||
| XLSX/XLS | `application/vnd.openxmlformats...` | 是 | 按工作表分组 |
|
||||
| HTML | `text/html` | 否 | 使用部分策略 |
|
||||
| Markdown | `text/markdown` | 否 | 使用部分策略 |
|
||||
| 纯文本 | `text/plain` | 否 | 使用部分策略 |
|
||||
| CSV | `text/csv` | 否 | 使用部分策略 |
|
||||
| RST | `text/x-rst` | 否 | 使用部分策略 |
|
||||
| RTF | `application/rtf` | 否 | 使用部分策略 |
|
||||
| ODT | `application/vnd.oasis...` | 否 | 使用部分策略 |
|
||||
| TSV | `text/tab-separated-values` | 否 | 使用部分策略 |
|
||||
|
||||
### 溯源元数据
|
||||
|
||||
每个页面/部分实体会记录位置元数据,作为 `GRAPH_SOURCE` 命名图中位置元数据的溯源三元组。
|
||||
|
||||
#### 现有字段(已包含在 `derived_entity_triples` 中)
|
||||
|
||||
- `page_number` — 页面/工作表/幻灯片编号(从 1 开始,仅适用于基于页面的格式)
|
||||
- `char_offset` — 此页面/部分在整个文档文本中的字符偏移量
|
||||
- `char_length` — 此页面/部分的文本的字符长度
|
||||
|
||||
#### 新字段(扩展 `derived_entity_triples`)
|
||||
|
||||
- `mime_type` — 原始文档格式(例如,`application/pdf`)
|
||||
- `element_types` — 使用 `unstructured` 提取的元素的类别的逗号分隔列表(例如,`Title,NarrativeText,Table`)
|
||||
- `table_count` — 此页面/部分中的表格数量
|
||||
- `image_count` — 此页面/部分中的图像数量
|
||||
|
||||
这些需要新的 TG 命名空间谓词:
|
||||
|
||||
```
|
||||
TG_SECTION_TYPE = "https://trustgraph.ai/ns/Section"
|
||||
TG_IMAGE_TYPE = "https://trustgraph.ai/ns/Image"
|
||||
TG_ELEMENT_TYPES = "https://trustgraph.ai/ns/elementTypes"
|
||||
TG_TABLE_COUNT = "https://trustgraph.ai/ns/tableCount"
|
||||
TG_IMAGE_COUNT = "https://trustgraph.ai/ns/imageCount"
|
||||
```
|
||||
|
||||
图像 URN 方案:`urn:image:{uuid}`
|
||||
|
||||
(`TG_MIME_TYPE` 已经存在。)
|
||||
|
||||
#### 完整的溯源链
|
||||
|
||||
```
|
||||
知识图谱三元组
|
||||
→ subgraph (提取溯源)
|
||||
→ chunk (页面/部分的字符偏移量和长度)
|
||||
→ page/section (页面编号、字符偏移量和长度、MIME 类型、元素类型)
|
||||
→ document (库管理员中的原始文件)
|
||||
```
|
||||
|
||||
每个链接都是 `GRAPH_SOURCE` 命名图中一组三元组。
|
||||
|
||||
### 服务配置
|
||||
|
||||
命令行参数:
|
||||
|
||||
```
|
||||
--strategy 分区策略:auto, hi_res, fast (默认:auto)
|
||||
--languages 逗号分隔的 OCR 语言代码 (默认: eng)
|
||||
--section-strategy 部分分组:whole-document, heading, element-type,
|
||||
count, size (默认: whole-document)
|
||||
--section-element-count 每个部分包含的元素数量(`count` 策略),(默认: 20)
|
||||
--section-max-size 每个部分的最大字符数(`size` 策略),(默认: 4000 个字符)
|
||||
--section-within-pages 是否在页面内部应用部分策略 (默认: false)
|
||||
```
|
||||
|
||||
以及标准的 `FlowProcessor` 和库管理员队列参数。
|
||||
|
||||
### 流集成
|
||||
|
||||
通用解码器在处理流程中占据与现有 PDF 解码器相同的 position:
|
||||
|
||||
```
|
||||
Document → [通用解码器] → TextDocument → [分块器] → Chunk → ...
|
||||
```
|
||||
|
||||
它注册:
|
||||
- `input` 消费者(`Document` 模式)
|
||||
- `output` 生产者(`TextDocument` 模式)
|
||||
- `triples` 生产者(`Triples` 模式)
|
||||
- 库管理员请求/响应(用于获取和创建子文档)
|
||||
|
||||
### 部署
|
||||
|
||||
- 新的容器:`trustgraph-flow-universal-decoder`
|
||||
- 依赖项:`unstructured[all-docs]`(包含 PDF、DOCX、PPTX 等)
|
||||
- 可以与现有的 PDF 解码器并行运行或替换它,具体取决于流程配置。
|
||||
- 现有的 PDF 解码器仍然可用,适用于 `unstructured` 依赖项过多的环境。
|
||||
|
||||
### 变更内容
|
||||
|
||||
| 组件 | 变更 |
|
||||
|------------------------------|-------------------------------------------------|
|
||||
| `provenance/namespaces.py` | 添加 `TG_SECTION_TYPE`、`TG_IMAGE_TYPE`、`TG_ELEMENT_TYPES`、`TG_TABLE_COUNT`、`TG_IMAGE_COUNT` |
|
||||
| `provenance/triples.py` | 添加 `mime_type`、`element_types`、`table_count`、`image_count` 参数 |
|
||||
| `provenance/__init__.py` | 导出新的常量 |
|
||||
| 新的:`decoding/universal/` | 新的解码器服务模块 |
|
||||
| `setup.cfg` / `pyproject` | 添加 `unstructured[all-docs]` 依赖项 |
|
||||
| Docker | 新的容器镜像 |
|
||||
| 流程定义 | 将通用解码器设置为文档输入 |
|
||||
|
||||
### 未变更的内容
|
||||
|
||||
- 分块器(接收 `TextDocument`,行为与之前相同)
|
||||
- 下游提取器(接收 `Chunk`,未发生变化)
|
||||
- 库管理员(存储子文档,未发生变化)
|
||||
- 模式(`Document`、`TextDocument`、`Chunk` 未发生变化)
|
||||
- 查询时溯源(未发生变化)
|
||||
|
||||
## 风险
|
||||
|
||||
- `unstructured[all-docs]` 具有大量的依赖项(包括 poppler、tesseract 和 libreoffice,用于某些格式)。容器镜像会更大。
|
||||
缓解措施:提供不包含 OCR/办公室依赖项的 `[light]` 版本。
|
||||
- 某些格式可能导致文本提取效果不佳(扫描的 PDF 文件缺少 OCR,复杂的 XLSX 布局)。
|
||||
缓解措施:配置 `strategy` 参数,并且现有的 Mistral OCR 解码器仍然可用于高质量的 PDF OCR。
|
||||
- `unstructured` 版本更新可能会更改元素元数据。
|
||||
缓解措施:固定版本,并为每个格式测试提取质量。
|
||||
307
docs/tech-specs/zh-cn/vector-store-lifecycle.zh-cn.md
Normal file
307
docs/tech-specs/zh-cn/vector-store-lifecycle.zh-cn.md
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
---
|
||||
layout: default
|
||||
title: "向量存储生命周期管理"
|
||||
parent: "Chinese (Beta)"
|
||||
---
|
||||
|
||||
# 向量存储生命周期管理
|
||||
|
||||
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
|
||||
|
||||
## 概述
|
||||
|
||||
本文档描述了 TrustGraph 如何管理跨不同后端实现(Qdrant、Pinecone、Milvus)的向量存储集合。该设计解决了在不硬编码维度值的情况下,支持具有不同维度的嵌入向量的挑战。
|
||||
|
||||
## 问题陈述
|
||||
|
||||
向量存储在创建集合/索引时需要指定嵌入维度。但是:
|
||||
不同的嵌入模型会产生不同的维度(例如,384、768、1536)
|
||||
直到生成第一个嵌入向量,维度才已知
|
||||
单个 TrustGraph 集合可能会接收来自多个模型的嵌入向量
|
||||
强制指定一个维度(例如,384)会导致使用其他嵌入向量大小时出现故障
|
||||
|
||||
## 设计原则
|
||||
|
||||
1. **延迟创建**: 集合是在首次写入时按需创建的,而不是在集合管理操作期间创建的。
|
||||
2. **基于维度的命名**: 集合名称包含嵌入维度作为后缀。
|
||||
3. **优雅降级**: 对不存在的集合执行的查询返回空结果,而不是错误。
|
||||
4. **多维度支持**: 单个逻辑集合可以有多个物理集合(每个维度一个)。
|
||||
|
||||
## 架构
|
||||
|
||||
### 集合命名约定
|
||||
|
||||
向量存储集合使用维度后缀来支持多种嵌入向量大小:
|
||||
|
||||
**文档嵌入:**
|
||||
Qdrant: `d_{user}_{collection}_{dimension}`
|
||||
Pinecone: `d-{user}-{collection}-{dimension}`
|
||||
Milvus: `doc_{user}_{collection}_{dimension}`
|
||||
|
||||
**图嵌入:**
|
||||
Qdrant: `t_{user}_{collection}_{dimension}`
|
||||
Pinecone: `t-{user}-{collection}-{dimension}`
|
||||
Milvus: `entity_{user}_{collection}_{dimension}`
|
||||
|
||||
示例:
|
||||
`d_alice_papers_384` - Alice 的论文集合,使用 384 维的嵌入向量
|
||||
`d_alice_papers_768` - 相同的逻辑集合,使用 768 维的嵌入向量
|
||||
`t_bob_knowledge_1536` - Bob 的知识图谱,使用 1536 维的嵌入向量
|
||||
|
||||
### 生命周期阶段
|
||||
|
||||
#### 1. 集合创建请求
|
||||
|
||||
**请求流程:**
|
||||
```
|
||||
User/System → Librarian → Storage Management Topic → Vector Stores
|
||||
```
|
||||
|
||||
**行为:**
|
||||
库管理员将 `create-collection` 请求广播到所有存储后端。
|
||||
向量存储处理器确认该请求,但**不创建物理集合**。
|
||||
立即返回成功响应。
|
||||
实际集合的创建将在第一次写入时延迟。
|
||||
|
||||
**理由:**
|
||||
在创建时,维度是未知的。
|
||||
避免创建具有错误维度的集合。
|
||||
简化集合管理逻辑。
|
||||
|
||||
#### 2. 写入操作(延迟创建)
|
||||
|
||||
**写入流程:**
|
||||
```
|
||||
Data → Storage Processor → Check Collection → Create if Needed → Insert
|
||||
```
|
||||
|
||||
**行为:**
|
||||
1. 从向量中提取嵌入维度:`dim = len(vector)`
|
||||
2. 使用维度后缀构建集合名称
|
||||
3. 检查是否存在具有该特定维度的集合
|
||||
4. 如果不存在:
|
||||
创建具有正确维度的集合
|
||||
记录:`"Lazily creating collection {name} with dimension {dim}"`
|
||||
5. 将嵌入信息插入到特定维度的集合中
|
||||
|
||||
**示例场景:**
|
||||
```
|
||||
1. User creates collection "papers"
|
||||
→ No physical collections created yet
|
||||
|
||||
2. First document with 384-dim embedding arrives
|
||||
→ Creates d_user_papers_384
|
||||
→ Inserts data
|
||||
|
||||
3. Second document with 768-dim embedding arrives
|
||||
→ Creates d_user_papers_768
|
||||
→ Inserts data
|
||||
|
||||
Result: Two physical collections for one logical collection
|
||||
```
|
||||
|
||||
#### 3. 查询操作
|
||||
|
||||
**查询流程:**
|
||||
```
|
||||
Query Vector → Determine Dimension → Check Collection → Search or Return Empty
|
||||
```
|
||||
|
||||
**行为:**
|
||||
1. 从查询向量中提取维度:`dim = len(vector)`
|
||||
2. 使用维度后缀构建集合名称
|
||||
3. 检查集合是否存在
|
||||
4. 如果存在:
|
||||
执行相似度搜索
|
||||
返回结果
|
||||
5. 如果不存在:
|
||||
记录日志:`"Collection {name} does not exist, returning empty results"`
|
||||
返回空列表(不引发错误)
|
||||
|
||||
**同一查询中的多个维度:**
|
||||
如果查询包含不同维度的向量
|
||||
每个维度查询其对应的集合
|
||||
结果进行聚合
|
||||
缺失的集合将被跳过(不被视为错误)
|
||||
|
||||
**原理:**
|
||||
查询空集合是一种有效的用例
|
||||
返回空结果在语义上是正确的
|
||||
避免在系统启动或在数据摄取之前发生的错误
|
||||
|
||||
#### 4. 集合删除
|
||||
|
||||
**删除流程:**
|
||||
```
|
||||
Delete Request → List All Collections → Filter by Prefix → Delete All Matches
|
||||
```
|
||||
|
||||
**行为:**
|
||||
1. 构造前缀模式:`d_{user}_{collection}_` (注意尾随的下划线)
|
||||
2. 列出向量存储中的所有集合
|
||||
3. 过滤与前缀匹配的集合
|
||||
4. 删除所有匹配的集合
|
||||
5. 记录每个删除操作:`"Deleted collection {name}"`
|
||||
6. 汇总日志:`"Deleted {count} collection(s) for {user}/{collection}"`
|
||||
|
||||
**示例:**
|
||||
```
|
||||
Collections in store:
|
||||
- d_alice_papers_384
|
||||
- d_alice_papers_768
|
||||
- d_alice_reports_384
|
||||
- d_bob_papers_384
|
||||
|
||||
Delete "papers" for alice:
|
||||
→ Deletes: d_alice_papers_384, d_alice_papers_768
|
||||
→ Keeps: d_alice_reports_384, d_bob_papers_384
|
||||
```
|
||||
|
||||
**原理:**
|
||||
确保彻底清理所有维度变体。
|
||||
模式匹配可防止意外删除不相关的集合。
|
||||
从用户角度来看,这是一个原子操作(所有维度一起删除)。
|
||||
|
||||
## 行为特性
|
||||
|
||||
### 正常操作
|
||||
|
||||
**集合创建:**
|
||||
✓ 立即返回成功。
|
||||
✓ 不分配任何物理存储空间。
|
||||
✓ 快速操作(没有后端 I/O)。
|
||||
|
||||
**首次写入:**
|
||||
✓ 创建具有正确维度的集合。
|
||||
✓ 由于集合创建的开销,速度略慢。
|
||||
✓ 之后对同一维度的写入速度很快。
|
||||
|
||||
**在任何写入之前进行查询:**
|
||||
✓ 返回空结果。
|
||||
✓ 没有错误或异常。
|
||||
✓ 系统保持稳定。
|
||||
|
||||
**混合维度写入:**
|
||||
✓ 自动为每个维度创建单独的集合。
|
||||
✓ 每个维度都隔离在自己的集合中。
|
||||
✓ 没有维度冲突或模式错误。
|
||||
|
||||
**集合删除:**
|
||||
✓ 移除所有维度变体。
|
||||
✓ 彻底清理。
|
||||
✓ 没有孤立的集合。
|
||||
|
||||
### 边界情况
|
||||
|
||||
**多个嵌入模型:**
|
||||
```
|
||||
Scenario: User switches from model A (384-dim) to model B (768-dim)
|
||||
Behavior:
|
||||
- Both dimensions coexist in separate collections
|
||||
- Old data (384-dim) remains queryable with 384-dim vectors
|
||||
- New data (768-dim) queryable with 768-dim vectors
|
||||
- Cross-dimension queries return results only for matching dimension
|
||||
```
|
||||
|
||||
**并发首次写入:**
|
||||
```
|
||||
Scenario: Multiple processes write to same collection simultaneously
|
||||
Behavior:
|
||||
- Each process checks for existence before creating
|
||||
- Most vector stores handle concurrent creation gracefully
|
||||
- If race condition occurs, second create is typically idempotent
|
||||
- Final state: Collection exists and both writes succeed
|
||||
```
|
||||
|
||||
**维度迁移:**
|
||||
```
|
||||
Scenario: User wants to migrate from 384-dim to 768-dim embeddings
|
||||
Behavior:
|
||||
- No automatic migration
|
||||
- Old collection (384-dim) persists
|
||||
- New collection (768-dim) created on first new write
|
||||
- Both dimensions remain accessible
|
||||
- Manual deletion of old dimension collections possible
|
||||
```
|
||||
|
||||
**空集合查询:**
|
||||
```
|
||||
Scenario: Query a collection that has never received data
|
||||
Behavior:
|
||||
- Collection doesn't exist (never created)
|
||||
- Query returns empty list
|
||||
- No error state
|
||||
- System logs: "Collection does not exist, returning empty results"
|
||||
```
|
||||
|
||||
## 实现说明
|
||||
|
||||
### 存储后端特定说明
|
||||
|
||||
**Qdrant:**
|
||||
使用 `collection_exists()` 进行存在性检查
|
||||
使用 `get_collections()` 在删除期间进行列表操作
|
||||
集合创建需要 `VectorParams(size=dim, distance=Distance.COSINE)`
|
||||
|
||||
**Pinecone:**
|
||||
使用 `has_index()` 进行存在性检查
|
||||
使用 `list_indexes()` 在删除期间进行列表操作
|
||||
索引创建需要等待 "ready" 状态
|
||||
Serverless 规格配置为云/区域
|
||||
|
||||
**Milvus:**
|
||||
直接类 (`DocVectors`, `EntityVectors`) 管理生命周期
|
||||
内部缓存 `self.collections[(dim, user, collection)]` 用于提高性能
|
||||
集合名称经过清理(仅限字母数字 + 下划线)
|
||||
支持具有自动递增 ID 的模式
|
||||
|
||||
### 性能考虑
|
||||
|
||||
**首次写入延迟:**
|
||||
由于集合创建而产生的额外开销
|
||||
Qdrant: ~100-500 毫秒
|
||||
Pinecone: ~10-30 秒 (serverless 预配置)
|
||||
Milvus: ~500-2000 毫秒 (包括索引)
|
||||
|
||||
**查询性能:**
|
||||
存在性检查会增加最小的开销 (~1-10 毫秒)
|
||||
集合存在后,不会对性能产生影响
|
||||
每个维度集合都独立优化
|
||||
|
||||
**存储开销:**
|
||||
每个集合的元数据非常少
|
||||
主要开销是每个维度的存储
|
||||
权衡:存储空间与维度灵活性
|
||||
|
||||
## 未来考虑
|
||||
|
||||
**自动维度合并:**
|
||||
可以添加后台进程来识别和合并未使用的维度变体
|
||||
这将需要重新嵌入或降维
|
||||
|
||||
**维度发现:**
|
||||
可以公开 API 来列出集合中使用的所有维度
|
||||
对于管理和监控非常有用
|
||||
|
||||
**默认维度偏好:**
|
||||
可以跟踪每个集合的 "主" 维度
|
||||
用于在维度上下文不可用的情况下进行查询
|
||||
|
||||
**存储配额:**
|
||||
可能需要每个集合的维度限制
|
||||
防止维度变体的过度繁殖
|
||||
|
||||
## 迁移说明
|
||||
|
||||
**从预维度后缀系统:**
|
||||
旧集合:`d_{user}_{collection}` (没有维度后缀)
|
||||
新集合:`d_{user}_{collection}_{dim}` (带有维度后缀)
|
||||
没有自动迁移 - 旧集合仍然可以访问
|
||||
如果需要,请考虑手动迁移脚本
|
||||
可以同时运行这两种命名方案
|
||||
|
||||
## 引用
|
||||
|
||||
集合管理:`docs/tech-specs/collection-management.md`
|
||||
存储模式:`trustgraph-base/trustgraph/schema/services/storage.py`
|
||||
Librarian 服务:`trustgraph-flow/trustgraph/librarian/service.py`
|
||||
Loading…
Add table
Add a link
Reference in a new issue