【撕开黑盒学大模型】从手写 Agent 到 LangChain / LangGraph,框架到底接管了什么?
源码级对照分析:从手写 Agent 到 LangChain / LangGraph,框架到底接管了什么?
配套代码:
../v4_langchain/代码地址:撕开黑盒学大模型-从白盒状态机演进到工业级Agent框架
本文目标:用无依赖的
MiniStateGraph对照 LangChain tools 和 LangGraphStateGraph,说明框架真正接管的是工具协议、状态推进、持久化边界和可观测性,而不是把 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 字符串中。这个方式简单,但有三个问题:
- 很难只修改某一类状态;
- 很难做恢复和持久化;
- 很难追踪每个节点前后的状态差异。
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,建议分三步:
- 先把
AgentState固定为 TypedDict 或 Pydantic 模型,明确messages、tool_result、answer等字段。 - 再把
call_model、call_tool、finalize迁移为真实StateGraph节点。 - 最后引入 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_model、call_tool、finalize三个节点;visualization.html能展示节点执行顺序;langchain_agent.py和langgraph_agent.py在未安装依赖时给出明确错误;- 文中所有 LangChain / LangGraph 表述都带有日期或官方文档依据;
- 所有工具示例都说明了输入边界和生产风险;
- 没有把本机绝对路径、密钥、token 或私有服务地址写入正文。
13. 总结
前三篇手写 demo 的价值,是让我们知道框架底层到底在封装什么。第 4 篇回到 LangChain / LangGraph,不是推翻手写实现,而是把手写实现里的机制映射到工业抽象:
- 工具注册变成 tools;
- 控制循环变成状态图;
- 局部变量变成显式 state;
- trace 文件变成可观测系统;
- 进程内状态变成可持久化 checkpoint;
- 隐含权限变成工具边界和审批节点。
真正有工程深度的表述不是“我会用 LangGraph”,而是“我知道为什么这个 Agent 需要状态图、持久化、条件边和工具权限边界”。
下一篇:小z疯狂码字ing…
感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!

更多推荐



所有评论(0)