源码级对照分析:从手写 Agent 到 LangChain / LangGraph,框架到底接管了什么?

配套代码:../v4_langchain/

代码地址:撕开黑盒学大模型-从白盒状态机演进到工业级Agent框架

本文目标:用无依赖的 MiniStateGraph 对照 LangChain tools 和 LangGraph StateGraph,说明框架真正接管的是工具协议、状态推进、持久化边界和可观测性,而不是把 Agent 的底层机制变成黑盒。



1. 先说结论:框架不是魔法,而是工程边界

前三篇文章分别手写了:

  • ReAct 工具调用循环;
  • 记忆分层治理;
  • ReWOO DAG 异步调度。

这些手写代码能把 Agent 的底层机制讲清楚,但如果直接扩展成生产框架,很快会遇到几个维护成本:

  • 不同模型供应商的 tool calling 格式不一致;
  • 工具描述、参数 Schema、返回结果和错误处理需要统一;
  • 状态分支越来越多,while True 难以维护;
  • 长任务需要中断、恢复、重试和持久化;
  • 工具调用需要 trace、审批、权限控制和审计;
  • 记忆、上下文和检索结果需要进入统一状态,而不是散落在局部变量里。

LangChain / LangGraph 的价值就在这里。它们不是让底层机制消失,而是把我们已经手写过的机制抽象成更稳定的工程接口。

2. 当前版本依据与适用范围

本文涉及的 LangChain / LangGraph 表述按 2026-06-23 可见官方文档核对:

  • LangChain tools 文档仍把 tool 描述为具有明确输入输出、会传递给聊天模型的可调用函数,模型根据上下文决定何时调用以及传入什么参数。
  • LangChain tools 文档强调 type hints 会定义工具输入 Schema,docstring 会帮助模型理解工具用途。
  • LangChain tools 文档已经把 state、context、store、stream writer、execution info 等运行时信息放到工具执行上下文里讨论。
  • LangGraph 文档仍把 LangGraph 定位为构建长期运行、有状态 Agent 的底层编排框架,核心能力包括 durable execution、streaming、human-in-the-loop、memory 和 persistence。
  • LangGraph persistence 文档仍围绕 checkpointer、thread、checkpoint、state snapshot、store 等概念组织。

所以本文不是讲某一个历史版本的旧 API,而是讲一个更稳定的迁移判断:当你的手写 Agent 已经开始出现工具协议、状态分支、恢复、审计和长期记忆问题时,就应该把这些边界迁移到框架抽象里。

需要注意:本文的 MiniStateGraph 是教学实现,不等于完整 LangGraph。它只保留 node、edge、conditional edge 和 state 这几个关键心智模型,帮助读者先看清框架抽象解决的问题。

3. 从手写 ToolRegistry 到 LangChain tools

v1_react/tools.py 中的手写注册器做了三件事:

signature = inspect.signature(func)
description = inspect.getdoc(func)
self._tools[func.__name__] = func

这说明一个工具至少需要具备:

  • 名称;
  • 描述;
  • 参数 Schema;
  • 运行时调用入口;
  • 返回结果约定;
  • 错误处理语义。

LangChain tools 解决的是同一类问题,只是边界更完整:

  • 与模型 tool calling 格式对接;
  • 用类型提示和 Schema 描述参数;
  • 用 docstring 或显式描述帮助模型判断何时调用;
  • 统一工具对象接口;
  • 支持工具访问 state、context、store 等运行时信息;
  • 能接入 Agent 执行链路、trace 和错误处理。

所以迁移时不要只看装饰器语法:

@tool
def calculator(expression: str) -> str:
    ...

真正要看的,是它如何把一个 Python 函数变成模型可选择、运行时可调用、结果可追踪、失败可治理的工具对象。

4. 一个容易忽略的安全边界:不要把工具等同于任意代码执行

配套目录里的 langchain_agent.py 保留了一个最小 LangChain tools 适配示例:

@tool
def calculator(expression: str) -> str:
    """Evaluate a small arithmetic expression."""
    return str(eval(expression, {"__builtins__": {}}, {}))

这段代码只适合教学演示:它表达的是“一个函数如何被包装成 tool”。即使这里限制了 __builtins__,也不应该把它当作生产级计算器,更不应该直接接收不可信用户输入。

生产环境里至少应该替换成下面几类方案之一:

  • 白名单表达式解析器,只允许数字、运算符和括号;
  • 固定参数工具,例如 calculate_budget(unit_price: float, count: int)
  • 独立沙箱服务,限制 CPU、内存、文件系统和网络访问;
  • 对高风险工具加入人工审批、权限检查和审计日志。

这也是 Agent 工程里最重要的判断之一:工具不是函数列表,而是权限边界。 框架能帮我们标准化工具调用,但不会自动替我们定义业务权限、输入校验和生产风险。

5. 从 while True 到状态图

v1_react/agent.py 的核心是循环:

for step in range(1, self.max_steps + 1):
    output = self.model.complete(transcript)
    if final_line:
        return answer
    observation = self.tools.call(tool_name, tool_arg)

这个循环适合教学,但复杂 Agent 很快会出现多种分支:

  • 模型需要调用工具;
  • 模型可以直接结束;
  • 工具调用失败后需要重试;
  • 高风险工具需要人工审批;
  • 上下文太长时需要先摘要;
  • 长任务需要暂停和恢复;
  • 某个分支失败后不能阻塞整个任务;
  • 某些节点需要流式返回中间进度。

这时,把所有逻辑塞进一个循环会变得难以维护。LangGraph 的 StateGraph 思路,是把状态推进拆成节点和边。

为了不强制读者安装框架,v4_langchain/handmade_graph.py 写了一个无依赖版本:

graph.add_node("call_model", call_model)
graph.add_node("call_tool", call_tool)
graph.add_node("finalize", finalize)
graph.add_conditional_edges(
    "call_model",
    route,
    {
        "tool": "call_tool",
        "final": "finalize",
    },
)

这段代码对应的不是完整 LangGraph 实现,而是它的核心心智模型:

  • node:一个状态转换函数;
  • edge:节点之间的固定流转;
  • conditional edge:根据 state 决定下一跳;
  • state:所有节点共享并持续更新的数据结构。

6. 中间状态为什么要显式化

手写 ReAct 里,状态主要藏在 transcript 字符串中。这个方式简单,但有三个问题:

  1. 很难只修改某一类状态;
  2. 很难做恢复和持久化;
  3. 很难追踪每个节点前后的状态差异。

MiniStateGraph 使用 State = dict[str, object],每个节点接收 state 并返回新 state:

def call_tool(state: State) -> State:
    messages = list(state.get("messages", []))
    messages.append("tool: calculator 返回 48.0")
    return {**state, "messages": messages, "tool_result": "48.0", "next": "final"}

这样做之后,状态就可以分层治理:

状态字段 作用 迁移到框架后的意义
messages 对话历史 可接入标准消息结构、裁剪和短期记忆
tool_result 工具返回 可转成 ToolMessage、结构化结果或错误对象
next 路由标记 可映射为 conditional edge
answer 最终答案 可作为终止节点产物
summary_memory 摘要记忆 可变成独立 summarization node
retrieved_docs 检索结果 可接入 retriever / vector store

LangGraph 的 checkpointer / persistence 能力,也建立在“状态是显式对象”这个前提上。只有状态显式化,才可能恢复、回放、审计和 time travel。

7. 手写模块到框架抽象的映射

自研模块 框架对应 框架解决的问题 迁移时要保留的业务判断
ToolRegistry LangChain tools / tool calling 标准化工具描述、参数和调用结果 工具权限、输入校验、失败语义
ReactAgent.run() LangGraph node + conditional edge 把循环拆成可维护的图结构 节点拆分粒度、终止条件
recent_messages Messages state / conversation state 标准化消息存储与裁剪 多轮上下文保留策略
summary_memory summarization node 把摘要压缩变成可观测节点 摘要触发阈值、信息损失控制
JsonVectorStore retriever / vector store 适配真实向量库和检索组件 多租户隔离、删除和更新策略
ExecutionTrace tracing / observability 记录节点、工具、耗时和错误 日志脱敏、保留周期
MiniStateGraph LangGraph StateGraph 教学版状态图,对照节点和边 状态 Schema、恢复点设计

这个表的重点不是“哪个 API 对应哪个 API”,而是“哪个工程问题被框架接管,哪个业务边界仍然必须由工程师负责”。

8. 运行 v4 对照 Demo:看 trace,而不是只看最终答案

v4_langchain 目录下运行:

python main.py

预期输出:

预算结果是 48.0
trace written: trace.json

打开:

visualization.html

或直接查看 trace.json,可以看到三个节点的状态快照:

{
  "node": "call_tool",
  "state": {
    "messages": [
      "user: 计算预算",
      "model: 需要调用 calculator 工具",
      "tool: calculator 返回 48.0"
    ],
    "next": "final",
    "tool_result": "48.0"
  }
}

这份 trace 的意义不是证明计算器多复杂,而是证明状态图的三个关键点:

  • 每个节点执行后都有状态快照;
  • 路由条件 next 是显式字段;
  • 工具结果 tool_result 没有混进自然语言 transcript,而是成为可检查的数据。

真正接入 LangGraph 时,这类状态快照才有机会进一步接入 checkpoint、恢复、审计和可视化调试。

9. 真实框架适配层应该放在哪里

目录里还保留了两个可选文件:

  • langchain_agent.py
  • langgraph_agent.py

它们在未安装依赖时会给出明确错误,而不是假装可运行:

except ImportError as exc:
    raise RuntimeError("Install LangGraph before running this adapter.") from exc

这是一个重要工程习惯:可选依赖要放在边界上。教学 demo 的基础路径不应该被重型依赖阻断;框架迁移路径也不应该和手写原理混在一起。

如果要把当前 demo 继续推进到真实 LangGraph,建议分三步:

  1. 先把 AgentState 固定为 TypedDict 或 Pydantic 模型,明确 messagestool_resultanswer 等字段。
  2. 再把 call_modelcall_toolfinalize 迁移为真实 StateGraph 节点。
  3. 最后引入 checkpointer,并用稳定的 thread_id 区分不同会话。

不要一开始就接入模型、向量库、数据库、审批和部署平台。那样会让读者看不清迁移的主线。

10. 迁移判据:什么时候应该从手写实现切到框架?

不是所有 demo 都需要 LangGraph。下面这个检查表更适合做迁移判断:

信号 继续手写是否合适 建议
只有 1 个模型调用和 1 个工具 合适 保持最小实现
工具有多种参数 Schema 开始吃力 引入 LangChain tools
分支超过 3 类,且有重试/终止条件 不合适 引入状态图
任务需要暂停、恢复、重放 不合适 引入 checkpointer / persistence
需要人工审批高风险工具 不合适 把审批设计成显式节点
需要多租户记忆隔离 不合适 明确 thread、context、store 边界
需要线上排查和审计 不合适 接入 trace 和日志治理

这张表也解释了为什么本文先写 MiniStateGraph:只有先看清状态和分支,框架迁移才不是“换 API”,而是“把正确的边界交给正确的抽象”。

11. 框架不是生产终点

LangChain / LangGraph 能解决很多问题,但它们不是生产系统的全部。

即使迁移到框架,仍然需要自己设计:

  • 工具权限边界;
  • Prompt 注入防护;
  • checkpoint 存储选型;
  • trace 数据脱敏和保留策略;
  • 高风险工具的人类审批;
  • 多租户记忆隔离;
  • 失败重试和幂等控制;
  • 依赖版本锁定和回滚策略;
  • 私有数据进入模型前的最小化处理。

框架提供抽象和运行时,工程师仍然要负责边界和取舍。

12. 发布前验证清单

发布或复用这份 demo 前,建议至少检查:

  • v4_langchain/main.py 能生成 trace.json
  • trace.json 中包含 call_modelcall_toolfinalize 三个节点;
  • visualization.html 能展示节点执行顺序;
  • langchain_agent.pylanggraph_agent.py 在未安装依赖时给出明确错误;
  • 文中所有 LangChain / LangGraph 表述都带有日期或官方文档依据;
  • 所有工具示例都说明了输入边界和生产风险;
  • 没有把本机绝对路径、密钥、token 或私有服务地址写入正文。

13. 总结

前三篇手写 demo 的价值,是让我们知道框架底层到底在封装什么。第 4 篇回到 LangChain / LangGraph,不是推翻手写实现,而是把手写实现里的机制映射到工业抽象:

  • 工具注册变成 tools;
  • 控制循环变成状态图;
  • 局部变量变成显式 state;
  • trace 文件变成可观测系统;
  • 进程内状态变成可持久化 checkpoint;
  • 隐含权限变成工具边界和审批节点。

真正有工程深度的表述不是“我会用 LangGraph”,而是“我知道为什么这个 Agent 需要状态图、持久化、条件边和工具权限边界”。


下一篇:小z疯狂码字ing…
感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!

在这里插入图片描述

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐