简介这是一份LangChain入门与实践的项目代码包面向正在学习大模型应用开发的Python开发者旨在帮助读者快速理解LangChain的模块化设计与核心用法。包内共3个文件包含一个HTML格式的入门讲解页、一个项目运行配置文件以及一个版本管理忽略文件压缩后仅5KB体量非常轻巧。LangChain作为连接LLM与外部数据、工具的开源框架其核心模块涉及提示词模板、链式操作、记忆功能和工具调用等读者可借助这份代码包中的HTML页面快速建立对框架结构的整体认知并根据配置信息了解项目的基础组织方式进而结合官方文档动手实践。该代码包对应的实践内容可迁移至智能问答、对话机器人等常见落地场景帮助初学者在极低时间成本下感受LangChain的典型用法。目前已有107人学习文件虽少但聚焦核心适合用极低成本开启LangChain的代码初体验。1. 拿到《LangChain入门与实践》项目代码别直接跑先看清四条主线再动手拿到一份《LangChain入门与实践》项目代码最容易被看出外行的动作是翻完 README 就直接跑 demo。LangChain 版本迭代速度很快代码里很多写法在你本地环境里可能已经换新跑通只是第一关真正值钱的是看懂 Chain、Agent、Memory、Retrieval 这四条主线在代码里是怎么被编排的。这篇文章要解决的不只是怎么运行而是沿着项目代码的结构把每条主线对应的实现、参数和常见翻车点讲清楚。适合刚写完一轮 Prompt 想开始做应用的开发者也适合准备在团队里搭 LLM 工具链、需要拆别人代码找思路的同学。2. 拆开项目代码外壳先认准 Chain、Agent、Memory、Retrieval 四根柱子LangChain 的入门项目代码通常会按功能拆出几个脚本看起来文件很多追一条请求进去就能发现所有逻辑都围绕四类组件Chain 负责编排一次或多次调用Agent 负责在循环里自主决策Memory 负责跨轮记忆Retrieval 负责把外部知识带进 Prompt。先认准它们读代码就不会迷路。2.1 最小 Chain一条 Prompt 通过管道符变成一次模型调用Chain 是理解 LangChain 的起点。最精简的一条链是 Prompt 加上模型再加上输出解析器。项目代码里的 chain_demo.py 往往长这样。# chain_demo.py from langchain_core.language_models import BaseChatModel from langchain_core.messages import AIMessage from langchain_core.outputs import ChatGeneration, ChatResult from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate class FakeChatModel(BaseChatModel): 离线模拟模型不连外部服务先把链路跑通。 def _generate(self, messages, stopNone, run_managerNone, **kwargs): last messages[-1].content return ChatResult(generations[ ChatGeneration(messageAIMessage( contentf这是离线模拟回答最后一段消息是{last[:20]} )) ]) property def _llm_type(self): return fake-chat-model prompt ChatPromptTemplate.from_template(请用一句话解释{topic}) fake_llm FakeChatModel() chain prompt | fake_llm | StrOutputParser() print(chain.invoke({topic: LangChain}))这段代码里的管道符|是 LangChain Expression Language 的串联语法也可以叫 LCEL。执行到chain.invoke时LangChain 先把{topic: LangChain}填进 Prompt 模板再把格式化后的消息发给模型拿到ChatResult后由StrOutputParser转成纯字符串返回。FakeChatModel继承的是BaseChatModel它强制实现_generate方法这是所有 ChatModel 的统一入口真实项目里你只需要把FakeChatModel替换成你接入的模型服务商提供的驱动类链的其余代码不用改。BaseChatModel抽象的价值在于把“换模型”的成本降到最低。你更换供应商时只改驱动类实例化那一行后面的 Prompt、解析器、调用方式全部保持一致。入门项目代码里用这种抽象目的就是让你先忽略供应商差异专注理解结构。如果想让这条链支持流式输出可以调用chain.stream({topic: Agent})它会逐块产出内容。chain.batch([{topic: Memory}, {topic: Retrieval}])则是一次传入多个输入并行处理。项目代码里这三种调用方式都可能出现看到.invoke、.stream、.batch就明白对应的是单次、流式、批量场景。2.2 Agent让模型自己决定调用哪个工具项目代码里长这样Agent 要解决的问题是模型不能只回答还要“动手”。比如用户问“3 加 4 再乘以 2 等于多少”模型如果直接口算算错的风险很高正确做法是调用加法工具和乘法工具。项目代码里的 agent_demo.py 通常会这样注册工具。# agent_demo.py from langchain.agents import create_tool_calling_agent, AgentExecutor from langchain_core.prompts import ChatPromptTemplate from langchain_core.tools import tool tool def add(a: int, b: int) - int: 两个整数相加结果返回整数。 return a b tool def multiply(a: int, b: int) - int: 两个整数相乘结果返回整数。 return a * b tools [add, multiply] prompt ChatPromptTemplate.from_messages([ (system, 你是数学助手必须用工具计算结果不要自己推断。), (human, {input}), (placeholder, {agent_scratchpad}), ]) # 注意这里需要替换成支持工具调用的真实模型驱动类FakeChatModel 只能演示 Chain agent create_tool_calling_agent(fake_llm, tools, prompt) executor AgentExecutor(agentagent, toolstools, verboseTrue) resp executor.invoke({input: 3加4再乘以2等于多少}) print(resp)先明确一点上面这段是项目代码里的标准结构但它能不能跑起来取决于fake_llm是否允许工具调用。自定义的FakeChatModel只是返回固定文本不具备任务决策能力所以你在本地复现时要把这一行换成真实模型服务商提供的 ChatModel 驱动类确认它支持 tool calling 再执行。tool装饰器做了两件事一是把普通函数变成一个 Tool 对象二是把函数签名和 docstring 转成模型能读到的 JSON Schema。模型的判断依据主要是函数名、参数类型和第一行描述。create_tool_calling_agent会把这些信息塞进系统上下文AgentExecutor负责循环模型先输出一个意图执行器决定调用哪个工具拿到结果后追加到agent_scratchpad再交回给模型直到模型认为任务完成或达到最大步数。这里有几个参数值得关注。verboseTrue会打印中间步骤调试时能看到模型先写了什么、工具返回了什么这是排查 Agent 行为最重要的开关。max_iterations可以限制循环上限防止模型在一个问题上反复调用工具停不下来。handle_parsing_errorsTrue则会在模型输出格式不合法时把错误信息回传给模型让它自己修正代价是多一次模型调用。如果项目代码里用的是create_react_agent那是另一套 ReAct 范式模型需要按照思考、动作、动作输入、观察四段格式输出再由解析器拆分。两套机制不要混用看到create_tool_calling_agent就别尝试用 ReAct 的 Prompt 模板反之亦然。2.3 Memory 与 Retrieval项目代码里的对话记忆和外部知识查询大模型本身不记上下文Memory 和 Retrieval 就是它的“外挂记忆”。Memory 管的是对话历史Retrieval 管的是外部文档。两者经常被混淆但代码形态完全不同。# memory_demo.py from langchain.memory import ConversationBufferMemory from langchain.chains import ConversationChain memory ConversationBufferMemory(return_messagesTrue) conversation ConversationChain(llmfake_llm, memorymemory, verboseTrue) print(conversation.predict(input你好我叫A同学)) print(conversation.predict(input请记住我的名字)) print(conversation.predict(input你记得我叫什么吗))ConversationBufferMemory会把每一轮对话原样缓存下次调用时把全部历史拼进 Prompt。return_messagesTrue表示以消息列表返回而不是拼成一整段文本这对 ChatModel 更友好因为它天然支持多轮消息结构。ConversationChain是老项目代码里最常见的写法新版 LangChain 更推荐你直接用RunnableWithMessageHistory配合一个历史存储对象。入门代码里出现ConversationChain不奇怪但你要知道它在新版里已经被标为过时新项目建议换掉。Retrieval 的离线演示代码一般是这样# retrieval_demo.py from langchain_core.embeddings import DeterministicFakeEmbeddings from langchain_core.vectorstores import InMemoryVectorStore texts [ LangChain可以用链的方式编排大模型调用, Memory组件让对话记住历史信息, Retrieval组件把外部知识检索进提示词, ] embeddings DeterministicFakeEmbeddings(size8) vector_store InMemoryVectorStore.from_texts(texts, embeddingembeddings) retriever vector_store.as_retriever(search_kwargs{k: 2}) hits retriever.invoke(LangChain的核心组件有哪些) for doc in hits: print(doc.page_content)这个演示刻意用了假嵌入和内存向量库目的是让你在没有任何外部服务的情况下跑通检索流程。DeterministicFakeEmbeddings生成固定的随机向量每个文本都会被变成一个数组InMemoryVectorStore把这些向量放在内存里做近似搜索重启进程就丢失。真实项目里这两行会换成真正的嵌入模型和持久化向量库。search_kwargs{k: 2}是检索参数表示返回最相近的前 2 段。k 值太大容易引入噪声k 值太小可能漏掉关键信息。项目代码里如果检索质量不好先不是改模型而是先检查文本分块是否合理、嵌入模型是否一致紧接着去看 4.4 的那组避坑记录。3. 在本地跑通项目代码依赖版本、配置项与最小复现路径看懂结构之后下一步就是动手跑。这个阶段最容易出问题的是环境和配置。LangChain 的安装逻辑过去几年变过多次把环境搭对了后面踩坑数量能少一大半。3.1 先建虚拟环境再装依赖Python 版本与包管理器怎么选拿到项目代码第一件事不是pip install langchain而是先建一个干净的虚拟环境。LangChain 依赖树比较复杂不同项目对版本要求差异很大直接用全局环境装容易把系统 Python 搞乱。我一般会用下面这组命令。python3.11 -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install langchain0.1,1.0 langchain-core langchain-community pip install -e .虚拟环境命令是通用做法python3.11指 Python 3.11 解释器版本。LangChain 在 Python 3.8 到 3.12 上基本都能跑但建议选 3.10 或 3.11这个区间对异步调用和类型注解的支持最稳也少遇到某些依赖包编译报错。langchain0.1,1.0是一个比较宽的范围因为 LangChain 从 0.1 开始把组件拆分到独立包老代码里常见的langchain.llms在新版里变成了langchain_community或langchain_core。langchain-core是核心抽象层包含 BaseChatModel、Prompt 模板、输出解析器这些与供应商无关的部分。langchain-community是社区集成层里面放着文档加载器、向量库、各类模型驱动。如果项目代码里用到了文本分切还需要单独安装配套包常见做法是把它们写进项目的依赖声明文件用一条pip install -e .把当前项目及其依赖一起装入当前虚拟环境。这种方式的好处是项目代码的入口脚本可以直接用包内模块名导入不会出现路径找不到的问题。还要提醒一句Windows 上激活虚拟环境的命令是.venv\Scripts\activatemacOS 和 Linux 才是source .venv/bin/activate。跑通一次之后建议用pip freeze requirements.lock把当前环境版本固定下来下次复现或交给其他同学时能少很多“在我这是好的”的争议。3.2 最小运行先跑通一个不带外部依赖的脚本环境就绪后不要急着跑 Agent那个脚本依赖真实模型服务。先跑只靠 FakeChatModel 就能完成的最小脚本验证框架安装有没有问题。python src/chain_demo.py预期输出是一行以“这是离线模拟回答”开头的话内容会包含你传入的 topic 的前几个字。只要这个脚本能正常输出就说明 langchain-core、langchain-community 以及项目里的核心模块都装对了。接下来再依次跑 memory_demo.py 和 retrieval_demo.py这两个同样不依赖真实模型服务。项目代码通常会把入口脚本放在src/目录下有的还会带tests/目录。最小运行顺序可以参考这张表脚本外部依赖验证目标chain_demo.py无Prompt、模型、输出解析器能否串成一条链memory_demo.py无Memory 是否记住上一轮输入retrieval_demo.py无分块、向量化、检索是否能返回文档agent_demo.py支持工具调用的真实模型服务Agent 是否能正确调用工具如果 chain_demo 就报错先看报错信息里有没有ModuleNotFoundError。一眼扫到某个langchain.xxx找不到基本就是版本拆分问题直接去 4.1 看解法。如果提示缺少依赖包用pip list看当前环境装了哪些包再对照项目的依赖清单补齐。不要凭感觉装最新版很多入门代码锁定的是一两个月前的 API装太新的版本反而会跑挂。3.3 接入真实模型环境变量、Temperature、MaxTokens 等必调参数跑通离线脚本后就该让代码连上真实模型服务了。常见做法是把 API Key 和模型名放在环境变量里避免硬编码进源码。项目代码里通常会有这样一个配置模块。# llm_config.py import os from dataclasses import dataclass dataclass class LLMConfig: model_name: str default-chat-model temperature: float 0.7 max_tokens: int 512 top_p: float 1.0 api_key: str def load_config_from_env() - LLMConfig: return LLMConfig( model_nameos.getenv(MODEL_NAME, default-chat-model), temperaturefloat(os.getenv(TEMPERATURE, 0.7)), max_tokensint(os.getenv(MAX_TOKENS, 512)), top_pfloat(os.getenv(TOP_P, 1.0)), api_keyos.getenv(LLM_API_KEY, ), )这段代码只负责读取配置真正创建模型时需要把LLMConfig传给项目接入的模型驱动类。不同供应商的驱动类参数名略有区别但model_name、temperature、max_tokens、top_p这四类是通用参数。参数名默认值影响model_namedefault-chat-model决定模型能力边界复杂任务要选更强模型temperature0.7值越大输出越随机值越小越稳定max_tokens512单次生成的最大 token 数超出会被截断top_p1.0核采样阈值调低后只从累积概率内的词中采样项目代码里的.env.example文件通常会列出这些变量名复制一份改成.env再配合一个环境变量加载工具就能使用。没有加载工具时也可以直接在终端里用export设置效果一样。调参时我的一个经验是不要同时动 temperature 和 top_p。先固定一个再单独调另一个一次只改一个变量否则你很难判断是哪个参数导致输出突然变差。温度调参在不同模型上的表现差异很大很多现象只能用体感解释所以每次修改都记录当时的输出不然会陷入调参玄学。4. LangChain 项目代码的 5 个常见坑现象、原因、排查路径读 LangChain 项目代码最常见的情况是“版本看着对但跑不通”。很多坑不是代码逻辑问题而是框架演进造成的兼容性问题。下面五条是按出现频率排的每一条都按现象、原因、解决三部分展开方便你照着排查。4.1 一导入就报错ModuleNotFoundError 背后的 LangChain 包拆分现象运行脚本时出现ModuleNotFoundError: No module named langchain.llms或者langchain.vectorstores。代码明明没问题导入就是过不了。原因LangChain 0.1 之后做了包拆分原先集中在langchain主包里的模型、向量库、文档加载器被移到了langchain-community或langchain-core。旧版入门项目代码写的导入路径自然就失效了。解决先确认当前版本再更新导入路径。python -c import langchain; print(langchain.__version__)如果版本是 0.1 及以上把旧写法改成新写法# 旧写法from langchain.llms import YourLLM # 新写法from langchain_community.llms import YourLLM这里YourLLM只是占位符真实项目里会是某个具体模型驱动类的名字你在项目代码里看到哪个类导入失败就把包名改成langchain_community再试。还有一种情况是langchain.chains里的某些链被拆到langchain主包的特定子模块改动相对少核心思路是先看报错路径再判断它属于抽象层、社区集成层还是应用层。不要图省事直接降级到老版本那样会丢掉后续安全修复而且其他依赖也容易被拖进版本地狱。4.2 Agent 不调用工具反而把工具描述当答案输出现象Agent 脚本能正常执行但模型没有调用add或multiply反而在回复里写出“根据工具描述你可以使用加法函数”或者直接自己心算一个结果给你。原因通常是三种情况叠加。第一你接入的模型驱动类不支持工具调用LangChain 只是把工具列表传给模型模型没有相关能力去解析和决策第二工具函数的描述太含糊模型不知道什么情况下该触发它第三系统提示词里写了“必须用工具”但没有说明判断条件模型在挣扎后选择自由发挥。解决先换一个支持工具调用的模型驱动类然后在工具 docstring 第一行写明触发条件。tool def add(a: int, b: int) - int: 仅当用户要求计算加法时调用返回 a b 的整数结果。 return a bdocstring 第一行会被模型当作工具的主要说明所以不要写“这是一个加法函数”这种同义反复要让模型知道“用户在什么场景下应该选你”。改完工具描述后再把verboseTrue打开观察中间步骤。如果模型输出里出现了Action Input但解析器报错说明输出格式带有多余字符先在工具描述的结尾补一句“参数必须是合法 JSON”或者开启handle_parsing_errorsTrue。这类问题最怕的就是只改 prompt 不看执行日志日志会告诉你到底是模型不调工具还是调了但解析失败。4.3 Memory 越聊越慢最后直接报告上下文超限现象开启 Memory 后前几轮对话正常聊到第 5 轮以后首字延迟明显变长再往后直接报context length exceeded或类似错误。原因ConversationBufferMemory会把所有历史消息原封不动地拼进 Prompttoken 数是线性增长的。模型上下文窗口有限塞满了之后被迫报错。很多入门项目代码天生就带这个坑因为示例里只聊两三轮看不出问题一旦真用起来立刻翻车。解决换成带摘要能力的 Memory给历史设一个 token 上限。from langchain.memory import ConversationSummaryBufferMemory memory ConversationSummaryBufferMemory( llmfake_llm, # 真实项目替换为能生成摘要的模型驱动类 max_token_limit300, return_messagesTrue, )ConversationSummaryBufferMemory的逻辑是当原始历史超过max_token_limit时它会用llm生成一段摘要然后只保留最近几条原始消息其余全部替换成摘要。这样无论聊多久历史长度都能被压在可控范围内。代价是每次触发摘要都会多一次模型调用所以max_token_limit不能设得太小否则每聊几轮就要摘要一次成本和延迟反而更高。一个相对保险的起点是 300 到 500再根据你任务的上下文长度和预算调整。如果你只是临时验证不想引入摘要可以用手动截断直接操作memory.chat_memory.messages只保留最后 N 条但这会丢掉前面所有的语义不是长期方案。项目代码里如果看到 BufferMemory 跑在长对话场景基本可以直接判断是设计失误。4.4 Retrieval 查出来的是无关内容分块、Embedding 与 k 值现象检索脚本没有报错返回的doc.page_content却跟用户问题完全没关系或者命中了一个通用句子但没命中真正有用的细节。原因大部分情况是分块方式不对。整段文本过长时一条 chunk 里塞了多个主题生成向量后这些语义相互抵消检索效果自然变差。也可能是因为构建向量库和查询时用了不同的 Embedding 模型导致向量空间不一致。还有一种可能是 k 值设得不准太小漏掉关键段落太大把噪声捞进来。解决先看分块参数再确认 Embedding 一致。from langchain_text_splitters import RecursiveCharacterTextSplitter splitter RecursiveCharacterTextSplitter( chunk_size200, chunk_overlap50, ) chunks splitter.split_text(long_text)chunk_size200是按字符数切分不是 token 数。过大会稀释语义过小会切断一句话。chunk_overlap50表示相邻块之间保留 50 个字符的重叠确保被切到边界的那句话在两个块里都保持完整。切完以后先打印两段结果肉眼看语义是否完整这是最笨也最有效的检查方式。嵌入模型不一致的问题更隐蔽。项目代码里如果分了两步建索引和查询要确保构建vector_store时用的embeddings实例和retriever里实际查询用的实例是同一个模型。哪怕都是同一个向量库文件如果查询时换了新模型同一个句子的向量也可能需要重新生成。排查方法是在两处各打印一条文本的向量维度维度变了或者数值范围对不上基本就能锁定。4.5 异步并发时遇到事件循环报错Future attached to a different loop现象在 Jupyter Notebook 或者一段脚本里调用ainvoke或异步 Agent 时报错RuntimeError: Event loop is closed或Future attached to a different loop。同步调用一切正常改成异步就翻车。原因LangChain 的异步接口依赖 asyncio 事件循环。Notebook 环境自带一个正在运行的事件循环代码里如果又用asyncio.run()创建新循环二者会互相争抢导致 Future 被绑定到错误的循环上。这种情况几乎和项目代码无关是调用方式的问题。解决把整个异步流程收敛到一个统一的 async 函数入口里只在最后调用一次asyncio.run。import asyncio async def main(): resp await agent_executor.ainvoke({input: 3加4再乘以2等于多少}) print(resp) return resp if __name__ __main__: asyncio.run(main())不要在 async 函数内部再嵌套asyncio.run也不要在一个函数里反复创建新的事件循环。如果需要并发处理多个输入用await asyncio.gather(...)把多个任务包在一起等它们全部完成后统一返回。这个坑常见于从同步示例改成异步批量调用时改的时候注意把入口收口避免每个请求都自成一套循环。5. 从跑通到敢改给项目代码加一层观测再做一次端到端验证跑通只是起点真正敢改项目代码需要先知道每一次模型调用和工具调用到底发生了什么。LangChain 提供了一套回调机制比verboseTrue更清晰也更容易沉淀成自己团队的调试工具。5.1 给 LangChain 项目代码加一个回调每一步发生了什么都能看见# trace_callback.py from langchain_core.callbacks import BaseCallbackHandler class TraceCallback(BaseCallbackHandler): def on_llm_start(self, serialized, prompts, **kwargs): print(f[LLM 启动] prompts{prompts}) def on_llm_end(self, response, **kwargs): text response.generations[0][0].text print(f[LLM 结束] text{text[:100]}) def on_tool_start(self, serialized, input_str, **kwargs): print(f[工具调用] name{serialized.get(name)} input{input_str}) def on_tool_end(self, output, **kwargs): print(f[工具结束] output{output})使用方式简单直接在invoke时把回调实例放到 config 里。chain.invoke({topic: LangChain}, config{callbacks: [TraceCallback()]})模型一启动回调就会打印传入的 prompt模型一结束就会打印生成的前 100 字工具被调用时会打印工具名和输入参数。这套东西的价值在于你可以把项目代码当黑匣子打开每次改动 prompt 或者调 temperature 后直接在日志里对比模型的决策变化。回调还能统计 token 用量和耗时只要在on_llm_start里记一个时间戳在on_llm_end里做差就能得到单次模型调用的耗时。5.2 用一段代码记录端到端耗时验证性能是不是真的可用给项目代码做功能验证之外还要做性能验证。最简单的做法是在调用入口前后各打一个时间点。import time start time.perf_counter() resp agent_executor.invoke({input: 3加4再乘以2等于多少}) elapsed_ms (time.perf_counter() - start) * 1000 print(f端到端耗时{elapsed_ms:.1f} ms) print(f输出{resp[output]})time.perf_counter()比直接用time.time()更精确适合测量短耗时调用。记录到的总耗时包括模型调用、工具执行和 Agent 循环调度如果时间长得不可接受再拆开看是哪一步慢。工具函数如果本身是同步的常见瓶颈还是模型调用次数比如 Agent 在循环里多做了几次决策每次决策都是一次完整的大模型请求。给端到端验证列一个检查清单能让项目代码从“能跑”升级到“能用”验证项检查点通过条件基础问答无额外上下文时的普通问题输出内容正确且不报错工具兜底给 Agent 一个需要多步计算的问题实际调用了工具且结果计算正确记忆持久化同一会话内问上一轮信息新的一轮能引用上一轮内容检索相关性对知识库提问返回的前 2 条文档与问题语义相关超时控制给耗时空任务设置超时超时后能快速失败并返回可读错误我最早带同学入门时总觉得跑通 demo 就算学会。后来发现每次改参数都要靠猜因为代码里没有观测层。自从习惯给最小脚本加一个回调把模型输入输出和耗时都打印出来很多“玄学”就变成了可见的调用序列。改代码前先看一次 trace改完再看一次对比差异就能知道问题出在模型还是链路。希望这些细节能帮你在《LangChain入门与实践》这份项目代码上少走几段弯路也希望帮到你真正把这套框架敢用起来。本文还有配套的精品资源点击获取