LangChain 设计原理分析¹⁴ | 模拟实现一个精简版 LangChain

主题:从零开发小框架,逐步搭建核心能力

目标:综合运用前文知识,动手复刻核心原型


一、总体设计思路

我们实现一个最小可用(MVP)版本,覆盖 LangChain 的核心理念:

  • 统一协议: Runnable 抽象,提供 invoke / stream / batch
  • 表达式拼接: | 组合出链(pipeline),RunnableMap 做字典式并行;
  • Prompt & LLM: 模板化输入、可替换的"模型"实现;
  • 工程能力: with_retrywith_fallbackswith_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 -> 请提供两个要相乘的数字。

三、实现拆解与设计要点

  1. 统一执行协议
  • Runnable 定义 invoke/stream/batch,并以 | 运算符拼接为 RunnableSequence
  • RunnableMap 用于 并行聚合 (字典多路输出),模拟 LCEL 的组合式表达。
  1. Prompt/LLM/Parser 解耦
  • PromptTemplate 仅负责字符串模板
  • MockChatModel 仅负责生成(这里用规则模拟 LLM);
  • StrOutputParser 仅负责输出格式化
  • 三者通过 | 解耦组合,可替换性强。
  1. 工程能力内建
  • RunnableRetry 捕获异常并重试(带简单退避);
  • RunnableFallbacks 允许主链失败后切换备选链;
  • 这两项是实战中稳定性的关键。
  1. 迷你 Agent 的 ReAct 循环
  • _build_prompt 给出工具清单与 scratchpad;
  • _plan 使用"LLM"产出 ActionFinal
  • 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 应用工程的下一阶段能力版图与最佳实践。

相关推荐
掘我的金1 小时前
20_LangChain多数据源生成
langchain
掘我的金1 小时前
19_LangChain结合SQL实现数据分析问答
langchain
王国强20093 小时前
LangChain 设计原理分析¹³ | LangChain Serve 快速部署
langchain
前端双越老师10 小时前
【干货】使用 langChian.js 实现掘金“智能总结” 考虑大文档和 token 限制
人工智能·langchain·node.js
Dajiaonew11 小时前
Spring AI RAG 检索增强 应用
java·人工智能·spring·ai·langchain
xuanwuziyou1 天前
LangChain 多任务应用开发
人工智能·langchain
大志说编程1 天前
LangChain框架入门16:智能客服系统RAG应用实战
后端·langchain·aigc
AI大模型1 天前
从零开始,亲手开发你的第一个AI大模型(一)基础知识
程序员·langchain·agent
掘我的金2 天前
15_LangChain自定义Callback组件
langchain