认识 LangChain
《LangChain指南》是一本轻量 Python 用法手册:不讲玄学,只讲「这个东西怎么用」。 全书示例围绕一个虚构场景——拾光书屋:一个线上阅读社区,有书籍、书评、阅读笔记和会员。你要给它装一个「AI 店员」
《LangChain指南》是一本轻量 Python 用法手册:不讲玄学,只讲「这个东西怎么用」。 全书示例围绕一个虚构场景——拾光书屋:一个线上阅读社区,有书籍、书评、阅读笔记和会员。你要给它装一个「AI 店员」。
本章技术基线
| 项目 | 约定 |
|---|---|
| 语言 | Python 3.10+ |
| 包管理 | uv(uv add / uv run) |
| 框架 | LangChain 1.x(langchain + langchain-ollama 两个包) |
| 模型 | 本地 Ollama 跑 qwen3:4b(不联网、不花钱) |
| 场景 | 拾光书屋(书籍 / 书评 / 阅读笔记 / 会员) |
学完本章,你能:跑通第一次模型对话、看懂返回值里的每一个字段、让回答一个字一个字蹦出来(流式)、给模型装上缓存让重复提问「秒回」。
1. 环境准备:开工前先摆好工具
作用:本章所有代码都依赖三样东西——Python 项目、LangChain 包、本地 Ollama。先把它们摆好,后面 11 章就不用再折腾环境。
装法:
# 1. 安装 uv(一个极快的 Python 包管理器,已装可跳过)
# macOS / Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh
# 2. 建项目、装依赖(langchain-core 会作为依赖自动带上,不用单独装)
uv init shiguang-ai
cd shiguang-ai
uv add langchain langchain-ollama
# 3. 安装 Ollama 桌面版(或 brew install ollama),拉模型
ollama pull qwen3:4b
# 4. 确认服务在跑(默认监听 http://localhost:11434)
ollama list
最小示例(全书第一段代码,5 行跑通):
from langchain_ollama import ChatOllama
model = ChatOllama(model="qwen3:4b")
ai = model.invoke("用一句话向新会员介绍「拾光书屋」。")
print(ai.content)
保存后运行:uv run demo/demo_chat.py(demo 文件在配套的 demo/ 目录里)。
常见坑:
- Python 版本:LangChain 1.x 要求 Python 3.10+。如果你在 3.9 上
pip install,只会装到老的 0.3.x,很多导入路径都不一样,教程会对不上号。 - ConnectError 报错:
httpx.ConnectError ... localhost:11434说明 Ollama 服务没启动——打开 Ollama App 或跑ollama serve。 - 404 model not found:服务在跑但没拉模型,先
ollama pull qwen3:4b。 - 别用系统自带的
pip裸装,容易污染全局环境;跟着 uv 走,省心。
2. LangChain 是什么:一块乐高底板
作用:回答「我为什么需要它」。一句话:LangChain 是搭 AI 应用的乐高底板——大模型只是其中一块积木,你还需要工具、记忆、文档检索这些积木;LangChain 把它们做成统一接口,让你「咔哒」一声拼在一起。
对拾光书屋来说,你想要的「AI 店员」其实是好几种能力的组合:
| 你想要的效果 | 背后需要的能力 |
|---|---|
| 会员能和店员连续聊 | 多轮对话 + 记忆 |
| 店员能查书、算折扣 | 工具调用 |
| 店员答得上来书屋自己的规矩 | RAG(检索书屋知识库) |
| 店员自己决定先查库存还是先写推荐语 | Agent |
| 店员每句话风格统一 | 提示词模板 |
这些能力 LangChain 全都预置了「积木」。没有它,你就得自己写胶水代码:拼接请求、管理上下文、解析输出、重试……有了它,你专注拼装。
顺带澄清一个常见误解:LangChain 不是 Python 专属,它有 JS/TS 版(还挺能打),Go、Rust 版也在长。但 Python 是旗舰:新功能最先落地、集成最全、社区最大。所以这本手册走 Python 线——同样的思路,语言只是外壳。
常见坑:
- LangChain 不生产模型,也不替你思考。它只是「编排层」,模型行不行,还是看模型本身(qwen3:4b 是个 4B 小模型,别拿它当 GPT-5 用)。
- 别一上来就研究「架构图」,先跑起来,再回头理解。
3. 生态对比:为什么这本手册选 LangChain
作用:选型不纠结。AI 应用框架不止一家,各有各的地盘:
| 框架 | 一句话定位 | 适合谁 |
|---|---|---|
| LangChain | 全场景积木库:RAG / Agent / 记忆 / 工具全覆盖 | 想一站式掌握 AI 应用开发的人 |
| LlamaIndex | 数据索引与检索专精 | 只做 RAG、且检索是绝对核心 |
| Haystack | 企业级搜索问答管线,带评估体系 | 偏工程化、要严格评测的团队 |
| AutoGen / CrewAI 等 | 多 Agent 协作编排 | 研究「多个 AI 分工合作」 |
选 LangChain 的三个理由(很实在):
- 一张全家桶门票:12 章目录就是它的能力地图——提示词、LCEL、流式、解析器、文档加载、向量库、RAG、记忆、工具、Agent、回调,一路学下去不用换框架。
- 生态最厚:Ollama 这种本地引擎开箱即用;FAISS、Chroma 这些向量库无缝对接;出问题搜一下基本都有人踩过坑。
- 对学习者友好:命名直白(
invoke、stream这类词看名字就懂),模块能拆开单独玩,适合边拆边学。
常见坑:
- 别陷入「框架对比内耗」。这三个框架的核心概念高度相通,学会一个,换别的只是换 API 名字。
- LangChain 不是数据库、不是 Web 框架,也别用它替代你的业务代码——它只管「和模型打交道」这一段。
4. 模块全景:拾光书屋的部门分布图
作用:把全书 12 章映射到 LangChain 的模块上,心里有张地图,走到哪都知道自己在哪。
| 模块 | 管什么 | 拾光书屋的对应物 | 对应章节 |
|---|---|---|---|
| Models(模型) | 跟大模型说话的统一接口 | 你雇的驻店作家 | 01 / 03 |
| Prompts(提示词) | 需求单模板,可复用可插变量 | 店员话术手册 | 02 |
| LCEL / Runnable | 管道编排 | 后厨流水线 | 03 / 04 |
| Output Parsers | 把回答变成 Python 对象 | 回答装订成标准表格 | 05 |
| Indexes(文档与检索) | 知识库的入库与查找 | 书评库分类上架 | 06 / 07 / 08 |
| Memory(记忆) | 多轮对话的上下文 | 会员档案卡 | 09 |
| Tools(工具) | 让模型能调用外部函数 | 查库存的计算器 | 10 |
| Agents | 模型自己决定下一步 | 值班店经理 | 11 |
| Callbacks(回调) | 观测、日志、调试 | 店内监控摄像头 | 12 |
装法:Python 侧的包是「四层结构」(这一点和 JS 版的三包结构思路一致,只是拆得更细):
| 包 | 角色 | 你要手动装吗 |
|---|---|---|
langchain-core |
内核抽象:Runnable、消息、缓存等 | 不用,自动带上 |
langchain |
高层入口:init_chat_model、通用封装 |
要(uv add langchain) |
langchain-community |
社区集成大杂烩 | 按需(本章 SQLite 缓存会用到) |
langchain-ollama 等 |
每个模型供应商一个专属包 | 要(uv add langchain-ollama) |
最小示例(四层结构在代码里的样子):
# 高层入口(langchain 包)
from langchain.chat_models import init_chat_model
# 供应商专属包(langchain-ollama 包)
from langchain_ollama import ChatOllama
# 内核抽象(langchain-core 包,自动安装)
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.caches import InMemoryCache
常见坑:
- 老教程里
from langchain.chat_models import ChatOllama这种写法在 1.x 已经移除——供应商类都搬进各自的集成包了。看到报ImportError,先检查导入路径。 langchain-community里也有一些和集成包同名的类(历史上搬过家),优先用langchain-xxx专属包里的版本。langchain-core你几乎永远不需要显式安装,它是所有包的公共地基。
5. 模型接口(上):ChatOllama,把作家请进店
作用:模型接口是后面 11 章的「发动机」。在其他教程里你常见到 ChatOpenAI(连 OpenAI 云端 API);本手册用 ChatOllama——同样的事,只是作家住在自己家(本地),不要钱。
先补一个背景知识:LangChain 里有两类模型接口——
| 补全模型 | 对话模型 | |
|---|---|---|
| 类 | OllamaLLM(继承 BaseLLM) |
ChatOllama(继承 BaseChatModel) |
| 输入 | 纯字符串 | 消息列表(带角色) |
| 输出 | 纯字符串 | AIMessage 对象 |
| 场景 | 单轮文本任务:改写、摘要 | 聊天、Agent、结构化输出 |
如今主流是对话模型(OpenAI 官方都停了补全接口),全书只用 ChatOllama。知道 OllamaLLM 存在即可,遇到了别慌。
装法:uv add langchain-ollama(第 1 节已装)。
最小示例(两种输入写法):
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_ollama import ChatOllama
model = ChatOllama(model="qwen3:4b", temperature=0.7)
# 写法 A:直接丢一个字符串(LangChain 会自动把它包成一条用户消息)
ai = model.invoke("给《夜晚的潜水艇》写一句 30 字以内的推荐语。")
# 写法 B:消息列表,能带上「角色」——店员的人设就是 SystemMessage 放的
messages = [
SystemMessage(content="你是拾光书屋的资深店员,说话简洁、只谈书。"),
HumanMessage(content="会员问:有适合通勤时读的短篇集吗?"),
]
ai = model.invoke(messages)
print(ai.content)
看懂返回值:AIMessage 不是字符串,是快递包裹
invoke 返回的不是 str,而是一个 AIMessage 对象。拆包裹:
from langchain_core.messages import AIMessage
ai: AIMessage = model.invoke("用一句话介绍「拾光书屋」。")
ai.content # 货物:正文本身(多数时候就是 str)
ai.response_metadata # 面单:模型侧回执,比如 model 名、耗时(字段随供应商变化)
ai.usage_metadata # 称重单:token 统计 {"input_tokens": …, "output_tokens": …, "total_tokens": …}
ai.id # 运单号:本次调用的唯一 ID,形如 "run-xxxx"
ai.tool_calls # 工具调用记录(第 10 章才用得上,现在是空列表)
为什么非要包一层?因为后面链条(LCEL)、工具调用、日志追踪都需要这些元信息。取正文永远用 ai.content——这是新手最常忘的一步。
常见坑:
- qwen3 会「思考」:默认模式下,它会把思考过程用特殊标签混在
content里输出,正文看起来脏脏的。解法:ChatOllama(model="qwen3:4b", reasoning=False)直接关;或reasoning=True让思考单独放进ai.additional_kwargs["reasoning_content"],正文保持干净。本课 demo 统一用reasoning=False。 ai是对象不是字符串:print(ai)会打出一堆结构,print(ai.content)才是你要的。- 消息别自己拼 dict(
{"role": "user"}那种 OpenAI 原生格式),用SystemMessage/HumanMessage/AIMessage,LangChain 的组件都认这些。 base_url默认就是http://localhost:11434,Ollama 跑在本机时不用写。
6. 模型接口(下):init_chat_model,一行换作家
作用:今天用本地 qwen3:4b 白嫖,明天要上 OpenAI 收费档,你希望只改一个字符串,其余代码一行不动。LangChain 1.x 给的答案就是 init_chat_model——像个「作家中介」:你说要谁,它负责把人带进门(自动 import 对应的包)。
最小示例:
from langchain.chat_models import init_chat_model
# 「供应商:模型名」写在一根字符串里
model = init_chat_model("ollama:qwen3:4b", temperature=0)
# 等价写法:分开传
model = init_chat_model("qwen3:4b", model_provider="ollama")
# 哪天要换作家(先 uv add langchain-openai):
# model = init_chat_model("openai:gpt-4o-mini")
print(model.invoke("用一句话介绍「拾光书屋」。").content)
拿到的 model 就是一个如假包换的 ChatOllama 实例(可以 type(model) 验证),.invoke、.stream、后面章节的 bind_tools 全部照常用。好处是「换供应商」可以做成配置项:
import os
model = init_chat_model(os.getenv("BOOKSTORE_MODEL", "ollama:qwen3:4b"))
那到底用哪种? 简单决策:
| 场景 | 选择 |
|---|---|
| 学习、demo、单供应商 | ChatOllama 直连——IDE 补全友好,参数一目了然 |
| 应用要可配置、多环境切换 | init_chat_model——供应商写进配置,代码零改动 |
常见坑:
- 字符串按第一个冒号切分,所以
ollama:qwen3:4b会被正确切成「ollama」+「qwen3:4b」,模型名里的冒号不会切错。 - 没装对应集成包会直接
ImportError,提示你pip install langchain-xxx——照做即可(用 uv 就是uv add)。 - 供应商名写错(比如
ollama拼成olama)报的是「不认识的 provider」,不是「拼写错误」,别被带偏。
7. 参数旋钮:temperature 与它的家族
作用:控制作家的「发挥程度」。写书评要中规中矩,写活动文案要天马行空——同一模型,一个参数切换。
最小示例:
from langchain_ollama import ChatOllama
稳重派 = ChatOllama(model="qwen3:4b", temperature=0, reasoning=False) # 照标准话术办事
发挥派 = ChatOllama(model="qwen3:4b", temperature=0.9, reasoning=False) # 允许自由发挥
print(稳重派.invoke("为「拾光书屋」写一句迎宾语。").content)
print(发挥派.invoke("为「拾光书屋」写一句迎宾语。").content)
常用参数速查(Ollama 侧):
| 参数 | 管什么 | 常用值 |
|---|---|---|
temperature |
随机度:0 最稳,越高越发散 | 0(严谨)/ 0.7(日常)/ 1.0+(创意) |
top_k / top_p |
另外两个采样旋钮,和 temperature 配合 | 一般不动 |
num_ctx |
上下文窗口(能塞进去多少 token) | 长文档时调大,默认只有几千 |
num_predict |
最多生成多少 token | 限制废话长度时用 |
seed |
随机种子 | 想复现同一次输出时设 |
repeat_penalty |
重复惩罚 | 模型车轱辘话多时调 |
keep_alive |
模型在内存里驻留多久 | 高频调用设 "5m" 省冷启动 |
base_url |
Ollama 服务地址 | 默认 http://localhost:11434 |
常见坑:
temperature=0不等于逐字复读——本地推理仍有微小浮动,要可复现请配合seed。- 高 temperature + 缓存 = 白开(见第 9 节的坑 5)。
num_ctx默认不大(2k~4k 量级,随版本而异),长文档会被「悄悄截断」——这是第 06 章要学「文档切割」的原因之一。
8. 流式输出初见:店员边写边递
作用:invoke 是「店员写完一整封信再交给你」;stream 是「边写边撕页递给你」。对聊天界面来说,后者体验完全不是一个档次——用户 1 秒内看到第一个字,而不是干等 20 秒。
最小示例:
from langchain_ollama import ChatOllama
model = ChatOllama(model="qwen3:4b", temperature=0.7, reasoning=False)
pieces: list[str] = []
for chunk in model.stream("为《百年孤独》写一段 60 字左右的书评开头。"):
print(chunk.content, end="", flush=True) # 每拿到一片就立刻打印
pieces.append(chunk.content)
print() # 换行
print("拼接结果:", "".join(pieces))
for 循环里拿到的 chunk 是 AIMessageChunk——AIMessage 的切片,chunk.content 是增量片段,不是完整回答,要自己拼。
常见坑:
- 忘了拼接:把最后一个 chunk 当成完整回答(内容只有尾巴)。
print(chunk.content)不加end="",屏幕上一行一片,乱成一锅粥。- 流式不走缓存——即使答案已经缓存过,
stream还是会逐字重新生成(第 9 节专门演示这个坑)。 - 流式的进阶玩法(异步流、并发、批处理)在第 04 章展开,这里先会「用」就行。
9. 缓存:让第二次提问变成「白嫖」
作用:书屋会员特别爱问重复问题——「营业时间几点」「怎么积分」。让店员每个问题都现场写一遍答案,又慢又费钱(如果调的是云端 API,那就是真金白银)。缓存的思路:前台放一个金句卡片盒,同一个问题第二次直接发卡。
三个好处:快(毫秒级返回)、省(少调模型)、稳(同一问题永远同一答案)。
装法:InMemoryCache 就在 langchain-core 里(已自动安装);SQLiteCache 在 langchain-community 里,用它之前先 uv add langchain-community。
最小示例 A:全局缓存(最常用)——设一次,全店生效:
from langchain_core.caches import InMemoryCache
from langchain_core.globals import set_llm_cache
from langchain_ollama import ChatOllama
set_llm_cache(InMemoryCache()) # 开店时设一次:所有模型调用默认走它
model = ChatOllama(model="qwen3:4b", temperature=0, reasoning=False)
# 第一次:真调用(慢)
model.invoke("用一句话介绍「拾光书屋」。")
# 第二次:命中缓存(毫秒级,返回和第一次完全相同)
model.invoke("用一句话介绍「拾光书屋」。")
最小示例 B:SQLite 持久化——内存缓存关机就没,SQLite 落到磁盘文件,重启进程也还在:
from langchain_community.cache import SQLiteCache
set_llm_cache(SQLiteCache(database_path="shiguang.db"))
# 想清空缓存:删掉 shiguang.db 文件即可
显式缓存(挂在模型实例上):不想全店生效,只想给某一个模型挂缓存?ChatOllama 构造参数里藏着一个 cache,文档里很低调,但很好用:
from langchain_core.caches import InMemoryCache
model_a = ChatOllama(model="qwen3:4b", cache=InMemoryCache()) # 自带私有缓存,不依赖全局
model_b = ChatOllama(model="qwen3:4b", cache=False) # 强制绕过全局缓存(即使 set_llm_cache 设过)
model_c = ChatOllama(model="qwen3:4b", cache=True) # 明确要求用全局缓存
三条规则记一下:传缓存实例 = 用这个实例;cache=False = 强制关闭;cache=True = 必须先 set_llm_cache,否则一调用就抛 ValueError。
常见坑:
- 缓存 key 是「精确匹配」:key = 提示词的序列化串 + 模型配置串。「营业时间几点?」和「请问营业时间是几点?」是两个不同的 key,不会命中。改一个字、换一个参数(比如 temperature)、调换消息顺序,都会 miss。想让「相似问题也命中」得自己实现语义缓存(用嵌入算相似度),属于进阶玩法。
stream()不走缓存:缓存的检查发生在invoke这类整体调用的入口,而stream返回的是逐块迭代器,拿到的是生成过程本身——所以哪怕答案在缓存里躺着,流式仍然会完整生成一遍。(想要「流式 + 缓存」,常见做法是缓存命中后自己把整段答案切成小块模拟推送。)cache=True没设全局缓存会直接报错:ValueError: Asked to cache, but no cache found at 'langchain.cache'。看到这个错,要么先set_llm_cache(...),要么直接传缓存实例。- 开发期的缓存幻觉:缓存开着,你会觉得「模型怎么突然又快又稳」——其实是缓存。调试新提示词前,删
.db文件或InMemoryCache().clear()。 - 高 temperature + 缓存 = 自欺欺人:同一个 key 永远返回第一次的答案,你以为模型稳定输出,其实只是缓存冻结了某一次的发挥。缓存场景建议
temperature=0。 - 一条无害的警告:用
langchain-community的SQLiteCache读缓存时,可能看到LangChainPendingDeprecationWarning(提示allowed_objects默认值未来会收紧)——库自己在做迁移预告,不影响使用,忽略即可。
10. 本章小结
你拿到了什么:
- ✅ 环境:uv + LangChain 1.x + 本地 Ollama(qwen3:4b)
- ✅ 认知:LangChain 是乐高底板,Python 是旗舰语言,12 章就是模块地图
- ✅ 模型接口两种姿势:
ChatOllama直连(学习用)与init_chat_model(可切换用) - ✅
AIMessage五件套:content/response_metadata/usage_metadata/id/tool_calls - ✅ 参数:
temperature家族,qwen3 记得reasoning=False - ✅ 流式:
stream()+ chunk 拼接 - ✅ 缓存:
set_llm_cache全局、cache=实例级、SQLite 持久化,以及五个坑
配套 demo(在本章 demo/ 目录):
| 文件 | 演示内容 |
|---|---|
demo/demo_chat.py |
invoke 两种写法、AIMessage 结构、流式输出、init_chat_model |
demo/demo_cache.py |
显式缓存、全局缓存、cache=False 绕过、SQLite 持久化、流式不走缓存 |
跑法:uv run demo/demo_chat.py、uv run demo/demo_cache.py(先按第 1 节把环境装好)。
下一章:02 · 提示词模板与消息