
1. 为什么我们需要 LangGraph?(逻辑推演)
在学习 LangGraph 之前,我们要先思考一个问题:LangChain 原本的 LCEL(链式表达式)哪里不够用?
- 传统 Chain 的局限性: 传统的链状结构(如 LCEL)本质上是一个有向无环图(DAG)。数据像流水线一样,从A节点走到B节点,再走到C节点,最后输出。它是一锤子买卖。
- Agent(智能体)的真实需求: 真正的智能体(特别是你要做的可以写代码、测试代码的智能体)是需要循环(Loop)和分支(Branch)的 。当测试节点(Tester)发现代码有 Bug 时,数据流不能直接结束,它必须带着报错信息回退到编写节点(Coder)重新生成。
- LangGraph 的解法: 它引入了状态机(State Machine)的概念,允许我们构建包含**循环(Cycles)**的图。在这个图里,大模型不仅是数据处理单元,更是图中的"路由决策者"。
2. 核心概念拆解:全局状态(State)与 Reducer
在 LangGraph 中,最重要、也是最容易让人困惑的概念就是 State(状态)。
在复杂的后端系统或微服务架构中,我们经常需要在不同的执行模块之间传递一个全局的上下文(Context)。LangGraph 的 State 就是扮演这个角色。它是一个贯穿整个图运行周期的"全局数据结构"。
图中的每一个节点(Node,本质上就是一个 Python 函数)都遵循同一个严格的接口逻辑:
- 输入: 接收当前的全局 State。
- 执行: 运行你写的逻辑(比如调用 LLM、执行本地代码、查数据库等)。
- 输出: 返回一个字典(Dictionary),这个字典里包含了需要更新的状态字段。
💡 思考难点:状态是如何更新的?(Reducer 机制)
假设我们的 State 里有一个字段记录了生成的代码。当节点返回新的数据时,LangGraph 是直接覆盖旧数据,还是追加在旧数据后面?这就引入了 Reducer(归约器) 的概念。
- 默认行为(覆盖): 如果你在定义 State 时不加任何修饰,当节点返回
{"code": "print('hello')"}时,原来的code字段会被直接覆盖。 - 追加行为(Annotated): 对于聊天记录(Messages)或报错日志,我们通常希望是不断追加的。这时在 Python 中就需要引入 typing.Annotated****和 operator.add**(或自带的** add_messages)。这告诉框架:"当节点返回新的 message 时,请把它追加到列表末尾,而不是覆盖整个列表。"
3. 结合 Skills Creator:定义我们的 State
基于上述思考,如果要实现一个"技能创造者",我们需要在 State 中记录哪些关键信息来维持整个系统的运转?
我们可以初步构思这样一个状态字典:
- user_requirement**(str):** 用户的初始需求(如:"写一个获取天气的脚本")。它一旦设定,通常不需要改变(默认覆盖逻辑)。
- current_code**(str):** 当前 LLM 生成的 Python 代码。每次 Coder 节点重新生成时,都会覆盖它。
- execution_logs**(list):** 代码测试节点(Tester)运行代码后的输出或报错日志。这个可能需要是追加逻辑(Reducer),或者如果每次只看最新一次的报错,也可以设计为覆盖逻辑。
- messages**(list):** 智能体与环境(或内部节点之间)的交互历史记录。通常必须带有追加(Reducer)属性。
- iteration_count**(int):** 循环次数。为了防止模型无限写出带 Bug 的代码死循环,我们需要一个计数器,每次循环加 1,达到阈值就强制退出。