Initial commit
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 339 KiB |
|
After Width: | Height: | Size: 105 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 161 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 233 KiB |
@@ -0,0 +1,486 @@
|
||||
# Neo4J 简单应用
|
||||
|
||||
## 一、什么是知识图谱
|
||||
|
||||
**知识图谱(Knowledge Graph, KG)** 源于自然语言理解,其目标是用一种结构化的方式,来描述现实世界中的实体及其相互关系。它主要由两个核心要素构成:
|
||||
|
||||
1. **节点(Nodes)**:代表现实世界中的“实体”(Entities),例如一个人、一部电影、一家公司或一个具体概念。
|
||||
2. **边(Edges)**:代表实体与实体之间的“关系”(Relations)。
|
||||
|
||||
这些元素共同构成了一个庞大的语义网络,其基本结构可以表示为 **(实体)- [关系] -> (实体)** 的三元组(Triples)。例如,“饺子”和“哪吒2”是两个实体,“导演”就是它们之间的关系,构成一个知识三元组:(饺子)- [导演] -> (哪吒2)。
|
||||
|
||||
## 二、知识图谱的应用
|
||||
|
||||
知识图谱并非一个孤立的学术概念,它在工业界有着广泛且深入的应用,尤其是在需要深度结合领域知识的场景中。
|
||||
|
||||
### 2.1 风险识别与网络分析
|
||||
|
||||
俗话说“近朱者赤,近墨者黑”,在许多领域,一个实体的风险或属性,往往与其关联的其他实体有很强的相关性。知识图谱正是挖掘这种关联性的利器。
|
||||
|
||||
- **犯罪网络侦查**:公安部门可以利用通话记录、社交关系、转账流水等信息构建犯罪嫌疑人网络。在这个网络中,如果某个节点与多个已知的犯罪分子有直接或间接的联系,那么他参与犯罪的可能性就大大增加。通过分析网络中的核心人物(连接数最多的节点)和资金流向,可以有效地打击整个犯罪团伙。
|
||||
- **信用卡反欺诈**:银行可以将申请人的信息(如电话、地址、公司)构建成一个庞大的关系网络。通过分析这张网络,可以识别出“欺诈团伙”——例如,多个申请人共享同一个联系电话或家庭住址,或者与已知的欺诈分子有紧密的社交关系。
|
||||
|
||||
### 2.2 智能诊断与运维
|
||||
|
||||
- **工业设备运维**:将设备的各种“故障现象”、“故障原因”、“解决方案”和“所需零件”构建成知识图谱。当设备出现问题时,系统可以根据上报的现象,在图谱中进行推理,快速定位可能的原因,并给出维修建议,甚至可以提示维修人员需要携带哪些工具和备件,从而提高维修效率。
|
||||
- **医疗辅助诊断**:医疗领域知识繁杂,可以通过构建“病症”、“疾病”、“检查项目”、“治疗方案”、“药品”之间的关系图谱。医生输入患者的症状后,系统可以辅助推荐需要进行的检查,并根据检查结果在图谱中推理,给出可能的诊断建议和治疗方案,帮助实现规范化诊疗。
|
||||
|
||||
### 2.3 特定领域聊天机器人
|
||||
|
||||
对于通用领域的开放式聊天,大语言模型(LLM)已展现出强大的能力。但在许多垂直领域,基于知识图谱的问答系统(KBQA)因其答案的准确性和可解释性,仍然具有不可替代的价值。其工作流程通常如下:
|
||||
|
||||
1. **意图识别**:首先判断用户提问的意图。例如,“我想买一张明天上午的故宫门票”这个问题的意图是“票务预订”。
|
||||
2. **槽位填充 (实体抽取)**:从问题中抽取出关键信息,即“实体”。例如:`景点: 故宫`, `时间: 明天上午`, `数量: 一张`。
|
||||
3. **知识查询**:利用抽取出的实体,在知识图谱(或数据库)中进行精确查询。
|
||||
4. **回复生成**:将查询到的结果,通过预设的模板生成自然语言回复。
|
||||
|
||||
这种方式虽然不如 LLM 灵活,但在机票预订、酒店查询、银行客服等业务逻辑明确的场景中,能够提供更加可靠和可控的服务。
|
||||
|
||||
## 三、知识图谱的构建
|
||||
|
||||
如何从海量的、非结构化的文本(如新闻、财报、医疗记录)中,自动地构建出结构化的知识图谱,是整个技术流程的核心挑战。
|
||||
|
||||
### 3.1 经典构建流程
|
||||
|
||||
传统的知识图谱构建过程主要依赖于两项关键的 NLP 技术:
|
||||
|
||||
1. **命名实体识别 (Named Entity Recognition, NER)**:从文本中识别并抽取出特定类别的实体。例如,在“英伟达发布了专为 AI 设计的 Blackwell 芯片”这句话中,识别出“英伟达”(公司)、“Blackwell”(产品)。这些被抽取的实体将成为知识图谱中的 **节点**。
|
||||
2. **关系抽取 (Relation Extraction, RE)**:在识别出实体的基础上,进一步判断实体与实体之间存在何种语义关系。在上面的例子中,模型需要判断“英伟达”和“Blackwell”之间的关系是“发布”。这个关系将成为连接两个节点的 **边**。
|
||||
|
||||
通过对大量文本进行这两步处理,我们就能源源不断地抽取出知识三元组,最终汇聚成一个庞大的知识图谱。
|
||||
|
||||
### 3.2 大模型带来的革新
|
||||
|
||||
随着大语言模型的兴起,传统的 NLP 任务流程正在被重塑。LLM 同样具备强大的实体识别和关系抽取能力,但这并不意味着对传统流程的简单替代,而是呈现出深度融合的趋势。
|
||||
|
||||
- **局限性与挑战**:完全依赖 LLM 会面临成本高昂、数据隐私(使用闭源 API 时)、以及“幻觉”问题,即模型可能会编造事实。
|
||||
- **融合方案**:为了结合知识图谱的准确性和大模型的推理能力,微软提出了 GraphRAG。原理是将知识图谱作为一个可靠、可随时更新的 **外部知识库**,并基于图结构进行“子图检索”(如社区发现、路径搜索等),而非检索孤立事实。当用户提问时:
|
||||
1. 利用模型从问题中识别出核心实体与约束。
|
||||
2. 在图中检索与之高度相关的子图(社区/路径/邻域),获得准确且可解释的事实与关系。
|
||||
3. 将该子图的结构化信息作为上下文,连同原始问题一起输入给大语言模型,生成基于证据的答案。
|
||||
|
||||
## 四、图数据库:Neo4j
|
||||
|
||||
> [Neo4j 官方文档](https://neo4j.com/docs/)
|
||||
|
||||
知识图谱需要专门的数据库进行存储和查询,这类数据库被称为 **图数据库 (Graph Database)**。其中,与传统的关系型数据库(如 MySQL)相比,图数据库的优势在于其对“关系”的查询性能。对于需要进行多层关系遍历的复杂查询(例如,查询“我朋友的朋友”),图数据库的效果远超关系型数据库。而 **Neo4j** 就是目前比较流行的一款开源图数据库。
|
||||
|
||||
### 4.1 核心概念
|
||||
|
||||
Neo4j 的数据模型主要包含以下几个概念:
|
||||
|
||||
- **节点 (Node)**:节点是图中的基本数据单元,用于表示现实世界中的实体,例如一个人、一家公司、一本书或一个账户。在关系型数据库中,节点可以类比为表中的一行。
|
||||
|
||||
- **标签 (Label)**:用于为节点分类或打上“类型”标记。一个节点可以拥有一个或多个标签。例如,一个节点可以同时拥有 `:Person` 和 `:Author` 两个标签,表示这个人既是一个普通人,也是一位作者。
|
||||
|
||||
- **关系 (Relationship)**:这是图数据库的精髓所在,它以一种富有表现力的方式连接两个节点,并明确地定义了它们之间的联系。每个关系都具有以下特点:
|
||||
- **有方向**:关系总是从一个“起始节点”指向一个“结束节点”。
|
||||
- **有类型**:每个关系都必须有一个类型(例如 `:FRIENDS_WITH`, `:PURCHASED`),用来描述连接的性质。
|
||||
- **可以拥有属性**:和节点一样,关系也可以存储属性,例如,一个 `:PURCHASED` 关系可以有一个 `date` 属性来记录购买日期。
|
||||
|
||||
- **属性 (Property)**:属性是以键值对(Key-Value)形式存储在节点和关系上的详细信息。键是字符串,值可以是各种基本数据类型(如字符串、数字、布尔值)或它们的数组。
|
||||
|
||||
这四个概念共同构成了一个灵活而强大的数据模型。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["Alice:Person {name: 'Alice'}"]
|
||||
B["Bob:Person {name: 'Bob'}"]
|
||||
A -- "KNOWS {since: 2020}" --> B
|
||||
```
|
||||
> 在上图中,`Alice` 和 `Bob` 是 **节点**,`:Person` 是 **标签**,`{name: 'Alice'}` 是 **属性**,`KNOWS` 则是连接它们的 **关系** 类型,而 `{since: 2020}` 是这段关系上的 **属性**。
|
||||
|
||||
### 4.2 查询语言:Cypher
|
||||
|
||||
Cypher 是 Neo4j 的声明式图形查询语言,它的语法灵感来源于 SQL,但针对图的特性进行了优化。通过 Cypher,我们可以用一种直观且高效的方式来查询和操作图数据。
|
||||
|
||||
例如,要查找在电影《黑客帝国》(The Matrix) 中出演过的所有演员,可以使用以下查询:
|
||||
|
||||
```cypher
|
||||
MATCH (actor:Person)-[:ACTED_IN]->(movie:Movie {title: 'The Matrix'})
|
||||
RETURN actor.name
|
||||
```
|
||||
|
||||
官方的 Cypher 语法速查表([在线版本](https://neo4j.com/docs/cypher-refcard/4.4/))汇总了常用的命令、操作符和语法结构,可供读者快速查阅。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/1_4_2_1.png" alt="Cypher 语法速查表" width="100%" />
|
||||
<p>图 1.1: Cypher 语法速查表 (Cypher Refcard)</p>
|
||||
</div>
|
||||
|
||||
### 4.3 安装与使用
|
||||
|
||||
对于初学者和开发者,推荐以下两种主流的安装方式。
|
||||
|
||||
1. **Neo4j Desktop (推荐用于本地学习)**
|
||||
- **安装**:
|
||||
1. 访问 [Neo4j 官网](https://neo4j.com/download/),在 “Neo4j for Desktop” 板块点击 “Download” 按钮。
|
||||
<div align="center">
|
||||
<img src="images/1_4_3_1.png" alt="Neo4j Desktop 下载页面" width="100%" />
|
||||
<p>图 1.2: 在官网点击下载</p>
|
||||
</div>
|
||||
2. 页面会跳转至一个注册表单。可以填写任意信息,然后点击 “Download Desktop” 按钮,浏览器将自动开始下载安装包。
|
||||
<div align="center">
|
||||
<img src="images/1_4_3_2.png" alt="下载前填写表单" width="100%" />
|
||||
<p>图 1.3: 填写注册表单</p>
|
||||
</div>
|
||||
3. 下载完成后,双击安装文件,程序会自动进行安装。
|
||||
4. 安装完成后首次启动,会看到许可协议界面,点击 “Continue” 即可完成最后的设置。
|
||||
<div align="center">
|
||||
<img src="images/1_4_3_3.png" alt="同意许可协议" width="100%" />
|
||||
<p>图 1.4: 首次启动并同意许可协议</p>
|
||||
</div>
|
||||
|
||||
2. **Docker (推荐用于服务器部署与跨平台开发)**
|
||||
- **安装**: 只需一行命令即可完成拉取镜像和启动容器。
|
||||
```bash
|
||||
docker run \
|
||||
--name my-neo4j \
|
||||
-p 7474:7474 -p 7687:7687 \
|
||||
-d \
|
||||
-v $HOME/neo4j/data:/data \
|
||||
-v $HOME/neo4j/logs:/logs \
|
||||
--env NEO4J_AUTH=neo4j/password \
|
||||
neo4j:latest
|
||||
```
|
||||
- **参数说明**:
|
||||
- `-p 7474:7474`: 将容器的 HTTP 端口映射到本机,用于浏览器访问。
|
||||
- `-p 7687:7687`: 将容器的 Bolt 驱动端口映射到本机,用于代码连接。
|
||||
- `-v $HOME/neo4j/data:/data`: 将数据目录挂载到本机,确保数据持久化。
|
||||
- `--env NEO4J_AUTH=neo4j/password`: 设置数据库的初始用户名和密码(此处为 `neo4j/password`)。
|
||||
- `neo4j:latest`: 使用最新的官方镜像。
|
||||
|
||||
安装好 Neo4j 后。我们就可以学习一些 Neo4j 的基本用法了。
|
||||
|
||||
## 五、创建并连接数据库
|
||||
|
||||
在使用 Neo4j 进行开发时,首先需要在 Neo4j Desktop 中创建一个本地数据库实例(Instance)。这个过程非常直观。
|
||||
|
||||
1. **创建实例**:打开 Neo4j Desktop,在 “Local instances” 页面点击 “Create instance” 按钮。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/2_1_1.png" alt="创建实例" width="100%" />
|
||||
<p>图 2.1: 点击创建实例</p>
|
||||
</div>
|
||||
|
||||
2. **配置实例**:在弹出的窗口中,为实例命名(例如 `base nlp`),选择所需的 Neo4j 版本,并为默认用户 `neo4j` 设置一个能记住的密码。完成后点击 “Create”。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/2_1_2.png" alt="配置实例" width="100%" />
|
||||
<p>图 2.2: 配置实例信息</p>
|
||||
</div>
|
||||
|
||||
3. **启动与连接**:实例创建后会自动启动,状态显示为 “RUNNING”。此时,可以通过浏览器直接访问 `http://127.0.0.1:7474` 来打开 Neo4j Browser。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/2_1_3.png" alt="启动实例" width="100%" />
|
||||
<p>图 2.3: 实例创建成功并运行</p>
|
||||
</div>
|
||||
|
||||
在浏览器打开的连接界面中,使用刚刚设置的密码进行连接。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/2_1_4.png" alt="连接实例" width="100%" />
|
||||
<p>图 2.4: 使用密码连接数据库</p>
|
||||
</div>
|
||||
|
||||
## 六、增删查改
|
||||
|
||||
数据库操作的核心无外乎增删查改(CRUD),下面来使用 Cypher,围绕一个菜品信息图谱的场景,逐一介绍这些基本操作。
|
||||
|
||||
### 6.1 场景设定
|
||||
|
||||
为了方便演示,先设定好本次实践所需要用到的实体、属性和关系。
|
||||
|
||||
- **实体/标签 (Labels)**:
|
||||
- `Ingredient`: 食材,拥有 `name`, `category`(类别), `origin`(产地), `tags`(标签,数组)等属性。
|
||||
- `Dish`: 菜品,拥有 `name`, `cuisine`(菜系)等属性。
|
||||
- **关系 (Relationships)**:
|
||||
- `(Dish)-[:包含]->(Ingredient)`: 表示某菜品包含某种食材,关系上可以有 `用量` 属性。
|
||||
- `(Dish)-[:主要食材]->(Ingredient)`: 表示某菜品的主要食材是某种食材。
|
||||
- `(Dish)-[:调味]->(Ingredient)`: 表示某菜品使用某种食材进行调味。
|
||||
|
||||
### 6.2 创建 (CREATE)
|
||||
|
||||
`CREATE` 语句用于在图中创建新的节点和关系。
|
||||
|
||||
#### 6.2.1 创建节点
|
||||
|
||||
创建节点的基本语法是 `CREATE (变量:标签 {属性: 值})`。
|
||||
|
||||
- **变量 (Variable)**: 如 `pork`,是一个临时名称,用于在同一条语句中引用该节点。如果后续不需要引用,可以省略。
|
||||
- **标签 (Label)**: 如 `Ingredient`,用于对节点进行分类。
|
||||
- **属性 (Properties)**: 一个包含键值对的 map/字典,用于描述节点的具体信息。
|
||||
|
||||
最基础的创建语句包含一个临时变量(`pork`)、一个标签(`Ingredient`)和一组属性。
|
||||
|
||||
```cypher
|
||||
CREATE (pork:Ingredient {name:'猪肉', category:'肉类', origin:'杭州'});
|
||||
```
|
||||
|
||||
如果在创建后不需要立刻使用这个节点(例如,在同一查询中创建关系),可以省略临时变量名,这样语法更简洁。
|
||||
|
||||
```cypher
|
||||
CREATE (:Ingredient {name:'土豆', category:'蔬菜', origin:'北京'});
|
||||
```
|
||||
|
||||
还可以在创建节点后,使用 `RETURN` 子句立即将其返回。这对于调试或确认节点是否按预期创建非常有用。`RETURN n` 会在结果面板中直接显示刚刚创建的 `鸡蛋` 节点的信息。
|
||||
|
||||
```cypher
|
||||
CREATE (n:Ingredient {name:'鸡蛋'}) RETURN n;
|
||||
```
|
||||
|
||||
执行上述三条命令后,数据库中就创建了三个 `Ingredient` 类型的节点。能够通过 Neo4j Browser 的可视化界面直观地看到这些新创建的数据。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/2_2_1_1.png" alt="创建节点后的数据库信息" width="100%" />
|
||||
<p>图 2.5: 执行创建命令后,左侧面板显示已有 3 个 Ingredient 节点</p>
|
||||
</div>
|
||||
|
||||
点击左侧面板中的 `Ingredient` 标签,Neo4j Browser 会自动执行 `MATCH (n:Ingredient) RETURN n LIMIT 25;` 查询,并在主窗口中展示所有食材节点。如图 2.6 所示,点击其中一个节点(如“土豆”),右侧会显示其详细属性。这里可以观察到:
|
||||
|
||||
- **`<id>` 字段**:这是 Neo4j 为每个节点自动生成的内部唯一标识符。
|
||||
- **Key-Value 结构**:右侧的 “Key” 和 “Value” 两列展示了节点属性是以键值对的形式存储的。
|
||||
- **自定义属性**:`name`、`category`、`origin` 三个字段的值与前面 `CREATE` 语句中设定的值完全一致。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/2_2_1_2.png" alt="查询并查看节点详情" width="100%" />
|
||||
<p>图 2.6: 查询并查看新创建的节点及其属性</p>
|
||||
</div>
|
||||
|
||||
#### 6.2.2 创建关系
|
||||
|
||||
关系的创建通常需要先指定关系两端的节点,然后用 `-[变量:类型 {属性}]->` 来定义关系。
|
||||
|
||||
- 关系必须有 **方向** 和 **类型 (Type)**。
|
||||
- 小括号 `()` 用于表示节点,中括号 `[]` 用于表示关系。
|
||||
|
||||
在实际应用中,常常需要一次性创建多个节点以及它们之间的关系。`CREATE` 语句支持通过逗号分隔,在一个查询中完成复杂图谱的构建。下面的例子将创建一个更复杂的菜品关系网络,以体现“多对多”的特性(一道菜包含多种食材,一种食材可用于多道菜)。
|
||||
|
||||
```cypher
|
||||
CREATE
|
||||
// 创建食材节点
|
||||
(rousi:Ingredient {name:'猪里脊'}),
|
||||
(muer:Ingredient {name:'木耳'}),
|
||||
(huluobo:Ingredient {name:'胡萝卜'}),
|
||||
(qingjiao:Ingredient {name:'青椒'}),
|
||||
// 创建菜品节点
|
||||
(d1:Dish {name:'鱼香肉丝', cuisine:'川菜'}),
|
||||
(d2:Dish {name:'木须肉', cuisine:'鲁菜'}),
|
||||
// 创建关系
|
||||
(d1)-[:包含 {amount:'250g'}]->(rousi), (d1)-[:包含]->(muer), (d1)-[:包含]->(huluobo),
|
||||
(d2)-[:包含 {amount:'150g'}]->(rousi), (d2)-[:包含]->(muer),
|
||||
// 创建双向关系
|
||||
(rousi)-[:被用于]->(d1), (muer)-[:被用于]->(d1), (huluobo)-[:被用于]->(d1),
|
||||
(rousi)-[:被用于]->(d2), (muer)-[:被用于]->(d2);
|
||||
```
|
||||
这个查询语句做了以下几件事:
|
||||
1. **创建了 4 个 `Ingredient` 节点**:猪里脊、木耳、胡萝卜、青椒。
|
||||
2. **创建了 2 个 `Dish` 节点**:鱼香肉丝、木须肉。
|
||||
3. **创建了 5 条 `包含` 关系**:从菜品指向食材。
|
||||
4. **创建了 5 条 `被用于` 关系**:从食材指向菜品。这样既可以方便地查询“一道菜包含哪些食材”,也可以高效地反向查询“一种食材被用在了哪些菜里”。
|
||||
|
||||
执行 `MATCH p=()-[:包含]->() RETURN p LIMIT 25;` 查询可以可视化展示所有“包含”关系。点击关系(箭头),可以在右侧看到其详细信息,例如“鱼香肉丝”到“猪里脊”的关系上,就包含了在 `CREATE` 语句中定义的 `amount: '250g'` 这一属性。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/2_2_2_1.png" alt="同时创建节点和关系后的图谱" width="100%" />
|
||||
<p>图 2.7: 创建关系后的图谱结构</p>
|
||||
</div>
|
||||
|
||||
### 6.3 查询 (MATCH)
|
||||
|
||||
`MATCH` 是 Cypher 中用于查询图数据的命令,它允许你描述你想要寻找的节点和关系的模式。
|
||||
|
||||
#### 6.3.1 基本查询
|
||||
|
||||
最简单的查询是匹配并返回图中的任意节点,可以使用 `LIMIT` 关键字限制返回数量,避免因数据量过大导致浏览器卡顿。
|
||||
|
||||
```cypher
|
||||
// 匹配并返回图中的任意 25 个节点
|
||||
MATCH (n)
|
||||
RETURN n
|
||||
LIMIT 25;
|
||||
```
|
||||
|
||||
也可以根据标签和属性进行精确匹配。
|
||||
|
||||
```cypher
|
||||
// 匹配所有标签为 Ingredient,且名字为'猪里脊'的节点
|
||||
MATCH (n:Ingredient {name:'猪里脊'}) RETURN n;
|
||||
```
|
||||
|
||||
#### 6.3.2 条件查询 (WHERE)
|
||||
|
||||
`WHERE` 子句提供了更灵活的过滤能力,可以对节点的属性进行复杂的逻辑判断。
|
||||
|
||||
例如,查询名字是'猪里脊'或'鸡蛋'的 `Ingredient` 节点。
|
||||
|
||||
```cypher
|
||||
MATCH (n:Ingredient)
|
||||
WHERE n.name IN ['猪里脊','鸡蛋']
|
||||
RETURN n;
|
||||
```
|
||||
|
||||
也可以使用 `AND`、`OR` 等关键字构建复合查询条件。
|
||||
|
||||
```cypher
|
||||
// 复合条件:查询指定名称且类别为“肉类”的节点
|
||||
MATCH (n:Ingredient)
|
||||
WHERE n.name IN ['猪肉', '猪里脊', '鸡蛋'] AND n.category = '肉类'
|
||||
RETURN n;
|
||||
```
|
||||
|
||||
#### 6.3.3 返回指定属性
|
||||
|
||||
默认情况下,`RETURN n` 会返回整个节点对象。也可以只返回节点的特定属性,并使用 `AS` 为返回的列起别名,使结果更具可读性。
|
||||
|
||||
```cypher
|
||||
MATCH (n:Ingredient)
|
||||
WHERE n.name IN ['猪里脊','鸡蛋']
|
||||
RETURN n.name AS 食材名称, n.category AS 类别;
|
||||
```
|
||||
|
||||
#### 6.3.4 关联查询
|
||||
|
||||
图数据库最强大的地方在于对关系的查询。例如,可以一次性查询“鱼香肉丝”和“木须肉”分别包含了哪些食材。
|
||||
|
||||
```cypher
|
||||
MATCH (d:Dish)-[:包含]->(i:Ingredient)
|
||||
WHERE d.name IN ['鱼香肉丝', '木须肉']
|
||||
RETURN d.name AS 菜品, collect(i.name) AS 食材列表;
|
||||
```
|
||||
> `collect()` 是一个聚合函数,可以将匹配到的多个同类结果(这里是食材名称 `i.name`)收集到一个列表中。
|
||||
|
||||
#### 6.3.5 查询并创建 (MATCH + CREATE)
|
||||
|
||||
在实际应用中,一个常见的操作是先找到图中已经存在的节点,然后为它们添加新的关系。这可以通过组合使用 `MATCH` 和 `CREATE` 来实现。
|
||||
|
||||
例如,我们已经创建了“鱼香肉丝”和“猪里脊”,现在想为它们添加一条“主要食材”的关系。
|
||||
|
||||
```cypher
|
||||
MATCH
|
||||
(d:Dish {name:'鱼香肉丝'}),
|
||||
(i:Ingredient {name:'猪里脊'})
|
||||
MERGE
|
||||
(d)-[r:主要食材]->(i)
|
||||
RETURN d, i, r;
|
||||
```
|
||||
> 这个模式确保了是在已有的、正确的实体之间建立关联,并通过 `MERGE` 避免重复的关系。
|
||||
|
||||
#### 6.3.6 排序 (ORDER BY)
|
||||
|
||||
可以使用 `ORDER BY` 子句对返回的结果进行排序。默认是升序 (`ASC`),也可以指定为降序 (`DESC`)。
|
||||
|
||||
```cypher
|
||||
// 查询所有食材,并按名称升序排序
|
||||
MATCH (i:Ingredient)
|
||||
RETURN i.name, i.category
|
||||
ORDER BY i.name ASC;
|
||||
```
|
||||
|
||||
### 6.4 更新 (SET & MERGE)
|
||||
|
||||
#### 6.4.1 更新属性 (SET)
|
||||
|
||||
`SET` 语句用于修改或添加节点/关系的属性。它必须和 `MATCH` 配合使用,先找到要更新的实体,再进行修改。
|
||||
|
||||
```cypher
|
||||
MATCH (i:Ingredient {name:'猪肉'})
|
||||
SET
|
||||
i.is_frozen = true,
|
||||
i.origin = '金华'
|
||||
RETURN i;
|
||||
```
|
||||
|
||||
#### 6.4.2 插入或更新 (MERGE)
|
||||
|
||||
在构建知识图谱时,经常遇到这样的场景:如果某个节点已存在,则更新其属性;如果不存在,则创建它。`MERGE` 语句就可以解决这个问题。
|
||||
|
||||
`MERGE` 会根据你提供的模式在图中查找,如果找到匹配项,则执行 `ON MATCH` 部分;如果未找到,则执行 `ON CREATE` 部分,从而避免了重复创建实体。
|
||||
|
||||
```cypher
|
||||
// 查找名为'大蒜'的 Ingredient 节点
|
||||
MERGE (n:Ingredient {name: '大蒜'})
|
||||
// 如果不存在,则创建该节点,并设置创建时间和初始库存
|
||||
ON CREATE SET
|
||||
n.created = timestamp(),
|
||||
n.stock = 100
|
||||
// 如果已存在,则更新其库存、访问次数和访问时间
|
||||
ON MATCH SET
|
||||
n.stock = coalesce(n.stock, 0) - 1,
|
||||
n.counter = coalesce(n.counter, 0) + 1,
|
||||
n.accessTime = timestamp()
|
||||
RETURN n;
|
||||
```
|
||||
> `coalesce(property, defaultValue)` 是一个非常有用的函数,它会检查属性 `property` 是否存在,如果存在则返回其值,否则返回 `defaultValue`。
|
||||
|
||||
### 6.5 删除 (DELETE & REMOVE)
|
||||
|
||||
#### 6.5.1 删除属性 (REMOVE)
|
||||
|
||||
`REMOVE` 用于移除节点或关系上的某个属性。在下面的例子中,先用 `MATCH` 找到名为“大蒜”的节点,然后移除由 `MERGE` 命令在创建它时添加的 `created` 属性。
|
||||
|
||||
```cypher
|
||||
MATCH (i:Ingredient {name:'大蒜'})
|
||||
REMOVE i.created
|
||||
RETURN i;
|
||||
```
|
||||
|
||||
#### 6.5.2 删除节点和关系 (DELETE)
|
||||
|
||||
`DELETE` 用于删除节点和关系。但需要 **特别注意**:Neo4j 不允许直接删除一个还存在关联关系的节点。你必须先删除关系,才能删除节点。
|
||||
|
||||
```cypher
|
||||
// 错误示范:如果'大蒜'还有关系连着,这条语句会报错
|
||||
MATCH (i:Ingredient {name:'大蒜'})
|
||||
DELETE i;
|
||||
```
|
||||
|
||||
正确的做法有两种。第一种是先手动删除与节点相关的所有关系,然后再删除节点本身。
|
||||
|
||||
```cypher
|
||||
// 正确做法 1:先删除关系,再删除节点
|
||||
MATCH (i:Ingredient {name:'大蒜'})-[r]-() // 匹配与'大蒜'相连的任意关系
|
||||
DELETE r, i; // 先删除关系 r,再删除节点 i
|
||||
```
|
||||
|
||||
第二种做法更简洁,也是官方推荐的方式:使用 `DETACH DELETE`。它会自动删除指定节点以及所有与它直接相连的关系。
|
||||
|
||||
```cypher
|
||||
// 正确做法 2:使用 DETACH DELETE (推荐)
|
||||
MATCH (i:Ingredient {name:'大蒜'})
|
||||
DETACH DELETE i;
|
||||
```
|
||||
|
||||
此外,还可以通过节点的内部 ID 进行精确查找和删除。每个节点都有一个由 Neo4j 自动分配的唯一 ID,可以通过 `id()` 函数获取。
|
||||
|
||||
```cypher
|
||||
// 假设我们通过查询得知“大蒜”的 ID 为 5
|
||||
MATCH (i:Ingredient)
|
||||
WHERE id(i) = 5
|
||||
DETACH DELETE i;
|
||||
```
|
||||
|
||||
#### 6.5.3 清空数据库
|
||||
|
||||
如果想删除数据库中的所有节点和关系,可以使用以下命令:
|
||||
|
||||
```cypher
|
||||
// 匹配所有节点 n
|
||||
MATCH (n)
|
||||
// 强制删除节点 n 及其所有关系
|
||||
DETACH DELETE n;
|
||||
```
|
||||
|
||||
#### 6.5.4 软删除
|
||||
|
||||
在生产环境中,直接从数据库中物理删除(`DELETE`)数据是一种高风险操作。一种更安全、更常见的做法是“软删除”。软删除并非真的将数据移除,而是通过 `SET` 命令为其添加一个状态属性,将其标记为“已删除”或“不活跃”。
|
||||
|
||||
```cypher
|
||||
// 将“木耳”标记为不活跃
|
||||
MATCH (i:Ingredient {name:'木耳'})
|
||||
SET i.is_active = false;
|
||||
```
|
||||
这样,在后续的查询中,只需要增加一个 `WHERE i.is_active = true` 的过滤条件,就能只使用那些“活跃”的数据,而被软删除的数据依然保留在数据库中,以备审计或恢复。
|
||||
|
||||
> 删除操作是高风险行为,尤其是在生产环境中。执行前请务必确认操作对象和范围,并做好数据备份。
|
||||
@@ -0,0 +1,26 @@
|
||||
# PowerRAG (RAGFlow) SDK demo config
|
||||
|
||||
# SDK endpoint (from your docker-compose env: SVR_HTTP_PORT=9380)
|
||||
RAGFLOW_BASE_URL=http://127.0.0.1:9380
|
||||
|
||||
# SDK API key (format: ragflow-...; created via /v1/api/new_token)
|
||||
RAGFLOW_API_KEY=ragflow-REPLACE_ME
|
||||
|
||||
# Optional: override dataset name created by the demo
|
||||
RAGFLOW_DATASET_NAME=powerrag_text_qa_demo
|
||||
|
||||
# Optional: override embedding model for dataset creation (recommended to leave empty and use tenant default)
|
||||
# Format: <model>@<factory>
|
||||
# Example:
|
||||
# RAGFLOW_EMBEDDING_MODEL=text-embedding-3-small@OpenAI
|
||||
RAGFLOW_EMBEDDING_MODEL=
|
||||
|
||||
# -----------------------------
|
||||
# Optional: embedding provider config (used by the README “API 配置 embedding” steps)
|
||||
# -----------------------------
|
||||
|
||||
# Use the factory/model name shown by your PowerRAG UI/API.
|
||||
EMB_FACTORY=REPLACE_ME
|
||||
EMB_MODEL=REPLACE_ME
|
||||
EMB_API_BASE=REPLACE_ME
|
||||
EMB_API_KEY=REPLACE_ME
|
||||
@@ -0,0 +1,46 @@
|
||||
"""
|
||||
PowerRAG (RAGFlow) SDK Demo configuration.
|
||||
|
||||
This module follows the `code/` directory convention:
|
||||
- Provide a small config object
|
||||
- Load `.env` automatically (if present)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _bool_env(name: str, default: bool = False) -> bool:
|
||||
raw = os.getenv(name)
|
||||
if raw is None:
|
||||
return default
|
||||
raw = raw.strip().lower()
|
||||
if raw in {"1", "true", "yes", "y", "on"}:
|
||||
return True
|
||||
if raw in {"0", "false", "no", "n", "off"}:
|
||||
return False
|
||||
return default
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PowerRAGDemoConfig:
|
||||
base_url: str = os.getenv("RAGFLOW_BASE_URL", "http://127.0.0.1:9380").strip()
|
||||
api_key: str = os.getenv("RAGFLOW_API_KEY", "").strip()
|
||||
dataset_name: str = os.getenv("RAGFLOW_DATASET_NAME", "powerrag_text_qa_demo").strip()
|
||||
embedding_model: str = os.getenv("RAGFLOW_EMBEDDING_MODEL", "").strip()
|
||||
|
||||
top_k: int = int(os.getenv("RAGFLOW_TOP_K", "5"))
|
||||
candidate_k: int = int(os.getenv("RAGFLOW_CANDIDATE_K", "1024"))
|
||||
similarity_threshold: float = float(os.getenv("RAGFLOW_SIMILARITY_THRESHOLD", "0.2"))
|
||||
vector_similarity_weight: float = float(os.getenv("RAGFLOW_VECTOR_SIMILARITY_WEIGHT", "0.3"))
|
||||
keyword: bool = _bool_env("RAGFLOW_KEYWORD", False)
|
||||
|
||||
|
||||
DEFAULT_CONFIG = PowerRAGDemoConfig()
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from config import DEFAULT_CONFIG
|
||||
|
||||
|
||||
def _env(name: str, default: str | None = None) -> str | None:
|
||||
value = os.getenv(name)
|
||||
if value is None or value.strip() == "":
|
||||
return default
|
||||
return value.strip()
|
||||
|
||||
|
||||
def _require(value: str | None, hint: str) -> str:
|
||||
if value is None or value.strip() == "":
|
||||
raise SystemExit(hint)
|
||||
return value.strip()
|
||||
|
||||
|
||||
def _read_bytes(path: Path) -> bytes:
|
||||
try:
|
||||
return path.read_bytes()
|
||||
except FileNotFoundError:
|
||||
raise SystemExit(f"File not found: {path}")
|
||||
|
||||
|
||||
def _safe_get(obj: Any, attr: str, default: Any = None) -> Any:
|
||||
try:
|
||||
return getattr(obj, attr)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="PowerRAG (RAGFlow) SDK demo: upload Markdown, parse, retrieve top-k chunks.",
|
||||
)
|
||||
parser.add_argument("--file", type=Path, required=True, help="Markdown file path, e.g. ./data/sample.md")
|
||||
parser.add_argument("--question", type=str, required=True, help="User question for retrieval")
|
||||
parser.add_argument("--top-k", type=int, default=DEFAULT_CONFIG.top_k, help="How many chunks to return (mapped to page_size)")
|
||||
parser.add_argument(
|
||||
"--embedding-model",
|
||||
type=str,
|
||||
default=DEFAULT_CONFIG.embedding_model or _env("RAGFLOW_EMBEDDING_MODEL"),
|
||||
help=(
|
||||
"Embedding model string in '<model>@<factory>' format. "
|
||||
"If omitted, server tenant default is used."
|
||||
),
|
||||
)
|
||||
parser.add_argument("--candidate-k", type=int, default=DEFAULT_CONFIG.candidate_k, help="RAGFlow.retrieve(top_k=...) candidate pool size")
|
||||
parser.add_argument("--similarity-threshold", type=float, default=DEFAULT_CONFIG.similarity_threshold, help="Filter chunks below this similarity")
|
||||
parser.add_argument("--vector-similarity-weight", type=float, default=DEFAULT_CONFIG.vector_similarity_weight, help="Weight of vector similarity in hybrid score")
|
||||
parser.add_argument("--keyword", action="store_true", default=DEFAULT_CONFIG.keyword, help="Enable keyword matching (hybrid retrieval)")
|
||||
parser.add_argument("--dataset-name", type=str, default=DEFAULT_CONFIG.dataset_name, help="Dataset name to create")
|
||||
parser.add_argument(
|
||||
"--base-url",
|
||||
type=str,
|
||||
default=DEFAULT_CONFIG.base_url or _env("RAGFLOW_BASE_URL") or _env("POWERRAG_BASE_URL") or _env("BASE_URL"),
|
||||
help="RAGFlow/PowerRAG base_url (or env RAGFLOW_BASE_URL / POWERRAG_BASE_URL / BASE_URL)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--api-key",
|
||||
type=str,
|
||||
default=DEFAULT_CONFIG.api_key or _env("RAGFLOW_API_KEY") or _env("POWERRAG_API_KEY") or _env("API_KEY"),
|
||||
help="RAGFlow/PowerRAG api_key (or env RAGFLOW_API_KEY / POWERRAG_API_KEY / API_KEY)",
|
||||
)
|
||||
parser.add_argument("--cleanup", action="store_true", help="Delete created dataset after finishing")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
base_url = _require(args.base_url, "Missing base_url. Use --base-url or set env RAGFLOW_BASE_URL.")
|
||||
api_key = _require(args.api_key, "Missing api_key. Use --api-key or set env RAGFLOW_API_KEY.")
|
||||
|
||||
if args.top_k <= 0:
|
||||
raise SystemExit("--top-k must be > 0")
|
||||
if args.candidate_k <= 0:
|
||||
raise SystemExit("--candidate-k must be > 0")
|
||||
|
||||
blob = _read_bytes(args.file)
|
||||
display_name = args.file.name
|
||||
if not display_name.lower().endswith(".md"):
|
||||
display_name = f"{display_name}.md"
|
||||
|
||||
try:
|
||||
from ragflow_sdk import RAGFlow # type: ignore
|
||||
except Exception as e:
|
||||
raise SystemExit(
|
||||
"Failed to import ragflow_sdk. Install dependencies first:\n"
|
||||
" pip install -r requirements.txt\n"
|
||||
f"Original error: {e}"
|
||||
)
|
||||
|
||||
rag = RAGFlow(api_key=api_key, base_url=base_url)
|
||||
|
||||
dataset_kwargs: dict[str, Any] = {"name": args.dataset_name}
|
||||
if args.embedding_model:
|
||||
dataset_kwargs["embedding_model"] = args.embedding_model
|
||||
dataset = rag.create_dataset(**dataset_kwargs)
|
||||
try:
|
||||
docs = dataset.upload_documents([{"display_name": display_name, "blob": blob}])
|
||||
if not docs:
|
||||
raise SystemExit("Upload succeeded but no document returned by SDK.")
|
||||
doc = docs[0]
|
||||
|
||||
parse_results = dataset.parse_documents([doc.id])
|
||||
# parse_results: list[tuple[doc_id, status, success_count, failure_count]] (per API ref)
|
||||
print("Parse results:")
|
||||
print(parse_results)
|
||||
if parse_results and isinstance(parse_results, list):
|
||||
statuses = {r[1] for r in parse_results if isinstance(r, (list, tuple)) and len(r) >= 2}
|
||||
if statuses and statuses != {"DONE"}:
|
||||
raise SystemExit(
|
||||
"Document parsing failed (status not DONE). "
|
||||
"Most common cause is missing/unauthorized embedding model.\n"
|
||||
"Try:\n"
|
||||
" - set tenant default embedding model in UI or via /v1/user/set_tenant_info, OR\n"
|
||||
" - rerun with --embedding-model '<model>@<factory>' (must be supported & configured for the tenant)\n"
|
||||
"If it still fails, check PowerRAG logs inside the container (task executor) for the detailed error.\n"
|
||||
)
|
||||
|
||||
chunks = rag.retrieve(
|
||||
question=args.question,
|
||||
dataset_ids=[dataset.id],
|
||||
document_ids=[doc.id],
|
||||
page=1,
|
||||
page_size=args.top_k,
|
||||
similarity_threshold=args.similarity_threshold,
|
||||
vector_similarity_weight=args.vector_similarity_weight,
|
||||
top_k=args.candidate_k,
|
||||
keyword=args.keyword,
|
||||
)
|
||||
|
||||
print("\nRetrieved chunks:")
|
||||
if not chunks:
|
||||
print("(empty)")
|
||||
return 0
|
||||
|
||||
for i, c in enumerate(chunks, start=1):
|
||||
similarity = _safe_get(c, "similarity")
|
||||
vector_similarity = _safe_get(c, "vector_similarity")
|
||||
term_similarity = _safe_get(c, "term_similarity")
|
||||
content = _safe_get(c, "content", "")
|
||||
content_preview = (content or "").strip().replace("\n", " ")
|
||||
if len(content_preview) > 260:
|
||||
content_preview = content_preview[:260] + "…"
|
||||
print(f"{i:02d}. similarity={similarity} vector={vector_similarity} term={term_similarity}")
|
||||
print(f" {content_preview}")
|
||||
|
||||
return 0
|
||||
finally:
|
||||
if args.cleanup:
|
||||
try:
|
||||
rag.delete_datasets(ids=[dataset.id])
|
||||
except Exception as e:
|
||||
print(f"Warning: failed to cleanup dataset {dataset.id}: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,3 @@
|
||||
ragflow-sdk
|
||||
python-dotenv
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
1) 这个 demo 的验收标准是什么?
|
||||
2) 餐厅排队系统里,如果顾客过号,通常怎么处理?
|
||||
3) 已发货未签收的退款规则是什么?
|
||||
4) 如何估算排队等待时间?
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# PowerRAG 文本问答 Demo · 示例文档
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
本示例用于演示:上传一份 Markdown 文档 → 服务端自动解析与分块 → 基于问题检索相关 chunks。
|
||||
|
||||
## 2. 关键概念
|
||||
|
||||
- **分块(Chunk)**:把长文切成多个小段,便于向量化与检索。
|
||||
- **向量检索(Vector Search)**:把文本映射到向量空间,通过相似度找到相关片段。
|
||||
- **Top-k**:返回最相关的 k 个片段。
|
||||
|
||||
## 3. 规则与约束
|
||||
|
||||
1) 只有当“检索到的 chunks 与问题语义相关”时,才算成功。
|
||||
2) 本 demo 不要求大模型生成最终回答(可选)。
|
||||
|
||||
## 4. 示例内容:餐厅排队系统
|
||||
|
||||
我们要做一个餐厅排队系统,核心流程如下:
|
||||
|
||||
1. 顾客在前台取号,系统生成排队号(例如 A001)。
|
||||
2. 服务员在就餐区空位出现时叫号,顾客到号后入座。
|
||||
3. 如果顾客过号,可选择重新排队或延后若干位。
|
||||
4. 系统需要支持查询当前排队情况,以及某个号码前面还有多少人。
|
||||
|
||||
### 4.1 常见问题
|
||||
|
||||
- “过号后怎么处理?”:可以延后或重新取号,策略由门店决定。
|
||||
- “如何估算等待时间?”:可以用平均翻台时间 × 前方人数估算。
|
||||
- “如何处理多人同时取号?”:需要对取号操作加锁或用原子自增保证顺序。
|
||||
|
||||
## 5. 示例内容:退款规则
|
||||
|
||||
退款规则如下:
|
||||
|
||||
- 未发货:可全额退款。
|
||||
- 已发货未签收:可申请退款,但需要承担退货运费。
|
||||
- 已签收:7 天内可退货退款;超过 7 天视情况处理。
|
||||
|
||||
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 190 KiB |
@@ -0,0 +1,404 @@
|
||||
# PowerRAG SDK 文本问答检索 Demo
|
||||
|
||||
## 一、这篇专题要解决什么问题?
|
||||
|
||||
很多同学做 RAG 时会先把注意力放在“怎么让大模型回答得更像人”。但只要检索没找对上下文,生成再花哨也只是“把错讲得更顺”。
|
||||
|
||||
这个专题做一件更朴素、也更值得先掌握的事:
|
||||
|
||||
> **只做检索,不做生成。**
|
||||
|
||||
你会把一份 Markdown 文档交给服务端,让服务端完成解析、切分、向量化,然后用问题去做 Top‑K 检索,拿回最相关的原文片段(chunks)。
|
||||
|
||||
**验收标准也很直接**:Top‑K chunks 是否与问题语义相关(不要求最终答案)。
|
||||
|
||||
本专题目录结构:
|
||||
|
||||
- `readme.md`:本文(教学文档)
|
||||
- `images/`:配图
|
||||
- `code/`:可运行脚本与配置(`main.py`、`config.py`、`.env.example`、`requirements.txt`)
|
||||
- `data/`:可复现样例数据(`sample.md` + `questions.txt`)
|
||||
|
||||
---
|
||||
|
||||
## 二、技术方案:从 Markdown 到 Top‑K chunks(图文讲清楚)
|
||||
|
||||
下面这张图展示了端到端链路,也基本对应 `code/main.py` 的执行顺序。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/10_1_1.webp" alt="端到端流程图:上传→解析/切分→向量化→Top-K 检索" width="100%" />
|
||||
<p>图 10.1: 端到端流程(本 demo 只验收检索结果,不要求生成最终回答)</p>
|
||||
</div>
|
||||
|
||||
为了避免“看完图还是不知道自己要做什么”,这里把图 10.1 的关键节点按顺序讲清楚(你可以边对照图边往下读):
|
||||
|
||||
**(1)本地输入:Markdown 文档**
|
||||
|
||||
你可以直接用本专题提供的 `data/sample.md`。这份文件故意写得短:包含“排队规则”和“退款规则”,方便你用不同问题去验证检索是否命中。
|
||||
|
||||
**(2)Upload:上传到 dataset**
|
||||
|
||||
上传不是“把文本发过去就结束”,它的意义在于:服务端要把这份文档纳入某个 **dataset**(容器)里,后续切分出来的 chunks、embedding、索引都挂在这个容器下面。
|
||||
|
||||
**(3)Parse/Chunk:解析 + 切分**
|
||||
|
||||
这一步会把 Markdown 解析成可检索的文本结构,并按服务端策略切成多个 chunk。
|
||||
|
||||
> ⚠️ 图里标了一个常见失败点:如果你的 tenant 没有配置默认 embedding(`embd_id` 为空或未授权),解析任务可能直接 FAIL。
|
||||
|
||||
**(4)Embedding:向量化**
|
||||
|
||||
每个 chunk 会被映射成向量(embedding)。这一步是向量检索的前提——没有向量,后面就谈不上“语义相似”。
|
||||
|
||||
**(5)写入向量库/索引**
|
||||
|
||||
chunk + embedding 会写入向量索引(图里叫 Vector Store / Index)。
|
||||
|
||||
**(6)Retrieve Top‑K:检索并返回 chunks**
|
||||
|
||||
输入一个问题(question),服务端从索引里找出最相关的 K 个 chunk,并把这些原文片段返回给你。本 demo 的验收就看这里:**返回的 chunks 是否包含你期望的规则段落**。
|
||||
|
||||
---
|
||||
|
||||
到这里,你应该已经能把这条链路从头到尾“顺着说一遍”了:
|
||||
|
||||
> 文档上传 → 服务端解析/切分/向量化 → 写入索引 → 问题检索 → 返回 Top‑K chunks。
|
||||
|
||||
但很多初学者还有一个常见困惑:**这些名词到底对应什么对象?我拿到的结果到底是谁?**
|
||||
|
||||
所以下面我们换一个视角:不再看“流程”,而是看“对象之间的关系”。
|
||||
|
||||
---
|
||||
|
||||
再看图 10.2(对象关系)。这张图的目的只有一个:把“你上传的文件”和“检索返回的结果”彻底区分开。
|
||||
|
||||
很多同学第一次用 RAG 平台 SDK,会把这些概念混在一起。你只要记住:
|
||||
|
||||
- **dataset**:容器(装很多文档)
|
||||
- **document**:你上传的那份文件
|
||||
- **chunk**:文档切分出来的文本片段(检索返回的就是它)
|
||||
|
||||
<div align="center">
|
||||
<img src="images/10_1_2.webp" alt="对象关系图:dataset-document-chunk-embedding 与 Top-K 返回" width="100%" />
|
||||
<p>图 10.2: 对象关系与返回结构(检索返回的核心对象是 chunk)</p>
|
||||
</div>
|
||||
|
||||
图 10.2 里最容易忽略、但最关键的一点是:**检索返回的是 chunk,不是 document。**
|
||||
|
||||
- document 是“你上传的整份文件”
|
||||
- chunk 是“切分后的片段”,它才是检索、重排、压缩、最终拼上下文的基本单位
|
||||
|
||||
所以你在终端里看到的 Top‑K 结果,应该是一段段原文片段,而不是整篇 Markdown。
|
||||
|
||||
> 💡 小白自检:我怎么判断“这段 chunk 就是我想要的那段”?
|
||||
>
|
||||
> 很简单:用你自己的语言把问题再复述一遍,然后在返回的 chunk 里找“能直接支撑答案的原文句子”。
|
||||
> 例如你问“已发货未签收能不能退款”,chunk 里应当出现“已发货未签收:可申请退款,但需要承担退货运费”这一类关键句。
|
||||
|
||||
---
|
||||
## 三、实现思路:从零写一版“最小检索脚本”(带代码块)
|
||||
|
||||
先给一个“最小骨架”(你可以把它当作伪代码,但它基本就是 `code/main.py` 的主干):
|
||||
|
||||
```python
|
||||
rag = RAGFlow(api_key=..., base_url=...)
|
||||
|
||||
# 1) 创建 dataset(容器)
|
||||
dataset = rag.create_dataset(name=...)
|
||||
|
||||
# 2) 上传文档(拿到 doc.id)
|
||||
doc = dataset.upload_documents([{...}])[0]
|
||||
|
||||
# 3) 解析/切分/向量化(失败大多发生在这里)
|
||||
parse_results = dataset.parse_documents([doc.id])
|
||||
|
||||
# 4) 检索 Top-K chunks(验收点)
|
||||
chunks = rag.retrieve(question=..., dataset_ids=[dataset.id], document_ids=[doc.id], page_size=top_k)
|
||||
```
|
||||
|
||||
下面把每一步展开讲清楚(并配上代码片段)。
|
||||
|
||||
### 3.1 参数与配置:先让脚本可复现
|
||||
|
||||
先从命令行参数入手,理解脚本“能调什么”。`code/main.py` 里最常用的是这几个:
|
||||
|
||||
```python
|
||||
parser.add_argument("--file", type=Path, required=True)
|
||||
parser.add_argument("--question", type=str, required=True)
|
||||
parser.add_argument("--top-k", type=int, default=DEFAULT_CONFIG.top_k)
|
||||
parser.add_argument("--dataset-name", type=str, default=DEFAULT_CONFIG.dataset_name)
|
||||
parser.add_argument("--base-url", type=str, default=DEFAULT_CONFIG.base_url)
|
||||
parser.add_argument("--api-key", type=str, default=DEFAULT_CONFIG.api_key)
|
||||
```
|
||||
|
||||
- `--file`:你要上传哪份 Markdown
|
||||
- `--question`:你想验证的提问
|
||||
- `--top-k`:返回多少个 chunk
|
||||
- `--dataset-name`:本次创建/使用的数据集名字
|
||||
- `--base-url/--api-key`:PowerRAG 服务端地址与 SDK token
|
||||
|
||||
这几个参数足够让你完成“换文档、换问题、调 Top‑K、连不同服务端”这四类最常见实验。
|
||||
|
||||
> 💡 小白自检:为什么这里既支持命令行参数,又支持 `.env`?
|
||||
>
|
||||
> 因为这两种场景都很常见:
|
||||
>
|
||||
> - 你本地调试时,喜欢用 `.env` 固定住 base_url/api_key
|
||||
> - 你改参数做实验时,喜欢命令行直接覆盖(不用反复改文件)
|
||||
|
||||
### 3.2 初始化 SDK:先连上再说
|
||||
|
||||
```python
|
||||
from ragflow_sdk import RAGFlow
|
||||
|
||||
rag = RAGFlow(api_key=api_key, base_url=base_url)
|
||||
```
|
||||
|
||||
这里没有花活:就是把请求的 base_url 和 token 配好。
|
||||
|
||||
### 3.3 创建 dataset:把文档放进“一个篮子里”
|
||||
|
||||
```python
|
||||
dataset_kwargs = {"name": args.dataset_name}
|
||||
if args.embedding_model:
|
||||
dataset_kwargs["embedding_model"] = args.embedding_model
|
||||
dataset = rag.create_dataset(**dataset_kwargs)
|
||||
```
|
||||
|
||||
为什么要先有 dataset?因为“上传/解析/检索”都需要一个边界。
|
||||
你不希望每次检索都在整个租户的所有文档里搜;你希望“只在这次实验的文档集合里搜”。
|
||||
|
||||
> 💡 小白自检:能不能不建 dataset,直接上传然后检索?
|
||||
>
|
||||
> 取决于平台能力。但在 PowerRAG/RAGFlow 这类系统里,dataset 是“组织边界”。
|
||||
> 没有边界,检索要么全库搜(不可控),要么压根没有地方挂索引。
|
||||
|
||||
### 3.4 上传 document:得到 doc.id,后面都靠它
|
||||
|
||||
```python
|
||||
docs = dataset.upload_documents([
|
||||
{"display_name": display_name, "blob": blob}
|
||||
])
|
||||
doc = docs[0]
|
||||
```
|
||||
|
||||
上传成功后,SDK 会返回一个 document 对象(至少包含 `doc.id`)。
|
||||
后续的 parse 和 retrieve 都要用它来限定范围。
|
||||
|
||||
> 💡 小白自检:为什么要限定 `document_ids=[doc.id]`?
|
||||
>
|
||||
> 因为你这次实验只关心“这份文档”的检索效果。
|
||||
> 如果不限定,dataset 里有多份文档时,你可能会检索到别的文档的 chunk,导致结果看起来“跑偏”。
|
||||
|
||||
### 3.5 解析 / 切分 / 向量化:最容易踩坑的一步
|
||||
|
||||
```python
|
||||
parse_results = dataset.parse_documents([doc.id])
|
||||
print("Parse results:")
|
||||
print(parse_results)
|
||||
```
|
||||
|
||||
脚本会把 parse 的状态打印出来,并且做了一个很直接的判断:
|
||||
|
||||
```python
|
||||
statuses = {r[1] for r in parse_results if isinstance(r, (list, tuple)) and len(r) >= 2}
|
||||
if statuses and statuses != {"DONE"}:
|
||||
raise SystemExit("Document parsing failed (status not DONE)...")
|
||||
```
|
||||
|
||||
你可以把它理解为“验收关卡”:
|
||||
|
||||
- **DONE**:说明服务端已经把文档切成 chunk,并完成(或至少开始完成)向量化与索引写入
|
||||
- **FAIL/其他状态**:先别着急改代码,优先排查 tenant 默认 embedding
|
||||
|
||||
> 经验:`Model(@None) not authorized` 基本就是在提示“默认 embedding 没配/没权限”。
|
||||
|
||||
> 💡 小白自检:为什么 embedding 配置会影响“解析(parse)”?
|
||||
>
|
||||
> 因为这里的 parse 往往不是“纯语法解析 Markdown”,而是一条“解析 → 切分 → 向量化 → 写索引”的流水线任务。
|
||||
> embedding 不可用时,流水线中途失败,平台就会把整个任务标为 FAIL。
|
||||
|
||||
### 3.6 检索 Top‑K:你真正要验收的结果
|
||||
|
||||
```python
|
||||
chunks = rag.retrieve(
|
||||
question=args.question,
|
||||
dataset_ids=[dataset.id],
|
||||
document_ids=[doc.id],
|
||||
page=1,
|
||||
page_size=args.top_k,
|
||||
similarity_threshold=args.similarity_threshold,
|
||||
vector_similarity_weight=args.vector_similarity_weight,
|
||||
top_k=args.candidate_k,
|
||||
keyword=args.keyword,
|
||||
)
|
||||
```
|
||||
|
||||
这里有两个点值得你留意(也是很多人调参的入口):
|
||||
|
||||
- `page_size=args.top_k`:你最终想看多少条 chunk
|
||||
- `similarity_threshold`:太高会过滤掉结果导致空,太低会混进无关段落
|
||||
|
||||
最后脚本会把每条 chunk 的内容预览打印出来:
|
||||
|
||||
```python
|
||||
for i, c in enumerate(chunks, start=1):
|
||||
content = _safe_get(c, "content", "")
|
||||
print(f"{i:02d}. {content[:260]}")
|
||||
```
|
||||
|
||||
你要做的“人工验收”也很简单:看看这几段文字是不是回答问题所需的那几段原文。
|
||||
|
||||
> 💡 小白自检:Top‑K 是不是越大越好?
|
||||
>
|
||||
> 不是。Top‑K 太大容易把无关 chunk 混进来;太小又可能漏掉关键段落。
|
||||
> 教学 demo 里一般用 3~8 都够用。
|
||||
|
||||
---
|
||||
|
||||
### 3.7 先跑通一次(最短路径)
|
||||
|
||||
> ⚠️ 注意:解析/向量化依赖 embedding。如果你的 tenant 没有配置默认 embedding(`embd_id` 为空或未授权),解析阶段会 FAIL。不要先怀疑 Python。
|
||||
|
||||
```bash
|
||||
# 1) 安装依赖
|
||||
cd Extra-chapter/PowerRAG-SDK-Text-QA/code
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 2) 配置 .env(在 code/ 目录下)
|
||||
cp .env.example .env
|
||||
|
||||
# 3) 回到专题根目录运行(data/ 路径更直观)
|
||||
cd ..
|
||||
python code/main.py \
|
||||
--file data/sample.md \
|
||||
--question "已发货未签收的退款规则是什么?" \
|
||||
--top-k 5 \
|
||||
--cleanup
|
||||
```
|
||||
|
||||
你会看到两段关键输出:
|
||||
|
||||
1. `Parse results`:解析/分块状态(期望 `DONE`)
|
||||
2. `Retrieved chunks`:Top‑K chunks 的内容预览
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 四、经验总结与坑点(把时间花在对的地方)
|
||||
|
||||
很多时候问题不在“你写的 Python”,而在“服务端是不是已经把 embedding 产出来了”。
|
||||
|
||||
<div align="center">
|
||||
<img src="images/10_1_3.webp" alt="简化时序图:上传→解析→写入索引→Top-K 检索→返回 chunks" width="100%" />
|
||||
<p>图 10.3: code/main.py 与服务端 API 的交互顺序(简化版)</p>
|
||||
</div>
|
||||
|
||||
如果你只记一条顺序,就记这句:
|
||||
|
||||
> **先上传 → 再解析(产出 chunk+embedding)→ 最后检索(返回 chunk)**
|
||||
|
||||
很多“为什么检索不到”的问题,本质是解析还没成功,索引里根本没有向量。
|
||||
|
||||
---
|
||||
|
||||
### 4.1 Parse results 是 FAIL
|
||||
|
||||
优先检查 tenant 的默认 embedding(`embd_id`)是否已配置且可用。典型错误:
|
||||
|
||||
- `Model(@None) not authorized`
|
||||
- `Parse results: ... FAIL ...`
|
||||
|
||||
如果已经配置仍失败,直接看 task executor 日志最省时间:
|
||||
|
||||
```bash
|
||||
docker exec powerrag-powerrag-1 sh -lc 'tail -n 200 /ragflow/logs/task_executor_* | tail -n 200'
|
||||
```
|
||||
|
||||
### 4.2 401/403:token 类型搞混
|
||||
|
||||
PowerRAG 常见会同时出现两类 token:
|
||||
|
||||
- Web 层 `AUTH`(用于 `/v1/*`)
|
||||
- SDK 的 `ragflow-...` token(用于 `/api/v1/*`,通常写在 `Authorization: Bearer <ragflow-...>`)
|
||||
|
||||
如果你看到 401/403,先确认 token 类型和接口前缀是否匹配。
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 附录:用 API 配默认 embedding + 生成 ragflow token(重操作区)
|
||||
|
||||
> 这部分是“环境/账号/服务端配置”,放到附录,避免主线被淹没。
|
||||
|
||||
### A1. 用 API 配好 embedding(通用)
|
||||
|
||||
这一步需要一个 Web 层的 `AUTH`(`/v1/*` 使用),它和 SDK 的 `ragflow-...` key 不是一回事。
|
||||
|
||||
你可以把 embedding 配置写进 `.env`(见 `.env.example` 的 `EMB_*`),下面命令会读取 `EMB_FACTORY/EMB_MODEL/EMB_API_BASE/EMB_API_KEY`。
|
||||
|
||||
#### A1.1 获取 `AUTH`(注册并从响应头拿 Authorization)
|
||||
|
||||
PowerRAG 的 `/v1/user/register` 要求 password 先用服务端的 RSA public key 加密。最省事的方式是在容器内调用它自带的加密函数:
|
||||
|
||||
```bash
|
||||
BASE_URL="http://127.0.0.1:9380"
|
||||
|
||||
ENC_PW="$(docker exec powerrag-powerrag-1 sh -lc 'python - <<"PY"\nfrom api.utils.crypt import crypt\nprint(crypt("powerrag"))\nPY')"
|
||||
|
||||
EMAIL="powerrag.demo.$(date +%s)@example.com"
|
||||
AUTH="$(curl -sS -D - -o /dev/null -X POST "$BASE_URL/v1/user/register" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"nickname\":\"demo\",\"email\":\"$EMAIL\",\"password\":\"$ENC_PW\"}" \
|
||||
| awk 'BEGIN{IGNORECASE=1} /^authorization:/{print $2}' | tr -d '\r')"
|
||||
```
|
||||
|
||||
#### A1.2 绑定 embedding 的外部 API
|
||||
|
||||
> 注意:`max_tokens` 需要显式传,否则可能报数据库字段错误。
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "$BASE_URL/v1/llm/add_llm" \
|
||||
-H "Authorization: $AUTH" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{
|
||||
"llm_factory": "'"${EMB_FACTORY}"'",
|
||||
"model_type": "embedding",
|
||||
"llm_name": "'"${EMB_MODEL}"'",
|
||||
"api_base": "'"${EMB_API_BASE}"'",
|
||||
"api_key": "'"${EMB_API_KEY}"'",
|
||||
"max_tokens": 8192
|
||||
}'
|
||||
```
|
||||
|
||||
#### A1.3 设置 tenant 默认 `embd_id`
|
||||
|
||||
```bash
|
||||
TENANT_ID="$(curl -sS -H "Authorization: $AUTH" "$BASE_URL/v1/user/tenant_info" | python -c 'import sys,json; print(json.load(sys.stdin)["data"]["tenant_id"])')"
|
||||
|
||||
curl -sS -X POST "$BASE_URL/v1/user/set_tenant_info" \
|
||||
-H "Authorization: $AUTH" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"tenant_id\":\"$TENANT_ID\",\"llm_id\":\"\",\"embd_id\":\"${EMB_MODEL}@${EMB_FACTORY}\",\"asr_id\":\"\",\"img2txt_id\":\"\"}"
|
||||
```
|
||||
|
||||
### A2. 生成 SDK 的 `ragflow-...` api_key
|
||||
|
||||
SDK 接口在 `/api/v1/*`,它不认 `AUTH`,需要 `ragflow-...` 这种 token(放在 header:`Authorization: Bearer <ragflow-...>`)。
|
||||
|
||||
用 `AUTH` 创建一个 SDK key:
|
||||
|
||||
```bash
|
||||
DIALOG_ID="$(python -c 'import uuid; print(uuid.uuid4().hex)')"
|
||||
API_KEY="$(curl -sS -X POST "$BASE_URL/v1/api/new_token" \
|
||||
-H "Authorization: $AUTH" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d "{\"dialog_id\":\"$DIALOG_ID\"}" \
|
||||
| python -c 'import sys,json; print(json.load(sys.stdin)["data"]["token"])')"
|
||||
|
||||
echo "$API_KEY"
|
||||
```
|
||||
@@ -0,0 +1,149 @@
|
||||
<div align="center">
|
||||
|
||||
<h2>🚀 All-in-RAG · Extra Chapter</h2>
|
||||
|
||||
<p><em>主教程之外的「知识拓展与社区实践」专区</em></p>
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## 📖 Extra Chapter 是什么?
|
||||
|
||||
`Extra-chapter` 目录用于存放 **不直接属于主线章节,但对 RAG / LLM 应用非常有价值的补充内容**。这些内容可以是你对某个相关技术(如 Neo4j、Milvus、GraphRAG 等)的系统整理,也可以是与你的研究/工作紧密相关的专题总结。
|
||||
|
||||
在主仓库的「知识拓展」部分,我们会以清单形式挂出这里的优秀专题,例如:
|
||||
|
||||
- `Neo4J 简单应用`(本目录下的第一个示例专题)
|
||||
- `PowerRAG SDK 文本检索 Demo`(上传 Markdown → 解析/切分/向量化 → Top‑K 检索)
|
||||
|
||||
我们希望通过 Extra Chapter:
|
||||
|
||||
- **补充主教程**:覆盖 RAG 周边的生态组件和工程化话题
|
||||
- **沉淀实践经验**:记录真实项目中的设计思路与解决方案
|
||||
- **鼓励多元视角**:允许不同风格的探索与实验性内容
|
||||
- **形成可引用的知识单元**:每个专题都可以被单独阅读和复用
|
||||
|
||||
---
|
||||
|
||||
## 📂 推荐目录结构
|
||||
|
||||
每一个新专题建议在 `Extra-chapter/` 下新建一个独立子目录,并至少包含一个 `readme.md` 作为主文档。参考结构如下(可按需要增减):
|
||||
|
||||
```text
|
||||
Extra-chapter/
|
||||
├── your-topic-name/ # 你的专题目录(必需)
|
||||
│ ├── readme.md # 专题主文档(必需)
|
||||
│ ├── images/ # 图片资源(建议有图片时使用)
|
||||
│ │ ├── figure1.png
|
||||
│ │ └── figure2.jpg
|
||||
│ ├── code/ # 代码示例(如有代码建议单独放)
|
||||
│ │ ├── demo.py
|
||||
│ │ └── requirements.txt
|
||||
│ └── data/ # 示例数据(如有)
|
||||
│ └── sample_data.json
|
||||
└── README.md # 本说明文件
|
||||
```
|
||||
|
||||
> 当前仓库中的 `Neo4J 简单应用` 专题位于 `Extra-chapter/Neo4J/readme.md`,你可以参考其组织方式,但不必完全照搬。
|
||||
|
||||
---
|
||||
|
||||
## 🧱 命名与排版建议
|
||||
|
||||
### 1. 目录与文件命名
|
||||
|
||||
- **专题目录名 (`your-topic-name/`)**
|
||||
- 使用有语义的英文或中英文混合
|
||||
- 避免过长,尽量能一眼看出主题,例如:
|
||||
- `Neo4J-Simple-Application`
|
||||
- `graph-rag-practice`
|
||||
|
||||
- **主文档**
|
||||
- 固定为:`readme.md`(小写),便于 GitHub 直接展示
|
||||
|
||||
- **图片 / 代码 / 数据**
|
||||
- 统一放在 `images/`、`code/`、`data/` 等子目录中
|
||||
- 文件名建议包含用途或内容关键信息,例如:`pipeline-overview.png`、`retriever_benchmark.ipynb`
|
||||
|
||||
### 2. 内容组织与标题层级
|
||||
|
||||
推荐的章节骨架(可根据需要微调):
|
||||
|
||||
- `# 专题标题`
|
||||
- `## 背景与动机`
|
||||
- `## 场景或问题描述`
|
||||
- `## 技术方案 / 实现思路`
|
||||
- `## 实践步骤 / 代码示例`
|
||||
- `## 经验总结与坑点`
|
||||
|
||||
标题层级请保持清晰,避免过多嵌套(一般不超过三级:`##` / `###` / `####`)。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 内容质量与范围
|
||||
|
||||
为了让内容对读者真正有帮助,提交 PR 前请尽量满足以下要求:
|
||||
|
||||
- **与 RAG / LLM 应用相关**
|
||||
- 例如:图数据库在 RAG 中的用法、检索评估方案、多模态扩展、部署/监控经验等
|
||||
|
||||
- **技术与事实尽量准确**
|
||||
- 如有推测或实验性的结论,请明确标注
|
||||
- 关键代码建议给出可运行环境说明(Python 版本、依赖库等)
|
||||
|
||||
- **结构清晰、可独立阅读**
|
||||
- 读者不看主教程,也能从该专题收获完整的一段知识
|
||||
|
||||
- **尊重版权与引用规范**
|
||||
- 如引用论文、博客或开源项目,请在文末列出参考资料或链接
|
||||
|
||||
---
|
||||
|
||||
## 🔀 提交 PR 的具体步骤(建议)
|
||||
|
||||
1. **Fork 仓库并创建分支**
|
||||
- 从 `main` 或当前默认分支拉出个人分支,例如:`feat/extra-chapter-neo4j-practice`
|
||||
|
||||
2. **在 `Extra-chapter/` 下创建你的专题目录**
|
||||
- 遵循上文的目录组织方案
|
||||
- 如有需要,可在根目录 `README.md` 的「第六部分:知识拓展」列表中,添加指向你专题的链接(格式参考已有的 `Neo4J 简单应用`)
|
||||
|
||||
3. **本地检查排版与图片路径**
|
||||
- 确认 Markdown 渲染正常、图片路径相对位置正确
|
||||
|
||||
4. **编写有信息量的 Commit Message**
|
||||
|
||||
推荐包含以下信息(可按实际场景调整):
|
||||
|
||||
```text
|
||||
Extra-chapter: <你的专题标题>
|
||||
|
||||
- 新增专题目录:Extra-chapter/<your-topic-name>
|
||||
- 主要内容:简要说明该专题解决了什么问题 / 分享了哪些经验
|
||||
- 代码与数据:如有,说明放在哪些子目录,如何运行
|
||||
- 个人信息(可选但推荐):你的 GitHub 链接、研究 / 工作方向等
|
||||
```
|
||||
|
||||
5. **发起 PR**
|
||||
- PR 标题中建议包含:`[Extra-chapter]` + 你的专题名
|
||||
- 在 PR 描述里可以:
|
||||
- 说明该专题与 All-in-RAG 主线章节的关系
|
||||
- 标注阅读顺序建议(例如:适合在读完第 3 章之后再看)
|
||||
|
||||
---
|
||||
|
||||
## 💬 沟通与反馈
|
||||
|
||||
如果你在撰写 Extra Chapter 的过程中:
|
||||
|
||||
- 不确定选题是否合适
|
||||
- 想讨论目录设计或技术路线
|
||||
- 希望对草稿先做一次技术性 Review
|
||||
|
||||
可以在仓库的 `Discussions` 或 `Issues` 中发起话题,标题中带上 `Extra-chapter` 关键字,方便维护者和其他贡献者一起参与讨论。
|
||||
|
||||
---
|
||||
|
||||
欢迎你把自己的实践经验沉淀在这里,让更多学习 RAG / LLM 应用的同学受益。 🎉
|
||||
|
||||