langchain学习笔记

成为AI全栈工程师的路上

  1. 为什么叫langchain?

langchain中存在LCEL(langchain expression language),我们可以使用简单的操作连接各个组件,比如我们常用的LLMchain:
chain = prompt | model | parser

想要调用并得到模型的回答,只需要:chain.invoke({...}) ...代表参数,以键值对标识

prompt :

prompt_template, 里面可以包含占位符

from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt_template = ChatPromptTemplate.from_messages(
        [
            ("system", system_template),
            ("user", "hello"),
            MessagesPlaceholder(variable_name="messages"), # 这里的placeholder是用来传入更多的交互信息的,只需要在后面在字典中的messages的值传入
        ]

    )

model

接受prompt,返回补全和metadata

from langchain_openai import ChatOpenAI

model = ChatOpenAI(model="gpt-4-turbo")

parser解析器

接受model的结果,解析成结果字符串

from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()

流式输出

直接把.invoke变成.stream(),就会变成一个可迭代对象

    for r in with_message_history_chatbot.stream(
        {
            "messages": [HumanMessage(content=user_prompt)],  
        },
        config=config
    ):
        yield r

下一步,加入历史记录

一种思路是直接维护一个历史记录列表,手动append,并且把它传入prompt_template

但是官方建议使用

from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

这两个类用很多特性,后面见分晓吧。

ChatMessageHistory is not runnable, so it can't be invoked

他是RunnableWithMessageHistory调用的参数

可以run的是RunnableWithMessageHistory

但它不能被加入chain,因为它不属于其中的一个流程

我们可以指定RunnableWithMessageHistory()的

  1. 获取历史记录的方法,
  2. invoke的时候实际invoke的chain

如下

def get_session_history(session_id: str) :
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]
 with_message_history_chatbot = RunnableWithMessageHistory(
        chain,
        get_session_history,
        input_messages_key="messages"
    )

调用:(必须要传入config,指定历史记录是那个对话session的)

    config = {"configurable": {"session_id": session_id}}
    for r in with_message_history_chatbot.stream(
        {
            "messages": [HumanMessage(content=user_prompt)],
        },
        config=config
    ):
        yield r

"messages": [HumanMessage(content=user_prompt)]会传给 with_message_history_chatbot 加入对话历史,构造一个新的 message对象,同时这个对象会被 with_message_history_chatbot 传给 placeholder ,构成完整的 prompt_template,最后传给model

加入”只记录最后十轮对话“功能

在chain前面加入一个万金油组件

from langchain_core.runnables import RunnablePassthrough
def filter_messages(messages, k=2):
        return messages[-k:]
chain = (RunnablePassthrough.assign(messages=lambda x: filter_messages(x["messages"])) |
             prompt_template | model | parser)

历史记录会被传入为x,取后面几个元素,再传递给prompt_template

加入检索知识功能

在chain中再链接一个组件,叫做 retriever

retriver是由 vectorstoreas_retriever()方法实例化得来的。实例化的时候可以指定检索模式和检索条数。

retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 6})

vectorstore要提前存储内容,选择合适的embedding来实例化:

from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings

# 存进vectorstore
vectorstore = Chroma.from_documents(documents=all_splits, embedding=OpenAIEmbeddings())

很有趣的地方,关于rag_chain的实现

我们来看一段代码,这是rag_chain的最终实现:

# 定义chain
rag_chain = (
    {"context": retriever | format_docs, "question": RunnablePassthrough()}  # 实际上这里就分叉了,参考runnableparallel
    | prompt
    | llm
    | StrOutputParser()
)

用一个图来表示:

image-fauo.png

一些细节

HumanMessage()和("user", prompt)的区别:

  1. 后者prompt中所有的{str}格式会被解析为待传入的变量,后面你不传入会报错。如果是上一步模型的结果,并且出现了类似结构,会报错。###

vectorstore的持久化

开始的想法是直接dump运行时变量

考虑用pickle包的dump到本地

但是会遇到vectorstore对象“不能序列化”的问题。

后来发现vectorbase有自己的持久化方法

实例化的时候指定保存路径(不会保存)

vdb(persist_dictory="")

保存到本地

vdb.persist()

langserver的使用

用这个模板快速把应用部署成RESTful API

4. App definition

app = FastAPI(

title="LangChain Server",

version="1.0",

description="A simple API server using LangChain's Runnable interfaces",

)

5. Adding chain route

add_routes(

app,
chain,
path="/chain",

)

if name == "main":

import uvicorn
uvicorn.run(app, host="localhost", port=8000)

调用也十分简单和遵循直觉,只需要在client中实例化一个remotechain

from langserve import RemoteRunnable

remote_chain = RemoteRunnable("http://localhost:8000/chain/")

remote_chain.invoke({"language": "italian", "text": "hi"})

简单的streamlit应用

学习中遇到的棘手的问题/bug?

  1. 本地调用chain的时候没事,在client传入同样的输入,传到server的方法的时候会少参数。
    1. 尝试在方法中加入对这个参数的占位符需求,还是没有识别到
    2. 考虑自己构造一个runnable,指定输入和输出需求键
  2. 有时候playground少输入框,和上一个问题相关但不对等(有时候playground没有但可以成功传入)。
    1. 如果第一个runnable是prompt_template,占位符的参数识别的很准确