1. 项目概述:为什么我们需要LangChain?

如果你最近在捣鼓大语言模型,想把它们真正用起来,而不是仅仅在网页对话框里聊聊天,那你大概率已经听过LangChain这个名字了。它就像一个“乐高积木”的官方工具箱,让你能把大模型、你自己的数据、各种外部工具和API,按照你想要的方式拼装成一个能实际干活的智能应用。我刚开始接触时,觉得它概念挺多,有点绕,但真正用起来才发现,它解决的是大模型落地中最实际、最痛的那些点:比如,怎么让模型记住超长的对话?怎么让它基于我的私有文档回答问题?怎么让它按步骤调用工具完成任务?这些,LangChain都给出了标准化的“搭积木”方案。

简单来说,LangChain的核心价值在于 标准化 组件化 。在大模型生态早期,每个人都在重复造轮子,用各种临时脚本去连接模型、处理提示词、管理上下文。LangChain把这些通用模式抽象成了可复用的组件,比如 PromptTemplate LLMChain Agent Memory Retriever 。你不需要从零开始写网络请求、处理JSON、管理状态,而是像搭积木一样,声明式地组合这些组件,快速构建出功能复杂的应用。这对于开发者,尤其是希望快速验证想法、构建原型的团队来说,效率提升是巨大的。

那么,谁适合学习LangChain呢?首先是 应用开发者 ,你想基于GPT、Claude、文心一言等模型开发智能客服、知识库问答、自动报表生成等应用,LangChain是当前最主流、生态最丰富的框架。其次是 AI产品经理或技术负责人 ,了解LangChain的组件和能力边界,能帮助你更合理地设计产品架构,评估技术可行性。最后,即便是 初学者或研究者 ,通过学习LangChain的设计思想,也能更系统地理解大模型应用开发的完整链路,而不仅仅是调个API。

接下来的内容,是我在学习和使用LangChain近一年时间里,从踩坑到熟练的实战笔记。我会避开官方文档那种平铺直叙的介绍,而是围绕“如何用LangChain解决真实问题”这条主线,拆解其核心设计、手把手演示关键环节的实现,并分享那些只有实际用过才知道的“坑”和技巧。目标是让你看完后,不仅能理解概念,更能立刻动手搭建起自己的第一个智能应用。

2. 核心设计思想与架构拆解

要玩转LangChain,不能只停留在调用API的层面,必须理解其背后的设计哲学。它不是一个黑盒魔法,而是一套精心设计的“组装说明书”。

2.1 一切皆链:Chain的核心思想

LangChain的名字里就带着“Chain”,链是其最核心的抽象。它的基本思想是:将大模型应用看作一个 由多个可组合单元(Links)按顺序执行的数据流 。每个单元负责一项具体任务,比如格式化提示词、调用模型、解析输出、调用工具。通过将复杂任务分解为链,我们获得了模块化、可测试和可复用的能力。

举个例子,一个简单的问答链可能包含三个环节:1. 用用户问题+固定指令组装提示词;2. 调用大模型;3. 将模型的输出解析为结构化的答案。在LangChain里,这就是一个最简单的 LLMChain 。更复杂的链可以包含条件判断、循环、并行调用等。这种“链式”思维,迫使开发者从线性流程的角度思考问题,极大地提升了代码的可读性和可维护性。你不再需要在一个巨大的函数里混杂着提示词工程、API调用、结果后处理和错误处理,而是将它们拆分成独立的、可测试的组件。

2.2 六大核心模块全景图

LangChain的架构围绕六个核心模块构建,理解它们就掌握了LangChain的全局。

  1. 模型 I/O (Model I/O) :这是与各种大模型交互的抽象层。它提供了统一的接口来调用不同厂商的模型(OpenAI, Anthropic, 本地部署的Llama等),核心组件是 LLM (用于文本补全)和 ChatModel (用于对话)类。更重要的是,它标准化了**提示词(Prompt)**的管理,通过 PromptTemplate 让你能轻松创建带有变量、少样本示例的复杂提示,并与模型调用解耦。

  2. 检索 (Retrieval) :这是让大模型“拥有”你私有知识的关键。核心思想是“检索增强生成”(RAG)。该模块提供了全套工具,将你的文档(TXT, PDF, Word)进行 加载 分割 成语义片段、 向量化 并存入向量数据库(如Chroma, Pinecone),最后根据用户问题 检索 出最相关的片段,作为上下文提供给模型。 DocumentLoader , TextSplitter , Embeddings , VectorStore Retriever 是这里的明星组件。

  3. 链 (Chains) :如前所述,这是组合其他组件的“粘合剂”。除了简单的 LLMChain ,还有专为特定场景设计的链,如 RetrievalQA (用于知识库问答)、 ConversationalRetrievalChain (带历史记忆的问答)、 APIChain (让模型学习调用API文档)。高级用法允许你创建自定义链,实现复杂的业务逻辑。

  4. 代理 (Agents) :这是LangChain最强大也最有趣的部分。代理赋予模型使用**工具(Tools)**的能力。模型可以根据用户目标,自主决定调用哪个工具、以什么参数调用,并根据工具返回的结果决定下一步行动。这模拟了人类的推理和行动过程,使得模型能完成需要多步骤、与外界交互的任务,比如“查一下北京明天的天气,然后推荐一件适合的穿搭”。

  5. 记忆 (Memory) :为了让模型在对话中保持连贯性,记忆模块负责存储和查询历史交互信息。简单的有 ConversationBufferMemory (保存所有对话),复杂的有 ConversationSummaryMemory (总结长对话以节省token)、 EntityMemory (记住对话中提到的实体信息)。记忆的本质是为链或代理提供额外的上下文。

  6. 回调 (Callbacks) :这是一个用于日志记录、监控和流式传输的辅助系统。你可以通过回调在链执行的各个阶段(如模型调用开始、结束时)插入自定义逻辑,用于调试、记录token消耗或实现实时的流式输出,提升用户体验。

这六大模块并非孤立,而是高度协同的。一个典型的RAG应用会用到模型I/O、检索和链;一个智能助手则会用到模型I/O、代理、工具和记忆。理解它们之间的关系,是灵活运用LangChain的基础。

注意 :LangChain的版本迭代很快,目前社区活跃度更高的是 LangGraph ,它基于LangChain构建,但采用了 有向图 的模型来编排代理和工作流,更适合描述具有循环、分支等复杂状态的应用。对于新手,建议先从LangChain的核心概念入手,建立直觉,当你的应用需要复杂的状态管理时,再自然过渡到LangGraph。

3. 从零搭建你的第一个LangChain应用:一个智能知识库问答机器人

理论讲得再多,不如亲手做一遍。让我们来构建一个最常见的应用:基于私有文档的智能问答机器人。这个过程会串联起模型I/O、检索和链三大核心模块。

3.1 环境准备与依赖安装

首先,确保你的Python环境在3.8以上。创建一个新的虚拟环境是个好习惯。然后安装核心包:

pip install langchain langchain-community langchain-openai

这里解释一下:

  • langchain : 核心框架。
  • langchain-community : 包含大量第三方集成(如各种向量数据库、工具)。
  • langchain-openai : OpenAI模型的官方集成包(如果你用其他模型,如 langchain-anthropic )。

我们还需要一个嵌入模型和向量数据库。为了本地运行方便,我们选用:

  • 嵌入模型 :OpenAI的 text-embedding-3-small ,性价比高。你需要一个OpenAI API Key。
  • 向量数据库 Chroma ,轻量级,可持久化到磁盘。
pip install chromadb tiktoken

tiktoken 是OpenAI用于计算token的库。

3.2 文档加载、分割与向量化

假设我们有一个关于公司产品手册的PDF文件 product_manual.pdf 。第一步是让它能被模型“阅读”。

from langchain_community.document_loaders import PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma

# 1. 加载文档
loader = PyPDFLoader("./product_manual.pdf")
documents = loader.load()
print(f"加载了 {len(documents)} 页文档。")

# 2. 分割文本
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,  # 每个片段约500字符
    chunk_overlap=50, # 片段间重叠50字符,保持上下文连贯
    separators=["\n\n", "\n", "。", "!", "?", ",", " ", ""] # 中文优先按句分割
)
split_docs = text_splitter.split_documents(documents)
print(f"分割为 {len(split_docs)} 个文本片段。")

# 3. 创建嵌入模型并向量化存储
embeddings = OpenAIEmbeddings(model="text-embedding-3-small", openai_api_key="你的API_KEY")
# 持久化到本地目录 ./chroma_db
vectorstore = Chroma.from_documents(
    documents=split_docs,
    embedding=embeddings,
    persist_directory="./chroma_db"
)
vectorstore.persist() # 显式保存到磁盘
print("向量数据库已创建并持久化。")

关键点解析

  • 分割策略 chunk_size 是关键参数。太小会丢失上下文,太大会降低检索精度并增加模型处理负担。对于中文,500-800是个不错的起点。 chunk_overlap 能防止一个句子被腰斩,是提升检索质量的小技巧。
  • 嵌入模型 :这里用了OpenAI的付费接口。如果你需要完全本地化,可以选用 langchain.embeddings 中的 HuggingFaceEmbeddings ,加载如 BAAI/bge-small-zh 这样的开源模型,但需要一定的GPU资源。
  • 向量数据库 Chroma.from_documents 一步完成了向量化和存储。 persist_directory 让数据保存在本地,下次启动无需重新处理。

3.3 构建检索链并进行问答

数据库建好后,我们就可以构建一个问答链了。这里使用LangChain提供的 RetrievalQA 链,它封装了检索+问答的通用模式。

from langchain_openai import ChatOpenAI
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# 1. 加载已有的向量数据库
embeddings = OpenAIEmbeddings(model="text-embedding-3-small", openai_api_key="你的API_KEY")
vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)

# 2. 创建检索器
retriever = vectorstore.as_retriever(
    search_type="similarity", # 相似度搜索
    search_kwargs={"k": 4} # 返回最相关的4个片段
)

# 3. 定义大模型
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0, openai_api_key="你的API_KEY")
# temperature=0使输出更确定,适合事实性问答

# 4. 自定义提示模板(可选但推荐)
prompt_template = """
请根据以下上下文信息回答问题。如果你不知道答案,就诚实地回答不知道,不要编造信息。

上下文:
{context}

问题:{question}
请用中文给出详细、准确的答案:
"""
PROMPT = PromptTemplate(
    template=prompt_template, input_variables=["context", "question"]
)

# 5. 创建检索问答链
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff", # 最简单的方式:将所有检索到的上下文塞进提示词
    retriever=retriever,
    chain_type_kwargs={"prompt": PROMPT}, # 使用自定义提示
    return_source_documents=True # 返回参考来源,便于验证
)

# 6. 进行问答
question = "你们产品的高级版和企业版在数据存储容量上有什么区别?"
result = qa_chain.invoke({"query": question})
print(f"问题:{question}")
print(f"答案:{result['result']}")
print("\n--- 参考来源 ---")
for i, doc in enumerate(result['source_documents'][:2]): # 显示前两个来源
    print(f"[来源{i+1}] {doc.page_content[:200]}...") # 截取前200字符

实操心得

  • chain_type 选择 "stuff" 是最直接的方式,但如果检索到的上下文总长度超过模型限制(如GPT-3.5的4K token),就会报错。对于超长文档,可以考虑 "map_reduce" (先分别总结各片段,再总结总结结果)或 "refine" (迭代式完善答案),但它们更耗token和时间。
  • 提示词工程 :不要依赖链的默认提示。像上面例子中,明确指令模型“根据上下文”、“不知道就说不知道”,能显著提升答案的准确性和可靠性,减少幻觉。
  • 检索数量 k k=4 是一个常用起始值。增加 k 可以提供更多上下文,但也可能引入噪声并增加token消耗。需要根据实际效果调整。
  • 返回来源 return_source_documents=True 对于调试和建立用户信任至关重要。你可以展示答案引用了哪部分原文,让回答更可信。

4. 进阶实战:打造一个能使用工具的智能代理

如果知识库问答是“读”和“答”,那么代理就是“想”和“做”。让我们创建一个能查询天气、计算数学、搜索网络的智能代理。

4.1 定义工具:代理的手和脚

工具是代理与外界交互的接口。每个工具都需要一个清晰的名称、描述和函数实现。

from langchain.agents import Tool
from langchain_community.utilities import SerpAPIWrapper
from langchain_community.tools import WikipediaQueryRun
from langchain_community.utilities import WikipediaAPIWrapper
import math
from datetime import datetime

# 工具1: 搜索网络 (需要SerpAPI Key,这里用模拟函数替代)
def search_web(query: str) -> str:
    """当需要获取最新的、搜索引擎上的信息时使用此工具。输入应为搜索关键词。"""
    # 实际应用中,你应该接入SerpAPI或类似服务
    print(f"[模拟网络搜索] 关键词: {query}")
    # 模拟返回
    if "天气" in query:
        return f"模拟返回:根据网络信息,{query.split('天气')[0]}今天晴,气温20-25度。"
    return f"关于'{query}'的模拟搜索结果摘要。"

# 工具2: 计算器
def calculator(expression: str) -> str:
    """用于执行数学计算。输入应为数学表达式,如 '2 + 3' 或 'sqrt(16)'。"""
    try:
        # 安全地评估数学表达式,避免执行任意代码
        allowed_names = {"sqrt": math.sqrt, "sin": math.sin, "cos": math.cos, "pi": math.pi}
        result = eval(expression, {"__builtins__": {}}, allowed_names)
        return str(result)
    except Exception as e:
        return f"计算错误:{e}"

# 工具3: 查询时间
def get_current_time(_) -> str: # 代理调用时可能会传入无关参数,用_接收
    """当被问到当前时间、日期或星期几时使用此工具。"""
    now = datetime.now()
    return now.strftime("当前时间是:%Y年%m月%d日 %H时%M分,星期%w").replace("星期0", "星期日")

# 包装成LangChain Tool对象
tools = [
    Tool(
        name="Web_Search",
        func=search_web,
        description="当问题涉及实时信息、新闻或无法从已知知识中获取时使用。输入:搜索关键词。"
    ),
    Tool(
        name="Calculator",
        func=calculator,
        description="用于解决数学计算、算术问题。输入:一个数学表达式,例如 '3 * 7 + 5'。"
    ),
    Tool(
        name="Time_Query",
        func=get_current_time,
        description="当用户询问当前时间、今天日期或星期几时使用。无需输入。"
    )
]

注意事项

  • 工具描述至关重要 :代理模型(如GPT)完全依赖 description 字段来决定是否以及如何调用工具。描述必须清晰、准确,说明工具的用途和输入格式。
  • 安全性 calculator 工具中,我们使用了受限的 eval 在生产环境中,这是极度危险的! 必须使用更安全的数学表达式解析库(如 numexpr )或严格的白名单验证,防止代码注入攻击。这里仅为演示。
  • 错误处理 :工具函数内部应有良好的错误处理,并返回对代理友好的错误信息,以便代理能理解并采取下一步行动(如重试或向用户道歉)。

4.2 创建代理并观察其思考过程

有了工具,我们就可以初始化一个代理。我们将使用OpenAI的函数调用(Function Calling)能力,这是目前最稳定高效的代理实现方式。

from langchain_openai import ChatOpenAI
from langchain.agents import initialize_agent, AgentType
from langchain.callbacks import StdOutCallbackHandler

# 初始化大模型
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0, openai_api_key="你的API_KEY")
# 为了观察思考过程,我们使用一个回调来打印日志
callbacks = [StdOutCallbackHandler()]

# 初始化代理
agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.OPENAI_FUNCTIONS, # 使用OpenAI函数调用代理
    verbose=True, # 开启详细日志,会在控制台打印思考步骤
    handle_parsing_errors=True, # 优雅处理解析错误
    callbacks=callbacks,
    max_iterations=5, # 防止代理陷入死循环
    early_stopping_method="generate" # 当代理认为任务完成时停止
)

# 运行代理
try:
    result = agent.invoke("请先计算一下15的平方根是多少,然后告诉我现在几点了?")
    print(f"\n最终回答:{result['output']}")
except Exception as e:
    print(f"代理执行出错:{e}")

当你运行这段代码时,如果 verbose=True ,你会在控制台看到类似下面的思考过程(这是LangChain打印的,不是模型原始输出):

> Entering new AgentExecutor chain...
我需要按顺序回答两个问题:计算平方根和查询时间。我有计算器和时间查询工具。
Action: Calculator
Action Input: sqrt(15)
Observation: 3.872983346207417
Thought: 第一个问题解决了。现在需要查询当前时间。
Action: Time_Query
Action Input: 
Observation: 当前时间是:2024年05月20日 14时30分,星期一
Thought: 我现在有了两个答案,可以组合起来回答用户了。
Final Answer: 15的平方根约等于3.873。现在是2024年5月20日星期一,下午2点30分。
> Finished chain.
最终回答:15的平方根约等于3.873。现在是2024年5月20日星期一,下午2点30分。

核心机制解析

  • ReAct模式 :代理遵循“思考(Thought)-行动(Action)-观察(Observation)”的循环。模型先思考要做什么,然后选择工具并调用,得到观察结果后,再进行下一轮思考,直到得出最终答案。
  • AgentType.OPENAI_FUNCTIONS :这是目前最推荐的代理类型。它利用OpenAI模型原生支持函数调用的能力,将工具描述以JSON Schema格式传给模型,模型直接返回需要调用的函数名和参数。这比早期的 ZERO_SHOT_REACT_DESCRIPTION (依赖文本指令让模型格式化输出)更稳定、更高效。
  • max_iterations early_stopping :这是防止代理“跑飞”的关键安全措施。设定最大迭代次数,避免因逻辑错误或工具失败导致无限循环。

4.3 为代理添加记忆能力

上面的代理是“单次对话”,它不记得之前说过什么。要让代理在多轮对话中保持连贯,需要引入记忆。

from langchain.memory import ConversationBufferMemory
from langchain.agents import AgentExecutor
from langchain.agents.openai_functions_agent.base import OpenAIFunctionsAgent
from langchain.agents.openai_functions_agent.agent_token_buffer_memory import AgentTokenBufferMemory
from langchain.schema.messages import SystemMessage

# 1. 创建带有token限制的记忆(节省token)
memory = AgentTokenBufferMemory(
    memory_key="chat_history",
    llm=llm,
    max_token_limit=1000 # 限制记忆的token数
)
# 先预存一条系统消息,设定代理角色
memory.chat_memory.add_message(SystemMessage(content="你是一个乐于助人的助手,可以使用工具来回答问题。"))

# 2. 创建自定义提示,其中包含记忆变量的占位符
from langchain.prompts import MessagesPlaceholder
system_message = SystemMessage(content="你是一个强大的助手,可以使用工具。之前的对话历史会提供给你作为参考。")
prompt = OpenAIFunctionsAgent.create_prompt(
    system_message=system_message,
    extra_prompt_messages=[MessagesPlaceholder(variable_name="chat_history")]
)

# 3. 创建代理和代理执行器
agent_with_memory = OpenAIFunctionsAgent(llm=llm, tools=tools, prompt=prompt)
agent_executor = AgentExecutor(
    agent=agent_with_memory,
    tools=tools,
    memory=memory,
    verbose=True,
    max_iterations=3,
    handle_parsing_errors=True
)

# 4. 进行多轮对话
print("第一轮:")
result1 = agent_executor.invoke({"input": "今天北京天气怎么样?"})
print(f"回答:{result1['output']}\n")

print("第二轮(基于历史):")
result2 = agent_executor.invoke({"input": "那我需要带伞吗?"}) # 代理应能联系到“天气”
print(f"回答:{result2['output']}")

在这个例子中,第二轮提问“那我需要带伞吗?”,代理会从 chat_history 中看到上一轮关于北京天气的问答,从而做出合理的推断(如果之前返回的天气是“晴”,它可能会说不需要;如果是“雨”,则会建议带伞)。记忆让代理具备了上下文感知能力。

5. 生产级部署与性能优化核心要点

当你开发完一个原型,准备将其投入生产环境时,会面临一系列新的挑战。以下是几个关键的优化方向。

5.1 检索质量优化:RAG的命门

RAG应用的效果,八成取决于检索质量。如果检索不到相关文档,再强的模型也无力回天。

  1. 文本分割策略调优

    • 尝试不同的分割器 :除了 RecursiveCharacterTextSplitter ,还有 TokenTextSplitter (按token数分割,更精确控制上下文长度)、 SemanticChunker (尝试按语义边界分割,但更复杂)。
    • 重叠(Overlap)不是越大越好 :通常10%的 chunk_size 作为 overlap 是个好的起点。太大的重叠会增加冗余和存储成本。
    • 文档类型适配 :对于代码、Markdown等高度结构化的文档,可以使用 MarkdownHeaderTextSplitter LanguageTextSplitter ,按标题或语法结构分割,能更好地保持语义完整性。
  2. 检索器(Retriever)调优

    • 混合搜索(Hybrid Search) :结合 稠密向量检索 (语义相似度)和 稀疏向量检索 (关键词匹配,如BM25)。前者理解语义,后者保证关键词命中。Chroma等数据库已支持。
    # Chroma中启用混合检索示例(需对应版本支持)
    retriever = vectorstore.as_retriever(
        search_type="mmr", # 最大边际相关性,兼顾相似性与多样性
        search_kwargs={"k": 6, "fetch_k": 20, "lambda_mult": 0.7}
    )
    
    • 重排序(Re-ranking) :先用向量检索出大量候选(如20个),再用一个更小、更精的交叉编码器模型(如 BGE-reranker )对候选进行重排序,只取Top K个最相关的。这能显著提升精度,但会增加延迟。
    • 元数据过滤 :在存入向量数据库时,为每个片段添加元数据(如来源文件、章节、页码)。检索时,可以添加过滤器,例如“只从产品手册的第三章检索”。

5.2 流式输出与异步处理提升用户体验

对于Web应用,让用户等待模型生成完整答案(尤其是长答案)体验很差。流式输出是必备功能。

from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler

# 1. 流式模型调用
streaming_llm = ChatOpenAI(
    model="gpt-3.5-turbo",
    streaming=True, # 启用流式
    callbacks=[StreamingStdOutCallbackHandler()], # 回调处理流式数据
    temperature=0,
    openai_api_key="你的API_KEY"
)

# 2. 在链中使用流式LLM
qa_chain_streaming = RetrievalQA.from_chain_type(
    llm=streaming_llm,
    chain_type="stuff",
    retriever=retriever
)
# 调用时,答案会逐词打印到控制台
# 在FastAPI或Streamlit等Web框架中,你需要将回调与Server-Sent Events (SSE)结合。

# 3. 异步调用提升吞吐量
import asyncio
from langchain.agents import AgentExecutor

async def run_agent_async(question):
    agent_executor = AgentExecutor.from_agent_and_tools(...) # 初始化你的代理
    try:
        result = await agent_executor.ainvoke({"input": question}) # 注意是 ainvoke
        return result['output']
    except Exception as e:
        return f"错误:{e}"

# 在异步框架(如FastAPI)中调用

要点 :流式输出主要依赖于模型提供商的支持(如OpenAI的流式响应)。在Web后端,你需要创建一个端点,将模型返回的token通过SSE或WebSocket实时推送给前端。

5.3 监控、日志与成本控制

生产应用必须可观测、可管理。

  1. Token消耗监控 :这是成本核心。利用LangChain的回调系统,可以轻松记录每次调用的token数。

    from langchain.callbacks import get_openai_callback
    
    with get_openai_callback() as cb:
        result = qa_chain.invoke({"query": "长问题..."})
        print(f"本次调用消耗:{cb.total_tokens} tokens, 成本约 ${cb.total_cost:.4f}")
    

    定期汇总这些数据,分析哪些问题或用户消耗最多,优化提示词或检索策略。

  2. 链路追踪(Tracing) :使用像 LangSmith (LangChain官方平台)或 OpenTelemetry 这样的工具,记录每一次链、每一次模型调用的输入、输出、耗时和中间步骤。这对于调试复杂代理的决策过程、定位性能瓶颈至关重要。

  3. 缓存 :对于频繁出现的相同或相似查询,使用缓存可以极大减少模型调用和成本。LangChain支持内存缓存( InMemoryCache )、Redis缓存等。

    from langchain.cache import InMemoryCache
    from langchain.globals import set_llm_cache
    set_llm_cache(InMemoryCache())
    

    设置后,相同的提示词调用LLM会直接返回缓存结果。

6. 避坑指南与常见问题排查

在实际开发中,你会遇到各种各样的问题。这里记录了一些高频“坑点”和解决方法。

6.1 模型响应不符合预期或出现幻觉

  • 问题 :模型答非所问,或编造不存在的信息(幻觉)。
  • 排查
    1. 检查检索结果 :首先打印出 source_documents ,看检索到的上下文是否真的与问题相关。如果不相关,问题在检索端(分割策略、嵌入模型、检索参数)。
    2. 检查提示词 :你的提示词是否足够清晰、强硬?尝试在提示词中加入更严格的指令,如“ 必须严格依据上下文回答,上下文未提及的内容一律回答‘不知道’ ”。对于 ChatModel ,使用 SystemMessage 来设定角色和规则往往比放在用户消息里更有效。
    3. 调整模型参数 :将 temperature 设为0或一个很低的值(如0.1),减少随机性。对于事实性任务,高 temperature 是幻觉的温床。
    4. 使用更高能力的模型 :如果预算允许,尝试 gpt-4-turbo 等更强大的模型,它们在遵循指令和减少幻觉方面通常表现更好。

6.2 代理陷入循环或调用错误工具

  • 问题 :代理不停地重复同一个动作,或者总是选择错误的工具。
  • 排查
    1. 精简工具描述 :工具的描述要极度精确,避免歧义。如果两个工具描述相似,模型容易混淆。确保每个工具的名称和描述都能清晰界定其职责范围。
    2. 提供少量示例(Few-Shot) :在给代理的系统提示中,加入一两个正确使用工具的对话示例,能显著提升其工具调用的准确性。
    3. 设置迭代限制和超时 :务必设置 max_iterations (如5-10次)和 max_execution_time ,这是防止死循环的最后防线。
    4. 使用 handle_parsing_errors=True :当模型输出无法被解析为工具调用时,这个参数能让代理尝试重新生成,而不是直接崩溃。

6.3 处理长文档时上下文超限

  • 问题 :在RAG中,当检索到的文档片段总长度超过模型上下文窗口时,会报错。
  • 解决方案
    1. 优化分割 :减小 chunk_size ,确保单个片段不会太长。
    2. 使用更智能的链类型 :不要只用 "stuff" 。对于长文档,切换到 "map_reduce" "refine" "map_reduce" 先对每个片段单独生成答案(Map),再汇总这些答案(Reduce); "refine" 则迭代式地完善答案。它们都通过分而治之的方式处理长上下文,但代价是更多的模型调用和更高的延迟与成本。
    3. 选择性检索 :不要一次性检索所有相关片段。可以先检索出Top N个,然后根据相关性分数或使用一个小的分类模型,只选择最关键的几个片段送入最终提示。
    4. 升级模型 :使用支持更长上下文(如128K)的模型,如 gpt-4-turbo Claude 3 系列,但这会显著增加成本。

6.4 依赖包版本冲突与API变更

  • 问题 langchain 和其社区包 langchain-community 更新非常快,API经常变动,导致旧代码报错。
  • 最佳实践
    1. 使用虚拟环境并锁定版本 :在 requirements.txt 中明确指定主要包的版本,例如 langchain==0.1.0 。定期有计划地升级,而不是随时更新到最新。
    2. 关注更新日志 :在升级前,务必阅读GitHub的Release Notes,了解破坏性变更(Breaking Changes)。
    3. langchain 核心包导入 :一些常用组件后来被移到了 langchain-community 。建议的导入方式是先尝试从 langchain 导入,如果失败,再从 langchain_community 导入,并做好兼容性处理。
    4. 官方文档是朋友 :遇到问题,首先查阅对应版本的官方API文档,而不是盲目搜索博客,因为博客可能已经过时。

7. LangChain vs. LangGraph:如何选择?

随着项目复杂度的提升,你可能会听到LangGraph。它和LangChain是什么关系?该用哪个?

简单来说, LangChain是“组件库”,LangGraph是“编排框架”

  • LangChain :提供了构建大模型应用所需的各种标准化组件(模型、检索、记忆、工具等)和基础组合模式(链)。它擅长构建线性的、确定性的工作流。比如“检索文档 -> 生成答案”这个流程,用 RetrievalQA 链非常合适。

  • LangGraph :它基于LangChain构建,但引入了 有向图 的概念。在LangGraph中,你将应用定义为一个由节点(Node)和边(Edge)组成的图。节点可以执行任何操作(调用LLM、工具、条件判断等),边决定了流程的走向。这带来了两个关键能力:

    1. 循环(Cycles) :可以让流程回到之前的节点,实现多轮对话、迭代优化等场景。这在纯链中很难优雅实现。
    2. 状态(State) :在整个图执行过程中,有一个持久化的状态对象在节点间传递和修改,这比在链之间传递简单的输入输出更强大和清晰。

选择建议

  • 新手或简单应用 :直接从LangChain开始。它的学习曲线更平缓,能解决80%的常见需求(问答、摘要、简单代理)。
  • 复杂、有状态的工作流 :如果你的应用涉及多角色协作(如模拟一个软件团队:产品经理、工程师、测试员)、需要复杂的审批流程、或者有严格的循环执行逻辑(如“生成代码 -> 运行测试 -> 如果失败则修改代码 -> 再次测试”),那么LangGraph是更合适的选择。它用图来定义流程,可视化后更直观,也更容易管理和调试。

一个形象的比喻:LangChain像是一盒乐高积木,你可以搭出房子和车子;而LangGraph则提供了搭建复杂机械结构(如带传动装置的起重机)的图纸和连接件。先从积木玩起,当你想搭更复杂、会动的东西时,再学习图纸。

我个人在项目中的体会是,对于大多数内部工具、原型验证和相对标准的应用,LangChain已经完全够用。只有当流程逻辑变得非常复杂,用链描述起来很别扭时,才需要考虑引入LangGraph。不要为了用新技术而用,而是根据实际问题选择最合适的工具。

更多推荐