Runnable interface
许多 LangChain 组件都实现了Runnable
协议,包括聊天模型、LLMs、输出解析器、检索器、提示模板等等。此外,还有一些有用的基本组件可用于处理可运行对象。 这是一个标准接口,可以轻松定义自定义链并以标准方式调用它们。 标准接口包括:
stream
: 返回响应的数据块invoke
: 对输入调用链batch
: 对输入列表调用链
这些还有相应的异步方法,应该与 asyncio 一起使用await语法以实现并发astream
: 异步返回响应的数据块ainvoke
: 异步对输入调用链abatch
: 异步对输入列表调用链astream log
: 异步返回中间步骤,以及最终响应astream_events
:beta 流式传输链中发生的事件(在langchain-core 0.1.14
中引入)
不同的组件,它的输入和输出的类型是不同的
组件 | 输入类型 | 输出类型 |
---|---|---|
Prompt | 字典 | PromptValue |
ChatModel | 单个字符串、聊天消息列表或 PromptValue | ChatMessage |
LLM | 单个字符串、聊天消息列表或 PromptValue | 字符串 |
OutputParser | LLM 或 ChatModel 的输出 | 取决于解析器 |
Retriever | 单个字符串 | 文档列表 |
Tool | 单个字符串或字典,取决于工具 | 取决于工具 |
所有可运行对象都公开输入和输出的模式以检查输入和输出: |
input_schema
: 从 Runnable 的结构动态生成的输入 Pydantic 模型output_schema
: 从 Runnable 的结构动态生成的输出 Pydantic 模型
通过对可运行对象的输入输出模式的检查,我们就可以知道这个chain需要的输入格式和返回的输出格式,我们可以根据输入格式填入相应的数据,将其通过链式处理,得到预想的输出结果格式
我们以简单的 prompt | model | output_parser 为例
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI(model="gpt-4o-mini")
template = "给我一个关于{topic}的故事"
prompt = ChatPromptTemplate.from_template(template)
output_parser = StrOutputParser()
chain = prompt | model | output_parser
我们来看chain的输入模式
chain.input_schema.schema()
{'properties': {'topic': {'title': 'Topic', 'type': 'string'}},
'required': ['topic'],
'title': 'PromptInput',
'type': 'object'}
可以看到整个chain的输入模式其实是chain的第一部分(prompt)的输入模式,需要一个string类型的topic变量
接下来是输出模式
chain.output_schema.schema()
{'title': 'StrOutputParserOutput', 'type': 'string'}
chain的输出模式其实是chain的最后一个部分(output_parser)的输出模式,表示返回一个string类型的字符串
invoke 、 Batch
invoke只能处理一个输入,而batch可以批量处理一个输入列表中的各个输入
chain.invoke({"topic":"狗"})
'小狗勇救落水主,被主人抱紧不撒手。'
chain.batch([{"topic":"猪"},{"topic":"猫"}])
['小猪努力奋斗,终于成为养猪达人。', '猫咪误入古宅,发现藏宝图,终成富翁。']
Stream
所有可以运行的任务(Runnable对象)都提供了一个名为stream
的同步方法和一个名为astream
的异步变体。这些方法的目标是逐步以块的形式传输最终输出,并尽早返回每个块。要进行流式传输,程序中的所有步骤都必须能够处理输入流,这包括按顺序处理输入块,并为每个块产生相应的输出块。流式传输的复杂性取决于具体任务的难度,从简单的生成LLM生成的令牌开始,到复杂到需要在整个JSON完成之前分部分传输JSON结果的部分。为了了解流式传输的最优方法,最好从LLM的应用程序中最重要的一环——LLM本身——开始学习。
流式运行是让基于LLM的应用程序对最终用户表现出响应性的关键。大多数LangChain原语,如聊天模型、输出解析器、提示模板、检索器和代理,都实现了LangChainRunnable
接口。该接口提供了两种通用的流式内容方法:
-
同步
stream
方法和异步astream
方法:这是链式结构最终输出的默认实现方式。 -
异步
astream events
和异步astream log
方法:这些方法提供了一种能够从链中流式传输中间步骤和最终输出的方式。
for s in chain.stream({"topic": "书包"}):
print(s, end="|", flush=True)
书包|里|藏着|秘密|,|承载|着|青春|梦想|。||
服务端会以数据流的方式,每一次返回一个数据块,这里特意使用"|"将整个数据流拼接起来,可以更明显的感受到每个数据块的接收
我们也可以使用异步的方式来调用大模型
async for s in chain.astream({"topic": "书包"}):
print(s, end="|", flush=True)
ainvoke 和 abatch 也是同理
异步方式在一个单任务执行看不出来效果。在多个任务并发的进行时,如果各个任务之间彼此独立,便可以采用异步的方式,利用多个线程更快得执行这些任务
Stream envent (事件流)
LangChain的链式调用使用的是事件处理机制
当使用 astream_events API 时,请确保以下所有内容都能正常工作:
- 在整个代码中尽可能使用
async
(包括异步工具等) - 如果定义自定义函数/运行器,请传递回调。
- 每当使用不是 LCEL 上的运行器时,请确保在 LLM 上调用
.astream()
而不是.ainvoke
以强制 LLM 流式传输令牌。
事件参考
下面是一个参考表,显示了各种 Runnable 对象可能发出的一些事件。
表后面包含一些 Runnable 的定义。
当流式处理时,输入的可运行对象将在输入流被完全消耗之后才可用。这意味着输入将在对应的end
钩子而不是start
事件中可用。
事件 | 名称 | 块 | 输入 | 输出 | |
---|---|---|---|---|---|
on_chat_model_start | [model name] | {“messages”: SystemMessage, HumanMessage} | |||
on_chat_model_stream | [model name] | AIMessageChunk(content=“hello”) | |||
on_chat_model_end | [model name] | {“messages”: SystemMessage, HumanMessage} | {“generations”: […], “llm_output”: None, …} | ||
on_llm_start | [model name] | {‘input’: ‘hello’} | |||
on_llm_stream | [model name] | ‘Hello’ | |||
on_llm_end | [model name] | ‘Hello human!’ | |||
on_chain_start | format_docs | ||||
on_chain_stream | format_docs | “hello world!, goodbye world!” | |||
on_chain_end | format_docs | [Document(…)] | “hello world!, goodbye world!” | ||
on_tool_start | some_tool | {“x”: 1, “y”: “2”} | |||
on_tool_stream | some_tool | {“x”: 1, “y”: “2”} | |||
on_tool_end | some_tool | {“x”: 1, “y”: “2”} | |||
on_retriever_start | [retriever name] | {“query”: “hello”} | |||
on_retriever_chunk | [retriever name] | {documents: […]} | |||
on_retriever_end | [retriever name] | {“query”: “hello”} | {documents: […]} | ||
on_prompt_start | [template_name] | {“question”: “hello”} | |||
on_prompt_end | [template_name] | {“question”: “hello”} | ChatPromptValue(messages: [SystemMessage, …]) |
示例
我们可以看到这个chain进行链式调用时,各个事件的输入输出以及响应处理
所以事件机制更方便于我们进行日志的打印分析,代码的调试优化
events = []
# version指得是event的版本 , 有"v1","v2"两种
async for event in chain.astream_events("hello",version="v2"):
events.append(event)
print(events)
[{'event': 'on_chain_start', 'data': {'input': 'hello'}, 'name': 'RunnableSequence', 'tags': [], 'run_id': '52e6976d-e5ad-4d19-8af1-c7c9841de891', 'metadata': {}, 'parent_ids': []}, {'event': 'on_prompt_start', 'data': {'input': 'hello'}, 'name': 'ChatPromptTemplate', 'tags': ['seq:step:1'], 'run_id': '50d7f5c7-c323-419b-9b72-96f72d39b1c5', 'metadata': {}, 'parent_ids': ['52e6976d-e5ad-4d19-8af1-c7c9841de891']}, {'event': 'on_prompt_end', 'data': {'output': ChatPromptValue(messages=[HumanMessage(content='给我一个关于hello的故事,20字左右', additional_kwargs={}, response_metadata={})]), 'input': 'hello'}, 'run_id': '50d7f5c7-c323-419b-9b72-96f72d39b1c5', 'name': 'ChatPromptTemplate', 'tags': ['seq:step:1'], 'metadata': {}, 'parent_ids': ['52e6976d-e5ad-4d19-8af1-c7c9841de891']}, {'event': 'on_chat_model_start', 'data': {'input': {'messages': [[HumanMessage(content='给我一个关于hello的故事,20字左右', additional_kwargs={}, response_metadata={})]]}}, 'name': 'ChatZhipuAI', 'tags': ['seq:step:2'], 'run_id': '30be109e-6ed9-4663-af95-e07b18efad97', 'metadata': {'ls_provider': 'zhipuai', 'ls_model_type': 'chat', 'ls_model_name': 'glm-4-flash', 'ls_temperature': 0.95}, 'parent_ids': ['52e6976d-e5ad-4d19-8af1-c7c9841de891']},
此处过长,我只列举了一部分,大家就可以自己运行一下代码进行查看
并行处理
当使用RunnableParallel
(通常写成字典形式)时,它会并行执行每个元素。
from langchain_core.runnables import RunnableParallel
chain1 = ChatPromptTemplate.from_template("告诉我一个关于{topic}的笑话,20字以内") | model | output_parser
chain2 = (
ChatPromptTemplate.from_template("给我一首关于{topic}的短诗(2行)")
| model
| output_parser
)
combined = RunnableParallel(joke=chain1, poem=chain2)
%%time
combined.invoke({"topic":"兔子"})
CPU times: total: 859 ms
Wall time: 1.31 s
{'joke': '兔子跳得高,结果把尾巴摔断了。', 'poem': '碧野跳兔影,清风送欢歌。'}
%%time
chain1.invoke({"topic":"兔子"})
CPU times: total: 250 ms
Wall time: 1.1 s
'兔子被狗追赶,兔子说:“你不会追上我的,我还会变兔子呢!”'
%%time
chain2.invoke({"topic":"兔子"})
CPU times: total: 250 ms
Wall time: 1.52 s
'玉兔跃枝头,欢舞踏春风。'
可以看到并行处理的速度要优于两个任务单独处理
并行处理可以与其他可运行对象结合使用。 例如我们尝试将并行处理与批处理结合使用。
%%time
chain1.batch([{"topic": "熊"}, {"topic": "猫"}])
CPU times: total: 906 ms
Wall time: 1.5 s
['熊在河边钓鱼,钓到了一条鱼,鱼说:“我是熊,你放了我吧!”', '猫:我不是懒,只是在思考猫生。']
%%time
chain2.batch([{"topic": "熊"}, {"topic": "猫"}])
CPU times: total: 922 ms
Wall time: 1.41 s
['熊步山林静,冬日暖阳融。', '懒猫卧窗前,午后梦悠然。']
%%time
combined.batch([{"topic": "熊"}, {"topic": "猫"}])
CPU times: total: 2.48 s
Wall time: 1.87 s
[{'joke': '熊看到镜子里的自己,说:“哟,这是谁家的狗啊?”', 'poem': '憨态可掬熊宝宝,林间漫步笑声多。'},
{'joke': '猫:你给我按个爪印,我就告诉你秘密。', 'poem': '懒卧窗台晒夕阳,悠然自在猫影摇。'}]