Memory¶
ระบบ memory ให้บริการเก็บเอกสารแบบถาวรและค้นหาได้ สำหรับ retrieval-augmented generation (RAG) รองรับ retrieval backend หลายตัว, pipeline การแบ่ง chunk ที่ตั้งค่าได้, การนำเข้าเอกสารจากไฟล์และไดเรกทอรี และการแทรกบริบทเข้าไปใน prompt โดยอัตโนมัติ
สถาปัตยกรรม¶
Documents --> Chunking Pipeline --> Memory Backend --> Context Injection --> Prompt
(files) (split + overlap) (store + index) (retrieve + format) (to LLM)
MemoryBackend ABC¶
Memory backend ทั้งหมด implement abstract base class MemoryBackend
class MemoryBackend(ABC):
backend_id: str
def store(self, content: str, *, source: str = "", metadata: dict | None = None) -> str:
"""Persist content and return a unique document ID."""
def retrieve(self, query: str, *, top_k: int = 5, **kwargs) -> list[RetrievalResult]:
"""Search for query and return the top-k results."""
def delete(self, doc_id: str) -> bool:
"""Delete a document by ID. Return True if it existed."""
def clear(self) -> None:
"""Remove all stored documents."""
RetrievalResult¶
แต่ละการดึงข้อมูลคืนรายการของ object RetrievalResult:
| ฟิลด์ | ชนิด | คำอธิบาย |
|---|---|---|
content |
str |
ข้อความ chunk ที่ดึงมาได้ |
score |
float |
คะแนนความเกี่ยวข้อง (ยิ่งสูงยิ่งดี) |
source |
str |
Path ไฟล์ต้นทางหรือ identifier |
metadata |
dict[str, Any] |
Metadata เพิ่มเติม (ดัชนี chunk ฯลฯ) |
Backend¶
SQLite / FTS5 (ค่าเริ่มต้น)¶
Registry key: sqlite
Backend เริ่มต้นที่ใช้ extension full-text search FTS5 ในตัวของ SQLite ไม่มี
dependency ภายนอกเลย -- ใช้โมดูล sqlite3 มาตรฐานของ Python
- การให้คะแนน: จัดอันดับแบบ BM25 ผ่าน query FTS5 MATCH
- ความคงอยู่: ไฟล์ฐานข้อมูล SQLite (ค่าเริ่มต้น:
~/.openjarvis/memory.db) - Dependency: ไม่มี (มีอยู่ใน Python อยู่แล้ว)
from openjarvis.core.registry import MemoryRegistry
backend = MemoryRegistry.create("sqlite", db_path="./memory.db")
doc_id = backend.store("Hello world", source="test.txt")
results = backend.retrieve("hello")
backend.close()
เมื่อไหร่ควรใช้ SQLite/FTS5
ใช้ backend นี้เมื่อคุณต้องการตั้งค่าแบบไม่ต้อง configure, การค้นหาแบบ keyword เพียงพอแล้ว และต้องการเก็บข้อมูลถาวรข้ามการ restart เหมาะกับคอลเลกชันเอกสารขนาดเล็กถึงกลาง
FAISS¶
Registry key: faiss
Dense neural retrieval ที่ใช้ Facebook AI Similarity Search แปลงเอกสารและ คำค้นหาเป็น dense vector แล้วดึงข้อมูลด้วย cosine similarity
- การให้คะแนน: Cosine similarity ผ่าน inner-product search บน vector ที่ normalize แบบ L2
- ความคงอยู่: ในหน่วยความจำเท่านั้น (ข้อมูลหายเมื่อ restart)
- Dependency:
faiss-cpu(หรือfaiss-gpu),sentence-transformers
backend = MemoryRegistry.create("faiss")
doc_id = backend.store("Neural networks are computational models")
results = backend.retrieve("deep learning architectures")
เมื่อไหร่ควรใช้ FAISS
ใช้ backend นี้เมื่อต้องการค้นหาแบบ semantic (หาเนื้อหาที่ใกล้เคียงเชิงแนวคิดแม้ไม่ตรงคำ) เหมาะกับกรณีที่ re-index ได้ทุกครั้งที่รัน เพราะข้อมูลไม่คงอยู่ถาวร
ColBERTv2¶
Registry key: colbert
Late-interaction retrieval ที่ใช้ token-level embedding ของ ColBERT พร้อม คะแนนแบบ MaxSim ให้คุณภาพการดึงข้อมูลสูงที่สุดในบรรดา backend ที่มี
- การให้คะแนน: MaxSim -- สำหรับแต่ละ token ของคำค้นหา หา cosine similarity สูงสุดข้าม token ของเอกสารทั้งหมด แล้วรวมกัน
- ความคงอยู่: ในหน่วยความจำเท่านั้น
- Dependency:
colbert-ai,torch
| พารามิเตอร์ | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|
checkpoint |
"colbert-ir/colbertv2.0" |
Checkpoint ของโมเดล ColBERT |
device |
"cpu" |
อุปกรณ์คำนวณ (cpu หรือ cuda) |
เมื่อไหร่ควรใช้ ColBERTv2
ใช้ backend นี้เมื่อคุณภาพการดึงข้อมูลเป็นสิ่งสำคัญที่สุด และคุณมีทรัพยากรคำนวณเพียงพอ Checkpoint จะถูกโหลดแบบ lazy ตอนใช้งานครั้งแรกเพื่อหลีกเลี่ยง import ที่ช้า เหมาะกับงานวิจัยและการประเมินผล
BM25¶
Registry key: bm25
การจัดอันดับแบบความน่าจะเป็นดั้งเดิม โดยใช้อัลกอริทึม BM25 Okapi Implement
ในหน่วยความจำโดยใช้ library rank_bm25
- การให้คะแนน: คะแนน term-frequency แบบ BM25 Okapi
- ความคงอยู่: ในหน่วยความจำเท่านั้น
- Dependency:
rank-bm25
backend = MemoryRegistry.create("bm25")
backend.store("Python is a programming language", source="intro.txt")
results = backend.retrieve("programming language")
เมื่อไหร่ควรใช้ BM25
ใช้ backend นี้เมื่อต้องการการดึงข้อมูลแบบ keyword ดั้งเดิม โดยไม่ต้องพึ่งฐานข้อมูล มีประโยชน์ในฐานะองค์ประกอบ sparse ในการตั้งค่า retrieval แบบ hybrid
Hybrid (RRF Fusion)¶
Registry key: hybrid
รวม sparse retriever และ dense retriever เข้าด้วยกันด้วย Reciprocal Rank Fusion (RRF) เอกสารถูกเก็บไว้ใน sub-backend ทั้งสอง และผลลัพธ์การดึงข้อมูล ถูกรวมเข้าด้วยกัน
- การให้คะแนน:
RRF_score(d) = sum(weight_i / (k + rank_i(d)))ข้ามรายการอันดับทั้งสอง - ความคงอยู่: ขึ้นอยู่กับ sub-backend
- Dependency: ขึ้นอยู่กับ sub-backend
from openjarvis.tools.storage.bm25 import BM25Memory
from openjarvis.tools.storage.faiss_backend import FAISSMemory
sparse = BM25Memory()
dense = FAISSMemory()
backend = MemoryRegistry.create(
"hybrid",
sparse=sparse,
dense=dense,
k=60,
sparse_weight=1.0,
dense_weight=1.0,
)
ความเข้ากันได้ย้อนหลัง
from openjarvis.memory.bm25 import BM25Memory แบบเดิมยังใช้งานได้ผ่าน shim ที่เข้ากันได้ย้อนหลัง แต่โค้ดใหม่ควรใช้ import มาตรฐาน openjarvis.tools.storage.*
| พารามิเตอร์ | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|
sparse |
-- | Sparse retrieval backend (เช่น BM25) |
dense |
-- | Dense retrieval backend (เช่น FAISS) |
k |
60 |
ค่าคงที่ RRF |
sparse_weight |
1.0 |
น้ำหนักของผลลัพธ์จาก sparse retriever |
dense_weight |
1.0 |
น้ำหนักของผลลัพธ์จาก dense retriever |
Backend แบบ hybrid จะดึงเกิน (3 เท่าของ top_k) จากแต่ละ sub-backend ก่อน
นำมา fusion เพื่อปรับปรุงคุณภาพผลลัพธ์
เมื่อไหร่ควรใช้ Hybrid
ใช้ backend นี้เมื่อต้องการทั้งการจับคู่ keyword และความคล้ายเชิงความหมาย วิธี RRF fusion แข็งแรงและไม่ต้องปรับแต่งการกระจายคะแนนข้าม retrieval method ที่ต่างกัน
เปรียบเทียบ Backend¶
| Backend | ประเภทการค้นหา | ความคงอยู่ | Dependency | คุณภาพ | ความเร็ว |
|---|---|---|---|---|---|
| SQLite/FTS5 | Keyword (BM25) | มี | ไม่มี | ดี | เร็ว |
| FAISS | Dense (cosine) | ไม่มี | faiss, transformers | ดีกว่า | เร็ว |
| ColBERTv2 | Late interaction | ไม่มี | colbert-ai, torch | ดีที่สุด | ช้ากว่า |
| BM25 | Keyword (Okapi) | ไม่มี | rank-bm25 | ดี | เร็ว |
| Hybrid | Fusion (RRF) | ผสม | ตาม sub-backend | ดีกว่า | ปานกลาง |
Chunking Pipeline¶
เอกสารถูกแบ่งเป็น chunk ก่อนจัดเก็บ โดยใช้ pipeline ที่ตั้งค่าได้ ตัวแบ่ง chunk เคารพขอบเขตของย่อหน้าเมื่อทำได้
ChunkConfig¶
| ฟิลด์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
chunk_size |
int |
512 |
ขนาด chunk เป้าหมาย เป็น token คั่นด้วยช่องว่าง |
chunk_overlap |
int |
64 |
การทับซ้อนระหว่าง chunk ที่ติดกัน |
min_chunk_size |
int |
50 |
ขนาด chunk ขั้นต่ำ (chunk ที่เล็กกว่าจะถูกทิ้ง) |
Chunking ทำงานอย่างไร¶
- เอกสารถูกแบ่งเป็นย่อหน้า (คั่นด้วย newline คู่)
- ย่อหน้าจะถูกสะสมไว้จนกว่าจำนวน token จะเกิน
chunk_size - เนื้อหาที่สะสมไว้จะถูกส่งออกเป็น chunk
- Token
chunk_overlapสุดท้ายจะถูกเก็บไว้เป็นบริบทให้ chunk ถัดไป - ย่อหน้าที่เกิน
chunk_sizeจะถูกแบ่งเป็นหน้าต่างขนาดคงที่พร้อมการทับซ้อน
ผลลัพธ์ของ Chunk¶
แต่ละ chunk เป็น object Chunk ที่มี:
| ฟิลด์ | ชนิด | คำอธิบาย |
|---|---|---|
content |
str |
ข้อความของ chunk |
source |
str |
Path ไฟล์ต้นทาง |
offset |
int |
ตำแหน่ง token ภายในเอกสาร |
index |
int |
ดัชนี chunk ตามลำดับ |
metadata |
dict[str, Any] |
Metadata เพิ่มเติม |
การนำเข้าเอกสาร¶
ฟังก์ชัน ingest_path() อ่านไฟล์หรือไล่ดูไดเรกทอรีแบบ recursive สร้าง chunk
ที่พร้อมสำหรับจัดเก็บ
ประเภทไฟล์ที่รองรับ¶
| ประเภท | นามสกุลไฟล์ |
|---|---|
| ข้อความ | .txt และไฟล์ข้อความล้วนอื่นๆ |
| Markdown | .md, .markdown, .mdx |
| โค้ด | .py, .js, .ts, .rs, .go, .java, .c, .cpp, .rb, .sh, .yaml, .json, .html, .css และอื่นๆ |
.pdf (ต้องมี pdfplumber: uv sync --extra memory-pdf) |
การข้ามอัตโนมัติ¶
Pipeline การนำเข้าจะข้ามสิ่งเหล่านี้โดยอัตโนมัติ:
- ไฟล์และไดเรกทอรีที่ซ่อน (ขึ้นต้นด้วย
.) - ไดเรกทอรีที่ไม่ใช่เนื้อหาทั่วไป:
__pycache__,node_modules,.venv,.gitฯลฯ - ไฟล์ไบนารี: รูปภาพ, เสียง, วิดีโอ, archive, ไฟล์ที่ compile แล้ว
- ไฟล์ที่อ่านไม่ได้ (ปัญหาสิทธิ์, ปัญหา encoding)
การใช้งาน¶
from pathlib import Path
from openjarvis.tools.storage.chunking import ChunkConfig
from openjarvis.tools.storage.ingest import ingest_path
# Default chunking
chunks = ingest_path(Path("./docs/"))
# Custom chunking
config = ChunkConfig(chunk_size=256, chunk_overlap=32)
chunks = ingest_path(Path("./notes.md"), config=config)
print(f"Produced {len(chunks)} chunks")
for chunk in chunks[:3]:
print(f" [{chunk.index}] {chunk.source}: {chunk.content[:60]}...")
การแทรกบริบท¶
เมื่อเปิดใช้งานการแทรกบริบทจาก memory (ค่าเริ่มต้น) คำถามจะถูกเสริมด้วย เอกสารที่เกี่ยวข้องที่ดึงมาได้โดยอัตโนมัติ ก่อนส่งไปยังโมเดล แต่ละข้อความ ที่ดึงมาจะมีการระบุแหล่งที่มาแนบมาด้วย
ContextConfig¶
| ฟิลด์ | ชนิด | ค่าเริ่มต้น | คำอธิบาย |
|---|---|---|---|
enabled |
bool |
True |
การแทรกบริบทเปิดใช้งานอยู่หรือไม่ |
top_k |
int |
5 |
จำนวนผลลัพธ์ที่จะดึงมา |
min_score |
float |
0.1 |
คะแนนความเกี่ยวข้องขั้นต่ำ |
max_context_tokens |
int |
2048 |
จำนวน token สูงสุดทั้งหมดในบริบทที่แทรก |
ทำงานอย่างไร¶
- คำถามของผู้ใช้ถูกนำไปค้นหาใน memory backend
- ผลลัพธ์ที่ต่ำกว่า
min_scoreจะถูกกรองออก - ผลลัพธ์ถูกตัดให้พอดีกับ
max_context_tokens - system message ถูกต่อไว้หน้าบทสนทนา พร้อมบริบทที่จัดรูปแบบแล้ว:
The following context was retrieved from the knowledge base.
Use it to inform your response, citing sources where applicable:
[Source: docs/intro.md] OpenJarvis is a modular AI framework...
[Source: docs/config.md] Configuration is stored in TOML format...
การปิดการแทรกบริบท¶
การใช้งานผ่าน CLI¶
# Index a directory
jarvis memory index ./docs/
# Index with custom chunking
jarvis memory index ./notes/ --chunk-size 256 --chunk-overlap 32
# Search the memory store
jarvis memory search "machine learning"
# Search with more results
jarvis memory search -k 10 "neural networks"
# Show memory statistics
jarvis memory stats
การใช้งานผ่าน SDK¶
from openjarvis import Jarvis
j = Jarvis()
# Index documents
result = j.memory.index("./docs/", chunk_size=512, chunk_overlap=64)
print(f"Indexed {result['chunks']} chunks")
# Search
results = j.memory.search("configuration", top_k=3)
for r in results:
print(f" [{r['score']:.4f}] {r['source']}: {r['content'][:80]}...")
# Statistics
stats = j.memory.stats()
print(f"Backend: {stats['backend']}, Documents: {stats.get('count', 'N/A')}")
# Clean up
j.close()