LangChain于8月1日0.254版本更新,声称采用新的语法来创建带和组合功能的Chain,同时提供一个新的接口,支持批处理、异步和流处理,将这种语法成为LangChain Expression Language(LCEL)。体验了新版本LangChain的LCEL特性,确实是个重大有意义的更新,朝工程化应用方向发展了一大步。
LangChain的文档的Cookbook有丰富的例程,不想当简单的文档翻译和搬运工,尽可能从自己角度和理解试图解构LCEL。
1. 直观体验
1)Pipeline的方式
代码语言:javascript复制from langchain.prompts import ChatPromptTemplate
from langchain.chat_models import ChatOpenAI
chain = prompt | ChatOpenAI() | StrOutputParser()
chain.invoke({"foo": "bears"})
LangChain的Prompt、LLM、OutputParser都是基本单元,通过“|”构成一个Pipeline。这与Linux的Shell Pipeline异曲同工,级联嵌套Shell调用可以串成一长串,将简单的程序连接在一起并修改彼此。
另外一种排版是不是更引入注目:
代码语言:javascript复制from langchain.output_parsers.openai_functions import JsonKeyOutputFunctionsParser
chain = (
prompt
| model.bind(function_call= {"name": "joke"}, functions= functions)
| JsonKeyOutputFunctionsParser(key_name="setup")
)
chain.invoke({"foo": "bears"})
2) batch、async、streaming简单实现
代码语言:javascript复制# Stream
for s in chain.stream({"topic": "bears"}):
print(s.content, end="", flush=True)
# Invoke
chain.invoke({"topic": "bears"})
# Batch
chain.batch([{"topic": "bears"}, {"topic": "cats"}])
# Async Stream
async for s in chain.astream({"topic": "bears"}):
print(s.content, end="", flush=True)
# Async Invoke
await chain.ainvoke({"topic": "bears"})
# Async Batch
await chain.abatch([{"topic": "bears"}])
3)通用操作、数据也可以封装成基本Block支持
代码语言:javascript复制template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| model
| StrOutputParser()
)
chain.invoke("where did harrison work?")
- 第1个Block是人为定义的Dict,具有2个Key;这个Dict是第二个Block的输入(这就表明:Dict/KV是LCEL的通用接口格式,并且Value主要是String);
- 调用发生时,输入“where did harrison work?” 只有一个值时,被同时发往retriver和RunnablePassthrough(这里只是透传数据),这就构成Pipe/DAG中很重要的操作流的分叉(fork);
- 第2个Block:Prompt输入时context、question两个变量,这和第一个Block输出匹配。
这里可以看到
- 可以把其他功能函数/模块、通用操作输出封装成Dict(KV)的方式构造Block,这就大大丰富Block的元件库(类似lamabda匿名函数的作用)。
- LangChain的Pipeline的Block的标准输入接口时Dict(KeyValue),其中Value是String/Text文本。
4) Prompt提升重要性,级联和嵌套
ChatGPT出现伴随着Prompt,“Prompt Engineer”开始是戏称后面慢慢成为现实;Prompt设计重要性不言而喻。但是LangChain以往版本,Prompt 被嵌套在最内部,隐蔽且难于修改。现在把Prompt放在前面或者中心的位置,更加突出也容易修改替换。
代码语言:javascript复制from langchain.schema.runnable import RunnableMap
from langchain.schema import format_document
from langchain.prompts.prompt import PromptTemplate
_template = """Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language.
Chat History:
{chat_history}
Follow Up Input: {question}
Standalone question:"""
CONDENSE_QUESTION_PROMPT = PromptTemplate.from_template(_template)
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
ANSWER_PROMPT = ChatPromptTemplate.from_template(template)
DEFAULT_DOCUMENT_PROMPT = PromptTemplate.from_template(template="{page_content}")
def _combine_documents(docs, document_prompt = DEFAULT_DOCUMENT_PROMPT, document_separator="nn"):
doc_strings = [format_document(doc, document_prompt) for doc in docs]
return document_separator.join(doc_strings)
from typing import Tuple, List
def _format_chat_history(chat_history: List[Tuple]) -> str:
buffer = ""
for dialogue_turn in chat_history:
human = "Human: " dialogue_turn[0]
ai = "Assistant: " dialogue_turn[1]
buffer = "n" "n".join([human, ai])
return buffer
_inputs = RunnableMap(
{
"standalone_question": {
"question": lambda x: x["question"],
"chat_history": lambda x: _format_chat_history(x['chat_history'])
} | CONDENSE_QUESTION_PROMPT | ChatOpenAI(temperature=0) | StrOutputParser(),
}
)
_context = {
"context": itemgetter("standalone_question") | retriever | _combine_documents,
"question": lambda x: x["standalone_question"]
}
conversational_qa_chain = _inputs | _context | ANSWER_PROMPT | ChatOpenAI()
conversational_qa_chain.invoke({
"question": "where did harrison work?",
"chat_history": [],
})
这是一个复杂的例子例子,Prompt 相对于以前更加显目突出且易于修改;同时也展示组合的方式:级联和嵌套。
2.关于“Expression Language”、设计哲学
1) 标准化部件Block、接口
刚看到LangChain以“Expression Language”来命名这次版本变更,多少有点“飘了吧?”的感觉。这段时间学习体验以来,感觉多少有点匹配:以前版本缺乏统一设计的功能繁杂且散乱、各自为政、接口不互通,像极赶工的堆砌品;重构后,有了精巧的抽象的层级类设计,有了统一基类Runnable,各种模块可以方便的级联、嵌套起来。
标准化Block(通过基类定义标准Op),标准化部件间接口(输入输出);LangChain采用了Dict(key:Value)作为默认接口,并且重载了管道操作符“|”以及对应的有操作符。对于单独的string输入估计是通过对输入类型检测来支持,增加了灵活性。
2) 组合以及定义Pipeline
TensorFlow定义计算图DAG,然后运行计算图进行推理或者训练;李沐大佬口头禅”神经网络是一门语言“。类比过来,LangChain是通过组合(级联、嵌套)各种功能部件Block构建一个任务的执行管道网络(Pipeline),这个管道网络(Pipeline)是以语言文本(Prompt/Text)驱动的。从这个视角上看,新的LangChain是一门语言。
”Text Is the Universal Interface",正如LangChain的发布Blog引用一样,文本Text是很好一种中介数据类型,Linxu Shell时代的Pipeline管道组合有限简单程序,完成众多近乎无限多样的复杂任务。与之类似,LangChain把Dict、Text/String作为默认基本接口数据类型。
reference
- Cookbook
- Interface
- LangChain Expression Language (crowdcast.io)