惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

C
Cyber Attacks, Cyber Crime and Cyber Security
V
V2EX - 技术
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Schneier on Security
Schneier on Security
SecWiki News
SecWiki News
Security Latest
Security Latest
P
Privacy & Cybersecurity Law Blog
Google Online Security Blog
Google Online Security Blog
N
News | PayPal Newsroom
T
Tor Project blog
T
The Exploit Database - CXSecurity.com
MongoDB | Blog
MongoDB | Blog
Google DeepMind News
Google DeepMind News
酷 壳 – CoolShell
酷 壳 – CoolShell
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
L
LINUX DO - 最新话题
C
Cisco Blogs
G
GRAHAM CLULEY
C
Comments on: Blog
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
量子位
S
Secure Thoughts
美团技术团队
E
Exploit-DB.com RSS Feed
PCI Perspectives
PCI Perspectives
Cisco Talos Blog
Cisco Talos Blog
J
Java Code Geeks
The Cloudflare Blog
I
Intezer
D
Darknet – Hacking Tools, Hacker News & Cyber Security
V
Vulnerabilities – Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
K
Kaspersky official blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Stack Overflow Blog
Stack Overflow Blog
腾讯CDC
H
Hacker News: Front Page
小众软件
小众软件
博客园 - 【当耐特】
GbyAI
GbyAI
I
InfoQ
M
Microsoft Research Blog - Microsoft Research
爱范儿
爱范儿
雷峰网
雷峰网
罗磊的独立博客
Microsoft Azure Blog
Microsoft Azure Blog
WordPress大学
WordPress大学

掘金

从“连接不上”到“交易成功”:我用 @solana/web3.js 在 React 中搞定 Solana 钱包交互的全过程 juejin.cn 海量人群包存储优化:基于 RoaringBitmap 交换格式与 Redis 分片 Bitmap 的实践 juejin.cn juejin.cn 鸿蒙项目首页启动链路与 ArkUI 架构学习总结 如何手写一个 AI Agent 工具调用循环(Tool Loop) Tauri 应用首次上架 App Store 被驳回了 3 次(iOS)和 12 轮(macOS)的经历 juejin.cn Flutter 桌面小组件开发 现代多模态大模型的核心基础:Unified Self-Attention juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn Transformer 原论文怎么训出来的:8 张 P100、12 小时、warmup 4000 步 Hermes 升级后,我的 Telegram 附件突然发不出来了 Transformer 中的前馈网络:那个看似平平无奇的两层 MLP,其实是「记忆」所在 AI Coding开始进入第四个时代,我还没上车呢! 【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (1)---基础 juejin.cn Vibe Coding 全栈实战:章鱼哥解题 01|搭好产品底座与登录链路 juejin.cn 我让 AI 加了一个开关,结果代码走了原本不该走的分支 Manim物理模拟:别自己写欧拉了! 我也该升级了,陪伴了我7年的博客 juejin.cn juejin.cn MCP 高德地图实战:当 AI 学会使用工具,一个协议如何重塑大模型的行动边界 用魔法打败魔法:我让AI替我去面试前端岗,AI面试官给我打了92分,还发了offer juejin.cn juejin.cn juejin.cn juejin.cn Android Input Spy Window Claude Code CLI 命令大全:60 个原生命令一次讲清 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn 关于一个新手小白靠claude帮助下的全栈留言板项目开发 juejin.cn juejin.cn juejin.cn 从本地开发到生产部署:用 Docker Compose 跑通 NestJS、MySQL 与 Milvus AI应用开发七:可以替代 RAG 的技术 juejin.cn 小书匠:一款本地优先、去中心化的全能笔记软件 juejin.cn juejin.cn juejin.cn Shadow实战接入与生产落地:从零搭建到稳定运行 Shadow Transform:编译期的魔法——字节码替换实战 juejin.cn juejin.cn Hermes Agent:一个真正“会成长”的开源 AI Agent,正在改变 AI 自动化玩法 juejin.cn juejin.cn juejin.cn 残差连接:为什么深层网络必须留一条直路 juejin.cn FastAPI 从入门到实战:3 分钟构建高性能异步 API juejin.cn juejin.cn CryptoJS:数据安全的JavaScript加密利器 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn ArkClaw AI 盯盘管家 —— 从手动口令到自动推送,4 套预置定时任务模版一键启用 juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn juejin.cn “杀!杀!杀!”、“我最讨厌事后道歉”——骂“杀哥”之前,谁还没当过情绪崩溃的人
Java 自研 ReAct Agent 半年后,我用 LangGraph 验证了这些设计取舍
世rui睿 · 2026-05-14 · via 掘金

Java 自研 ReAct Agent 半年后,我用 LangGraph 验证了这些设计取舍

本文基于真实生产项目:智能售货机运营 Agent,25 个 Tool,双模型网关,RAG,钉钉集成,已上线运营。


背景

半年前我用 Java 从零实现了一个 ReAct Agent,接了 Kimi 和 DeepSeek 双模型,做了 25 个业务 Tool,跑在 Spring Boot 微服务里。最近在补 Python AI 生态,把 LangGraph 认真看了一遍。

看完有一种感觉:不是 LangGraph 更好,是它把你写在 while 循环里的东西都显式化了。

这篇文章不是教程,是我作为一个 Agent 应用 工程师对两种实现方式的真实理解。核心观点是:如果你只是想让 Agent 跑起来,两种方式都够用;如果你需要状态持久化、任务中断、人工干预,那 LangGraph 解决的是你真正的痛点。


一、自研实现长什么样

先讲我自己写的东西,方便后面做对比。

核心结构

AgentServiceImpl.java
├── 检查用户配额(Redis)
├── 加载会话历史(Caffeine Cache)
└── while (true):
    ├── MessageHistoryManager.truncate() ← 三步截断
    ├── ModelGateway.chat(messages, tools) ← 双模型
    ├── 解析 LLM 响应
    │   ├── 纯文本 → 返回,退出循环
    │   └── tool_calls → 校验权限 → 执行 Tool → 结果压缩 → 加入历史
    └── 继续下一轮

Tool 注册用 Spring 自动装配,@PostConstruct 扫描所有 AgentTool Bean 建索引,每次循环把工具描述打包成 JSON Schema 发给 LLM。

流式版本(SSE)

非流式逻辑清晰,但用户等待感差。流式版 StreamingAgentServiceImpl 改成 SSE,核心是 SseEmitter + 事件分类:

session_start → text(逐字) → tool_call(running) → tool_result → tool_call(done/❌) → done

ConcurrentHashMap<sessionId, SseEmitter> 管理连接,前端发停止请求时 remove 掉 emitter,下一轮循环检测到连接不存在就退出——这是自研实现"中断"的方式,后面会对比 LangGraph 的中断。

图一:两种实现的结构对比

ReAct循环对比图.png

左侧是自研 Java 实现:while(true) 隐式循环,状态存在内存里,Resilience4j 管熔断,整个控制流在代码里是线性的。右侧是 LangGraph:显式有向图(StateGraph),每个节点是一个函数,边带条件,状态是 TypedDict,天然可序列化。

两种方式都能跑通 ReAct 逻辑。区别在于对"循环状态"的态度:自研是隐式的,LangGraph 是显式的。


二、最难搞的部分:消息历史管理

如果你用过 OpenAI / Kimi 的 API,一定遇到过这个报错:

400 Bad Request: messages[3].content is required

或者超长上下文导致的费用暴涨。这是每个自研 Agent 必须面对的问题。

三步截断策略

我的 MessageHistoryManager 做了三步截断,顺序不能乱:

步骤一:数量截断

保留最新 30 条消息。超出的从最旧开始扔。

if (messages.size() > MAX_COUNT) {
    messages = messages.subList(messages.size() - MAX_COUNT, messages.size());
}

步骤二:长度截断

数量没超,但总字符数可能超过 8000(传给 LLM 的上下文预算)。从最旧的消息开始逐条删,直到总长度达标。

while (totalChars(messages) > MAX_CHARS && messages.size() > 1) {
    messages.remove(0); // ArrayList remove(0) 是 O(n),消息量大时可改用 LinkedList
}

步骤三:孤立修复(最关键,也最容易漏)

前两步截断之后,可能产生一种情况:tool_result 消息还在,但它对应的 tool_call 消息被截掉了。Kimi 会直接返回 400,DeepSeek 会返回乱序回复。

修复逻辑:遍历消息列表,遇到 tool_result 时检查前面是否有匹配的 tool_call id,没有就直接删除。同时处理空 content 的 assistant 消息——某些模型对空 content 的 assistant 消息会报错。

// 收集所有 assistant 消息里发出的 tool_call id(tool_call 在 assistant 消息的 tool_calls 数组里)
Set<String> toolCallIds = messages.stream()
    .filter(m -> "assistant".equals(m.getRole()))
    .flatMap(m -> m.getToolCalls().stream())
    .map(ToolCall::getId)
    .collect(Collectors.toSet());

// 删除找不到对应 tool_call 的孤立 tool_result
messages.removeIf(m ->
    "tool".equals(m.getRole()) && !toolCallIds.contains(m.getToolCallId())
);

图二:三步截断示意图

消息历史三步截断示意图.png

第三步的坑最容易踩到,但也最容易被忽略。我们上线前测试没发现,是真实用户用了一周后反馈"偶尔返回 400"才查出来的。根本原因是长会话 + 密集工具调用时,截断后孤立 tool_result 的概率大幅上升。


三、双模型网关:比你想的更复杂

用 Kimi 作主力,DeepSeek 做备用,看起来很简单,实际有几个细节:

非流式降级很直接:主模型抛异常就切备用,同时钉钉报警。

流式降级复杂:HTTP 流式回包一旦开始,回调已经在 onData 里了,try-catch 捕不到——你得在 onError 回调里判断 hasData 标志位:

  • hasData = false(还没收到任何数据)→ 可以无感切换 DeepSeek
  • hasData = true(已经有数据流出去了)→ 没有办法撤回,只能透传错误

这个细节 LangGraph 不帮你解决,框架层面不感知你用哪家模型。


四、再看 LangGraph:它解决了什么

好,说完了自研实现里真实踩过的坑,现在回过头来看 LangGraph,就能理解它为什么那样设计了。

LangGraph 的核心抽象是 StateGraph

from langgraph.graph import StateGraph, END
from typing import TypedDict

class AgentState(TypedDict):
    messages: list
    tool_calls: list

graph = StateGraph(AgentState)
graph.add_node("llm_call", call_llm)
graph.add_node("tool_exec", execute_tools)
graph.add_conditional_edges(
    "llm_call",
    lambda s: "continue" if s["tool_calls"] else "end",
    {"continue": "tool_exec", "end": END}
)
graph.add_edge("tool_exec", "llm_call")
graph.set_entry_point("llm_call")
app = graph.compile()

这段代码 while(true) 里面的逻辑画成了一张图(即文章开头的图一)。功能上等价,但有两个重要差别:

差别一:状态是一等公民

LangGraph 的 State 是一个 TypedDict,每一步都在更新它。这意味着:

  • 持久化:用 Checkpointer(SQLite/Redis)存储 State,崩溃后从断点恢复
  • 回放:任意给定 State,重新跑一遍
  • 时间旅行:LangGraph Studio 里可以回到某一步重新执行

我的 Caffeine Cache 只是把整个消息列表序列化存了,粒度是"会话",不是"每一步的中间状态"。

差别二:中断(Human-in-the-loop)

LangGraph 的 interrupt_before / interrupt_after 可以在节点执行前后暂停,等待外部输入再继续。

graph.compile(interrupt_before=["tool_exec"])

这个在需要"执行写操作前让人确认"的场景非常有用。

我的实现里,写操作权限是在 Tool execute 方法里校验的,用户如果没有权限就报错返回。但 LangGraph 的中断是在图执行层面,暂停期间可以修改 State 再继续——比如用户可以改工具参数再确认。


五、横向对比

维度自研 JavaLangGraph
循环控制while(true) 手写,完全可控StateGraph 显式图,可视化
状态粒度会话级(消息列表整体)步骤级(每个节点后都可 checkpoint)
中断 / 恢复靠 emitter remove 间接实现interrupt_before/after 原生支持
消息截断自己写三步逻辑(踩坑)无内置;LangChain 有 trim_messages 但仍需自己配置策略
熔断降级Resilience4j 完整支持无内置,需自己包装
流式SseEmitter + 自定义事件协议stream_mode 内置多种模式
Tool 注册Spring List<AgentTool> 自动装配@tool 装饰器 + 列表传入
调试可见性自写日志 + SSE 事件verbose=True + LangGraph Studio
多租户TenantContextHolder 手动传递无概念,需自己处理
部署Spring Boot 微服务,天然融入现有体系FastAPI / 独立服务,需额外集成

六、我的判断

什么时候选自研 Java:

  • 业务在 Spring Cloud 生态里,需要和现有 Feign/MyBatis/Redis 无缝集成
  • 有熔断、多租户、权限等横切需求,Resilience4j 等工具很成熟
  • 循环逻辑不复杂,Tool 数量可控,不需要状态持久化

什么时候 LangGraph 值得迁移:

  • 需要人工干预节点(审批、确认、二次输入)
  • 需要任务中断后从断点继续(长时间任务、多步规划)
  • Agent 逻辑复杂,节点有并行分支,图结构能帮助推理

现实答案: 对我们的项目,自研 Java 现在足够用。但我在用 LangGraph 复现核心功能的过程中学到的最有价值的一点是:把 Agent 的控制流画出来——哪怕最后不用 LangGraph,这个"图思维"也让我把自研的代码重新审视了一遍,发现了几个隐藏的状态管理 bug。

工具是手段,清晰的思维模型才是真正的价值。


参考


如果你也在做企业级 AI Agent,你是选自研还是接框架?欢迎评论区聊聊你的权衡逻辑。