LangChain目录
学习资料
模型,提示和输出解析(第1章节)
记忆(第2章节)
链(第3章节)
https://juejin.cn/post/7248599585735114789
基于文档的问答(第4章节)
评估(第5章节)
代理(第6章节)
https://juejin.cn/post/7250669238419439673
langchain官方文档
https://python.langchain.com/docs/modules/model_io/
Langchain Hub网址
https://smith.langchain.com/hub/search?q=hwchase17%2Freact
LangChain模块
LangChain Expression Language (LCEL)
RunnablePassthrough
-
RunnablePassthrough()
获得当前的输入,通常放在链的开头 -
RunnablePassthrough.assign(aa=一个函数)
会将当前的输入给函数,并将当前输入与{“aa”:函数的输出}拼接起来
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
runnable = RunnableParallel(
passed=RunnablePassthrough(),
extra=RunnablePassthrough.assign(mult=lambda x: x["num"] * 3),
modified=lambda x: x["num"] + 1,
)
runnable.invoke({"num": 1})
{'passed': {'num': 1}, 'extra': {'num': 1, 'mult': 3}, 'modified': 2}
RunnableParallel
总的来说,在含有|的链中,用()把链包起来之后就能调用方法了,
- 如果希望一个输入能同时给好几个函数,则把这些函数写在一个字典里
- 如果希望前面的输出能作为下一步的输入、且要把前一步的输出和下一步的输出拼起来,就调assign
一般来收最好把含有|的链用()包起来。
在用|构成的链中,直接写的{}相当于被RunnableParallel包裹起来了
-
RunnableParallel(aa=函数fn1, bb=函数fn2)
同时执行fn1和fn2
-
RunnableParallel({‘aa’:函数fn1, ’bb‘:函数fn2})
同时执行fn1和fn2
在链中,下面三种等价等价:
{"context": retriever, "question": RunnablePassthrough()}
RunnableParallel({"context": retriever, "question": RunnablePassthrough()})
RunnableParallel(context=retriever, question=RunnablePassthrough())
意味着:
def test(){
return "ok'
}
prompt | llm | {"answer": StrOutputParser(), "answer2": test}
等价于
prompt | llm | RunnableParallel({"answer": StrOutputParser(),"answer2": test})
等价于
prompt | llm | RunnableParallel(answer=StrOutputParser(), answer2=test})
说明在|构建的链中,{}里的函数是会收到同样的参数并同时执行的
- 以下6种写法等价
总的来说,还是用()把链包裹起来更好,因为这样就允许|
换行了
第二种写法最简洁、好用、易懂
{"questoin":RunnablePassthrough()} | prompt | llm
({"questoin":RunnablePassthrough()} | prompt | llm) # 用()包起来
RunnableParallel({"questoin":RunnablePassthrough()}) | prompt | llm ## RunnableParallel的几种等价写法
(RunnableParallel({"questoin":RunnablePassthrough()}) | prompt | llm)
RunnableParallel(questoin=RunnablePassthrough()) | prompt | llm
(RunnableParallel(questoin=RunnablePassthrough()) | prompt | llm)
- RunnableParallel().assign(aa=函数fn1, bb=函数fn2)
把RunnableParallel()的输出给函数fn1、fn2,并将输出与{‘aa’:fn1的输出, “bb”:fn2的输出}拼接起来
注意:在|
构建的链中,用()包起来之后就可以东西可以调用方法
下面两种都可以,效果也一样。
({"questoin":RunnablePassthrough()} | prompt | llm).assign()
RunnableParallel({"一个key":({"questoin":RunnablePassthrough()} | prompt | llm)}).assign()
但是({"questoin":RunnablePassthrough()} | prompt | llm)是RunnableSequence
RunnableParallel({"一个key":({"questoin":RunnablePassthrough()} | prompt | llm)})是RunnableParallel
两者都能调用方法,却不是属于同一个类
({"questoin":RunnablePassthrough()} | prompt | llm).assign() 和 RunnableParallel({"一个key":({"questoin":RunnablePassthrough()} | prompt | llm)}).assign() 都是RunnableSequence
# _inputs = RunnableParallel(
# test=RunnablePassthrough.assign(
# question2=lambda x: x["question"] + "ok"
# )
# )
# {'test': {'question': 'where did he work?', 'question2': 'where did he work?ok'}}
# print(_inputs.invoke({"question": "where did he work?"}))
# _inputs = (
# RunnablePassthrough.assign(
# question2=lambda x: x["question"] + "ok"
# )
# )
#
# print(type(_inputs))
_inputs = (
{"test": RunnablePassthrough.assign(
question2=lambda x: x["question"] + "ok"
)}
)
print(type(_inputs)) # <class 'dict'>
print(type(_inputs['test'])) # <class 'langchain_core.runnables.passthrough.RunnableAssign'>
def demo(w):
return w
_inputs = (
{"test": RunnablePassthrough.assign(
question2=lambda x: x["question"] + "ok"
)} | RunnableLambda(demo)
)
print(type(_inputs)) # <class 'langchain_core.runnables.base.RunnableSequence'>
# {'test': {'question': 'where did he work?', 'question2': 'where did he work?ok'}}
print(_inputs.invoke({"question": "where did he work?"}))
Model I/O
Prompt
- few-shot promt,在prompt中添加多个示例
- example selector,从多个实例进行挑选
- message prompt,用于chatmodel的prompt
- partial prompt,部分填充prompt
Chat Model
- 自定一个ChatModel
LLM
- 自定义一个Model
Output Parser
- 对输出进行格式变换,如转成csv
Retrieval
Document loaders
用于加载文档
Text Splitting
用于把大文档分割成一块块小的文档
Text embedding models
生成文本嵌入
Vector stores
存储文本嵌入
Retrievers
检索
- langchain中到底依据什么进行的检索呢?通常都是根据语义相似度来检索的,当然也可以指定使用BM5来检索
- retriever中支持的有:
- Contextual Compression:需要一个基本检索器、一个压缩器,会对基本检索器检索到的原始文档再压缩,得到最最相关的
- Ensemble:有的时候用语义相似度去检索效果好、有的适合用基于BM5的去检索效果好,这个就可以支持指定多个检索器,它返回多个检索器中,检索效果最好的那个检索器的结果
- Long-Context Reorder:当我们检索到很多很多信息给到大模型时,大模型有个特性就是:它更关心靠前、靠后的信息,这个就是改变信息的顺序,把更重要的信息放在靠前、靠后的位置
- 还有很多类型,除了内置的还有很多第三方的。
Indexing
索引,为存储好的嵌入绑定索引
Agents
- Agent Types:Langchain提供了哪些agent
agent跟chain不同的点在于:chain中所有的要做的事情都是硬编码的,但是agent要做的事情由大模型自己决定。
注意有些agent只支持调用需要一个参数的tool,例如我们现在在用的react
agent
agent是让大模型自己取决定用什么工具。在agent系统中,llm充当了agent的大脑,此外agent中还有一些模块,以下内容来自这里:
- planning计划
- 代理将大型任务分解为较小的、可管理的子目标,从而实现对复杂任务的高效处理。
- 对过去的行为进行自我批评和反思,从错误中吸取教训,并为未来的步骤进行提炼,从而提高最终结果的质量。
- memory
- 短期记忆:通过大模型支持的对话历史记录实现
- 长期记忆:通常由外部知识库支撑。这使得大模型拥有在长时间内保留和调用(无限)信息的能力
- tools工具
llm根据目标选择调用外部api来获取大模型中缺乏的信息,例如通过所有工具调用谷歌搜索api获取最新信息 - action
llm基于memory、planning、tool给出的下一步应采取的行动
Chains
LangChain应用
Pdf助手
fastapi服务器
from fastapi import FastAPI, File, UploadFile, Form
from typing import Annotated
import os
from langchain_helper import *
app = FastAPI()
file_upload_path = "./upload"
@app.get("/")
def read_root():
return "hello"
@app.post("/upload") # 上传
def upload_pdf(
file: Annotated[UploadFile, File()],
index_name: Annotated[str, Form()]
):
file_upload_target_path = os.path.join(file_upload_path, file.filename)
with open(file_upload_target_path, "wb") as f:
contents = file.file.read()
f.write(contents)
load_pdf_and_save_to_index(file_upload_target_path, index_name)
return {"filename": file.filename, "index_name": index_name}
from pydantic import BaseModel
class Query(BaseModel):
index_name: str
query: str
@app.post("query") # 查询
def query_index(request: Query):
index_name = request.index_name
query = request.query
ans = query_index_lc(index_name, query)
return {"ans": ans}
启动服务器
uvicorn main:app --reload
langchain_helper.py如下:
主要要注意的是langchain中的类、方法都默认用的是openai的,所以要手动指明大模型是什么
另外:query_with_sources这个方法会自动到hugging_face2上面去下载gpt2的tokenizer,要挂梯子。
from langchain.document_loaders import PyPDFLoader
from langchain.indexes.vectorstore import VectorstoreIndexCreator
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_community.llms import Tongyi
from langchain.vectorstores import Chroma
from langchain.indexes.vectorstore import VectorStoreIndexWrapper
import os
os.environ["DASHSCOPE_API_KEY"] = "sk-dea0261124ec4df6999f6b2ccd26c9ea"
local_persist_path = r"./vector_store"
embeddings = DashScopeEmbeddings(model="text-embedding-v1") # embedding模型
llm = Tongyi(model_name="qwen-max") # Qwen
def get_index_path(index_name):
return os.path.join(local_persist_path, index_name)
def load_pdf_and_save_to_index(file_path, index_name):
loader = PyPDFLoader(file_path) # 加载文档
# VectorstoreIndexCreator能完成文档分割、向量化、存储
index = VectorstoreIndexCreator(embedding=embeddings,
vectorstore_kwargs={"persist_directory": get_index_path(index_name)}).from_loaders(
[loader])
index.vectorstore.persist() # 保存到本地
def load_index(index_name):
# 从磁盘中读取index
index_path = get_index_path(index_name)
vector_db = Chroma(
persist_directory=index_path,
embedding_function=embeddings
)
return VectorStoreIndexWrapper(vectorstore=vector_db)
def query_index_lc(index, query):
ans = index.query_with_sources(query, llm=llm, chain_type='map_reduce')
return ans["answer"]
# index_name = 'first'
# index = load_index(index_name, embeddings)
# print(index.query_with_sources("entity", llm=llm, chain_type='map_reduce'))
agent初级使用
用agent做搜索
serpapi需要api-key
from langchain_community.chat_models.tongyi import ChatTongyi
from langchain import hub
from langchain.agents import AgentExecutor, create_react_agent, load_tools
import os
# 使用灵积云的key调用qwen
os.environ["DASHSCOPE_API_KEY"] = "sk-dea0261124ec4df6999f6b2ccd26c9ea"
chatLLM = ChatTongyi(
streaming=True,
model_name='qwen-max'
)
tools = load_tools(["serpapi", "llm-math"], llm=chatLLM)
# Get the prompt to use - you can modify this!
prompt = hub.pull("hwchase17/react")
# Construct the ReAct agent
agent = create_react_agent(chatLLM, tools, prompt)
# Create an agent executor by passing in the agent and tools
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
ans1 = agent_executor.invoke({"input": "谁是高启强的扮演者"})
print(ans1)
ans2 = agent_executor.invoke({"input": "高启强的扮演者10年后多少岁?"})
print(ans2)
新闻变相声(gradio展示)
- 先写好输入url、输出相声剧本的代码
from langchain_community.llms import Tongyi
import os
os.environ["DASHSCOPE_API_KEY"] = "sk-dea0261124ec4df6999f6b2ccd26c9ea"
llm = Tongyi(model_name="qwen-max") # Qwen
import bs4
from langchain_community.document_loaders import WebBaseLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains.summarize import load_summarize_chain
from langchain.prompts import PromptTemplate
from pydantic import BaseModel, Field
from langchain.output_parsers import PydanticOutputParser
class Line(BaseModel):
character: str = Field(description="说这句话的角色名字")
content: str = Field(description="台词的具体内容,不包括角色的名字")
class XiangSheng(BaseModel):
script: list[Line] = Field(description="一段相声的台词剧本")
def url2doc(url):
loader = WebBaseLoader(
web_paths=(url,),
bs_kwargs=dict(
parse_only=bs4.SoupStrainer(
class_=("post-content", "post-title", "post-header") # 只要类名是指定这些的
)
),
)
loader.load()
text_splitter = RecursiveCharacterTextSplitter(separators=["\n\n"], chunk_size=100, chunk_overlap=20,
length_function=len)
data = loader.load_and_split(text_splitter=text_splitter)
return data[10:20]
def url2script(url, requirement):
map_prompt_template = """总结下列内容:
"{text}"
总结的内容是:"""
MAP_PROMPT = PromptTemplate(template=map_prompt_template, input_variables=["text"])
combine_prompt_template = """总结下列内容:
"{text}"
总结的内容是:"""
COMBINE_PROMPT = PromptTemplate(template=combine_prompt_template, input_variables=["text"])
# 使用map_reduce:
# sum_chain = load_summarize_chain(llm, chain_type='map_reduce', map_prompt=MAP_PROMPT, combine_prompt=COMBINE_PROMPT)
# 使用stuff
sum_chain = load_summarize_chain(llm)
parser = PydanticOutputParser(pydantic_object=XiangSheng)
template = """
我将给你一段文字,请你帮我把这段文字改写成郭德纲和于谦的对口相声剧本。
文字:{content}
要求:{requirement}
{output_instruction}
"""
prompt = PromptTemplate.from_template(
template=template,
input_bariables=["content", "requirement"],
partial_variables={"output_instruction": parser.get_format_instructions()}
)
chain = (prompt | llm | parser)
script_ = chain.invoke({"content": sum_chain.invoke(url2doc(url)), "requirement": requirement})
return script_
# url = "https://lilianweng.github.io/posts/2023-06-23-agent/"
# script = url2script(url, "幽默风趣")
# print(script)
- 写gradio页面的代码
import gradio as gr
from langchain_helper import *
with gr.Blocks() as demo:
url = gr.Textbox()
chatbot = gr.Chatbot()
submit_btn = gr.Button('生成相声')
def generate_conversation(url):
xiangsheng: XiangSheng = url2script(url, "幽默风趣")
chat_history = []
def parse_line(line: Line):
"""
parse Lineto String
:param line:
:return: str
"""
if line is None:
return ''
return f"{line.character}:{line.content}"
for i in range(0, len(xiangsheng.script), 2):
line1 = xiangsheng.script[i]
line2 = xiangsheng.script[i + 1] if (i + 1) < len(xiangsheng.script) else None
chat_history.append((parse_line(line1), parse_line(line2)))
return chat_history
submit_btn.click(fn=generate_conversation, inputs=url, outputs=chatbot)
if __name__ == "__main__":
demo.launch()