从零实现一个最小版 LangChain
用 ~300 行 Python 代码复刻 LangChain 的核心抽象,让你 1 小时看懂它的本质。
目标读者:有后端工程经验、想穿透 LangChain 的设计哲学而不是只会调 API 的人。
一、LangChain 到底是什么?
一句话:LangChain 是"把 LLM 应用拆成可组合组件"的框架。
它本身不提供智能,只提供抽象 和粘合剂。就像:
| 传统后端 | LangChain |
|---|---|
| Spring 不发明业务逻辑,只提供 IoC/AOP 容器 | LangChain 不发明模型,只提供 LLM 调用的容器 |
| Servlet → Filter → Controller 责任链 | Prompt → LLM → Parser → Tool 责任链 |
| Bean 依赖注入 | Chain 组合(`prompt |
核心洞察 :LLM 应用 = 输入变形 × 模型调用 × 输出解析 × 工具调用 × 状态管理 的流水线。LangChain 把这条流水线的每一段都抽成可替换的积木。
二、核心抽象(六块积木)
markdown
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ PromptTemplate│──▶│ LLM │──▶│ OutputParser │
└──────────────┘ └──────────────┘ └──────────────┘
│ ▲ │
│ │ ▼
│ ┌──────────────┐ ┌──────────────┐
└───────────▶│ Memory │ │ Tool │
└──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ Agent │
└──────────────┘
- LLM:对模型 API 的抽象(OpenAI / Anthropic / 本地模型统一接口)
- PromptTemplate:带变量的字符串模板
- OutputParser:把 LLM 的字符串输出结构化
- Chain:把前三者串成流水线
- Memory:多轮对话的状态存储
- Tool + Agent:让 LLM 能调外部函数,实现"推理---行动"循环
下面逐个从零实现。
三、最小实现(可直接运行)
准备:一个假的 LLM
为了让代码能独立跑通,先实现一个"假 LLM",真实场景替换成 OpenAI / Claude 的 SDK 调用即可。
python
# file: minilangchain.py
from abc import ABC, abstractmethod
from typing import Any, Callable
import re
import json
class BaseLLM(ABC):
"""LLM 抽象基类:所有模型只暴露一个 invoke 方法"""
@abstractmethod
def invoke(self, prompt: str) -> str: ...
class FakeLLM(BaseLLM):
"""用于演示的假 LLM,按规则返回内容"""
def __init__(self, responses: dict[str, str] | None = None):
self.responses = responses or {}
def invoke(self, prompt: str) -> str:
for key, value in self.responses.items():
if key in prompt:
return value
return f"[FakeLLM echo]: {prompt[:100]}"
class OpenAILLM(BaseLLM):
"""生产环境的真实现示意"""
def __init__(self, model: str = "gpt-4o-mini", api_key: str = ""):
from openai import OpenAI
self.client = OpenAI(api_key=api_key)
self.model = model
def invoke(self, prompt: str) -> str:
resp = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
)
return resp.choices[0].message.content
设计要点 :BaseLLM 只有一个 invoke(str) -> str。这就是 LangChain 最底层的契约------字符串进、字符串出。其他一切都围绕这个契约做变形。
积木 1:PromptTemplate
python
class PromptTemplate:
"""
带占位符的 prompt 模板。
示例:PromptTemplate("你是{role},请回答:{question}")
"""
def __init__(self, template: str):
self.template = template
self.variables = re.findall(r"\{(\w+)\}", template)
def format(self, **kwargs) -> str:
missing = set(self.variables) - set(kwargs.keys())
if missing:
raise ValueError(f"缺少变量: {missing}")
return self.template.format(**kwargs)
一眼看穿 :就是 str.format() + 变量校验。LangChain 的 PromptTemplate 再复杂,核心也就这几行。
积木 2:OutputParser
LLM 吐出的是字符串,但后续代码需要结构化数据(dict / 对象 / 枚举)。Parser 负责这一步。
python
class OutputParser(ABC):
@abstractmethod
def parse(self, text: str) -> Any: ...
class StrOutputParser(OutputParser):
def parse(self, text: str) -> str:
return text.strip()
class JSONOutputParser(OutputParser):
"""从 LLM 输出里提取 JSON"""
def parse(self, text: str) -> dict:
match = re.search(r"\{.*\}", text, re.DOTALL)
if not match:
raise ValueError(f"未找到 JSON: {text}")
return json.loads(match.group())
设计要点:Parser 让"不可靠的字符串"变成"类型安全的数据"。这是 LangChain 之所以能工程化的关键------边界处做格式收敛。
积木 3:Chain(核心!)
Chain 是 LangChain 的灵魂------把 Prompt、LLM、Parser 串起来的流水线。
python
class Runnable(ABC):
"""可被 invoke 的东西,支持用 | 组合"""
@abstractmethod
def invoke(self, input: Any) -> Any: ...
def __or__(self, other: "Runnable") -> "Chain":
"""支持 prompt | llm | parser 这种语法"""
return Chain([self, other])
class Chain(Runnable):
def __init__(self, steps: list[Runnable]):
self.steps = []
for step in steps:
# 扁平化嵌套的 Chain
if isinstance(step, Chain):
self.steps.extend(step.steps)
else:
self.steps.append(step)
def invoke(self, input: Any) -> Any:
out = input
for step in self.steps:
out = step.invoke(out)
return out
# 让之前定义的类都变成 Runnable
class PromptRunnable(Runnable):
def __init__(self, template: PromptTemplate):
self.template = template
def invoke(self, input: dict) -> str:
return self.template.format(**input)
class LLMRunnable(Runnable):
def __init__(self, llm: BaseLLM):
self.llm = llm
def invoke(self, input: str) -> str:
return self.llm.invoke(input)
class ParserRunnable(Runnable):
def __init__(self, parser: OutputParser):
self.parser = parser
def invoke(self, input: str) -> Any:
return self.parser.parse(input)
使用示例:
python
llm = FakeLLM({"首都": "北京"})
prompt = PromptTemplate("{country}的首都是哪里?")
chain = PromptRunnable(prompt) | LLMRunnable(llm) | ParserRunnable(StrOutputParser())
result = chain.invoke({"country": "中国"})
print(result) # -> "北京"(FakeLLM 命中规则)
类比后端 :这就是 Unix 管道 / Scala 的 andThen / Java Stream 的 map.map.map------函数组合。LangChain 的 LCEL(LangChain Expression Language)本质上就是重新发明了一遍管道。
积木 4:Memory
聊天机器人需要记住上下文,Memory 就是这个"上下文管理器"。
python
class Memory:
"""最简单的对话历史管理"""
def __init__(self):
self.history: list[tuple[str, str]] = [] # [(user, ai), ...]
def add(self, user: str, ai: str):
self.history.append((user, ai))
def format(self) -> str:
"""把历史格式化进 prompt"""
if not self.history:
return ""
lines = []
for user, ai in self.history:
lines.append(f"用户: {user}")
lines.append(f"助手: {ai}")
return "\n".join(lines)
class ChatChain:
"""带记忆的对话链"""
def __init__(self, llm: BaseLLM, memory: Memory | None = None):
self.llm = llm
self.memory = memory or Memory()
self.template = PromptTemplate(
"以下是历史对话:\n{history}\n\n用户: {question}\n助手:"
)
def chat(self, question: str) -> str:
prompt = self.template.format(
history=self.memory.format(),
question=question,
)
answer = self.llm.invoke(prompt)
self.memory.add(question, answer)
return answer
设计要点 :Memory 不是魔法,就是把历史字符串拼进 prompt。LangChain 的各种 Memory 变体(滑动窗口、摘要、向量召回)都是在"怎么压缩历史"上做文章。
积木 5:Tool + Agent(让 LLM 会用工具)
这是 LangChain 最"像 AI"的部分:LLM 不再只聊天,它能选择并调用函数。
Tool 抽象
python
class Tool:
"""可供 LLM 调用的工具"""
def __init__(self, name: str, description: str, func: Callable[[str], str]):
self.name = name
self.description = description
self.func = func
def run(self, arg: str) -> str:
return self.func(arg)
# 示例工具
def calculator(expr: str) -> str:
try:
return str(eval(expr, {"__builtins__": {}}, {}))
except Exception as e:
return f"计算错误: {e}"
def search(query: str) -> str:
fake_db = {"天气": "北京今天晴,25℃", "股价": "苹果今日收盘 $180"}
for k, v in fake_db.items():
if k in query:
return v
return "未找到相关信息"
CALCULATOR_TOOL = Tool("calculator", "计算数学表达式,输入如 '1+2*3'", calculator)
SEARCH_TOOL = Tool("search", "搜索实时信息,输入搜索词", search)
Agent:ReAct 循环
Agent 的核心是 ReAct 模式(Reasoning + Acting):让 LLM 在"思考 → 行动 → 观察"循环里推进任务。
python
AGENT_PROMPT = """你是一个能使用工具的 AI 助手。
可用工具:
{tools}
严格按以下格式输出(每步只输出一段):
Thought: <你的思考>
Action: <工具名>
Action Input: <工具输入>
Observation: <工具返回,由系统填入>
... (可重复多轮)
Thought: 我已得到答案
Final Answer: <最终回答>
问题: {question}
{scratchpad}"""
class Agent:
def __init__(self, llm: BaseLLM, tools: list[Tool], max_steps: int = 5):
self.llm = llm
self.tools = {t.name: t for t in tools}
self.max_steps = max_steps
def _tools_desc(self) -> str:
return "\n".join(f"- {t.name}: {t.description}" for t in self.tools.values())
def run(self, question: str) -> str:
scratchpad = ""
for step in range(self.max_steps):
prompt = AGENT_PROMPT.format(
tools=self._tools_desc(),
question=question,
scratchpad=scratchpad,
)
output = self.llm.invoke(prompt)
# 终止条件:LLM 给出 Final Answer
if "Final Answer:" in output:
return output.split("Final Answer:")[-1].strip()
# 解析 Action / Action Input
action_match = re.search(r"Action:\s*(\w+)", output)
input_match = re.search(r"Action Input:\s*(.+)", output)
if not (action_match and input_match):
return f"[Agent 解析失败]:\n{output}"
tool_name = action_match.group(1).strip()
tool_input = input_match.group(1).strip()
if tool_name not in self.tools:
observation = f"错误: 工具 {tool_name} 不存在"
else:
observation = self.tools[tool_name].run(tool_input)
# 把这一轮追加进 scratchpad,给下一轮 LLM 看
scratchpad += f"\n{output}\nObservation: {observation}\n"
return "[Agent 超出最大步数]"
使用示例:
python
# 真实场景用 OpenAILLM,这里用 FakeLLM 模拟
fake = FakeLLM({
"scratchpad": """Thought: 我需要计算 2+3
Action: calculator
Action Input: 2+3""",
"Observation: 5": """Thought: 我已得到答案
Final Answer: 2+3=5""",
})
agent = Agent(fake, [CALCULATOR_TOOL, SEARCH_TOOL])
print(agent.run("请帮我算 2+3"))
# -> "2+3=5"
核心洞察 :Agent = 循环调用 LLM + 解析 LLM 指令 + 执行函数 。所谓"智能体",本质是一个 while 循环。
积木 6:Retriever + VectorStore(RAG 的基石)
RAG(Retrieval-Augmented Generation)解决的痛点是:LLM 不知道你公司的私有数据。做法是先检索相关文档片段,再把片段塞进 prompt 让 LLM 参考作答。
RAG 流水线:
markdown
原始文档 ──► TextSplitter ──► Embeddings ──► VectorStore
│
用户问题 ──► Embeddings ──► 相似度检索 ◄──────────┘
│
▼
(相关片段 + 问题) ──► LLM ──► 回答
Document & TextSplitter
python
class Document:
"""带元数据的文本片段"""
def __init__(self, content: str, metadata: dict | None = None):
self.content = content
self.metadata = metadata or {}
class TextSplitter:
"""把长文档切成定长片段,片段之间可重叠(防止语义在边界被切断)"""
def __init__(self, chunk_size: int = 200, overlap: int = 20):
self.chunk_size = chunk_size
self.overlap = overlap
def split(self, text: str, metadata: dict | None = None) -> list[Document]:
chunks = []
start = 0
while start < len(text):
end = min(start + self.chunk_size, len(text))
chunks.append(Document(text[start:end], metadata))
if end == len(text):
break
start = end - self.overlap
return chunks
VectorStore(用 Jaccard 相似度模拟向量检索)
真实 VectorStore 用 Embedding 模型把文本转成高维向量,用余弦相似度检索。这里用 Jaccard 相似度(词集合交并比) 替代,把抽象讲清楚不引入 numpy 依赖。
python
class VectorStore:
"""
最简实现。真实版本:
- add() 时调用 Embeddings 模型把文本转向量,存到 FAISS/Chroma/Milvus
- search() 时把 query 也转向量,用余弦相似度 top-k
这里用 Jaccard 替代向量相似度,便于零依赖运行。
"""
def __init__(self):
self.docs: list[Document] = []
def add(self, docs: list[Document]):
self.docs.extend(docs)
@staticmethod
def _tokenize(text: str) -> set[str]:
# 真实场景这里是 embedding 向量化
return set(re.findall(r"\w+", text.lower()))
def search(self, query: str, k: int = 3) -> list[Document]:
q_tokens = self._tokenize(query)
if not q_tokens:
return []
scored = []
for doc in self.docs:
d_tokens = self._tokenize(doc.content)
if not d_tokens:
continue
score = len(q_tokens & d_tokens) / len(q_tokens | d_tokens)
scored.append((score, doc))
scored.sort(key=lambda x: x[0], reverse=True)
return [doc for _, doc in scored[:k] if _ > 0]
Retriever(Runnable 化,可插入 Chain)
python
class Retriever(Runnable):
"""把 VectorStore 包装成 Runnable,可用 | 串进 Chain"""
def __init__(self, vectorstore: VectorStore, k: int = 3):
self.vectorstore = vectorstore
self.k = k
def invoke(self, query: str) -> str:
docs = self.vectorstore.search(query, self.k)
if not docs:
return "(未检索到相关内容)"
return "\n---\n".join(d.content for d in docs)
设计要点 :Retriever 的输入是字符串(问题),输出也是字符串(拼好的上下文)------符合 Runnable 的契约 ,所以可以无缝插入 | 管道。这是 LangChain 抽象最优雅的地方:RAG 不是特殊的东西,只是管道里多加了一节水管。
四、把所有积木拼起来:一个完整的 RAG + Agent 应用
python
class MiniLangChain:
"""所有抽象的集大成者"""
def __init__(self, llm: BaseLLM):
self.llm = llm
self.memory = Memory()
self.tools: list[Tool] = []
self.vectorstore = VectorStore()
self.splitter = TextSplitter(chunk_size=100, overlap=10)
def add_tool(self, tool: Tool):
self.tools.append(tool)
def ingest(self, text: str, metadata: dict | None = None):
"""把文档切片入库,为 RAG 做准备"""
docs = self.splitter.split(text, metadata)
self.vectorstore.add(docs)
def chain(self, template: str, parser: OutputParser | None = None):
parser = parser or StrOutputParser()
return (
PromptRunnable(PromptTemplate(template))
| LLMRunnable(self.llm)
| ParserRunnable(parser)
)
def rag_chain(self, k: int = 3):
"""构建 RAG 链: 问题 -> 检索 -> 拼 prompt -> LLM -> 回答"""
retriever = Retriever(self.vectorstore, k=k)
rag_prompt = PromptTemplate(
"基于以下上下文回答问题。若上下文没有答案,请回答\"不知道\"。\n\n"
"上下文:\n{context}\n\n问题: {question}\n回答:"
)
def invoke(question: str) -> str:
context = retriever.invoke(question)
prompt = rag_prompt.format(context=context, question=question)
return self.llm.invoke(prompt)
return invoke
def agent(self) -> Agent:
return Agent(self.llm, self.tools)
def chat(self, question: str) -> str:
return ChatChain(self.llm, self.memory).chat(question)
# ============ 完整使用示例 ============
fake = FakeLLM({
"LangChain": "LangChain 是一个让 LLM 应用组件化的 Python 框架",
"首都": "北京",
})
app = MiniLangChain(fake)
# 1. 简单链
summarizer = app.chain("请用一句话总结: {text}")
print(summarizer.invoke({"text": "关于 LangChain 的长篇介绍..."}))
# 2. RAG: 先喂知识,再问
app.ingest("""
LangChain 是 2022 年由 Harrison Chase 创建的开源项目。
它的核心抽象包括 LLM、PromptTemplate、Chain、Memory、Tool、Agent。
LangChain 的配套产品 LangSmith 提供 tracing 和调试能力。
后来推出的 LangGraph 用状态机替代了线性 Chain,更适合复杂 Agent。
""")
rag = app.rag_chain(k=2)
print(rag("LangChain 是什么?"))
# LLM 会基于检索到的上下文回答,而不是瞎编
# 3. 带记忆的对话
app.chat("你好")
app.chat("我刚才说了什么?") # 会记住上下文
# 4. Agent 使用工具
app.add_tool(CALCULATOR_TOOL)
app.add_tool(SEARCH_TOOL)
app.agent().run("今天北京天气怎么样?然后帮我算 23*17")
至此,你手工搭出了一个能做 RAG、对话、工具调用的 LLM 应用框架。 所有概念都压缩在 ~300 行里,且每一行都在你控制之下。
五、LangChain 真实代码 vs 我们的最小版
| 抽象 | 最小版 | LangChain 真实实现 |
|---|---|---|
BaseLLM |
1 个方法 | BaseLanguageModel + BaseChatModel,支持流式、批量、异步、回调 |
PromptTemplate |
正则替换 | 支持 few-shot、partial、MessagesPlaceholder、多模态 |
Chain |
` | ` 串联 |
Memory |
list 拼接 | BufferMemory、SummaryMemory、VectorStoreMemory(向量召回) |
Tool |
函数包装 | 支持 Pydantic 参数 Schema、异步、权限控制 |
Agent |
字符串解析 ReAct | 支持 function calling、OpenAI tools、LangGraph 状态机 |
Retriever / VectorStore |
Jaccard 相似度词袋 | 真实 Embeddings (OpenAI/BGE) + FAISS/Chroma/Milvus + 多种检索策略 (MMR、Hybrid、Re-rank) |
结论 :LangChain 本质没什么神秘的------它是在这 6 块积木上加了 100 种变体,外加对几十家模型/向量库/工具的适配器。
六、用真实 LangChain 重写上面的示例
下面把第四节里那个"RAG + 对话 + Agent"应用用真实 LangChain 重写一遍。对照着看,你会立刻明白"我们自己搭的 MiniLangChain"和"真货"之间的映射关系。
环境准备
bash
pip install langchain langchain-openai langchain-community langchain-chroma \
langchain-text-splitters langgraph
export OPENAI_API_KEY=sk-...
LangChain 从 0.3 版本开始全面拥抱 LCEL (LangChain Expression Language,用
|组合 Runnable)和 LangGraph (Agent 状态机)。下面的代码是 2024-2025 的推荐写法,不是早期的LLMChain/AgentExecutor那套老 API。
示例 1:简单链(对应我们的 app.chain(...))
python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
prompt = ChatPromptTemplate.from_template("请用一句话总结: {text}")
# 这就是 LCEL: 和我们 MiniLangChain 里的 | 组合一模一样
summarizer = prompt | llm | StrOutputParser()
print(summarizer.invoke({"text": "LangChain 是 LLM 应用的组件化框架..."}))
映射关系:
| 我们的最小版 | 真实 LangChain |
|---|---|
PromptRunnable(PromptTemplate(...)) |
ChatPromptTemplate.from_template(...) |
LLMRunnable(OpenAILLM(...)) |
ChatOpenAI(...) |
ParserRunnable(StrOutputParser()) |
StrOutputParser() |
| 我们的 ` | ` 操作符 |
示例 2:RAG(对应我们的 app.rag_chain(...))
真实 LangChain 的 RAG 要比我们的 MiniLangChain 多两步:真实 Embeddings 模型 和真实向量库。
python
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
from langchain_core.runnables import RunnablePassthrough
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# 1. 切片(对应 TextSplitter)
splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=10)
docs = splitter.create_documents([
"""LangChain 是 2022 年由 Harrison Chase 创建的开源项目。
它的核心抽象包括 LLM、PromptTemplate、Chain、Memory、Tool、Agent。
LangChain 的配套产品 LangSmith 提供 tracing 和调试能力。
后来推出的 LangGraph 用状态机替代了线性 Chain,更适合复杂 Agent。"""
])
# 2. 向量化 + 入库(对应 VectorStore.add)
# OpenAIEmbeddings 会调 API 把每个 chunk 转成 1536 维向量
# Chroma 是本地向量库,生产环境可换 FAISS/Milvus/Pinecone
vectorstore = Chroma.from_documents(docs, OpenAIEmbeddings())
retriever = vectorstore.as_retriever(search_kwargs={"k": 2})
# 3. 组装 RAG 链(对应 rag_chain)
rag_prompt = ChatPromptTemplate.from_template(
"基于以下上下文回答问题。若上下文没有答案,请回答\"不知道\"。\n\n"
"上下文:\n{context}\n\n问题: {question}"
)
def format_docs(docs):
return "\n---\n".join(d.page_content for d in docs)
# 这个 dict 写法是 LCEL 的并行分发: 同一个输入分别流向 context 和 question 两路
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| rag_prompt
| ChatOpenAI(model="gpt-4o-mini")
| StrOutputParser()
)
print(rag_chain.invoke("LangChain 是什么?"))
关键差异:
| 我们的最小版 | 真实 LangChain |
|---|---|
| Jaccard 相似度(词集合交并比) | OpenAI Embeddings + 余弦相似度(1536 维向量) |
| Python list 存文档 | Chroma(本地)/FAISS/Milvus/Pinecone |
rag_chain() 返回函数 |
LCEL 管道(原生支持流式、异步、batch) |
| 手动拼 context 字符串 | LCEL dict 并行分发 + RunnablePassthrough |
示例 3:带记忆的对话(对应我们的 app.chat(...))
python
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个乐于助人的助手"),
MessagesPlaceholder(variable_name="history"), # 历史消息插入点
("human", "{question}"),
])
chain = prompt | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
# 按 session_id 隔离不同用户的对话历史
store: dict[str, InMemoryChatMessageHistory] = {}
def get_history(session_id: str) -> InMemoryChatMessageHistory:
if session_id not in store:
store[session_id] = InMemoryChatMessageHistory()
return store[session_id]
chat_with_memory = RunnableWithMessageHistory(
chain,
get_history,
input_messages_key="question",
history_messages_key="history",
)
# 注意 config 里的 session_id - 这是 LangChain 多租户的核心
cfg = {"configurable": {"session_id": "user_42"}}
print(chat_with_memory.invoke({"question": "你好,我叫杰克"}, config=cfg))
print(chat_with_memory.invoke({"question": "我刚才说了什么?"}, config=cfg))
# -> 会正确回答"你说你叫杰克"
关键差异:
| 我们的最小版 | 真实 LangChain |
|---|---|
单例 Memory 存在内存 list |
RunnableWithMessageHistory + 可插拔 ChatMessageHistory(Redis/Postgres/DynamoDB) |
| 手动拼 prompt 字符串 | MessagesPlaceholder 自动注入 |
| 无多用户隔离 | session_id 多租户开箱即用 |
示例 4:Agent(对应我们的 app.agent()...)
现代 LangChain 推荐用 LangGraph 的 create_react_agent 而不是旧的 AgentExecutor。LangGraph 把 Agent 建模成状态图,比我们的 while 循环强大得多(支持分支、并行、人在回路、checkpoint 回放)。
python
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
@tool
def calculator(expr: str) -> str:
"""计算一个数学表达式,输入如 '2+3*5'"""
try:
return str(eval(expr, {"__builtins__": {}}, {}))
except Exception as e:
return f"计算错误: {e}"
@tool
def search(query: str) -> str:
"""搜索实时信息,输入搜索词"""
fake_db = {"天气": "北京今天晴,25℃", "股价": "苹果今日收盘 $180"}
for k, v in fake_db.items():
if k in query:
return v
return "未找到"
# 一行代码构建 ReAct Agent
agent = create_react_agent(
ChatOpenAI(model="gpt-4o-mini"),
tools=[calculator, search],
)
# 注意输入输出格式是消息列表
result = agent.invoke({
"messages": [("user", "今天北京天气怎么样?然后帮我算 23*17")]
})
print(result["messages"][-1].content)
关键差异:
| 我们的最小版 | 真实 LangChain |
|---|---|
字符串正则解析 Action: / Action Input: |
原生 function calling(OpenAI/Anthropic 模型直接吐结构化 tool call) |
| 手写 ReAct prompt | LangGraph 内置 prompt + 状态机 |
while 循环手工推进 |
状态图调度,可中断、可回放、可并行工具调用 |
Tool(name, description, func) |
@tool 装饰器自动从函数签名和 docstring 提取 schema |
| 无 Schema 校验 | 自动生成 Pydantic schema,输入类型校验 |
示例 5:把它们串成一个生产可用的应用
上面 4 个示例在生产里常常需要组合起来:带记忆 + 有 RAG 检索 + 能调工具的 Agent。这是真实业务里最常见的形态。
python
from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.memory import MemorySaver
from langchain_core.tools import tool
# 把 retriever 包装成 tool,让 Agent 自己决定什么时候查知识库
@tool
def lookup_knowledge(query: str) -> str:
"""查询公司内部知识库"""
docs = retriever.invoke(query)
return "\n---\n".join(d.page_content for d in docs)
# checkpointer = Agent 的"记忆"(状态持久化)
agent = create_react_agent(
ChatOpenAI(model="gpt-4o-mini"),
tools=[calculator, search, lookup_knowledge],
checkpointer=MemorySaver(), # 生产环境换 PostgresSaver/RedisSaver
)
cfg = {"configurable": {"thread_id": "conversation_1"}}
# 多轮对话,Agent 自己决定什么时候 RAG、什么时候算数、什么时候搜
agent.invoke({"messages": [("user", "LangChain 核心组件有哪些?")]}, config=cfg)
agent.invoke({"messages": [("user", "那你刚才提到的第三个是什么?")]}, config=cfg)
这就是"LangChain 全家桶"的真实形态:LCEL 做管道、LangGraph 做 Agent、LangSmith 做 tracing(下一节提)。
整体对照表
| 能力 | MiniLangChain(~300 行) | 真实 LangChain |
|---|---|---|
| 简单链 | `prompt | llm |
| RAG | Jaccard + list | OpenAIEmbeddings + Chroma/FAISS + LCEL 分发 |
| Memory | 单例 list 拼字符串 | RunnableWithMessageHistory + 多后端 |
| Agent | 正则解析 ReAct 循环 | LangGraph 状态机 + native function calling |
| Tracing / 调试 | print |
LangSmith(DAG 可视化、token 计费、回放) |
| 异步 / 流式 / batch | 无 | 全部 Runnable 原生支持 |
| 模型切换 | 换个 BaseLLM 子类 |
换 langchain-* 包的导入路径,其余不动 |
看完这节应该明白 :你自己写的 MiniLangChain 在抽象层面 和真实 LangChain 是同构的------都是 Runnable 组合。真实 LangChain 多出来的价值,90% 在适配器生态 (对接 50+ 模型、30+ 向量库、100+ 工具)和周边工程能力(LangSmith tracing、流式、异步、checkpoint),而不在核心抽象。
这也是为什么建议"先裸写再决定用不用" ------ 你现在已经有能力判断了。
七、给后端工程师的理解捷径
如果你来自 Spring/Scala/Java 生态,可以这样映射:
| Spring 概念 | LangChain 对应 |
|---|---|
@Component Bean |
Runnable |
BeanFactory |
LLM provider |
| Filter Chain | LCEL Chain |
@Cacheable |
set_llm_cache |
| AOP 切面 | Callback Handler |
ApplicationContext |
Agent(持有一堆 tools) |
| Spring Integration 的 EIP | LangGraph 的 state machine |
核心范式转变:
- 传统后端:确定性流程 + 偶尔的不确定输入
- LLM 应用:不确定性组件 + 确定性的编排层
LangChain 做的就是把不确定性(LLM 输出)封在 Runnable.invoke() 里,让外层保持工程师熟悉的确定性组合模式。
八、什么时候不需要 LangChain?
诚实讲,LangChain 在 2024 年之后争议很大:
用它的场景:
- 要快速切换多个模型供应商(抽象价值)
- 复杂多步骤 Agent、需要现成的 Memory/Retriever 实现
- 需要 LangSmith 做 tracing 和调试
不用它的场景:
- 单一模型(比如只用 OpenAI),直接调 SDK 更简单
- 对延迟和代码可读性要求高------LangChain 抽象层深,栈跟踪像噩梦
- 复杂状态机应该用 LangGraph(LangChain 自己出的继任者)或 DIY
我的建议:
先用 SDK 裸写一个 demo → 遇到"重复造抽象"的痛点时再引入 LangChain → 如果发现抽象比问题还复杂,就退回裸写。
九、延伸阅读
- LangChain 官方文档:python.langchain.com/
- LCEL(表达式语言):python.langchain.com/docs/concep...
- LangGraph(状态机版 Agent):langchain-ai.github.io/langgraph/
- ReAct 论文:ReAct: Synergizing Reasoning and Acting in Language Models (Yao et al., 2022)
- 推荐批判性阅读:Hacker News 上大量"为什么我放弃了 LangChain"的讨论
十、把代码跑起来
上文所有代码片段拼在一起就是可运行的 minilangchain.py(约 300 行)。跑一下:
bash
pip install openai # 仅 OpenAILLM 需要,FakeLLM 零依赖
python minilangchain.py
看懂这 300 行,LangChain 的 95% 概念你就都理解了。剩下 5% 是工程细节(流式、回调、异步、适配器、真实向量检索),等你真的用上再去补即可。
一句话总结 :LangChain = Prompt 模板化 + LLM 抽象化 + 组件管道化 + 工具调用循环。核心不是 AI,是软件工程。