课程由 datawhale 组织成员编纂,发布在 github,并通过组队学习的方式进行学习。主要目的为给部分开发者介绍大语言模型开发的背景知识并提供相关实操过程。课程地址:https://datawhalechina.github.io/llm-universe

个人背景:有一定编程经验,使用过部分大语言模型进行日常问题解答

个人期望:能够通过课程真正实操相关开发,完成简单的知识库问答系统构建

大语言模型简介

该部分主要为大语言模型相关的一些背景介绍。从当前回看,其实大语言很早就有相关的论文及产品化实现, 但是 ChatGPT 真正将其推向终端用户,引爆了整个 AI 生态圈的热潮,其后很多开源或者闭源的大语言模型如雨后春笋般涌现,相关应用建设、商业投资等也纷至沓来,涌现了很多机会。

找到一个 Awesome 系列的 LLM 汇总仓库,https://github.com/Hannibal046/Awesome-LLM

Awesome LLM

该项目从里程碑论文、开源 LLM、LLM 训练、学习等各个层面介绍了 LLM 的相关资源,后面可以日常关注并学习里面的各项内容。

大语言模型的能力和特点,个人感觉其涌现能力导致了大众的热情,实际相关理论还待研究。

LLM ability and features

部分人认为当前的 LLM 激发了对 AGI 的探索,可能会加速 AGI 的到来。但我个人认为 AGI 可能不是当前这种模型形式,最近了解了一些哥德尔机相关的研究,我个人对 LLM 进化到 AGI 这条线路持怀疑态度。今年 nature 的一篇浅脑理论的论文也许会指向不一样的路径(个人能力有限,对相关领域只能看到表面)。

RAG

检索增强生成(RAG, Retrieval-Augmented Generation)。应该是目前在内容问答等领域比较通用的处理模式,核心逻辑为通过预处理及 embedding 将数据汇聚到向量数据库中,使用向量搜索解决 LLM 当前的一些缺陷,最后将搜索的知识交由 LLM 进行汇总生产更为精准的答案。

课程中指出了 LLM 目前的一些限制:

  • 信息偏差/幻觉
  • 知识更新滞后性
  • 内容不可追溯
  • 领域专业知识能力欠缺
  • 推理能力限制
  • 应用场景适应性受限
  • 长文本处理能力较弱

作为企业开发者,之前曾想通过 LLM 结合飞书机器人实现企业内部的知识库问答,LLM 本身能调整的较少, 而相关知识库的预处理、向量化以及检索等,可能是更需要关注的点。课程中介绍了 RAG 的四个阶段, 个人对其中“增强阶段”目前不甚理解,后续会重点学习与关注。

langchain

langchain 必然是去年开源界的当红炸子鸡,核心组建为

  • Model I/O - LLM 的输入输出
    • Prompts
    • Chat models
    • LLMs
  • Retrieval - 应用数据交互,如 RAG 相关数据
    • Document loaders
    • Text splitters
    • Embedding models
    • Vectorstores
    • Retrievers
  • Composition - 高阶组件,将其他组建组合封装
    • Tools
    • Agents
    • Chains
  • Additional
    • Memory
    • Callbacks

之前检索过部分 langchain 源代码,主要涉及其中 ReAct 部分的处理,感觉其核心还是提示词处理加上结果解析,整体是个类似多轮问答的形式。有篇不错的博客详细介绍了 langchain 在该过程中的处理流程:https://tsmatz.wordpress.com/2023/03/07/react-with-openai-gpt-and-langchain/ .

langchain ReAct

整体项目流程

从图看比较明确,我会重点学习其中文本分割、embedding 等内容

langchain LLM

开发环境

我的工作机器是 Ubuntu,本次课程我使用本地 linux 开发+云服务器部署的模式,本地环境:

pyenv virtualenv llm
pyenv activate llm
pip install -r requirements.txt

Jupyter 环境为我之前创建的 jupyterhub 环境 (之前还写了博客介绍了搭建过程https://tomo.dev/posts/deploy-jupyterhub-for-team/ ),大语言模型可能想尝试下之前申请的 gemini 新出的 1.5 pro 版本,之前使用 1.0 版本进行一些文本翻译工作,效果还行, 但是不知道实际与课程涉及的知识库内容是否水土不服,不排除后续调整到其他 LLM API。

ENV - jupyter

使用 LLM API 开发应用

本章节主要介绍提供 LLM API 的服务及申请方式,这部分较为基础,之前已经申请各种平台的 API 资质。另外较为核心的就是 Prompt Engineer,及提示词工程。

Prompt Engineer 随着大语言模型的发展,更广泛地为人所熟知。LLM 有人称其为 AI 时代的操作系统, 而提示词则是这个操作系统的输入,掌握提示词对我们更好使用 LLM 有着不可或缺的作用。

LLM Prompt

提示词工程师也成为一个新兴的职业(不过可能博眼球的目的更多些)

Prompt Engineer

这边介绍一个更详细的关于提示词工程的站点https://www.promptingguide.ai/ 。提示词工程有多种形式以及其背后的原理,主要的提示词编写方式有如下几种:

  • Zero-shot Prompting
  • Few-shot Prompting
  • Chain-of-Thought Prompting
  • Self-Consistency
  • Generate Knowledge Prompting
  • Prompt Chaining
  • Tree of Thoughts
  • Retrieval Augmented Generation
  • Automatic Reasoning and Tool-use
  • Automatic Prompt Engineer
  • Active-Prompt
  • Directional Stimulus Prompting
  • Program-Aided Language Models
  • ReAct
  • Reflexion
  • Multimodal CoT
  • Graph Prompting

下面是我之前在处理我的博客 markdown 文件国际化翻译的提示词模板:

prompt_template = '''You are a translator and your task is to translate l10n file to different languages.
The l10n file is provided in TOML format. The file contains {{ KEY }} for variables and use
`one` for singular and `other` for plural.

The TOML file is quoted in triple backtick. Please translate the content to {lang}
and keep the original content structure, also remove triple backtick in output:

```toml
{en_file_content}
```
'''

类似的,LangChain 中也提供了相关提示词的模板示例,如 ReAct 形式的 wiki 提示词模板可以在 langchain 的源码中找到langchain/libs/langchain/langchain/agents/react/wiki_prompt.py at 3f156e0ece3cb8acb50dfe8013a581892a

# flake8: noqa
from langchain_core.prompts.prompt import PromptTemplate

EXAMPLES = [
    """Question: What is the elevation range for the area that the eastern sector of the Colorado orogeny extends into?
Thought: I need to search Colorado orogeny, find the area that the eastern sector of the Colorado orogeny extends into, then find the elevation range of the area.
Action: Search[Colorado orogeny]
Observation: The Colorado orogeny was an episode of mountain building (an orogeny) in Colorado and surrounding areas.
Thought: It does not mention the eastern sector. So I need to look up eastern sector.
Action: Lookup[eastern sector]
Observation: (Result 1 / 1) The eastern sector extends into the High Plains and is called the Central Plains orogeny.
Thought: The eastern sector of Colorado orogeny extends into the High Plains. So I need to search High Plains and find its elevation range.
Action: Search[High Plains]
Observation: High Plains refers to one of two distinct land regions
Thought: I need to instead search High Plains (United States).
Action: Search[High Plains (United States)]
Observation: The High Plains are a subregion of the Great Plains. From east to west, the High Plains rise in elevation from around 1,800 to 7,000 ft (550 to 2,130 m).[3]
Thought: High Plains rise in elevation from around 1,800 to 7,000 ft, so the answer is 1,800 to 7,000 ft.
Action: Finish[1,800 to 7,000 ft]""",
    ...
]
SUFFIX = """\nQuestion: {input}
{agent_scratchpad}"""

WIKI_PROMPT = PromptTemplate.from_examples(
    EXAMPLES, SUFFIX, ["input", "agent_scratchpad"]
)

可以看到,ReAct 提示词形式为一种思维链 Chain-of-thought (CoT),并在 Action 阶段可以引用外部的工具,来解决事实幻觉和错误传播等问题,整个过程有下面四个命令:

  • Question - 问题
  • Thought - 对于问题的思考及细化
  • Action - 解决问题需要采取的行动
  • Observation - 观察问题有没有被解决

且上述的四个阶段会有一定循环,在 Action 未给出 Finish[yes]结果时,Thought Action Observation 的过程可能会执行多次。Langchain 会在过程中不断解析输出,提取其中的内容,调用外部工具并将数据拼接到原始的提示词之后再次调用大语言模型。

当然在日常使用中,提示词是需要迭代的。在使用符合一些规范的提示词模板时,可能仍然不能达到我们的预期结果,这时候就需要结合结果及我们的预期对提示词进行调整,课程也提供了一个流程图:

prompt engineering

搭建知识库

基本概念

本章节重点介绍词向量与向量数据库的概念,以及通过 embedding API 的使用,将文本进行向量化并存储至向量数据库中,为后续 RAG 的应用构建基础的向量知识库。

其实向量搜索有着比较久的历史,之前以图搜图其实也是一种向量搜索,最简单的余弦相似度中的可以认为是高维空间中的向量夹角(后文得知教程中 Chroma 相似度检测使用的就是余弦相似度)。

$$ Cosine(x,y) = \frac{x \cdot y}{|x||y|} $$

对于文本的处理,由于无法直接处理文本字符串,需要将文本转化成数值型数据,由此引出了 Word Embedding(词嵌入)的概念。而 2013 年出现的 Word2Vec,使自然语言处理领域各项任务效果均得到极大提升。

而关于向量数据库,其实在大模型为大众熟知前已经有一定的研究,但是大模型的应用结合相关 RAG 架构的发展,使得向量数据库也迎来了技术和资本的追逐。

NameLicense
Apache Cassandra [9] [10]Apache License 2.0
Chroma[11] [12]Apache License 2.0 [13]
Azure Cosmos DB Integrated Vector Database [14]Proprietary (Managed Service)
Couchbase [15] [16]BSL 1.1 [17]
Elasticsearch [18]Server Side Public License , Elastic License[19]
Lantern[20]BSL 1.1 [21]
LlamaIndex[22]MIT License [23]
Milvus[24] [25]Apache License 2.0
MongoDB Atlas[26]Server Side Public License (Managed service)
OpenSearch [27] [28] [29]Apache License 2.0 [30]
Pinecone[31]Proprietary (Managed Service)
Postgres with pgvector[32]PostgreSQL License[33]
Qdrant[34]Apache License 2.0 [35]
Redis Stack[36] [37]Redis Source Available License [38]
SurrealDB [39]BSL 1.1 [40]
Vespa[41]Apache License 2.0 [42]
Weaviate[43]BSD 3-Clause [44]

https://en.wikipedia.org/wiki/Vector_database 上有介绍当前主流的向量数据库,其中可以看到原生以向量数据库为目的构建的数据库,如 Milvus,也有 NoSQL 如 ElasticSearch/MongoDB 或关系型数据库如 PostgreSQL 插件,在原有数据库功能的基础上提供向量服务的。

API 测试

之前通过 Google AI Studio 申请的申请的 API key (申请地址:https://aistudio.google.com/app/apikey ),通过下面的语句可以获取支持的 embedding 模型:

for m in genai.list_models():
  if 'embedContent' in m.supported_generation_methods:
    print(m.name)

# output:
# models/embedding-001
# models/text-embedding-004

实际结果与官方文档存在一定出入https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/text-embeddings

textembedding-gecko modelRelease dateDiscontinuation date
textembedding-gecko@003December 12, 2023Not applicable
textembedding-gecko@002November 2, 2023October 9, 2024
textembedding-gecko-multilingual@001November 2, 2023Not applicable
textembedding-gecko@001June 7, 2023October 9, 2024
text-embedding-preview-0409April 9, 2024To be updated to a stable version.
text-multilingual-embedding-preview-0409April 9, 2024To be updated to a stable version.

Anyway, 通过调用接口,尝试是否能够生成 embedding 结果:

title = "The next generation of AI for developers and Google Workspace"
sample_text = ("Title: The next generation of AI for developers and Google Workspace"
    "\n"
    "Full article:\n"
    "\n"
    "Gemini API & Google AI Studio: An approachable way to explore and prototype with generative AI applications")

model = 'models/text-embedding-004'
embedding = genai.embed_content(model=model,
                                content=sample_text,
                                task_type="retrieval_document",
                                title=title)

print(embedding)

上面的语句会输出一段浮点数,类似

{'embedding': [-0.0021609126, -0.003164448, -0.060120765, ...]}

整体说明路径是通的。

数据处理

在这一小节中,我们主要尝试对 PDF、markdown 等文本数据进行数据处理,这里我们使用的是 datawhale 其他项目的文件作为知识库演示:

在 jupyter 中运行示例代码,可以看到与教程一致的输出

doc loader

接下来的处理用到了一些正则替换

pattern = re.compile(r'[^\u4e00-\u9fff](\n)[^\u4e00-\u9fff]', re.DOTALL)

其中\u4e00-\u9fff应该是匹配中文字符用,^将中文排除,(\n)匹配换行符并进行 group 操作,re.DOTALL则是为了匹配包括换行符在内的所有字符(默认不匹配换行符)。

做了这部分处理后,之前一些非预期的换行符已经被重新去除,测试代码及输出如下:

import re
pattern = re.compile(r'[^\u4e00-\u9fff](\n)[^\u4e00-\u9fff]', re.DOTALL)

page_content = '''前言
“周志华老师的《机器学习》
(西瓜书)是机器学习领域的经典入门课程之一,周老师为了使尽可能多的读
者通过西瓜书对机器学习有所了解, 所以在书中对部分公式的推导细节没有详述,但是这对那些想深究公式推
导细节的读者来说可能“不太友好”
,本书旨在对西瓜书里比较难理解的公式加以解析,以及对部分公式补充
具体的推导细节。
”'''
print(re.sub(pattern, lambda match: match.group(0).replace('\n', ''), page_content))

doc process

Markdown 文档也做了类似处理。

在做了基础的数据清洗后,需要将文档导入 langchain 中进行分割处理,来解决上下文过长导致大模型处理问题(虽然目前部分大模型能够处理一本书大小的上下文,但是从效率和性价比角度考虑,预处理及分割仍然是值得做的)。

文本分割主要流程如下:

  1. 将文本拆分为语义上有意义的小块(通常是句子)。
  2. 开始将这些小块组合成一个更大的块,直到达到一定大小(由某些函数测量)。
  3. 达到该大小后,将该块设为自己的文本片段,然后开始创建一个具有一些重叠的新文本块(以保持块之间的上下文)。

langchain 的分割有两个参数:chunk_size (块大小)和 chunk_overlap (块与块之间的重叠大小)

langchain text split

langchain 官方介绍了相关内置的文本分割类型:

NameSplits OnAdds MetadataDescription
RecursiveA list of user defined charactersRecursively splits text. Splitting text recursively serves the purpose of trying to keep related pieces of text next to each other. This is the recommended way to start splitting text.
HTMLHTML specific charactersSplits text based on HTML-specific characters. Notably, this adds in relevant information about where that chunk came from (based on the HTML)
MarkdownMarkdown specific charactersSplits text based on Markdown-specific characters. Notably, this adds in relevant information about where that chunk came from (based on the Markdown)
CodeCode (Python, JS) specific charactersSplits text based on characters specific to coding languages. 15 different languages are available to choose from.
TokenTokensSplits text on tokens. There exist a few different ways to measure tokens.
CharacterA user defined characterSplits text based on a user defined character. One of the simpler methods.
[Experimental] Semantic ChunkerSentencesFirst splits on sentences. Then combines ones next to each other if they are semantically similar enough. Taken from Greg Kamradt
AI21 Semantic Text SplitterSemanticsIdentifies distinct topics that form coherent pieces of text and splits along those.

课程中演示了 RecursiveCharacterTextSplitter 的效果,官方的链接为:https://python.langchain.com/docs/modules/data_connection/document_transformers/recursive_text_splitter/

加载至向量数据库

参考课程中的处理

import os
file_paths = []
folder_path = './llm-universe/data_base/knowledge_db'
for root, dirs, files in os.walk(folder_path):
    for file in files:
        file_path = os.path.join(root, file)
        file_paths.append(file_path)

from langchain.document_loaders.pdf import PyMuPDFLoader
from langchain.document_loaders.markdown import UnstructuredMarkdownLoader

# 遍历文件路径并把实例化的loader存放在loaders里
loaders = []

for file_path in file_paths:
    _, file_type = os.path.splitext(file_path)  # os.path.splitext是更pythonic的写法
    if file_type == '.pdf':
        loaders.append(PyMuPDFLoader(file_path))
    elif file_type == '.md':
        loaders.append(UnstructuredMarkdownLoader(file_path))

我使用langchain-google-genai处理embedding部分:

!pip install --quiet langchain-google-genai
from langchain.vectorstores.chroma import Chroma
from langchain_google_genai import GoogleGenerativeAIEmbeddings
gemini_embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")

vectordb = Chroma.from_documents(
                     documents=split_docs[:20],      # Data
                     embedding=gemini_embeddings,    # Embedding model
                     persist_directory="./chroma_db" # Directory to save data
                     )

vectordb.persist()

运行persist()后可以看到创建了数据库文件夹:

VectorDB persist

测试向量搜索:

question="什么是大语言模型"
sim_docs = vectordb.similarity_search(question,k=3)
print(f"检索到的内容数:{len(sim_docs)}")
for i, sim_doc in enumerate(sim_docs):
    print(f"检索到的第{i}个内容: \n{sim_doc.page_content[:200]}", end="\n--------------\n")

可以看到返回的内容与课程有一定差异,具体待后续观测

VectorDB search

总结

该章节介绍了大模型开发中重要的数据处理和向量存储部分,整体内容偏实战处理,课程中介绍了不同的大模型 embedding 接口和文本分割方案。在实际生产中可以灵活整合运用。在其他项目的实验中, 事实证明对于文本的预处理能够极大优化检索性能与正确率,该部分应该是整个过程中最重要的部分, 实际运用过程中需要基于知识库内容及特点,灵活运用。

构建 RAG 应用

本章节主要基于 LangChain 及之前介绍的向量数据库,构建一个 RAG 问答应用,并使用 streamlit 将该应用部署到网络进行在线访问。

将 LLM 接入 LangChain

LangChain 提供了基类用来创建与各个大模型的对接https://api.python.langchain.com/en/latest/language_models/langchain_core.language_models.chat_models.BaseChatModel.html

官方提供了绝大部分市面上的 LLM 的集成,参考https://python.langchain.com/docs/integrations/llms/

作为 Gemini 模型,我这边尝试的是 langchain-google-genai。前文在 embedding 处理过程中已经安装和测试过该 Python 库,所以跳过安装过程。

from langchain_google_genai import ChatGoogleGenerativeAI
llm = ChatGoogleGenerativeAI(model="gemini-pro", temperature=0.1, top_p=0.85)
llm.invoke("Who are you?")

直接通过简单的提示词进行 invoke 会输出如下内容。

langchain LLM invoke

这部分目前测试 gemini-pro 是可以正常输出,但是最新的 gemini-1.5-pro-latest 却报错,需要后续进行定位排查。

接下来我们需要测试和定制我们的提示词模板,课程示例中给的是翻译的例子。

from langchain.prompts.chat import ChatPromptTemplate

template = "你是一个翻译助手,可以帮助我将 {input_language} 翻译成 {output_language}."
human_template = "{text}"

chat_prompt = ChatPromptTemplate.from_messages([
    ("system", template),
    ("human", human_template),
])

text = "我带着比身体重的行李,\
游入尼罗河底,\
经过几道闪电 看到一堆光圈,\
不确定是不是这里。\
"
messages  = chat_prompt.format_messages(input_language="中文", output_language="英文", text=text)
print(messages)
llm.invoke(messages)

与群中的同学问题一致,这段代码没有返回预期的结果。

langchain test prompt

通过搜索发现是 Google 的 Gemini 模型不支持 SystemMessage。

param convert_system_message_to_human: bool = False Whether to merge any leading SystemMessage into the following HumanMessage. Gemini does not support system messages; any unsupported messages will raise an error.

我们需要重新定义 LLM 并将convert_system_message_to_human参数设置为True, 或者直接使用PromptTemplate

from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser

template = """你是一个翻译助手,可以帮助我将 {input_language} 翻译成 {output_language}.
{text}"""

prompt = PromptTemplate.from_template(template)
chain = prompt | llm | StrOutputParser()
chain.invoke({"input_language":"中文", "output_language":"英文","text": text})

langchain test prompt success

经过上面的调整,输出了预期的结果。

构建检索问答链

经过尝试,gemini-pro模型在后续处理中依旧报错,换成最新的gemini-1.5-pro-latest模型。

llm = ChatGoogleGenerativeAI(model="gemini-1.5-pro-latest",
                 temperature=0.1, top_p=0.85)

通过ConversationalRetrievalChain以及memory,可以让问答具有连续对话的能力。

qa = ConversationalRetrievalChain.from_llm(
    llm,
    retriever=retriever,
    memory=memory
)

测试结果,可以看到第二次的回答能够明白“这门课”指代的是第一问中的“提示词工程”

RAG QA test

同时测试了智谱的 LLM,回答更精简些。

RAG QA test for zhipu

最后,基于课程提供的streamlit脚本,适配ZhipuAILLM,结果展示如下:

RAG streamlit

总结

该章节基本算是前序内容的串联,通过将前序章节的文本处理、向量数据库的应用,配合 LangChain+LLM, 构成了完整的 RAG 应用,通过调用能够索引向量数据库的内容并经由 LLM 汇总输出,完成了 RAG 应用的雏形。

系统评估与优化

由于大语言模型的回答有较多的不确定性,所以系统构建后的迭代优化是非常重要的过程。本章节主要对构建后的 RAG 问答系统做后续优化,涉及评判的指标选定,迭代思路等,从检索、生成的维度分别进行阐述。

RAG evaluation and optimization

在系统构建的早期,我们可以很快通过观察一些 Bad Case 来定位问题并进行优化。据说在 Google 搜索引擎早期,工程师也经常分析 Bad Case 来优化搜索引擎的排序算法。

在规模大到一定程度后,则可以引入自动评估机制。其中我之前一直对 NLP 中模型结果评估很好奇, 通过课程知道其实可以针对性构造标准答案并计算相似度来实现自动化评估。

LLM 应用精选案例

本章节主要介绍两个大语言模型相关的 RAG 项目。本人有幸参与过其中 tianji 的官网开发,但未参与其中数据处理部分,但实际在日常交流中,这部分数据处理其实是更重要的部分,包括最初的物料准备, 数据处理等,以及基于整理好的文本物料进行大模型微调(InternLM2)。同时也参考了类似哄哄模拟器的交互逻辑设计 prompt。

但这部分需要实际跟随项目并手动调试,才能有比较深刻的体会,暂时精力有限,后续我期望能够打造自己专属的知识库系统。