在构建 LLM 应用时,我们经常听到 Chain(链)Agent(智能体) 这两个概念。随着业务逻辑的复杂化,简单的 Chain 难以应付多变的场景,而传统的 Agent 又容易陷入“死循环”或产生“幻觉”。

今天,我们将通过一个电商售后机器人的实战案例,探讨如何从传统的 Agent 模式进化到更可控、更强大的 LangGraph

1. 传统模式:什么是 Agent?

在 LangChain 的早期概念中,Chain 是硬编码的步骤序列(Step A -> Step B -> Step C),而 Agent 则引入了“推理”层。简单来说,Agent 就是 LLM + Tools + Runtime (Executor)

场景定义:电商售后机器人

我们需要构建一个 Agent,它必须严格遵循以下 多步推理(Multi-step Reasoning) 逻辑:

  1. 查订单:调用 lookup_order 获取详情。
  2. 查政策:调用 check_return_policy 判断是否允许退货。
  3. 退款:只有前两步验证通过,才能调用 process_refund

传统 Agent 代码实现

注意:传统的 createAgent 只是定义了 Agent 的规划逻辑,必须配合 Agent 才能真正执行工具调用循环。

import { ChatOpenAI } from "@langchain/openai";
import { tool, createAgent } from "langchain";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
import { z } from "zod";

const lookupOrderTool = tool(
  async ({ orderId }) => {
    if (orderId === "ORD-123") {
      return JSON.stringify({
        id: "ORD-123",
        item: "AirPods Pro",
        status: "delivered",
        days_since_delivery: 5,
        price: 249,
      });
    }
    return "订单不存在";
  },
  {
    name: "lookup_order",
    description: "根据订单ID查询订单详情和状态",
    schema: z.object({ orderId: z.string() }),
  }
);

const checkPolicyTool = tool(
  async ({ itemType }) => {
    if (
      itemType.toLowerCase().includes("airpods") ||
      itemType.toLowerCase().includes("electronics")
    ) {
      return "电子产品在收货后 7 天内且包装完好可申请无理由退货。";
    }
    return "普通商品支持 15 天无理由退货。";
  },
  {
    name: "check_return_policy",
    description: "查询某类商品或特定条件的退货规则",
    schema: z.object({ itemType: z.string() }),
  }
);

const processRefundTool = tool(
  async ({ orderId, reason }) => {
    return "退款申请已提交,系统正在处理中。";
  },
  {
    name: "process_refund",
    description: "执行退款。注意:必须在确认符合政策后才能调用。",
    schema: z.object({ orderId: z.string(), reason: z.string() }),
  }
);

const tools = [lookupOrderTool, checkPolicyTool, processRefundTool];

const model = new ChatOpenAI({
  temperature: 0.7,
  model: "", 
  configuration: { baseURL: "https://ark.cn-beijing.volces.com/api/v3" },
  apiKey: "",
});

const executor = createAgent({
  model,
  tools,
});

async function main() {
  const result = await executor.invoke({
    messages: [
      new SystemMessage(
        "你是一个售后助手。你必须先查询订单,再查询政策,最后根据结果决定是否退款。"
      ),
      new HumanMessage("我买的订单 ORD-123 里的东西坏了,我想退款。"),
    ],
  });
  console.log(result);
}

main();

Agent 的局限性

虽然 Agent 可以跑通上述流程,但在工程化落地时存在巨大隐患:

  • 黑盒执行:你很难在“查完订单”和“查政策”中间插入一段自定义的业务代码(比如:强制人工介入)。
  • 状态难以管理Agent 内部封装了状态,很难与外部系统(如前端 UI 或数据库)进行细粒度的状态同步。
  • 资源浪费:即使用户只是闲聊,Agent 也会加载所有工具定义的 Prompt,消耗大量 Token。

2. LangGraph:构建可控的状态机

LangGraph 允许我们将流程显式地建模为图(Graph)。我们不再依赖单一的 Prompt 来控制全局,而是通过 节点(Nodes)边(Edges) 来拆解逻辑。

2.1 优化后的架构图

我们引入一个 意图分类器(Classifier) 作为路由:

闲聊

售后

调用工具

结果回传

完成

Start

意图识别

Chat Node

Refund Agent Node

Tools Execution

End

这种架构的优势:

  1. 节省成本:闲聊请求不会加载复杂的 Tool Schema。
  2. 逻辑清晰:售后逻辑与闲聊逻辑物理隔离。

3. LangGraph 代码实现

第一步:环境与状态定义 (State)

import { ChatOpenAI } from "@langchain/openai";
import { StateGraph, Annotation } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { BaseMessage, SystemMessage, HumanMessage } from "@langchain/core/messages";

// 定义图的 State
// 这里不仅存储消息历史,还额外存储了 intent 字段用于路由判断
const GraphState = Annotation.Root({
  messages: Annotation<BaseMessage[]>({
    reducer: (x, y) => x.concat(y),
    default: () => [],
  }),
  intent: Annotation<string>({
    reducer: (x, y) => y ?? x,
    default: () => "unknown",
  }),
});

第二步:构建意图分类节点

这是一个纯逻辑节点,我们使用 withStructuredOutput 强制 LLM 输出 JSON,确保路由稳定。

import { z } from "zod";

const IntentSchema = z.object({
  category: z.enum(["refund_service", "general_chat"]),
  reasoning: z.string().describe("判断依据"),
});

const model = new ChatOpenAI({
  temperature: 0.7,
  model: "", 
  configuration: { baseURL: "https://ark.cn-beijing.volces.com/api/v3" },
  apiKey: "",
});

async function intentClassifierNode(state: typeof GraphState.State) {
  const { messages } = state;
  const lastMessage = messages[messages.length - 1];

  const classifier = model.withStructuredOutput(IntentSchema);
  
  const systemPrompt = "你是电商客服路由助手。涉及订单、退货、坏了等问题属于 refund_service,打招呼或天气等属于 general_chat。";
  
  const result = await classifier.invoke([
    new SystemMessage(systemPrompt),
    lastMessage
  ]);

  // 更新 state 中的 intent,不更新 messages
  return { intent: result.category };
}

第三步:定义核心业务节点

这里我们将工具绑定到 Refund Agent,而 Chat Node 则是一个纯粹的聊天模型。

// 绑定工具到模型
// 注意:tools 数组复用上文中定义的 tools
const modelWithTools = model.bindTools(tools);

// 1. 售后 Agent 节点
async function refundAgentNode(state: typeof GraphState.State) {
  const { messages } = state;
  const systemMsg = new SystemMessage("你是一个专业的售后专员。处理退款必须遵循:查订单 -> 查政策 -> 退款 的顺序。");
  
  // 过滤掉之前的 SystemMessage,确保当前上下文是最新的
  const messagesForModel = [systemMsg, ...messages.filter(m => !(m instanceof SystemMessage))];
  
  const response = await modelWithTools.invoke(messagesForModel);
  return { messages: [response] };
}

// 2. 闲聊节点
async function chatNode(state: typeof GraphState.State) {
  const { messages } = state;
  const response = await model.invoke([
    new SystemMessage("你是一个友好的客服助手,简短回答用户的闲聊。不要尝试解决订单问题。"),
    ...messages
  ]);
  return { messages: [response] };
}

第四步:组装图 (The Architecture)

这是 LangGraph 最核心的部分:编排逻辑。

// 实例化预置的工具节点
const toolsNode = new ToolNode(tools);

// 路由逻辑:根据 state.intent 决定去哪里
function routeIntent(state: typeof GraphState.State) {
  if (state.intent === "refund_service") {
    return "refund_agent";
  }
  return "chat";
}

// 循环逻辑:Agent 决定是继续调工具还是结束
function shouldContinue(state: typeof GraphState.State) {
  const messages = state.messages;
  const lastMessage = messages[messages.length - 1];

  // 检查是否有工具调用请求
  if ("tool_calls" in lastMessage && Array.isArray(lastMessage.tool_calls) && lastMessage.tool_calls.length > 0) {
    return "tools";
  }
  return "__end__";
}

// --- 构图 ---
const workflow = new StateGraph(GraphState)
  // 1. 添加节点
  .addNode("classifier", intentClassifierNode)
  .addNode("refund_agent", refundAgentNode)
  .addNode("chat", chatNode)
  .addNode("tools", toolsNode)

  // 2. 设置起点
  .addEdge("__start__", "classifier")

  // 3. 添加基于意图的条件边
  .addConditionalEdges(
    "classifier",
    routeIntent, 
    {
      refund_agent: "refund_agent",
      chat: "chat"
    }
  )
  
  // 4. 普通聊天直接结束
  .addEdge("chat", "__end__")

  // 5. 添加 Agent 的循环逻辑 (Think -> Act -> Observe)
  .addConditionalEdges(
    "refund_agent",
    shouldContinue,
    {
      tools: "tools",
      __end__: "__end__"
    }
  )

  // 6. 工具执行完,必须回到 Agent 继续思考下一步
  .addEdge("tools", "refund_agent");

// 编译图
const app = workflow.compile();

4. 实战演示

现在,我们看看这个“更聪明”的机器人如何处理不同请求。

场景 A:用户闲聊

console.log("--- 测试闲聊场景 ---");
const inputChat = {
  messages: [new HumanMessage("你好,今天天气怎么样?")],
};

const streamChat = await app.stream(inputChat);
for await (const chunk of streamChat) {
    // 这里的 chunk 会包含各节点更新的状态
    console.log(chunk); 
}
// 预期路径: Start -> Classifier -> Chat -> End
// 结果: 不会触发 refund_agent,响应极快。

场景 B:复杂售后请求

console.log("--- 测试售后场景 ---");
const inputRefund = {
  messages: [new HumanMessage("我买的订单 ORD-123 里的东西坏了,我想退款。")],
};

const stream = await app.stream(inputRefund, {
  streamMode: "values" // 每次节点执行完,返回完整的 state
});

for await (const chunk of stream) {
    const lastMsg = chunk.messages[chunk.messages.length - 1];
    
    if (chunk.intent && chunk.messages.length === 1) {
       console.log(`🧭 [Router] 意图识别为: ${chunk.intent}`);
    }

    if (lastMsg?.tool_calls?.length) {
        console.log(`🤖 [Agent] 决定调用工具: ${lastMsg.tool_calls.map(tc => tc.name).join(", ")}`);
    } else if (lastMsg?.content && lastMsg.name !== "classifier") {
        console.log(`📝 [Message] ${lastMsg.content}`);
    }
}

运行结果日志:

--- 测试售后场景 ---
🧭 [Router] 意图识别为: refund_service
🤖 [Agent] 决定调用工具: lookup_order
📝 [Message] {"id":"ORD-123","item":"AirPods Pro","status":"delivered"...}
🤖 [Agent] 决定调用工具: check_return_policy
📝 [Message] 电子产品在收货后 7 天内且包装完好可申请无理由退货。
🤖 [Agent] 决定调用工具: process_refund
📝 [Message] 退款申请已提交,系统正在处理中。
📝 [Message] 您的订单 ORD-123 符合退货政策,已为您提交退款申请。

5. 总结

通过 LangGraph,我们不再是简单地把 Prompt 扔给 LLM,而是像编写代码一样编写流程

  • 显式控制:你可以清楚地看到每一步流转。
  • 状态持久化:图的状态可以持久化(配合 Checkpointer),支持长周期的“人机协作”任务。
  • 易调试性:相比 Agent 的黑盒,LangGraph 让调试变得简单,因为你知道是哪个节点出了问题。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐