为什么我放弃了传统方案,改用 LangChain
一个真实的痛点
去年我负责给公司内部知识库做一个问答系统。最初的方案很“传统”:自己写代码调用 OpenAI API,自己管理对话历史,自己拼接 prompt。听起来不难,对吧?
三个月后,我的项目目录变成了这样:
my-rag-project/
├── prompts/
│ ├── qa_prompt_v1.txt
│ ├── qa_prompt_v2.txt
│ ├── qa_prompt_final.txt
│ └── qa_prompt_really_final.txt
├── utils/
│ ├── chunking.py
│ ├── embedding_cache.py
│ ├── memory_manager.py
│ └── document_loader.py
└── main.py (800+ 行)
压死骆驼的最后一根稻草是:产品经理要求把向量数据库从 Chroma 换成 Milvus,同时支持 PDF 和 Word 两种文档格式。我预估要重构一周。
就在那时,我认真看了一遍 LangChain 的文档,决定试一试。结果是:换向量库这件事,我改了 3 行代码,花了 10 分钟。
下面是我完整的使用心得。
第一步:安装和最简示例
pip install langchain langchain-openai langchain-community chromadb
把原来 800 行的 main.py 简化成一个最小可运行的问答链:
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_template(
"根据以下背景回答问题。如果不知道就说不知道。\n\n背景:{context}\n\n问题:{question}"
)
chain = prompt | llm | StrOutputParser()
result = chain.invoke({
"context": "公司年假为 15 天,需提前 3 个工作日申请。",
"question": "年假有多少天?"
})
print(result)
这里的 prompt | llm | parser 是 LangChain 的 LCEL 语法,用管道符把组件串起来。说实话,第一次看到这个写法我是抗拒的——觉得太“魔法”了。但用了一周后发现它有几个实打实的好处:可以流式输出(chain.stream())、可以并发(chain.batch())、整个链自动可追踪。
第二步:接入文档,这才是重头戏
RAG 系统的核心是文档处理。传统方案里我自己写的 chunking.py 有各种边界情况处理不好。LangChain 的文档加载器直接解决了这个问题:
from langchain_community.document_loaders import PyPDFLoader, Docx2txtLoader
# PDF 和 Word 用同一套接口
loaders = [PyPDFLoader("员工手册.pdf"), Docx2txtLoader("规章制度.docx")]
docs = []
for loader in loaders:
docs.extend(loader.load())
切分也有现成的、经过大量实践检验的策略:
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", ","] # 中文要自定义分隔符!
)
chunks = splitter.split_documents(docs)
踩坑提醒:默认分隔符是英文的 ["\n\n", "\n", " ", ""],对中文文档效果很差,经常把一句话拦腰切断。加上中文标点后,检索质量明显提升——这是我们系统准确率从 68% 提到 81% 的关键改动之一。
第三步:换向量库,3 行代码的体验
这就是开头说的那件事。原来自封装的向量库调用代码:
# 旧方案:300 行自写代码,换库要全部重写
client = chromadb.Client()
collection = client.create_collection("docs")
# ... 手动管理 embedding、metadata、查询逻辑
LangChain 方案:
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
vectorstore = Chroma.from_documents(chunks, OpenAIEmbeddings())
results = vectorstore.similarity_search("年假政策", k=3)
换成 Milvus 时,只需要:
from langchain_community.vectorstores import Milvus
vectorstore = Milvus.from_documents(chunks, OpenAIEmbeddings(),
connection_args={"uri": "http://localhost:19530"})
后续的检索、RAG 链代码一行都不用改,因为接口是统一的。这就是抽象层的价值。
第四步:对话记忆
传统方案里我手写了 200 行的 memory_manager.py 来截断历史、控制 token 数。LangChain 一句话搞定:
from langchain_community.chat_message_histories import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
history = ChatMessageHistory()
chain_with_memory = RunnableWithMessageHistory(
chain,
lambda session_id: history,
input_messages_key="question",
history_messages_key="chat_history",
)
进阶技巧:调试别靠 print
我早期调试 RAG 链,检索到了什么内容全靠 print。后来发现 LangSmith(官方的追踪平台)可以看每一步的输入输出、token 消耗、延迟。免费额度对个人项目够用,强烈建议接入,排查“为什么模型答错了”这类问题时效率提升是数量级的。
实用技巧汇总
- 中文场景务必自定义 text splitter 的分隔符
- temperature 设 0 做知识问答,别让模型发挥
- 检索数量 k=3~5 起步,不是越多越好,太多噪音反而降准确率
- 用
chain.stream()做流式输出,用户体验天差地别 - prompt 和代码分离:用
ChatPromptTemplate集中管理,别散落在字符串里 - 嵌入模型要和语言匹配,中文可以试试 BGE 系列,通过
HuggingFaceEmbeddings接入
诚实的局限性
最后说说我不满意的地方,免得你以为我在无脑吹:
- 抽象有漏:LCEL 管道的报错信息经常很隐晦,出错时往往需要拆开链逐步调试
- 版本变动频繁:LangChain 0.1 到 0.3 迁移时我改了不少废弃 API,文档和教程经常对不上
- 简单场景是杀鸡用牛刀:如果只是“一个 prompt 调一次 API”,直接用 OpenAI SDK 更清爽。LangChain 的价值在于组合多个组件的场景
- 性能开销:多层抽象有轻微开销,高频调用场景要自己压测
结论
我的最终判断是:如果你的项目涉及 RAG、多步链、多模型切换中任意两项以上,LangChain 值得用。它没有让我写出更好的代码,但它让我把精力从“工程胶水”上挪开,专注在 prompt 优化和检索质量这些真正影响效果的事情上。
那个知识库系统现在稳定运行,准确率 85%,而当年的重构预估是一周——实际用 LangChain 重写只花了一天半。这就是我“叛逃”的全部理由。