
我学习 LangChain 的过程其实是从一肚子怨气开始的。最开始写 LLM 应用每个功能都要自己封装一遍 API 调用处理上下文拼拼凑凑写工具调用更是麻烦代码搞到后面自己都看不懂。后来接触了 LangChain才发现这框架确实是干这个的——把大模型应用开发里那些重复劳动抽象出来让你把精力放在业务流程上而不是去研究怎么拼接字符串。这篇笔记其实就是我学下来的一些理解对一些核心概念做了拆解记录了完整的实操案例也把这段时间踩过的坑一并列出来。如果你想快速把 LangChain 用起来或者正在纠结该怎么学它这篇笔记应该能帮你省点时间。1. 从调API到搭框架LangChain 到底帮我省了什么我先说个最直观的感受。最早我写一个调用大模型的功能逻辑大概是拼系统提示词、拼历史记录、带上用户问题、调用接口、解析返回结果。这五个步骤哪个都不能少每次写新功能都得从头来一遍。代码看着也不复杂但功能一多、提示词一变、上下文一长维护起来就头疼。真正让人崩溃的是每换一个大模型服务商接口参数、返回格式、计费逻辑全都不一样迁移成本高得离谱。LangChain 做的事情就是把这层重复逻辑抽象成标准化的接口。它不关心你底层接的是哪家大模型的 API你只需要在配置层指定模型来源业务代码那头不用动。我后来把应用从一家模型服务商切到另一家业务代码几乎没改只调整了模型初始化部分的配置这是我最直观的感受抽象层是有用的。1.1 三层抽象把 LLM 应用开发变成了搭积木用久了以后我习惯把 LangChain 的抽象方式理解为三层。第一层是模型层的封装。不管是文本生成还是对话补全框架统一封装成两个核心接口——ChatModel用于对话场景LLM用于纯文本补全场景。两者的核心方法都是传入消息列表或文本返回模型响应。这一层的作用是让你忘掉具体 API 的各种细节比如认证方式、重试机制、超时设置框架在底层已经处理好了。第二层是数据流的标准化。模型接收的内容往往需要经过模板化处理、格式解析、结果抽取这套流程 LangChain 用PromptTemplate、OutputParser、StructuredOutputParser这些组件串起来。说白了就是把散落在代码里的提示词工程做成了可复用、可维护的模块而不是到处硬编码字符串。第三层是应用逻辑的组合编排。框架提供Chain链和Agent智能体两类编排方式。链适合固定的、流程确定的任务比如先检索再生成智能体则适合需要模型自主推理、决定调用什么工具、按什么顺序执行的任务。我刚开始学时总觉得概念多其实抓住一条主线就行模型负责生成组件负责加工输入输出链和智能体负责组织整个流程。抓住这条线LangChain 的结构就清晰了。1.2 用与不用框架差距到底在哪我举个例子吧。不用的框架的时候写一个从问题中提取关键词再根据关键词回答的功能代码大约是def answer_with_keywords(question: str, llm_client) - str: # 第一步让模型提取关键词 extraction_prompt f请提取以下问题中的关键词只输出关键词\n{question} keywords llm_client.complete(extraction_prompt).strip() # 第二步根据关键词生成回答 answer_prompt f根据关键词 {keywords}回答用户问题{question} answer llm_client.complete(answer_prompt).strip() return answer这段逻辑本身没问题但只要需求稍微变化比如提取关键词时要有置信度回答时必须使用某个格式你就得往函数里塞更多逻辑而且一步步的流程被固定死了很难在中间插入缓存、校验、日志这些横切逻辑。用 LangChain 写同样的功能会把每个步骤拆成独立的组件然后用Chain将它们串联from langchain.llms import FakeLLM from langchain.prompts import PromptTemplate from langchain.schema import StrOutputParser from langchain.chains import LLMChain extract_prompt PromptTemplate.from_template(请提取问题中的关键词{question}) answer_prompt PromptTemplate.from_template(根据关键词 {keywords}回答{question}) chain ( extract_prompt | llm | StrOutputParser() | (lambda keywords: {keywords: keywords, question: original_question}) | answer_prompt | llm | StrOutputParser() )你可能看出来了新版 LangChain 用|管道符把组件串起来跟 Unix 管道命令的思路一致整个过程既是代码又是流程图。中间想加日志、加缓存、加错误处理插进对应的管道位置就行模块化程度明显高了不少。2. 五件必须弄明白的核心装备Model、Prompt、Memory、Chain 和 AgentLangChain 学习曲线陡核心原因是概念多而且每个概念之间还互相依赖。我梳理了很久发现真正绕不开的就五个Model模型、Prompt提示词、Memory记忆、Chain链、Agent智能体。其他的组件基本都是在围绕这五个打转。2.1 Model所有能力的起点但别被它限制住Model 层是最容易理解的部分也是最容易让人产生误解的部分。很多人以为 LangChain 就是某个大模型的产品包装其实不是。它是一个中立的抽象层你既可以接闭源的大模型 API也可以接开源模型甚至可以通过FakeLLM这类测试替身来调试业务逻辑不必每次跑代码都消耗真实调用。我自己调试链的时候遇到 model 配置有问题排查半天分不清是模型传参写错了还是链的编排逻辑错了。后来学聪明了用FakeListLLM模拟返回结果把链的流程逻辑先跑通from langchain.llms.fake import FakeListLLM fake_llm FakeListLLM(responses[关键词python; 答案Python 是一门编程语言。])这种思路很实用。业务逻辑开发阶段不要让模型 API 成为瓶颈先保证链路本身没问题最后再接入真实模型做验证。2.2 PromptTemplate提示词的工程化不只是字符串拼接PromptTemplate 是 LangChain 里最不起眼但最实用的组件。表面上它只是字符串模板加上变量替换实际用下来它的价值在于强制你把提示词当作一等公民来管理。比如你要做一个翻译助手提示词可能长这样from langchain.prompts import PromptTemplate translate_template PromptTemplate.from_template( 你是一名专业翻译。请将下面的内容翻译成{target_language}保持原有语气\n{content} ) # 填充变量 formatted_prompt translate_template.format( target_language英文, content今天天气不错适合出去玩。, )这样做的好处是提示词和业务代码解耦了。后续想调整系统设定、few-shot 示例、输出格式要求不用去翻业务代码直接在模板文件里改就行。项目变大以后我甚至会把所有模板集中到单独的文件里方便统一管理、统一做版本控制。2.3 Memory不止是把历史消息塞回去那么简单Memory 是学习 LangChain 过程中最容易低估、也最容易踩坑的组件。直觉上记忆就是保存历史对话然后把历史记录拼进提示词。真实情况远比这个复杂记忆的存储格式、长度控制、检索策略、持久化方式每一个环节都可能出问题。最典型的是内存暴增。对话轮次一多直接把全部历史拼进提示词token 消耗直接爆炸。LangChain 里提供了几个不同类型的 Memory 组件各自有适用边界组件机制适用场景ConversationBufferMemory完整保存最近 N 轮简单场景、轮次少ConversationBufferWindowMemory只保留最近 K 轮控制 token 消耗丢失早期信息ConversationSummaryMemory对历史做摘要再保存长对话、需要全局信息VectorStoreRetrieverMemory用向量检索相关记忆知识库型对话、海量历史我实际做问答应用时发现真正好用的组合是窗口记忆摘要记忆并用短期对话用窗口保存跑出摘要进长期存储。但这里有一个隐藏问题——摘要本身也是模型生成的摘要质量直接决定后续回答效果对于关键任务摘要记忆反而可能让你丢失关键细节需要做取舍验证。2.4 Chain从单步调用到流程编排的关键一步Chain 是 LangChain 设计中最抽象、也最有力量的部分。早期版本里 Chain 是一个个 Python 类每个 Chain 封装一种固定流程从新版本开始LangChain 主推 LCELLangChain Expression Language核心思路就是我用上文那个管道符写法——prompt | llm | parser。LCEL 最值得学的点不是语法而是它的惰性执行和并行调度。一个链路由多个步骤组成时LCEL 会自动判断哪些步骤可以并行执行哪些必须串行等待。比如检索生成链检索和生成必须串行但多个来源的信息搜集链多个检索步骤可以并行LCEL 会在底层自动处理并发不需要手动写线程或异步代码。我实际测过并行带来的收益。在一个需要从三个文档库同时检索的链路里用 LCEL 的表达方式比起手动串行编写整体耗时大约减少了原来的三分之一。这个提升不是模型的功劳而是流程编排的功劳。2.5 Agent让模型自己决定下一招是什么前面说的 Chain流程是开发者预先定死的Agent 则相反它让模型在推理过程中动态决定接下来调用什么工具、要不要继续执行、什么时候给出最终答案。我用一个简单的例子来说明 Agent 的价值。假设你做一个客服助手用户问我的订单预计什么时候到如果只用 Chain你需要预先判断用户可能会问物流、问退货、问商品信息每一个问题单独建一条链还要自己写路由逻辑。如果用户问题变了规则没覆盖到就答不上来。Agent 的思路是给模型准备几个工具比如查订单接口查物流接口查商品接口然后让它根据用户问题自己判断调用哪个工具。模型输出一个意图框架解析意图执行对应工具再把结果交回模型模型决定下一步动作。这个过程循环执行直到它认为可以回答用户为止。LangChain 里的 Agent 核心要素包括工具清单、工具描述、模型推理循环、停止条件。写工具的时候工具的描述信息特别重要——模型是靠描述来决定什么时候调用这个工具的描述不清晰功能再强模型也不会用。3. 动手搭一个能跑的问答应用完整流程与全部代码学习 LangChain 最忌只看概念不动手。我在这部分分享一个自己从零搭起来的文档问答应用代码可以直接参考数据流程和关键点我都会拆开讲清楚。这部分的实践用的是本地向量库、开源嵌入式模型和通用对话模型整套环境部署不需要复杂的 GPU 资源普通开发机能跑。3.1 项目结构与环境准备我先列一下目录结构和依赖项这块卡过不少人。langchain-qa/ ├── app.py # 主入口负责加载文档、建向量库、启动问答 ├── ingest.py # 文档导入脚本切分、向量化、写入向量库 ├── requirements.txt # 依赖清单 └── config.py # 模型配置、向量库路径等requirements.txt 里的关键依赖langchain0.1.0 langchain-community0.0.10 chromadb0.4.0 sentence-transformers2.2.0 pypdf3.15.0安装的时候有个小坑LangChain 新版本做了模块拆分很多组件不在主包里。比如langchain_community包负责社区维护的集成能力langchain_chroma负责 Chroma 向量库的接入。直接pip install langchain然后 import 某些模块会报错要把配套的额外依赖装全。3.2 文档导入与向量化Retriever 的质量决定回答质量问答应用的第一步是把原始文档切碎并向量化。这一步做的质量直接决定后面检索结果的质量可以说比模型选择还关键。# ingest.py from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.embeddings import HuggingFaceEmbeddings from langchain_community.vectorstores import Chroma # 1. 加载 PDF loader PyPDFLoader(docs/manual.pdf) documents loader.load() # 2. 切分文档 text_splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap80, separators[\n\n, \n, 。, , , , ], ) chunks text_splitter.split_documents(documents) # 3. 向量化并存入本地向量库 embeddings HuggingFaceEmbeddings(model_nameparaphrase-multilingual-MiniLM-L12-v2) vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directory./chroma_db ) vectorstore.persist()切分参数里chunk_size500表示每段大约 500 个字符chunk_overlap80表示相邻两段之间重叠 80 个字符。这里为什么要重叠因为如果语句恰好落在切分边界上前后信息会被截断检索的时候容易出现缺头少尾的情况。重叠部分相当于补丁尽量保持语义完整。RecursiveCharacterTextSplitter的切分逻辑也值得说一下。它是按优先级依次尝试分隔符切分的先用双换行\n\n切切出来的块还太大再用单换行\n切还大就按句号、感叹号切。这个顺序设计是为了优先保留语义完整的段落而不是无脑按字符数砍一刀。3.3 组装检索问答链RetrievalQA 简版实现文档进入向量库以后问答环节就是典型的 RAG检索增强生成流程先把用户问题向量化在向量库里找最相关的文档片段把片段和问题一起送给大模型让它基于片段回答。LangChain 里最省事的封装是RetrievalQA它是 Chain 的一种具体实现# app.py from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate from langchain_community.vectorstores import Chroma from langchain_community.embeddings import HuggingFaceEmbeddings embeddings HuggingFaceEmbeddings(model_nameparaphrase-multilingual-MiniLM-L12-v2) vectorstore Chroma(persist_directory./chroma_db, embedding_functionembeddings) retriever vectorstore.as_retriever(search_kwargs{k: 4}) llm load_your_llm() # 换成你的模型接入方式 prompt_template 你是文档助手。请根据以下资料回答问题。如果资料中没有答案请明确说“根据现有资料无法回答”。不要编造内容。 资料 {context} 问题{question} 回答 custom_prompt PromptTemplate.from_template(prompt_template) qa_chain RetrievalQA.from_chain_type( llmllm, retrieverretriever, chain_typestuff, chain_type_kwargs{prompt: custom_prompt}, ) answer qa_chain.invoke(这个产品的保修期是多久) print(answer[result])这里search_kwargs{k: 4}表示从向量库中检索 4 个最相关的文档片段。k 值大小直接影响效果和成本k 太小相关片段可能漏掉k 太大无关内容混进提示词模型容易被干扰token 消耗也大。我通常从 k4 开始调最多试到 k6观察检索到的内容是否有噪声。chain_typestuff表示把检索到的所有片段一次性全部塞进提示词。这种方式简单直接适合片段不多、总长可控的场景。如果文档量大、检索片段特别长就要考虑map_reduce或refine这类更复杂的链类型避免单次请求超出模型上下文限制。3.4 上一步的隐藏问题向量化模型的语言能力这套代码刚搭出来时效果并没有想象中好。中文问题经常答非所问后来排查发现问题不在 LangChain 本身而是我选的 embedding 模型对中文支持不够好。paraphrase-multilingual-MiniLM-L12-v2是个多语言模型理论上支持中文但实际对中文语义的区分度和专门的中文 embedding 模型相比还是有差距。领域专业文档里近义词、同义表达、简称和全称这类模型经常分不清。换成一个针对中文优化过的 embedding 模型之后检索准确率提升非常明显。这也提醒我RAG 系统中检索效果差的锅很多时候不该甩给 LangChain而是 embedding 模型没选对。判断 embedding 模型好不好用最直接的方法是在你自己的领域文档上跑几个典型问题看看检索回来的片段是不是真的相关。4. 学习期间的踩坑记录排查思路与修复全过程这一节记录我学习过程中印象最深的几个坑。LangChain 版本迭代非常频繁API 变动大如果直接照着网上旧教程抄代码大概率会遇到兼容问题。我把排查思路写出来希望你能少走弯路。4.1 版本混用API 突然从initialize_agent变成create_agent我第一次接触 LangChain 时网上主流教程还停在 0.0.x 版本代码普遍是from langchain.agents import initialize_agent。等我真正动手做项目版本已经到 0.1.x 甚至更新这个导入路径直接报错。遇到报错我犯过一个典型错误直接搜索报错信息复制别人的解决方案粘贴结果代码能跑但功能不对。后来冷静下来先查了官方文档的迁移指南才搞清楚新版把 Agent 的构建方式改成了create_agent风格并把一些工具管理逻辑拆分到langchain_community。排查思路大致是先确认你安装的版本号pip show langchain再对照当前版本文档写代码。不要盲目相信教程里的代码优先看官方 API 参考和你本机版本对应的文档。4.2 向量库持久化导致的老数据污染这是个隐蔽但杀伤力很大的坑。我有一次重新导入了一批新文档发现问答结果还是引用旧文档里的内容。排查了很久最后发现是 Chroma 的持久化目录没有清空新数据没有覆盖旧的向量索引检索的时候新旧内容混在一起返回。原因在于Chroma.from_documents默认是在已有集合上追加数据。第二次运行导入脚本时不会先清空旧索引而是直接把新文档向量追加到原集合里。如果你在测试阶段反复调整文档集合里就会积累一堆旧版本的数据。修复办法很简单重新导入前先删除持久化目录或者指定一个新的集合名称。import shutil # 删除旧的持久化目录 shutil.rmtree(./chroma_db, ignore_errorsTrue)这个坑提醒我向量库和普通数据库不同它没有覆盖写的概念数据管理策略必须自己设计好。4.3 大模型上下文窗口被塞满检索结果太多并不是好事使用 RAG 时我一度以为检索片段越多答案越丰富于是把 k 调到了 8结果回答质量反而直线下降。仔细分析后发现文档片段多了以后模型要在多个片段之间判断哪些信息可信而如果不同片段之间存在矛盾比如不同章节的表述不一致模型可能会被带偏或者回答里开始出现根据资料 A 和资料 B可能会出现... 这样含糊的内容。后来我把 k 调回到 4并且在提示词里加了一条明确要求如果检索资料之间存在矛盾优先采用最近修订的文档并说明你的依据。效果立刻好了不少。这说明了 RAG 调优的一个核心原则给模型的信息不是越多越好而是越有条理、越明确越好。4.4 代理工具的描述陷阱模型用不对工具别急着骂模型Agent 相关场景里有个很常见的问题模型不调用应该调用的工具或者老是调用错误的工具。我一开始以为是模型推理能力不够后来意识到工具描述写得不够清晰才是主因。比如我写了一个查天气的工具描述是查询天气信息。模型遇到北京今天冷不冷这种问法可能不会联想到要调用天气工具因为描述里没有温度气温冷热这类关联词。后来我把描述改成当用户询问气温、温度、冷暖、降水、天气等情况时调用此工具查询实时天气数据调用准确率立刻上去了。给 Agent 写工具描述时一定要站在模型的角度思考它看到这个描述能不能在它的推理过程中想到用这个工具描述里应该包含触发场景、入参格式、返回内容越具体越好。5. 学 LangChain 的一点个人建议抓住主线按需深入最后聊聊学习路径。LangChain 的文档量非常大组件多到让人一开始会觉得无从下手。我的经验是不要按文档顺序从头读到尾那样很容易迷失。先做一个小项目比如把最简单的提示词模型解析器跑通然后逐步加记忆、加快检、加工具调用。每加一个能力就深入学习一个概念这样知识是长在项目里的不容易忘。另外版本变化快旧教程和旧示例参考价值有限建议直接看官方文档和更新日志学习过程中主动关注两个点一是你装的版本是什么二是你用的 API 是否已经标注为 deprecated。如果发现自己参考的代码来自很老的版本别急着跑先确认迁移说明。我现在自己的开发流程一般是先用假数据和 FakeLLM 把链路逻辑跑通再接入真实模型做效果验证最后再做性能优化和数据管理策略。这个节奏踩过了不少坑但对新项目上手是效率最高的。LangChain 本质上只是一个工具它降低的是工程化的成本但降低不了你对模型边界、数据质量、业务逻辑的理解成本。真正把 RAG 调好、把 Agent 调好的人往往不是最会写框架代码的而是最懂自己领域数据和用户需求的人。这个观念想通了LangChain 反而没那么复杂了。