前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅谈LangChain Expression Language (LCEL)

浅谈LangChain Expression Language (LCEL)

原创
作者头像
flavorfan
发布2023-08-11 16:28:49
7.1K0
发布2023-08-11 16:28:49
举报
文章被收录于专栏:范传康的专栏范传康的专栏

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="\n\n"):
    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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 直观体验
    • 1)Pipeline的方式
      • 2) batch、async、streaming简单实现
        • 3)通用操作、数据也可以封装成基本Block支持
          • 4) Prompt提升重要性,级联和嵌套
          • 2.关于“Expression Language”、设计哲学
            • 1) 标准化部件Block、接口
              • 2) 组合以及定义Pipeline
              • reference
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档