1. 项目概述:为什么我们需要一个健壮的 Agent 存储层?

如果你正在搭建一个 AI Agent 系统,无论是个人项目还是企业级应用,迟早会撞上一个核心问题: Agent 的记忆和知识放在哪里? 这听起来像是个简单的存储问题,但背后牵扯的复杂性远超想象。一个简单的对话 Agent 可能只需要记住上文的几句话,但一个面向复杂任务、需要长期记忆、并能从海量文档中检索知识的 Agent,其存储和检索需求就完全是另一个量级了。

我最近在重构一个企业内部的智能客服 Agent 项目,就深刻体会到了这一点。最初的版本,Agent 的状态、对话历史、乃至从知识库(KB)里检索到的片段,都一股脑地塞在内存里,或者用简单的键值对文件存储。当并发用户数上来,或者知识文档超过几百篇时,系统就开始变得迟缓、不稳定,甚至出现“记忆错乱”——Agent 把不同会话的内容混在了一起。这迫使我停下来思考:一个生产级的 Agent 系统,其“基础设施”到底应该是什么样子?

这正是“Store 协议”要解决的问题。它不是一个具体的数据库,而是一个 抽象层 ,一个契约。它定义了 Agent 系统如何与底层存储介质进行交互,无论是保存临时的会话状态,还是查询庞大的企业知识库。把 Postgres 作为存储路径,则是这个抽象协议下一个非常经典且强大的具体实现。而“企业 KB 词法检索”,则是这个存储层之上,面向特定业务场景(知识问答)的核心能力增强。今天,我就结合自己的踩坑和重构经验,来拆解这三者如何协同工作,构建出 Agent 系统坚实的数据地基。

2. 核心需求解析:Agent 系统对存储的独特要求

在深入技术细节之前,我们必须先搞清楚,一个 AI Agent 系统到底对存储提出了哪些不同于传统应用的需求。理解这些,才能明白为什么不能随便找个数据库就往上套。

2.1 状态管理的复杂性与实时性

Agent 的核心是拥有“状态”。这个状态可能包括:

  • 会话上下文 :当前多轮对话的历史消息。
  • 工作记忆 :为解决当前任务而临时记住的中间信息、工具调用结果。
  • 长期记忆 :用户偏好、历史交互的关键结论等需要持久化的信息。
  • 执行状态 :一个复杂任务被分解成多个步骤后,当前执行到哪一步了。

这些状态需要被 频繁、低延迟地读写 。想象一下,Agent 每说一句话,都可能需要更新它的工作记忆;每执行一个工具,都需要记录结果。这就要求存储层必须有极高的写入性能和毫秒级的读取延迟。同时,状态之间可能存在复杂的关联,比如一个会话状态关联多个工具调用记录,这又要求存储能支持一定程度的关系型查询。

注意 :把 Agent 的所有状态都塞进同一个大 JSON 对象存到数据库的一个字段里,是最初级的做法。虽然简单,但在高并发下,对这个字段的频繁更新会成为性能瓶颈和锁冲突的重灾区。

2.2 知识检索的精准度与效率矛盾

企业知识库(KB)检索是 Agent 能力的放大器。但这里的挑战在于“语义”与“词法”的平衡。

  • 语义检索(如向量检索) :优点是能理解意图。用户问“怎么报销差旅费”,即使知识库里只有一篇名为《员工费用报销流程》的文档,也能被找出来。但它依赖嵌入模型,计算开销大,且对于非常具体的术语、代码、型号(如“ERROR-404A”、“Spring Boot 2.7.x”)可能不够精准。
  • 词法检索(如全文搜索) :优点是快、准。对于上述具体的术语,传统的倒排索引能瞬间找到精确匹配的文档。但它无法处理表述差异,用户说“电脑开不了机”,知识库里是“主机无法启动”,就可能检索不到。

一个健壮的 Agent 系统,尤其是企业级应用,必须能融合两者。 词法检索在这里不是落后的代名词,而是对语义检索的必要补充和兜底 ,确保关键信息不被遗漏。

2.3 数据模式的灵活性与演化能力

Agent 系统在快速迭代。今天你可能只需要存储对话,明天可能就需要记录每个决策的置信度,后天又需要关联外部业务系统的 ID。存储层的数据模式(Schema)必须具备 灵活性 。传统关系型数据库严格的表结构,在项目早期可能会成为阻碍;而 NoSQL 的完全无模式,又可能在后期数据一致性上埋坑。

我们需要一种折中:有基本的结构约束以保证核心数据的可靠性,同时又允许部分字段能动态扩展。这正是像 Postgres 的 JSONB 数据类型这类技术大放异彩的地方。

3. Store 协议设计:定义数据访问的通用语言

“Store 协议”听起来高大上,其实核心思想就是 面向接口编程 。它为 Agent 系统内部各种需要存储的组件(如记忆、知识库、工具缓存)定义了一套统一的、标准化的读写接口。

3.1 协议的核心接口抽象

一个最小化的 Store 协议通常包含以下几个核心接口:

# 这是一个概念示例,并非特定框架代码
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Generic, TypeVar

T = TypeVar('T')  # 实体类型

class Store(ABC, Generic[T]):
    """存储抽象基类"""
    
    @abstractmethod
    async def put(self, key: str, value: T, **kwargs) -> bool:
        """插入或更新一个键值对。"""
        pass
    
    @abstractmethod
    async def get(self, key: str, **kwargs) -> Optional[T]:
        """根据键获取值。"""
        pass
    
    async def delete(self, key: str, **kwargs) -> bool:
        """根据键删除值。"""
        pass
    
    @abstractmethod
    async def search(self, query: str, filter_dict: Optional[Dict] = None, limit: int = 10, **kwargs) -> List[T]:
        """搜索接口。对于不同存储,query的含义不同(可能是文本,也可能是向量)。"""
        pass

class StateStore(Store[Dict]):
    """专门用于存储Agent状态的Store,值通常是字典。"""
    pass

class KnowledgeStore(Store[Document]):
    """专门用于存储知识文档的Store。"""
    
    @abstractmethod
    async def lexical_search(self, query: str, field: str = "content", **kwargs) -> List[Document]:
        """词法检索接口。"""
        pass
    
    @abstractmethod
    async def semantic_search(self, query_embedding: List[float], **kwargs) -> List[Document]:
        """语义(向量)检索接口。"""
        pass

为什么这么设计?

  1. 解耦 :Agent 的业务逻辑(如推理、决策)不再关心数据是存在 Redis、Postgres 还是云存储里。它只调用 store.get() store.search()
  2. 可替换性 :今天你用 SQLite 做原型开发,明天要上线了,只需要实现一个基于 Postgres 的 PostgresKnowledgeStore 类,替换掉原来的实现,业务代码一行都不用改。
  3. 测试友好 :你可以轻松实现一个 MockStore 用于单元测试,而不需要搭建真实的数据库环境。

3.2 协议中的关键设计决策

在设计协议时,有几个细节决定了它的好用程度:

  • 异步优先 :现代 AI 应用框架(如 FastAPI、LangChain)普遍基于异步 I/O。存储操作(尤其是网络 I/O)是主要的阻塞源,因此 Store 协议的方法应该设计为 async ,以充分利用异步生态。
  • 泛型支持 :使用 Generic[T] 可以让协议更类型安全。一个 StateStore 返回 Dict ,一个 KnowledgeStore 返回 Document 对象,IDE 和类型检查器能提供更好的支持。
  • 扩展性 :通过 **kwargs 参数,为不同的后端存储实现提供传递特殊参数的通道。例如,向 PostgresKnowledgeStore.search() 传递 use_lexical_first=True 参数,来控制检索策略。

实操心得 :在定义协议时,不要试图一开始就设计一个“万能”的接口。从最核心的 get put search 开始,在实际开发中遇到新的需求(比如按范围查询、批量操作)时,再谨慎地添加到协议中。过度设计的前期协议往往会变得臃肿且难以实现。

4. Postgres 作为存储路径的深度实践

为什么是 Postgres?在众多数据库中,Postgres 因其惊人的“全能性”,成为了实现 Store 协议的绝佳选择。它不仅仅是一个关系型数据库。

4.1 利用 JSONB 实现灵活的模式

对于 Agent 的状态存储,我们可以在 Postgres 中设计这样一张表:

CREATE TABLE agent_sessions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    session_id VARCHAR(255) NOT NULL UNIQUE, -- 业务会话ID
    agent_id VARCHAR(100) NOT NULL, -- 哪个Agent
    state_data JSONB NOT NULL DEFAULT '{}'::jsonb, -- 核心状态,JSON格式
    metadata JSONB DEFAULT '{}'::jsonb, -- 扩展元数据
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- 为常用查询字段和JSONB中的关键路径创建索引
CREATE INDEX idx_sessions_agent ON agent_sessions(agent_id);
CREATE INDEX idx_sessions_created ON agent_sessions(created_at);
CREATE INDEX idx_sessions_state ON agent_sessions USING GIN (state_data); -- GIN索引加速JSONB查询

关键点

  • state_data 字段使用 JSONB 类型,可以存储任意结构的 Agent 状态(对话历史、工作记忆等)。你可以直接在里面嵌套数组、对象。
  • metadata 字段同样使用 JSONB ,用于存放那些未来可能增加、但当前模式不明确的扩展信息。
  • JSONB 字段上创建 GIN 索引 ,可以极大地加速对其中特定键值的查询。例如,如果你想快速找到所有 state_data->'user_preference'->>'theme' dark 的会话,GIN 索引能派上大用场。
  • updated_at 字段的自动更新(可通过触发器实现)对于清理过期会话和监控非常有用。

4.2 实现具体的 Store 类

基于上述表结构,我们可以实现一个 PostgresStateStore

import asyncpg
from your_store_protocol import StateStore

class PostgresStateStore(StateStore):
    def __init__(self, connection_pool: asyncpg.Pool):
        self.pool = connection_pool
    
    async def put(self, key: str, value: dict, **kwargs) -> bool:
        """插入或更新会话状态。这里key对应session_id。"""
        query = """
            INSERT INTO agent_sessions (session_id, agent_id, state_data, metadata)
            VALUES ($1, $2, $3, $4)
            ON CONFLICT (session_id) 
            DO UPDATE SET 
                state_data = EXCLUDED.state_data,
                metadata = EXCLUDED.metadata,
                updated_at = NOW()
            RETURNING id;
        """
        agent_id = kwargs.get('agent_id', 'default')
        metadata = kwargs.get('metadata', {})
        
        async with self.pool.acquire() as conn:
            try:
                await conn.execute(query, key, agent_id, value, metadata)
                return True
            except Exception as e:
                # 实际项目中应有更细致的异常处理和日志
                return False
    
    async def get(self, key: str, **kwargs) -> Optional[dict]:
        """获取会话状态。"""
        query = "SELECT state_data FROM agent_sessions WHERE session_id = $1;"
        async with self.pool.acquire() as conn:
            row = await conn.fetchrow(query, key)
            return dict(row['state_data']) if row else None
    
    async def search(self, query: str, filter_dict: Optional[Dict] = None, limit: int = 10, **kwargs) -> List[dict]:
        """示例:根据JSONB字段内的内容进行查询。"""
        # 这里构建一个基于JSONB路径的简单查询
        base_sql = "SELECT state_data FROM agent_sessions WHERE 1=1"
        params = []
        param_counter = 1
        
        if filter_dict:
            for field, value in filter_dict.items():
                # 假设filter_dict的key是JSONB路径,如 `user_id`
                base_sql += f" AND state_data->>${{{param_counter}}} = ${param_counter + 1}"
                params.extend([field, value])
                param_counter += 2
        
        base_sql += f" LIMIT ${param_counter};"
        params.append(limit)
        
        async with self.pool.acquire() as conn:
            rows = await conn.fetch(base_sql, *params)
            return [dict(row['state_data']) for row in rows]

这个实现展示了如何将抽象的协议映射到具体的 SQL 操作。使用 asyncpg 这样的异步驱动,能保证整个数据访问层是非阻塞的。

5. 企业 KB 词法检索的工程化实现

词法检索的核心是 全文搜索引擎 。Postgres 内置了强大的全文搜索功能,足以应对大多数企业知识库的场景。

5.1 知识库表结构与全文搜索索引

首先,设计存储知识文档的表:

CREATE TABLE knowledge_documents (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    doc_id VARCHAR(255) NOT NULL UNIQUE, -- 外部文档ID
    title TEXT NOT NULL,
    content TEXT NOT NULL, -- 文档全文内容
    content_tsvector TSVECTOR, -- 用于全文搜索的向量列
    category VARCHAR(100),
    tags TEXT[], -- 使用数组类型存储标签
    metadata JSONB DEFAULT '{}'::jsonb,
    embedding vector(1536), -- 假设使用1536维的向量(例如OpenAI text-embedding-3)
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- 创建GIN索引加速全文搜索
CREATE INDEX idx_knowledge_fts ON knowledge_documents USING GIN(content_tsvector);
-- 创建向量索引(例如使用pgvector的ivfflat或hnsw)
CREATE INDEX idx_knowledge_embedding ON knowledge_documents USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- 为常用过滤条件创建索引
CREATE INDEX idx_knowledge_category ON knowledge_documents(category);

关键的一步是自动生成 content_tsvector 。我们可以创建一个触发器:

CREATE OR REPLACE FUNCTION knowledge_documents_tsvector_update()
RETURNS TRIGGER AS $$
BEGIN
    NEW.content_tsvector = 
        setweight(to_tsvector('english', coalesce(NEW.title, '')), 'A') ||
        setweight(to_tsvector('english', coalesce(NEW.content, '')), 'B');
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tsvector_update BEFORE INSERT OR UPDATE 
ON knowledge_documents FOR EACH ROW 
EXECUTE FUNCTION knowledge_documents_tsvector_update();

这个触发器做了两件事:

  1. to_tsvector('english', ...) :将文本解析为词位(lexeme),并进行词干提取、移除停用词等。 'english' 是文本搜索配置,针对英文优化,中文需要其他配置(如 zhparser )。
  2. setweight(... , 'A') :给标题(A)和内容(B)赋予不同的权重,这样在搜索结果中,标题匹配的文档排名会更靠前。

5.2 实现混合检索策略

现在,我们可以在 PostgresKnowledgeStore 中实现混合检索方法:

class PostgresKnowledgeStore(KnowledgeStore):
    def __init__(self, connection_pool: asyncpg.Pool):
        self.pool = connection_pool
    
    async def lexical_search(self, query: str, field: str = "content", limit: int = 5, **kwargs) -> List[Document]:
        """基于Postgres全文搜索的词法检索。"""
        # 构建全文搜索查询,`plainto_tsquery` 将查询字符串转换为tsquery
        sql = """
            SELECT id, doc_id, title, content, category, tags,
                   ts_rank_cd(content_tsvector, plainto_tsquery('english', $1)) as rank
            FROM knowledge_documents
            WHERE content_tsvector @@ plainto_tsquery('english', $1)
            ORDER BY rank DESC
            LIMIT $2;
        """
        async with self.pool.acquire() as conn:
            rows = await conn.fetch(sql, query, limit)
            return [self._row_to_document(row) for row in rows]
    
    async def semantic_search(self, query_embedding: List[float], limit: int = 5, **kwargs) -> List[Document]:
        """基于pgvector的向量语义检索。"""
        # 假设已安装pgvector扩展,并且embedding列类型为`vector`
        sql = """
            SELECT id, doc_id, title, content, category, tags,
                   1 - (embedding <=> $1) as cosine_similarity -- pgvector的<=>运算符计算余弦距离
            FROM knowledge_documents
            WHERE embedding IS NOT NULL
            ORDER BY embedding <=> $1
            LIMIT $2;
        """
        async with self.pool.acquire() as conn:
            rows = await conn.fetch(sql, query_embedding, limit)
            return [self._row_to_document(row) for row in rows]
    
    async def hybrid_search(self, query_text: str, query_embedding: List[float], 
                            lexical_weight: float = 0.3, semantic_weight: float = 0.7,
                            limit: int = 10, **kwargs) -> List[Document]:
        """
        混合检索:结合词法检索和语义检索的分数。
        这是一种简单的线性加权融合方式。
        """
        lexical_results = await self.lexical_search(query_text, limit=limit*2) # 多取一些
        semantic_results = await self.semantic_search(query_embedding, limit=limit*2)
        
        # 将结果合并到字典,key为doc_id
        scored_docs = {}
        
        # 给词法检索结果赋分 (归一化排名分数)
        for i, doc in enumerate(lexical_results):
            # 排名越靠前,分数越高(例如,第1名得1分,第2名得0.9分...)
            lexical_score = 1.0 / (i + 1) 
            base_score = scored_docs.get(doc.doc_id, {'doc': doc, 'lexical': 0.0, 'semantic': 0.0})
            base_score['lexical'] = lexical_score
            scored_docs[doc.doc_id] = base_score
        
        # 给语义检索结果赋分 (使用余弦相似度)
        for i, doc in enumerate(semantic_results):
            # 假设doc对象有一个similarity属性,来自SQL查询
            semantic_score = getattr(doc, 'cosine_similarity', 1.0 / (i + 1))
            base_score = scored_docs.get(doc.doc_id, {'doc': doc, 'lexical': 0.0, 'semantic': 0.0})
            base_score['semantic'] = semantic_score
            scored_docs[doc.doc_id] = base_score
        
        # 计算加权总分并排序
        def calc_final_score(item):
            scores = item[1]
            return (scores['lexical'] * lexical_weight + scores['semantic'] * semantic_weight)
        
        sorted_items = sorted(scored_docs.items(), key=calc_final_score, reverse=True)
        final_docs = [item[1]['doc'] for item in sorted_items[:limit]]
        
        return final_docs
    
    def _row_to_document(self, row) -> Document:
        """将数据库行转换为业务层的Document对象。"""
        # 这里是一个简单示例,实际项目中的Document类可能更复杂
        from your_models import Document
        return Document(
            id=row['id'],
            doc_id=row['doc_id'],
            title=row['title'],
            content=row['content'],
            metadata={
                'category': row['category'],
                'tags': row['tags'],
                'rank': getattr(row, 'rank', None),
                'cosine_similarity': getattr(row, 'cosine_similarity', None)
            }
        )

这个 hybrid_search 方法是混合检索的核心。它分别进行词法和语义检索,然后通过加权分数进行融合。 lexical_weight semantic_weight 参数需要根据你的具体数据和查询类型进行 调优 。例如,对于术语性很强的技术文档查询,可以调高词法权重;对于开放性的、重语义的理解类查询,则调高语义权重。

6. 系统集成与性能调优实战

设计好各个组件后,如何将它们优雅地集成到 Agent 系统中,并保证高性能、高可用,是下一个挑战。

6.1 依赖注入与配置化管理

不要在业务代码里硬编码 new PostgresStateStore(...) 。应该使用依赖注入容器来管理这些存储实例的生命周期和配置。

# 示例:使用FastAPI的依赖注入系统
from fastapi import Depends
import asyncpg

async def get_db_pool():
    """创建数据库连接池(单例)。"""
    pool = await asyncpg.create_pool(
        host=settings.db_host,
        port=settings.db_port,
        user=settings.db_user,
        password=settings.db_password,
        database=settings.db_name,
        min_size=5,
        max_size=20
    )
    yield pool
    await pool.close()

async def get_state_store(pool: asyncpg.Pool = Depends(get_db_pool)) -> StateStore:
    """获取状态存储实例。"""
    return PostgresStateStore(pool)

async def get_knowledge_store(pool: asyncpg.Pool = Depends(get_db_pool)) -> KnowledgeStore:
    """获取知识存储实例。"""
    return PostgresKnowledgeStore(pool)

# 在Agent服务中使用
@app.post("/chat")
async def chat_endpoint(
    message: str,
    session_id: str,
    state_store: StateStore = Depends(get_state_store),
    knowledge_store: KnowledgeStore = Depends(get_knowledge_store)
):
    # 1. 获取当前会话状态
    current_state = await state_store.get(session_id) or {}
    
    # 2. 如果需要,从知识库检索
    if need_knowledge(message):
        # 先获取查询的向量嵌入(这里简化,实际可能调用嵌入模型API)
        query_embedding = await get_embedding(message)
        # 使用混合检索
        relevant_docs = await knowledge_store.hybrid_search(
            query_text=message,
            query_embedding=query_embedding,
            lexical_weight=0.4,
            semantic_weight=0.6
        )
        current_state['retrieved_context'] = [doc.content for doc in relevant_docs[:3]]
    
    # 3. 调用LLM生成回复...
    # agent_logic = YourAgentLogic(state=current_state, context=...)
    # response = await agent_logic.generate(message)
    
    # 4. 更新状态
    # current_state['conversation_history'].append({'role':'user', 'content':message})
    # current_state['conversation_history'].append({'role':'assistant', 'content':response})
    # await state_store.put(session_id, current_state)
    
    # return response

这种方式使得存储后端的更换(比如从开发环境的 SQLite 切换到生产环境的 Postgres)只需要修改配置和依赖提供函数,业务逻辑完全不受影响。

6.2 性能调优关键点

当数据量增长后,以下几个方面的调优至关重要:

  1. 连接池配置 asyncpg.create_pool 中的 min_size max_size 需要根据你的应用负载调整。设置过小会导致频繁创建连接,过大则会浪费资源。监控数据库连接数是一个好习惯。
  2. 索引优化
    • JSONB GIN 索引 :确保对 state_data metadata 中需要频繁查询的路径创建索引,例如 CREATE INDEX idx_state_user ON agent_sessions USING GIN ((state_data->'user'))
    • 全文搜索 GIN 索引 content_tsvector 上的索引是全文搜索快的根本。
    • 向量索引 pgvector 提供的 ivfflat hnsw 索引对于加速向量相似度搜索是必须的。创建索引时 lists 参数的选择需要权衡查询速度和索引构建速度/精度。
  3. 查询优化
    • 避免 N+1 查询 :在 Agent 处理中,如果需要根据多个 ID 获取知识文档,应使用 WHERE id IN (...) 一次性查询,而不是循环查询。
    • 合理使用事务 :对于需要原子性更新的多个状态操作,使用数据库事务。
    • 限制返回字段 SELECT * 是性能杀手。在查询中明确指定需要的字段,尤其是当表中有 TEXT JSONB 这类大字段时。
  4. 缓存策略
    • 状态缓存 :Agent 的会话状态在短时间内可能被频繁读取。可以考虑在 StateStore 之上增加一层内存缓存(如 Redis),缓存最近活跃的会话。 StateStore.get 方法先查缓存,未命中再查数据库并回填缓存。
    • 知识缓存 :对于热点知识文档,或者混合检索的结果,也可以进行缓存。但要注意知识库更新的缓存失效问题。

7. 常见问题与排查技巧实录

在实际部署和运行中,我遇到了不少典型问题,这里分享一些排查思路和解决方案。

7.1 问题排查速查表

问题现象 可能原因 排查步骤与解决方案
Agent 响应突然变慢 1. 数据库连接池耗尽。
2. 关键查询缺少索引。
3. 知识库表体积暴涨,向量检索变慢。
1. 查看数据库监控,检查活跃连接数。适当调大连接池 max_size ,并检查是否有连接泄漏(未正确关闭)。
2. 使用 EXPLAIN ANALYZE 分析慢查询 SQL,检查是否进行了全表扫描。为 WHERE ORDER BY 子句中的字段加索引。
3. 检查 knowledge_documents 表行数。为 embedding 列重建更优化的向量索引(如调整 hnsw m ef_construction 参数)。
词法检索查不到明明存在的关键词 1. 全文搜索配置语言不匹配。
2. 停用词(Stop Words)导致关键词被过滤。
3. 词干提取(Stemming)导致词形变化。
1. 确认 to_tsvector plainto_tsquery 使用的配置(如 'english' )与文档语言一致。对于中文,需使用 zhparser 等插件。
2. 查询 SELECT * FROM ts_debug('english', 'your-word'); 查看该词是否被识别为停用词。必要时创建自定义词典。
3. 理解这是全文搜索的特性。如需精确匹配,可考虑结合 LIKE ILIKE 操作符,或使用 tsquery 的短语搜索操作符 <->
混合检索结果不理想 1. 词法和语义检索的权重比例不合适。
2. 两种检索返回的结果集重叠度低,简单加权融合效果差。
1. 进行 A/B 测试 。准备一批标准查询和预期文档,手动调整 lexical_weight semantic_weight ,计算召回率(Recall)和平均精度(MAP)。
2. 尝试更复杂的融合策略,如 倒数融合排名(Reciprocal Rank Fusion, RRF) 。这种方法不依赖绝对分数,只依赖排名,通常对异构检索系统的融合更鲁棒。
更新 Agent 状态时出现并发冲突 多个请求同时读写同一个 session_id 的状态。 1. 乐观锁 :在 state_data 中增加一个版本号字段。更新时 WHERE session_id=$1 AND version=$2 ,如果版本不对则更新失败,业务层重试或合并。
2. 悲观锁 :对于冲突概率极高的场景,在事务开始时 SELECT ... FOR UPDATE 锁定该行。但这会降低并发度,需谨慎使用。
3. 最终一致性 :如果业务允许,可以将状态更新放入消息队列,由单个消费者串行处理。
向量索引占用磁盘空间过大 hnsw 索引为了追求速度,会占用比原始数据大得多的空间。 1. 这是空间换时间的典型权衡。确保服务器磁盘空间充足。
2. 评估是否可以接受稍低的召回率,通过调整 hnsw 索引的 ef_search 参数来降低搜索时的内存和CPU开销,但索引体积不会减小。
3. 定期清理或归档旧的、不常用的知识文档。

7.2 一个真实的调试案例:模糊匹配的陷阱

有一次,用户反馈 Agent 在回答关于“Python 装饰器”的问题时,总是引用一篇讲“Java 注解”的文档。词法检索显示“装饰器”和“注解”的英文都是“decorator”,所以排名很高。但这两者其实是不同的概念。

解决方案 :我们改进了词法检索的查询构造。不再仅仅使用 plainto_tsquery ,而是结合了 短语搜索 领域词权重提升

-- 改进前的简单查询
SELECT ... WHERE content_tsvector @@ plainto_tsquery('english', 'python decorator');

-- 改进后的查询:要求“python”和“decorator”以一定距离接近,并提升标题中匹配的权重。
SELECT ...,
       ts_rank_cd(
           content_tsvector,
           to_tsquery('english', 'python <-> decorator'), -- <-> 表示相邻
           32 -- 排名标准化参数
       ) as rank_phrase,
       ts_rank_cd(
           setweight(to_tsvector('english', coalesce(title, '')), 'A'), -- 标题权重高
           to_tsquery('english', 'decorator')
       ) as rank_title
FROM knowledge_documents
WHERE content_tsvector @@ to_tsquery('english', 'python & decorator') -- 必须同时包含
   OR title_tsvector @@ to_tsquery('english', 'decorator') -- 或者在标题里
ORDER BY (rank_phrase * 0.7 + rank_title * 0.3) DESC; -- 综合排名

这个案例说明, 词法检索的精度高度依赖于查询的构建技巧 。简单的关键词匹配往往不够,需要结合业务知识设计更精细的查询逻辑。

构建 Agent 系统的存储层,远不止是选个数据库那么简单。它要求我们在抽象与具体、灵活与规范、语义与词法、性能与精度之间做出持续的权衡和设计。通过定义清晰的 Store 协议,我们为系统奠定了可扩展的基础;通过深度利用 Postgres 这样的多模数据库,我们获得了关系模型、JSON 文档、全文搜索和向量检索于一体的强大能力;而通过精心设计混合检索策略,我们让 Agent 的知识查找能力既智能又可靠。

这套架构不是一蹴而就的。我的建议是,从最简单的内存存储开始,快速验证 Agent 的核心逻辑。当遇到状态持久化、知识检索的需求时,再引入 Store 协议和 Postgres。先实现基础版本,然后在真实流量的考验下,逐步迭代出适合你业务场景的混合检索权重、缓存策略和索引优化方案。记住,基础设施的价值,最终体现在它让上层的 Agent 智能变得多么稳定、强大和易用。

更多推荐