This commit is contained in:
2026-02-20 23:04:09 +08:00
parent 43d69d5a99
commit 5d8412bcb6
244 changed files with 9304 additions and 1463 deletions

View File

@@ -1,39 +1,62 @@
# RAG & Map Tools
# RAG 工具说明
该目录包含了地图构建、知识库生成和向量数据库管理的相关工具
该目录负责知识构建与向量入库
## 目录结构
- **knowledge_base/**: 存放源文档数据。
- 支持格式: `.txt`, `.md`, `.pdf`
- 生成格式: `.json`, `.ndjson` (由 `build_knowledge_base.py` 生成)
- **map/**: 存放地图原始数据。
- `.osm` (OpenStreetMap 数据)
- `.world` (Gazebo 仿真环境数据)
- **vector_store/**: ChromaDB 向量数据库的持久化存储目录
## 脚本说明
### 1. `build_knowledge_base.py`
**功能**: 处理 `map/` 目录下的地图文件,提取地理信息和语义描述,生成知识库文件到 `knowledge_base/` 目录。
**使用方法**:
```bash
python build_knowledge_base.py
```text
tools/rag/
├── map/ # 原始地图数据
├── knowledge_base/
│ ├── location/ # 地点知识(可选分目录)
├── pattern/ # 模式知识(任务模板)
├── rules/ # 规则知识
└── *.ndjson
├── vector_store/ # Chroma 持久化目录
├── build_knowledge_base.py # 从 map 构建 ndjson
└── ingest.py # 入库到 Chroma多集合 + 兼容集合)
```
### 2. `ingest.py`
**功能**: 读取 `knowledge_base/` 中的所有文档调用嵌入模型Embedding Model将其向量化并存入 `vector_store/` 中的 ChromaDB 数据库。
**使用方法**:
## 运行前准备
```bash
python ingest.py
cd /home/huangfukk/DronePlanning
source backend_service/venv/bin/activate
export ORIN_IP="localhost"
```
**依赖**: 需要确保后端嵌入服务(如 `llama-server`)已启动,或者配置正确的 `ORIN_IP` 环境变量。
## 工作流
1. 将地图文件放入 `map/`
2. 运行 `build_knowledge_base.py` 生成文本描述。
3. 将其他补充文档放入 `knowledge_base/`
4. 运行 `ingest.py` 构建向量索引。
并确保 embedding 服务可用(默认 `8090`
```bash
curl -s http://localhost:8090/v1/embeddings \
-H "Content-Type: application/json" \
-d '{"input":["test"]}'
```
## 完整流程(可直接执行)
```bash
cd /home/huangfukk/DronePlanning
source backend_service/venv/bin/activate
# 1) 由 map 构建知识文本
python tools/rag/build_knowledge_base.py
# 2) 入库
python tools/rag/ingest.py
```
## 入库结果
`ingest.py` 会同时写入:
- 兼容集合:`drone_docs`
- 新集合:`location_kb``pattern_kb``rules_kb`
并在 metadata 中写入 `kb_type`,支持检索时按知识域筛选。
## 常见问题
- **Chroma 初始化异常**:先确认使用的是项目 venv再检查 `tools/rag/vector_store/` 是否损坏(备份后重建)。
- **连接 embedding 失败**:检查 `ORIN_IP`、端口 8090、以及模型服务是否已启动。

Binary file not shown.

View File

@@ -5,7 +5,6 @@ import chromadb
# from chromadb.utils import embedding_functions - 不再需要
from chromadb.api.types import Documents, EmbeddingFunction, Embeddings, Embeddable
from unstructured.partition.auto import partition
from rich.progress import track
import logging
import requests # 导入requests
import json # 导入json模块
@@ -19,6 +18,11 @@ SCRIPT_DIR = Path(__file__).resolve().parent
KNOWLEDGE_BASE_DIR = SCRIPT_DIR / "knowledge_base"
VECTOR_STORE_DIR = SCRIPT_DIR / "vector_store"
COLLECTION_NAME = "drone_docs"
COLLECTIONS_BY_KB_TYPE = {
"location": "location_kb",
"pattern": "pattern_kb",
"rules": "rules_kb",
}
# EMBEDDING_MODEL_NAME = "bge-small-zh-v1.5" # 不再需要,模型名在函数内部处理
# --- 自定义远程嵌入函数 ---
@@ -66,6 +70,18 @@ class RemoteEmbeddingFunction(EmbeddingFunction[Embeddable]):
return []
def _infer_kb_type(file_path: Path, base_dir: Path) -> str:
try:
rel = file_path.relative_to(base_dir)
except ValueError:
return "location"
if len(rel.parts) > 1:
first = rel.parts[0].lower()
if first in COLLECTIONS_BY_KB_TYPE:
return first
return "location"
def get_documents(directory: Path):
"""从知识库目录加载所有文档并进行切分"""
documents = []
@@ -74,11 +90,12 @@ def get_documents(directory: Path):
if file_path.is_file() and not file_path.name.startswith('.'):
try:
# 对简单文本文件直接读取
kb_type = _infer_kb_type(file_path, directory)
if file_path.suffix in ['.txt', '.md']:
text = file_path.read_text(encoding='utf-8')
documents.append({
"text": text,
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
logging.info(f"成功处理文本文件: {file_path.name}")
# 特别处理常规的JSON文件
@@ -89,13 +106,13 @@ def get_documents(directory: Path):
for element in data['elements']:
documents.append({
"text": json.dumps(element, ensure_ascii=False),
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
logging.info(f"成功处理JSON文件: {file_path.name}, 提取了 {len(data['elements'])} 个元素。")
else:
documents.append({
"text": json.dumps(data, ensure_ascii=False),
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
logging.info(f"成功处理JSON文件: {file_path.name} (作为单个文档)")
# 新增:专门处理我们生成的 NDJSON 文件
@@ -115,7 +132,7 @@ def get_documents(directory: Path):
text = json.dumps(record, ensure_ascii=False)
documents.append({
"text": text,
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
count += 1
except json.JSONDecodeError:
@@ -128,7 +145,7 @@ def get_documents(directory: Path):
for element in elements:
documents.append({
"text": element.text,
"metadata": {"source": str(file_path.name)}
"metadata": {"source": str(file_path.name), "kb_type": kb_type}
})
logging.info(f"成功处理文件: {file_path.name} (使用unstructured)")
except Exception as e:
@@ -159,38 +176,48 @@ def main():
client = chromadb.PersistentClient(path=str(VECTOR_STORE_DIR))
# 3. 创建或获取集合
logging.info(f"正在访问ChromaDB集合: {COLLECTION_NAME}")
collection = client.get_or_create_collection(
name=COLLECTION_NAME,
embedding_function=embedding_func
)
# 3. 创建或获取集合(兼容旧集合 + 新多知识库集合)
collections = {
"fallback": client.get_or_create_collection(name=COLLECTION_NAME, embedding_function=embedding_func)
}
for kb_type, coll_name in COLLECTIONS_BY_KB_TYPE.items():
collections[kb_type] = client.get_or_create_collection(name=coll_name, embedding_function=embedding_func)
# 4. 将文档向量化并存入数据库
logging.info(f"开始将 {len(docs_to_ingest)} 个文档块入库...")
# 为了避免重复添加,可以先检查
# (这里为了简单,我们每次都重新添加,生产环境需要更复杂的逻辑)
doc_texts = [doc['text'] for doc in docs_to_ingest]
metadatas = [doc['metadata'] for doc in docs_to_ingest]
ids = [f"doc_{KNOWLEDGE_BASE_DIR.name}_{i}" for i in range(len(doc_texts))]
grouped_docs = {}
for doc in docs_to_ingest:
kb_type = doc["metadata"].get("kb_type", "location")
grouped_docs.setdefault(kb_type, []).append(doc)
# 先入旧集合,保持兼容
all_texts = [doc['text'] for doc in docs_to_ingest]
all_metadatas = [doc['metadata'] for doc in docs_to_ingest]
all_ids = [f"doc_{KNOWLEDGE_BASE_DIR.name}_{i}" for i in range(len(all_texts))]
try:
# ChromaDB的add方法会自动处理嵌入
collection.add(
documents=doc_texts,
metadatas=metadatas,
ids=ids
)
logging.info("所有文档块已成功入库!")
collections["fallback"].add(documents=all_texts, metadatas=all_metadatas, ids=all_ids)
logging.info("兼容集合 drone_docs 入库完成。")
except Exception as e:
logging.error(f"ChromaDB添加文档时出错: {e}")
logging.error(f"兼容集合添加文档时出错: {e}")
for kb_type, docs in grouped_docs.items():
if kb_type not in collections:
continue
doc_texts = [doc["text"] for doc in docs]
metadatas = [doc["metadata"] for doc in docs]
ids = [f"doc_{kb_type}_{i}" for i in range(len(doc_texts))]
try:
collections[kb_type].add(documents=doc_texts, metadatas=metadatas, ids=ids)
logging.info(f"{kb_type} 集合入库完成,条目数: {len(doc_texts)}")
except Exception as e:
logging.error(f"{kb_type} 集合添加文档时出错: {e}")
# 验证一下
count = collection.count()
logging.info(f"数据库中现在有 {count} 个条目。")
for name, collection in collections.items():
try:
count = collection.count()
logging.info(f"集合 {name} 当前条目数: {count}")
except Exception:
pass
print("\n✅ 数据入库完成!")
print(f"知识库位于: {KNOWLEDGE_BASE_DIR}")

View File

@@ -0,0 +1,6 @@
{"scene_id":"scene1_perimeter_window_ground","intent_type":"patrol_or_monitor","trigger_terms":["面前大楼","12米","外围","窗户"],"text":"地面起飞后到面前大楼约12米高度沿外围巡查打开窗户如发现窗户则拍照回传。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"system_checks","params":{"check_level":"comprehensive"}},{"type":"action","name":"takeoff","params":{"altitude":12}},{"type":"action","name":"rotate_search","params":{"target_class":"window"}},{"type":"condition","name":"object_detected","params":{"target_class":"window"}},{"type":"action","name":"take_photos","params":{"target_class":"window","track_time":10}}]}}}
{"scene_id":"scene1_perimeter_person_air","intent_type":"patrol_or_monitor","trigger_terms":["这栋楼","空中","外围","人"],"text":"无人机已在空中,先调整高度后沿建筑外围巡查人员,发现目标后拍照。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"move_direction","params":{"direction":"up","distance":3}},{"type":"action","name":"rotate_search","params":{"target_class":"person"}},{"type":"condition","name":"object_detected","params":{"target_class":"person"}},{"type":"action","name":"take_photos","params":{"target_class":"person","track_time":10}}]}}}
{"scene_id":"scene1_perimeter_trash_ground","intent_type":"patrol_or_monitor","trigger_terms":["面前大楼","杂物","外围"],"text":"地面起飞至指定高度后沿楼体外围侦察杂物堆积并拍照。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"takeoff","params":{"altitude":12}},{"type":"action","name":"rotate_search","params":{"target_class":"garbage"}},{"type":"condition","name":"object_detected","params":{"target_class":"garbage"}},{"type":"action","name":"take_photos","params":{"target_class":"garbage","track_time":8}}]}}}
{"scene_id":"scene4_named_place_search_photo","intent_type":"search_and_photo","trigger_terms":["广场","查找","拍照"],"text":"前往命名地点后搜索目标并拍照。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"fly_to_waypoint","params":{"x":0,"y":0,"z":10,"acceptance_radius":2}},{"type":"action","name":"rotate_search","params":{"target_class":"person"}},{"type":"condition","name":"object_detected","params":{"target_class":"person"}},{"type":"action","name":"take_photos","params":{"target_class":"person","track_time":8}}]}}}
{"scene_id":"scene4_named_place_monitor_return","intent_type":"return_or_land","trigger_terms":["广场南边","监控","返航"],"text":"先到达命名地点偏移区域执行监控,到时后返航。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"fly_to_waypoint","params":{"x":40,"y":80,"z":15,"acceptance_radius":2}},{"type":"action","name":"loiter","params":{"duration":300}},{"type":"action","name":"return_emergency","params":{"reason":"mission_complete"}}]}}}
{"scene_id":"scene4_named_place_confirm_then_action","intent_type":"search_and_photo","trigger_terms":["确认后","拍照","返航"],"text":"在命名区域发现目标后等待人工确认,再执行拍照或返航。","pattern_json":{"root":{"type":"Sequence","name":"Sequence","children":[{"type":"action","name":"fly_to_waypoint","params":{"x":10,"y":10,"z":10,"acceptance_radius":2}},{"type":"action","name":"rotate_search","params":{"target_class":"car"}},{"type":"condition","name":"object_detected","params":{"target_class":"car"}},{"type":"action","name":"manual_confirmation","params":{}},{"type":"action","name":"take_photos","params":{"target_class":"car","track_time":8}}]}}}

View File

@@ -0,0 +1,3 @@
{"rule_id":"rules_manual_confirmation","intent_type":"search_and_photo","trigger_terms":["我确认","等待确认","经允许"],"text":"仅当用户明确要求人工确认时才注入 manual_confirmation 节点;否则禁止主动添加。"}
{"rule_id":"rules_direction_priority","intent_type":"generic_mission","trigger_terms":["东边","西边","北边","南边","米"],"text":"当只有方向+距离且无具体地点名词时,优先使用 move_direction不要生成 fly_to_waypoint。"}
{"rule_id":"rules_return_emergency_scope","intent_type":"return_or_land","trigger_terms":["返航","回到","去"],"text":"有明确目的地时使用 fly_to_waypoint无明确目的地的立即返航才使用 return_emergency。"}