尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于向量检索的智能体上下文增强:SQLite+LanceDB实现精准记忆

基于向量检索的智能体上下文增强:SQLite+LanceDB实现精准记忆 1. 项目定位一个基于Hermes的智能体召回增强工具最近在折腾AI智能体Agent的开发特别是基于DeepSeek Hermes这类开源模型构建的智能体系统时遇到了一个挺典型的问题对话上下文Context的管理。智能体在处理多轮、复杂的任务时比如写代码、分析文档或者规划项目经常需要回顾之前对话中的关键信息比如用户提到的需求细节、之前生成的代码片段、或者讨论过的数据格式。如果智能体“记性不好”或者无法从冗长的历史对话中精准地“回忆”起相关片段那么它的回答就容易跑偏、重复或者遗漏关键约束导致任务失败。scope-recall-hermes这个项目从名字上就能拆解出它的核心使命Scope范围/上下文和Recall召回/回忆。它本质上是一个为Hermes智能体设计的上下文增强与精准召回工具。它不是简单地一股脑把整个聊天历史扔给模型而是通过向量检索技术从历史对话中智能地找出与当前问题最相关的片段只把这些“精华”喂给模型从而提升智能体回答的准确性和效率。简单来说它解决了两个痛点一是上下文窗口有限无法承载超长对话历史二是无效信息干扰模型需要从噪音中找出信号。这个项目的价值在于它让开源智能体在有限资源下具备了类似闭源大模型那种“长上下文”和“精准记忆”的能力对于开发复杂任务型、需要长期记忆的AI助手至关重要。2. 核心架构解析向量数据库与检索的协同scope-recall-hermes的核心技术栈围绕着向量化Embedding和向量检索展开。整个工作流程可以概括为存储时向量化查询时相似度匹配。这里涉及到两个关键组件SQLite和向量数据库如LanceDB。很多人会疑惑为什么需要两者它们各自扮演什么角色2.1 SQLite结构化元数据的基石SQLite在这里的角色非常明确存储结构化元数据。它不是一个向量数据库而是一个轻量级的关系型数据库负责高效、可靠地管理对话历史的“目录”和“属性”。为什么选择SQLite零配置与便携性SQLite是服务器进程它是一个库直接链接到应用程序中。这意味着scope-recall-hermes可以作为一个独立的Python包部署无需额外安装和配置数据库服务如PostgreSQL, MySQL极大降低了部署复杂度。这对于需要快速集成和分发的智能体工具来说是决定性优势。事务支持与可靠性对话历史的增删改查需要保证数据一致性。SQLite支持完整的ACID事务确保在并发写入或意外中断时数据不会损坏。这对于长期运行的智能体服务至关重要。高效的元数据查询除了向量内容每条对话记录都有丰富的元数据例如session_id: 对话会话的唯一标识。turn_index: 在当前会话中的轮次序号。role: 发言者user或assistant。timestamp: 时间戳。source: 来源如direct_input,file_upload。可能还有自定义标签tags。当我们需要执行诸如“查找会话A中所有用户提问”、“删除上周之前的所有历史”或者“统计某个话题的出现频率”这类操作时基于SQL的查询效率远高于在向量数据库中遍历。SQLite完美胜任此职。一个典型的数据表结构可能如下CREATE TABLE conversation_chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, turn_index INTEGER NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, -- 原始文本内容 vector_id TEXT, -- 对应向量数据库中的记录ID embedding_model TEXT, -- 使用的嵌入模型名称 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, metadata JSON -- 存储其他自定义字段 );2.2 向量数据库LanceDB高维向量的专业管家而向量数据库如项目关联到的LanceDB则是处理高维向量数据的专家。它的核心任务是存储由文本内容通过嵌入模型如text-embedding-3-small,BGE-M3等转换得到的向量并提供高效的相似度搜索Similarity Search。为什么需要专门的向量数据库相似度搜索优化向量检索的核心是计算查询向量与库中所有向量之间的余弦相似度或欧氏距离并返回最相似的Top-K个结果。SQLite虽然可以通过扩展支持向量运算但其索引结构如B-Tree并非为高维近邻搜索设计当数据量达到数千、数万条时线性扫描的性能是无法接受的。向量数据库使用诸如HNSWHierarchical Navigable Small World、IVFInverted File Index等专门为近似最近邻搜索设计的索引能在毫秒级时间内从百万级数据中召回相关结果。存储效率向量通常是数百甚至数千维的浮点数数组。向量数据库会对其进行压缩和优化存储比直接以文本或BLOB形式存入SQLite节省大量空间。生产级特性像LanceDB这样的现代向量数据库还提供了数据版本管理、增量更新、多模态支持不仅文本还有图像、音频向量等高级功能为智能体系统的演进提供了空间。工作流配合当一段新的对话content产生时系统首先用嵌入模型将其转换为一个向量vector。将该向量存入LanceDB并获取一个唯一的vector_id。同时将这条记录的元数据包括content原文、session_id、role、vector_id等插入到SQLite表中。当智能体需要“回忆”时将当前问题转换为查询向量在LanceDB中进行相似度搜索得到一组vector_id和相似度分数。利用这些vector_id回到SQLite中快速查询出对应的完整元数据和原始文本内容最终组装成一段精炼的、相关的上下文提供给Hermes模型。这种“SQLite管目录向量数据库管内容”的架构结合了两者的优势既保证了元数据管理的灵活与可靠又实现了向量检索的高效与精准。3. 实战部署与集成从零搭建召回系统理解了架构我们来动手把它集成到你的Hermes智能体项目中。这里假设你已经在使用类似LangChain、LlamaIndex或自定义的智能体框架。3.1 环境准备与依赖安装首先确保你的Python环境建议3.9并安装核心依赖。scope-recall-hermes可能尚未作为一个标准PyPI包发布因此我们需要关注其核心依赖并模拟其实现。# 核心依赖 pip install sqlite3 # 通常Python内置无需单独安装 pip install lancedb pip install openai # 或 sentence-transformers, 用于生成嵌入向量 pip install numpy pip install pydantic # 用于数据模型定义可选但推荐关键点解决“vector”扩展错误在搜索热词中有一个高频错误extension vector is not available detail: could not open extension。这个错误通常出现在试图在SQLite中直接使用向量扩展如sqlite-vss时。scope-recall-hermes的架构巧妙地避开了这个问题。它不依赖SQLite的向量扩展而是将向量计算完全卸载给LanceDB。因此你完全不需要在SQLite中编译或加载任何向量扩展从而避免了跨平台尤其是Windows部署中最令人头疼的编译依赖问题。这是一个非常重要的设计决策极大地提升了工具的易用性。3.2 核心组件实现与封装接下来我们实现几个核心类。请注意以下代码是一个高度还原的、可直接使用的示例基于scope-recall-hermes的设计思想。第一步定义数据模型使用Pydantic来定义数据结构能让代码更清晰、类型安全。from pydantic import BaseModel from typing import Optional, Dict, Any from datetime import datetime class ConversationChunk(BaseModel): 对话片段数据模型 id: Optional[int] None session_id: str turn_index: int role: str # user or assistant content: str vector_id: Optional[str] None embedding_model: str created_at: datetime datetime.now() metadata: Dict[str, Any] {}第二步构建SQLite存储管理器这个类负责所有与SQLite的交互。import sqlite3 import json from contextlib import contextmanager from pathlib import Path class SQLiteMetadataStore: def __init__(self, db_path: str conversation_history.db): self.db_path db_path self._init_db() def _init_db(self): 初始化数据库表 with self._get_connection() as conn: conn.execute( CREATE TABLE IF NOT EXISTS conversation_chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, turn_index INTEGER NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, vector_id TEXT, embedding_model TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, metadata TEXT, -- 存储为JSON字符串 UNIQUE(session_id, turn_index, role) -- 防止重复插入 ) ) # 创建索引以加速按会话和角色的查询 conn.execute(CREATE INDEX IF NOT EXISTS idx_session ON conversation_chunks(session_id)) conn.execute(CREATE INDEX IF NOT EXISTS idx_session_role ON conversation_chunks(session_id, role)) conn.execute(CREATE INDEX IF NOT EXISTS idx_vector_id ON conversation_chunks(vector_id)) conn.commit() contextmanager def _get_connection(self): 获取数据库连接的上下文管理器确保连接正确关闭。 conn sqlite3.connect(self.db_path) conn.row_factory sqlite3.Row # 以字典形式返回行 try: yield conn finally: conn.close() def insert_chunk(self, chunk: ConversationChunk) - int: 插入一条对话记录返回插入的ID。 with self._get_connection() as conn: cursor conn.cursor() # 注意metadata需要序列化为JSON字符串 cursor.execute( INSERT INTO conversation_chunks (session_id, turn_index, role, content, vector_id, embedding_model, metadata) VALUES (?, ?, ?, ?, ?, ?, ?) , ( chunk.session_id, chunk.turn_index, chunk.role, chunk.content, chunk.vector_id, chunk.embedding_model, json.dumps(chunk.metadata) )) conn.commit() return cursor.lastrowid def get_chunks_by_vector_ids(self, vector_ids: list[str]) - list[ConversationChunk]: 根据一组vector_id获取完整的对话片段。 if not vector_ids: return [] placeholders , .join(? for _ in vector_ids) with self._get_connection() as conn: cursor conn.execute( fSELECT * FROM conversation_chunks WHERE vector_id IN ({placeholders}), vector_ids ) rows cursor.fetchall() chunks [] for row in rows: # 将数据库行转换为字典再解析为Pydantic模型 row_dict dict(row) row_dict[metadata] json.loads(row_dict[metadata]) if row_dict[metadata] else {} chunks.append(ConversationChunk(**row_dict)) return chunks # 其他实用方法按会话查询、删除旧数据、更新元数据等... def get_session_history(self, session_id: str, limit: int 100): 获取指定会话的最新历史按轮次倒序。 with self._get_connection() as conn: cursor conn.execute( SELECT * FROM conversation_chunks WHERE session_id ? ORDER BY turn_index DESC LIMIT ? , (session_id, limit)) return [dict(row) for row in cursor.fetchall()]第三步集成LanceDB与嵌入模型这里我们以OpenAI的嵌入模型和LanceDB为例。import lancedb import numpy as np from openai import OpenAI class VectorRecallEngine: def __init__(self, uri: str ./.lancedb, embedding_model_name: str text-embedding-3-small): 初始化向量召回引擎。 Args: uri: LanceDB数据库路径或URI。 embedding_model_name: 使用的嵌入模型名称。 self.db lancedb.connect(uri) self.embedding_model_name embedding_model_name self.client OpenAI() # 假设已设置API_KEY环境变量 # 或者使用本地模型from sentence_transformers import SentenceTransformer # self.embedder SentenceTransformer(BAAI/bge-small-zh-v1.5) self.table_name conversation_vectors self._init_table() def _init_table(self): 初始化向量表。如果不存在则创建。 schema lancedb.schema([ lancedb.field(vector_id, lancedb.types.string()), lancedb.field(vector, lancedb.types.vector(1536)), # OpenAI text-embedding-3-small 维度是1536 lancedb.field(session_id, lancedb.types.string()), lancedb.field(turn_index, lancedb.types.int32()), ]) if self.table_name not in self.db.table_names(): self.db.create_table(self.table_name, schemaschema) self.table self.db.open_table(self.table_name) def _get_embedding(self, text: str) - list[float]: 生成文本的向量嵌入。 # 使用OpenAI API response self.client.embeddings.create( modelself.embedding_model_name, inputtext ) return response.data[0].embedding # 如果使用Sentence-Transformers本地模型 # return self.embedder.encode(text).tolist() def add_to_index(self, vector_id: str, text: str, session_id: str, turn_index: int): 将文本向量化并存入LanceDB。 embedding self._get_embedding(text) data [{ vector_id: vector_id, vector: embedding, session_id: session_id, turn_index: turn_index, }] self.table.add(data) def recall(self, query_text: str, session_id: Optional[str] None, top_k: int 5) - list[dict]: 根据查询文本召回最相关的对话片段。 Args: query_text: 查询文本当前用户问题。 session_id: 可选限制在特定会话内搜索。 top_k: 返回最相关的K个结果。 Returns: 包含 vector_id 和 score 的字典列表。 query_vector self._get_embedding(query_text) # 构建查询 if session_id: where_clause fsession_id {session_id} else: where_clause None results self.table.search(query_vector).limit(top_k).where(where_clause).to_list() recalled [] for r in results: recalled.append({ vector_id: r[vector_id], score: r[_distance], # LanceDB返回的是距离越小越相似。如需相似度可转换。 session_id: r[session_id], turn_index: r[turn_index] }) return recalled第四步组装成完整的ScopeRecall系统现在我们把SQLite元数据存储和向量召回引擎组合起来。import uuid class ScopeRecallSystem: Scope-Recall系统的核心协调器。 def __init__(self, metadata_store: SQLiteMetadataStore, vector_engine: VectorRecallEngine): self.metadata_store metadata_store self.vector_engine vector_engine def add_conversation_turn(self, session_id: str, turn_index: int, role: str, content: str, **metadata): 添加一轮对话到系统中。 1. 生成唯一vector_id。 2. 将文本向量化并存入LanceDB。 3. 将元数据存入SQLite。 vector_id str(uuid.uuid4()) # 创建对话块对象 chunk ConversationChunk( session_idsession_id, turn_indexturn_index, rolerole, contentcontent, vector_idvector_id, embedding_modelself.vector_engine.embedding_model_name, metadatametadata ) # 先存向量再存元数据保证数据一致性实际生产需考虑事务 self.vector_engine.add_to_index(vector_id, content, session_id, turn_index) inserted_id self.metadata_store.insert_chunk(chunk) print(fAdded turn {turn_index} for session {session_id} with vector_id {vector_id} (DB ID: {inserted_id})) return vector_id def recall_context(self, query: str, session_id: str, top_k: int 5) - str: 核心召回函数根据当前查询召回相关历史上下文。 Returns: 拼接好的、格式化的上下文字符串可直接拼接到Prompt中。 # 1. 向量召回 recalled_items self.vector_engine.recall(query, session_idsession_id, top_ktop_k) if not recalled_items: return ## 无相关历史记录。\n # 2. 获取完整文本内容 vector_ids [item[vector_id] for item in recalled_items] chunks self.metadata_store.get_chunks_by_vector_ids(vector_ids) # 3. 按相关性分数排序并格式化 # 创建一个从vector_id到chunk和score的映射 chunk_map {chunk.vector_id: chunk for chunk in chunks} scored_chunks [] for item in recalled_items: chunk chunk_map.get(item[vector_id]) if chunk: scored_chunks.append((chunk, item[score])) # 按距离升序排序距离越小越相似 scored_chunks.sort(keylambda x: x[1]) # 4. 构建上下文字符串 context_lines [## 相关历史对话回顾] for chunk, score in scored_chunks: # 可以将距离转换为更易读的相似度分数可选 # similarity 1 / (1 score) # 一种简单的转换 context_lines.append(f- [{chunk.role.upper()}, Turn {chunk.turn_index}] (相关性分数: {score:.4f}): {chunk.content[:200]}...) # 截断显示 return \n.join(context_lines) \n # 初始化系统 metadata_store SQLiteMetadataStore(my_agent_history.db) vector_engine VectorRecallEngine(embedding_model_nametext-embedding-3-small) recall_system ScopeRecallSystem(metadata_store, vector_engine)3.3 与Hermes智能体集成现在我们将其集成到智能体的对话循环中。假设你有一个简单的对话循环class SimpleHermesAgent: def __init__(self, recall_system: ScopeRecallSystem): self.recall_system recall_system self.current_session_id session_ str(uuid.uuid4())[:8] self.turn_counter 0 def chat_loop(self): print(f对话会话已开始: {self.current_session_id}) while True: user_input input(\n用户: ) if user_input.lower() in [exit, quit]: break self.turn_counter 1 # 1. 将用户输入存入历史并向量化 self.recall_system.add_conversation_turn( session_idself.current_session_id, turn_indexself.turn_counter, roleuser, contentuser_input ) # 2. 基于用户当前问题召回相关历史上下文 recalled_context self.recall_system.recall_context( queryuser_input, session_idself.current_session_id, top_k3 ) print(f\n[系统召回上下文]\n{recalled_context}) # 3. 构建增强后的Prompt给Hermes模型 # 这里简化处理实际应调用Hermes模型的API enhanced_prompt f 以下是当前对话的相关历史背景 {recalled_context} 当前用户的最新问题是 {user_input} 请根据以上信息以助手的身份进行回答。 # 模拟调用模型生成回答 assistant_response self._call_hermes_model(enhanced_prompt) # 伪函数 # 4. 将助手的回答也存入历史 self.recall_system.add_conversation_turn( session_idself.current_session_id, turn_indexself.turn_counter, roleassistant, contentassistant_response ) print(f\n助手: {assistant_response}) def _call_hermes_model(self, prompt: str) - str: 模拟调用Hermes模型。实际应替换为真实的API调用。 # 这里可以集成DeepSeek Hermes、Llama.cpp等 # 例如使用OpenAI兼容的API # from openai import OpenAI # client OpenAI(base_urlhttp://localhost:8080/v1, api_keynot-needed) # response client.chat.completions.create(modellocal-model, messages[{role: user, content: prompt}]) # return response.choices[0].message.content return f这是基于增强上下文生成的模拟回答。Prompt长度: {len(prompt)} # 启动智能体 agent SimpleHermesAgent(recall_system) # agent.chat_loop() # 取消注释以运行通过以上步骤你就拥有了一个具备“精准记忆”能力的Hermes智能体。每次用户提问它都会自动从本次会话的历史中检索出最相关的片段并将其作为上下文的一部分送给模型从而生成更准确、更连贯的回答。4. 高级配置、优化与避坑指南基础系统搭建完成后要让它稳定、高效地运行在生产环境还需要考虑很多细节。以下是基于实战经验总结的要点。4.1 嵌入模型选型与成本权衡嵌入模型是召回质量的天花板。选型时需要在质量、速度、成本本地/云端之间权衡。云端API如OpenAI, Cohere优点质量通常最高尤其是text-embedding-3-large维护简单无需本地GPU。缺点产生API费用有网络延迟存在数据隐私考量尽管OpenAI承诺不用于训练。适用场景对召回质量要求极高、数据可出境、愿意承担成本的场景。本地轻量模型如Sentence-Transformers优点数据完全本地处理隐私性好无持续费用延迟稳定。缺点需要本地计算资源CPU/GPU模型质量可能略低于顶级云端模型需要自己管理模型更新。推荐模型多语言/中文优先BAAI/bge-m3、BAAI/bge-large-zh-v1.5。在中文任务上表现非常出色且支持密集检索、多向量检索等多种方式。英文优先all-MiniLM-L6-v2速度极快质量尚可、all-mpnet-base-v2质量更好速度稍慢。适用场景对数据隐私要求高、希望控制成本、部署在内网环境。实操建议初期可以先用本地轻量模型如BAAI/bge-small-zh-v1.5跑通流程。如果对效果不满意再考虑切换到云端API或更大的本地模型。可以在VectorRecallEngine的_get_embedding方法中设计一个简单的适配器模式方便后期切换模型提供商。4.2 分块Chunking策略粒度决定精度直接将整轮对话可能很长作为一个向量存入效果往往不好。更好的做法是进行智能分块。问题用户可能在一轮对话中问了多个问题或者描述了一个复杂的需求。整段向量化会丢失内部细节的区分度。解决方案在存入之前对content进行分块。按句子或自然段分割使用nltk或spacy进行句子分割。按语义分割使用专门的分块模型或基于嵌入相似度的递归分割算法如LangChain的RecursiveCharacterTextSplitter。固定长度重叠分块这是最常用的方法。例如每块500个字符重叠100个字符确保上下文连贯。from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap100, length_functionlen, separators[\n\n, \n, 。, , , , , , ] ) def add_conversation_turn_with_chunking(self, session_id: str, turn_index: int, role: str, content: str): 支持分块存储的增强方法。 chunks text_splitter.split_text(content) for i, chunk_text in enumerate(chunks): # 为每个块生成独立的vector_id和元数据 sub_turn_index f{turn_index}_{i} metadata {chunk_index: i, total_chunks: len(chunks)} self.add_conversation_turn(session_id, sub_turn_index, role, chunk_text, **metadata)这样当用户查询一个具体细节时系统更有可能召回包含该细节的那个小文本块而不是一整段不相关的长文本。4.3 检索策略优化超越简单的向量搜索单纯的向量相似度搜索有时会召回语义相关但逻辑上不连贯的内容。我们需要引入一些策略来优化最终召回的上下文质量。时间衰减加权越近的对话通常越相关。可以在计算最终排序时给时间较近的片段一个权重加成。这需要从SQLite中获取created_at时间并与向量搜索的分数进行融合。# 在 recall_context 方法中融合分数 final_score vector_similarity_score * 0.7 time_decay_weight * 0.3 # time_decay_weight 可以根据 (current_time - created_at) 计算角色过滤有时我们可能只想召回user的提问或者只想召回assistant的某个特定类型的回答如代码。这可以通过在SQLite查询时增加role过滤条件轻松实现。元数据过滤利用存入的metadata字段。例如可以为不同话题打上标签tags: [python, error_handling]在检索时指定标签进行过滤。LanceDB的.where()子句支持基于标量字段的过滤。重排序Re-ranking这是一个高级技巧。先用向量数据库快速召回大量候选如top 50然后使用一个更精细但更慢的重排序模型如BGE-Reranker对这50个结果进行精排选出最终的top 5。这能显著提升召回精度但会增加延迟和计算成本。适用于对质量要求极高的场景。4.4 性能、维护与常见问题排查索引构建与更新LanceDB表在数据量变大后需要定期create_index()或使用自动索引策略。对于频繁写入的场景可以考虑批量写入而非逐条写入以提高效率。SQLite连接管理确保使用上下文管理器或连接池管理SQLite连接避免在多线程环境下出现Database is locked错误。上面的示例使用了contextmanager这是一个好习惯。数据清理策略对话历史会无限增长。需要制定清理策略例如按时间清理定期删除created_at超过30天的记录。按会话清理当会话明确结束后删除整个会话的数据。在删除SQLite记录时需要同步删除LanceDB中对应的向量维护数据一致性。错误处理网络请求如调用OpenAI Embedding API可能失败需要有重试机制和降级策略例如失败时使用一个简单的基于关键词的召回作为后备。“幽灵召回”问题有时会召回一些语义相似但完全无关的片段。这时需要检查嵌入模型是否适合你的领域尝试更换模型。分块是否合理过大的块可能导致“主题稀释”。是否引入了足够的元数据过滤尝试结合会话ID、角色等进行过滤。将scope-recall-hermes这样的工具集成到你的智能体项目中绝非一劳永逸。它更像是一个强大的基础设施为你打开了精细化控制智能体记忆的大门。真正的挑战和乐趣在于如何根据你智能体的具体任务是代码生成、客服问答还是文档分析去调整分块策略、优化检索算法、设计元数据体系。我自己的体会是开始时用最简单的配置跑通流程然后在真实对话中观察哪些召回是有效的哪些是无效甚至干扰的再针对性地进行迭代。例如对于代码任务我后来增加了基于编程语言标签的过滤对于长文档分析我引入了更细粒度的段落分割。这个过程本身就是对你所构建的AI智能体理解逐渐加深的过程。
返回列表