主题:从零开发小框架,逐步搭建核心能力
目标:综合运用前文知识,动手复刻核心原型
一、总体设计思路
我们实现一个最小可用(MVP)版本,覆盖 LangChain 的核心理念:
- 统一协议:
Runnable
抽象,提供invoke / stream / batch
; - 表达式拼接:
|
组合出链(pipeline),RunnableMap
做字典式并行; - Prompt & LLM: 模板化输入、可替换的"模型"实现;
- 工程能力:
with_retry
、with_fallbacks
、with_config
; - Agent 回路: ReAct 风格(思考→行动→观察),Tool 调度与 scratchpad 记录。
说明:为便于直观与可运行,本文的"LLM"是规则/模板驱动的本地模拟,不用 API Key,也能跑通所有流程与示例。
二、核心代码(可直接运行)
保存为
mini_langchain.py
,然后python mini_langchain.py
运行即可。
python
from __future__ import annotations
from dataclasses import dataclass
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Optional,
Protocol,
Union
)
import time
# =========================
# 1) Runnable 抽象与管道拼接
# =========================
class Runnable(Protocol):
"""最小可用 Runnable 协议:统一执行入口"""
def invoke(self, input: Any, config: Optional[dict] = None) -> Any: ...
def stream(self, input: Any, config: Optional[dict] = None) -> Iterable[Any]: ...
def batch(self, inputs: List[Any], config: Optional[dict] = None) -> List[Any]: ...
# 语法糖:a | b 形成顺序链
def __or__(self, other: "Runnable") -> "Runnable":
return RunnableSequence([self, other])
class RunnableBase:
"""提供默认实现与可复用能力"""
def __init__(self):
self._config: dict = {}
def invoke(self, input: Any, config: Optional[dict] = None) -> Any:
raise NotImplementedError
def stream(self, input: Any, config: Optional[dict] = None) -> Iterable[Any]:
# 默认:将 invoke 的结果一次性作为单块流
yield self.invoke(input, config=config)
def batch(self, inputs: List[Any], config: Optional[dict] = None) -> List[Any]:
return [self.invoke(x, config=config) for x in inputs]
# 配置下发
def with_config(self, **kv) -> "RunnableBase":
self._config = {**self._config, **kv}
return self
def __or__(self, other: Runnable) -> "Runnable":
return RunnableSequence([self, other])
class RunnableSequence(RunnableBase):
"""顺序执行的链:a | b | c"""
def __init__(self, steps: List[Runnable]):
super().__init__()
self.steps = steps
# print(self.steps)
def invoke(self, input: Any, config: Optional[dict] = None) -> Any:
val = input
for step in self.steps:
val = step.invoke(val, config=config)
return val
def stream(self, input: Any, config: Optional[dict] = None) -> Iterable[Any]:
# 简化策略:前 N-1 步走 invoke,最后一步调用 stream,获得更细粒度输出
val = input
for step in self.steps[:-1]:
val = step.invoke(val, config=config)
yield from self.steps[-1].stream(val, config=config)
class RunnableLambda(RunnableBase):
"""把任意函数包装为 Runnable"""
def __init__(self, func: Callable[[Any], Any]):
super().__init__()
self.func = func
def invoke(self, input: Any, config: Optional[dict] = None) -> Any:
return self.func(input)
class RunnableMap(RunnableBase):
"""字典式并行,将输入分发给多路 runnable,聚合为字典输出"""
def __init__(self, mapping: Dict[str, Runnable]):
super().__init__()
self.mapping = mapping
def invoke(self, input: Any, config: Optional[dict] = None) -> Dict[str, Any]:
return {k: r.invoke(input, config=config) for k, r in self.mapping.items()}
# =========================
# 2) Prompt、Parser、模拟 LLM
# =========================
class PromptTemplate(RunnableBase):
"""最小 Prompt:使用 Python format"""
def __init__(self, template: str):
super().__init__()
self.template = template
def invoke(self, input: Dict[str, Any], config: Optional[dict] = None) -> str:
return self.template.format(**input)
class StrOutputParser(RunnableBase):
"""最小解析器:确保输出是字符串"""
def invoke(self, input: Any, config: Optional[dict] = None) -> str:
return str(input)
class MockChatModel(RunnableBase):
"""
规则驱动的"LLM"模拟器:
- 遇到 ReAct 提示词时,产出一个包含 Action/Action Input 的片段
- 否则返回简短回答
- 支持 stream:按词分块输出
"""
def __init__(self, temperature: float = 0.0):
super().__init__()
self.temperature = temperature
def _react_decision(self, prompt: str) -> str:
# 极简规则:如果包含"乘/乘以/multiply",调用 multiply 工具;包含"反转/倒序/reverse",调用 reverse 工具
lower = prompt.lower()
if "multiply" in lower or "乘以" in prompt or "乘" in prompt:
# 抽出两个数字
import re
nums = list(map(int, re.findall(r"\d+", prompt)))
if len(nums) >= 2:
return f"Thought: 需要计算\nAction: multiply\nAction Input: {nums[0]}*{nums[1]}"
else:
return "Thought: 未找到数字\nFinal: 请提供两个要相乘的数字。"
if "reverse" in lower or "反转" in prompt or "倒序" in prompt:
import re
words = re.findall(r"[A-Za-z\u4e00-\u9fa5]+", prompt)
target = words[-1] if words else "文本"
return f"Thought: 需要反转\nAction: reverse\nAction Input: {target}"
# 默认:直接回答
return "Final: 这是一个简短回答。"
def invoke(self, input: str, config: Optional[dict] = None) -> str:
text = input.strip()
# 如果像 ReAct 提示,返回可解析的动作
if "Action:" in text or "给出你的行动" in text or "你可以使用工具" in text:
return self._react_decision(text)
# 普通问答
return f"回答:{text[:80]}"
def stream(self, input: str, config: Optional[dict] = None) -> Iterable[str]:
out = self.invoke(input, config=config)
# 按空格切分模拟 token 流
for tok in out.split(" "):
yield tok + " "
# =========================
# 3) 工程能力:重试与回退
# =========================
class RunnableRetry(RunnableBase):
def __init__(self, inner: Runnable, max_attempts: int = 3, backoff: float = 0.1):
super().__init__()
self.inner = inner
self.max_attempts = max_attempts
self.backoff = backoff
def invoke(self, input: Any, config: Optional[dict] = None) -> Any:
err = None
for i in range(1, self.max_attempts + 1):
try:
return self.inner.invoke(input, config=config)
except Exception as e:
err = e
time.sleep(self.backoff * i) # 线性退避,演示用
raise RuntimeError(f"Retry failed after {self.max_attempts} attempts") from err
class RunnableFallbacks(RunnableBase):
def __init__(self, primary: Runnable, fallbacks: List[Runnable]):
super().__init__()
self.primary = primary
self.fallbacks = fallbacks
def invoke(self, input: Any, config: Optional[dict] = None) -> Any:
try:
return self.primary.invoke(input, config=config)
except Exception:
for fb in self.fallbacks:
try:
return fb.invoke(input, config=config)
except Exception:
continue
raise
# =========================
# 4) Tool 定义与执行
# =========================
@dataclass
class Tool:
name: str
description: str
func: Callable[[str], str]
def invoke(self, tool_input: str) -> str:
return self.func(tool_input)
# 示例工具
def multiply_tool_fn(expr: str) -> str:
"""输入形如 '19*21' 的表达式,返回乘积"""
a, b = expr.split("*")
return str(int(a.strip()) * int(b.strip()))
def reverse_tool_fn(text: str) -> str:
return text[::-1]
MULTIPLY = Tool(
name="multiply",
description="计算 'a*b' 形式表达式的乘积(仅整数)",
func=multiply_tool_fn,
)
REVERSE = Tool(
name="reverse",
description="反转输入字符串",
func=reverse_tool_fn,
)
# =========================
# 5) 迷你 ReAct Agent 实现
# =========================
@dataclass
class AgentAction:
tool: str
tool_input: str
log: str = ""
@dataclass
class AgentFinish:
output: str
log: str = ""
class MiniReActAgent(RunnableBase):
"""
- 接受 {'input': str, 'scratchpad': str} 作为输入
- 使用 MockChatModel 产生 Thought/Action 或 Final
- 调用工具 -> 观察 -> 追加到 scratchpad
- 循环直到 Final 或达到步数上限
"""
def __init__(self, llm: MockChatModel, tools: Dict[str, Tool], max_steps: int = 3):
super().__init__()
self.llm = llm
self.tools = tools
self.max_steps = max_steps
def _plan(self, prompt: str) -> Union[AgentAction, AgentFinish]:
# 解析 LLM 输出为 Action/Final
text = self.llm.invoke(prompt)
lines = [l.strip() for l in text.splitlines() if l.strip()]
# 简化解析器
action = None
action_input = None
final = None
for ln in lines:
if ln.lower().startswith("final:"):
final = ln.split(":", 1)[1].strip()
if ln.lower().startswith("action:"):
action = ln.split(":", 1)[1].strip()
if ln.lower().startswith("action input:"):
action_input = ln.split(":", 1)[1].strip()
if final:
return AgentFinish(output=final, log=text)
if action and action_input:
return AgentAction(tool=action, tool_input=action_input, log=text)
# 兜底:直接终止
return AgentFinish(output="无法解析模型计划,直接给出简短回答。", log=text)
def _build_prompt(self, question: str, scratchpad: str, tools_desc: str) -> str:
return f"""你可以使用工具完成任务。可用工具:
{tools_desc}
请根据需求做出一步计划(如需调用工具,给出 Action 与 Action Input;否则直接 Final):
问题:{question}
历史推理:
{scratchpad}
"""
def invoke(self, input: Dict[str, Any], config: Optional[dict] = None) -> Dict[
str, Any]:
question = input.get("input", "")
scratchpad = input.get("scratchpad", "")
tools_desc = "\n".join(
[f"- {t.name}: {t.description}" for t in self.tools.values()])
for step in range(1, self.max_steps + 1):
prompt = self._build_prompt(question, scratchpad, tools_desc)
decision = self._plan(prompt)
if isinstance(decision, AgentFinish):
scratchpad += f"[LLM] Final -> {decision.output}\n"
return {"output": decision.output, "scratchpad": scratchpad,
"steps": step}
# 执行动作
if decision.tool not in self.tools:
scratchpad += f"[LLM] Unknown tool={decision.tool}\n"
return {"output": "工具不存在,终止。", "scratchpad": scratchpad,
"steps": step}
try:
obs = self.tools[decision.tool].invoke(decision.tool_input)
scratchpad += f"[LLM] {decision.log}\n[TOOL:{decision.tool}] <- {decision.tool_input}\n[OBS] {obs}\n"
except Exception as e:
scratchpad += f"[ERROR] 工具执行异常:{e}\n"
# 简化策略:异常则结束
return {"output": "工具执行失败。", "scratchpad": scratchpad,
"steps": step}
# 将观察反馈给 LLM,尝试结束
final_try = self.llm.invoke(f"根据观察结果给出最终答案:{obs}")
scratchpad += f"[LLM] Final -> {final_try}\n"
return {"output": final_try.replace("Final:", "").strip(),
"scratchpad": scratchpad, "steps": step}
return {"output": "达到最大步数仍未完成。", "scratchpad": scratchpad,
"steps": self.max_steps}
# =========================
# 6) 演示:链、流式、重试/回退、并行、Agent
# =========================
def demo_pipeline():
print("\n=== Demo A: 最小链(Prompt | LLM | Parser)===")
prompt = PromptTemplate("请用一句话回答:{question}")
llm = MockChatModel()
parser = StrOutputParser()
chain = prompt | llm | parser
print("invoke:", chain.invoke({"question": "什么是可组合的 Runnable?"}))
print("stream:", "".join(list(chain.stream({"question": "请解释流式输出"}))))
def demo_retry_fallback():
print("\n=== Demo B: 重试与回退 ===")
class Unstable(RunnableBase):
def __init__(self):
super().__init__()
self.count = 0
def invoke(self, input: Any, config: Optional[dict] = None) -> str:
self.count += 1
if self.count < 4:
raise RuntimeError("偶发错误")
return f"成功(第{self.count}次)"
fallback = RunnableLambda(lambda _: "来自回退的结果")
primary_success = RunnableRetry(Unstable(), max_attempts=4)
robust_success = RunnableFallbacks(primary_success, [fallback])
print("result_success:", robust_success.invoke(None))
primary_fallback = RunnableRetry(Unstable(), max_attempts=3)
robust_fallback = RunnableFallbacks(primary_fallback, [fallback])
print("result_fallback:", robust_fallback.invoke(None))
def demo_map_parallel():
print("\n=== Demo C: RunnableMap 并行聚合 ===")
llm = MockChatModel()
chain1 = PromptTemplate("学术风格回答:{q}") | llm
chain2 = PromptTemplate("通俗风格回答:{q}") | llm
mp = RunnableMap({"academic": chain1, "casual": chain2})
out = mp.invoke({"q": "黑洞如何形成?"})
print("map output:", out)
def demo_agent():
print("\n=== Demo D: 迷你 ReAct Agent ===")
llm = MockChatModel()
tools = {t.name: t for t in [MULTIPLY, REVERSE]}
agent = MiniReActAgent(llm=llm, tools=tools, max_steps=2)
# 1) 数学问题:19*21
out1 = agent.invoke({"input": "请计算 19 乘以 21 的结果"})
print("Q1 result:", out1["output"])
print("scratchpad:")
print(out1["scratchpad"])
# 2) 字符串反转
out2 = agent.invoke({"input": "请把 LangChain 反转"})
print("Q2 result:", out2["output"])
print("scratchpad:")
print(out2["scratchpad"])
if __name__ == "__main__":
demo_pipeline()
demo_retry_fallback()
demo_map_parallel()
demo_agent()
输出
yaml
=== Demo A: 最小链(Prompt | LLM | Parser)===
invoke: 回答:请用一句话回答:什么是可组合的 Runnable?
stream: 回答:请用一句话回答:请解释流式输出
=== Demo B: 重试与回退 ===
result_success: 成功(第4次)
result_fallback: 来自回退的结果
=== Demo C: RunnableMap 并行聚合 ===
map output: {'academic': '回答:学术风格回答:黑洞如何形成?', 'casual': '回答:通俗风格回答:黑洞如何形成?'}
=== Demo D: 迷你 ReAct Agent ===
Q1 result: 回答:根据观察结果给出最终答案:399
scratchpad:
[LLM] Thought: 需要计算
Action: multiply
Action Input: 19*21
[TOOL:multiply] <- 19*21
[OBS] 399
[LLM] Final -> 回答:根据观察结果给出最终答案:399
Q2 result: 请提供两个要相乘的数字。
scratchpad:
[LLM] Final -> 请提供两个要相乘的数字。
三、实现拆解与设计要点
- 统一执行协议
Runnable
定义invoke/stream/batch
,并以|
运算符拼接为RunnableSequence
;RunnableMap
用于 并行聚合 (字典多路输出),模拟 LCEL 的组合式表达。
- Prompt/LLM/Parser 解耦
PromptTemplate
仅负责字符串模板;MockChatModel
仅负责生成(这里用规则模拟 LLM);StrOutputParser
仅负责输出格式化;- 三者通过
|
解耦组合,可替换性强。
- 工程能力内建
RunnableRetry
捕获异常并重试(带简单退避);RunnableFallbacks
允许主链失败后切换备选链;- 这两项是实战中稳定性的关键。
- 迷你 Agent 的 ReAct 循环
_build_prompt
给出工具清单与 scratchpad;_plan
使用"LLM"产出Action
或Final
;- 当
Action
存在时调用工具,得到Observation
,再促使 LLM 生成Final
; - scratchpad 作为中间态沉淀推理过程,便于追踪与调试。
四、你可以怎样扩展它
- 真流式输出 :在
stream()
中按字符/分词器切分,同时配合 Web 框架(SSE/WS)推送; - 更强的 Router/Branch :实现条件分支(类似
RunnableBranch
),实现多路径决策; - 更完善的重试:指数/抖动退避、基于异常类型的差异化策略;
- 更强解析器 :从 LLM 输出中稳定解析
Action/Action Input
(正则/JSON Schema); - 观测与追踪:埋点(tags、run_name)、统一日志/事件回调,再接 LangSmith 等平台。
🔚 小结
- 我们用不到 300 行代码,复刻了一个可运行的微型 LangChain : 统一
Runnable
协议、|
链式组合、Prompt/LLM/Parser 解耦、重试与回退、并行聚合、以及 ReAct Agent(含 Tool、scratchpad)。 - 这套骨架能帮你把"理解"变成"可落地的最小工程原型",再逐步替换为真实 LLM、向量检索、LangServe 部署等。
接下来我们将从调试观测(LangSmith)、图式工作流(LangGraph)、到服务化与平台化编排的视角,梳理 LLM 应用工程的下一阶段能力版图与最佳实践。