LangChain学习
LangChain是什么?
首先LangChain和LangGraph一样,其实都是为了把模型、工具、第三方服务和各种组件组合起来,也就是官方描述的构建 Agent 和 LLM 应用 的框架,也就是提供一个框架,解决了很多原本需要程序员手动写的胶水代码。
LangChain和LangGraph的区别是什么?
那它和LangGraph的区别其实从字面上来看,最大的区别首先是链接方式,LangChain明显采用的是Chain去链接(|),而对于LangGraph来说它更像是一张图,通过节点和边构建出来。这就决定了二者控制力不一样,适合的复杂度不一样。说简单点如何区分呢,LangChain 更偏“快速把大模型应用拼起来”,LangGraph 更偏“精细控制复杂 Agent 的每一步”。
举个例子
假设你要做一个"AI 旅行助手"
A:用LangChain
用户说需求 → 查航班 → 查酒店 → 生成推荐
B:用LangGraph
这个项目实现可以参考文章 https://www.cnblogs.com/LiYellowDuck/p/19969479 末尾补充代码
用户说需求
→ 判断预算
→ 查航班
→ 查酒店
→ 如果超预算,重新规划
→ 如果时间冲突,换方案
→ 如果用户确认,才下单
→ 如果接口失败,从失败点继续
读懂LangChain
1.常用头文件
from __future__ import annotations
import os
from typing import TypedDict
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda
from langchain_core.runnables import RunnableParallel
from langchain_openai import ChatOpenAI
2.RunnableLambda
把普通 Python 函数包装成 LangChain 能串起来的步骤。官方文档也说,可以用 RunnableLambda 把自定义函数变成 Runnable。类似LangGraph,我们可以把自定义函数变成节点放入图中,这里可以把自定义函数变成Runnable,放入链中。
LangChain:
函数 → Runnable → 放进链
LangGraph:
函数 → Node 节点 → 放进图
3.lambda表达式
lambda 表达式就是临时写一个很短的小函数。它适合,函数很简单,只用一次,懒得专门 def 一个名字。
#比如你想写一个“取价格”的函数:
def get_price(x):
return x["price"]
#lambda写法
lambda x: x["price"]
#可以理解为lambda 参数: 返回值
#lambda表达式经常用于min,max等比较函数中一般写法为min(flight,key=lambda x : x["price"])
'''
flight
[
{
"name":"..."
"message":"..."
"price":"..."
},#元组即为遍历的x1,x2,x
{
...
},
]
'''
4.**data用法
把 data 这个字典拆开,铺到新的字典里,常用于返回
#例如
return {
--------------------------------------------------
**data,
--------------------------------------------------
#这里就是展开data字典 与后续的进行拼接
'''
data {
"user_need": user_need,
"origin": "Beijing",
"destination": "Tokyo",
"nights": 3,
"budget": 6000,
}
'''
---------------------------------------------------
"flight": selected_flight,
---------------------------------------------------
#假定的select_flight
'''
selected_flight {
"id": "H002",
"name": f"{destination} Budget Inn",
"nights": nights,
"price": 2800,
}
'''
}
===================================================
#最终return
{
"user_need": user_need,
"origin": "Beijing",
"destination": "Tokyo",
"nights": 3,
"budget": 6000,
"flight":{
"id": "H002",
"name": f"{destination} Budget Inn",
"nights": nights,
"price": 2800,
}
}
5.结构化输出(介绍常用输出)
(1)如何定义输出模板(为后续模板输出做准备)
#三种结构模板
TypedDict
Pydantic BaseModel
JSON Schema
这三者都是为了写一个模板 TypeDict 是字典模板,返回的是字典,运行时的类型校验没有 Pydantic 强。
运行时校验 说人话:程序真的检查一遍:“你这个分数是不是数字?情绪是不是只能正面/负面/中性?”
而pydantic是更严格的表格模板,返回的是Pydantic对象
关于JSON Schema模板略有区别,后续用另一篇博客说明
#关于它们的用法
#A.TypeDict
class ReviewResult(TypedDict):
sentiment: Literal["正面", "负面", "中性"]
score: int
reason: str
#B.Pydantic
from pydantic import BaseModel, Field
from typing import Literal
#ps:str不需要模板 如果很难理解可以先去看str
class ReviewResult(BaseModel):
sentiment: Literal["正面", "负面", "中性"] = Field(description="评论情绪")
score: int = Field(description="1到5分")
reason: str = Field(description="判断原因")
#这里的literal的意义是列表里只能选取一个
如果不想看太多这里给一个总结,方便后续阅读:
TypedDict -> with_structured_output
BaseModel -> PydanticOutputParser
普通 JSON -> JsonOutputParser
普通文字 -> StrOutputParser
(2)TypedDict -> with_structured_output
#适合什么?
#想要如下结构:
{
"sentiment": "正面",
"score": 4,
"reason": "音质不错,但戴久了夹耳朵"
}
#如何使用?
from typing import Literal
from typing_extensions import TypedDict
from langchain_openai import ChatOpenAI
class ReviewResult(TypedDict):
sentiment: Literal["正面", "负面", "中性"]
score: int
reason: str
#先前的模板 不再赘述
llm = ChatOpenAI(...)
#定义模型
structured_llm = llm.with_structured_output(ReviewResult)
#核心
result = structured_llm.invoke(
"分析评论:这个耳机音质不错,但是戴久了有点夹耳朵。"
)
print(result)
print(result["sentiment"]) #核心
print(result["score"])
print(result["reason"])
返回dict!!!!
(3)BaseModel -> PydanticOutputParser
#适合什么?
#适合你想让输出更严格。
#BaseModel 通俗来说区别就是"更严格的表格模板"
#你规定 score 必须是整数,sentiment 只能是固定几个词,它会帮你检查。
#PydanticOutputParser 就是"把模型回答收拾成 Pydantic 对象"的工具。
#如何使用?
from typing import Literal
from pydantic import BaseModel, Field
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import PydanticOutputParser
class ReviewResult(BaseModel):
sentiment: Literal["正面", "负面", "中性"] = Field(description="评论情绪")
score: int = Field(description="1到5分")
reason: str = Field(description="判断原因")
#先前模板
llm = ChatOpenAI(model="gpt-4o-mini")
#同上
parser = PydanticOutputParser(pydantic_object=ReviewResult)
#重点不同 使用parser解析器
prompt = ChatPromptTemplate.from_template("""
请分析下面评论:
评论:
{review}
请严格按下面格式输出:
{format_instructions}
""").partial(
format_instructions=parser.get_format_instructions()
)
chain = prompt | llm | parser
result = chain.invoke({
"review": "这个耳机音质不错,但是戴久了有点夹耳朵。"
})
print(result)
print(result.sentiment)
print(result.score)
print(result.reason) #注意输出区别 可与上面对比
返回Pydantic对象,也就是这里的ReviewResult
(4)StrOutputParser(无需模板,返回str)
输出一段文本时使用
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_template(
"用一句话解释:{topic}"
)
chain = prompt | llm | StrOutputParser() #核心
result = chain.invoke({
"topic": "LangChain"
})
print(result)
6.LCEL 与链式编排
什么是LCEL,全称是LangChain Expression Language,LangChain的表达语言,其实也就与我们熟悉的 c ,c++ , python , java 一样,它们都有各自的语法,这个就是为了使用LangChain的语法
下面介绍常见的:
invoke,单次调用链,在上面的代码中一般是跟在chain.invoke,形象一点理解这个就是我们的输出,和input以及cin以及scanf就我目前的知识来看似乎差不多。
from __future__ import annotations
import os
from typing import TypedDict
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda
from langchain_core.runnables import RunnableParallel
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
load_dotenv()
prompt = ChatPromptTemplate.from_messages(
[
("system", "你是一个讲笑话的专家,你总会用你的笑话逗得别人哈哈大笑"),
("human", "帮我讲一个关于{human_input}的笑话"),
]
)
llm = ChatOpenAI(
model=os.getenv("DASHSCOPE_MODEL"),
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL"),
)
chain = prompt | llm | StrOutputParser()
result = chain.invoke(
{
"human_input": "咖啡"
}
)
print(result)
点击查看返回结果
哈哈,来一个咖啡界的“冷萃”笑话!
有一天,一杯浓缩咖啡走进酒吧,气势汹汹地对酒保说:“来杯水!”
酒保吓了一跳:“你干嘛?你是咖啡,不是人!”
浓缩咖啡翻了个白眼:“我当然知道我是咖啡!但我已经熬了一整夜了,现在急需兑点水冷静一下……再不休息,我就要‘萃’不住了!”
(沉默两秒)
酒保点点头:“行吧,那你这杯……算‘啡’常敬业。”
😂☕
怎么样?是不是笑得比咖啡因还上头?要不要再来一杯“笑”力十足的?
batch,批量调用链,与上面同理不过增加一点,需要使用RunnableParallel,使得批量传输的并行执行
#其他同上 这里有小变化 可以仔细观察
parellel_chain = RunnableParallel(
joke1 = chain,
joke2 = chain,
)
result = parellel_chain.batch([
{"human_input": "咖啡"},
{"human_input": "啤酒"},
])
print(result[0]["joke1"])
print(result[1]["joke2"])
点击查看返回结果
哈哈,来一个咖啡界的“冷萃”笑话!
有一天,一杯浓缩咖啡走进酒吧,气势汹汹地对酒保说:“来杯水!”
酒保吓了一跳:“你干嘛?你是咖啡,不是人!”
浓缩咖啡翻了个白眼:“我当然知道我是咖啡!但我已经熬了一整夜了,现在急需兑点水冷静一下……再不休息,我就要‘萃’不住了!”
(沉默两秒)
酒保点点头:“行吧,那你这杯……算‘啡’常敬业。”
😂☕
怎么样?是不是笑得比咖啡因还上头?要不要再来一杯“笑”力十足的?
哈哈,来一个我最近在酒吧收集的“精酿”笑话!
有一天,一瓶啤酒走进了酒吧。
酒保一看,吓了一跳:“哎?你咋自己走来的?”
啤酒叹了口气说:“别提了,我被‘甩了’。”
酒保纳闷:“你一个啤酒,谁甩你?”
啤酒苦着脸:“我女朋友……她爱上了一瓶威士忌。”
酒保安慰道:“别难过,至少你还‘有泡沫’(有希望)!”
啤酒一听,眼泪差点从瓶口冒出来:“可她嫌我‘不够烈’……说我人生没‘酒精度’!”
酒保拍拍它肩膀:“兄弟,那你得发酵一下自己,别老是‘常温存放’!”
——笑点在哪?
在中文谐音啊!“有泡沫”=“有希望”,“酒精度”=“人生高度”,这不就是现代人的情感写照吗?
连啤酒都逃不过“内卷”和“情感焦虑”!
😂 怎么样?要不要再来一瓶“冷笑话”下酒?
LangChain实现的 用户说需求 → 查航班 → 查酒店 → 生成推荐 流程,可检验是否能读懂
点击查看代码
from __future__ import annotations
import os
from typing import TypedDict
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableLambda
# =========================
# 1. 定义数据结构
# =========================
class Flight(TypedDict):
id: str
airline: str
depart_time: str
arrive_time: str
price: int
class Hotel(TypedDict):
id: str
name: str
nights: int
price: int
class TravelInput(TypedDict):
user_need: str
class TravelPlan(TypedDict):
user_need: str
destination: str
budget: int
flight: Flight
hotel: Hotel
total_price: int
# =========================
# 2. 假接口:真实项目里换成真实 API
# =========================
def fake_search_flights(destination: str) -> list[Flight]:
return [
{
"id": "F001",
"airline": "Air Demo",
"depart_time": "2026-06-01 09:30",
"arrive_time": "2026-06-01 14:10",
"price": 2600,
},
{
"id": "F002",
"airline": "Budget Air",
"depart_time": "2026-06-01 20:30",
"arrive_time": "2026-06-01 23:50",
"price": 1800,
},
]
def fake_search_hotels(destination: str, nights: int) -> list[Hotel]:
return [
{
"id": "H001",
"name": f"{destination} Central Hotel",
"nights": nights,
"price": 4200,
},
{
"id": "H002",
"name": f"{destination} Budget Inn",
"nights": nights,
"price": 2800,
},
]
# =========================
# 3. 每一步都写成一个函数
# =========================
def parse_user_need(data: TravelInput) -> dict:
"""
理解用户需求。
真实项目里,这一步可以让 LLM 把自然语言解析成:
- 出发地
- 目的地
- 日期
- 预算
- 人数
这里为了讲清楚 LangChain 流程,先写死。
"""
user_need = data["user_need"]
return {
"user_need": user_need,
"origin": "Beijing",
"destination": "Tokyo",
"nights": 3,
"budget": 6000,
}
def search_flight_step(data: dict) -> dict:
"""
查航班。
"""
flights = fake_search_flights(data["destination"])
# 简单策略:选最便宜的航班
selected_flight = min(flights, key=lambda x: x["price"])
return {
**data,
"flight": selected_flight,
}
def search_hotel_step(data: dict) -> dict:
"""
查酒店。
"""
hotels = fake_search_hotels(
destination=data["destination"],
nights=data["nights"],
)
# 简单策略:选最便宜的酒店
selected_hotel = min(hotels, key=lambda x: x["price"])
total_price = data["flight"]["price"] + selected_hotel["price"]
return {
**data,
"hotel": selected_hotel,
"total_price": total_price,
}
def format_plan_for_prompt(data: TravelPlan) -> dict:
"""
把前面查到的信息整理成 prompt 需要的格式。
"""
return {
"user_need": data["user_need"],
"destination": data["destination"],
"budget": data["budget"],
"flight": data["flight"],
"hotel": data["hotel"],
"total_price": data["total_price"],
}
# =========================
# 4. 用 LangChain 串起来
# =========================
model = init_chat_model(
os.getenv("MODEL_NAME", "openai:gpt-4o-mini"),
temperature=0,
)
recommend_prompt = ChatPromptTemplate.from_messages([
(
"system",
"你是一个旅行规划助手。请用中文回答,语气自然,别啰嗦。",
),
(
"user",
"""
用户需求:
{user_need}
目的地:
{destination}
预算:
{budget}
航班:
{flight}
酒店:
{hotel}
总价:
{total_price}
请生成一个旅行推荐。
要求:
1. 说明推荐的航班和酒店
2. 说明总价是否在预算内
3. 如果超预算,提醒用户
4. 给出一句简短建议
""",
),
])
chain = (
RunnableLambda(parse_user_need)
| RunnableLambda(search_flight_step)
| RunnableLambda(search_hotel_step)
| RunnableLambda(format_plan_for_prompt)
| recommend_prompt
| model
| StrOutputParser()
)
# =========================
# 5. 执行
# =========================
if __name__ == "__main__":
result = chain.invoke({
"user_need": "我想从北京去东京玩 3 晚,预算 6000,帮我推荐航班和酒店。"
})
print(result)
RAG基础知识
这个是检索中很常用的技巧,Retrieval-Augmented Generation,意思是检索增强生成
检索增强流程:
这里存在的问题是,我们要去检索,但是我们去哪里检索呢,很容易想到可以用普通的数据库,然后可以根据关键字进行检索,比如我需要搜索"小猫",那么我就去数据库找到带有"小猫"关键字的所有字段,然后取出来,但是这里如果我想要检索的是"喵星人",这样的关键字检索是否就失效了呢?所以我们就必须引入向量数据库的概念,它是检索增强生成的关键。
tips:这里只介绍基础知识 后续实现会写一篇RAG博客 从基础RAG到RAG优化到高级RAG
1.向量数据库
向量数据库,顾名思义,里面存的都是向量,但是为什么我们非得用向量,而不能用我们的关键字进行检索呢,由于初学本人只有大概的自我理解,我认为每一个不同的词汇,在向量空间中都可以代表一个向量,此时可以去衡量"喵星人"与"小猫"的相似度,也就是向量空间中它们的远近,很容易想到看两个向量指的地方是否相近,越近就代表越相似。那么就引出向量数据库中的余弦相似度。
2.余弦相似度
关于如何计算相关性
以下公式就是用于计算两个向量的相似度,这里只做基础介绍,后续关于RAG和RAG优化会做更详细的博客
3.文档处理
关于如何处理文档
处理文档主要是因为模型的上下文有限,如果数据量大可能会有几本书,或者说大量资料,一次全部传给模型肯定处理不了,所以需要对于文档进切分,切分出的每一块,我们称之为chunking,切分出chunking之后只需要使用向量模型将其向量化,然后存入向量库即可,后续查询都是基于向量库进行查询
附上简版RAG便于理解,详细RAG优化请见后续
点击查看代码
#读取文档 → 切分 → 向量化 → 存储 → 检索 → 拼 prompt → 大模型回答
import os
from dotenv import load_dotenv
from langchain_core.documents import Document
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
load_dotenv()
# 1. 准备文档 AI生成的文档
# Document 可以理解成“带资料内容的小文件对象”
docs = [
Document(
page_content="""
RAG 是 Retrieval-Augmented Generation 的缩写。
它的意思是:大模型回答问题前,先去资料库里找相关内容,
再根据找到的资料生成答案。
""",
metadata={"source": "rag_intro"}
),
Document(
page_content="""
Chunk 是文档切分后的一小段内容。
好的 chunk 应该语义完整、长度适中、来源清楚。
普通知识库可以按 500 到 800 tokens 切分。
""",
metadata={"source": "chunk_rule"}
),
Document(
page_content="""
Embedding 是把文字变成数字向量。
向量越相近,说明两段文字意思越相似。
RAG 会用 embedding 来检索相关资料。
""",
metadata={"source": "embedding_intro"}
)
]
# 2. 切分文档
# RecursiveCharacterTextSplitter:
# 白话说,就是“按段落、句子、字符一层层切”,尽量别把话切碎
# 这里之所以要用到chunk_overlap其实是为了保证语义的连贯性
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=300,
chunk_overlap=50
)
splits = text_splitter.split_documents(docs)
# 3. 创建 embedding 模型
# Embedding:把文字变成数字,也就是把一段文字进行向量化 ,可以自己选取模型更换
embeddings = OpenAIEmbeddings(
model= "..."
api_key= "..."
base_url= "..."
)
# 4. 存入向量库
# 这里用内存向量库,程序一关数据就没了,适合最简 demo
# 如果想要长久保存 又不想太复杂可以使用ChormaDB
vector_store = InMemoryVectorStore.from_documents(
documents=splits,
embedding=embeddings
)
#ChormaDB用法
'''
from langchain_chroma import Chroma
vector_store = Chroma.from_documents(
documents=splits,
embedding=embeddings,
collection_name="rag_demo", #chunking的集合
persist_directory="./chroma_db" #关键的持久保存
)
'''
# 5. 创建检索器
# Retriever:白话说,就是去找资料
retriever = vector_store.as_retriever(
search_kwargs={"k": 3}
#这里是传入搜索参数 在RAG优化会讲到 这里可以理解为选取 Topk个最相关的片段
)
# 6. 创建大模型
llm = ChatOpenAI(
model= "..."
api_key= "..."
base_url= "..."
)
# 7. 准备 prompt
# Prompt:白话说,就是你交给模型的“答题说明”
prompt = ChatPromptTemplate.from_template("""
你是一个基于资料回答问题的助手。
请只根据下面的资料回答问题。
如果资料里没有答案,就回答:资料中没有相关信息。
资料:
{context}
问题:
{question}
""")
# 8. 格式化检索结果
def format_docs(docs):
return "\n\n".join(
f"来源:{doc.metadata.get('source')}\n内容:{doc.page_content}"
for doc in docs
)
'''
"\n\n".join(["苹果", "香蕉", "橘子"])
输出:
苹果
香蕉
橘子
f"来源:{doc.metadata.get('source')}\n内容:{doc.page_content}"
输出
来源:政策文件.pdf
内容:这是文档里的具体内容。
来源:...
内容:...
'''
# 9. RAG 主函数
def rag_answer(question: str):
# 先检索相关资料
retrieved_docs = retriever.invoke(question)
# 把资料整理成上下文
context = format_docs(retrieved_docs)
# 把上下文和问题塞进 prompt
messages = prompt.invoke({
"context": context,
"question": question
})
# 让模型回答
answer = llm.invoke(messages)
return answer.content, retrieved_docs
if __name__ == "__main__":
question = "RAG 是怎么工作的?"
answer, sources = rag_answer(question)
print("问题:")
print(question)
print("\n回答:")
print(answer)
print("\n引用资料:")
for doc in sources:
print("-", doc.metadata.get("source"))
LangChain初学总结
首先这里仅仅是个人观点,而且是新手小白刚学一点点感悟,结合搜索和使用的资料。
经过YellowDuck不断地搜索和反复拷打AI,一直想找到LangChain和LangGraph的区别,我发现其实二者的区别仅仅是在于,对于复杂项目的适配其实不同,由于自己写的一些项目远远不足企业级的项目,所以并不能很深刻的体会二者差距,但是会有一种LangGraph与LangChain相比之下其实更注重整体流程的控制,比如不同的分支,不同的角色,可能更侧重一点,LangGraph 把很多你本来要自己写的复杂流程能力变成了框架自带 ,但是对于LangChain,就会更像零件一点,官方也说 LangChain 是做 LLM 应用的主要入口,包含模型、工具、Runnables、Retrievers 等组件,一个比喻就是LangChain更像工具箱,LangGraph更像流程编排器。
声明:这仅仅是初学者一点点心得体会,如有不对希望大佬批评指正!!
本文来自博客园,作者:LiYellowDuck,转载请注明原文链接:https://www.cnblogs.com/LiYellowDuck/p/19974315,YellowDuck热爱手搓!!!

浙公网安备 33010602011771号