ข้ามไปที่เนื้อหา

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
uv sync --extra memory-faiss
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
uv sync --extra memory-colbert
backend = MemoryRegistry.create(
    "colbert",
    checkpoint="colbert-ir/colbertv2.0",
    device="cpu",
)
พารามิเตอร์ ค่าเริ่มต้น คำอธิบาย
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
uv sync --extra memory-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 ทำงานอย่างไร

  1. เอกสารถูกแบ่งเป็นย่อหน้า (คั่นด้วย newline คู่)
  2. ย่อหน้าจะถูกสะสมไว้จนกว่าจำนวน token จะเกิน chunk_size
  3. เนื้อหาที่สะสมไว้จะถูกส่งออกเป็น chunk
  4. Token chunk_overlap สุดท้ายจะถูกเก็บไว้เป็นบริบทให้ chunk ถัดไป
  5. ย่อหน้าที่เกิน 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 .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 สูงสุดทั้งหมดในบริบทที่แทรก

ทำงานอย่างไร

  1. คำถามของผู้ใช้ถูกนำไปค้นหาใน memory backend
  2. ผลลัพธ์ที่ต่ำกว่า min_score จะถูกกรองออก
  3. ผลลัพธ์ถูกตัดให้พอดีกับ max_context_tokens
  4. 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...

การปิดการแทรกบริบท

jarvis ask --no-context "Tell me about Python"
response = j.ask("Tell me about Python", context=False)

การใช้งานผ่าน 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()