这回我们来介绍Memory管理机制,学一下其中的Prompt,调用流程,还有设计,尝试理解一下LLM工程的思想。
什么?之前不是讲了吗?不不,那个是history,Memory和history是完全不一样的东西,我实现的Summary的摘要记忆其实只是接下来要说的东西的一个雏形。
我们请chatgpt-o1来说明一下两者的区别:
总结一下!:
在大部分现代 LLM 应用场景中,Memory 的设计和使用逐渐取代了单纯的 History,特别是在长对话或高效上下文管理的需求下。
之前学到的history并不会真的在实际项目里经常用到,但是也还是有一些地方会用:
短期会话或不需要额外处理的场景中尤为明显。
在现代 LLM 工程中:
- Memory 是主流:它通过各种机制(如摘要、关键词提取)实现了对上下文的高效管理。
- History 辅助:在当前会话内用于临时存储,并为 Memory 提供原始输入。
Memory
Memory这个单词就顾名思义,就是希望能够像人类一样在不断的对话里去汇总有用的信息,而不是暴力的记忆完整的信息。
LangchainJS目前是个快速迭代发展的框架,之前我反复强调了它的一个开发范式LCEL
,非常遗憾的是Memory现在还处于官方的beta阶段,没有对LCEL做出完善的兼容。
这部分的内容要持续关注官方什么时候把Memory脱离beta阶段,到时候成熟的方法可以重新更新。
所以这回我只能先用Langchain内置的ConversationChain来介绍一下Memory,在后面讲一下怎么把Memory用在LCEL的开发范式里。
温习一下LCEL范式的思想:LLM APP开发的每一个节点,都应该抽象成Runnable节点,通过一系列的工具函数来协助我们组合成自己的Chain,尽量模块化,Langchain内部也更容易对每个Runnable根据依赖关系来做并行化。
ConversationChain
并不是LCEL范式,是高度封装的Chain,我们基本没法对其进行修改,自由度也就大大降低了。所以我才说LCEL才是应该全面拥抱的对象,以方便我们进行更多的客制化
来用ConversationChain
试试吧:
import { ChatOpenAI } from "@langchain/openai";
import { BufferMemory } from "langchain/memory"
import { ConversationChain } from "langchain/chains"
const chatModel = new ChatOpenAI({
configuration: {
baseURL: process.env.OPENAI_API_URL,
},
modelName: process.env.OPENAI_MODEL
})
const memory = new BufferMemory()
const chain = new ConversationChain({
llm: chatModel,
memory
})
const res1 = await chain.call({input: "你好,我是八六"})
res1;
可以看到,ConversationChain
用起来还是很方便,加上debug我们来看看内部发生了什么?
const chain = new ConversationChain({
llm: chatModel,
memory,
verbose: true
})
我们把kwargs的content内容取出来:
The following is a friendly conversation between a human and an AI. The AI is talkative and provides lots of specific details from its context. If the AI does not know the answer to a question, it truthfully says it does not know.
Current conversation:
Human: 你好,我是八六
AI: 你好,八六!很高兴认识你!我是一个人工智能,随时准备和你聊天。你最近过得怎么样?或者有什么想聊的话题吗?
Human: 你好,你知道我的名字吗?
AI:
所以,其实就是在运行的时候,ConversationChain会自动传入一个history的属性,是字符串化之后的chat history
[1:chain:ConversationChain] Entering Chain run with input: {
"input": "你好,你知道我的名字吗?",
"history": "Human: 你好,我是八六
AI: 你好,八六!很高兴认识你!我是一个人工智能,随时准备和你聊天。你最近过得怎么样?或者有什么想聊的话题吗?"
}
在调用LLM的时候传入的Prompt就是上面我提取出来的content👆🏻。
这些步骤基本上跟我们之前自己实现的记忆对话的Chain是一样的,这里的Prompt是可以自定义的,但是问题在于,如果想要加入更多的复杂机制,比如跟我们之前实现的Summary那样,就很困难,因为执行的过程并不能嵌入自己的处理函数,没有暴露自定义接口的属性。
我认为Langchain的LCEL的初衷就是希望成为最基础的LLM APP开发框架,提供基础的开发范式和工具,并且定义标准方便社区的工具用模块化的形式加入到LCEL Chain里。
还是那句:全面拥抱LCEL🤗
回到Memory,我们来看看Langchain里面几个比较常用的Memory,更多的关键是关注思维,Langchain官方提供了哪些去管理Memory的机制,有哪些比较有趣,可以为以后自定义Memory作为参考?
BufferWindowMemory
import { BufferWindowMemory } from "langchain/memory";
const memory = new BufferWindowMemory({ k: 1 })
const bufferWindowChain = new ConversationChain({
llm: chatModel,
memory,
verbose: true
})
await bufferWindowChain.call({input: "你好,我是八六"})
这非常好理解,对聊天记录添加了一个滑动窗口,只会记忆K个对话,k个对话之后,最开始的那个对话就会忘记掉了。这是很基础的记忆机制,不多说。
ConversationSummaryMemory
在之前,我们实现了一个随着聊天不断生成和更新对话摘要的chat bot,而Langchain本身也提供了一个类似的工具:ConversationSummaryMemory
import { ConversationSummaryMemory } from "langchain/memory"
import { PromptTemplate } from "@langchain/core/prompts"
import { ConversationChain } from "langchain/chains"
const memory = new ConversationSummaryMemory({
memoryKey: "summary",
llm: chatModel,
})
const prompt = PromptTemplate.fromTemplate(`
你是一个乐于助人的助手。尽你所能地回答所有的问题。
这是聊天记录摘要:
{summary}
Human: {input}
AI:`)
const chain = new ConversationChain({ llm: chatModel, memory, prompt, verbose: true })
const res1 = await chain.call({ input:"我的名字是八六" })
console.log(res1)
const res2 = await chain.call({ input:"我的名字是?" })
console.log(res2)
我们启动Verbose模式来看看内部发生了什么事情,实际上和我们之前的实现是很类似的,ConversationSummaryMemory
使用LLM渐变式地总结聊天记录,来生成Summary。
Progressively summarize the lines of conversation provided, adding onto the previous summary returning a new summary.
EXAMPLE
Current summary:
The human asks what the AI thinks of artificial intelligence. The AI thinks artificial intelligence is a force for good.
New lines of conversation:
Human: Why do you think artificial intelligence is a force for good?
AI: Because artificial intelligence will help humans reach their full potential.
New summary:
The human asks what the AI thinks of artificial intelligence. The AI thinks artificial intelligence is a force for good because it will help humans reach their full potential.
END OF EXAMPLE
Current summary:
New lines of conversation:
Human: 我的名字是八六
AI: 您好,八六!很高兴认识您。有什么我可以帮助您的吗?
New summary:
然后新的Summary就是:
The human introduces themselves as 八六. The AI responds warmly, expressing pleasure in meeting 八六 and asking how it can assist them.
紧接着,在下次对话的时候,这一次新生成的Summary已经添加了:
和我们的实现基本一样,Langchain为了提升Summary的效果,会在Prompt里面嵌入一些example来保证LLM理解我们的目的,生成的效果也会更好一点。
在我们后面设计Prompt的时候可以学习一下这种写法
那么,自然而然的,我们把BufferWindowMemory
和ConversationSummaryMemory
组合起来,根据token的数量,如果上下文过大就切换为Summary,上下文比较小就使用原始的聊天记录,这便成了ConversationSummaryBufferMemory
ConversationSummaryBufferMemory
import { ConversationSummaryBufferMemory } from "langchain/memory"
const memory = new ConversationSummaryBufferMemory({
llm: chatModel,
maxTokenLimit: 200
})
const chain = new ConversationChain({ llm: chatModel, memory, prompt, verbose: true })
这个原理跟前两个Memory基本上一样的,就是会计算当前完整聊天记录的token数量,去判断是否超过了我们设置的maxTokenLimit
,如果超了,那么就会总结为Summary输入。这就不深入了,因为和前面一模一样。
老实说,ConversationSummaryBufferMemory的设计有点暴力,因为它的思路就是短聊天用BufferWindowMemory
、长聊天就变成ConversationSummaryMemory
,没有太实际上的提升。
对于我来说,我认为更为合理的一种方案是:
每次对话的时候,带上前k次对话的原始内容+一直在持续更新的Summary,这样在长对话的时候也能让LLM记忆最近的对话+长期总结的Summary,这会是一种更好的选择。
这里可以TODO,记录一下我的想法,后续再来看看怎么实现
EntityMemory
在人类聊天的过程里,我实际是在建立对各种实体(Entity)的记忆,比如有两个刚刚认识的人,聊职业,聊兴趣,聊吃饭娱乐,我们记忆里,存储方式就是根据实体进行分类存储,拿我正在交流的这个人来说:
- 什么职业,什么年龄
- 兴趣是什么,和我的兴趣是否有交集
- 吃饭娱乐喜欢在什么场所
大概就分成这三种实体分类。
EntityMemory
就是希望模拟在聊天里生成和更新不同实体的过程。
这玩意儿还挺复杂的,要有很多次LLM调用。来看看代码吧:
import { EntityMemory, ENTITY_MEMORY_CONVERSATION_TEMPLATE } from "langchain/memory"
const memory = new EntityMemory({
llm: chatModel,
chatHistoryKey: "history",
entitiesKey: "entities",
})
const chain = new ConversationChain({ llm: chatModel, memory, prompt: ENTITY_MEMORY_CONVERSATION_TEMPLATE, verbose: true })
这其中。ENTITY_MEMORY_CONVERSATION_TEMPLATE
是Langchain提供的默认用于EntityMemory chat的Prompt模板,当然啦,我们也可以自己定义。
先来试试对话:
import { EntityMemory, ENTITY_MEMORY_CONVERSATION_TEMPLATE } from "langchain/memory"
const memory = new EntityMemory({
llm: chatModel,
chatHistoryKey: "history",
entitiesKey: "entities",
})
const chain = new ConversationChain({ llm: chatModel, memory, prompt: ENTITY_MEMORY_CONVERSATION_TEMPLATE, verbose: true })
const res1 = await chain.call({ input:"我的名字是八六,今年25岁" })
console.log(res1)
const res2 = await chain.call({ input:"米哈游是一家游戏公司,主要是制造动漫游戏的公司" })
console.log(res2)
打开Verbose,让我们来看看内部发生了什么~
首先我们会看到EntityMemory会使用LLM提取对话里出现的主体,具体的Prompt是:
You are an AI assistant reading the transcript of a conversation between an AI and a human. Extract all of the proper nouns from the last line of conversation. As a guideline, a proper noun is generally capitalized. You should definitely extract all names and places.
The conversation history is provided just in case of a coreference (e.g. \"What do you know about him\" where \"him\" is defined in a previous line) -- ignore items mentioned there that are not in the last line.
Return the output as a single comma-separated list, or NONE if there is nothing of note to return (e.g. the user is just issuing a greeting or having a simple conversation).
EXAMPLE
Conversation history:
Person #1: my name is Jacob. how's it going today?
AI: \"It's going great! How about you?\"
Person #1: good! busy working on Langchain. lots to do.
AI: \"That sounds like a lot of work! What kind of things are you doing to make Langchain better?\"
Last line:
Person #1: i'm trying to improve Langchain's interfaces, the UX, its integrations with various products the user might want ... a lot of stuff.
Output: Jacob,Langchain
END OF EXAMPLE
EXAMPLE
Conversation history:
Person #1: how's it going today?
AI: \"It's going great! How about you?\"
Person #1: good! busy working on Langchain. lots to do.
AI: \"That sounds like a lot of work! What kind of things are you doing to make Langchain better?\"
Last line:
Person #1: i'm trying to improve Langchain's interfaces, the UX, its integrations with various products the user might want ... a lot of stuff. I'm working with Person #2.
Output: Langchain, Person #2
END OF EXAMPLE
Conversation history (for reference only):
Human: 我的名字是八六,今年25岁
AI: 很高兴认识你,八六!你现在在做什么呢?或者你有什么想聊的话题?
Last line of conversation (for extraction):
Human: 米哈游是一家游戏公司,主要是制造动漫游戏的公司
Output:
好长的Prompt,来慢慢解析一下:
- 首先第一段去讲明了任务的背景,一个阅读对话记录,从最后一次对话中提取名词的AI,因为核心目标是英语,这里给出了一些提示,专有名词是大写的。强调了一定要提取所有的名词。这部分给出了任务、任务提示和要求
- 第二段和第三段,强调了历史聊天记录仅仅用于参考,再次强调了只提取最后一次对话里出现的专有名词,并且指定了多个专有名词的返回格式和没有任何专有名词的返回格式
- 然后给出了两个例子,第一个例子是普通的例子,主要是用例子更具像化地介绍任务。第二个例子是以Person #2 为例强化对名词的概念。
few-shot prompt
,也就是通过例子去强化LLM对任务的理解是常见且非常好用的技巧 - 最后在
Conversation history(for reference only)
里再次强化了chat history只是作为参考,last line of conversation(for extraction)
才是作为提取的目标这件事
那么,我们可以看到最后LLM提取到了关键的名词:
“米哈游”,并没有错误的提取到聊天历史的我的名字“八六”
之后就是正常的Conversation
让LLM对话的内容了
聊天之后,EntityMemory会提取出对实体的描述认知,Prompt是:
You are an AI assistant helping a human keep track of facts about relevant people, places, and concepts in their life. Update and add to the summary of the provided entity in the \"Entity\" section based on the last line of your conversation with the human. If you are writing the summary for the first time, return a single sentence.
The update should only include facts that are relayed in the last line of conversation about the provided entity, and should only contain facts about the provided entity.
If there is no new information about the provided entity or the information is not worth noting (not an important or relevant fact to remember long-term), output the exact string \"UNCHANGED\" below.
Full conversation history (for context):
Human: 我的名字是八六,今年25岁
AI: 很高兴认识你,八六!25岁正是一个充满活力和机会的年龄。你对未来有什么计划或者目标吗?
Human: 米哈游是一家游戏公司,主要是制造动漫游戏的公司
AI: 是的,米哈游是一家知名的游戏开发公司,以其精美的动画风格和引人入胜的游戏世界而闻名。它们的代表作包括《崩坏》系列和《原神》,后者在全球范围内获得了极大的关注和成功。你对米哈游的游戏有特别喜欢的作品吗?或者有什么想要讨论的方面?
Entity to summarize:
米哈游
Existing summary of 米哈游:
No current information known.
Last line of conversation:
Human: 米哈游是一家游戏公司,主要是制造动漫游戏的公司
Updated summary (or the exact string \"UNCHANGED\" if there is no new information about 米哈游 above):
这一部分的目的就是,根据本次用户对话提到的实体,也就是说上一个Prompt提取出来的实体,去更新用户提供的实体信息。
- 第一段强调了LLM的任务,记录有关实体的信息
- 第二段把范围控制在用户最新一条信息里,只包含跟目标实体有关系的内容
- 第三段就是指定如果没有更新或者更新并不值得长期记忆,就返回字符
UNCHANGED
- 后面就是提供聊天记录,需要记录的实体,当前记忆的实体信息,以及和用户的最后一条聊天记录
然后LLM就会返回和实体有关系的信息:
经过上面两次的沟通,我们再来问:
const res3 = await chain.call({ input:"介绍八六和米哈游" })
res3
EntityMemory
就会像上面一样,使用LLM提取实体的列表,然后返回这些实体的信息,以及聊天记录,传入到ConversationChain
的ENTITY_MEMORY_CONVERSATION_TEMPLATE
里,来看看Prompt:
You are an assistant to a human, powered by a large language model trained by OpenAI.
You are designed to be able to assist with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. As a language model, you are able to generate human-like text based on the input you receive, allowing you to engage in natural-sounding conversations and provide responses that are coherent and relevant to the topic at hand.
You are constantly learning and improving, and your capabilities are constantly evolving. You are able to process and understand large amounts of text, and can use this knowledge to provide accurate and informative responses to a wide range of questions. You have access to some personalized information provided by the human in the Context section below. Additionally, you are able to generate your own text based on the input you receive, allowing you to engage in discussions and provide explanations and descriptions on a wide range of topics.
Overall, you are a powerful tool that can help with a wide range of tasks and provide valuable insights and information on a wide range of topics. Whether the human needs help with a specific question or just wants to have a conversation about a particular topic, you are here to assist.
Context:
[object Object]
Current conversation:
Human: 我的名字是八六,今年25岁
AI: 很高兴认识你,八六!25岁正是一个充满活力和机会的年龄。你对未来有什么计划或者目标吗?
Human: 米哈游是一家游戏公司,主要是制造动漫游戏的公司
AI: 是的,米哈游是一家知名的游戏开发公司,以其精美的动画风格和引人入胜的游戏世界而闻名。它们的代表作包括《崩坏》系列和《原神》,后者在全球范围内获得了极大的关注和成功。你对米哈游的游戏有特别喜欢的作品吗?或者有什么想要讨论的方面?
Last line:
Human: 介绍八六和米哈游
You:
同样的,这几段Prompt的质量也很值得探讨,目标就是构建一个通用性的chat bot。我们的重点在于You have access to some personalized information provided by the human in the Context section below.
。这句话就是对LLM指定了在context部分提供了与上下文有关的背景信息,用来给LLM参考。
这里打印不出来context长啥样,但是我可以直接说明一下:Context 是由 LLM 处理和总结后的内容。类似于:八六:是一个25岁的人 米哈游:是一家动漫游戏公司
总结
所以说,Langchain为了实现对实体的记忆,在一次沟通中调用了很多次LLM进行知识的总结和提取,多个LLM的协同方式,流程,设计思路可以给未来我们制作LLM APP提供学习的参考。
同理,如何避免token花费?太简单了,我说了很多次了,本地大模型来做这些总结的工作,生成回复的时候使用GPT就行了。