LangChain.js 进阶:从 Agent 到 LangGraph
本文探讨了从传统Agent模式向可控性更强的LangGraph架构的演进过程。以电商售后机器人为例,首先分析了传统Agent(LLM+Tools+Executor)的局限性,如黑盒执行、状态管理困难和资源浪费等问题。随后提出了基于LangGraph的优化方案,通过状态机建模将流程拆解为意图分类器、业务节点和工具执行等模块。具体实现展示了如何定义状态、构建意图分类节点和核心业务节点,利用结构化输出来
在构建 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) 逻辑:
- 查订单:调用
lookup_order获取详情。 - 查政策:调用
check_return_policy判断是否允许退货。 - 退款:只有前两步验证通过,才能调用
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) 作为路由:
这种架构的优势:
- 节省成本:闲聊请求不会加载复杂的 Tool Schema。
- 逻辑清晰:售后逻辑与闲聊逻辑物理隔离。
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 让调试变得简单,因为你知道是哪个节点出了问题。
更多推荐

所有评论(0)