← 博客书房

Agent 实战:从 Dify 到 LangGraph·Agentic Loop:从裸循环到图编排

实战30 分钟

Agentic Loop:从裸循环到图编排

Agent 的心脏是一个 while 循环:Reason → Act → Observe → 再循环。从 easy-agent 的 20 行裸实现拆到 LangGraph 的 StateGraph 图编排,一次讲透。

Agent 的心脏不是模型,是循环。这一章从 20 行裸代码拆到 LangGraph 的图编排。

最简 Agentic Loop:20 行讲清楚

easy-agent Step 4 用一个 while 循环实现了 Agent 的核心——这 20 行就是所有 Agent 框架的心脏:

while (turnCount < maxTurns) {
  // 1. 把全部消息发给模型
  const result = await streamMessage({ messages, tools });

  // 2. 模型的回答放进历史
  messages.push(result.assistantMessage);

  // 3. 如果模型不想调工具了 → 任务完成,退出循环
  if (result.stopReason !== "tool_use") {
    return { reason: "completed" };
  }

  // 4. 模型想调工具 → 执行 → 结果放回历史 → 回到第 1 步
  const toolResults = await runTools(result.assistantMessage.content);
  messages.push(toolResults);
}
+ + + + + + < 模型(思考) 看完所有消息,决定下一步 + < stop_reason 是什么? 模型自己决定 + < end_turn(做完了) 完成 ✓ 返回结果 + < tool_use(要调工具) 执行工具 你的代码干活 + < 结果回填 tool_result 塞回消息数组 + ReAct = Reason(思考)→ Act(调工具)→ Observe(看结果)→ 再思考 → … 模型不是一次给出答案 · 是在循环中逐步逼近答案 · 你设的 maxTurns 就是安全网

ReAct:不是什么高深算法,就是一个模式名

你刚才看懂的那个循环,有个正式名字叫 ReAct(Reason + Act):

阶段谁在做做什么
Reason模型看完所有消息,决定「我需要先读那个文件」
Act你的代码执行模型指定的工具(读文件、跑命令、查数据库)
Observe模型看到工具返回的结果,决定「接下来我要改这行代码」
循环while重复直到模型说「做完了」(stop_reason = end_turn)

前端类比:while (!done) { 想; 做; 看结果 }——就这么直白。

stop_reason:模型在告诉你它想干什么

模型的每次回答都带一个 stop_reason,这是循环的转向灯:

stop_reason含义你的代码应该
end_turn「我做完了」退出循环,把最终回答给用户
tool_use「我想调用工具」执行工具,结果塞回去,继续循环
max_tokens「 token 用完了」截断了,内容不完整

循环的退出条件不是你决定的,是模型自己决定的。 它觉得任务完成了就输出 end_turn,你的代码只是尊重这个决定。

maxTurns:安全网

模型可能陷入无限循环——反复调用同一个工具,或者一直说「让我再检查一下」。maxTurns 就是防这个的:

while (turnCount < maxTurns) {  // 默认 8 次
  // ...
}
// 如果到这里,说明超过轮次了
return { reason: "max_turns" };

easy-agent 默认设 8 轮。Claude Code 内部也有类似的上限。永远要设安全网——模型没有「我已经循环太多次了」的自我意识。

工具执行:不是并发,是顺序

看 easy-agent 的 runTools 函数:

export async function runTools(contentBlocks, toolContext) {
  const results = [];
  for (const block of contentBlocks) {
    if (block.type !== "tool_use") continue;
    const tool = findToolByName(block.name);
    const result = await tool.call(block.input, toolContext);
    results.push({
      type: "tool_result",
      tool_use_id: block.id,
      content: result.content,
    });
  }
  return { role: "user", content: results };
}

三个细节:

  1. for...of 顺序执行——不是 Promise.all。因为工具之间可能有依赖(先读文件才能改文件)
  2. tool_use_id 回填——模型发的每个工单有唯一 ID,结果要标记是对哪张工单的回复
  3. 结果以 role: "user" 塞回去——API 约定 tool_result 放在 user 角色里

那 LangGraph 做了什么?

LangGraph 把这个 while 循环画成了图:

import { StateGraph } from "@langchain/langgraph";

const graph = new StateGraph(AgentState);
graph.addNode("agent", callModel);
graph.addNode("tools", executeTools);
graph.addConditionalEdges("agent", shouldContinue, {
  continue: "tools",
  end: END,
});
graph.addEdge("tools", "agent");  // 工具结果 → 回到模型

const app = graph.compile();

对比一下:

裸 while 循环LangGraph 图
while (turn < max)addConditionalEdges("agent", shouldContinue)
if (stopReason === "tool_use")shouldContinue() 函数返回 "continue" 或 "end"
messages.push(toolResults)图的 edge 自动传递 state
没有检查点(checkpoint)——中途暂停/恢复
没有人工审批——在某个节点暂停等人确认
没有时间旅行——回到之前任一步

LangGraph 的价值不是「能跑通」,是「跑通之后还能管控」——检查点、审批、回溯,这些在裸 while 循环里要自己写很多代码才能实现。

本章要点

  1. Agent 的心脏 = while 循环:模型 → 工具 → 结果回填 → 再循环
  2. ReAct = Reason + Act + Observe,不是算法,是模式名
  3. stop_reason 是转向灯:end_turn 退出,tool_use 继续
  4. maxTurns 是安全网——模型没有循环自我意识
  5. 工具顺序执行(不是并发),结果按 tool_use_id 回填
  6. LangGraph 把 while 画成图,核心增值是检查点/审批/回溯

→ 下一章:Human-in-the-loop 与检查点