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,意思是检索增强生成
检索增强流程:

\[\text{用户问题} \xrightarrow{\text{①检索}} \text{相关文档片段} \xrightarrow{\text{②增强}} \text{拼成提示词} \xrightarrow{\text{③生成}} \text{最终答案} \]

这里存在的问题是,我们要去检索,但是我们去哪里检索呢,很容易想到可以用普通的数据库,然后可以根据关键字进行检索,比如我需要搜索"小猫",那么我就去数据库找到带有"小猫"关键字的所有字段,然后取出来,但是这里如果我想要检索的是"喵星人",这样的关键字检索是否就失效了呢?所以我们就必须引入向量数据库的概念,它是检索增强生成的关键。

tips:这里只介绍基础知识 后续实现会写一篇RAG博客 从基础RAG到RAG优化到高级RAG

1.向量数据库

向量数据库,顾名思义,里面存的都是向量,但是为什么我们非得用向量,而不能用我们的关键字进行检索呢,由于初学本人只有大概的自我理解,我认为每一个不同的词汇,在向量空间中都可以代表一个向量,此时可以去衡量"喵星人"与"小猫"的相似度,也就是向量空间中它们的远近,很容易想到看两个向量指的地方是否相近,越近就代表越相似。那么就引出向量数据库中的余弦相似度。

2.余弦相似度

关于如何计算相关性

以下公式就是用于计算两个向量的相似度,这里只做基础介绍,后续关于RAG和RAG优化会做更详细的博客

\[\text{相似度} = \cos(\theta) = \frac{\vec{A} \cdot \vec{B}}{|\vec{A}||\vec{B}|} \]

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更像流程编排器

声明:这仅仅是初学者一点点心得体会,如有不对希望大佬批评指正!!

posted @ 2026-05-06 14:47  LiYellowDuck  阅读(73)  评论(0)    收藏  举报