1. 项目缘起:为什么客服AI需要“记忆”?

做AI应用开发这些年,我接触过不少客服场景的自动化方案。从早期的规则引擎到后来的大语言模型(LLM)驱动对话,一个核心痛点始终挥之不去: 对话没有连续性,用户每次都得从头说起 。想象一下,你打电话给客服,每次接通都是不同的人,而且对方完全不记得你之前说过什么,你得一遍遍重复自己的账号、问题背景、甚至刚刚试过的解决方案。这种体验有多糟糕,我们的AI客服就给用户带来了多糟糕的体验。

这就是我启动这个项目的初衷: 构建一个真正能“记住”对话历史的客服AI智能体 。这里的“记忆”不是指简单的把最近几十条对话记录塞进上下文窗口(Context Window)那么简单。那种方式成本高昂且低效,一旦对话轮次变多,核心信息就会被淹没在冗长的上下文中,模型的理解和响应质量会急剧下降。我想要的,是一个能像人类优秀客服一样,主动识别、提取、结构化存储关键信息,并在后续对话中精准、自然地调用这些信息的系统。

这个智能体最终要达成的效果是:用户首次咨询时,它不仅能解决问题,还能自动提取并记录用户的核心诉求、产品型号、订单号、问题状态等关键实体(Entity)。当用户几天后再次回来,无论是通过同一条对话链还是新的会话入口,智能体都能立刻“认出”用户,并基于历史记录提供连贯的服务,比如主动询问“您上次反馈的XX问题,按照我们提供的方案尝试后解决了吗?”。这种体验的跃升,才是AI赋能客服的真正价值所在。

2. 核心架构设计:从“健忘症”到“长效记忆”

要实现长效记忆,不能只靠扩大上下文窗口这种“蛮力”方案。我设计的核心架构围绕 “记忆的写入、存储与读取” 三个环节展开,其核心思路是将对话流与记忆流分离。

2.1 整体架构与组件选型

整个系统可以看作是一个增强版的LLM应用架构,核心新增了“记忆管理”模块。

用户输入 -> 对话理解与记忆触发 -> [短期上下文] -> LLM核心处理 -> 响应生成
                          ↓                             ↑
                    [记忆提取与编码]              [记忆查询与注入]
                          ↓                             ↑
                    [向量记忆库 + 结构化数据库] <- [记忆检索与关联]

1. 对话理解与记忆触发层: 这一层负责实时分析用户当前的话语,判断是否需要触发记忆的“写入”或“读取”。我使用了轻量级的意图识别(Intent Recognition)和命名实体识别(NER)模型。例如,当用户说出“我的订单号是123456”时,NER模型会识别出“订单号:123456”这个实体。同时,结合意图判断(如“查询订单状态”、“报告问题”),系统决定是否将此实体作为关键记忆点进行存储。这里没有用特别复杂的模型,基于BERT微调的小模型在准确率和速度上取得了很好的平衡。

2. 记忆存储层(双存储引擎): 这是记忆系统的核心。我采用了 “向量数据库 + 关系型数据库” 的混合存储方案。

  • 向量数据库(如ChromaDB, Pinecone) :用于存储非结构化的“情景记忆”。我将每轮对话中涉及用户核心诉求、问题描述、情绪倾向的文本片段,通过嵌入模型(Embedding Model)转化为向量后存储。这便于后续进行语义相似度搜索。例如,用户描述“电脑开机有异响”,这个描述会被向量化存储。
  • 关系型数据库(如PostgreSQL) :用于存储结构化的“事实记忆”。这就是一个精心设计的“用户记忆表”,字段包括: session_id (可关联到长期用户ID)、 entity_type (如“产品型号”、“订单号”、“软件版本”)、 entity_value extracted_from (来源对话片段)、 timestamp confidence (提取置信度)。当NER识别出“订单号:123456”,它就会被清洗后存入这张表。

为什么用混合存储? 单一向量库难以做精确匹配和批量管理(如“找出所有提供过订单号的会话”),单一关系库又无法处理模糊的自然语言查询。混合方案兼顾了精确查找和语义联想。

3. 记忆读取与注入层: 在LLM生成回复前,系统会执行一次记忆检索。首先,根据当前会话ID或用户标识,从关系库中拉取所有相关的结构化事实。然后,将当前用户问题向量化,去向量库中搜索最相关的几条历史情景记忆。最后,将这些记忆以特定的格式(如“用户历史信息:...”)作为系统提示词(System Prompt)的一部分,注入给LLM。这样,LLM在生成回答时,就拥有了相关的背景知识。

4. LLM核心与上下文管理: LLM我选用的是性能与成本权衡较好的GPT-4 Turbo或Claude 3 Haiku。关键在于 上下文窗口的精细管理 。我们不再把原始对话历史全部塞进去,而是只放入:当前问题、检索到的相关记忆、以及极短的最新对话轮次(用于维持对话连贯性)。这极大地降低了Token消耗,提升了响应速度,并避免了关键信息被稀释。

2.2 关键技术选型背后的思考

  • 嵌入模型选择 :我没有使用OpenAI的收费嵌入模型,而是选用了开源的 text-embedding-3-small 。对于客服场景的短文本,它在精度和成本上完全足够,且数据隐私更可控。
  • 向量数据库选型 :项目初期我选择了 ChromaDB ,因为它轻量、易集成,且完全开源可本地部署,避免了云服务的数据传输风险。如果未来数据量极大,会考虑迁移至Weaviate或Qdrant。
  • 记忆提取策略 :这是项目的难点。单纯的NER不够,因为用户不会总是规整地陈述事实。我增加了 基于LLM的摘要与提取 作为补充:对于较长的用户描述,我会用一个小参数的LLM(如GPT-3.5-Turbo)先进行摘要,并指令其提取关键事实项,再将结果送入NER校验和结构化存储。这形成了“NER为主,LLM摘要提取为辅”的混合提取管道,准确率提升了约40%。

3. 记忆系统的核心实现细节

架构搭好了,但让记忆系统稳定、准确地工作,细节决定成败。下面拆解几个核心环节的实现。

3.1 记忆的提取与编码:从对话中“挖出”金子

记忆提取是第一步,也是最容易出错的一步。我设计了一个三级过滤管道:

第一级:实时NER与关键词触发 这是最基础的层。我预定义了客服领域的关键实体类型: PRODUCT_NAME ORDER_ID ERROR_CODE SOFTWARE_VERSION CONTACT_PREFERENCE 等。使用一个在领域内数据上微调过的NER模型进行实时提取。同时,设置一些关键词触发器,例如当用户说出“记一下”、“以后就用这个”等短语时,会提升后续句子中实体提取的优先级。

第二级:基于LLM的意图感知摘要 对于用户大段的故障描述或复杂诉求,NER可能只能抓住零散实体,丢失逻辑关联。此时,系统会调用一个专用的“摘要提取”LLM。我给它的提示词非常具体:

“你是一名客服专家。请将以下用户描述总结为最多3个关键事实点,每个事实点尽量包含【实体:值】的格式。例如:'【问题现象】:电脑无法开机;【已尝试操作】:重启三次;【期望】:恢复数据'。用户描述:{user_input}”

这样得到的输出已经是半结构化的文本,便于后续解析。

第三级:冲突消解与置信度评估 同一个事实可能在不同轮次被多次提取(比如用户先后说了订单号“123456”和“一二三四五六”)。系统会维护一个“记忆暂存区”,所有提取到的实体先放在这里,进行去重、归一化(将中文数字转阿拉伯数字)和冲突校验。对于冲突项(如两个会话提取的产品型号不同),系统会记录各自的置信度(基于提取模型的分数和上下文一致性),并暂时保留置信度高的。同时,可以设计一个简单的投票机制,或在下一次用户确认时进行澄清。

编码存储时 ,除了实体本身,我还会存储“来源上下文”(即提取出该实体的原句)和“提取时间戳”。这对于后续判断记忆的新鲜度和相关性至关重要。

3.2 混合记忆库的构建与关联

记忆不是孤立存在的,它们之间有关联。我的实现方案是:

在关系数据库中 ,除了 user_memory 表,还有一张 memory_relationship 表,用于记录记忆间的关系。例如,一条 ORDER_ID 记忆和一条 PROBLEM_DESCRIPTION 记忆可能属于同一个客服工单( CASE_ID )。通过这种关联,可以一次性拉取与某个工单相关的所有记忆。

在向量数据库中 ,我存储的不仅仅是用户原话的嵌入。为了提高检索质量,我采用了“增强嵌入”的策略。在将一段文本(如“电脑蓝屏,错误代码0x0000001A”)存入向量库前,我会用LLM对其进行一次“重写扩充”,使其包含更多隐含信息。提示词可以是:“请从客服支持角度,用多种方式描述以下问题,以便未来能通过相似问题匹配到它:{原始描述}”。生成的扩充文本(如“计算机出现蓝色屏幕死机,系统报错代码为0x0000001A,可能涉及内存或驱动问题”)再被嵌入存储。这相当于手动给记忆加了“标签”,大大提升了后续语义检索的召回率。

3.3 记忆的检索、评分与注入策略

当新用户输入到来时,记忆检索流程如下:

  1. 精确检索 :通过用户标识或会话ID,从关系数据库中直接拉取所有相关的结构化记忆(事实列表)。
  2. 语义检索 :将用户当前输入转化为向量,在向量数据库中进行相似度搜索,找出最相关的K条(例如前3条)情景记忆。
  3. 记忆评分与融合 :检索到的记忆不是直接使用。我设计了一个简单的评分函数: 记忆最终分数 = 语义相似度得分 * 0.6 + 记忆新鲜度系数(随时间衰减)* 0.3 + 历史使用频率系数 * 0.1 新鲜度系数鼓励使用较新的记忆,使用频率系数则对反复被调用的记忆给予轻微奖励(说明它可能是重要信息)。根据分数对记忆排序,过滤掉低于阈值的。
  4. 格式化注入 :将高分记忆格式化后放入LLM的系统提示词。格式非常重要,混乱的格式会让LLM困惑。我使用的格式是:
    [用户历史记忆]
    - 已知事实:用户的产品型号是MacBook Pro 2023 (订单号: MBPA123)。
    - 已知事实:用户上次(2023-10-27)报告的问题是“电池续航异常下降”。
    - 相关历史对话摘要:用户曾尝试重置SMC但未解决。
    
    这种清晰的项目符号列表格式,LLM理解起来非常高效。

4. 实战开发:搭建一个可运行的记忆型AI客服原型

理论说再多,不如动手搭一个。下面我以Python为核心,展示如何一步步构建一个最小可行产品(MVP)。

4.1 环境准备与依赖安装

首先,创建一个新的项目目录并初始化虚拟环境。

mkdir customer-support-agent-with-memory
cd customer-support-agent-with-memory
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

安装核心依赖库。这里我们选择ChromaDB作为向量库,LangChain作为编排框架(简化流程),SQLite作为初期关系数据库。

pip install langchain langchain-openai chromadb openai tiktoken sqlite3
# 如果需要本地嵌入模型,例如使用sentence-transformers
pip install sentence-transformers

4.2 初始化数据库与记忆管理类

我们创建两个基础的数据存储:SQLite表用于结构化记忆,ChromaDB集合用于向量记忆。

# memory_manager.py
import sqlite3
from datetime import datetime
import chromadb
from chromadb.config import Settings
from sentence_transformers import SentenceTransformer
import json

class MemoryManager:
    def __init__(self, user_id, db_path="memories.db"):
        self.user_id = user_id
        # 初始化SQLite
        self.conn = sqlite3.connect(db_path)
        self._init_sqlite_tables()
        # 初始化ChromaDB(持久化模式)
        self.chroma_client = chromadb.PersistentClient(path="./chroma_db")
        self.collection = self.chroma_client.get_or_create_collection(name=f"user_{user_id}_conversations")
        # 初始化嵌入模型(本地)
        self.embedder = SentenceTransformer('all-MiniLM-L6-v2')

    def _init_sqlite_tables(self):
        cursor = self.conn.cursor()
        cursor.execute('''
            CREATE TABLE IF NOT EXISTS user_fact_memory (
                id INTEGER PRIMARY KEY,
                user_id TEXT,
                entity_type TEXT,
                entity_value TEXT,
                source_context TEXT,
                extracted_at TIMESTAMP,
                confidence REAL
            )
        ''')
        self.conn.commit()

    def add_fact_memory(self, entity_type, entity_value, source_context, confidence=0.9):
        """添加一条结构化事实记忆"""
        cursor = self.conn.cursor()
        cursor.execute('''
            INSERT INTO user_fact_memory (user_id, entity_type, entity_value, source_context, extracted_at, confidence)
            VALUES (?, ?, ?, ?, ?, ?)
        ''', (self.user_id, entity_type, entity_value, source_context, datetime.now(), confidence))
        self.conn.commit()

    def add_conversation_memory(self, conversation_text):
        """将一段对话文本存入向量库"""
        embedding = self.embedder.encode(conversation_text).tolist()
        self.collection.add(
            embeddings=[embedding],
            documents=[conversation_text],
            metadatas=[{"user_id": self.user_id, "timestamp": datetime.now().isoformat()}],
            ids=[f"conv_{datetime.now().timestamp()}"]
        )

    def retrieve_related_memories(self, query_text, n_facts=5, n_conversations=2):
        """检索相关记忆:返回事实列表和相关对话片段"""
        # 1. 检索事实记忆
        cursor = self.conn.cursor()
        cursor.execute('''
            SELECT entity_type, entity_value FROM user_fact_memory
            WHERE user_id = ? ORDER BY extracted_at DESC LIMIT ?
        ''', (self.user_id, n_facts))
        facts = cursor.fetchall()
        fact_list = [f"{etype}: {evalue}" for etype, evalue in facts]

        # 2. 检索相关对话记忆(语义搜索)
        query_embedding = self.embedder.encode(query_text).tolist()
        results = self.collection.query(
            query_embeddings=[query_embedding],
            n_results=n_conversations
        )
        conversation_snippets = results['documents'][0] if results['documents'] else []

        return fact_list, conversation_snippets

4.3 构建记忆感知的对话链

使用LangChain来编排整个流程。我们创建一个自定义的 MemoryAwareChain

# agent_chain.py
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.schema import StrOutputParser
from langchain_openai import ChatOpenAI
from memory_manager import MemoryManager
import os

# 设置你的OpenAI API Key (建议从环境变量读取)
os.environ["OPENAI_API_KEY"] = "your-api-key-here"

class MemoryAwareCustomerSupportAgent:
    def __init__(self, user_id):
        self.user_id = user_id
        self.memory_manager = MemoryManager(user_id)
        self.llm = ChatOpenAI(model="gpt-4-turbo-preview", temperature=0.1)

        # 定义系统提示词模板,包含记忆插槽
        self.system_prompt_template = ChatPromptTemplate.from_messages([
            ("system", """你是一名专业、耐心的客服AI助手。你的目标是准确理解用户问题,并利用已知的用户信息提供高效帮助。

已知的用户历史信息如下:
{formatted_memory}

请基于以上历史信息(如果有),并结合当前对话,回应用户的请求。如果历史信息与当前问题相关,请自然地引用它。回答应简洁、专业、直接切入重点。"""),
            MessagesPlaceholder(variable_name="chat_history"), # 保留最近几轮对话
            ("human", "{user_input}")
        ])

        self.chain = self.system_prompt_template | self.llm | StrOutputParser()

    def format_memory(self, facts, conversations):
        """将检索到的记忆格式化为字符串"""
        memory_parts = []
        if facts:
            memory_parts.append("已知事实:")
            for fact in facts:
                memory_parts.append(f"- {fact}")
        if conversations:
            memory_parts.append("相关历史对话片段:")
            for snippet in conversations:
                memory_parts.append(f"- \"{snippet[:100]}...\"") # 截取片段
        return "\n".join(memory_parts) if memory_parts else "暂无相关历史信息。"

    def process_user_message(self, user_input, chat_history=[]):
        """处理用户输入的核心方法"""
        # 1. 记忆检索
        facts, conv_snippets = self.memory_manager.retrieve_related_memories(user_input)

        # 2. 记忆提取(简易版:这里可以集成更复杂的NER/LLM提取逻辑)
        # 假设我们有一个简单的规则提取订单号(仅为示例)
        if "订单" in user_input and any(char.isdigit() for char in user_input):
            # 简单演示:提取连续数字串作为订单号(实际应用需更健壮)
            import re
            order_match = re.search(r'订单[号]?[::]?\s*(\d+)', user_input)
            if order_match:
                order_num = order_match.group(1)
                self.memory_manager.add_fact_memory("ORDER_ID", order_num, user_input, confidence=0.8)

        # 3. 将当前对话存入向量记忆(供未来检索)
        self.memory_manager.add_conversation_memory(user_input)

        # 4. 格式化记忆并调用LLM
        formatted_memory = self.format_memory(facts, conv_snippets)
        prompt_input = {
            "formatted_memory": formatted_memory,
            "chat_history": chat_history[-4:], # 只保留最近4轮作为短期上下文
            "user_input": user_input
        }

        response = self.chain.invoke(prompt_input)

        # 5. 更新对话历史
        chat_history.append(("human", user_input))
        chat_history.append(("ai", response))

        return response, chat_history

4.4 运行一个完整的对话示例

现在,让我们模拟一个跨会话的对话流程。

# main.py
from agent_chain import MemoryAwareCustomerSupportAgent

def simulate_conversation():
    user_id = "user_001"
    agent = MemoryAwareCustomerSupportAgent(user_id)
    chat_history = []

    print("=== 第一次会话(用户报告问题)===")
    user_msg1 = "你好,我的订单号是123456,刚收到的MacBook Pro开不了机,按下电源键没反应。"
    resp1, chat_history = agent.process_user_message(user_msg1, chat_history)
    print(f"用户: {user_msg1}")
    print(f"AI: {resp1}\n")

    # 模拟一段时间后,用户再次咨询
    print("=== 第二次会话(几天后,用户跟进)===")
    # 注意:这里我们新建了一个对话链,但使用相同的user_id,所以能读取记忆
    # 在实际应用中,可能是新的HTTP请求,但会携带用户标识
    agent2 = MemoryAwareCustomerSupportAgent(user_id) # 相同user_id
    chat_history2 = [] # 新的短期对话历史

    user_msg2 = "我上次反馈的电脑不开机的问题,你们有解决方案了吗?"
    resp2, chat_history2 = agent2.process_user_message(user_msg2, chat_history2)
    print(f"用户: {user_msg2}")
    print(f"AI: {resp2}")

if __name__ == "__main__":
    simulate_conversation()

预期输出效果: 在第二次会话中,AI的回复应该类似于:

“您好,根据记录,您之前反馈了订单号123456对应的MacBook Pro无法开机的问题(按下电源键无反应)。关于这个问题,我们通常建议先尝试以下步骤:1. 检查电源适配器是否连接牢固并充电至少一小时;2. 尝试重置SMC(系统管理控制器)... 请问您之前是否尝试过这些操作呢?”

你看,AI主动提及了订单号和具体问题,实现了记忆的跨会话传递。

5. 避坑指南与性能优化实战

在实际开发和部署中,我踩过不少坑,也总结了一些优化经验。

5.1 常见问题与解决方案

问题1:记忆提取错误导致“记忆污染”

  • 现象 :NER模型错误地将“我明天下午三点有空”中的“三点”提取为 ERROR_CODE ,或将无关信息当作关键事实存储。
  • 解决方案
    1. 领域微调 :一定要用自己客服对话的历史数据对NER模型进行微调,提升领域内实体的识别准确率。
    2. 置信度过滤 :为每个提取的记忆设置置信度阈值(如0.7),低于阈值的不直接入库,可以放入待确认区。
    3. 上下文校验 :建立简单的规则进行后处理。例如, ERROR_CODE 实体值通常包含字母和数字,且前后文可能有“错误”、“代码”等词。 PRODUCT_NAME 则可能出现在“我的XXX”、“型号是”之后。
    4. 人工反馈闭环 :设计一个机制,当AI基于某条记忆做出关键判断(如确认订单)时,可以主动向用户求证:“系统显示您的订单号是123456,对吗?”用户的确认或否定可以作为强化或删除该记忆的信号。

问题2:记忆检索不相关或遗漏关键信息

  • 现象 :用户问“电池问题”,却检索出了完全不相关的“屏幕维修”历史。
  • 解决方案
    1. 查询扩展 :在检索前,先用LLM对用户原始查询进行扩展。例如,将“电池问题”扩展为“电池续航短、电池鼓包、充电不进去、电池健康度下降”等多个相关查询词,分别进行向量检索,再合并结果。
    2. 混合检索(Hybrid Search) :结合关键词(BM25)检索和向量检索。ChromaDB等现代向量库已支持。关键词检索能保证精确匹配(如“错误代码0x0000001A”),向量检索保证语义匹配(如“蓝屏”匹配“蓝色屏幕死机”)。
    3. 元数据过滤 :在向量检索时,利用元数据(如 user_id , timestamp , entity_type )进行前置过滤,缩小搜索范围,提升相关性。

问题3:记忆注入导致提示词过长或LLM混淆

  • 现象 :注入的记忆太多,挤占了问题本身的上下文,或者记忆格式混乱导致LLM无法正确理解。
  • 解决方案
    1. 记忆摘要 :对于检索到的多条相似对话记忆,不是全部注入,而是先用一个小模型对其进行摘要,只注入摘要结果。例如:“用户曾三次咨询电池问题,主要诉求是续航不足,已尝试过校准操作。”
    2. 严格格式化 :如前文所示,使用清晰、固定的模板(如Markdown列表、JSON等)来呈现记忆。LLM对结构化的提示词响应更稳定。
    3. 优先级排序与截断 :为记忆设置优先级分数(基于相关性、新鲜度、重要性),只注入Top-N条,并设置总Token数上限。

5.2 性能与成本优化

  • 向量检索优化 :对于海量记忆,全量扫描计算余弦相似度是不可行的。务必使用支持高效近似最近邻搜索(ANN)的向量数据库,它们通过HNSW或IVF等索引技术,能在精度损失很小的情况下,将检索速度提升几个数量级。
  • 嵌入模型轻量化 all-MiniLM-L6-v2 模型只有80MB左右,推理速度快,在客服短文本上效果与大型嵌入模型差距不大,是性价比之选。
  • LLM调用优化
    • 缓存 :对频繁出现的、标准的用户查询(如“怎么重置密码?”)及其回复,可以建立缓存,直接返回,避免调用LLM和记忆检索。
    • 流式响应 :对于长回复,使用流式传输(Streaming)提升用户体验感。
    • 模型分级 :不是所有任务都需要GPT-4。记忆提取、查询扩展等对创造性要求不高的任务,可以使用更便宜、更快的模型(如GPT-3.5-Turbo、Claude Haiku甚至小型开源模型)。
  • 记忆存储的冷热分层 :用户最近的、高频访问的记忆是“热记忆”,可以放在更快的存储(如内存缓存)中。很久未访问的“冷记忆”可以归档到对象存储(如S3),并在元数据中记录索引,需要时再加载。

5.3 扩展性与维护性考量

  • 记忆的更新与遗忘 :记忆不是只增不减的。需要设计“记忆衰减”或“记忆更新”机制。例如,用户的手机号变了,新的记忆应该覆盖旧的。可以设置记忆的“有效期”或基于用户确认来主动更新。
  • 记忆的隐私与安全 :所有记忆数据必须加密存储,并严格遵守数据隐私法规。提供用户查看、更正、删除其个人记忆的接口。在向LLM注入记忆时,注意脱敏敏感信息(如身份证号、完整银行卡号)。
  • 系统可观测性 :必须记录每一次记忆的提取、存储、检索和调用日志。这有助于调试错误记忆,分析记忆系统的有效性,并持续优化算法。可以创建一个简单的仪表盘,查看“最常被调用的记忆”、“最近提取的新记忆”等。

构建一个真正有记忆的AI客服智能体,远不止是接上一个向量数据库那么简单。它需要一套从理解、提取、存储、检索到巧妙应用的完整工程体系。这个过程充满了挑战,但当你看到AI能够与用户进行连贯、个性化的对话时,所有的努力都是值得的。这个项目让我深刻体会到,AI应用的深度,往往就藏在这些关乎用户体验的细节设计之中。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐