LangChain入门实战:从Prompt模板到RAG知识库的AI应用搭建指南

发布时间:2026/10/17 18:04:20
LangChain入门实战:从Prompt模板到RAG知识库的AI应用搭建指南
这两年身边越来越多写业务代码的同学开始往AI应用上靠。尤其是刚工作不久的小白程序员看到别人用大模型快速做出一个聊天助手、一个文档问答机器人心里多少有点痒。但真到自己上手第一反应往往是直接调模型接口不就行了于是开始拼Prompt字符串、手动解析输出、自己维护历史对话……Demo倒是能跑一旦想做得更完整代码立刻变成一团乱麻。我就是从这一步走过来的。那会儿我写了个简单的问答脚本需求稍微一加Prompt拼接的引号嵌套、JSON解析失败的try/except堆了一地那感觉比写业务代码还难受。后来接触了LangChain大模型框架算是把这条野路子收编成了正经流水线。它不是让模型变聪明而是把“调模型接口”这件事的周边环节——Prompt管理、输出解析、记忆、检索——全部标准化用统一的方式串起来。这篇文章就是给小白程序员的一份LangChain上手笔记我会从环境准备开始逐步拆解Prompt模板、输出解析、对话记忆、知识库检索这几块核心拼图最后再聊聊我踩过的几个坑。目标只有一个让你看完就能动手搭一个真正能用的AI应用。1. 先别急着敲代码LangChain到底帮你省了哪些事1.1 没有LangChain时手搓AI应用的三种狼狈先回想一下没有框架时一个“接入大模型”的功能是怎么写的。以聊天问答为例典型代码差不多长这样import requests def ask_model(api_key, user_input, historyNone): history history or [] messages [{role: system, content: 你是一个乐于助人的助手。}] messages history messages.append({role: user, content: user_input}) resp requests.post( https://api.example.com/v1/chat/completions, headers{Authorization: fBearer {api_key}}, json{model: your-model, messages: messages}, ) data resp.json() return data[choices][0][message][content]这段代码看着没问题但真实项目里需求会变今天要加角色设定明天要规定回答风格后天要模型输出JSON给前端用。于是你开始写各种拼接逻辑prompt f 你现在是一个{field}专家。 用户问题{question} 请用{style}的语气回答控制在{max_words}字以内。 这种写法有三个典型隐患。第一Prompt的全部逻辑散落在业务代码里字段一多引号、换行、转义就会出各种奇怪问题排错排到怀疑人生。第二模型返回的是纯文本前端要结构化数据时你得自己写解析器今天用正则抠明天用JSON去空格遇到模型偶尔输出的多余文字还得反复试错。第三换一个模型厂商SDK风格完全不同认证方式不同请求参数不同等于把对接代码重写一遍。这些琐碎工作本身不难但堆积起来非常消耗精力。而我接触LangChain之后最大的感受是它把这些高频重复的脏活累活抽象成了标准组件你需要操心的不再是“怎么把文本拼对”而是“我的流水线需要哪几个工位”。1.2 LangChain的核心思路把AI应用当成流水线来搭LangChain的核心抽象可以用一条流水线来理解。工厂里生产一个产品原材料经过若干工位每个工位只负责一件事最终出来成品。LangChain里你的输入文本就是原材料模型调用、Prompt组装、输出格式化就是工位用管道符连接起来就成了一条链。它提供的主要组件如下组件作用流水线类比Prompt模板把动态变量填进预设的Prompt结构工位操作手册模型封装统一不同厂商模型的调用差异万能插座输出解析器把散乱的模型回答变成结构化数据质检与打包岗记忆组件保存和整理多轮对话上下文工位旁的笔记本检索器从外部文档中找相关资料资料员最关键的还不是这些组件本身而是它们都遵守同一个接口协议——Runnable。只要实现了这个协议任意两个组件就能用管道符拼接。拼接完成后的整体本身又成了一个更大的Runnable可以继续参与拼接。这种设计带来的直接好处是你的应用从一个“塞满逻辑的函数”变成了“一段可读、可复用、可替换的流水线描述”每换一个环节的影响范围都被限制在了那一小段。1.3 也有不需要LangChain的时候可能有人会问那是不是所有AI功能都得套LangChain我的答案是未必这一点必须说清楚。如果你的需求只是一次性脚本比如临时调一下模型整理文本用requests几行代码搞定反而更爽。如果你的流程极其简单总共就“一个Prompt、一个模型调用”直接写裸调用也没什么问题——框架的抽象在这里不仅帮不上忙还会增加理解成本。我的判断标准就三条一是调用次数是否频繁、是否涉及多个模型二是输出格式是否多变、是否需要程序化消费三是是否要加记忆、检索之类的外围能力。只要命中两条以上LangChain就值得引入如果只是单个模型单次调用别迷信框架能少一层依赖就少一层。这也是所谓的“温伯格定律”在工具选择上的体现简单问题用简单工具复杂问题才用组合工具。2. 环境准备装依赖之前先把这几件事搞清楚2.1 Python版本与虚拟环境LangChain官方支持Python 3.8及以上但我强烈建议你直接用3.10或更新的版本。原因很实际新版本对类型注解的支持更完整LangChain内部很多类型提示在3.10下表现得更好调试时IDE的自动补全和类型检查也更准。对小白来说这能减少很多“莫名其妙跑不起来”的问题。项目级别一定要建虚拟环境这能避免不同项目之间的依赖打架。按我的习惯一个干净的项目环境只需要三步mkdir my-llm-app cd my-llm-app python -m venv .venv然后在命令行里激活虚拟环境。Windows系统执行.venv\Scripts\activatemacOS或Linux执行source .venv/bin/activate激活成功的标志是命令行提示符前面出现了(.venv)。从这一步开始你装的所有包都会隔离在这个项目里将来项目不要了删掉整个目录即可不会污染全局环境。2.2 安装包为什么拆成这么多先认清几个核心包第一次接触LangChain的同学很容易被它的包列表吓到——langchain、langchain-core、langchain-community、langchain-openai、langchain-chroma……怎么这么多这其实是LangChain的一个设计决策按需安装避免把全家桶都拖进你的项目。简单来说langchain-core是最底层的核心抽象定义了Runnable、Prompt模板、输出解析器这些基础概念类似手机的操作系统内核langchain是面向开发者的聚合包把常用能力打包在一起langchain-community是社区维护的第三方集成比如各种文档加载器langchain-openai则是模型厂商的专属适配层专门负责对接OpenAI风格的模型接口。以我常用的最小配置为例一条命令可以装完基础环境pip install langchain langchain-core langchain-community langchain-openai python-dotenv装完之后跑一个小命令验证环境是否正常python -c import langchain, langchain_openai; print(langchain.__version__)如果打印出版本号说明安装成功。后面用到的向量数据库、文本切分工具我会在对应章节说明需要额外补装哪些包到时候现装就行不用一开始就把所有东西都装上。2.3 API密钥管理与环境变量连接模型服务需要API密钥。新手最容易犯的错误是把密钥直接写死在代码里然后一不小心提交到了代码仓库。密钥泄露的后果轻则被刷掉额度重则被拿去跑一些不明用途的任务这个坑一定要从第一天就避开。我的习惯是在项目根目录建一个.env文件专门存放密钥OPENAI_API_KEYsk-xxxx OPENAI_API_BASEhttps://api.example.com/v1然后在代码入口加载环境变量from dotenv import load_dotenv load_dotenv()LangChain的很多模型封装类会自己读取OPENAI_API_KEY这个环境变量所以你不需要手动把它传进构造函数。假如你们公司用的是兼容OpenAI接口格式的模型服务只需要在创建模型对象时指定base_url参数即可。另外记得把.env加进.gitignore这个文件可以这么说进仓库前先三思。3. 第一个应用用Prompt模板和管道符跑通你的第一条链3.1 从裸调用的痛点说起新手最容易踩的坑就是把Prompt直接拿字符串拼。比如prompt f你是一位{field}专家。用户的问题是{question}。请用{style}回答。刚写的时候觉得挺灵活等字段多起来就麻烦了。角色设定、背景资料、示例、格式要求、字数限制……全部挤在一个字符串里改一个小需求就要调整整个模板结构而且变量一旦包含引号或反斜杠拼接出来的内容模型未必能正确理解。Prompt模板就是来解决这个问题的它把“角色、任务、输入、格式”拆成结构化的消息列表每个动态位都留好了占位符。这就像把菜谱从“随手写在大纸上”改成“每个步骤一张卡片”怎么改、改哪里一目了然。3.2 完整代码与逐行解读下面这段代码是我带人入门时最喜欢用的第一个例子。它完整跑通了一条最基础的链from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser # 1. 创建模型对象 llm ChatOpenAI( modelgpt-4o-mini, temperature0.3, ) # 2. 定义Prompt模板 prompt ChatPromptTemplate.from_messages([ (system, 你是一位{field}方面的专家回答问题要直接、具体。), (human, 我的问题是{question}), ]) # 3. 把三个组件用管道符拼成链 chain prompt | llm | StrOutputParser() # 4. 调用 answer chain.invoke({field: Java面试, question: HashMap底层原理是什么}) print(answer)拆开看每个部分。ChatOpenAI是对模型接口的封装temperature控制回答的随机性数值越低回答越稳定适合需要精确输出的场景。ChatPromptTemplate接收一个消息列表每条消息由角色和内容组成系统消息就是给模型立人设的用户消息才是真正的输入{field}和{question}是待填充的占位符。关键在第3步prompt | llm | StrOutputParser()。这里的竖线是LangChain表达式LCEL的管道符含义是“把上一段输出作为下一段的输入”。提示词模板的输出是一组消息喂给模型模型返回一个包含多个字段的响应对象经过StrOutputParser提取出纯文本内容最终得到一个字符串。整条链调用时只需要传入一个字典模板里的占位符会被自动填上。3.3 输出解析器让模型交作业前先按格式来很多人第一次写AI应用时都想过让模型输出JSON。但直接说“请输出JSON”很容易翻车模型偶尔会在JSON外面包一层Markdown代码块或者在里面夹带解释文字。输出解析器就是用来根治这个问题的。以JsonOutputParser为例它会自动生成一段“格式说明”附加到Prompt后面要求模型只输出纯净的JSON同时解析器内部自带纠错逻辑from langchain_core.output_parsers import JsonOutputParser from langchain_core.prompts import ChatPromptTemplate parser JsonOutputParser() prompt ChatPromptTemplate.from_messages([ (system, 只输出JSON不要多余解释。{format_instructions}), (human, {query}), ]).partial(format_instructionsparser.get_format_instructions()) chain prompt | llm | parser data chain.invoke({query: 推荐3本适合初学者的Python书包含书名、作者、推荐理由}) print(data)这样拿到的data就是正经的Python字典或列表可以直接丢给前端或写进数据库。格式说明那段文字由get_format_instructions()动态生成你不用自己去琢磨措辞。3.4 Runnable协议为什么所有积木都能拼在一起上面用到了三个组件它们能互相拼接的底层原因是都实现了Runnable协议。这个协议规定了四个核心方法invoke()标准调用输入一个对象返回一个结果batch()批量调用传入一组输入返回一组结果stream()流式调用逐块返回结果适合打字机效果ainvoke()异步版本的invoke()用于协程环境。正因为所有组件都遵守这套协议所以你拼出来的一条链、一个管道本身也实现了同样的方法。这意味着你可以把若干条链作为一个大组件继续往更大的流水线上拼。一个特别实用的小验证是把上面的链改成流式输出chain prompt | llm | StrOutputParser() for chunk in chain.stream({field: 健身, question: 每天只做俯卧撑能练出好身材吗}): print(chunk, end, flushTrue)终端里会一个字一个字蹦出回答。如果做前端这就是“打字机”效果的数据来源。4. 对话记忆与结构化输出让应用看起来“有脑子”4.1 无状态接口做不了聊天机器人大模型接口本身是无状态的你每调用一次它都不知道上一次聊了什么。所以做聊天机器人第一步就是自己保存对话历史。新手最常见的做法是把所有历史消息攒在一个列表里每次请求全部塞进去。这个方案在最开始几轮确实有效但它有两个问题一是历史越长消耗的Token越多成本线性上涨二是模型对上下文的接受范围有限超出窗口后最老的对话会被“挤掉”模型就开始“失忆”。4.2 用会话历史把记忆挂到链上LangChain处理记忆的思路很清晰把历史消息当成一个看不见的输入源自动和当前输入一起喂给Prompt模板。它会根据你指定的会话ID来区分不同的对话窗口这样同一个后端就能同时服务多个用户互不串台。具体代码如下from langchain_openai import ChatOpenAI from langchain_core.chat_history import InMemoryChatMessageHistory from langchain_core.runnables.history import RunnableWithMessageHistory from langchain_core.prompts import ChatPromptTemplate llm ChatOpenAI(modelgpt-4o-mini, temperature0.3) sessions {} def get_session(session_id: str): return sessions.setdefault(session_id, InMemoryChatMessageHistory()) prompt ChatPromptTemplate.from_messages([ (system, 你是一个耐心的学习助手。), (placeholder, {history}), (human, {input}), ]) chain prompt | llm chain_with_history RunnableWithMessageHistory( chain, get_session, input_messages_keyinput, history_messages_keyhistory, ) chain_with_history.invoke( {input: 我叫小明请记住这个名字。}, config{configurable: {session_id: stu-001}}, ) chain_with_history.invoke( {input: 我叫什么名字}, config{configurable: {session_id: stu-001}}, )第二句问答模型会正确回答“你叫小明”因为历史已经通过{history}这个占位符注入到Prompt里了。通信用同一个session_id就能延续上下文换成另一个ID就是全新的对话。这里用的InMemoryChatMessageHistory是把历史存在进程内存里适合开发和演示生产环境建议替换成数据库或Redis的实现避免服务重启后记忆丢失。还有一个细节值得注意如果对话轮数很多历史消息累积到几万Token调用成本和质量都会出问题。我的建议是保留最近N轮完整对话更早的内容定期用模型做摘要压缩把“完整对话”变成“一句话梗概”既保住大方向上的上下文又不至于撑爆窗口。4.3 结构化输出与其和JSON搏斗不如让模型直接给规范结果4.1到4.2解决的是“记忆”但AI应用还经常遇到另一个需求让模型输出强结构化的数据比如“从这段简历里抽取姓名、工作年限、技能列表”。LangChain的with_structured_output可以说是这个场景下的神器它借助模型内置的工具调用能力直接返回一个校验过的Pydantic对象。先定义一个数据模型from pydantic import BaseModel, Field class Book(BaseModel): title: str Field(description书名) author: str Field(description作者) rating: float Field(description推荐指数满分10分) class BookList(BaseModel): books: list[Book] Field(description推荐书目列表) structured_llm llm.with_structured_output(BookList) result structured_llm.invoke(推荐3本适合新手的Python入门书) for book in result.books: print(book.title, book.author, book.rating)用这个方式拿到的结果字段类型早就被Field描述约束住了无需再写parser和容错逻辑。字段缺失、类型不对这类问题模型在生成阶段就会被引导着规避。如果你手头的数据结构比较灵活也可以直接传一个JSON Schema进去让with_structured_output按Schema约束生成。这个能力在做信息抽取、表单自动填充、搜索结果整理之类的功能时能显著减少脏数据。5. 知识库问答实战把内部文档变成AI的“外接大脑”5.1 RAG的三步套路很多小白会问想让模型回答我公司内部文档里的问题是不是要微调模型答案通常是不用。微调成本高、周期长而且对“知识更新型”的需求并不划算。更主流的做法是RAG检索增强生成。它的思路特别朴素每次用户提问时先去你的文档库里把最相关的几段内容找出来连同问题一起塞给模型让模型“看着资料回答”。整个过程就是三板斧步骤做什么关键点1. 文档处理加载文档并切成小段切分粒度直接影响检索质量2. 向量化入库把每段文字变成向量存入向量数据库相似文本的向量距离近3. 检索增强生成查相似片段连同问题交给模型回答检索结果决定回答上限形象点说RAG像是给模型配了一个随身的资料员模型每次回答前资料员先把相关页码翻好递过去模型照着资料说话既不凭空编造又能覆盖你私有文档里的内容。5.2 从文档到可检索向量库的完整代码这段代码以加载一个纯文本文档为例完整演示了前三板斧。先安装需要的包pip install langchain-text-splitters langchain-chroma然后写流程代码from langchain_community.document_loaders import TextLoader from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_openai import OpenAIEmbeddings, ChatOpenAI from langchain_chroma import Chroma # 第一步加载文档 loader TextLoader(product_manual.txt, encodingutf-8) docs loader.load() # 第二步切分 splitter RecursiveCharacterTextSplitter( chunk_size500, chunk_overlap50, ) chunks splitter.split_documents(docs) print(f共切出 {len(chunks)} 个片段) # 第三步向量化并存入向量库 embeddings OpenAIEmbeddings(modeltext-embedding-3-small) vectorstore Chroma.from_documents( documentschunks, embeddingembeddings, persist_directory./chroma_db, ) retriever vectorstore.as_retriever(search_kwargs{k: 4})这里有个值得解释的设计RecursiveCharacterTextSplitter会按照一组分隔符从大到小递归切割文本优先保持段落和句子的完整性。chunk_size500表示每个片段大约500个字符chunk_overlap50表示相邻片段保留50个字符的重叠。重叠部分的作用是避免一个完整句子恰好被拦腰切断导致检索时上下文缺失。接下来组装最终的问答链from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough def docs_to_text(docs): return \n\n.join(doc.page_content for doc in docs) rag_prompt ChatPromptTemplate.from_messages([ (system, 只根据以下资料回答问题资料里没有的信息就说不知道不要编造。\n\n资料\n{context}), (human, {question}), ]) rag_chain ( {context: retriever | docs_to_text, question: RunnablePassthrough()} | rag_prompt | llm | StrOutputParser() ) answer rag_chain.invoke(这款产品支持哪些售后服务) print(answer)这个链的巧妙之处在开头那个字典它把原始问题同时分发到两条支路一条经过检索器找出相关片段并拼接成文字另一条原样保留问题本身两路结果最终合并进模板的context和question。这种并行结构正是Runnable协议带来的表达能力。5.3 检索效果不好时先调这几个旋钮RAG系统的回答质量很大程度取决于检索阶段能不能找到对的片段。如果你发现模型经常答非所问按优先级检查下面几项。第一是切分粒度。chunk_size太大一个片段里混入太多无关信息向量相似度会被拉低太小一个知识点被切碎到多个片段里检索命中的只是其中一小截。通常我会从500到800之间起步再结合实际文档微调。第二是重叠大小遇到代码、表格这类结构文本建议把chunk_overlap调到100以上避免关键行被切断。第三是返回数量k默认4够用但如果你发现答案太少可以适当提到6或8给模型更多参考素材。还有一个进阶技巧是加元数据过滤。比如文档包含售后、安装、参数多个章节可以通过给每个片段打标签再在检索时限定范围vectorstore.as_retriever( search_kwargs{k: 4, filter: {category: 售后服务}} )这样检索就不会被其他章节的相似文本带偏。中文场景下embedding模型本身的选择也值得多试几个有些模型对中文语义的理解明显更好。这些参数没有绝对的最优值关键是要建立“先看检索结果再调参数”的调试习惯。6. 小白最容易翻车的几个坑我一个个替你踩过了6.1 版本漂移教程代码跑不起来的头号原因LangChain迭代速度非常快网上大量教程和远古版本的代码对不上是小白最常见的挫败来源。我经常看到有人在社区提问说“照着教程抄都跑不起来”点开一看他抄的是半年前的写法而装的是最新版。应对这个问题没有花哨技巧就是三点一是装完包之后先打印版本号心里有数二是直接用requirements.txt把关键依赖的版本固定住防止某天更新后行为突变三是在代码里看到DeprecationWarning别随手忽略这个警告就是在告诉你“你这样写迟早会坏趁早改”。按我当时用的版本一份可复现的依赖清单长这样langchain0.3.7 langchain-core0.3.15 langchain-openai0.2.10 langchain-community0.3.7另外提醒一个细节很多新版功能已经迁移到了langchain-core里如果你在langchain包下找不到某个类先试试从langchain_core导入。这种“搬家”式改动在LangChain里很常见写代码时以官方文档为准而不是以老教程为准。6.2 上下文窗口模型“失忆”和报错的真凶第二个高频坑是上下文窗口超限。表现有两种一种是在长对话或长文档的场景下调用直接报错说超过了最大Token限制另一种是没报错但回答质量明显下降模型开始忘记最初的指令或前提——本质是超长内容里“挤掉”了关键信息。排查思路很简单先估算每次调用送进去多少Token。我常用tiktoken库来做这件事import tiktoken enc tiktoken.get_encoding(cl100k_base) text 待估算的文本内容 print(len(enc.encode(text)))然后结合模型的最大上下文给对话历史设一个硬上限比如最多保留最近20轮消息另外准备一个超限兜底逻辑如果历史太长就把最早的若干轮做一个摘要用摘要替代原文。这个“滚动摘要”方案比简单截断要优雅得多能在很小成本下保住长程记忆。6.3 把业务逻辑写进链里维护噩梦的开端最后一个坑是新手很容易把业务判断直接塞进链的lambda表达式里比如chain ( {context: ...} | rag_prompt | llm | RunnableLambda(lambda x: 紧急 if 紧急 in x else x) )一两个lambda还能忍多了之后整条链读起来非常吃力也更难测试。我的原则是链只负责“输入到输出的标准转换”业务规则一律抽成普通Python函数写在链外面再通过RunnableLambda包一层接进来。这样业务逻辑可以单独写单元测试链的流转只负责数据形态的变换。顺带分享两个我从项目里总结的实操习惯。第一个是Prompt也要做版本管理最好把系统提示词抽到配置文件或单独文件里改动时留一份历史记录否则线上反馈“回答变了”时你很难说清是模型变了还是Prompt变了。第二个是给入口调用加日志记录每次调用的输入、输出和Token消耗排查问题时会轻松非常多。这些细节单看不值钱积累起来就是生产级应用和Demo之间的分水岭。最后说点我自己的体会。用LangChain最舒服的不是那些花哨组件而是那个不起眼的管道符。它逼着我把所有输入、输出、中间转换都想成标准接口想通了这一点遇到新需求时的心态完全不一样不是“这功能怎么实现”而是“哪个现成的Runnable拼上去就行”。如果你读到这里准备动手我的建议是别贪多。先照着第3节的例子把第一条链跑起来然后挑一个自己的小需求——比如把你手头的一个文档做成问答机器人——把第5节那套流程完整走一遍。中间遇到的问题再回头对照第6节的坑。框架更新很快但“输入标准化、处理流水线化、输出结构化”这套思路不会变。想清楚这个LangChain对你来说就是一层随时能掀开的盖子而不是一个必须背下来的黑盒。