Langchain2024大语言模型应用开发框架精选深度对比权威测评榜单推荐
聊到大型语言模型的应用落地,Langchain 几乎是一个绕不开的名字。它就像一个万能工具箱,把调用大模型、管理提示词、连接外部工具这些琐碎又关键的事情,都给你规整得明明白白。今天,咱们就深入聊聊这个框架,看看它到底解决了什么问题,以及在实际项目中,我们能拿它来做什么。
LLM的应用开发框架——Langchain
一. Langchain是什么
Langchain 官网文档:https://python.langchain.com/v0.2/docs/introduction/
LLM崛起出现了哪些需求?
随着大模型的能力越来越强,我们很快发现,光有模型本身是不够的。直接提问,得到的答案格式千奇百怪;想让它总结本书,提示词长得突破天际;希望它去网上查点实时信息,模型又无能为力。于是,一系列现实需求就浮出水面了:
- 格式化输出:我们想要的是结构化的JSON、CSV,而不是大段大段的散文。
- 处理长文本:比如要浓缩一本几十万字的小说,不能让模型一次性读进去,得想个办法。
- 多步推理与API调用:有时候需要模型先算一算,再根据结果去查个东西,然后综合给出答案,这涉及多次、有依赖关系的API调用。
- 打通外部世界:模型的知识是有截止日期的,让它能联网搜索、查询数据库,能力才能边界拓展。
- 标准化与可切换性:今天是GPT-4,明天可能换用百川或GLM。代码应该能平滑迁移,而不是因为换了个模型就得重写一遍。
这些,正是Langchain试图一揽子解决的痛点。
二. Langchain支撑LLM的应用
下面,咱们就通过几个最典型的应用场景,看看Langchain具体是怎么落地的。
2.1 支持多种LLM
无论是OpenAI的GPT-4,Meta的LLaMA,还是国内的ChatGLM、百川,Langchain都提供了统一的接口来调用。这一点对于团队快速迭代模型,进行A/B测试非常关键。下面就以从Huggingface下载并使用百川2模型为例,展示一个自定义LLM类的写法。
from huggingface_hub import snapshot_download
from langchain.llms.base import LLM
# 指定下载目录(在当前文件夹下)
snapshot_download(repo_id="baichuan-inc/Baichuan2-7B-Chat-4bits", local_dir="baichuan-inc/Baichuan2-7B-Chat-4bits")
class baichuan2_LLM(LLM):
# 基于本地 Baichuan 自定义 LLM 类
tokenizer: AutoTokenizer = None
model: AutoModelForCausalLM = None
def __init__(self, model_path: str, dtype = torch.bfloat16):
# model_path: Baichuan-7B-chat模型路径
# 从本地初始化模型
super().__init__()
print("正在从本地加载模型...")
self.tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
self.model = AutoModelForCausalLM.from_pretrained(model_path, trust_remote_code=True,
torch_dtype=dtype, device_map="auto")
self.model.generation_config = GenerationConfig.from_pretrained(model_path)
self.model = self.model.eval()
print("完成本地模型的加载")
def _call(self, prompt: str, stop: Optional[List[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any):
# 重写调用函数
messages = [
{"role": "user", "content": prompt}
]
# 重写调用函数
response = self.model.chat(self.tokenizer, messages)
return response
@property
def _llm_type(self) -> str:
return "baichuan2_LLM"
2.2 零样本少样本提示
一个经常被忽视的原则是:能用Prompt解决的,就别急着去微调模型。这能节省大量成本和时间。对于少样本甚至零样本的场景,Langchain提供了非常便捷的封装。通过给模型几个经典的“提问-回答”案例,就能引导它学会特定的回复风格。
from langchain.prompts.few_shot import FewShotPromptTemplate
examples = [
{
"question": "你好吗?",
"answer": "帅哥,我很好"
},
{
"question": "今天周几?",
"answer": "帅哥,今天周日"
},
{
"question": "天气好吗?",
"answer": "帅哥,是的,今天天气很不错"
}
]
example_prompt = PromptTemplate(input_variables=["question", "answer"], template="提问: {question} n回答:{answer}")
prompt = FewShotPromptTemplate(examples=examples,
example_prompt=example_prompt,
suffix="提问: {input} n",
input_variables=["input"])
print(prompt.format(input="我怎么这么丑"))
# 这里相当于将前缀的这些少样本和当前问题全部放入llm,让其知道前后关系或者学习规则
print(llm.predict((prompt.format(input="我怎么这么丑"))))
2.3 文档问答
这是目前最成熟、应用最广的场景之一,典型代表就是 LangChain + ChatGLM 的本地知识库方案(GitHub - chatchat-space/Langchain-Chatchat)。整体逻辑非常清晰:
核心流程如下:
- 加载:用Loader读取本地文档(PDF、TXT等)。
- 分割:因为大模型有上下文窗口限制,需要把长文档切成固定大小的chunk。
- 向量化:利用Embedding模型将这些chunk转换成向量。
- 存库:将向量存入向量数据库(如Chroma、FAISS)。
- 检索:对于用户的提问,同样向量化,然后去库里搜索最相似的几个chunk。
- 生成:将检索回来的chunk和提问一起,打包成Prompt交给大模型,生成最终答案。
Langchain的强大之处在于它已经把这些步骤都模块化了。在这个过程中,有两个关键点决定了最终效果:Embedding模型的召回率和LLM的回答能力。遇到bad case时,可以先定位到底是检索环节没找到相关信息,还是模型在理解上出了问题。下面是用《庆余年》文本做的一个简单演示,LLM和Embedding模型都来自Huggingface。
from langchain.indexes.vectorstore import VectorStoreIndexWrapper
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores import FAISS
from langchain_community.document_loaders import TextLoader
def load_text_sa ve_index(file_path, index_name):
loader = TextLoader(file_path)
text_qyn = loader.load()
splitter = RecursiveCharacterTextSplitter(
chunk_size=100, # 分割出来的文本长度
chunk_overlap=10, # 块之间的重叠文字
length_function=len, # 计算每个块的长度
add_start_index=True # 决定是否在metadata中包含每个块在原始文档中的起始位置
)
texts = splitter.split_documents(text_qyn)[:100]
faiss_db = FAISS.from_documents(texts, hf_embeddings)
faiss_db.sa ve_local(os.path.join(local_persist_path, index_name))
print('faiss db sa ved !')
def load_index(index_name):
index_path = os.path.join(local_persist_path, index_name)
faiss_db = FAISS.load_local(index_path, embeddings = hf_embeddings, allow_dangerous_deserialization=True)
index = VectorStoreIndexWrapper(vectorstore = faiss_db)
return index
file_path = '庆余年.txt'
local_persist_path = './vector_store'
# load_text_sa ve_index(file_path, index_name='庆余年')
index = load_index(index_name='庆余年')
result = index.query("五竹是谁", llm=llm)
print(result)
2.4 搜索助手
这个功能让模型的“知识边界”无限扩展。通过Langchain的Agent模块,模型可以自主判断何时需要调用外部工具(比如搜索引擎),然后获取实时信息并整合答案。需要申请一个搜索API的key,例如SerpAPI。
from langchain.agents import initialize_agent
from langchain_community.agent_toolkits.load_tools import load_tools
tools = load_tools(['serpapi', 'llm-math'], llm=llm)
print(tools[1].name, tools[1].description)
agent = initialize_agent(tools, llm, agent = 'zero-shot-react-description', verbose=True)
agent.run('苹果的CEO,他10年后多少岁?')
2.5 文章总结
面对超长文章,一次性塞给模型必然导致内存溢出。Langchain提供了三种经典的“总结链”来处理这个问题:
- Stuff:简单粗暴,将所有chunk拼在一起一次性总结。最直接,但受限于上下文窗口。
- Map Reduce:先对每个chunk单独总结,然后再将所有总结结果进行第二次汇总。这种策略更通用,效果也更好。
- Refine:迭代式总结。模型会逐个读取chunk,并在阅读每个新chunk时,对已有的总结进行修正和补充。适合需要不断迭代深化的场景。
下面是一个对网页新闻进行汇总的代码示例:
from langchain_community.document_loaders import UnstructuredURLLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains.summarize import load_summarize_chain
from langchain_core.prompts import PromptTemplate
def load_news(url):
text_splitter = RecursiveCharacterTextSplitter(
# separators=['正文', '撰稿'],
chunk_size=300,
chunk_overlap=10)
loader = UnstructuredURLLoader([url])
data = loader.load_and_split(text_splitter)
print(f'doc lenth is {len(data)}')
return data
def summary_news():
map_prompt_temp = """总结这段新闻的内容在50字以内:{text}, 总结:"""
ch_prompt = PromptTemplate(template=map_prompt_temp, input_variables=['text'])
chain = load_summarize_chain(llm, chain_type='map_reduce', map_prompt=ch_prompt, combine_prompt=ch_prompt)
# summary = chain_ch.run(doc)
summary = chain.invoke({"input_documents": doc})['output_text']
print(summary) # 这里展示的是中文的总结
2.6 输出解析
很多时候,我们需要的不是一串文字,而是结构化的数据。Langchain中的输出解析器能让模型按照我们预设的格式,比如生成一个JSON对象或一个剧本格式。下面是利用Pydantic定义剧本结构,并让模型根据一段文章生成剧本的示例。
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field
from typing import List
# 注意:这里的BaseModel的类的description 不能用中文,因为到template的prompt的时候,会乱码,导致模型不懂描述的是什么
class Line(BaseModel):
# character:str = Field(description=u"说这句台词的角色名字",)
character: str = Field(description=u"The name of the character who said this line",)
# content:str = Field(description=u"台词的具体内容,其中不再包含角色的名字")
content: str = Field(description=u"The specific content of the line, which no longer contains the character's name")
class JuBen(BaseModel):
# script: List[Line] = Field(description=u"一段的台词剧本")
script: List[Line] = Field(description=u"A talk script")
def parse_process():
temp = """我将给你一段文章,请按照要求把这段文章改写成一个电视剧的剧本。
文章:"{docs}"
要求:"{request}"
{output_instructions}
"""
parser = PydanticOutputParser(pydantic_object=JuBen)
prompt = PromptTemplate(template=temp,
input_variables=['docs', 'request'],
partial_variables={"output_instructions": parser.get_format_instructions()},
# pattern = re.compile('n')
)
jb_content = prompt.format_prompt(docs=docs, request="风格大胆悲情,剧本对话角色不少于三个人,以他们的自我介绍为开头")
# msg = [HumanMessage(content=jb_content)]
# rs = llm.predict_messages(msg)
rs = llm(jb_content.to_string())
jb = parser.parse(rs)
# chain = jb_prompt | llm | parser
# xiangsheng = chain.invoke({
# "docs" : docs,
# "request" : "风格大胆悲情,剧本对话角色不少于三个人,以他们的自我介绍为开头"
# })
# print(jb)
return jb
