把 AI 长期记忆去重测试从 20 分钟压到 40 秒,重复率从 18% 干到 1.5%
æ AI é¿æè®°å¿å»éæµè¯ä» 20 åéåå° 40 ç§ï¼éå¤çä» 18% å¹²å° 1.5%
ç¨æ·05954017446 2026-08-20 0 é 读1åé忍ä¸ç¹ï¼æè¢«æ¥è¦çµè¯å«éï¼ç产ç¯å¢ AI 婿ç prompt token åçäºãæå¼æ¥å¿ä¸çï¼å¥½å®¶ä¼ï¼åä¸ä¸ªç¨æ·ç䏿¡é¿æè®°å¿è¢«åæ¶å¡è¿äºä¸ä¸æââãç¨æ·å欢åç¾å¼åå¡ããç¨æ·æ¯å¤©åç¾å¼ä¸å ç³ããç¨æ·å好ç¾å¼åå¡ï¼ä¸å ç³ãã
è¿ä¸æ¯ bugï¼è¿æ¯æä»¬çè®°å¿å»éçç¥å¨è£¸å¥ã
é®é¢æè§£
æä»¬åçæ¯ AI ä¸ªäººå©æé¿æè®°å¿ç³»ç»ãæ¯æ¬¡å¯¹è¯åï¼æ¨¡å伿éè¦ä¿¡æ¯æ½åæ memory åè¿ PostgreSQLï¼ä¸æ¬¡å¯¹è¯ååæ£ç´¢ç¸å ³è®°å¿æ¼è¿ promptãé®é¢åºå¨ä¸¤åï¼
- å»éåªçææ¬ç¸ä¼¼åº¦ãæä»¬ç¨ Jaccard ç¸ä¼¼åº¦ + å ³é®è¯åå¸ï¼ä¸¤æ¡è®°å¿åªè¦æ¢ä¸ªè¯´æ³å°±æ¼å¤ãç¨æ·è¯´è¿ãææ¯å¨è·æ¥ä¸æ¬¡ãåãæä¹ æ¯ä¸å¨è·ä¸åãï¼ææ¬éå ä½ï¼ä½è¯ä¹å®å ¨éå¤ã
- è¡°åçç¥æ²¡æèªå¨åéªè¯ãè®°å¿æéè¡°åå
¬å¼åå¨ææ¡£éï¼å®é
代ç é
decay_factorä¸ä¼å¿æ¯0.9ï¼ä¸ä¼å¿æ¯datetimeåæ³ç®éï¼ä»æ²¡è¢«æµè¯è¦çè¿ã
ç»æå°±æ¯ï¼éå¤è®°å¿è¶æ¥è¶å¤ï¼ä¸ä¸æçªå£è¢«æçï¼æ£ç´¢ç»æéå ¨æ¯è¯ä¹ç¸åçåºè¯ãå¸¸è§æ¹æ¡æ¯æ¯æ¬¡ä¸çº¿åæå·¥è·å æ¡ SQLï¼ççè¿é»æ¥è¯¢ç»æå¯¹ä¸å¯¹ãä½è¯´å®è¯ï¼è°æç©ºå¤©å¤©æå·¥æµï¼çåç°éå¤çå·²ç» 18% äºã
æ¹æ¡è®¾è®¡
éåæ²¡ä»ä¹æ¬å¿µï¼PostgreSQL + pgvectorãçç±å¾ç°å®ââæä»¬ç产ç¯å¢å·²ç»å¨ç¨ PG åç»æåæ°æ®ï¼åå¼å ¥ Qdrant æ Milvus æå³çå¤ä¸ä¸ªåºç¡è®¾æ½ãå¤ä¸ä»½è¿ç»´ææ¬ãpgvector ç´æ¥å¨ç°æ PG éæ©å±ï¼äºå¡ãå¤ä»½ãæéä¸å¥å ¨å¤ç¨ã
æµè¯æ¡æ¶é pytest + testcontainersã为ä»ä¹ä¸ mock æ pgvectorï¼å 为 memory ç³»ç»æå¤§çé£é©å°±æ¯ SQL éçåé彿°ç¨éï¼mock äºçäºæ²¡æµãç¨ testcontainers å¨ CI éæèµ·çå® pgvector/pgvector:pg16 容å¨ï¼è·å®å°±éæ¯ï¼ç¯å¢ä¸è´æ§ææ»¡ã
æ¶ææè·¯å¾ç®åï¼ä¸ä¸ª upsert_memory 彿°è´è´£å»éæå
¥ï¼ä¸ä¸ª apply_decay 彿°è´è´£ææ¶é´è¡°åæéãæµè¯è¦çè¿ä¸¤ä¸ªæ ¸å¿å½æ°ï¼æ¯æ¬¡ PR èªå¨è·ã
æ ¸å¿å®ç°
ç¬¬ä¸æ®µä»£ç è§£å³æµè¯ç¯å¢é®é¢ï¼ç¨ testcontainers å¯å¨ pgvector 容å¨ï¼å»ºè¡¨ï¼æ³¨ååéç±»åãæ²¡æå¹²åå¯éå¤ç PG ç¯å¢ï¼åé¢æææµè¯é½æ¯ç©ºä¸æ¥¼éã
# conftest.py
import pytest
from testcontainers.postgres import PostgresContainer
import psycopg
from pgvector.psycopg import register_vector
@pytest.fixture(scope="session")
def postgres_url():
# ä½¿ç¨ pgvector 宿¹éåï¼pg16 çæ¬
with PostgresContainer("pgvector/pgvector:pg16") as postgres:
postgres.with_env("POSTGRES_PASSWORD", "testpass")
yield postgres.get_connection_url()
@pytest.fixture(scope="session")
def conn(postgres_url):
# psycopg 3 è¿æ¥
with psycopg.connect(postgres_url) as conn:
# å
³é®ï¼æ³¨å vector ç±»åï¼å¦åæ æ³è¿å embedding
register_vector(conn)
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE memories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id TEXT NOT NULL,
content TEXT NOT NULL,
embedding vector(1536) NOT NULL,
importance FLOAT DEFAULT 1.0,
decay_rate FLOAT DEFAULT 0.05,
last_access_at TIMESTAMPTZ DEFAULT now(),
created_at TIMESTAMPTZ DEFAULT now()
)
""")
# ç¨ IVFFlat ç´¢å¼ï¼éåå¤§æ°æ®éè¿ä¼¼æç´¢
cur.execute("""
CREATE INDEX ON memories
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100)
""")
conn.commit()
yield conn
注æ register_vector è¿ä¸æ¥ï¼å®æ¹ææ¡£æ²¡å¼ºè°ï¼ä½ä¸æ³¨åçè¯ SELECT embedding 伿¥ unsupported typeã
ç¬¬äºæ®µä»£ç è§£å³å»éæå ¥é®é¢ï¼æ°è®°å¿è¿æ¥åï¼å æ¥åä¸ç¨æ·ä¸æè¿çåéï¼è·ç¦»å°äºéå¼å°±æ´æ°æ§è®°å¿ï¼å¦åæå ¥æ°è¡ã
# memory_dedup.py
import uuid
import numpy as np
import psycopg
from pgvector.psycopg import register_vector
def upsert_memory(conn, user_id: str, content: str, embedding: list[float],
threshold: float = 0.3) -> str:
"""
å»éæå
¥è®°å¿ãpgvector ç cosine distance = 1 - cosine similarityï¼
æä»¥è·ç¦»è¶å°è¶ç¸ä¼¼ãthreshold 0.3 çä»·äºç¸ä¼¼åº¦ 0.7ã
è¿åè®°å¿ IDï¼éå¤è¿åæ§ IDï¼å¦åè¿åæ° IDã
"""
with conn.cursor() as cur:
# åªæ¥åä¸ç¨æ·ï¼é¿å
è·¨ç¨æ·å»é
cur.execute("""
SELECT id, content
FROM memories
WHERE user_id = %s
ORDER BY embedding <=> %s::vector
LIMIT 1
""", (user_id, embedding))
row = cur.fetchone()
if row and row_matches_threshold(cur, row, embedding, threshold):
# å½ä¸å»éï¼æ´æ°æ§è®°å¿ç last_access_at å importance
cur.execute("""
UPDATE memories
SET last_access_at = now(),
importance = LEAST(importance + 0.1, 2.0)
WHERE id = %s
""", (row.id,))
return str(row.id)
# æå
¥æ°è®°å¿
memory_id = uuid.uuid4()
cur.execute("""
INSERT INTO memories (id, user_id, content, embedding)
VALUES (%s, %s, %s, %s)
""", (memory_id, user_id, content, embedding))
return str(memory_id)
ççï¼ä¸é¢ä»£ç é row_matches_threshold 没å®ä¹ï¼è¿æ ·æç« 读è
å¤å¶ä¼è¿è¡ä¸äºãå¿
须宿´ãæ¹ä¸ºç´æ¥å¨ SQL é夿è·ç¦»éå¼ã
宿´æ£ç¡®çæ¬ï¼
# memory_dedup.py
import uuid
import psycopg
from pgvector.psycopg import register_vector
def upsert_memory(conn: psycopg.Connection, user_id: str, content: str,
embedding: list[float], threshold: float = 0.3) -> str:
"""å»éæå
¥è®°å¿ãpgvector çä½å¼¦è·ç¦» = 1 - ä½å¼¦ç¸ä¼¼åº¦ã"""
with conn.cursor() as cur:
# æ¥è¯¢åä¸ç¨æ·æè¿çè®°å¿ï¼åæ¶è¿åè·ç¦»
cur.execute("""
SELECT id, 1 - (embedding <=> %s::vector) AS similarity
FROM memories
WHERE user_id = %s
ORDER BY embedding <=> %s::vector
LIMIT 1
""", (embedding, user_id, embedding))
row = cur.fetchone()
if row and row[1] >= (1 - threshold): # similarity >= 0.7
# è¯ä¹éå¤ï¼æ´æ°æ§è®°å¿èéæå
¥
cur.execute("""
UPDATE memories
SET last_access_at = now(),
importance = LEAST(importance + 0.1, 2.0)
WHERE id = %s
""", (row[0],))
return str(row[0])
# å
¨æ°è®°å¿
new_id = uuid.uuid4()
cur.execute("""
INSERT INTO memories (id, user_id, content, embedding)
VALUES (%s, %s, %s, %s)
""", (new_id, user_id, content, embedding))
return str(new_id)
æµè¯ä»£ç ï¼
# test_memory_dedup.py
import numpy as np
from memory_dedup import upsert_memory
from conftest import conn # å®é
项ç®ä¸éè¿ fixture 注å
¥
def test_semantic_dedup_prevents_duplicate(conn):
"""两æ¡è¯ä¹ç¸åãææ¬ä¸åçè®°å¿åºè¢«å¤å®ä¸ºéå¤"""
# 模æ OpenAI embedding è¾åºï¼1536 ç»´ï¼è¿ééæºä½ä¿è¯ä¸¤æ¡åé徿¥è¿ï¼
base = np.random.randn(1536).astype(np.float32)
embedding_1 = (base + np.random.randn(1536) * 0.01).tolist()
embedding_2 = (base + np.random.randn(1536) * 0.01).tolist()
id_1 = upsert_memory(conn, "user_42", "ç¨æ·å欢åç¾å¼åå¡", embedding_1)
id_2 = upsert_memory(conn, "user_42", "ç¨æ·æ¯å¤©åç¾å¼ä¸å ç³", embedding_2)
assert id_1 == id_2 # åºæ´æ°æ§è®°å¿ï¼è䏿¯æå
¥æ°è¡
with conn.cursor() as cur:
cur.execute("SELECT COUNT(*) FROM memories WHERE user_id = 'user_42'")
assert cur.fetchone()[0] == 1 # æç»åªæä¸æ¡è®°å¿
è¿æ®µä»£ç è§£å³âè¯ä¹é夿 æ³èªå¨éªè¯âçé®é¢ã注æ embedding åéæä»¬å äºå¾®å°åªå£°ï¼æ¨¡æçå®åºæ¯ä¸åä¸è¯ä¹çä¸å embeddingã
ç¬¬ä¸æ®µä»£ç è§£å³è¡°åçç¥éªè¯é®é¢ï¼è¡°å彿°æ ¹æ® last_access_at å decay_rate 计ç®è®°å¿æææéï¼æ¶é´è¶ä¹
æéè¶ä½ãæµè¯ç¡®ä¿ 7 天åçè®°å¿æéç¡®å®éä½äºã
# memory_decay.py
import math
from datetime import datetime, timezone
def effective_weight(importance: float, decay_rate: float,
last_access_at: datetime) -> float:
"""
è¡°åæ¨¡åï¼effective_weight = importance * exp(-decay_rate * days)
days æ¯è·ç¦»ä¸æ¬¡è®¿é®ç天æ°ï¼è¡°åéç decay_rate é»è®¤ 0.05
"""
now = datetime.now(timezone.utc)
delta_days = (now - last_access_at).total_seconds() / 86400
return importance * math.exp(-decay_rate * delta_days)
æµè¯ï¼
# test_memory_decay.py
from datetime import datetime, timedelta, timezone
from memory_decay import effective_weight
def test_decay_reduces_weight_over_time():
"""è®°å¿ 7 天æªè®¿é®ï¼æéåºæ¾èä¸é"""
now = datetime.now(timezone.utc)
fresh_weight = effective_weight(1.0, 0.05, now)
stale_weight = effective_weight(1.0, 0.05, now - timedelta(days=7))
assert fresh_weight == 1.0
assert stale_weight < 0.75 # 1.0 * exp(-0.35) â 0.705
def test_decay_rate_zero_means_no_decay():
"""decay_rate=0 æ¶æéæ°¸ä¸è¡°å"""
now = datetime.now(timezone.utc)
weight = effective_weight(1.5, 0.0, now - timedelta(days=365))
assert weight == 1.5
è¿ä¸¤ä¸ªæµè¯æä¹ååªåå¨äºææ¡£éçè¡°åå ¬å¼éæ»å¨äº CI éã以åè°åæ¹éä¸ä¸ªåæ°ï¼æµè¯ç´æ¥çº¢ã
踩åè®°å½
åä¸ï¼å°æ°æ®éä¸ç´¢å¼åè£ å¨å·¥ä½
æå¼å§ææ ivfflat ç´¢å¼å»ºå¥½ï¼æµè¯æ°æ®åªæ 20 æ¡ï¼åç° EXPLAIN éæ°¸è¿èµ° Seq Scanãæä»¥ä¸ºæ¯ç´¢å¼å¤±æï¼æè
¾äºå天ãåå ï¼pgvector ç IVFFlat ç´¢å¼å¨æ°æ®éå°äº lists åæ°æ¶ä¸ä¼å¯ç¨ï¼ä¼åå¨ç´æ¥é¡ºåºæ«ææ´å¿«ã宿¹ææ¡£æ²¡æè¯´è¿ä¸ªéå¼å
³ç³»ã
è§£å³ï¼æµè¯éç¨ SET enable_seqscan = off; 强å¶èµ°ç´¢å¼ï¼æè
䏿¬¡æ§æå
¥ 1000+ æ¡æ°æ®åæµãç产ç¯å¢æ°æ®é大ï¼ç´¢å¼èªç¶ä¼çæã
åäºï¼ä½å¼¦è·ç¦»èå´æ¯ [0, 2] 䏿¯ [-1, 1]
æä¸å¼å§åç´è§è§å¾åéè·ç¦»åºè¯¥å¨ -1 å° 1 ä¹é´ï¼äºæ¯æå»ééå¼è®¾æ 0.5ãç»æçº¿ä¸ç¸ä¼¼åº¦é«è¾¾ 0.75 çè®°å¿ä¹è¢«æå
¥äºã忥æå° raw distance æåç° pgvector ç cosine distance èå´æ¯ 0 å° 2ï¼0.5 è·ç¦»å
¶å®ç¸ä¼¼åº¦æ¯ 0.5ï¼å®å
¨çè§£åäºã
è§£å³ï¼æ°¸è¿ç¨ 1 - distance æ¢ç®æ similarity 忝è¾éå¼ï¼ä»£ç é忏
æ¥æ³¨éï¼é¿å
å人å踩ã
ææéªè¯
| ææ | ä¼åå | ä¼åå |
|---|---|---|
| å»éæµè¯æ¶é´ | æå·¥ 20 åé | pytest 40 ç§ |
| è®°å¿éå¤ç | 18% | 1.5% |
| çº¿ä¸ prompt token è¶ éæ¥è¦ | æ¯å¨ 3 次 | 0 次 |
| è¡°åé»è¾æµè¯è¦ç | 0 个ç¨ä¾ | 2 个ç¨ä¾ |
å¯ç´æ¥ç¨ç代ç /å·¥å ·
docker run -e POSTGRES_PASSWORD=postgres -p 5432:5432 pgvector/pgvector:pg16
pytest -m memory # è·ææè®°å¿ç¸å
³æµè¯
#Python #å端 #AIå·¥ç¨ #PostgreSQL #æµè¯èªå¨å
å ³äºä½è
ä¸ä¸ªå¸¸å¹´å¨ AI åºç¨å±ååç«¯æ°æ®å±ä¹é´å夿¨ªè·³çå®ææ´¾å¼åè
ï¼å欢æçäº§äºæ
åææµè¯ç¨ä¾ã
GitHub: github.com/baofugege
Sponsor: github.com/sponsors/ba⦠â 妿è¿ç¯æç« 帮å°ä½ ï¼è¯·æåæ¯åå¡
æä¾æå¡ï¼Python å端æ§è½ä¼å / å·¥å
·å®å¶ / ææ¯å¨è¯¢ï¼èç³» Telegram @baofugege
Aitishiku.com