15 长期记忆:让大模型更了解你
你好,我是郑晔!
前面我们讲了几种不同类型的应用开发,你现在完全有能力做出一个非常好的大模型应用了。有了这些基础之后,之后我们会把目光转到一些工程实践上。
接下来的几讲,我会谈谈在实际的项目中需要注意的一些问题,解决好这些问题有助于我们打造更好的大模型应用。
这一讲,我们就来讨论第一个工程实践:记忆。
记忆
所谓记忆,是表现得像大模型能够记住一些事情。在之前的课程里,我们说过,大模型的 API 是无状态的,所以,大模型本质上是没有记忆的。大模型记忆的实现是通过在提示词中传递更多的内容实现的。
讨论 Agent 系统实现的时候,我们谈到了记忆组件,它包括两个部分,短期记忆和长期记忆。其中,短期记忆我们在讨论聊天机器人时已经谈到了,实现记忆的方案就是将聊天历史放到提示词中,这是一个通用的做法。但是,正如我们那一讲里所说的,能放到提示词的聊天历史是有限的,所以,它只能记住“近期”的事,这也是这种方案被称为短期记忆的原因。
长期记忆要解决的就是短期记忆未能解决的问题,希望我们的 AI 应用能够记住更久远的聊天历史。如果能够拥有长期记忆,事情就会变得更有趣,一个聊天机器人就会变得像你的一个老朋友,它会对你的偏好有更多的了解,如果是一个 Agent,它就可以更好地针对你的特点,为你提供服务。
为什么长期记忆是一个问题?从本质上说,这是大模型上下文大小有限造成的问题。前面说过,几乎每个模型的上下文窗口都是有限的。如果上下文窗口是无限的,我们完全可以用短期记忆的解决方案,也就是把所有的聊天历史都发送给大模型,让大模型“记住”所有的东西。
不过,即便上下文可以很大,还有一个现实的问题,大模型是根据 Token 计费的,过大的上下文会让费用很高。再退一步,即便上下文窗口很大且不在乎计费,如何把过大的内容传输给大模型,也会是一个非常实际的工程问题。
该如何解决长期记忆问题呢?很遗憾,长期记忆的实现在业界还没有统一的方案。但值得欣慰的是,有很多人在尝试。
常见的一个思路是,把需要记忆的内容存放到向量数据库中,采用类似于 RAG 的方案,在生成的时候,先到向量数据库中进行索引,把索引到内容放到提示词里面。当然,在具体的实现里,什么样的内容是需要记忆的内容、怎样提取怎样的内容等等,都是需要解决的问题,更有甚者,有的实现还要实现深度的挖掘,找到不同事物之间的关系。
尽管没有哪个方案取得主导的地位,但长期记忆在这个领域里确实是非常重要的一个组成部分。所以,这一讲,我还是会选择一个项目来重点学习,这个项目就是 mem0。
Mem0
根据 mem0 的自我介绍,它是为大模型应用提供的一个能够自我改进的记忆层。
这个项目甫一开源就受到了极大的关注,其中固然有这个项目本身的魅力,还有一个很重要的原因就是,它是由之前的一个项目改造而来。前一个项目叫 embedchain,是一个 RAG 框架,可以通过配置实现一个 RAG 应用。在研发过程中,研发团队发现一个长期记忆的项目是比 RAG 框架更有价值,于是,mem0 替代了 embedchain。
选择 mem0 作为长期记忆的实现方案作为我们的学习对象,固然是因为它很强大,能够满足介绍长期记忆的需要。还有一点是,它的 API 设计得很简洁,相对于其它一些方案,mem0 的 API 更容易理解。下面就是一个例子:
from mem0 import Memory
config = {
"version": "v1.1",
"llm": {
"provider": "openai",
"config": {
"model": "gpt-4o-mini",
"temperature": 0,
"max_tokens": 1500,
}
},
"embedder": {
"provider": "openai",
"config": {
"model": "text-embedding-ada-002"
}
},
"vector_store": {
"provider": "chroma",
"config": {
"collection_name": "mem0db",
"path": "mem0db",
}
},
"history_db_path": "history.db",
}
m = Memory.from_config(config)
m.add("我喜欢读书", user_id="dreamhead", metadata={"category": "hobbies"})
m.add("我喜欢编程", user_id="dreamhead", metadata={"category": "hobbies"})
related_memories = m.search(query="dreamhead有哪些爱好?", user_id="dreamhead")
print(' '.join([mem["memory"] for mem in related_memories['results']]))
抛开配置部分,这里我调用了 add
向 Memory 中添加了我的信息。然后,调用 search
查找相关的信息:
如果查看 mem0 的文档,你会发现它的 API 相当简单,无非是常见的增删改查。如果不是知道它的作用,我们甚至以为自己看到的是一个数据库的接口。这就是这个 API 设计好的地方:我们把长期记忆看作一个数据库,对长期记忆的处理相当于对数据库的访问,而复杂的细节隐藏在了简洁的接口之下。所以,从理解的角度看,它对我们几乎没有什么负担。
我们再来看配置。我们配置了大模型、Embedding 模型,还有向量数据库。对于长期记忆的搜索需要基于语义,所以,这里配置 Embedding 模型和向量数据库是很容易理解的。
但为什么还要配置大模型呢?因为 mem0 并不是把数据直接存到向量数据库里的。调用 add
时,mem0 会先把内容发送给大模型,让大模型从内容中提取出一些事实(fact),真正存放到向量数据库里的实际上是这些事实。
使用 mem0 实现长期记忆
到这里,你已经对 mem0 有了一个初步的印象,那怎样使用 mem0 实现长期记忆呢?接下来,我们就结合具体的代码,看看在一个大模型应用中可以怎样使用 mem0。有一点需要说明的是,目前 mem0 并没有提供一个专门的 LangChain 集成,下面的代码只能说是利用了 LangChain 的一些基础抽象完成:
# mem0 配置如上例所示
mem0 = Memory.from_config(config)
llm = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_messages([
("system", """"你现在扮演孔子的角色,尽量按照孔子的风格回复,不要出现‘子曰’。
利用提供的上下文进行个性化回复,并记住用户的偏好和以往的交互行为。
上下文:{context}"""),
("user", "{input}")
])
chain = prompt | llm
def retrieve_context(query: str, user_id: str) -> str:
memories = mem0.search(query, user_id=user_id)
return ' '.join([mem["memory"] for mem in memories['results']])
def save_interaction(user_id: str, user_input: str, assistant_response: str):
interaction = [
{
"role": "user",
"content": user_input
},
{
"role": "assistant",
"content": assistant_response
}
]
mem0.add(interaction, user_id=user_id)
def invoke(user_input: str, user_id: str) -> str:
context = retrieve_context(user_input, user_id)
response = chain.invoke({
"context": context,
"input": user_input
})
content = response.content
save_interaction(user_id, user_input, content)
return content
user_id = "dreamhead"
while True:
user_input = input("You:> ")
if user_input.lower() == 'exit':
break
response = invoke(user_input, user_id)
print(response)
前面我们已经了解过 LangChain 和 mem0 的基本用法,所以,这段代码看起来就非常容易理解了。这段代码的关键就是在 invoke
里:调用大模型前先取得相关的上下文信息,调用大模型之后,再把聊天历史存到 mem0 里。
下面是我的一次调用结果,这里因为用了上个例子的配置,所以,它对我的喜好也有所了解:
You:> 我是 dreamhead
梦之所向,心之所归。读书之乐,编程之妙,皆可助君在追求智慧的道路上更进一步。愿汝在书海与代码间,找到自我,探索更深的真理。若有疑问,或欲交流,尽可畅所欲言。
You:> 你知道我的爱好吗?
读书与编程,乃是心灵与智慧的结合。书中自有黄金屋,编程则是创造未来的钥匙。汝若潜心于此,定能在知识的海洋中遨游,收获丰硕之果。常思,常学,方能明理。
结合这段代码,我们就能理解 mem0 是怎样做长期记忆的。在会话过程中,我们只要把会话历史交给 mem0,包括用户的提问和大模型的回答,mem0 可以从这些内容中提取出相关的事实,存放到向量数据库。
在下一次对话时,我们会先根据用户消息在向量数据库里搜索,找到所需的上下文,拼装成一个完整的消息发给大模型。因为这里采用了向量数据库,能够存放的信息趋近于无限,我们与大模型之间会话的核心内容就都得到了记录,这样就实现了长期记忆的效果。
理解了 mem0 是怎样工作的,你会发现,有了 mem0 实现的长期记忆,我们似乎就不再需要短期记忆了。因为我们会在拼装消息时,把相关上下文中从长期记忆中找出来。
再进一步,如果我们不只是把聊天历史放到 mem0 里,而是把我们的一些业务资料也放到 mem0,它就可以起到 RAG 的效果。所以,你现在应该明白了,mem0 要做的不只是一个长期记忆的组件,而是要做一个统一的记忆层解决方案,包括各种业务信息。虽然它的野心不小,但真的要用它替代 RAG,还需要大量工程方面的工作去完成,毕竟,现在已经有了不少更完整的 RAG 方案。
说了这么多 mem0 的优点,如果你真的选型时考虑它,也需要知道它的一些问题。作为一个起步时间不长的项目,它尚在剧烈的开发过程之中,变动会比较大,比如,在 1.1 版本中,mem0 引入了对图(Graph)的支持,发掘事物之间的关系。目前的 mem0 实现在每次添加信息时,都会调用大模型,这也就意味着成本的增加,这也是我们在选型时必须要考虑的。
另外,mem0 在细节上也有很多问题,比如,存放聊天历史时,除了向量数据库,mem0 还会把聊天历史存到关系数据库里,目前这个方案只支持了 SQLite;代码里还有一些监控的代码,会把一些操作的内容上报到一个云平台等等。当然,这些问题是在我写下课程的时候存在,如果你发现这些问题并不存在,那就说明 mem0 对此做了修改。
总结时刻
这一讲,我们讲了记忆,尤其是长期记忆,这是在大模型应用开发中的一个重要组成部分。有了记忆,大模型就有了更多的上下文,能够更好地为用户服务。
记忆包括短期记忆和长期记忆,短期记忆的实现方案通常是将聊天历史放到提示词里。由于大模型上下文的限制,我们无法把所有的聊天历史都放到提示词中,所以就有了长期记忆的需求。长期记忆常见的解决方案是借鉴 RAG 的方案来实现,将一些信息存放到向量数据库中。
我介绍了一种长期记忆的实现方案——mem0,这个开源项目的 API 比较简洁,容易理解。这个项目本身还是比较有野心的,它希望可以统一长期记忆和短期记忆,甚至可以成为 RAG 的替代品。
如果今天的内容你只能记住一件事,那请记住,拥有长期记忆的大模型应用可以更好地服务用户。
思考题
这一讲的重点是长期记忆,你可以搜索其它的长期记忆解决方案,看看不同的实现之间有什么异同,欢迎在留言区分享你的所得。
- gevin 👍(2) 💬(1)
感谢郑老师给开拓了视野,之前只知道用langchain去处理短期记忆
2024-12-04 - kergee 👍(0) 💬(1)
其实吧就是把历史记录提炼之后保存到向量数据库里,是吧,随着不停的使用还会不停的更新是吧
2025-02-15 - 张申傲 👍(1) 💬(0)
第15讲打卡~
2025-01-24 - 范 👍(1) 💬(0)
类似于cursor的AI编程工具,针对整个项目的代码,应该是RAG,不是长期记忆的方式吧。
2024-12-24