我用 LangChain 搭了一套 AI 工作流引擎:3 个实战案例 + 7 个踩坑记录
读者对象:想用 LangChain 做应用的 Python 开发者、在 LangChain 和原生 SDK 之间纠结的人
解决的问题:LangChain 教程很多,但真正踩过坑的人才知道哪些 API 能用、哪些早该淘汰了。本文来自 3 个月实战经验。
一、LangChain 到底值不值得用
先说结论:具体场景具体分析。
| 场景 | 建议 |
|---|---|
| 简单对话(一问一答) | ❌ 别用,直接调 OpenAI SDK |
| 多步工具调用(搜索 → 分析 → 写报告) | ✅ 用 LangChain,Chain/Agent 编排省事 |
| RAG(检索增强生成) | ⚠️ 用 LCEL,别用老版 VectorStoreRetriever |
| 复杂的条件分支工作流 | ✅ LangGraph(LangChain 的子项目) |
我用了 3 个月,结论是:LangChain 最大的价值在 Chain 编排和多步骤协调,但对简单场景是过度设计。
二、我不建议你用的 3 个 API
用了会后悔的:
1. LLMChain(已过时)
# ❌ 别这么写(LangChain 0.1 的旧 API)
from langchain.chains import LLMChain
chain = LLMChain(llm=llm, prompt=prompt, output_parser=parser)
# ✅ 用 LCEL(LangChain Expression Language)
chain = prompt | llm | parser
LLMChain 2024 年就标记为 deprecated,但网上教程 80% 还在用。
2. load_summarize_chain(不可控)
# ❌ 黑盒,你不知道它怎么分段、怎么合并
from langchain.chains.summarize import load_summarize_chain
# ✅ 自己写分段逻辑,明确可控
def my_summarize(text, chunk_size=3000):
chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]
summaries = [llm.invoke(f"总结:{chunk}") for chunk in chunks]
return llm.invoke(f"合并以下总结:{summaries}")
3. ConversationBufferMemory(内存泄漏)
# ❌ 对话长了之后,每次请求都带完整历史,token 爆炸
from langchain.memory import ConversationBufferMemory
# ✅ 用滑动窗口
from langchain.memory import ConversationBufferWindowMemory
memory = ConversationBufferWindowMemory(k=10) # 只保留最近 10 轮
三、实战案例 1:自动生成周报
需求:读取 Git 提交记录 → 翻译成白话 → 结构化周报。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
import subprocess
# 初始化模型
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.3)
# Step 1:获取 Git 提交
def get_git_log(days=7):
cmd = f"git log --since='{days} days ago' --oneline --no-merges"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return result.stdout.strip()
# Step 2:翻译成白话
translate_prompt = ChatPromptTemplate.from_messages([
("system", "把 Git 提交记录翻译成可读的工作描述。保留关键信息,去掉无意义的提交信息。"),
("human", "{git_log}")
])
# Step 3:结构化周报
report_prompt = ChatPromptTemplate.from_messages([
("system", """根据工作描述,生成结构化的周报:
## 本周完成
- (用 bullet points)
## 关键进展
- (1-2 句话)
## 下周计划
- (推断可能的下一步工作)
## 遇到问题
- (如果有,根据提交频率推断)"""),
("human", "{work_desc}")
])
# Step 4:用 LCEL 串联
chain = (
RunnablePassthrough.assign(git_log=get_git_log)
| RunnablePassthrough.assign(work_desc=translate_prompt | llm | StrOutputParser())
| RunnablePassthrough.assign(report=report_prompt | llm | StrOutputParser())
)
# 运行
result = chain.invoke({})
print(result["report"])
踩坑点:RunnablePassthrough.assign 的键名决定了后续步骤能拿到什么数据。如果第二步的 key 是 work_desc,第三步的 prompt 里变量名必须是 {work_desc},否则拿不到。
实战案例 2:批量长文档问答
需求:上传多个 PDF → 提问 → AI 从文档中找答案。
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_community.vectorstores import Chroma
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import PyPDFLoader
# 1. 加载 PDF
loader = PyPDFLoader("report_2026.pdf")
pages = loader.load()
# 2. 文本分割(注意 chunk_overlap 不要设为 0)
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=200, # 重叠避免截断语义
separators=["\n\n", "\n", "。", ",", " "] # 中文友好的分隔符
)
chunks = splitter.split_documents(pages)
# 3. 向量化(用便宜的模型)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = Chroma.from_documents(chunks, embeddings)
retriever = vectorstore.as_retriever(search_kwargs={"k": 5})
# 4. RAG prompt
rag_prompt = ChatPromptTemplate.from_messages([
("system", """根据以下参考资料回答问题。如果参考资料中没有相关信息,请明确说"参考资料中未找到相关信息",不要编造。
参考资料:
{context}"""),
("human", "{question}")
])
# 5. RAG chain
def format_docs(docs):
return "\n\n".join(f"[来源{i}] {doc.page_content[:500]}"
for i, doc in enumerate(docs))
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| rag_prompt
| llm
| StrOutputParser()
)
# 提问
answer = rag_chain.invoke("你们公司的营收目标是多少?")
print(answer)
实战案例 3:多模型投票决策
需求:同一个问题发给 3 个模型 → 投票决定最终答案。用于需要高可靠性的场景。
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel
import json
# 初始化 3 个模型
gpt4o = ChatOpenAI(model="gpt-4o", temperature=0)
gpt4o_mini = ChatOpenAI(model="gpt-4o-mini", temperature=0)
claude = ChatOpenAI( # 假设你有 Claude API
model="claude-3-5-sonnet",
temperature=0,
base_url="https://api.anthropic.com/v1"
)
prompt = ChatPromptTemplate.from_messages([
("system", "你是分类专家。将文本分为:投诉/咨询/建议/其他。只返回类别名称。"),
("human", "{text}")
])
# 并行调用
parallel_chain = RunnableParallel(
gpt4o=prompt | gpt4o,
gpt4o_mini=prompt | gpt4o_mini,
claude=prompt | claude
)
# 投票
def majority_vote(results):
votes = {}
for model, result in results.items():
category = result.content.strip()
votes[category] = votes.get(category, 0) + 1
winner = max(votes, key=votes.get)
confidence = votes[winner] / len(results)
return {
"category": winner,
"confidence": confidence,
"votes": votes
}
# 运行
text = "你们的 APP 太慢了,点一下要等 10 秒,能不能修一下?"
results = parallel_chain.invoke({"text": text})
decision = majority_vote(results)
print(f"分类结果:{decision['category']}")
print(f"置信度:{decision['confidence']*100:.0f}%")
print(f"各模型投票:{decision['votes']}")
# 输出:
# 分类结果:投诉
# 置信度:100%
# 各模型投票:{'投诉': 3}
四、7 个踩坑记录
坑 1:chunk_overlap=0,关键信息被截断
症状:用户问"Q2 营收",文档里写的是"Q2 营收为 5000 万,同比增长 20%",但正好"Q2 营收为"在 chunk A 结尾,“5000 万"在 chunk B 开头。检索召回 A,没回 B,AI 回答"不知道”。
原因:RecursiveCharacterTextSplitter 按固定长度切,不关心语义边界。
解决方案:chunk_overlap 设 200-300,保证相邻 chunk 有重叠:
splitter = RecursiveCharacterTextSplitter(
chunk_size=1000,
chunk_overlap=300, # 30% 重叠
)
坑 2:用 stuff 模式处理长文档,token 爆了
症状:用 load_qa_chain(llm, chain_type="stuff") 处理 50 页 PDF,API 直接报 context length exceeded。
原因:stuff 模式把所有检索到的文档拼成一个大 prompt,超 token 限制。
解决方案:用 map_reduce 模式(每个文档独立分析,再汇总),或者自己控制 chunk 数量:
# 在 RAG 里限制检索文档数
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 只取 top 3
坑 3:Chroma 默认存内存,重启全丢
症状:每次重启应用,向量库是空的,重新 embedding 一遍又花 5 分钟。
原因:Chroma.from_documents() 不传 persist_directory 默认存内存。
解决方案:
vectorstore = Chroma.from_documents(
chunks, embeddings,
persist_directory="./chroma_db" # 持久化到磁盘
)
vectorstore.persist() # 显式保存
# 下次直接用
vectorstore = Chroma(
embedding_function=embeddings,
persist_directory="./chroma_db"
)
坑 4:ChatPromptTemplate 的变量名写错,不报错但输出不正常
症状:Prompt 里写了 {contexts}(复数),但代码里传的是 {"context": docs}(单数)。不报错,只是 AI 回复质量很差。
原因:LCEL 没找到 {contexts} 这个变量,替换成了空字符串,等于没给上下文。
解决方案:用 prompt.input_variables 检查变量名是否一致:
print(prompt.input_variables) # ['context', 'question']
# 如果和代码里传的不一致,马上能发现
坑 5:OpenAIEmbeddings 被限流后 LangChain 不自动重试
症状:批量 embedding 5000 个 chunk 时,中途被限流,LangChain 直接抛异常退出。
原因:LangChain 的默认 OpenAIEmbeddings 没配 retry。
解决方案:
from tenacity import retry, stop_after_attempt, wait_exponential
embeddings = OpenAIEmbeddings(
model="text-embedding-3-small",
max_retries=5, # 重试 5 次
request_timeout=60 # 超时 60 秒
)
坑 6:串行跑大吞吐任务,LangChain 的 RunnableParallel 是异步的
症状:有 3 个独立的 API 调用,用 asyncio.gather 并行跑,但 LangChain 的 RunnableParallel 用了 invoke() 而非 ainvoke(),还是串行。
原因:invoke() 是同步的,在 RunnableParallel 里是伪并行(quick switch 而非真正并发)。
解决方案:用 ainvoke():
# ❌ 假并行
parallel_chain.invoke({"text": text})
# ✅ 真并行(asyncio 事件循环)
import asyncio
result = asyncio.run(parallel_chain.ainvoke({"text": text}))
坑 7:Prompt 模板里的 { 导致解析错误
症状:Prompt 里包含 JSON 示例 {"key": "value"},LangChain 把 { 识别为变量占位符。
原因:LangChain 默认用 {variable} 作为变量标记,遇到 { 就尝试解析。
解决方案:JSON 示例里用 {{ 和 }} 转义:
prompt = ChatPromptTemplate.from_messages([
("system", """请输出 JSON 格式:
{{"category": "投诉/咨询/建议", "confidence": 0-1}}"""),
("human", "{text}")
])
五、总结
| 要点 | 说明 |
|---|---|
| LangChain 适合什么 | 多步编排、Agent 工具调用、RAG 管线 |
| LangChain 不适合什么 | 简单对话、一次性脚本 |
| 最大的坑 | 老 API 还在网上大量传播(LLMChain / stuff 模式 / 内存 Memory) |
| 我的建议 | 用 LCEL(prompt | llm | parser),别用老式 Chain 类 |
三条经验:
- 看文档别看教程:网上 80% 的 LangChain 教程用了已废弃的 API,直接看 LangChain 官方文档 的 LCEL 部分。
- 先搭原型再上 LangChain:先用原生 OpenAI SDK 搭好单步逻辑,确认可行后再用 LangChain 做编排。
- 持久化一切:向量库、Memory、配置,全存磁盘,别依赖内存。
互动:你用过 LangChain 吗?最大的感受是什么——真香还是后悔?评论区聊聊。

1275

被折叠的 条评论
为什么被折叠?



