LangChain-v0.2文档翻译:3.8、如何在LangChain中进行流式传输

流式传输对于使基于大型语言模型(LLM)的应用对最终用户具有响应性至关重要。

LangChain中的重要原语,如聊天模型、输出解析器、提示、检索器和代理,都实现了LangChain可运行接口。

这个接口提供了两种通用的流式传输方法:

  1. 同步stream和异步astream:这是流式传输链的最终输出默认实现
  2. 异步astream_events和异步astream_log:这些提供了一种流式传输链的中间步骤最终输出的方法。

让我们看看这两种方法,并尝试理解如何使用它们。

使用Stream

所有Runnable对象都实现了一个名为stream的同步方法和一个名为astream的异步变体。

这些方法旨在按块流式传输最终输出,一旦有块可用就立即产生。

只有在程序中的所有步骤都知道如何处理输入流时,才能进行流式传输;即,一次处理一个输入块,并产生相应的输出块。

这种处理的复杂性可以变化,从由LLM产生的令牌的简单任务到更具挑战性的任务,如在完整的JSON完成之前流式传输JSON结果的部分。

探索流式传输的最佳起点是LLM应用中最重要的组件——LLM本身!

LLMs和聊天模型

大型语言模型及其聊天变体是基于LLM的应用中的主要瓶颈。

大型语言模型可能需要几秒钟才能对查询生成完整的响应。这比应用程序对最终用户感觉响应式的**~200-300毫秒**阈值要慢得多。

使应用程序感觉更响应的关键策略是显示中间进度;即,按令牌从模型流式传输输出。

我们将展示使用聊天模型进行流式传输的示例。从以下选项中选择一个:

  • OpenAI
# 安装 langchain-openai 库
pip install -qU langchain-openai

# 导入 getpass 和 os 模块
import getpass
import os

# 设置 OPENAI_API_KEY 环境变量
os.environ["OPENAI_API_KEY"] = getpass.getpass()

from langchain_openai import ChatOpenAI

# 创建 ChatOpenAI 实例
model = ChatOpenAI(model="gpt-3.5-turbo-0125")

让我们从同步stream API开始:

# 定义一个空列表来存储流式传输的块
chunks = []

# 使用模型的 stream 方法进行流式传输
for chunk in model.stream("what color is the sky?"):
    chunks.append(chunk)
    # 打印每个块的内容
    print(chunk.content, end="|", flush=True)

# 输出示例
The| sky| appears| blue| during| the| day|.|

或者,如果你在异步环境中工作,你可能会考虑使用异步astream API:

# 定义一个空列表来存储流式传输的块
chunks = []

# 使用模型的 astream 方法进行异步流式传输
async for chunk in model.astream("what color is the sky?"):
    chunks.append(chunk)
    # 打印每个块的内容
    print(chunk.content, end="|", flush=True)

# 输出示例
The| sky| appears| blue| during| the| day|.|

让我们检查其中一个块:

chunks[0]
# 输出示例
AIMessageChunk(content='The', id='run-b36bea64-5511-4d7a-b6a3-a07b3db0c8e7')

我们得到了一个名为AIMessageChunk的东西。这个块表示AIMessage的一部分。

消息块设计上是累加的——简单地将它们相加就可以得到到目前为止的响应状态!

# 将块相加得到当前的响应状态
chunks[0] + chunks[1] + chunks[2] + chunks[3] + chunks[4]
# 输出示例
AIMessageChunk(content='The sky appears blue during', id='run-b36bea64-5511-4d7a-b6a3-a07b3db0c8e7')

几乎所有基于LLM的应用都涉及不止一个调用语言模型的步骤。

让我们使用LangChain Expression LanguageLCEL)构建一个简单的链,该链结合了提示、模型和一个解析器,并验证流式传输是否有效。

我们将使用StrOutputParser来解析模型的输出。这是一个简单的解析器,它从AIMessageChunk中提取content字段,给我们模型返回的token

LCEL是一种声明式方法,通过串联不同的LangChain原语来指定一个“程序”。使用LCEL创建的链将自动实现streamastream,允许流式传输最终输出。事实上,使用LCEL创建的链实现了整个标准可运行接口。

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

# 使用给定模板创建 ChatPromptTemplate 对象
prompt = ChatPromptTemplate.from_template("tell me a joke about {topic}")

# 创建 StrOutputParser 实例
parser = StrOutputParser()

# 串联提示模板、模型和解析器
chain = prompt | model | parser

# 使用链的 astream 方法进行异步流式传输
async for chunk in chain.astream({"topic": "parrot"}):
    # 打印每个块的内容
    print(chunk, end="|", flush=True)

# 输出示例
Here|'s| a| joke| about| a| par|rot|:|

A man| goes| to| a| pet| shop| to| buy| a| par|rot|.| The| shop| owner| shows| him| two| stunning| pa|rr|ots| with| beautiful| pl|um|age|.|

"|There|'s| a| talking| par|rot| an|d a| non|-|talking| par|rot|,"| the| owner| says|.| "|The| talking| par|rot| costs| $|100|,| an|d the| non|-|talking| par|rot| is| $|20|."|

The| man| says|,| "|I|'ll| take| the| non|-|talking| par|rot| at| $|20|."|

He| pays| an|d leaves| with| the| par|rot|.| As| he|'s| walking| down| the| street|,| the| par|rot| looks| up| at| him| an|d says|,| "|You| know|,| you| really| are| a| stupi|d man|!"|

The| man| is| stun|ne|d an|d looks| at| the| par|rot| in| dis|bel|ief|.| The| par|rot| continues|,| "|Yes|,| you| got| r|ippe|d off| big| time|!| I| can| talk| just| as| well| as| that| other| par|rot|,| an|d you| only| pai|d $|20| |for| me|!"|

请注意,即使我们在上面的链中在末尾使用了parser,我们仍然得到了流式传输的输出。parser对每个流式传输的块分别操作。许多LCEL原语也支持这种类型的转换式传递流式传输,这在构建应用程序时非常方便。

自定义函数可以设计为返回生成器,这些生成器能够操作流。

某些可运行组件,如提示模板和聊天模型,不能处理单独的块,而是聚合所有先前的步骤。这样的可运行组件可能会中断流式传输过程。

LangChain表达式语言允许你将链的构建与使用模式(例如,同步/异步、批处理/流式传输等)分离。如果这对你要构建的内容不相关,你也可以依靠标准的命令式编程方法,通过分别调用每个组件的invokebatchstream,将结果分配给变量,然后根据需要在下游使用它们。

使用输入流工作

如果你想在生成时流式传输JSON输出怎么办?

如果你依赖json.loads解析部分json,解析将会失败,因为部分json不是有效的json。

你可能会完全不知所措,声称不可能流式传输JSON。

好吧,事实证明有一种方法可以做到——解析器需要在输入流上操作,并尝试将部分json“自动完成”为有效状态。

让我们看看这样一个解析器的实际工作,以理解这意味着什么。

from langchain_core.output_parsers import JsonOutputParser

# 由于Langchain旧版本中的一个错误,JsonOutputParser没有从某些模型流式传输结果
chain = (model | JsonOutputParser())

# 使用链的 astream 方法进行异步流式传输
async for text in chain.astream(
    "output a list of the countries france, spain and japan and their populations in JSON format. "
    'Use a dict with an outer key of "countries" which contains a list of countries. '
    "Each country should have the key `name` and `population`"):
    # 打印流式传输的文本
    print(text, flush=True)

# 输出示例
{}
{'countries': []}
{'countries': [{}]}
{'countries': [{'name': ''}]}
{'countries': [{'name': 'France'}]}
{'countries': [{'name': 'France', 'population': 67}]}
{'countries': [{'name': 'France', 'population': 67413}]}
{'countries': [{'name': 'France', 'population': 67413000}]}
{'countries': [{'name': 'France', 'population': 67413000}, {}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': ''}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain'}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain', 'population': 47}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain', 'population': 47351}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain', 'population': 47351567}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain', 'population': 47351567}, {}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain', 'population': 47351567}, {'name': ''}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain', 'population': 47351567}, {'name': 'Japan'}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain', 'population': 47351567}, {'name': 'Japan', 'population': 125}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain', 'population': 47351567}, {'name': 'Japan', 'population': 125584}]}
{'countries': [{'name': 'France', 'population': 67413000}, {'name': 'Spain', 'population': 47351567}, {'name': 'Japan', 'population': 125584000}]}

现在,让我们破坏流式传输。我们将使用前面的示例,并在末尾添加一个提取函数,从最终确定的JSON中提取国家名称。

任何在链中操作最终确定的输入而不是输入流的步骤都可能通过streamastream破坏流式传输功能。

稍后,我们将讨论用于传输中间步骤结果的 astream_events API。即使链包含仅对最终输入进行操作的步骤,此 API 也会流式传输中间步骤的结果。

from langchain_core.output_parsers import JsonOutputParser

# 定义一个操作最终确定的输入而不是输入流的函数
def _extract_country_names(inputs):
    """一个不操作输入流并破坏流式传输的函数。"""
    if not isinstance(inputs, dict):
        return ""

    if "countries" not in inputs:
        return ""

    countries = inputs["countries"]

    if not isinstance(countries, list):
        return ""

    country_names = [
        country.get("name") for country in countries if isinstance(country, dict)
    ]
    return country_names

# 串联模型、JsonOutputParser 和提取函数
chain = model | JsonOutputParser() | _extract_country_names

# 使用链的 astream 方法进行异步流式传输
async for text in chain.astream(
    "output a list of the countries france, spain and japan and their populations in JSON format. "
    'Use a dict with an outer key of "countries" which contains a list of countries. '
    "Each country should have the key `name` and `population`"):
    # 打印流式传输的文本
    print(text, end="|", flush=True)

# 输出示例
['France', 'Spain', 'Japan']|
生成器函数

让我们使用一个可以操作输入流的生成器函数来修复流式传输。

生成器函数(使用yield的函数)允许编写操作输入流的代码。

from langchain_core.output_parsers import JsonOutputParser

# 定义一个异步生成器函数来操作输入流
async def _extract_country_names_streaming(input_stream):
    """一个操作输入流的函数。"""
    country_names_so_far = set()

    # 遍历输入流中的每个项目
    async for input in input_stream:
        if not isinstance(input, dict):
            continue

        if "countries" not in input:
            continue

        countries = input["countries"]

        if not isinstance(countries, list):
            continue

        # 遍历国家列表
        for country in countries:
            name = country.get("name")
            if not name:
                continue
            if name not in country_names_so_far:
                # 产生国家名称
                yield name
                country_names_so_far.add(name)

# 串联模型、JsonOutputParser 和生成器函数
chain = model | JsonOutputParser() | _extract_country_names_streaming

# 使用链的 astream 方法进行异步流式传输
async for text in chain.astream(
    "output a list of the countries france, spain and japan and their populations in JSON format. "
    'Use a dict with an outer key of "countries" which contains a list of countries. '
    "Each country should have the key `name` and `population`"):
    # 打印产生的国家名称
    print(text, end="|", flush=True)

# 输出示例
France|Spain|Japan|

注意,由于上述代码依赖于JSON自动完成,您可能会看到国家名称的部分(例如SpSpain),这不是提取结果所希望的!

我们专注于流式传输概念,而不仅仅是链的结果。

非流式组件

一些内置组件(如检索器)不提供任何streaming。如果我们尝试对它们进行stream,会发生什么?🤨

from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings

# 使用给定文本创建 FAISS 实例
vectorstore = FAISS.from_texts(
    ["harrison worked at kensho", "harrison likes spicy food"],
    embedding=OpenAIEmbeddings(),
)

# 创建检索器
retriever = vectorstore.as_retriever()

# 使用检索器的 stream 方法进行流式传输
chunks = [chunk for chunk in retriever.stream("where did harrison work?")]
chunks

# 输出示例
[[Document(page_content='harrison worked at kensho'),
  Document(page_content='harrison likes spicy food')]]

流式传输只是产生了那个组件的最终结果。

这没问题 🥹! 不是所有组件都必须实现流式传输——在某些情况下,流式传输是不必要的、困难的或根本没有意义。

使用非流式组件构建的LCEL链,在许多情况下仍然可以流式传输,部分输出的流式传输在链中最后一个非流式步骤之后开始。

# 构建一个检索链
retrieval_chain = (
    {
        "context": retriever.with_config(run_name="Docs"),
        "question": RunnablePassthrough(),
    }
    | prompt
    | model
    | StrOutputParser()
)

# 使用检索链的 stream 方法进行流式传输
for chunk in retrieval_chain.stream(
    "Where did harrison work? Write 3 made up sentences about this place."
):
    # 打印每个块的内容
    print(chunk, end="|", flush=True)

# 输出示例
Base|d on| the| given| context|,| Harrison| worke|d at| K|ens|ho|.|

Here| are| |3| |made| up| sentences| about| this| place|:|

1|.| K|ens|ho| was| a| cutting|-|edge| technology| company| known| for| its| innovative| solutions| in| artificial| intelligence| an|d data| analytics|.|

2|.| The| modern| office| space| at| K|ens|ho| feature|d open| floor| plans|,| collaborative| work|sp|aces|,| an|d a| vib|rant| atmosphere| that| fos|tere|d creativity| an|d team|work|.|

3|.| With| its| prime| location| in| the| heart| of| the| city|,| K|ens|ho| attracte|d top| talent| from| aroun|d the| worl|d,| creating| a| diverse| an|d dynamic| work| environment|.|

现在我们已经看到了streamastream的工作原理,让我们进入流式传输事件的世界。 🏞️

使用流式传输事件

事件流式传输是一个测试版API。根据反馈,这个API可能会有所变化。

这个指南展示了V2 API,需要 langchain-core >= 0.2。对于与旧版LangChain兼容的V1
API,请点击这里。

import langchain_core

langchain_core.__version__

为了astream_events API能够正常工作:

  • 尽可能在整个代码中使用async(例如,异步工具等)
  • 如果定义自定义函数/可运行组件,请传播回调
  • 当使用没有LCEL的可运行组件时,请确保在LLM上调用.astream()而不是.ainvoke,以强制LLM流式传输令牌
  • 如果有任何不符合预期的工作,请让我们知道!😃

事件参考

下面是一个参考表,显示了各种可运行对象可能发出的一些事件。

当流式传输正确实现时,一个可运行组件的输入在输入流完全消耗之后才会被知晓。这意味着inputs通常只会在end事件中包含,而不是在start事件中。

事件名称输入输出
on_chat_model_start[模型名称]{“messages”: [[SystemMessage, HumanMessage]]}
on_chat_model_stream[模型名称]AIMessageChunk(content=“hello”)
on_chat_model_end[模型名称]{“messages”: [[SystemMessage, HumanMessage]]}AIMessageChunk(content=“hello world”)
on_llm_start[模型名称]{‘input’: ‘hello’}
on_llm_stream[模型名称]‘Hello’
on_llm_end[模型名称]‘Hello human!’
on_chain_startformat_docs
on_chain_streamformat_docs“hello world!, goodbye world!”
on_chain_endformat_docs[Document(…)]“hello world!, goodbye world!”
on_tool_startsome_tool{“x”: 1, “y”: “2”}
on_tool_endsome_tool{“x”: 1, “y”: “2”}
on_retriever_start[检索器名称]{“query”: “hello”}
on_retriever_end[检索器名称]{“query”: “hello”}[Document(…), …]
on_prompt_start[模板名称]{“question”: “hello”}
on_prompt_end[模板名称]{“question”: “hello”}ChatPromptValue(messages: [SystemMessage, …])

聊天模型

让我们从查看聊天模型产生的事件开始。

# 定义一个空列表来存储事件
events = []

# 使用模型的 astream_events 方法进行异步流式传输事件
async for event in model.astream_events("hello", version="v2"):
    events.append(event)

# 输出示例
/home/eugene/src/langchain/libs/core/langchain_core/_api/beta_decorator.py:87: LangChainBetaWarning: This API is in beta and may change in the future.
  warn_beta(

嘿,那个有趣的version="v2"参数在API中是干什么的?!

这是一个测试版API,我们几乎肯定会对它进行一些更改(事实上,我们已经做了!)

这个版本参数将允许我们尽量减少对您代码的破坏性更改。

简而言之,我们现在烦扰您,是为了以后不烦扰您。

v2仅在langchain-core>=0.2.0中可用。

让我们看看一些开始事件和一些结束事件。

events[:3]

# 输出示例
[{'event': 'on_chat_model_start',
  'data': {'input': 'hello'},
  'name': 'ChatAnthropic',
  'tags': [],
  'run_id': 'a81e4c0f-fc36-4d33-93bc-1ac25b9bb2c3',
  'metadata': {}},
 {'event': 'on_chat_model_stream',
  'data': {'chunk': AIMessageChunk(content='Hello', id='run-a81e4c0f-fc36-4d33-93bc-1ac25b9bb2c3')},
  'run_id': 'a81e4c0f-fc36-4d33-93bc-1ac25b9bb2c3',
  'name': 'ChatAnthropic',
  'tags': [],
  'metadata': {}},
 {'event': 'on_chat_model_stream',
  'data': {'chunk': AIMessageChunk(content='!', id='run-a81e4c0f-fc36-4d33-93bc-1ac25b9bb2c3')},
  'run_id': 'a81e4c0f-fc36-4d33-93bc-1ac25b9bb2c3',
  'name': 'ChatAnthropic',
  'tags': [],
  'metadata': {}}]

events[-2:]

# 输出示例
[{'event': 'on_chat_model_stream',
  'data': {'chunk': AIMessageChunk(content='?', id='run-a81e4c0f-fc36-4d33-93bc-1ac25b9bb2c3')},
  'run_id': 'a81e4c0f-fc36-4d33-93bc-1ac25b9bb2c3',
  'name': 'ChatAnthropic',
  'tags': [],
  'metadata': {}},
 {'event': 'on_chat_model_end',
  'data': {'output': AIMessageChunk(content='Hello! How can I assist you today?', id='run-a81e4c0f-fc36-4d33-93bc-1ac25b9bb2c3')},
  'run_id': 'a81e4c0f-fc36-4d33-93bc-1ac25b9bb2c3',
  'name': 'ChatAnthropic',
  'tags': [],
  'metadata': {}}]

让我们重新审视一下解析流式传输JSON的示例链,探索流式传输事件API。

# 串联模型和 JsonOutputParser
chain = (model | JsonOutputParser())

# 定义一个空列表来存储事件
events = [
    event
    async for event in chain.astream_events(
        "output a list of the countries france, spain and japan and their populations in JSON format. "
        'Use a dict with an outer key of "countries" which contains a list of countries. '
        "Each country should have the key `name` and `population`",
        version="v2",
    )
]

如果你查看前几个事件,你会注意到有3个不同的开始事件,而不是2个开始事件。

这三个开始事件对应于:

  1. 链(模型 + 解析器)
  2. 模型
  3. 解析器
events[:3]

# 输出示例
[{'event': 'on_chain_start',
  'data': {'input': 'output a list of the countries france, spain and japan and their populations in JSON format. Use a dict with an outer key of "countries" which contains a list of countries. Each country should have the key `name` and `population`'},
  'name': 'RunnableSequence',
  'tags': [],
  'run_id': '4765006b-16e2-4b1d-a523-edd9fd64cb92',
  'metadata': {}},
 {'event': 'on_chat_model_start',
  'data': {'input': {'messages': [[HumanMessage(content='output a list of the countries france, spain and japan and their populations in JSON format. Use a dict with an outer key of "countries" which contains a list of countries. Each country should have the key `name` and `population`')]]}},
  'name': 'ChatAnthropic',
  'tags': ['seq:step:1'],
  'run_id': '0320c234-7b52-4a14-ae4e-5f100949e589',
  'metadata': {}},
 {'event': 'on_chat_model_stream',
  'data': {'chunk': AIMessageChunk(content='{', id='run-0320c234-7b52-4a14-ae4e-5f100949e589')},
  'run_id': '0320c234-7b52-4a14-ae4e-5f100949e589',
  'name': 'ChatAnthropic',
  'tags': ['seq:step:1'],
  'metadata': {}}]

你认为如果你看最后3个事件会看到什么?中间的呢?

让我们使用这个API来输出模型和解析器的流式传输事件。我们忽略了开始事件、结束事件和来自链的事件。

# 定义一个计数器来存储事件数量
num_events = 0

# 使用链的 astream_events 方法进行异步流式传输事件
async for event in chain.astream_events(
    "output a list of the countries france, spain and japan and their populations in JSON format. "
    'Use a dict with an outer key of "countries" which contains a list of countries. '
    "Each country should have the key `name` and `population`",
    version="v2",
):
    # 事件类型
    kind = event["event"]
    if kind == "on_chat_model_stream":
        # 打印聊天模型块的内容
        print(
            f"Chat model chunk: {repr(event['data']['chunk'].content)}",
            flush=True,
        )
    if kind == "on_parser_stream":
        # 打印解析器块的内容
        print(f"Parser chunk: {event['data']['chunk']}", flush=True)
    num_events += 1
    if num_events > 30:
        # 截断输出
        print("...")
        break

# 输出示例
Chat model chunk: '{'
Parser chunk: {}
Chat model chunk: '\n  '
Chat model chunk: '"'
Chat model chunk: 'countries'
Chat model chunk: '":'
Chat model chunk: ' ['
Parser chunk: {'countries': []}
Chat model chunk: '\n    '
Chat model chunk: '{'
Parser chunk: {'countries': [{}]}
Chat model chunk: '\n      '
Chat model chunk: '"'
Chat model chunk: 'name'
Chat model chunk: '":'
Chat model chunk: ' "'
Parser chunk: {'countries': [{'name': ''}]}
Chat model chunk: 'France'
Parser chunk: {'countries': [{'name': 'France'}]}
Chat model chunk: '",'
Chat model chunk: '\n      '
Chat model chunk: '"'
Chat model chunk: 'population'
...

因为模型和解析器都支持流式传输,我们看到两个组件都实时产生了流式传输事件!这是不是有点酷?🦜

过滤事件

由于这个API产生了大量的事件,能够过滤事件非常有用。

你可以通过组件name、组件tags或组件type来过滤。

按名称过滤
# 为模型和解析器设置运行名称
chain = model.with_config({"run_name": "model"}) | JsonOutputParser().with_config(
    {"run_name": "my_parser"}
)

# 定义一个计数器来存储事件数量
max_events = 0

# 使用链的 astream_events 方法进行异步流式传输事件,并按名称过滤
async for event in chain.astream_events(
    "output a list of the countries france, spain and japan and their populations in JSON format. "
    'Use a dict with an outer key of "countries" which contains a list of countries. '
    "Each country should have the key `name` and `population`",
    version="v2",
    include_names=["my_parser"],
):
    # 打印事件
    print(event)
    max_events += 1
    if max_events > 10:
        # 截断输出
        print("...")
        break

# 输出示例
{'event': 'on_parser_start', 'data': {'input': 'output a list of the countries france, spain and japan and their populations in JSON format. Use a dict with an outer key of "countries" which contains a list of countries. Each country should have the key `name` and `population`'}, 'name': 'my_parser', 'tags': ['seq:step:2'], 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {'countries': []}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {'countries': [{}]}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {'countries': [{'name': ''}]}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {'countries': [{'name': 'France'}]}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {'countries': [{'name': 'France', 'population': 67}]}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {'countries': [{'name': 'France', 'population': 67413}]}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {'countries': [{'name': 'France', 'population': 67413000}]}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {'countries': [{'name': 'France', 'population': 67413000}, {}]}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {'countries': [{'name': 'France', 'population': 67413000}, {'name': ''}]}}, 'run_id': 'e058d750-f2c2-40f6-aa61-10f84cd671a9', 'name': 'my_parser', 'tags': ['seq:step:2'], 'metadata': {}}
...
按类型过滤
# 使用链的 astream_events 方法进行异步流式传输事件,并按类型过滤
max_events = 0
async for event in chain.astream_events(
    'output a list of the countries france, spain and japan and their populations in JSON format. Use a dict with an outer key of "countries" which contains a list of countries. Each country should have the key `name` and `population`',
    version="v2",
    include_types=["chat_model"],
):
    # 打印事件
    print(event)
    max_events += 1
    if max_events > 10:
        # 截断输出
        print("...")
        break

# 输出示例
{'event': 'on_chat_model_start', 'data': {'input': 'output a list of the countries france, spain and japan and their populations in JSON format. Use a dict with an outer key of "countries" which contains a list of countries. Each country should have the key `name` and `population`'}, 'name': 'model', 'tags': ['seq:step:1'], 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='{', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='\n  ', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='"', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='countries', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='":', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content=' [', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='\n    ', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='{', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='\n      ', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='"', id='run-db246792-2a91-4eb3-a14b-29658947065d')}, 'run_id': 'db246792-2a91-4eb3-a14b-29658947065d', 'name': 'model', 'tags': ['seq:step:1'], 'metadata': {}}
...
按标签过滤

标签是由给定可运行组件的子组件继承的。

如果您使用标签进行过滤,请确保这是您想要的。

# 为链设置标签
chain = (model | JsonOutputParser()).with_config({"tags": ["my_chain"]})

# 定义一个计数器来存储事件数量
max_events = 0

# 使用链的 astream_events 方法进行异步流式传输事件,并按标签过滤
async for event in chain.astream_events(
    'output a list of the countries france, spain and japan and their populations in JSON format. Use a dict with an outer key of "countries" which contains a list of countries. Each country should have the key `name` and `population`',
    version="v2",
    include_tags=["my_chain"],
):
    # 打印事件
    print(event)
    max_events += 1
    if max_events > 10:
        # 截断输出
        print("...")
        break

# 输出示例
{'event': 'on_chain_start', 'data': {'input': 'output a list of the countries france, spain and japan and their populations in JSON format. Use a dict with an outer key of "countries" which contains a list of countries. Each country should have the key `name` and `population`'}, 'name': 'RunnableSequence', 'tags': ['my_chain'], 'run_id': 'fd68dd64-7a4d-4bdb-a0c2-ee592db0d024', 'metadata': {}}
{'event': 'on_chat_model_start', 'data': {'input': {'messages': [[HumanMessage(content='output a list of the countries france, spain and japan and their populations in JSON format. Use a dict with an outer key of "countries" which contains a list of countries. Each country should have the key `name` and `population`')]]}}, 'name': 'ChatAnthropic', 'tags': ['seq:step:1', 'my_chain'], 'run_id': 'efd3c8af-4be5-4f6c-9327-e3f9865dd1cd', 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='{', id='run-efd3c8af-4be5-4f6c-9327-e3f9865dd1cd')}, 'run_id': 'efd3c8af-4be5-4f6c-9327-e3f9865dd1cd', 'name': 'ChatAnthropic', 'tags': ['seq:step:1', 'my_chain'], 'metadata': {}}
{'event': 'on_parser_start', 'data': {}, 'name': 'JsonOutputParser', 'tags': ['seq:step:2', 'my_chain'], 'run_id': 'afde30b9-beac-4b36-b4c7-dbbe423ddcdb', 'metadata': {}}
{'event': 'on_parser_stream', 'data': {'chunk': {}}, 'run_id': 'afde30b9-beac-4b36-b4c7-dbbe423ddcdb', 'name': 'JsonOutputParser', 'tags': ['seq:step:2', 'my_chain'], 'metadata': {}}
{'event': 'on_chain_stream', 'data': {'chunk': {}}, 'run_id': 'fd68dd64-7a4d-4bdb-a0c2-ee592db0d024', 'name': 'RunnableSequence', 'tags': ['my_chain'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='\n  ', id='run-efd3c8af-4be5-4f6c-9327-e3f9865dd1cd')}, 'run_id': 'efd3c8af-4be5-4f6c-9327-e3f9865dd1cd', 'name': 'ChatAnthropic', 'tags': ['seq:step:1', 'my_chain'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='"', id='run-efd3c8af-4be5-4f6c-9327-e3f9865dd1cd')}, 'run_id': 'efd3c8af-4be5-4f6c-9327-e3f9865dd1cd', 'name': 'ChatAnthropic', 'tags': ['seq:step:1', 'my_chain'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='countries', id='run-efd3c8af-4be5-4f6c-9327-e3f9865dd1cd')}, 'run_id': 'efd3c8af-4be5-4f6c-9327-e3f9865dd1cd', 'name': 'ChatAnthropic', 'tags': ['seq:step:1', 'my_chain'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content='":', id='run-efd3c8af-4be5-4f6c-9327-e3f9865dd1cd')}, 'run_id': 'efd3c8af-4be5-4f6c-9327-e3f9865dd1cd', 'name': 'ChatAnthropic', 'tags': ['seq:step:1', 'my_chain'], 'metadata': {}}
{'event': 'on_chat_model_stream', 'data': {'chunk': AIMessageChunk(content=' [', id='run-efd3c8af-4be5-4f6c-9327-e3f9865dd1cd')}, 'run_id': 'efd3c8af-4be5-4f6c-9327-e3f9865dd1cd', 'name': 'ChatAnthropic', 'tags': ['seq:step:1', 'my_chain'], 'metadata': {}}
...

非流式组件​

还记得某些组件由于不对输入流进行操作而无法很好地进行流传输吗?

虽然在使用 astream 时此类组件可能会中断最终输出的流式传输,但 astream_events 仍会从支持流式传输的中间步骤中产生流式传输事件!

# Function that does not support streaming.
# It operates on the finalizes inputs rather than
# operating on the input stream.
def _extract_country_names(inputs):
    """A function that does not operates on input streams and breaks streaming."""
    if not isinstance(inputs, dict):
        return ""

    if "countries" not in inputs:
        return ""

    countries = inputs["countries"]

    if not isinstance(countries, list):
        return ""

    country_names = [
        country.get("name") for country in countries if isinstance(country, dict)
    ]
    return country_names


chain = (
    model | JsonOutputParser() | _extract_country_names
)  # This parser only works with OpenAI right now

正如预期的那样, astream API 无法正常工作,因为 _extract_country_names 不对流进行操作。

async for chunk in chain.astream(
    "output a list of the countries france, spain and japan and their populations in JSON format. "
    'Use a dict with an outer key of "countries" which contains a list of countries. '
    "Each country should have the key `name` and `population`",
):
    print(chunk, flush=True)

现在,让我们确认通过 astream_events 我们仍然可以看到来自模型和解析器的流输出。

num_events = 0

async for event in chain.astream_events(
    "output a list of the countries france, spain and japan and their populations in JSON format. "
    'Use a dict with an outer key of "countries" which contains a list of countries. '
    "Each country should have the key `name` and `population`",
    version="v2",
):
    kind = event["event"]
    if kind == "on_chat_model_stream":
        print(
            f"Chat model chunk: {repr(event['data']['chunk'].content)}",
            flush=True,
        )
    if kind == "on_parser_stream":
        print(f"Parser chunk: {event['data']['chunk']}", flush=True)
    num_events += 1
    if num_events > 30:
        # Truncate the output
        print("...")
        break

传播回调

如果您在工具中调用可运行组件,您需要将回调传播到可运行组件;否则,将不会产生流式传输事件。

当使用RunnableLambdas@chain装饰器时,回调会自动在后台传播。

from langchain_core.runnables import RunnableLambda
from langchain_core.tools import tool

# 定义一个反转单词的函数
def reverse_word(word: str):
    return word[::-1]

# 将反转单词函数转换为 RunnableLambda
reverse_word = RunnableLambda(reverse_word)

# 定义一个不传播回调的自定义工具
@tool
def bad_tool(word: str):
    """自定义工具,不传播回调。"""
    return reverse_word.invoke(word)

# 使用 bad_tool 的 astream_events 方法进行异步流式传输事件
async for event in bad_tool.astream_events("hello", version="v2"):
    print(event)

# 输出示例
{'event': 'on_tool_start', 'data': {'input': 'hello'}, 'name': 'bad_tool', 'tags': [], 'run_id': 'ea900472-a8f7-425d-b627-facdef936ee8', 'metadata': {}}
{'event': 'on_chain_start', 'data': {'input': 'hello'}, 'name': 'reverse_word', 'tags': [], 'run_id': '77b01284-0515-48f4-8d7c-eb27c1882f86', 'metadata': {}}
{'event': 'on_chain_end', 'data': {'output': 'olleh', 'input': 'hello'}, 'run_id': '77b01284-0515-48f4-8d7c-eb27c1882f86', 'name': 'reverse_word', 'tags': [], 'metadata': {}}
{'event': 'on_tool_end', 'data': {'output': 'olleh'}, 'run_id': 'ea900472-a8f7-425d-b627-facdef936ee8', 'name': 'bad_tool', 'tags': [], 'metadata': {}}

这里有一个正确传播回调的重新实现。你会注意到现在我们也得到了来自reverse_word可运行组件的事件。

# 定义一个正确传播回调的自定义工具
@tool
def correct_tool(word: str, callbacks):
    """正确传播回调的工具。"""
    return reverse_word.invoke(word, {"callbacks": callbacks})

# 使用 correct_tool 的 astream_events 方法进行异步流式传输事件
async for event in correct_tool.astream_events("hello", version="v2"):
    print(event)

# 输出示例
{'event': 'on_tool_start', 'data': {'input': 'hello'}, 'name': 'correct_tool', 'tags': [], 'run_id': 'd5ea83b9-9278-49cc-9f1d-aa302d671040', 'metadata': {}}
{'event': 'on_chain_start', 'data': {'input': 'hello'}, 'name': 'reverse_word', 'tags': [], 'run_id': '44dafbf4-2f87-412b-ae0e-9f71713810df', 'metadata': {}}
{'event': 'on_chain_end', 'data': {'output': 'olleh', 'input': 'hello'}, 'run_id': '44dafbf4-2f87-412b-ae0e-9f71713810df', 'name': 'reverse_word', 'tags': [], 'metadata': {}}
{'event': 'on_tool_end', 'data': {'output': 'olleh'}, 'run_id': 'd5ea83b9-9278-49cc-9f1d-aa302d671040', 'name': 'correct_tool', 'tags': [], 'metadata': {}}

如果您在Runnable Lambdas@chains中调用可运行组件,那么回调将自动代表您传递。

from langchain_core.runnables import chain

# 定义一个异步函数,反转单词并将其加倍
async def reverse_and_double(word: str):
    return await reverse_word.ainvoke(word) * 2

# 将反转和加倍函数转换为 RunnableLambda
reverse_and_double = RunnableLambda(reverse_and_double)

# 使用 reverse_and_double 的 ainvoke 方法进行异步调用
await reverse_and_double.ainvoke("1234")

# 使用 reverse_and_double 的 astream_events 方法进行异步流式传输事件
async for event in reverse_and_double.astream_events("1234", version="v2"):
    print(event)

# 输出示例
{'event': 'on_chain_start', 'data': {'input': '1234'}, 'name': 'reverse_and_double', 'tags': [], 'run_id': '03b0e6a1-3e60-42fc-8373-1e7829198d80', 'metadata': {}}
{'event': 'on_chain_start', 'data': {'input': '1234'}, 'name': 'reverse_word', 'tags': [], 'run_id': '5cf26fc8-840b-4642-98ed-623dda28707a', 'metadata': {}}
{'event': 'on_chain_end', 'data': {'output': '4321', 'input': '1234'}, 'run_id': '5cf26fc8-840b-4642-98ed-623dda28707a', 'name': 'reverse_word', 'tags': [], 'metadata': {}}
{'event': 'on_chain_stream', 'data': {'chunk': '43214321'}, 'run_id': '03b0e6a1-3e60-42fc-8373-1e7829198d80', 'name': 'reverse_and_double', 'tags': [], 'metadata': {}}
{'event': 'on_chain_end', 'data': {'output': '43214321'}, 'run_id': '03b0e6a1-3e60-42fc-8373-1e7829198d80', 'name': 'reverse_and_double', 'tags': [], 'metadata': {}}

使用@chain装饰器:

from langchain_core.runnables import chain

# 使用 @chain 装饰器定义一个反转并加倍的异步函数
@chain
async def reverse_and_double(word: str):
    return await reverse_word.ainvoke(word) * 2

# 使用 reverse_and_double 的 ainvoke 方法进行异步调用
await reverse_and_double.ainvoke("1234")

# 使用 reverse_and_double 的 astream_events 方法进行异步流式传输事件
async for event in reverse_and_double.astream_events("1234", version="v2"):
    print(event)

# 输出示例
{'event': 'on_chain_start', 'data': {'input': '1234'}, 'name': 'reverse_and_double', 'tags': [], 'run_id': '1bfcaedc-f4aa-4d8e-beee-9bba6ef17008', 'metadata': {}}
{'event': 'on_chain_start', 'data': {'input': '1234'}, 'name': 'reverse_word', 'tags': [], 'run_id': '64fc99f0-5d7d-442b-b4f5-4537129f67d1', 'metadata': {}}
{'event': 'on_chain_end', 'data': {'output': '4321', 'input': '1234'}, 'run_id': '64fc99f0-5d7d-442b-b4f5-4537129f67d1', 'name': 'reverse_word', 'tags': [], 'metadata': {}}
{'event': 'on_chain_stream', 'data': {'chunk': '43214321'}, 'run_id': '1bfcaedc-f4aa-4d8e-beee-9bba6ef17008', 'name': 'reverse_and_double', 'tags': [], 'metadata': {}}
{'event': 'on_chain_end', 'data': {'output': '43214321'}, 'run_id': '1bfcaedc-f4aa-4d8e-beee-9bba6ef17008', 'name': 'reverse_and_double', 'tags': [], 'metadata': {}}

下一步

现在您已经了解了几种使用LangChain流式传输最终输出和内部步骤的方法。

要了解更多,请查看本节中的其他操作指南,或查看有关LangChain表达式语言的概念指南。

总结
本文详细介绍了如何在LangChain中使用流式传输,包括同步和异步方法,以及如何通过流式传输事件来监控和处理应用程序的中间步骤。通过实际的代码示例,展示了如何使用streamastreamastream_events等方法来实现流式传输,以及如何通过过滤事件来控制流式传输过程。此外,还讨论了在构建流式传输应用程序时需要注意的事项,如传播回调和处理非流式组件。这些技术可以帮助开发者构建更加响应式和高效的基于LLM的应用。

扩展知识

  • LangChain Expression Language (LCEL):LangChain的表达式语言,允许通过串联不同的LangChain原语来构建程序。
  • 流式传输事件:LangChain提供的一种机制,允许开发者实时监控和响应应用程序的运行状态。
  • 异步编程:使用asyncawait关键字来实现非阻塞的并发编程。
  • 回调传播:在LangChain中,正确传播回调对于生成流式传输事件至关重要。
  • 非流式组件:即使某些组件不支持流式传输,astream_events API仍然可以提供中间步骤的流式传输事件。
### 回答1: 这个命令有几个错误: 1. `-p` 选项应该放在 `docker run` 命令的最后面,因为它用来映射端口。 2. `-v` 选项后面的挂载路径应该是这样的: `-v /opt/python3.6/demo/aMedicalBot_demo_quesoar:/app` 3. 在这个命令里面使用了一个多余的 `/bin/bash`,这个参数是在你想要在容器里面执行一个 shell 命令的时候用的, 但在这里似乎没有什么必要。 正确的命令应该是这样的: ``` docker run -itd --name aMedicalBot -p 5005:5005 -v /opt/python3.6/demo/aMedicalBot_demo_quesoar:/app python:3.8-buster ``` 如果你想要在容器里面执行命令你需要在run 后面加上你需要执行的命令,如下面这样: ``` docker run -itd --name aMedicalBot -p 5005:5005 -v /opt/python3.6/demo/aMedicalBot_demo_quesoar:/app python:3.8-buster /bin/bash ``` ### 回答2: 在这个命令中,有一个语法错误。正确的命令应该是使用-d参数后紧随镜像名称,并且使用--name参数后指定容器名称,然后才是其他的选项参数。 正确的docker命令是: ``` docker run -itd --name aMedicalBot -p 5005:5005 -v /opt/python3.6/demo/aMedicalBot_demo_quesoar:/app python:3.8-buster /bin/bash ``` 修正后的命令将会创建一个名为"aMedicalBot"的容器,使用"python:3.8-buster"镜像,并设置端口映射(将主机的5005端口映射到容器的5005端口),同时将主机路径"/opt/python3.6/demo/aMedicalBot_demo_quesoar"挂载到容器内的"/app"路径下。最后,容器将以交互模式运行并启动"/bin/bash"。 ### 回答3: 原始的docker run命令的语法存在一些错误。正确的命令应该是: docker run -itd --name aMedicalBot -p 5005:5005 -v /opt/python3.6/demo/aMedicalBot_demo_quesoar:/app python:3.8-buster /bin/bash 以下是对命令中的错误进行更正的解释: 1. -p 参数应该位于容器名称(aMedicalBot)和镜像名称(python:3.8-buster)之间。 2. -v 参数与容器名称(aMedicalBot)和镜像名称(python:3.8-buster)之间应该没有空格。 3. 镜像名称(python:3.8-buster)应该紧随 -v 参数之后。 4. 将 /bin/bash 移到镜像名称(python:3.8-buster)之后。 正确的命令将会将容器命名为aMedicalBot,并在主机的端口5005上将主机的/opt/python3.6/demo/aMedicalBot_demo_quesoar目录挂载到容器的/app目录中。容器将使用python:3.8-buster镜像,并在后台运行/bin/bash作为启动命令。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值