三国杀武将觉醒马超怎么样 三国杀武将觉醒手游安卓手机版马超角色详解
2026-07-29 3433719
2026-07-29 0
半年前我用 Java 从零实现了一个 ReAct Agent,接了 Kimi 和 DeepSeek 双模型,做了 25 个业务 Tool,跑在 Spring Boot 微服务里。最近在补 Python AI 生态,把 LangGraph 认真看了一遍。
看完后的直观感受是:并非 LangGraph 更优秀,而是它将你原本写在 while 循环中的内容全部显式呈现出来。
两种实现方式都足以让 Agent 先运行起来;但需求一旦包含任务中断、人工干预和状态持久化,真正的痛点才会由 LangGraph 处理。这是本文的核心判断,也是我以 Agent 应用 工程师身份形成的真实认识,并非一篇教程。
先介绍我自行编写的实现,方便后续进行比较。
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。
非流式实现逻辑清楚,但会让用户产生较强的等待感。流式版 StreamingAgentServiceImpl 调整为 SSE 后,核心在于 SseEmitter + 事件分类:
session_start → text(逐字) → tool_call(running) → tool_result → tool_call(done/❌) → done
用 ConcurrentHashMap 管理连接,前端发停止请求时 remove 掉 emitter,下一轮循环检测到连接不存在就退出——这是自研实现"中断"的方式,后面会对比 LangGraph 的中断。
左边展示自研 Java 实现:while(true) 右侧的 LangGraph 把控制过程组织成显式有向图(StateGraph):函数构成各个节点,条件附着于边,TypedDict 承载状态,因此状态天然能够序列化。另一侧则把状态留在内存,以隐式循环运行,并交由 Resilience4j 处理熔断,代码中的整体控制流呈线性。
两种方案都可以跑通 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());}
步骤二:按长度截断
即便消息数量合规,传给 LLM 的上下文预算仍可能因总字符数超过 8000 而超限。处理方式是按时间从旧到新删除消息,逐条进行,直至总长度符合限制。
while (totalChars(messages) > MAX_CHARS && messages.size() > 1) {messages.remove(0); // ArrayList remove(0) 是 O(n),消息量大时可改用 LinkedList}
步骤三:孤立修复(最关键,也最容易漏)
完成前两步截断后,可能出现下面这种情况:tool_result 消息仍被保留,但与它对应的 tool_call 截断消息之后,返回结果会有差异:DeepSeek 给出乱序回复,Kimi 则直接报 400。
修复方式是遍历消息列表,遇到 tool_result 时,检查前面有没有匹配的 tool_call id;若不存在便直接删除,同时还要处理空 content 的 assistant 部分模型会因 assistant 消息的 content 为空而报错——说的就是这种消息。
// 收集所有 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_resultmessages.removeIf(m ->"tool".equals(m.getRole()) && !toolCallIds.contains(m.getToolCallId()));
第三步最容易踩坑,也最容易受到忽略。上线前的测试没有发现问题,直到真实用户使用一周后反馈"偶尔返回 400",我们才定位到它。根本原因在于长会话伴随密集工具调用时,截断会显著增加孤立 tool_result 出现的概率。
以 Kimi 为主力、DeepSeek 为备用,看似简单,实际包含几个需要注意的细节:
非流式降级相对直接:主模型抛出异常后切换到备用模型,并同步发送钉钉报警。
流式降级则更复杂:HTTP 流式回包一旦启动,回调已经进入 onData,try-catch 无法捕获,因此必须在 onError 回调内部判断 hasData 标志位:
hasData = false→ 无感切换 DeepSeek 的前提是(任何数据都还没有收到)hasData = true(数据已经流出)→ 无法撤回,只能透传错误LangGraph 无法替你处理这个细节,因为框架层并不知道你具体使用了哪家模型。
介绍完自研实现中实际遇到的坑,再回头观察 LangGraph,就更容易理解它为何采用这样的设计。
LangGraph 的核心抽象是 StateGraph:
from langgraph.graph import StateGraph, ENDfrom typing import TypedDictclass AgentState(TypedDict):messages: listtool_calls: listgraph = 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) 里面的逻辑画成了一张图(即文章开头的图一)。功能上等价,但有两个重要差别:
每执行一步,TypedDict 承载的内容都会更新;这正是 LangGraph 的 State,意味着:
我的 Caffeine Cache 其保存粒度并非"每一步的中间状态",而是"会话";被序列化存储的只是完整消息列表。
LangGraph 的 interrupt_before / interrupt_after 能够在节点执行之前或之后暂停,取得外部输入后再继续运行。
graph.compile(interrupt_before=["tool_exec"])
在"执行写操作前由人确认"的场景中,这项能力非常实用。
当用户不具备写操作权限时,我的实现会直接报错返回,因为权限检查放在 Tool execute 方法内部。LangGraph 则把中断安排在图执行层面;暂停以后,State 可以先被修改再恢复执行,例如让用户调整工具参数并重新确认。
| 维度 | 自研 Java | LangGraph |
|---|---|---|
| 循环控制 | while(true) 手工编写,完全可控 | StateGraph 显式图,支持可视化 |
| 状态粒度 | 会话级(完整消息列表) | 每个节点后都能 checkpoint,粒度落实到步骤级 |
| 中断 / 恢复 | 靠 emitter remove 间接实现 | interrupt_before/after 原生支持 |
| 消息截断 | 自行实现三步逻辑(踩坑) | 没有内置;LangChain 有 trim_messages 但策略仍需要自行配置 |
| 熔断降级 | Resilience4j 提供完整支持 | 没有内置,需要自行包装 |
| 流式 | 自定义事件协议 + SseEmitter | stream_mode 内置多种模式 |
| Tool 注册 | Spring List 自动装配 | @tool 装饰器 + 通过列表传入 |
| 调试可见性 | 自行编写日志 + SSE 事件 | verbose=True + LangGraph Studio |
| 多租户 | 手动传递交由 TenantContextHolder 完成 | 没有相关概念,需要自行处理 |
| 部署 | 现有体系可天然接入 Spring Boot 微服务 | 额外集成是 FastAPI / 独立服务的必要环节 |
适合选择自研 Java 的情况:
值得迁移到 LangGraph 的情况:
现实结论是: 对我们的项目而言,自研 Java 当前已经够用。不过,在使用 LangGraph 复现核心功能时,我得到的最有价值经验是把 Agent 控制流画出来。即使最终不采用 LangGraph,这种"图思维"也促使我重新检查自研代码,并发现了几个隐藏的状态管理 bug。
工具只是实现手段,真正有价值的是清晰的思维模型。
自研和接入框架之间,你会怎样为企业级 AI Agent 作选择?欢迎把自己的权衡逻辑留在评论区。