Contents
요즘 AI 개발의 중심이 에이전트로 이동하면서, 에이전트 메모리 구현이 핵심 기술로 주목받고 있습니다.최상의 Agent 개발을 위한 3가지 Agent 메모리 환경:
short-term, long-term, reasoningTutorials원문의 모델 세팅 및 결과
(spacy: en_core_web_md, gliner: gliner_medium-v2.5, llm: gpt4o mini)상위 모델로 변경 및 결과
(spacy: en_core_web_lg, gliner: knowledgator/gliner-bi-large-v2.0, llm: gpt-5.4-nano)요즘 AI 개발의 중심이 에이전트로 이동하면서, 에이전트 메모리 구현이 핵심 기술로 주목받고 있습니다.
Neo4j도 그래프 기반으로 에이전트 메모리를 구축하는 방법을 제시하고 있으며, 관련 가이드와 소스 코드도 꾸준히 제공하고 있습니다.
이번에 시리즈로 소개하려는 내용은 Neo4j가 그동안 고객들에게 무료로 제공해 온 코드 중에서 APOC을 제외하면 가장 충실하게 가이드와 소스를 제공하는 경우 일 것입니다.
지속적인 버전 업데이트를 통해 다양한 옵션들을 제공하고 있으며, LangChain, LlamaIndex, CrewAI, OpenAI, Google, AWS 등 주요 에이전트 프레임워크와 환경에서 활용할 수 있도록 폭넓게 소스 코드를 제공하고 있습니다.
최상의 Agent 개발을 위한 3가지 Agent 메모리 환경: short-term, long-term, reasoning

Tutorials
Chapter1 - Build Your First Memory-Enabled Agent
# Create a new project directory
mkdir my-first-memory-agent
cd my-first-memory-agent
# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate # On Windows: venv\\Scripts\\activate
uv로 설치시에는 uv venv
# Install neo4j-agent-memory with all extras
pip install neo4j-agent-memory\[all\]
또는 uv add "neo4j-agent-memory[all]"
python -m spacy download en_core_web_smDocker 커맨드로 설치하는 방법을 안내하고 있으나 docker compose로 설치를 해 보았습니다.
< docker-compose.yml>
services:
neo4j-memory:
image: neo4j:5.26-community
container_name: neo4j-memory
ports:
- "7474:7474"
- "7687:7687"
environment:
- NEO4J_AUTH=neo4j/password123
- NEO4J_PLUGINS=["apoc"]위의 파일을 저장한 폴더에서 “docker compose up -d”로 실행하면 됩니다.
.env 파일에 아래와 같은 내용을 지정하여 사용합니다.
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=password123
OPENAI_API_KEY=your-openai-api-key-heNeo4j에 연결된 MemoryClient 인스턴스를 하나 생성한 후 그 안에 short_term 메모리로 메세지를 추가하는 메소드를 사용하여 하나의 입력 문장을 처리하는 예제입니다.
import asyncio
import os
from dotenv import load_dotenv
from neo4j_agent_memory import MemoryClient, MemorySettings
...전체 코드 보기
import asyncio
import os
from dotenv import load_dotenv
from neo4j_agent_memory import MemoryClient, MemorySettings
# Load environment variables
load_dotenv()
async def main():
# Create memory settings
settings = MemorySettings(
neo4j={"uri": os.getenv("NEO4J_URI", "bolt://localhost:7687"), "username": os.getenv("NEO4J_USER", "neo4j"), "password": os.getenv("NEO4J_PASSWORD", "password123")}
)
async with MemoryClient(settings) as client:
print("✓ Connected to Neo4j!")
# Store a test message
message = await client.short_term.add_message(
session_id="test-session-001",
role="user",
content="Hello! I'm looking for running shoes. I really like Nike products.",
)
print(f"✓ Stored message: {message.id}")
# Retrieve the message
conversation = await client.short_term.get_conversation(
session_id="test-session-001"
)
print(f"✓ Retrieved {len(conversation.messages)} message(s)")
for msg in conversation.messages:
print(f" [{msg.role}]: {msg.content}")
print("✓ Connection closed!")
if __name__ == "__main__":
asyncio.run(main())Neo4j에 연결된 MemoryClient 인스턴스를 하나 생성한 후 그 안에 short_term 메모리로 메세지를 추가하는 메소드를 사용하여 하나의 입력 문장을 처리하는 예제입니다.
[ 실행 결과 ]
다음과 같은 (Conversation)->(Message)→(Entity) 그래프가 생성 됩니다.

위에서 사용된 예시 코드에서 입력 문장을 저장하면서 엔티티를 추출하는데 사용된 3가지 엔티티 Extractor가 있는데 각 추출기의 엔티티 추출 시간을 측정하도록 코드를 수정하여 추출 시간을 비교해 보았습니다.
test - Entity Extraction Time
테스트 코드
import asyncio
import logging
import os
import time
from pathlib import Path
from dotenv import load_dotenv
from neo4j_agent_memory import MemoryClient, MemorySettings
from neo4j_agent_memory.extraction.pipeline import ExtractionPipeline
from neo4j_agent_memory.extraction.gliner_extractor import GLiNEREntityExtractor
from neo4j_agent_memory.extraction.llm_extractor import LLMEntityExtractor
from neo4j_agent_memory.extraction.spacy_extractor import SpacyEntityExtractor
load_dotenv()
LOG_DIR = Path("logs")
LOG_FILE = LOG_DIR / "three_extractor_time.log"
logger = logging.getLogger(__name__)
def get_torch_device() -> str:
"""Prefer GPU acceleration for GLiNER when PyTorch can use it."""
try:
import torch
except ImportError:
return "cpu"
if torch.cuda.is_available():
return "cuda"
if torch.backends.mps.is_available():
return "mps"
return "cpu"
def setup_logging():
"""Configure logs to write to both terminal and the script-specific log file."""
LOG_DIR.mkdir(exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
handlers=[
logging.StreamHandler(),
logging.FileHandler(LOG_FILE, encoding="utf-8"),
],
)
async def extract_and_log(extractor, content: str):
"""Run extraction once, log per-stage timing and entity results, return entities."""
if extractor is None:
logger.info(" No extractor configured")
return []
t0 = time.perf_counter()
if isinstance(extractor, ExtractionPipeline):
pipeline_result = await extractor.extract_with_details(content)
total_ms = (time.perf_counter() - t0) * 1000
logger.info(" Pipeline total: %.1fms", total_ms)
for stage in pipeline_result.stage_results:
if stage.success:
entity_names = [e.name for e in stage.result.entities]
logger.info(
" ✓ %s: %d entities in %.1fms%s",
stage.stage_name,
len(entity_names),
stage.duration_ms,
f" → {entity_names}" if entity_names else "",
)
else:
logger.info(
" ✗ %s: failed in %.1fms",
stage.stage_name,
stage.duration_ms,
)
return pipeline_result.final_result.entities
else:
result = await extractor.extract(content)
elapsed_ms = (time.perf_counter() - t0) * 1000
logger.info(
" %s: %d entities in %.1fms",
extractor.__class__.__name__,
result.entity_count,
elapsed_ms,
)
return result.entities
async def prewarm_extractor(extractor, content: str) -> bool:
"""Warm one extractor before measured extraction when it has lazy startup work."""
if extractor is None:
return False
try:
if isinstance(extractor, GLiNEREntityExtractor):
t0 = time.perf_counter()
await extractor.extract(content)
logger.info(
"Pre-warmed GLiNER extractor in %.1fms",
(time.perf_counter() - t0) * 1000,
)
return True
if isinstance(extractor, SpacyEntityExtractor):
t0 = time.perf_counter()
await extractor.extract(content)
logger.info(
"Pre-warmed spaCy extractor in %.1fms",
(time.perf_counter() - t0) * 1000,
)
return True
if isinstance(extractor, LLMEntityExtractor):
t0 = time.perf_counter()
extractor._ensure_client()
logger.info(
"Pre-warmed LLM extractor client in %.1fms",
(time.perf_counter() - t0) * 1000,
)
return True
except Exception as exc:
logger.warning(
"Pre-warm for %s failed: %s",
extractor.__class__.__name__,
exc,
)
return True
return False
async def prewarm_extractors(extractor, content: str):
"""Warm configured extractors before measured extraction."""
if extractor is None:
return
if await prewarm_extractor(extractor, content):
return
if isinstance(extractor, ExtractionPipeline):
for stage in extractor.stages:
stage_extractor = getattr(stage, "_extractor", None)
await prewarm_extractor(stage_extractor, content)
async def main():
setup_logging()
gliner_device = get_torch_device()
logger.info("Using GLiNER device: %s", gliner_device)
settings = MemorySettings(
neo4j={"uri": os.getenv("NEO4J_URI", "bolt://localhost:7687"), "username": os.getenv("NEO4J_USER", "neo4j"), "password": os.getenv("NEO4J_PASSWORD", "password123")},
extraction={"gliner_device": gliner_device},
)
async with MemoryClient(settings) as client:
logger.info("✓ Connected to Neo4j!")
content = "Hello! I'm looking for running shoes. I really like Nike products."
await prewarm_extractors(client._extractor, content)
logger.info("\nExtraction pipeline:")
entities = await extract_and_log(client._extractor, content)
t0 = time.perf_counter()
message = await client.short_term.add_message(
session_id="test-session-001",
role="user",
content=content,
extraction_mode="explicit",
explicit_mentions=entities,
)
elapsed_ms = (time.perf_counter() - t0) * 1000
logger.info("\n✓ Stored message: %s (%.1fms)", message.id, elapsed_ms)
conversation = await client.short_term.get_conversation(
session_id="test-session-001"
)
logger.info("✓ Retrieved %d message(s)", len(conversation.messages))
for msg in conversation.messages:
logger.info(" [%s]: %s", msg.role, msg.content)
logger.info("✓ Connection closed!")
if __name__ == "__main__":
asyncio.run(main())Extractor별로 걸린 시간을 측정하기 위해 기본 제공된 agent.py 코드에서 다음과 같은 내용을 추가하였습니다.
- Extractor별로 추출한 엔티티의 개수와 시간을 출력
- 출력된 내용을 로그 파일로 저장
- 실제 추출에 사용되는 시간을 측정하기 위하여 최초 추출 이전에 필요한 작업을 진행하도록 하는 warm up 코드 추가
측정 결과는 아래와 같았습니다.
Loaded spaCy model: en_core_web_sm
Loading GLiNER model: gliner-community/gliner_medium-v2.5
GLiNER model loaded on cpu
Pipeline total: 8402.9ms
✓ SpacyEntityExtractor: 1 entities in 793.0ms → ['Nike']
✓ GLiNEREntityExtractor: 3 entities in 3892.2ms → ['I', 'running shoes', 'Nike']
✓ LLMEntityExtractor: 2 entities in 3717.7ms → ['Nike', 'running shoes']
Remark: spaCy, GLiNER, LLM extract가 모두 성공했고 GLiNER는 CPU에서 실행됨. 총 8.4초가 걸림.
Remark: warm up 코드를 추가하기 전의 코드로 측청한 것이어서 각 Extractor를 로드하는 과정의 시간이 포함되어 있습니다.
Pipeline total: 3699.6ms
✓ SpacyEntityExtractor: 1 entities in 5.3ms → ['Nike']
✓ GLiNEREntityExtractor: 2 entities in 50.0ms → ['running shoes', 'Nike']
✓ LLMEntityExtractor: 2 entities in 3644.2ms → ['Nike', 'running shoes']
Remark:
- GLiNER 추출기의 경우 모델을 로드할 때 시간이 많이 걸려서, warm up 이후에는 매우 빠르게 처리됩니다. CPU나 GPU/MPS를 사용한 경우에 따른 차이는 크지 않았습니다.
- LLM Extractor의 경우 warm up 이전과 이후에 별 차이가 없는 것은 다른 Extractor처럼 모델을 로컬에서 로드하지 않고 API 서비스를 이용하기 때문이고 API 처리가 Spacy나 GLiNER보다는 오래 걸리는 것을 알 수 있습니다. 요청 텍스트가 많거나 빠른 처리를 위해서는 Spacy나 GLiNER 같은 빠른 방식을 메인으로 처리해야 합니다.
Extractor별로 걸린 시간을 측정하기 위해 기본 제공된 agent.py 코드에서 다음과 같은 내용을 추가하였습니다.
- Extractor별로 추출한 엔티티의 개수와 시간을 출력
- 출력된 내용을 로그 파일로 저장
- 실제 추출에 사용되는 시간을 측정하기 위하여 최초 추출 이전에 필요한 작업을 진행하도록 하는 warm up 코드 추가
측정 결과는 아래와 같았습니다.
Loaded spaCy model: en_core_web_sm
Loading GLiNER model: gliner-community/gliner_medium-v2.5
GLiNER model loaded on cpu
Pipeline total: 8402.9ms
✓ SpacyEntityExtractor: 1 entities in 793.0ms → ['Nike']
✓ GLiNEREntityExtractor: 3 entities in 3892.2ms → ['I', 'running shoes', 'Nike']
✓ LLMEntityExtractor: 2 entities in 3717.7ms → ['Nike', 'running shoes']
Remark: spaCy, GLiNER, LLM extract가 모두 성공했고 GLiNER는 CPU에서 실행됨. 총 8.4초가 걸림.
Remark: warm up 코드를 추가하기 전의 코드로 측청한 것이어서 각 Extractor를 로드하는 과정의 시간이 포함되어 있습니다.
Pipeline total: 3699.6ms
✓ SpacyEntityExtractor: 1 entities in 5.3ms → ['Nike']
✓ GLiNEREntityExtractor: 2 entities in 50.0ms → ['running shoes', 'Nike']
✓ LLMEntityExtractor: 2 entities in 3644.2ms → ['Nike', 'running shoes']
Remark:
- GLiNER 추출기의 경우 모델을 로드할 때 시간이 많이 걸려서, warm up 이후에는 매우 빠르게 처리됩니다. CPU나 GPU/MPS를 사용한 경우에 따른 차이는 크지 않았습니다.
- LLM Extractor의 경우 warm up 이전과 이후에 별 차이가 없는 것은 다른 Extractor처럼 모델을 로컬에서 로드하지 않고 API 서비스를 이용하기 때문이고 API 처리가 Spacy나 GLiNER보다는 오래 걸리는 것을 알 수 있습니다. 요청 텍스트가 많거나 빠른 처리를 위해서는 Spacy나 GLiNER 같은 빠른 방식을 메인으로 처리해야 합니다.
import asyncio
import os
from dotenv import load_dotenv
from neo4j_agent_memory import MemoryClient, MemorySettings
from neo4j_agent_memory.extraction import GLiNEREntityExtractor
...전체 코드 보기
import asyncio
import os
from dotenv import load_dotenv
from neo4j_agent_memory import MemoryClient, MemorySettings
from neo4j_agent_memory.extraction import GLiNEREntityExtractor
load_dotenv()
async def main():
settings = MemorySettings(
neo4j={"uri": os.getenv("NEO4J_URI", "bolt://localhost:7687"), "username": os.getenv("NEO4J_USER", "neo4j"), "password": os.getenv("NEO4J_PASSWORD", "password123")}
)
async with MemoryClient(settings) as client:
# Create an entity extractor
extractor = GLiNEREntityExtractor.for_poleo()
print("✓ Entity extractor ready!")
# Sample conversation
messages = [
"Hi, I'm John and I'm looking for running shoes for my marathon training.",
"I've been using Nike Air Max shoes and really liked them.",
"My budget is around $150. I prefer lightweight shoes.",
"I live in San Francisco and train at Golden Gate Park.",
]
# Process each message
for msg_content in messages:
# Store the message
message = await client.short_term.add_message(
session_id="shopping-session-001",
role="user",
content=msg_content,
)
print(f"\n[User]: {msg_content}")
# Extract entities
result = await extractor.extract(msg_content)
if result.entities:
print(" Extracted entities:")
for entity in result.entities:
print(f" - {entity.name} ({entity.type})")
# Store entity in context graph
entity_obj, _ = await client.long_term.add_entity(
name=entity.name,
entity_type=entity.type,
attributes={"confidence": entity.confidence},
)
# Show what's in the context graph
print("\n" + "="*50)
print("Context Graph Summary:")
print("="*50)
# Get all entities
entities = await client.long_term.search_entities(query="", limit=20)
print(f"\nEntities in graph: {len(entities)}")
for entity in entities:
print(f" - {entity.name} ({entity.type})")
if __name__ == "__main__":
asyncio.run(main())4개의 사용자 입력문을 short_term의 add_message()로 그래프에 추가하고, 별도로 GLiNER 엔티티 추출기를 사용해서 입력 문장들의 엔티티를 추가적으로 추출하고 long_term 메모리의 엔티티 추가 메소드 add_entity()를 사용하여 그래프에 저장하는 예제 코드 입니다.
[ 실행 결과 ]
✓ Entity extractor ready!
[User]: Hi, I'm John and I'm looking for running shoes for my marathon training.
Extracted entities:
- John (PERSON)
- running shoes (OBJECT)
- marathon training (EVENT)
[User]: I've been using Nike Air Max shoes and really liked them.
Extracted entities:
- Nike Air Max (OBJECT)
[User]: My budget is around $150. I prefer lightweight shoes.
Extracted entities:
- lightweight shoes (OBJECT)
[User]: I live in San Francisco and train at Golden Gate Park.
Extracted entities:
- I (PERSON)
- San Francisco (LOCATION)
- Golden Gate Park (OBJECT)
==================================================
Context Graph Summary:
==================================================
Entities for in graph: 0
1. GLiNEREntityExtractor.for_poleo() 가 short_term 메모리의 메세지 추가 메소드 add_message()에서 디폴트로 수행되는 엔티티 추출 과정이 있는데 위의 엔티티 추가 과정과 차이점이 무엇인가?
-> short_term 메모리에서 엔티티 추출은 3가지(Spacy, GLiNER, LLM) 방식을 합쳐서 추출하는데 여기에 GLiNER 방식도 같은 GLiNEREntityExtractor 를 사용하기 때문에 같다고 볼수도 있으나, short_term 메모리의 GLiNER는 DEFAULT_POLEO_LABELS에 기본 정의된 라벨 리스트를 사용하고 for_poleo()에서는 "poleo": DomainSchema에 기본 정의되어 있는 라벨:설명 리스트를 사용하는 것이 다릅니다. 두 리스트에 차이가 있어서 결과도 조금 다르게 나오게 되는 것을 소스 코드에서 확인할 수 있었습니다.
2. GLiNEREntityExtractor.for_poleo()에서 별도로 추출한 엔티티를 long_term.add_entity()로 그래프에 추가할 때 그 앞에 short_term.add_message()의 처리 과정에서 추출된 엔티티 노드와 어떻게 머지가 되는가?
-> long_term.add_entity()에서 MERGE (e:Entity {name: $name, type: $type})를 사용하여 엔티티를 저장하기 때문에 별도 추출기에서 이름과 종류가 같은 엔티티가 추출되는 경우에는 해당 엔티티를 별도로 생성되지 않고 일부 속성만(subtype, canonical_name, description, embedding, metadata, updated_at) 업데이트하는 것으로 소스 코드에서 확인 됩니다. https://github.com/neo4j-labs/agent-memory/blob/208733c1aa229b07011ce7afe310fc82d72b47e1/src/neo4j_agent_memory/graph/query_builder.py#L4
-> 현재 코드의 long_term.add_entity()에서 전달되는 데이터는 canonical_name, embedding, updated_at 이므로 실제로는 이 세가지 속성만 업데이트 됩니다.위의 그래프 결과에서 각 대화문(분홍노드)에 연결된 엔티티(갈색노드) 중 일부 updated_at 속성을 가지고 있는 엔티티들이 바로 그렇게 두가지 추출 과정에서 동일하게 추출이 된 경우 입니다.
3. 생성된 그래프에서 연결이 없는 3개의 엔티티 노드는 무엇인가?
-> GLiNER 추출 과정에서 생성이 된 엔티티 노드들 중에서 short_term.add_message()에서 추출된 엔티티와 name이나 type속성이 일치하지 않아 노드만 생성된 경우인데, 실제 구현 코드라면 대화문 노드와 연결하는 과정도 있어야 할 것 입니다.
4. "Context Graph Summary:” 이하 출력하는 코드에서 어떤 결과가 나오지 않는 이유는?
-> Neo4j 원문에는 여기에 일부 엔티티가 출력되어 있지만 실제 실행결과에서는 나오지 않는데 이유는 원문 소스 코드 중 long_term.search_entities(query="", limit=20)의 query 항목에 내용이 없기 때문입니다. 위의 코드는 주어진 query의 내용을 임베딩하여 이미 임베딩되어 있는 엔티티들과 유사도를 비교하는 것인데 query가 주어지지 않으면 비교 결과가 나오지 않게 됩니다. 이 코드의 결과를 보여주려면 long_term.search_entities(query="running shoes", limit=20) 처럼 query 항목에 적당한 내용을 주면 됩니다.
5. 원문의 코드에서 엔티티 추출 결과가 일부 좋지 않아서 원문 코드에서 사용된 모델들을 바꾸어서 좀 더 나은 엔티티 추출 결과를 얻을 수 있을 것인가?
test - Entity Extraction Quality
원문의 모델 세팅 및 결과 (spacy: en_core_web_md, gliner: gliner_medium-v2.5, llm: gpt4o mini)
Added spaCy extractor to pipeline
Added GLiNER extractor to pipeline
Added LLM extractor to pipeline
✓ Entity extractor ready: ExtractionPipeline
[User]: Hi, I'm John and I'm looking for running shoes for my marathon training.
Loaded spaCy model: en_core_web_sm
Loading GLiNER model: gliner-community/gliner_medium-v2.5
Extracted entities by extractor:
SpacyEntityExtractor: 1 entities
- John (PERSON, confidence=0.85)
GLiNEREntityExtractor: 3 entities
- John (PERSON, confidence=0.99)
- running shoes (OBJECT, confidence=0.88)
- marathon training (EVENT, confidence=0.96)
LLMEntityExtractor: 4 entities
- John (PERSON, confidence=0.90)
- running shoes (OBJECT, confidence=0.90)
- marathon (EVENT, confidence=0.85)
- training (EVENT, confidence=0.80)
[User]: I've been using Nike Air Max shoes and really liked them.
Extracted entities by extractor:
SpacyEntityExtractor: 1 entities
- Nike Air Max (ORGANIZATION, confidence=0.85)
GLiNEREntityExtractor: 2 entities
- I (PERSON, confidence=0.70)
- Nike (ORGANIZATION, confidence=0.89)
LLMEntityExtractor: 1 entities
- Nike Air Max (OBJECT, confidence=0.90)
[User]: My budget is around $150. I prefer lightweight shoes.
Extracted entities by extractor:
SpacyEntityExtractor: 1 entities
- around $150 (OBJECT, confidence=0.85)
GLiNEREntityExtractor: 2 entities
- I (PERSON, confidence=0.69)
- lightweight shoes (OBJECT, confidence=0.64)
LLMEntityExtractor: 0 entities
[User]: I live in San Francisco and train at Golden Gate Park.
Extracted entities by extractor:
SpacyEntityExtractor: 2 entities
- San Francisco (LOCATION, confidence=0.85)
- Golden Gate Park (LOCATION, confidence=0.85)
GLiNEREntityExtractor: 3 entities
- I (PERSON, confidence=0.88)
- San Francisco (LOCATION, confidence=1.00)
- Golden Gate Park (LOCATION, confidence=0.91)
LLMEntityExtractor: 2 entities
- San Francisco (LOCATION, confidence=0.90)
- Golden Gate Park (LOCATION, confidence=0.90)
==================================================
Context Graph Summary:
==================================================
Entities extracted from full messages: 13
- John (PERSON) [GLiNEREntityExtractor]
- running shoes (OBJECT) [LLMEntityExtractor]
- marathon training (EVENT) [GLiNEREntityExtractor]
- marathon (EVENT) [LLMEntityExtractor]
- training (EVENT) [LLMEntityExtractor]
- Nike Air Max (ORGANIZATION) [SpacyEntityExtractor]
- I (PERSON) [GLiNEREntityExtractor]
- Nike (ORGANIZATION) [GLiNEREntityExtractor]
- Nike Air Max (OBJECT) [LLMEntityExtractor]
- around $150 (OBJECT) [SpacyEntityExtractor]
- lightweight shoes (OBJECT) [GLiNEREntityExtractor]
- San Francisco (LOCATION) [GLiNEREntityExtractor]
- Golden Gate Park (LOCATION) [GLiNEREntityExtractor]실제 프로젝트나 제품의 코드가 아니어서 추출 품질이 좋지 않은 것은 예상이 되지만 각 Extractor의 기본 모델이 너무 낮은 모델이어서 좀 더 나은 모델로 바꾸어서 품질의 변화를 비교해 보았습니다.
상위 모델로 변경 및 결과 (spacy: en_core_web_lg, gliner: knowledgator/gliner-bi-large-v2.0, llm: gpt-5.4-nano)
Added spaCy extractor to pipeline
Added GLiNER extractor to pipeline
Added LLM extractor to pipeline
✓ Entity extractor ready: ExtractionPipeline
[User]: Hi, I'm John and I'm looking for running shoes for my marathon training.
Loaded spaCy model: en_core_web_lg
Loading GLiNER model: knowledgator/gliner-bi-large-v2.0
Loading the following GLiNER type: <class 'gliner.model.BiEncoderSpanGLiNER'>...
GLiNER model loaded on cpu
Extracted entities by extractor:
SpacyEntityExtractor: 1 entities
- John (PERSON, confidence=0.85)
GLiNEREntityExtractor: 3 entities
- John (PERSON, confidence=0.80)
- running shoes (OBJECT, confidence=0.49)
- marathon (EVENT, confidence=0.48)
LLMEntityExtractor: 3 entities
- John (PERSON, confidence=0.95)
- running shoes (OBJECT, confidence=0.90)
- marathon training (EVENT, confidence=0.75)
[User]: I've been using Nike Air Max shoes and really liked them.
Extracted entities by extractor:
SpacyEntityExtractor: 1 entities
- Nike Air Max (ORGANIZATION, confidence=0.85)
GLiNEREntityExtractor: 4 entities
- I (PERSON, confidence=0.34)
- Nike (ORGANIZATION, confidence=0.46)
- Air Max (OBJECT, confidence=0.31)
- shoes (OBJECT, confidence=0.41)
LLMEntityExtractor: 1 entities
- Nike Air Max (OBJECT, confidence=0.90)
[User]: My budget is around $150. I prefer lightweight shoes.
Extracted entities by extractor:
SpacyEntityExtractor: 1 entities
- around $150 (OBJECT, confidence=0.85)
GLiNEREntityExtractor: 1 entities
- lightweight shoes (OBJECT, confidence=0.40)
LLMEntityExtractor: 2 entities
- 150 (OBJECT, confidence=0.35)
- lightweight shoes (OBJECT, confidence=0.90)
[User]: I live in San Francisco and train at Golden Gate Park.
Extracted entities by extractor:
SpacyEntityExtractor: 2 entities
- San Francisco (LOCATION, confidence=0.85)
- Golden Gate Park (LOCATION, confidence=0.85)
GLiNEREntityExtractor: 2 entities
- San Francisco (LOCATION, confidence=0.67)
- Golden Gate Park (LOCATION, confidence=0.70)
LLMEntityExtractor: 3 entities
- San Francisco (LOCATION, confidence=0.95)
- Golden Gate Park (LOCATION, confidence=0.95)
- I (PERSON, confidence=0.60)
==================================================
Context Graph Summary:
==================================================
Entities extracted from full messages: 15
- John (PERSON) [LLMEntityExtractor]
- running shoes (OBJECT) [LLMEntityExtractor]
- marathon (EVENT) [GLiNEREntityExtractor]
- marathon training (EVENT) [LLMEntityExtractor]
- Nike Air Max (ORGANIZATION) [SpacyEntityExtractor]
- I (PERSON) [GLiNEREntityExtractor, LLMEntityExtractor]
- Nike (ORGANIZATION) [GLiNEREntityExtractor]
- Air Max (OBJECT) [GLiNEREntityExtractor]
- shoes (OBJECT) [GLiNEREntityExtractor]
- Nike Air Max (OBJECT) [LLMEntityExtractor]
- around $150 (OBJECT) [SpacyEntityExtractor]
- lightweight shoes (OBJECT) [LLMEntityExtractor]
- 150 (OBJECT) [LLMEntityExtractor]
- San Francisco (LOCATION) [LLMEntityExtractor]
- Golden Gate Park (LOCATION) [LLMEntityExtractor]좀 더 좋은 모델을 사용하여 엔티티 추출 품질이 상승이 되었지만 여전히 잘못 추출하는 경우가 포함되어 있습니다.
더 높은 고사양의 모델들은 대량 처리시 시간이나 비용이 문제가 될 수 있어서 다음 단계는 다른 보완적인 옵션들을 통한 테스트를 해 보려고 합니다.
[ 실행 결과 ]
Storing user preferences...
brand: Prefers Nike products
budget: Budget around $150
style: Prefers lightweight shoes
activity: Trains for marathos
location: Lives in San Francisco
==================================================
Retrieved preferences for personalization
==================================================
Building personalized context for agent...
==================================================
Preferences found: 2
Entities found: 4
Context ready for LLM:
## Uers Preferences
- style: Prefers lightweight shoes
- brand: Prefers Nike products
## Known Information
- running shoes (OBJECT)
- lightweight shoes (OBJECT)
- marathon training (EVENT)
- Nike Air Max (OBJECT)client.long_term.add_preference(): 고객의 특성을 category와 preference 의 조합으로 Preference 노드를 생성하여 속성으로 저장하는 함수. 두 항목(category와 preference)의 조합된 텍스트의 임베딩을 속성으로 저장합니다.
client.long_term.search_preferences(query=""): 저장된 Preference 노드의 embedding 중에서 ‘query’ 에 지정된 텍스트의 임베딩과 유사도가 제일 높은 Preference들을 유사도 순으로 리턴합니다.
client.long_term.search_entities(query=""): 저장된 Entity 노드의 embedding 중에서 ‘query’ 에 지정된 텍스트의 임베딩과 유사도가 제일 높은 Entity들을 유사도 순으로 리턴합니다.
< 검토 사항 >
사용자의 Preference는 어디에서 어떻게 추출할 것인지?
→ 이후 챕터에서 몇가지 추출 방식이 구현되어 있는데 다음 회에 좀 더 자세히 다루도록 하겠습니다.
웹브라우저에서 전체 그래프 보기
MATCH (n)
OPTIONAL MATCH (n)-[r]->(m)
RETURN n, r, mStep 8: Context Graph 검색
client.short_term.search_messages(): query에 주어진 검색 문장을 임베딩 한 후 다음의 쿼리를 사용하여
CALL db.index.vector.queryNodes('message_embedding_idx', $limit, $embedding)
YIELD node, score
WHERE score >= $threshold
RETURN node AS m, score
ORDER BY score DESC유사도가 높은 순으로 Message 노드의 content를 score와 함께 출력하는 코드 입니다.
실행 결과
Searching messages for 'athletic footwear'...
[0.69] My budget is around $150. I prefer lightweight shoes....
[0.69] Hi, I'm John and I'm looking for running shoes for my marath...
[0.68] I've been using Nike Air Max shoes and really liked them....
Searching entities for 'sports brand'...
[0.90] running shoes (OBJECT)
[0.64] lightweight shoes (OBJECT)< 검토 사항 >
지금까지 첫 챕터에서는 벡터 임베딩의 유사도를 이용한 검색만 사용한 단편적인 내용입니다. 그래프를 제대로 활용하는 복합적인 코드는 다음 챕터에서 부터 나옵니다.
Add Conversation Memory to a Chatbot
Step 1: Project Setup
mkdir shopping-assistant
cd shopping-assistant
python -m venv venv
source venv/bin/activate
pip install neo4j-agent-memory[all] openai python-dotenvNEO4J_URI=bolt://localhost:7687 NEO4J_USER=neo4j NEO4J_PASSWORD=password123 OPENAI_API_KEY=your-openai-api-key
이전 챕터에서 사용했던 환경을 그대로 사용해도 됩니다.
Step 2: Memory-Enabled Assistant 개발
챗봇과의 대화를 통해서 Customer
Share article