🚋 从流水线到地铁网:为什么复杂 AI 都要拆成多 Agent(上)——LangGraph 基础入门

36 阅读7分钟

写在前面:之前学 LangChain 时,我们做过不少线性工作流——chain = prompt | model | parser,一步接一步,像流水线。但 readme 今天抛出了一个残酷的现实——复杂的 Agent 产品基本都是多 Agent 架构。为什么?因为单 Agent 把什么都扛在身上太累了。这就像一家餐厅:LangChain 是一条"传菜流水线",所有菜都从同一个窗口出;而真正的连锁餐饮是"中央厨房"——切菜、炒菜、装盘、质检各有专人,并行开工。今天的主角 LangGraph,就是那张把各种"专人"串成网状协作的流程图。readme 说得直白:LangChain 线性编排,LangGraph 网状编排。以下所有代码均来自课堂真实文件。


一、单 Agent 到底累在哪?

readme 先分析了单 Agent 架构的痛点:

"单 Agent 架构下,所有 tool 的描述,每个功能的 prompt 都放到 system prompt 里。实际上执行每个功能只需要一部分 prompt,但每次都带上。token 消耗更高,更重要的是很多无关信息干扰,思考效率低且容易出错。"

翻译一下——假设一个 Agent 会 10 个技能(查天气、写代码、订机票、翻译……)。它的 system prompt 里躺着全部 10 个技能的描述。可你这次只是问天气——它却背着另外 9 个技能的说明书在思考。

两个代价:

代价说明
token 浪费无关的 prompt 每次都算钱
干扰思考无关信息多了,LLM 容易"想歪",准确率下降

readme 的公式先回顾了一下:

"Agent = LLM(大脑)+ Harness(tool + mcp + rag + skill ...)"

单 Agent 只有一个 LLM 大脑——所有决策都靠它一步步想、一个个 tool 调。大脑再强,同时扛 10 个工种也累。


二、多 Agent:从全能小作坊到中央厨房

readme 描述了多 Agent 的理想形态:

"多 Agent 多个大脑,并行思考。主 Agent 下发任务,子 Agent 并行处理完成后返回。每个大脑需要选择合适的模型。agent 组合式,按需加载,动态加载。"

"多 agent 分工合作,编程 Agent 负责写代码,让测试 agent 测试 TDD,让验证 Agent 验证代码是否符合预期。告诉主 Agent 通过了。"

一个经典的"写代码"分工场景:

主 Agent(产品经理/项目经理)
  │ 下发任务
  ├── 编程 Agent:写代码
  ├── 测试 Agent:TDD 测试
  └── 验证 Agent:验证代码是否符合预期
         │
         └── 结果上报主 Agent

编程的写代码、测试的写测试、验证的做验收——各管一摊,并行开工。

readme 总结了多 Agent 的三大好处:

"基于三个原因:1. 决策准确率高,token 消耗更低。每个 Agent 只带必要的最少 Prompt,没有冗余信息干扰。调用 llm 次数多,但更省 token。2. 并行思考和任务处理。主管分派子任务,子 Agent 并行处理,整体效率更高。3. 多角色互相讨论,纠错能力更强。AutoGen 法庭。"

优势关键点
决策准确率 ↑ / token ↓每个 Agent 只带自己需要的 prompt
并行处理主管分派,子 Agent 同时干活
纠错能力多角色互评——"AutoGen 法庭"

"AutoGen 法庭"这个例子很形象——微软 AutoGen 做过一个 demo:多个 Agent 扮演检察官、辩护律师、法官,互相辩论质证,最终得出更严谨的结论。多角色互喷,反而比一个人自说自话更靠谱。

"单 Agent 只有一个 llm 大脑,需要一步步思考,调用 tool。规则。多 Agent 多个大脑,并行思考。"

规则 = 单线程;并行 = 多线程。这就是本质区别。


三、LangChain → LangGraph:从流水线到地铁网

readme 给出了演进路线:

"llm api, document loaders, splitter, embedding, vector store, output parser, memory... 基础模块。langchain 线性工作流编排。langgraph 网状工作流编排。"

基础模块两边共用——LLM API、文档加载、向量库、解析器、记忆……LangGraph 不是推倒重来,而是把 LangChain 的积木换了一种拼法。

对比LangChainLangGraph
编排形态线性(一条道走到黑)网状(分支、循环、暂停)
比喻流水线 / 队列地铁线路网
复杂度简单流程够用复杂 Agent 协作
典型能力prompt → model → parser状态 + 节点 + 边 + 条件跳转

为什么要网状?因为真实流程没有一条直线走完的——要判断走哪条分支(你是数学题还是闲聊?)、要循环重试(失败了再来一次)、要暂停等人确认(转账确认)。 这些都是线性编排做不到的。

readme 对网状编排 API 的概括只有一句话,但字字关键:

"工作节点 + 组织方式(api)。开始节点——初始状态。工作节点——职责 状态 state。链接工作节点。"

三个概念:节点(做什么)、状态(记什么)、边(怎么走)


四、状态图基础:第一个 LangGraph 程序

basic-graph.mjs 是最小可运行的 LangGraph 程序——两个节点串成一条线。

完整代码

import {
    Annotation,   // 状态值的描述
    END,          // 结束节点
    START,        // 开始节点
    StateGraph,   // 状态图:流程编排器,节点的组织
} from "@langchain/langgraph";

const StateAnnotation = Annotation.Root({
    text: Annotation({
        // reducer 怎么处理状态的改变
        reducer: (_prev, next) => next,  // js 数组reduce 消消乐
        default: () => "",               // 默认值
    })
})

const step1 = (state) => ({ text: `${state.text} -> step1` });
const step2 = (state) => ({ text: `${state.text} -> step2` });

const graph = new StateGraph(StateAnnotation)
    .addNode("step1", step1)
    .addNode("step2", step2)
    .addEdge(START, "step1")
    .addEdge("step1", "step2")
    .addEdge("step2", END)
    .compile()

const result = await graph.invoke({ text: "hello" });
console.log(result);  // { text: "hello -> step1 -> step2" }

四个核心概念

1. Annotation:状态的定义

const StateAnnotation = Annotation.Root({
    text: Annotation({
        reducer: (_prev, next) => next,
        default: () => "",
    })
})

Annotation.Root 定义整个图共享的状态结构。每个字段两个配置:

  • reducer:节点返回新值时怎么合并旧状态。(_prev, next) => next 就是"用新的覆盖旧的"
  • default:初始默认值

注释打了个比方:

"js 数组 reduce 消消乐"

reduce 是 JS 数组的归并方法——把一数组"消"成一个值。这里 reducer 决定"上一站的状态"和"节点返回的新值"怎么变成"下一站的状态"。(_prev, next) => next 最简单——旧的不看,直接上新值

2. 节点:函数就是节点

const step1 = (state) => ({ text: `${state.text} -> step1` });

在 LangGraph 里,一个普通函数就是一个节点——接收当前 state,返回要更新的部分。step1 收到 {text: "hello"},返回 {text: "hello -> step1"}

3. 边:节点怎么连

.addNode("step1", step1)
.addNode("step2", step2)
.addEdge(START, "step1")    // 开始 → step1
.addEdge("step1", "step2")  // step1 → step2
.addEdge("step2", END)      // step2 → 结束

addNode 注册节点(名字 + 函数),addEdge 连接节点。STARTEND 是内置的特殊节点——图的起点和终点。

整个流程:

START → step1 → step2 → END
  ↓       ↓       ↓
"hello" 加 step1  加 step2

4. compile + invoke

.compile()
const result = await graph.invoke({ text: "hello" });

compile() 编译工作流(准备执行),invoke({text: "hello"}) 用初始状态启动图。结果:

{ text: "hello -> step1 -> step2" }

状态像接力棒一样,从 START 一路传到 END,每经过一个节点就多一段内容。

附赠技能:mermaid 可视化

basic-graph.mjs 里还有一段画图代码:

const drawable = await graph.getGraphAsync();
const mermaid = drawable.drawMermaid({ withStyles: true });
console.log(mermaid);

代码注释说:

"mermaid 文本画图工具,简单的 markdown,自动生成流程图。可视化整个节点流转关系。"

LangGraph 自带把图转成 mermaid 流程图的能力——一种用文本描述流程图的标记语言。运行这段代码,控制台会输出 mermaid 语法,粘贴到支持 mermaid 的工具(Typora、GitHub、mermaid.live)就能看到节点流转的可视化图。

用代码描述流程,还能自动画出流程图——这就是 StateGraph 的"可视化红利"。


五、条件路由:地铁换乘的分叉口

basic-graph 是直线——一条道走到黑。但真实业务要分叉:用户问"1+2"要走计算节点,用户说"你好"要走闲聊节点。

conditional-routing.mjs 演示了条件路由。

状态定义

const StateAnnotation = Annotation.Root({
    query: Annotation({
        reducer: (_prev, next) => next,
        default: () => "",
    }),
    route: Annotation({
        reducer: (_prev, next) => next,
        default: () => "chat",   // 默认走 chat
    }),
})

两个字段:query(用户问题)、route(路由决定)。默认 route: "chat"

路由器:看门人

const router = (state) => {
    const isMath = /[+\-*]/.test(state.query);  // 检测是否含 + - * 
    return { route: isMath ? "math" : "chat" }
}

router 节点用正则检测 query 里有没有 +-*——有就是数学问题,否则是闲聊。

console.log("result", await graph.invoke({query: "你好"}))  // 走 chat
console.log("result", await graph.invoke({query: "1+2"}))   // 走 math

两个处理节点

const mathNode = (state) => {
    try {
        return { answer: String(eval(state.query)) }   // 计算表达式
    } catch {
        return { answer: "表达式无法计算" }
    }
}

const chatNode = (state) => ({ answer: `你说的是:${state.query}` })
  • math 节点:eval(state.query) 计算表达式——"1+2" → "3"
  • chat 节点:原样复述——"你说的是:你好"

readme 专门强调了一句:

"eval() 会把传入的字符串当作 JS 代码来执行,返回代码执行结果。"

test.mjs 也验证了这一点:

let str = "1+2";
console.log(eval(str));  // 3

eval 是把字符串当代码执行——"1+2" 被当作 JS 表达式算出 3。注意课堂代码用 try/catch 兜底——因为 eval 遇到不合法表达式(比如 "1+")会抛异常,捕获后返回"表达式无法计算"。

条件边:路由分叉的关键

const graph = new StateGraph(StateAnnotation)
    .addNode("router", router)
    .addNode("math", mathNode)
    .addNode("chat", chatNode)
    .addEdge(START, "router")   // 走向固定:先进 router
    .addConditionalEdges("router", (state) => state.route, {
        math: "math",   // route === "math" → 走 math 节点
        chat: "chat",   // route === "chat" → 走 chat 节点
    })
    .addEdge("math", END)
    .addEdge("chat", END)
    .compile()

核心是 addConditionalEdges——三个参数:

参数含义本例
起始节点从哪开始判断"router"
判断函数根据 state 决定走哪条路(state) => state.route
路由表返回值 → 目标节点映射{ math: "math", chat: "chat" }

流程变成:

           ┌──────────────┐
           ▼              │
START → router           │
           │              │
      state.route?        │
     ┌────┴────┐          │
     ▼         ▼          │
   math      chat         │
     │         │          │
     ▼         ▼          │
    END       END ────────┘

地铁到这里出现了换乘站——同一个入口(router),根据目的地(route)分向不同线路。


六、为什么要从"流水线"升级到"地铁网"

回到开头的问题——LangGraph 到底比 LangChain 强在哪?

一条流水线只能做一件事:A → B → C → D。它假设流程是确定的、线性的。

但真实的 Agent 流程充满了不确定性:

真实场景需要的编排能力LangChain 能吗
判断问题是数学还是闲聊条件分支勉强(写死在代码里)
调用工具失败要重试循环不能
转账前要等用户确认暂停 / 中断不能
记住上次聊到哪状态持久化不能

LangGraph 的 StateGraph 把这些都变成了一等公民——状态(Annotation)、节点(函数)、边(addEdge)、条件边(addConditionalEdges)、还有下期要讲的循环、检查点、中断。

网状不是炫技,是复杂流程的刚需。

readme 一句话总结了两代框架的关系:

"工作节点 + 组织方式(api)。简单 Agent -> 复杂多 Agent 协作。"

LangChain 负责简单 Agent(一条流水线),LangGraph 负责复杂多 Agent 协作(一张地铁网)。两者共享底层 LLM 基础设施,只是"组织方式"升级了。


PS:下期我们继续坐地铁——遇到坐过站怎么办(循环重试)、出站忘带卡怎么办(状态持久化)、过闸机要人工开箱检查怎么办(中断确认)。LangGraph 的网状世界,才刚刚铺开。