LangGraph4j 落地(二)— ReAct 多步工具与 trace 扩展

上一篇LangGraph4j 落地(一)--- 图编排骨架与线性管道等价迁移

写在前面

真实客服里常见链路:

text 复制代码
用户:订单 123 一直没到,我要投诉
  → order_query(123)
  → 判断物流异常
  → ticket_create(投诉工单)
  → 生成回复

原来的 AgentRouter + 单次 executeNamed 无法表达这条链。现阶段引入 ReAct 子循环 :LLM 规划 → 执行工具 → 再规划,直到 respond 或达到循环上限。

你将学到什么

  • tool_plan / tool_execute 条件边如何闭合
  • ToolCallRecord 与多步 prompt 渲染
  • graph 模式下 Router 如何「降级」为意图标签
  • 前端 toolCalls 时间线展示
  • max-tool-loops 防死循环

1. 从单次工具到 ReAct 循环

1.1 线性时代的限制

#mermaid-svg-aS5SelwIMjJHRTrL{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aS5SelwIMjJHRTrL .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aS5SelwIMjJHRTrL .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aS5SelwIMjJHRTrL .error-icon{fill:#552222;}#mermaid-svg-aS5SelwIMjJHRTrL .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aS5SelwIMjJHRTrL .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aS5SelwIMjJHRTrL .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aS5SelwIMjJHRTrL .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aS5SelwIMjJHRTrL .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aS5SelwIMjJHRTrL .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aS5SelwIMjJHRTrL .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aS5SelwIMjJHRTrL .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aS5SelwIMjJHRTrL .marker.cross{stroke:#333333;}#mermaid-svg-aS5SelwIMjJHRTrL svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aS5SelwIMjJHRTrL p{margin:0;}#mermaid-svg-aS5SelwIMjJHRTrL .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-aS5SelwIMjJHRTrL .cluster-label text{fill:#333;}#mermaid-svg-aS5SelwIMjJHRTrL .cluster-label span{color:#333;}#mermaid-svg-aS5SelwIMjJHRTrL .cluster-label span p{background-color:transparent;}#mermaid-svg-aS5SelwIMjJHRTrL .label text,#mermaid-svg-aS5SelwIMjJHRTrL span{fill:#333;color:#333;}#mermaid-svg-aS5SelwIMjJHRTrL .node rect,#mermaid-svg-aS5SelwIMjJHRTrL .node circle,#mermaid-svg-aS5SelwIMjJHRTrL .node ellipse,#mermaid-svg-aS5SelwIMjJHRTrL .node polygon,#mermaid-svg-aS5SelwIMjJHRTrL .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aS5SelwIMjJHRTrL .rough-node .label text,#mermaid-svg-aS5SelwIMjJHRTrL .node .label text,#mermaid-svg-aS5SelwIMjJHRTrL .image-shape .label,#mermaid-svg-aS5SelwIMjJHRTrL .icon-shape .label{text-anchor:middle;}#mermaid-svg-aS5SelwIMjJHRTrL .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aS5SelwIMjJHRTrL .rough-node .label,#mermaid-svg-aS5SelwIMjJHRTrL .node .label,#mermaid-svg-aS5SelwIMjJHRTrL .image-shape .label,#mermaid-svg-aS5SelwIMjJHRTrL .icon-shape .label{text-align:center;}#mermaid-svg-aS5SelwIMjJHRTrL .node.clickable{cursor:pointer;}#mermaid-svg-aS5SelwIMjJHRTrL .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aS5SelwIMjJHRTrL .arrowheadPath{fill:#333333;}#mermaid-svg-aS5SelwIMjJHRTrL .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aS5SelwIMjJHRTrL .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aS5SelwIMjJHRTrL .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aS5SelwIMjJHRTrL .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aS5SelwIMjJHRTrL .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aS5SelwIMjJHRTrL .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aS5SelwIMjJHRTrL .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aS5SelwIMjJHRTrL .cluster text{fill:#333;}#mermaid-svg-aS5SelwIMjJHRTrL .cluster span{color:#333;}#mermaid-svg-aS5SelwIMjJHRTrL div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aS5SelwIMjJHRTrL .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aS5SelwIMjJHRTrL rect.text{fill:none;stroke-width:0;}#mermaid-svg-aS5SelwIMjJHRTrL .icon-shape,#mermaid-svg-aS5SelwIMjJHRTrL .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aS5SelwIMjJHRTrL .icon-shape p,#mermaid-svg-aS5SelwIMjJHRTrL .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aS5SelwIMjJHRTrL .icon-shape .label rect,#mermaid-svg-aS5SelwIMjJHRTrL .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aS5SelwIMjJHRTrL .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aS5SelwIMjJHRTrL .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aS5SelwIMjJHRTrL :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 一次决策
AgentRouter
executeNamed
PromptComposer
主 LLM

  • Router LLM + 主 LLM = 至少 2 次调用
  • toolName 只能选一个
  • trace 里只有一个 toolResult 字符串

1.2 Phase 2 目标结构

#mermaid-svg-VStbmpy2GedYjCWj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-VStbmpy2GedYjCWj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-VStbmpy2GedYjCWj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-VStbmpy2GedYjCWj .error-icon{fill:#552222;}#mermaid-svg-VStbmpy2GedYjCWj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-VStbmpy2GedYjCWj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-VStbmpy2GedYjCWj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-VStbmpy2GedYjCWj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-VStbmpy2GedYjCWj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-VStbmpy2GedYjCWj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-VStbmpy2GedYjCWj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-VStbmpy2GedYjCWj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-VStbmpy2GedYjCWj .marker.cross{stroke:#333333;}#mermaid-svg-VStbmpy2GedYjCWj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-VStbmpy2GedYjCWj p{margin:0;}#mermaid-svg-VStbmpy2GedYjCWj defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-VStbmpy2GedYjCWj g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-VStbmpy2GedYjCWj g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-VStbmpy2GedYjCWj g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-VStbmpy2GedYjCWj g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-VStbmpy2GedYjCWj g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-VStbmpy2GedYjCWj .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-VStbmpy2GedYjCWj .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-VStbmpy2GedYjCWj .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-VStbmpy2GedYjCWj .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-VStbmpy2GedYjCWj .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-VStbmpy2GedYjCWj .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-VStbmpy2GedYjCWj .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-VStbmpy2GedYjCWj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-VStbmpy2GedYjCWj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-VStbmpy2GedYjCWj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-VStbmpy2GedYjCWj .edgeLabel .label text{fill:#333;}#mermaid-svg-VStbmpy2GedYjCWj .label div .edgeLabel{color:#333;}#mermaid-svg-VStbmpy2GedYjCWj .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-VStbmpy2GedYjCWj .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-VStbmpy2GedYjCWj .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-VStbmpy2GedYjCWj .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-VStbmpy2GedYjCWj .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-VStbmpy2GedYjCWj .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VStbmpy2GedYjCWj .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VStbmpy2GedYjCWj #statediagram-barbEnd{fill:#333333;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-VStbmpy2GedYjCWj .cluster-label,#mermaid-svg-VStbmpy2GedYjCWj .nodeLabel{color:#131300;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-VStbmpy2GedYjCWj .note-edge{stroke-dasharray:5;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-note text{fill:black;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram-note .nodeLabel{color:black;}#mermaid-svg-VStbmpy2GedYjCWj .statediagram .edgeLabel{color:red;}#mermaid-svg-VStbmpy2GedYjCWj #dependencyStart,#mermaid-svg-VStbmpy2GedYjCWj #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-VStbmpy2GedYjCWj .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-VStbmpy2GedYjCWj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} call_tool
needMore
done
respond
tool_plan
tool_execute
build_prompt

配置开关:aics.orchestration.graph.react-enabled: true(默认已开)。

2. 图状态扩展

ChatGraphState 新增:

字段 含义
toolCalls List<ToolCallRecord> 多步历史
toolLoopCount 当前循环次数
aggregatedToolResult 合并后的工具输出
nextToolAction call_tool / respond
nextToolName / nextToolInput 规划结果

ToolCallRecord 定义在 ai-common,供编排 trace 与前端共用:

java 复制代码
public record ToolCallRecord(
    String name, String input, String output, Instant timestamp
) implements Serializable { }

3. ToolPlanNode:LLM 输出结构化 JSON

GraphNodes.toolPlan 将以下上下文发给 LLM:

  • 会话 history + 当前 message
  • 已通过 ToolCatalog 列举的 可用工具清单
  • 已有 toolCalls 历史

期望 JSON:

json 复制代码
{"action":"call_tool","toolName":"order_query","toolInput":"订单123"}

或:

json 复制代码
{"action":"respond","toolName":"","toolInput":""}

终止条件:

  1. action == respond
  2. toolLoopCount >= max-tool-loops(默认 5,配置项 aics.orchestration.graph.max-tool-loops

解析失败时 安全回退respond,避免图卡死。

4. Router 在 graph 模式下的角色变化

react-enabled: true 时:

  • 不再 为每次对话单独走 LlmAgentRouter 的 JSON 决策(避免双 LLM)
  • route 节点改为 启发式意图标签consult / order / complaint / escalate
  • 意图供 Phase 4 子图路由与 trace 展示

agent-router-llm-enabled 在 graph + react 场景下让位于 tool_plan 内的规划 LLM

5. Prompt 与 trace 契约扩展

5.1 PromptComposer 多步 overload

PromptComposer 新增带 List<ToolCallRecord>build(...) 默认方法;DefaultPromptComposer 将多步结果渲染为编号列表:

text 复制代码
### 工具结果
[1] order_query
{...json...}

[2] ticket_create
{...json...}

旧四参数 build 保留,线性管道与单步场景无感。

5.2 ChatTurnTraceResult

ChatTurnTraceResult 扩展字段:

  • toolCalls
  • executedNodes
  • graphExecutionId
  • durationMs
  • pendingApproval / approvalToken(Phase 4 使用)

ChatTraceResponse 同步扩展 toolCalls 视图。

5.3 前端 ToolResultPanel

ToolResultPanel 优先渲染 toolCalls 列表;无多步数据时回退展示 toolResult 字符串。

6. 动手验证

6.1 配置确认

yaml 复制代码
aics:
  orchestration:
    engine: graph
    graph:
      react-enabled: true
      max-tool-loops: 5

6.2 触发多步工具(示例话术)

bash 复制代码
curl -s -X POST http://localhost:8081/api/chat \
  -H "Content-Type: application/json" \
  -d '{"sessionId":"react-demo","message":"订单ORD-123一直没收到,帮我查一下并创建投诉工单"}' \
  | jq '{toolCalls, executedNodes, toolResult}'

在 demo 模型与 mock 工具下,期望 executedNodestool_plantool_execute 等;toolCalls 数组长度 ≥ 1(具体取决于 LLM 规划)。

6.3 循环上限

max-tool-loops 设为 1,观察日志出现 tool loop limit reached,图强制进入 build_prompt

7. 与第 16 篇 Tools 设计的关系

维度 第 16 篇(Router 分离) ReAct
工具次数 1 次 多次循环
决策方 独立 Router LLM tool_plan LLM
审计 agentDecision + toolResult + toolCalls[] 时间线
适用 简单演示 工单/订单链式场景

ToolRegistry 未改:新工具仍是 Spring Bean 自动注册。

FAQ

Q:ReAct 会不会无限调工具?

A:max-tool-loops + max-steps(图递归上限)双重保护。

Q:linear 引擎支持多步吗?

A:不支持。多步是图编排能力;linear 保留单步基线。

Q:tool_plan 的 JSON 不稳怎么办?

A:解析失败回退 respond;生产可换更强模型或加 JSON schema 约束(后续可增强)。

相关推荐
大家的林语冰1 小时前
Rust 再次打败 Go,Astro 7 首发,什么叫“万物皆可锈化“!
前端·javascript·性能优化
lichenyang4532 小时前
H5 如何调用 ArkTS:JavaScriptProxy、requestId 和 Promise 回调
前端
lichenyang4532 小时前
我在 HarmonyOS 上写了一个轻量级小程序运行时:从 Web 容器到 JSBridge
前端
雪隐2 小时前
用Flutter做背单词APP-02我画了设计稿,然后让AI帮我设计了数据库(顺便聊了会天)
前端·人工智能·后端
lichenyang4532 小时前
不要把 JSBridge 写成 if else:Dispatcher、Registry 和 Biz/Imp 分层
前端
前端甜糖2 小时前
前端如何实现在线预览office常见文件功能?
前端·vue.js
北鸟南游2 小时前
学习-claude code 工程化实战的笔记
前端·ai编程·claude
Kyrie6782 小时前
TypeScript 7.0 正式发布
前端
lichenyang4532 小时前
JSBridge 不是发消息就完了:runJavaScript、Timeout 和 Callback Lost
前端