Skip to content

LangGraph 入门 + MCP 试水:从手写循环到图编排的一周

一个 8 年 Java 后端,把 Week 1 手写的 ReAct while 循环画成 LangGraph 的图,再用 MCP 协议把工具层从 Java 拆到 Python 的一周复盘。

核心就一句话:Agent 不是 DAG,Agent 是循环。LangGraph 把 while 循环变成了图,MCP 把工具变成了协议。


缘起:Week 1 的 while 循环到底丑在哪

Week 1 手写 ReAct 的时候,核心结构长这样:

java
while (true) {
    String reply = callLlm(messages);
    if (reply.contains("Final Answer")) break;   // if 判断
    String obs = callTool(parse(reply));          // 工具调用
    messages.add(obs);                            // 追加历史
}

能跑,但丑在三个地方:

  • 控制流和业务焊死:想加个"调完工具先人工确认"的分支?改 while 循环体。
  • 中断即丢失:进程崩了,messages 在内存里,一切重来。
  • 图看不见:流程长什么样只能脑补,新人接手先读 200 行循环。

Week 3 的主题就是把这三个丑点挨个解决:LangGraph 管"流程",Checkpoint 管"中断",MCP 管"工具在哪"。


一、StateGraph:把 while 和 if 画成图

LangGraph 的核心抽象只有四个:State / Node / Edge / ConditionalEdge

第一天写的最简 ReAct 图:

java
var workflow = new StateGraph<>(MyAgentState.SCHEMA, MyAgentState::new)
    .addNode("llm", node_async(llmNode))
    .addNode("tool", node_async(toolNode))
    .addEdge(START, "llm")
    .addConditionalEdges("llm",
        edge_async(state -> state.nextAction()),      // ← Week1 的 if
        Map.of("tool", "tool", "end", END))
    .addEdge("tool", "llm");                          // ← Week1 的 while 回环

对照关系一目了然:

手写 ReAct(Week 1)LangGraph 图(Week 3)
while(true){} 循环体回环边 tool → llm
if(final) break条件边命中 "end" → END
callTool()tool 节点
history.add(msg)SCHEMA 里的 Channels.appender

compile() 之后还能直接吐 PlantUML,那个六边形 condition1 就是被物化成图节点的 if 判断。控制流从代码里的关键字,变成了可声明、可持久化、可视化的数据结构——这是本周第一个认知升级。

State 不是 HashMap,是带 reducer 的状态机

第二个坑/收获:State 的每个字段要声明合并策略(reducer):

java
public static final Map<String, Channel<?>> SCHEMA = Map.of(
    "messages", Channels.appender(ArrayList::new),   // 追加:消息历史
    "intermediateSteps", Channels.appender(ArrayList::new)
    // nextAction 不声明 → 默认覆盖,只留最新决策
);

Node 只 return Map.of("key", value) 增量片段,引擎按字段的 reducer 合并。消息要追加、标志位要覆盖,同一个 State 里两种策略混用——想通这个,Week 1 的 history.add() 在框架里对应什么就彻底清楚了。


二、Checkpoint:Agent 的虚拟机快照

第二天的实验是中断恢复:第一次跑在迭代第 3 步 break 模拟崩溃,第二次用同一个 threadId 重启,结果:

第一次:llm(steps=0) → tool(step-1) → 💥 崩溃
第二次:llm(steps=1) → tool(step-2) → llm(steps=2) → END ✅

关键发现是 Checkpoint 有两层

  • 数据层:messages / intermediateSteps —— 恢复成功
  • 执行指针层:上次停在哪个节点 —— 这个 Demo 没恢复,从 START 重走,靠节点逻辑读 steps.size() 跳过已执行步骤

和 Week 2 Spring AI 的 ChatMemory 对比最直观:ChatMemory 是聊天记录.txt,Checkpoint 是虚拟机快照。前者只记住"聊过啥",后者要记住"跑到哪了"。Agent 框架和普通框架的本质区别就在这——Agent 是长时运行任务,必须支持半路续跑。


三、微调:最后一张牌,不是第一张

周四没写代码,纯读 + 建判断力。岗位技能云里"LLM 微调(SFT/RL)"连续 3/4 天出现,得能说清楚边界。

一张图记住微调的位置:

预训练基座模型
   ├─ 不微调 → Prompt / RAG / Tool(改输入,不碰模型)
   └─ 微调 → SFT → RLHF/DPO(改模型本身)

对我(8 年 Java 转 Agent)的判断标准:

  • 80% 场景不需要微调:Agent 工程的核心是编排——RAG、Tool、Memory、Graph,全在外面
  • 该微调的 20%:垂直领域术语注入、输出格式 Prompt 锁不死、小模型补能力
  • LoRA 的意义:冻结原模型、只训 0.1%-1% 参数,7B QLoRA 约 8GB 显存,个人游戏本也能玩

一句话总结给面试:"先调 Prompt,再调 RAG,最后才调模型。微调是静态知识,RAG 是动态知识,两者搭配而不是互替。"


四、MCP:工具层的 HTTP 协议

周五把 MCP 从"听过"变成"跑通"。最小 Server 用 Python FastMCP 十几行:

python
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("calculator")

@mcp.tool()
def add(a: float, b: float) -> float:
    """两数相加"""
    return a + b

if __name__ == "__main__":
    mcp.run(transport="stdio")

FastMCP 自动把函数签名 + docstring 转成 JSON Schema,客户端拿到的就是 Function Calling 的 functions 定义。mcp dev calc_server.py 起 Inspector,浏览器里直接调 add(3, 4) 返回 7。

MCP 和 Function Calling 的关系,本周想清楚的最值钱的一个问题:

  • Function Calling 是 API 约定:工具定义塞在一次 LLM 请求里,谁调谁提供焊死在一起
  • MCP 是 协议标准:拆成 tools/list(发现)+ tools/call(执行)两步,工具层和 LLM 层彻底解耦

代价是多一次往返,收益是跨语言、跨服务、独立部署——工具可以独立升级、独立测试、独立团队维护


五、收网:LangGraph + MCP 跨语言 Agent

周六把整条链路串起来。场景一句话:

"查一下上海的天气,然后算一下上海比北京高多少度。"

架构是 Java 编排 + Python 工具,两个进程走 MCP stdio:

Java (Spring Boot + LangGraph4j)
  ├─ ChatClient(火山引擎 LLM,内部自动 ReAct)
  └─ MCP Client ──stdio──┬─ Python weather_server(get_weather)
                         └─ Python calc_server(add/subtract)

跑出来的实际链路,LLM 自主调了 3 次工具,零人工干预:

[LLM 调用 get_weather("上海") → 30℃, 多云]
[LLM 调用 get_weather("北京") → 25℃, 晴]
[LLM 调用 subtract(30, 25) → 5]
→ 上海当前天气多云,气温30℃,比北京(晴,25℃)高5℃。

对比 Day 3 的变化最能说明问题:

Day 3Day 6
工具注册Tools.REGISTRY Java MapMCP 协议从 Python 进程拉取
工具绑定编译时,焊死在项目里运行时,Agent 不知道工具用什么语言写的
LLMFakeLlm 硬编码真实 LLM

工具层从"编译时绑定"变成"运行时发现"——这就是 Agent 工具生态的基础设施。

踩坑实录(Windows 用户必读)

  1. JDK 8 跑不起来:Spring AI 1.0 / text block 全要 JDK 17+,升了 Temurin 17
  2. Spring Boot 4.1 与 Spring AI 1.0.0 不兼容:降到 3.4.10
  3. 编码对不上:Python stdout 默认 GBK,JSON 里"北京"变乱码,Jackson 直接崩。两层修:Python 端 sys.stdout.reconfigure(encoding='utf-8') + JVM 端 -Dfile.encoding=UTF-8
  4. Maven 编译报"非法字符 '\u3002'":pom 里加 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

stdio 的编码坑是隐形税——生产环境应该换 streamable-http,用 HTTP 天然避开字节流编码问题。


总结:这一周站到了哪一层

Week 1 手写 while 循环,Week 2 框架一行调用,Week 3 把控制流画成图、把工具拆成协议。三层抽象我都站过,现在的判断是:

  • 1-2 个工具、一次性调用:手写够了,省框架依赖
  • 3+ 工具、多轮迭代、要中断恢复:上图编排
  • 工具要跨语言、独立部署:上 MCP

面试被问"你写过 Agent 吗",现在的答案是:手撕过 ReAct 的 while 循环,也用 LangGraph 画过带回环的图,最后通过 MCP 让 Java 编排调 Python 工具,LLM 自主调了 3 次工具完成任务。底层原理和上层编排,都不虚。

下一步 Week 4:RAG 进阶 + 向量检索工程化,把"会跑"往"能上线"推。


参考:LangGraph4j 1.8.20 官方文档 · modelcontextprotocol.io 规范 · 代码仓 F:\code\week3-langgraph、F:\code\week3-mcp

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。